From 0b2ad92de8d13f53857a3ae12bdeecb24b8f473b Mon Sep 17 00:00:00 2001
From: Alexander Korotkov <akorotkov@postgresql.org>
Date: Tue, 3 Nov 2020 03:43:53 +0300
Subject: [PATCH] Stopevents

---
 src/backend/Makefile                               |  10 +-
 src/backend/access/gin/ginget.c                    |  28 ++
 src/backend/postmaster/pgstat.c                    |   3 +
 src/backend/storage/buffer/bufmgr.c                |  28 ++
 src/backend/storage/ipc/ipci.c                     |   3 +
 src/backend/storage/lmgr/.gitignore                |   2 +
 src/backend/storage/lmgr/Makefile                  |  10 +-
 .../storage/lmgr/generate-stopeventnames.pl        |  62 ++++
 src/backend/storage/lmgr/stopevent.c               | 348 +++++++++++++++++++++
 src/backend/storage/lmgr/stopeventnames.txt        |   2 +
 src/backend/utils/adt/lockfuncs.c                  |   4 +
 src/backend/utils/misc/guc.c                       |  23 ++
 src/include/catalog/pg_proc.dat                    |  13 +
 src/include/pgstat.h                               |   1 +
 src/include/storage/.gitignore                     |   1 +
 src/include/storage/stopevent.h                    |  33 ++
 .../expected/gin-traverse-deleted-pages.out        |  26 ++
 src/test/isolation/isolation_schedule              |   1 +
 .../specs/gin-traverse-deleted-pages.spec          |  30 ++
 src/tools/pgindent/typedefs.list                   |   1 +
 20 files changed, 627 insertions(+), 2 deletions(-)
 create mode 100644 src/backend/storage/lmgr/generate-stopeventnames.pl
 create mode 100644 src/backend/storage/lmgr/stopevent.c
 create mode 100644 src/backend/storage/lmgr/stopeventnames.txt
 create mode 100644 src/include/storage/stopevent.h
 create mode 100644 src/test/isolation/expected/gin-traverse-deleted-pages.out
 create mode 100644 src/test/isolation/specs/gin-traverse-deleted-pages.spec

diff --git a/src/backend/Makefile b/src/backend/Makefile
index 9706a958488..e1a0c50081d 100644
--- a/src/backend/Makefile
+++ b/src/backend/Makefile
@@ -139,6 +139,9 @@ parser/gram.h: parser/gram.y
 storage/lmgr/lwlocknames.h: storage/lmgr/generate-lwlocknames.pl storage/lmgr/lwlocknames.txt
 	$(MAKE) -C storage/lmgr lwlocknames.h lwlocknames.c
 
+storage/lmgr/stopeventnames.h: storage/lmgr/generate-stopeventnames.pl storage/lmgr/stopeventnames.txt
+	$(MAKE) -C storage/lmgr stopeventnames.h stopeventnames.c
+
 # run this unconditionally to avoid needing to know its dependencies here:
 submake-catalog-headers:
 	$(MAKE) -C catalog distprep generated-header-symlinks
@@ -162,7 +165,7 @@ submake-utils-headers:
 
 .PHONY: generated-headers
 
-generated-headers: $(top_builddir)/src/include/parser/gram.h $(top_builddir)/src/include/storage/lwlocknames.h submake-catalog-headers submake-utils-headers
+generated-headers: $(top_builddir)/src/include/parser/gram.h $(top_builddir)/src/include/storage/lwlocknames.h $(top_builddir)/src/include/storage/stopeventnames.h submake-catalog-headers submake-utils-headers
 
 $(top_builddir)/src/include/parser/gram.h: parser/gram.h
 	prereqdir=`cd '$(dir $<)' >/dev/null && pwd` && \
@@ -174,6 +177,11 @@ $(top_builddir)/src/include/storage/lwlocknames.h: storage/lmgr/lwlocknames.h
 	  cd '$(dir $@)' && rm -f $(notdir $@) && \
 	  $(LN_S) "$$prereqdir/$(notdir $<)" .
 
+$(top_builddir)/src/include/storage/stopeventnames.h: storage/lmgr/stopeventnames.h
+	prereqdir=`cd '$(dir $<)' >/dev/null && pwd` && \
+	  cd '$(dir $@)' && rm -f $(notdir $@) && \
+	  $(LN_S) "$$prereqdir/$(notdir $<)" .
+
 utils/probes.o: utils/probes.d $(SUBDIROBJS)
 	$(DTRACE) $(DTRACEFLAGS) -C -G -s $(call expand_subsys,$^) -o $@
 
