mirror of
https://github.com/postgres/postgres.git
synced 2025-07-05 07:21:24 +03:00
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/20240512232923.aa.nmisch@google.com
This commit is contained in:
@ -63,6 +63,7 @@
|
|||||||
#include "storage/procarray.h"
|
#include "storage/procarray.h"
|
||||||
#include "storage/standby.h"
|
#include "storage/standby.h"
|
||||||
#include "utils/datum.h"
|
#include "utils/datum.h"
|
||||||
|
#include "utils/injection_point.h"
|
||||||
#include "utils/inval.h"
|
#include "utils/inval.h"
|
||||||
#include "utils/relcache.h"
|
#include "utils/relcache.h"
|
||||||
#include "utils/snapmgr.h"
|
#include "utils/snapmgr.h"
|
||||||
@ -6080,6 +6081,7 @@ heap_inplace_update(Relation relation, HeapTuple tuple)
|
|||||||
(errcode(ERRCODE_INVALID_TRANSACTION_STATE),
|
(errcode(ERRCODE_INVALID_TRANSACTION_STATE),
|
||||||
errmsg("cannot update tuples during a parallel operation")));
|
errmsg("cannot update tuples during a parallel operation")));
|
||||||
|
|
||||||
|
INJECTION_POINT("inplace-before-pin");
|
||||||
buffer = ReadBuffer(relation, ItemPointerGetBlockNumber(&(tuple->t_self)));
|
buffer = ReadBuffer(relation, ItemPointerGetBlockNumber(&(tuple->t_self)));
|
||||||
LockBuffer(buffer, BUFFER_LOCK_EXCLUSIVE);
|
LockBuffer(buffer, BUFFER_LOCK_EXCLUSIVE);
|
||||||
page = (Page) BufferGetPage(buffer);
|
page = (Page) BufferGetPage(buffer);
|
||||||
|
@ -14,8 +14,13 @@
|
|||||||
|
|
||||||
#include "catalog/pg_type.h"
|
#include "catalog/pg_type.h"
|
||||||
#include "storage/predicate_internals.h"
|
#include "storage/predicate_internals.h"
|
||||||
|
#include "storage/proc.h"
|
||||||
|
#include "storage/procarray.h"
|
||||||
#include "utils/array.h"
|
#include "utils/array.h"
|
||||||
#include "utils/builtins.h"
|
#include "utils/builtins.h"
|
||||||
|
#include "utils/wait_event.h"
|
||||||
|
|
||||||
|
#define UINT32_ACCESS_ONCE(var) ((uint32)(*((volatile uint32 *)&(var))))
|
||||||
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@ -23,8 +28,9 @@
|
|||||||
*
|
*
|
||||||
* Check if specified PID is blocked by any of the PIDs listed in the second
|
* Check if specified PID is blocked by any of the PIDs listed in the second
|
||||||
* argument. Currently, this looks for blocking caused by waiting for
|
* argument. Currently, this looks for blocking caused by waiting for
|
||||||
* heavyweight locks or safe snapshots. We ignore blockage caused by PIDs
|
* injection points, heavyweight locks, or safe snapshots. We ignore blockage
|
||||||
* not directly under the isolationtester's control, eg autovacuum.
|
* caused by PIDs not directly under the isolationtester's control, eg
|
||||||
|
* autovacuum.
|
||||||
*
|
*
|
||||||
* This is an undocumented function intended for use by the isolation tester,
|
* This is an undocumented function intended for use by the isolation tester,
|
||||||
* and may change in future releases as required for testing purposes.
|
* and may change in future releases as required for testing purposes.
|
||||||
@ -34,6 +40,8 @@ pg_isolation_test_session_is_blocked(PG_FUNCTION_ARGS)
|
|||||||
{
|
{
|
||||||
int blocked_pid = PG_GETARG_INT32(0);
|
int blocked_pid = PG_GETARG_INT32(0);
|
||||||
ArrayType *interesting_pids_a = PG_GETARG_ARRAYTYPE_P(1);
|
ArrayType *interesting_pids_a = PG_GETARG_ARRAYTYPE_P(1);
|
||||||
|
PGPROC *proc;
|
||||||
|
const char *wait_event_type;
|
||||||
ArrayType *blocking_pids_a;
|
ArrayType *blocking_pids_a;
|
||||||
int32 *interesting_pids;
|
int32 *interesting_pids;
|
||||||
int32 *blocking_pids;
|
int32 *blocking_pids;
|
||||||
@ -43,6 +51,15 @@ pg_isolation_test_session_is_blocked(PG_FUNCTION_ARGS)
|
|||||||
int i,
|
int i,
|
||||||
j;
|
j;
|
||||||
|
|
||||||
|
/* Check if blocked_pid is in an injection point. */
|
||||||
|
proc = BackendPidGetProc(blocked_pid);
|
||||||
|
if (proc == NULL)
|
||||||
|
PG_RETURN_BOOL(false); /* session gone: definitely unblocked */
|
||||||
|
wait_event_type =
|
||||||
|
pgstat_get_wait_event_type(UINT32_ACCESS_ONCE(proc->wait_event_info));
|
||||||
|
if (wait_event_type && strcmp("InjectionPoint", wait_event_type) == 0)
|
||||||
|
PG_RETURN_BOOL(true);
|
||||||
|
|
||||||
/* Validate the passed-in array */
|
/* Validate the passed-in array */
|
||||||
Assert(ARR_ELEMTYPE(interesting_pids_a) == INT4OID);
|
Assert(ARR_ELEMTYPE(interesting_pids_a) == INT4OID);
|
||||||
if (array_contains_nulls(interesting_pids_a))
|
if (array_contains_nulls(interesting_pids_a))
|
||||||
|
@ -9,6 +9,8 @@ PGFILEDESC = "injection_points - facility for injection points"
|
|||||||
REGRESS = injection_points
|
REGRESS = injection_points
|
||||||
REGRESS_OPTS = --dlpath=$(top_builddir)/src/test/regress
|
REGRESS_OPTS = --dlpath=$(top_builddir)/src/test/regress
|
||||||
|
|
||||||
|
ISOLATION = inplace
|
||||||
|
|
||||||
# The injection points are cluster-wide, so disable installcheck
|
# The injection points are cluster-wide, so disable installcheck
|
||||||
NO_INSTALLCHECK = 1
|
NO_INSTALLCHECK = 1
|
||||||
|
|
||||||
|
43
src/test/modules/injection_points/expected/inplace.out
Normal file
43
src/test/modules/injection_points/expected/inplace.out
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
Parsed test spec with 3 sessions
|
||||||
|
|
||||||
|
starting permutation: vac1 grant2 vac3 mkrels3 read1
|
||||||
|
mkrels
|
||||||
|
------
|
||||||
|
|
||||||
|
(1 row)
|
||||||
|
|
||||||
|
injection_points_attach
|
||||||
|
-----------------------
|
||||||
|
|
||||||
|
(1 row)
|
||||||
|
|
||||||
|
step vac1: VACUUM vactest.orig50; -- wait during inplace update <waiting ...>
|
||||||
|
step grant2: GRANT SELECT ON TABLE vactest.orig50 TO PUBLIC;
|
||||||
|
step vac3: VACUUM pg_class;
|
||||||
|
step mkrels3:
|
||||||
|
SELECT vactest.mkrels('intruder', 1, 100); -- repopulate LP_UNUSED
|
||||||
|
SELECT injection_points_detach('inplace-before-pin');
|
||||||
|
SELECT injection_points_wakeup('inplace-before-pin');
|
||||||
|
|
||||||
|
mkrels
|
||||||
|
------
|
||||||
|
|
||||||
|
(1 row)
|
||||||
|
|
||||||
|
injection_points_detach
|
||||||
|
-----------------------
|
||||||
|
|
||||||
|
(1 row)
|
||||||
|
|
||||||
|
injection_points_wakeup
|
||||||
|
-----------------------
|
||||||
|
|
||||||
|
(1 row)
|
||||||
|
|
||||||
|
step vac1: <... completed>
|
||||||
|
step read1:
|
||||||
|
REINDEX TABLE pg_class; -- look for duplicates
|
||||||
|
SELECT reltuples = -1 AS reltuples_unknown
|
||||||
|
FROM pg_class WHERE oid = 'vactest.orig50'::regclass;
|
||||||
|
|
||||||
|
ERROR: could not create unique index "pg_class_oid_index"
|
@ -37,4 +37,9 @@ tests += {
|
|||||||
# The injection points are cluster-wide, so disable installcheck
|
# The injection points are cluster-wide, so disable installcheck
|
||||||
'runningcheck': false,
|
'runningcheck': false,
|
||||||
},
|
},
|
||||||
|
'isolation': {
|
||||||
|
'specs': [
|
||||||
|
'inplace',
|
||||||
|
],
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
83
src/test/modules/injection_points/specs/inplace.spec
Normal file
83
src/test/modules/injection_points/specs/inplace.spec
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
# Test race conditions involving:
|
||||||
|
# - s1: VACUUM inplace-updating a pg_class row
|
||||||
|
# - s2: GRANT/REVOKE making pg_class rows dead
|
||||||
|
# - s3: "VACUUM pg_class" making dead rows LP_UNUSED; DDL reusing them
|
||||||
|
|
||||||
|
# Need GRANT to make a non-HOT update. Otherwise, "VACUUM pg_class" would
|
||||||
|
# leave an LP_REDIRECT that persists. To get non-HOT, make rels so the
|
||||||
|
# pg_class row for vactest.orig50 is on a filled page (assuming BLCKSZ=8192).
|
||||||
|
# Just to save on filesystem syscalls, use relkind=c for every other rel.
|
||||||
|
setup
|
||||||
|
{
|
||||||
|
CREATE EXTENSION injection_points;
|
||||||
|
CREATE SCHEMA vactest;
|
||||||
|
CREATE FUNCTION vactest.mkrels(text, int, int) RETURNS void
|
||||||
|
LANGUAGE plpgsql SET search_path = vactest AS $$
|
||||||
|
DECLARE
|
||||||
|
tname text;
|
||||||
|
BEGIN
|
||||||
|
FOR i in $2 .. $3 LOOP
|
||||||
|
tname := $1 || i;
|
||||||
|
EXECUTE FORMAT('CREATE TYPE ' || tname || ' AS ()');
|
||||||
|
RAISE DEBUG '% at %', tname, ctid
|
||||||
|
FROM pg_class WHERE oid = tname::regclass;
|
||||||
|
END LOOP;
|
||||||
|
END
|
||||||
|
$$;
|
||||||
|
}
|
||||||
|
setup { VACUUM FULL pg_class; -- reduce free space }
|
||||||
|
setup
|
||||||
|
{
|
||||||
|
SELECT vactest.mkrels('orig', 1, 49);
|
||||||
|
CREATE TABLE vactest.orig50 ();
|
||||||
|
SELECT vactest.mkrels('orig', 51, 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
# XXX DROP causes an assertion failure; adopt DROP once fixed
|
||||||
|
teardown
|
||||||
|
{
|
||||||
|
--DROP SCHEMA vactest CASCADE;
|
||||||
|
DO $$BEGIN EXECUTE 'ALTER SCHEMA vactest RENAME TO schema' || oid FROM pg_namespace where nspname = 'vactest'; END$$;
|
||||||
|
DROP EXTENSION injection_points;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Wait during inplace update, in a VACUUM of vactest.orig50.
|
||||||
|
session s1
|
||||||
|
setup {
|
||||||
|
SELECT injection_points_set_local();
|
||||||
|
SELECT injection_points_attach('inplace-before-pin', 'wait');
|
||||||
|
}
|
||||||
|
step vac1 { VACUUM vactest.orig50; -- wait during inplace update }
|
||||||
|
# One bug scenario leaves two live pg_class tuples for vactest.orig50 and zero
|
||||||
|
# live tuples for one of the "intruder" rels. REINDEX observes the duplicate.
|
||||||
|
step read1 {
|
||||||
|
REINDEX TABLE pg_class; -- look for duplicates
|
||||||
|
SELECT reltuples = -1 AS reltuples_unknown
|
||||||
|
FROM pg_class WHERE oid = 'vactest.orig50'::regclass;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# Transactional updates of the tuple vac1 is waiting to inplace-update.
|
||||||
|
session s2
|
||||||
|
step grant2 { GRANT SELECT ON TABLE vactest.orig50 TO PUBLIC; }
|
||||||
|
|
||||||
|
|
||||||
|
# Non-blocking actions.
|
||||||
|
session s3
|
||||||
|
step vac3 { VACUUM pg_class; }
|
||||||
|
# Reuse the lp that vac1 is waiting to change. I've observed reuse at the 1st
|
||||||
|
# or 18th CREATE, so create excess.
|
||||||
|
step mkrels3 {
|
||||||
|
SELECT vactest.mkrels('intruder', 1, 100); -- repopulate LP_UNUSED
|
||||||
|
SELECT injection_points_detach('inplace-before-pin');
|
||||||
|
SELECT injection_points_wakeup('inplace-before-pin');
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# XXX extant bug
|
||||||
|
permutation
|
||||||
|
vac1(mkrels3) # reads pg_class tuple T0 for vactest.orig50, xmax invalid
|
||||||
|
grant2 # T0 becomes eligible for pruning, T1 is successor
|
||||||
|
vac3 # T0 becomes LP_UNUSED
|
||||||
|
mkrels3 # T0 reused; vac1 wakes and overwrites the reused T0
|
||||||
|
read1
|
Reference in New Issue
Block a user