summaryrefslogtreecommitdiff
path: root/src/bin/pg_upgrade/t/007_multixact_conversion.pl
blob: 14db40e45eddb0d6ab1f1fa9d4ef8031cf7cd303 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
# Copyright (c) 2025, PostgreSQL Global Development Group

# Version 19 expanded MultiXactOffset from 32 to 64 bits.  Upgrading
# across that requires rewriting the SLRU files to the new format.
# This file contains tests for the conversion.
#
# To run, set 'oldinstall' ENV variable to point to a pre-v19
# installation.  If it's not set, or if it points to a v19 or above
# installation, this still performs a very basic test, upgrading a
# cluster with some multixacts.  It's not very interesting, however,
# because there's no conversion involved in that case.

use strict;
use warnings FATAL => 'all';

use Math::BigInt;
use PostgreSQL::Test::Cluster;
use PostgreSQL::Test::Utils;
use Test::More;

# Temp dir for a dumps.
my $tempdir = PostgreSQL::Test::Utils::tempdir;

# A workload that consumes multixids.  The purpose of this is to
# generate some multixids in the old cluster, so that we can test
# upgrading them.  The workload is a mix of KEY SHARE locking queries
# and UPDATEs, and commits and aborts, to generate a mix of multixids
# with different statuses.  It consumes around 3000 multixids with
# 60000 members in total.  That's enough to span more than one
# multixids 'offsets' page, and more than one 'members' segment with
# the default block size.
#
# The workload leaves behind a table called 'mxofftest' containing a
# small number of rows referencing some of the generated multixids.
#
# Because this function is used to generate test data on the old
# installation, it needs to work with older PostgreSQL server
# versions.
#
# The first argument is the cluster to connect to, the second argument
# is a cluster using the new version.  We need the 'psql' binary from
# the new version, the new cluster is otherwise unused.  (We need to
# use the new 'psql' because some of the more advanced background psql
# perl module features depend on a fairly recent psql version.)
sub mxact_workload
{
	my $node = shift;       # Cluster to connect to
	my $binnode = shift;    # Use the psql binary from this cluster

	my $connstr = $node->connstr('postgres');

	$node->start;
	$node->safe_psql(
		'postgres', qq[
		CREATE TABLE mxofftest (id INT PRIMARY KEY, n_updated INT)
		  WITH (AUTOVACUUM_ENABLED=FALSE);
		INSERT INTO mxofftest SELECT G, 0 FROM GENERATE_SERIES(1, 50) G;
	]);

	my $nclients = 20;
	my $update_every = 13;
	my $abort_every = 11;
	my @connections = ();

	# Silence the logging of the statements we run to avoid
	# unnecessarily bloating the test logs.  This runs before the
	# upgrade we're testing, so the details should not be very
	# interesting for debugging.  But if needed, you can make it more
	# verbose by setting this.
	my $verbose = 0;

	# Bump the timeout on the connections to avoid false negatives on
	# slow test systems. The timeout covers the whole duration that
	# the connections are open rather than the individual queries.
	my $connection_timeout_secs =
	  4 * $PostgreSQL::Test::Utils::timeout_default;

	# Open multiple connections to the database.  Start a transaction
	# in each connection.
	for (0 .. $nclients)
	{
		# Use the psql binary from the new installation.  The
		# BackgroundPsql functionality doesn't work with older psql
		# versions.
		my $conn = $binnode->background_psql(
			'',
			connstr => $node->connstr('postgres'),
			timeout => $connection_timeout_secs);

		$conn->query_safe("SET log_statement=none", verbose => $verbose)
		  unless $verbose;
		$conn->query_safe("SET enable_seqscan=off", verbose => $verbose);
		$conn->query_safe("BEGIN", verbose => $verbose);

		push(@connections, $conn);
	}

	# Run queries using cycling through the connections in a
	# round-robin fashion.  We keep a transaction open in each
	# connection at all times, and lock/update the rows.  With 20
	# connections, each SELECT FOR KEY SHARE query generates a new
	# multixid, containing the XIDs of all the transactions running at
	# the time, ie. around 20 XIDs.
	for (my $i = 0; $i < 3000; $i++)
	{
		note "generating multixids $i / 3000\n" if ($i % 100 == 0);

		my $conn = $connections[ $i % $nclients ];

		my $sql = ($i % $abort_every == 0) ? "ABORT" : "COMMIT";
		$conn->query_safe($sql, verbose => $verbose);

		$conn->query_safe("BEGIN", verbose => $verbose);
		if ($i % $update_every == 0)
		{
			$sql = qq[
			  UPDATE mxofftest SET n_updated = n_updated + 1 WHERE id = ${i} % 50;
			];
		}
		else
		{
			my $threshold = int($i / 3000 * 50);
			$sql = qq[
			  select count(*) from (
				SELECT * FROM mxofftest WHERE id >= $threshold FOR KEY SHARE
			  ) as x
			];
		}
		$conn->query_safe($sql, verbose => $verbose);
	}

	for my $conn (@connections)
	{
		$conn->quit();
	}

	$node->stop;
	return;
}