diff --git a/src/backend/access/gin/ginget.c b/src/backend/access/gin/ginget.c
index 2cfccdedcf5..04ce2d8a2b0 100644
--- a/src/backend/access/gin/ginget.c
+++ b/src/backend/access/gin/ginget.c
@@ -18,7 +18,9 @@
 #include "access/relscan.h"
 #include "miscadmin.h"
 #include "storage/predicate.h"
+#include "storage/stopevent.h"
 #include "utils/datum.h"
+#include "utils/jsonb.h"
 #include "utils/memutils.h"
 #include "utils/rel.h"
 
@@ -642,6 +644,27 @@ startScan(IndexScanDesc scan)
 		startScanKey(ginstate, so, so->keys + i);
 }
 
+static Jsonb *
+EntryFindPostingLeafPageStopEventParams(Relation index,
+										OffsetNumber attnum,
+										BlockNumber rootBlockNum)
+{
+	MemoryContext oldCxt;
+	JsonbParseState *state = NULL;
+	Jsonb	   *res;
+
+	stopevents_make_cxt();
+	oldCxt = MemoryContextSwitchTo(stopevents_cxt);
+	pushJsonbValue(&state, WJB_BEGIN_OBJECT, NULL);
+	relation_stopevent_params(&state, index);
+	jsonb_push_int8_key(&state, "attnum", attnum);
+	jsonb_push_int8_key(&state, "rootBlockNum", rootBlockNum);
+	res = JsonbValueToJsonb(pushJsonbValue(&state, WJB_END_OBJECT, NULL));
+	MemoryContextSwitchTo(oldCxt);
+
+	return res;
+}
+
 /*
  * Load the next batch of item pointers from a posting tree.
  *
@@ -695,6 +718,11 @@ entryLoadMoreItems(GinState *ginstate, GinScanEntry entry,
 						   OffsetNumberNext(GinItemPointerGetOffsetNumber(&advancePast)));
 		}
 		entry->btree.fullScan = false;
+
+		STOPEVENT(EntryFindPostingLeafPageStopEvent,
+				  EntryFindPostingLeafPageStopEventParams(ginstate->index,
+														  entry->attnum,
+														  entry->btree.rootBlkno));
 		stack = ginFindLeafPage(&entry->btree, true, false, snapshot);
 
 		/* we don't need the stack, just the buffer. */
diff --git a/src/backend/postmaster/pgstat.c b/src/backend/postmaster/pgstat.c
index e76e627c6b2..809aa7746da 100644
--- a/src/backend/postmaster/pgstat.c
+++ b/src/backend/postmaster/pgstat.c
@@ -4018,6 +4018,9 @@ pgstat_get_wait_ipc(WaitEventIPC w)
 		case WAIT_EVENT_SAFE_SNAPSHOT:
 			event_name = "SafeSnapshot";
 			break;
+		case WAIT_EVENT_STOPEVENT:
+			event_name = "StopEvent";
+			break;
 		case WAIT_EVENT_SYNC_REP:
 			event_name = "SyncRep";
 			break;
diff --git a/src/backend/storage/buffer/bufmgr.c b/src/backend/storage/buffer/bufmgr.c
index ad0d1a9abc0..6028c4485d4 100644
--- a/src/backend/storage/buffer/bufmgr.c
+++ b/src/backend/storage/buffer/bufmgr.c
@@ -49,6 +49,7 @@
 #include "storage/proc.h"
 #include "storage/smgr.h"
 #include "storage/standby.h"
+#include "storage/stopevent.h"
 #include "utils/memdebug.h"
 #include "utils/ps_status.h"
 #include "utils/rel.h"
@@ -1517,6 +1518,28 @@ MarkBufferDirty(Buffer buffer)
 	}
 }
 
