test-inplace-inconsistent-read-v0.patch

text/plain

Filename: test-inplace-inconsistent-read-v0.patch
Type: text/plain
Part: 0
Message: datfrozen/relfrozen update race condition

Patch

Format: unified
Series: patch v0
File+
src/backend/commands/vacuum.c 14 0
src/backend/utils/adt/lockfuncs.c 20 2
src/test/modules/injection_points/expected/inplace_update_reader.out 292 0
src/test/modules/injection_points/injection_points.c 25 5
src/test/modules/injection_points/Makefile 2 0
src/test/modules/injection_points/specs/inplace_update_reader.spec 75 0
Author:     Noah Misch <noah@leadboat.com>
Commit:     Noah Misch <noah@leadboat.com>

    Test datfrozenxid/relfrozenxid update race condition
    
    Not for commit, mostly because the injection point won't make sense
    after the fix; it would be obvious that the test is unlikely to detect
    similar future bugs.  (A commit-ready test would also need to remove
    temporary WARNING messages added to clarify what's happening, and it
    would need to hide outputs sensitive to exact XID consumption.)

diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index b589279..73f0245 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -53,9 +53,11 @@
 #include "storage/proc.h"
 #include "storage/procarray.h"
 #include "utils/acl.h"
+#include "utils/builtins.h"
 #include "utils/fmgroids.h"
 #include "utils/guc.h"
 #include "utils/guc_hooks.h"
+#include "utils/injection_point.h"
 #include "utils/memutils.h"
 #include "utils/snapmgr.h"
 #include "utils/syscache.h"
@@ -1620,6 +1622,8 @@ vac_update_datfrozenxid(void)
 	while ((classTup = systable_getnext(scan)) != NULL)
 	{
 		Form_pg_class classForm = (Form_pg_class) GETSTRUCT(classTup);
+		TransactionId before, after;
+		before = classForm->relfrozenxid;
 
 		/*
 		 * Only consider relations able to hold unfrozen XIDs (anything else
@@ -1662,7 +1666,11 @@ vac_update_datfrozenxid(void)
 
 			/* determine new horizon */
 			if (TransactionIdPrecedes(classForm->relfrozenxid, newFrozenXid))
+			{
+				if (namestrcmp(&classForm->relname, "pg_test_last_table") == 0)
+					INJECTION_POINT("between-relfrozenxid-reads");
 				newFrozenXid = classForm->relfrozenxid;
+			}
 		}
 
 		if (MultiXactIdIsValid(classForm->relminmxid))
@@ -1678,6 +1686,12 @@ vac_update_datfrozenxid(void)
 			if (MultiXactIdPrecedes(classForm->relminmxid, newMinMulti))
 				newMinMulti = classForm->relminmxid;
 		}
+
+		after = classForm->relfrozenxid;
+		elog(WARNING, "done reading %s before=%u after=%u myxid=%u myxmin=%u",
+			 NameStr(classForm->relname),
+			 before, after,
+			 MyProc->xid, MyProc->xmin);
 	}
 
 	/* we're done with pg_class */
diff --git a/src/backend/utils/adt/lockfuncs.c b/src/backend/utils/adt/lockfuncs.c
index 13009cc..bb2db58 100644
--- a/src/backend/utils/adt/lockfuncs.c
+++ b/src/backend/utils/adt/lockfuncs.c
@@ -17,8 +17,11 @@
 #include "funcapi.h"
 #include "miscadmin.h"
 #include "storage/predicate_internals.h"
+#include "storage/proc.h"
+#include "storage/procarray.h"
 #include "utils/array.h"
 #include "utils/builtins.h"