# Return contents of the 'mxofftest' table, created by mxact_workload
sub get_test_table_contents
{
	my ($node, $filename) = @_;

	my $contents = $node->safe_psql('postgres',
		"SELECT ctid, xmin, xmax, * FROM mxofftest");

	my $path = $tempdir . '/' . $filename;
	open(my $fh, '>', $path)
	  || die "could not open $path for writing $!";
	print $fh $contents;
	close($fh);

	return $path;
}

# Return the members of all updating multixids in the given range
sub get_updating_multixact_members
{
	my ($node, $from, $to, $filename) = @_;

	my $path = $tempdir . '/' . $filename;
	open(my $fh, '>', $path)
	  || die "could not open $path for writing $!";

	if ($to >= $from)
	{
		my $res = $node->safe_psql(
			'postgres', qq[
			SELECT multi, mode, xid
			FROM generate_series($from, $to - 1) as multi,
				 pg_get_multixact_members(multi::text::xid)
			WHERE mode not in ('keysh', 'sh');
		]);
		print $fh $res;
	}
	else
	{
		# Multixids wrapped around.  Split the query into two parts,
		# before and after the wraparound.
		my $res = $node->safe_psql(
			'postgres', qq[
			SELECT multi, mode, xid
			FROM generate_series($from, 4294967295) as multi,
				 pg_get_multixact_members(multi::text::xid)
			WHERE mode not in ('keysh', 'sh');
		]);
		print $fh $res;
		$res = $node->safe_psql(
			'postgres', qq[
			SELECT multi, mode, xid
			FROM generate_series(1, $to - 1) as multi,
				 pg_get_multixact_members(multi::text::xid)
			WHERE mode not in ('keysh', 'sh');
		]);
		print $fh $res;
	}

	close($fh);
	return $path;
}

# Read multixid related fields from the control file
#
# Note: This is used on both the old and the new installation, so the
# command arguments and the output parsing used here must work with
# all PostgreSQL versions supported by the test.
sub read_multixid_fields
{
	my $node = shift;

	my $pg_controldata_path = $node->installed_command('pg_controldata');
	my ($stdout, $stderr) =
	  run_command([ $pg_controldata_path, $node->data_dir ]);
	$stdout =~ /^Latest checkpoint's oldestMultiXid:\s*(.*)$/m
	  or die "could not read oldestMultiXid from pg_controldata";
	my $oldest_multi_xid = $1;
	$stdout =~ /^Latest checkpoint's NextMultiXactId:\s*(.*)$/m
	  or die "could not read NextMultiXactId from pg_controldata";
	my $next_multi_xid = $1;
	$stdout =~ /^Latest checkpoint's NextMultiOffset:\s*(.*)$/m
	  or die "could not read NextMultiOffset from pg_controldata";
	my $next_multi_offset = $1;

	return ($oldest_multi_xid, $next_multi_xid, $next_multi_offset);
}