+static Jsonb *
+ReleaseAndReadBufferStopEventParams(Relation relation,
+									BlockNumber oldBlockNum,
+									BlockNumber newBlockNum)
+{
+	MemoryContext oldCxt;
+	JsonbParseState *state = NULL;
+	Jsonb	   *res;
+
+	stopevents_make_cxt();
+	oldCxt = MemoryContextSwitchTo(stopevents_cxt);
+	pushJsonbValue(&state, WJB_BEGIN_OBJECT, NULL);
+	relation_stopevent_params(&state, relation);
+	jsonb_push_int8_key(&state, "oldBlockNum", oldBlockNum);
+	jsonb_push_int8_key(&state, "newBlockNum", newBlockNum);
+	res = JsonbValueToJsonb(pushJsonbValue(&state, WJB_END_OBJECT, NULL));
+	MemoryContextSwitchTo(oldCxt);
+
+	return res;
+}
+
+
 /*
  * ReleaseAndReadBuffer -- combine ReleaseBuffer() and ReadBuffer()
  *
@@ -1537,9 +1560,11 @@ ReleaseAndReadBuffer(Buffer buffer,
 {
 	ForkNumber	forkNum = MAIN_FORKNUM;
 	BufferDesc *bufHdr;
+	BlockNumber oldBlockNum = InvalidBlockNumber;
 
 	if (BufferIsValid(buffer))
 	{
+		oldBlockNum = BufferGetBlockNumber(buffer);
 		Assert(BufferIsPinned(buffer));
 		if (BufferIsLocal(buffer))
 		{
@@ -1563,6 +1588,9 @@ ReleaseAndReadBuffer(Buffer buffer,
 		}
 	}
 
+	STOPEVENT(ReleaseAndReadBufferStopEvent,
+			  ReleaseAndReadBufferStopEventParams(relation, oldBlockNum, blockNum));
+
 	return ReadBuffer(relation, blockNum);
 }
 
diff --git a/src/backend/storage/ipc/ipci.c b/src/backend/storage/ipc/ipci.c
index 96c2aaabbd6..f96a74f15ed 100644
--- a/src/backend/storage/ipc/ipci.c
+++ b/src/backend/storage/ipc/ipci.c
@@ -45,6 +45,7 @@
 #include "storage/procsignal.h"
 #include "storage/sinvaladt.h"
 #include "storage/spin.h"
+#include "storage/stopevent.h"
 #include "utils/snapmgr.h"
 
 /* GUCs */
@@ -149,6 +150,7 @@ CreateSharedMemoryAndSemaphores(void)
 		size = add_size(size, BTreeShmemSize());
 		size = add_size(size, SyncScanShmemSize());
 		size = add_size(size, AsyncShmemSize());
+		size = add_size(size, StopEventShmemSize());
 #ifdef EXEC_BACKEND
 		size = add_size(size, ShmemBackendArraySize());
 #endif
@@ -267,6 +269,7 @@ CreateSharedMemoryAndSemaphores(void)
 	BTreeShmemInit();
 	SyncScanShmemInit();
 	AsyncShmemInit();
+	StopEventShmemInit();
 
 #ifdef EXEC_BACKEND
 
diff --git a/src/backend/storage/lmgr/.gitignore b/src/backend/storage/lmgr/.gitignore
index 9355caea8c1..b9f0a43d507 100644
--- a/src/backend/storage/lmgr/.gitignore
+++ b/src/backend/storage/lmgr/.gitignore
@@ -1,2 +1,4 @@
 /lwlocknames.c
 /lwlocknames.h
+/stopeventnames.c
+/stopeventnames.h
diff --git a/src/backend/storage/lmgr/Makefile b/src/backend/storage/lmgr/Makefile
index 829b792fcb1..b1a3c4f4430 100644
--- a/src/backend/storage/lmgr/Makefile
+++ b/src/backend/storage/lmgr/Makefile
@@ -22,7 +22,9 @@ OBJS = \
 	predicate.o \
 	proc.o \
 	s_lock.o \
-	spin.o
+	spin.o \
+	stopevent.o \
+	stopeventnames.o
 
 include $(top_srcdir)/src/backend/common.mk
 
@@ -41,6 +43,12 @@ lwlocknames.c: lwlocknames.h
 lwlocknames.h: $(top_srcdir)/src/backend/storage/lmgr/lwlocknames.txt generate-lwlocknames.pl
 	$(PERL) $(srcdir)/generate-lwlocknames.pl $<
 
+stopeventnames.c: stopeventnames.h
+	touch $@
+
+stopeventnames.h: $(top_srcdir)/src/backend/storage/lmgr/stopeventnames.txt generate-stopeventnames.pl
+	$(PERL) $(srcdir)/generate-stopeventnames.pl $<
+
 check: s_lock_test
 	./s_lock_test
 
