Skip to content

Commit c35f419

Browse files
committed
Add an injection_points isolation test suite.
Make the isolation harness recognize injection_points wait events as a type of blocked state. Test an extant inplace-update bug. Reviewed by Robert Haas and Michael Paquier. Discussion: https://postgr.es/m/[email protected]
1 parent abfbd13 commit c35f419

File tree

6 files changed

+154
-2
lines changed

6 files changed

+154
-2
lines changed

src/backend/access/heap/heapam.c

+2
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@
6363
#include "storage/procarray.h"
6464
#include "storage/standby.h"
6565
#include "utils/datum.h"
66+
#include "utils/injection_point.h"
6667
#include "utils/inval.h"
6768
#include "utils/relcache.h"
6869
#include "utils/snapmgr.h"
@@ -6080,6 +6081,7 @@ heap_inplace_update(Relation relation, HeapTuple tuple)
60806081
(errcode(ERRCODE_INVALID_TRANSACTION_STATE),
60816082
errmsg("cannot update tuples during a parallel operation")));
60826083

6084+
INJECTION_POINT("inplace-before-pin");
60836085
buffer = ReadBuffer(relation, ItemPointerGetBlockNumber(&(tuple->t_self)));
60846086
LockBuffer(buffer, BUFFER_LOCK_EXCLUSIVE);
60856087
page = (Page) BufferGetPage(buffer);

src/backend/utils/adt/waitfuncs.c

+19-2
Original file line numberDiff line numberDiff line change
@@ -14,17 +14,23 @@
1414

1515
#include "catalog/pg_type.h"
1616
#include "storage/predicate_internals.h"
17+
#include "storage/proc.h"
18+
#include "storage/procarray.h"
1719
#include "utils/array.h"
1820
#include "utils/builtins.h"
21+
#include "utils/wait_event.h"
22+
23+
#define UINT32_ACCESS_ONCE(var) ((uint32)(*((volatile uint32 *)&(var))))
1924

2025

2126
/*
2227
* pg_isolation_test_session_is_blocked - support function for isolationtester
2328
*
2429
* Check if specified PID is blocked by any of the PIDs listed in the second
2530
* argument. Currently, this looks for blocking caused by waiting for
26-
* heavyweight locks or safe snapshots. We ignore blockage caused by PIDs
27-
* not directly under the isolationtester's control, eg autovacuum.
31+
* injection points, heavyweight locks, or safe snapshots. We ignore blockage
32+
* caused by PIDs not directly under the isolationtester's control, eg
33+
* autovacuum.
2834
*
2935
* This is an undocumented function intended for use by the isolation tester,
3036
* and may change in future releases as required for testing purposes.
@@ -34,6 +40,8 @@ pg_isolation_test_session_is_blocked(PG_FUNCTION_ARGS)
3440
{
3541
int blocked_pid = PG_GETARG_INT32(0);
3642
ArrayType *interesting_pids_a = PG_GETARG_ARRAYTYPE_P(1);
43+
PGPROC *proc;
44+
const char *wait_event_type;
3745
ArrayType *blocking_pids_a;
3846
int32 *interesting_pids;
3947
int32 *blocking_pids;
@@ -43,6 +51,15 @@ pg_isolation_test_session_is_blocked(PG_FUNCTION_ARGS)
4351
int i,
4452
j;
4553

54+
/* Check if blocked_pid is in an injection point. */
55+
proc = BackendPidGetProc(blocked_pid);
56+
if (proc == NULL)
57+
PG_RETURN_BOOL(false); /* session gone: definitely unblocked */
58+
wait_event_type =
59+
pgstat_get_wait_event_type(UINT32_ACCESS_ONCE(proc->wait_event_info));
60+
if (wait_event_type && strcmp("InjectionPoint", wait_event_type) == 0)
61+
PG_RETURN_BOOL(true);
62+
4663
/* Validate the passed-in array */
4764
Assert(ARR_ELEMTYPE(interesting_pids_a) == INT4OID);
4865
if (array_contains_nulls(interesting_pids_a))

src/test/modules/injection_points/Makefile

+2
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ PGFILEDESC = "injection_points - facility for injection points"
99
REGRESS = injection_points
1010
REGRESS_OPTS = --dlpath=$(top_builddir)/src/test/regress
1111

12+
ISOLATION = inplace
13+
1214
# The injection points are cluster-wide, so disable installcheck
1315
NO_INSTALLCHECK = 1
1416

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
Parsed test spec with 3 sessions
2+
3+
starting permutation: vac1 grant2 vac3 mkrels3 read1
4+
mkrels
5+
------
6+
7+
(1 row)
8+
9+
injection_points_attach
10+
-----------------------
11+
12+
(1 row)
13+
14+
step vac1: VACUUM vactest.orig50; -- wait during inplace update <waiting ...>
15+
step grant2: GRANT SELECT ON TABLE vactest.orig50 TO PUBLIC;
16+
step vac3: VACUUM pg_class;
17+
step mkrels3:
18+
SELECT vactest.mkrels('intruder', 1, 100); -- repopulate LP_UNUSED
19+
SELECT injection_points_detach('inplace-before-pin');
20+
SELECT injection_points_wakeup('inplace-before-pin');
21+
22+
mkrels
23+
------
24+
25+
(1 row)
26+
27+
injection_points_detach
28+
-----------------------
29+
30+
(1 row)
31+
32+
injection_points_wakeup
33+
-----------------------
34+
35+
(1 row)
36+
37+
step vac1: <... completed>
38+
step read1:
39+
REINDEX TABLE pg_class; -- look for duplicates
40+
SELECT reltuples = -1 AS reltuples_unknown
41+
FROM pg_class WHERE oid = 'vactest.orig50'::regclass;
42+
43+
ERROR: could not create unique index "pg_class_oid_index"

