Skip to content

Commit 90bcc7c

Browse files
alvherrehughcapetCyberDem0n
committed
Avoid deleting critical WAL segments during pg_rewind
Previously, in unlucky cases, it was possible for pg_rewind to remove certain WAL segments from the rewound demoted primary. In particular this happens if those files have been marked for archival (i.e., their .ready files were created) but not yet archived; the newly promoted node no longer has such files because of them having been recycled, but they are likely critical for recovery in the demoted node. If pg_rewind removes them, recovery is not possible anymore. Fix this by maintaining a hash table of files in this situation in the scan that looks for a checkpoint, which the decide_file_actions phase can consult so that it knows to preserve them. Backpatch to 14. The problem also exists in 13, but that branch was not blessed with commit eb00f1d, so this patch is difficult to apply there. Users of older releases will just have to continue to be extra careful when rewinding. Co-authored-by: Полина Бунгина (Polina Bungina) <bungina@gmail.com> Co-authored-by: Alexander Kukushkin <cyberdemn@gmail.com> Reviewed-by: Kyotaro Horiguchi <horikyota.ntt@gmail.com> Reviewed-by: Atsushi Torikoshi <torikoshia@oss.nttdata.com> Discussion: https://postgr.es/m/CAAtGL4AhzmBRsEsaDdz7065T+k+BscNadfTqP1NcPmsqwA5HBw@mail.gmail.com
1 parent d31bbfb commit 90bcc7c

File tree

7 files changed

+169
-7
lines changed

7 files changed

+169
-7
lines changed

src/bin/pg_rewind/filemap.c

+77-7
Original file line numberDiff line numberDiff line change
@@ -38,14 +38,14 @@
3838
* Define a hash table which we can use to store information about the files
3939
* appearing in source and target systems.
4040
*/
41-
#define SH_PREFIX filehash
42-
#define SH_ELEMENT_TYPE file_entry_t
43-
#define SH_KEY_TYPE const char *
44-
#define SH_KEY path
41+
#define SH_PREFIX filehash
42+
#define SH_ELEMENT_TYPE file_entry_t
43+
#define SH_KEY_TYPE const char *
44+
#define SH_KEY path
4545
#define SH_HASH_KEY(tb, key) hash_string(key)
4646
#define SH_EQUAL(tb, a, b) (strcmp(a, b) == 0)
47-
#define SH_SCOPE static inline
48-
#define SH_RAW_ALLOCATOR pg_malloc0
47+
#define SH_SCOPE static inline
48+
#define SH_RAW_ALLOCATOR pg_malloc0
4949
#define SH_DECLARE
5050
#define SH_DEFINE
5151
#include "lib/simplehash.h"
@@ -60,7 +60,36 @@ static char *datasegpath(RelFileLocator rlocator, ForkNumber forknum,
6060

6161
static file_entry_t *insert_filehash_entry(const char *path);
6262
static file_entry_t *lookup_filehash_entry(const char *path);
63+
64+
/*
65+
* A separate hash table which tracks WAL files that must not be deleted.
66+
*/
67+
typedef struct keepwal_entry
68+
{
69+
const char *path;
70+
uint32 status;
71+
} keepwal_entry;
72+
73+
#define SH_PREFIX keepwal
74+
#define SH_ELEMENT_TYPE keepwal_entry
75+
#define SH_KEY_TYPE const char *
76+
#define SH_KEY path
77+
#define SH_HASH_KEY(tb, key) hash_string(key)
78+
#define SH_EQUAL(tb, a, b) (strcmp(a, b) == 0)
79+
#define SH_SCOPE static inline
80+
#define SH_RAW_ALLOCATOR pg_malloc0
81+
#define SH_DECLARE
82+
#define SH_DEFINE
83+
#include "lib/simplehash.h"
84+
85+
#define KEEPWAL_INITIAL_SIZE 1000
86+
87+
88+
static keepwal_hash *keepwal = NULL;
89+
static bool keepwal_entry_exists(const char *path);
90+
6391
static int final_filemap_cmp(const void *a, const void *b);
92+
6493
static bool check_file_excluded(const char *path, bool is_source);
6594

6695
/*
@@ -206,6 +235,39 @@ lookup_filehash_entry(const char *path)
206235
return filehash_lookup(filehash, path);
207236
}
208237

238+
/*
239+
* Initialize a hash table to store WAL file names that must be kept.
240+
*/
241+
void
242+
keepwal_init(void)
243+
{
244+
/* An initial hash size out of thin air */
245+
keepwal = keepwal_create(KEEPWAL_INITIAL_SIZE, NULL);
246+
}
247+
248+
/* Mark the given file to prevent its removal */
249+
void
250+
keepwal_add_entry(const char *path)
251+
{
252+
keepwal_entry *entry;
253+
bool found;
254+
255+
/* Should only be called with keepwal initialized */
256+
Assert(keepwal != NULL);
257+
258+
entry = keepwal_insert(keepwal, path, &found);
259+
260+
if (!found)
261+
entry->path = pg_strdup(path);
262+
}
263+
264+
/* Return true if file is marked as not to be removed, false otherwise */
265+
static bool
266+
keepwal_entry_exists(const char *path)
267+
{
268+
return keepwal_lookup(keepwal, path) != NULL;
269+
}
270+
209271
/*
210272
* Callback for processing source file list.
211273
*
@@ -685,7 +747,15 @@ decide_file_action(file_entry_t *entry)
685747
}
686748
else if (entry->target_exists && !entry->source_exists)
687749
{
688-
/* File exists in target, but not source. Remove it. */
750+
/*
751+
* For files that exist in target but not in source, we check the
752+
* keepwal hash table; any files listed therein must not be removed.
753+
*/
754+
if (keepwal_entry_exists(path))
755+
{
756+
pg_log_debug("Not removing file \"%s\" because it is required for recovery", path);
757+
return FILE_ACTION_NONE;
758+
}
689759
return FILE_ACTION_REMOVE;
690760
}
691761
else if (!entry->target_exists && !entry->source_exists)