diff --git a/src/backend/storage/lmgr/generate-stopeventnames.pl b/src/backend/storage/lmgr/generate-stopeventnames.pl
new file mode 100644
index 00000000000..8fbf1f54dee
--- /dev/null
+++ b/src/backend/storage/lmgr/generate-stopeventnames.pl
@@ -0,0 +1,62 @@
+#!/usr/bin/perl
+#
+# Generate stopeventnames.h and stopeventnames.c from stopeventnames.txt
+# Copyright (c) 2020, PostgreSQL Global Development Group
+
+use strict;
+use warnings;
+
+my $continue    = "\n";
+
+open my $stopeventnames, '<', $ARGV[0] or die;
+
+# Include PID in suffix in case parallel make runs this multiple times.
+my $htmp = "stopeventnames.h.tmp$$";
+my $ctmp = "stopeventnames.c.tmp$$";
+open my $h, '>', $htmp or die "Could not open $htmp: $!";
+open my $c, '>', $ctmp or die "Could not open $ctmp: $!";
+
+my $autogen =
+  "/* autogenerated from src/backend/storage/lmgr/stopeventnames.txt, do not edit */\n";
+print $h $autogen;
+print $h "/* there is deliberately not an #ifndef STOPEVENTNAMES_H here */\n\n";
+print $c $autogen, "\n";
+
+print $c "const char *const stopeventnames[] = {";
+
+my $eventidx = 0;
+while (<$stopeventnames>)
+{
+	chomp;
+
+	# Skip comments
+	next if /^#/;
+	next if /^\s*$/;
+
+	die "unable to parse stopeventnames.txt"
+	  unless /^(\w+)$/;
+
+	my $eventname = $1;
+
+	my $trimmedeventname = $eventname;
+	$trimmedeventname =~ s/StopEvent$//;
+	die "event names must end with 'StopEvent'" if $trimmedeventname eq $eventname;
+
+	printf $c "%s	\"%s\"", $continue, $trimmedeventname;
+	$continue    = ",\n";
+
+	print $h "#define $eventname $eventidx\n";
+	$eventidx++;
+}
+
+printf $c "\n};\n";
+print $h "\n";
+printf $h "#define NUM_BUILTIN_STOPEVENTS		%s\n", $eventidx;
+
+close $h;
+close $c;
+
+rename($htmp, 'stopeventnames.h') || die "rename: $htmp: $!";
+rename($ctmp, 'stopeventnames.c') || die "rename: $ctmp: $!";
+
+close $stopeventnames;
diff --git a/src/backend/storage/lmgr/stopevent.c b/src/backend/storage/lmgr/stopevent.c
new file mode 100644
index 00000000000..6b6eb2b6683
--- /dev/null
+++ b/src/backend/storage/lmgr/stopevent.c
@@ -0,0 +1,348 @@
+/*-------------------------------------------------------------------------
+ *
+ * stopevent.c
+ *	  Auxiliary infrastructure for automated testing of concurrency issues
+ *
+ * Copyright (c) 2020, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ *	  src/backend/storage/lmgr/stopevent.c
+*/
+#include "postgres.h"
+
+#include "commands/dbcommands.h"
+#include "miscadmin.h"
+#include "nodes/execnodes.h"
+#include "pgstat.h"
+#include "storage/condition_variable.h"
+#include "storage/proclist.h"
+#include "storage/shmem.h"
+#include "storage/stopevent.h"
+#include "utils/builtins.h"
+#include "utils/jsonpath.h"
+#include "utils/memutils.h"
+#include "utils/rel.h"
+
+#define QUERY_BUFFER_SIZE 1024
+
+typedef struct
+{
+	char		condition[QUERY_BUFFER_SIZE];
+	bool		enabled;
+	slock_t		lock;
+	ConditionVariable cv;
+} StopEvent;
+
+bool		enable_stopevents = false;
+bool		trace_stopevents = false;
+StopEvent  *stopevents = NULL;
+MemoryContext stopevents_cxt = NULL;
+
+Size
+StopEventShmemSize(void)
+{
+	Size		size;
+
+	size = mul_size(NUM_BUILTIN_STOPEVENTS, sizeof(StopEvent));
+	return size;
+}
+
+void
+StopEventShmemInit(void)
+{
+	Size		size = StopEventShmemSize();
+	bool		found;
+
+	stopevents = (StopEvent *) ShmemInitStruct("Stop events Data",
+											   size,
+											   &found);
+
+	if (!found)
+	{
+		int			i;
+
+		for (i = 0; i < NUM_BUILTIN_STOPEVENTS; i++)
+		{
+			SpinLockInit(&stopevents[i].lock);
+			stopevents[i].enabled = false;
+			ConditionVariableInit(&stopevents[i].cv);
+		}
+	}
+}
+
+static StopEvent *
+find_stop_event(text *name)
+{
+	int			i;
+	char	   *name_data = VARDATA_ANY(name);
+	int			len = VARSIZE_ANY_EXHDR(name);
+
+	for (i = 0; i < NUM_BUILTIN_STOPEVENTS; i++)
+	{
+		if (strlen(stopeventnames[i]) == len &&
+			memcmp(name_data, stopeventnames[i], len) == 0)
+			return &stopevents[i];
+	}
+
+	elog(ERROR, "unknown stop event: \"%s\"", text_to_cstring(name));
+	return NULL;
+}
+
+Datum
+pg_stopevent_set(PG_FUNCTION_ARGS)
+{
+	text	   *event_name = PG_GETARG_TEXT_PP(0);
+	JsonPath   *condition = PG_GETARG_JSONPATH_P(1);
+	StopEvent  *event;
+
+	event = find_stop_event(event_name);
+
+	if (VARSIZE_ANY(condition) > QUERY_BUFFER_SIZE)
+		elog(ERROR, "jsonpath condition is too long");
+
+	SpinLockAcquire(&event->lock);
+	event->enabled = true;
+	memcpy(&event->condition, condition, VARSIZE_ANY(condition));
+	SpinLockRelease(&event->lock);
+
+	ConditionVariableBroadcast(&event->cv);
+
+	PG_FREE_IF_COPY(condition, 1);
+	PG_RETURN_VOID();
+}
+
+Datum
+pg_stopevent_reset(PG_FUNCTION_ARGS)
+{
+	text	   *event_name = PG_GETARG_TEXT_PP(0);
+	StopEvent  *event;
+
+	event = find_stop_event(event_name);
+
+	SpinLockAcquire(&event->lock);
+	event->enabled = false;
+	SpinLockRelease(&event->lock);
+
+	ConditionVariableBroadcast(&event->cv);
+
+	PG_RETURN_VOID();
+}
+
+Datum
+pg_stopevents(PG_FUNCTION_ARGS)
+{
+	ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo;
+	bool		randomAccess;
+	TupleDesc	tupdesc;
+	Tuplestorestate *tupstore;
+	MemoryContext oldcontext;
+	AttrNumber	attnum;
+	int			i;
+
+	/* The tupdesc and tuplestore must be created in ecxt_per_query_memory */
+	oldcontext = MemoryContextSwitchTo(rsinfo->econtext->ecxt_per_query_memory);
+
+	tupdesc = CreateTemplateTupleDesc(3);
+	attnum = (AttrNumber) 1;
+	TupleDescInitEntry(tupdesc, attnum, "stopevent", TEXTOID, -1, 0);
+	attnum++;
+	TupleDescInitEntry(tupdesc, attnum, "condition", JSONPATHOID, -1, 0);
+	attnum++;
+	TupleDescInitEntry(tupdesc, attnum, "waiters", INT4ARRAYOID, -1, 0);
+
+	randomAccess = (rsinfo->allowedModes & SFRM_Materialize_Random) != 0;
+	tupstore = tuplestore_begin_heap(randomAccess, false, work_mem);
+	rsinfo->returnMode = SFRM_Materialize;
+	rsinfo->setResult = tupstore;
+	rsinfo->setDesc = tupdesc;
+
+	MemoryContextSwitchTo(oldcontext);
+
+	for (i = 0; i < NUM_BUILTIN_STOPEVENTS; i++)
+	{
+		Datum		values[3];
+		bool		nulls[3] = {false, false, false};
+		StopEvent  *event = &stopevents[i];
+		proclist_mutable_iter iter;
+		List	   *waiters = NIL;
+		Datum	   *elems;
+		ListCell   *lc;
+		int			j;
+
+		SpinLockAcquire(&event->lock);
+		if (!event->enabled)
+		{
+			SpinLockRelease(&event->lock);
+			continue;
+		}
+		values[0] = PointerGetDatum(cstring_to_text(stopeventnames[i]));
+		values[1] = PointerGetDatum(&event->condition);
+
+		SpinLockAcquire(&event->cv.mutex);
+		proclist_foreach_modify(iter, &event->cv.wakeup, cvWaitLink)
+		{
+			PGPROC	   *waiter = GetPGProcByNumber(iter.cur);
+
+			waiters = lappend_int(waiters, waiter->pid);
+		}
+		SpinLockRelease(&event->cv.mutex);
+
+		elems = (Datum *) palloc(sizeof(Datum) * list_length(waiters));
+		j = 0;
+		foreach(lc, waiters)
+		{
+			elems[j] = Int32GetDatum(lfirst_int(lc));
+			j++;
+		}
+		values[2] = PointerGetDatum(construct_array(elems, list_length(waiters), INT4OID, 4, true, 'i'));
+
+		tuplestore_putvalues(tupstore, tupdesc, values, nulls);
+		SpinLockRelease(&event->lock);
+	}
+	PG_RETURN_VOID();
+}
+
+bool
+pid_is_waiting_for_stopevent(int pid)
+{
+	int			i;
+
+	for (i = 0; i < NUM_BUILTIN_STOPEVENTS; i++)
+	{
+		StopEvent  *event = &stopevents[i];
+		proclist_mutable_iter iter;
+
+		SpinLockAcquire(&event->lock);
+		if (!event->enabled)
+		{
+			SpinLockRelease(&event->lock);
+			continue;
+		}
+
+		SpinLockAcquire(&event->cv.mutex);
+		proclist_foreach_modify(iter, &event->cv.wakeup, cvWaitLink)
+		{
+			PGPROC	   *waiter = GetPGProcByNumber(iter.cur);
+
+			if (waiter->pid == pid)
+			{
+				SpinLockRelease(&event->cv.mutex);
+				SpinLockRelease(&event->lock);
+				return true;
+			}
+		}
+		SpinLockRelease(&event->cv.mutex);
+		SpinLockRelease(&event->lock);
+	}
+	return false;
+}
+
+static bool
+check_stopevent_condition(StopEvent *event, Jsonb *params)
+{
+	Datum		res;
+
+	SpinLockAcquire(&event->lock);
+	if (!event->enabled)
+	{
+		SpinLockRelease(&event->lock);
+		return false;
+	}
+
+	res = DirectFunctionCall2(jsonb_path_match,
+							  PointerGetDatum(params),
+							  PointerGetDatum(&event->condition));
+
+	SpinLockRelease(&event->lock);
+
+	return DatumGetBool(res);
+}
+
+void
+handle_stopevent(int event_id, Jsonb *params)
+{
+	StopEvent  *event = &stopevents[event_id];
+
+	Assert(event_id >= 0 && event_id < NUM_BUILTIN_STOPEVENTS);
+
+	if (event->enabled && check_stopevent_condition(event, params))
+	{
+		ConditionVariablePrepareToSleep(&event->cv);
+		for (;;)
+		{
+			if (!check_stopevent_condition(event, params))
+				break;
+			ConditionVariableSleep(&event->cv, WAIT_EVENT_STOPEVENT);
+		}
+		ConditionVariableCancelSleep();
+	}
+
+	if (trace_stopevents)
+	{
+		char	   *params_string;
+
+		params_string = DatumGetCString(DirectFunctionCall1(jsonb_out, PointerGetDatum(params)));
+		elog(DEBUG2, "stop event \"%s\", params \"%s\"",
+			 stopeventnames[event_id],
+			 params_string);
+		pfree(params_string);
+	}
+
+	MemoryContextReset(stopevents_cxt);
+}
+
+void
+stopevents_make_cxt(void)
+{
+	if (!stopevents_cxt)
+		stopevents_cxt = AllocSetContextCreate(TopMemoryContext,
+											   "CacheMemoryContext",
+											   ALLOCSET_DEFAULT_SIZES);
+}
+
+void
+jsonb_push_key(JsonbParseState **state, char *key)
+{
+	JsonbValue	jval;
+
+	jval.type = jbvString;
+	jval.val.string.len = strlen(key);
+	jval.val.string.val = key;
+	(void) pushJsonbValue(state, WJB_KEY, &jval);
+}
+
+void
+jsonb_push_int8_key(JsonbParseState **state, char *key, int64 value)
+{
+	JsonbValue	jval;
+
+	jsonb_push_key(state, key);
+
+	jval.type = jbvNumeric;
+	jval.val.numeric = DatumGetNumeric(DirectFunctionCall1(int8_numeric, Int64GetDatum(value)));
+	(void) pushJsonbValue(state, WJB_VALUE, &jval);
+
+}
+
+void
+jsonb_push_string_key(JsonbParseState **state, const char *key,
+					  const char *value)
+{
+	JsonbValue	jval;
+
+	jsonb_push_key(state, (char *) key);
+
+	jval.type = jbvString;
+	jval.val.string.len = strlen(value);
+	jval.val.string.val = (char *) value;
+	(void) pushJsonbValue(state, WJB_VALUE, &jval);
+}
+
+void
+relation_stopevent_params(JsonbParseState **state, Relation relation)
+{
+	jsonb_push_int8_key(state, "datoid", MyDatabaseId);
+	jsonb_push_string_key(state, "datname", get_database_name(MyDatabaseId));
+	jsonb_push_int8_key(state, "reloid", relation->rd_id);
+	jsonb_push_string_key(state, "relname", NameStr(relation->rd_rel->relname));
+}
diff --git a/src/backend/storage/lmgr/stopeventnames.txt b/src/backend/storage/lmgr/stopeventnames.txt
new file mode 100644
index 00000000000..b5330a1d13f
--- /dev/null
+++ b/src/backend/storage/lmgr/stopeventnames.txt
@@ -0,0 +1,2 @@
+ReleaseAndReadBufferStopEvent
+EntryFindPostingLeafPageStopEvent
diff --git a/src/backend/utils/adt/lockfuncs.c b/src/backend/utils/adt/lockfuncs.c
index f592292d067..93de0da60ea 100644
--- a/src/backend/utils/adt/lockfuncs.c
+++ b/src/backend/utils/adt/lockfuncs.c
@@ -18,6 +18,7 @@
 #include "funcapi.h"
 #include "miscadmin.h"
 #include "storage/predicate_internals.h"