# Reset a cluster's next multixid and mxoffset to given values.
#
# Note: This is used on the old insallation, so the command arguments
# and the output parsing used here must work with all pre-v19
# PostgreSQL versions supported by the test.
sub reset_mxid_mxoffset_pre_v19
{
	my $node = shift;
	my $mxid = shift;
	my $mxoffset = shift;

	my $pg_resetwal_path = $node->installed_command('pg_resetwal');
	# Get block size
	my ($out, $err) =
	  run_command([ $pg_resetwal_path, '--dry-run', $node->data_dir ]);
	$out =~ /^Database block size: *(\d+)$/m or die;

	# Verify that no multixids are currently in use.  Resetting would
	# destroy them.  (A freshly initialized cluster has no multixids.)
	$out =~ /^Latest checkpoint's NextMultiXactId: *(\d+)$/m or die;
	my $next_mxid = $1;
	$out =~ /^Latest checkpoint's oldestMultiXid: *(\d+)$/m or die;
	my $oldest_mxid = $1;
	die "cluster has some multixids in use" unless $next_mxid == $oldest_mxid;

	# Extract a few other values from pg_resetwal --dry-run output
	# that we need for the calculations below
	$out =~ /^Database block size: *(\d+)$/m or die;
	my $blcksz = $1;
	# SLRU_PAGES_PER_SEGMENT is always 32 on pre-19 versions
	my $slru_pages_per_segment = 32;

	# Do the reset
	my @cmd = (
		$pg_resetwal_path,
		'--pgdata' => $node->data_dir,
		'--multixact-offset' => $mxoffset,
		'--multixact-ids' => "$mxid,$mxid");
	command_ok(\@cmd, 'reset multixids and offset');

	# pg_resetwal just updates the control file.  The cluster will
	# refuse to start up, if the SLRU segments corresponding to the
	# next multixid and offset does not exist.  Create a segments that
	# covers the given values, filled with zeros.  But first remove
	# any old segments.
	unlink glob $node->data_dir . "/pg_multixact/offsets/*";
	unlink glob $node->data_dir . "/pg_multixact/members/*";

	# Initialize the 'offsets' SLRU file containing the new next multixid
	# with zeros
	#
	# sizeof(MultiXactOffset) == 4 in PostgreSQL versions before 19
	my $multixact_offsets_per_page = $blcksz / 4;
	my $segno =
	  int($mxid / $multixact_offsets_per_page / $slru_pages_per_segment);
	my $path =
	  sprintf('%s/pg_multixact/offsets/%04X', $node->data_dir, $segno);
	open my $fh, ">", $path
	  or die "could not open \"$path\": $!";
	binmode $fh;
	my $bytes_per_seg = $slru_pages_per_segment * $blcksz;
	syswrite($fh, "\0" x $bytes_per_seg) == $bytes_per_seg
	  or die "could not write to \"$path\": $!";
	close $fh;

	# Same for the 'members' SLRU
	my $multixact_members_per_page = int($blcksz / 20) * 4;
	$segno =
	  int($mxoffset / $multixact_members_per_page / $slru_pages_per_segment);
	$path = sprintf "%s/pg_multixact/members/%04X", $node->data_dir, $segno;
	open $fh, ">", $path
	  or die "could not open \"$path\": $!";
	binmode $fh;
	syswrite($fh, "\0" x $bytes_per_seg) == $bytes_per_seg
	  or die "could not write to \"$path\": $!";
	close($fh);
}