src/bin/pg_rewind/filemap.h

+3
Original file line numberDiff line numberDiff line change
@@ -110,4 +110,7 @@ extern filemap_t *decide_file_actions(void);
110110
extern void calculate_totals(filemap_t *filemap);
111111
extern void print_filemap(filemap_t *filemap);
112112

113+
extern void keepwal_init(void);
114+
extern void keepwal_add_entry(const char *path);
115+
113116
#endif /* FILEMAP_H */

src/bin/pg_rewind/meson.build

+1
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ tests += {
4343
't/007_standby_source.pl',
4444
't/008_min_recovery_point.pl',
4545
't/009_growing_files.pl',
46+
't/010_keep_recycled_wals.pl',
4647
],
4748
},
4849
}

src/bin/pg_rewind/parsexlog.c

+21
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,8 @@ findLastCheckpoint(const char *datadir, XLogRecPtr forkptr, int tliIndex,
175175
XLogReaderState *xlogreader;
176176
char *errormsg;
177177
XLogPageReadPrivate private;
178+
XLogSegNo current_segno = 0;
179+
TimeLineID current_tli = 0;
178180

179181
/*
180182
* The given fork pointer points to the end of the last common record,
@@ -217,6 +219,25 @@ findLastCheckpoint(const char *datadir, XLogRecPtr forkptr, int tliIndex,
217219
LSN_FORMAT_ARGS(searchptr));
218220
}
219221

222+
/* Detect if a new WAL file has been opened */
223+
if (xlogreader->seg.ws_tli != current_tli ||
224+
xlogreader->seg.ws_segno != current_segno)
225+
{
226+
char xlogfname[MAXFNAMELEN];
227+
228+
snprintf(xlogfname, MAXFNAMELEN, XLOGDIR "/");
229+
230+
/* update curent values */
231+
current_tli = xlogreader->seg.ws_tli;
232+
current_segno = xlogreader->seg.ws_segno;
233+
234+
XLogFileName(xlogfname + sizeof(XLOGDIR),
235+
current_tli, current_segno, WalSegSz);
236+
237+
/* Track this filename as one to not remove */
238+
keepwal_add_entry(xlogfname);
239+
}
240+
220241
/*
221242
* Check if it is a checkpoint record. This checkpoint record needs to
222243
* be the latest checkpoint before WAL forked and not the checkpoint

src/bin/pg_rewind/pg_rewind.c

+3
Original file line numberDiff line numberDiff line change
@@ -455,6 +455,9 @@ main(int argc, char **argv)
455455
exit(0);
456456
}
457457

458+
/* Initialize hashtable that tracks WAL files protected from removal */
459+
keepwal_init();
460+
458461
findLastCheckpoint(datadir_target, divergerec, lastcommontliIndex,
459462
&chkptrec, &chkpttli, &chkptredo, restore_command);
460463
pg_log_info("rewinding from last common checkpoint at %X/%X on timeline %u",
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
# Copyright (c) 2021-2024, PostgreSQL Global Development Group
2+
#
3+
# Test situation where a target data directory contains
4+
# WAL files that were already recycled by the new primary.
5+
#
6+
7+
use strict;
8+
use warnings FATAL => 'all';
9+
use PostgreSQL::Test::Utils;
10+
use Test::More;
11+
12+
use FindBin;
13+
use lib $FindBin::RealBin;
14+
use RewindTest;
15+
16+
RewindTest::setup_cluster();
17+
$node_primary->enable_archiving();
18+
RewindTest::start_primary();
19+
20+
RewindTest::create_standby();
21+
$node_standby->enable_restoring($node_primary, 0);
22+
$node_standby->reload();
23+
24+
RewindTest::primary_psql("CHECKPOINT"); # last common checkpoint
25+
26+
# We use "perl -e 'exit(1)'" as an alternative to "false", because the latter
27+
# might not be available on Windows.
28+
my $false = "$^X -e 'exit(1)'";
29+
$node_primary->append_conf(
30+
'postgresql.conf', qq(
31+
archive_command = '$false'
32+
));
33+
$node_primary->reload();
34+
35+
# advance WAL on primary; this WAL segment will never make it to the archive
36+
RewindTest::primary_psql("CREATE TABLE t(a int)");
37+
RewindTest::primary_psql("INSERT INTO t VALUES(0)");
38+
RewindTest::primary_psql("SELECT pg_switch_wal()");
39+
40+
RewindTest::promote_standby;
41+
42+
# new primary loses diverging WAL segment
43+
RewindTest::standby_psql("INSERT INTO t values(0)");
44+
RewindTest::standby_psql("SELECT pg_switch_wal()");
45+
46+
$node_standby->stop();
47+
$node_primary->stop();
48+
49+
my ($stdout, $stderr) = run_command(
50+
[
51+
'pg_rewind', '--debug',
52+
'--source-pgdata', $node_standby->data_dir,
53+
'--target-pgdata', $node_primary->data_dir,
54+
'--no-sync',
55+
]);
56+
57+
like(
58+
$stderr,
59+
qr/Not removing file .* because it is required for recovery/,
60+
"some WAL files were skipped");
61+
62+
done_testing();

src/tools/pgindent/typedefs.list

+2
Original file line numberDiff line numberDiff line change
@@ -3596,6 +3596,8 @@ json_manifest_version_callback
35963596
json_ofield_action
35973597
json_scalar_action
35983598
json_struct_action
3599+
keepwal_entry
3600+
keepwal_hash
35993601
keyEntryData
36003602
key_t
36013603
lclContext

0 commit comments

Comments
 (0)