+#include "storage/stopevent.h"
 #include "utils/array.h"
 #include "utils/builtins.h"
 
@@ -653,6 +654,9 @@ pg_isolation_test_session_is_blocked(PG_FUNCTION_ARGS)
 	if (GetSafeSnapshotBlockingPids(blocked_pid, &dummy, 1) > 0)
 		PG_RETURN_BOOL(true);
 
+	if (pid_is_waiting_for_stopevent(blocked_pid))
+		PG_RETURN_BOOL(true);
+
 	PG_RETURN_BOOL(false);
 }
 
diff --git a/src/backend/utils/misc/guc.c b/src/backend/utils/misc/guc.c
index bb34630e8e4..df049fa4f60 100644
--- a/src/backend/utils/misc/guc.c
+++ b/src/backend/utils/misc/guc.c
@@ -83,6 +83,7 @@
 #include "storage/predicate.h"
 #include "storage/proc.h"
 #include "storage/standby.h"
+#include "storage/stopevent.h"
 #include "tcop/tcopprot.h"
 #include "tsearch/ts_cache.h"
 #include "utils/acl.h"
@@ -2036,6 +2037,28 @@ static struct config_bool ConfigureNamesBool[] =
 		NULL, NULL, NULL
 	},
 
+	{
+		{"enable_stopevents", PGC_SUSET, DEVELOPER_OPTIONS,
+			gettext_noop("Sets whether stop events are enabled."),
+			NULL,
+			GUC_NOT_IN_SAMPLE
+		},
+		&enable_stopevents,
+		false,
+		NULL, NULL, NULL
+	},
+
+	{
+		{"trace_stopevents", PGC_SUSET, DEVELOPER_OPTIONS,
+			gettext_noop("Sets whether trace stop events to the log."),
+			NULL,
+			GUC_NOT_IN_SAMPLE
+		},
+		&trace_stopevents,
+		false,
+		NULL, NULL, NULL
+	},
+
 	/* End-of-list marker */
 	{
 		{NULL, 0, 0, NULL, NULL}, NULL, false, NULL, NULL, NULL
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index e7fbda9f81a..372f505f3a9 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -11009,4 +11009,17 @@
   proname => 'is_normalized', prorettype => 'bool', proargtypes => 'text text',
   prosrc => 'unicode_is_normalized' },
 