+#include "utils/wait_event.h"
 
 
 /*
@@ -606,8 +609,9 @@ pg_safe_snapshot_blocking_pids(PG_FUNCTION_ARGS)
  *
  * 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
- * heavyweight locks or safe snapshots.  We ignore blockage caused by PIDs
- * not directly under the isolationtester's control, eg autovacuum.
+ * injection points, heavyweight locks, or safe snapshots.  We ignore blockage
+ * caused by PIDs not directly under the isolationtester's control, eg
+ * autovacuum.
  *
  * This is an undocumented function intended for use by the isolation tester,
  * and may change in future releases as required for testing purposes.
@@ -615,8 +619,11 @@ pg_safe_snapshot_blocking_pids(PG_FUNCTION_ARGS)
 Datum
 pg_isolation_test_session_is_blocked(PG_FUNCTION_ARGS)
 {
+#define UINT32_ACCESS_ONCE(var)		 ((uint32)(*((volatile uint32 *)&(var))))
 	int			blocked_pid = PG_GETARG_INT32(0);
 	ArrayType  *interesting_pids_a = PG_GETARG_ARRAYTYPE_P(1);
+	PGPROC	   *proc;
+	const char *wait_event;
 	ArrayType  *blocking_pids_a;
 	int32	   *interesting_pids;
 	int32	   *blocking_pids;
@@ -626,6 +633,17 @@ pg_isolation_test_session_is_blocked(PG_FUNCTION_ARGS)
 	int			i,
 				j;
 
+	/* Check if blocked_pid is in injection_wait(). */
+	proc = BackendPidGetProc(blocked_pid);
+	if (proc == NULL)
+		PG_RETURN_BOOL(false);	/* session gone: definitely unblocked */
+	wait_event =
+		pgstat_get_wait_event(UINT32_ACCESS_ONCE(proc->wait_event_info));
+	if (wait_event && strncmp("INJECTION_POINT(",
+							  wait_event,
+							  strlen("INJECTION_POINT(")) == 0)
+		PG_RETURN_BOOL(true);
+
 	/* Validate the passed-in array */
 	Assert(ARR_ELEMTYPE(interesting_pids_a) == INT4OID);
 	if (array_contains_nulls(interesting_pids_a))
diff --git a/src/test/modules/injection_points/Makefile b/src/test/modules/injection_points/Makefile
index 31bd787..7ee6135d 100644
--- a/src/test/modules/injection_points/Makefile
+++ b/src/test/modules/injection_points/Makefile
@@ -9,6 +9,8 @@ PGFILEDESC = "injection_points - facility for injection points"
 REGRESS = injection_points
 REGRESS_OPTS = --dlpath=$(top_builddir)/src/test/regress
 
+ISOLATION = inplace_update_reader
+
 # The injection points are cluster-wide, so disable installcheck
 NO_INSTALLCHECK = 1
 