# Main test workhorse routine.  Dump data on old version, run
# pg_upgrade, compare data after upgrade.
sub upgrade_and_compare
{
	my $tag = shift;
	my $oldnode = shift;
	my $newnode = shift;

	command_ok(
		[
			'pg_upgrade', '--no-sync',
			'--old-datadir' => $oldnode->data_dir,
			'--new-datadir' => $newnode->data_dir,
			'--old-bindir' => $oldnode->config_data('--bindir'),
			'--new-bindir' => $newnode->config_data('--bindir'),
			'--socketdir' => $newnode->host,
			'--old-port' => $oldnode->port,
			'--new-port' => $newnode->port,
		],
		'run of pg_upgrade for new instance');

	# Dump contents of the test table, and the status of all updating
	# multixids from the old cluster.  (Locking-only multixids don't
	# need to be preserved so we ignore those)
	#
	# Note: we do this *after* running pg_upgrade, to ensure that we
	# don't set all the hint bits before upgrade by doing the SELECT
	# on the table.
	my ($multixids_start, $multixids_end, undef) =
	  read_multixid_fields($oldnode);
	$oldnode->start;
	my $old_table_contents =
	  get_test_table_contents($oldnode, "oldnode_${tag}_table_contents");
	my $old_multixacts =
	  get_updating_multixact_members($oldnode, $multixids_start,
		$multixids_end, "oldnode_${tag}_multixacts");
	$oldnode->stop;

	# Compare them with the upgraded cluster
	$newnode->start;
	my $new_table_contents =
	  get_test_table_contents($newnode, "newnode_${tag}_table_contents");
	my $new_multixacts =
	  get_updating_multixact_members($newnode, $multixids_start,
		$multixids_end, "newnode_${tag}_multixacts");
	$newnode->stop;

	compare_files($old_table_contents, $new_table_contents,
		'test table contents from original and upgraded clusters match');
	compare_files($old_multixacts, $new_multixacts,
		'multixact members from original and upgraded clusters match');
}

my $old_version;

# Basic scenario: Create a cluster using old installation, run
# multixid-creating workload on it, then upgrade.
#
# This works even even if the old and new version is the same,
# although it's not very interesting as the conversion routines only
# run when upgrading from a pre-v19 cluster.
{
	my $tag = 'basic';
	my $old =
	  PostgreSQL::Test::Cluster->new("${tag}_oldnode",
		install_path => $ENV{oldinstall});
	my $new = PostgreSQL::Test::Cluster->new("${tag}_newnode");

	$old->init(extra => ['-k']);

	$old_version = $old->pg_version;
	note "old installation is version $old_version\n";

	# Run the workload
	my (undef, $start_mxid, $start_mxoff) = read_multixid_fields($old);
	mxact_workload($old, $new);
	my (undef, $finish_mxid, $finish_mxoff) = read_multixid_fields($old);

	note "Testing upgrade, ${tag} scenario\n"
	  . " mxid from ${start_mxid} to ${finish_mxid}\n"
	  . " oldnode mxoff from ${start_mxoff} to ${finish_mxoff}\n";

	$new->init;
	upgrade_and_compare($tag, $old, $new);
}

# Wraparound scenario: This is the same as the basic scenario, but the
# old cluster goes through multixid and offset wraparound.
#
# This requires the old installation to be version 18 or older,
# because the hacks we use to reset the old cluster to a state just
# before the wraparound rely on the pre-v19 file format.  If the old
# cluster is of v19 or above, multixact SLRU conversion is not needed
# anyway.
SKIP:
{
	skip
	  "skipping mxoffset conversion tests because upgrading from the old version does not require conversion"
	  if ($old_version >= '19devel');

	my $tag = 'wraparound';
	my $old =
	  PostgreSQL::Test::Cluster->new("${tag}_oldnode",
		install_path => $ENV{oldinstall});
	my $new = PostgreSQL::Test::Cluster->new("${tag}_newnode");

	$old->init(extra => ['-k']);

	# Reset the old cluster to just before multixid and 32-bit offset
	# wraparound.
	reset_mxid_mxoffset_pre_v19($old, 0xFFFFFA00, 0xFFFFEC00);

	# Run the workload.  This crosses multixid and offset wraparound.
	my (undef, $start_mxid, $start_mxoff) = read_multixid_fields($old);
	mxact_workload($old, $new);
	my (undef, $finish_mxid, $finish_mxoff) = read_multixid_fields($old);

	note "Testing upgrade, ${tag} scenario\n"
	  . " mxid from ${start_mxid} to ${finish_mxid}\n"
	  . " oldnode mxoff from ${start_mxoff} to ${finish_mxoff}\n";

	# Verify that wraparounds happened.
	cmp_ok($finish_mxid, '<', $start_mxid,
		"multixid wrapped around in old cluster");
	cmp_ok($finish_mxoff, '<', $start_mxoff,
		"mxoff wrapped around in old cluster");

	$new->init;
	upgrade_and_compare($tag, $old, $new);
}

done_testing();