+{ oid => '8000', descr => 'set stop event',
+  proname => 'pg_stopevent_set', prorettype => 'void', proargtypes => 'text jsonpath',
+  prosrc => 'pg_stopevent_set', provolatile => 'v' },
+
+{ oid => '8001', descr => 'reset stop event',
+  proname => 'pg_stopevent_reset', prorettype => 'void', proargtypes => 'text',
+  prosrc => 'pg_stopevent_reset', provolatile => 'v' },
+
+{ oid => '8002', descr => 'view stop events',
+  proname => 'pg_stopevents', prorows => '10', proretset => 't',
+  provolatile => 'v', prorettype => 'record', proargtypes => '',
+  proallargtypes => '{text,jsonpath,_int4}', proargmodes => '{o,o,o}',
+  proargnames => '{event_name,condition,waiters}', prosrc => 'pg_stopevents' },
 ]
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 257e515bfe7..b8986dd1bef 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -957,6 +957,7 @@ typedef enum
 	WAIT_EVENT_REPLICATION_ORIGIN_DROP,
 	WAIT_EVENT_REPLICATION_SLOT_DROP,
 	WAIT_EVENT_SAFE_SNAPSHOT,
+	WAIT_EVENT_STOPEVENT,
 	WAIT_EVENT_SYNC_REP,
 	WAIT_EVENT_XACT_GROUP_UPDATE
 } WaitEventIPC;