src/test/modules/injection_points/meson.build

+5
Original file line numberDiff line numberDiff line change
@@ -37,4 +37,9 @@ tests += {
3737
# The injection points are cluster-wide, so disable installcheck
3838
'runningcheck': false,
3939
},
40+
'isolation': {
41+
'specs': [
42+
'inplace',
43+
],
44+
},
4045
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
# Test race conditions involving:
2+
# - s1: VACUUM inplace-updating a pg_class row
3+
# - s2: GRANT/REVOKE making pg_class rows dead
4+
# - s3: "VACUUM pg_class" making dead rows LP_UNUSED; DDL reusing them
5+
6+
# Need GRANT to make a non-HOT update. Otherwise, "VACUUM pg_class" would
7+
# leave an LP_REDIRECT that persists. To get non-HOT, make rels so the
8+
# pg_class row for vactest.orig50 is on a filled page (assuming BLCKSZ=8192).
9+
# Just to save on filesystem syscalls, use relkind=c for every other rel.
10+
setup
11+
{
12+
CREATE EXTENSION injection_points;
13+
CREATE SCHEMA vactest;
14+
CREATE FUNCTION vactest.mkrels(text, int, int) RETURNS void
15+
LANGUAGE plpgsql SET search_path = vactest AS $$
16+
DECLARE
17+
tname text;
18+
BEGIN
19+
FOR i in $2 .. $3 LOOP
20+
tname := $1 || i;
21+
EXECUTE FORMAT('CREATE TYPE ' || tname || ' AS ()');
22+
RAISE DEBUG '% at %', tname, ctid
23+
FROM pg_class WHERE oid = tname::regclass;
24+
END LOOP;
25+
END
26+
$$;
27+
}
28+
setup { VACUUM FULL pg_class; -- reduce free space }
29+
setup
30+
{
31+
SELECT vactest.mkrels('orig', 1, 49);
32+
CREATE TABLE vactest.orig50 ();
33+
SELECT vactest.mkrels('orig', 51, 100);
34+
}
35+
36+
# XXX DROP causes an assertion failure; adopt DROP once fixed
37+
teardown
38+
{
39+
--DROP SCHEMA vactest CASCADE;
40+
DO $$BEGIN EXECUTE 'ALTER SCHEMA vactest RENAME TO schema' || oid FROM pg_namespace where nspname = 'vactest'; END$$;
41+
DROP EXTENSION injection_points;
42+
}
43+
44+
# Wait during inplace update, in a VACUUM of vactest.orig50.
45+
session s1
46+
setup {
47+
SELECT injection_points_set_local();
48+
SELECT injection_points_attach('inplace-before-pin', 'wait');
49+
}
50+
step vac1 { VACUUM vactest.orig50; -- wait during inplace update }
51+
# One bug scenario leaves two live pg_class tuples for vactest.orig50 and zero
52+
# live tuples for one of the "intruder" rels. REINDEX observes the duplicate.
53+
step read1 {
54+
REINDEX TABLE pg_class; -- look for duplicates
55+
SELECT reltuples = -1 AS reltuples_unknown
56+
FROM pg_class WHERE oid = 'vactest.orig50'::regclass;
57+
}
58+
59+
60+
# Transactional updates of the tuple vac1 is waiting to inplace-update.
61+
session s2
62+
step grant2 { GRANT SELECT ON TABLE vactest.orig50 TO PUBLIC; }
63+
64+
65+
# Non-blocking actions.
66+
session s3
67+
step vac3 { VACUUM pg_class; }
68+
# Reuse the lp that vac1 is waiting to change. I've observed reuse at the 1st
69+
# or 18th CREATE, so create excess.
70+
step mkrels3 {
71+
SELECT vactest.mkrels('intruder', 1, 100); -- repopulate LP_UNUSED
72+
SELECT injection_points_detach('inplace-before-pin');
73+
SELECT injection_points_wakeup('inplace-before-pin');
74+
}
75+
76+
77+
# XXX extant bug
78+
permutation
79+
vac1(mkrels3) # reads pg_class tuple T0 for vactest.orig50, xmax invalid
80+
grant2 # T0 becomes eligible for pruning, T1 is successor
81+
vac3 # T0 becomes LP_UNUSED
82+
mkrels3 # T0 reused; vac1 wakes and overwrites the reused T0
83+
read1

0 commit comments

Comments
 (0)