Received: from malur.postgresql.org ([217.196.149.56]) by arkaria.postgresql.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_CBC_SHA1:256) (Exim 4.89) (envelope-from ) id 1gvDmg-0002dv-0C for pgsql-hackers@arkaria.postgresql.org; Sun, 17 Feb 2019 04:10:30 +0000 Received: from localhost ([127.0.0.1] helo=malur.postgresql.org) by malur.postgresql.org with esmtp (Exim 4.89) (envelope-from ) id 1gvDlf-000369-1c for pgsql-hackers@arkaria.postgresql.org; Sun, 17 Feb 2019 04:09:27 +0000 Received: from makus.postgresql.org ([2001:4800:3e1:1::229]) by malur.postgresql.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_CBC_SHA1:256) (Exim 4.89) (envelope-from ) id 1gvDle-00032U-F0 for pgsql-hackers@lists.postgresql.org; Sun, 17 Feb 2019 04:09:26 +0000 Received: from mail-pl1-x632.google.com ([2607:f8b0:4864:20::632]) by makus.postgresql.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_CBC_SHA1:256) (Exim 4.89) (envelope-from ) id 1gvDlW-0004OD-Hz for pgsql-hackers@postgresql.org; Sun, 17 Feb 2019 04:09:24 +0000 Received: by mail-pl1-x632.google.com with SMTP id 101so7026650pld.6 for ; Sat, 16 Feb 2019 20:09:18 -0800 (PST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=leadboat.com; s=google; h=date:from:to:subject:message-id:references:mime-version :content-disposition:in-reply-to:user-agent; bh=HTPEJVHKPO8pklHygRmsazJpnZV9NVkbXf82MH0Bdng=; b=FRG0+GGKflTadZzZkhHLHh7XaP5UTAEaA5jN189mFYe3Y39uU2xaXHljCIK3Gr0V+t pJ34LmtD0/37kNbddHEVtVNM+zZgIEIH4Kix3atEA8u9Fb2kUDZ071mDzTOC3n1uwibv +jXmnOzIb4RRhm3KIPBBWx77P1vbKzps1uwO4= X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20161025; h=x-gm-message-state:date:from:to:subject:message-id:references :mime-version:content-disposition:in-reply-to:user-agent; bh=HTPEJVHKPO8pklHygRmsazJpnZV9NVkbXf82MH0Bdng=; b=psdR8AusDPRPjUZZM4npTubmKS3KZBQ/qaZN63mECUYRQhEZBO+l/UHsiWH9YuJS+1 47eTtWiOkeEtKLHX8ax6kMMSC5Vng/pLRlJ37JlySLexmS0o2U6u7lSNjnvu8R9FP0ku o5aKTPhZ1rW1ld4Ipq9UyVc1Fyq6Xg/M6h8YVcrH5/fTKgSAIcnkMRO0eF71/aCNfkag usOByz0sY+3gAcv/RfWiKAJAXsfQfVtfIEUOXp10HR+AIqWgbRPPzWkHmfVPDrM17ryv Egmt7Wh8rjW105FL9XuOUkdo+y3L3mztcTF4Hos3dE2bkaEUrgB4Ixv46KFRnKB33WWc yMvQ== X-Gm-Message-State: AHQUAuZq+7mLkj59G8EGgW/Qwhdk4ZyVMnxKZDv+IrOu5V+BebmEMkDr Eft21ZeKqzSZooMbnavoAGyWG/gZWJ4= X-Google-Smtp-Source: AHgI3IblNQmp1k2+ANkR9qgGYj7T1wZ3cvdP/OL2g3xDY4yYhEq3uEys3Clqp1H5v1RHTgtMi1PEaQ== X-Received: by 2002:a17:902:243:: with SMTP id 61mr17756135plc.249.1550376556606; Sat, 16 Feb 2019 20:09:16 -0800 (PST) Received: from rfd.leadboat.com (108-233-125-46.lightspeed.sntcca.sbcglobal.net. [108.233.125.46]) by smtp.gmail.com with ESMTPSA id b12sm14960624pfb.30.2019.02.16.20.09.14 for (version=TLS1_2 cipher=ECDHE-RSA-AES128-GCM-SHA256 bits=128/128); Sat, 16 Feb 2019 20:09:15 -0800 (PST) Date: Sat, 16 Feb 2019 20:09:13 -0800 From: Noah Misch To: pgsql-hackers@postgresql.org Subject: Re: Spurious "apparent wraparound" via SimpleLruTruncate() rounding Message-ID: <20190217040913.GA1305059@rfd.leadboat.com> References: <20190202083822.GC32531@gust.leadboat.com> <20190214072623.GA1139206@rfd.leadboat.com> MIME-Version: 1.0 Content-Type: multipart/mixed; boundary="ZGiS0Q5IWpPtfppv" Content-Disposition: inline In-Reply-To: <20190214072623.GA1139206@rfd.leadboat.com> User-Agent: Mutt/1.5.24 (2015-08-30) List-Id: List-Help: List-Subscribe: List-Post: List-Owner: List-Archive: Precedence: bulk --ZGiS0Q5IWpPtfppv Content-Type: text/plain; charset=us-ascii Content-Disposition: inline On Wed, Feb 13, 2019 at 11:26:23PM -0800, Noah Misch wrote: > On further study, I was able to reproduce data loss > Fixes are available: > > a. Fix the rounding in SimpleLruTruncate(). (The patch I posted upthread is > wrong; I will correct it in a separate message.) Here's a corrected version. I now delete a segment only if both its first page and its last page are considered to precede the cutoff; see the new comment at SlruMayDeleteSegment(). --ZGiS0Q5IWpPtfppv Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename="slru-truncate-modulo-v2.patch" commit 09393a1 (HEAD) Author: Noah Misch AuthorDate: Sat Feb 16 20:02:51 2019 -0800 Commit: Noah Misch CommitDate: Sat Feb 16 20:02:51 2019 -0800 Don't round SimpleLruTruncate() cutoffPage values. Every core SLRU wraps around. The rounding did not account for that; in rare cases, it permitted deletion of the most recently-populated page of SLRU data. This closes a rare opportunity for data loss, which manifested as "could not access status of transaction" errors. If a user's physical replication primary logged ": apparent wraparound" messages, the user should rebuild that primary's standbys regardless of symptoms. At less risk is a cluster having emitted "not accepting commands" errors or "must be vacuumed" warnings at some point. One can test a cluster for this data loss by running VACUUM FREEZE in every database. Back-patch to 9.4 (all supported versions). Discussion: https://postgr.es/m/20190202083822.GC32531@gust.leadboat.com diff --git a/src/backend/access/transam/slru.c b/src/backend/access/transam/slru.c index 3623352..71e29b9 100644 --- a/src/backend/access/transam/slru.c +++ b/src/backend/access/transam/slru.c @@ -1171,11 +1171,6 @@ SimpleLruTruncate(SlruCtl ctl, int cutoffPage) int slotno; /* - * The cutoff point is the start of the segment containing cutoffPage. - */ - cutoffPage -= cutoffPage % SLRU_PAGES_PER_SEGMENT; - - /* * Scan shared memory and remove any pages preceding the cutoff page, to * ensure we won't rewrite them later. (Since this is normally called in * or just after a checkpoint, any dirty pages should have been flushed @@ -1221,8 +1216,11 @@ restart:; * Hmm, we have (or may have) I/O operations acting on the page, so * we've got to wait for them to finish and then start again. This is * the same logic as in SlruSelectLRUPage. (XXX if page is dirty, - * wouldn't it be OK to just discard it without writing it? For now, - * keep the logic the same as it was.) + * wouldn't it be OK to just discard it without writing it? + * SlruMayDeleteSegment() uses a stricter qualification, so we might + * not delete this page in the end; even if we don't delete it, we + * won't have cause to read its data again. For now, keep the logic + * the same as it was.) */ if (shared->page_status[slotno] == SLRU_PAGE_VALID) SlruInternalWritePage(ctl, slotno, NULL); @@ -1313,18 +1311,40 @@ restart: } /* + * Determine whether a segment is okay to delete. + * + * segpage is the first page of the segment, and cutoffPage is the oldest (in + * PagePrecedes order) page in the SLRU containing still-useful data. Since + * every core PagePrecedes callback implements "wrap around", check the + * segment's first and last pages: + * + * first=cutoff: no; cutoff falls inside this segment + * first>=cutoff && last=cutoff && last>=cutoff: no; every page of this segment is too young + */ +static bool +SlruMayDeleteSegment(SlruCtl ctl, int segpage, int cutoffPage) +{ + int seg_last_page = segpage + SLRU_PAGES_PER_SEGMENT - 1; + + Assert(segpage % SLRU_PAGES_PER_SEGMENT == 0); + + return (ctl->PagePrecedes(segpage, cutoffPage) && + ctl->PagePrecedes(seg_last_page, cutoffPage)); +} + +/* * SlruScanDirectory callback - * This callback reports true if there's any segment prior to the one - * containing the page passed as "data". + * This callback reports true if there's any segment wholly prior to the + * one containing the page passed as "data". */ bool SlruScanDirCbReportPresence(SlruCtl ctl, char *filename, int segpage, void *data) { int cutoffPage = *(int *) data; - cutoffPage -= cutoffPage % SLRU_PAGES_PER_SEGMENT; - - if (ctl->PagePrecedes(segpage, cutoffPage)) + if (SlruMayDeleteSegment(ctl, segpage, cutoffPage)) return true; /* found one; don't iterate any more */ return false; /* keep going */ @@ -1339,7 +1359,7 @@ SlruScanDirCbDeleteCutoff(SlruCtl ctl, char *filename, int segpage, void *data) { int cutoffPage = *(int *) data; - if (ctl->PagePrecedes(segpage, cutoffPage)) + if (SlruMayDeleteSegment(ctl, segpage, cutoffPage)) SlruInternalDeleteSegment(ctl, filename); return false; /* keep going */ --ZGiS0Q5IWpPtfppv--