diff --git a/src/include/storage/.gitignore b/src/include/storage/.gitignore
index 209c8be7223..a1e65d8df4e 100644
--- a/src/include/storage/.gitignore
+++ b/src/include/storage/.gitignore
@@ -1 +1,2 @@
 /lwlocknames.h
+/stopeventnames.h
diff --git a/src/include/storage/stopevent.h b/src/include/storage/stopevent.h
new file mode 100644
index 00000000000..a8b62f20c25
--- /dev/null
+++ b/src/include/storage/stopevent.h
@@ -0,0 +1,33 @@
+#ifndef SRC_STOPEVENT_H
+#define SRC_STOPEVENT_H
+
+#include "utils/jsonb.h"
+#include "storage/stopeventnames.h"
+
+extern bool enable_stopevents;
+extern bool trace_stopevents;
+extern const char *const stopeventnames[];
+extern MemoryContext stopevents_cxt;
+
+#define STOPEVENT(event_id, params) \
+	do { \
+		if (enable_stopevents || trace_stopevents) \
+			handle_stopevent((event_id), (params)); \
+	} while(0)
+
+extern Size StopEventShmemSize(void);
+extern void StopEventShmemInit(void);
+extern Datum pg_stopevent_set(PG_FUNCTION_ARGS);
+extern Datum pg_stopevent_reset(PG_FUNCTION_ARGS);
+extern Datum pg_stopevents(PG_FUNCTION_ARGS);
+extern bool pid_is_waiting_for_stopevent(int pid);
+extern void handle_stopevent(int event_id, Jsonb *params);
+extern void stopevents_make_cxt(void);
+extern void jsonb_push_key(JsonbParseState **state, char *key);
+extern void jsonb_push_int8_key(JsonbParseState **state, char *key, int64 value);
+extern void jsonb_push_string_key(JsonbParseState **state, const char *key,
+								  const char *value);
+extern void relation_stopevent_params(JsonbParseState **state,
+									  Relation relation);
+
+#endif							/* SRC_STOPEVENT_H */
diff --git a/src/test/isolation/expected/gin-traverse-deleted-pages.out b/src/test/isolation/expected/gin-traverse-deleted-pages.out
new file mode 100644
index 00000000000..18d86b95db2
--- /dev/null
+++ b/src/test/isolation/expected/gin-traverse-deleted-pages.out
@@ -0,0 +1,26 @@
+Parsed test spec with 2 sessions
+
+starting permutation: s2s1 s1s s2s2 s2v s2s3
+step s2s1: select pg_stopevent_set('EntryFindPostingLeafPage',
+									  '$.datname == "isolation_regression" && $.relname == "tmp_idx"');
+pg_stopevent_set
+
+               
+step s1s: select * from tmp where ar @> array[1,2]; <waiting ...>
+step s2s2: select pg_stopevent_set('ReleaseAndReadBuffer',
+									  '$.datname == "isolation_regression" && $.relname == "tmp_idx"');
+			  select pg_stopevent_reset('EntryFindPostingLeafPage');
+pg_stopevent_set
+
+               
+pg_stopevent_reset
+
+               
+step s2v: vacuum tmp;
+step s2s3: select pg_stopevent_reset('ReleaseAndReadBuffer');
+pg_stopevent_reset
+
+               
+step s1s: <... completed>
+ar             
+
diff --git a/src/test/isolation/isolation_schedule b/src/test/isolation/isolation_schedule
index f2e752c4454..2e7063168be 100644
--- a/src/test/isolation/isolation_schedule
+++ b/src/test/isolation/isolation_schedule
@@ -91,3 +91,4 @@ test: plpgsql-toast
 test: truncate-conflict
 test: serializable-parallel
 test: serializable-parallel-2