diff --git a/src/test/modules/injection_points/expected/inplace_update_reader.out b/src/test/modules/injection_points/expected/inplace_update_reader.out
new file mode 100644
index 0000000..799100e
--- /dev/null
+++ b/src/test/modules/injection_points/expected/inplace_update_reader.out
@@ -0,0 +1,292 @@
+Parsed test spec with 5 sessions
+
+starting permutation: assign1 lock2 vac4 assign1 lock3 attach4 vac4 r2 vac5 detach1 read1 r3
+step assign1: SELECT txid_current();
+txid_current
+------------
+         743
+(1 row)
+
+step lock2: BEGIN; LOCK pg_test_last_table IN SHARE UPDATE EXCLUSIVE MODE;
+s4: WARNING:  done reading pg_statistic before=744 after=744 myxid=0 myxmin=744
+s4: WARNING:  done reading pg_type before=744 after=744 myxid=0 myxmin=744
+s4: WARNING:  done reading pg_toast_1255 before=744 after=744 myxid=0 myxmin=744
+s4: WARNING:  done reading pg_toast_1247 before=744 after=744 myxid=0 myxmin=744
+s4: WARNING:  done reading pg_toast_2604 before=744 after=744 myxid=0 myxmin=744
+s4: WARNING:  done reading pg_toast_2606 before=744 after=744 myxid=0 myxmin=744
+s4: WARNING:  done reading pg_toast_2612 before=744 after=744 myxid=0 myxmin=744
+s4: WARNING:  done reading pg_toast_2600 before=744 after=744 myxid=0 myxmin=744
+s4: WARNING:  done reading pg_toast_2619 before=744 after=744 myxid=0 myxmin=744
+s4: WARNING:  done reading pg_toast_3381 before=744 after=744 myxid=0 myxmin=744
+s4: WARNING:  done reading pg_toast_3429 before=744 after=744 myxid=0 myxmin=744
+s4: WARNING:  done reading pg_toast_2618 before=744 after=744 myxid=0 myxmin=744
+s4: WARNING:  done reading pg_toast_2620 before=744 after=744 myxid=0 myxmin=744
+s4: WARNING:  done reading pg_toast_3466 before=744 after=744 myxid=0 myxmin=744
+s4: WARNING:  done reading pg_toast_2609 before=744 after=744 myxid=0 myxmin=744
+s4: WARNING:  done reading pg_foreign_table before=744 after=744 myxid=0 myxmin=744
+s4: WARNING:  done reading pg_toast_2615 before=744 after=744 myxid=0 myxmin=744
+s4: WARNING:  done reading pg_toast_1262 before=744 after=744 myxid=0 myxmin=744
+s4: WARNING:  done reading pg_toast_2964 before=744 after=744 myxid=0 myxmin=744
+s4: WARNING:  done reading pg_toast_1213 before=744 after=744 myxid=0 myxmin=744
+s4: WARNING:  done reading pg_toast_1260 before=744 after=744 myxid=0 myxmin=744
+s4: WARNING:  done reading pg_toast_2396 before=744 after=744 myxid=0 myxmin=744
+s4: WARNING:  done reading pg_toast_3600 before=744 after=744 myxid=0 myxmin=744
+s4: WARNING:  done reading pg_toast_3079 before=744 after=744 myxid=0 myxmin=744
+s4: WARNING:  done reading pg_toast_2328 before=744 after=744 myxid=0 myxmin=744
+s4: WARNING:  done reading pg_toast_1417 before=744 after=744 myxid=0 myxmin=744
+s4: WARNING:  done reading pg_toast_1418 before=744 after=744 myxid=0 myxmin=744
+s4: WARNING:  done reading pg_toast_3118 before=744 after=744 myxid=0 myxmin=744
+s4: WARNING:  done reading pg_toast_3256 before=744 after=744 myxid=0 myxmin=744
+s4: WARNING:  done reading pg_toast_6000 before=744 after=744 myxid=0 myxmin=744
+s4: WARNING:  done reading pg_toast_826 before=744 after=744 myxid=0 myxmin=744
+s4: WARNING:  done reading pg_toast_3394 before=744 after=744 myxid=0 myxmin=744
+s4: WARNING:  done reading pg_toast_3596 before=744 after=744 myxid=0 myxmin=744
+s4: WARNING:  done reading pg_toast_3592 before=744 after=744 myxid=0 myxmin=744
+s4: WARNING:  done reading pg_toast_3456 before=744 after=744 myxid=0 myxmin=744
+s4: WARNING:  done reading pg_toast_6243 before=744 after=744 myxid=0 myxmin=744
+s4: WARNING:  done reading pg_toast_3350 before=744 after=744 myxid=0 myxmin=744
+s4: WARNING:  done reading pg_toast_6106 before=744 after=744 myxid=0 myxmin=744
+s4: WARNING:  done reading pg_toast_6100 before=744 after=744 myxid=0 myxmin=744
+s4: WARNING:  done reading pg_authid before=744 after=744 myxid=0 myxmin=744
+s4: WARNING:  done reading pg_statistic_ext_data before=744 after=744 myxid=0 myxmin=744
+s4: WARNING:  done reading pg_user_mapping before=744 after=744 myxid=0 myxmin=744
+s4: WARNING:  done reading pg_subscription before=744 after=744 myxid=0 myxmin=744
+s4: WARNING:  done reading pg_attribute before=744 after=744 myxid=0 myxmin=744
+s4: WARNING:  done reading pg_proc before=744 after=744 myxid=0 myxmin=744
+s4: WARNING:  done reading pg_attrdef before=744 after=744 myxid=0 myxmin=744
+s4: WARNING:  done reading pg_constraint before=744 after=744 myxid=0 myxmin=744
+s4: WARNING:  done reading pg_inherits before=744 after=744 myxid=0 myxmin=744
+s4: WARNING:  done reading pg_index before=744 after=744 myxid=0 myxmin=744
+s4: WARNING:  done reading pg_operator before=744 after=744 myxid=0 myxmin=744
+s4: WARNING:  done reading pg_opfamily before=744 after=744 myxid=0 myxmin=744
+s4: WARNING:  done reading pg_opclass before=744 after=744 myxid=0 myxmin=744
+s4: WARNING:  done reading pg_am before=744 after=744 myxid=0 myxmin=744
+s4: WARNING:  done reading pg_amop before=744 after=744 myxid=0 myxmin=744
+s4: WARNING:  done reading pg_amproc before=744 after=744 myxid=0 myxmin=744
+s4: WARNING:  done reading pg_language before=744 after=744 myxid=0 myxmin=744
+s4: WARNING:  done reading pg_largeobject_metadata before=744 after=744 myxid=0 myxmin=744
+s4: WARNING:  done reading pg_aggregate before=744 after=744 myxid=0 myxmin=744
+s4: WARNING:  done reading pg_statistic_ext before=744 after=744 myxid=0 myxmin=744
+s4: WARNING:  done reading pg_rewrite before=744 after=744 myxid=0 myxmin=744
+s4: WARNING:  done reading pg_trigger before=744 after=744 myxid=0 myxmin=744
+s4: WARNING:  done reading pg_event_trigger before=744 after=744 myxid=0 myxmin=744
+s4: WARNING:  done reading pg_description before=744 after=744 myxid=0 myxmin=744
+s4: WARNING:  done reading pg_cast before=744 after=744 myxid=0 myxmin=744
+s4: WARNING:  done reading pg_enum before=744 after=744 myxid=0 myxmin=744
+s4: WARNING:  done reading pg_namespace before=744 after=744 myxid=0 myxmin=744
+s4: WARNING:  done reading pg_conversion before=744 after=744 myxid=0 myxmin=744
+s4: WARNING:  done reading pg_depend before=744 after=744 myxid=0 myxmin=744
+s4: WARNING:  done reading pg_database before=744 after=744 myxid=0 myxmin=744
+s4: WARNING:  done reading pg_db_role_setting before=744 after=744 myxid=0 myxmin=744
+s4: WARNING:  done reading pg_tablespace before=744 after=744 myxid=0 myxmin=744
+s4: WARNING:  done reading pg_auth_members before=744 after=744 myxid=0 myxmin=744
+s4: WARNING:  done reading pg_shdepend before=744 after=744 myxid=0 myxmin=744
+s4: WARNING:  done reading pg_shdescription before=744 after=744 myxid=0 myxmin=744
+s4: WARNING:  done reading pg_ts_config before=744 after=744 myxid=0 myxmin=744
+s4: WARNING:  done reading pg_ts_config_map before=744 after=744 myxid=0 myxmin=744
+s4: WARNING:  done reading pg_ts_dict before=744 after=744 myxid=0 myxmin=744
+s4: WARNING:  done reading pg_ts_parser before=744 after=744 myxid=0 myxmin=744
+s4: WARNING:  done reading pg_ts_template before=744 after=744 myxid=0 myxmin=744
+s4: WARNING:  done reading pg_extension before=744 after=744 myxid=0 myxmin=744
+s4: WARNING:  done reading pg_foreign_data_wrapper before=744 after=744 myxid=0 myxmin=744
+s4: WARNING:  done reading pg_foreign_server before=744 after=744 myxid=0 myxmin=744
+s4: WARNING:  done reading pg_policy before=744 after=744 myxid=0 myxmin=744
+s4: WARNING:  done reading pg_replication_origin before=744 after=744 myxid=0 myxmin=744
+s4: WARNING:  done reading pg_default_acl before=744 after=744 myxid=0 myxmin=744
+s4: WARNING:  done reading pg_init_privs before=744 after=744 myxid=0 myxmin=744
+s4: WARNING:  done reading pg_seclabel before=744 after=744 myxid=0 myxmin=744
+s4: WARNING:  done reading pg_shseclabel before=744 after=744 myxid=0 myxmin=744
+s4: WARNING:  done reading pg_collation before=744 after=744 myxid=0 myxmin=744
+s4: WARNING:  done reading pg_parameter_acl before=744 after=744 myxid=0 myxmin=744
+s4: WARNING:  done reading pg_partitioned_table before=744 after=744 myxid=0 myxmin=744
+s4: WARNING:  done reading pg_range before=744 after=744 myxid=0 myxmin=744
+s4: WARNING:  done reading pg_transform before=744 after=744 myxid=0 myxmin=744
+s4: WARNING:  done reading pg_sequence before=744 after=744 myxid=0 myxmin=744
+s4: WARNING:  done reading pg_publication before=744 after=744 myxid=0 myxmin=744
+s4: WARNING:  done reading pg_publication_namespace before=744 after=744 myxid=0 myxmin=744
+s4: WARNING:  done reading pg_publication_rel before=744 after=744 myxid=0 myxmin=744
+s4: WARNING:  done reading pg_subscription_rel before=744 after=744 myxid=0 myxmin=744
+s4: WARNING:  done reading pg_largeobject before=744 after=744 myxid=0 myxmin=744
+s4: WARNING:  done reading pg_toast_14108 before=744 after=744 myxid=0 myxmin=744
+s4: WARNING:  done reading sql_features before=744 after=744 myxid=0 myxmin=744
+s4: WARNING:  done reading pg_toast_14113 before=744 after=744 myxid=0 myxmin=744
+s4: WARNING:  done reading sql_implementation_info before=744 after=744 myxid=0 myxmin=744
+s4: WARNING:  done reading pg_toast_14118 before=744 after=744 myxid=0 myxmin=744
+s4: WARNING:  done reading sql_parts before=744 after=744 myxid=0 myxmin=744
+s4: WARNING:  done reading pg_toast_14123 before=744 after=744 myxid=0 myxmin=744
+s4: WARNING:  done reading sql_sizing before=744 after=744 myxid=0 myxmin=744
+s4: WARNING:  done reading pg_class before=744 after=744 myxid=0 myxmin=744
+s4: WARNING:  done reading second_oldest before=744 after=744 myxid=0 myxmin=744
+s4: WARNING:  done reading pg_test_last_table before=742 after=742 myxid=0 myxmin=744
+step vac4: VACUUM (FREEZE, SKIP_LOCKED);
+step assign1: SELECT txid_current();
+txid_current
+------------
+         744
+(1 row)
+
+step lock3: BEGIN; LOCK second_oldest IN SHARE UPDATE EXCLUSIVE MODE;
+step attach4: 
+ SELECT injection_points_set_local();
+ SELECT injection_points_attach('between-relfrozenxid-reads', 'wait');
+
+injection_points_set_local
+--------------------------
+                          
+(1 row)
+
+injection_points_attach
+-----------------------
+                       
+(1 row)
+
+s4: WARNING:  done reading pg_statistic before=745 after=745 myxid=0 myxmin=745
+s4: WARNING:  done reading pg_type before=745 after=745 myxid=0 myxmin=745
+s4: WARNING:  done reading pg_toast_1255 before=745 after=745 myxid=0 myxmin=745
+s4: WARNING:  done reading pg_toast_1247 before=745 after=745 myxid=0 myxmin=745
+s4: WARNING:  done reading pg_toast_2604 before=745 after=745 myxid=0 myxmin=745
+s4: WARNING:  done reading pg_toast_2606 before=745 after=745 myxid=0 myxmin=745
+s4: WARNING:  done reading pg_toast_2612 before=745 after=745 myxid=0 myxmin=745
+s4: WARNING:  done reading pg_toast_2600 before=745 after=745 myxid=0 myxmin=745
+s4: WARNING:  done reading pg_toast_2619 before=745 after=745 myxid=0 myxmin=745
+s4: WARNING:  done reading pg_toast_3381 before=745 after=745 myxid=0 myxmin=745
+s4: WARNING:  done reading pg_toast_3429 before=745 after=745 myxid=0 myxmin=745
+s4: WARNING:  done reading pg_toast_2618 before=745 after=745 myxid=0 myxmin=745
+s4: WARNING:  done reading pg_toast_2620 before=745 after=745 myxid=0 myxmin=745
+s4: WARNING:  done reading pg_toast_3466 before=745 after=745 myxid=0 myxmin=745
+s4: WARNING:  done reading pg_toast_2609 before=745 after=745 myxid=0 myxmin=745
+s4: WARNING:  done reading pg_foreign_table before=745 after=745 myxid=0 myxmin=745
+s4: WARNING:  done reading pg_toast_2615 before=745 after=745 myxid=0 myxmin=745
+s4: WARNING:  done reading pg_toast_1262 before=745 after=745 myxid=0 myxmin=745
+s4: WARNING:  done reading pg_toast_2964 before=745 after=745 myxid=0 myxmin=745
+s4: WARNING:  done reading pg_toast_1213 before=745 after=745 myxid=0 myxmin=745
+s4: WARNING:  done reading pg_toast_1260 before=745 after=745 myxid=0 myxmin=745
+s4: WARNING:  done reading pg_toast_2396 before=745 after=745 myxid=0 myxmin=745
+s4: WARNING:  done reading pg_toast_3600 before=745 after=745 myxid=0 myxmin=745
+s4: WARNING:  done reading pg_toast_3079 before=745 after=745 myxid=0 myxmin=745
+s4: WARNING:  done reading pg_toast_2328 before=745 after=745 myxid=0 myxmin=745
+s4: WARNING:  done reading pg_toast_1417 before=745 after=745 myxid=0 myxmin=745
+s4: WARNING:  done reading pg_toast_1418 before=745 after=745 myxid=0 myxmin=745
+s4: WARNING:  done reading pg_toast_3118 before=745 after=745 myxid=0 myxmin=745
+s4: WARNING:  done reading pg_toast_3256 before=745 after=745 myxid=0 myxmin=745
+s4: WARNING:  done reading pg_toast_6000 before=745 after=745 myxid=0 myxmin=745
+s4: WARNING:  done reading pg_toast_826 before=745 after=745 myxid=0 myxmin=745
+s4: WARNING:  done reading pg_toast_3394 before=745 after=745 myxid=0 myxmin=745
+s4: WARNING:  done reading pg_toast_3596 before=745 after=745 myxid=0 myxmin=745
+s4: WARNING:  done reading pg_toast_3592 before=745 after=745 myxid=0 myxmin=745
+s4: WARNING:  done reading pg_toast_3456 before=745 after=745 myxid=0 myxmin=745
+s4: WARNING:  done reading pg_toast_6243 before=745 after=745 myxid=0 myxmin=745
+s4: WARNING:  done reading pg_toast_3350 before=745 after=745 myxid=0 myxmin=745
+s4: WARNING:  done reading pg_toast_6106 before=745 after=745 myxid=0 myxmin=745
+s4: WARNING:  done reading pg_toast_6100 before=745 after=745 myxid=0 myxmin=745
+s4: WARNING:  done reading pg_authid before=745 after=745 myxid=0 myxmin=745
+s4: WARNING:  done reading pg_statistic_ext_data before=745 after=745 myxid=0 myxmin=745
+s4: WARNING:  done reading pg_user_mapping before=745 after=745 myxid=0 myxmin=745
+s4: WARNING:  done reading pg_subscription before=745 after=745 myxid=0 myxmin=745
+s4: WARNING:  done reading pg_attribute before=745 after=745 myxid=0 myxmin=745
+s4: WARNING:  done reading pg_proc before=745 after=745 myxid=0 myxmin=745
+s4: WARNING:  done reading pg_attrdef before=745 after=745 myxid=0 myxmin=745
+s4: WARNING:  done reading pg_constraint before=745 after=745 myxid=0 myxmin=745
+s4: WARNING:  done reading pg_inherits before=745 after=745 myxid=0 myxmin=745
+s4: WARNING:  done reading pg_index before=745 after=745 myxid=0 myxmin=745
+s4: WARNING:  done reading pg_operator before=745 after=745 myxid=0 myxmin=745
+s4: WARNING:  done reading pg_opfamily before=745 after=745 myxid=0 myxmin=745
+s4: WARNING:  done reading pg_opclass before=745 after=745 myxid=0 myxmin=745
+s4: WARNING:  done reading pg_am before=745 after=745 myxid=0 myxmin=745
+s4: WARNING:  done reading pg_amop before=745 after=745 myxid=0 myxmin=745
+s4: WARNING:  done reading pg_amproc before=745 after=745 myxid=0 myxmin=745
+s4: WARNING:  done reading pg_language before=745 after=745 myxid=0 myxmin=745
+s4: WARNING:  done reading pg_largeobject_metadata before=745 after=745 myxid=0 myxmin=745
+s4: WARNING:  done reading pg_aggregate before=745 after=745 myxid=0 myxmin=745
+s4: WARNING:  done reading pg_statistic_ext before=745 after=745 myxid=0 myxmin=745
+s4: WARNING:  done reading pg_rewrite before=745 after=745 myxid=0 myxmin=745
+s4: WARNING:  done reading pg_trigger before=745 after=745 myxid=0 myxmin=745
+s4: WARNING:  done reading pg_event_trigger before=745 after=745 myxid=0 myxmin=745
+s4: WARNING:  done reading pg_description before=745 after=745 myxid=0 myxmin=745
+s4: WARNING:  done reading pg_cast before=745 after=745 myxid=0 myxmin=745
+s4: WARNING:  done reading pg_enum before=745 after=745 myxid=0 myxmin=745
+s4: WARNING:  done reading pg_namespace before=745 after=745 myxid=0 myxmin=745
+s4: WARNING:  done reading pg_conversion before=745 after=745 myxid=0 myxmin=745
+s4: WARNING:  done reading pg_depend before=745 after=745 myxid=0 myxmin=745
+s4: WARNING:  done reading pg_database before=745 after=745 myxid=0 myxmin=745
+s4: WARNING:  done reading pg_db_role_setting before=745 after=745 myxid=0 myxmin=745
+s4: WARNING:  done reading pg_tablespace before=745 after=745 myxid=0 myxmin=745
+s4: WARNING:  done reading pg_auth_members before=745 after=745 myxid=0 myxmin=745
+s4: WARNING:  done reading pg_shdepend before=745 after=745 myxid=0 myxmin=745
+s4: WARNING:  done reading pg_shdescription before=745 after=745 myxid=0 myxmin=745
+s4: WARNING:  done reading pg_ts_config before=745 after=745 myxid=0 myxmin=745
+s4: WARNING:  done reading pg_ts_config_map before=745 after=745 myxid=0 myxmin=745
+s4: WARNING:  done reading pg_ts_dict before=745 after=745 myxid=0 myxmin=745
+s4: WARNING:  done reading pg_ts_parser before=745 after=745 myxid=0 myxmin=745
+s4: WARNING:  done reading pg_ts_template before=745 after=745 myxid=0 myxmin=745
+s4: WARNING:  done reading pg_extension before=745 after=745 myxid=0 myxmin=745
+s4: WARNING:  done reading pg_foreign_data_wrapper before=745 after=745 myxid=0 myxmin=745
+s4: WARNING:  done reading pg_foreign_server before=745 after=745 myxid=0 myxmin=745
+s4: WARNING:  done reading pg_policy before=745 after=745 myxid=0 myxmin=745
+s4: WARNING:  done reading pg_replication_origin before=745 after=745 myxid=0 myxmin=745
+s4: WARNING:  done reading pg_default_acl before=745 after=745 myxid=0 myxmin=745
+s4: WARNING:  done reading pg_init_privs before=745 after=745 myxid=0 myxmin=745
+s4: WARNING:  done reading pg_seclabel before=745 after=745 myxid=0 myxmin=745
+s4: WARNING:  done reading pg_shseclabel before=745 after=745 myxid=0 myxmin=745
+s4: WARNING:  done reading pg_collation before=745 after=745 myxid=0 myxmin=745
+s4: WARNING:  done reading pg_parameter_acl before=745 after=745 myxid=0 myxmin=745
+s4: WARNING:  done reading pg_partitioned_table before=745 after=745 myxid=0 myxmin=745
+s4: WARNING:  done reading pg_range before=745 after=745 myxid=0 myxmin=745
+s4: WARNING:  done reading pg_transform before=745 after=745 myxid=0 myxmin=745
+s4: WARNING:  done reading pg_sequence before=745 after=745 myxid=0 myxmin=745
+s4: WARNING:  done reading pg_publication before=745 after=745 myxid=0 myxmin=745
+s4: WARNING:  done reading pg_publication_namespace before=745 after=745 myxid=0 myxmin=745
+s4: WARNING:  done reading pg_publication_rel before=745 after=745 myxid=0 myxmin=745
+s4: WARNING:  done reading pg_subscription_rel before=745 after=745 myxid=0 myxmin=745
+s4: WARNING:  done reading pg_largeobject before=745 after=745 myxid=0 myxmin=745
+s4: WARNING:  done reading pg_toast_14108 before=745 after=745 myxid=0 myxmin=745
+s4: WARNING:  done reading sql_features before=745 after=745 myxid=0 myxmin=745
+s4: WARNING:  done reading pg_toast_14113 before=745 after=745 myxid=0 myxmin=745
+s4: WARNING:  done reading sql_implementation_info before=745 after=745 myxid=0 myxmin=745
+s4: WARNING:  done reading pg_toast_14118 before=745 after=745 myxid=0 myxmin=745
+s4: WARNING:  done reading sql_parts before=745 after=745 myxid=0 myxmin=745
+s4: WARNING:  done reading pg_toast_14123 before=745 after=745 myxid=0 myxmin=745
+s4: WARNING:  done reading sql_sizing before=745 after=745 myxid=0 myxmin=745
+s4: WARNING:  done reading pg_class before=745 after=745 myxid=0 myxmin=745
+s4: WARNING:  done reading second_oldest before=744 after=744 myxid=0 myxmin=745
+step vac4: VACUUM (FREEZE, SKIP_LOCKED); <waiting ...>
+step r2: ROLLBACK;
+step vac5: VACUUM (FREEZE, SKIP_DATABASE_STATS) pg_test_last_table;
+step detach1: 
+ SELECT injection_points_detach('between-relfrozenxid-reads');
+
+injection_points_detach
+-----------------------
+                       
+(1 row)
+
+s4: WARNING:  done reading pg_test_last_table before=742 after=745 myxid=0 myxmin=745
+step vac4: <... completed>
+step read1: 
+ -- should never return rows, but it does in this test case
+ select c.oid::regclass
+ from pg_database d, pg_class c
+ where relkind = 'r' and age(relfrozenxid) > age(datfrozenxid);
+
+ select age(datfrozenxid) from pg_database where datname = current_catalog;
+
+ select min(age(relfrozenxid)), max(age(relfrozenxid))
+ from pg_class c
+ where relkind = 'r';
+
+oid          
+-------------
+second_oldest
+(1 row)
+
+age
+---
+  0
+(1 row)
+
+min|max
+---+---
+  0|  1
+(1 row)
+
+step r3: ROLLBACK;
diff --git a/src/test/modules/injection_points/injection_points.c b/src/test/modules/injection_points/injection_points.c
index ace386d..f4fe678 100644
--- a/src/test/modules/injection_points/injection_points.c
+++ b/src/test/modules/injection_points/injection_points.c
@@ -205,6 +205,7 @@ injection_notice(const char *name)
 void
 injection_wait(const char *name)
 {
+	char		event_name[NAMEDATALEN];
 	uint32		old_wait_counts = 0;
 	int			index = -1;
 	uint32		injection_wait_event = 0;
@@ -216,11 +217,11 @@ injection_wait(const char *name)
 		return;
 
 	/*
-	 * Use the injection point name for this custom wait event.  Note that
-	 * this custom wait event name is not released, but we don't care much for
-	 * testing as this should be short-lived.
+	 * Note that this custom wait event name is not released, but we don't
+	 * care much for testing as this should be short-lived.
 	 */
-	injection_wait_event = WaitEventExtensionNew(name);
+	snprintf(event_name, sizeof(event_name), "INJECTION_POINT(%s)", name);
+	injection_wait_event = WaitEventExtensionNew(event_name);
 
 	/*
 	 * Find a free slot to wait for, and register this injection point's name.
@@ -402,14 +403,28 @@ Datum
 injection_points_detach(PG_FUNCTION_ARGS)
 {
 	char	   *name = text_to_cstring(PG_GETARG_TEXT_PP(0));
+	int			wait_index = -1;
 
 	InjectionPointDetach(name);
 
 	if (inj_state == NULL)
 		injection_init_shmem();
 
-	/* Clean up any conditions associated to this injection point */
 	SpinLockAcquire(&inj_state->lock);
+
+	/* First bump any wait counter for the injection point to wake up */
+	for (int i = 0; i < INJ_MAX_WAIT; i++)
+	{
+		if (strcmp(name, inj_state->name[i]) == 0)
+		{
+			wait_index = i;
+			break;
+		}
+	}
+	if (wait_index >= 0)
+		inj_state->wait_counts[wait_index]++;
+
+	/* Clean up any conditions associated to this injection point */
 	for (int i = 0; i < INJ_MAX_CONDITION; i++)
 	{
 		InjectionPointCondition *condition = &inj_state->conditions[i];
@@ -420,7 +435,12 @@ injection_points_detach(PG_FUNCTION_ARGS)
 			condition->name[0] = '\0';
 		}
 	}
+
 	SpinLockRelease(&inj_state->lock);
 
+	/* And broadcast the change to any waiters */
+	if (wait_index >= 0)
+		ConditionVariableBroadcast(&inj_state->wait_point);
+
 	PG_RETURN_VOID();
 }
diff --git a/src/test/modules/injection_points/specs/inplace_update_reader.spec b/src/test/modules/injection_points/specs/inplace_update_reader.spec
new file mode 100644
index 0000000..325ba12
--- /dev/null
+++ b/src/test/modules/injection_points/specs/inplace_update_reader.spec
@@ -0,0 +1,75 @@
+# Test race between vac_update_relstats() and vac_update_datfrozenxid()
+
+# damage is hard to reach because:
+# - When v_u_d() first reads the value, the value must be a new minimum.
+# - The same v_u_d() must not read any later tuple that wipes out the damage.
+
+setup
+{
+ CREATE EXTENSION injection_points;
+}
+setup
+{
+ VACUUM FULL pg_class;  -- help CREATE TABLE place at end
+}
+setup
+{
+ CREATE TABLE second_oldest ();
+ CREATE TABLE pg_test_last_table ();
+}
+
+teardown
+{
+ DROP EXTENSION injection_points;
+}
+
+
+# s1 does non-blocking, no-transaction-block activities
+session s1
+step assign1	{ SELECT txid_current(); }
+step read1
+{
+ -- should never return rows, but it does in this test case
+ select c.oid::regclass
+ from pg_database d, pg_class c
+ where relkind = 'r' and age(relfrozenxid) > age(datfrozenxid);
+
+ select age(datfrozenxid) from pg_database where datname = current_catalog;
+
+ select min(age(relfrozenxid)), max(age(relfrozenxid))
+ from pg_class c
+ where relkind = 'r';
+}
+step detach1
+{
+ SELECT injection_points_detach('between-relfrozenxid-reads');
+}
+
+# s2 holds the lock on pg_test_last_table
+session s2
+step lock2	{ BEGIN; LOCK pg_test_last_table IN SHARE UPDATE EXCLUSIVE MODE; }
+step r2		{ ROLLBACK; }
+
+# s3 holds the lock on second_oldest
+session s3
+step lock3	{ BEGIN; LOCK second_oldest IN SHARE UPDATE EXCLUSIVE MODE; }
+step r3		{ ROLLBACK; }
+
+# s4 does database-wide VACUUM w/ SKIP_LOCKED
+session s4
+step attach4
+{
+ SELECT injection_points_set_local();
+ SELECT injection_points_attach('between-relfrozenxid-reads', 'wait');
+}
+step vac4	{ VACUUM (FREEZE, SKIP_LOCKED); }
+
+# s5 does single-table VACUUM
+session s5
+step vac5	{ VACUUM (FREEZE, SKIP_DATABASE_STATS) pg_test_last_table; }
+
+
+# 1st vac4 updates every relfrozenxid except pg_test_last_table
+# 2nd vac4 updates every relfrozenxid except pg_test_last_table and second_oldest
+# vac5 updates pg_test_last_table relfrozenxid between the two vac_update_datfrozenxid reads
+permutation assign1 lock2 vac4 assign1 lock3 attach4 vac4(detach1) r2 vac5 detach1 read1 r3