+test: gin-traverse-deleted-pages
diff --git a/src/test/isolation/specs/gin-traverse-deleted-pages.spec b/src/test/isolation/specs/gin-traverse-deleted-pages.spec
new file mode 100644
index 00000000000..5814cec8354
--- /dev/null
+++ b/src/test/isolation/specs/gin-traverse-deleted-pages.spec
@@ -0,0 +1,30 @@
+setup
+{
+  create table tmp (ar int[]) with (autovacuum_enabled = false);
+  insert into tmp (select array[1] from generate_series(1,10000) i);
+  insert into tmp values (array[1,2]);
+  insert into tmp (select array[1] from generate_series(1,10000) i);
+  create index tmp_idx on tmp using gin(ar);
+  delete from tmp;
+}
+
+teardown
+{
+  DROP TABLE tmp;
+}
+
+session "s1"
+setup		{ set enable_stopevents = true;
+			  set max_parallel_workers_per_gather = 0; }
+step "s1s"	{ select * from tmp where ar @> array[1,2]; }
+
+session "s2"
+step "s2s1"	{ select pg_stopevent_set('EntryFindPostingLeafPage',
+									  '$.datname == "isolation_regression" && $.relname == "tmp_idx"'); }
+step "s2v"	{ vacuum tmp; }
+step "s2s2"	{ select pg_stopevent_set('ReleaseAndReadBuffer',
+									  '$.datname == "isolation_regression" && $.relname == "tmp_idx"');
+			  select pg_stopevent_reset('EntryFindPostingLeafPage'); }
+step "s2s3"	{ select pg_stopevent_reset('ReleaseAndReadBuffer'); }
+
+permutation "s2s1" "s1s" "s2s2" "s2v" "s2s3"
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 4c40ae37b26..13c9d039709 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2377,6 +2377,7 @@ StatsExtInfo
 StdAnalyzeData
 StdRdOptions
 Step
+StopEvent
 StopList
 StopWorkersData
 StrategyNumber
-- 
2.14.3

