agora inbox for [email protected]  
help / color / mirror / Atom feed
[PATCH v21 4/8] Row pattern recognition patch (planner).
89+ messages / 2 participants
[nested] [flat]

* [PATCH v21 4/8] Row pattern recognition patch (planner).
@ 2024-08-26 04:32  Tatsuo Ishii <[email protected]>
  0 siblings, 0 replies; 89+ messages in thread

From: Tatsuo Ishii @ 2024-08-26 04:32 UTC (permalink / raw)

---
 src/backend/optimizer/plan/createplan.c   | 24 +++++++++++++++-----
 src/backend/optimizer/plan/planner.c      |  3 +++
 src/backend/optimizer/plan/setrefs.c      | 27 ++++++++++++++++++++++-
 src/backend/optimizer/prep/prepjointree.c |  9 ++++++++
 src/include/nodes/plannodes.h             | 19 ++++++++++++++++
 5 files changed, 76 insertions(+), 6 deletions(-)

diff --git a/src/backend/optimizer/plan/createplan.c b/src/backend/optimizer/plan/createplan.c
index 8e0e5977a9..a1b2c71364 100644
--- a/src/backend/optimizer/plan/createplan.c
+++ b/src/backend/optimizer/plan/createplan.c
@@ -288,9 +288,11 @@ static WindowAgg *make_windowagg(List *tlist, Index winref,
 								 int ordNumCols, AttrNumber *ordColIdx, Oid *ordOperators, Oid *ordCollations,
 								 int frameOptions, Node *startOffset, Node *endOffset,
 								 Oid startInRangeFunc, Oid endInRangeFunc,
-								 Oid inRangeColl, bool inRangeAsc, bool inRangeNullsFirst,
-								 List *runCondition, List *qual, bool topWindow,
-								 Plan *lefttree);
+								 Oid inRangeColl, bool inRangeAsc, bool inRangeNullsFirst, List *runCondition,
+								 RPSkipTo rpSkipTo, List *patternVariable, List *patternRegexp,
+								 List *defineClause,
+								 List *defineInitial,
+								 List *qual, bool topWindow, Plan *lefttree);
 static Group *make_group(List *tlist, List *qual, int numGroupCols,
 						 AttrNumber *grpColIdx, Oid *grpOperators, Oid *grpCollations,
 						 Plan *lefttree);
@@ -2697,6 +2699,11 @@ create_windowagg_plan(PlannerInfo *root, WindowAggPath *best_path)
 						  wc->inRangeAsc,
 						  wc->inRangeNullsFirst,
 						  best_path->runCondition,
+						  wc->rpSkipTo,
+						  wc->patternVariable,
+						  wc->patternRegexp,
+						  wc->defineClause,
+						  wc->defineInitial,
 						  best_path->qual,
 						  best_path->topwindow,
 						  subplan);
@@ -6631,8 +6638,10 @@ make_windowagg(List *tlist, Index winref,
 			   int ordNumCols, AttrNumber *ordColIdx, Oid *ordOperators, Oid *ordCollations,
 			   int frameOptions, Node *startOffset, Node *endOffset,
 			   Oid startInRangeFunc, Oid endInRangeFunc,
-			   Oid inRangeColl, bool inRangeAsc, bool inRangeNullsFirst,
-			   List *runCondition, List *qual, bool topWindow, Plan *lefttree)
+			   Oid inRangeColl, bool inRangeAsc, bool inRangeNullsFirst, List *runCondition,
+			   RPSkipTo rpSkipTo, List *patternVariable, List *patternRegexp, List *defineClause,
+			   List *defineInitial,
+			   List *qual, bool topWindow, Plan *lefttree)
 {
 	WindowAgg  *node = makeNode(WindowAgg);
 	Plan	   *plan = &node->plan;
@@ -6658,6 +6667,11 @@ make_windowagg(List *tlist, Index winref,
 	node->inRangeAsc = inRangeAsc;
 	node->inRangeNullsFirst = inRangeNullsFirst;
 	node->topWindow = topWindow;
+	node->rpSkipTo = rpSkipTo,
+		node->patternVariable = patternVariable;
+	node->patternRegexp = patternRegexp;
+	node->defineClause = defineClause;
+	node->defineInitial = defineInitial;
 
 	plan->targetlist = tlist;
 	plan->lefttree = lefttree;
diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c
index b5827d3980..ccb66336a8 100644
--- a/src/backend/optimizer/plan/planner.c
+++ b/src/backend/optimizer/plan/planner.c
@@ -870,6 +870,9 @@ subquery_planner(PlannerGlobal *glob, Query *parse, PlannerInfo *parent_root,
 												EXPRKIND_LIMIT);
 		wc->endOffset = preprocess_expression(root, wc->endOffset,
 											  EXPRKIND_LIMIT);
+		wc->defineClause = (List *) preprocess_expression(root,
+														  (Node *) wc->defineClause,
+														  EXPRKIND_TARGET);
 	}
 
 	parse->limitOffset = preprocess_expression(root, parse->limitOffset,
diff --git a/src/backend/optimizer/plan/setrefs.c b/src/backend/optimizer/plan/setrefs.c
index 7aed84584c..7d726729ae 100644
--- a/src/backend/optimizer/plan/setrefs.c
+++ b/src/backend/optimizer/plan/setrefs.c
@@ -210,7 +210,6 @@ static List *set_windowagg_runcondition_references(PlannerInfo *root,
 												   List *runcondition,
 												   Plan *plan);
 
-
 /*****************************************************************************
  *
  *		SUBPLAN REFERENCES
@@ -2471,6 +2470,32 @@ set_upper_references(PlannerInfo *root, Plan *plan, int rtoffset)
 					   NRM_EQUAL,
 					   NUM_EXEC_QUAL(plan));
 
+	/*
+	 * Modifies an expression tree in each DEFINE clause so that all Var
+	 * nodes's varno refers to OUTER_VAR.
+	 */
+	if (IsA(plan, WindowAgg))
+	{
+		WindowAgg  *wplan = (WindowAgg *) plan;
+
+		if (wplan->defineClause != NIL)
+		{
+			foreach(l, wplan->defineClause)
+			{
+				TargetEntry *tle = (TargetEntry *) lfirst(l);
+
+				tle->expr = (Expr *)
+					fix_upper_expr(root,
+								   (Node *) tle->expr,
+								   subplan_itlist,
+								   OUTER_VAR,
+								   rtoffset,
+								   NRM_EQUAL,
+								   NUM_EXEC_QUAL(plan));
+			}
+		}
+	}
+
 	pfree(subplan_itlist);
 }
 
diff --git a/src/backend/optimizer/prep/prepjointree.c b/src/backend/optimizer/prep/prepjointree.c
index 969e257f70..3cedb5c8e5 100644
--- a/src/backend/optimizer/prep/prepjointree.c
+++ b/src/backend/optimizer/prep/prepjointree.c
@@ -2175,6 +2175,15 @@ perform_pullup_replace_vars(PlannerInfo *root,
 	parse->returningList = (List *)
 		pullup_replace_vars((Node *) parse->returningList, rvcontext);
 
+	foreach(lc, parse->windowClause)
+	{
+		WindowClause *wc = lfirst_node(WindowClause, lc);
+
+		if (wc->defineClause != NIL)
+			wc->defineClause = (List *)
+				pullup_replace_vars((Node *) wc->defineClause, rvcontext);
+	}
+
 	if (parse->onConflict)
 	{
 		parse->onConflict->onConflictSet = (List *)
diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h
index 62cd6a6666..b7b2ac4aaf 100644
--- a/src/include/nodes/plannodes.h
+++ b/src/include/nodes/plannodes.h
@@ -20,6 +20,7 @@
 #include "lib/stringinfo.h"
 #include "nodes/bitmapset.h"
 #include "nodes/lockoptions.h"
+#include "nodes/parsenodes.h"
 #include "nodes/primnodes.h"
 
 
@@ -1099,6 +1100,24 @@ typedef struct WindowAgg
 	/* nulls sort first for in_range tests? */
 	bool		inRangeNullsFirst;
 
+	/* Row Pattern Recognition AFTER MACH SKIP clause */
+	RPSkipTo	rpSkipTo;		/* Row Pattern Skip To type */
+
+	/* Row Pattern PATTERN variable name (list of String) */
+	List	   *patternVariable;
+
+	/*
+	 * Row Pattern RPATTERN regular expression quantifier ('+' or ''. list of
+	 * String)
+	 */
+	List	   *patternRegexp;
+
+	/* Row Pattern DEFINE clause (list of TargetEntry) */
+	List	   *defineClause;
+
+	/* Row Pattern DEFINE variable initial names (list of String) */
+	List	   *defineInitial;
+
 	/*
 	 * false for all apart from the WindowAgg that's closest to the root of
 	 * the plan
-- 
2.25.1


----Next_Part(Mon_Aug_26_13_39_47_2024_878)--
Content-Type: Text/X-Patch; charset=us-ascii
Content-Transfer-Encoding: 7bit
Content-Disposition: inline;
 filename="v21-0005-Row-pattern-recognition-patch-executor.patch"



^ permalink  raw  reply  [nested|flat] 89+ messages in thread

* [PATCH v44b 4/4] hack deadlock detector
@ 2026-03-25 19:35  Álvaro Herrera <[email protected]>
  0 siblings, 0 replies; 89+ messages in thread

From: Álvaro Herrera @ 2026-03-25 19:35 UTC (permalink / raw)

---
 src/backend/commands/cluster.c                | 10 +++
 src/backend/storage/lmgr/deadlock.c           | 12 +++
 src/include/storage/proc.h                    |  1 +
 src/test/modules/injection_points/Makefile    |  1 +
 .../expected/repack_deadlock.out              | 63 ++++++++++++++
 src/test/modules/injection_points/meson.build |  1 +
 .../specs/repack_deadlock.spec                | 83 +++++++++++++++++++
 7 files changed, 171 insertions(+)
 create mode 100644 src/test/modules/injection_points/expected/repack_deadlock.out
 create mode 100644 src/test/modules/injection_points/specs/repack_deadlock.spec

diff --git a/src/backend/commands/cluster.c b/src/backend/commands/cluster.c
index 2c3058ba10d..d5b1dfbff69 100644
--- a/src/backend/commands/cluster.c
+++ b/src/backend/commands/cluster.c
@@ -372,6 +372,16 @@ ExecRepack(ParseState *pstate, RepackStmt *stmt, bool isTopLevel)
 	/* Determine the lock mode to use. */
 	lockmode = RepackLockLevel((params.options & CLUOPT_CONCURRENT) != 0);
 
+	/*
+	 * If in concurrent mode, set the PROC_IN_CONCURRENT_REPACK flag.  This
+	 * makes the deadlock checker cause anyone that would conflict with us
+	 * to error out.  It's important to set this flag ahead of actually locking
+	 * the relation; it won't of course affect anyone until we do have a lock
+	 * that others can conflict with.
+	 */
+	if ((params.options & CLUOPT_CONCURRENT) != 0)
+		MyProc->statusFlags |= PROC_IN_CONCURRENT_REPACK;
+
 	/*
 	 * If a single relation is specified, process it and we're done ... unless
 	 * the relation is a partitioned table, in which case we fall through.
diff --git a/src/backend/storage/lmgr/deadlock.c b/src/backend/storage/lmgr/deadlock.c
index b8962d875b6..10266e384aa 100644
--- a/src/backend/storage/lmgr/deadlock.c
+++ b/src/backend/storage/lmgr/deadlock.c
@@ -620,6 +620,18 @@ FindLockCycleRecurseMember(PGPROC *checkProc,
 						proc->statusFlags & PROC_IS_AUTOVACUUM)
 						blocking_autovacuum_proc = proc;
 
+					/*
+					 * If we note that we're blocked by some process running
+					 * REPACK (CONCURRENTLY), just fail.  That process is
+					 * going to upgrade its lock at some point, and it would
+					 * be inappropriate for any other process to cause that
+					 * to fail.
+					 */
+					if (checkProc == MyProc &&
+						proc->statusFlags & PROC_IN_CONCURRENT_REPACK)
+						/* not a serious error message proposal */
+						elog(ERROR, "oops, we're blocked by REPACK, time to die");
+
 					/* We're done looking at this proclock */
 					break;
 				}
diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h
index 1dad125706e..4e340ead455 100644
--- a/src/include/storage/proc.h
+++ b/src/include/storage/proc.h
@@ -69,6 +69,7 @@ struct XidCache
 #define		PROC_AFFECTS_ALL_HORIZONS	0x20	/* this proc's xmin must be
 												 * included in vacuum horizons
 												 * in all databases */
+#define		PROC_IN_CONCURRENT_REPACK	0x40	/* REPACK (CONCURRENTLY) */
 
 /* flags reset at EOXact */
 #define		PROC_VACUUM_STATE_MASK \
diff --git a/src/test/modules/injection_points/Makefile b/src/test/modules/injection_points/Makefile
index 2cd7d87c533..f7663859fe2 100644
--- a/src/test/modules/injection_points/Makefile
+++ b/src/test/modules/injection_points/Makefile
@@ -15,6 +15,7 @@ REGRESS_OPTS = --dlpath=$(top_builddir)/src/test/regress
 ISOLATION = basic \
 	    inplace \
 	    repack \
+	    repack_deadlock \
 	    repack_toast \
 	    syscache-update-pruned \
 	    heap_lock_update
diff --git a/src/test/modules/injection_points/expected/repack_deadlock.out b/src/test/modules/injection_points/expected/repack_deadlock.out
new file mode 100644
index 00000000000..42505d10933
--- /dev/null
+++ b/src/test/modules/injection_points/expected/repack_deadlock.out
@@ -0,0 +1,63 @@
+Parsed test spec with 2 sessions
+
+starting permutation: wait_before_lock add_column wakeup_before_lock check1
+injection_points_attach
+-----------------------
+                       
+(1 row)
+
+step wait_before_lock: 
+	REPACK (CONCURRENTLY) repack_deadlock USING INDEX repack_deadlock_pkey;
+ <waiting ...>
+step add_column: 
+	alter table repack_deadlock add column noise text;
+ <waiting ...>
+step add_column: <... completed>
+ERROR:  oops, we're blocked by REPACK, time to die
+step wakeup_before_lock: 
+	SELECT injection_points_wakeup('repack-concurrently-before-lock');
+
+injection_points_wakeup
+-----------------------
+                       
+(1 row)
+
+step wait_before_lock: <... completed>
+step check1: 
+	INSERT INTO relfilenodes(node)
+	SELECT relfilenode FROM pg_class WHERE relname='repack_deadlock';
+
+	SELECT count(DISTINCT node) FROM relfilenodes;
+
+	SELECT i, j FROM repack_deadlock ORDER BY i, j;
+
+	INSERT INTO data_s1(i, j)
+	SELECT i, j FROM repack_deadlock;
+
+	SELECT count(*)
+	FROM data_s1 d1 FULL JOIN data_s2 d2 USING (i, j)
+	WHERE d1.i ISNULL OR d2.i ISNULL;
+
+count
+-----
+    1
+(1 row)
+
+i|j
+-+-
+1|1
+2|2
+3|3
+4|4
+(4 rows)
+
+count
+-----
+    4
+(1 row)
+
+injection_points_detach
+-----------------------
+                       
+(1 row)
+
diff --git a/src/test/modules/injection_points/meson.build b/src/test/modules/injection_points/meson.build
index a414abb924b..1cd88d6db65 100644
--- a/src/test/modules/injection_points/meson.build
+++ b/src/test/modules/injection_points/meson.build
@@ -46,6 +46,7 @@ tests += {
       'basic',
       'inplace',
       'repack',
+      'repack_deadlock',
       'repack_toast',
       'syscache-update-pruned',
       'heap_lock_update',
diff --git a/src/test/modules/injection_points/specs/repack_deadlock.spec b/src/test/modules/injection_points/specs/repack_deadlock.spec
new file mode 100644
index 00000000000..9d23a6588c2
--- /dev/null
+++ b/src/test/modules/injection_points/specs/repack_deadlock.spec
@@ -0,0 +1,83 @@
+# Test REPACK with a concurrent transaction that would cause a deadlock
+setup
+{
+	CREATE EXTENSION injection_points;
+
+	CREATE TABLE repack_deadlock(i int PRIMARY KEY, j int);
+	INSERT INTO repack_deadlock(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4);
+
+	CREATE TABLE relfilenodes(node oid);
+
+	CREATE TABLE data_s1(i int, j int);
+	CREATE TABLE data_s2(i int, j int);
+}
+
+teardown
+{
+	DROP TABLE repack_deadlock;
+	DROP EXTENSION injection_points;
+
+	DROP TABLE relfilenodes;
+	DROP TABLE data_s1;
+	DROP TABLE data_s2;
+}
+
+session s1
+setup
+{
+	SELECT injection_points_set_local();
+	SELECT injection_points_attach('repack-concurrently-before-lock', 'wait');
+}
+# Perform the initial load and wait for s2 to do some data changes.
+step wait_before_lock
+{
+	REPACK (CONCURRENTLY) repack_deadlock USING INDEX repack_deadlock_pkey;
+}
+# Check the table from the perspective of s1.
+#
+# Besides the contents, we also check that relfilenode has changed.
+
+# Have each session write the contents into a table and use FULL JOIN to check
+# if the outputs are identical.
+step check1
+{
+	INSERT INTO relfilenodes(node)
+	SELECT relfilenode FROM pg_class WHERE relname='repack_deadlock';
+
+	SELECT count(DISTINCT node) FROM relfilenodes;
+
+	SELECT i, j FROM repack_deadlock ORDER BY i, j;
+
+	INSERT INTO data_s1(i, j)
+	SELECT i, j FROM repack_deadlock;
+
+	SELECT count(*)
+	FROM data_s1 d1 FULL JOIN data_s2 d2 USING (i, j)
+	WHERE d1.i ISNULL OR d2.i ISNULL;
+}
+teardown
+{
+	SELECT injection_points_detach('repack-concurrently-before-lock');
+}
+
+session s2
+# Change the existing data. UPDATE changes both key and non-key columns. Also
+# update one row twice to test whether tuple version generated by this session
+# can be found.
+step add_column
+{
+	alter table repack_deadlock add column noise text;
+}
+
+step wakeup_before_lock
+{
+	SELECT injection_points_wakeup('repack-concurrently-before-lock');
+}
+
+# Test if data changes introduced while one session is performing REPACK
+# CONCURRENTLY find their way into the table.
+permutation
+	wait_before_lock
+	add_column
+	wakeup_before_lock
+	check1
-- 
2.47.3


--fot5plqmb33ey33c--





^ permalink  raw  reply  [nested|flat] 89+ messages in thread

* [PATCH v44b 4/4] hack deadlock detector
@ 2026-03-25 19:35  Álvaro Herrera <[email protected]>
  0 siblings, 0 replies; 89+ messages in thread

From: Álvaro Herrera @ 2026-03-25 19:35 UTC (permalink / raw)

---
 src/backend/commands/cluster.c                | 10 +++
 src/backend/storage/lmgr/deadlock.c           | 12 +++
 src/include/storage/proc.h                    |  1 +
 src/test/modules/injection_points/Makefile    |  1 +
 .../expected/repack_deadlock.out              | 63 ++++++++++++++
 src/test/modules/injection_points/meson.build |  1 +
 .../specs/repack_deadlock.spec                | 83 +++++++++++++++++++
 7 files changed, 171 insertions(+)
 create mode 100644 src/test/modules/injection_points/expected/repack_deadlock.out
 create mode 100644 src/test/modules/injection_points/specs/repack_deadlock.spec

diff --git a/src/backend/commands/cluster.c b/src/backend/commands/cluster.c
index 2c3058ba10d..d5b1dfbff69 100644
--- a/src/backend/commands/cluster.c
+++ b/src/backend/commands/cluster.c
@@ -372,6 +372,16 @@ ExecRepack(ParseState *pstate, RepackStmt *stmt, bool isTopLevel)
 	/* Determine the lock mode to use. */
 	lockmode = RepackLockLevel((params.options & CLUOPT_CONCURRENT) != 0);
 
+	/*
+	 * If in concurrent mode, set the PROC_IN_CONCURRENT_REPACK flag.  This
+	 * makes the deadlock checker cause anyone that would conflict with us
+	 * to error out.  It's important to set this flag ahead of actually locking
+	 * the relation; it won't of course affect anyone until we do have a lock
+	 * that others can conflict with.
+	 */
+	if ((params.options & CLUOPT_CONCURRENT) != 0)
+		MyProc->statusFlags |= PROC_IN_CONCURRENT_REPACK;
+
 	/*
 	 * If a single relation is specified, process it and we're done ... unless
 	 * the relation is a partitioned table, in which case we fall through.
diff --git a/src/backend/storage/lmgr/deadlock.c b/src/backend/storage/lmgr/deadlock.c
index b8962d875b6..10266e384aa 100644
--- a/src/backend/storage/lmgr/deadlock.c
+++ b/src/backend/storage/lmgr/deadlock.c
@@ -620,6 +620,18 @@ FindLockCycleRecurseMember(PGPROC *checkProc,
 						proc->statusFlags & PROC_IS_AUTOVACUUM)
 						blocking_autovacuum_proc = proc;
 
+					/*
+					 * If we note that we're blocked by some process running
+					 * REPACK (CONCURRENTLY), just fail.  That process is
+					 * going to upgrade its lock at some point, and it would
+					 * be inappropriate for any other process to cause that
+					 * to fail.
+					 */
+					if (checkProc == MyProc &&
+						proc->statusFlags & PROC_IN_CONCURRENT_REPACK)
+						/* not a serious error message proposal */
+						elog(ERROR, "oops, we're blocked by REPACK, time to die");
+
 					/* We're done looking at this proclock */
 					break;
 				}
diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h
index 1dad125706e..4e340ead455 100644
--- a/src/include/storage/proc.h
+++ b/src/include/storage/proc.h
@@ -69,6 +69,7 @@ struct XidCache
 #define		PROC_AFFECTS_ALL_HORIZONS	0x20	/* this proc's xmin must be
 												 * included in vacuum horizons
 												 * in all databases */
+#define		PROC_IN_CONCURRENT_REPACK	0x40	/* REPACK (CONCURRENTLY) */
 
 /* flags reset at EOXact */
 #define		PROC_VACUUM_STATE_MASK \
diff --git a/src/test/modules/injection_points/Makefile b/src/test/modules/injection_points/Makefile
index 2cd7d87c533..f7663859fe2 100644
--- a/src/test/modules/injection_points/Makefile
+++ b/src/test/modules/injection_points/Makefile
@@ -15,6 +15,7 @@ REGRESS_OPTS = --dlpath=$(top_builddir)/src/test/regress
 ISOLATION = basic \
 	    inplace \
 	    repack \
+	    repack_deadlock \
 	    repack_toast \
 	    syscache-update-pruned \
 	    heap_lock_update
diff --git a/src/test/modules/injection_points/expected/repack_deadlock.out b/src/test/modules/injection_points/expected/repack_deadlock.out
new file mode 100644
index 00000000000..42505d10933
--- /dev/null
+++ b/src/test/modules/injection_points/expected/repack_deadlock.out
@@ -0,0 +1,63 @@
+Parsed test spec with 2 sessions
+
+starting permutation: wait_before_lock add_column wakeup_before_lock check1
+injection_points_attach
+-----------------------
+                       
+(1 row)
+
+step wait_before_lock: 
+	REPACK (CONCURRENTLY) repack_deadlock USING INDEX repack_deadlock_pkey;
+ <waiting ...>
+step add_column: 
+	alter table repack_deadlock add column noise text;
+ <waiting ...>
+step add_column: <... completed>
+ERROR:  oops, we're blocked by REPACK, time to die
+step wakeup_before_lock: 
+	SELECT injection_points_wakeup('repack-concurrently-before-lock');
+
+injection_points_wakeup
+-----------------------
+                       
+(1 row)
+
+step wait_before_lock: <... completed>
+step check1: 
+	INSERT INTO relfilenodes(node)
+	SELECT relfilenode FROM pg_class WHERE relname='repack_deadlock';
+
+	SELECT count(DISTINCT node) FROM relfilenodes;
+
+	SELECT i, j FROM repack_deadlock ORDER BY i, j;
+
+	INSERT INTO data_s1(i, j)
+	SELECT i, j FROM repack_deadlock;
+
+	SELECT count(*)
+	FROM data_s1 d1 FULL JOIN data_s2 d2 USING (i, j)
+	WHERE d1.i ISNULL OR d2.i ISNULL;
+
+count
+-----
+    1
+(1 row)
+
+i|j
+-+-
+1|1
+2|2
+3|3
+4|4
+(4 rows)
+
+count
+-----
+    4
+(1 row)
+
+injection_points_detach
+-----------------------
+                       
+(1 row)
+
diff --git a/src/test/modules/injection_points/meson.build b/src/test/modules/injection_points/meson.build
index a414abb924b..1cd88d6db65 100644
--- a/src/test/modules/injection_points/meson.build
+++ b/src/test/modules/injection_points/meson.build
@@ -46,6 +46,7 @@ tests += {
       'basic',
       'inplace',
       'repack',
+      'repack_deadlock',
       'repack_toast',
       'syscache-update-pruned',
       'heap_lock_update',
diff --git a/src/test/modules/injection_points/specs/repack_deadlock.spec b/src/test/modules/injection_points/specs/repack_deadlock.spec
new file mode 100644
index 00000000000..9d23a6588c2
--- /dev/null
+++ b/src/test/modules/injection_points/specs/repack_deadlock.spec
@@ -0,0 +1,83 @@
+# Test REPACK with a concurrent transaction that would cause a deadlock
+setup
+{
+	CREATE EXTENSION injection_points;
+
+	CREATE TABLE repack_deadlock(i int PRIMARY KEY, j int);
+	INSERT INTO repack_deadlock(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4);
+
+	CREATE TABLE relfilenodes(node oid);
+
+	CREATE TABLE data_s1(i int, j int);
+	CREATE TABLE data_s2(i int, j int);
+}
+
+teardown
+{
+	DROP TABLE repack_deadlock;
+	DROP EXTENSION injection_points;
+
+	DROP TABLE relfilenodes;
+	DROP TABLE data_s1;
+	DROP TABLE data_s2;
+}
+
+session s1
+setup
+{
+	SELECT injection_points_set_local();
+	SELECT injection_points_attach('repack-concurrently-before-lock', 'wait');
+}
+# Perform the initial load and wait for s2 to do some data changes.
+step wait_before_lock
+{
+	REPACK (CONCURRENTLY) repack_deadlock USING INDEX repack_deadlock_pkey;
+}
+# Check the table from the perspective of s1.
+#
+# Besides the contents, we also check that relfilenode has changed.
+
+# Have each session write the contents into a table and use FULL JOIN to check
+# if the outputs are identical.
+step check1
+{
+	INSERT INTO relfilenodes(node)
+	SELECT relfilenode FROM pg_class WHERE relname='repack_deadlock';
+
+	SELECT count(DISTINCT node) FROM relfilenodes;
+
+	SELECT i, j FROM repack_deadlock ORDER BY i, j;
+
+	INSERT INTO data_s1(i, j)
+	SELECT i, j FROM repack_deadlock;
+
+	SELECT count(*)
+	FROM data_s1 d1 FULL JOIN data_s2 d2 USING (i, j)
+	WHERE d1.i ISNULL OR d2.i ISNULL;
+}
+teardown
+{
+	SELECT injection_points_detach('repack-concurrently-before-lock');
+}
+
+session s2
+# Change the existing data. UPDATE changes both key and non-key columns. Also
+# update one row twice to test whether tuple version generated by this session
+# can be found.
+step add_column
+{
+	alter table repack_deadlock add column noise text;
+}
+
+step wakeup_before_lock
+{
+	SELECT injection_points_wakeup('repack-concurrently-before-lock');
+}
+
+# Test if data changes introduced while one session is performing REPACK
+# CONCURRENTLY find their way into the table.
+permutation
+	wait_before_lock
+	add_column
+	wakeup_before_lock
+	check1
-- 
2.47.3


--fot5plqmb33ey33c--





^ permalink  raw  reply  [nested|flat] 89+ messages in thread

* [PATCH v44b 4/4] hack deadlock detector
@ 2026-03-25 19:35  Álvaro Herrera <[email protected]>
  0 siblings, 0 replies; 89+ messages in thread

From: Álvaro Herrera @ 2026-03-25 19:35 UTC (permalink / raw)

---
 src/backend/commands/cluster.c                | 10 +++
 src/backend/storage/lmgr/deadlock.c           | 12 +++
 src/include/storage/proc.h                    |  1 +
 src/test/modules/injection_points/Makefile    |  1 +
 .../expected/repack_deadlock.out              | 63 ++++++++++++++
 src/test/modules/injection_points/meson.build |  1 +
 .../specs/repack_deadlock.spec                | 83 +++++++++++++++++++
 7 files changed, 171 insertions(+)
 create mode 100644 src/test/modules/injection_points/expected/repack_deadlock.out
 create mode 100644 src/test/modules/injection_points/specs/repack_deadlock.spec

diff --git a/src/backend/commands/cluster.c b/src/backend/commands/cluster.c
index 2c3058ba10d..d5b1dfbff69 100644
--- a/src/backend/commands/cluster.c
+++ b/src/backend/commands/cluster.c
@@ -372,6 +372,16 @@ ExecRepack(ParseState *pstate, RepackStmt *stmt, bool isTopLevel)
 	/* Determine the lock mode to use. */
 	lockmode = RepackLockLevel((params.options & CLUOPT_CONCURRENT) != 0);
 
+	/*
+	 * If in concurrent mode, set the PROC_IN_CONCURRENT_REPACK flag.  This
+	 * makes the deadlock checker cause anyone that would conflict with us
+	 * to error out.  It's important to set this flag ahead of actually locking
+	 * the relation; it won't of course affect anyone until we do have a lock
+	 * that others can conflict with.
+	 */
+	if ((params.options & CLUOPT_CONCURRENT) != 0)
+		MyProc->statusFlags |= PROC_IN_CONCURRENT_REPACK;
+
 	/*
 	 * If a single relation is specified, process it and we're done ... unless
 	 * the relation is a partitioned table, in which case we fall through.
diff --git a/src/backend/storage/lmgr/deadlock.c b/src/backend/storage/lmgr/deadlock.c
index b8962d875b6..10266e384aa 100644
--- a/src/backend/storage/lmgr/deadlock.c
+++ b/src/backend/storage/lmgr/deadlock.c
@@ -620,6 +620,18 @@ FindLockCycleRecurseMember(PGPROC *checkProc,
 						proc->statusFlags & PROC_IS_AUTOVACUUM)
 						blocking_autovacuum_proc = proc;
 
+					/*
+					 * If we note that we're blocked by some process running
+					 * REPACK (CONCURRENTLY), just fail.  That process is
+					 * going to upgrade its lock at some point, and it would
+					 * be inappropriate for any other process to cause that
+					 * to fail.
+					 */
+					if (checkProc == MyProc &&
+						proc->statusFlags & PROC_IN_CONCURRENT_REPACK)
+						/* not a serious error message proposal */
+						elog(ERROR, "oops, we're blocked by REPACK, time to die");
+
 					/* We're done looking at this proclock */
 					break;
 				}
diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h
index 1dad125706e..4e340ead455 100644
--- a/src/include/storage/proc.h
+++ b/src/include/storage/proc.h
@@ -69,6 +69,7 @@ struct XidCache
 #define		PROC_AFFECTS_ALL_HORIZONS	0x20	/* this proc's xmin must be
 												 * included in vacuum horizons
 												 * in all databases */
+#define		PROC_IN_CONCURRENT_REPACK	0x40	/* REPACK (CONCURRENTLY) */
 
 /* flags reset at EOXact */
 #define		PROC_VACUUM_STATE_MASK \
diff --git a/src/test/modules/injection_points/Makefile b/src/test/modules/injection_points/Makefile
index 2cd7d87c533..f7663859fe2 100644
--- a/src/test/modules/injection_points/Makefile
+++ b/src/test/modules/injection_points/Makefile
@@ -15,6 +15,7 @@ REGRESS_OPTS = --dlpath=$(top_builddir)/src/test/regress
 ISOLATION = basic \
 	    inplace \
 	    repack \
+	    repack_deadlock \
 	    repack_toast \
 	    syscache-update-pruned \
 	    heap_lock_update
diff --git a/src/test/modules/injection_points/expected/repack_deadlock.out b/src/test/modules/injection_points/expected/repack_deadlock.out
new file mode 100644
index 00000000000..42505d10933
--- /dev/null
+++ b/src/test/modules/injection_points/expected/repack_deadlock.out
@@ -0,0 +1,63 @@
+Parsed test spec with 2 sessions
+
+starting permutation: wait_before_lock add_column wakeup_before_lock check1
+injection_points_attach
+-----------------------
+                       
+(1 row)
+
+step wait_before_lock: 
+	REPACK (CONCURRENTLY) repack_deadlock USING INDEX repack_deadlock_pkey;
+ <waiting ...>
+step add_column: 
+	alter table repack_deadlock add column noise text;
+ <waiting ...>
+step add_column: <... completed>
+ERROR:  oops, we're blocked by REPACK, time to die
+step wakeup_before_lock: 
+	SELECT injection_points_wakeup('repack-concurrently-before-lock');
+
+injection_points_wakeup
+-----------------------
+                       
+(1 row)
+
+step wait_before_lock: <... completed>
+step check1: 
+	INSERT INTO relfilenodes(node)
+	SELECT relfilenode FROM pg_class WHERE relname='repack_deadlock';
+
+	SELECT count(DISTINCT node) FROM relfilenodes;
+
+	SELECT i, j FROM repack_deadlock ORDER BY i, j;
+
+	INSERT INTO data_s1(i, j)
+	SELECT i, j FROM repack_deadlock;
+
+	SELECT count(*)
+	FROM data_s1 d1 FULL JOIN data_s2 d2 USING (i, j)
+	WHERE d1.i ISNULL OR d2.i ISNULL;
+
+count
+-----
+    1
+(1 row)
+
+i|j
+-+-
+1|1
+2|2
+3|3
+4|4
+(4 rows)
+
+count
+-----
+    4
+(1 row)
+
+injection_points_detach
+-----------------------
+                       
+(1 row)
+
diff --git a/src/test/modules/injection_points/meson.build b/src/test/modules/injection_points/meson.build
index a414abb924b..1cd88d6db65 100644
--- a/src/test/modules/injection_points/meson.build
+++ b/src/test/modules/injection_points/meson.build
@@ -46,6 +46,7 @@ tests += {
       'basic',
       'inplace',
       'repack',
+      'repack_deadlock',
       'repack_toast',
       'syscache-update-pruned',
       'heap_lock_update',
diff --git a/src/test/modules/injection_points/specs/repack_deadlock.spec b/src/test/modules/injection_points/specs/repack_deadlock.spec
new file mode 100644
index 00000000000..9d23a6588c2
--- /dev/null
+++ b/src/test/modules/injection_points/specs/repack_deadlock.spec
@@ -0,0 +1,83 @@
+# Test REPACK with a concurrent transaction that would cause a deadlock
+setup
+{
+	CREATE EXTENSION injection_points;
+
+	CREATE TABLE repack_deadlock(i int PRIMARY KEY, j int);
+	INSERT INTO repack_deadlock(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4);
+
+	CREATE TABLE relfilenodes(node oid);
+
+	CREATE TABLE data_s1(i int, j int);
+	CREATE TABLE data_s2(i int, j int);
+}
+
+teardown
+{
+	DROP TABLE repack_deadlock;
+	DROP EXTENSION injection_points;
+
+	DROP TABLE relfilenodes;
+	DROP TABLE data_s1;
+	DROP TABLE data_s2;
+}
+
+session s1
+setup
+{
+	SELECT injection_points_set_local();
+	SELECT injection_points_attach('repack-concurrently-before-lock', 'wait');
+}
+# Perform the initial load and wait for s2 to do some data changes.
+step wait_before_lock
+{
+	REPACK (CONCURRENTLY) repack_deadlock USING INDEX repack_deadlock_pkey;
+}
+# Check the table from the perspective of s1.
+#
+# Besides the contents, we also check that relfilenode has changed.
+
+# Have each session write the contents into a table and use FULL JOIN to check
+# if the outputs are identical.
+step check1
+{
+	INSERT INTO relfilenodes(node)
+	SELECT relfilenode FROM pg_class WHERE relname='repack_deadlock';
+
+	SELECT count(DISTINCT node) FROM relfilenodes;
+
+	SELECT i, j FROM repack_deadlock ORDER BY i, j;
+
+	INSERT INTO data_s1(i, j)
+	SELECT i, j FROM repack_deadlock;
+
+	SELECT count(*)
+	FROM data_s1 d1 FULL JOIN data_s2 d2 USING (i, j)
+	WHERE d1.i ISNULL OR d2.i ISNULL;
+}
+teardown
+{
+	SELECT injection_points_detach('repack-concurrently-before-lock');
+}
+
+session s2
+# Change the existing data. UPDATE changes both key and non-key columns. Also
+# update one row twice to test whether tuple version generated by this session
+# can be found.
+step add_column
+{
+	alter table repack_deadlock add column noise text;
+}
+
+step wakeup_before_lock
+{
+	SELECT injection_points_wakeup('repack-concurrently-before-lock');
+}
+
+# Test if data changes introduced while one session is performing REPACK
+# CONCURRENTLY find their way into the table.
+permutation
+	wait_before_lock
+	add_column
+	wakeup_before_lock
+	check1
-- 
2.47.3


--fot5plqmb33ey33c--





^ permalink  raw  reply  [nested|flat] 89+ messages in thread

* [PATCH v44b 4/4] hack deadlock detector
@ 2026-03-25 19:35  Álvaro Herrera <[email protected]>
  0 siblings, 0 replies; 89+ messages in thread

From: Álvaro Herrera @ 2026-03-25 19:35 UTC (permalink / raw)

---
 src/backend/commands/cluster.c                | 10 +++
 src/backend/storage/lmgr/deadlock.c           | 12 +++
 src/include/storage/proc.h                    |  1 +
 src/test/modules/injection_points/Makefile    |  1 +
 .../expected/repack_deadlock.out              | 63 ++++++++++++++
 src/test/modules/injection_points/meson.build |  1 +
 .../specs/repack_deadlock.spec                | 83 +++++++++++++++++++
 7 files changed, 171 insertions(+)
 create mode 100644 src/test/modules/injection_points/expected/repack_deadlock.out
 create mode 100644 src/test/modules/injection_points/specs/repack_deadlock.spec

diff --git a/src/backend/commands/cluster.c b/src/backend/commands/cluster.c
index 2c3058ba10d..d5b1dfbff69 100644
--- a/src/backend/commands/cluster.c
+++ b/src/backend/commands/cluster.c
@@ -372,6 +372,16 @@ ExecRepack(ParseState *pstate, RepackStmt *stmt, bool isTopLevel)
 	/* Determine the lock mode to use. */
 	lockmode = RepackLockLevel((params.options & CLUOPT_CONCURRENT) != 0);
 
+	/*
+	 * If in concurrent mode, set the PROC_IN_CONCURRENT_REPACK flag.  This
+	 * makes the deadlock checker cause anyone that would conflict with us
+	 * to error out.  It's important to set this flag ahead of actually locking
+	 * the relation; it won't of course affect anyone until we do have a lock
+	 * that others can conflict with.
+	 */
+	if ((params.options & CLUOPT_CONCURRENT) != 0)
+		MyProc->statusFlags |= PROC_IN_CONCURRENT_REPACK;
+
 	/*
 	 * If a single relation is specified, process it and we're done ... unless
 	 * the relation is a partitioned table, in which case we fall through.
diff --git a/src/backend/storage/lmgr/deadlock.c b/src/backend/storage/lmgr/deadlock.c
index b8962d875b6..10266e384aa 100644
--- a/src/backend/storage/lmgr/deadlock.c
+++ b/src/backend/storage/lmgr/deadlock.c
@@ -620,6 +620,18 @@ FindLockCycleRecurseMember(PGPROC *checkProc,
 						proc->statusFlags & PROC_IS_AUTOVACUUM)
 						blocking_autovacuum_proc = proc;
 
+					/*
+					 * If we note that we're blocked by some process running
+					 * REPACK (CONCURRENTLY), just fail.  That process is
+					 * going to upgrade its lock at some point, and it would
+					 * be inappropriate for any other process to cause that
+					 * to fail.
+					 */
+					if (checkProc == MyProc &&
+						proc->statusFlags & PROC_IN_CONCURRENT_REPACK)
+						/* not a serious error message proposal */
+						elog(ERROR, "oops, we're blocked by REPACK, time to die");
+
 					/* We're done looking at this proclock */
 					break;
 				}
diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h
index 1dad125706e..4e340ead455 100644
--- a/src/include/storage/proc.h
+++ b/src/include/storage/proc.h
@@ -69,6 +69,7 @@ struct XidCache
 #define		PROC_AFFECTS_ALL_HORIZONS	0x20	/* this proc's xmin must be
 												 * included in vacuum horizons
 												 * in all databases */
+#define		PROC_IN_CONCURRENT_REPACK	0x40	/* REPACK (CONCURRENTLY) */
 
 /* flags reset at EOXact */
 #define		PROC_VACUUM_STATE_MASK \
diff --git a/src/test/modules/injection_points/Makefile b/src/test/modules/injection_points/Makefile
index 2cd7d87c533..f7663859fe2 100644
--- a/src/test/modules/injection_points/Makefile
+++ b/src/test/modules/injection_points/Makefile
@@ -15,6 +15,7 @@ REGRESS_OPTS = --dlpath=$(top_builddir)/src/test/regress
 ISOLATION = basic \
 	    inplace \
 	    repack \
+	    repack_deadlock \
 	    repack_toast \
 	    syscache-update-pruned \
 	    heap_lock_update
diff --git a/src/test/modules/injection_points/expected/repack_deadlock.out b/src/test/modules/injection_points/expected/repack_deadlock.out
new file mode 100644
index 00000000000..42505d10933
--- /dev/null
+++ b/src/test/modules/injection_points/expected/repack_deadlock.out
@@ -0,0 +1,63 @@
+Parsed test spec with 2 sessions
+
+starting permutation: wait_before_lock add_column wakeup_before_lock check1
+injection_points_attach
+-----------------------
+                       
+(1 row)
+
+step wait_before_lock: 
+	REPACK (CONCURRENTLY) repack_deadlock USING INDEX repack_deadlock_pkey;
+ <waiting ...>
+step add_column: 
+	alter table repack_deadlock add column noise text;
+ <waiting ...>
+step add_column: <... completed>
+ERROR:  oops, we're blocked by REPACK, time to die
+step wakeup_before_lock: 
+	SELECT injection_points_wakeup('repack-concurrently-before-lock');
+
+injection_points_wakeup
+-----------------------
+                       
+(1 row)
+
+step wait_before_lock: <... completed>
+step check1: 
+	INSERT INTO relfilenodes(node)
+	SELECT relfilenode FROM pg_class WHERE relname='repack_deadlock';
+
+	SELECT count(DISTINCT node) FROM relfilenodes;
+
+	SELECT i, j FROM repack_deadlock ORDER BY i, j;
+
+	INSERT INTO data_s1(i, j)
+	SELECT i, j FROM repack_deadlock;
+
+	SELECT count(*)
+	FROM data_s1 d1 FULL JOIN data_s2 d2 USING (i, j)
+	WHERE d1.i ISNULL OR d2.i ISNULL;
+
+count
+-----
+    1
+(1 row)
+
+i|j
+-+-
+1|1
+2|2
+3|3
+4|4
+(4 rows)
+
+count
+-----
+    4
+(1 row)
+
+injection_points_detach
+-----------------------
+                       
+(1 row)
+
diff --git a/src/test/modules/injection_points/meson.build b/src/test/modules/injection_points/meson.build
index a414abb924b..1cd88d6db65 100644
--- a/src/test/modules/injection_points/meson.build
+++ b/src/test/modules/injection_points/meson.build
@@ -46,6 +46,7 @@ tests += {
       'basic',
       'inplace',
       'repack',
+      'repack_deadlock',
       'repack_toast',
       'syscache-update-pruned',
       'heap_lock_update',
diff --git a/src/test/modules/injection_points/specs/repack_deadlock.spec b/src/test/modules/injection_points/specs/repack_deadlock.spec
new file mode 100644
index 00000000000..9d23a6588c2
--- /dev/null
+++ b/src/test/modules/injection_points/specs/repack_deadlock.spec
@@ -0,0 +1,83 @@
+# Test REPACK with a concurrent transaction that would cause a deadlock
+setup
+{
+	CREATE EXTENSION injection_points;
+
+	CREATE TABLE repack_deadlock(i int PRIMARY KEY, j int);
+	INSERT INTO repack_deadlock(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4);
+
+	CREATE TABLE relfilenodes(node oid);
+
+	CREATE TABLE data_s1(i int, j int);
+	CREATE TABLE data_s2(i int, j int);
+}
+
+teardown
+{
+	DROP TABLE repack_deadlock;
+	DROP EXTENSION injection_points;
+
+	DROP TABLE relfilenodes;
+	DROP TABLE data_s1;
+	DROP TABLE data_s2;
+}
+
+session s1
+setup
+{
+	SELECT injection_points_set_local();
+	SELECT injection_points_attach('repack-concurrently-before-lock', 'wait');
+}
+# Perform the initial load and wait for s2 to do some data changes.
+step wait_before_lock
+{
+	REPACK (CONCURRENTLY) repack_deadlock USING INDEX repack_deadlock_pkey;
+}
+# Check the table from the perspective of s1.
+#
+# Besides the contents, we also check that relfilenode has changed.
+
+# Have each session write the contents into a table and use FULL JOIN to check
+# if the outputs are identical.
+step check1
+{
+	INSERT INTO relfilenodes(node)
+	SELECT relfilenode FROM pg_class WHERE relname='repack_deadlock';
+
+	SELECT count(DISTINCT node) FROM relfilenodes;
+
+	SELECT i, j FROM repack_deadlock ORDER BY i, j;
+
+	INSERT INTO data_s1(i, j)
+	SELECT i, j FROM repack_deadlock;
+
+	SELECT count(*)
+	FROM data_s1 d1 FULL JOIN data_s2 d2 USING (i, j)
+	WHERE d1.i ISNULL OR d2.i ISNULL;
+}
+teardown
+{
+	SELECT injection_points_detach('repack-concurrently-before-lock');
+}
+
+session s2
+# Change the existing data. UPDATE changes both key and non-key columns. Also
+# update one row twice to test whether tuple version generated by this session
+# can be found.
+step add_column
+{
+	alter table repack_deadlock add column noise text;
+}
+
+step wakeup_before_lock
+{
+	SELECT injection_points_wakeup('repack-concurrently-before-lock');
+}
+
+# Test if data changes introduced while one session is performing REPACK
+# CONCURRENTLY find their way into the table.
+permutation
+	wait_before_lock
+	add_column
+	wakeup_before_lock
+	check1
-- 
2.47.3


--fot5plqmb33ey33c--





^ permalink  raw  reply  [nested|flat] 89+ messages in thread

* [PATCH v44b 4/4] hack deadlock detector
@ 2026-03-25 19:35  Álvaro Herrera <[email protected]>
  0 siblings, 0 replies; 89+ messages in thread

From: Álvaro Herrera @ 2026-03-25 19:35 UTC (permalink / raw)

---
 src/backend/commands/cluster.c                | 10 +++
 src/backend/storage/lmgr/deadlock.c           | 12 +++
 src/include/storage/proc.h                    |  1 +
 src/test/modules/injection_points/Makefile    |  1 +
 .../expected/repack_deadlock.out              | 63 ++++++++++++++
 src/test/modules/injection_points/meson.build |  1 +
 .../specs/repack_deadlock.spec                | 83 +++++++++++++++++++
 7 files changed, 171 insertions(+)
 create mode 100644 src/test/modules/injection_points/expected/repack_deadlock.out
 create mode 100644 src/test/modules/injection_points/specs/repack_deadlock.spec

diff --git a/src/backend/commands/cluster.c b/src/backend/commands/cluster.c
index 2c3058ba10d..d5b1dfbff69 100644
--- a/src/backend/commands/cluster.c
+++ b/src/backend/commands/cluster.c
@@ -372,6 +372,16 @@ ExecRepack(ParseState *pstate, RepackStmt *stmt, bool isTopLevel)
 	/* Determine the lock mode to use. */
 	lockmode = RepackLockLevel((params.options & CLUOPT_CONCURRENT) != 0);
 
+	/*
+	 * If in concurrent mode, set the PROC_IN_CONCURRENT_REPACK flag.  This
+	 * makes the deadlock checker cause anyone that would conflict with us
+	 * to error out.  It's important to set this flag ahead of actually locking
+	 * the relation; it won't of course affect anyone until we do have a lock
+	 * that others can conflict with.
+	 */
+	if ((params.options & CLUOPT_CONCURRENT) != 0)
+		MyProc->statusFlags |= PROC_IN_CONCURRENT_REPACK;
+
 	/*
 	 * If a single relation is specified, process it and we're done ... unless
 	 * the relation is a partitioned table, in which case we fall through.
diff --git a/src/backend/storage/lmgr/deadlock.c b/src/backend/storage/lmgr/deadlock.c
index b8962d875b6..10266e384aa 100644
--- a/src/backend/storage/lmgr/deadlock.c
+++ b/src/backend/storage/lmgr/deadlock.c
@@ -620,6 +620,18 @@ FindLockCycleRecurseMember(PGPROC *checkProc,
 						proc->statusFlags & PROC_IS_AUTOVACUUM)
 						blocking_autovacuum_proc = proc;
 
+					/*
+					 * If we note that we're blocked by some process running
+					 * REPACK (CONCURRENTLY), just fail.  That process is
+					 * going to upgrade its lock at some point, and it would
+					 * be inappropriate for any other process to cause that
+					 * to fail.
+					 */
+					if (checkProc == MyProc &&
+						proc->statusFlags & PROC_IN_CONCURRENT_REPACK)
+						/* not a serious error message proposal */
+						elog(ERROR, "oops, we're blocked by REPACK, time to die");
+
 					/* We're done looking at this proclock */
 					break;
 				}
diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h
index 1dad125706e..4e340ead455 100644
--- a/src/include/storage/proc.h
+++ b/src/include/storage/proc.h
@@ -69,6 +69,7 @@ struct XidCache
 #define		PROC_AFFECTS_ALL_HORIZONS	0x20	/* this proc's xmin must be
 												 * included in vacuum horizons
 												 * in all databases */
+#define		PROC_IN_CONCURRENT_REPACK	0x40	/* REPACK (CONCURRENTLY) */
 
 /* flags reset at EOXact */
 #define		PROC_VACUUM_STATE_MASK \
diff --git a/src/test/modules/injection_points/Makefile b/src/test/modules/injection_points/Makefile
index 2cd7d87c533..f7663859fe2 100644
--- a/src/test/modules/injection_points/Makefile
+++ b/src/test/modules/injection_points/Makefile
@@ -15,6 +15,7 @@ REGRESS_OPTS = --dlpath=$(top_builddir)/src/test/regress
 ISOLATION = basic \
 	    inplace \
 	    repack \
+	    repack_deadlock \
 	    repack_toast \
 	    syscache-update-pruned \
 	    heap_lock_update
diff --git a/src/test/modules/injection_points/expected/repack_deadlock.out b/src/test/modules/injection_points/expected/repack_deadlock.out
new file mode 100644
index 00000000000..42505d10933
--- /dev/null
+++ b/src/test/modules/injection_points/expected/repack_deadlock.out
@@ -0,0 +1,63 @@
+Parsed test spec with 2 sessions
+
+starting permutation: wait_before_lock add_column wakeup_before_lock check1
+injection_points_attach
+-----------------------
+                       
+(1 row)
+
+step wait_before_lock: 
+	REPACK (CONCURRENTLY) repack_deadlock USING INDEX repack_deadlock_pkey;
+ <waiting ...>
+step add_column: 
+	alter table repack_deadlock add column noise text;
+ <waiting ...>
+step add_column: <... completed>
+ERROR:  oops, we're blocked by REPACK, time to die
+step wakeup_before_lock: 
+	SELECT injection_points_wakeup('repack-concurrently-before-lock');
+
+injection_points_wakeup
+-----------------------
+                       
+(1 row)
+
+step wait_before_lock: <... completed>
+step check1: 
+	INSERT INTO relfilenodes(node)
+	SELECT relfilenode FROM pg_class WHERE relname='repack_deadlock';
+
+	SELECT count(DISTINCT node) FROM relfilenodes;
+
+	SELECT i, j FROM repack_deadlock ORDER BY i, j;
+
+	INSERT INTO data_s1(i, j)
+	SELECT i, j FROM repack_deadlock;
+
+	SELECT count(*)
+	FROM data_s1 d1 FULL JOIN data_s2 d2 USING (i, j)
+	WHERE d1.i ISNULL OR d2.i ISNULL;
+
+count
+-----
+    1
+(1 row)
+
+i|j
+-+-
+1|1
+2|2
+3|3
+4|4
+(4 rows)
+
+count
+-----
+    4
+(1 row)
+
+injection_points_detach
+-----------------------
+                       
+(1 row)
+
diff --git a/src/test/modules/injection_points/meson.build b/src/test/modules/injection_points/meson.build
index a414abb924b..1cd88d6db65 100644
--- a/src/test/modules/injection_points/meson.build
+++ b/src/test/modules/injection_points/meson.build
@@ -46,6 +46,7 @@ tests += {
       'basic',
       'inplace',
       'repack',
+      'repack_deadlock',
       'repack_toast',
       'syscache-update-pruned',
       'heap_lock_update',
diff --git a/src/test/modules/injection_points/specs/repack_deadlock.spec b/src/test/modules/injection_points/specs/repack_deadlock.spec
new file mode 100644
index 00000000000..9d23a6588c2
--- /dev/null
+++ b/src/test/modules/injection_points/specs/repack_deadlock.spec
@@ -0,0 +1,83 @@
+# Test REPACK with a concurrent transaction that would cause a deadlock
+setup
+{
+	CREATE EXTENSION injection_points;
+
+	CREATE TABLE repack_deadlock(i int PRIMARY KEY, j int);
+	INSERT INTO repack_deadlock(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4);
+
+	CREATE TABLE relfilenodes(node oid);
+
+	CREATE TABLE data_s1(i int, j int);
+	CREATE TABLE data_s2(i int, j int);
+}
+
+teardown
+{
+	DROP TABLE repack_deadlock;
+	DROP EXTENSION injection_points;
+
+	DROP TABLE relfilenodes;
+	DROP TABLE data_s1;
+	DROP TABLE data_s2;
+}
+
+session s1
+setup
+{
+	SELECT injection_points_set_local();
+	SELECT injection_points_attach('repack-concurrently-before-lock', 'wait');
+}
+# Perform the initial load and wait for s2 to do some data changes.
+step wait_before_lock
+{
+	REPACK (CONCURRENTLY) repack_deadlock USING INDEX repack_deadlock_pkey;
+}
+# Check the table from the perspective of s1.
+#
+# Besides the contents, we also check that relfilenode has changed.
+
+# Have each session write the contents into a table and use FULL JOIN to check
+# if the outputs are identical.
+step check1
+{
+	INSERT INTO relfilenodes(node)
+	SELECT relfilenode FROM pg_class WHERE relname='repack_deadlock';
+
+	SELECT count(DISTINCT node) FROM relfilenodes;
+
+	SELECT i, j FROM repack_deadlock ORDER BY i, j;
+
+	INSERT INTO data_s1(i, j)
+	SELECT i, j FROM repack_deadlock;
+
+	SELECT count(*)
+	FROM data_s1 d1 FULL JOIN data_s2 d2 USING (i, j)
+	WHERE d1.i ISNULL OR d2.i ISNULL;
+}
+teardown
+{
+	SELECT injection_points_detach('repack-concurrently-before-lock');
+}
+
+session s2
+# Change the existing data. UPDATE changes both key and non-key columns. Also
+# update one row twice to test whether tuple version generated by this session
+# can be found.
+step add_column
+{
+	alter table repack_deadlock add column noise text;
+}
+
+step wakeup_before_lock
+{
+	SELECT injection_points_wakeup('repack-concurrently-before-lock');
+}
+
+# Test if data changes introduced while one session is performing REPACK
+# CONCURRENTLY find their way into the table.
+permutation
+	wait_before_lock
+	add_column
+	wakeup_before_lock
+	check1
-- 
2.47.3


--fot5plqmb33ey33c--





^ permalink  raw  reply  [nested|flat] 89+ messages in thread

* [PATCH v44b 4/4] hack deadlock detector
@ 2026-03-25 19:35  Álvaro Herrera <[email protected]>
  0 siblings, 0 replies; 89+ messages in thread

From: Álvaro Herrera @ 2026-03-25 19:35 UTC (permalink / raw)

---
 src/backend/commands/cluster.c                | 10 +++
 src/backend/storage/lmgr/deadlock.c           | 12 +++
 src/include/storage/proc.h                    |  1 +
 src/test/modules/injection_points/Makefile    |  1 +
 .../expected/repack_deadlock.out              | 63 ++++++++++++++
 src/test/modules/injection_points/meson.build |  1 +
 .../specs/repack_deadlock.spec                | 83 +++++++++++++++++++
 7 files changed, 171 insertions(+)
 create mode 100644 src/test/modules/injection_points/expected/repack_deadlock.out
 create mode 100644 src/test/modules/injection_points/specs/repack_deadlock.spec

diff --git a/src/backend/commands/cluster.c b/src/backend/commands/cluster.c
index 2c3058ba10d..d5b1dfbff69 100644
--- a/src/backend/commands/cluster.c
+++ b/src/backend/commands/cluster.c
@@ -372,6 +372,16 @@ ExecRepack(ParseState *pstate, RepackStmt *stmt, bool isTopLevel)
 	/* Determine the lock mode to use. */
 	lockmode = RepackLockLevel((params.options & CLUOPT_CONCURRENT) != 0);
 
+	/*
+	 * If in concurrent mode, set the PROC_IN_CONCURRENT_REPACK flag.  This
+	 * makes the deadlock checker cause anyone that would conflict with us
+	 * to error out.  It's important to set this flag ahead of actually locking
+	 * the relation; it won't of course affect anyone until we do have a lock
+	 * that others can conflict with.
+	 */
+	if ((params.options & CLUOPT_CONCURRENT) != 0)
+		MyProc->statusFlags |= PROC_IN_CONCURRENT_REPACK;
+
 	/*
 	 * If a single relation is specified, process it and we're done ... unless
 	 * the relation is a partitioned table, in which case we fall through.
diff --git a/src/backend/storage/lmgr/deadlock.c b/src/backend/storage/lmgr/deadlock.c
index b8962d875b6..10266e384aa 100644
--- a/src/backend/storage/lmgr/deadlock.c
+++ b/src/backend/storage/lmgr/deadlock.c
@@ -620,6 +620,18 @@ FindLockCycleRecurseMember(PGPROC *checkProc,
 						proc->statusFlags & PROC_IS_AUTOVACUUM)
 						blocking_autovacuum_proc = proc;
 
+					/*
+					 * If we note that we're blocked by some process running
+					 * REPACK (CONCURRENTLY), just fail.  That process is
+					 * going to upgrade its lock at some point, and it would
+					 * be inappropriate for any other process to cause that
+					 * to fail.
+					 */
+					if (checkProc == MyProc &&
+						proc->statusFlags & PROC_IN_CONCURRENT_REPACK)
+						/* not a serious error message proposal */
+						elog(ERROR, "oops, we're blocked by REPACK, time to die");
+
 					/* We're done looking at this proclock */
 					break;
 				}
diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h
index 1dad125706e..4e340ead455 100644
--- a/src/include/storage/proc.h
+++ b/src/include/storage/proc.h
@@ -69,6 +69,7 @@ struct XidCache
 #define		PROC_AFFECTS_ALL_HORIZONS	0x20	/* this proc's xmin must be
 												 * included in vacuum horizons
 												 * in all databases */
+#define		PROC_IN_CONCURRENT_REPACK	0x40	/* REPACK (CONCURRENTLY) */
 
 /* flags reset at EOXact */
 #define		PROC_VACUUM_STATE_MASK \
diff --git a/src/test/modules/injection_points/Makefile b/src/test/modules/injection_points/Makefile
index 2cd7d87c533..f7663859fe2 100644
--- a/src/test/modules/injection_points/Makefile
+++ b/src/test/modules/injection_points/Makefile
@@ -15,6 +15,7 @@ REGRESS_OPTS = --dlpath=$(top_builddir)/src/test/regress
 ISOLATION = basic \
 	    inplace \
 	    repack \
+	    repack_deadlock \
 	    repack_toast \
 	    syscache-update-pruned \
 	    heap_lock_update
diff --git a/src/test/modules/injection_points/expected/repack_deadlock.out b/src/test/modules/injection_points/expected/repack_deadlock.out
new file mode 100644
index 00000000000..42505d10933
--- /dev/null
+++ b/src/test/modules/injection_points/expected/repack_deadlock.out
@@ -0,0 +1,63 @@
+Parsed test spec with 2 sessions
+
+starting permutation: wait_before_lock add_column wakeup_before_lock check1
+injection_points_attach
+-----------------------
+                       
+(1 row)
+
+step wait_before_lock: 
+	REPACK (CONCURRENTLY) repack_deadlock USING INDEX repack_deadlock_pkey;
+ <waiting ...>
+step add_column: 
+	alter table repack_deadlock add column noise text;
+ <waiting ...>
+step add_column: <... completed>
+ERROR:  oops, we're blocked by REPACK, time to die
+step wakeup_before_lock: 
+	SELECT injection_points_wakeup('repack-concurrently-before-lock');
+
+injection_points_wakeup
+-----------------------
+                       
+(1 row)
+
+step wait_before_lock: <... completed>
+step check1: 
+	INSERT INTO relfilenodes(node)
+	SELECT relfilenode FROM pg_class WHERE relname='repack_deadlock';
+
+	SELECT count(DISTINCT node) FROM relfilenodes;
+
+	SELECT i, j FROM repack_deadlock ORDER BY i, j;
+
+	INSERT INTO data_s1(i, j)
+	SELECT i, j FROM repack_deadlock;
+
+	SELECT count(*)
+	FROM data_s1 d1 FULL JOIN data_s2 d2 USING (i, j)
+	WHERE d1.i ISNULL OR d2.i ISNULL;
+
+count
+-----
+    1
+(1 row)
+
+i|j
+-+-
+1|1
+2|2
+3|3
+4|4
+(4 rows)
+
+count
+-----
+    4
+(1 row)
+
+injection_points_detach
+-----------------------
+                       
+(1 row)
+
diff --git a/src/test/modules/injection_points/meson.build b/src/test/modules/injection_points/meson.build
index a414abb924b..1cd88d6db65 100644
--- a/src/test/modules/injection_points/meson.build
+++ b/src/test/modules/injection_points/meson.build
@@ -46,6 +46,7 @@ tests += {
       'basic',
       'inplace',
       'repack',
+      'repack_deadlock',
       'repack_toast',
       'syscache-update-pruned',
       'heap_lock_update',
diff --git a/src/test/modules/injection_points/specs/repack_deadlock.spec b/src/test/modules/injection_points/specs/repack_deadlock.spec
new file mode 100644
index 00000000000..9d23a6588c2
--- /dev/null
+++ b/src/test/modules/injection_points/specs/repack_deadlock.spec
@@ -0,0 +1,83 @@
+# Test REPACK with a concurrent transaction that would cause a deadlock
+setup
+{
+	CREATE EXTENSION injection_points;
+
+	CREATE TABLE repack_deadlock(i int PRIMARY KEY, j int);
+	INSERT INTO repack_deadlock(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4);
+
+	CREATE TABLE relfilenodes(node oid);
+
+	CREATE TABLE data_s1(i int, j int);
+	CREATE TABLE data_s2(i int, j int);
+}
+
+teardown
+{
+	DROP TABLE repack_deadlock;
+	DROP EXTENSION injection_points;
+
+	DROP TABLE relfilenodes;
+	DROP TABLE data_s1;
+	DROP TABLE data_s2;
+}
+
+session s1
+setup
+{
+	SELECT injection_points_set_local();
+	SELECT injection_points_attach('repack-concurrently-before-lock', 'wait');
+}
+# Perform the initial load and wait for s2 to do some data changes.
+step wait_before_lock
+{
+	REPACK (CONCURRENTLY) repack_deadlock USING INDEX repack_deadlock_pkey;
+}
+# Check the table from the perspective of s1.
+#
+# Besides the contents, we also check that relfilenode has changed.
+
+# Have each session write the contents into a table and use FULL JOIN to check
+# if the outputs are identical.
+step check1
+{
+	INSERT INTO relfilenodes(node)
+	SELECT relfilenode FROM pg_class WHERE relname='repack_deadlock';
+
+	SELECT count(DISTINCT node) FROM relfilenodes;
+
+	SELECT i, j FROM repack_deadlock ORDER BY i, j;
+
+	INSERT INTO data_s1(i, j)
+	SELECT i, j FROM repack_deadlock;
+
+	SELECT count(*)
+	FROM data_s1 d1 FULL JOIN data_s2 d2 USING (i, j)
+	WHERE d1.i ISNULL OR d2.i ISNULL;
+}
+teardown
+{
+	SELECT injection_points_detach('repack-concurrently-before-lock');
+}
+
+session s2
+# Change the existing data. UPDATE changes both key and non-key columns. Also
+# update one row twice to test whether tuple version generated by this session
+# can be found.
+step add_column
+{
+	alter table repack_deadlock add column noise text;
+}
+
+step wakeup_before_lock
+{
+	SELECT injection_points_wakeup('repack-concurrently-before-lock');
+}
+
+# Test if data changes introduced while one session is performing REPACK
+# CONCURRENTLY find their way into the table.
+permutation
+	wait_before_lock
+	add_column
+	wakeup_before_lock
+	check1
-- 
2.47.3


--fot5plqmb33ey33c--





^ permalink  raw  reply  [nested|flat] 89+ messages in thread

* [PATCH v44b 4/4] hack deadlock detector
@ 2026-03-25 19:35  Álvaro Herrera <[email protected]>
  0 siblings, 0 replies; 89+ messages in thread

From: Álvaro Herrera @ 2026-03-25 19:35 UTC (permalink / raw)

---
 src/backend/commands/cluster.c                | 10 +++
 src/backend/storage/lmgr/deadlock.c           | 12 +++
 src/include/storage/proc.h                    |  1 +
 src/test/modules/injection_points/Makefile    |  1 +
 .../expected/repack_deadlock.out              | 63 ++++++++++++++
 src/test/modules/injection_points/meson.build |  1 +
 .../specs/repack_deadlock.spec                | 83 +++++++++++++++++++
 7 files changed, 171 insertions(+)
 create mode 100644 src/test/modules/injection_points/expected/repack_deadlock.out
 create mode 100644 src/test/modules/injection_points/specs/repack_deadlock.spec

diff --git a/src/backend/commands/cluster.c b/src/backend/commands/cluster.c
index 2c3058ba10d..d5b1dfbff69 100644
--- a/src/backend/commands/cluster.c
+++ b/src/backend/commands/cluster.c
@@ -372,6 +372,16 @@ ExecRepack(ParseState *pstate, RepackStmt *stmt, bool isTopLevel)
 	/* Determine the lock mode to use. */
 	lockmode = RepackLockLevel((params.options & CLUOPT_CONCURRENT) != 0);
 
+	/*
+	 * If in concurrent mode, set the PROC_IN_CONCURRENT_REPACK flag.  This
+	 * makes the deadlock checker cause anyone that would conflict with us
+	 * to error out.  It's important to set this flag ahead of actually locking
+	 * the relation; it won't of course affect anyone until we do have a lock
+	 * that others can conflict with.
+	 */
+	if ((params.options & CLUOPT_CONCURRENT) != 0)
+		MyProc->statusFlags |= PROC_IN_CONCURRENT_REPACK;
+
 	/*
 	 * If a single relation is specified, process it and we're done ... unless
 	 * the relation is a partitioned table, in which case we fall through.
diff --git a/src/backend/storage/lmgr/deadlock.c b/src/backend/storage/lmgr/deadlock.c
index b8962d875b6..10266e384aa 100644
--- a/src/backend/storage/lmgr/deadlock.c
+++ b/src/backend/storage/lmgr/deadlock.c
@@ -620,6 +620,18 @@ FindLockCycleRecurseMember(PGPROC *checkProc,
 						proc->statusFlags & PROC_IS_AUTOVACUUM)
 						blocking_autovacuum_proc = proc;
 
+					/*
+					 * If we note that we're blocked by some process running
+					 * REPACK (CONCURRENTLY), just fail.  That process is
+					 * going to upgrade its lock at some point, and it would
+					 * be inappropriate for any other process to cause that
+					 * to fail.
+					 */
+					if (checkProc == MyProc &&
+						proc->statusFlags & PROC_IN_CONCURRENT_REPACK)
+						/* not a serious error message proposal */
+						elog(ERROR, "oops, we're blocked by REPACK, time to die");
+
 					/* We're done looking at this proclock */
 					break;
 				}
diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h
index 1dad125706e..4e340ead455 100644
--- a/src/include/storage/proc.h
+++ b/src/include/storage/proc.h
@@ -69,6 +69,7 @@ struct XidCache
 #define		PROC_AFFECTS_ALL_HORIZONS	0x20	/* this proc's xmin must be
 												 * included in vacuum horizons
 												 * in all databases */
+#define		PROC_IN_CONCURRENT_REPACK	0x40	/* REPACK (CONCURRENTLY) */
 
 /* flags reset at EOXact */
 #define		PROC_VACUUM_STATE_MASK \
diff --git a/src/test/modules/injection_points/Makefile b/src/test/modules/injection_points/Makefile
index 2cd7d87c533..f7663859fe2 100644
--- a/src/test/modules/injection_points/Makefile
+++ b/src/test/modules/injection_points/Makefile
@@ -15,6 +15,7 @@ REGRESS_OPTS = --dlpath=$(top_builddir)/src/test/regress
 ISOLATION = basic \
 	    inplace \
 	    repack \
+	    repack_deadlock \
 	    repack_toast \
 	    syscache-update-pruned \
 	    heap_lock_update
diff --git a/src/test/modules/injection_points/expected/repack_deadlock.out b/src/test/modules/injection_points/expected/repack_deadlock.out
new file mode 100644
index 00000000000..42505d10933
--- /dev/null
+++ b/src/test/modules/injection_points/expected/repack_deadlock.out
@@ -0,0 +1,63 @@
+Parsed test spec with 2 sessions
+
+starting permutation: wait_before_lock add_column wakeup_before_lock check1
+injection_points_attach
+-----------------------
+                       
+(1 row)
+
+step wait_before_lock: 
+	REPACK (CONCURRENTLY) repack_deadlock USING INDEX repack_deadlock_pkey;
+ <waiting ...>
+step add_column: 
+	alter table repack_deadlock add column noise text;
+ <waiting ...>
+step add_column: <... completed>
+ERROR:  oops, we're blocked by REPACK, time to die
+step wakeup_before_lock: 
+	SELECT injection_points_wakeup('repack-concurrently-before-lock');
+
+injection_points_wakeup
+-----------------------
+                       
+(1 row)
+
+step wait_before_lock: <... completed>
+step check1: 
+	INSERT INTO relfilenodes(node)
+	SELECT relfilenode FROM pg_class WHERE relname='repack_deadlock';
+
+	SELECT count(DISTINCT node) FROM relfilenodes;
+
+	SELECT i, j FROM repack_deadlock ORDER BY i, j;
+
+	INSERT INTO data_s1(i, j)
+	SELECT i, j FROM repack_deadlock;
+
+	SELECT count(*)
+	FROM data_s1 d1 FULL JOIN data_s2 d2 USING (i, j)
+	WHERE d1.i ISNULL OR d2.i ISNULL;
+
+count
+-----
+    1
+(1 row)
+
+i|j
+-+-
+1|1
+2|2
+3|3
+4|4
+(4 rows)
+
+count
+-----
+    4
+(1 row)
+
+injection_points_detach
+-----------------------
+                       
+(1 row)
+
diff --git a/src/test/modules/injection_points/meson.build b/src/test/modules/injection_points/meson.build
index a414abb924b..1cd88d6db65 100644
--- a/src/test/modules/injection_points/meson.build
+++ b/src/test/modules/injection_points/meson.build
@@ -46,6 +46,7 @@ tests += {
       'basic',
       'inplace',
       'repack',
+      'repack_deadlock',
       'repack_toast',
       'syscache-update-pruned',
       'heap_lock_update',
diff --git a/src/test/modules/injection_points/specs/repack_deadlock.spec b/src/test/modules/injection_points/specs/repack_deadlock.spec
new file mode 100644
index 00000000000..9d23a6588c2
--- /dev/null
+++ b/src/test/modules/injection_points/specs/repack_deadlock.spec
@@ -0,0 +1,83 @@
+# Test REPACK with a concurrent transaction that would cause a deadlock
+setup
+{
+	CREATE EXTENSION injection_points;
+
+	CREATE TABLE repack_deadlock(i int PRIMARY KEY, j int);
+	INSERT INTO repack_deadlock(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4);
+
+	CREATE TABLE relfilenodes(node oid);
+
+	CREATE TABLE data_s1(i int, j int);
+	CREATE TABLE data_s2(i int, j int);
+}
+
+teardown
+{
+	DROP TABLE repack_deadlock;
+	DROP EXTENSION injection_points;
+
+	DROP TABLE relfilenodes;
+	DROP TABLE data_s1;
+	DROP TABLE data_s2;
+}
+
+session s1
+setup
+{
+	SELECT injection_points_set_local();
+	SELECT injection_points_attach('repack-concurrently-before-lock', 'wait');
+}
+# Perform the initial load and wait for s2 to do some data changes.
+step wait_before_lock
+{
+	REPACK (CONCURRENTLY) repack_deadlock USING INDEX repack_deadlock_pkey;
+}
+# Check the table from the perspective of s1.
+#
+# Besides the contents, we also check that relfilenode has changed.
+
+# Have each session write the contents into a table and use FULL JOIN to check
+# if the outputs are identical.
+step check1
+{
+	INSERT INTO relfilenodes(node)
+	SELECT relfilenode FROM pg_class WHERE relname='repack_deadlock';
+
+	SELECT count(DISTINCT node) FROM relfilenodes;
+
+	SELECT i, j FROM repack_deadlock ORDER BY i, j;
+
+	INSERT INTO data_s1(i, j)
+	SELECT i, j FROM repack_deadlock;
+
+	SELECT count(*)
+	FROM data_s1 d1 FULL JOIN data_s2 d2 USING (i, j)
+	WHERE d1.i ISNULL OR d2.i ISNULL;
+}
+teardown
+{
+	SELECT injection_points_detach('repack-concurrently-before-lock');
+}
+
+session s2
+# Change the existing data. UPDATE changes both key and non-key columns. Also
+# update one row twice to test whether tuple version generated by this session
+# can be found.
+step add_column
+{
+	alter table repack_deadlock add column noise text;
+}
+
+step wakeup_before_lock
+{
+	SELECT injection_points_wakeup('repack-concurrently-before-lock');
+}
+
+# Test if data changes introduced while one session is performing REPACK
+# CONCURRENTLY find their way into the table.
+permutation
+	wait_before_lock
+	add_column
+	wakeup_before_lock
+	check1
-- 
2.47.3


--fot5plqmb33ey33c--





^ permalink  raw  reply  [nested|flat] 89+ messages in thread

* [PATCH v44b 4/4] hack deadlock detector
@ 2026-03-25 19:35  Álvaro Herrera <[email protected]>
  0 siblings, 0 replies; 89+ messages in thread

From: Álvaro Herrera @ 2026-03-25 19:35 UTC (permalink / raw)

---
 src/backend/commands/cluster.c                | 10 +++
 src/backend/storage/lmgr/deadlock.c           | 12 +++
 src/include/storage/proc.h                    |  1 +
 src/test/modules/injection_points/Makefile    |  1 +
 .../expected/repack_deadlock.out              | 63 ++++++++++++++
 src/test/modules/injection_points/meson.build |  1 +
 .../specs/repack_deadlock.spec                | 83 +++++++++++++++++++
 7 files changed, 171 insertions(+)
 create mode 100644 src/test/modules/injection_points/expected/repack_deadlock.out
 create mode 100644 src/test/modules/injection_points/specs/repack_deadlock.spec

diff --git a/src/backend/commands/cluster.c b/src/backend/commands/cluster.c
index 2c3058ba10d..d5b1dfbff69 100644
--- a/src/backend/commands/cluster.c
+++ b/src/backend/commands/cluster.c
@@ -372,6 +372,16 @@ ExecRepack(ParseState *pstate, RepackStmt *stmt, bool isTopLevel)
 	/* Determine the lock mode to use. */
 	lockmode = RepackLockLevel((params.options & CLUOPT_CONCURRENT) != 0);
 
+	/*
+	 * If in concurrent mode, set the PROC_IN_CONCURRENT_REPACK flag.  This
+	 * makes the deadlock checker cause anyone that would conflict with us
+	 * to error out.  It's important to set this flag ahead of actually locking
+	 * the relation; it won't of course affect anyone until we do have a lock
+	 * that others can conflict with.
+	 */
+	if ((params.options & CLUOPT_CONCURRENT) != 0)
+		MyProc->statusFlags |= PROC_IN_CONCURRENT_REPACK;
+
 	/*
 	 * If a single relation is specified, process it and we're done ... unless
 	 * the relation is a partitioned table, in which case we fall through.
diff --git a/src/backend/storage/lmgr/deadlock.c b/src/backend/storage/lmgr/deadlock.c
index b8962d875b6..10266e384aa 100644
--- a/src/backend/storage/lmgr/deadlock.c
+++ b/src/backend/storage/lmgr/deadlock.c
@@ -620,6 +620,18 @@ FindLockCycleRecurseMember(PGPROC *checkProc,
 						proc->statusFlags & PROC_IS_AUTOVACUUM)
 						blocking_autovacuum_proc = proc;
 
+					/*
+					 * If we note that we're blocked by some process running
+					 * REPACK (CONCURRENTLY), just fail.  That process is
+					 * going to upgrade its lock at some point, and it would
+					 * be inappropriate for any other process to cause that
+					 * to fail.
+					 */
+					if (checkProc == MyProc &&
+						proc->statusFlags & PROC_IN_CONCURRENT_REPACK)
+						/* not a serious error message proposal */
+						elog(ERROR, "oops, we're blocked by REPACK, time to die");
+
 					/* We're done looking at this proclock */
 					break;
 				}
diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h
index 1dad125706e..4e340ead455 100644
--- a/src/include/storage/proc.h
+++ b/src/include/storage/proc.h
@@ -69,6 +69,7 @@ struct XidCache
 #define		PROC_AFFECTS_ALL_HORIZONS	0x20	/* this proc's xmin must be
 												 * included in vacuum horizons
 												 * in all databases */
+#define		PROC_IN_CONCURRENT_REPACK	0x40	/* REPACK (CONCURRENTLY) */
 
 /* flags reset at EOXact */
 #define		PROC_VACUUM_STATE_MASK \
diff --git a/src/test/modules/injection_points/Makefile b/src/test/modules/injection_points/Makefile
index 2cd7d87c533..f7663859fe2 100644
--- a/src/test/modules/injection_points/Makefile
+++ b/src/test/modules/injection_points/Makefile
@@ -15,6 +15,7 @@ REGRESS_OPTS = --dlpath=$(top_builddir)/src/test/regress
 ISOLATION = basic \
 	    inplace \
 	    repack \
+	    repack_deadlock \
 	    repack_toast \
 	    syscache-update-pruned \
 	    heap_lock_update
diff --git a/src/test/modules/injection_points/expected/repack_deadlock.out b/src/test/modules/injection_points/expected/repack_deadlock.out
new file mode 100644
index 00000000000..42505d10933
--- /dev/null
+++ b/src/test/modules/injection_points/expected/repack_deadlock.out
@@ -0,0 +1,63 @@
+Parsed test spec with 2 sessions
+
+starting permutation: wait_before_lock add_column wakeup_before_lock check1
+injection_points_attach
+-----------------------
+                       
+(1 row)
+
+step wait_before_lock: 
+	REPACK (CONCURRENTLY) repack_deadlock USING INDEX repack_deadlock_pkey;
+ <waiting ...>
+step add_column: 
+	alter table repack_deadlock add column noise text;
+ <waiting ...>
+step add_column: <... completed>
+ERROR:  oops, we're blocked by REPACK, time to die
+step wakeup_before_lock: 
+	SELECT injection_points_wakeup('repack-concurrently-before-lock');
+
+injection_points_wakeup
+-----------------------
+                       
+(1 row)
+
+step wait_before_lock: <... completed>
+step check1: 
+	INSERT INTO relfilenodes(node)
+	SELECT relfilenode FROM pg_class WHERE relname='repack_deadlock';
+
+	SELECT count(DISTINCT node) FROM relfilenodes;
+
+	SELECT i, j FROM repack_deadlock ORDER BY i, j;
+
+	INSERT INTO data_s1(i, j)
+	SELECT i, j FROM repack_deadlock;
+
+	SELECT count(*)
+	FROM data_s1 d1 FULL JOIN data_s2 d2 USING (i, j)
+	WHERE d1.i ISNULL OR d2.i ISNULL;
+
+count
+-----
+    1
+(1 row)
+
+i|j
+-+-
+1|1
+2|2
+3|3
+4|4
+(4 rows)
+
+count
+-----
+    4
+(1 row)
+
+injection_points_detach
+-----------------------
+                       
+(1 row)
+
diff --git a/src/test/modules/injection_points/meson.build b/src/test/modules/injection_points/meson.build
index a414abb924b..1cd88d6db65 100644
--- a/src/test/modules/injection_points/meson.build
+++ b/src/test/modules/injection_points/meson.build
@@ -46,6 +46,7 @@ tests += {
       'basic',
       'inplace',
       'repack',
+      'repack_deadlock',
       'repack_toast',
       'syscache-update-pruned',
       'heap_lock_update',
diff --git a/src/test/modules/injection_points/specs/repack_deadlock.spec b/src/test/modules/injection_points/specs/repack_deadlock.spec
new file mode 100644
index 00000000000..9d23a6588c2
--- /dev/null
+++ b/src/test/modules/injection_points/specs/repack_deadlock.spec
@@ -0,0 +1,83 @@
+# Test REPACK with a concurrent transaction that would cause a deadlock
+setup
+{
+	CREATE EXTENSION injection_points;
+
+	CREATE TABLE repack_deadlock(i int PRIMARY KEY, j int);
+	INSERT INTO repack_deadlock(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4);
+
+	CREATE TABLE relfilenodes(node oid);
+
+	CREATE TABLE data_s1(i int, j int);
+	CREATE TABLE data_s2(i int, j int);
+}
+
+teardown
+{
+	DROP TABLE repack_deadlock;
+	DROP EXTENSION injection_points;
+
+	DROP TABLE relfilenodes;
+	DROP TABLE data_s1;
+	DROP TABLE data_s2;
+}
+
+session s1
+setup
+{
+	SELECT injection_points_set_local();
+	SELECT injection_points_attach('repack-concurrently-before-lock', 'wait');
+}
+# Perform the initial load and wait for s2 to do some data changes.
+step wait_before_lock
+{
+	REPACK (CONCURRENTLY) repack_deadlock USING INDEX repack_deadlock_pkey;
+}
+# Check the table from the perspective of s1.
+#
+# Besides the contents, we also check that relfilenode has changed.
+
+# Have each session write the contents into a table and use FULL JOIN to check
+# if the outputs are identical.
+step check1
+{
+	INSERT INTO relfilenodes(node)
+	SELECT relfilenode FROM pg_class WHERE relname='repack_deadlock';
+
+	SELECT count(DISTINCT node) FROM relfilenodes;
+
+	SELECT i, j FROM repack_deadlock ORDER BY i, j;
+
+	INSERT INTO data_s1(i, j)
+	SELECT i, j FROM repack_deadlock;
+
+	SELECT count(*)
+	FROM data_s1 d1 FULL JOIN data_s2 d2 USING (i, j)
+	WHERE d1.i ISNULL OR d2.i ISNULL;
+}
+teardown
+{
+	SELECT injection_points_detach('repack-concurrently-before-lock');
+}
+
+session s2
+# Change the existing data. UPDATE changes both key and non-key columns. Also
+# update one row twice to test whether tuple version generated by this session
+# can be found.
+step add_column
+{
+	alter table repack_deadlock add column noise text;
+}
+
+step wakeup_before_lock
+{
+	SELECT injection_points_wakeup('repack-concurrently-before-lock');
+}
+
+# Test if data changes introduced while one session is performing REPACK
+# CONCURRENTLY find their way into the table.
+permutation
+	wait_before_lock
+	add_column
+	wakeup_before_lock
+	check1
-- 
2.47.3


--fot5plqmb33ey33c--





^ permalink  raw  reply  [nested|flat] 89+ messages in thread

* [PATCH v44b 4/4] hack deadlock detector
@ 2026-03-25 19:35  Álvaro Herrera <[email protected]>
  0 siblings, 0 replies; 89+ messages in thread

From: Álvaro Herrera @ 2026-03-25 19:35 UTC (permalink / raw)

---
 src/backend/commands/cluster.c                | 10 +++
 src/backend/storage/lmgr/deadlock.c           | 12 +++
 src/include/storage/proc.h                    |  1 +
 src/test/modules/injection_points/Makefile    |  1 +
 .../expected/repack_deadlock.out              | 63 ++++++++++++++
 src/test/modules/injection_points/meson.build |  1 +
 .../specs/repack_deadlock.spec                | 83 +++++++++++++++++++
 7 files changed, 171 insertions(+)
 create mode 100644 src/test/modules/injection_points/expected/repack_deadlock.out
 create mode 100644 src/test/modules/injection_points/specs/repack_deadlock.spec

diff --git a/src/backend/commands/cluster.c b/src/backend/commands/cluster.c
index 2c3058ba10d..d5b1dfbff69 100644
--- a/src/backend/commands/cluster.c
+++ b/src/backend/commands/cluster.c
@@ -372,6 +372,16 @@ ExecRepack(ParseState *pstate, RepackStmt *stmt, bool isTopLevel)
 	/* Determine the lock mode to use. */
 	lockmode = RepackLockLevel((params.options & CLUOPT_CONCURRENT) != 0);
 
+	/*
+	 * If in concurrent mode, set the PROC_IN_CONCURRENT_REPACK flag.  This
+	 * makes the deadlock checker cause anyone that would conflict with us
+	 * to error out.  It's important to set this flag ahead of actually locking
+	 * the relation; it won't of course affect anyone until we do have a lock
+	 * that others can conflict with.
+	 */
+	if ((params.options & CLUOPT_CONCURRENT) != 0)
+		MyProc->statusFlags |= PROC_IN_CONCURRENT_REPACK;
+
 	/*
 	 * If a single relation is specified, process it and we're done ... unless
 	 * the relation is a partitioned table, in which case we fall through.
diff --git a/src/backend/storage/lmgr/deadlock.c b/src/backend/storage/lmgr/deadlock.c
index b8962d875b6..10266e384aa 100644
--- a/src/backend/storage/lmgr/deadlock.c
+++ b/src/backend/storage/lmgr/deadlock.c
@@ -620,6 +620,18 @@ FindLockCycleRecurseMember(PGPROC *checkProc,
 						proc->statusFlags & PROC_IS_AUTOVACUUM)
 						blocking_autovacuum_proc = proc;
 
+					/*
+					 * If we note that we're blocked by some process running
+					 * REPACK (CONCURRENTLY), just fail.  That process is
+					 * going to upgrade its lock at some point, and it would
+					 * be inappropriate for any other process to cause that
+					 * to fail.
+					 */
+					if (checkProc == MyProc &&
+						proc->statusFlags & PROC_IN_CONCURRENT_REPACK)
+						/* not a serious error message proposal */
+						elog(ERROR, "oops, we're blocked by REPACK, time to die");
+
 					/* We're done looking at this proclock */
 					break;
 				}
diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h
index 1dad125706e..4e340ead455 100644
--- a/src/include/storage/proc.h
+++ b/src/include/storage/proc.h
@@ -69,6 +69,7 @@ struct XidCache
 #define		PROC_AFFECTS_ALL_HORIZONS	0x20	/* this proc's xmin must be
 												 * included in vacuum horizons
 												 * in all databases */
+#define		PROC_IN_CONCURRENT_REPACK	0x40	/* REPACK (CONCURRENTLY) */
 
 /* flags reset at EOXact */
 #define		PROC_VACUUM_STATE_MASK \
diff --git a/src/test/modules/injection_points/Makefile b/src/test/modules/injection_points/Makefile
index 2cd7d87c533..f7663859fe2 100644
--- a/src/test/modules/injection_points/Makefile
+++ b/src/test/modules/injection_points/Makefile
@@ -15,6 +15,7 @@ REGRESS_OPTS = --dlpath=$(top_builddir)/src/test/regress
 ISOLATION = basic \
 	    inplace \
 	    repack \
+	    repack_deadlock \
 	    repack_toast \
 	    syscache-update-pruned \
 	    heap_lock_update
diff --git a/src/test/modules/injection_points/expected/repack_deadlock.out b/src/test/modules/injection_points/expected/repack_deadlock.out
new file mode 100644
index 00000000000..42505d10933
--- /dev/null
+++ b/src/test/modules/injection_points/expected/repack_deadlock.out
@@ -0,0 +1,63 @@
+Parsed test spec with 2 sessions
+
+starting permutation: wait_before_lock add_column wakeup_before_lock check1
+injection_points_attach
+-----------------------
+                       
+(1 row)
+
+step wait_before_lock: 
+	REPACK (CONCURRENTLY) repack_deadlock USING INDEX repack_deadlock_pkey;
+ <waiting ...>
+step add_column: 
+	alter table repack_deadlock add column noise text;
+ <waiting ...>
+step add_column: <... completed>
+ERROR:  oops, we're blocked by REPACK, time to die
+step wakeup_before_lock: 
+	SELECT injection_points_wakeup('repack-concurrently-before-lock');
+
+injection_points_wakeup
+-----------------------
+                       
+(1 row)
+
+step wait_before_lock: <... completed>
+step check1: 
+	INSERT INTO relfilenodes(node)
+	SELECT relfilenode FROM pg_class WHERE relname='repack_deadlock';
+
+	SELECT count(DISTINCT node) FROM relfilenodes;
+
+	SELECT i, j FROM repack_deadlock ORDER BY i, j;
+
+	INSERT INTO data_s1(i, j)
+	SELECT i, j FROM repack_deadlock;
+
+	SELECT count(*)
+	FROM data_s1 d1 FULL JOIN data_s2 d2 USING (i, j)
+	WHERE d1.i ISNULL OR d2.i ISNULL;
+
+count
+-----
+    1
+(1 row)
+
+i|j
+-+-
+1|1
+2|2
+3|3
+4|4
+(4 rows)
+
+count
+-----
+    4
+(1 row)
+
+injection_points_detach
+-----------------------
+                       
+(1 row)
+
diff --git a/src/test/modules/injection_points/meson.build b/src/test/modules/injection_points/meson.build
index a414abb924b..1cd88d6db65 100644
--- a/src/test/modules/injection_points/meson.build
+++ b/src/test/modules/injection_points/meson.build
@@ -46,6 +46,7 @@ tests += {
       'basic',
       'inplace',
       'repack',
+      'repack_deadlock',
       'repack_toast',
       'syscache-update-pruned',
       'heap_lock_update',
diff --git a/src/test/modules/injection_points/specs/repack_deadlock.spec b/src/test/modules/injection_points/specs/repack_deadlock.spec
new file mode 100644
index 00000000000..9d23a6588c2
--- /dev/null
+++ b/src/test/modules/injection_points/specs/repack_deadlock.spec
@@ -0,0 +1,83 @@
+# Test REPACK with a concurrent transaction that would cause a deadlock
+setup
+{
+	CREATE EXTENSION injection_points;
+
+	CREATE TABLE repack_deadlock(i int PRIMARY KEY, j int);
+	INSERT INTO repack_deadlock(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4);
+
+	CREATE TABLE relfilenodes(node oid);
+
+	CREATE TABLE data_s1(i int, j int);
+	CREATE TABLE data_s2(i int, j int);
+}
+
+teardown
+{
+	DROP TABLE repack_deadlock;
+	DROP EXTENSION injection_points;
+
+	DROP TABLE relfilenodes;
+	DROP TABLE data_s1;
+	DROP TABLE data_s2;
+}
+
+session s1
+setup
+{
+	SELECT injection_points_set_local();
+	SELECT injection_points_attach('repack-concurrently-before-lock', 'wait');
+}
+# Perform the initial load and wait for s2 to do some data changes.
+step wait_before_lock
+{
+	REPACK (CONCURRENTLY) repack_deadlock USING INDEX repack_deadlock_pkey;
+}
+# Check the table from the perspective of s1.
+#
+# Besides the contents, we also check that relfilenode has changed.
+
+# Have each session write the contents into a table and use FULL JOIN to check
+# if the outputs are identical.
+step check1
+{
+	INSERT INTO relfilenodes(node)
+	SELECT relfilenode FROM pg_class WHERE relname='repack_deadlock';
+
+	SELECT count(DISTINCT node) FROM relfilenodes;
+
+	SELECT i, j FROM repack_deadlock ORDER BY i, j;
+
+	INSERT INTO data_s1(i, j)
+	SELECT i, j FROM repack_deadlock;
+
+	SELECT count(*)
+	FROM data_s1 d1 FULL JOIN data_s2 d2 USING (i, j)
+	WHERE d1.i ISNULL OR d2.i ISNULL;
+}
+teardown
+{
+	SELECT injection_points_detach('repack-concurrently-before-lock');
+}
+
+session s2
+# Change the existing data. UPDATE changes both key and non-key columns. Also
+# update one row twice to test whether tuple version generated by this session
+# can be found.
+step add_column
+{
+	alter table repack_deadlock add column noise text;
+}
+
+step wakeup_before_lock
+{
+	SELECT injection_points_wakeup('repack-concurrently-before-lock');
+}
+
+# Test if data changes introduced while one session is performing REPACK
+# CONCURRENTLY find their way into the table.
+permutation
+	wait_before_lock
+	add_column
+	wakeup_before_lock
+	check1
-- 
2.47.3


--fot5plqmb33ey33c--





^ permalink  raw  reply  [nested|flat] 89+ messages in thread

* [PATCH v44b 4/4] hack deadlock detector
@ 2026-03-25 19:35  Álvaro Herrera <[email protected]>
  0 siblings, 0 replies; 89+ messages in thread

From: Álvaro Herrera @ 2026-03-25 19:35 UTC (permalink / raw)

---
 src/backend/commands/cluster.c                | 10 +++
 src/backend/storage/lmgr/deadlock.c           | 12 +++
 src/include/storage/proc.h                    |  1 +
 src/test/modules/injection_points/Makefile    |  1 +
 .../expected/repack_deadlock.out              | 63 ++++++++++++++
 src/test/modules/injection_points/meson.build |  1 +
 .../specs/repack_deadlock.spec                | 83 +++++++++++++++++++
 7 files changed, 171 insertions(+)
 create mode 100644 src/test/modules/injection_points/expected/repack_deadlock.out
 create mode 100644 src/test/modules/injection_points/specs/repack_deadlock.spec

diff --git a/src/backend/commands/cluster.c b/src/backend/commands/cluster.c
index 2c3058ba10d..d5b1dfbff69 100644
--- a/src/backend/commands/cluster.c
+++ b/src/backend/commands/cluster.c
@@ -372,6 +372,16 @@ ExecRepack(ParseState *pstate, RepackStmt *stmt, bool isTopLevel)
 	/* Determine the lock mode to use. */
 	lockmode = RepackLockLevel((params.options & CLUOPT_CONCURRENT) != 0);
 
+	/*
+	 * If in concurrent mode, set the PROC_IN_CONCURRENT_REPACK flag.  This
+	 * makes the deadlock checker cause anyone that would conflict with us
+	 * to error out.  It's important to set this flag ahead of actually locking
+	 * the relation; it won't of course affect anyone until we do have a lock
+	 * that others can conflict with.
+	 */
+	if ((params.options & CLUOPT_CONCURRENT) != 0)
+		MyProc->statusFlags |= PROC_IN_CONCURRENT_REPACK;
+
 	/*
 	 * If a single relation is specified, process it and we're done ... unless
 	 * the relation is a partitioned table, in which case we fall through.
diff --git a/src/backend/storage/lmgr/deadlock.c b/src/backend/storage/lmgr/deadlock.c
index b8962d875b6..10266e384aa 100644
--- a/src/backend/storage/lmgr/deadlock.c
+++ b/src/backend/storage/lmgr/deadlock.c
@@ -620,6 +620,18 @@ FindLockCycleRecurseMember(PGPROC *checkProc,
 						proc->statusFlags & PROC_IS_AUTOVACUUM)
 						blocking_autovacuum_proc = proc;
 
+					/*
+					 * If we note that we're blocked by some process running
+					 * REPACK (CONCURRENTLY), just fail.  That process is
+					 * going to upgrade its lock at some point, and it would
+					 * be inappropriate for any other process to cause that
+					 * to fail.
+					 */
+					if (checkProc == MyProc &&
+						proc->statusFlags & PROC_IN_CONCURRENT_REPACK)
+						/* not a serious error message proposal */
+						elog(ERROR, "oops, we're blocked by REPACK, time to die");
+
 					/* We're done looking at this proclock */
 					break;
 				}
diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h
index 1dad125706e..4e340ead455 100644
--- a/src/include/storage/proc.h
+++ b/src/include/storage/proc.h
@@ -69,6 +69,7 @@ struct XidCache
 #define		PROC_AFFECTS_ALL_HORIZONS	0x20	/* this proc's xmin must be
 												 * included in vacuum horizons
 												 * in all databases */
+#define		PROC_IN_CONCURRENT_REPACK	0x40	/* REPACK (CONCURRENTLY) */
 
 /* flags reset at EOXact */
 #define		PROC_VACUUM_STATE_MASK \
diff --git a/src/test/modules/injection_points/Makefile b/src/test/modules/injection_points/Makefile
index 2cd7d87c533..f7663859fe2 100644
--- a/src/test/modules/injection_points/Makefile
+++ b/src/test/modules/injection_points/Makefile
@@ -15,6 +15,7 @@ REGRESS_OPTS = --dlpath=$(top_builddir)/src/test/regress
 ISOLATION = basic \
 	    inplace \
 	    repack \
+	    repack_deadlock \
 	    repack_toast \
 	    syscache-update-pruned \
 	    heap_lock_update
diff --git a/src/test/modules/injection_points/expected/repack_deadlock.out b/src/test/modules/injection_points/expected/repack_deadlock.out
new file mode 100644
index 00000000000..42505d10933
--- /dev/null
+++ b/src/test/modules/injection_points/expected/repack_deadlock.out
@@ -0,0 +1,63 @@
+Parsed test spec with 2 sessions
+
+starting permutation: wait_before_lock add_column wakeup_before_lock check1
+injection_points_attach
+-----------------------
+                       
+(1 row)
+
+step wait_before_lock: 
+	REPACK (CONCURRENTLY) repack_deadlock USING INDEX repack_deadlock_pkey;
+ <waiting ...>
+step add_column: 
+	alter table repack_deadlock add column noise text;
+ <waiting ...>
+step add_column: <... completed>
+ERROR:  oops, we're blocked by REPACK, time to die
+step wakeup_before_lock: 
+	SELECT injection_points_wakeup('repack-concurrently-before-lock');
+
+injection_points_wakeup
+-----------------------
+                       
+(1 row)
+
+step wait_before_lock: <... completed>
+step check1: 
+	INSERT INTO relfilenodes(node)
+	SELECT relfilenode FROM pg_class WHERE relname='repack_deadlock';
+
+	SELECT count(DISTINCT node) FROM relfilenodes;
+
+	SELECT i, j FROM repack_deadlock ORDER BY i, j;
+
+	INSERT INTO data_s1(i, j)
+	SELECT i, j FROM repack_deadlock;
+
+	SELECT count(*)
+	FROM data_s1 d1 FULL JOIN data_s2 d2 USING (i, j)
+	WHERE d1.i ISNULL OR d2.i ISNULL;
+
+count
+-----
+    1
+(1 row)
+
+i|j
+-+-
+1|1
+2|2
+3|3
+4|4
+(4 rows)
+
+count
+-----
+    4
+(1 row)
+
+injection_points_detach
+-----------------------
+                       
+(1 row)
+
diff --git a/src/test/modules/injection_points/meson.build b/src/test/modules/injection_points/meson.build
index a414abb924b..1cd88d6db65 100644
--- a/src/test/modules/injection_points/meson.build
+++ b/src/test/modules/injection_points/meson.build
@@ -46,6 +46,7 @@ tests += {
       'basic',
       'inplace',
       'repack',
+      'repack_deadlock',
       'repack_toast',
       'syscache-update-pruned',
       'heap_lock_update',
diff --git a/src/test/modules/injection_points/specs/repack_deadlock.spec b/src/test/modules/injection_points/specs/repack_deadlock.spec
new file mode 100644
index 00000000000..9d23a6588c2
--- /dev/null
+++ b/src/test/modules/injection_points/specs/repack_deadlock.spec
@@ -0,0 +1,83 @@
+# Test REPACK with a concurrent transaction that would cause a deadlock
+setup
+{
+	CREATE EXTENSION injection_points;
+
+	CREATE TABLE repack_deadlock(i int PRIMARY KEY, j int);
+	INSERT INTO repack_deadlock(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4);
+
+	CREATE TABLE relfilenodes(node oid);
+
+	CREATE TABLE data_s1(i int, j int);
+	CREATE TABLE data_s2(i int, j int);
+}
+
+teardown
+{
+	DROP TABLE repack_deadlock;
+	DROP EXTENSION injection_points;
+
+	DROP TABLE relfilenodes;
+	DROP TABLE data_s1;
+	DROP TABLE data_s2;
+}
+
+session s1
+setup
+{
+	SELECT injection_points_set_local();
+	SELECT injection_points_attach('repack-concurrently-before-lock', 'wait');
+}
+# Perform the initial load and wait for s2 to do some data changes.
+step wait_before_lock
+{
+	REPACK (CONCURRENTLY) repack_deadlock USING INDEX repack_deadlock_pkey;
+}
+# Check the table from the perspective of s1.
+#
+# Besides the contents, we also check that relfilenode has changed.
+
+# Have each session write the contents into a table and use FULL JOIN to check
+# if the outputs are identical.
+step check1
+{
+	INSERT INTO relfilenodes(node)
+	SELECT relfilenode FROM pg_class WHERE relname='repack_deadlock';
+
+	SELECT count(DISTINCT node) FROM relfilenodes;
+
+	SELECT i, j FROM repack_deadlock ORDER BY i, j;
+
+	INSERT INTO data_s1(i, j)
+	SELECT i, j FROM repack_deadlock;
+
+	SELECT count(*)
+	FROM data_s1 d1 FULL JOIN data_s2 d2 USING (i, j)
+	WHERE d1.i ISNULL OR d2.i ISNULL;
+}
+teardown
+{
+	SELECT injection_points_detach('repack-concurrently-before-lock');
+}
+
+session s2
+# Change the existing data. UPDATE changes both key and non-key columns. Also
+# update one row twice to test whether tuple version generated by this session
+# can be found.
+step add_column
+{
+	alter table repack_deadlock add column noise text;
+}
+
+step wakeup_before_lock
+{
+	SELECT injection_points_wakeup('repack-concurrently-before-lock');
+}
+
+# Test if data changes introduced while one session is performing REPACK
+# CONCURRENTLY find their way into the table.
+permutation
+	wait_before_lock
+	add_column
+	wakeup_before_lock
+	check1
-- 
2.47.3


--fot5plqmb33ey33c--





^ permalink  raw  reply  [nested|flat] 89+ messages in thread

* [PATCH v44b 4/4] hack deadlock detector
@ 2026-03-25 19:35  Álvaro Herrera <[email protected]>
  0 siblings, 0 replies; 89+ messages in thread

From: Álvaro Herrera @ 2026-03-25 19:35 UTC (permalink / raw)

---
 src/backend/commands/cluster.c                | 10 +++
 src/backend/storage/lmgr/deadlock.c           | 12 +++
 src/include/storage/proc.h                    |  1 +
 src/test/modules/injection_points/Makefile    |  1 +
 .../expected/repack_deadlock.out              | 63 ++++++++++++++
 src/test/modules/injection_points/meson.build |  1 +
 .../specs/repack_deadlock.spec                | 83 +++++++++++++++++++
 7 files changed, 171 insertions(+)
 create mode 100644 src/test/modules/injection_points/expected/repack_deadlock.out
 create mode 100644 src/test/modules/injection_points/specs/repack_deadlock.spec

diff --git a/src/backend/commands/cluster.c b/src/backend/commands/cluster.c
index 2c3058ba10d..d5b1dfbff69 100644
--- a/src/backend/commands/cluster.c
+++ b/src/backend/commands/cluster.c
@@ -372,6 +372,16 @@ ExecRepack(ParseState *pstate, RepackStmt *stmt, bool isTopLevel)
 	/* Determine the lock mode to use. */
 	lockmode = RepackLockLevel((params.options & CLUOPT_CONCURRENT) != 0);
 
+	/*
+	 * If in concurrent mode, set the PROC_IN_CONCURRENT_REPACK flag.  This
+	 * makes the deadlock checker cause anyone that would conflict with us
+	 * to error out.  It's important to set this flag ahead of actually locking
+	 * the relation; it won't of course affect anyone until we do have a lock
+	 * that others can conflict with.
+	 */
+	if ((params.options & CLUOPT_CONCURRENT) != 0)
+		MyProc->statusFlags |= PROC_IN_CONCURRENT_REPACK;
+
 	/*
 	 * If a single relation is specified, process it and we're done ... unless
 	 * the relation is a partitioned table, in which case we fall through.
diff --git a/src/backend/storage/lmgr/deadlock.c b/src/backend/storage/lmgr/deadlock.c
index b8962d875b6..10266e384aa 100644
--- a/src/backend/storage/lmgr/deadlock.c
+++ b/src/backend/storage/lmgr/deadlock.c
@@ -620,6 +620,18 @@ FindLockCycleRecurseMember(PGPROC *checkProc,
 						proc->statusFlags & PROC_IS_AUTOVACUUM)
 						blocking_autovacuum_proc = proc;
 
+					/*
+					 * If we note that we're blocked by some process running
+					 * REPACK (CONCURRENTLY), just fail.  That process is
+					 * going to upgrade its lock at some point, and it would
+					 * be inappropriate for any other process to cause that
+					 * to fail.
+					 */
+					if (checkProc == MyProc &&
+						proc->statusFlags & PROC_IN_CONCURRENT_REPACK)
+						/* not a serious error message proposal */
+						elog(ERROR, "oops, we're blocked by REPACK, time to die");
+
 					/* We're done looking at this proclock */
 					break;
 				}
diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h
index 1dad125706e..4e340ead455 100644
--- a/src/include/storage/proc.h
+++ b/src/include/storage/proc.h
@@ -69,6 +69,7 @@ struct XidCache
 #define		PROC_AFFECTS_ALL_HORIZONS	0x20	/* this proc's xmin must be
 												 * included in vacuum horizons
 												 * in all databases */
+#define		PROC_IN_CONCURRENT_REPACK	0x40	/* REPACK (CONCURRENTLY) */
 
 /* flags reset at EOXact */
 #define		PROC_VACUUM_STATE_MASK \
diff --git a/src/test/modules/injection_points/Makefile b/src/test/modules/injection_points/Makefile
index 2cd7d87c533..f7663859fe2 100644
--- a/src/test/modules/injection_points/Makefile
+++ b/src/test/modules/injection_points/Makefile
@@ -15,6 +15,7 @@ REGRESS_OPTS = --dlpath=$(top_builddir)/src/test/regress
 ISOLATION = basic \
 	    inplace \
 	    repack \
+	    repack_deadlock \
 	    repack_toast \
 	    syscache-update-pruned \
 	    heap_lock_update
diff --git a/src/test/modules/injection_points/expected/repack_deadlock.out b/src/test/modules/injection_points/expected/repack_deadlock.out
new file mode 100644
index 00000000000..42505d10933
--- /dev/null
+++ b/src/test/modules/injection_points/expected/repack_deadlock.out
@@ -0,0 +1,63 @@
+Parsed test spec with 2 sessions
+
+starting permutation: wait_before_lock add_column wakeup_before_lock check1
+injection_points_attach
+-----------------------
+                       
+(1 row)
+
+step wait_before_lock: 
+	REPACK (CONCURRENTLY) repack_deadlock USING INDEX repack_deadlock_pkey;
+ <waiting ...>
+step add_column: 
+	alter table repack_deadlock add column noise text;
+ <waiting ...>
+step add_column: <... completed>
+ERROR:  oops, we're blocked by REPACK, time to die
+step wakeup_before_lock: 
+	SELECT injection_points_wakeup('repack-concurrently-before-lock');
+
+injection_points_wakeup
+-----------------------
+                       
+(1 row)
+
+step wait_before_lock: <... completed>
+step check1: 
+	INSERT INTO relfilenodes(node)
+	SELECT relfilenode FROM pg_class WHERE relname='repack_deadlock';
+
+	SELECT count(DISTINCT node) FROM relfilenodes;
+
+	SELECT i, j FROM repack_deadlock ORDER BY i, j;
+
+	INSERT INTO data_s1(i, j)
+	SELECT i, j FROM repack_deadlock;
+
+	SELECT count(*)
+	FROM data_s1 d1 FULL JOIN data_s2 d2 USING (i, j)
+	WHERE d1.i ISNULL OR d2.i ISNULL;
+
+count
+-----
+    1
+(1 row)
+
+i|j
+-+-
+1|1
+2|2
+3|3
+4|4
+(4 rows)
+
+count
+-----
+    4
+(1 row)
+
+injection_points_detach
+-----------------------
+                       
+(1 row)
+
diff --git a/src/test/modules/injection_points/meson.build b/src/test/modules/injection_points/meson.build
index a414abb924b..1cd88d6db65 100644
--- a/src/test/modules/injection_points/meson.build
+++ b/src/test/modules/injection_points/meson.build
@@ -46,6 +46,7 @@ tests += {
       'basic',
       'inplace',
       'repack',
+      'repack_deadlock',
       'repack_toast',
       'syscache-update-pruned',
       'heap_lock_update',
diff --git a/src/test/modules/injection_points/specs/repack_deadlock.spec b/src/test/modules/injection_points/specs/repack_deadlock.spec
new file mode 100644
index 00000000000..9d23a6588c2
--- /dev/null
+++ b/src/test/modules/injection_points/specs/repack_deadlock.spec
@@ -0,0 +1,83 @@
+# Test REPACK with a concurrent transaction that would cause a deadlock
+setup
+{
+	CREATE EXTENSION injection_points;
+
+	CREATE TABLE repack_deadlock(i int PRIMARY KEY, j int);
+	INSERT INTO repack_deadlock(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4);
+
+	CREATE TABLE relfilenodes(node oid);
+
+	CREATE TABLE data_s1(i int, j int);
+	CREATE TABLE data_s2(i int, j int);
+}
+
+teardown
+{
+	DROP TABLE repack_deadlock;
+	DROP EXTENSION injection_points;
+
+	DROP TABLE relfilenodes;
+	DROP TABLE data_s1;
+	DROP TABLE data_s2;
+}
+
+session s1
+setup
+{
+	SELECT injection_points_set_local();
+	SELECT injection_points_attach('repack-concurrently-before-lock', 'wait');
+}
+# Perform the initial load and wait for s2 to do some data changes.
+step wait_before_lock
+{
+	REPACK (CONCURRENTLY) repack_deadlock USING INDEX repack_deadlock_pkey;
+}
+# Check the table from the perspective of s1.
+#
+# Besides the contents, we also check that relfilenode has changed.
+
+# Have each session write the contents into a table and use FULL JOIN to check
+# if the outputs are identical.
+step check1
+{
+	INSERT INTO relfilenodes(node)
+	SELECT relfilenode FROM pg_class WHERE relname='repack_deadlock';
+
+	SELECT count(DISTINCT node) FROM relfilenodes;
+
+	SELECT i, j FROM repack_deadlock ORDER BY i, j;
+
+	INSERT INTO data_s1(i, j)
+	SELECT i, j FROM repack_deadlock;
+
+	SELECT count(*)
+	FROM data_s1 d1 FULL JOIN data_s2 d2 USING (i, j)
+	WHERE d1.i ISNULL OR d2.i ISNULL;
+}
+teardown
+{
+	SELECT injection_points_detach('repack-concurrently-before-lock');
+}
+
+session s2
+# Change the existing data. UPDATE changes both key and non-key columns. Also
+# update one row twice to test whether tuple version generated by this session
+# can be found.
+step add_column
+{
+	alter table repack_deadlock add column noise text;
+}
+
+step wakeup_before_lock
+{
+	SELECT injection_points_wakeup('repack-concurrently-before-lock');
+}
+
+# Test if data changes introduced while one session is performing REPACK
+# CONCURRENTLY find their way into the table.
+permutation
+	wait_before_lock
+	add_column
+	wakeup_before_lock
+	check1
-- 
2.47.3


--fot5plqmb33ey33c--





^ permalink  raw  reply  [nested|flat] 89+ messages in thread

* [PATCH v44b 4/4] hack deadlock detector
@ 2026-03-25 19:35  Álvaro Herrera <[email protected]>
  0 siblings, 0 replies; 89+ messages in thread

From: Álvaro Herrera @ 2026-03-25 19:35 UTC (permalink / raw)

---
 src/backend/commands/cluster.c                | 10 +++
 src/backend/storage/lmgr/deadlock.c           | 12 +++
 src/include/storage/proc.h                    |  1 +
 src/test/modules/injection_points/Makefile    |  1 +
 .../expected/repack_deadlock.out              | 63 ++++++++++++++
 src/test/modules/injection_points/meson.build |  1 +
 .../specs/repack_deadlock.spec                | 83 +++++++++++++++++++
 7 files changed, 171 insertions(+)
 create mode 100644 src/test/modules/injection_points/expected/repack_deadlock.out
 create mode 100644 src/test/modules/injection_points/specs/repack_deadlock.spec

diff --git a/src/backend/commands/cluster.c b/src/backend/commands/cluster.c
index 2c3058ba10d..d5b1dfbff69 100644
--- a/src/backend/commands/cluster.c
+++ b/src/backend/commands/cluster.c
@@ -372,6 +372,16 @@ ExecRepack(ParseState *pstate, RepackStmt *stmt, bool isTopLevel)
 	/* Determine the lock mode to use. */
 	lockmode = RepackLockLevel((params.options & CLUOPT_CONCURRENT) != 0);
 
+	/*
+	 * If in concurrent mode, set the PROC_IN_CONCURRENT_REPACK flag.  This
+	 * makes the deadlock checker cause anyone that would conflict with us
+	 * to error out.  It's important to set this flag ahead of actually locking
+	 * the relation; it won't of course affect anyone until we do have a lock
+	 * that others can conflict with.
+	 */
+	if ((params.options & CLUOPT_CONCURRENT) != 0)
+		MyProc->statusFlags |= PROC_IN_CONCURRENT_REPACK;
+
 	/*
 	 * If a single relation is specified, process it and we're done ... unless
 	 * the relation is a partitioned table, in which case we fall through.
diff --git a/src/backend/storage/lmgr/deadlock.c b/src/backend/storage/lmgr/deadlock.c
index b8962d875b6..10266e384aa 100644
--- a/src/backend/storage/lmgr/deadlock.c
+++ b/src/backend/storage/lmgr/deadlock.c
@@ -620,6 +620,18 @@ FindLockCycleRecurseMember(PGPROC *checkProc,
 						proc->statusFlags & PROC_IS_AUTOVACUUM)
 						blocking_autovacuum_proc = proc;
 
+					/*
+					 * If we note that we're blocked by some process running
+					 * REPACK (CONCURRENTLY), just fail.  That process is
+					 * going to upgrade its lock at some point, and it would
+					 * be inappropriate for any other process to cause that
+					 * to fail.
+					 */
+					if (checkProc == MyProc &&
+						proc->statusFlags & PROC_IN_CONCURRENT_REPACK)
+						/* not a serious error message proposal */
+						elog(ERROR, "oops, we're blocked by REPACK, time to die");
+
 					/* We're done looking at this proclock */
 					break;
 				}
diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h
index 1dad125706e..4e340ead455 100644
--- a/src/include/storage/proc.h
+++ b/src/include/storage/proc.h
@@ -69,6 +69,7 @@ struct XidCache
 #define		PROC_AFFECTS_ALL_HORIZONS	0x20	/* this proc's xmin must be
 												 * included in vacuum horizons
 												 * in all databases */
+#define		PROC_IN_CONCURRENT_REPACK	0x40	/* REPACK (CONCURRENTLY) */
 
 /* flags reset at EOXact */
 #define		PROC_VACUUM_STATE_MASK \
diff --git a/src/test/modules/injection_points/Makefile b/src/test/modules/injection_points/Makefile
index 2cd7d87c533..f7663859fe2 100644
--- a/src/test/modules/injection_points/Makefile
+++ b/src/test/modules/injection_points/Makefile
@@ -15,6 +15,7 @@ REGRESS_OPTS = --dlpath=$(top_builddir)/src/test/regress
 ISOLATION = basic \
 	    inplace \
 	    repack \
+	    repack_deadlock \
 	    repack_toast \
 	    syscache-update-pruned \
 	    heap_lock_update
diff --git a/src/test/modules/injection_points/expected/repack_deadlock.out b/src/test/modules/injection_points/expected/repack_deadlock.out
new file mode 100644
index 00000000000..42505d10933
--- /dev/null
+++ b/src/test/modules/injection_points/expected/repack_deadlock.out
@@ -0,0 +1,63 @@
+Parsed test spec with 2 sessions
+
+starting permutation: wait_before_lock add_column wakeup_before_lock check1
+injection_points_attach
+-----------------------
+                       
+(1 row)
+
+step wait_before_lock: 
+	REPACK (CONCURRENTLY) repack_deadlock USING INDEX repack_deadlock_pkey;
+ <waiting ...>
+step add_column: 
+	alter table repack_deadlock add column noise text;
+ <waiting ...>
+step add_column: <... completed>
+ERROR:  oops, we're blocked by REPACK, time to die
+step wakeup_before_lock: 
+	SELECT injection_points_wakeup('repack-concurrently-before-lock');
+
+injection_points_wakeup
+-----------------------
+                       
+(1 row)
+
+step wait_before_lock: <... completed>
+step check1: 
+	INSERT INTO relfilenodes(node)
+	SELECT relfilenode FROM pg_class WHERE relname='repack_deadlock';
+
+	SELECT count(DISTINCT node) FROM relfilenodes;
+
+	SELECT i, j FROM repack_deadlock ORDER BY i, j;
+
+	INSERT INTO data_s1(i, j)
+	SELECT i, j FROM repack_deadlock;
+
+	SELECT count(*)
+	FROM data_s1 d1 FULL JOIN data_s2 d2 USING (i, j)
+	WHERE d1.i ISNULL OR d2.i ISNULL;
+
+count
+-----
+    1
+(1 row)
+
+i|j
+-+-
+1|1
+2|2
+3|3
+4|4
+(4 rows)
+
+count
+-----
+    4
+(1 row)
+
+injection_points_detach
+-----------------------
+                       
+(1 row)
+
diff --git a/src/test/modules/injection_points/meson.build b/src/test/modules/injection_points/meson.build
index a414abb924b..1cd88d6db65 100644
--- a/src/test/modules/injection_points/meson.build
+++ b/src/test/modules/injection_points/meson.build
@@ -46,6 +46,7 @@ tests += {
       'basic',
       'inplace',
       'repack',
+      'repack_deadlock',
       'repack_toast',
       'syscache-update-pruned',
       'heap_lock_update',
diff --git a/src/test/modules/injection_points/specs/repack_deadlock.spec b/src/test/modules/injection_points/specs/repack_deadlock.spec
new file mode 100644
index 00000000000..9d23a6588c2
--- /dev/null
+++ b/src/test/modules/injection_points/specs/repack_deadlock.spec
@@ -0,0 +1,83 @@
+# Test REPACK with a concurrent transaction that would cause a deadlock
+setup
+{
+	CREATE EXTENSION injection_points;
+
+	CREATE TABLE repack_deadlock(i int PRIMARY KEY, j int);
+	INSERT INTO repack_deadlock(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4);
+
+	CREATE TABLE relfilenodes(node oid);
+
+	CREATE TABLE data_s1(i int, j int);
+	CREATE TABLE data_s2(i int, j int);
+}
+
+teardown
+{
+	DROP TABLE repack_deadlock;
+	DROP EXTENSION injection_points;
+
+	DROP TABLE relfilenodes;
+	DROP TABLE data_s1;
+	DROP TABLE data_s2;
+}
+
+session s1
+setup
+{
+	SELECT injection_points_set_local();
+	SELECT injection_points_attach('repack-concurrently-before-lock', 'wait');
+}
+# Perform the initial load and wait for s2 to do some data changes.
+step wait_before_lock
+{
+	REPACK (CONCURRENTLY) repack_deadlock USING INDEX repack_deadlock_pkey;
+}
+# Check the table from the perspective of s1.
+#
+# Besides the contents, we also check that relfilenode has changed.
+
+# Have each session write the contents into a table and use FULL JOIN to check
+# if the outputs are identical.
+step check1
+{
+	INSERT INTO relfilenodes(node)
+	SELECT relfilenode FROM pg_class WHERE relname='repack_deadlock';
+
+	SELECT count(DISTINCT node) FROM relfilenodes;
+
+	SELECT i, j FROM repack_deadlock ORDER BY i, j;
+
+	INSERT INTO data_s1(i, j)
+	SELECT i, j FROM repack_deadlock;
+
+	SELECT count(*)
+	FROM data_s1 d1 FULL JOIN data_s2 d2 USING (i, j)
+	WHERE d1.i ISNULL OR d2.i ISNULL;
+}
+teardown
+{
+	SELECT injection_points_detach('repack-concurrently-before-lock');
+}
+
+session s2
+# Change the existing data. UPDATE changes both key and non-key columns. Also
+# update one row twice to test whether tuple version generated by this session
+# can be found.
+step add_column
+{
+	alter table repack_deadlock add column noise text;
+}
+
+step wakeup_before_lock
+{
+	SELECT injection_points_wakeup('repack-concurrently-before-lock');
+}
+
+# Test if data changes introduced while one session is performing REPACK
+# CONCURRENTLY find their way into the table.
+permutation
+	wait_before_lock
+	add_column
+	wakeup_before_lock
+	check1
-- 
2.47.3


--fot5plqmb33ey33c--





^ permalink  raw  reply  [nested|flat] 89+ messages in thread

* [PATCH v44b 4/4] hack deadlock detector
@ 2026-03-25 19:35  Álvaro Herrera <[email protected]>
  0 siblings, 0 replies; 89+ messages in thread

From: Álvaro Herrera @ 2026-03-25 19:35 UTC (permalink / raw)

---
 src/backend/commands/cluster.c                | 10 +++
 src/backend/storage/lmgr/deadlock.c           | 12 +++
 src/include/storage/proc.h                    |  1 +
 src/test/modules/injection_points/Makefile    |  1 +
 .../expected/repack_deadlock.out              | 63 ++++++++++++++
 src/test/modules/injection_points/meson.build |  1 +
 .../specs/repack_deadlock.spec                | 83 +++++++++++++++++++
 7 files changed, 171 insertions(+)
 create mode 100644 src/test/modules/injection_points/expected/repack_deadlock.out
 create mode 100644 src/test/modules/injection_points/specs/repack_deadlock.spec

diff --git a/src/backend/commands/cluster.c b/src/backend/commands/cluster.c
index 2c3058ba10d..d5b1dfbff69 100644
--- a/src/backend/commands/cluster.c
+++ b/src/backend/commands/cluster.c
@@ -372,6 +372,16 @@ ExecRepack(ParseState *pstate, RepackStmt *stmt, bool isTopLevel)
 	/* Determine the lock mode to use. */
 	lockmode = RepackLockLevel((params.options & CLUOPT_CONCURRENT) != 0);
 
+	/*
+	 * If in concurrent mode, set the PROC_IN_CONCURRENT_REPACK flag.  This
+	 * makes the deadlock checker cause anyone that would conflict with us
+	 * to error out.  It's important to set this flag ahead of actually locking
+	 * the relation; it won't of course affect anyone until we do have a lock
+	 * that others can conflict with.
+	 */
+	if ((params.options & CLUOPT_CONCURRENT) != 0)
+		MyProc->statusFlags |= PROC_IN_CONCURRENT_REPACK;
+
 	/*
 	 * If a single relation is specified, process it and we're done ... unless
 	 * the relation is a partitioned table, in which case we fall through.
diff --git a/src/backend/storage/lmgr/deadlock.c b/src/backend/storage/lmgr/deadlock.c
index b8962d875b6..10266e384aa 100644
--- a/src/backend/storage/lmgr/deadlock.c
+++ b/src/backend/storage/lmgr/deadlock.c
@@ -620,6 +620,18 @@ FindLockCycleRecurseMember(PGPROC *checkProc,
 						proc->statusFlags & PROC_IS_AUTOVACUUM)
 						blocking_autovacuum_proc = proc;
 
+					/*
+					 * If we note that we're blocked by some process running
+					 * REPACK (CONCURRENTLY), just fail.  That process is
+					 * going to upgrade its lock at some point, and it would
+					 * be inappropriate for any other process to cause that
+					 * to fail.
+					 */
+					if (checkProc == MyProc &&
+						proc->statusFlags & PROC_IN_CONCURRENT_REPACK)
+						/* not a serious error message proposal */
+						elog(ERROR, "oops, we're blocked by REPACK, time to die");
+
 					/* We're done looking at this proclock */
 					break;
 				}
diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h
index 1dad125706e..4e340ead455 100644
--- a/src/include/storage/proc.h
+++ b/src/include/storage/proc.h
@@ -69,6 +69,7 @@ struct XidCache
 #define		PROC_AFFECTS_ALL_HORIZONS	0x20	/* this proc's xmin must be
 												 * included in vacuum horizons
 												 * in all databases */
+#define		PROC_IN_CONCURRENT_REPACK	0x40	/* REPACK (CONCURRENTLY) */
 
 /* flags reset at EOXact */
 #define		PROC_VACUUM_STATE_MASK \
diff --git a/src/test/modules/injection_points/Makefile b/src/test/modules/injection_points/Makefile
index 2cd7d87c533..f7663859fe2 100644
--- a/src/test/modules/injection_points/Makefile
+++ b/src/test/modules/injection_points/Makefile
@@ -15,6 +15,7 @@ REGRESS_OPTS = --dlpath=$(top_builddir)/src/test/regress
 ISOLATION = basic \
 	    inplace \
 	    repack \
+	    repack_deadlock \
 	    repack_toast \
 	    syscache-update-pruned \
 	    heap_lock_update
diff --git a/src/test/modules/injection_points/expected/repack_deadlock.out b/src/test/modules/injection_points/expected/repack_deadlock.out
new file mode 100644
index 00000000000..42505d10933
--- /dev/null
+++ b/src/test/modules/injection_points/expected/repack_deadlock.out
@@ -0,0 +1,63 @@
+Parsed test spec with 2 sessions
+
+starting permutation: wait_before_lock add_column wakeup_before_lock check1
+injection_points_attach
+-----------------------
+                       
+(1 row)
+
+step wait_before_lock: 
+	REPACK (CONCURRENTLY) repack_deadlock USING INDEX repack_deadlock_pkey;
+ <waiting ...>
+step add_column: 
+	alter table repack_deadlock add column noise text;
+ <waiting ...>
+step add_column: <... completed>
+ERROR:  oops, we're blocked by REPACK, time to die
+step wakeup_before_lock: 
+	SELECT injection_points_wakeup('repack-concurrently-before-lock');
+
+injection_points_wakeup
+-----------------------
+                       
+(1 row)
+
+step wait_before_lock: <... completed>
+step check1: 
+	INSERT INTO relfilenodes(node)
+	SELECT relfilenode FROM pg_class WHERE relname='repack_deadlock';
+
+	SELECT count(DISTINCT node) FROM relfilenodes;
+
+	SELECT i, j FROM repack_deadlock ORDER BY i, j;
+
+	INSERT INTO data_s1(i, j)
+	SELECT i, j FROM repack_deadlock;
+
+	SELECT count(*)
+	FROM data_s1 d1 FULL JOIN data_s2 d2 USING (i, j)
+	WHERE d1.i ISNULL OR d2.i ISNULL;
+
+count
+-----
+    1
+(1 row)
+
+i|j
+-+-
+1|1
+2|2
+3|3
+4|4
+(4 rows)
+
+count
+-----
+    4
+(1 row)
+
+injection_points_detach
+-----------------------
+                       
+(1 row)
+
diff --git a/src/test/modules/injection_points/meson.build b/src/test/modules/injection_points/meson.build
index a414abb924b..1cd88d6db65 100644
--- a/src/test/modules/injection_points/meson.build
+++ b/src/test/modules/injection_points/meson.build
@@ -46,6 +46,7 @@ tests += {
       'basic',
       'inplace',
       'repack',
+      'repack_deadlock',
       'repack_toast',
       'syscache-update-pruned',
       'heap_lock_update',
diff --git a/src/test/modules/injection_points/specs/repack_deadlock.spec b/src/test/modules/injection_points/specs/repack_deadlock.spec
new file mode 100644
index 00000000000..9d23a6588c2
--- /dev/null
+++ b/src/test/modules/injection_points/specs/repack_deadlock.spec
@@ -0,0 +1,83 @@
+# Test REPACK with a concurrent transaction that would cause a deadlock
+setup
+{
+	CREATE EXTENSION injection_points;
+
+	CREATE TABLE repack_deadlock(i int PRIMARY KEY, j int);
+	INSERT INTO repack_deadlock(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4);
+
+	CREATE TABLE relfilenodes(node oid);
+
+	CREATE TABLE data_s1(i int, j int);
+	CREATE TABLE data_s2(i int, j int);
+}
+
+teardown
+{
+	DROP TABLE repack_deadlock;
+	DROP EXTENSION injection_points;
+
+	DROP TABLE relfilenodes;
+	DROP TABLE data_s1;
+	DROP TABLE data_s2;
+}
+
+session s1
+setup
+{
+	SELECT injection_points_set_local();
+	SELECT injection_points_attach('repack-concurrently-before-lock', 'wait');
+}
+# Perform the initial load and wait for s2 to do some data changes.
+step wait_before_lock
+{
+	REPACK (CONCURRENTLY) repack_deadlock USING INDEX repack_deadlock_pkey;
+}
+# Check the table from the perspective of s1.
+#
+# Besides the contents, we also check that relfilenode has changed.
+
+# Have each session write the contents into a table and use FULL JOIN to check
+# if the outputs are identical.
+step check1
+{
+	INSERT INTO relfilenodes(node)
+	SELECT relfilenode FROM pg_class WHERE relname='repack_deadlock';
+
+	SELECT count(DISTINCT node) FROM relfilenodes;
+
+	SELECT i, j FROM repack_deadlock ORDER BY i, j;
+
+	INSERT INTO data_s1(i, j)
+	SELECT i, j FROM repack_deadlock;
+
+	SELECT count(*)
+	FROM data_s1 d1 FULL JOIN data_s2 d2 USING (i, j)
+	WHERE d1.i ISNULL OR d2.i ISNULL;
+}
+teardown
+{
+	SELECT injection_points_detach('repack-concurrently-before-lock');
+}
+
+session s2
+# Change the existing data. UPDATE changes both key and non-key columns. Also
+# update one row twice to test whether tuple version generated by this session
+# can be found.
+step add_column
+{
+	alter table repack_deadlock add column noise text;
+}
+
+step wakeup_before_lock
+{
+	SELECT injection_points_wakeup('repack-concurrently-before-lock');
+}
+
+# Test if data changes introduced while one session is performing REPACK
+# CONCURRENTLY find their way into the table.
+permutation
+	wait_before_lock
+	add_column
+	wakeup_before_lock
+	check1
-- 
2.47.3


--fot5plqmb33ey33c--





^ permalink  raw  reply  [nested|flat] 89+ messages in thread

* [PATCH v44b 4/4] hack deadlock detector
@ 2026-03-25 19:35  Álvaro Herrera <[email protected]>
  0 siblings, 0 replies; 89+ messages in thread

From: Álvaro Herrera @ 2026-03-25 19:35 UTC (permalink / raw)

---
 src/backend/commands/cluster.c                | 10 +++
 src/backend/storage/lmgr/deadlock.c           | 12 +++
 src/include/storage/proc.h                    |  1 +
 src/test/modules/injection_points/Makefile    |  1 +
 .../expected/repack_deadlock.out              | 63 ++++++++++++++
 src/test/modules/injection_points/meson.build |  1 +
 .../specs/repack_deadlock.spec                | 83 +++++++++++++++++++
 7 files changed, 171 insertions(+)
 create mode 100644 src/test/modules/injection_points/expected/repack_deadlock.out
 create mode 100644 src/test/modules/injection_points/specs/repack_deadlock.spec

diff --git a/src/backend/commands/cluster.c b/src/backend/commands/cluster.c
index 2c3058ba10d..d5b1dfbff69 100644
--- a/src/backend/commands/cluster.c
+++ b/src/backend/commands/cluster.c
@@ -372,6 +372,16 @@ ExecRepack(ParseState *pstate, RepackStmt *stmt, bool isTopLevel)
 	/* Determine the lock mode to use. */
 	lockmode = RepackLockLevel((params.options & CLUOPT_CONCURRENT) != 0);
 
+	/*
+	 * If in concurrent mode, set the PROC_IN_CONCURRENT_REPACK flag.  This
+	 * makes the deadlock checker cause anyone that would conflict with us
+	 * to error out.  It's important to set this flag ahead of actually locking
+	 * the relation; it won't of course affect anyone until we do have a lock
+	 * that others can conflict with.
+	 */
+	if ((params.options & CLUOPT_CONCURRENT) != 0)
+		MyProc->statusFlags |= PROC_IN_CONCURRENT_REPACK;
+
 	/*
 	 * If a single relation is specified, process it and we're done ... unless
 	 * the relation is a partitioned table, in which case we fall through.
diff --git a/src/backend/storage/lmgr/deadlock.c b/src/backend/storage/lmgr/deadlock.c
index b8962d875b6..10266e384aa 100644
--- a/src/backend/storage/lmgr/deadlock.c
+++ b/src/backend/storage/lmgr/deadlock.c
@@ -620,6 +620,18 @@ FindLockCycleRecurseMember(PGPROC *checkProc,
 						proc->statusFlags & PROC_IS_AUTOVACUUM)
 						blocking_autovacuum_proc = proc;
 
+					/*
+					 * If we note that we're blocked by some process running
+					 * REPACK (CONCURRENTLY), just fail.  That process is
+					 * going to upgrade its lock at some point, and it would
+					 * be inappropriate for any other process to cause that
+					 * to fail.
+					 */
+					if (checkProc == MyProc &&
+						proc->statusFlags & PROC_IN_CONCURRENT_REPACK)
+						/* not a serious error message proposal */
+						elog(ERROR, "oops, we're blocked by REPACK, time to die");
+
 					/* We're done looking at this proclock */
 					break;
 				}
diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h
index 1dad125706e..4e340ead455 100644
--- a/src/include/storage/proc.h
+++ b/src/include/storage/proc.h
@@ -69,6 +69,7 @@ struct XidCache
 #define		PROC_AFFECTS_ALL_HORIZONS	0x20	/* this proc's xmin must be
 												 * included in vacuum horizons
 												 * in all databases */
+#define		PROC_IN_CONCURRENT_REPACK	0x40	/* REPACK (CONCURRENTLY) */
 
 /* flags reset at EOXact */
 #define		PROC_VACUUM_STATE_MASK \
diff --git a/src/test/modules/injection_points/Makefile b/src/test/modules/injection_points/Makefile
index 2cd7d87c533..f7663859fe2 100644
--- a/src/test/modules/injection_points/Makefile
+++ b/src/test/modules/injection_points/Makefile
@@ -15,6 +15,7 @@ REGRESS_OPTS = --dlpath=$(top_builddir)/src/test/regress
 ISOLATION = basic \
 	    inplace \
 	    repack \
+	    repack_deadlock \
 	    repack_toast \
 	    syscache-update-pruned \
 	    heap_lock_update
diff --git a/src/test/modules/injection_points/expected/repack_deadlock.out b/src/test/modules/injection_points/expected/repack_deadlock.out
new file mode 100644
index 00000000000..42505d10933
--- /dev/null
+++ b/src/test/modules/injection_points/expected/repack_deadlock.out
@@ -0,0 +1,63 @@
+Parsed test spec with 2 sessions
+
+starting permutation: wait_before_lock add_column wakeup_before_lock check1
+injection_points_attach
+-----------------------
+                       
+(1 row)
+
+step wait_before_lock: 
+	REPACK (CONCURRENTLY) repack_deadlock USING INDEX repack_deadlock_pkey;
+ <waiting ...>
+step add_column: 
+	alter table repack_deadlock add column noise text;
+ <waiting ...>
+step add_column: <... completed>
+ERROR:  oops, we're blocked by REPACK, time to die
+step wakeup_before_lock: 
+	SELECT injection_points_wakeup('repack-concurrently-before-lock');
+
+injection_points_wakeup
+-----------------------
+                       
+(1 row)
+
+step wait_before_lock: <... completed>
+step check1: 
+	INSERT INTO relfilenodes(node)
+	SELECT relfilenode FROM pg_class WHERE relname='repack_deadlock';
+
+	SELECT count(DISTINCT node) FROM relfilenodes;
+
+	SELECT i, j FROM repack_deadlock ORDER BY i, j;
+
+	INSERT INTO data_s1(i, j)
+	SELECT i, j FROM repack_deadlock;
+
+	SELECT count(*)
+	FROM data_s1 d1 FULL JOIN data_s2 d2 USING (i, j)
+	WHERE d1.i ISNULL OR d2.i ISNULL;
+
+count
+-----
+    1
+(1 row)
+
+i|j
+-+-
+1|1
+2|2
+3|3
+4|4
+(4 rows)
+
+count
+-----
+    4
+(1 row)
+
+injection_points_detach
+-----------------------
+                       
+(1 row)
+
diff --git a/src/test/modules/injection_points/meson.build b/src/test/modules/injection_points/meson.build
index a414abb924b..1cd88d6db65 100644
--- a/src/test/modules/injection_points/meson.build
+++ b/src/test/modules/injection_points/meson.build
@@ -46,6 +46,7 @@ tests += {
       'basic',
       'inplace',
       'repack',
+      'repack_deadlock',
       'repack_toast',
       'syscache-update-pruned',
       'heap_lock_update',
diff --git a/src/test/modules/injection_points/specs/repack_deadlock.spec b/src/test/modules/injection_points/specs/repack_deadlock.spec
new file mode 100644
index 00000000000..9d23a6588c2
--- /dev/null
+++ b/src/test/modules/injection_points/specs/repack_deadlock.spec
@@ -0,0 +1,83 @@
+# Test REPACK with a concurrent transaction that would cause a deadlock
+setup
+{
+	CREATE EXTENSION injection_points;
+
+	CREATE TABLE repack_deadlock(i int PRIMARY KEY, j int);
+	INSERT INTO repack_deadlock(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4);
+
+	CREATE TABLE relfilenodes(node oid);
+
+	CREATE TABLE data_s1(i int, j int);
+	CREATE TABLE data_s2(i int, j int);
+}
+
+teardown
+{
+	DROP TABLE repack_deadlock;
+	DROP EXTENSION injection_points;
+
+	DROP TABLE relfilenodes;
+	DROP TABLE data_s1;
+	DROP TABLE data_s2;
+}
+
+session s1
+setup
+{
+	SELECT injection_points_set_local();
+	SELECT injection_points_attach('repack-concurrently-before-lock', 'wait');
+}
+# Perform the initial load and wait for s2 to do some data changes.
+step wait_before_lock
+{
+	REPACK (CONCURRENTLY) repack_deadlock USING INDEX repack_deadlock_pkey;
+}
+# Check the table from the perspective of s1.
+#
+# Besides the contents, we also check that relfilenode has changed.
+
+# Have each session write the contents into a table and use FULL JOIN to check
+# if the outputs are identical.
+step check1
+{
+	INSERT INTO relfilenodes(node)
+	SELECT relfilenode FROM pg_class WHERE relname='repack_deadlock';
+
+	SELECT count(DISTINCT node) FROM relfilenodes;
+
+	SELECT i, j FROM repack_deadlock ORDER BY i, j;
+
+	INSERT INTO data_s1(i, j)
+	SELECT i, j FROM repack_deadlock;
+
+	SELECT count(*)
+	FROM data_s1 d1 FULL JOIN data_s2 d2 USING (i, j)
+	WHERE d1.i ISNULL OR d2.i ISNULL;
+}
+teardown
+{
+	SELECT injection_points_detach('repack-concurrently-before-lock');
+}
+
+session s2
+# Change the existing data. UPDATE changes both key and non-key columns. Also
+# update one row twice to test whether tuple version generated by this session
+# can be found.
+step add_column
+{
+	alter table repack_deadlock add column noise text;
+}
+
+step wakeup_before_lock
+{
+	SELECT injection_points_wakeup('repack-concurrently-before-lock');
+}
+
+# Test if data changes introduced while one session is performing REPACK
+# CONCURRENTLY find their way into the table.
+permutation
+	wait_before_lock
+	add_column
+	wakeup_before_lock
+	check1
-- 
2.47.3


--fot5plqmb33ey33c--





^ permalink  raw  reply  [nested|flat] 89+ messages in thread

* [PATCH v44b 4/4] hack deadlock detector
@ 2026-03-25 19:35  Álvaro Herrera <[email protected]>
  0 siblings, 0 replies; 89+ messages in thread

From: Álvaro Herrera @ 2026-03-25 19:35 UTC (permalink / raw)

---
 src/backend/commands/cluster.c                | 10 +++
 src/backend/storage/lmgr/deadlock.c           | 12 +++
 src/include/storage/proc.h                    |  1 +
 src/test/modules/injection_points/Makefile    |  1 +
 .../expected/repack_deadlock.out              | 63 ++++++++++++++
 src/test/modules/injection_points/meson.build |  1 +
 .../specs/repack_deadlock.spec                | 83 +++++++++++++++++++
 7 files changed, 171 insertions(+)
 create mode 100644 src/test/modules/injection_points/expected/repack_deadlock.out
 create mode 100644 src/test/modules/injection_points/specs/repack_deadlock.spec

diff --git a/src/backend/commands/cluster.c b/src/backend/commands/cluster.c
index 2c3058ba10d..d5b1dfbff69 100644
--- a/src/backend/commands/cluster.c
+++ b/src/backend/commands/cluster.c
@@ -372,6 +372,16 @@ ExecRepack(ParseState *pstate, RepackStmt *stmt, bool isTopLevel)
 	/* Determine the lock mode to use. */
 	lockmode = RepackLockLevel((params.options & CLUOPT_CONCURRENT) != 0);
 
+	/*
+	 * If in concurrent mode, set the PROC_IN_CONCURRENT_REPACK flag.  This
+	 * makes the deadlock checker cause anyone that would conflict with us
+	 * to error out.  It's important to set this flag ahead of actually locking
+	 * the relation; it won't of course affect anyone until we do have a lock
+	 * that others can conflict with.
+	 */
+	if ((params.options & CLUOPT_CONCURRENT) != 0)
+		MyProc->statusFlags |= PROC_IN_CONCURRENT_REPACK;
+
 	/*
 	 * If a single relation is specified, process it and we're done ... unless
 	 * the relation is a partitioned table, in which case we fall through.
diff --git a/src/backend/storage/lmgr/deadlock.c b/src/backend/storage/lmgr/deadlock.c
index b8962d875b6..10266e384aa 100644
--- a/src/backend/storage/lmgr/deadlock.c
+++ b/src/backend/storage/lmgr/deadlock.c
@@ -620,6 +620,18 @@ FindLockCycleRecurseMember(PGPROC *checkProc,
 						proc->statusFlags & PROC_IS_AUTOVACUUM)
 						blocking_autovacuum_proc = proc;
 
+					/*
+					 * If we note that we're blocked by some process running
+					 * REPACK (CONCURRENTLY), just fail.  That process is
+					 * going to upgrade its lock at some point, and it would
+					 * be inappropriate for any other process to cause that
+					 * to fail.
+					 */
+					if (checkProc == MyProc &&
+						proc->statusFlags & PROC_IN_CONCURRENT_REPACK)
+						/* not a serious error message proposal */
+						elog(ERROR, "oops, we're blocked by REPACK, time to die");
+
 					/* We're done looking at this proclock */
 					break;
 				}
diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h
index 1dad125706e..4e340ead455 100644
--- a/src/include/storage/proc.h
+++ b/src/include/storage/proc.h
@@ -69,6 +69,7 @@ struct XidCache
 #define		PROC_AFFECTS_ALL_HORIZONS	0x20	/* this proc's xmin must be
 												 * included in vacuum horizons
 												 * in all databases */
+#define		PROC_IN_CONCURRENT_REPACK	0x40	/* REPACK (CONCURRENTLY) */
 
 /* flags reset at EOXact */
 #define		PROC_VACUUM_STATE_MASK \
diff --git a/src/test/modules/injection_points/Makefile b/src/test/modules/injection_points/Makefile
index 2cd7d87c533..f7663859fe2 100644
--- a/src/test/modules/injection_points/Makefile
+++ b/src/test/modules/injection_points/Makefile
@@ -15,6 +15,7 @@ REGRESS_OPTS = --dlpath=$(top_builddir)/src/test/regress
 ISOLATION = basic \
 	    inplace \
 	    repack \
+	    repack_deadlock \
 	    repack_toast \
 	    syscache-update-pruned \
 	    heap_lock_update
diff --git a/src/test/modules/injection_points/expected/repack_deadlock.out b/src/test/modules/injection_points/expected/repack_deadlock.out
new file mode 100644
index 00000000000..42505d10933
--- /dev/null
+++ b/src/test/modules/injection_points/expected/repack_deadlock.out
@@ -0,0 +1,63 @@
+Parsed test spec with 2 sessions
+
+starting permutation: wait_before_lock add_column wakeup_before_lock check1
+injection_points_attach
+-----------------------
+                       
+(1 row)
+
+step wait_before_lock: 
+	REPACK (CONCURRENTLY) repack_deadlock USING INDEX repack_deadlock_pkey;
+ <waiting ...>
+step add_column: 
+	alter table repack_deadlock add column noise text;
+ <waiting ...>
+step add_column: <... completed>
+ERROR:  oops, we're blocked by REPACK, time to die
+step wakeup_before_lock: 
+	SELECT injection_points_wakeup('repack-concurrently-before-lock');
+
+injection_points_wakeup
+-----------------------
+                       
+(1 row)
+
+step wait_before_lock: <... completed>
+step check1: 
+	INSERT INTO relfilenodes(node)
+	SELECT relfilenode FROM pg_class WHERE relname='repack_deadlock';
+
+	SELECT count(DISTINCT node) FROM relfilenodes;
+
+	SELECT i, j FROM repack_deadlock ORDER BY i, j;
+
+	INSERT INTO data_s1(i, j)
+	SELECT i, j FROM repack_deadlock;
+
+	SELECT count(*)
+	FROM data_s1 d1 FULL JOIN data_s2 d2 USING (i, j)
+	WHERE d1.i ISNULL OR d2.i ISNULL;
+
+count
+-----
+    1
+(1 row)
+
+i|j
+-+-
+1|1
+2|2
+3|3
+4|4
+(4 rows)
+
+count
+-----
+    4
+(1 row)
+
+injection_points_detach
+-----------------------
+                       
+(1 row)
+
diff --git a/src/test/modules/injection_points/meson.build b/src/test/modules/injection_points/meson.build
index a414abb924b..1cd88d6db65 100644
--- a/src/test/modules/injection_points/meson.build
+++ b/src/test/modules/injection_points/meson.build
@@ -46,6 +46,7 @@ tests += {
       'basic',
       'inplace',
       'repack',
+      'repack_deadlock',
       'repack_toast',
       'syscache-update-pruned',
       'heap_lock_update',
diff --git a/src/test/modules/injection_points/specs/repack_deadlock.spec b/src/test/modules/injection_points/specs/repack_deadlock.spec
new file mode 100644
index 00000000000..9d23a6588c2
--- /dev/null
+++ b/src/test/modules/injection_points/specs/repack_deadlock.spec
@@ -0,0 +1,83 @@
+# Test REPACK with a concurrent transaction that would cause a deadlock
+setup
+{
+	CREATE EXTENSION injection_points;
+
+	CREATE TABLE repack_deadlock(i int PRIMARY KEY, j int);
+	INSERT INTO repack_deadlock(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4);
+
+	CREATE TABLE relfilenodes(node oid);
+
+	CREATE TABLE data_s1(i int, j int);
+	CREATE TABLE data_s2(i int, j int);
+}
+
+teardown
+{
+	DROP TABLE repack_deadlock;
+	DROP EXTENSION injection_points;
+
+	DROP TABLE relfilenodes;
+	DROP TABLE data_s1;
+	DROP TABLE data_s2;
+}
+
+session s1
+setup
+{
+	SELECT injection_points_set_local();
+	SELECT injection_points_attach('repack-concurrently-before-lock', 'wait');
+}
+# Perform the initial load and wait for s2 to do some data changes.
+step wait_before_lock
+{
+	REPACK (CONCURRENTLY) repack_deadlock USING INDEX repack_deadlock_pkey;
+}
+# Check the table from the perspective of s1.
+#
+# Besides the contents, we also check that relfilenode has changed.
+
+# Have each session write the contents into a table and use FULL JOIN to check
+# if the outputs are identical.
+step check1
+{
+	INSERT INTO relfilenodes(node)
+	SELECT relfilenode FROM pg_class WHERE relname='repack_deadlock';
+
+	SELECT count(DISTINCT node) FROM relfilenodes;
+
+	SELECT i, j FROM repack_deadlock ORDER BY i, j;
+
+	INSERT INTO data_s1(i, j)
+	SELECT i, j FROM repack_deadlock;
+
+	SELECT count(*)
+	FROM data_s1 d1 FULL JOIN data_s2 d2 USING (i, j)
+	WHERE d1.i ISNULL OR d2.i ISNULL;
+}
+teardown
+{
+	SELECT injection_points_detach('repack-concurrently-before-lock');
+}
+
+session s2
+# Change the existing data. UPDATE changes both key and non-key columns. Also
+# update one row twice to test whether tuple version generated by this session
+# can be found.
+step add_column
+{
+	alter table repack_deadlock add column noise text;
+}
+
+step wakeup_before_lock
+{
+	SELECT injection_points_wakeup('repack-concurrently-before-lock');
+}
+
+# Test if data changes introduced while one session is performing REPACK
+# CONCURRENTLY find their way into the table.
+permutation
+	wait_before_lock
+	add_column
+	wakeup_before_lock
+	check1
-- 
2.47.3


--fot5plqmb33ey33c--





^ permalink  raw  reply  [nested|flat] 89+ messages in thread

* [PATCH v44b 4/4] hack deadlock detector
@ 2026-03-25 19:35  Álvaro Herrera <[email protected]>
  0 siblings, 0 replies; 89+ messages in thread

From: Álvaro Herrera @ 2026-03-25 19:35 UTC (permalink / raw)

---
 src/backend/commands/cluster.c                | 10 +++
 src/backend/storage/lmgr/deadlock.c           | 12 +++
 src/include/storage/proc.h                    |  1 +
 src/test/modules/injection_points/Makefile    |  1 +
 .../expected/repack_deadlock.out              | 63 ++++++++++++++
 src/test/modules/injection_points/meson.build |  1 +
 .../specs/repack_deadlock.spec                | 83 +++++++++++++++++++
 7 files changed, 171 insertions(+)
 create mode 100644 src/test/modules/injection_points/expected/repack_deadlock.out
 create mode 100644 src/test/modules/injection_points/specs/repack_deadlock.spec

diff --git a/src/backend/commands/cluster.c b/src/backend/commands/cluster.c
index 2c3058ba10d..d5b1dfbff69 100644
--- a/src/backend/commands/cluster.c
+++ b/src/backend/commands/cluster.c
@@ -372,6 +372,16 @@ ExecRepack(ParseState *pstate, RepackStmt *stmt, bool isTopLevel)
 	/* Determine the lock mode to use. */
 	lockmode = RepackLockLevel((params.options & CLUOPT_CONCURRENT) != 0);
 
+	/*
+	 * If in concurrent mode, set the PROC_IN_CONCURRENT_REPACK flag.  This
+	 * makes the deadlock checker cause anyone that would conflict with us
+	 * to error out.  It's important to set this flag ahead of actually locking
+	 * the relation; it won't of course affect anyone until we do have a lock
+	 * that others can conflict with.
+	 */
+	if ((params.options & CLUOPT_CONCURRENT) != 0)
+		MyProc->statusFlags |= PROC_IN_CONCURRENT_REPACK;
+
 	/*
 	 * If a single relation is specified, process it and we're done ... unless
 	 * the relation is a partitioned table, in which case we fall through.
diff --git a/src/backend/storage/lmgr/deadlock.c b/src/backend/storage/lmgr/deadlock.c
index b8962d875b6..10266e384aa 100644
--- a/src/backend/storage/lmgr/deadlock.c
+++ b/src/backend/storage/lmgr/deadlock.c
@@ -620,6 +620,18 @@ FindLockCycleRecurseMember(PGPROC *checkProc,
 						proc->statusFlags & PROC_IS_AUTOVACUUM)
 						blocking_autovacuum_proc = proc;
 
+					/*
+					 * If we note that we're blocked by some process running
+					 * REPACK (CONCURRENTLY), just fail.  That process is
+					 * going to upgrade its lock at some point, and it would
+					 * be inappropriate for any other process to cause that
+					 * to fail.
+					 */
+					if (checkProc == MyProc &&
+						proc->statusFlags & PROC_IN_CONCURRENT_REPACK)
+						/* not a serious error message proposal */
+						elog(ERROR, "oops, we're blocked by REPACK, time to die");
+
 					/* We're done looking at this proclock */
 					break;
 				}
diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h
index 1dad125706e..4e340ead455 100644
--- a/src/include/storage/proc.h
+++ b/src/include/storage/proc.h
@@ -69,6 +69,7 @@ struct XidCache
 #define		PROC_AFFECTS_ALL_HORIZONS	0x20	/* this proc's xmin must be
 												 * included in vacuum horizons
 												 * in all databases */
+#define		PROC_IN_CONCURRENT_REPACK	0x40	/* REPACK (CONCURRENTLY) */
 
 /* flags reset at EOXact */
 #define		PROC_VACUUM_STATE_MASK \
diff --git a/src/test/modules/injection_points/Makefile b/src/test/modules/injection_points/Makefile
index 2cd7d87c533..f7663859fe2 100644
--- a/src/test/modules/injection_points/Makefile
+++ b/src/test/modules/injection_points/Makefile
@@ -15,6 +15,7 @@ REGRESS_OPTS = --dlpath=$(top_builddir)/src/test/regress
 ISOLATION = basic \
 	    inplace \
 	    repack \
+	    repack_deadlock \
 	    repack_toast \
 	    syscache-update-pruned \
 	    heap_lock_update
diff --git a/src/test/modules/injection_points/expected/repack_deadlock.out b/src/test/modules/injection_points/expected/repack_deadlock.out
new file mode 100644
index 00000000000..42505d10933
--- /dev/null
+++ b/src/test/modules/injection_points/expected/repack_deadlock.out
@@ -0,0 +1,63 @@
+Parsed test spec with 2 sessions
+
+starting permutation: wait_before_lock add_column wakeup_before_lock check1
+injection_points_attach
+-----------------------
+                       
+(1 row)
+
+step wait_before_lock: 
+	REPACK (CONCURRENTLY) repack_deadlock USING INDEX repack_deadlock_pkey;
+ <waiting ...>
+step add_column: 
+	alter table repack_deadlock add column noise text;
+ <waiting ...>
+step add_column: <... completed>
+ERROR:  oops, we're blocked by REPACK, time to die
+step wakeup_before_lock: 
+	SELECT injection_points_wakeup('repack-concurrently-before-lock');
+
+injection_points_wakeup
+-----------------------
+                       
+(1 row)
+
+step wait_before_lock: <... completed>
+step check1: 
+	INSERT INTO relfilenodes(node)
+	SELECT relfilenode FROM pg_class WHERE relname='repack_deadlock';
+
+	SELECT count(DISTINCT node) FROM relfilenodes;
+
+	SELECT i, j FROM repack_deadlock ORDER BY i, j;
+
+	INSERT INTO data_s1(i, j)
+	SELECT i, j FROM repack_deadlock;
+
+	SELECT count(*)
+	FROM data_s1 d1 FULL JOIN data_s2 d2 USING (i, j)
+	WHERE d1.i ISNULL OR d2.i ISNULL;
+
+count
+-----
+    1
+(1 row)
+
+i|j
+-+-
+1|1
+2|2
+3|3
+4|4
+(4 rows)
+
+count
+-----
+    4
+(1 row)
+
+injection_points_detach
+-----------------------
+                       
+(1 row)
+
diff --git a/src/test/modules/injection_points/meson.build b/src/test/modules/injection_points/meson.build
index a414abb924b..1cd88d6db65 100644
--- a/src/test/modules/injection_points/meson.build
+++ b/src/test/modules/injection_points/meson.build
@@ -46,6 +46,7 @@ tests += {
       'basic',
       'inplace',
       'repack',
+      'repack_deadlock',
       'repack_toast',
       'syscache-update-pruned',
       'heap_lock_update',
diff --git a/src/test/modules/injection_points/specs/repack_deadlock.spec b/src/test/modules/injection_points/specs/repack_deadlock.spec
new file mode 100644
index 00000000000..9d23a6588c2
--- /dev/null
+++ b/src/test/modules/injection_points/specs/repack_deadlock.spec
@@ -0,0 +1,83 @@
+# Test REPACK with a concurrent transaction that would cause a deadlock
+setup
+{
+	CREATE EXTENSION injection_points;
+
+	CREATE TABLE repack_deadlock(i int PRIMARY KEY, j int);
+	INSERT INTO repack_deadlock(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4);
+
+	CREATE TABLE relfilenodes(node oid);
+
+	CREATE TABLE data_s1(i int, j int);
+	CREATE TABLE data_s2(i int, j int);
+}
+
+teardown
+{
+	DROP TABLE repack_deadlock;
+	DROP EXTENSION injection_points;
+
+	DROP TABLE relfilenodes;
+	DROP TABLE data_s1;
+	DROP TABLE data_s2;
+}
+
+session s1
+setup
+{
+	SELECT injection_points_set_local();
+	SELECT injection_points_attach('repack-concurrently-before-lock', 'wait');
+}
+# Perform the initial load and wait for s2 to do some data changes.
+step wait_before_lock
+{
+	REPACK (CONCURRENTLY) repack_deadlock USING INDEX repack_deadlock_pkey;
+}
+# Check the table from the perspective of s1.
+#
+# Besides the contents, we also check that relfilenode has changed.
+
+# Have each session write the contents into a table and use FULL JOIN to check
+# if the outputs are identical.
+step check1
+{
+	INSERT INTO relfilenodes(node)
+	SELECT relfilenode FROM pg_class WHERE relname='repack_deadlock';
+
+	SELECT count(DISTINCT node) FROM relfilenodes;
+
+	SELECT i, j FROM repack_deadlock ORDER BY i, j;
+
+	INSERT INTO data_s1(i, j)
+	SELECT i, j FROM repack_deadlock;
+
+	SELECT count(*)
+	FROM data_s1 d1 FULL JOIN data_s2 d2 USING (i, j)
+	WHERE d1.i ISNULL OR d2.i ISNULL;
+}
+teardown
+{
+	SELECT injection_points_detach('repack-concurrently-before-lock');
+}
+
+session s2
+# Change the existing data. UPDATE changes both key and non-key columns. Also
+# update one row twice to test whether tuple version generated by this session
+# can be found.
+step add_column
+{
+	alter table repack_deadlock add column noise text;
+}
+
+step wakeup_before_lock
+{
+	SELECT injection_points_wakeup('repack-concurrently-before-lock');
+}
+
+# Test if data changes introduced while one session is performing REPACK
+# CONCURRENTLY find their way into the table.
+permutation
+	wait_before_lock
+	add_column
+	wakeup_before_lock
+	check1
-- 
2.47.3


--fot5plqmb33ey33c--





^ permalink  raw  reply  [nested|flat] 89+ messages in thread

* [PATCH v44b 4/4] hack deadlock detector
@ 2026-03-25 19:35  Álvaro Herrera <[email protected]>
  0 siblings, 0 replies; 89+ messages in thread

From: Álvaro Herrera @ 2026-03-25 19:35 UTC (permalink / raw)

---
 src/backend/commands/cluster.c                | 10 +++
 src/backend/storage/lmgr/deadlock.c           | 12 +++
 src/include/storage/proc.h                    |  1 +
 src/test/modules/injection_points/Makefile    |  1 +
 .../expected/repack_deadlock.out              | 63 ++++++++++++++
 src/test/modules/injection_points/meson.build |  1 +
 .../specs/repack_deadlock.spec                | 83 +++++++++++++++++++
 7 files changed, 171 insertions(+)
 create mode 100644 src/test/modules/injection_points/expected/repack_deadlock.out
 create mode 100644 src/test/modules/injection_points/specs/repack_deadlock.spec

diff --git a/src/backend/commands/cluster.c b/src/backend/commands/cluster.c
index 2c3058ba10d..d5b1dfbff69 100644
--- a/src/backend/commands/cluster.c
+++ b/src/backend/commands/cluster.c
@@ -372,6 +372,16 @@ ExecRepack(ParseState *pstate, RepackStmt *stmt, bool isTopLevel)
 	/* Determine the lock mode to use. */
 	lockmode = RepackLockLevel((params.options & CLUOPT_CONCURRENT) != 0);
 
+	/*
+	 * If in concurrent mode, set the PROC_IN_CONCURRENT_REPACK flag.  This
+	 * makes the deadlock checker cause anyone that would conflict with us
+	 * to error out.  It's important to set this flag ahead of actually locking
+	 * the relation; it won't of course affect anyone until we do have a lock
+	 * that others can conflict with.
+	 */
+	if ((params.options & CLUOPT_CONCURRENT) != 0)
+		MyProc->statusFlags |= PROC_IN_CONCURRENT_REPACK;
+
 	/*
 	 * If a single relation is specified, process it and we're done ... unless
 	 * the relation is a partitioned table, in which case we fall through.
diff --git a/src/backend/storage/lmgr/deadlock.c b/src/backend/storage/lmgr/deadlock.c
index b8962d875b6..10266e384aa 100644
--- a/src/backend/storage/lmgr/deadlock.c
+++ b/src/backend/storage/lmgr/deadlock.c
@@ -620,6 +620,18 @@ FindLockCycleRecurseMember(PGPROC *checkProc,
 						proc->statusFlags & PROC_IS_AUTOVACUUM)
 						blocking_autovacuum_proc = proc;
 
+					/*
+					 * If we note that we're blocked by some process running
+					 * REPACK (CONCURRENTLY), just fail.  That process is
+					 * going to upgrade its lock at some point, and it would
+					 * be inappropriate for any other process to cause that
+					 * to fail.
+					 */
+					if (checkProc == MyProc &&
+						proc->statusFlags & PROC_IN_CONCURRENT_REPACK)
+						/* not a serious error message proposal */
+						elog(ERROR, "oops, we're blocked by REPACK, time to die");
+
 					/* We're done looking at this proclock */
 					break;
 				}
diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h
index 1dad125706e..4e340ead455 100644
--- a/src/include/storage/proc.h
+++ b/src/include/storage/proc.h
@@ -69,6 +69,7 @@ struct XidCache
 #define		PROC_AFFECTS_ALL_HORIZONS	0x20	/* this proc's xmin must be
 												 * included in vacuum horizons
 												 * in all databases */
+#define		PROC_IN_CONCURRENT_REPACK	0x40	/* REPACK (CONCURRENTLY) */
 
 /* flags reset at EOXact */
 #define		PROC_VACUUM_STATE_MASK \
diff --git a/src/test/modules/injection_points/Makefile b/src/test/modules/injection_points/Makefile
index 2cd7d87c533..f7663859fe2 100644
--- a/src/test/modules/injection_points/Makefile
+++ b/src/test/modules/injection_points/Makefile
@@ -15,6 +15,7 @@ REGRESS_OPTS = --dlpath=$(top_builddir)/src/test/regress
 ISOLATION = basic \
 	    inplace \
 	    repack \
+	    repack_deadlock \
 	    repack_toast \
 	    syscache-update-pruned \
 	    heap_lock_update
diff --git a/src/test/modules/injection_points/expected/repack_deadlock.out b/src/test/modules/injection_points/expected/repack_deadlock.out
new file mode 100644
index 00000000000..42505d10933
--- /dev/null
+++ b/src/test/modules/injection_points/expected/repack_deadlock.out
@@ -0,0 +1,63 @@
+Parsed test spec with 2 sessions
+
+starting permutation: wait_before_lock add_column wakeup_before_lock check1
+injection_points_attach
+-----------------------
+                       
+(1 row)
+
+step wait_before_lock: 
+	REPACK (CONCURRENTLY) repack_deadlock USING INDEX repack_deadlock_pkey;
+ <waiting ...>
+step add_column: 
+	alter table repack_deadlock add column noise text;
+ <waiting ...>
+step add_column: <... completed>
+ERROR:  oops, we're blocked by REPACK, time to die
+step wakeup_before_lock: 
+	SELECT injection_points_wakeup('repack-concurrently-before-lock');
+
+injection_points_wakeup
+-----------------------
+                       
+(1 row)
+
+step wait_before_lock: <... completed>
+step check1: 
+	INSERT INTO relfilenodes(node)
+	SELECT relfilenode FROM pg_class WHERE relname='repack_deadlock';
+
+	SELECT count(DISTINCT node) FROM relfilenodes;
+
+	SELECT i, j FROM repack_deadlock ORDER BY i, j;
+
+	INSERT INTO data_s1(i, j)
+	SELECT i, j FROM repack_deadlock;
+
+	SELECT count(*)
+	FROM data_s1 d1 FULL JOIN data_s2 d2 USING (i, j)
+	WHERE d1.i ISNULL OR d2.i ISNULL;
+
+count
+-----
+    1
+(1 row)
+
+i|j
+-+-
+1|1
+2|2
+3|3
+4|4
+(4 rows)
+
+count
+-----
+    4
+(1 row)
+
+injection_points_detach
+-----------------------
+                       
+(1 row)
+
diff --git a/src/test/modules/injection_points/meson.build b/src/test/modules/injection_points/meson.build
index a414abb924b..1cd88d6db65 100644
--- a/src/test/modules/injection_points/meson.build
+++ b/src/test/modules/injection_points/meson.build
@@ -46,6 +46,7 @@ tests += {
       'basic',
       'inplace',
       'repack',
+      'repack_deadlock',
       'repack_toast',
       'syscache-update-pruned',
       'heap_lock_update',
diff --git a/src/test/modules/injection_points/specs/repack_deadlock.spec b/src/test/modules/injection_points/specs/repack_deadlock.spec
new file mode 100644
index 00000000000..9d23a6588c2
--- /dev/null
+++ b/src/test/modules/injection_points/specs/repack_deadlock.spec
@@ -0,0 +1,83 @@
+# Test REPACK with a concurrent transaction that would cause a deadlock
+setup
+{
+	CREATE EXTENSION injection_points;
+
+	CREATE TABLE repack_deadlock(i int PRIMARY KEY, j int);
+	INSERT INTO repack_deadlock(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4);
+
+	CREATE TABLE relfilenodes(node oid);
+
+	CREATE TABLE data_s1(i int, j int);
+	CREATE TABLE data_s2(i int, j int);
+}
+
+teardown
+{
+	DROP TABLE repack_deadlock;
+	DROP EXTENSION injection_points;
+
+	DROP TABLE relfilenodes;
+	DROP TABLE data_s1;
+	DROP TABLE data_s2;
+}
+
+session s1
+setup
+{
+	SELECT injection_points_set_local();
+	SELECT injection_points_attach('repack-concurrently-before-lock', 'wait');
+}
+# Perform the initial load and wait for s2 to do some data changes.
+step wait_before_lock
+{
+	REPACK (CONCURRENTLY) repack_deadlock USING INDEX repack_deadlock_pkey;
+}
+# Check the table from the perspective of s1.
+#
+# Besides the contents, we also check that relfilenode has changed.
+
+# Have each session write the contents into a table and use FULL JOIN to check
+# if the outputs are identical.
+step check1
+{
+	INSERT INTO relfilenodes(node)
+	SELECT relfilenode FROM pg_class WHERE relname='repack_deadlock';
+
+	SELECT count(DISTINCT node) FROM relfilenodes;
+
+	SELECT i, j FROM repack_deadlock ORDER BY i, j;
+
+	INSERT INTO data_s1(i, j)
+	SELECT i, j FROM repack_deadlock;
+
+	SELECT count(*)
+	FROM data_s1 d1 FULL JOIN data_s2 d2 USING (i, j)
+	WHERE d1.i ISNULL OR d2.i ISNULL;
+}
+teardown
+{
+	SELECT injection_points_detach('repack-concurrently-before-lock');
+}
+
+session s2
+# Change the existing data. UPDATE changes both key and non-key columns. Also
+# update one row twice to test whether tuple version generated by this session
+# can be found.
+step add_column
+{
+	alter table repack_deadlock add column noise text;
+}
+
+step wakeup_before_lock
+{
+	SELECT injection_points_wakeup('repack-concurrently-before-lock');
+}
+
+# Test if data changes introduced while one session is performing REPACK
+# CONCURRENTLY find their way into the table.
+permutation
+	wait_before_lock
+	add_column
+	wakeup_before_lock
+	check1
-- 
2.47.3


--fot5plqmb33ey33c--





^ permalink  raw  reply  [nested|flat] 89+ messages in thread

* [PATCH v44b 4/4] hack deadlock detector
@ 2026-03-25 19:35  Álvaro Herrera <[email protected]>
  0 siblings, 0 replies; 89+ messages in thread

From: Álvaro Herrera @ 2026-03-25 19:35 UTC (permalink / raw)

---
 src/backend/commands/cluster.c                | 10 +++
 src/backend/storage/lmgr/deadlock.c           | 12 +++
 src/include/storage/proc.h                    |  1 +
 src/test/modules/injection_points/Makefile    |  1 +
 .../expected/repack_deadlock.out              | 63 ++++++++++++++
 src/test/modules/injection_points/meson.build |  1 +
 .../specs/repack_deadlock.spec                | 83 +++++++++++++++++++
 7 files changed, 171 insertions(+)
 create mode 100644 src/test/modules/injection_points/expected/repack_deadlock.out
 create mode 100644 src/test/modules/injection_points/specs/repack_deadlock.spec

diff --git a/src/backend/commands/cluster.c b/src/backend/commands/cluster.c
index 2c3058ba10d..d5b1dfbff69 100644
--- a/src/backend/commands/cluster.c
+++ b/src/backend/commands/cluster.c
@@ -372,6 +372,16 @@ ExecRepack(ParseState *pstate, RepackStmt *stmt, bool isTopLevel)
 	/* Determine the lock mode to use. */
 	lockmode = RepackLockLevel((params.options & CLUOPT_CONCURRENT) != 0);
 
+	/*
+	 * If in concurrent mode, set the PROC_IN_CONCURRENT_REPACK flag.  This
+	 * makes the deadlock checker cause anyone that would conflict with us
+	 * to error out.  It's important to set this flag ahead of actually locking
+	 * the relation; it won't of course affect anyone until we do have a lock
+	 * that others can conflict with.
+	 */
+	if ((params.options & CLUOPT_CONCURRENT) != 0)
+		MyProc->statusFlags |= PROC_IN_CONCURRENT_REPACK;
+
 	/*
 	 * If a single relation is specified, process it and we're done ... unless
 	 * the relation is a partitioned table, in which case we fall through.
diff --git a/src/backend/storage/lmgr/deadlock.c b/src/backend/storage/lmgr/deadlock.c
index b8962d875b6..10266e384aa 100644
--- a/src/backend/storage/lmgr/deadlock.c
+++ b/src/backend/storage/lmgr/deadlock.c
@@ -620,6 +620,18 @@ FindLockCycleRecurseMember(PGPROC *checkProc,
 						proc->statusFlags & PROC_IS_AUTOVACUUM)
 						blocking_autovacuum_proc = proc;
 
+					/*
+					 * If we note that we're blocked by some process running
+					 * REPACK (CONCURRENTLY), just fail.  That process is
+					 * going to upgrade its lock at some point, and it would
+					 * be inappropriate for any other process to cause that
+					 * to fail.
+					 */
+					if (checkProc == MyProc &&
+						proc->statusFlags & PROC_IN_CONCURRENT_REPACK)
+						/* not a serious error message proposal */
+						elog(ERROR, "oops, we're blocked by REPACK, time to die");
+
 					/* We're done looking at this proclock */
 					break;
 				}
diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h
index 1dad125706e..4e340ead455 100644
--- a/src/include/storage/proc.h
+++ b/src/include/storage/proc.h
@@ -69,6 +69,7 @@ struct XidCache
 #define		PROC_AFFECTS_ALL_HORIZONS	0x20	/* this proc's xmin must be
 												 * included in vacuum horizons
 												 * in all databases */
+#define		PROC_IN_CONCURRENT_REPACK	0x40	/* REPACK (CONCURRENTLY) */
 
 /* flags reset at EOXact */
 #define		PROC_VACUUM_STATE_MASK \
diff --git a/src/test/modules/injection_points/Makefile b/src/test/modules/injection_points/Makefile
index 2cd7d87c533..f7663859fe2 100644
--- a/src/test/modules/injection_points/Makefile
+++ b/src/test/modules/injection_points/Makefile
@@ -15,6 +15,7 @@ REGRESS_OPTS = --dlpath=$(top_builddir)/src/test/regress
 ISOLATION = basic \
 	    inplace \
 	    repack \
+	    repack_deadlock \
 	    repack_toast \
 	    syscache-update-pruned \
 	    heap_lock_update
diff --git a/src/test/modules/injection_points/expected/repack_deadlock.out b/src/test/modules/injection_points/expected/repack_deadlock.out
new file mode 100644
index 00000000000..42505d10933
--- /dev/null
+++ b/src/test/modules/injection_points/expected/repack_deadlock.out
@@ -0,0 +1,63 @@
+Parsed test spec with 2 sessions
+
+starting permutation: wait_before_lock add_column wakeup_before_lock check1
+injection_points_attach
+-----------------------
+                       
+(1 row)
+
+step wait_before_lock: 
+	REPACK (CONCURRENTLY) repack_deadlock USING INDEX repack_deadlock_pkey;
+ <waiting ...>
+step add_column: 
+	alter table repack_deadlock add column noise text;
+ <waiting ...>
+step add_column: <... completed>
+ERROR:  oops, we're blocked by REPACK, time to die
+step wakeup_before_lock: 
+	SELECT injection_points_wakeup('repack-concurrently-before-lock');
+
+injection_points_wakeup
+-----------------------
+                       
+(1 row)
+
+step wait_before_lock: <... completed>
+step check1: 
+	INSERT INTO relfilenodes(node)
+	SELECT relfilenode FROM pg_class WHERE relname='repack_deadlock';
+
+	SELECT count(DISTINCT node) FROM relfilenodes;
+
+	SELECT i, j FROM repack_deadlock ORDER BY i, j;
+
+	INSERT INTO data_s1(i, j)
+	SELECT i, j FROM repack_deadlock;
+
+	SELECT count(*)
+	FROM data_s1 d1 FULL JOIN data_s2 d2 USING (i, j)
+	WHERE d1.i ISNULL OR d2.i ISNULL;
+
+count
+-----
+    1
+(1 row)
+
+i|j
+-+-
+1|1
+2|2
+3|3
+4|4
+(4 rows)
+
+count
+-----
+    4
+(1 row)
+
+injection_points_detach
+-----------------------
+                       
+(1 row)
+
diff --git a/src/test/modules/injection_points/meson.build b/src/test/modules/injection_points/meson.build
index a414abb924b..1cd88d6db65 100644
--- a/src/test/modules/injection_points/meson.build
+++ b/src/test/modules/injection_points/meson.build
@@ -46,6 +46,7 @@ tests += {
       'basic',
       'inplace',
       'repack',
+      'repack_deadlock',
       'repack_toast',
       'syscache-update-pruned',
       'heap_lock_update',
diff --git a/src/test/modules/injection_points/specs/repack_deadlock.spec b/src/test/modules/injection_points/specs/repack_deadlock.spec
new file mode 100644
index 00000000000..9d23a6588c2
--- /dev/null
+++ b/src/test/modules/injection_points/specs/repack_deadlock.spec
@@ -0,0 +1,83 @@
+# Test REPACK with a concurrent transaction that would cause a deadlock
+setup
+{
+	CREATE EXTENSION injection_points;
+
+	CREATE TABLE repack_deadlock(i int PRIMARY KEY, j int);
+	INSERT INTO repack_deadlock(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4);
+
+	CREATE TABLE relfilenodes(node oid);
+
+	CREATE TABLE data_s1(i int, j int);
+	CREATE TABLE data_s2(i int, j int);
+}
+
+teardown
+{
+	DROP TABLE repack_deadlock;
+	DROP EXTENSION injection_points;
+
+	DROP TABLE relfilenodes;
+	DROP TABLE data_s1;
+	DROP TABLE data_s2;
+}
+
+session s1
+setup
+{
+	SELECT injection_points_set_local();
+	SELECT injection_points_attach('repack-concurrently-before-lock', 'wait');
+}
+# Perform the initial load and wait for s2 to do some data changes.
+step wait_before_lock
+{
+	REPACK (CONCURRENTLY) repack_deadlock USING INDEX repack_deadlock_pkey;
+}
+# Check the table from the perspective of s1.
+#
+# Besides the contents, we also check that relfilenode has changed.
+
+# Have each session write the contents into a table and use FULL JOIN to check
+# if the outputs are identical.
+step check1
+{
+	INSERT INTO relfilenodes(node)
+	SELECT relfilenode FROM pg_class WHERE relname='repack_deadlock';
+
+	SELECT count(DISTINCT node) FROM relfilenodes;
+
+	SELECT i, j FROM repack_deadlock ORDER BY i, j;
+
+	INSERT INTO data_s1(i, j)
+	SELECT i, j FROM repack_deadlock;
+
+	SELECT count(*)
+	FROM data_s1 d1 FULL JOIN data_s2 d2 USING (i, j)
+	WHERE d1.i ISNULL OR d2.i ISNULL;
+}
+teardown
+{
+	SELECT injection_points_detach('repack-concurrently-before-lock');
+}
+
+session s2
+# Change the existing data. UPDATE changes both key and non-key columns. Also
+# update one row twice to test whether tuple version generated by this session
+# can be found.
+step add_column
+{
+	alter table repack_deadlock add column noise text;
+}
+
+step wakeup_before_lock
+{
+	SELECT injection_points_wakeup('repack-concurrently-before-lock');
+}
+
+# Test if data changes introduced while one session is performing REPACK
+# CONCURRENTLY find their way into the table.
+permutation
+	wait_before_lock
+	add_column
+	wakeup_before_lock
+	check1
-- 
2.47.3


--fot5plqmb33ey33c--





^ permalink  raw  reply  [nested|flat] 89+ messages in thread

* [PATCH v44b 4/4] hack deadlock detector
@ 2026-03-25 19:35  Álvaro Herrera <[email protected]>
  0 siblings, 0 replies; 89+ messages in thread

From: Álvaro Herrera @ 2026-03-25 19:35 UTC (permalink / raw)

---
 src/backend/commands/cluster.c                | 10 +++
 src/backend/storage/lmgr/deadlock.c           | 12 +++
 src/include/storage/proc.h                    |  1 +
 src/test/modules/injection_points/Makefile    |  1 +
 .../expected/repack_deadlock.out              | 63 ++++++++++++++
 src/test/modules/injection_points/meson.build |  1 +
 .../specs/repack_deadlock.spec                | 83 +++++++++++++++++++
 7 files changed, 171 insertions(+)
 create mode 100644 src/test/modules/injection_points/expected/repack_deadlock.out
 create mode 100644 src/test/modules/injection_points/specs/repack_deadlock.spec

diff --git a/src/backend/commands/cluster.c b/src/backend/commands/cluster.c
index 2c3058ba10d..d5b1dfbff69 100644
--- a/src/backend/commands/cluster.c
+++ b/src/backend/commands/cluster.c
@@ -372,6 +372,16 @@ ExecRepack(ParseState *pstate, RepackStmt *stmt, bool isTopLevel)
 	/* Determine the lock mode to use. */
 	lockmode = RepackLockLevel((params.options & CLUOPT_CONCURRENT) != 0);
 
+	/*
+	 * If in concurrent mode, set the PROC_IN_CONCURRENT_REPACK flag.  This
+	 * makes the deadlock checker cause anyone that would conflict with us
+	 * to error out.  It's important to set this flag ahead of actually locking
+	 * the relation; it won't of course affect anyone until we do have a lock
+	 * that others can conflict with.
+	 */
+	if ((params.options & CLUOPT_CONCURRENT) != 0)
+		MyProc->statusFlags |= PROC_IN_CONCURRENT_REPACK;
+
 	/*
 	 * If a single relation is specified, process it and we're done ... unless
 	 * the relation is a partitioned table, in which case we fall through.
diff --git a/src/backend/storage/lmgr/deadlock.c b/src/backend/storage/lmgr/deadlock.c
index b8962d875b6..10266e384aa 100644
--- a/src/backend/storage/lmgr/deadlock.c
+++ b/src/backend/storage/lmgr/deadlock.c
@@ -620,6 +620,18 @@ FindLockCycleRecurseMember(PGPROC *checkProc,
 						proc->statusFlags & PROC_IS_AUTOVACUUM)
 						blocking_autovacuum_proc = proc;
 
+					/*
+					 * If we note that we're blocked by some process running
+					 * REPACK (CONCURRENTLY), just fail.  That process is
+					 * going to upgrade its lock at some point, and it would
+					 * be inappropriate for any other process to cause that
+					 * to fail.
+					 */
+					if (checkProc == MyProc &&
+						proc->statusFlags & PROC_IN_CONCURRENT_REPACK)
+						/* not a serious error message proposal */
+						elog(ERROR, "oops, we're blocked by REPACK, time to die");
+
 					/* We're done looking at this proclock */
 					break;
 				}
diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h
index 1dad125706e..4e340ead455 100644
--- a/src/include/storage/proc.h
+++ b/src/include/storage/proc.h
@@ -69,6 +69,7 @@ struct XidCache
 #define		PROC_AFFECTS_ALL_HORIZONS	0x20	/* this proc's xmin must be
 												 * included in vacuum horizons
 												 * in all databases */
+#define		PROC_IN_CONCURRENT_REPACK	0x40	/* REPACK (CONCURRENTLY) */
 
 /* flags reset at EOXact */
 #define		PROC_VACUUM_STATE_MASK \
diff --git a/src/test/modules/injection_points/Makefile b/src/test/modules/injection_points/Makefile
index 2cd7d87c533..f7663859fe2 100644
--- a/src/test/modules/injection_points/Makefile
+++ b/src/test/modules/injection_points/Makefile
@@ -15,6 +15,7 @@ REGRESS_OPTS = --dlpath=$(top_builddir)/src/test/regress
 ISOLATION = basic \
 	    inplace \
 	    repack \
+	    repack_deadlock \
 	    repack_toast \
 	    syscache-update-pruned \
 	    heap_lock_update
diff --git a/src/test/modules/injection_points/expected/repack_deadlock.out b/src/test/modules/injection_points/expected/repack_deadlock.out
new file mode 100644
index 00000000000..42505d10933
--- /dev/null
+++ b/src/test/modules/injection_points/expected/repack_deadlock.out
@@ -0,0 +1,63 @@
+Parsed test spec with 2 sessions
+
+starting permutation: wait_before_lock add_column wakeup_before_lock check1
+injection_points_attach
+-----------------------
+                       
+(1 row)
+
+step wait_before_lock: 
+	REPACK (CONCURRENTLY) repack_deadlock USING INDEX repack_deadlock_pkey;
+ <waiting ...>
+step add_column: 
+	alter table repack_deadlock add column noise text;
+ <waiting ...>
+step add_column: <... completed>
+ERROR:  oops, we're blocked by REPACK, time to die
+step wakeup_before_lock: 
+	SELECT injection_points_wakeup('repack-concurrently-before-lock');
+
+injection_points_wakeup
+-----------------------
+                       
+(1 row)
+
+step wait_before_lock: <... completed>
+step check1: 
+	INSERT INTO relfilenodes(node)
+	SELECT relfilenode FROM pg_class WHERE relname='repack_deadlock';
+
+	SELECT count(DISTINCT node) FROM relfilenodes;
+
+	SELECT i, j FROM repack_deadlock ORDER BY i, j;
+
+	INSERT INTO data_s1(i, j)
+	SELECT i, j FROM repack_deadlock;
+
+	SELECT count(*)
+	FROM data_s1 d1 FULL JOIN data_s2 d2 USING (i, j)
+	WHERE d1.i ISNULL OR d2.i ISNULL;
+
+count
+-----
+    1
+(1 row)
+
+i|j
+-+-
+1|1
+2|2
+3|3
+4|4
+(4 rows)
+
+count
+-----
+    4
+(1 row)
+
+injection_points_detach
+-----------------------
+                       
+(1 row)
+
diff --git a/src/test/modules/injection_points/meson.build b/src/test/modules/injection_points/meson.build
index a414abb924b..1cd88d6db65 100644
--- a/src/test/modules/injection_points/meson.build
+++ b/src/test/modules/injection_points/meson.build
@@ -46,6 +46,7 @@ tests += {
       'basic',
       'inplace',
       'repack',
+      'repack_deadlock',
       'repack_toast',
       'syscache-update-pruned',
       'heap_lock_update',
diff --git a/src/test/modules/injection_points/specs/repack_deadlock.spec b/src/test/modules/injection_points/specs/repack_deadlock.spec
new file mode 100644
index 00000000000..9d23a6588c2
--- /dev/null
+++ b/src/test/modules/injection_points/specs/repack_deadlock.spec
@@ -0,0 +1,83 @@
+# Test REPACK with a concurrent transaction that would cause a deadlock
+setup
+{
+	CREATE EXTENSION injection_points;
+
+	CREATE TABLE repack_deadlock(i int PRIMARY KEY, j int);
+	INSERT INTO repack_deadlock(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4);
+
+	CREATE TABLE relfilenodes(node oid);
+
+	CREATE TABLE data_s1(i int, j int);
+	CREATE TABLE data_s2(i int, j int);
+}
+
+teardown
+{
+	DROP TABLE repack_deadlock;
+	DROP EXTENSION injection_points;
+
+	DROP TABLE relfilenodes;
+	DROP TABLE data_s1;
+	DROP TABLE data_s2;
+}
+
+session s1
+setup
+{
+	SELECT injection_points_set_local();
+	SELECT injection_points_attach('repack-concurrently-before-lock', 'wait');
+}
+# Perform the initial load and wait for s2 to do some data changes.
+step wait_before_lock
+{
+	REPACK (CONCURRENTLY) repack_deadlock USING INDEX repack_deadlock_pkey;
+}
+# Check the table from the perspective of s1.
+#
+# Besides the contents, we also check that relfilenode has changed.
+
+# Have each session write the contents into a table and use FULL JOIN to check
+# if the outputs are identical.
+step check1
+{
+	INSERT INTO relfilenodes(node)
+	SELECT relfilenode FROM pg_class WHERE relname='repack_deadlock';
+
+	SELECT count(DISTINCT node) FROM relfilenodes;
+
+	SELECT i, j FROM repack_deadlock ORDER BY i, j;
+
+	INSERT INTO data_s1(i, j)
+	SELECT i, j FROM repack_deadlock;
+
+	SELECT count(*)
+	FROM data_s1 d1 FULL JOIN data_s2 d2 USING (i, j)
+	WHERE d1.i ISNULL OR d2.i ISNULL;
+}
+teardown
+{
+	SELECT injection_points_detach('repack-concurrently-before-lock');
+}
+
+session s2
+# Change the existing data. UPDATE changes both key and non-key columns. Also
+# update one row twice to test whether tuple version generated by this session
+# can be found.
+step add_column
+{
+	alter table repack_deadlock add column noise text;
+}
+
+step wakeup_before_lock
+{
+	SELECT injection_points_wakeup('repack-concurrently-before-lock');
+}
+
+# Test if data changes introduced while one session is performing REPACK
+# CONCURRENTLY find their way into the table.
+permutation
+	wait_before_lock
+	add_column
+	wakeup_before_lock
+	check1
-- 
2.47.3


--fot5plqmb33ey33c--





^ permalink  raw  reply  [nested|flat] 89+ messages in thread

* [PATCH v44b 4/4] hack deadlock detector
@ 2026-03-25 19:35  Álvaro Herrera <[email protected]>
  0 siblings, 0 replies; 89+ messages in thread

From: Álvaro Herrera @ 2026-03-25 19:35 UTC (permalink / raw)

---
 src/backend/commands/cluster.c                | 10 +++
 src/backend/storage/lmgr/deadlock.c           | 12 +++
 src/include/storage/proc.h                    |  1 +
 src/test/modules/injection_points/Makefile    |  1 +
 .../expected/repack_deadlock.out              | 63 ++++++++++++++
 src/test/modules/injection_points/meson.build |  1 +
 .../specs/repack_deadlock.spec                | 83 +++++++++++++++++++
 7 files changed, 171 insertions(+)
 create mode 100644 src/test/modules/injection_points/expected/repack_deadlock.out
 create mode 100644 src/test/modules/injection_points/specs/repack_deadlock.spec

diff --git a/src/backend/commands/cluster.c b/src/backend/commands/cluster.c
index 2c3058ba10d..d5b1dfbff69 100644
--- a/src/backend/commands/cluster.c
+++ b/src/backend/commands/cluster.c
@@ -372,6 +372,16 @@ ExecRepack(ParseState *pstate, RepackStmt *stmt, bool isTopLevel)
 	/* Determine the lock mode to use. */
 	lockmode = RepackLockLevel((params.options & CLUOPT_CONCURRENT) != 0);
 
+	/*
+	 * If in concurrent mode, set the PROC_IN_CONCURRENT_REPACK flag.  This
+	 * makes the deadlock checker cause anyone that would conflict with us
+	 * to error out.  It's important to set this flag ahead of actually locking
+	 * the relation; it won't of course affect anyone until we do have a lock
+	 * that others can conflict with.
+	 */
+	if ((params.options & CLUOPT_CONCURRENT) != 0)
+		MyProc->statusFlags |= PROC_IN_CONCURRENT_REPACK;
+
 	/*
 	 * If a single relation is specified, process it and we're done ... unless
 	 * the relation is a partitioned table, in which case we fall through.
diff --git a/src/backend/storage/lmgr/deadlock.c b/src/backend/storage/lmgr/deadlock.c
index b8962d875b6..10266e384aa 100644
--- a/src/backend/storage/lmgr/deadlock.c
+++ b/src/backend/storage/lmgr/deadlock.c
@@ -620,6 +620,18 @@ FindLockCycleRecurseMember(PGPROC *checkProc,
 						proc->statusFlags & PROC_IS_AUTOVACUUM)
 						blocking_autovacuum_proc = proc;
 
+					/*
+					 * If we note that we're blocked by some process running
+					 * REPACK (CONCURRENTLY), just fail.  That process is
+					 * going to upgrade its lock at some point, and it would
+					 * be inappropriate for any other process to cause that
+					 * to fail.
+					 */
+					if (checkProc == MyProc &&
+						proc->statusFlags & PROC_IN_CONCURRENT_REPACK)
+						/* not a serious error message proposal */
+						elog(ERROR, "oops, we're blocked by REPACK, time to die");
+
 					/* We're done looking at this proclock */
 					break;
 				}
diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h
index 1dad125706e..4e340ead455 100644
--- a/src/include/storage/proc.h
+++ b/src/include/storage/proc.h
@@ -69,6 +69,7 @@ struct XidCache
 #define		PROC_AFFECTS_ALL_HORIZONS	0x20	/* this proc's xmin must be
 												 * included in vacuum horizons
 												 * in all databases */
+#define		PROC_IN_CONCURRENT_REPACK	0x40	/* REPACK (CONCURRENTLY) */
 
 /* flags reset at EOXact */
 #define		PROC_VACUUM_STATE_MASK \
diff --git a/src/test/modules/injection_points/Makefile b/src/test/modules/injection_points/Makefile
index 2cd7d87c533..f7663859fe2 100644
--- a/src/test/modules/injection_points/Makefile
+++ b/src/test/modules/injection_points/Makefile
@@ -15,6 +15,7 @@ REGRESS_OPTS = --dlpath=$(top_builddir)/src/test/regress
 ISOLATION = basic \
 	    inplace \
 	    repack \
+	    repack_deadlock \
 	    repack_toast \
 	    syscache-update-pruned \
 	    heap_lock_update
diff --git a/src/test/modules/injection_points/expected/repack_deadlock.out b/src/test/modules/injection_points/expected/repack_deadlock.out
new file mode 100644
index 00000000000..42505d10933
--- /dev/null
+++ b/src/test/modules/injection_points/expected/repack_deadlock.out
@@ -0,0 +1,63 @@
+Parsed test spec with 2 sessions
+
+starting permutation: wait_before_lock add_column wakeup_before_lock check1
+injection_points_attach
+-----------------------
+                       
+(1 row)
+
+step wait_before_lock: 
+	REPACK (CONCURRENTLY) repack_deadlock USING INDEX repack_deadlock_pkey;
+ <waiting ...>
+step add_column: 
+	alter table repack_deadlock add column noise text;
+ <waiting ...>
+step add_column: <... completed>
+ERROR:  oops, we're blocked by REPACK, time to die
+step wakeup_before_lock: 
+	SELECT injection_points_wakeup('repack-concurrently-before-lock');
+
+injection_points_wakeup
+-----------------------
+                       
+(1 row)
+
+step wait_before_lock: <... completed>
+step check1: 
+	INSERT INTO relfilenodes(node)
+	SELECT relfilenode FROM pg_class WHERE relname='repack_deadlock';
+
+	SELECT count(DISTINCT node) FROM relfilenodes;
+
+	SELECT i, j FROM repack_deadlock ORDER BY i, j;
+
+	INSERT INTO data_s1(i, j)
+	SELECT i, j FROM repack_deadlock;
+
+	SELECT count(*)
+	FROM data_s1 d1 FULL JOIN data_s2 d2 USING (i, j)
+	WHERE d1.i ISNULL OR d2.i ISNULL;
+
+count
+-----
+    1
+(1 row)
+
+i|j
+-+-
+1|1
+2|2
+3|3
+4|4
+(4 rows)
+
+count
+-----
+    4
+(1 row)
+
+injection_points_detach
+-----------------------
+                       
+(1 row)
+
diff --git a/src/test/modules/injection_points/meson.build b/src/test/modules/injection_points/meson.build
index a414abb924b..1cd88d6db65 100644
--- a/src/test/modules/injection_points/meson.build
+++ b/src/test/modules/injection_points/meson.build
@@ -46,6 +46,7 @@ tests += {
       'basic',
       'inplace',
       'repack',
+      'repack_deadlock',
       'repack_toast',
       'syscache-update-pruned',
       'heap_lock_update',
diff --git a/src/test/modules/injection_points/specs/repack_deadlock.spec b/src/test/modules/injection_points/specs/repack_deadlock.spec
new file mode 100644
index 00000000000..9d23a6588c2
--- /dev/null
+++ b/src/test/modules/injection_points/specs/repack_deadlock.spec
@@ -0,0 +1,83 @@
+# Test REPACK with a concurrent transaction that would cause a deadlock
+setup
+{
+	CREATE EXTENSION injection_points;
+
+	CREATE TABLE repack_deadlock(i int PRIMARY KEY, j int);
+	INSERT INTO repack_deadlock(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4);
+
+	CREATE TABLE relfilenodes(node oid);
+
+	CREATE TABLE data_s1(i int, j int);
+	CREATE TABLE data_s2(i int, j int);
+}
+
+teardown
+{
+	DROP TABLE repack_deadlock;
+	DROP EXTENSION injection_points;
+
+	DROP TABLE relfilenodes;
+	DROP TABLE data_s1;
+	DROP TABLE data_s2;
+}
+
+session s1
+setup
+{
+	SELECT injection_points_set_local();
+	SELECT injection_points_attach('repack-concurrently-before-lock', 'wait');
+}
+# Perform the initial load and wait for s2 to do some data changes.
+step wait_before_lock
+{
+	REPACK (CONCURRENTLY) repack_deadlock USING INDEX repack_deadlock_pkey;
+}
+# Check the table from the perspective of s1.
+#
+# Besides the contents, we also check that relfilenode has changed.
+
+# Have each session write the contents into a table and use FULL JOIN to check
+# if the outputs are identical.
+step check1
+{
+	INSERT INTO relfilenodes(node)
+	SELECT relfilenode FROM pg_class WHERE relname='repack_deadlock';
+
+	SELECT count(DISTINCT node) FROM relfilenodes;
+
+	SELECT i, j FROM repack_deadlock ORDER BY i, j;
+
+	INSERT INTO data_s1(i, j)
+	SELECT i, j FROM repack_deadlock;
+
+	SELECT count(*)
+	FROM data_s1 d1 FULL JOIN data_s2 d2 USING (i, j)
+	WHERE d1.i ISNULL OR d2.i ISNULL;
+}
+teardown
+{
+	SELECT injection_points_detach('repack-concurrently-before-lock');
+}
+
+session s2
+# Change the existing data. UPDATE changes both key and non-key columns. Also
+# update one row twice to test whether tuple version generated by this session
+# can be found.
+step add_column
+{
+	alter table repack_deadlock add column noise text;
+}
+
+step wakeup_before_lock
+{
+	SELECT injection_points_wakeup('repack-concurrently-before-lock');
+}
+
+# Test if data changes introduced while one session is performing REPACK
+# CONCURRENTLY find their way into the table.
+permutation
+	wait_before_lock
+	add_column
+	wakeup_before_lock
+	check1
-- 
2.47.3


--fot5plqmb33ey33c--





^ permalink  raw  reply  [nested|flat] 89+ messages in thread

* [PATCH v44b 4/4] hack deadlock detector
@ 2026-03-25 19:35  Álvaro Herrera <[email protected]>
  0 siblings, 0 replies; 89+ messages in thread

From: Álvaro Herrera @ 2026-03-25 19:35 UTC (permalink / raw)

---
 src/backend/commands/cluster.c                | 10 +++
 src/backend/storage/lmgr/deadlock.c           | 12 +++
 src/include/storage/proc.h                    |  1 +
 src/test/modules/injection_points/Makefile    |  1 +
 .../expected/repack_deadlock.out              | 63 ++++++++++++++
 src/test/modules/injection_points/meson.build |  1 +
 .../specs/repack_deadlock.spec                | 83 +++++++++++++++++++
 7 files changed, 171 insertions(+)
 create mode 100644 src/test/modules/injection_points/expected/repack_deadlock.out
 create mode 100644 src/test/modules/injection_points/specs/repack_deadlock.spec

diff --git a/src/backend/commands/cluster.c b/src/backend/commands/cluster.c
index 2c3058ba10d..d5b1dfbff69 100644
--- a/src/backend/commands/cluster.c
+++ b/src/backend/commands/cluster.c
@@ -372,6 +372,16 @@ ExecRepack(ParseState *pstate, RepackStmt *stmt, bool isTopLevel)
 	/* Determine the lock mode to use. */
 	lockmode = RepackLockLevel((params.options & CLUOPT_CONCURRENT) != 0);
 
+	/*
+	 * If in concurrent mode, set the PROC_IN_CONCURRENT_REPACK flag.  This
+	 * makes the deadlock checker cause anyone that would conflict with us
+	 * to error out.  It's important to set this flag ahead of actually locking
+	 * the relation; it won't of course affect anyone until we do have a lock
+	 * that others can conflict with.
+	 */
+	if ((params.options & CLUOPT_CONCURRENT) != 0)
+		MyProc->statusFlags |= PROC_IN_CONCURRENT_REPACK;
+
 	/*
 	 * If a single relation is specified, process it and we're done ... unless
 	 * the relation is a partitioned table, in which case we fall through.
diff --git a/src/backend/storage/lmgr/deadlock.c b/src/backend/storage/lmgr/deadlock.c
index b8962d875b6..10266e384aa 100644
--- a/src/backend/storage/lmgr/deadlock.c
+++ b/src/backend/storage/lmgr/deadlock.c
@@ -620,6 +620,18 @@ FindLockCycleRecurseMember(PGPROC *checkProc,
 						proc->statusFlags & PROC_IS_AUTOVACUUM)
 						blocking_autovacuum_proc = proc;
 
+					/*
+					 * If we note that we're blocked by some process running
+					 * REPACK (CONCURRENTLY), just fail.  That process is
+					 * going to upgrade its lock at some point, and it would
+					 * be inappropriate for any other process to cause that
+					 * to fail.
+					 */
+					if (checkProc == MyProc &&
+						proc->statusFlags & PROC_IN_CONCURRENT_REPACK)
+						/* not a serious error message proposal */
+						elog(ERROR, "oops, we're blocked by REPACK, time to die");
+
 					/* We're done looking at this proclock */
 					break;
 				}
diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h
index 1dad125706e..4e340ead455 100644
--- a/src/include/storage/proc.h
+++ b/src/include/storage/proc.h
@@ -69,6 +69,7 @@ struct XidCache
 #define		PROC_AFFECTS_ALL_HORIZONS	0x20	/* this proc's xmin must be
 												 * included in vacuum horizons
 												 * in all databases */
+#define		PROC_IN_CONCURRENT_REPACK	0x40	/* REPACK (CONCURRENTLY) */
 
 /* flags reset at EOXact */
 #define		PROC_VACUUM_STATE_MASK \
diff --git a/src/test/modules/injection_points/Makefile b/src/test/modules/injection_points/Makefile
index 2cd7d87c533..f7663859fe2 100644
--- a/src/test/modules/injection_points/Makefile
+++ b/src/test/modules/injection_points/Makefile
@@ -15,6 +15,7 @@ REGRESS_OPTS = --dlpath=$(top_builddir)/src/test/regress
 ISOLATION = basic \
 	    inplace \
 	    repack \
+	    repack_deadlock \
 	    repack_toast \
 	    syscache-update-pruned \
 	    heap_lock_update
diff --git a/src/test/modules/injection_points/expected/repack_deadlock.out b/src/test/modules/injection_points/expected/repack_deadlock.out
new file mode 100644
index 00000000000..42505d10933
--- /dev/null
+++ b/src/test/modules/injection_points/expected/repack_deadlock.out
@@ -0,0 +1,63 @@
+Parsed test spec with 2 sessions
+
+starting permutation: wait_before_lock add_column wakeup_before_lock check1
+injection_points_attach
+-----------------------
+                       
+(1 row)
+
+step wait_before_lock: 
+	REPACK (CONCURRENTLY) repack_deadlock USING INDEX repack_deadlock_pkey;
+ <waiting ...>
+step add_column: 
+	alter table repack_deadlock add column noise text;
+ <waiting ...>
+step add_column: <... completed>
+ERROR:  oops, we're blocked by REPACK, time to die
+step wakeup_before_lock: 
+	SELECT injection_points_wakeup('repack-concurrently-before-lock');
+
+injection_points_wakeup
+-----------------------
+                       
+(1 row)
+
+step wait_before_lock: <... completed>
+step check1: 
+	INSERT INTO relfilenodes(node)
+	SELECT relfilenode FROM pg_class WHERE relname='repack_deadlock';
+
+	SELECT count(DISTINCT node) FROM relfilenodes;
+
+	SELECT i, j FROM repack_deadlock ORDER BY i, j;
+
+	INSERT INTO data_s1(i, j)
+	SELECT i, j FROM repack_deadlock;
+
+	SELECT count(*)
+	FROM data_s1 d1 FULL JOIN data_s2 d2 USING (i, j)
+	WHERE d1.i ISNULL OR d2.i ISNULL;
+
+count
+-----
+    1
+(1 row)
+
+i|j
+-+-
+1|1
+2|2
+3|3
+4|4
+(4 rows)
+
+count
+-----
+    4
+(1 row)
+
+injection_points_detach
+-----------------------
+                       
+(1 row)
+
diff --git a/src/test/modules/injection_points/meson.build b/src/test/modules/injection_points/meson.build
index a414abb924b..1cd88d6db65 100644
--- a/src/test/modules/injection_points/meson.build
+++ b/src/test/modules/injection_points/meson.build
@@ -46,6 +46,7 @@ tests += {
       'basic',
       'inplace',
       'repack',
+      'repack_deadlock',
       'repack_toast',
       'syscache-update-pruned',
       'heap_lock_update',
diff --git a/src/test/modules/injection_points/specs/repack_deadlock.spec b/src/test/modules/injection_points/specs/repack_deadlock.spec
new file mode 100644
index 00000000000..9d23a6588c2
--- /dev/null
+++ b/src/test/modules/injection_points/specs/repack_deadlock.spec
@@ -0,0 +1,83 @@
+# Test REPACK with a concurrent transaction that would cause a deadlock
+setup
+{
+	CREATE EXTENSION injection_points;
+
+	CREATE TABLE repack_deadlock(i int PRIMARY KEY, j int);
+	INSERT INTO repack_deadlock(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4);
+
+	CREATE TABLE relfilenodes(node oid);
+
+	CREATE TABLE data_s1(i int, j int);
+	CREATE TABLE data_s2(i int, j int);
+}
+
+teardown
+{
+	DROP TABLE repack_deadlock;
+	DROP EXTENSION injection_points;
+
+	DROP TABLE relfilenodes;
+	DROP TABLE data_s1;
+	DROP TABLE data_s2;
+}
+
+session s1
+setup
+{
+	SELECT injection_points_set_local();
+	SELECT injection_points_attach('repack-concurrently-before-lock', 'wait');
+}
+# Perform the initial load and wait for s2 to do some data changes.
+step wait_before_lock
+{
+	REPACK (CONCURRENTLY) repack_deadlock USING INDEX repack_deadlock_pkey;
+}
+# Check the table from the perspective of s1.
+#
+# Besides the contents, we also check that relfilenode has changed.
+
+# Have each session write the contents into a table and use FULL JOIN to check
+# if the outputs are identical.
+step check1
+{
+	INSERT INTO relfilenodes(node)
+	SELECT relfilenode FROM pg_class WHERE relname='repack_deadlock';
+
+	SELECT count(DISTINCT node) FROM relfilenodes;
+
+	SELECT i, j FROM repack_deadlock ORDER BY i, j;
+
+	INSERT INTO data_s1(i, j)
+	SELECT i, j FROM repack_deadlock;
+
+	SELECT count(*)
+	FROM data_s1 d1 FULL JOIN data_s2 d2 USING (i, j)
+	WHERE d1.i ISNULL OR d2.i ISNULL;
+}
+teardown
+{
+	SELECT injection_points_detach('repack-concurrently-before-lock');
+}
+
+session s2
+# Change the existing data. UPDATE changes both key and non-key columns. Also
+# update one row twice to test whether tuple version generated by this session
+# can be found.
+step add_column
+{
+	alter table repack_deadlock add column noise text;
+}
+
+step wakeup_before_lock
+{
+	SELECT injection_points_wakeup('repack-concurrently-before-lock');
+}
+
+# Test if data changes introduced while one session is performing REPACK
+# CONCURRENTLY find their way into the table.
+permutation
+	wait_before_lock
+	add_column
+	wakeup_before_lock
+	check1
-- 
2.47.3


--fot5plqmb33ey33c--





^ permalink  raw  reply  [nested|flat] 89+ messages in thread

* [PATCH v44b 4/4] hack deadlock detector
@ 2026-03-25 19:35  Álvaro Herrera <[email protected]>
  0 siblings, 0 replies; 89+ messages in thread

From: Álvaro Herrera @ 2026-03-25 19:35 UTC (permalink / raw)

---
 src/backend/commands/cluster.c                | 10 +++
 src/backend/storage/lmgr/deadlock.c           | 12 +++
 src/include/storage/proc.h                    |  1 +
 src/test/modules/injection_points/Makefile    |  1 +
 .../expected/repack_deadlock.out              | 63 ++++++++++++++
 src/test/modules/injection_points/meson.build |  1 +
 .../specs/repack_deadlock.spec                | 83 +++++++++++++++++++
 7 files changed, 171 insertions(+)
 create mode 100644 src/test/modules/injection_points/expected/repack_deadlock.out
 create mode 100644 src/test/modules/injection_points/specs/repack_deadlock.spec

diff --git a/src/backend/commands/cluster.c b/src/backend/commands/cluster.c
index 2c3058ba10d..d5b1dfbff69 100644
--- a/src/backend/commands/cluster.c
+++ b/src/backend/commands/cluster.c
@@ -372,6 +372,16 @@ ExecRepack(ParseState *pstate, RepackStmt *stmt, bool isTopLevel)
 	/* Determine the lock mode to use. */
 	lockmode = RepackLockLevel((params.options & CLUOPT_CONCURRENT) != 0);
 
+	/*
+	 * If in concurrent mode, set the PROC_IN_CONCURRENT_REPACK flag.  This
+	 * makes the deadlock checker cause anyone that would conflict with us
+	 * to error out.  It's important to set this flag ahead of actually locking
+	 * the relation; it won't of course affect anyone until we do have a lock
+	 * that others can conflict with.
+	 */
+	if ((params.options & CLUOPT_CONCURRENT) != 0)
+		MyProc->statusFlags |= PROC_IN_CONCURRENT_REPACK;
+
 	/*
 	 * If a single relation is specified, process it and we're done ... unless
 	 * the relation is a partitioned table, in which case we fall through.
diff --git a/src/backend/storage/lmgr/deadlock.c b/src/backend/storage/lmgr/deadlock.c
index b8962d875b6..10266e384aa 100644
--- a/src/backend/storage/lmgr/deadlock.c
+++ b/src/backend/storage/lmgr/deadlock.c
@@ -620,6 +620,18 @@ FindLockCycleRecurseMember(PGPROC *checkProc,
 						proc->statusFlags & PROC_IS_AUTOVACUUM)
 						blocking_autovacuum_proc = proc;
 
+					/*
+					 * If we note that we're blocked by some process running
+					 * REPACK (CONCURRENTLY), just fail.  That process is
+					 * going to upgrade its lock at some point, and it would
+					 * be inappropriate for any other process to cause that
+					 * to fail.
+					 */
+					if (checkProc == MyProc &&
+						proc->statusFlags & PROC_IN_CONCURRENT_REPACK)
+						/* not a serious error message proposal */
+						elog(ERROR, "oops, we're blocked by REPACK, time to die");
+
 					/* We're done looking at this proclock */
 					break;
 				}
diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h
index 1dad125706e..4e340ead455 100644
--- a/src/include/storage/proc.h
+++ b/src/include/storage/proc.h
@@ -69,6 +69,7 @@ struct XidCache
 #define		PROC_AFFECTS_ALL_HORIZONS	0x20	/* this proc's xmin must be
 												 * included in vacuum horizons
 												 * in all databases */
+#define		PROC_IN_CONCURRENT_REPACK	0x40	/* REPACK (CONCURRENTLY) */
 
 /* flags reset at EOXact */
 #define		PROC_VACUUM_STATE_MASK \
diff --git a/src/test/modules/injection_points/Makefile b/src/test/modules/injection_points/Makefile
index 2cd7d87c533..f7663859fe2 100644
--- a/src/test/modules/injection_points/Makefile
+++ b/src/test/modules/injection_points/Makefile
@@ -15,6 +15,7 @@ REGRESS_OPTS = --dlpath=$(top_builddir)/src/test/regress
 ISOLATION = basic \
 	    inplace \
 	    repack \
+	    repack_deadlock \
 	    repack_toast \
 	    syscache-update-pruned \
 	    heap_lock_update
diff --git a/src/test/modules/injection_points/expected/repack_deadlock.out b/src/test/modules/injection_points/expected/repack_deadlock.out
new file mode 100644
index 00000000000..42505d10933
--- /dev/null
+++ b/src/test/modules/injection_points/expected/repack_deadlock.out
@@ -0,0 +1,63 @@
+Parsed test spec with 2 sessions
+
+starting permutation: wait_before_lock add_column wakeup_before_lock check1
+injection_points_attach
+-----------------------
+                       
+(1 row)
+
+step wait_before_lock: 
+	REPACK (CONCURRENTLY) repack_deadlock USING INDEX repack_deadlock_pkey;
+ <waiting ...>
+step add_column: 
+	alter table repack_deadlock add column noise text;
+ <waiting ...>
+step add_column: <... completed>
+ERROR:  oops, we're blocked by REPACK, time to die
+step wakeup_before_lock: 
+	SELECT injection_points_wakeup('repack-concurrently-before-lock');
+
+injection_points_wakeup
+-----------------------
+                       
+(1 row)
+
+step wait_before_lock: <... completed>
+step check1: 
+	INSERT INTO relfilenodes(node)
+	SELECT relfilenode FROM pg_class WHERE relname='repack_deadlock';
+
+	SELECT count(DISTINCT node) FROM relfilenodes;
+
+	SELECT i, j FROM repack_deadlock ORDER BY i, j;
+
+	INSERT INTO data_s1(i, j)
+	SELECT i, j FROM repack_deadlock;
+
+	SELECT count(*)
+	FROM data_s1 d1 FULL JOIN data_s2 d2 USING (i, j)
+	WHERE d1.i ISNULL OR d2.i ISNULL;
+
+count
+-----
+    1
+(1 row)
+
+i|j
+-+-
+1|1
+2|2
+3|3
+4|4
+(4 rows)
+
+count
+-----
+    4
+(1 row)
+
+injection_points_detach
+-----------------------
+                       
+(1 row)
+
diff --git a/src/test/modules/injection_points/meson.build b/src/test/modules/injection_points/meson.build
index a414abb924b..1cd88d6db65 100644
--- a/src/test/modules/injection_points/meson.build
+++ b/src/test/modules/injection_points/meson.build
@@ -46,6 +46,7 @@ tests += {
       'basic',
       'inplace',
       'repack',
+      'repack_deadlock',
       'repack_toast',
       'syscache-update-pruned',
       'heap_lock_update',
diff --git a/src/test/modules/injection_points/specs/repack_deadlock.spec b/src/test/modules/injection_points/specs/repack_deadlock.spec
new file mode 100644
index 00000000000..9d23a6588c2
--- /dev/null
+++ b/src/test/modules/injection_points/specs/repack_deadlock.spec
@@ -0,0 +1,83 @@
+# Test REPACK with a concurrent transaction that would cause a deadlock
+setup
+{
+	CREATE EXTENSION injection_points;
+
+	CREATE TABLE repack_deadlock(i int PRIMARY KEY, j int);
+	INSERT INTO repack_deadlock(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4);
+
+	CREATE TABLE relfilenodes(node oid);
+
+	CREATE TABLE data_s1(i int, j int);
+	CREATE TABLE data_s2(i int, j int);
+}
+
+teardown
+{
+	DROP TABLE repack_deadlock;
+	DROP EXTENSION injection_points;
+
+	DROP TABLE relfilenodes;
+	DROP TABLE data_s1;
+	DROP TABLE data_s2;
+}
+
+session s1
+setup
+{
+	SELECT injection_points_set_local();
+	SELECT injection_points_attach('repack-concurrently-before-lock', 'wait');
+}
+# Perform the initial load and wait for s2 to do some data changes.
+step wait_before_lock
+{
+	REPACK (CONCURRENTLY) repack_deadlock USING INDEX repack_deadlock_pkey;
+}
+# Check the table from the perspective of s1.
+#
+# Besides the contents, we also check that relfilenode has changed.
+
+# Have each session write the contents into a table and use FULL JOIN to check
+# if the outputs are identical.
+step check1
+{
+	INSERT INTO relfilenodes(node)
+	SELECT relfilenode FROM pg_class WHERE relname='repack_deadlock';
+
+	SELECT count(DISTINCT node) FROM relfilenodes;
+
+	SELECT i, j FROM repack_deadlock ORDER BY i, j;
+
+	INSERT INTO data_s1(i, j)
+	SELECT i, j FROM repack_deadlock;
+
+	SELECT count(*)
+	FROM data_s1 d1 FULL JOIN data_s2 d2 USING (i, j)
+	WHERE d1.i ISNULL OR d2.i ISNULL;
+}
+teardown
+{
+	SELECT injection_points_detach('repack-concurrently-before-lock');
+}
+
+session s2
+# Change the existing data. UPDATE changes both key and non-key columns. Also
+# update one row twice to test whether tuple version generated by this session
+# can be found.
+step add_column
+{
+	alter table repack_deadlock add column noise text;
+}
+
+step wakeup_before_lock
+{
+	SELECT injection_points_wakeup('repack-concurrently-before-lock');
+}
+
+# Test if data changes introduced while one session is performing REPACK
+# CONCURRENTLY find their way into the table.
+permutation
+	wait_before_lock
+	add_column
+	wakeup_before_lock
+	check1
-- 
2.47.3


--fot5plqmb33ey33c--





^ permalink  raw  reply  [nested|flat] 89+ messages in thread

* [PATCH v44b 4/4] hack deadlock detector
@ 2026-03-25 19:35  Álvaro Herrera <[email protected]>
  0 siblings, 0 replies; 89+ messages in thread

From: Álvaro Herrera @ 2026-03-25 19:35 UTC (permalink / raw)

---
 src/backend/commands/cluster.c                | 10 +++
 src/backend/storage/lmgr/deadlock.c           | 12 +++
 src/include/storage/proc.h                    |  1 +
 src/test/modules/injection_points/Makefile    |  1 +
 .../expected/repack_deadlock.out              | 63 ++++++++++++++
 src/test/modules/injection_points/meson.build |  1 +
 .../specs/repack_deadlock.spec                | 83 +++++++++++++++++++
 7 files changed, 171 insertions(+)
 create mode 100644 src/test/modules/injection_points/expected/repack_deadlock.out
 create mode 100644 src/test/modules/injection_points/specs/repack_deadlock.spec

diff --git a/src/backend/commands/cluster.c b/src/backend/commands/cluster.c
index 2c3058ba10d..d5b1dfbff69 100644
--- a/src/backend/commands/cluster.c
+++ b/src/backend/commands/cluster.c
@@ -372,6 +372,16 @@ ExecRepack(ParseState *pstate, RepackStmt *stmt, bool isTopLevel)
 	/* Determine the lock mode to use. */
 	lockmode = RepackLockLevel((params.options & CLUOPT_CONCURRENT) != 0);
 
+	/*
+	 * If in concurrent mode, set the PROC_IN_CONCURRENT_REPACK flag.  This
+	 * makes the deadlock checker cause anyone that would conflict with us
+	 * to error out.  It's important to set this flag ahead of actually locking
+	 * the relation; it won't of course affect anyone until we do have a lock
+	 * that others can conflict with.
+	 */
+	if ((params.options & CLUOPT_CONCURRENT) != 0)
+		MyProc->statusFlags |= PROC_IN_CONCURRENT_REPACK;
+
 	/*
 	 * If a single relation is specified, process it and we're done ... unless
 	 * the relation is a partitioned table, in which case we fall through.
diff --git a/src/backend/storage/lmgr/deadlock.c b/src/backend/storage/lmgr/deadlock.c
index b8962d875b6..10266e384aa 100644
--- a/src/backend/storage/lmgr/deadlock.c
+++ b/src/backend/storage/lmgr/deadlock.c
@@ -620,6 +620,18 @@ FindLockCycleRecurseMember(PGPROC *checkProc,
 						proc->statusFlags & PROC_IS_AUTOVACUUM)
 						blocking_autovacuum_proc = proc;
 
+					/*
+					 * If we note that we're blocked by some process running
+					 * REPACK (CONCURRENTLY), just fail.  That process is
+					 * going to upgrade its lock at some point, and it would
+					 * be inappropriate for any other process to cause that
+					 * to fail.
+					 */
+					if (checkProc == MyProc &&
+						proc->statusFlags & PROC_IN_CONCURRENT_REPACK)
+						/* not a serious error message proposal */
+						elog(ERROR, "oops, we're blocked by REPACK, time to die");
+
 					/* We're done looking at this proclock */
 					break;
 				}
diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h
index 1dad125706e..4e340ead455 100644
--- a/src/include/storage/proc.h
+++ b/src/include/storage/proc.h
@@ -69,6 +69,7 @@ struct XidCache
 #define		PROC_AFFECTS_ALL_HORIZONS	0x20	/* this proc's xmin must be
 												 * included in vacuum horizons
 												 * in all databases */
+#define		PROC_IN_CONCURRENT_REPACK	0x40	/* REPACK (CONCURRENTLY) */
 
 /* flags reset at EOXact */
 #define		PROC_VACUUM_STATE_MASK \
diff --git a/src/test/modules/injection_points/Makefile b/src/test/modules/injection_points/Makefile
index 2cd7d87c533..f7663859fe2 100644
--- a/src/test/modules/injection_points/Makefile
+++ b/src/test/modules/injection_points/Makefile
@@ -15,6 +15,7 @@ REGRESS_OPTS = --dlpath=$(top_builddir)/src/test/regress
 ISOLATION = basic \
 	    inplace \
 	    repack \
+	    repack_deadlock \
 	    repack_toast \
 	    syscache-update-pruned \
 	    heap_lock_update
diff --git a/src/test/modules/injection_points/expected/repack_deadlock.out b/src/test/modules/injection_points/expected/repack_deadlock.out
new file mode 100644
index 00000000000..42505d10933
--- /dev/null
+++ b/src/test/modules/injection_points/expected/repack_deadlock.out
@@ -0,0 +1,63 @@
+Parsed test spec with 2 sessions
+
+starting permutation: wait_before_lock add_column wakeup_before_lock check1
+injection_points_attach
+-----------------------
+                       
+(1 row)
+
+step wait_before_lock: 
+	REPACK (CONCURRENTLY) repack_deadlock USING INDEX repack_deadlock_pkey;
+ <waiting ...>
+step add_column: 
+	alter table repack_deadlock add column noise text;
+ <waiting ...>
+step add_column: <... completed>
+ERROR:  oops, we're blocked by REPACK, time to die
+step wakeup_before_lock: 
+	SELECT injection_points_wakeup('repack-concurrently-before-lock');
+
+injection_points_wakeup
+-----------------------
+                       
+(1 row)
+
+step wait_before_lock: <... completed>
+step check1: 
+	INSERT INTO relfilenodes(node)
+	SELECT relfilenode FROM pg_class WHERE relname='repack_deadlock';
+
+	SELECT count(DISTINCT node) FROM relfilenodes;
+
+	SELECT i, j FROM repack_deadlock ORDER BY i, j;
+
+	INSERT INTO data_s1(i, j)
+	SELECT i, j FROM repack_deadlock;
+
+	SELECT count(*)
+	FROM data_s1 d1 FULL JOIN data_s2 d2 USING (i, j)
+	WHERE d1.i ISNULL OR d2.i ISNULL;
+
+count
+-----
+    1
+(1 row)
+
+i|j
+-+-
+1|1
+2|2
+3|3
+4|4
+(4 rows)
+
+count
+-----
+    4
+(1 row)
+
+injection_points_detach
+-----------------------
+                       
+(1 row)
+
diff --git a/src/test/modules/injection_points/meson.build b/src/test/modules/injection_points/meson.build
index a414abb924b..1cd88d6db65 100644
--- a/src/test/modules/injection_points/meson.build
+++ b/src/test/modules/injection_points/meson.build
@@ -46,6 +46,7 @@ tests += {
       'basic',
       'inplace',
       'repack',
+      'repack_deadlock',
       'repack_toast',
       'syscache-update-pruned',
       'heap_lock_update',
diff --git a/src/test/modules/injection_points/specs/repack_deadlock.spec b/src/test/modules/injection_points/specs/repack_deadlock.spec
new file mode 100644
index 00000000000..9d23a6588c2
--- /dev/null
+++ b/src/test/modules/injection_points/specs/repack_deadlock.spec
@@ -0,0 +1,83 @@
+# Test REPACK with a concurrent transaction that would cause a deadlock
+setup
+{
+	CREATE EXTENSION injection_points;
+
+	CREATE TABLE repack_deadlock(i int PRIMARY KEY, j int);
+	INSERT INTO repack_deadlock(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4);
+
+	CREATE TABLE relfilenodes(node oid);
+
+	CREATE TABLE data_s1(i int, j int);
+	CREATE TABLE data_s2(i int, j int);
+}
+
+teardown
+{
+	DROP TABLE repack_deadlock;
+	DROP EXTENSION injection_points;
+
+	DROP TABLE relfilenodes;
+	DROP TABLE data_s1;
+	DROP TABLE data_s2;
+}
+
+session s1
+setup
+{
+	SELECT injection_points_set_local();
+	SELECT injection_points_attach('repack-concurrently-before-lock', 'wait');
+}
+# Perform the initial load and wait for s2 to do some data changes.
+step wait_before_lock
+{
+	REPACK (CONCURRENTLY) repack_deadlock USING INDEX repack_deadlock_pkey;
+}
+# Check the table from the perspective of s1.
+#
+# Besides the contents, we also check that relfilenode has changed.
+
+# Have each session write the contents into a table and use FULL JOIN to check
+# if the outputs are identical.
+step check1
+{
+	INSERT INTO relfilenodes(node)
+	SELECT relfilenode FROM pg_class WHERE relname='repack_deadlock';
+
+	SELECT count(DISTINCT node) FROM relfilenodes;
+
+	SELECT i, j FROM repack_deadlock ORDER BY i, j;
+
+	INSERT INTO data_s1(i, j)
+	SELECT i, j FROM repack_deadlock;
+
+	SELECT count(*)
+	FROM data_s1 d1 FULL JOIN data_s2 d2 USING (i, j)
+	WHERE d1.i ISNULL OR d2.i ISNULL;
+}
+teardown
+{
+	SELECT injection_points_detach('repack-concurrently-before-lock');
+}
+
+session s2
+# Change the existing data. UPDATE changes both key and non-key columns. Also
+# update one row twice to test whether tuple version generated by this session
+# can be found.
+step add_column
+{
+	alter table repack_deadlock add column noise text;
+}
+
+step wakeup_before_lock
+{
+	SELECT injection_points_wakeup('repack-concurrently-before-lock');
+}
+
+# Test if data changes introduced while one session is performing REPACK
+# CONCURRENTLY find their way into the table.
+permutation
+	wait_before_lock
+	add_column
+	wakeup_before_lock
+	check1
-- 
2.47.3


--fot5plqmb33ey33c--





^ permalink  raw  reply  [nested|flat] 89+ messages in thread

* [PATCH v44b 4/4] hack deadlock detector
@ 2026-03-25 19:35  Álvaro Herrera <[email protected]>
  0 siblings, 0 replies; 89+ messages in thread

From: Álvaro Herrera @ 2026-03-25 19:35 UTC (permalink / raw)

---
 src/backend/commands/cluster.c                | 10 +++
 src/backend/storage/lmgr/deadlock.c           | 12 +++
 src/include/storage/proc.h                    |  1 +
 src/test/modules/injection_points/Makefile    |  1 +
 .../expected/repack_deadlock.out              | 63 ++++++++++++++
 src/test/modules/injection_points/meson.build |  1 +
 .../specs/repack_deadlock.spec                | 83 +++++++++++++++++++
 7 files changed, 171 insertions(+)
 create mode 100644 src/test/modules/injection_points/expected/repack_deadlock.out
 create mode 100644 src/test/modules/injection_points/specs/repack_deadlock.spec

diff --git a/src/backend/commands/cluster.c b/src/backend/commands/cluster.c
index 2c3058ba10d..d5b1dfbff69 100644
--- a/src/backend/commands/cluster.c
+++ b/src/backend/commands/cluster.c
@@ -372,6 +372,16 @@ ExecRepack(ParseState *pstate, RepackStmt *stmt, bool isTopLevel)
 	/* Determine the lock mode to use. */
 	lockmode = RepackLockLevel((params.options & CLUOPT_CONCURRENT) != 0);
 
+	/*
+	 * If in concurrent mode, set the PROC_IN_CONCURRENT_REPACK flag.  This
+	 * makes the deadlock checker cause anyone that would conflict with us
+	 * to error out.  It's important to set this flag ahead of actually locking
+	 * the relation; it won't of course affect anyone until we do have a lock
+	 * that others can conflict with.
+	 */
+	if ((params.options & CLUOPT_CONCURRENT) != 0)
+		MyProc->statusFlags |= PROC_IN_CONCURRENT_REPACK;
+
 	/*
 	 * If a single relation is specified, process it and we're done ... unless
 	 * the relation is a partitioned table, in which case we fall through.
diff --git a/src/backend/storage/lmgr/deadlock.c b/src/backend/storage/lmgr/deadlock.c
index b8962d875b6..10266e384aa 100644
--- a/src/backend/storage/lmgr/deadlock.c
+++ b/src/backend/storage/lmgr/deadlock.c
@@ -620,6 +620,18 @@ FindLockCycleRecurseMember(PGPROC *checkProc,
 						proc->statusFlags & PROC_IS_AUTOVACUUM)
 						blocking_autovacuum_proc = proc;
 
+					/*
+					 * If we note that we're blocked by some process running
+					 * REPACK (CONCURRENTLY), just fail.  That process is
+					 * going to upgrade its lock at some point, and it would
+					 * be inappropriate for any other process to cause that
+					 * to fail.
+					 */
+					if (checkProc == MyProc &&
+						proc->statusFlags & PROC_IN_CONCURRENT_REPACK)
+						/* not a serious error message proposal */
+						elog(ERROR, "oops, we're blocked by REPACK, time to die");
+
 					/* We're done looking at this proclock */
 					break;
 				}
diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h
index 1dad125706e..4e340ead455 100644
--- a/src/include/storage/proc.h
+++ b/src/include/storage/proc.h
@@ -69,6 +69,7 @@ struct XidCache
 #define		PROC_AFFECTS_ALL_HORIZONS	0x20	/* this proc's xmin must be
 												 * included in vacuum horizons
 												 * in all databases */
+#define		PROC_IN_CONCURRENT_REPACK	0x40	/* REPACK (CONCURRENTLY) */
 
 /* flags reset at EOXact */
 #define		PROC_VACUUM_STATE_MASK \
diff --git a/src/test/modules/injection_points/Makefile b/src/test/modules/injection_points/Makefile
index 2cd7d87c533..f7663859fe2 100644
--- a/src/test/modules/injection_points/Makefile
+++ b/src/test/modules/injection_points/Makefile
@@ -15,6 +15,7 @@ REGRESS_OPTS = --dlpath=$(top_builddir)/src/test/regress
 ISOLATION = basic \
 	    inplace \
 	    repack \
+	    repack_deadlock \
 	    repack_toast \
 	    syscache-update-pruned \
 	    heap_lock_update
diff --git a/src/test/modules/injection_points/expected/repack_deadlock.out b/src/test/modules/injection_points/expected/repack_deadlock.out
new file mode 100644
index 00000000000..42505d10933
--- /dev/null
+++ b/src/test/modules/injection_points/expected/repack_deadlock.out
@@ -0,0 +1,63 @@
+Parsed test spec with 2 sessions
+
+starting permutation: wait_before_lock add_column wakeup_before_lock check1
+injection_points_attach
+-----------------------
+                       
+(1 row)
+
+step wait_before_lock: 
+	REPACK (CONCURRENTLY) repack_deadlock USING INDEX repack_deadlock_pkey;
+ <waiting ...>
+step add_column: 
+	alter table repack_deadlock add column noise text;
+ <waiting ...>
+step add_column: <... completed>
+ERROR:  oops, we're blocked by REPACK, time to die
+step wakeup_before_lock: 
+	SELECT injection_points_wakeup('repack-concurrently-before-lock');
+
+injection_points_wakeup
+-----------------------
+                       
+(1 row)
+
+step wait_before_lock: <... completed>
+step check1: 
+	INSERT INTO relfilenodes(node)
+	SELECT relfilenode FROM pg_class WHERE relname='repack_deadlock';
+
+	SELECT count(DISTINCT node) FROM relfilenodes;
+
+	SELECT i, j FROM repack_deadlock ORDER BY i, j;
+
+	INSERT INTO data_s1(i, j)
+	SELECT i, j FROM repack_deadlock;
+
+	SELECT count(*)
+	FROM data_s1 d1 FULL JOIN data_s2 d2 USING (i, j)
+	WHERE d1.i ISNULL OR d2.i ISNULL;
+
+count
+-----
+    1
+(1 row)
+
+i|j
+-+-
+1|1
+2|2
+3|3
+4|4
+(4 rows)
+
+count
+-----
+    4
+(1 row)
+
+injection_points_detach
+-----------------------
+                       
+(1 row)
+
diff --git a/src/test/modules/injection_points/meson.build b/src/test/modules/injection_points/meson.build
index a414abb924b..1cd88d6db65 100644
--- a/src/test/modules/injection_points/meson.build
+++ b/src/test/modules/injection_points/meson.build
@@ -46,6 +46,7 @@ tests += {
       'basic',
       'inplace',
       'repack',
+      'repack_deadlock',
       'repack_toast',
       'syscache-update-pruned',
       'heap_lock_update',
diff --git a/src/test/modules/injection_points/specs/repack_deadlock.spec b/src/test/modules/injection_points/specs/repack_deadlock.spec
new file mode 100644
index 00000000000..9d23a6588c2
--- /dev/null
+++ b/src/test/modules/injection_points/specs/repack_deadlock.spec
@@ -0,0 +1,83 @@
+# Test REPACK with a concurrent transaction that would cause a deadlock
+setup
+{
+	CREATE EXTENSION injection_points;
+
+	CREATE TABLE repack_deadlock(i int PRIMARY KEY, j int);
+	INSERT INTO repack_deadlock(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4);
+
+	CREATE TABLE relfilenodes(node oid);
+
+	CREATE TABLE data_s1(i int, j int);
+	CREATE TABLE data_s2(i int, j int);
+}
+
+teardown
+{
+	DROP TABLE repack_deadlock;
+	DROP EXTENSION injection_points;
+
+	DROP TABLE relfilenodes;
+	DROP TABLE data_s1;
+	DROP TABLE data_s2;
+}
+
+session s1
+setup
+{
+	SELECT injection_points_set_local();
+	SELECT injection_points_attach('repack-concurrently-before-lock', 'wait');
+}
+# Perform the initial load and wait for s2 to do some data changes.
+step wait_before_lock
+{
+	REPACK (CONCURRENTLY) repack_deadlock USING INDEX repack_deadlock_pkey;
+}
+# Check the table from the perspective of s1.
+#
+# Besides the contents, we also check that relfilenode has changed.
+
+# Have each session write the contents into a table and use FULL JOIN to check
+# if the outputs are identical.
+step check1
+{
+	INSERT INTO relfilenodes(node)
+	SELECT relfilenode FROM pg_class WHERE relname='repack_deadlock';
+
+	SELECT count(DISTINCT node) FROM relfilenodes;
+
+	SELECT i, j FROM repack_deadlock ORDER BY i, j;
+
+	INSERT INTO data_s1(i, j)
+	SELECT i, j FROM repack_deadlock;
+
+	SELECT count(*)
+	FROM data_s1 d1 FULL JOIN data_s2 d2 USING (i, j)
+	WHERE d1.i ISNULL OR d2.i ISNULL;
+}
+teardown
+{
+	SELECT injection_points_detach('repack-concurrently-before-lock');
+}
+
+session s2
+# Change the existing data. UPDATE changes both key and non-key columns. Also
+# update one row twice to test whether tuple version generated by this session
+# can be found.
+step add_column
+{
+	alter table repack_deadlock add column noise text;
+}
+
+step wakeup_before_lock
+{
+	SELECT injection_points_wakeup('repack-concurrently-before-lock');
+}
+
+# Test if data changes introduced while one session is performing REPACK
+# CONCURRENTLY find their way into the table.
+permutation
+	wait_before_lock
+	add_column
+	wakeup_before_lock
+	check1
-- 
2.47.3


--fot5plqmb33ey33c--





^ permalink  raw  reply  [nested|flat] 89+ messages in thread

* [PATCH v44b 4/4] hack deadlock detector
@ 2026-03-25 19:35  Álvaro Herrera <[email protected]>
  0 siblings, 0 replies; 89+ messages in thread

From: Álvaro Herrera @ 2026-03-25 19:35 UTC (permalink / raw)

---
 src/backend/commands/cluster.c                | 10 +++
 src/backend/storage/lmgr/deadlock.c           | 12 +++
 src/include/storage/proc.h                    |  1 +
 src/test/modules/injection_points/Makefile    |  1 +
 .../expected/repack_deadlock.out              | 63 ++++++++++++++
 src/test/modules/injection_points/meson.build |  1 +
 .../specs/repack_deadlock.spec                | 83 +++++++++++++++++++
 7 files changed, 171 insertions(+)
 create mode 100644 src/test/modules/injection_points/expected/repack_deadlock.out
 create mode 100644 src/test/modules/injection_points/specs/repack_deadlock.spec

diff --git a/src/backend/commands/cluster.c b/src/backend/commands/cluster.c
index 2c3058ba10d..d5b1dfbff69 100644
--- a/src/backend/commands/cluster.c
+++ b/src/backend/commands/cluster.c
@@ -372,6 +372,16 @@ ExecRepack(ParseState *pstate, RepackStmt *stmt, bool isTopLevel)
 	/* Determine the lock mode to use. */
 	lockmode = RepackLockLevel((params.options & CLUOPT_CONCURRENT) != 0);
 
+	/*
+	 * If in concurrent mode, set the PROC_IN_CONCURRENT_REPACK flag.  This
+	 * makes the deadlock checker cause anyone that would conflict with us
+	 * to error out.  It's important to set this flag ahead of actually locking
+	 * the relation; it won't of course affect anyone until we do have a lock
+	 * that others can conflict with.
+	 */
+	if ((params.options & CLUOPT_CONCURRENT) != 0)
+		MyProc->statusFlags |= PROC_IN_CONCURRENT_REPACK;
+
 	/*
 	 * If a single relation is specified, process it and we're done ... unless
 	 * the relation is a partitioned table, in which case we fall through.
diff --git a/src/backend/storage/lmgr/deadlock.c b/src/backend/storage/lmgr/deadlock.c
index b8962d875b6..10266e384aa 100644
--- a/src/backend/storage/lmgr/deadlock.c
+++ b/src/backend/storage/lmgr/deadlock.c
@@ -620,6 +620,18 @@ FindLockCycleRecurseMember(PGPROC *checkProc,
 						proc->statusFlags & PROC_IS_AUTOVACUUM)
 						blocking_autovacuum_proc = proc;
 
+					/*
+					 * If we note that we're blocked by some process running
+					 * REPACK (CONCURRENTLY), just fail.  That process is
+					 * going to upgrade its lock at some point, and it would
+					 * be inappropriate for any other process to cause that
+					 * to fail.
+					 */
+					if (checkProc == MyProc &&
+						proc->statusFlags & PROC_IN_CONCURRENT_REPACK)
+						/* not a serious error message proposal */
+						elog(ERROR, "oops, we're blocked by REPACK, time to die");
+
 					/* We're done looking at this proclock */
 					break;
 				}
diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h
index 1dad125706e..4e340ead455 100644
--- a/src/include/storage/proc.h
+++ b/src/include/storage/proc.h
@@ -69,6 +69,7 @@ struct XidCache
 #define		PROC_AFFECTS_ALL_HORIZONS	0x20	/* this proc's xmin must be
 												 * included in vacuum horizons
 												 * in all databases */
+#define		PROC_IN_CONCURRENT_REPACK	0x40	/* REPACK (CONCURRENTLY) */
 
 /* flags reset at EOXact */
 #define		PROC_VACUUM_STATE_MASK \
diff --git a/src/test/modules/injection_points/Makefile b/src/test/modules/injection_points/Makefile
index 2cd7d87c533..f7663859fe2 100644
--- a/src/test/modules/injection_points/Makefile
+++ b/src/test/modules/injection_points/Makefile
@@ -15,6 +15,7 @@ REGRESS_OPTS = --dlpath=$(top_builddir)/src/test/regress
 ISOLATION = basic \
 	    inplace \
 	    repack \
+	    repack_deadlock \
 	    repack_toast \
 	    syscache-update-pruned \
 	    heap_lock_update
diff --git a/src/test/modules/injection_points/expected/repack_deadlock.out b/src/test/modules/injection_points/expected/repack_deadlock.out
new file mode 100644
index 00000000000..42505d10933
--- /dev/null
+++ b/src/test/modules/injection_points/expected/repack_deadlock.out
@@ -0,0 +1,63 @@
+Parsed test spec with 2 sessions
+
+starting permutation: wait_before_lock add_column wakeup_before_lock check1
+injection_points_attach
+-----------------------
+                       
+(1 row)
+
+step wait_before_lock: 
+	REPACK (CONCURRENTLY) repack_deadlock USING INDEX repack_deadlock_pkey;
+ <waiting ...>
+step add_column: 
+	alter table repack_deadlock add column noise text;
+ <waiting ...>
+step add_column: <... completed>
+ERROR:  oops, we're blocked by REPACK, time to die
+step wakeup_before_lock: 
+	SELECT injection_points_wakeup('repack-concurrently-before-lock');
+
+injection_points_wakeup
+-----------------------
+                       
+(1 row)
+
+step wait_before_lock: <... completed>
+step check1: 
+	INSERT INTO relfilenodes(node)
+	SELECT relfilenode FROM pg_class WHERE relname='repack_deadlock';
+
+	SELECT count(DISTINCT node) FROM relfilenodes;
+
+	SELECT i, j FROM repack_deadlock ORDER BY i, j;
+
+	INSERT INTO data_s1(i, j)
+	SELECT i, j FROM repack_deadlock;
+
+	SELECT count(*)
+	FROM data_s1 d1 FULL JOIN data_s2 d2 USING (i, j)
+	WHERE d1.i ISNULL OR d2.i ISNULL;
+
+count
+-----
+    1
+(1 row)
+
+i|j
+-+-
+1|1
+2|2
+3|3
+4|4
+(4 rows)
+
+count
+-----
+    4
+(1 row)
+
+injection_points_detach
+-----------------------
+                       
+(1 row)
+
diff --git a/src/test/modules/injection_points/meson.build b/src/test/modules/injection_points/meson.build
index a414abb924b..1cd88d6db65 100644
--- a/src/test/modules/injection_points/meson.build
+++ b/src/test/modules/injection_points/meson.build
@@ -46,6 +46,7 @@ tests += {
       'basic',
       'inplace',
       'repack',
+      'repack_deadlock',
       'repack_toast',
       'syscache-update-pruned',
       'heap_lock_update',
diff --git a/src/test/modules/injection_points/specs/repack_deadlock.spec b/src/test/modules/injection_points/specs/repack_deadlock.spec
new file mode 100644
index 00000000000..9d23a6588c2
--- /dev/null
+++ b/src/test/modules/injection_points/specs/repack_deadlock.spec
@@ -0,0 +1,83 @@
+# Test REPACK with a concurrent transaction that would cause a deadlock
+setup
+{
+	CREATE EXTENSION injection_points;
+
+	CREATE TABLE repack_deadlock(i int PRIMARY KEY, j int);
+	INSERT INTO repack_deadlock(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4);
+
+	CREATE TABLE relfilenodes(node oid);
+
+	CREATE TABLE data_s1(i int, j int);
+	CREATE TABLE data_s2(i int, j int);
+}
+
+teardown
+{
+	DROP TABLE repack_deadlock;
+	DROP EXTENSION injection_points;
+
+	DROP TABLE relfilenodes;
+	DROP TABLE data_s1;
+	DROP TABLE data_s2;
+}
+
+session s1
+setup
+{
+	SELECT injection_points_set_local();
+	SELECT injection_points_attach('repack-concurrently-before-lock', 'wait');
+}
+# Perform the initial load and wait for s2 to do some data changes.
+step wait_before_lock
+{
+	REPACK (CONCURRENTLY) repack_deadlock USING INDEX repack_deadlock_pkey;
+}
+# Check the table from the perspective of s1.
+#
+# Besides the contents, we also check that relfilenode has changed.
+
+# Have each session write the contents into a table and use FULL JOIN to check
+# if the outputs are identical.
+step check1
+{
+	INSERT INTO relfilenodes(node)
+	SELECT relfilenode FROM pg_class WHERE relname='repack_deadlock';
+
+	SELECT count(DISTINCT node) FROM relfilenodes;
+
+	SELECT i, j FROM repack_deadlock ORDER BY i, j;
+
+	INSERT INTO data_s1(i, j)
+	SELECT i, j FROM repack_deadlock;
+
+	SELECT count(*)
+	FROM data_s1 d1 FULL JOIN data_s2 d2 USING (i, j)
+	WHERE d1.i ISNULL OR d2.i ISNULL;
+}
+teardown
+{
+	SELECT injection_points_detach('repack-concurrently-before-lock');
+}
+
+session s2
+# Change the existing data. UPDATE changes both key and non-key columns. Also
+# update one row twice to test whether tuple version generated by this session
+# can be found.
+step add_column
+{
+	alter table repack_deadlock add column noise text;
+}
+
+step wakeup_before_lock
+{
+	SELECT injection_points_wakeup('repack-concurrently-before-lock');
+}
+
+# Test if data changes introduced while one session is performing REPACK
+# CONCURRENTLY find their way into the table.
+permutation
+	wait_before_lock
+	add_column
+	wakeup_before_lock
+	check1
-- 
2.47.3


--fot5plqmb33ey33c--





^ permalink  raw  reply  [nested|flat] 89+ messages in thread

* [PATCH v44b 4/4] hack deadlock detector
@ 2026-03-25 19:35  Álvaro Herrera <[email protected]>
  0 siblings, 0 replies; 89+ messages in thread

From: Álvaro Herrera @ 2026-03-25 19:35 UTC (permalink / raw)

---
 src/backend/commands/cluster.c                | 10 +++
 src/backend/storage/lmgr/deadlock.c           | 12 +++
 src/include/storage/proc.h                    |  1 +
 src/test/modules/injection_points/Makefile    |  1 +
 .../expected/repack_deadlock.out              | 63 ++++++++++++++
 src/test/modules/injection_points/meson.build |  1 +
 .../specs/repack_deadlock.spec                | 83 +++++++++++++++++++
 7 files changed, 171 insertions(+)
 create mode 100644 src/test/modules/injection_points/expected/repack_deadlock.out
 create mode 100644 src/test/modules/injection_points/specs/repack_deadlock.spec

diff --git a/src/backend/commands/cluster.c b/src/backend/commands/cluster.c
index 2c3058ba10d..d5b1dfbff69 100644
--- a/src/backend/commands/cluster.c
+++ b/src/backend/commands/cluster.c
@@ -372,6 +372,16 @@ ExecRepack(ParseState *pstate, RepackStmt *stmt, bool isTopLevel)
 	/* Determine the lock mode to use. */
 	lockmode = RepackLockLevel((params.options & CLUOPT_CONCURRENT) != 0);
 
+	/*
+	 * If in concurrent mode, set the PROC_IN_CONCURRENT_REPACK flag.  This
+	 * makes the deadlock checker cause anyone that would conflict with us
+	 * to error out.  It's important to set this flag ahead of actually locking
+	 * the relation; it won't of course affect anyone until we do have a lock
+	 * that others can conflict with.
+	 */
+	if ((params.options & CLUOPT_CONCURRENT) != 0)
+		MyProc->statusFlags |= PROC_IN_CONCURRENT_REPACK;
+
 	/*
 	 * If a single relation is specified, process it and we're done ... unless
 	 * the relation is a partitioned table, in which case we fall through.
diff --git a/src/backend/storage/lmgr/deadlock.c b/src/backend/storage/lmgr/deadlock.c
index b8962d875b6..10266e384aa 100644
--- a/src/backend/storage/lmgr/deadlock.c
+++ b/src/backend/storage/lmgr/deadlock.c
@@ -620,6 +620,18 @@ FindLockCycleRecurseMember(PGPROC *checkProc,
 						proc->statusFlags & PROC_IS_AUTOVACUUM)
 						blocking_autovacuum_proc = proc;
 
+					/*
+					 * If we note that we're blocked by some process running
+					 * REPACK (CONCURRENTLY), just fail.  That process is
+					 * going to upgrade its lock at some point, and it would
+					 * be inappropriate for any other process to cause that
+					 * to fail.
+					 */
+					if (checkProc == MyProc &&
+						proc->statusFlags & PROC_IN_CONCURRENT_REPACK)
+						/* not a serious error message proposal */
+						elog(ERROR, "oops, we're blocked by REPACK, time to die");
+
 					/* We're done looking at this proclock */
 					break;
 				}
diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h
index 1dad125706e..4e340ead455 100644
--- a/src/include/storage/proc.h
+++ b/src/include/storage/proc.h
@@ -69,6 +69,7 @@ struct XidCache
 #define		PROC_AFFECTS_ALL_HORIZONS	0x20	/* this proc's xmin must be
 												 * included in vacuum horizons
 												 * in all databases */
+#define		PROC_IN_CONCURRENT_REPACK	0x40	/* REPACK (CONCURRENTLY) */
 
 /* flags reset at EOXact */
 #define		PROC_VACUUM_STATE_MASK \
diff --git a/src/test/modules/injection_points/Makefile b/src/test/modules/injection_points/Makefile
index 2cd7d87c533..f7663859fe2 100644
--- a/src/test/modules/injection_points/Makefile
+++ b/src/test/modules/injection_points/Makefile
@@ -15,6 +15,7 @@ REGRESS_OPTS = --dlpath=$(top_builddir)/src/test/regress
 ISOLATION = basic \
 	    inplace \
 	    repack \
+	    repack_deadlock \
 	    repack_toast \
 	    syscache-update-pruned \
 	    heap_lock_update
diff --git a/src/test/modules/injection_points/expected/repack_deadlock.out b/src/test/modules/injection_points/expected/repack_deadlock.out
new file mode 100644
index 00000000000..42505d10933
--- /dev/null
+++ b/src/test/modules/injection_points/expected/repack_deadlock.out
@@ -0,0 +1,63 @@
+Parsed test spec with 2 sessions
+
+starting permutation: wait_before_lock add_column wakeup_before_lock check1
+injection_points_attach
+-----------------------
+                       
+(1 row)
+
+step wait_before_lock: 
+	REPACK (CONCURRENTLY) repack_deadlock USING INDEX repack_deadlock_pkey;
+ <waiting ...>
+step add_column: 
+	alter table repack_deadlock add column noise text;
+ <waiting ...>
+step add_column: <... completed>
+ERROR:  oops, we're blocked by REPACK, time to die
+step wakeup_before_lock: 
+	SELECT injection_points_wakeup('repack-concurrently-before-lock');
+
+injection_points_wakeup
+-----------------------
+                       
+(1 row)
+
+step wait_before_lock: <... completed>
+step check1: 
+	INSERT INTO relfilenodes(node)
+	SELECT relfilenode FROM pg_class WHERE relname='repack_deadlock';
+
+	SELECT count(DISTINCT node) FROM relfilenodes;
+
+	SELECT i, j FROM repack_deadlock ORDER BY i, j;
+
+	INSERT INTO data_s1(i, j)
+	SELECT i, j FROM repack_deadlock;
+
+	SELECT count(*)
+	FROM data_s1 d1 FULL JOIN data_s2 d2 USING (i, j)
+	WHERE d1.i ISNULL OR d2.i ISNULL;
+
+count
+-----
+    1
+(1 row)
+
+i|j
+-+-
+1|1
+2|2
+3|3
+4|4
+(4 rows)
+
+count
+-----
+    4
+(1 row)
+
+injection_points_detach
+-----------------------
+                       
+(1 row)
+
diff --git a/src/test/modules/injection_points/meson.build b/src/test/modules/injection_points/meson.build
index a414abb924b..1cd88d6db65 100644
--- a/src/test/modules/injection_points/meson.build
+++ b/src/test/modules/injection_points/meson.build
@@ -46,6 +46,7 @@ tests += {
       'basic',
       'inplace',
       'repack',
+      'repack_deadlock',
       'repack_toast',
       'syscache-update-pruned',
       'heap_lock_update',
diff --git a/src/test/modules/injection_points/specs/repack_deadlock.spec b/src/test/modules/injection_points/specs/repack_deadlock.spec
new file mode 100644
index 00000000000..9d23a6588c2
--- /dev/null
+++ b/src/test/modules/injection_points/specs/repack_deadlock.spec
@@ -0,0 +1,83 @@
+# Test REPACK with a concurrent transaction that would cause a deadlock
+setup
+{
+	CREATE EXTENSION injection_points;
+
+	CREATE TABLE repack_deadlock(i int PRIMARY KEY, j int);
+	INSERT INTO repack_deadlock(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4);
+
+	CREATE TABLE relfilenodes(node oid);
+
+	CREATE TABLE data_s1(i int, j int);
+	CREATE TABLE data_s2(i int, j int);
+}
+
+teardown
+{
+	DROP TABLE repack_deadlock;
+	DROP EXTENSION injection_points;
+
+	DROP TABLE relfilenodes;
+	DROP TABLE data_s1;
+	DROP TABLE data_s2;
+}
+
+session s1
+setup
+{
+	SELECT injection_points_set_local();
+	SELECT injection_points_attach('repack-concurrently-before-lock', 'wait');
+}
+# Perform the initial load and wait for s2 to do some data changes.
+step wait_before_lock
+{
+	REPACK (CONCURRENTLY) repack_deadlock USING INDEX repack_deadlock_pkey;
+}
+# Check the table from the perspective of s1.
+#
+# Besides the contents, we also check that relfilenode has changed.
+
+# Have each session write the contents into a table and use FULL JOIN to check
+# if the outputs are identical.
+step check1
+{
+	INSERT INTO relfilenodes(node)
+	SELECT relfilenode FROM pg_class WHERE relname='repack_deadlock';
+
+	SELECT count(DISTINCT node) FROM relfilenodes;
+
+	SELECT i, j FROM repack_deadlock ORDER BY i, j;
+
+	INSERT INTO data_s1(i, j)
+	SELECT i, j FROM repack_deadlock;
+
+	SELECT count(*)
+	FROM data_s1 d1 FULL JOIN data_s2 d2 USING (i, j)
+	WHERE d1.i ISNULL OR d2.i ISNULL;
+}
+teardown
+{
+	SELECT injection_points_detach('repack-concurrently-before-lock');
+}
+
+session s2
+# Change the existing data. UPDATE changes both key and non-key columns. Also
+# update one row twice to test whether tuple version generated by this session
+# can be found.
+step add_column
+{
+	alter table repack_deadlock add column noise text;
+}
+
+step wakeup_before_lock
+{
+	SELECT injection_points_wakeup('repack-concurrently-before-lock');
+}
+
+# Test if data changes introduced while one session is performing REPACK
+# CONCURRENTLY find their way into the table.
+permutation
+	wait_before_lock
+	add_column
+	wakeup_before_lock
+	check1
-- 
2.47.3


--fot5plqmb33ey33c--





^ permalink  raw  reply  [nested|flat] 89+ messages in thread

* [PATCH v44b 4/4] hack deadlock detector
@ 2026-03-25 19:35  Álvaro Herrera <[email protected]>
  0 siblings, 0 replies; 89+ messages in thread

From: Álvaro Herrera @ 2026-03-25 19:35 UTC (permalink / raw)

---
 src/backend/commands/cluster.c                | 10 +++
 src/backend/storage/lmgr/deadlock.c           | 12 +++
 src/include/storage/proc.h                    |  1 +
 src/test/modules/injection_points/Makefile    |  1 +
 .../expected/repack_deadlock.out              | 63 ++++++++++++++
 src/test/modules/injection_points/meson.build |  1 +
 .../specs/repack_deadlock.spec                | 83 +++++++++++++++++++
 7 files changed, 171 insertions(+)
 create mode 100644 src/test/modules/injection_points/expected/repack_deadlock.out
 create mode 100644 src/test/modules/injection_points/specs/repack_deadlock.spec

diff --git a/src/backend/commands/cluster.c b/src/backend/commands/cluster.c
index 2c3058ba10d..d5b1dfbff69 100644
--- a/src/backend/commands/cluster.c
+++ b/src/backend/commands/cluster.c
@@ -372,6 +372,16 @@ ExecRepack(ParseState *pstate, RepackStmt *stmt, bool isTopLevel)
 	/* Determine the lock mode to use. */
 	lockmode = RepackLockLevel((params.options & CLUOPT_CONCURRENT) != 0);
 
+	/*
+	 * If in concurrent mode, set the PROC_IN_CONCURRENT_REPACK flag.  This
+	 * makes the deadlock checker cause anyone that would conflict with us
+	 * to error out.  It's important to set this flag ahead of actually locking
+	 * the relation; it won't of course affect anyone until we do have a lock
+	 * that others can conflict with.
+	 */
+	if ((params.options & CLUOPT_CONCURRENT) != 0)
+		MyProc->statusFlags |= PROC_IN_CONCURRENT_REPACK;
+
 	/*
 	 * If a single relation is specified, process it and we're done ... unless
 	 * the relation is a partitioned table, in which case we fall through.
diff --git a/src/backend/storage/lmgr/deadlock.c b/src/backend/storage/lmgr/deadlock.c
index b8962d875b6..10266e384aa 100644
--- a/src/backend/storage/lmgr/deadlock.c
+++ b/src/backend/storage/lmgr/deadlock.c
@@ -620,6 +620,18 @@ FindLockCycleRecurseMember(PGPROC *checkProc,
 						proc->statusFlags & PROC_IS_AUTOVACUUM)
 						blocking_autovacuum_proc = proc;
 
+					/*
+					 * If we note that we're blocked by some process running
+					 * REPACK (CONCURRENTLY), just fail.  That process is
+					 * going to upgrade its lock at some point, and it would
+					 * be inappropriate for any other process to cause that
+					 * to fail.
+					 */
+					if (checkProc == MyProc &&
+						proc->statusFlags & PROC_IN_CONCURRENT_REPACK)
+						/* not a serious error message proposal */
+						elog(ERROR, "oops, we're blocked by REPACK, time to die");
+
 					/* We're done looking at this proclock */
 					break;
 				}
diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h
index 1dad125706e..4e340ead455 100644
--- a/src/include/storage/proc.h
+++ b/src/include/storage/proc.h
@@ -69,6 +69,7 @@ struct XidCache
 #define		PROC_AFFECTS_ALL_HORIZONS	0x20	/* this proc's xmin must be
 												 * included in vacuum horizons
 												 * in all databases */
+#define		PROC_IN_CONCURRENT_REPACK	0x40	/* REPACK (CONCURRENTLY) */
 
 /* flags reset at EOXact */
 #define		PROC_VACUUM_STATE_MASK \
diff --git a/src/test/modules/injection_points/Makefile b/src/test/modules/injection_points/Makefile
index 2cd7d87c533..f7663859fe2 100644
--- a/src/test/modules/injection_points/Makefile
+++ b/src/test/modules/injection_points/Makefile
@@ -15,6 +15,7 @@ REGRESS_OPTS = --dlpath=$(top_builddir)/src/test/regress
 ISOLATION = basic \
 	    inplace \
 	    repack \
+	    repack_deadlock \
 	    repack_toast \
 	    syscache-update-pruned \
 	    heap_lock_update
diff --git a/src/test/modules/injection_points/expected/repack_deadlock.out b/src/test/modules/injection_points/expected/repack_deadlock.out
new file mode 100644
index 00000000000..42505d10933
--- /dev/null
+++ b/src/test/modules/injection_points/expected/repack_deadlock.out
@@ -0,0 +1,63 @@
+Parsed test spec with 2 sessions
+
+starting permutation: wait_before_lock add_column wakeup_before_lock check1
+injection_points_attach
+-----------------------
+                       
+(1 row)
+
+step wait_before_lock: 
+	REPACK (CONCURRENTLY) repack_deadlock USING INDEX repack_deadlock_pkey;
+ <waiting ...>
+step add_column: 
+	alter table repack_deadlock add column noise text;
+ <waiting ...>
+step add_column: <... completed>
+ERROR:  oops, we're blocked by REPACK, time to die
+step wakeup_before_lock: 
+	SELECT injection_points_wakeup('repack-concurrently-before-lock');
+
+injection_points_wakeup
+-----------------------
+                       
+(1 row)
+
+step wait_before_lock: <... completed>
+step check1: 
+	INSERT INTO relfilenodes(node)
+	SELECT relfilenode FROM pg_class WHERE relname='repack_deadlock';
+
+	SELECT count(DISTINCT node) FROM relfilenodes;
+
+	SELECT i, j FROM repack_deadlock ORDER BY i, j;
+
+	INSERT INTO data_s1(i, j)
+	SELECT i, j FROM repack_deadlock;
+
+	SELECT count(*)
+	FROM data_s1 d1 FULL JOIN data_s2 d2 USING (i, j)
+	WHERE d1.i ISNULL OR d2.i ISNULL;
+
+count
+-----
+    1
+(1 row)
+
+i|j
+-+-
+1|1
+2|2
+3|3
+4|4
+(4 rows)
+
+count
+-----
+    4
+(1 row)
+
+injection_points_detach
+-----------------------
+                       
+(1 row)
+
diff --git a/src/test/modules/injection_points/meson.build b/src/test/modules/injection_points/meson.build
index a414abb924b..1cd88d6db65 100644
--- a/src/test/modules/injection_points/meson.build
+++ b/src/test/modules/injection_points/meson.build
@@ -46,6 +46,7 @@ tests += {
       'basic',
       'inplace',
       'repack',
+      'repack_deadlock',
       'repack_toast',
       'syscache-update-pruned',
       'heap_lock_update',
diff --git a/src/test/modules/injection_points/specs/repack_deadlock.spec b/src/test/modules/injection_points/specs/repack_deadlock.spec
new file mode 100644
index 00000000000..9d23a6588c2
--- /dev/null
+++ b/src/test/modules/injection_points/specs/repack_deadlock.spec
@@ -0,0 +1,83 @@
+# Test REPACK with a concurrent transaction that would cause a deadlock
+setup
+{
+	CREATE EXTENSION injection_points;
+
+	CREATE TABLE repack_deadlock(i int PRIMARY KEY, j int);
+	INSERT INTO repack_deadlock(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4);
+
+	CREATE TABLE relfilenodes(node oid);
+
+	CREATE TABLE data_s1(i int, j int);
+	CREATE TABLE data_s2(i int, j int);
+}
+
+teardown
+{
+	DROP TABLE repack_deadlock;
+	DROP EXTENSION injection_points;
+
+	DROP TABLE relfilenodes;
+	DROP TABLE data_s1;
+	DROP TABLE data_s2;
+}
+
+session s1
+setup
+{
+	SELECT injection_points_set_local();
+	SELECT injection_points_attach('repack-concurrently-before-lock', 'wait');
+}
+# Perform the initial load and wait for s2 to do some data changes.
+step wait_before_lock
+{
+	REPACK (CONCURRENTLY) repack_deadlock USING INDEX repack_deadlock_pkey;
+}
+# Check the table from the perspective of s1.
+#
+# Besides the contents, we also check that relfilenode has changed.
+
+# Have each session write the contents into a table and use FULL JOIN to check
+# if the outputs are identical.
+step check1
+{
+	INSERT INTO relfilenodes(node)
+	SELECT relfilenode FROM pg_class WHERE relname='repack_deadlock';
+
+	SELECT count(DISTINCT node) FROM relfilenodes;
+
+	SELECT i, j FROM repack_deadlock ORDER BY i, j;
+
+	INSERT INTO data_s1(i, j)
+	SELECT i, j FROM repack_deadlock;
+
+	SELECT count(*)
+	FROM data_s1 d1 FULL JOIN data_s2 d2 USING (i, j)
+	WHERE d1.i ISNULL OR d2.i ISNULL;
+}
+teardown
+{
+	SELECT injection_points_detach('repack-concurrently-before-lock');
+}
+
+session s2
+# Change the existing data. UPDATE changes both key and non-key columns. Also
+# update one row twice to test whether tuple version generated by this session
+# can be found.
+step add_column
+{
+	alter table repack_deadlock add column noise text;
+}
+
+step wakeup_before_lock
+{
+	SELECT injection_points_wakeup('repack-concurrently-before-lock');
+}
+
+# Test if data changes introduced while one session is performing REPACK
+# CONCURRENTLY find their way into the table.
+permutation
+	wait_before_lock
+	add_column
+	wakeup_before_lock
+	check1
-- 
2.47.3


--fot5plqmb33ey33c--





^ permalink  raw  reply  [nested|flat] 89+ messages in thread

* [PATCH v44b 4/4] hack deadlock detector
@ 2026-03-25 19:35  Álvaro Herrera <[email protected]>
  0 siblings, 0 replies; 89+ messages in thread

From: Álvaro Herrera @ 2026-03-25 19:35 UTC (permalink / raw)

---
 src/backend/commands/cluster.c                | 10 +++
 src/backend/storage/lmgr/deadlock.c           | 12 +++
 src/include/storage/proc.h                    |  1 +
 src/test/modules/injection_points/Makefile    |  1 +
 .../expected/repack_deadlock.out              | 63 ++++++++++++++
 src/test/modules/injection_points/meson.build |  1 +
 .../specs/repack_deadlock.spec                | 83 +++++++++++++++++++
 7 files changed, 171 insertions(+)
 create mode 100644 src/test/modules/injection_points/expected/repack_deadlock.out
 create mode 100644 src/test/modules/injection_points/specs/repack_deadlock.spec

diff --git a/src/backend/commands/cluster.c b/src/backend/commands/cluster.c
index 2c3058ba10d..d5b1dfbff69 100644
--- a/src/backend/commands/cluster.c
+++ b/src/backend/commands/cluster.c
@@ -372,6 +372,16 @@ ExecRepack(ParseState *pstate, RepackStmt *stmt, bool isTopLevel)
 	/* Determine the lock mode to use. */
 	lockmode = RepackLockLevel((params.options & CLUOPT_CONCURRENT) != 0);
 
+	/*
+	 * If in concurrent mode, set the PROC_IN_CONCURRENT_REPACK flag.  This
+	 * makes the deadlock checker cause anyone that would conflict with us
+	 * to error out.  It's important to set this flag ahead of actually locking
+	 * the relation; it won't of course affect anyone until we do have a lock
+	 * that others can conflict with.
+	 */
+	if ((params.options & CLUOPT_CONCURRENT) != 0)
+		MyProc->statusFlags |= PROC_IN_CONCURRENT_REPACK;
+
 	/*
 	 * If a single relation is specified, process it and we're done ... unless
 	 * the relation is a partitioned table, in which case we fall through.
diff --git a/src/backend/storage/lmgr/deadlock.c b/src/backend/storage/lmgr/deadlock.c
index b8962d875b6..10266e384aa 100644
--- a/src/backend/storage/lmgr/deadlock.c
+++ b/src/backend/storage/lmgr/deadlock.c
@@ -620,6 +620,18 @@ FindLockCycleRecurseMember(PGPROC *checkProc,
 						proc->statusFlags & PROC_IS_AUTOVACUUM)
 						blocking_autovacuum_proc = proc;
 
+					/*
+					 * If we note that we're blocked by some process running
+					 * REPACK (CONCURRENTLY), just fail.  That process is
+					 * going to upgrade its lock at some point, and it would
+					 * be inappropriate for any other process to cause that
+					 * to fail.
+					 */
+					if (checkProc == MyProc &&
+						proc->statusFlags & PROC_IN_CONCURRENT_REPACK)
+						/* not a serious error message proposal */
+						elog(ERROR, "oops, we're blocked by REPACK, time to die");
+
 					/* We're done looking at this proclock */
 					break;
 				}
diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h
index 1dad125706e..4e340ead455 100644
--- a/src/include/storage/proc.h
+++ b/src/include/storage/proc.h
@@ -69,6 +69,7 @@ struct XidCache
 #define		PROC_AFFECTS_ALL_HORIZONS	0x20	/* this proc's xmin must be
 												 * included in vacuum horizons
 												 * in all databases */
+#define		PROC_IN_CONCURRENT_REPACK	0x40	/* REPACK (CONCURRENTLY) */
 
 /* flags reset at EOXact */
 #define		PROC_VACUUM_STATE_MASK \
diff --git a/src/test/modules/injection_points/Makefile b/src/test/modules/injection_points/Makefile
index 2cd7d87c533..f7663859fe2 100644
--- a/src/test/modules/injection_points/Makefile
+++ b/src/test/modules/injection_points/Makefile
@@ -15,6 +15,7 @@ REGRESS_OPTS = --dlpath=$(top_builddir)/src/test/regress
 ISOLATION = basic \
 	    inplace \
 	    repack \
+	    repack_deadlock \
 	    repack_toast \
 	    syscache-update-pruned \
 	    heap_lock_update
diff --git a/src/test/modules/injection_points/expected/repack_deadlock.out b/src/test/modules/injection_points/expected/repack_deadlock.out
new file mode 100644
index 00000000000..42505d10933
--- /dev/null
+++ b/src/test/modules/injection_points/expected/repack_deadlock.out
@@ -0,0 +1,63 @@
+Parsed test spec with 2 sessions
+
+starting permutation: wait_before_lock add_column wakeup_before_lock check1
+injection_points_attach
+-----------------------
+                       
+(1 row)
+
+step wait_before_lock: 
+	REPACK (CONCURRENTLY) repack_deadlock USING INDEX repack_deadlock_pkey;
+ <waiting ...>
+step add_column: 
+	alter table repack_deadlock add column noise text;
+ <waiting ...>
+step add_column: <... completed>
+ERROR:  oops, we're blocked by REPACK, time to die
+step wakeup_before_lock: 
+	SELECT injection_points_wakeup('repack-concurrently-before-lock');
+
+injection_points_wakeup
+-----------------------
+                       
+(1 row)
+
+step wait_before_lock: <... completed>
+step check1: 
+	INSERT INTO relfilenodes(node)
+	SELECT relfilenode FROM pg_class WHERE relname='repack_deadlock';
+
+	SELECT count(DISTINCT node) FROM relfilenodes;
+
+	SELECT i, j FROM repack_deadlock ORDER BY i, j;
+
+	INSERT INTO data_s1(i, j)
+	SELECT i, j FROM repack_deadlock;
+
+	SELECT count(*)
+	FROM data_s1 d1 FULL JOIN data_s2 d2 USING (i, j)
+	WHERE d1.i ISNULL OR d2.i ISNULL;
+
+count
+-----
+    1
+(1 row)
+
+i|j
+-+-
+1|1
+2|2
+3|3
+4|4
+(4 rows)
+
+count
+-----
+    4
+(1 row)
+
+injection_points_detach
+-----------------------
+                       
+(1 row)
+
diff --git a/src/test/modules/injection_points/meson.build b/src/test/modules/injection_points/meson.build
index a414abb924b..1cd88d6db65 100644
--- a/src/test/modules/injection_points/meson.build
+++ b/src/test/modules/injection_points/meson.build
@@ -46,6 +46,7 @@ tests += {
       'basic',
       'inplace',
       'repack',
+      'repack_deadlock',
       'repack_toast',
       'syscache-update-pruned',
       'heap_lock_update',
diff --git a/src/test/modules/injection_points/specs/repack_deadlock.spec b/src/test/modules/injection_points/specs/repack_deadlock.spec
new file mode 100644
index 00000000000..9d23a6588c2
--- /dev/null
+++ b/src/test/modules/injection_points/specs/repack_deadlock.spec
@@ -0,0 +1,83 @@
+# Test REPACK with a concurrent transaction that would cause a deadlock
+setup
+{
+	CREATE EXTENSION injection_points;
+
+	CREATE TABLE repack_deadlock(i int PRIMARY KEY, j int);
+	INSERT INTO repack_deadlock(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4);
+
+	CREATE TABLE relfilenodes(node oid);
+
+	CREATE TABLE data_s1(i int, j int);
+	CREATE TABLE data_s2(i int, j int);
+}
+
+teardown
+{
+	DROP TABLE repack_deadlock;
+	DROP EXTENSION injection_points;
+
+	DROP TABLE relfilenodes;
+	DROP TABLE data_s1;
+	DROP TABLE data_s2;
+}
+
+session s1
+setup
+{
+	SELECT injection_points_set_local();
+	SELECT injection_points_attach('repack-concurrently-before-lock', 'wait');
+}
+# Perform the initial load and wait for s2 to do some data changes.
+step wait_before_lock
+{
+	REPACK (CONCURRENTLY) repack_deadlock USING INDEX repack_deadlock_pkey;
+}
+# Check the table from the perspective of s1.
+#
+# Besides the contents, we also check that relfilenode has changed.
+
+# Have each session write the contents into a table and use FULL JOIN to check
+# if the outputs are identical.
+step check1
+{
+	INSERT INTO relfilenodes(node)
+	SELECT relfilenode FROM pg_class WHERE relname='repack_deadlock';
+
+	SELECT count(DISTINCT node) FROM relfilenodes;
+
+	SELECT i, j FROM repack_deadlock ORDER BY i, j;
+
+	INSERT INTO data_s1(i, j)
+	SELECT i, j FROM repack_deadlock;
+
+	SELECT count(*)
+	FROM data_s1 d1 FULL JOIN data_s2 d2 USING (i, j)
+	WHERE d1.i ISNULL OR d2.i ISNULL;
+}
+teardown
+{
+	SELECT injection_points_detach('repack-concurrently-before-lock');
+}
+
+session s2
+# Change the existing data. UPDATE changes both key and non-key columns. Also
+# update one row twice to test whether tuple version generated by this session
+# can be found.
+step add_column
+{
+	alter table repack_deadlock add column noise text;
+}
+
+step wakeup_before_lock
+{
+	SELECT injection_points_wakeup('repack-concurrently-before-lock');
+}
+
+# Test if data changes introduced while one session is performing REPACK
+# CONCURRENTLY find their way into the table.
+permutation
+	wait_before_lock
+	add_column
+	wakeup_before_lock
+	check1
-- 
2.47.3


--fot5plqmb33ey33c--





^ permalink  raw  reply  [nested|flat] 89+ messages in thread

* [PATCH v44b 4/4] hack deadlock detector
@ 2026-03-25 19:35  Álvaro Herrera <[email protected]>
  0 siblings, 0 replies; 89+ messages in thread

From: Álvaro Herrera @ 2026-03-25 19:35 UTC (permalink / raw)

---
 src/backend/commands/cluster.c                | 10 +++
 src/backend/storage/lmgr/deadlock.c           | 12 +++
 src/include/storage/proc.h                    |  1 +
 src/test/modules/injection_points/Makefile    |  1 +
 .../expected/repack_deadlock.out              | 63 ++++++++++++++
 src/test/modules/injection_points/meson.build |  1 +
 .../specs/repack_deadlock.spec                | 83 +++++++++++++++++++
 7 files changed, 171 insertions(+)
 create mode 100644 src/test/modules/injection_points/expected/repack_deadlock.out
 create mode 100644 src/test/modules/injection_points/specs/repack_deadlock.spec

diff --git a/src/backend/commands/cluster.c b/src/backend/commands/cluster.c
index 2c3058ba10d..d5b1dfbff69 100644
--- a/src/backend/commands/cluster.c
+++ b/src/backend/commands/cluster.c
@@ -372,6 +372,16 @@ ExecRepack(ParseState *pstate, RepackStmt *stmt, bool isTopLevel)
 	/* Determine the lock mode to use. */
 	lockmode = RepackLockLevel((params.options & CLUOPT_CONCURRENT) != 0);
 
+	/*
+	 * If in concurrent mode, set the PROC_IN_CONCURRENT_REPACK flag.  This
+	 * makes the deadlock checker cause anyone that would conflict with us
+	 * to error out.  It's important to set this flag ahead of actually locking
+	 * the relation; it won't of course affect anyone until we do have a lock
+	 * that others can conflict with.
+	 */
+	if ((params.options & CLUOPT_CONCURRENT) != 0)
+		MyProc->statusFlags |= PROC_IN_CONCURRENT_REPACK;
+
 	/*
 	 * If a single relation is specified, process it and we're done ... unless
 	 * the relation is a partitioned table, in which case we fall through.
diff --git a/src/backend/storage/lmgr/deadlock.c b/src/backend/storage/lmgr/deadlock.c
index b8962d875b6..10266e384aa 100644
--- a/src/backend/storage/lmgr/deadlock.c
+++ b/src/backend/storage/lmgr/deadlock.c
@@ -620,6 +620,18 @@ FindLockCycleRecurseMember(PGPROC *checkProc,
 						proc->statusFlags & PROC_IS_AUTOVACUUM)
 						blocking_autovacuum_proc = proc;
 
+					/*
+					 * If we note that we're blocked by some process running
+					 * REPACK (CONCURRENTLY), just fail.  That process is
+					 * going to upgrade its lock at some point, and it would
+					 * be inappropriate for any other process to cause that
+					 * to fail.
+					 */
+					if (checkProc == MyProc &&
+						proc->statusFlags & PROC_IN_CONCURRENT_REPACK)
+						/* not a serious error message proposal */
+						elog(ERROR, "oops, we're blocked by REPACK, time to die");
+
 					/* We're done looking at this proclock */
 					break;
 				}
diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h
index 1dad125706e..4e340ead455 100644
--- a/src/include/storage/proc.h
+++ b/src/include/storage/proc.h
@@ -69,6 +69,7 @@ struct XidCache
 #define		PROC_AFFECTS_ALL_HORIZONS	0x20	/* this proc's xmin must be
 												 * included in vacuum horizons
 												 * in all databases */
+#define		PROC_IN_CONCURRENT_REPACK	0x40	/* REPACK (CONCURRENTLY) */
 
 /* flags reset at EOXact */
 #define		PROC_VACUUM_STATE_MASK \
diff --git a/src/test/modules/injection_points/Makefile b/src/test/modules/injection_points/Makefile
index 2cd7d87c533..f7663859fe2 100644
--- a/src/test/modules/injection_points/Makefile
+++ b/src/test/modules/injection_points/Makefile
@@ -15,6 +15,7 @@ REGRESS_OPTS = --dlpath=$(top_builddir)/src/test/regress
 ISOLATION = basic \
 	    inplace \
 	    repack \
+	    repack_deadlock \
 	    repack_toast \
 	    syscache-update-pruned \
 	    heap_lock_update
diff --git a/src/test/modules/injection_points/expected/repack_deadlock.out b/src/test/modules/injection_points/expected/repack_deadlock.out
new file mode 100644
index 00000000000..42505d10933
--- /dev/null
+++ b/src/test/modules/injection_points/expected/repack_deadlock.out
@@ -0,0 +1,63 @@
+Parsed test spec with 2 sessions
+
+starting permutation: wait_before_lock add_column wakeup_before_lock check1
+injection_points_attach
+-----------------------
+                       
+(1 row)
+
+step wait_before_lock: 
+	REPACK (CONCURRENTLY) repack_deadlock USING INDEX repack_deadlock_pkey;
+ <waiting ...>
+step add_column: 
+	alter table repack_deadlock add column noise text;
+ <waiting ...>
+step add_column: <... completed>
+ERROR:  oops, we're blocked by REPACK, time to die
+step wakeup_before_lock: 
+	SELECT injection_points_wakeup('repack-concurrently-before-lock');
+
+injection_points_wakeup
+-----------------------
+                       
+(1 row)
+
+step wait_before_lock: <... completed>
+step check1: 
+	INSERT INTO relfilenodes(node)
+	SELECT relfilenode FROM pg_class WHERE relname='repack_deadlock';
+
+	SELECT count(DISTINCT node) FROM relfilenodes;
+
+	SELECT i, j FROM repack_deadlock ORDER BY i, j;
+
+	INSERT INTO data_s1(i, j)
+	SELECT i, j FROM repack_deadlock;
+
+	SELECT count(*)
+	FROM data_s1 d1 FULL JOIN data_s2 d2 USING (i, j)
+	WHERE d1.i ISNULL OR d2.i ISNULL;
+
+count
+-----
+    1
+(1 row)
+
+i|j
+-+-
+1|1
+2|2
+3|3
+4|4
+(4 rows)
+
+count
+-----
+    4
+(1 row)
+
+injection_points_detach
+-----------------------
+                       
+(1 row)
+
diff --git a/src/test/modules/injection_points/meson.build b/src/test/modules/injection_points/meson.build
index a414abb924b..1cd88d6db65 100644
--- a/src/test/modules/injection_points/meson.build
+++ b/src/test/modules/injection_points/meson.build
@@ -46,6 +46,7 @@ tests += {
       'basic',
       'inplace',
       'repack',
+      'repack_deadlock',
       'repack_toast',
       'syscache-update-pruned',
       'heap_lock_update',
diff --git a/src/test/modules/injection_points/specs/repack_deadlock.spec b/src/test/modules/injection_points/specs/repack_deadlock.spec
new file mode 100644
index 00000000000..9d23a6588c2
--- /dev/null
+++ b/src/test/modules/injection_points/specs/repack_deadlock.spec
@@ -0,0 +1,83 @@
+# Test REPACK with a concurrent transaction that would cause a deadlock
+setup
+{
+	CREATE EXTENSION injection_points;
+
+	CREATE TABLE repack_deadlock(i int PRIMARY KEY, j int);
+	INSERT INTO repack_deadlock(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4);
+
+	CREATE TABLE relfilenodes(node oid);
+
+	CREATE TABLE data_s1(i int, j int);
+	CREATE TABLE data_s2(i int, j int);
+}
+
+teardown
+{
+	DROP TABLE repack_deadlock;
+	DROP EXTENSION injection_points;
+
+	DROP TABLE relfilenodes;
+	DROP TABLE data_s1;
+	DROP TABLE data_s2;
+}
+
+session s1
+setup
+{
+	SELECT injection_points_set_local();
+	SELECT injection_points_attach('repack-concurrently-before-lock', 'wait');
+}
+# Perform the initial load and wait for s2 to do some data changes.
+step wait_before_lock
+{
+	REPACK (CONCURRENTLY) repack_deadlock USING INDEX repack_deadlock_pkey;
+}
+# Check the table from the perspective of s1.
+#
+# Besides the contents, we also check that relfilenode has changed.
+
+# Have each session write the contents into a table and use FULL JOIN to check
+# if the outputs are identical.
+step check1
+{
+	INSERT INTO relfilenodes(node)
+	SELECT relfilenode FROM pg_class WHERE relname='repack_deadlock';
+
+	SELECT count(DISTINCT node) FROM relfilenodes;
+
+	SELECT i, j FROM repack_deadlock ORDER BY i, j;
+
+	INSERT INTO data_s1(i, j)
+	SELECT i, j FROM repack_deadlock;
+
+	SELECT count(*)
+	FROM data_s1 d1 FULL JOIN data_s2 d2 USING (i, j)
+	WHERE d1.i ISNULL OR d2.i ISNULL;
+}
+teardown
+{
+	SELECT injection_points_detach('repack-concurrently-before-lock');
+}
+
+session s2
+# Change the existing data. UPDATE changes both key and non-key columns. Also
+# update one row twice to test whether tuple version generated by this session
+# can be found.
+step add_column
+{
+	alter table repack_deadlock add column noise text;
+}
+
+step wakeup_before_lock
+{
+	SELECT injection_points_wakeup('repack-concurrently-before-lock');
+}
+
+# Test if data changes introduced while one session is performing REPACK
+# CONCURRENTLY find their way into the table.
+permutation
+	wait_before_lock
+	add_column
+	wakeup_before_lock
+	check1
-- 
2.47.3


--fot5plqmb33ey33c--





^ permalink  raw  reply  [nested|flat] 89+ messages in thread

* [PATCH v44b 4/4] hack deadlock detector
@ 2026-03-25 19:35  Álvaro Herrera <[email protected]>
  0 siblings, 0 replies; 89+ messages in thread

From: Álvaro Herrera @ 2026-03-25 19:35 UTC (permalink / raw)

---
 src/backend/commands/cluster.c                | 10 +++
 src/backend/storage/lmgr/deadlock.c           | 12 +++
 src/include/storage/proc.h                    |  1 +
 src/test/modules/injection_points/Makefile    |  1 +
 .../expected/repack_deadlock.out              | 63 ++++++++++++++
 src/test/modules/injection_points/meson.build |  1 +
 .../specs/repack_deadlock.spec                | 83 +++++++++++++++++++
 7 files changed, 171 insertions(+)
 create mode 100644 src/test/modules/injection_points/expected/repack_deadlock.out
 create mode 100644 src/test/modules/injection_points/specs/repack_deadlock.spec

diff --git a/src/backend/commands/cluster.c b/src/backend/commands/cluster.c
index 2c3058ba10d..d5b1dfbff69 100644
--- a/src/backend/commands/cluster.c
+++ b/src/backend/commands/cluster.c
@@ -372,6 +372,16 @@ ExecRepack(ParseState *pstate, RepackStmt *stmt, bool isTopLevel)
 	/* Determine the lock mode to use. */
 	lockmode = RepackLockLevel((params.options & CLUOPT_CONCURRENT) != 0);
 
+	/*
+	 * If in concurrent mode, set the PROC_IN_CONCURRENT_REPACK flag.  This
+	 * makes the deadlock checker cause anyone that would conflict with us
+	 * to error out.  It's important to set this flag ahead of actually locking
+	 * the relation; it won't of course affect anyone until we do have a lock
+	 * that others can conflict with.
+	 */
+	if ((params.options & CLUOPT_CONCURRENT) != 0)
+		MyProc->statusFlags |= PROC_IN_CONCURRENT_REPACK;
+
 	/*
 	 * If a single relation is specified, process it and we're done ... unless
 	 * the relation is a partitioned table, in which case we fall through.
diff --git a/src/backend/storage/lmgr/deadlock.c b/src/backend/storage/lmgr/deadlock.c
index b8962d875b6..10266e384aa 100644
--- a/src/backend/storage/lmgr/deadlock.c
+++ b/src/backend/storage/lmgr/deadlock.c
@@ -620,6 +620,18 @@ FindLockCycleRecurseMember(PGPROC *checkProc,
 						proc->statusFlags & PROC_IS_AUTOVACUUM)
 						blocking_autovacuum_proc = proc;
 
+					/*
+					 * If we note that we're blocked by some process running
+					 * REPACK (CONCURRENTLY), just fail.  That process is
+					 * going to upgrade its lock at some point, and it would
+					 * be inappropriate for any other process to cause that
+					 * to fail.
+					 */
+					if (checkProc == MyProc &&
+						proc->statusFlags & PROC_IN_CONCURRENT_REPACK)
+						/* not a serious error message proposal */
+						elog(ERROR, "oops, we're blocked by REPACK, time to die");
+
 					/* We're done looking at this proclock */
 					break;
 				}
diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h
index 1dad125706e..4e340ead455 100644
--- a/src/include/storage/proc.h
+++ b/src/include/storage/proc.h
@@ -69,6 +69,7 @@ struct XidCache
 #define		PROC_AFFECTS_ALL_HORIZONS	0x20	/* this proc's xmin must be
 												 * included in vacuum horizons
 												 * in all databases */
+#define		PROC_IN_CONCURRENT_REPACK	0x40	/* REPACK (CONCURRENTLY) */
 
 /* flags reset at EOXact */
 #define		PROC_VACUUM_STATE_MASK \
diff --git a/src/test/modules/injection_points/Makefile b/src/test/modules/injection_points/Makefile
index 2cd7d87c533..f7663859fe2 100644
--- a/src/test/modules/injection_points/Makefile
+++ b/src/test/modules/injection_points/Makefile
@@ -15,6 +15,7 @@ REGRESS_OPTS = --dlpath=$(top_builddir)/src/test/regress
 ISOLATION = basic \
 	    inplace \
 	    repack \
+	    repack_deadlock \
 	    repack_toast \
 	    syscache-update-pruned \
 	    heap_lock_update
diff --git a/src/test/modules/injection_points/expected/repack_deadlock.out b/src/test/modules/injection_points/expected/repack_deadlock.out
new file mode 100644
index 00000000000..42505d10933
--- /dev/null
+++ b/src/test/modules/injection_points/expected/repack_deadlock.out
@@ -0,0 +1,63 @@
+Parsed test spec with 2 sessions
+
+starting permutation: wait_before_lock add_column wakeup_before_lock check1
+injection_points_attach
+-----------------------
+                       
+(1 row)
+
+step wait_before_lock: 
+	REPACK (CONCURRENTLY) repack_deadlock USING INDEX repack_deadlock_pkey;
+ <waiting ...>
+step add_column: 
+	alter table repack_deadlock add column noise text;
+ <waiting ...>
+step add_column: <... completed>
+ERROR:  oops, we're blocked by REPACK, time to die
+step wakeup_before_lock: 
+	SELECT injection_points_wakeup('repack-concurrently-before-lock');
+
+injection_points_wakeup
+-----------------------
+                       
+(1 row)
+
+step wait_before_lock: <... completed>
+step check1: 
+	INSERT INTO relfilenodes(node)
+	SELECT relfilenode FROM pg_class WHERE relname='repack_deadlock';
+
+	SELECT count(DISTINCT node) FROM relfilenodes;
+
+	SELECT i, j FROM repack_deadlock ORDER BY i, j;
+
+	INSERT INTO data_s1(i, j)
+	SELECT i, j FROM repack_deadlock;
+
+	SELECT count(*)
+	FROM data_s1 d1 FULL JOIN data_s2 d2 USING (i, j)
+	WHERE d1.i ISNULL OR d2.i ISNULL;
+
+count
+-----
+    1
+(1 row)
+
+i|j
+-+-
+1|1
+2|2
+3|3
+4|4
+(4 rows)
+
+count
+-----
+    4
+(1 row)
+
+injection_points_detach
+-----------------------
+                       
+(1 row)
+
diff --git a/src/test/modules/injection_points/meson.build b/src/test/modules/injection_points/meson.build
index a414abb924b..1cd88d6db65 100644
--- a/src/test/modules/injection_points/meson.build
+++ b/src/test/modules/injection_points/meson.build
@@ -46,6 +46,7 @@ tests += {
       'basic',
       'inplace',
       'repack',
+      'repack_deadlock',
       'repack_toast',
       'syscache-update-pruned',
       'heap_lock_update',
diff --git a/src/test/modules/injection_points/specs/repack_deadlock.spec b/src/test/modules/injection_points/specs/repack_deadlock.spec
new file mode 100644
index 00000000000..9d23a6588c2
--- /dev/null
+++ b/src/test/modules/injection_points/specs/repack_deadlock.spec
@@ -0,0 +1,83 @@
+# Test REPACK with a concurrent transaction that would cause a deadlock
+setup
+{
+	CREATE EXTENSION injection_points;
+
+	CREATE TABLE repack_deadlock(i int PRIMARY KEY, j int);
+	INSERT INTO repack_deadlock(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4);
+
+	CREATE TABLE relfilenodes(node oid);
+
+	CREATE TABLE data_s1(i int, j int);
+	CREATE TABLE data_s2(i int, j int);
+}
+
+teardown
+{
+	DROP TABLE repack_deadlock;
+	DROP EXTENSION injection_points;
+
+	DROP TABLE relfilenodes;
+	DROP TABLE data_s1;
+	DROP TABLE data_s2;
+}
+
+session s1
+setup
+{
+	SELECT injection_points_set_local();
+	SELECT injection_points_attach('repack-concurrently-before-lock', 'wait');
+}
+# Perform the initial load and wait for s2 to do some data changes.
+step wait_before_lock
+{
+	REPACK (CONCURRENTLY) repack_deadlock USING INDEX repack_deadlock_pkey;
+}
+# Check the table from the perspective of s1.
+#
+# Besides the contents, we also check that relfilenode has changed.
+
+# Have each session write the contents into a table and use FULL JOIN to check
+# if the outputs are identical.
+step check1
+{
+	INSERT INTO relfilenodes(node)
+	SELECT relfilenode FROM pg_class WHERE relname='repack_deadlock';
+
+	SELECT count(DISTINCT node) FROM relfilenodes;
+
+	SELECT i, j FROM repack_deadlock ORDER BY i, j;
+
+	INSERT INTO data_s1(i, j)
+	SELECT i, j FROM repack_deadlock;
+
+	SELECT count(*)
+	FROM data_s1 d1 FULL JOIN data_s2 d2 USING (i, j)
+	WHERE d1.i ISNULL OR d2.i ISNULL;
+}
+teardown
+{
+	SELECT injection_points_detach('repack-concurrently-before-lock');
+}
+
+session s2
+# Change the existing data. UPDATE changes both key and non-key columns. Also
+# update one row twice to test whether tuple version generated by this session
+# can be found.
+step add_column
+{
+	alter table repack_deadlock add column noise text;
+}
+
+step wakeup_before_lock
+{
+	SELECT injection_points_wakeup('repack-concurrently-before-lock');
+}
+
+# Test if data changes introduced while one session is performing REPACK
+# CONCURRENTLY find their way into the table.
+permutation
+	wait_before_lock
+	add_column
+	wakeup_before_lock
+	check1
-- 
2.47.3


--fot5plqmb33ey33c--





^ permalink  raw  reply  [nested|flat] 89+ messages in thread

* [PATCH v44b 4/4] hack deadlock detector
@ 2026-03-25 19:35  Álvaro Herrera <[email protected]>
  0 siblings, 0 replies; 89+ messages in thread

From: Álvaro Herrera @ 2026-03-25 19:35 UTC (permalink / raw)

---
 src/backend/commands/cluster.c                | 10 +++
 src/backend/storage/lmgr/deadlock.c           | 12 +++
 src/include/storage/proc.h                    |  1 +
 src/test/modules/injection_points/Makefile    |  1 +
 .../expected/repack_deadlock.out              | 63 ++++++++++++++
 src/test/modules/injection_points/meson.build |  1 +
 .../specs/repack_deadlock.spec                | 83 +++++++++++++++++++
 7 files changed, 171 insertions(+)
 create mode 100644 src/test/modules/injection_points/expected/repack_deadlock.out
 create mode 100644 src/test/modules/injection_points/specs/repack_deadlock.spec

diff --git a/src/backend/commands/cluster.c b/src/backend/commands/cluster.c
index 2c3058ba10d..d5b1dfbff69 100644
--- a/src/backend/commands/cluster.c
+++ b/src/backend/commands/cluster.c
@@ -372,6 +372,16 @@ ExecRepack(ParseState *pstate, RepackStmt *stmt, bool isTopLevel)
 	/* Determine the lock mode to use. */
 	lockmode = RepackLockLevel((params.options & CLUOPT_CONCURRENT) != 0);
 
+	/*
+	 * If in concurrent mode, set the PROC_IN_CONCURRENT_REPACK flag.  This
+	 * makes the deadlock checker cause anyone that would conflict with us
+	 * to error out.  It's important to set this flag ahead of actually locking
+	 * the relation; it won't of course affect anyone until we do have a lock
+	 * that others can conflict with.
+	 */
+	if ((params.options & CLUOPT_CONCURRENT) != 0)
+		MyProc->statusFlags |= PROC_IN_CONCURRENT_REPACK;
+
 	/*
 	 * If a single relation is specified, process it and we're done ... unless
 	 * the relation is a partitioned table, in which case we fall through.
diff --git a/src/backend/storage/lmgr/deadlock.c b/src/backend/storage/lmgr/deadlock.c
index b8962d875b6..10266e384aa 100644
--- a/src/backend/storage/lmgr/deadlock.c
+++ b/src/backend/storage/lmgr/deadlock.c
@@ -620,6 +620,18 @@ FindLockCycleRecurseMember(PGPROC *checkProc,
 						proc->statusFlags & PROC_IS_AUTOVACUUM)
 						blocking_autovacuum_proc = proc;
 
+					/*
+					 * If we note that we're blocked by some process running
+					 * REPACK (CONCURRENTLY), just fail.  That process is
+					 * going to upgrade its lock at some point, and it would
+					 * be inappropriate for any other process to cause that
+					 * to fail.
+					 */
+					if (checkProc == MyProc &&
+						proc->statusFlags & PROC_IN_CONCURRENT_REPACK)
+						/* not a serious error message proposal */
+						elog(ERROR, "oops, we're blocked by REPACK, time to die");
+
 					/* We're done looking at this proclock */
 					break;
 				}
diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h
index 1dad125706e..4e340ead455 100644
--- a/src/include/storage/proc.h
+++ b/src/include/storage/proc.h
@@ -69,6 +69,7 @@ struct XidCache
 #define		PROC_AFFECTS_ALL_HORIZONS	0x20	/* this proc's xmin must be
 												 * included in vacuum horizons
 												 * in all databases */
+#define		PROC_IN_CONCURRENT_REPACK	0x40	/* REPACK (CONCURRENTLY) */
 
 /* flags reset at EOXact */
 #define		PROC_VACUUM_STATE_MASK \
diff --git a/src/test/modules/injection_points/Makefile b/src/test/modules/injection_points/Makefile
index 2cd7d87c533..f7663859fe2 100644
--- a/src/test/modules/injection_points/Makefile
+++ b/src/test/modules/injection_points/Makefile
@@ -15,6 +15,7 @@ REGRESS_OPTS = --dlpath=$(top_builddir)/src/test/regress
 ISOLATION = basic \
 	    inplace \
 	    repack \
+	    repack_deadlock \
 	    repack_toast \
 	    syscache-update-pruned \
 	    heap_lock_update
diff --git a/src/test/modules/injection_points/expected/repack_deadlock.out b/src/test/modules/injection_points/expected/repack_deadlock.out
new file mode 100644
index 00000000000..42505d10933
--- /dev/null
+++ b/src/test/modules/injection_points/expected/repack_deadlock.out
@@ -0,0 +1,63 @@
+Parsed test spec with 2 sessions
+
+starting permutation: wait_before_lock add_column wakeup_before_lock check1
+injection_points_attach
+-----------------------
+                       
+(1 row)
+
+step wait_before_lock: 
+	REPACK (CONCURRENTLY) repack_deadlock USING INDEX repack_deadlock_pkey;
+ <waiting ...>
+step add_column: 
+	alter table repack_deadlock add column noise text;
+ <waiting ...>
+step add_column: <... completed>
+ERROR:  oops, we're blocked by REPACK, time to die
+step wakeup_before_lock: 
+	SELECT injection_points_wakeup('repack-concurrently-before-lock');
+
+injection_points_wakeup
+-----------------------
+                       
+(1 row)
+
+step wait_before_lock: <... completed>
+step check1: 
+	INSERT INTO relfilenodes(node)
+	SELECT relfilenode FROM pg_class WHERE relname='repack_deadlock';
+
+	SELECT count(DISTINCT node) FROM relfilenodes;
+
+	SELECT i, j FROM repack_deadlock ORDER BY i, j;
+
+	INSERT INTO data_s1(i, j)
+	SELECT i, j FROM repack_deadlock;
+
+	SELECT count(*)
+	FROM data_s1 d1 FULL JOIN data_s2 d2 USING (i, j)
+	WHERE d1.i ISNULL OR d2.i ISNULL;
+
+count
+-----
+    1
+(1 row)
+
+i|j
+-+-
+1|1
+2|2
+3|3
+4|4
+(4 rows)
+
+count
+-----
+    4
+(1 row)
+
+injection_points_detach
+-----------------------
+                       
+(1 row)
+
diff --git a/src/test/modules/injection_points/meson.build b/src/test/modules/injection_points/meson.build
index a414abb924b..1cd88d6db65 100644
--- a/src/test/modules/injection_points/meson.build
+++ b/src/test/modules/injection_points/meson.build
@@ -46,6 +46,7 @@ tests += {
       'basic',
       'inplace',
       'repack',
+      'repack_deadlock',
       'repack_toast',
       'syscache-update-pruned',
       'heap_lock_update',
diff --git a/src/test/modules/injection_points/specs/repack_deadlock.spec b/src/test/modules/injection_points/specs/repack_deadlock.spec
new file mode 100644
index 00000000000..9d23a6588c2
--- /dev/null
+++ b/src/test/modules/injection_points/specs/repack_deadlock.spec
@@ -0,0 +1,83 @@
+# Test REPACK with a concurrent transaction that would cause a deadlock
+setup
+{
+	CREATE EXTENSION injection_points;
+
+	CREATE TABLE repack_deadlock(i int PRIMARY KEY, j int);
+	INSERT INTO repack_deadlock(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4);
+
+	CREATE TABLE relfilenodes(node oid);
+
+	CREATE TABLE data_s1(i int, j int);
+	CREATE TABLE data_s2(i int, j int);
+}
+
+teardown
+{
+	DROP TABLE repack_deadlock;
+	DROP EXTENSION injection_points;
+
+	DROP TABLE relfilenodes;
+	DROP TABLE data_s1;
+	DROP TABLE data_s2;
+}
+
+session s1
+setup
+{
+	SELECT injection_points_set_local();
+	SELECT injection_points_attach('repack-concurrently-before-lock', 'wait');
+}
+# Perform the initial load and wait for s2 to do some data changes.
+step wait_before_lock
+{
+	REPACK (CONCURRENTLY) repack_deadlock USING INDEX repack_deadlock_pkey;
+}
+# Check the table from the perspective of s1.
+#
+# Besides the contents, we also check that relfilenode has changed.
+
+# Have each session write the contents into a table and use FULL JOIN to check
+# if the outputs are identical.
+step check1
+{
+	INSERT INTO relfilenodes(node)
+	SELECT relfilenode FROM pg_class WHERE relname='repack_deadlock';
+
+	SELECT count(DISTINCT node) FROM relfilenodes;
+
+	SELECT i, j FROM repack_deadlock ORDER BY i, j;
+
+	INSERT INTO data_s1(i, j)
+	SELECT i, j FROM repack_deadlock;
+
+	SELECT count(*)
+	FROM data_s1 d1 FULL JOIN data_s2 d2 USING (i, j)
+	WHERE d1.i ISNULL OR d2.i ISNULL;
+}
+teardown
+{
+	SELECT injection_points_detach('repack-concurrently-before-lock');
+}
+
+session s2
+# Change the existing data. UPDATE changes both key and non-key columns. Also
+# update one row twice to test whether tuple version generated by this session
+# can be found.
+step add_column
+{
+	alter table repack_deadlock add column noise text;
+}
+
+step wakeup_before_lock
+{
+	SELECT injection_points_wakeup('repack-concurrently-before-lock');
+}
+
+# Test if data changes introduced while one session is performing REPACK
+# CONCURRENTLY find their way into the table.
+permutation
+	wait_before_lock
+	add_column
+	wakeup_before_lock
+	check1
-- 
2.47.3


--fot5plqmb33ey33c--





^ permalink  raw  reply  [nested|flat] 89+ messages in thread

* [PATCH v44b 4/4] hack deadlock detector
@ 2026-03-25 19:35  Álvaro Herrera <[email protected]>
  0 siblings, 0 replies; 89+ messages in thread

From: Álvaro Herrera @ 2026-03-25 19:35 UTC (permalink / raw)

---
 src/backend/commands/cluster.c                | 10 +++
 src/backend/storage/lmgr/deadlock.c           | 12 +++
 src/include/storage/proc.h                    |  1 +
 src/test/modules/injection_points/Makefile    |  1 +
 .../expected/repack_deadlock.out              | 63 ++++++++++++++
 src/test/modules/injection_points/meson.build |  1 +
 .../specs/repack_deadlock.spec                | 83 +++++++++++++++++++
 7 files changed, 171 insertions(+)
 create mode 100644 src/test/modules/injection_points/expected/repack_deadlock.out
 create mode 100644 src/test/modules/injection_points/specs/repack_deadlock.spec

diff --git a/src/backend/commands/cluster.c b/src/backend/commands/cluster.c
index 2c3058ba10d..d5b1dfbff69 100644
--- a/src/backend/commands/cluster.c
+++ b/src/backend/commands/cluster.c
@@ -372,6 +372,16 @@ ExecRepack(ParseState *pstate, RepackStmt *stmt, bool isTopLevel)
 	/* Determine the lock mode to use. */
 	lockmode = RepackLockLevel((params.options & CLUOPT_CONCURRENT) != 0);
 
+	/*
+	 * If in concurrent mode, set the PROC_IN_CONCURRENT_REPACK flag.  This
+	 * makes the deadlock checker cause anyone that would conflict with us
+	 * to error out.  It's important to set this flag ahead of actually locking
+	 * the relation; it won't of course affect anyone until we do have a lock
+	 * that others can conflict with.
+	 */
+	if ((params.options & CLUOPT_CONCURRENT) != 0)
+		MyProc->statusFlags |= PROC_IN_CONCURRENT_REPACK;
+
 	/*
 	 * If a single relation is specified, process it and we're done ... unless
 	 * the relation is a partitioned table, in which case we fall through.
diff --git a/src/backend/storage/lmgr/deadlock.c b/src/backend/storage/lmgr/deadlock.c
index b8962d875b6..10266e384aa 100644
--- a/src/backend/storage/lmgr/deadlock.c
+++ b/src/backend/storage/lmgr/deadlock.c
@@ -620,6 +620,18 @@ FindLockCycleRecurseMember(PGPROC *checkProc,
 						proc->statusFlags & PROC_IS_AUTOVACUUM)
 						blocking_autovacuum_proc = proc;
 
+					/*
+					 * If we note that we're blocked by some process running
+					 * REPACK (CONCURRENTLY), just fail.  That process is
+					 * going to upgrade its lock at some point, and it would
+					 * be inappropriate for any other process to cause that
+					 * to fail.
+					 */
+					if (checkProc == MyProc &&
+						proc->statusFlags & PROC_IN_CONCURRENT_REPACK)
+						/* not a serious error message proposal */
+						elog(ERROR, "oops, we're blocked by REPACK, time to die");
+
 					/* We're done looking at this proclock */
 					break;
 				}
diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h
index 1dad125706e..4e340ead455 100644
--- a/src/include/storage/proc.h
+++ b/src/include/storage/proc.h
@@ -69,6 +69,7 @@ struct XidCache
 #define		PROC_AFFECTS_ALL_HORIZONS	0x20	/* this proc's xmin must be
 												 * included in vacuum horizons
 												 * in all databases */
+#define		PROC_IN_CONCURRENT_REPACK	0x40	/* REPACK (CONCURRENTLY) */
 
 /* flags reset at EOXact */
 #define		PROC_VACUUM_STATE_MASK \
diff --git a/src/test/modules/injection_points/Makefile b/src/test/modules/injection_points/Makefile
index 2cd7d87c533..f7663859fe2 100644
--- a/src/test/modules/injection_points/Makefile
+++ b/src/test/modules/injection_points/Makefile
@@ -15,6 +15,7 @@ REGRESS_OPTS = --dlpath=$(top_builddir)/src/test/regress
 ISOLATION = basic \
 	    inplace \
 	    repack \
+	    repack_deadlock \
 	    repack_toast \
 	    syscache-update-pruned \
 	    heap_lock_update
diff --git a/src/test/modules/injection_points/expected/repack_deadlock.out b/src/test/modules/injection_points/expected/repack_deadlock.out
new file mode 100644
index 00000000000..42505d10933
--- /dev/null
+++ b/src/test/modules/injection_points/expected/repack_deadlock.out
@@ -0,0 +1,63 @@
+Parsed test spec with 2 sessions
+
+starting permutation: wait_before_lock add_column wakeup_before_lock check1
+injection_points_attach
+-----------------------
+                       
+(1 row)
+
+step wait_before_lock: 
+	REPACK (CONCURRENTLY) repack_deadlock USING INDEX repack_deadlock_pkey;
+ <waiting ...>
+step add_column: 
+	alter table repack_deadlock add column noise text;
+ <waiting ...>
+step add_column: <... completed>
+ERROR:  oops, we're blocked by REPACK, time to die
+step wakeup_before_lock: 
+	SELECT injection_points_wakeup('repack-concurrently-before-lock');
+
+injection_points_wakeup
+-----------------------
+                       
+(1 row)
+
+step wait_before_lock: <... completed>
+step check1: 
+	INSERT INTO relfilenodes(node)
+	SELECT relfilenode FROM pg_class WHERE relname='repack_deadlock';
+
+	SELECT count(DISTINCT node) FROM relfilenodes;
+
+	SELECT i, j FROM repack_deadlock ORDER BY i, j;
+
+	INSERT INTO data_s1(i, j)
+	SELECT i, j FROM repack_deadlock;
+
+	SELECT count(*)
+	FROM data_s1 d1 FULL JOIN data_s2 d2 USING (i, j)
+	WHERE d1.i ISNULL OR d2.i ISNULL;
+
+count
+-----
+    1
+(1 row)
+
+i|j
+-+-
+1|1
+2|2
+3|3
+4|4
+(4 rows)
+
+count
+-----
+    4
+(1 row)
+
+injection_points_detach
+-----------------------
+                       
+(1 row)
+
diff --git a/src/test/modules/injection_points/meson.build b/src/test/modules/injection_points/meson.build
index a414abb924b..1cd88d6db65 100644
--- a/src/test/modules/injection_points/meson.build
+++ b/src/test/modules/injection_points/meson.build
@@ -46,6 +46,7 @@ tests += {
       'basic',
       'inplace',
       'repack',
+      'repack_deadlock',
       'repack_toast',
       'syscache-update-pruned',
       'heap_lock_update',
diff --git a/src/test/modules/injection_points/specs/repack_deadlock.spec b/src/test/modules/injection_points/specs/repack_deadlock.spec
new file mode 100644
index 00000000000..9d23a6588c2
--- /dev/null
+++ b/src/test/modules/injection_points/specs/repack_deadlock.spec
@@ -0,0 +1,83 @@
+# Test REPACK with a concurrent transaction that would cause a deadlock
+setup
+{
+	CREATE EXTENSION injection_points;
+
+	CREATE TABLE repack_deadlock(i int PRIMARY KEY, j int);
+	INSERT INTO repack_deadlock(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4);
+
+	CREATE TABLE relfilenodes(node oid);
+
+	CREATE TABLE data_s1(i int, j int);
+	CREATE TABLE data_s2(i int, j int);
+}
+
+teardown
+{
+	DROP TABLE repack_deadlock;
+	DROP EXTENSION injection_points;
+
+	DROP TABLE relfilenodes;
+	DROP TABLE data_s1;
+	DROP TABLE data_s2;
+}
+
+session s1
+setup
+{
+	SELECT injection_points_set_local();
+	SELECT injection_points_attach('repack-concurrently-before-lock', 'wait');
+}
+# Perform the initial load and wait for s2 to do some data changes.
+step wait_before_lock
+{
+	REPACK (CONCURRENTLY) repack_deadlock USING INDEX repack_deadlock_pkey;
+}
+# Check the table from the perspective of s1.
+#
+# Besides the contents, we also check that relfilenode has changed.
+
+# Have each session write the contents into a table and use FULL JOIN to check
+# if the outputs are identical.
+step check1
+{
+	INSERT INTO relfilenodes(node)
+	SELECT relfilenode FROM pg_class WHERE relname='repack_deadlock';
+
+	SELECT count(DISTINCT node) FROM relfilenodes;
+
+	SELECT i, j FROM repack_deadlock ORDER BY i, j;
+
+	INSERT INTO data_s1(i, j)
+	SELECT i, j FROM repack_deadlock;
+
+	SELECT count(*)
+	FROM data_s1 d1 FULL JOIN data_s2 d2 USING (i, j)
+	WHERE d1.i ISNULL OR d2.i ISNULL;
+}
+teardown
+{
+	SELECT injection_points_detach('repack-concurrently-before-lock');
+}
+
+session s2
+# Change the existing data. UPDATE changes both key and non-key columns. Also
+# update one row twice to test whether tuple version generated by this session
+# can be found.
+step add_column
+{
+	alter table repack_deadlock add column noise text;
+}
+
+step wakeup_before_lock
+{
+	SELECT injection_points_wakeup('repack-concurrently-before-lock');
+}
+
+# Test if data changes introduced while one session is performing REPACK
+# CONCURRENTLY find their way into the table.
+permutation
+	wait_before_lock
+	add_column
+	wakeup_before_lock
+	check1
-- 
2.47.3


--fot5plqmb33ey33c--





^ permalink  raw  reply  [nested|flat] 89+ messages in thread

* [PATCH v44b 4/4] hack deadlock detector
@ 2026-03-25 19:35  Álvaro Herrera <[email protected]>
  0 siblings, 0 replies; 89+ messages in thread

From: Álvaro Herrera @ 2026-03-25 19:35 UTC (permalink / raw)

---
 src/backend/commands/cluster.c                | 10 +++
 src/backend/storage/lmgr/deadlock.c           | 12 +++
 src/include/storage/proc.h                    |  1 +
 src/test/modules/injection_points/Makefile    |  1 +
 .../expected/repack_deadlock.out              | 63 ++++++++++++++
 src/test/modules/injection_points/meson.build |  1 +
 .../specs/repack_deadlock.spec                | 83 +++++++++++++++++++
 7 files changed, 171 insertions(+)
 create mode 100644 src/test/modules/injection_points/expected/repack_deadlock.out
 create mode 100644 src/test/modules/injection_points/specs/repack_deadlock.spec

diff --git a/src/backend/commands/cluster.c b/src/backend/commands/cluster.c
index 2c3058ba10d..d5b1dfbff69 100644
--- a/src/backend/commands/cluster.c
+++ b/src/backend/commands/cluster.c
@@ -372,6 +372,16 @@ ExecRepack(ParseState *pstate, RepackStmt *stmt, bool isTopLevel)
 	/* Determine the lock mode to use. */
 	lockmode = RepackLockLevel((params.options & CLUOPT_CONCURRENT) != 0);
 
+	/*
+	 * If in concurrent mode, set the PROC_IN_CONCURRENT_REPACK flag.  This
+	 * makes the deadlock checker cause anyone that would conflict with us
+	 * to error out.  It's important to set this flag ahead of actually locking
+	 * the relation; it won't of course affect anyone until we do have a lock
+	 * that others can conflict with.
+	 */
+	if ((params.options & CLUOPT_CONCURRENT) != 0)
+		MyProc->statusFlags |= PROC_IN_CONCURRENT_REPACK;
+
 	/*
 	 * If a single relation is specified, process it and we're done ... unless
 	 * the relation is a partitioned table, in which case we fall through.
diff --git a/src/backend/storage/lmgr/deadlock.c b/src/backend/storage/lmgr/deadlock.c
index b8962d875b6..10266e384aa 100644
--- a/src/backend/storage/lmgr/deadlock.c
+++ b/src/backend/storage/lmgr/deadlock.c
@@ -620,6 +620,18 @@ FindLockCycleRecurseMember(PGPROC *checkProc,
 						proc->statusFlags & PROC_IS_AUTOVACUUM)
 						blocking_autovacuum_proc = proc;
 
+					/*
+					 * If we note that we're blocked by some process running
+					 * REPACK (CONCURRENTLY), just fail.  That process is
+					 * going to upgrade its lock at some point, and it would
+					 * be inappropriate for any other process to cause that
+					 * to fail.
+					 */
+					if (checkProc == MyProc &&
+						proc->statusFlags & PROC_IN_CONCURRENT_REPACK)
+						/* not a serious error message proposal */
+						elog(ERROR, "oops, we're blocked by REPACK, time to die");
+
 					/* We're done looking at this proclock */
 					break;
 				}
diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h
index 1dad125706e..4e340ead455 100644
--- a/src/include/storage/proc.h
+++ b/src/include/storage/proc.h
@@ -69,6 +69,7 @@ struct XidCache
 #define		PROC_AFFECTS_ALL_HORIZONS	0x20	/* this proc's xmin must be
 												 * included in vacuum horizons
 												 * in all databases */
+#define		PROC_IN_CONCURRENT_REPACK	0x40	/* REPACK (CONCURRENTLY) */
 
 /* flags reset at EOXact */
 #define		PROC_VACUUM_STATE_MASK \
diff --git a/src/test/modules/injection_points/Makefile b/src/test/modules/injection_points/Makefile
index 2cd7d87c533..f7663859fe2 100644
--- a/src/test/modules/injection_points/Makefile
+++ b/src/test/modules/injection_points/Makefile
@@ -15,6 +15,7 @@ REGRESS_OPTS = --dlpath=$(top_builddir)/src/test/regress
 ISOLATION = basic \
 	    inplace \
 	    repack \
+	    repack_deadlock \
 	    repack_toast \
 	    syscache-update-pruned \
 	    heap_lock_update
diff --git a/src/test/modules/injection_points/expected/repack_deadlock.out b/src/test/modules/injection_points/expected/repack_deadlock.out
new file mode 100644
index 00000000000..42505d10933
--- /dev/null
+++ b/src/test/modules/injection_points/expected/repack_deadlock.out
@@ -0,0 +1,63 @@
+Parsed test spec with 2 sessions
+
+starting permutation: wait_before_lock add_column wakeup_before_lock check1
+injection_points_attach
+-----------------------
+                       
+(1 row)
+
+step wait_before_lock: 
+	REPACK (CONCURRENTLY) repack_deadlock USING INDEX repack_deadlock_pkey;
+ <waiting ...>
+step add_column: 
+	alter table repack_deadlock add column noise text;
+ <waiting ...>
+step add_column: <... completed>
+ERROR:  oops, we're blocked by REPACK, time to die
+step wakeup_before_lock: 
+	SELECT injection_points_wakeup('repack-concurrently-before-lock');
+
+injection_points_wakeup
+-----------------------
+                       
+(1 row)
+
+step wait_before_lock: <... completed>
+step check1: 
+	INSERT INTO relfilenodes(node)
+	SELECT relfilenode FROM pg_class WHERE relname='repack_deadlock';
+
+	SELECT count(DISTINCT node) FROM relfilenodes;
+
+	SELECT i, j FROM repack_deadlock ORDER BY i, j;
+
+	INSERT INTO data_s1(i, j)
+	SELECT i, j FROM repack_deadlock;
+
+	SELECT count(*)
+	FROM data_s1 d1 FULL JOIN data_s2 d2 USING (i, j)
+	WHERE d1.i ISNULL OR d2.i ISNULL;
+
+count
+-----
+    1
+(1 row)
+
+i|j
+-+-
+1|1
+2|2
+3|3
+4|4
+(4 rows)
+
+count
+-----
+    4
+(1 row)
+
+injection_points_detach
+-----------------------
+                       
+(1 row)
+
diff --git a/src/test/modules/injection_points/meson.build b/src/test/modules/injection_points/meson.build
index a414abb924b..1cd88d6db65 100644
--- a/src/test/modules/injection_points/meson.build
+++ b/src/test/modules/injection_points/meson.build
@@ -46,6 +46,7 @@ tests += {
       'basic',
       'inplace',
       'repack',
+      'repack_deadlock',
       'repack_toast',
       'syscache-update-pruned',
       'heap_lock_update',
diff --git a/src/test/modules/injection_points/specs/repack_deadlock.spec b/src/test/modules/injection_points/specs/repack_deadlock.spec
new file mode 100644
index 00000000000..9d23a6588c2
--- /dev/null
+++ b/src/test/modules/injection_points/specs/repack_deadlock.spec
@@ -0,0 +1,83 @@
+# Test REPACK with a concurrent transaction that would cause a deadlock
+setup
+{
+	CREATE EXTENSION injection_points;
+
+	CREATE TABLE repack_deadlock(i int PRIMARY KEY, j int);
+	INSERT INTO repack_deadlock(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4);
+
+	CREATE TABLE relfilenodes(node oid);
+
+	CREATE TABLE data_s1(i int, j int);
+	CREATE TABLE data_s2(i int, j int);
+}
+
+teardown
+{
+	DROP TABLE repack_deadlock;
+	DROP EXTENSION injection_points;
+
+	DROP TABLE relfilenodes;
+	DROP TABLE data_s1;
+	DROP TABLE data_s2;
+}
+
+session s1
+setup
+{
+	SELECT injection_points_set_local();
+	SELECT injection_points_attach('repack-concurrently-before-lock', 'wait');
+}
+# Perform the initial load and wait for s2 to do some data changes.
+step wait_before_lock
+{
+	REPACK (CONCURRENTLY) repack_deadlock USING INDEX repack_deadlock_pkey;
+}
+# Check the table from the perspective of s1.
+#
+# Besides the contents, we also check that relfilenode has changed.
+
+# Have each session write the contents into a table and use FULL JOIN to check
+# if the outputs are identical.
+step check1
+{
+	INSERT INTO relfilenodes(node)
+	SELECT relfilenode FROM pg_class WHERE relname='repack_deadlock';
+
+	SELECT count(DISTINCT node) FROM relfilenodes;
+
+	SELECT i, j FROM repack_deadlock ORDER BY i, j;
+
+	INSERT INTO data_s1(i, j)
+	SELECT i, j FROM repack_deadlock;
+
+	SELECT count(*)
+	FROM data_s1 d1 FULL JOIN data_s2 d2 USING (i, j)
+	WHERE d1.i ISNULL OR d2.i ISNULL;
+}
+teardown
+{
+	SELECT injection_points_detach('repack-concurrently-before-lock');
+}
+
+session s2
+# Change the existing data. UPDATE changes both key and non-key columns. Also
+# update one row twice to test whether tuple version generated by this session
+# can be found.
+step add_column
+{
+	alter table repack_deadlock add column noise text;
+}
+
+step wakeup_before_lock
+{
+	SELECT injection_points_wakeup('repack-concurrently-before-lock');
+}
+
+# Test if data changes introduced while one session is performing REPACK
+# CONCURRENTLY find their way into the table.
+permutation
+	wait_before_lock
+	add_column
+	wakeup_before_lock
+	check1
-- 
2.47.3


--fot5plqmb33ey33c--





^ permalink  raw  reply  [nested|flat] 89+ messages in thread

* [PATCH v44b 4/4] hack deadlock detector
@ 2026-03-25 19:35  Álvaro Herrera <[email protected]>
  0 siblings, 0 replies; 89+ messages in thread

From: Álvaro Herrera @ 2026-03-25 19:35 UTC (permalink / raw)

---
 src/backend/commands/cluster.c                | 10 +++
 src/backend/storage/lmgr/deadlock.c           | 12 +++
 src/include/storage/proc.h                    |  1 +
 src/test/modules/injection_points/Makefile    |  1 +
 .../expected/repack_deadlock.out              | 63 ++++++++++++++
 src/test/modules/injection_points/meson.build |  1 +
 .../specs/repack_deadlock.spec                | 83 +++++++++++++++++++
 7 files changed, 171 insertions(+)
 create mode 100644 src/test/modules/injection_points/expected/repack_deadlock.out
 create mode 100644 src/test/modules/injection_points/specs/repack_deadlock.spec

diff --git a/src/backend/commands/cluster.c b/src/backend/commands/cluster.c
index 2c3058ba10d..d5b1dfbff69 100644
--- a/src/backend/commands/cluster.c
+++ b/src/backend/commands/cluster.c
@@ -372,6 +372,16 @@ ExecRepack(ParseState *pstate, RepackStmt *stmt, bool isTopLevel)
 	/* Determine the lock mode to use. */
 	lockmode = RepackLockLevel((params.options & CLUOPT_CONCURRENT) != 0);
 
+	/*
+	 * If in concurrent mode, set the PROC_IN_CONCURRENT_REPACK flag.  This
+	 * makes the deadlock checker cause anyone that would conflict with us
+	 * to error out.  It's important to set this flag ahead of actually locking
+	 * the relation; it won't of course affect anyone until we do have a lock
+	 * that others can conflict with.
+	 */
+	if ((params.options & CLUOPT_CONCURRENT) != 0)
+		MyProc->statusFlags |= PROC_IN_CONCURRENT_REPACK;
+
 	/*
 	 * If a single relation is specified, process it and we're done ... unless
 	 * the relation is a partitioned table, in which case we fall through.
diff --git a/src/backend/storage/lmgr/deadlock.c b/src/backend/storage/lmgr/deadlock.c
index b8962d875b6..10266e384aa 100644
--- a/src/backend/storage/lmgr/deadlock.c
+++ b/src/backend/storage/lmgr/deadlock.c
@@ -620,6 +620,18 @@ FindLockCycleRecurseMember(PGPROC *checkProc,
 						proc->statusFlags & PROC_IS_AUTOVACUUM)
 						blocking_autovacuum_proc = proc;
 
+					/*
+					 * If we note that we're blocked by some process running
+					 * REPACK (CONCURRENTLY), just fail.  That process is
+					 * going to upgrade its lock at some point, and it would
+					 * be inappropriate for any other process to cause that
+					 * to fail.
+					 */
+					if (checkProc == MyProc &&
+						proc->statusFlags & PROC_IN_CONCURRENT_REPACK)
+						/* not a serious error message proposal */
+						elog(ERROR, "oops, we're blocked by REPACK, time to die");
+
 					/* We're done looking at this proclock */
 					break;
 				}
diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h
index 1dad125706e..4e340ead455 100644
--- a/src/include/storage/proc.h
+++ b/src/include/storage/proc.h
@@ -69,6 +69,7 @@ struct XidCache
 #define		PROC_AFFECTS_ALL_HORIZONS	0x20	/* this proc's xmin must be
 												 * included in vacuum horizons
 												 * in all databases */
+#define		PROC_IN_CONCURRENT_REPACK	0x40	/* REPACK (CONCURRENTLY) */
 
 /* flags reset at EOXact */
 #define		PROC_VACUUM_STATE_MASK \
diff --git a/src/test/modules/injection_points/Makefile b/src/test/modules/injection_points/Makefile
index 2cd7d87c533..f7663859fe2 100644
--- a/src/test/modules/injection_points/Makefile
+++ b/src/test/modules/injection_points/Makefile
@@ -15,6 +15,7 @@ REGRESS_OPTS = --dlpath=$(top_builddir)/src/test/regress
 ISOLATION = basic \
 	    inplace \
 	    repack \
+	    repack_deadlock \
 	    repack_toast \
 	    syscache-update-pruned \
 	    heap_lock_update
diff --git a/src/test/modules/injection_points/expected/repack_deadlock.out b/src/test/modules/injection_points/expected/repack_deadlock.out
new file mode 100644
index 00000000000..42505d10933
--- /dev/null
+++ b/src/test/modules/injection_points/expected/repack_deadlock.out
@@ -0,0 +1,63 @@
+Parsed test spec with 2 sessions
+
+starting permutation: wait_before_lock add_column wakeup_before_lock check1
+injection_points_attach
+-----------------------
+                       
+(1 row)
+
+step wait_before_lock: 
+	REPACK (CONCURRENTLY) repack_deadlock USING INDEX repack_deadlock_pkey;
+ <waiting ...>
+step add_column: 
+	alter table repack_deadlock add column noise text;
+ <waiting ...>
+step add_column: <... completed>
+ERROR:  oops, we're blocked by REPACK, time to die
+step wakeup_before_lock: 
+	SELECT injection_points_wakeup('repack-concurrently-before-lock');
+
+injection_points_wakeup
+-----------------------
+                       
+(1 row)
+
+step wait_before_lock: <... completed>
+step check1: 
+	INSERT INTO relfilenodes(node)
+	SELECT relfilenode FROM pg_class WHERE relname='repack_deadlock';
+
+	SELECT count(DISTINCT node) FROM relfilenodes;
+
+	SELECT i, j FROM repack_deadlock ORDER BY i, j;
+
+	INSERT INTO data_s1(i, j)
+	SELECT i, j FROM repack_deadlock;
+
+	SELECT count(*)
+	FROM data_s1 d1 FULL JOIN data_s2 d2 USING (i, j)
+	WHERE d1.i ISNULL OR d2.i ISNULL;
+
+count
+-----
+    1
+(1 row)
+
+i|j
+-+-
+1|1
+2|2
+3|3
+4|4
+(4 rows)
+
+count
+-----
+    4
+(1 row)
+
+injection_points_detach
+-----------------------
+                       
+(1 row)
+
diff --git a/src/test/modules/injection_points/meson.build b/src/test/modules/injection_points/meson.build
index a414abb924b..1cd88d6db65 100644
--- a/src/test/modules/injection_points/meson.build
+++ b/src/test/modules/injection_points/meson.build
@@ -46,6 +46,7 @@ tests += {
       'basic',
       'inplace',
       'repack',
+      'repack_deadlock',
       'repack_toast',
       'syscache-update-pruned',
       'heap_lock_update',
diff --git a/src/test/modules/injection_points/specs/repack_deadlock.spec b/src/test/modules/injection_points/specs/repack_deadlock.spec
new file mode 100644
index 00000000000..9d23a6588c2
--- /dev/null
+++ b/src/test/modules/injection_points/specs/repack_deadlock.spec
@@ -0,0 +1,83 @@
+# Test REPACK with a concurrent transaction that would cause a deadlock
+setup
+{
+	CREATE EXTENSION injection_points;
+
+	CREATE TABLE repack_deadlock(i int PRIMARY KEY, j int);
+	INSERT INTO repack_deadlock(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4);
+
+	CREATE TABLE relfilenodes(node oid);
+
+	CREATE TABLE data_s1(i int, j int);
+	CREATE TABLE data_s2(i int, j int);
+}
+
+teardown
+{
+	DROP TABLE repack_deadlock;
+	DROP EXTENSION injection_points;
+
+	DROP TABLE relfilenodes;
+	DROP TABLE data_s1;
+	DROP TABLE data_s2;
+}
+
+session s1
+setup
+{
+	SELECT injection_points_set_local();
+	SELECT injection_points_attach('repack-concurrently-before-lock', 'wait');
+}
+# Perform the initial load and wait for s2 to do some data changes.
+step wait_before_lock
+{
+	REPACK (CONCURRENTLY) repack_deadlock USING INDEX repack_deadlock_pkey;
+}
+# Check the table from the perspective of s1.
+#
+# Besides the contents, we also check that relfilenode has changed.
+
+# Have each session write the contents into a table and use FULL JOIN to check
+# if the outputs are identical.
+step check1
+{
+	INSERT INTO relfilenodes(node)
+	SELECT relfilenode FROM pg_class WHERE relname='repack_deadlock';
+
+	SELECT count(DISTINCT node) FROM relfilenodes;
+
+	SELECT i, j FROM repack_deadlock ORDER BY i, j;
+
+	INSERT INTO data_s1(i, j)
+	SELECT i, j FROM repack_deadlock;
+
+	SELECT count(*)
+	FROM data_s1 d1 FULL JOIN data_s2 d2 USING (i, j)
+	WHERE d1.i ISNULL OR d2.i ISNULL;
+}
+teardown
+{
+	SELECT injection_points_detach('repack-concurrently-before-lock');
+}
+
+session s2
+# Change the existing data. UPDATE changes both key and non-key columns. Also
+# update one row twice to test whether tuple version generated by this session
+# can be found.
+step add_column
+{
+	alter table repack_deadlock add column noise text;
+}
+
+step wakeup_before_lock
+{
+	SELECT injection_points_wakeup('repack-concurrently-before-lock');
+}
+
+# Test if data changes introduced while one session is performing REPACK
+# CONCURRENTLY find their way into the table.
+permutation
+	wait_before_lock
+	add_column
+	wakeup_before_lock
+	check1
-- 
2.47.3


--fot5plqmb33ey33c--





^ permalink  raw  reply  [nested|flat] 89+ messages in thread

* [PATCH v44b 4/4] hack deadlock detector
@ 2026-03-25 19:35  Álvaro Herrera <[email protected]>
  0 siblings, 0 replies; 89+ messages in thread

From: Álvaro Herrera @ 2026-03-25 19:35 UTC (permalink / raw)

---
 src/backend/commands/cluster.c                | 10 +++
 src/backend/storage/lmgr/deadlock.c           | 12 +++
 src/include/storage/proc.h                    |  1 +
 src/test/modules/injection_points/Makefile    |  1 +
 .../expected/repack_deadlock.out              | 63 ++++++++++++++
 src/test/modules/injection_points/meson.build |  1 +
 .../specs/repack_deadlock.spec                | 83 +++++++++++++++++++
 7 files changed, 171 insertions(+)
 create mode 100644 src/test/modules/injection_points/expected/repack_deadlock.out
 create mode 100644 src/test/modules/injection_points/specs/repack_deadlock.spec

diff --git a/src/backend/commands/cluster.c b/src/backend/commands/cluster.c
index 2c3058ba10d..d5b1dfbff69 100644
--- a/src/backend/commands/cluster.c
+++ b/src/backend/commands/cluster.c
@@ -372,6 +372,16 @@ ExecRepack(ParseState *pstate, RepackStmt *stmt, bool isTopLevel)
 	/* Determine the lock mode to use. */
 	lockmode = RepackLockLevel((params.options & CLUOPT_CONCURRENT) != 0);
 
+	/*
+	 * If in concurrent mode, set the PROC_IN_CONCURRENT_REPACK flag.  This
+	 * makes the deadlock checker cause anyone that would conflict with us
+	 * to error out.  It's important to set this flag ahead of actually locking
+	 * the relation; it won't of course affect anyone until we do have a lock
+	 * that others can conflict with.
+	 */
+	if ((params.options & CLUOPT_CONCURRENT) != 0)
+		MyProc->statusFlags |= PROC_IN_CONCURRENT_REPACK;
+
 	/*
 	 * If a single relation is specified, process it and we're done ... unless
 	 * the relation is a partitioned table, in which case we fall through.
diff --git a/src/backend/storage/lmgr/deadlock.c b/src/backend/storage/lmgr/deadlock.c
index b8962d875b6..10266e384aa 100644
--- a/src/backend/storage/lmgr/deadlock.c
+++ b/src/backend/storage/lmgr/deadlock.c
@@ -620,6 +620,18 @@ FindLockCycleRecurseMember(PGPROC *checkProc,
 						proc->statusFlags & PROC_IS_AUTOVACUUM)
 						blocking_autovacuum_proc = proc;
 
+					/*
+					 * If we note that we're blocked by some process running
+					 * REPACK (CONCURRENTLY), just fail.  That process is
+					 * going to upgrade its lock at some point, and it would
+					 * be inappropriate for any other process to cause that
+					 * to fail.
+					 */
+					if (checkProc == MyProc &&
+						proc->statusFlags & PROC_IN_CONCURRENT_REPACK)
+						/* not a serious error message proposal */
+						elog(ERROR, "oops, we're blocked by REPACK, time to die");
+
 					/* We're done looking at this proclock */
 					break;
 				}
diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h
index 1dad125706e..4e340ead455 100644
--- a/src/include/storage/proc.h
+++ b/src/include/storage/proc.h
@@ -69,6 +69,7 @@ struct XidCache
 #define		PROC_AFFECTS_ALL_HORIZONS	0x20	/* this proc's xmin must be
 												 * included in vacuum horizons
 												 * in all databases */
+#define		PROC_IN_CONCURRENT_REPACK	0x40	/* REPACK (CONCURRENTLY) */
 
 /* flags reset at EOXact */
 #define		PROC_VACUUM_STATE_MASK \
diff --git a/src/test/modules/injection_points/Makefile b/src/test/modules/injection_points/Makefile
index 2cd7d87c533..f7663859fe2 100644
--- a/src/test/modules/injection_points/Makefile
+++ b/src/test/modules/injection_points/Makefile
@@ -15,6 +15,7 @@ REGRESS_OPTS = --dlpath=$(top_builddir)/src/test/regress
 ISOLATION = basic \
 	    inplace \
 	    repack \
+	    repack_deadlock \
 	    repack_toast \
 	    syscache-update-pruned \
 	    heap_lock_update
diff --git a/src/test/modules/injection_points/expected/repack_deadlock.out b/src/test/modules/injection_points/expected/repack_deadlock.out
new file mode 100644
index 00000000000..42505d10933
--- /dev/null
+++ b/src/test/modules/injection_points/expected/repack_deadlock.out
@@ -0,0 +1,63 @@
+Parsed test spec with 2 sessions
+
+starting permutation: wait_before_lock add_column wakeup_before_lock check1
+injection_points_attach
+-----------------------
+                       
+(1 row)
+
+step wait_before_lock: 
+	REPACK (CONCURRENTLY) repack_deadlock USING INDEX repack_deadlock_pkey;
+ <waiting ...>
+step add_column: 
+	alter table repack_deadlock add column noise text;
+ <waiting ...>
+step add_column: <... completed>
+ERROR:  oops, we're blocked by REPACK, time to die
+step wakeup_before_lock: 
+	SELECT injection_points_wakeup('repack-concurrently-before-lock');
+
+injection_points_wakeup
+-----------------------
+                       
+(1 row)
+
+step wait_before_lock: <... completed>
+step check1: 
+	INSERT INTO relfilenodes(node)
+	SELECT relfilenode FROM pg_class WHERE relname='repack_deadlock';
+
+	SELECT count(DISTINCT node) FROM relfilenodes;
+
+	SELECT i, j FROM repack_deadlock ORDER BY i, j;
+
+	INSERT INTO data_s1(i, j)
+	SELECT i, j FROM repack_deadlock;
+
+	SELECT count(*)
+	FROM data_s1 d1 FULL JOIN data_s2 d2 USING (i, j)
+	WHERE d1.i ISNULL OR d2.i ISNULL;
+
+count
+-----
+    1
+(1 row)
+
+i|j
+-+-
+1|1
+2|2
+3|3
+4|4
+(4 rows)
+
+count
+-----
+    4
+(1 row)
+
+injection_points_detach
+-----------------------
+                       
+(1 row)
+
diff --git a/src/test/modules/injection_points/meson.build b/src/test/modules/injection_points/meson.build
index a414abb924b..1cd88d6db65 100644
--- a/src/test/modules/injection_points/meson.build
+++ b/src/test/modules/injection_points/meson.build
@@ -46,6 +46,7 @@ tests += {
       'basic',
       'inplace',
       'repack',
+      'repack_deadlock',
       'repack_toast',
       'syscache-update-pruned',
       'heap_lock_update',
diff --git a/src/test/modules/injection_points/specs/repack_deadlock.spec b/src/test/modules/injection_points/specs/repack_deadlock.spec
new file mode 100644
index 00000000000..9d23a6588c2
--- /dev/null
+++ b/src/test/modules/injection_points/specs/repack_deadlock.spec
@@ -0,0 +1,83 @@
+# Test REPACK with a concurrent transaction that would cause a deadlock
+setup
+{
+	CREATE EXTENSION injection_points;
+
+	CREATE TABLE repack_deadlock(i int PRIMARY KEY, j int);
+	INSERT INTO repack_deadlock(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4);
+
+	CREATE TABLE relfilenodes(node oid);
+
+	CREATE TABLE data_s1(i int, j int);
+	CREATE TABLE data_s2(i int, j int);
+}
+
+teardown
+{
+	DROP TABLE repack_deadlock;
+	DROP EXTENSION injection_points;
+
+	DROP TABLE relfilenodes;
+	DROP TABLE data_s1;
+	DROP TABLE data_s2;
+}
+
+session s1
+setup
+{
+	SELECT injection_points_set_local();
+	SELECT injection_points_attach('repack-concurrently-before-lock', 'wait');
+}
+# Perform the initial load and wait for s2 to do some data changes.
+step wait_before_lock
+{
+	REPACK (CONCURRENTLY) repack_deadlock USING INDEX repack_deadlock_pkey;
+}
+# Check the table from the perspective of s1.
+#
+# Besides the contents, we also check that relfilenode has changed.
+
+# Have each session write the contents into a table and use FULL JOIN to check
+# if the outputs are identical.
+step check1
+{
+	INSERT INTO relfilenodes(node)
+	SELECT relfilenode FROM pg_class WHERE relname='repack_deadlock';
+
+	SELECT count(DISTINCT node) FROM relfilenodes;
+
+	SELECT i, j FROM repack_deadlock ORDER BY i, j;
+
+	INSERT INTO data_s1(i, j)
+	SELECT i, j FROM repack_deadlock;
+
+	SELECT count(*)
+	FROM data_s1 d1 FULL JOIN data_s2 d2 USING (i, j)
+	WHERE d1.i ISNULL OR d2.i ISNULL;
+}
+teardown
+{
+	SELECT injection_points_detach('repack-concurrently-before-lock');
+}
+
+session s2
+# Change the existing data. UPDATE changes both key and non-key columns. Also
+# update one row twice to test whether tuple version generated by this session
+# can be found.
+step add_column
+{
+	alter table repack_deadlock add column noise text;
+}
+
+step wakeup_before_lock
+{
+	SELECT injection_points_wakeup('repack-concurrently-before-lock');
+}
+
+# Test if data changes introduced while one session is performing REPACK
+# CONCURRENTLY find their way into the table.
+permutation
+	wait_before_lock
+	add_column
+	wakeup_before_lock
+	check1
-- 
2.47.3


--fot5plqmb33ey33c--





^ permalink  raw  reply  [nested|flat] 89+ messages in thread

* [PATCH v44b 4/4] hack deadlock detector
@ 2026-03-25 19:35  Álvaro Herrera <[email protected]>
  0 siblings, 0 replies; 89+ messages in thread

From: Álvaro Herrera @ 2026-03-25 19:35 UTC (permalink / raw)

---
 src/backend/commands/cluster.c                | 10 +++
 src/backend/storage/lmgr/deadlock.c           | 12 +++
 src/include/storage/proc.h                    |  1 +
 src/test/modules/injection_points/Makefile    |  1 +
 .../expected/repack_deadlock.out              | 63 ++++++++++++++
 src/test/modules/injection_points/meson.build |  1 +
 .../specs/repack_deadlock.spec                | 83 +++++++++++++++++++
 7 files changed, 171 insertions(+)
 create mode 100644 src/test/modules/injection_points/expected/repack_deadlock.out
 create mode 100644 src/test/modules/injection_points/specs/repack_deadlock.spec

diff --git a/src/backend/commands/cluster.c b/src/backend/commands/cluster.c
index 2c3058ba10d..d5b1dfbff69 100644
--- a/src/backend/commands/cluster.c
+++ b/src/backend/commands/cluster.c
@@ -372,6 +372,16 @@ ExecRepack(ParseState *pstate, RepackStmt *stmt, bool isTopLevel)
 	/* Determine the lock mode to use. */
 	lockmode = RepackLockLevel((params.options & CLUOPT_CONCURRENT) != 0);
 
+	/*
+	 * If in concurrent mode, set the PROC_IN_CONCURRENT_REPACK flag.  This
+	 * makes the deadlock checker cause anyone that would conflict with us
+	 * to error out.  It's important to set this flag ahead of actually locking
+	 * the relation; it won't of course affect anyone until we do have a lock
+	 * that others can conflict with.
+	 */
+	if ((params.options & CLUOPT_CONCURRENT) != 0)
+		MyProc->statusFlags |= PROC_IN_CONCURRENT_REPACK;
+
 	/*
 	 * If a single relation is specified, process it and we're done ... unless
 	 * the relation is a partitioned table, in which case we fall through.
diff --git a/src/backend/storage/lmgr/deadlock.c b/src/backend/storage/lmgr/deadlock.c
index b8962d875b6..10266e384aa 100644
--- a/src/backend/storage/lmgr/deadlock.c
+++ b/src/backend/storage/lmgr/deadlock.c
@@ -620,6 +620,18 @@ FindLockCycleRecurseMember(PGPROC *checkProc,
 						proc->statusFlags & PROC_IS_AUTOVACUUM)
 						blocking_autovacuum_proc = proc;
 
+					/*
+					 * If we note that we're blocked by some process running
+					 * REPACK (CONCURRENTLY), just fail.  That process is
+					 * going to upgrade its lock at some point, and it would
+					 * be inappropriate for any other process to cause that
+					 * to fail.
+					 */
+					if (checkProc == MyProc &&
+						proc->statusFlags & PROC_IN_CONCURRENT_REPACK)
+						/* not a serious error message proposal */
+						elog(ERROR, "oops, we're blocked by REPACK, time to die");
+
 					/* We're done looking at this proclock */
 					break;
 				}
diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h
index 1dad125706e..4e340ead455 100644
--- a/src/include/storage/proc.h
+++ b/src/include/storage/proc.h
@@ -69,6 +69,7 @@ struct XidCache
 #define		PROC_AFFECTS_ALL_HORIZONS	0x20	/* this proc's xmin must be
 												 * included in vacuum horizons
 												 * in all databases */
+#define		PROC_IN_CONCURRENT_REPACK	0x40	/* REPACK (CONCURRENTLY) */
 
 /* flags reset at EOXact */
 #define		PROC_VACUUM_STATE_MASK \
diff --git a/src/test/modules/injection_points/Makefile b/src/test/modules/injection_points/Makefile
index 2cd7d87c533..f7663859fe2 100644
--- a/src/test/modules/injection_points/Makefile
+++ b/src/test/modules/injection_points/Makefile
@@ -15,6 +15,7 @@ REGRESS_OPTS = --dlpath=$(top_builddir)/src/test/regress
 ISOLATION = basic \
 	    inplace \
 	    repack \
+	    repack_deadlock \
 	    repack_toast \
 	    syscache-update-pruned \
 	    heap_lock_update
diff --git a/src/test/modules/injection_points/expected/repack_deadlock.out b/src/test/modules/injection_points/expected/repack_deadlock.out
new file mode 100644
index 00000000000..42505d10933
--- /dev/null
+++ b/src/test/modules/injection_points/expected/repack_deadlock.out
@@ -0,0 +1,63 @@
+Parsed test spec with 2 sessions
+
+starting permutation: wait_before_lock add_column wakeup_before_lock check1
+injection_points_attach
+-----------------------
+                       
+(1 row)
+
+step wait_before_lock: 
+	REPACK (CONCURRENTLY) repack_deadlock USING INDEX repack_deadlock_pkey;
+ <waiting ...>
+step add_column: 
+	alter table repack_deadlock add column noise text;
+ <waiting ...>
+step add_column: <... completed>
+ERROR:  oops, we're blocked by REPACK, time to die
+step wakeup_before_lock: 
+	SELECT injection_points_wakeup('repack-concurrently-before-lock');
+
+injection_points_wakeup
+-----------------------
+                       
+(1 row)
+
+step wait_before_lock: <... completed>
+step check1: 
+	INSERT INTO relfilenodes(node)
+	SELECT relfilenode FROM pg_class WHERE relname='repack_deadlock';
+
+	SELECT count(DISTINCT node) FROM relfilenodes;
+
+	SELECT i, j FROM repack_deadlock ORDER BY i, j;
+
+	INSERT INTO data_s1(i, j)
+	SELECT i, j FROM repack_deadlock;
+
+	SELECT count(*)
+	FROM data_s1 d1 FULL JOIN data_s2 d2 USING (i, j)
+	WHERE d1.i ISNULL OR d2.i ISNULL;
+
+count
+-----
+    1
+(1 row)
+
+i|j
+-+-
+1|1
+2|2
+3|3
+4|4
+(4 rows)
+
+count
+-----
+    4
+(1 row)
+
+injection_points_detach
+-----------------------
+                       
+(1 row)
+
diff --git a/src/test/modules/injection_points/meson.build b/src/test/modules/injection_points/meson.build
index a414abb924b..1cd88d6db65 100644
--- a/src/test/modules/injection_points/meson.build
+++ b/src/test/modules/injection_points/meson.build
@@ -46,6 +46,7 @@ tests += {
       'basic',
       'inplace',
       'repack',
+      'repack_deadlock',
       'repack_toast',
       'syscache-update-pruned',
       'heap_lock_update',
diff --git a/src/test/modules/injection_points/specs/repack_deadlock.spec b/src/test/modules/injection_points/specs/repack_deadlock.spec
new file mode 100644
index 00000000000..9d23a6588c2
--- /dev/null
+++ b/src/test/modules/injection_points/specs/repack_deadlock.spec
@@ -0,0 +1,83 @@
+# Test REPACK with a concurrent transaction that would cause a deadlock
+setup
+{
+	CREATE EXTENSION injection_points;
+
+	CREATE TABLE repack_deadlock(i int PRIMARY KEY, j int);
+	INSERT INTO repack_deadlock(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4);
+
+	CREATE TABLE relfilenodes(node oid);
+
+	CREATE TABLE data_s1(i int, j int);
+	CREATE TABLE data_s2(i int, j int);
+}
+
+teardown
+{
+	DROP TABLE repack_deadlock;
+	DROP EXTENSION injection_points;
+
+	DROP TABLE relfilenodes;
+	DROP TABLE data_s1;
+	DROP TABLE data_s2;
+}
+
+session s1
+setup
+{
+	SELECT injection_points_set_local();
+	SELECT injection_points_attach('repack-concurrently-before-lock', 'wait');
+}
+# Perform the initial load and wait for s2 to do some data changes.
+step wait_before_lock
+{
+	REPACK (CONCURRENTLY) repack_deadlock USING INDEX repack_deadlock_pkey;
+}
+# Check the table from the perspective of s1.
+#
+# Besides the contents, we also check that relfilenode has changed.
+
+# Have each session write the contents into a table and use FULL JOIN to check
+# if the outputs are identical.
+step check1
+{
+	INSERT INTO relfilenodes(node)
+	SELECT relfilenode FROM pg_class WHERE relname='repack_deadlock';
+
+	SELECT count(DISTINCT node) FROM relfilenodes;
+
+	SELECT i, j FROM repack_deadlock ORDER BY i, j;
+
+	INSERT INTO data_s1(i, j)
+	SELECT i, j FROM repack_deadlock;
+
+	SELECT count(*)
+	FROM data_s1 d1 FULL JOIN data_s2 d2 USING (i, j)
+	WHERE d1.i ISNULL OR d2.i ISNULL;
+}
+teardown
+{
+	SELECT injection_points_detach('repack-concurrently-before-lock');
+}
+
+session s2
+# Change the existing data. UPDATE changes both key and non-key columns. Also
+# update one row twice to test whether tuple version generated by this session
+# can be found.
+step add_column
+{
+	alter table repack_deadlock add column noise text;
+}
+
+step wakeup_before_lock
+{
+	SELECT injection_points_wakeup('repack-concurrently-before-lock');
+}
+
+# Test if data changes introduced while one session is performing REPACK
+# CONCURRENTLY find their way into the table.
+permutation
+	wait_before_lock
+	add_column
+	wakeup_before_lock
+	check1
-- 
2.47.3


--fot5plqmb33ey33c--





^ permalink  raw  reply  [nested|flat] 89+ messages in thread

* [PATCH v44b 4/4] hack deadlock detector
@ 2026-03-25 19:35  Álvaro Herrera <[email protected]>
  0 siblings, 0 replies; 89+ messages in thread

From: Álvaro Herrera @ 2026-03-25 19:35 UTC (permalink / raw)

---
 src/backend/commands/cluster.c                | 10 +++
 src/backend/storage/lmgr/deadlock.c           | 12 +++
 src/include/storage/proc.h                    |  1 +
 src/test/modules/injection_points/Makefile    |  1 +
 .../expected/repack_deadlock.out              | 63 ++++++++++++++
 src/test/modules/injection_points/meson.build |  1 +
 .../specs/repack_deadlock.spec                | 83 +++++++++++++++++++
 7 files changed, 171 insertions(+)
 create mode 100644 src/test/modules/injection_points/expected/repack_deadlock.out
 create mode 100644 src/test/modules/injection_points/specs/repack_deadlock.spec

diff --git a/src/backend/commands/cluster.c b/src/backend/commands/cluster.c
index 2c3058ba10d..d5b1dfbff69 100644
--- a/src/backend/commands/cluster.c
+++ b/src/backend/commands/cluster.c
@@ -372,6 +372,16 @@ ExecRepack(ParseState *pstate, RepackStmt *stmt, bool isTopLevel)
 	/* Determine the lock mode to use. */
 	lockmode = RepackLockLevel((params.options & CLUOPT_CONCURRENT) != 0);
 
+	/*
+	 * If in concurrent mode, set the PROC_IN_CONCURRENT_REPACK flag.  This
+	 * makes the deadlock checker cause anyone that would conflict with us
+	 * to error out.  It's important to set this flag ahead of actually locking
+	 * the relation; it won't of course affect anyone until we do have a lock
+	 * that others can conflict with.
+	 */
+	if ((params.options & CLUOPT_CONCURRENT) != 0)
+		MyProc->statusFlags |= PROC_IN_CONCURRENT_REPACK;
+
 	/*
 	 * If a single relation is specified, process it and we're done ... unless
 	 * the relation is a partitioned table, in which case we fall through.
diff --git a/src/backend/storage/lmgr/deadlock.c b/src/backend/storage/lmgr/deadlock.c
index b8962d875b6..10266e384aa 100644
--- a/src/backend/storage/lmgr/deadlock.c
+++ b/src/backend/storage/lmgr/deadlock.c
@@ -620,6 +620,18 @@ FindLockCycleRecurseMember(PGPROC *checkProc,
 						proc->statusFlags & PROC_IS_AUTOVACUUM)
 						blocking_autovacuum_proc = proc;
 
+					/*
+					 * If we note that we're blocked by some process running
+					 * REPACK (CONCURRENTLY), just fail.  That process is
+					 * going to upgrade its lock at some point, and it would
+					 * be inappropriate for any other process to cause that
+					 * to fail.
+					 */
+					if (checkProc == MyProc &&
+						proc->statusFlags & PROC_IN_CONCURRENT_REPACK)
+						/* not a serious error message proposal */
+						elog(ERROR, "oops, we're blocked by REPACK, time to die");
+
 					/* We're done looking at this proclock */
 					break;
 				}
diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h
index 1dad125706e..4e340ead455 100644
--- a/src/include/storage/proc.h
+++ b/src/include/storage/proc.h
@@ -69,6 +69,7 @@ struct XidCache
 #define		PROC_AFFECTS_ALL_HORIZONS	0x20	/* this proc's xmin must be
 												 * included in vacuum horizons
 												 * in all databases */
+#define		PROC_IN_CONCURRENT_REPACK	0x40	/* REPACK (CONCURRENTLY) */
 
 /* flags reset at EOXact */
 #define		PROC_VACUUM_STATE_MASK \
diff --git a/src/test/modules/injection_points/Makefile b/src/test/modules/injection_points/Makefile
index 2cd7d87c533..f7663859fe2 100644
--- a/src/test/modules/injection_points/Makefile
+++ b/src/test/modules/injection_points/Makefile
@@ -15,6 +15,7 @@ REGRESS_OPTS = --dlpath=$(top_builddir)/src/test/regress
 ISOLATION = basic \
 	    inplace \
 	    repack \
+	    repack_deadlock \
 	    repack_toast \
 	    syscache-update-pruned \
 	    heap_lock_update
diff --git a/src/test/modules/injection_points/expected/repack_deadlock.out b/src/test/modules/injection_points/expected/repack_deadlock.out
new file mode 100644
index 00000000000..42505d10933
--- /dev/null
+++ b/src/test/modules/injection_points/expected/repack_deadlock.out
@@ -0,0 +1,63 @@
+Parsed test spec with 2 sessions
+
+starting permutation: wait_before_lock add_column wakeup_before_lock check1
+injection_points_attach
+-----------------------
+                       
+(1 row)
+
+step wait_before_lock: 
+	REPACK (CONCURRENTLY) repack_deadlock USING INDEX repack_deadlock_pkey;
+ <waiting ...>
+step add_column: 
+	alter table repack_deadlock add column noise text;
+ <waiting ...>
+step add_column: <... completed>
+ERROR:  oops, we're blocked by REPACK, time to die
+step wakeup_before_lock: 
+	SELECT injection_points_wakeup('repack-concurrently-before-lock');
+
+injection_points_wakeup
+-----------------------
+                       
+(1 row)
+
+step wait_before_lock: <... completed>
+step check1: 
+	INSERT INTO relfilenodes(node)
+	SELECT relfilenode FROM pg_class WHERE relname='repack_deadlock';
+
+	SELECT count(DISTINCT node) FROM relfilenodes;
+
+	SELECT i, j FROM repack_deadlock ORDER BY i, j;
+
+	INSERT INTO data_s1(i, j)
+	SELECT i, j FROM repack_deadlock;
+
+	SELECT count(*)
+	FROM data_s1 d1 FULL JOIN data_s2 d2 USING (i, j)
+	WHERE d1.i ISNULL OR d2.i ISNULL;
+
+count
+-----
+    1
+(1 row)
+
+i|j
+-+-
+1|1
+2|2
+3|3
+4|4
+(4 rows)
+
+count
+-----
+    4
+(1 row)
+
+injection_points_detach
+-----------------------
+                       
+(1 row)
+
diff --git a/src/test/modules/injection_points/meson.build b/src/test/modules/injection_points/meson.build
index a414abb924b..1cd88d6db65 100644
--- a/src/test/modules/injection_points/meson.build
+++ b/src/test/modules/injection_points/meson.build
@@ -46,6 +46,7 @@ tests += {
       'basic',
       'inplace',
       'repack',
+      'repack_deadlock',
       'repack_toast',
       'syscache-update-pruned',
       'heap_lock_update',
diff --git a/src/test/modules/injection_points/specs/repack_deadlock.spec b/src/test/modules/injection_points/specs/repack_deadlock.spec
new file mode 100644
index 00000000000..9d23a6588c2
--- /dev/null
+++ b/src/test/modules/injection_points/specs/repack_deadlock.spec
@@ -0,0 +1,83 @@
+# Test REPACK with a concurrent transaction that would cause a deadlock
+setup
+{
+	CREATE EXTENSION injection_points;
+
+	CREATE TABLE repack_deadlock(i int PRIMARY KEY, j int);
+	INSERT INTO repack_deadlock(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4);
+
+	CREATE TABLE relfilenodes(node oid);
+
+	CREATE TABLE data_s1(i int, j int);
+	CREATE TABLE data_s2(i int, j int);
+}
+
+teardown
+{
+	DROP TABLE repack_deadlock;
+	DROP EXTENSION injection_points;
+
+	DROP TABLE relfilenodes;
+	DROP TABLE data_s1;
+	DROP TABLE data_s2;
+}
+
+session s1
+setup
+{
+	SELECT injection_points_set_local();
+	SELECT injection_points_attach('repack-concurrently-before-lock', 'wait');
+}
+# Perform the initial load and wait for s2 to do some data changes.
+step wait_before_lock
+{
+	REPACK (CONCURRENTLY) repack_deadlock USING INDEX repack_deadlock_pkey;
+}
+# Check the table from the perspective of s1.
+#
+# Besides the contents, we also check that relfilenode has changed.
+
+# Have each session write the contents into a table and use FULL JOIN to check
+# if the outputs are identical.
+step check1
+{
+	INSERT INTO relfilenodes(node)
+	SELECT relfilenode FROM pg_class WHERE relname='repack_deadlock';
+
+	SELECT count(DISTINCT node) FROM relfilenodes;
+
+	SELECT i, j FROM repack_deadlock ORDER BY i, j;
+
+	INSERT INTO data_s1(i, j)
+	SELECT i, j FROM repack_deadlock;
+
+	SELECT count(*)
+	FROM data_s1 d1 FULL JOIN data_s2 d2 USING (i, j)
+	WHERE d1.i ISNULL OR d2.i ISNULL;
+}
+teardown
+{
+	SELECT injection_points_detach('repack-concurrently-before-lock');
+}
+
+session s2
+# Change the existing data. UPDATE changes both key and non-key columns. Also
+# update one row twice to test whether tuple version generated by this session
+# can be found.
+step add_column
+{
+	alter table repack_deadlock add column noise text;
+}
+
+step wakeup_before_lock
+{
+	SELECT injection_points_wakeup('repack-concurrently-before-lock');
+}
+
+# Test if data changes introduced while one session is performing REPACK
+# CONCURRENTLY find their way into the table.
+permutation
+	wait_before_lock
+	add_column
+	wakeup_before_lock
+	check1
-- 
2.47.3


--fot5plqmb33ey33c--





^ permalink  raw  reply  [nested|flat] 89+ messages in thread

* [PATCH v44b 4/4] hack deadlock detector
@ 2026-03-25 19:35  Álvaro Herrera <[email protected]>
  0 siblings, 0 replies; 89+ messages in thread

From: Álvaro Herrera @ 2026-03-25 19:35 UTC (permalink / raw)

---
 src/backend/commands/cluster.c                | 10 +++
 src/backend/storage/lmgr/deadlock.c           | 12 +++
 src/include/storage/proc.h                    |  1 +
 src/test/modules/injection_points/Makefile    |  1 +
 .../expected/repack_deadlock.out              | 63 ++++++++++++++
 src/test/modules/injection_points/meson.build |  1 +
 .../specs/repack_deadlock.spec                | 83 +++++++++++++++++++
 7 files changed, 171 insertions(+)
 create mode 100644 src/test/modules/injection_points/expected/repack_deadlock.out
 create mode 100644 src/test/modules/injection_points/specs/repack_deadlock.spec

diff --git a/src/backend/commands/cluster.c b/src/backend/commands/cluster.c
index 2c3058ba10d..d5b1dfbff69 100644
--- a/src/backend/commands/cluster.c
+++ b/src/backend/commands/cluster.c
@@ -372,6 +372,16 @@ ExecRepack(ParseState *pstate, RepackStmt *stmt, bool isTopLevel)
 	/* Determine the lock mode to use. */
 	lockmode = RepackLockLevel((params.options & CLUOPT_CONCURRENT) != 0);
 
+	/*
+	 * If in concurrent mode, set the PROC_IN_CONCURRENT_REPACK flag.  This
+	 * makes the deadlock checker cause anyone that would conflict with us
+	 * to error out.  It's important to set this flag ahead of actually locking
+	 * the relation; it won't of course affect anyone until we do have a lock
+	 * that others can conflict with.
+	 */
+	if ((params.options & CLUOPT_CONCURRENT) != 0)
+		MyProc->statusFlags |= PROC_IN_CONCURRENT_REPACK;
+
 	/*
 	 * If a single relation is specified, process it and we're done ... unless
 	 * the relation is a partitioned table, in which case we fall through.
diff --git a/src/backend/storage/lmgr/deadlock.c b/src/backend/storage/lmgr/deadlock.c
index b8962d875b6..10266e384aa 100644
--- a/src/backend/storage/lmgr/deadlock.c
+++ b/src/backend/storage/lmgr/deadlock.c
@@ -620,6 +620,18 @@ FindLockCycleRecurseMember(PGPROC *checkProc,
 						proc->statusFlags & PROC_IS_AUTOVACUUM)
 						blocking_autovacuum_proc = proc;
 
+					/*
+					 * If we note that we're blocked by some process running
+					 * REPACK (CONCURRENTLY), just fail.  That process is
+					 * going to upgrade its lock at some point, and it would
+					 * be inappropriate for any other process to cause that
+					 * to fail.
+					 */
+					if (checkProc == MyProc &&
+						proc->statusFlags & PROC_IN_CONCURRENT_REPACK)
+						/* not a serious error message proposal */
+						elog(ERROR, "oops, we're blocked by REPACK, time to die");
+
 					/* We're done looking at this proclock */
 					break;
 				}
diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h
index 1dad125706e..4e340ead455 100644
--- a/src/include/storage/proc.h
+++ b/src/include/storage/proc.h
@@ -69,6 +69,7 @@ struct XidCache
 #define		PROC_AFFECTS_ALL_HORIZONS	0x20	/* this proc's xmin must be
 												 * included in vacuum horizons
 												 * in all databases */
+#define		PROC_IN_CONCURRENT_REPACK	0x40	/* REPACK (CONCURRENTLY) */
 
 /* flags reset at EOXact */
 #define		PROC_VACUUM_STATE_MASK \
diff --git a/src/test/modules/injection_points/Makefile b/src/test/modules/injection_points/Makefile
index 2cd7d87c533..f7663859fe2 100644
--- a/src/test/modules/injection_points/Makefile
+++ b/src/test/modules/injection_points/Makefile
@@ -15,6 +15,7 @@ REGRESS_OPTS = --dlpath=$(top_builddir)/src/test/regress
 ISOLATION = basic \
 	    inplace \
 	    repack \
+	    repack_deadlock \
 	    repack_toast \
 	    syscache-update-pruned \
 	    heap_lock_update
diff --git a/src/test/modules/injection_points/expected/repack_deadlock.out b/src/test/modules/injection_points/expected/repack_deadlock.out
new file mode 100644
index 00000000000..42505d10933
--- /dev/null
+++ b/src/test/modules/injection_points/expected/repack_deadlock.out
@@ -0,0 +1,63 @@
+Parsed test spec with 2 sessions
+
+starting permutation: wait_before_lock add_column wakeup_before_lock check1
+injection_points_attach
+-----------------------
+                       
+(1 row)
+
+step wait_before_lock: 
+	REPACK (CONCURRENTLY) repack_deadlock USING INDEX repack_deadlock_pkey;
+ <waiting ...>
+step add_column: 
+	alter table repack_deadlock add column noise text;
+ <waiting ...>
+step add_column: <... completed>
+ERROR:  oops, we're blocked by REPACK, time to die
+step wakeup_before_lock: 
+	SELECT injection_points_wakeup('repack-concurrently-before-lock');
+
+injection_points_wakeup
+-----------------------
+                       
+(1 row)
+
+step wait_before_lock: <... completed>
+step check1: 
+	INSERT INTO relfilenodes(node)
+	SELECT relfilenode FROM pg_class WHERE relname='repack_deadlock';
+
+	SELECT count(DISTINCT node) FROM relfilenodes;
+
+	SELECT i, j FROM repack_deadlock ORDER BY i, j;
+
+	INSERT INTO data_s1(i, j)
+	SELECT i, j FROM repack_deadlock;
+
+	SELECT count(*)
+	FROM data_s1 d1 FULL JOIN data_s2 d2 USING (i, j)
+	WHERE d1.i ISNULL OR d2.i ISNULL;
+
+count
+-----
+    1
+(1 row)
+
+i|j
+-+-
+1|1
+2|2
+3|3
+4|4
+(4 rows)
+
+count
+-----
+    4
+(1 row)
+
+injection_points_detach
+-----------------------
+                       
+(1 row)
+
diff --git a/src/test/modules/injection_points/meson.build b/src/test/modules/injection_points/meson.build
index a414abb924b..1cd88d6db65 100644
--- a/src/test/modules/injection_points/meson.build
+++ b/src/test/modules/injection_points/meson.build
@@ -46,6 +46,7 @@ tests += {
       'basic',
       'inplace',
       'repack',
+      'repack_deadlock',
       'repack_toast',
       'syscache-update-pruned',
       'heap_lock_update',
diff --git a/src/test/modules/injection_points/specs/repack_deadlock.spec b/src/test/modules/injection_points/specs/repack_deadlock.spec
new file mode 100644
index 00000000000..9d23a6588c2
--- /dev/null
+++ b/src/test/modules/injection_points/specs/repack_deadlock.spec
@@ -0,0 +1,83 @@
+# Test REPACK with a concurrent transaction that would cause a deadlock
+setup
+{
+	CREATE EXTENSION injection_points;
+
+	CREATE TABLE repack_deadlock(i int PRIMARY KEY, j int);
+	INSERT INTO repack_deadlock(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4);
+
+	CREATE TABLE relfilenodes(node oid);
+
+	CREATE TABLE data_s1(i int, j int);
+	CREATE TABLE data_s2(i int, j int);
+}
+
+teardown
+{
+	DROP TABLE repack_deadlock;
+	DROP EXTENSION injection_points;
+
+	DROP TABLE relfilenodes;
+	DROP TABLE data_s1;
+	DROP TABLE data_s2;
+}
+
+session s1
+setup
+{
+	SELECT injection_points_set_local();
+	SELECT injection_points_attach('repack-concurrently-before-lock', 'wait');
+}
+# Perform the initial load and wait for s2 to do some data changes.
+step wait_before_lock
+{
+	REPACK (CONCURRENTLY) repack_deadlock USING INDEX repack_deadlock_pkey;
+}
+# Check the table from the perspective of s1.
+#
+# Besides the contents, we also check that relfilenode has changed.
+
+# Have each session write the contents into a table and use FULL JOIN to check
+# if the outputs are identical.
+step check1
+{
+	INSERT INTO relfilenodes(node)
+	SELECT relfilenode FROM pg_class WHERE relname='repack_deadlock';
+
+	SELECT count(DISTINCT node) FROM relfilenodes;
+
+	SELECT i, j FROM repack_deadlock ORDER BY i, j;
+
+	INSERT INTO data_s1(i, j)
+	SELECT i, j FROM repack_deadlock;
+
+	SELECT count(*)
+	FROM data_s1 d1 FULL JOIN data_s2 d2 USING (i, j)
+	WHERE d1.i ISNULL OR d2.i ISNULL;
+}
+teardown
+{
+	SELECT injection_points_detach('repack-concurrently-before-lock');
+}
+
+session s2
+# Change the existing data. UPDATE changes both key and non-key columns. Also
+# update one row twice to test whether tuple version generated by this session
+# can be found.
+step add_column
+{
+	alter table repack_deadlock add column noise text;
+}
+
+step wakeup_before_lock
+{
+	SELECT injection_points_wakeup('repack-concurrently-before-lock');
+}
+
+# Test if data changes introduced while one session is performing REPACK
+# CONCURRENTLY find their way into the table.
+permutation
+	wait_before_lock
+	add_column
+	wakeup_before_lock
+	check1
-- 
2.47.3


--fot5plqmb33ey33c--





^ permalink  raw  reply  [nested|flat] 89+ messages in thread

* [PATCH v44b 4/4] hack deadlock detector
@ 2026-03-25 19:35  Álvaro Herrera <[email protected]>
  0 siblings, 0 replies; 89+ messages in thread

From: Álvaro Herrera @ 2026-03-25 19:35 UTC (permalink / raw)

---
 src/backend/commands/cluster.c                | 10 +++
 src/backend/storage/lmgr/deadlock.c           | 12 +++
 src/include/storage/proc.h                    |  1 +
 src/test/modules/injection_points/Makefile    |  1 +
 .../expected/repack_deadlock.out              | 63 ++++++++++++++
 src/test/modules/injection_points/meson.build |  1 +
 .../specs/repack_deadlock.spec                | 83 +++++++++++++++++++
 7 files changed, 171 insertions(+)
 create mode 100644 src/test/modules/injection_points/expected/repack_deadlock.out
 create mode 100644 src/test/modules/injection_points/specs/repack_deadlock.spec

diff --git a/src/backend/commands/cluster.c b/src/backend/commands/cluster.c
index 2c3058ba10d..d5b1dfbff69 100644
--- a/src/backend/commands/cluster.c
+++ b/src/backend/commands/cluster.c
@@ -372,6 +372,16 @@ ExecRepack(ParseState *pstate, RepackStmt *stmt, bool isTopLevel)
 	/* Determine the lock mode to use. */
 	lockmode = RepackLockLevel((params.options & CLUOPT_CONCURRENT) != 0);
 
+	/*
+	 * If in concurrent mode, set the PROC_IN_CONCURRENT_REPACK flag.  This
+	 * makes the deadlock checker cause anyone that would conflict with us
+	 * to error out.  It's important to set this flag ahead of actually locking
+	 * the relation; it won't of course affect anyone until we do have a lock
+	 * that others can conflict with.
+	 */
+	if ((params.options & CLUOPT_CONCURRENT) != 0)
+		MyProc->statusFlags |= PROC_IN_CONCURRENT_REPACK;
+
 	/*
 	 * If a single relation is specified, process it and we're done ... unless
 	 * the relation is a partitioned table, in which case we fall through.
diff --git a/src/backend/storage/lmgr/deadlock.c b/src/backend/storage/lmgr/deadlock.c
index b8962d875b6..10266e384aa 100644
--- a/src/backend/storage/lmgr/deadlock.c
+++ b/src/backend/storage/lmgr/deadlock.c
@@ -620,6 +620,18 @@ FindLockCycleRecurseMember(PGPROC *checkProc,
 						proc->statusFlags & PROC_IS_AUTOVACUUM)
 						blocking_autovacuum_proc = proc;
 
+					/*
+					 * If we note that we're blocked by some process running
+					 * REPACK (CONCURRENTLY), just fail.  That process is
+					 * going to upgrade its lock at some point, and it would
+					 * be inappropriate for any other process to cause that
+					 * to fail.
+					 */
+					if (checkProc == MyProc &&
+						proc->statusFlags & PROC_IN_CONCURRENT_REPACK)
+						/* not a serious error message proposal */
+						elog(ERROR, "oops, we're blocked by REPACK, time to die");
+
 					/* We're done looking at this proclock */
 					break;
 				}
diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h
index 1dad125706e..4e340ead455 100644
--- a/src/include/storage/proc.h
+++ b/src/include/storage/proc.h
@@ -69,6 +69,7 @@ struct XidCache
 #define		PROC_AFFECTS_ALL_HORIZONS	0x20	/* this proc's xmin must be
 												 * included in vacuum horizons
 												 * in all databases */
+#define		PROC_IN_CONCURRENT_REPACK	0x40	/* REPACK (CONCURRENTLY) */
 
 /* flags reset at EOXact */
 #define		PROC_VACUUM_STATE_MASK \
diff --git a/src/test/modules/injection_points/Makefile b/src/test/modules/injection_points/Makefile
index 2cd7d87c533..f7663859fe2 100644
--- a/src/test/modules/injection_points/Makefile
+++ b/src/test/modules/injection_points/Makefile
@@ -15,6 +15,7 @@ REGRESS_OPTS = --dlpath=$(top_builddir)/src/test/regress
 ISOLATION = basic \
 	    inplace \
 	    repack \
+	    repack_deadlock \
 	    repack_toast \
 	    syscache-update-pruned \
 	    heap_lock_update
diff --git a/src/test/modules/injection_points/expected/repack_deadlock.out b/src/test/modules/injection_points/expected/repack_deadlock.out
new file mode 100644
index 00000000000..42505d10933
--- /dev/null
+++ b/src/test/modules/injection_points/expected/repack_deadlock.out
@@ -0,0 +1,63 @@
+Parsed test spec with 2 sessions
+
+starting permutation: wait_before_lock add_column wakeup_before_lock check1
+injection_points_attach
+-----------------------
+                       
+(1 row)
+
+step wait_before_lock: 
+	REPACK (CONCURRENTLY) repack_deadlock USING INDEX repack_deadlock_pkey;
+ <waiting ...>
+step add_column: 
+	alter table repack_deadlock add column noise text;
+ <waiting ...>
+step add_column: <... completed>
+ERROR:  oops, we're blocked by REPACK, time to die
+step wakeup_before_lock: 
+	SELECT injection_points_wakeup('repack-concurrently-before-lock');
+
+injection_points_wakeup
+-----------------------
+                       
+(1 row)
+
+step wait_before_lock: <... completed>
+step check1: 
+	INSERT INTO relfilenodes(node)
+	SELECT relfilenode FROM pg_class WHERE relname='repack_deadlock';
+
+	SELECT count(DISTINCT node) FROM relfilenodes;
+
+	SELECT i, j FROM repack_deadlock ORDER BY i, j;
+
+	INSERT INTO data_s1(i, j)
+	SELECT i, j FROM repack_deadlock;
+
+	SELECT count(*)
+	FROM data_s1 d1 FULL JOIN data_s2 d2 USING (i, j)
+	WHERE d1.i ISNULL OR d2.i ISNULL;
+
+count
+-----
+    1
+(1 row)
+
+i|j
+-+-
+1|1
+2|2
+3|3
+4|4
+(4 rows)
+
+count
+-----
+    4
+(1 row)
+
+injection_points_detach
+-----------------------
+                       
+(1 row)
+
diff --git a/src/test/modules/injection_points/meson.build b/src/test/modules/injection_points/meson.build
index a414abb924b..1cd88d6db65 100644
--- a/src/test/modules/injection_points/meson.build
+++ b/src/test/modules/injection_points/meson.build
@@ -46,6 +46,7 @@ tests += {
       'basic',
       'inplace',
       'repack',
+      'repack_deadlock',
       'repack_toast',
       'syscache-update-pruned',
       'heap_lock_update',
diff --git a/src/test/modules/injection_points/specs/repack_deadlock.spec b/src/test/modules/injection_points/specs/repack_deadlock.spec
new file mode 100644
index 00000000000..9d23a6588c2
--- /dev/null
+++ b/src/test/modules/injection_points/specs/repack_deadlock.spec
@@ -0,0 +1,83 @@
+# Test REPACK with a concurrent transaction that would cause a deadlock
+setup
+{
+	CREATE EXTENSION injection_points;
+
+	CREATE TABLE repack_deadlock(i int PRIMARY KEY, j int);
+	INSERT INTO repack_deadlock(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4);
+
+	CREATE TABLE relfilenodes(node oid);
+
+	CREATE TABLE data_s1(i int, j int);
+	CREATE TABLE data_s2(i int, j int);
+}
+
+teardown
+{
+	DROP TABLE repack_deadlock;
+	DROP EXTENSION injection_points;
+
+	DROP TABLE relfilenodes;
+	DROP TABLE data_s1;
+	DROP TABLE data_s2;
+}
+
+session s1
+setup
+{
+	SELECT injection_points_set_local();
+	SELECT injection_points_attach('repack-concurrently-before-lock', 'wait');
+}
+# Perform the initial load and wait for s2 to do some data changes.
+step wait_before_lock
+{
+	REPACK (CONCURRENTLY) repack_deadlock USING INDEX repack_deadlock_pkey;
+}
+# Check the table from the perspective of s1.
+#
+# Besides the contents, we also check that relfilenode has changed.
+
+# Have each session write the contents into a table and use FULL JOIN to check
+# if the outputs are identical.
+step check1
+{
+	INSERT INTO relfilenodes(node)
+	SELECT relfilenode FROM pg_class WHERE relname='repack_deadlock';
+
+	SELECT count(DISTINCT node) FROM relfilenodes;
+
+	SELECT i, j FROM repack_deadlock ORDER BY i, j;
+
+	INSERT INTO data_s1(i, j)
+	SELECT i, j FROM repack_deadlock;
+
+	SELECT count(*)
+	FROM data_s1 d1 FULL JOIN data_s2 d2 USING (i, j)
+	WHERE d1.i ISNULL OR d2.i ISNULL;
+}
+teardown
+{
+	SELECT injection_points_detach('repack-concurrently-before-lock');
+}
+
+session s2
+# Change the existing data. UPDATE changes both key and non-key columns. Also
+# update one row twice to test whether tuple version generated by this session
+# can be found.
+step add_column
+{
+	alter table repack_deadlock add column noise text;
+}
+
+step wakeup_before_lock
+{
+	SELECT injection_points_wakeup('repack-concurrently-before-lock');
+}
+
+# Test if data changes introduced while one session is performing REPACK
+# CONCURRENTLY find their way into the table.
+permutation
+	wait_before_lock
+	add_column
+	wakeup_before_lock
+	check1
-- 
2.47.3


--fot5plqmb33ey33c--





^ permalink  raw  reply  [nested|flat] 89+ messages in thread

* [PATCH v44b 4/4] hack deadlock detector
@ 2026-03-25 19:35  Álvaro Herrera <[email protected]>
  0 siblings, 0 replies; 89+ messages in thread

From: Álvaro Herrera @ 2026-03-25 19:35 UTC (permalink / raw)

---
 src/backend/commands/cluster.c                | 10 +++
 src/backend/storage/lmgr/deadlock.c           | 12 +++
 src/include/storage/proc.h                    |  1 +
 src/test/modules/injection_points/Makefile    |  1 +
 .../expected/repack_deadlock.out              | 63 ++++++++++++++
 src/test/modules/injection_points/meson.build |  1 +
 .../specs/repack_deadlock.spec                | 83 +++++++++++++++++++
 7 files changed, 171 insertions(+)
 create mode 100644 src/test/modules/injection_points/expected/repack_deadlock.out
 create mode 100644 src/test/modules/injection_points/specs/repack_deadlock.spec

diff --git a/src/backend/commands/cluster.c b/src/backend/commands/cluster.c
index 2c3058ba10d..d5b1dfbff69 100644
--- a/src/backend/commands/cluster.c
+++ b/src/backend/commands/cluster.c
@@ -372,6 +372,16 @@ ExecRepack(ParseState *pstate, RepackStmt *stmt, bool isTopLevel)
 	/* Determine the lock mode to use. */
 	lockmode = RepackLockLevel((params.options & CLUOPT_CONCURRENT) != 0);
 
+	/*
+	 * If in concurrent mode, set the PROC_IN_CONCURRENT_REPACK flag.  This
+	 * makes the deadlock checker cause anyone that would conflict with us
+	 * to error out.  It's important to set this flag ahead of actually locking
+	 * the relation; it won't of course affect anyone until we do have a lock
+	 * that others can conflict with.
+	 */
+	if ((params.options & CLUOPT_CONCURRENT) != 0)
+		MyProc->statusFlags |= PROC_IN_CONCURRENT_REPACK;
+
 	/*
 	 * If a single relation is specified, process it and we're done ... unless
 	 * the relation is a partitioned table, in which case we fall through.
diff --git a/src/backend/storage/lmgr/deadlock.c b/src/backend/storage/lmgr/deadlock.c
index b8962d875b6..10266e384aa 100644
--- a/src/backend/storage/lmgr/deadlock.c
+++ b/src/backend/storage/lmgr/deadlock.c
@@ -620,6 +620,18 @@ FindLockCycleRecurseMember(PGPROC *checkProc,
 						proc->statusFlags & PROC_IS_AUTOVACUUM)
 						blocking_autovacuum_proc = proc;
 
+					/*
+					 * If we note that we're blocked by some process running
+					 * REPACK (CONCURRENTLY), just fail.  That process is
+					 * going to upgrade its lock at some point, and it would
+					 * be inappropriate for any other process to cause that
+					 * to fail.
+					 */
+					if (checkProc == MyProc &&
+						proc->statusFlags & PROC_IN_CONCURRENT_REPACK)
+						/* not a serious error message proposal */
+						elog(ERROR, "oops, we're blocked by REPACK, time to die");
+
 					/* We're done looking at this proclock */
 					break;
 				}
diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h
index 1dad125706e..4e340ead455 100644
--- a/src/include/storage/proc.h
+++ b/src/include/storage/proc.h
@@ -69,6 +69,7 @@ struct XidCache
 #define		PROC_AFFECTS_ALL_HORIZONS	0x20	/* this proc's xmin must be
 												 * included in vacuum horizons
 												 * in all databases */
+#define		PROC_IN_CONCURRENT_REPACK	0x40	/* REPACK (CONCURRENTLY) */
 
 /* flags reset at EOXact */
 #define		PROC_VACUUM_STATE_MASK \
diff --git a/src/test/modules/injection_points/Makefile b/src/test/modules/injection_points/Makefile
index 2cd7d87c533..f7663859fe2 100644
--- a/src/test/modules/injection_points/Makefile
+++ b/src/test/modules/injection_points/Makefile
@@ -15,6 +15,7 @@ REGRESS_OPTS = --dlpath=$(top_builddir)/src/test/regress
 ISOLATION = basic \
 	    inplace \
 	    repack \
+	    repack_deadlock \
 	    repack_toast \
 	    syscache-update-pruned \
 	    heap_lock_update
diff --git a/src/test/modules/injection_points/expected/repack_deadlock.out b/src/test/modules/injection_points/expected/repack_deadlock.out
new file mode 100644
index 00000000000..42505d10933
--- /dev/null
+++ b/src/test/modules/injection_points/expected/repack_deadlock.out
@@ -0,0 +1,63 @@
+Parsed test spec with 2 sessions
+
+starting permutation: wait_before_lock add_column wakeup_before_lock check1
+injection_points_attach
+-----------------------
+                       
+(1 row)
+
+step wait_before_lock: 
+	REPACK (CONCURRENTLY) repack_deadlock USING INDEX repack_deadlock_pkey;
+ <waiting ...>
+step add_column: 
+	alter table repack_deadlock add column noise text;
+ <waiting ...>
+step add_column: <... completed>
+ERROR:  oops, we're blocked by REPACK, time to die
+step wakeup_before_lock: 
+	SELECT injection_points_wakeup('repack-concurrently-before-lock');
+
+injection_points_wakeup
+-----------------------
+                       
+(1 row)
+
+step wait_before_lock: <... completed>
+step check1: 
+	INSERT INTO relfilenodes(node)
+	SELECT relfilenode FROM pg_class WHERE relname='repack_deadlock';
+
+	SELECT count(DISTINCT node) FROM relfilenodes;
+
+	SELECT i, j FROM repack_deadlock ORDER BY i, j;
+
+	INSERT INTO data_s1(i, j)
+	SELECT i, j FROM repack_deadlock;
+
+	SELECT count(*)
+	FROM data_s1 d1 FULL JOIN data_s2 d2 USING (i, j)
+	WHERE d1.i ISNULL OR d2.i ISNULL;
+
+count
+-----
+    1
+(1 row)
+
+i|j
+-+-
+1|1
+2|2
+3|3
+4|4
+(4 rows)
+
+count
+-----
+    4
+(1 row)
+
+injection_points_detach
+-----------------------
+                       
+(1 row)
+
diff --git a/src/test/modules/injection_points/meson.build b/src/test/modules/injection_points/meson.build
index a414abb924b..1cd88d6db65 100644
--- a/src/test/modules/injection_points/meson.build
+++ b/src/test/modules/injection_points/meson.build
@@ -46,6 +46,7 @@ tests += {
       'basic',
       'inplace',
       'repack',
+      'repack_deadlock',
       'repack_toast',
       'syscache-update-pruned',
       'heap_lock_update',
diff --git a/src/test/modules/injection_points/specs/repack_deadlock.spec b/src/test/modules/injection_points/specs/repack_deadlock.spec
new file mode 100644
index 00000000000..9d23a6588c2
--- /dev/null
+++ b/src/test/modules/injection_points/specs/repack_deadlock.spec
@@ -0,0 +1,83 @@
+# Test REPACK with a concurrent transaction that would cause a deadlock
+setup
+{
+	CREATE EXTENSION injection_points;
+
+	CREATE TABLE repack_deadlock(i int PRIMARY KEY, j int);
+	INSERT INTO repack_deadlock(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4);
+
+	CREATE TABLE relfilenodes(node oid);
+
+	CREATE TABLE data_s1(i int, j int);
+	CREATE TABLE data_s2(i int, j int);
+}
+
+teardown
+{
+	DROP TABLE repack_deadlock;
+	DROP EXTENSION injection_points;
+
+	DROP TABLE relfilenodes;
+	DROP TABLE data_s1;
+	DROP TABLE data_s2;
+}
+
+session s1
+setup
+{
+	SELECT injection_points_set_local();
+	SELECT injection_points_attach('repack-concurrently-before-lock', 'wait');
+}
+# Perform the initial load and wait for s2 to do some data changes.
+step wait_before_lock
+{
+	REPACK (CONCURRENTLY) repack_deadlock USING INDEX repack_deadlock_pkey;
+}
+# Check the table from the perspective of s1.
+#
+# Besides the contents, we also check that relfilenode has changed.
+
+# Have each session write the contents into a table and use FULL JOIN to check
+# if the outputs are identical.
+step check1
+{
+	INSERT INTO relfilenodes(node)
+	SELECT relfilenode FROM pg_class WHERE relname='repack_deadlock';
+
+	SELECT count(DISTINCT node) FROM relfilenodes;
+
+	SELECT i, j FROM repack_deadlock ORDER BY i, j;
+
+	INSERT INTO data_s1(i, j)
+	SELECT i, j FROM repack_deadlock;
+
+	SELECT count(*)
+	FROM data_s1 d1 FULL JOIN data_s2 d2 USING (i, j)
+	WHERE d1.i ISNULL OR d2.i ISNULL;
+}
+teardown
+{
+	SELECT injection_points_detach('repack-concurrently-before-lock');
+}
+
+session s2
+# Change the existing data. UPDATE changes both key and non-key columns. Also
+# update one row twice to test whether tuple version generated by this session
+# can be found.
+step add_column
+{
+	alter table repack_deadlock add column noise text;
+}
+
+step wakeup_before_lock
+{
+	SELECT injection_points_wakeup('repack-concurrently-before-lock');
+}
+
+# Test if data changes introduced while one session is performing REPACK
+# CONCURRENTLY find their way into the table.
+permutation
+	wait_before_lock
+	add_column
+	wakeup_before_lock
+	check1
-- 
2.47.3


--fot5plqmb33ey33c--





^ permalink  raw  reply  [nested|flat] 89+ messages in thread

* [PATCH v44b 4/4] hack deadlock detector
@ 2026-03-25 19:35  Álvaro Herrera <[email protected]>
  0 siblings, 0 replies; 89+ messages in thread

From: Álvaro Herrera @ 2026-03-25 19:35 UTC (permalink / raw)

---
 src/backend/commands/cluster.c                | 10 +++
 src/backend/storage/lmgr/deadlock.c           | 12 +++
 src/include/storage/proc.h                    |  1 +
 src/test/modules/injection_points/Makefile    |  1 +
 .../expected/repack_deadlock.out              | 63 ++++++++++++++
 src/test/modules/injection_points/meson.build |  1 +
 .../specs/repack_deadlock.spec                | 83 +++++++++++++++++++
 7 files changed, 171 insertions(+)
 create mode 100644 src/test/modules/injection_points/expected/repack_deadlock.out
 create mode 100644 src/test/modules/injection_points/specs/repack_deadlock.spec

diff --git a/src/backend/commands/cluster.c b/src/backend/commands/cluster.c
index 2c3058ba10d..d5b1dfbff69 100644
--- a/src/backend/commands/cluster.c
+++ b/src/backend/commands/cluster.c
@@ -372,6 +372,16 @@ ExecRepack(ParseState *pstate, RepackStmt *stmt, bool isTopLevel)
 	/* Determine the lock mode to use. */
 	lockmode = RepackLockLevel((params.options & CLUOPT_CONCURRENT) != 0);
 
+	/*
+	 * If in concurrent mode, set the PROC_IN_CONCURRENT_REPACK flag.  This
+	 * makes the deadlock checker cause anyone that would conflict with us
+	 * to error out.  It's important to set this flag ahead of actually locking
+	 * the relation; it won't of course affect anyone until we do have a lock
+	 * that others can conflict with.
+	 */
+	if ((params.options & CLUOPT_CONCURRENT) != 0)
+		MyProc->statusFlags |= PROC_IN_CONCURRENT_REPACK;
+
 	/*
 	 * If a single relation is specified, process it and we're done ... unless
 	 * the relation is a partitioned table, in which case we fall through.
diff --git a/src/backend/storage/lmgr/deadlock.c b/src/backend/storage/lmgr/deadlock.c
index b8962d875b6..10266e384aa 100644
--- a/src/backend/storage/lmgr/deadlock.c
+++ b/src/backend/storage/lmgr/deadlock.c
@@ -620,6 +620,18 @@ FindLockCycleRecurseMember(PGPROC *checkProc,
 						proc->statusFlags & PROC_IS_AUTOVACUUM)
 						blocking_autovacuum_proc = proc;
 
+					/*
+					 * If we note that we're blocked by some process running
+					 * REPACK (CONCURRENTLY), just fail.  That process is
+					 * going to upgrade its lock at some point, and it would
+					 * be inappropriate for any other process to cause that
+					 * to fail.
+					 */
+					if (checkProc == MyProc &&
+						proc->statusFlags & PROC_IN_CONCURRENT_REPACK)
+						/* not a serious error message proposal */
+						elog(ERROR, "oops, we're blocked by REPACK, time to die");
+
 					/* We're done looking at this proclock */
 					break;
 				}
diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h
index 1dad125706e..4e340ead455 100644
--- a/src/include/storage/proc.h
+++ b/src/include/storage/proc.h
@@ -69,6 +69,7 @@ struct XidCache
 #define		PROC_AFFECTS_ALL_HORIZONS	0x20	/* this proc's xmin must be
 												 * included in vacuum horizons
 												 * in all databases */
+#define		PROC_IN_CONCURRENT_REPACK	0x40	/* REPACK (CONCURRENTLY) */
 
 /* flags reset at EOXact */
 #define		PROC_VACUUM_STATE_MASK \
diff --git a/src/test/modules/injection_points/Makefile b/src/test/modules/injection_points/Makefile
index 2cd7d87c533..f7663859fe2 100644
--- a/src/test/modules/injection_points/Makefile
+++ b/src/test/modules/injection_points/Makefile
@@ -15,6 +15,7 @@ REGRESS_OPTS = --dlpath=$(top_builddir)/src/test/regress
 ISOLATION = basic \
 	    inplace \
 	    repack \
+	    repack_deadlock \
 	    repack_toast \
 	    syscache-update-pruned \
 	    heap_lock_update
diff --git a/src/test/modules/injection_points/expected/repack_deadlock.out b/src/test/modules/injection_points/expected/repack_deadlock.out
new file mode 100644
index 00000000000..42505d10933
--- /dev/null
+++ b/src/test/modules/injection_points/expected/repack_deadlock.out
@@ -0,0 +1,63 @@
+Parsed test spec with 2 sessions
+
+starting permutation: wait_before_lock add_column wakeup_before_lock check1
+injection_points_attach
+-----------------------
+                       
+(1 row)
+
+step wait_before_lock: 
+	REPACK (CONCURRENTLY) repack_deadlock USING INDEX repack_deadlock_pkey;
+ <waiting ...>
+step add_column: 
+	alter table repack_deadlock add column noise text;
+ <waiting ...>
+step add_column: <... completed>
+ERROR:  oops, we're blocked by REPACK, time to die
+step wakeup_before_lock: 
+	SELECT injection_points_wakeup('repack-concurrently-before-lock');
+
+injection_points_wakeup
+-----------------------
+                       
+(1 row)
+
+step wait_before_lock: <... completed>
+step check1: 
+	INSERT INTO relfilenodes(node)
+	SELECT relfilenode FROM pg_class WHERE relname='repack_deadlock';
+
+	SELECT count(DISTINCT node) FROM relfilenodes;
+
+	SELECT i, j FROM repack_deadlock ORDER BY i, j;
+
+	INSERT INTO data_s1(i, j)
+	SELECT i, j FROM repack_deadlock;
+
+	SELECT count(*)
+	FROM data_s1 d1 FULL JOIN data_s2 d2 USING (i, j)
+	WHERE d1.i ISNULL OR d2.i ISNULL;
+
+count
+-----
+    1
+(1 row)
+
+i|j
+-+-
+1|1
+2|2
+3|3
+4|4
+(4 rows)
+
+count
+-----
+    4
+(1 row)
+
+injection_points_detach
+-----------------------
+                       
+(1 row)
+
diff --git a/src/test/modules/injection_points/meson.build b/src/test/modules/injection_points/meson.build
index a414abb924b..1cd88d6db65 100644
--- a/src/test/modules/injection_points/meson.build
+++ b/src/test/modules/injection_points/meson.build
@@ -46,6 +46,7 @@ tests += {
       'basic',
       'inplace',
       'repack',
+      'repack_deadlock',
       'repack_toast',
       'syscache-update-pruned',
       'heap_lock_update',
diff --git a/src/test/modules/injection_points/specs/repack_deadlock.spec b/src/test/modules/injection_points/specs/repack_deadlock.spec
new file mode 100644
index 00000000000..9d23a6588c2
--- /dev/null
+++ b/src/test/modules/injection_points/specs/repack_deadlock.spec
@@ -0,0 +1,83 @@
+# Test REPACK with a concurrent transaction that would cause a deadlock
+setup
+{
+	CREATE EXTENSION injection_points;
+
+	CREATE TABLE repack_deadlock(i int PRIMARY KEY, j int);
+	INSERT INTO repack_deadlock(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4);
+
+	CREATE TABLE relfilenodes(node oid);
+
+	CREATE TABLE data_s1(i int, j int);
+	CREATE TABLE data_s2(i int, j int);
+}
+
+teardown
+{
+	DROP TABLE repack_deadlock;
+	DROP EXTENSION injection_points;
+
+	DROP TABLE relfilenodes;
+	DROP TABLE data_s1;
+	DROP TABLE data_s2;
+}
+
+session s1
+setup
+{
+	SELECT injection_points_set_local();
+	SELECT injection_points_attach('repack-concurrently-before-lock', 'wait');
+}
+# Perform the initial load and wait for s2 to do some data changes.
+step wait_before_lock
+{
+	REPACK (CONCURRENTLY) repack_deadlock USING INDEX repack_deadlock_pkey;
+}
+# Check the table from the perspective of s1.
+#
+# Besides the contents, we also check that relfilenode has changed.
+
+# Have each session write the contents into a table and use FULL JOIN to check
+# if the outputs are identical.
+step check1
+{
+	INSERT INTO relfilenodes(node)
+	SELECT relfilenode FROM pg_class WHERE relname='repack_deadlock';
+
+	SELECT count(DISTINCT node) FROM relfilenodes;
+
+	SELECT i, j FROM repack_deadlock ORDER BY i, j;
+
+	INSERT INTO data_s1(i, j)
+	SELECT i, j FROM repack_deadlock;
+
+	SELECT count(*)
+	FROM data_s1 d1 FULL JOIN data_s2 d2 USING (i, j)
+	WHERE d1.i ISNULL OR d2.i ISNULL;
+}
+teardown
+{
+	SELECT injection_points_detach('repack-concurrently-before-lock');
+}
+
+session s2
+# Change the existing data. UPDATE changes both key and non-key columns. Also
+# update one row twice to test whether tuple version generated by this session
+# can be found.
+step add_column
+{
+	alter table repack_deadlock add column noise text;
+}
+
+step wakeup_before_lock
+{
+	SELECT injection_points_wakeup('repack-concurrently-before-lock');
+}
+
+# Test if data changes introduced while one session is performing REPACK
+# CONCURRENTLY find their way into the table.
+permutation
+	wait_before_lock
+	add_column
+	wakeup_before_lock
+	check1
-- 
2.47.3


--fot5plqmb33ey33c--





^ permalink  raw  reply  [nested|flat] 89+ messages in thread

* [PATCH v44b 4/4] hack deadlock detector
@ 2026-03-25 19:35  Álvaro Herrera <[email protected]>
  0 siblings, 0 replies; 89+ messages in thread

From: Álvaro Herrera @ 2026-03-25 19:35 UTC (permalink / raw)

---
 src/backend/commands/cluster.c                | 10 +++
 src/backend/storage/lmgr/deadlock.c           | 12 +++
 src/include/storage/proc.h                    |  1 +
 src/test/modules/injection_points/Makefile    |  1 +
 .../expected/repack_deadlock.out              | 63 ++++++++++++++
 src/test/modules/injection_points/meson.build |  1 +
 .../specs/repack_deadlock.spec                | 83 +++++++++++++++++++
 7 files changed, 171 insertions(+)
 create mode 100644 src/test/modules/injection_points/expected/repack_deadlock.out
 create mode 100644 src/test/modules/injection_points/specs/repack_deadlock.spec

diff --git a/src/backend/commands/cluster.c b/src/backend/commands/cluster.c
index 2c3058ba10d..d5b1dfbff69 100644
--- a/src/backend/commands/cluster.c
+++ b/src/backend/commands/cluster.c
@@ -372,6 +372,16 @@ ExecRepack(ParseState *pstate, RepackStmt *stmt, bool isTopLevel)
 	/* Determine the lock mode to use. */
 	lockmode = RepackLockLevel((params.options & CLUOPT_CONCURRENT) != 0);
 
+	/*
+	 * If in concurrent mode, set the PROC_IN_CONCURRENT_REPACK flag.  This
+	 * makes the deadlock checker cause anyone that would conflict with us
+	 * to error out.  It's important to set this flag ahead of actually locking
+	 * the relation; it won't of course affect anyone until we do have a lock
+	 * that others can conflict with.
+	 */
+	if ((params.options & CLUOPT_CONCURRENT) != 0)
+		MyProc->statusFlags |= PROC_IN_CONCURRENT_REPACK;
+
 	/*
 	 * If a single relation is specified, process it and we're done ... unless
 	 * the relation is a partitioned table, in which case we fall through.
diff --git a/src/backend/storage/lmgr/deadlock.c b/src/backend/storage/lmgr/deadlock.c
index b8962d875b6..10266e384aa 100644
--- a/src/backend/storage/lmgr/deadlock.c
+++ b/src/backend/storage/lmgr/deadlock.c
@@ -620,6 +620,18 @@ FindLockCycleRecurseMember(PGPROC *checkProc,
 						proc->statusFlags & PROC_IS_AUTOVACUUM)
 						blocking_autovacuum_proc = proc;
 
+					/*
+					 * If we note that we're blocked by some process running
+					 * REPACK (CONCURRENTLY), just fail.  That process is
+					 * going to upgrade its lock at some point, and it would
+					 * be inappropriate for any other process to cause that
+					 * to fail.
+					 */
+					if (checkProc == MyProc &&
+						proc->statusFlags & PROC_IN_CONCURRENT_REPACK)
+						/* not a serious error message proposal */
+						elog(ERROR, "oops, we're blocked by REPACK, time to die");
+
 					/* We're done looking at this proclock */
 					break;
 				}
diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h
index 1dad125706e..4e340ead455 100644
--- a/src/include/storage/proc.h
+++ b/src/include/storage/proc.h
@@ -69,6 +69,7 @@ struct XidCache
 #define		PROC_AFFECTS_ALL_HORIZONS	0x20	/* this proc's xmin must be
 												 * included in vacuum horizons
 												 * in all databases */
+#define		PROC_IN_CONCURRENT_REPACK	0x40	/* REPACK (CONCURRENTLY) */
 
 /* flags reset at EOXact */
 #define		PROC_VACUUM_STATE_MASK \
diff --git a/src/test/modules/injection_points/Makefile b/src/test/modules/injection_points/Makefile
index 2cd7d87c533..f7663859fe2 100644
--- a/src/test/modules/injection_points/Makefile
+++ b/src/test/modules/injection_points/Makefile
@@ -15,6 +15,7 @@ REGRESS_OPTS = --dlpath=$(top_builddir)/src/test/regress
 ISOLATION = basic \
 	    inplace \
 	    repack \
+	    repack_deadlock \
 	    repack_toast \
 	    syscache-update-pruned \
 	    heap_lock_update
diff --git a/src/test/modules/injection_points/expected/repack_deadlock.out b/src/test/modules/injection_points/expected/repack_deadlock.out
new file mode 100644
index 00000000000..42505d10933
--- /dev/null
+++ b/src/test/modules/injection_points/expected/repack_deadlock.out
@@ -0,0 +1,63 @@
+Parsed test spec with 2 sessions
+
+starting permutation: wait_before_lock add_column wakeup_before_lock check1
+injection_points_attach
+-----------------------
+                       
+(1 row)
+
+step wait_before_lock: 
+	REPACK (CONCURRENTLY) repack_deadlock USING INDEX repack_deadlock_pkey;
+ <waiting ...>
+step add_column: 
+	alter table repack_deadlock add column noise text;
+ <waiting ...>
+step add_column: <... completed>
+ERROR:  oops, we're blocked by REPACK, time to die
+step wakeup_before_lock: 
+	SELECT injection_points_wakeup('repack-concurrently-before-lock');
+
+injection_points_wakeup
+-----------------------
+                       
+(1 row)
+
+step wait_before_lock: <... completed>
+step check1: 
+	INSERT INTO relfilenodes(node)
+	SELECT relfilenode FROM pg_class WHERE relname='repack_deadlock';
+
+	SELECT count(DISTINCT node) FROM relfilenodes;
+
+	SELECT i, j FROM repack_deadlock ORDER BY i, j;
+
+	INSERT INTO data_s1(i, j)
+	SELECT i, j FROM repack_deadlock;
+
+	SELECT count(*)
+	FROM data_s1 d1 FULL JOIN data_s2 d2 USING (i, j)
+	WHERE d1.i ISNULL OR d2.i ISNULL;
+
+count
+-----
+    1
+(1 row)
+
+i|j
+-+-
+1|1
+2|2
+3|3
+4|4
+(4 rows)
+
+count
+-----
+    4
+(1 row)
+
+injection_points_detach
+-----------------------
+                       
+(1 row)
+
diff --git a/src/test/modules/injection_points/meson.build b/src/test/modules/injection_points/meson.build
index a414abb924b..1cd88d6db65 100644
--- a/src/test/modules/injection_points/meson.build
+++ b/src/test/modules/injection_points/meson.build
@@ -46,6 +46,7 @@ tests += {
       'basic',
       'inplace',
       'repack',
+      'repack_deadlock',
       'repack_toast',
       'syscache-update-pruned',
       'heap_lock_update',
diff --git a/src/test/modules/injection_points/specs/repack_deadlock.spec b/src/test/modules/injection_points/specs/repack_deadlock.spec
new file mode 100644
index 00000000000..9d23a6588c2
--- /dev/null
+++ b/src/test/modules/injection_points/specs/repack_deadlock.spec
@@ -0,0 +1,83 @@
+# Test REPACK with a concurrent transaction that would cause a deadlock
+setup
+{
+	CREATE EXTENSION injection_points;
+
+	CREATE TABLE repack_deadlock(i int PRIMARY KEY, j int);
+	INSERT INTO repack_deadlock(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4);
+
+	CREATE TABLE relfilenodes(node oid);
+
+	CREATE TABLE data_s1(i int, j int);
+	CREATE TABLE data_s2(i int, j int);
+}
+
+teardown
+{
+	DROP TABLE repack_deadlock;
+	DROP EXTENSION injection_points;
+
+	DROP TABLE relfilenodes;
+	DROP TABLE data_s1;
+	DROP TABLE data_s2;
+}
+
+session s1
+setup
+{
+	SELECT injection_points_set_local();
+	SELECT injection_points_attach('repack-concurrently-before-lock', 'wait');
+}
+# Perform the initial load and wait for s2 to do some data changes.
+step wait_before_lock
+{
+	REPACK (CONCURRENTLY) repack_deadlock USING INDEX repack_deadlock_pkey;
+}
+# Check the table from the perspective of s1.
+#
+# Besides the contents, we also check that relfilenode has changed.
+
+# Have each session write the contents into a table and use FULL JOIN to check
+# if the outputs are identical.
+step check1
+{
+	INSERT INTO relfilenodes(node)
+	SELECT relfilenode FROM pg_class WHERE relname='repack_deadlock';
+
+	SELECT count(DISTINCT node) FROM relfilenodes;
+
+	SELECT i, j FROM repack_deadlock ORDER BY i, j;
+
+	INSERT INTO data_s1(i, j)
+	SELECT i, j FROM repack_deadlock;
+
+	SELECT count(*)
+	FROM data_s1 d1 FULL JOIN data_s2 d2 USING (i, j)
+	WHERE d1.i ISNULL OR d2.i ISNULL;
+}
+teardown
+{
+	SELECT injection_points_detach('repack-concurrently-before-lock');
+}
+
+session s2
+# Change the existing data. UPDATE changes both key and non-key columns. Also
+# update one row twice to test whether tuple version generated by this session
+# can be found.
+step add_column
+{
+	alter table repack_deadlock add column noise text;
+}
+
+step wakeup_before_lock
+{
+	SELECT injection_points_wakeup('repack-concurrently-before-lock');
+}
+
+# Test if data changes introduced while one session is performing REPACK
+# CONCURRENTLY find their way into the table.
+permutation
+	wait_before_lock
+	add_column
+	wakeup_before_lock
+	check1
-- 
2.47.3


--fot5plqmb33ey33c--





^ permalink  raw  reply  [nested|flat] 89+ messages in thread

* [PATCH v44b 4/4] hack deadlock detector
@ 2026-03-25 19:35  Álvaro Herrera <[email protected]>
  0 siblings, 0 replies; 89+ messages in thread

From: Álvaro Herrera @ 2026-03-25 19:35 UTC (permalink / raw)

---
 src/backend/commands/cluster.c                | 10 +++
 src/backend/storage/lmgr/deadlock.c           | 12 +++
 src/include/storage/proc.h                    |  1 +
 src/test/modules/injection_points/Makefile    |  1 +
 .../expected/repack_deadlock.out              | 63 ++++++++++++++
 src/test/modules/injection_points/meson.build |  1 +
 .../specs/repack_deadlock.spec                | 83 +++++++++++++++++++
 7 files changed, 171 insertions(+)
 create mode 100644 src/test/modules/injection_points/expected/repack_deadlock.out
 create mode 100644 src/test/modules/injection_points/specs/repack_deadlock.spec

diff --git a/src/backend/commands/cluster.c b/src/backend/commands/cluster.c
index 2c3058ba10d..d5b1dfbff69 100644
--- a/src/backend/commands/cluster.c
+++ b/src/backend/commands/cluster.c
@@ -372,6 +372,16 @@ ExecRepack(ParseState *pstate, RepackStmt *stmt, bool isTopLevel)
 	/* Determine the lock mode to use. */
 	lockmode = RepackLockLevel((params.options & CLUOPT_CONCURRENT) != 0);
 
+	/*
+	 * If in concurrent mode, set the PROC_IN_CONCURRENT_REPACK flag.  This
+	 * makes the deadlock checker cause anyone that would conflict with us
+	 * to error out.  It's important to set this flag ahead of actually locking
+	 * the relation; it won't of course affect anyone until we do have a lock
+	 * that others can conflict with.
+	 */
+	if ((params.options & CLUOPT_CONCURRENT) != 0)
+		MyProc->statusFlags |= PROC_IN_CONCURRENT_REPACK;
+
 	/*
 	 * If a single relation is specified, process it and we're done ... unless
 	 * the relation is a partitioned table, in which case we fall through.
diff --git a/src/backend/storage/lmgr/deadlock.c b/src/backend/storage/lmgr/deadlock.c
index b8962d875b6..10266e384aa 100644
--- a/src/backend/storage/lmgr/deadlock.c
+++ b/src/backend/storage/lmgr/deadlock.c
@@ -620,6 +620,18 @@ FindLockCycleRecurseMember(PGPROC *checkProc,
 						proc->statusFlags & PROC_IS_AUTOVACUUM)
 						blocking_autovacuum_proc = proc;
 
+					/*
+					 * If we note that we're blocked by some process running
+					 * REPACK (CONCURRENTLY), just fail.  That process is
+					 * going to upgrade its lock at some point, and it would
+					 * be inappropriate for any other process to cause that
+					 * to fail.
+					 */
+					if (checkProc == MyProc &&
+						proc->statusFlags & PROC_IN_CONCURRENT_REPACK)
+						/* not a serious error message proposal */
+						elog(ERROR, "oops, we're blocked by REPACK, time to die");
+
 					/* We're done looking at this proclock */
 					break;
 				}
diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h
index 1dad125706e..4e340ead455 100644
--- a/src/include/storage/proc.h
+++ b/src/include/storage/proc.h
@@ -69,6 +69,7 @@ struct XidCache
 #define		PROC_AFFECTS_ALL_HORIZONS	0x20	/* this proc's xmin must be
 												 * included in vacuum horizons
 												 * in all databases */
+#define		PROC_IN_CONCURRENT_REPACK	0x40	/* REPACK (CONCURRENTLY) */
 
 /* flags reset at EOXact */
 #define		PROC_VACUUM_STATE_MASK \
diff --git a/src/test/modules/injection_points/Makefile b/src/test/modules/injection_points/Makefile
index 2cd7d87c533..f7663859fe2 100644
--- a/src/test/modules/injection_points/Makefile
+++ b/src/test/modules/injection_points/Makefile
@@ -15,6 +15,7 @@ REGRESS_OPTS = --dlpath=$(top_builddir)/src/test/regress
 ISOLATION = basic \
 	    inplace \
 	    repack \
+	    repack_deadlock \
 	    repack_toast \
 	    syscache-update-pruned \
 	    heap_lock_update
diff --git a/src/test/modules/injection_points/expected/repack_deadlock.out b/src/test/modules/injection_points/expected/repack_deadlock.out
new file mode 100644
index 00000000000..42505d10933
--- /dev/null
+++ b/src/test/modules/injection_points/expected/repack_deadlock.out
@@ -0,0 +1,63 @@
+Parsed test spec with 2 sessions
+
+starting permutation: wait_before_lock add_column wakeup_before_lock check1
+injection_points_attach
+-----------------------
+                       
+(1 row)
+
+step wait_before_lock: 
+	REPACK (CONCURRENTLY) repack_deadlock USING INDEX repack_deadlock_pkey;
+ <waiting ...>
+step add_column: 
+	alter table repack_deadlock add column noise text;
+ <waiting ...>
+step add_column: <... completed>
+ERROR:  oops, we're blocked by REPACK, time to die
+step wakeup_before_lock: 
+	SELECT injection_points_wakeup('repack-concurrently-before-lock');
+
+injection_points_wakeup
+-----------------------
+                       
+(1 row)
+
+step wait_before_lock: <... completed>
+step check1: 
+	INSERT INTO relfilenodes(node)
+	SELECT relfilenode FROM pg_class WHERE relname='repack_deadlock';
+
+	SELECT count(DISTINCT node) FROM relfilenodes;
+
+	SELECT i, j FROM repack_deadlock ORDER BY i, j;
+
+	INSERT INTO data_s1(i, j)
+	SELECT i, j FROM repack_deadlock;
+
+	SELECT count(*)
+	FROM data_s1 d1 FULL JOIN data_s2 d2 USING (i, j)
+	WHERE d1.i ISNULL OR d2.i ISNULL;
+
+count
+-----
+    1
+(1 row)
+
+i|j
+-+-
+1|1
+2|2
+3|3
+4|4
+(4 rows)
+
+count
+-----
+    4
+(1 row)
+
+injection_points_detach
+-----------------------
+                       
+(1 row)
+
diff --git a/src/test/modules/injection_points/meson.build b/src/test/modules/injection_points/meson.build
index a414abb924b..1cd88d6db65 100644
--- a/src/test/modules/injection_points/meson.build
+++ b/src/test/modules/injection_points/meson.build
@@ -46,6 +46,7 @@ tests += {
       'basic',
       'inplace',
       'repack',
+      'repack_deadlock',
       'repack_toast',
       'syscache-update-pruned',
       'heap_lock_update',
diff --git a/src/test/modules/injection_points/specs/repack_deadlock.spec b/src/test/modules/injection_points/specs/repack_deadlock.spec
new file mode 100644
index 00000000000..9d23a6588c2
--- /dev/null
+++ b/src/test/modules/injection_points/specs/repack_deadlock.spec
@@ -0,0 +1,83 @@
+# Test REPACK with a concurrent transaction that would cause a deadlock
+setup
+{
+	CREATE EXTENSION injection_points;
+
+	CREATE TABLE repack_deadlock(i int PRIMARY KEY, j int);
+	INSERT INTO repack_deadlock(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4);
+
+	CREATE TABLE relfilenodes(node oid);
+
+	CREATE TABLE data_s1(i int, j int);
+	CREATE TABLE data_s2(i int, j int);
+}
+
+teardown
+{
+	DROP TABLE repack_deadlock;
+	DROP EXTENSION injection_points;
+
+	DROP TABLE relfilenodes;
+	DROP TABLE data_s1;
+	DROP TABLE data_s2;
+}
+
+session s1
+setup
+{
+	SELECT injection_points_set_local();
+	SELECT injection_points_attach('repack-concurrently-before-lock', 'wait');
+}
+# Perform the initial load and wait for s2 to do some data changes.
+step wait_before_lock
+{
+	REPACK (CONCURRENTLY) repack_deadlock USING INDEX repack_deadlock_pkey;
+}
+# Check the table from the perspective of s1.
+#
+# Besides the contents, we also check that relfilenode has changed.
+
+# Have each session write the contents into a table and use FULL JOIN to check
+# if the outputs are identical.
+step check1
+{
+	INSERT INTO relfilenodes(node)
+	SELECT relfilenode FROM pg_class WHERE relname='repack_deadlock';
+
+	SELECT count(DISTINCT node) FROM relfilenodes;
+
+	SELECT i, j FROM repack_deadlock ORDER BY i, j;
+
+	INSERT INTO data_s1(i, j)
+	SELECT i, j FROM repack_deadlock;
+
+	SELECT count(*)
+	FROM data_s1 d1 FULL JOIN data_s2 d2 USING (i, j)
+	WHERE d1.i ISNULL OR d2.i ISNULL;
+}
+teardown
+{
+	SELECT injection_points_detach('repack-concurrently-before-lock');
+}
+
+session s2
+# Change the existing data. UPDATE changes both key and non-key columns. Also
+# update one row twice to test whether tuple version generated by this session
+# can be found.
+step add_column
+{
+	alter table repack_deadlock add column noise text;
+}
+
+step wakeup_before_lock
+{
+	SELECT injection_points_wakeup('repack-concurrently-before-lock');
+}
+
+# Test if data changes introduced while one session is performing REPACK
+# CONCURRENTLY find their way into the table.
+permutation
+	wait_before_lock
+	add_column
+	wakeup_before_lock
+	check1
-- 
2.47.3


--fot5plqmb33ey33c--





^ permalink  raw  reply  [nested|flat] 89+ messages in thread

* [PATCH v44b 4/4] hack deadlock detector
@ 2026-03-25 19:35  Álvaro Herrera <[email protected]>
  0 siblings, 0 replies; 89+ messages in thread

From: Álvaro Herrera @ 2026-03-25 19:35 UTC (permalink / raw)

---
 src/backend/commands/cluster.c                | 10 +++
 src/backend/storage/lmgr/deadlock.c           | 12 +++
 src/include/storage/proc.h                    |  1 +
 src/test/modules/injection_points/Makefile    |  1 +
 .../expected/repack_deadlock.out              | 63 ++++++++++++++
 src/test/modules/injection_points/meson.build |  1 +
 .../specs/repack_deadlock.spec                | 83 +++++++++++++++++++
 7 files changed, 171 insertions(+)
 create mode 100644 src/test/modules/injection_points/expected/repack_deadlock.out
 create mode 100644 src/test/modules/injection_points/specs/repack_deadlock.spec

diff --git a/src/backend/commands/cluster.c b/src/backend/commands/cluster.c
index 2c3058ba10d..d5b1dfbff69 100644
--- a/src/backend/commands/cluster.c
+++ b/src/backend/commands/cluster.c
@@ -372,6 +372,16 @@ ExecRepack(ParseState *pstate, RepackStmt *stmt, bool isTopLevel)
 	/* Determine the lock mode to use. */
 	lockmode = RepackLockLevel((params.options & CLUOPT_CONCURRENT) != 0);
 
+	/*
+	 * If in concurrent mode, set the PROC_IN_CONCURRENT_REPACK flag.  This
+	 * makes the deadlock checker cause anyone that would conflict with us
+	 * to error out.  It's important to set this flag ahead of actually locking
+	 * the relation; it won't of course affect anyone until we do have a lock
+	 * that others can conflict with.
+	 */
+	if ((params.options & CLUOPT_CONCURRENT) != 0)
+		MyProc->statusFlags |= PROC_IN_CONCURRENT_REPACK;
+
 	/*
 	 * If a single relation is specified, process it and we're done ... unless
 	 * the relation is a partitioned table, in which case we fall through.
diff --git a/src/backend/storage/lmgr/deadlock.c b/src/backend/storage/lmgr/deadlock.c
index b8962d875b6..10266e384aa 100644
--- a/src/backend/storage/lmgr/deadlock.c
+++ b/src/backend/storage/lmgr/deadlock.c
@@ -620,6 +620,18 @@ FindLockCycleRecurseMember(PGPROC *checkProc,
 						proc->statusFlags & PROC_IS_AUTOVACUUM)
 						blocking_autovacuum_proc = proc;
 
+					/*
+					 * If we note that we're blocked by some process running
+					 * REPACK (CONCURRENTLY), just fail.  That process is
+					 * going to upgrade its lock at some point, and it would
+					 * be inappropriate for any other process to cause that
+					 * to fail.
+					 */
+					if (checkProc == MyProc &&
+						proc->statusFlags & PROC_IN_CONCURRENT_REPACK)
+						/* not a serious error message proposal */
+						elog(ERROR, "oops, we're blocked by REPACK, time to die");
+
 					/* We're done looking at this proclock */
 					break;
 				}
diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h
index 1dad125706e..4e340ead455 100644
--- a/src/include/storage/proc.h
+++ b/src/include/storage/proc.h
@@ -69,6 +69,7 @@ struct XidCache
 #define		PROC_AFFECTS_ALL_HORIZONS	0x20	/* this proc's xmin must be
 												 * included in vacuum horizons
 												 * in all databases */
+#define		PROC_IN_CONCURRENT_REPACK	0x40	/* REPACK (CONCURRENTLY) */
 
 /* flags reset at EOXact */
 #define		PROC_VACUUM_STATE_MASK \
diff --git a/src/test/modules/injection_points/Makefile b/src/test/modules/injection_points/Makefile
index 2cd7d87c533..f7663859fe2 100644
--- a/src/test/modules/injection_points/Makefile
+++ b/src/test/modules/injection_points/Makefile
@@ -15,6 +15,7 @@ REGRESS_OPTS = --dlpath=$(top_builddir)/src/test/regress
 ISOLATION = basic \
 	    inplace \
 	    repack \
+	    repack_deadlock \
 	    repack_toast \
 	    syscache-update-pruned \
 	    heap_lock_update
diff --git a/src/test/modules/injection_points/expected/repack_deadlock.out b/src/test/modules/injection_points/expected/repack_deadlock.out
new file mode 100644
index 00000000000..42505d10933
--- /dev/null
+++ b/src/test/modules/injection_points/expected/repack_deadlock.out
@@ -0,0 +1,63 @@
+Parsed test spec with 2 sessions
+
+starting permutation: wait_before_lock add_column wakeup_before_lock check1
+injection_points_attach
+-----------------------
+                       
+(1 row)
+
+step wait_before_lock: 
+	REPACK (CONCURRENTLY) repack_deadlock USING INDEX repack_deadlock_pkey;
+ <waiting ...>
+step add_column: 
+	alter table repack_deadlock add column noise text;
+ <waiting ...>
+step add_column: <... completed>
+ERROR:  oops, we're blocked by REPACK, time to die
+step wakeup_before_lock: 
+	SELECT injection_points_wakeup('repack-concurrently-before-lock');
+
+injection_points_wakeup
+-----------------------
+                       
+(1 row)
+
+step wait_before_lock: <... completed>
+step check1: 
+	INSERT INTO relfilenodes(node)
+	SELECT relfilenode FROM pg_class WHERE relname='repack_deadlock';
+
+	SELECT count(DISTINCT node) FROM relfilenodes;
+
+	SELECT i, j FROM repack_deadlock ORDER BY i, j;
+
+	INSERT INTO data_s1(i, j)
+	SELECT i, j FROM repack_deadlock;
+
+	SELECT count(*)
+	FROM data_s1 d1 FULL JOIN data_s2 d2 USING (i, j)
+	WHERE d1.i ISNULL OR d2.i ISNULL;
+
+count
+-----
+    1
+(1 row)
+
+i|j
+-+-
+1|1
+2|2
+3|3
+4|4
+(4 rows)
+
+count
+-----
+    4
+(1 row)
+
+injection_points_detach
+-----------------------
+                       
+(1 row)
+
diff --git a/src/test/modules/injection_points/meson.build b/src/test/modules/injection_points/meson.build
index a414abb924b..1cd88d6db65 100644
--- a/src/test/modules/injection_points/meson.build
+++ b/src/test/modules/injection_points/meson.build
@@ -46,6 +46,7 @@ tests += {
       'basic',
       'inplace',
       'repack',
+      'repack_deadlock',
       'repack_toast',
       'syscache-update-pruned',
       'heap_lock_update',
diff --git a/src/test/modules/injection_points/specs/repack_deadlock.spec b/src/test/modules/injection_points/specs/repack_deadlock.spec
new file mode 100644
index 00000000000..9d23a6588c2
--- /dev/null
+++ b/src/test/modules/injection_points/specs/repack_deadlock.spec
@@ -0,0 +1,83 @@
+# Test REPACK with a concurrent transaction that would cause a deadlock
+setup
+{
+	CREATE EXTENSION injection_points;
+
+	CREATE TABLE repack_deadlock(i int PRIMARY KEY, j int);
+	INSERT INTO repack_deadlock(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4);
+
+	CREATE TABLE relfilenodes(node oid);
+
+	CREATE TABLE data_s1(i int, j int);
+	CREATE TABLE data_s2(i int, j int);
+}
+
+teardown
+{
+	DROP TABLE repack_deadlock;
+	DROP EXTENSION injection_points;
+
+	DROP TABLE relfilenodes;
+	DROP TABLE data_s1;
+	DROP TABLE data_s2;
+}
+
+session s1
+setup
+{
+	SELECT injection_points_set_local();
+	SELECT injection_points_attach('repack-concurrently-before-lock', 'wait');
+}
+# Perform the initial load and wait for s2 to do some data changes.
+step wait_before_lock
+{
+	REPACK (CONCURRENTLY) repack_deadlock USING INDEX repack_deadlock_pkey;
+}
+# Check the table from the perspective of s1.
+#
+# Besides the contents, we also check that relfilenode has changed.
+
+# Have each session write the contents into a table and use FULL JOIN to check
+# if the outputs are identical.
+step check1
+{
+	INSERT INTO relfilenodes(node)
+	SELECT relfilenode FROM pg_class WHERE relname='repack_deadlock';
+
+	SELECT count(DISTINCT node) FROM relfilenodes;
+
+	SELECT i, j FROM repack_deadlock ORDER BY i, j;
+
+	INSERT INTO data_s1(i, j)
+	SELECT i, j FROM repack_deadlock;
+
+	SELECT count(*)
+	FROM data_s1 d1 FULL JOIN data_s2 d2 USING (i, j)
+	WHERE d1.i ISNULL OR d2.i ISNULL;
+}
+teardown
+{
+	SELECT injection_points_detach('repack-concurrently-before-lock');
+}
+
+session s2
+# Change the existing data. UPDATE changes both key and non-key columns. Also
+# update one row twice to test whether tuple version generated by this session
+# can be found.
+step add_column
+{
+	alter table repack_deadlock add column noise text;
+}
+
+step wakeup_before_lock
+{
+	SELECT injection_points_wakeup('repack-concurrently-before-lock');
+}
+
+# Test if data changes introduced while one session is performing REPACK
+# CONCURRENTLY find their way into the table.
+permutation
+	wait_before_lock
+	add_column
+	wakeup_before_lock
+	check1
-- 
2.47.3


--fot5plqmb33ey33c--





^ permalink  raw  reply  [nested|flat] 89+ messages in thread

* [PATCH v44b 4/4] hack deadlock detector
@ 2026-03-25 19:35  Álvaro Herrera <[email protected]>
  0 siblings, 0 replies; 89+ messages in thread

From: Álvaro Herrera @ 2026-03-25 19:35 UTC (permalink / raw)

---
 src/backend/commands/cluster.c                | 10 +++
 src/backend/storage/lmgr/deadlock.c           | 12 +++
 src/include/storage/proc.h                    |  1 +
 src/test/modules/injection_points/Makefile    |  1 +
 .../expected/repack_deadlock.out              | 63 ++++++++++++++
 src/test/modules/injection_points/meson.build |  1 +
 .../specs/repack_deadlock.spec                | 83 +++++++++++++++++++
 7 files changed, 171 insertions(+)
 create mode 100644 src/test/modules/injection_points/expected/repack_deadlock.out
 create mode 100644 src/test/modules/injection_points/specs/repack_deadlock.spec

diff --git a/src/backend/commands/cluster.c b/src/backend/commands/cluster.c
index 2c3058ba10d..d5b1dfbff69 100644
--- a/src/backend/commands/cluster.c
+++ b/src/backend/commands/cluster.c
@@ -372,6 +372,16 @@ ExecRepack(ParseState *pstate, RepackStmt *stmt, bool isTopLevel)
 	/* Determine the lock mode to use. */
 	lockmode = RepackLockLevel((params.options & CLUOPT_CONCURRENT) != 0);
 
+	/*
+	 * If in concurrent mode, set the PROC_IN_CONCURRENT_REPACK flag.  This
+	 * makes the deadlock checker cause anyone that would conflict with us
+	 * to error out.  It's important to set this flag ahead of actually locking
+	 * the relation; it won't of course affect anyone until we do have a lock
+	 * that others can conflict with.
+	 */
+	if ((params.options & CLUOPT_CONCURRENT) != 0)
+		MyProc->statusFlags |= PROC_IN_CONCURRENT_REPACK;
+
 	/*
 	 * If a single relation is specified, process it and we're done ... unless
 	 * the relation is a partitioned table, in which case we fall through.
diff --git a/src/backend/storage/lmgr/deadlock.c b/src/backend/storage/lmgr/deadlock.c
index b8962d875b6..10266e384aa 100644
--- a/src/backend/storage/lmgr/deadlock.c
+++ b/src/backend/storage/lmgr/deadlock.c
@@ -620,6 +620,18 @@ FindLockCycleRecurseMember(PGPROC *checkProc,
 						proc->statusFlags & PROC_IS_AUTOVACUUM)
 						blocking_autovacuum_proc = proc;
 
+					/*
+					 * If we note that we're blocked by some process running
+					 * REPACK (CONCURRENTLY), just fail.  That process is
+					 * going to upgrade its lock at some point, and it would
+					 * be inappropriate for any other process to cause that
+					 * to fail.
+					 */
+					if (checkProc == MyProc &&
+						proc->statusFlags & PROC_IN_CONCURRENT_REPACK)
+						/* not a serious error message proposal */
+						elog(ERROR, "oops, we're blocked by REPACK, time to die");
+
 					/* We're done looking at this proclock */
 					break;
 				}
diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h
index 1dad125706e..4e340ead455 100644
--- a/src/include/storage/proc.h
+++ b/src/include/storage/proc.h
@@ -69,6 +69,7 @@ struct XidCache
 #define		PROC_AFFECTS_ALL_HORIZONS	0x20	/* this proc's xmin must be
 												 * included in vacuum horizons
 												 * in all databases */
+#define		PROC_IN_CONCURRENT_REPACK	0x40	/* REPACK (CONCURRENTLY) */
 
 /* flags reset at EOXact */
 #define		PROC_VACUUM_STATE_MASK \
diff --git a/src/test/modules/injection_points/Makefile b/src/test/modules/injection_points/Makefile
index 2cd7d87c533..f7663859fe2 100644
--- a/src/test/modules/injection_points/Makefile
+++ b/src/test/modules/injection_points/Makefile
@@ -15,6 +15,7 @@ REGRESS_OPTS = --dlpath=$(top_builddir)/src/test/regress
 ISOLATION = basic \
 	    inplace \
 	    repack \
+	    repack_deadlock \
 	    repack_toast \
 	    syscache-update-pruned \
 	    heap_lock_update
diff --git a/src/test/modules/injection_points/expected/repack_deadlock.out b/src/test/modules/injection_points/expected/repack_deadlock.out
new file mode 100644
index 00000000000..42505d10933
--- /dev/null
+++ b/src/test/modules/injection_points/expected/repack_deadlock.out
@@ -0,0 +1,63 @@
+Parsed test spec with 2 sessions
+
+starting permutation: wait_before_lock add_column wakeup_before_lock check1
+injection_points_attach
+-----------------------
+                       
+(1 row)
+
+step wait_before_lock: 
+	REPACK (CONCURRENTLY) repack_deadlock USING INDEX repack_deadlock_pkey;
+ <waiting ...>
+step add_column: 
+	alter table repack_deadlock add column noise text;
+ <waiting ...>
+step add_column: <... completed>
+ERROR:  oops, we're blocked by REPACK, time to die
+step wakeup_before_lock: 
+	SELECT injection_points_wakeup('repack-concurrently-before-lock');
+
+injection_points_wakeup
+-----------------------
+                       
+(1 row)
+
+step wait_before_lock: <... completed>
+step check1: 
+	INSERT INTO relfilenodes(node)
+	SELECT relfilenode FROM pg_class WHERE relname='repack_deadlock';
+
+	SELECT count(DISTINCT node) FROM relfilenodes;
+
+	SELECT i, j FROM repack_deadlock ORDER BY i, j;
+
+	INSERT INTO data_s1(i, j)
+	SELECT i, j FROM repack_deadlock;
+
+	SELECT count(*)
+	FROM data_s1 d1 FULL JOIN data_s2 d2 USING (i, j)
+	WHERE d1.i ISNULL OR d2.i ISNULL;
+
+count
+-----
+    1
+(1 row)
+
+i|j
+-+-
+1|1
+2|2
+3|3
+4|4
+(4 rows)
+
+count
+-----
+    4
+(1 row)
+
+injection_points_detach
+-----------------------
+                       
+(1 row)
+
diff --git a/src/test/modules/injection_points/meson.build b/src/test/modules/injection_points/meson.build
index a414abb924b..1cd88d6db65 100644
--- a/src/test/modules/injection_points/meson.build
+++ b/src/test/modules/injection_points/meson.build
@@ -46,6 +46,7 @@ tests += {
       'basic',
       'inplace',
       'repack',
+      'repack_deadlock',
       'repack_toast',
       'syscache-update-pruned',
       'heap_lock_update',
diff --git a/src/test/modules/injection_points/specs/repack_deadlock.spec b/src/test/modules/injection_points/specs/repack_deadlock.spec
new file mode 100644
index 00000000000..9d23a6588c2
--- /dev/null
+++ b/src/test/modules/injection_points/specs/repack_deadlock.spec
@@ -0,0 +1,83 @@
+# Test REPACK with a concurrent transaction that would cause a deadlock
+setup
+{
+	CREATE EXTENSION injection_points;
+
+	CREATE TABLE repack_deadlock(i int PRIMARY KEY, j int);
+	INSERT INTO repack_deadlock(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4);
+
+	CREATE TABLE relfilenodes(node oid);
+
+	CREATE TABLE data_s1(i int, j int);
+	CREATE TABLE data_s2(i int, j int);
+}
+
+teardown
+{
+	DROP TABLE repack_deadlock;
+	DROP EXTENSION injection_points;
+
+	DROP TABLE relfilenodes;
+	DROP TABLE data_s1;
+	DROP TABLE data_s2;
+}
+
+session s1
+setup
+{
+	SELECT injection_points_set_local();
+	SELECT injection_points_attach('repack-concurrently-before-lock', 'wait');
+}
+# Perform the initial load and wait for s2 to do some data changes.
+step wait_before_lock
+{
+	REPACK (CONCURRENTLY) repack_deadlock USING INDEX repack_deadlock_pkey;
+}
+# Check the table from the perspective of s1.
+#
+# Besides the contents, we also check that relfilenode has changed.
+
+# Have each session write the contents into a table and use FULL JOIN to check
+# if the outputs are identical.
+step check1
+{
+	INSERT INTO relfilenodes(node)
+	SELECT relfilenode FROM pg_class WHERE relname='repack_deadlock';
+
+	SELECT count(DISTINCT node) FROM relfilenodes;
+
+	SELECT i, j FROM repack_deadlock ORDER BY i, j;
+
+	INSERT INTO data_s1(i, j)
+	SELECT i, j FROM repack_deadlock;
+
+	SELECT count(*)
+	FROM data_s1 d1 FULL JOIN data_s2 d2 USING (i, j)
+	WHERE d1.i ISNULL OR d2.i ISNULL;
+}
+teardown
+{
+	SELECT injection_points_detach('repack-concurrently-before-lock');
+}
+
+session s2
+# Change the existing data. UPDATE changes both key and non-key columns. Also
+# update one row twice to test whether tuple version generated by this session
+# can be found.
+step add_column
+{
+	alter table repack_deadlock add column noise text;
+}
+
+step wakeup_before_lock
+{
+	SELECT injection_points_wakeup('repack-concurrently-before-lock');
+}
+
+# Test if data changes introduced while one session is performing REPACK
+# CONCURRENTLY find their way into the table.
+permutation
+	wait_before_lock
+	add_column
+	wakeup_before_lock
+	check1
-- 
2.47.3


--fot5plqmb33ey33c--





^ permalink  raw  reply  [nested|flat] 89+ messages in thread

* [PATCH v44b 4/4] hack deadlock detector
@ 2026-03-25 19:35  Álvaro Herrera <[email protected]>
  0 siblings, 0 replies; 89+ messages in thread

From: Álvaro Herrera @ 2026-03-25 19:35 UTC (permalink / raw)

---
 src/backend/commands/cluster.c                | 10 +++
 src/backend/storage/lmgr/deadlock.c           | 12 +++
 src/include/storage/proc.h                    |  1 +
 src/test/modules/injection_points/Makefile    |  1 +
 .../expected/repack_deadlock.out              | 63 ++++++++++++++
 src/test/modules/injection_points/meson.build |  1 +
 .../specs/repack_deadlock.spec                | 83 +++++++++++++++++++
 7 files changed, 171 insertions(+)
 create mode 100644 src/test/modules/injection_points/expected/repack_deadlock.out
 create mode 100644 src/test/modules/injection_points/specs/repack_deadlock.spec

diff --git a/src/backend/commands/cluster.c b/src/backend/commands/cluster.c
index 2c3058ba10d..d5b1dfbff69 100644
--- a/src/backend/commands/cluster.c
+++ b/src/backend/commands/cluster.c
@@ -372,6 +372,16 @@ ExecRepack(ParseState *pstate, RepackStmt *stmt, bool isTopLevel)
 	/* Determine the lock mode to use. */
 	lockmode = RepackLockLevel((params.options & CLUOPT_CONCURRENT) != 0);
 
+	/*
+	 * If in concurrent mode, set the PROC_IN_CONCURRENT_REPACK flag.  This
+	 * makes the deadlock checker cause anyone that would conflict with us
+	 * to error out.  It's important to set this flag ahead of actually locking
+	 * the relation; it won't of course affect anyone until we do have a lock
+	 * that others can conflict with.
+	 */
+	if ((params.options & CLUOPT_CONCURRENT) != 0)
+		MyProc->statusFlags |= PROC_IN_CONCURRENT_REPACK;
+
 	/*
 	 * If a single relation is specified, process it and we're done ... unless
 	 * the relation is a partitioned table, in which case we fall through.
diff --git a/src/backend/storage/lmgr/deadlock.c b/src/backend/storage/lmgr/deadlock.c
index b8962d875b6..10266e384aa 100644
--- a/src/backend/storage/lmgr/deadlock.c
+++ b/src/backend/storage/lmgr/deadlock.c
@@ -620,6 +620,18 @@ FindLockCycleRecurseMember(PGPROC *checkProc,
 						proc->statusFlags & PROC_IS_AUTOVACUUM)
 						blocking_autovacuum_proc = proc;
 
+					/*
+					 * If we note that we're blocked by some process running
+					 * REPACK (CONCURRENTLY), just fail.  That process is
+					 * going to upgrade its lock at some point, and it would
+					 * be inappropriate for any other process to cause that
+					 * to fail.
+					 */
+					if (checkProc == MyProc &&
+						proc->statusFlags & PROC_IN_CONCURRENT_REPACK)
+						/* not a serious error message proposal */
+						elog(ERROR, "oops, we're blocked by REPACK, time to die");
+
 					/* We're done looking at this proclock */
 					break;
 				}
diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h
index 1dad125706e..4e340ead455 100644
--- a/src/include/storage/proc.h
+++ b/src/include/storage/proc.h
@@ -69,6 +69,7 @@ struct XidCache
 #define		PROC_AFFECTS_ALL_HORIZONS	0x20	/* this proc's xmin must be
 												 * included in vacuum horizons
 												 * in all databases */
+#define		PROC_IN_CONCURRENT_REPACK	0x40	/* REPACK (CONCURRENTLY) */
 
 /* flags reset at EOXact */
 #define		PROC_VACUUM_STATE_MASK \
diff --git a/src/test/modules/injection_points/Makefile b/src/test/modules/injection_points/Makefile
index 2cd7d87c533..f7663859fe2 100644
--- a/src/test/modules/injection_points/Makefile
+++ b/src/test/modules/injection_points/Makefile
@@ -15,6 +15,7 @@ REGRESS_OPTS = --dlpath=$(top_builddir)/src/test/regress
 ISOLATION = basic \
 	    inplace \
 	    repack \
+	    repack_deadlock \
 	    repack_toast \
 	    syscache-update-pruned \
 	    heap_lock_update
diff --git a/src/test/modules/injection_points/expected/repack_deadlock.out b/src/test/modules/injection_points/expected/repack_deadlock.out
new file mode 100644
index 00000000000..42505d10933
--- /dev/null
+++ b/src/test/modules/injection_points/expected/repack_deadlock.out
@@ -0,0 +1,63 @@
+Parsed test spec with 2 sessions
+
+starting permutation: wait_before_lock add_column wakeup_before_lock check1
+injection_points_attach
+-----------------------
+                       
+(1 row)
+
+step wait_before_lock: 
+	REPACK (CONCURRENTLY) repack_deadlock USING INDEX repack_deadlock_pkey;
+ <waiting ...>
+step add_column: 
+	alter table repack_deadlock add column noise text;
+ <waiting ...>
+step add_column: <... completed>
+ERROR:  oops, we're blocked by REPACK, time to die
+step wakeup_before_lock: 
+	SELECT injection_points_wakeup('repack-concurrently-before-lock');
+
+injection_points_wakeup
+-----------------------
+                       
+(1 row)
+
+step wait_before_lock: <... completed>
+step check1: 
+	INSERT INTO relfilenodes(node)
+	SELECT relfilenode FROM pg_class WHERE relname='repack_deadlock';
+
+	SELECT count(DISTINCT node) FROM relfilenodes;
+
+	SELECT i, j FROM repack_deadlock ORDER BY i, j;
+
+	INSERT INTO data_s1(i, j)
+	SELECT i, j FROM repack_deadlock;
+
+	SELECT count(*)
+	FROM data_s1 d1 FULL JOIN data_s2 d2 USING (i, j)
+	WHERE d1.i ISNULL OR d2.i ISNULL;
+
+count
+-----
+    1
+(1 row)
+
+i|j
+-+-
+1|1
+2|2
+3|3
+4|4
+(4 rows)
+
+count
+-----
+    4
+(1 row)
+
+injection_points_detach
+-----------------------
+                       
+(1 row)
+
diff --git a/src/test/modules/injection_points/meson.build b/src/test/modules/injection_points/meson.build
index a414abb924b..1cd88d6db65 100644
--- a/src/test/modules/injection_points/meson.build
+++ b/src/test/modules/injection_points/meson.build
@@ -46,6 +46,7 @@ tests += {
       'basic',
       'inplace',
       'repack',
+      'repack_deadlock',
       'repack_toast',
       'syscache-update-pruned',
       'heap_lock_update',
diff --git a/src/test/modules/injection_points/specs/repack_deadlock.spec b/src/test/modules/injection_points/specs/repack_deadlock.spec
new file mode 100644
index 00000000000..9d23a6588c2
--- /dev/null
+++ b/src/test/modules/injection_points/specs/repack_deadlock.spec
@@ -0,0 +1,83 @@
+# Test REPACK with a concurrent transaction that would cause a deadlock
+setup
+{
+	CREATE EXTENSION injection_points;
+
+	CREATE TABLE repack_deadlock(i int PRIMARY KEY, j int);
+	INSERT INTO repack_deadlock(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4);
+
+	CREATE TABLE relfilenodes(node oid);
+
+	CREATE TABLE data_s1(i int, j int);
+	CREATE TABLE data_s2(i int, j int);
+}
+
+teardown
+{
+	DROP TABLE repack_deadlock;
+	DROP EXTENSION injection_points;
+
+	DROP TABLE relfilenodes;
+	DROP TABLE data_s1;
+	DROP TABLE data_s2;
+}
+
+session s1
+setup
+{
+	SELECT injection_points_set_local();
+	SELECT injection_points_attach('repack-concurrently-before-lock', 'wait');
+}
+# Perform the initial load and wait for s2 to do some data changes.
+step wait_before_lock
+{
+	REPACK (CONCURRENTLY) repack_deadlock USING INDEX repack_deadlock_pkey;
+}
+# Check the table from the perspective of s1.
+#
+# Besides the contents, we also check that relfilenode has changed.
+
+# Have each session write the contents into a table and use FULL JOIN to check
+# if the outputs are identical.
+step check1
+{
+	INSERT INTO relfilenodes(node)
+	SELECT relfilenode FROM pg_class WHERE relname='repack_deadlock';
+
+	SELECT count(DISTINCT node) FROM relfilenodes;
+
+	SELECT i, j FROM repack_deadlock ORDER BY i, j;
+
+	INSERT INTO data_s1(i, j)
+	SELECT i, j FROM repack_deadlock;
+
+	SELECT count(*)
+	FROM data_s1 d1 FULL JOIN data_s2 d2 USING (i, j)
+	WHERE d1.i ISNULL OR d2.i ISNULL;
+}
+teardown
+{
+	SELECT injection_points_detach('repack-concurrently-before-lock');
+}
+
+session s2
+# Change the existing data. UPDATE changes both key and non-key columns. Also
+# update one row twice to test whether tuple version generated by this session
+# can be found.
+step add_column
+{
+	alter table repack_deadlock add column noise text;
+}
+
+step wakeup_before_lock
+{
+	SELECT injection_points_wakeup('repack-concurrently-before-lock');
+}
+
+# Test if data changes introduced while one session is performing REPACK
+# CONCURRENTLY find their way into the table.
+permutation
+	wait_before_lock
+	add_column
+	wakeup_before_lock
+	check1
-- 
2.47.3


--fot5plqmb33ey33c--





^ permalink  raw  reply  [nested|flat] 89+ messages in thread

* [PATCH v44b 4/4] hack deadlock detector
@ 2026-03-25 19:35  Álvaro Herrera <[email protected]>
  0 siblings, 0 replies; 89+ messages in thread

From: Álvaro Herrera @ 2026-03-25 19:35 UTC (permalink / raw)

---
 src/backend/commands/cluster.c                | 10 +++
 src/backend/storage/lmgr/deadlock.c           | 12 +++
 src/include/storage/proc.h                    |  1 +
 src/test/modules/injection_points/Makefile    |  1 +
 .../expected/repack_deadlock.out              | 63 ++++++++++++++
 src/test/modules/injection_points/meson.build |  1 +
 .../specs/repack_deadlock.spec                | 83 +++++++++++++++++++
 7 files changed, 171 insertions(+)
 create mode 100644 src/test/modules/injection_points/expected/repack_deadlock.out
 create mode 100644 src/test/modules/injection_points/specs/repack_deadlock.spec

diff --git a/src/backend/commands/cluster.c b/src/backend/commands/cluster.c
index 2c3058ba10d..d5b1dfbff69 100644
--- a/src/backend/commands/cluster.c
+++ b/src/backend/commands/cluster.c
@@ -372,6 +372,16 @@ ExecRepack(ParseState *pstate, RepackStmt *stmt, bool isTopLevel)
 	/* Determine the lock mode to use. */
 	lockmode = RepackLockLevel((params.options & CLUOPT_CONCURRENT) != 0);
 
+	/*
+	 * If in concurrent mode, set the PROC_IN_CONCURRENT_REPACK flag.  This
+	 * makes the deadlock checker cause anyone that would conflict with us
+	 * to error out.  It's important to set this flag ahead of actually locking
+	 * the relation; it won't of course affect anyone until we do have a lock
+	 * that others can conflict with.
+	 */
+	if ((params.options & CLUOPT_CONCURRENT) != 0)
+		MyProc->statusFlags |= PROC_IN_CONCURRENT_REPACK;
+
 	/*
 	 * If a single relation is specified, process it and we're done ... unless
 	 * the relation is a partitioned table, in which case we fall through.
diff --git a/src/backend/storage/lmgr/deadlock.c b/src/backend/storage/lmgr/deadlock.c
index b8962d875b6..10266e384aa 100644
--- a/src/backend/storage/lmgr/deadlock.c
+++ b/src/backend/storage/lmgr/deadlock.c
@@ -620,6 +620,18 @@ FindLockCycleRecurseMember(PGPROC *checkProc,
 						proc->statusFlags & PROC_IS_AUTOVACUUM)
 						blocking_autovacuum_proc = proc;
 
+					/*
+					 * If we note that we're blocked by some process running
+					 * REPACK (CONCURRENTLY), just fail.  That process is
+					 * going to upgrade its lock at some point, and it would
+					 * be inappropriate for any other process to cause that
+					 * to fail.
+					 */
+					if (checkProc == MyProc &&
+						proc->statusFlags & PROC_IN_CONCURRENT_REPACK)
+						/* not a serious error message proposal */
+						elog(ERROR, "oops, we're blocked by REPACK, time to die");
+
 					/* We're done looking at this proclock */
 					break;
 				}
diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h
index 1dad125706e..4e340ead455 100644
--- a/src/include/storage/proc.h
+++ b/src/include/storage/proc.h
@@ -69,6 +69,7 @@ struct XidCache
 #define		PROC_AFFECTS_ALL_HORIZONS	0x20	/* this proc's xmin must be
 												 * included in vacuum horizons
 												 * in all databases */
+#define		PROC_IN_CONCURRENT_REPACK	0x40	/* REPACK (CONCURRENTLY) */
 
 /* flags reset at EOXact */
 #define		PROC_VACUUM_STATE_MASK \
diff --git a/src/test/modules/injection_points/Makefile b/src/test/modules/injection_points/Makefile
index 2cd7d87c533..f7663859fe2 100644
--- a/src/test/modules/injection_points/Makefile
+++ b/src/test/modules/injection_points/Makefile
@@ -15,6 +15,7 @@ REGRESS_OPTS = --dlpath=$(top_builddir)/src/test/regress
 ISOLATION = basic \
 	    inplace \
 	    repack \
+	    repack_deadlock \
 	    repack_toast \
 	    syscache-update-pruned \
 	    heap_lock_update
diff --git a/src/test/modules/injection_points/expected/repack_deadlock.out b/src/test/modules/injection_points/expected/repack_deadlock.out
new file mode 100644
index 00000000000..42505d10933
--- /dev/null
+++ b/src/test/modules/injection_points/expected/repack_deadlock.out
@@ -0,0 +1,63 @@
+Parsed test spec with 2 sessions
+
+starting permutation: wait_before_lock add_column wakeup_before_lock check1
+injection_points_attach
+-----------------------
+                       
+(1 row)
+
+step wait_before_lock: 
+	REPACK (CONCURRENTLY) repack_deadlock USING INDEX repack_deadlock_pkey;
+ <waiting ...>
+step add_column: 
+	alter table repack_deadlock add column noise text;
+ <waiting ...>
+step add_column: <... completed>
+ERROR:  oops, we're blocked by REPACK, time to die
+step wakeup_before_lock: 
+	SELECT injection_points_wakeup('repack-concurrently-before-lock');
+
+injection_points_wakeup
+-----------------------
+                       
+(1 row)
+
+step wait_before_lock: <... completed>
+step check1: 
+	INSERT INTO relfilenodes(node)
+	SELECT relfilenode FROM pg_class WHERE relname='repack_deadlock';
+
+	SELECT count(DISTINCT node) FROM relfilenodes;
+
+	SELECT i, j FROM repack_deadlock ORDER BY i, j;
+
+	INSERT INTO data_s1(i, j)
+	SELECT i, j FROM repack_deadlock;
+
+	SELECT count(*)
+	FROM data_s1 d1 FULL JOIN data_s2 d2 USING (i, j)
+	WHERE d1.i ISNULL OR d2.i ISNULL;
+
+count
+-----
+    1
+(1 row)
+
+i|j
+-+-
+1|1
+2|2
+3|3
+4|4
+(4 rows)
+
+count
+-----
+    4
+(1 row)
+
+injection_points_detach
+-----------------------
+                       
+(1 row)
+
diff --git a/src/test/modules/injection_points/meson.build b/src/test/modules/injection_points/meson.build
index a414abb924b..1cd88d6db65 100644
--- a/src/test/modules/injection_points/meson.build
+++ b/src/test/modules/injection_points/meson.build
@@ -46,6 +46,7 @@ tests += {
       'basic',
       'inplace',
       'repack',
+      'repack_deadlock',
       'repack_toast',
       'syscache-update-pruned',
       'heap_lock_update',
diff --git a/src/test/modules/injection_points/specs/repack_deadlock.spec b/src/test/modules/injection_points/specs/repack_deadlock.spec
new file mode 100644
index 00000000000..9d23a6588c2
--- /dev/null
+++ b/src/test/modules/injection_points/specs/repack_deadlock.spec
@@ -0,0 +1,83 @@
+# Test REPACK with a concurrent transaction that would cause a deadlock
+setup
+{
+	CREATE EXTENSION injection_points;
+
+	CREATE TABLE repack_deadlock(i int PRIMARY KEY, j int);
+	INSERT INTO repack_deadlock(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4);
+
+	CREATE TABLE relfilenodes(node oid);
+
+	CREATE TABLE data_s1(i int, j int);
+	CREATE TABLE data_s2(i int, j int);
+}
+
+teardown
+{
+	DROP TABLE repack_deadlock;
+	DROP EXTENSION injection_points;
+
+	DROP TABLE relfilenodes;
+	DROP TABLE data_s1;
+	DROP TABLE data_s2;
+}
+
+session s1
+setup
+{
+	SELECT injection_points_set_local();
+	SELECT injection_points_attach('repack-concurrently-before-lock', 'wait');
+}
+# Perform the initial load and wait for s2 to do some data changes.
+step wait_before_lock
+{
+	REPACK (CONCURRENTLY) repack_deadlock USING INDEX repack_deadlock_pkey;
+}
+# Check the table from the perspective of s1.
+#
+# Besides the contents, we also check that relfilenode has changed.
+
+# Have each session write the contents into a table and use FULL JOIN to check
+# if the outputs are identical.
+step check1
+{
+	INSERT INTO relfilenodes(node)
+	SELECT relfilenode FROM pg_class WHERE relname='repack_deadlock';
+
+	SELECT count(DISTINCT node) FROM relfilenodes;
+
+	SELECT i, j FROM repack_deadlock ORDER BY i, j;
+
+	INSERT INTO data_s1(i, j)
+	SELECT i, j FROM repack_deadlock;
+
+	SELECT count(*)
+	FROM data_s1 d1 FULL JOIN data_s2 d2 USING (i, j)
+	WHERE d1.i ISNULL OR d2.i ISNULL;
+}
+teardown
+{
+	SELECT injection_points_detach('repack-concurrently-before-lock');
+}
+
+session s2
+# Change the existing data. UPDATE changes both key and non-key columns. Also
+# update one row twice to test whether tuple version generated by this session
+# can be found.
+step add_column
+{
+	alter table repack_deadlock add column noise text;
+}
+
+step wakeup_before_lock
+{
+	SELECT injection_points_wakeup('repack-concurrently-before-lock');
+}
+
+# Test if data changes introduced while one session is performing REPACK
+# CONCURRENTLY find their way into the table.
+permutation
+	wait_before_lock
+	add_column
+	wakeup_before_lock
+	check1
-- 
2.47.3


--fot5plqmb33ey33c--





^ permalink  raw  reply  [nested|flat] 89+ messages in thread

* [PATCH v44b 4/4] hack deadlock detector
@ 2026-03-25 19:35  Álvaro Herrera <[email protected]>
  0 siblings, 0 replies; 89+ messages in thread

From: Álvaro Herrera @ 2026-03-25 19:35 UTC (permalink / raw)

---
 src/backend/commands/cluster.c                | 10 +++
 src/backend/storage/lmgr/deadlock.c           | 12 +++
 src/include/storage/proc.h                    |  1 +
 src/test/modules/injection_points/Makefile    |  1 +
 .../expected/repack_deadlock.out              | 63 ++++++++++++++
 src/test/modules/injection_points/meson.build |  1 +
 .../specs/repack_deadlock.spec                | 83 +++++++++++++++++++
 7 files changed, 171 insertions(+)
 create mode 100644 src/test/modules/injection_points/expected/repack_deadlock.out
 create mode 100644 src/test/modules/injection_points/specs/repack_deadlock.spec

diff --git a/src/backend/commands/cluster.c b/src/backend/commands/cluster.c
index 2c3058ba10d..d5b1dfbff69 100644
--- a/src/backend/commands/cluster.c
+++ b/src/backend/commands/cluster.c
@@ -372,6 +372,16 @@ ExecRepack(ParseState *pstate, RepackStmt *stmt, bool isTopLevel)
 	/* Determine the lock mode to use. */
 	lockmode = RepackLockLevel((params.options & CLUOPT_CONCURRENT) != 0);
 
+	/*
+	 * If in concurrent mode, set the PROC_IN_CONCURRENT_REPACK flag.  This
+	 * makes the deadlock checker cause anyone that would conflict with us
+	 * to error out.  It's important to set this flag ahead of actually locking
+	 * the relation; it won't of course affect anyone until we do have a lock
+	 * that others can conflict with.
+	 */
+	if ((params.options & CLUOPT_CONCURRENT) != 0)
+		MyProc->statusFlags |= PROC_IN_CONCURRENT_REPACK;
+
 	/*
 	 * If a single relation is specified, process it and we're done ... unless
 	 * the relation is a partitioned table, in which case we fall through.
diff --git a/src/backend/storage/lmgr/deadlock.c b/src/backend/storage/lmgr/deadlock.c
index b8962d875b6..10266e384aa 100644
--- a/src/backend/storage/lmgr/deadlock.c
+++ b/src/backend/storage/lmgr/deadlock.c
@@ -620,6 +620,18 @@ FindLockCycleRecurseMember(PGPROC *checkProc,
 						proc->statusFlags & PROC_IS_AUTOVACUUM)
 						blocking_autovacuum_proc = proc;
 
+					/*
+					 * If we note that we're blocked by some process running
+					 * REPACK (CONCURRENTLY), just fail.  That process is
+					 * going to upgrade its lock at some point, and it would
+					 * be inappropriate for any other process to cause that
+					 * to fail.
+					 */
+					if (checkProc == MyProc &&
+						proc->statusFlags & PROC_IN_CONCURRENT_REPACK)
+						/* not a serious error message proposal */
+						elog(ERROR, "oops, we're blocked by REPACK, time to die");
+
 					/* We're done looking at this proclock */
 					break;
 				}
diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h
index 1dad125706e..4e340ead455 100644
--- a/src/include/storage/proc.h
+++ b/src/include/storage/proc.h
@@ -69,6 +69,7 @@ struct XidCache
 #define		PROC_AFFECTS_ALL_HORIZONS	0x20	/* this proc's xmin must be
 												 * included in vacuum horizons
 												 * in all databases */
+#define		PROC_IN_CONCURRENT_REPACK	0x40	/* REPACK (CONCURRENTLY) */
 
 /* flags reset at EOXact */
 #define		PROC_VACUUM_STATE_MASK \
diff --git a/src/test/modules/injection_points/Makefile b/src/test/modules/injection_points/Makefile
index 2cd7d87c533..f7663859fe2 100644
--- a/src/test/modules/injection_points/Makefile
+++ b/src/test/modules/injection_points/Makefile
@@ -15,6 +15,7 @@ REGRESS_OPTS = --dlpath=$(top_builddir)/src/test/regress
 ISOLATION = basic \
 	    inplace \
 	    repack \
+	    repack_deadlock \
 	    repack_toast \
 	    syscache-update-pruned \
 	    heap_lock_update
diff --git a/src/test/modules/injection_points/expected/repack_deadlock.out b/src/test/modules/injection_points/expected/repack_deadlock.out
new file mode 100644
index 00000000000..42505d10933
--- /dev/null
+++ b/src/test/modules/injection_points/expected/repack_deadlock.out
@@ -0,0 +1,63 @@
+Parsed test spec with 2 sessions
+
+starting permutation: wait_before_lock add_column wakeup_before_lock check1
+injection_points_attach
+-----------------------
+                       
+(1 row)
+
+step wait_before_lock: 
+	REPACK (CONCURRENTLY) repack_deadlock USING INDEX repack_deadlock_pkey;
+ <waiting ...>
+step add_column: 
+	alter table repack_deadlock add column noise text;
+ <waiting ...>
+step add_column: <... completed>
+ERROR:  oops, we're blocked by REPACK, time to die
+step wakeup_before_lock: 
+	SELECT injection_points_wakeup('repack-concurrently-before-lock');
+
+injection_points_wakeup
+-----------------------
+                       
+(1 row)
+
+step wait_before_lock: <... completed>
+step check1: 
+	INSERT INTO relfilenodes(node)
+	SELECT relfilenode FROM pg_class WHERE relname='repack_deadlock';
+
+	SELECT count(DISTINCT node) FROM relfilenodes;
+
+	SELECT i, j FROM repack_deadlock ORDER BY i, j;
+
+	INSERT INTO data_s1(i, j)
+	SELECT i, j FROM repack_deadlock;
+
+	SELECT count(*)
+	FROM data_s1 d1 FULL JOIN data_s2 d2 USING (i, j)
+	WHERE d1.i ISNULL OR d2.i ISNULL;
+
+count
+-----
+    1
+(1 row)
+
+i|j
+-+-
+1|1
+2|2
+3|3
+4|4
+(4 rows)
+
+count
+-----
+    4
+(1 row)
+
+injection_points_detach
+-----------------------
+                       
+(1 row)
+
diff --git a/src/test/modules/injection_points/meson.build b/src/test/modules/injection_points/meson.build
index a414abb924b..1cd88d6db65 100644
--- a/src/test/modules/injection_points/meson.build
+++ b/src/test/modules/injection_points/meson.build
@@ -46,6 +46,7 @@ tests += {
       'basic',
       'inplace',
       'repack',
+      'repack_deadlock',
       'repack_toast',
       'syscache-update-pruned',
       'heap_lock_update',
diff --git a/src/test/modules/injection_points/specs/repack_deadlock.spec b/src/test/modules/injection_points/specs/repack_deadlock.spec
new file mode 100644
index 00000000000..9d23a6588c2
--- /dev/null
+++ b/src/test/modules/injection_points/specs/repack_deadlock.spec
@@ -0,0 +1,83 @@
+# Test REPACK with a concurrent transaction that would cause a deadlock
+setup
+{
+	CREATE EXTENSION injection_points;
+
+	CREATE TABLE repack_deadlock(i int PRIMARY KEY, j int);
+	INSERT INTO repack_deadlock(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4);
+
+	CREATE TABLE relfilenodes(node oid);
+
+	CREATE TABLE data_s1(i int, j int);
+	CREATE TABLE data_s2(i int, j int);
+}
+
+teardown
+{
+	DROP TABLE repack_deadlock;
+	DROP EXTENSION injection_points;
+
+	DROP TABLE relfilenodes;
+	DROP TABLE data_s1;
+	DROP TABLE data_s2;
+}
+
+session s1
+setup
+{
+	SELECT injection_points_set_local();
+	SELECT injection_points_attach('repack-concurrently-before-lock', 'wait');
+}
+# Perform the initial load and wait for s2 to do some data changes.
+step wait_before_lock
+{
+	REPACK (CONCURRENTLY) repack_deadlock USING INDEX repack_deadlock_pkey;
+}
+# Check the table from the perspective of s1.
+#
+# Besides the contents, we also check that relfilenode has changed.
+
+# Have each session write the contents into a table and use FULL JOIN to check
+# if the outputs are identical.
+step check1
+{
+	INSERT INTO relfilenodes(node)
+	SELECT relfilenode FROM pg_class WHERE relname='repack_deadlock';
+
+	SELECT count(DISTINCT node) FROM relfilenodes;
+
+	SELECT i, j FROM repack_deadlock ORDER BY i, j;
+
+	INSERT INTO data_s1(i, j)
+	SELECT i, j FROM repack_deadlock;
+
+	SELECT count(*)
+	FROM data_s1 d1 FULL JOIN data_s2 d2 USING (i, j)
+	WHERE d1.i ISNULL OR d2.i ISNULL;
+}
+teardown
+{
+	SELECT injection_points_detach('repack-concurrently-before-lock');
+}
+
+session s2
+# Change the existing data. UPDATE changes both key and non-key columns. Also
+# update one row twice to test whether tuple version generated by this session
+# can be found.
+step add_column
+{
+	alter table repack_deadlock add column noise text;
+}
+
+step wakeup_before_lock
+{
+	SELECT injection_points_wakeup('repack-concurrently-before-lock');
+}
+
+# Test if data changes introduced while one session is performing REPACK
+# CONCURRENTLY find their way into the table.
+permutation
+	wait_before_lock
+	add_column
+	wakeup_before_lock
+	check1
-- 
2.47.3


--fot5plqmb33ey33c--





^ permalink  raw  reply  [nested|flat] 89+ messages in thread

* [PATCH v44b 4/4] hack deadlock detector
@ 2026-03-25 19:35  Álvaro Herrera <[email protected]>
  0 siblings, 0 replies; 89+ messages in thread

From: Álvaro Herrera @ 2026-03-25 19:35 UTC (permalink / raw)

---
 src/backend/commands/cluster.c                | 10 +++
 src/backend/storage/lmgr/deadlock.c           | 12 +++
 src/include/storage/proc.h                    |  1 +
 src/test/modules/injection_points/Makefile    |  1 +
 .../expected/repack_deadlock.out              | 63 ++++++++++++++
 src/test/modules/injection_points/meson.build |  1 +
 .../specs/repack_deadlock.spec                | 83 +++++++++++++++++++
 7 files changed, 171 insertions(+)
 create mode 100644 src/test/modules/injection_points/expected/repack_deadlock.out
 create mode 100644 src/test/modules/injection_points/specs/repack_deadlock.spec

diff --git a/src/backend/commands/cluster.c b/src/backend/commands/cluster.c
index 2c3058ba10d..d5b1dfbff69 100644
--- a/src/backend/commands/cluster.c
+++ b/src/backend/commands/cluster.c
@@ -372,6 +372,16 @@ ExecRepack(ParseState *pstate, RepackStmt *stmt, bool isTopLevel)
 	/* Determine the lock mode to use. */
 	lockmode = RepackLockLevel((params.options & CLUOPT_CONCURRENT) != 0);
 
+	/*
+	 * If in concurrent mode, set the PROC_IN_CONCURRENT_REPACK flag.  This
+	 * makes the deadlock checker cause anyone that would conflict with us
+	 * to error out.  It's important to set this flag ahead of actually locking
+	 * the relation; it won't of course affect anyone until we do have a lock
+	 * that others can conflict with.
+	 */
+	if ((params.options & CLUOPT_CONCURRENT) != 0)
+		MyProc->statusFlags |= PROC_IN_CONCURRENT_REPACK;
+
 	/*
 	 * If a single relation is specified, process it and we're done ... unless
 	 * the relation is a partitioned table, in which case we fall through.
diff --git a/src/backend/storage/lmgr/deadlock.c b/src/backend/storage/lmgr/deadlock.c
index b8962d875b6..10266e384aa 100644
--- a/src/backend/storage/lmgr/deadlock.c
+++ b/src/backend/storage/lmgr/deadlock.c
@@ -620,6 +620,18 @@ FindLockCycleRecurseMember(PGPROC *checkProc,
 						proc->statusFlags & PROC_IS_AUTOVACUUM)
 						blocking_autovacuum_proc = proc;
 
+					/*
+					 * If we note that we're blocked by some process running
+					 * REPACK (CONCURRENTLY), just fail.  That process is
+					 * going to upgrade its lock at some point, and it would
+					 * be inappropriate for any other process to cause that
+					 * to fail.
+					 */
+					if (checkProc == MyProc &&
+						proc->statusFlags & PROC_IN_CONCURRENT_REPACK)
+						/* not a serious error message proposal */
+						elog(ERROR, "oops, we're blocked by REPACK, time to die");
+
 					/* We're done looking at this proclock */
 					break;
 				}
diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h
index 1dad125706e..4e340ead455 100644
--- a/src/include/storage/proc.h
+++ b/src/include/storage/proc.h
@@ -69,6 +69,7 @@ struct XidCache
 #define		PROC_AFFECTS_ALL_HORIZONS	0x20	/* this proc's xmin must be
 												 * included in vacuum horizons
 												 * in all databases */
+#define		PROC_IN_CONCURRENT_REPACK	0x40	/* REPACK (CONCURRENTLY) */
 
 /* flags reset at EOXact */
 #define		PROC_VACUUM_STATE_MASK \
diff --git a/src/test/modules/injection_points/Makefile b/src/test/modules/injection_points/Makefile
index 2cd7d87c533..f7663859fe2 100644
--- a/src/test/modules/injection_points/Makefile
+++ b/src/test/modules/injection_points/Makefile
@@ -15,6 +15,7 @@ REGRESS_OPTS = --dlpath=$(top_builddir)/src/test/regress
 ISOLATION = basic \
 	    inplace \
 	    repack \
+	    repack_deadlock \
 	    repack_toast \
 	    syscache-update-pruned \
 	    heap_lock_update
diff --git a/src/test/modules/injection_points/expected/repack_deadlock.out b/src/test/modules/injection_points/expected/repack_deadlock.out
new file mode 100644
index 00000000000..42505d10933
--- /dev/null
+++ b/src/test/modules/injection_points/expected/repack_deadlock.out
@@ -0,0 +1,63 @@
+Parsed test spec with 2 sessions
+
+starting permutation: wait_before_lock add_column wakeup_before_lock check1
+injection_points_attach
+-----------------------
+                       
+(1 row)
+
+step wait_before_lock: 
+	REPACK (CONCURRENTLY) repack_deadlock USING INDEX repack_deadlock_pkey;
+ <waiting ...>
+step add_column: 
+	alter table repack_deadlock add column noise text;
+ <waiting ...>
+step add_column: <... completed>
+ERROR:  oops, we're blocked by REPACK, time to die
+step wakeup_before_lock: 
+	SELECT injection_points_wakeup('repack-concurrently-before-lock');
+
+injection_points_wakeup
+-----------------------
+                       
+(1 row)
+
+step wait_before_lock: <... completed>
+step check1: 
+	INSERT INTO relfilenodes(node)
+	SELECT relfilenode FROM pg_class WHERE relname='repack_deadlock';
+
+	SELECT count(DISTINCT node) FROM relfilenodes;
+
+	SELECT i, j FROM repack_deadlock ORDER BY i, j;
+
+	INSERT INTO data_s1(i, j)
+	SELECT i, j FROM repack_deadlock;
+
+	SELECT count(*)
+	FROM data_s1 d1 FULL JOIN data_s2 d2 USING (i, j)
+	WHERE d1.i ISNULL OR d2.i ISNULL;
+
+count
+-----
+    1
+(1 row)
+
+i|j
+-+-
+1|1
+2|2
+3|3
+4|4
+(4 rows)
+
+count
+-----
+    4
+(1 row)
+
+injection_points_detach
+-----------------------
+                       
+(1 row)
+
diff --git a/src/test/modules/injection_points/meson.build b/src/test/modules/injection_points/meson.build
index a414abb924b..1cd88d6db65 100644
--- a/src/test/modules/injection_points/meson.build
+++ b/src/test/modules/injection_points/meson.build
@@ -46,6 +46,7 @@ tests += {
       'basic',
       'inplace',
       'repack',
+      'repack_deadlock',
       'repack_toast',
       'syscache-update-pruned',
       'heap_lock_update',
diff --git a/src/test/modules/injection_points/specs/repack_deadlock.spec b/src/test/modules/injection_points/specs/repack_deadlock.spec
new file mode 100644
index 00000000000..9d23a6588c2
--- /dev/null
+++ b/src/test/modules/injection_points/specs/repack_deadlock.spec
@@ -0,0 +1,83 @@
+# Test REPACK with a concurrent transaction that would cause a deadlock
+setup
+{
+	CREATE EXTENSION injection_points;
+
+	CREATE TABLE repack_deadlock(i int PRIMARY KEY, j int);
+	INSERT INTO repack_deadlock(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4);
+
+	CREATE TABLE relfilenodes(node oid);
+
+	CREATE TABLE data_s1(i int, j int);
+	CREATE TABLE data_s2(i int, j int);
+}
+
+teardown
+{
+	DROP TABLE repack_deadlock;
+	DROP EXTENSION injection_points;
+
+	DROP TABLE relfilenodes;
+	DROP TABLE data_s1;
+	DROP TABLE data_s2;
+}
+
+session s1
+setup
+{
+	SELECT injection_points_set_local();
+	SELECT injection_points_attach('repack-concurrently-before-lock', 'wait');
+}
+# Perform the initial load and wait for s2 to do some data changes.
+step wait_before_lock
+{
+	REPACK (CONCURRENTLY) repack_deadlock USING INDEX repack_deadlock_pkey;
+}
+# Check the table from the perspective of s1.
+#
+# Besides the contents, we also check that relfilenode has changed.
+
+# Have each session write the contents into a table and use FULL JOIN to check
+# if the outputs are identical.
+step check1
+{
+	INSERT INTO relfilenodes(node)
+	SELECT relfilenode FROM pg_class WHERE relname='repack_deadlock';
+
+	SELECT count(DISTINCT node) FROM relfilenodes;
+
+	SELECT i, j FROM repack_deadlock ORDER BY i, j;
+
+	INSERT INTO data_s1(i, j)
+	SELECT i, j FROM repack_deadlock;
+
+	SELECT count(*)
+	FROM data_s1 d1 FULL JOIN data_s2 d2 USING (i, j)
+	WHERE d1.i ISNULL OR d2.i ISNULL;
+}
+teardown
+{
+	SELECT injection_points_detach('repack-concurrently-before-lock');
+}
+
+session s2
+# Change the existing data. UPDATE changes both key and non-key columns. Also
+# update one row twice to test whether tuple version generated by this session
+# can be found.
+step add_column
+{
+	alter table repack_deadlock add column noise text;
+}
+
+step wakeup_before_lock
+{
+	SELECT injection_points_wakeup('repack-concurrently-before-lock');
+}
+
+# Test if data changes introduced while one session is performing REPACK
+# CONCURRENTLY find their way into the table.
+permutation
+	wait_before_lock
+	add_column
+	wakeup_before_lock
+	check1
-- 
2.47.3


--fot5plqmb33ey33c--





^ permalink  raw  reply  [nested|flat] 89+ messages in thread

* [PATCH v44b 4/4] hack deadlock detector
@ 2026-03-25 19:35  Álvaro Herrera <[email protected]>
  0 siblings, 0 replies; 89+ messages in thread

From: Álvaro Herrera @ 2026-03-25 19:35 UTC (permalink / raw)

---
 src/backend/commands/cluster.c                | 10 +++
 src/backend/storage/lmgr/deadlock.c           | 12 +++
 src/include/storage/proc.h                    |  1 +
 src/test/modules/injection_points/Makefile    |  1 +
 .../expected/repack_deadlock.out              | 63 ++++++++++++++
 src/test/modules/injection_points/meson.build |  1 +
 .../specs/repack_deadlock.spec                | 83 +++++++++++++++++++
 7 files changed, 171 insertions(+)
 create mode 100644 src/test/modules/injection_points/expected/repack_deadlock.out
 create mode 100644 src/test/modules/injection_points/specs/repack_deadlock.spec

diff --git a/src/backend/commands/cluster.c b/src/backend/commands/cluster.c
index 2c3058ba10d..d5b1dfbff69 100644
--- a/src/backend/commands/cluster.c
+++ b/src/backend/commands/cluster.c
@@ -372,6 +372,16 @@ ExecRepack(ParseState *pstate, RepackStmt *stmt, bool isTopLevel)
 	/* Determine the lock mode to use. */
 	lockmode = RepackLockLevel((params.options & CLUOPT_CONCURRENT) != 0);
 
+	/*
+	 * If in concurrent mode, set the PROC_IN_CONCURRENT_REPACK flag.  This
+	 * makes the deadlock checker cause anyone that would conflict with us
+	 * to error out.  It's important to set this flag ahead of actually locking
+	 * the relation; it won't of course affect anyone until we do have a lock
+	 * that others can conflict with.
+	 */
+	if ((params.options & CLUOPT_CONCURRENT) != 0)
+		MyProc->statusFlags |= PROC_IN_CONCURRENT_REPACK;
+
 	/*
 	 * If a single relation is specified, process it and we're done ... unless
 	 * the relation is a partitioned table, in which case we fall through.
diff --git a/src/backend/storage/lmgr/deadlock.c b/src/backend/storage/lmgr/deadlock.c
index b8962d875b6..10266e384aa 100644
--- a/src/backend/storage/lmgr/deadlock.c
+++ b/src/backend/storage/lmgr/deadlock.c
@@ -620,6 +620,18 @@ FindLockCycleRecurseMember(PGPROC *checkProc,
 						proc->statusFlags & PROC_IS_AUTOVACUUM)
 						blocking_autovacuum_proc = proc;
 
+					/*
+					 * If we note that we're blocked by some process running
+					 * REPACK (CONCURRENTLY), just fail.  That process is
+					 * going to upgrade its lock at some point, and it would
+					 * be inappropriate for any other process to cause that
+					 * to fail.
+					 */
+					if (checkProc == MyProc &&
+						proc->statusFlags & PROC_IN_CONCURRENT_REPACK)
+						/* not a serious error message proposal */
+						elog(ERROR, "oops, we're blocked by REPACK, time to die");
+
 					/* We're done looking at this proclock */
 					break;
 				}
diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h
index 1dad125706e..4e340ead455 100644
--- a/src/include/storage/proc.h
+++ b/src/include/storage/proc.h
@@ -69,6 +69,7 @@ struct XidCache
 #define		PROC_AFFECTS_ALL_HORIZONS	0x20	/* this proc's xmin must be
 												 * included in vacuum horizons
 												 * in all databases */
+#define		PROC_IN_CONCURRENT_REPACK	0x40	/* REPACK (CONCURRENTLY) */
 
 /* flags reset at EOXact */
 #define		PROC_VACUUM_STATE_MASK \
diff --git a/src/test/modules/injection_points/Makefile b/src/test/modules/injection_points/Makefile
index 2cd7d87c533..f7663859fe2 100644
--- a/src/test/modules/injection_points/Makefile
+++ b/src/test/modules/injection_points/Makefile
@@ -15,6 +15,7 @@ REGRESS_OPTS = --dlpath=$(top_builddir)/src/test/regress
 ISOLATION = basic \
 	    inplace \
 	    repack \
+	    repack_deadlock \
 	    repack_toast \
 	    syscache-update-pruned \
 	    heap_lock_update
diff --git a/src/test/modules/injection_points/expected/repack_deadlock.out b/src/test/modules/injection_points/expected/repack_deadlock.out
new file mode 100644
index 00000000000..42505d10933
--- /dev/null
+++ b/src/test/modules/injection_points/expected/repack_deadlock.out
@@ -0,0 +1,63 @@
+Parsed test spec with 2 sessions
+
+starting permutation: wait_before_lock add_column wakeup_before_lock check1
+injection_points_attach
+-----------------------
+                       
+(1 row)
+
+step wait_before_lock: 
+	REPACK (CONCURRENTLY) repack_deadlock USING INDEX repack_deadlock_pkey;
+ <waiting ...>
+step add_column: 
+	alter table repack_deadlock add column noise text;
+ <waiting ...>
+step add_column: <... completed>
+ERROR:  oops, we're blocked by REPACK, time to die
+step wakeup_before_lock: 
+	SELECT injection_points_wakeup('repack-concurrently-before-lock');
+
+injection_points_wakeup
+-----------------------
+                       
+(1 row)
+
+step wait_before_lock: <... completed>
+step check1: 
+	INSERT INTO relfilenodes(node)
+	SELECT relfilenode FROM pg_class WHERE relname='repack_deadlock';
+
+	SELECT count(DISTINCT node) FROM relfilenodes;
+
+	SELECT i, j FROM repack_deadlock ORDER BY i, j;
+
+	INSERT INTO data_s1(i, j)
+	SELECT i, j FROM repack_deadlock;
+
+	SELECT count(*)
+	FROM data_s1 d1 FULL JOIN data_s2 d2 USING (i, j)
+	WHERE d1.i ISNULL OR d2.i ISNULL;
+
+count
+-----
+    1
+(1 row)
+
+i|j
+-+-
+1|1
+2|2
+3|3
+4|4
+(4 rows)
+
+count
+-----
+    4
+(1 row)
+
+injection_points_detach
+-----------------------
+                       
+(1 row)
+
diff --git a/src/test/modules/injection_points/meson.build b/src/test/modules/injection_points/meson.build
index a414abb924b..1cd88d6db65 100644
--- a/src/test/modules/injection_points/meson.build
+++ b/src/test/modules/injection_points/meson.build
@@ -46,6 +46,7 @@ tests += {
       'basic',
       'inplace',
       'repack',
+      'repack_deadlock',
       'repack_toast',
       'syscache-update-pruned',
       'heap_lock_update',
diff --git a/src/test/modules/injection_points/specs/repack_deadlock.spec b/src/test/modules/injection_points/specs/repack_deadlock.spec
new file mode 100644
index 00000000000..9d23a6588c2
--- /dev/null
+++ b/src/test/modules/injection_points/specs/repack_deadlock.spec
@@ -0,0 +1,83 @@
+# Test REPACK with a concurrent transaction that would cause a deadlock
+setup
+{
+	CREATE EXTENSION injection_points;
+
+	CREATE TABLE repack_deadlock(i int PRIMARY KEY, j int);
+	INSERT INTO repack_deadlock(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4);
+
+	CREATE TABLE relfilenodes(node oid);
+
+	CREATE TABLE data_s1(i int, j int);
+	CREATE TABLE data_s2(i int, j int);
+}
+
+teardown
+{
+	DROP TABLE repack_deadlock;
+	DROP EXTENSION injection_points;
+
+	DROP TABLE relfilenodes;
+	DROP TABLE data_s1;
+	DROP TABLE data_s2;
+}
+
+session s1
+setup
+{
+	SELECT injection_points_set_local();
+	SELECT injection_points_attach('repack-concurrently-before-lock', 'wait');
+}
+# Perform the initial load and wait for s2 to do some data changes.
+step wait_before_lock
+{
+	REPACK (CONCURRENTLY) repack_deadlock USING INDEX repack_deadlock_pkey;
+}
+# Check the table from the perspective of s1.
+#
+# Besides the contents, we also check that relfilenode has changed.
+
+# Have each session write the contents into a table and use FULL JOIN to check
+# if the outputs are identical.
+step check1
+{
+	INSERT INTO relfilenodes(node)
+	SELECT relfilenode FROM pg_class WHERE relname='repack_deadlock';
+
+	SELECT count(DISTINCT node) FROM relfilenodes;
+
+	SELECT i, j FROM repack_deadlock ORDER BY i, j;
+
+	INSERT INTO data_s1(i, j)
+	SELECT i, j FROM repack_deadlock;
+
+	SELECT count(*)
+	FROM data_s1 d1 FULL JOIN data_s2 d2 USING (i, j)
+	WHERE d1.i ISNULL OR d2.i ISNULL;
+}
+teardown
+{
+	SELECT injection_points_detach('repack-concurrently-before-lock');
+}
+
+session s2
+# Change the existing data. UPDATE changes both key and non-key columns. Also
+# update one row twice to test whether tuple version generated by this session
+# can be found.
+step add_column
+{
+	alter table repack_deadlock add column noise text;
+}
+
+step wakeup_before_lock
+{
+	SELECT injection_points_wakeup('repack-concurrently-before-lock');
+}
+
+# Test if data changes introduced while one session is performing REPACK
+# CONCURRENTLY find their way into the table.
+permutation
+	wait_before_lock
+	add_column
+	wakeup_before_lock
+	check1
-- 
2.47.3


--fot5plqmb33ey33c--





^ permalink  raw  reply  [nested|flat] 89+ messages in thread

* [PATCH v44b 4/4] hack deadlock detector
@ 2026-03-25 19:35  Álvaro Herrera <[email protected]>
  0 siblings, 0 replies; 89+ messages in thread

From: Álvaro Herrera @ 2026-03-25 19:35 UTC (permalink / raw)

---
 src/backend/commands/cluster.c                | 10 +++
 src/backend/storage/lmgr/deadlock.c           | 12 +++
 src/include/storage/proc.h                    |  1 +
 src/test/modules/injection_points/Makefile    |  1 +
 .../expected/repack_deadlock.out              | 63 ++++++++++++++
 src/test/modules/injection_points/meson.build |  1 +
 .../specs/repack_deadlock.spec                | 83 +++++++++++++++++++
 7 files changed, 171 insertions(+)
 create mode 100644 src/test/modules/injection_points/expected/repack_deadlock.out
 create mode 100644 src/test/modules/injection_points/specs/repack_deadlock.spec

diff --git a/src/backend/commands/cluster.c b/src/backend/commands/cluster.c
index 2c3058ba10d..d5b1dfbff69 100644
--- a/src/backend/commands/cluster.c
+++ b/src/backend/commands/cluster.c
@@ -372,6 +372,16 @@ ExecRepack(ParseState *pstate, RepackStmt *stmt, bool isTopLevel)
 	/* Determine the lock mode to use. */
 	lockmode = RepackLockLevel((params.options & CLUOPT_CONCURRENT) != 0);
 
+	/*
+	 * If in concurrent mode, set the PROC_IN_CONCURRENT_REPACK flag.  This
+	 * makes the deadlock checker cause anyone that would conflict with us
+	 * to error out.  It's important to set this flag ahead of actually locking
+	 * the relation; it won't of course affect anyone until we do have a lock
+	 * that others can conflict with.
+	 */
+	if ((params.options & CLUOPT_CONCURRENT) != 0)
+		MyProc->statusFlags |= PROC_IN_CONCURRENT_REPACK;
+
 	/*
 	 * If a single relation is specified, process it and we're done ... unless
 	 * the relation is a partitioned table, in which case we fall through.
diff --git a/src/backend/storage/lmgr/deadlock.c b/src/backend/storage/lmgr/deadlock.c
index b8962d875b6..10266e384aa 100644
--- a/src/backend/storage/lmgr/deadlock.c
+++ b/src/backend/storage/lmgr/deadlock.c
@@ -620,6 +620,18 @@ FindLockCycleRecurseMember(PGPROC *checkProc,
 						proc->statusFlags & PROC_IS_AUTOVACUUM)
 						blocking_autovacuum_proc = proc;
 
+					/*
+					 * If we note that we're blocked by some process running
+					 * REPACK (CONCURRENTLY), just fail.  That process is
+					 * going to upgrade its lock at some point, and it would
+					 * be inappropriate for any other process to cause that
+					 * to fail.
+					 */
+					if (checkProc == MyProc &&
+						proc->statusFlags & PROC_IN_CONCURRENT_REPACK)
+						/* not a serious error message proposal */
+						elog(ERROR, "oops, we're blocked by REPACK, time to die");
+
 					/* We're done looking at this proclock */
 					break;
 				}
diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h
index 1dad125706e..4e340ead455 100644
--- a/src/include/storage/proc.h
+++ b/src/include/storage/proc.h
@@ -69,6 +69,7 @@ struct XidCache
 #define		PROC_AFFECTS_ALL_HORIZONS	0x20	/* this proc's xmin must be
 												 * included in vacuum horizons
 												 * in all databases */
+#define		PROC_IN_CONCURRENT_REPACK	0x40	/* REPACK (CONCURRENTLY) */
 
 /* flags reset at EOXact */
 #define		PROC_VACUUM_STATE_MASK \
diff --git a/src/test/modules/injection_points/Makefile b/src/test/modules/injection_points/Makefile
index 2cd7d87c533..f7663859fe2 100644
--- a/src/test/modules/injection_points/Makefile
+++ b/src/test/modules/injection_points/Makefile
@@ -15,6 +15,7 @@ REGRESS_OPTS = --dlpath=$(top_builddir)/src/test/regress
 ISOLATION = basic \
 	    inplace \
 	    repack \
+	    repack_deadlock \
 	    repack_toast \
 	    syscache-update-pruned \
 	    heap_lock_update
diff --git a/src/test/modules/injection_points/expected/repack_deadlock.out b/src/test/modules/injection_points/expected/repack_deadlock.out
new file mode 100644
index 00000000000..42505d10933
--- /dev/null
+++ b/src/test/modules/injection_points/expected/repack_deadlock.out
@@ -0,0 +1,63 @@
+Parsed test spec with 2 sessions
+
+starting permutation: wait_before_lock add_column wakeup_before_lock check1
+injection_points_attach
+-----------------------
+                       
+(1 row)
+
+step wait_before_lock: 
+	REPACK (CONCURRENTLY) repack_deadlock USING INDEX repack_deadlock_pkey;
+ <waiting ...>
+step add_column: 
+	alter table repack_deadlock add column noise text;
+ <waiting ...>
+step add_column: <... completed>
+ERROR:  oops, we're blocked by REPACK, time to die
+step wakeup_before_lock: 
+	SELECT injection_points_wakeup('repack-concurrently-before-lock');
+
+injection_points_wakeup
+-----------------------
+                       
+(1 row)
+
+step wait_before_lock: <... completed>
+step check1: 
+	INSERT INTO relfilenodes(node)
+	SELECT relfilenode FROM pg_class WHERE relname='repack_deadlock';
+
+	SELECT count(DISTINCT node) FROM relfilenodes;
+
+	SELECT i, j FROM repack_deadlock ORDER BY i, j;
+
+	INSERT INTO data_s1(i, j)
+	SELECT i, j FROM repack_deadlock;
+
+	SELECT count(*)
+	FROM data_s1 d1 FULL JOIN data_s2 d2 USING (i, j)
+	WHERE d1.i ISNULL OR d2.i ISNULL;
+
+count
+-----
+    1
+(1 row)
+
+i|j
+-+-
+1|1
+2|2
+3|3
+4|4
+(4 rows)
+
+count
+-----
+    4
+(1 row)
+
+injection_points_detach
+-----------------------
+                       
+(1 row)
+
diff --git a/src/test/modules/injection_points/meson.build b/src/test/modules/injection_points/meson.build
index a414abb924b..1cd88d6db65 100644
--- a/src/test/modules/injection_points/meson.build
+++ b/src/test/modules/injection_points/meson.build
@@ -46,6 +46,7 @@ tests += {
       'basic',
       'inplace',
       'repack',
+      'repack_deadlock',
       'repack_toast',
       'syscache-update-pruned',
       'heap_lock_update',
diff --git a/src/test/modules/injection_points/specs/repack_deadlock.spec b/src/test/modules/injection_points/specs/repack_deadlock.spec
new file mode 100644
index 00000000000..9d23a6588c2
--- /dev/null
+++ b/src/test/modules/injection_points/specs/repack_deadlock.spec
@@ -0,0 +1,83 @@
+# Test REPACK with a concurrent transaction that would cause a deadlock
+setup
+{
+	CREATE EXTENSION injection_points;
+
+	CREATE TABLE repack_deadlock(i int PRIMARY KEY, j int);
+	INSERT INTO repack_deadlock(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4);
+
+	CREATE TABLE relfilenodes(node oid);
+
+	CREATE TABLE data_s1(i int, j int);
+	CREATE TABLE data_s2(i int, j int);
+}
+
+teardown
+{
+	DROP TABLE repack_deadlock;
+	DROP EXTENSION injection_points;
+
+	DROP TABLE relfilenodes;
+	DROP TABLE data_s1;
+	DROP TABLE data_s2;
+}
+
+session s1
+setup
+{
+	SELECT injection_points_set_local();
+	SELECT injection_points_attach('repack-concurrently-before-lock', 'wait');
+}
+# Perform the initial load and wait for s2 to do some data changes.
+step wait_before_lock
+{
+	REPACK (CONCURRENTLY) repack_deadlock USING INDEX repack_deadlock_pkey;
+}
+# Check the table from the perspective of s1.
+#
+# Besides the contents, we also check that relfilenode has changed.
+
+# Have each session write the contents into a table and use FULL JOIN to check
+# if the outputs are identical.
+step check1
+{
+	INSERT INTO relfilenodes(node)
+	SELECT relfilenode FROM pg_class WHERE relname='repack_deadlock';
+
+	SELECT count(DISTINCT node) FROM relfilenodes;
+
+	SELECT i, j FROM repack_deadlock ORDER BY i, j;
+
+	INSERT INTO data_s1(i, j)
+	SELECT i, j FROM repack_deadlock;
+
+	SELECT count(*)
+	FROM data_s1 d1 FULL JOIN data_s2 d2 USING (i, j)
+	WHERE d1.i ISNULL OR d2.i ISNULL;
+}
+teardown
+{
+	SELECT injection_points_detach('repack-concurrently-before-lock');
+}
+
+session s2
+# Change the existing data. UPDATE changes both key and non-key columns. Also
+# update one row twice to test whether tuple version generated by this session
+# can be found.
+step add_column
+{
+	alter table repack_deadlock add column noise text;
+}
+
+step wakeup_before_lock
+{
+	SELECT injection_points_wakeup('repack-concurrently-before-lock');
+}
+
+# Test if data changes introduced while one session is performing REPACK
+# CONCURRENTLY find their way into the table.
+permutation
+	wait_before_lock
+	add_column
+	wakeup_before_lock
+	check1
-- 
2.47.3


--fot5plqmb33ey33c--





^ permalink  raw  reply  [nested|flat] 89+ messages in thread

* [PATCH v44b 4/4] hack deadlock detector
@ 2026-03-25 19:35  Álvaro Herrera <[email protected]>
  0 siblings, 0 replies; 89+ messages in thread

From: Álvaro Herrera @ 2026-03-25 19:35 UTC (permalink / raw)

---
 src/backend/commands/cluster.c                | 10 +++
 src/backend/storage/lmgr/deadlock.c           | 12 +++
 src/include/storage/proc.h                    |  1 +
 src/test/modules/injection_points/Makefile    |  1 +
 .../expected/repack_deadlock.out              | 63 ++++++++++++++
 src/test/modules/injection_points/meson.build |  1 +
 .../specs/repack_deadlock.spec                | 83 +++++++++++++++++++
 7 files changed, 171 insertions(+)
 create mode 100644 src/test/modules/injection_points/expected/repack_deadlock.out
 create mode 100644 src/test/modules/injection_points/specs/repack_deadlock.spec

diff --git a/src/backend/commands/cluster.c b/src/backend/commands/cluster.c
index 2c3058ba10d..d5b1dfbff69 100644
--- a/src/backend/commands/cluster.c
+++ b/src/backend/commands/cluster.c
@@ -372,6 +372,16 @@ ExecRepack(ParseState *pstate, RepackStmt *stmt, bool isTopLevel)
 	/* Determine the lock mode to use. */
 	lockmode = RepackLockLevel((params.options & CLUOPT_CONCURRENT) != 0);
 
+	/*
+	 * If in concurrent mode, set the PROC_IN_CONCURRENT_REPACK flag.  This
+	 * makes the deadlock checker cause anyone that would conflict with us
+	 * to error out.  It's important to set this flag ahead of actually locking
+	 * the relation; it won't of course affect anyone until we do have a lock
+	 * that others can conflict with.
+	 */
+	if ((params.options & CLUOPT_CONCURRENT) != 0)
+		MyProc->statusFlags |= PROC_IN_CONCURRENT_REPACK;
+
 	/*
 	 * If a single relation is specified, process it and we're done ... unless
 	 * the relation is a partitioned table, in which case we fall through.
diff --git a/src/backend/storage/lmgr/deadlock.c b/src/backend/storage/lmgr/deadlock.c
index b8962d875b6..10266e384aa 100644
--- a/src/backend/storage/lmgr/deadlock.c
+++ b/src/backend/storage/lmgr/deadlock.c
@@ -620,6 +620,18 @@ FindLockCycleRecurseMember(PGPROC *checkProc,
 						proc->statusFlags & PROC_IS_AUTOVACUUM)
 						blocking_autovacuum_proc = proc;
 
+					/*
+					 * If we note that we're blocked by some process running
+					 * REPACK (CONCURRENTLY), just fail.  That process is
+					 * going to upgrade its lock at some point, and it would
+					 * be inappropriate for any other process to cause that
+					 * to fail.
+					 */
+					if (checkProc == MyProc &&
+						proc->statusFlags & PROC_IN_CONCURRENT_REPACK)
+						/* not a serious error message proposal */
+						elog(ERROR, "oops, we're blocked by REPACK, time to die");
+
 					/* We're done looking at this proclock */
 					break;
 				}
diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h
index 1dad125706e..4e340ead455 100644
--- a/src/include/storage/proc.h
+++ b/src/include/storage/proc.h
@@ -69,6 +69,7 @@ struct XidCache
 #define		PROC_AFFECTS_ALL_HORIZONS	0x20	/* this proc's xmin must be
 												 * included in vacuum horizons
 												 * in all databases */
+#define		PROC_IN_CONCURRENT_REPACK	0x40	/* REPACK (CONCURRENTLY) */
 
 /* flags reset at EOXact */
 #define		PROC_VACUUM_STATE_MASK \
diff --git a/src/test/modules/injection_points/Makefile b/src/test/modules/injection_points/Makefile
index 2cd7d87c533..f7663859fe2 100644
--- a/src/test/modules/injection_points/Makefile
+++ b/src/test/modules/injection_points/Makefile
@@ -15,6 +15,7 @@ REGRESS_OPTS = --dlpath=$(top_builddir)/src/test/regress
 ISOLATION = basic \
 	    inplace \
 	    repack \
+	    repack_deadlock \
 	    repack_toast \
 	    syscache-update-pruned \
 	    heap_lock_update
diff --git a/src/test/modules/injection_points/expected/repack_deadlock.out b/src/test/modules/injection_points/expected/repack_deadlock.out
new file mode 100644
index 00000000000..42505d10933
--- /dev/null
+++ b/src/test/modules/injection_points/expected/repack_deadlock.out
@@ -0,0 +1,63 @@
+Parsed test spec with 2 sessions
+
+starting permutation: wait_before_lock add_column wakeup_before_lock check1
+injection_points_attach
+-----------------------
+                       
+(1 row)
+
+step wait_before_lock: 
+	REPACK (CONCURRENTLY) repack_deadlock USING INDEX repack_deadlock_pkey;
+ <waiting ...>
+step add_column: 
+	alter table repack_deadlock add column noise text;
+ <waiting ...>
+step add_column: <... completed>
+ERROR:  oops, we're blocked by REPACK, time to die
+step wakeup_before_lock: 
+	SELECT injection_points_wakeup('repack-concurrently-before-lock');
+
+injection_points_wakeup
+-----------------------
+                       
+(1 row)
+
+step wait_before_lock: <... completed>
+step check1: 
+	INSERT INTO relfilenodes(node)
+	SELECT relfilenode FROM pg_class WHERE relname='repack_deadlock';
+
+	SELECT count(DISTINCT node) FROM relfilenodes;
+
+	SELECT i, j FROM repack_deadlock ORDER BY i, j;
+
+	INSERT INTO data_s1(i, j)
+	SELECT i, j FROM repack_deadlock;
+
+	SELECT count(*)
+	FROM data_s1 d1 FULL JOIN data_s2 d2 USING (i, j)
+	WHERE d1.i ISNULL OR d2.i ISNULL;
+
+count
+-----
+    1
+(1 row)
+
+i|j
+-+-
+1|1
+2|2
+3|3
+4|4
+(4 rows)
+
+count
+-----
+    4
+(1 row)
+
+injection_points_detach
+-----------------------
+                       
+(1 row)
+
diff --git a/src/test/modules/injection_points/meson.build b/src/test/modules/injection_points/meson.build
index a414abb924b..1cd88d6db65 100644
--- a/src/test/modules/injection_points/meson.build
+++ b/src/test/modules/injection_points/meson.build
@@ -46,6 +46,7 @@ tests += {
       'basic',
       'inplace',
       'repack',
+      'repack_deadlock',
       'repack_toast',
       'syscache-update-pruned',
       'heap_lock_update',
diff --git a/src/test/modules/injection_points/specs/repack_deadlock.spec b/src/test/modules/injection_points/specs/repack_deadlock.spec
new file mode 100644
index 00000000000..9d23a6588c2
--- /dev/null
+++ b/src/test/modules/injection_points/specs/repack_deadlock.spec
@@ -0,0 +1,83 @@
+# Test REPACK with a concurrent transaction that would cause a deadlock
+setup
+{
+	CREATE EXTENSION injection_points;
+
+	CREATE TABLE repack_deadlock(i int PRIMARY KEY, j int);
+	INSERT INTO repack_deadlock(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4);
+
+	CREATE TABLE relfilenodes(node oid);
+
+	CREATE TABLE data_s1(i int, j int);
+	CREATE TABLE data_s2(i int, j int);
+}
+
+teardown
+{
+	DROP TABLE repack_deadlock;
+	DROP EXTENSION injection_points;
+
+	DROP TABLE relfilenodes;
+	DROP TABLE data_s1;
+	DROP TABLE data_s2;
+}
+
+session s1
+setup
+{
+	SELECT injection_points_set_local();
+	SELECT injection_points_attach('repack-concurrently-before-lock', 'wait');
+}
+# Perform the initial load and wait for s2 to do some data changes.
+step wait_before_lock
+{
+	REPACK (CONCURRENTLY) repack_deadlock USING INDEX repack_deadlock_pkey;
+}
+# Check the table from the perspective of s1.
+#
+# Besides the contents, we also check that relfilenode has changed.
+
+# Have each session write the contents into a table and use FULL JOIN to check
+# if the outputs are identical.
+step check1
+{
+	INSERT INTO relfilenodes(node)
+	SELECT relfilenode FROM pg_class WHERE relname='repack_deadlock';
+
+	SELECT count(DISTINCT node) FROM relfilenodes;
+
+	SELECT i, j FROM repack_deadlock ORDER BY i, j;
+
+	INSERT INTO data_s1(i, j)
+	SELECT i, j FROM repack_deadlock;
+
+	SELECT count(*)
+	FROM data_s1 d1 FULL JOIN data_s2 d2 USING (i, j)
+	WHERE d1.i ISNULL OR d2.i ISNULL;
+}
+teardown
+{
+	SELECT injection_points_detach('repack-concurrently-before-lock');
+}
+
+session s2
+# Change the existing data. UPDATE changes both key and non-key columns. Also
+# update one row twice to test whether tuple version generated by this session
+# can be found.
+step add_column
+{
+	alter table repack_deadlock add column noise text;
+}
+
+step wakeup_before_lock
+{
+	SELECT injection_points_wakeup('repack-concurrently-before-lock');
+}
+
+# Test if data changes introduced while one session is performing REPACK
+# CONCURRENTLY find their way into the table.
+permutation
+	wait_before_lock
+	add_column
+	wakeup_before_lock
+	check1
-- 
2.47.3


--fot5plqmb33ey33c--





^ permalink  raw  reply  [nested|flat] 89+ messages in thread

* [PATCH v44b 4/4] hack deadlock detector
@ 2026-03-25 19:35  Álvaro Herrera <[email protected]>
  0 siblings, 0 replies; 89+ messages in thread

From: Álvaro Herrera @ 2026-03-25 19:35 UTC (permalink / raw)

---
 src/backend/commands/cluster.c                | 10 +++
 src/backend/storage/lmgr/deadlock.c           | 12 +++
 src/include/storage/proc.h                    |  1 +
 src/test/modules/injection_points/Makefile    |  1 +
 .../expected/repack_deadlock.out              | 63 ++++++++++++++
 src/test/modules/injection_points/meson.build |  1 +
 .../specs/repack_deadlock.spec                | 83 +++++++++++++++++++
 7 files changed, 171 insertions(+)
 create mode 100644 src/test/modules/injection_points/expected/repack_deadlock.out
 create mode 100644 src/test/modules/injection_points/specs/repack_deadlock.spec

diff --git a/src/backend/commands/cluster.c b/src/backend/commands/cluster.c
index 2c3058ba10d..d5b1dfbff69 100644
--- a/src/backend/commands/cluster.c
+++ b/src/backend/commands/cluster.c
@@ -372,6 +372,16 @@ ExecRepack(ParseState *pstate, RepackStmt *stmt, bool isTopLevel)
 	/* Determine the lock mode to use. */
 	lockmode = RepackLockLevel((params.options & CLUOPT_CONCURRENT) != 0);
 
+	/*
+	 * If in concurrent mode, set the PROC_IN_CONCURRENT_REPACK flag.  This
+	 * makes the deadlock checker cause anyone that would conflict with us
+	 * to error out.  It's important to set this flag ahead of actually locking
+	 * the relation; it won't of course affect anyone until we do have a lock
+	 * that others can conflict with.
+	 */
+	if ((params.options & CLUOPT_CONCURRENT) != 0)
+		MyProc->statusFlags |= PROC_IN_CONCURRENT_REPACK;
+
 	/*
 	 * If a single relation is specified, process it and we're done ... unless
 	 * the relation is a partitioned table, in which case we fall through.
diff --git a/src/backend/storage/lmgr/deadlock.c b/src/backend/storage/lmgr/deadlock.c
index b8962d875b6..10266e384aa 100644
--- a/src/backend/storage/lmgr/deadlock.c
+++ b/src/backend/storage/lmgr/deadlock.c
@@ -620,6 +620,18 @@ FindLockCycleRecurseMember(PGPROC *checkProc,
 						proc->statusFlags & PROC_IS_AUTOVACUUM)
 						blocking_autovacuum_proc = proc;
 
+					/*
+					 * If we note that we're blocked by some process running
+					 * REPACK (CONCURRENTLY), just fail.  That process is
+					 * going to upgrade its lock at some point, and it would
+					 * be inappropriate for any other process to cause that
+					 * to fail.
+					 */
+					if (checkProc == MyProc &&
+						proc->statusFlags & PROC_IN_CONCURRENT_REPACK)
+						/* not a serious error message proposal */
+						elog(ERROR, "oops, we're blocked by REPACK, time to die");
+
 					/* We're done looking at this proclock */
 					break;
 				}
diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h
index 1dad125706e..4e340ead455 100644
--- a/src/include/storage/proc.h
+++ b/src/include/storage/proc.h
@@ -69,6 +69,7 @@ struct XidCache
 #define		PROC_AFFECTS_ALL_HORIZONS	0x20	/* this proc's xmin must be
 												 * included in vacuum horizons
 												 * in all databases */
+#define		PROC_IN_CONCURRENT_REPACK	0x40	/* REPACK (CONCURRENTLY) */
 
 /* flags reset at EOXact */
 #define		PROC_VACUUM_STATE_MASK \
diff --git a/src/test/modules/injection_points/Makefile b/src/test/modules/injection_points/Makefile
index 2cd7d87c533..f7663859fe2 100644
--- a/src/test/modules/injection_points/Makefile
+++ b/src/test/modules/injection_points/Makefile
@@ -15,6 +15,7 @@ REGRESS_OPTS = --dlpath=$(top_builddir)/src/test/regress
 ISOLATION = basic \
 	    inplace \
 	    repack \
+	    repack_deadlock \
 	    repack_toast \
 	    syscache-update-pruned \
 	    heap_lock_update
diff --git a/src/test/modules/injection_points/expected/repack_deadlock.out b/src/test/modules/injection_points/expected/repack_deadlock.out
new file mode 100644
index 00000000000..42505d10933
--- /dev/null
+++ b/src/test/modules/injection_points/expected/repack_deadlock.out
@@ -0,0 +1,63 @@
+Parsed test spec with 2 sessions
+
+starting permutation: wait_before_lock add_column wakeup_before_lock check1
+injection_points_attach
+-----------------------
+                       
+(1 row)
+
+step wait_before_lock: 
+	REPACK (CONCURRENTLY) repack_deadlock USING INDEX repack_deadlock_pkey;
+ <waiting ...>
+step add_column: 
+	alter table repack_deadlock add column noise text;
+ <waiting ...>
+step add_column: <... completed>
+ERROR:  oops, we're blocked by REPACK, time to die
+step wakeup_before_lock: 
+	SELECT injection_points_wakeup('repack-concurrently-before-lock');
+
+injection_points_wakeup
+-----------------------
+                       
+(1 row)
+
+step wait_before_lock: <... completed>
+step check1: 
+	INSERT INTO relfilenodes(node)
+	SELECT relfilenode FROM pg_class WHERE relname='repack_deadlock';
+
+	SELECT count(DISTINCT node) FROM relfilenodes;
+
+	SELECT i, j FROM repack_deadlock ORDER BY i, j;
+
+	INSERT INTO data_s1(i, j)
+	SELECT i, j FROM repack_deadlock;
+
+	SELECT count(*)
+	FROM data_s1 d1 FULL JOIN data_s2 d2 USING (i, j)
+	WHERE d1.i ISNULL OR d2.i ISNULL;
+
+count
+-----
+    1
+(1 row)
+
+i|j
+-+-
+1|1
+2|2
+3|3
+4|4
+(4 rows)
+
+count
+-----
+    4
+(1 row)
+
+injection_points_detach
+-----------------------
+                       
+(1 row)
+
diff --git a/src/test/modules/injection_points/meson.build b/src/test/modules/injection_points/meson.build
index a414abb924b..1cd88d6db65 100644
--- a/src/test/modules/injection_points/meson.build
+++ b/src/test/modules/injection_points/meson.build
@@ -46,6 +46,7 @@ tests += {
       'basic',
       'inplace',
       'repack',
+      'repack_deadlock',
       'repack_toast',
       'syscache-update-pruned',
       'heap_lock_update',
diff --git a/src/test/modules/injection_points/specs/repack_deadlock.spec b/src/test/modules/injection_points/specs/repack_deadlock.spec
new file mode 100644
index 00000000000..9d23a6588c2
--- /dev/null
+++ b/src/test/modules/injection_points/specs/repack_deadlock.spec
@@ -0,0 +1,83 @@
+# Test REPACK with a concurrent transaction that would cause a deadlock
+setup
+{
+	CREATE EXTENSION injection_points;
+
+	CREATE TABLE repack_deadlock(i int PRIMARY KEY, j int);
+	INSERT INTO repack_deadlock(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4);
+
+	CREATE TABLE relfilenodes(node oid);
+
+	CREATE TABLE data_s1(i int, j int);
+	CREATE TABLE data_s2(i int, j int);
+}
+
+teardown
+{
+	DROP TABLE repack_deadlock;
+	DROP EXTENSION injection_points;
+
+	DROP TABLE relfilenodes;
+	DROP TABLE data_s1;
+	DROP TABLE data_s2;
+}
+
+session s1
+setup
+{
+	SELECT injection_points_set_local();
+	SELECT injection_points_attach('repack-concurrently-before-lock', 'wait');
+}
+# Perform the initial load and wait for s2 to do some data changes.
+step wait_before_lock
+{
+	REPACK (CONCURRENTLY) repack_deadlock USING INDEX repack_deadlock_pkey;
+}
+# Check the table from the perspective of s1.
+#
+# Besides the contents, we also check that relfilenode has changed.
+
+# Have each session write the contents into a table and use FULL JOIN to check
+# if the outputs are identical.
+step check1
+{
+	INSERT INTO relfilenodes(node)
+	SELECT relfilenode FROM pg_class WHERE relname='repack_deadlock';
+
+	SELECT count(DISTINCT node) FROM relfilenodes;
+
+	SELECT i, j FROM repack_deadlock ORDER BY i, j;
+
+	INSERT INTO data_s1(i, j)
+	SELECT i, j FROM repack_deadlock;
+
+	SELECT count(*)
+	FROM data_s1 d1 FULL JOIN data_s2 d2 USING (i, j)
+	WHERE d1.i ISNULL OR d2.i ISNULL;
+}
+teardown
+{
+	SELECT injection_points_detach('repack-concurrently-before-lock');
+}
+
+session s2
+# Change the existing data. UPDATE changes both key and non-key columns. Also
+# update one row twice to test whether tuple version generated by this session
+# can be found.
+step add_column
+{
+	alter table repack_deadlock add column noise text;
+}
+
+step wakeup_before_lock
+{
+	SELECT injection_points_wakeup('repack-concurrently-before-lock');
+}
+
+# Test if data changes introduced while one session is performing REPACK
+# CONCURRENTLY find their way into the table.
+permutation
+	wait_before_lock
+	add_column
+	wakeup_before_lock
+	check1
-- 
2.47.3


--fot5plqmb33ey33c--





^ permalink  raw  reply  [nested|flat] 89+ messages in thread

* [PATCH v44b 4/4] hack deadlock detector
@ 2026-03-25 19:35  Álvaro Herrera <[email protected]>
  0 siblings, 0 replies; 89+ messages in thread

From: Álvaro Herrera @ 2026-03-25 19:35 UTC (permalink / raw)

---
 src/backend/commands/cluster.c                | 10 +++
 src/backend/storage/lmgr/deadlock.c           | 12 +++
 src/include/storage/proc.h                    |  1 +
 src/test/modules/injection_points/Makefile    |  1 +
 .../expected/repack_deadlock.out              | 63 ++++++++++++++
 src/test/modules/injection_points/meson.build |  1 +
 .../specs/repack_deadlock.spec                | 83 +++++++++++++++++++
 7 files changed, 171 insertions(+)
 create mode 100644 src/test/modules/injection_points/expected/repack_deadlock.out
 create mode 100644 src/test/modules/injection_points/specs/repack_deadlock.spec

diff --git a/src/backend/commands/cluster.c b/src/backend/commands/cluster.c
index 2c3058ba10d..d5b1dfbff69 100644
--- a/src/backend/commands/cluster.c
+++ b/src/backend/commands/cluster.c
@@ -372,6 +372,16 @@ ExecRepack(ParseState *pstate, RepackStmt *stmt, bool isTopLevel)
 	/* Determine the lock mode to use. */
 	lockmode = RepackLockLevel((params.options & CLUOPT_CONCURRENT) != 0);
 
+	/*
+	 * If in concurrent mode, set the PROC_IN_CONCURRENT_REPACK flag.  This
+	 * makes the deadlock checker cause anyone that would conflict with us
+	 * to error out.  It's important to set this flag ahead of actually locking
+	 * the relation; it won't of course affect anyone until we do have a lock
+	 * that others can conflict with.
+	 */
+	if ((params.options & CLUOPT_CONCURRENT) != 0)
+		MyProc->statusFlags |= PROC_IN_CONCURRENT_REPACK;
+
 	/*
 	 * If a single relation is specified, process it and we're done ... unless
 	 * the relation is a partitioned table, in which case we fall through.
diff --git a/src/backend/storage/lmgr/deadlock.c b/src/backend/storage/lmgr/deadlock.c
index b8962d875b6..10266e384aa 100644
--- a/src/backend/storage/lmgr/deadlock.c
+++ b/src/backend/storage/lmgr/deadlock.c
@@ -620,6 +620,18 @@ FindLockCycleRecurseMember(PGPROC *checkProc,
 						proc->statusFlags & PROC_IS_AUTOVACUUM)
 						blocking_autovacuum_proc = proc;
 
+					/*
+					 * If we note that we're blocked by some process running
+					 * REPACK (CONCURRENTLY), just fail.  That process is
+					 * going to upgrade its lock at some point, and it would
+					 * be inappropriate for any other process to cause that
+					 * to fail.
+					 */
+					if (checkProc == MyProc &&
+						proc->statusFlags & PROC_IN_CONCURRENT_REPACK)
+						/* not a serious error message proposal */
+						elog(ERROR, "oops, we're blocked by REPACK, time to die");
+
 					/* We're done looking at this proclock */
 					break;
 				}
diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h
index 1dad125706e..4e340ead455 100644
--- a/src/include/storage/proc.h
+++ b/src/include/storage/proc.h
@@ -69,6 +69,7 @@ struct XidCache
 #define		PROC_AFFECTS_ALL_HORIZONS	0x20	/* this proc's xmin must be
 												 * included in vacuum horizons
 												 * in all databases */
+#define		PROC_IN_CONCURRENT_REPACK	0x40	/* REPACK (CONCURRENTLY) */
 
 /* flags reset at EOXact */
 #define		PROC_VACUUM_STATE_MASK \
diff --git a/src/test/modules/injection_points/Makefile b/src/test/modules/injection_points/Makefile
index 2cd7d87c533..f7663859fe2 100644
--- a/src/test/modules/injection_points/Makefile
+++ b/src/test/modules/injection_points/Makefile
@@ -15,6 +15,7 @@ REGRESS_OPTS = --dlpath=$(top_builddir)/src/test/regress
 ISOLATION = basic \
 	    inplace \
 	    repack \
+	    repack_deadlock \
 	    repack_toast \
 	    syscache-update-pruned \
 	    heap_lock_update
diff --git a/src/test/modules/injection_points/expected/repack_deadlock.out b/src/test/modules/injection_points/expected/repack_deadlock.out
new file mode 100644
index 00000000000..42505d10933
--- /dev/null
+++ b/src/test/modules/injection_points/expected/repack_deadlock.out
@@ -0,0 +1,63 @@
+Parsed test spec with 2 sessions
+
+starting permutation: wait_before_lock add_column wakeup_before_lock check1
+injection_points_attach
+-----------------------
+                       
+(1 row)
+
+step wait_before_lock: 
+	REPACK (CONCURRENTLY) repack_deadlock USING INDEX repack_deadlock_pkey;
+ <waiting ...>
+step add_column: 
+	alter table repack_deadlock add column noise text;
+ <waiting ...>
+step add_column: <... completed>
+ERROR:  oops, we're blocked by REPACK, time to die
+step wakeup_before_lock: 
+	SELECT injection_points_wakeup('repack-concurrently-before-lock');
+
+injection_points_wakeup
+-----------------------
+                       
+(1 row)
+
+step wait_before_lock: <... completed>
+step check1: 
+	INSERT INTO relfilenodes(node)
+	SELECT relfilenode FROM pg_class WHERE relname='repack_deadlock';
+
+	SELECT count(DISTINCT node) FROM relfilenodes;
+
+	SELECT i, j FROM repack_deadlock ORDER BY i, j;
+
+	INSERT INTO data_s1(i, j)
+	SELECT i, j FROM repack_deadlock;
+
+	SELECT count(*)
+	FROM data_s1 d1 FULL JOIN data_s2 d2 USING (i, j)
+	WHERE d1.i ISNULL OR d2.i ISNULL;
+
+count
+-----
+    1
+(1 row)
+
+i|j
+-+-
+1|1
+2|2
+3|3
+4|4
+(4 rows)
+
+count
+-----
+    4
+(1 row)
+
+injection_points_detach
+-----------------------
+                       
+(1 row)
+
diff --git a/src/test/modules/injection_points/meson.build b/src/test/modules/injection_points/meson.build
index a414abb924b..1cd88d6db65 100644
--- a/src/test/modules/injection_points/meson.build
+++ b/src/test/modules/injection_points/meson.build
@@ -46,6 +46,7 @@ tests += {
       'basic',
       'inplace',
       'repack',
+      'repack_deadlock',
       'repack_toast',
       'syscache-update-pruned',
       'heap_lock_update',
diff --git a/src/test/modules/injection_points/specs/repack_deadlock.spec b/src/test/modules/injection_points/specs/repack_deadlock.spec
new file mode 100644
index 00000000000..9d23a6588c2
--- /dev/null
+++ b/src/test/modules/injection_points/specs/repack_deadlock.spec
@@ -0,0 +1,83 @@
+# Test REPACK with a concurrent transaction that would cause a deadlock
+setup
+{
+	CREATE EXTENSION injection_points;
+
+	CREATE TABLE repack_deadlock(i int PRIMARY KEY, j int);
+	INSERT INTO repack_deadlock(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4);
+
+	CREATE TABLE relfilenodes(node oid);
+
+	CREATE TABLE data_s1(i int, j int);
+	CREATE TABLE data_s2(i int, j int);
+}
+
+teardown
+{
+	DROP TABLE repack_deadlock;
+	DROP EXTENSION injection_points;
+
+	DROP TABLE relfilenodes;
+	DROP TABLE data_s1;
+	DROP TABLE data_s2;
+}
+
+session s1
+setup
+{
+	SELECT injection_points_set_local();
+	SELECT injection_points_attach('repack-concurrently-before-lock', 'wait');
+}
+# Perform the initial load and wait for s2 to do some data changes.
+step wait_before_lock
+{
+	REPACK (CONCURRENTLY) repack_deadlock USING INDEX repack_deadlock_pkey;
+}
+# Check the table from the perspective of s1.
+#
+# Besides the contents, we also check that relfilenode has changed.
+
+# Have each session write the contents into a table and use FULL JOIN to check
+# if the outputs are identical.
+step check1
+{
+	INSERT INTO relfilenodes(node)
+	SELECT relfilenode FROM pg_class WHERE relname='repack_deadlock';
+
+	SELECT count(DISTINCT node) FROM relfilenodes;
+
+	SELECT i, j FROM repack_deadlock ORDER BY i, j;
+
+	INSERT INTO data_s1(i, j)
+	SELECT i, j FROM repack_deadlock;
+
+	SELECT count(*)
+	FROM data_s1 d1 FULL JOIN data_s2 d2 USING (i, j)
+	WHERE d1.i ISNULL OR d2.i ISNULL;
+}
+teardown
+{
+	SELECT injection_points_detach('repack-concurrently-before-lock');
+}
+
+session s2
+# Change the existing data. UPDATE changes both key and non-key columns. Also
+# update one row twice to test whether tuple version generated by this session
+# can be found.
+step add_column
+{
+	alter table repack_deadlock add column noise text;
+}
+
+step wakeup_before_lock
+{
+	SELECT injection_points_wakeup('repack-concurrently-before-lock');
+}
+
+# Test if data changes introduced while one session is performing REPACK
+# CONCURRENTLY find their way into the table.
+permutation
+	wait_before_lock
+	add_column
+	wakeup_before_lock
+	check1
-- 
2.47.3


--fot5plqmb33ey33c--





^ permalink  raw  reply  [nested|flat] 89+ messages in thread

* [PATCH v44b 4/4] hack deadlock detector
@ 2026-03-25 19:35  Álvaro Herrera <[email protected]>
  0 siblings, 0 replies; 89+ messages in thread

From: Álvaro Herrera @ 2026-03-25 19:35 UTC (permalink / raw)

---
 src/backend/commands/cluster.c                | 10 +++
 src/backend/storage/lmgr/deadlock.c           | 12 +++
 src/include/storage/proc.h                    |  1 +
 src/test/modules/injection_points/Makefile    |  1 +
 .../expected/repack_deadlock.out              | 63 ++++++++++++++
 src/test/modules/injection_points/meson.build |  1 +
 .../specs/repack_deadlock.spec                | 83 +++++++++++++++++++
 7 files changed, 171 insertions(+)
 create mode 100644 src/test/modules/injection_points/expected/repack_deadlock.out
 create mode 100644 src/test/modules/injection_points/specs/repack_deadlock.spec

diff --git a/src/backend/commands/cluster.c b/src/backend/commands/cluster.c
index 2c3058ba10d..d5b1dfbff69 100644
--- a/src/backend/commands/cluster.c
+++ b/src/backend/commands/cluster.c
@@ -372,6 +372,16 @@ ExecRepack(ParseState *pstate, RepackStmt *stmt, bool isTopLevel)
 	/* Determine the lock mode to use. */
 	lockmode = RepackLockLevel((params.options & CLUOPT_CONCURRENT) != 0);
 
+	/*
+	 * If in concurrent mode, set the PROC_IN_CONCURRENT_REPACK flag.  This
+	 * makes the deadlock checker cause anyone that would conflict with us
+	 * to error out.  It's important to set this flag ahead of actually locking
+	 * the relation; it won't of course affect anyone until we do have a lock
+	 * that others can conflict with.
+	 */
+	if ((params.options & CLUOPT_CONCURRENT) != 0)
+		MyProc->statusFlags |= PROC_IN_CONCURRENT_REPACK;
+
 	/*
 	 * If a single relation is specified, process it and we're done ... unless
 	 * the relation is a partitioned table, in which case we fall through.
diff --git a/src/backend/storage/lmgr/deadlock.c b/src/backend/storage/lmgr/deadlock.c
index b8962d875b6..10266e384aa 100644
--- a/src/backend/storage/lmgr/deadlock.c
+++ b/src/backend/storage/lmgr/deadlock.c
@@ -620,6 +620,18 @@ FindLockCycleRecurseMember(PGPROC *checkProc,
 						proc->statusFlags & PROC_IS_AUTOVACUUM)
 						blocking_autovacuum_proc = proc;
 
+					/*
+					 * If we note that we're blocked by some process running
+					 * REPACK (CONCURRENTLY), just fail.  That process is
+					 * going to upgrade its lock at some point, and it would
+					 * be inappropriate for any other process to cause that
+					 * to fail.
+					 */
+					if (checkProc == MyProc &&
+						proc->statusFlags & PROC_IN_CONCURRENT_REPACK)
+						/* not a serious error message proposal */
+						elog(ERROR, "oops, we're blocked by REPACK, time to die");
+
 					/* We're done looking at this proclock */
 					break;
 				}
diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h
index 1dad125706e..4e340ead455 100644
--- a/src/include/storage/proc.h
+++ b/src/include/storage/proc.h
@@ -69,6 +69,7 @@ struct XidCache
 #define		PROC_AFFECTS_ALL_HORIZONS	0x20	/* this proc's xmin must be
 												 * included in vacuum horizons
 												 * in all databases */
+#define		PROC_IN_CONCURRENT_REPACK	0x40	/* REPACK (CONCURRENTLY) */
 
 /* flags reset at EOXact */
 #define		PROC_VACUUM_STATE_MASK \
diff --git a/src/test/modules/injection_points/Makefile b/src/test/modules/injection_points/Makefile
index 2cd7d87c533..f7663859fe2 100644
--- a/src/test/modules/injection_points/Makefile
+++ b/src/test/modules/injection_points/Makefile
@@ -15,6 +15,7 @@ REGRESS_OPTS = --dlpath=$(top_builddir)/src/test/regress
 ISOLATION = basic \
 	    inplace \
 	    repack \
+	    repack_deadlock \
 	    repack_toast \
 	    syscache-update-pruned \
 	    heap_lock_update
diff --git a/src/test/modules/injection_points/expected/repack_deadlock.out b/src/test/modules/injection_points/expected/repack_deadlock.out
new file mode 100644
index 00000000000..42505d10933
--- /dev/null
+++ b/src/test/modules/injection_points/expected/repack_deadlock.out
@@ -0,0 +1,63 @@
+Parsed test spec with 2 sessions
+
+starting permutation: wait_before_lock add_column wakeup_before_lock check1
+injection_points_attach
+-----------------------
+                       
+(1 row)
+
+step wait_before_lock: 
+	REPACK (CONCURRENTLY) repack_deadlock USING INDEX repack_deadlock_pkey;
+ <waiting ...>
+step add_column: 
+	alter table repack_deadlock add column noise text;
+ <waiting ...>
+step add_column: <... completed>
+ERROR:  oops, we're blocked by REPACK, time to die
+step wakeup_before_lock: 
+	SELECT injection_points_wakeup('repack-concurrently-before-lock');
+
+injection_points_wakeup
+-----------------------
+                       
+(1 row)
+
+step wait_before_lock: <... completed>
+step check1: 
+	INSERT INTO relfilenodes(node)
+	SELECT relfilenode FROM pg_class WHERE relname='repack_deadlock';
+
+	SELECT count(DISTINCT node) FROM relfilenodes;
+
+	SELECT i, j FROM repack_deadlock ORDER BY i, j;
+
+	INSERT INTO data_s1(i, j)
+	SELECT i, j FROM repack_deadlock;
+
+	SELECT count(*)
+	FROM data_s1 d1 FULL JOIN data_s2 d2 USING (i, j)
+	WHERE d1.i ISNULL OR d2.i ISNULL;
+
+count
+-----
+    1
+(1 row)
+
+i|j
+-+-
+1|1
+2|2
+3|3
+4|4
+(4 rows)
+
+count
+-----
+    4
+(1 row)
+
+injection_points_detach
+-----------------------
+                       
+(1 row)
+
diff --git a/src/test/modules/injection_points/meson.build b/src/test/modules/injection_points/meson.build
index a414abb924b..1cd88d6db65 100644
--- a/src/test/modules/injection_points/meson.build
+++ b/src/test/modules/injection_points/meson.build
@@ -46,6 +46,7 @@ tests += {
       'basic',
       'inplace',
       'repack',
+      'repack_deadlock',
       'repack_toast',
       'syscache-update-pruned',
       'heap_lock_update',
diff --git a/src/test/modules/injection_points/specs/repack_deadlock.spec b/src/test/modules/injection_points/specs/repack_deadlock.spec
new file mode 100644
index 00000000000..9d23a6588c2
--- /dev/null
+++ b/src/test/modules/injection_points/specs/repack_deadlock.spec
@@ -0,0 +1,83 @@
+# Test REPACK with a concurrent transaction that would cause a deadlock
+setup
+{
+	CREATE EXTENSION injection_points;
+
+	CREATE TABLE repack_deadlock(i int PRIMARY KEY, j int);
+	INSERT INTO repack_deadlock(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4);
+
+	CREATE TABLE relfilenodes(node oid);
+
+	CREATE TABLE data_s1(i int, j int);
+	CREATE TABLE data_s2(i int, j int);
+}
+
+teardown
+{
+	DROP TABLE repack_deadlock;
+	DROP EXTENSION injection_points;
+
+	DROP TABLE relfilenodes;
+	DROP TABLE data_s1;
+	DROP TABLE data_s2;
+}
+
+session s1
+setup
+{
+	SELECT injection_points_set_local();
+	SELECT injection_points_attach('repack-concurrently-before-lock', 'wait');
+}
+# Perform the initial load and wait for s2 to do some data changes.
+step wait_before_lock
+{
+	REPACK (CONCURRENTLY) repack_deadlock USING INDEX repack_deadlock_pkey;
+}
+# Check the table from the perspective of s1.
+#
+# Besides the contents, we also check that relfilenode has changed.
+
+# Have each session write the contents into a table and use FULL JOIN to check
+# if the outputs are identical.
+step check1
+{
+	INSERT INTO relfilenodes(node)
+	SELECT relfilenode FROM pg_class WHERE relname='repack_deadlock';
+
+	SELECT count(DISTINCT node) FROM relfilenodes;
+
+	SELECT i, j FROM repack_deadlock ORDER BY i, j;
+
+	INSERT INTO data_s1(i, j)
+	SELECT i, j FROM repack_deadlock;
+
+	SELECT count(*)
+	FROM data_s1 d1 FULL JOIN data_s2 d2 USING (i, j)
+	WHERE d1.i ISNULL OR d2.i ISNULL;
+}
+teardown
+{
+	SELECT injection_points_detach('repack-concurrently-before-lock');
+}
+
+session s2
+# Change the existing data. UPDATE changes both key and non-key columns. Also
+# update one row twice to test whether tuple version generated by this session
+# can be found.
+step add_column
+{
+	alter table repack_deadlock add column noise text;
+}
+
+step wakeup_before_lock
+{
+	SELECT injection_points_wakeup('repack-concurrently-before-lock');
+}
+
+# Test if data changes introduced while one session is performing REPACK
+# CONCURRENTLY find their way into the table.
+permutation
+	wait_before_lock
+	add_column
+	wakeup_before_lock
+	check1
-- 
2.47.3


--fot5plqmb33ey33c--





^ permalink  raw  reply  [nested|flat] 89+ messages in thread

* [PATCH v44b 4/4] hack deadlock detector
@ 2026-03-25 19:35  Álvaro Herrera <[email protected]>
  0 siblings, 0 replies; 89+ messages in thread

From: Álvaro Herrera @ 2026-03-25 19:35 UTC (permalink / raw)

---
 src/backend/commands/cluster.c                | 10 +++
 src/backend/storage/lmgr/deadlock.c           | 12 +++
 src/include/storage/proc.h                    |  1 +
 src/test/modules/injection_points/Makefile    |  1 +
 .../expected/repack_deadlock.out              | 63 ++++++++++++++
 src/test/modules/injection_points/meson.build |  1 +
 .../specs/repack_deadlock.spec                | 83 +++++++++++++++++++
 7 files changed, 171 insertions(+)
 create mode 100644 src/test/modules/injection_points/expected/repack_deadlock.out
 create mode 100644 src/test/modules/injection_points/specs/repack_deadlock.spec

diff --git a/src/backend/commands/cluster.c b/src/backend/commands/cluster.c
index 2c3058ba10d..d5b1dfbff69 100644
--- a/src/backend/commands/cluster.c
+++ b/src/backend/commands/cluster.c
@@ -372,6 +372,16 @@ ExecRepack(ParseState *pstate, RepackStmt *stmt, bool isTopLevel)
 	/* Determine the lock mode to use. */
 	lockmode = RepackLockLevel((params.options & CLUOPT_CONCURRENT) != 0);
 
+	/*
+	 * If in concurrent mode, set the PROC_IN_CONCURRENT_REPACK flag.  This
+	 * makes the deadlock checker cause anyone that would conflict with us
+	 * to error out.  It's important to set this flag ahead of actually locking
+	 * the relation; it won't of course affect anyone until we do have a lock
+	 * that others can conflict with.
+	 */
+	if ((params.options & CLUOPT_CONCURRENT) != 0)
+		MyProc->statusFlags |= PROC_IN_CONCURRENT_REPACK;
+
 	/*
 	 * If a single relation is specified, process it and we're done ... unless
 	 * the relation is a partitioned table, in which case we fall through.
diff --git a/src/backend/storage/lmgr/deadlock.c b/src/backend/storage/lmgr/deadlock.c
index b8962d875b6..10266e384aa 100644
--- a/src/backend/storage/lmgr/deadlock.c
+++ b/src/backend/storage/lmgr/deadlock.c
@@ -620,6 +620,18 @@ FindLockCycleRecurseMember(PGPROC *checkProc,
 						proc->statusFlags & PROC_IS_AUTOVACUUM)
 						blocking_autovacuum_proc = proc;
 
+					/*
+					 * If we note that we're blocked by some process running
+					 * REPACK (CONCURRENTLY), just fail.  That process is
+					 * going to upgrade its lock at some point, and it would
+					 * be inappropriate for any other process to cause that
+					 * to fail.
+					 */
+					if (checkProc == MyProc &&
+						proc->statusFlags & PROC_IN_CONCURRENT_REPACK)
+						/* not a serious error message proposal */
+						elog(ERROR, "oops, we're blocked by REPACK, time to die");
+
 					/* We're done looking at this proclock */
 					break;
 				}
diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h
index 1dad125706e..4e340ead455 100644
--- a/src/include/storage/proc.h
+++ b/src/include/storage/proc.h
@@ -69,6 +69,7 @@ struct XidCache
 #define		PROC_AFFECTS_ALL_HORIZONS	0x20	/* this proc's xmin must be
 												 * included in vacuum horizons
 												 * in all databases */
+#define		PROC_IN_CONCURRENT_REPACK	0x40	/* REPACK (CONCURRENTLY) */
 
 /* flags reset at EOXact */
 #define		PROC_VACUUM_STATE_MASK \
diff --git a/src/test/modules/injection_points/Makefile b/src/test/modules/injection_points/Makefile
index 2cd7d87c533..f7663859fe2 100644
--- a/src/test/modules/injection_points/Makefile
+++ b/src/test/modules/injection_points/Makefile
@@ -15,6 +15,7 @@ REGRESS_OPTS = --dlpath=$(top_builddir)/src/test/regress
 ISOLATION = basic \
 	    inplace \
 	    repack \
+	    repack_deadlock \
 	    repack_toast \
 	    syscache-update-pruned \
 	    heap_lock_update
diff --git a/src/test/modules/injection_points/expected/repack_deadlock.out b/src/test/modules/injection_points/expected/repack_deadlock.out
new file mode 100644
index 00000000000..42505d10933
--- /dev/null
+++ b/src/test/modules/injection_points/expected/repack_deadlock.out
@@ -0,0 +1,63 @@
+Parsed test spec with 2 sessions
+
+starting permutation: wait_before_lock add_column wakeup_before_lock check1
+injection_points_attach
+-----------------------
+                       
+(1 row)
+
+step wait_before_lock: 
+	REPACK (CONCURRENTLY) repack_deadlock USING INDEX repack_deadlock_pkey;
+ <waiting ...>
+step add_column: 
+	alter table repack_deadlock add column noise text;
+ <waiting ...>
+step add_column: <... completed>
+ERROR:  oops, we're blocked by REPACK, time to die
+step wakeup_before_lock: 
+	SELECT injection_points_wakeup('repack-concurrently-before-lock');
+
+injection_points_wakeup
+-----------------------
+                       
+(1 row)
+
+step wait_before_lock: <... completed>
+step check1: 
+	INSERT INTO relfilenodes(node)
+	SELECT relfilenode FROM pg_class WHERE relname='repack_deadlock';
+
+	SELECT count(DISTINCT node) FROM relfilenodes;
+
+	SELECT i, j FROM repack_deadlock ORDER BY i, j;
+
+	INSERT INTO data_s1(i, j)
+	SELECT i, j FROM repack_deadlock;
+
+	SELECT count(*)
+	FROM data_s1 d1 FULL JOIN data_s2 d2 USING (i, j)
+	WHERE d1.i ISNULL OR d2.i ISNULL;
+
+count
+-----
+    1
+(1 row)
+
+i|j
+-+-
+1|1
+2|2
+3|3
+4|4
+(4 rows)
+
+count
+-----
+    4
+(1 row)
+
+injection_points_detach
+-----------------------
+                       
+(1 row)
+
diff --git a/src/test/modules/injection_points/meson.build b/src/test/modules/injection_points/meson.build
index a414abb924b..1cd88d6db65 100644
--- a/src/test/modules/injection_points/meson.build
+++ b/src/test/modules/injection_points/meson.build
@@ -46,6 +46,7 @@ tests += {
       'basic',
       'inplace',
       'repack',
+      'repack_deadlock',
       'repack_toast',
       'syscache-update-pruned',
       'heap_lock_update',
diff --git a/src/test/modules/injection_points/specs/repack_deadlock.spec b/src/test/modules/injection_points/specs/repack_deadlock.spec
new file mode 100644
index 00000000000..9d23a6588c2
--- /dev/null
+++ b/src/test/modules/injection_points/specs/repack_deadlock.spec
@@ -0,0 +1,83 @@
+# Test REPACK with a concurrent transaction that would cause a deadlock
+setup
+{
+	CREATE EXTENSION injection_points;
+
+	CREATE TABLE repack_deadlock(i int PRIMARY KEY, j int);
+	INSERT INTO repack_deadlock(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4);
+
+	CREATE TABLE relfilenodes(node oid);
+
+	CREATE TABLE data_s1(i int, j int);
+	CREATE TABLE data_s2(i int, j int);
+}
+
+teardown
+{
+	DROP TABLE repack_deadlock;
+	DROP EXTENSION injection_points;
+
+	DROP TABLE relfilenodes;
+	DROP TABLE data_s1;
+	DROP TABLE data_s2;
+}
+
+session s1
+setup
+{
+	SELECT injection_points_set_local();
+	SELECT injection_points_attach('repack-concurrently-before-lock', 'wait');
+}
+# Perform the initial load and wait for s2 to do some data changes.
+step wait_before_lock
+{
+	REPACK (CONCURRENTLY) repack_deadlock USING INDEX repack_deadlock_pkey;
+}
+# Check the table from the perspective of s1.
+#
+# Besides the contents, we also check that relfilenode has changed.
+
+# Have each session write the contents into a table and use FULL JOIN to check
+# if the outputs are identical.
+step check1
+{
+	INSERT INTO relfilenodes(node)
+	SELECT relfilenode FROM pg_class WHERE relname='repack_deadlock';
+
+	SELECT count(DISTINCT node) FROM relfilenodes;
+
+	SELECT i, j FROM repack_deadlock ORDER BY i, j;
+
+	INSERT INTO data_s1(i, j)
+	SELECT i, j FROM repack_deadlock;
+
+	SELECT count(*)
+	FROM data_s1 d1 FULL JOIN data_s2 d2 USING (i, j)
+	WHERE d1.i ISNULL OR d2.i ISNULL;
+}
+teardown
+{
+	SELECT injection_points_detach('repack-concurrently-before-lock');
+}
+
+session s2
+# Change the existing data. UPDATE changes both key and non-key columns. Also
+# update one row twice to test whether tuple version generated by this session
+# can be found.
+step add_column
+{
+	alter table repack_deadlock add column noise text;
+}
+
+step wakeup_before_lock
+{
+	SELECT injection_points_wakeup('repack-concurrently-before-lock');
+}
+
+# Test if data changes introduced while one session is performing REPACK
+# CONCURRENTLY find their way into the table.
+permutation
+	wait_before_lock
+	add_column
+	wakeup_before_lock
+	check1
-- 
2.47.3


--fot5plqmb33ey33c--





^ permalink  raw  reply  [nested|flat] 89+ messages in thread

* [PATCH v44b 4/4] hack deadlock detector
@ 2026-03-25 19:35  Álvaro Herrera <[email protected]>
  0 siblings, 0 replies; 89+ messages in thread

From: Álvaro Herrera @ 2026-03-25 19:35 UTC (permalink / raw)

---
 src/backend/commands/cluster.c                | 10 +++
 src/backend/storage/lmgr/deadlock.c           | 12 +++
 src/include/storage/proc.h                    |  1 +
 src/test/modules/injection_points/Makefile    |  1 +
 .../expected/repack_deadlock.out              | 63 ++++++++++++++
 src/test/modules/injection_points/meson.build |  1 +
 .../specs/repack_deadlock.spec                | 83 +++++++++++++++++++
 7 files changed, 171 insertions(+)
 create mode 100644 src/test/modules/injection_points/expected/repack_deadlock.out
 create mode 100644 src/test/modules/injection_points/specs/repack_deadlock.spec

diff --git a/src/backend/commands/cluster.c b/src/backend/commands/cluster.c
index 2c3058ba10d..d5b1dfbff69 100644
--- a/src/backend/commands/cluster.c
+++ b/src/backend/commands/cluster.c
@@ -372,6 +372,16 @@ ExecRepack(ParseState *pstate, RepackStmt *stmt, bool isTopLevel)
 	/* Determine the lock mode to use. */
 	lockmode = RepackLockLevel((params.options & CLUOPT_CONCURRENT) != 0);
 
+	/*
+	 * If in concurrent mode, set the PROC_IN_CONCURRENT_REPACK flag.  This
+	 * makes the deadlock checker cause anyone that would conflict with us
+	 * to error out.  It's important to set this flag ahead of actually locking
+	 * the relation; it won't of course affect anyone until we do have a lock
+	 * that others can conflict with.
+	 */
+	if ((params.options & CLUOPT_CONCURRENT) != 0)
+		MyProc->statusFlags |= PROC_IN_CONCURRENT_REPACK;
+
 	/*
 	 * If a single relation is specified, process it and we're done ... unless
 	 * the relation is a partitioned table, in which case we fall through.
diff --git a/src/backend/storage/lmgr/deadlock.c b/src/backend/storage/lmgr/deadlock.c
index b8962d875b6..10266e384aa 100644
--- a/src/backend/storage/lmgr/deadlock.c
+++ b/src/backend/storage/lmgr/deadlock.c
@@ -620,6 +620,18 @@ FindLockCycleRecurseMember(PGPROC *checkProc,
 						proc->statusFlags & PROC_IS_AUTOVACUUM)
 						blocking_autovacuum_proc = proc;
 
+					/*
+					 * If we note that we're blocked by some process running
+					 * REPACK (CONCURRENTLY), just fail.  That process is
+					 * going to upgrade its lock at some point, and it would
+					 * be inappropriate for any other process to cause that
+					 * to fail.
+					 */
+					if (checkProc == MyProc &&
+						proc->statusFlags & PROC_IN_CONCURRENT_REPACK)
+						/* not a serious error message proposal */
+						elog(ERROR, "oops, we're blocked by REPACK, time to die");
+
 					/* We're done looking at this proclock */
 					break;
 				}
diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h
index 1dad125706e..4e340ead455 100644
--- a/src/include/storage/proc.h
+++ b/src/include/storage/proc.h
@@ -69,6 +69,7 @@ struct XidCache
 #define		PROC_AFFECTS_ALL_HORIZONS	0x20	/* this proc's xmin must be
 												 * included in vacuum horizons
 												 * in all databases */
+#define		PROC_IN_CONCURRENT_REPACK	0x40	/* REPACK (CONCURRENTLY) */
 
 /* flags reset at EOXact */
 #define		PROC_VACUUM_STATE_MASK \
diff --git a/src/test/modules/injection_points/Makefile b/src/test/modules/injection_points/Makefile
index 2cd7d87c533..f7663859fe2 100644
--- a/src/test/modules/injection_points/Makefile
+++ b/src/test/modules/injection_points/Makefile
@@ -15,6 +15,7 @@ REGRESS_OPTS = --dlpath=$(top_builddir)/src/test/regress
 ISOLATION = basic \
 	    inplace \
 	    repack \
+	    repack_deadlock \
 	    repack_toast \
 	    syscache-update-pruned \
 	    heap_lock_update
diff --git a/src/test/modules/injection_points/expected/repack_deadlock.out b/src/test/modules/injection_points/expected/repack_deadlock.out
new file mode 100644
index 00000000000..42505d10933
--- /dev/null
+++ b/src/test/modules/injection_points/expected/repack_deadlock.out
@@ -0,0 +1,63 @@
+Parsed test spec with 2 sessions
+
+starting permutation: wait_before_lock add_column wakeup_before_lock check1
+injection_points_attach
+-----------------------
+                       
+(1 row)
+
+step wait_before_lock: 
+	REPACK (CONCURRENTLY) repack_deadlock USING INDEX repack_deadlock_pkey;
+ <waiting ...>
+step add_column: 
+	alter table repack_deadlock add column noise text;
+ <waiting ...>
+step add_column: <... completed>
+ERROR:  oops, we're blocked by REPACK, time to die
+step wakeup_before_lock: 
+	SELECT injection_points_wakeup('repack-concurrently-before-lock');
+
+injection_points_wakeup
+-----------------------
+                       
+(1 row)
+
+step wait_before_lock: <... completed>
+step check1: 
+	INSERT INTO relfilenodes(node)
+	SELECT relfilenode FROM pg_class WHERE relname='repack_deadlock';
+
+	SELECT count(DISTINCT node) FROM relfilenodes;
+
+	SELECT i, j FROM repack_deadlock ORDER BY i, j;
+
+	INSERT INTO data_s1(i, j)
+	SELECT i, j FROM repack_deadlock;
+
+	SELECT count(*)
+	FROM data_s1 d1 FULL JOIN data_s2 d2 USING (i, j)
+	WHERE d1.i ISNULL OR d2.i ISNULL;
+
+count
+-----
+    1
+(1 row)
+
+i|j
+-+-
+1|1
+2|2
+3|3
+4|4
+(4 rows)
+
+count
+-----
+    4
+(1 row)
+
+injection_points_detach
+-----------------------
+                       
+(1 row)
+
diff --git a/src/test/modules/injection_points/meson.build b/src/test/modules/injection_points/meson.build
index a414abb924b..1cd88d6db65 100644
--- a/src/test/modules/injection_points/meson.build
+++ b/src/test/modules/injection_points/meson.build
@@ -46,6 +46,7 @@ tests += {
       'basic',
       'inplace',
       'repack',
+      'repack_deadlock',
       'repack_toast',
       'syscache-update-pruned',
       'heap_lock_update',
diff --git a/src/test/modules/injection_points/specs/repack_deadlock.spec b/src/test/modules/injection_points/specs/repack_deadlock.spec
new file mode 100644
index 00000000000..9d23a6588c2
--- /dev/null
+++ b/src/test/modules/injection_points/specs/repack_deadlock.spec
@@ -0,0 +1,83 @@
+# Test REPACK with a concurrent transaction that would cause a deadlock
+setup
+{
+	CREATE EXTENSION injection_points;
+
+	CREATE TABLE repack_deadlock(i int PRIMARY KEY, j int);
+	INSERT INTO repack_deadlock(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4);
+
+	CREATE TABLE relfilenodes(node oid);
+
+	CREATE TABLE data_s1(i int, j int);
+	CREATE TABLE data_s2(i int, j int);
+}
+
+teardown
+{
+	DROP TABLE repack_deadlock;
+	DROP EXTENSION injection_points;
+
+	DROP TABLE relfilenodes;
+	DROP TABLE data_s1;
+	DROP TABLE data_s2;
+}
+
+session s1
+setup
+{
+	SELECT injection_points_set_local();
+	SELECT injection_points_attach('repack-concurrently-before-lock', 'wait');
+}
+# Perform the initial load and wait for s2 to do some data changes.
+step wait_before_lock
+{
+	REPACK (CONCURRENTLY) repack_deadlock USING INDEX repack_deadlock_pkey;
+}
+# Check the table from the perspective of s1.
+#
+# Besides the contents, we also check that relfilenode has changed.
+
+# Have each session write the contents into a table and use FULL JOIN to check
+# if the outputs are identical.
+step check1
+{
+	INSERT INTO relfilenodes(node)
+	SELECT relfilenode FROM pg_class WHERE relname='repack_deadlock';
+
+	SELECT count(DISTINCT node) FROM relfilenodes;
+
+	SELECT i, j FROM repack_deadlock ORDER BY i, j;
+
+	INSERT INTO data_s1(i, j)
+	SELECT i, j FROM repack_deadlock;
+
+	SELECT count(*)
+	FROM data_s1 d1 FULL JOIN data_s2 d2 USING (i, j)
+	WHERE d1.i ISNULL OR d2.i ISNULL;
+}
+teardown
+{
+	SELECT injection_points_detach('repack-concurrently-before-lock');
+}
+
+session s2
+# Change the existing data. UPDATE changes both key and non-key columns. Also
+# update one row twice to test whether tuple version generated by this session
+# can be found.
+step add_column
+{
+	alter table repack_deadlock add column noise text;
+}
+
+step wakeup_before_lock
+{
+	SELECT injection_points_wakeup('repack-concurrently-before-lock');
+}
+
+# Test if data changes introduced while one session is performing REPACK
+# CONCURRENTLY find their way into the table.
+permutation
+	wait_before_lock
+	add_column
+	wakeup_before_lock
+	check1
-- 
2.47.3


--fot5plqmb33ey33c--





^ permalink  raw  reply  [nested|flat] 89+ messages in thread

* [PATCH v44b 4/4] hack deadlock detector
@ 2026-03-25 19:35  Álvaro Herrera <[email protected]>
  0 siblings, 0 replies; 89+ messages in thread

From: Álvaro Herrera @ 2026-03-25 19:35 UTC (permalink / raw)

---
 src/backend/commands/cluster.c                | 10 +++
 src/backend/storage/lmgr/deadlock.c           | 12 +++
 src/include/storage/proc.h                    |  1 +
 src/test/modules/injection_points/Makefile    |  1 +
 .../expected/repack_deadlock.out              | 63 ++++++++++++++
 src/test/modules/injection_points/meson.build |  1 +
 .../specs/repack_deadlock.spec                | 83 +++++++++++++++++++
 7 files changed, 171 insertions(+)
 create mode 100644 src/test/modules/injection_points/expected/repack_deadlock.out
 create mode 100644 src/test/modules/injection_points/specs/repack_deadlock.spec

diff --git a/src/backend/commands/cluster.c b/src/backend/commands/cluster.c
index 2c3058ba10d..d5b1dfbff69 100644
--- a/src/backend/commands/cluster.c
+++ b/src/backend/commands/cluster.c
@@ -372,6 +372,16 @@ ExecRepack(ParseState *pstate, RepackStmt *stmt, bool isTopLevel)
 	/* Determine the lock mode to use. */
 	lockmode = RepackLockLevel((params.options & CLUOPT_CONCURRENT) != 0);
 
+	/*
+	 * If in concurrent mode, set the PROC_IN_CONCURRENT_REPACK flag.  This
+	 * makes the deadlock checker cause anyone that would conflict with us
+	 * to error out.  It's important to set this flag ahead of actually locking
+	 * the relation; it won't of course affect anyone until we do have a lock
+	 * that others can conflict with.
+	 */
+	if ((params.options & CLUOPT_CONCURRENT) != 0)
+		MyProc->statusFlags |= PROC_IN_CONCURRENT_REPACK;
+
 	/*
 	 * If a single relation is specified, process it and we're done ... unless
 	 * the relation is a partitioned table, in which case we fall through.
diff --git a/src/backend/storage/lmgr/deadlock.c b/src/backend/storage/lmgr/deadlock.c
index b8962d875b6..10266e384aa 100644
--- a/src/backend/storage/lmgr/deadlock.c
+++ b/src/backend/storage/lmgr/deadlock.c
@@ -620,6 +620,18 @@ FindLockCycleRecurseMember(PGPROC *checkProc,
 						proc->statusFlags & PROC_IS_AUTOVACUUM)
 						blocking_autovacuum_proc = proc;
 
+					/*
+					 * If we note that we're blocked by some process running
+					 * REPACK (CONCURRENTLY), just fail.  That process is
+					 * going to upgrade its lock at some point, and it would
+					 * be inappropriate for any other process to cause that
+					 * to fail.
+					 */
+					if (checkProc == MyProc &&
+						proc->statusFlags & PROC_IN_CONCURRENT_REPACK)
+						/* not a serious error message proposal */
+						elog(ERROR, "oops, we're blocked by REPACK, time to die");
+
 					/* We're done looking at this proclock */
 					break;
 				}
diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h
index 1dad125706e..4e340ead455 100644
--- a/src/include/storage/proc.h
+++ b/src/include/storage/proc.h
@@ -69,6 +69,7 @@ struct XidCache
 #define		PROC_AFFECTS_ALL_HORIZONS	0x20	/* this proc's xmin must be
 												 * included in vacuum horizons
 												 * in all databases */
+#define		PROC_IN_CONCURRENT_REPACK	0x40	/* REPACK (CONCURRENTLY) */
 
 /* flags reset at EOXact */
 #define		PROC_VACUUM_STATE_MASK \
diff --git a/src/test/modules/injection_points/Makefile b/src/test/modules/injection_points/Makefile
index 2cd7d87c533..f7663859fe2 100644
--- a/src/test/modules/injection_points/Makefile
+++ b/src/test/modules/injection_points/Makefile
@@ -15,6 +15,7 @@ REGRESS_OPTS = --dlpath=$(top_builddir)/src/test/regress
 ISOLATION = basic \
 	    inplace \
 	    repack \
+	    repack_deadlock \
 	    repack_toast \
 	    syscache-update-pruned \
 	    heap_lock_update
diff --git a/src/test/modules/injection_points/expected/repack_deadlock.out b/src/test/modules/injection_points/expected/repack_deadlock.out
new file mode 100644
index 00000000000..42505d10933
--- /dev/null
+++ b/src/test/modules/injection_points/expected/repack_deadlock.out
@@ -0,0 +1,63 @@
+Parsed test spec with 2 sessions
+
+starting permutation: wait_before_lock add_column wakeup_before_lock check1
+injection_points_attach
+-----------------------
+                       
+(1 row)
+
+step wait_before_lock: 
+	REPACK (CONCURRENTLY) repack_deadlock USING INDEX repack_deadlock_pkey;
+ <waiting ...>
+step add_column: 
+	alter table repack_deadlock add column noise text;
+ <waiting ...>
+step add_column: <... completed>
+ERROR:  oops, we're blocked by REPACK, time to die
+step wakeup_before_lock: 
+	SELECT injection_points_wakeup('repack-concurrently-before-lock');
+
+injection_points_wakeup
+-----------------------
+                       
+(1 row)
+
+step wait_before_lock: <... completed>
+step check1: 
+	INSERT INTO relfilenodes(node)
+	SELECT relfilenode FROM pg_class WHERE relname='repack_deadlock';
+
+	SELECT count(DISTINCT node) FROM relfilenodes;
+
+	SELECT i, j FROM repack_deadlock ORDER BY i, j;
+
+	INSERT INTO data_s1(i, j)
+	SELECT i, j FROM repack_deadlock;
+
+	SELECT count(*)
+	FROM data_s1 d1 FULL JOIN data_s2 d2 USING (i, j)
+	WHERE d1.i ISNULL OR d2.i ISNULL;
+
+count
+-----
+    1
+(1 row)
+
+i|j
+-+-
+1|1
+2|2
+3|3
+4|4
+(4 rows)
+
+count
+-----
+    4
+(1 row)
+
+injection_points_detach
+-----------------------
+                       
+(1 row)
+
diff --git a/src/test/modules/injection_points/meson.build b/src/test/modules/injection_points/meson.build
index a414abb924b..1cd88d6db65 100644
--- a/src/test/modules/injection_points/meson.build
+++ b/src/test/modules/injection_points/meson.build
@@ -46,6 +46,7 @@ tests += {
       'basic',
       'inplace',
       'repack',
+      'repack_deadlock',
       'repack_toast',
       'syscache-update-pruned',
       'heap_lock_update',
diff --git a/src/test/modules/injection_points/specs/repack_deadlock.spec b/src/test/modules/injection_points/specs/repack_deadlock.spec
new file mode 100644
index 00000000000..9d23a6588c2
--- /dev/null
+++ b/src/test/modules/injection_points/specs/repack_deadlock.spec
@@ -0,0 +1,83 @@
+# Test REPACK with a concurrent transaction that would cause a deadlock
+setup
+{
+	CREATE EXTENSION injection_points;
+
+	CREATE TABLE repack_deadlock(i int PRIMARY KEY, j int);
+	INSERT INTO repack_deadlock(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4);
+
+	CREATE TABLE relfilenodes(node oid);
+
+	CREATE TABLE data_s1(i int, j int);
+	CREATE TABLE data_s2(i int, j int);
+}
+
+teardown
+{
+	DROP TABLE repack_deadlock;
+	DROP EXTENSION injection_points;
+
+	DROP TABLE relfilenodes;
+	DROP TABLE data_s1;
+	DROP TABLE data_s2;
+}
+
+session s1
+setup
+{
+	SELECT injection_points_set_local();
+	SELECT injection_points_attach('repack-concurrently-before-lock', 'wait');
+}
+# Perform the initial load and wait for s2 to do some data changes.
+step wait_before_lock
+{
+	REPACK (CONCURRENTLY) repack_deadlock USING INDEX repack_deadlock_pkey;
+}
+# Check the table from the perspective of s1.
+#
+# Besides the contents, we also check that relfilenode has changed.
+
+# Have each session write the contents into a table and use FULL JOIN to check
+# if the outputs are identical.
+step check1
+{
+	INSERT INTO relfilenodes(node)
+	SELECT relfilenode FROM pg_class WHERE relname='repack_deadlock';
+
+	SELECT count(DISTINCT node) FROM relfilenodes;
+
+	SELECT i, j FROM repack_deadlock ORDER BY i, j;
+
+	INSERT INTO data_s1(i, j)
+	SELECT i, j FROM repack_deadlock;
+
+	SELECT count(*)
+	FROM data_s1 d1 FULL JOIN data_s2 d2 USING (i, j)
+	WHERE d1.i ISNULL OR d2.i ISNULL;
+}
+teardown
+{
+	SELECT injection_points_detach('repack-concurrently-before-lock');
+}
+
+session s2
+# Change the existing data. UPDATE changes both key and non-key columns. Also
+# update one row twice to test whether tuple version generated by this session
+# can be found.
+step add_column
+{
+	alter table repack_deadlock add column noise text;
+}
+
+step wakeup_before_lock
+{
+	SELECT injection_points_wakeup('repack-concurrently-before-lock');
+}
+
+# Test if data changes introduced while one session is performing REPACK
+# CONCURRENTLY find their way into the table.
+permutation
+	wait_before_lock
+	add_column
+	wakeup_before_lock
+	check1
-- 
2.47.3


--fot5plqmb33ey33c--





^ permalink  raw  reply  [nested|flat] 89+ messages in thread

* [PATCH v44b 4/4] hack deadlock detector
@ 2026-03-25 19:35  Álvaro Herrera <[email protected]>
  0 siblings, 0 replies; 89+ messages in thread

From: Álvaro Herrera @ 2026-03-25 19:35 UTC (permalink / raw)

---
 src/backend/commands/cluster.c                | 10 +++
 src/backend/storage/lmgr/deadlock.c           | 12 +++
 src/include/storage/proc.h                    |  1 +
 src/test/modules/injection_points/Makefile    |  1 +
 .../expected/repack_deadlock.out              | 63 ++++++++++++++
 src/test/modules/injection_points/meson.build |  1 +
 .../specs/repack_deadlock.spec                | 83 +++++++++++++++++++
 7 files changed, 171 insertions(+)
 create mode 100644 src/test/modules/injection_points/expected/repack_deadlock.out
 create mode 100644 src/test/modules/injection_points/specs/repack_deadlock.spec

diff --git a/src/backend/commands/cluster.c b/src/backend/commands/cluster.c
index 2c3058ba10d..d5b1dfbff69 100644
--- a/src/backend/commands/cluster.c
+++ b/src/backend/commands/cluster.c
@@ -372,6 +372,16 @@ ExecRepack(ParseState *pstate, RepackStmt *stmt, bool isTopLevel)
 	/* Determine the lock mode to use. */
 	lockmode = RepackLockLevel((params.options & CLUOPT_CONCURRENT) != 0);
 
+	/*
+	 * If in concurrent mode, set the PROC_IN_CONCURRENT_REPACK flag.  This
+	 * makes the deadlock checker cause anyone that would conflict with us
+	 * to error out.  It's important to set this flag ahead of actually locking
+	 * the relation; it won't of course affect anyone until we do have a lock
+	 * that others can conflict with.
+	 */
+	if ((params.options & CLUOPT_CONCURRENT) != 0)
+		MyProc->statusFlags |= PROC_IN_CONCURRENT_REPACK;
+
 	/*
 	 * If a single relation is specified, process it and we're done ... unless
 	 * the relation is a partitioned table, in which case we fall through.
diff --git a/src/backend/storage/lmgr/deadlock.c b/src/backend/storage/lmgr/deadlock.c
index b8962d875b6..10266e384aa 100644
--- a/src/backend/storage/lmgr/deadlock.c
+++ b/src/backend/storage/lmgr/deadlock.c
@@ -620,6 +620,18 @@ FindLockCycleRecurseMember(PGPROC *checkProc,
 						proc->statusFlags & PROC_IS_AUTOVACUUM)
 						blocking_autovacuum_proc = proc;
 
+					/*
+					 * If we note that we're blocked by some process running
+					 * REPACK (CONCURRENTLY), just fail.  That process is
+					 * going to upgrade its lock at some point, and it would
+					 * be inappropriate for any other process to cause that
+					 * to fail.
+					 */
+					if (checkProc == MyProc &&
+						proc->statusFlags & PROC_IN_CONCURRENT_REPACK)
+						/* not a serious error message proposal */
+						elog(ERROR, "oops, we're blocked by REPACK, time to die");
+
 					/* We're done looking at this proclock */
 					break;
 				}
diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h
index 1dad125706e..4e340ead455 100644
--- a/src/include/storage/proc.h
+++ b/src/include/storage/proc.h
@@ -69,6 +69,7 @@ struct XidCache
 #define		PROC_AFFECTS_ALL_HORIZONS	0x20	/* this proc's xmin must be
 												 * included in vacuum horizons
 												 * in all databases */
+#define		PROC_IN_CONCURRENT_REPACK	0x40	/* REPACK (CONCURRENTLY) */
 
 /* flags reset at EOXact */
 #define		PROC_VACUUM_STATE_MASK \
diff --git a/src/test/modules/injection_points/Makefile b/src/test/modules/injection_points/Makefile
index 2cd7d87c533..f7663859fe2 100644
--- a/src/test/modules/injection_points/Makefile
+++ b/src/test/modules/injection_points/Makefile
@@ -15,6 +15,7 @@ REGRESS_OPTS = --dlpath=$(top_builddir)/src/test/regress
 ISOLATION = basic \
 	    inplace \
 	    repack \
+	    repack_deadlock \
 	    repack_toast \
 	    syscache-update-pruned \
 	    heap_lock_update
diff --git a/src/test/modules/injection_points/expected/repack_deadlock.out b/src/test/modules/injection_points/expected/repack_deadlock.out
new file mode 100644
index 00000000000..42505d10933
--- /dev/null
+++ b/src/test/modules/injection_points/expected/repack_deadlock.out
@@ -0,0 +1,63 @@
+Parsed test spec with 2 sessions
+
+starting permutation: wait_before_lock add_column wakeup_before_lock check1
+injection_points_attach
+-----------------------
+                       
+(1 row)
+
+step wait_before_lock: 
+	REPACK (CONCURRENTLY) repack_deadlock USING INDEX repack_deadlock_pkey;
+ <waiting ...>
+step add_column: 
+	alter table repack_deadlock add column noise text;
+ <waiting ...>
+step add_column: <... completed>
+ERROR:  oops, we're blocked by REPACK, time to die
+step wakeup_before_lock: 
+	SELECT injection_points_wakeup('repack-concurrently-before-lock');
+
+injection_points_wakeup
+-----------------------
+                       
+(1 row)
+
+step wait_before_lock: <... completed>
+step check1: 
+	INSERT INTO relfilenodes(node)
+	SELECT relfilenode FROM pg_class WHERE relname='repack_deadlock';
+
+	SELECT count(DISTINCT node) FROM relfilenodes;
+
+	SELECT i, j FROM repack_deadlock ORDER BY i, j;
+
+	INSERT INTO data_s1(i, j)
+	SELECT i, j FROM repack_deadlock;
+
+	SELECT count(*)
+	FROM data_s1 d1 FULL JOIN data_s2 d2 USING (i, j)
+	WHERE d1.i ISNULL OR d2.i ISNULL;
+
+count
+-----
+    1
+(1 row)
+
+i|j
+-+-
+1|1
+2|2
+3|3
+4|4
+(4 rows)
+
+count
+-----
+    4
+(1 row)
+
+injection_points_detach
+-----------------------
+                       
+(1 row)
+
diff --git a/src/test/modules/injection_points/meson.build b/src/test/modules/injection_points/meson.build
index a414abb924b..1cd88d6db65 100644
--- a/src/test/modules/injection_points/meson.build
+++ b/src/test/modules/injection_points/meson.build
@@ -46,6 +46,7 @@ tests += {
       'basic',
       'inplace',
       'repack',
+      'repack_deadlock',
       'repack_toast',
       'syscache-update-pruned',
       'heap_lock_update',
diff --git a/src/test/modules/injection_points/specs/repack_deadlock.spec b/src/test/modules/injection_points/specs/repack_deadlock.spec
new file mode 100644
index 00000000000..9d23a6588c2
--- /dev/null
+++ b/src/test/modules/injection_points/specs/repack_deadlock.spec
@@ -0,0 +1,83 @@
+# Test REPACK with a concurrent transaction that would cause a deadlock
+setup
+{
+	CREATE EXTENSION injection_points;
+
+	CREATE TABLE repack_deadlock(i int PRIMARY KEY, j int);
+	INSERT INTO repack_deadlock(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4);
+
+	CREATE TABLE relfilenodes(node oid);
+
+	CREATE TABLE data_s1(i int, j int);
+	CREATE TABLE data_s2(i int, j int);
+}
+
+teardown
+{
+	DROP TABLE repack_deadlock;
+	DROP EXTENSION injection_points;
+
+	DROP TABLE relfilenodes;
+	DROP TABLE data_s1;
+	DROP TABLE data_s2;
+}
+
+session s1
+setup
+{
+	SELECT injection_points_set_local();
+	SELECT injection_points_attach('repack-concurrently-before-lock', 'wait');
+}
+# Perform the initial load and wait for s2 to do some data changes.
+step wait_before_lock
+{
+	REPACK (CONCURRENTLY) repack_deadlock USING INDEX repack_deadlock_pkey;
+}
+# Check the table from the perspective of s1.
+#
+# Besides the contents, we also check that relfilenode has changed.
+
+# Have each session write the contents into a table and use FULL JOIN to check
+# if the outputs are identical.
+step check1
+{
+	INSERT INTO relfilenodes(node)
+	SELECT relfilenode FROM pg_class WHERE relname='repack_deadlock';
+
+	SELECT count(DISTINCT node) FROM relfilenodes;
+
+	SELECT i, j FROM repack_deadlock ORDER BY i, j;
+
+	INSERT INTO data_s1(i, j)
+	SELECT i, j FROM repack_deadlock;
+
+	SELECT count(*)
+	FROM data_s1 d1 FULL JOIN data_s2 d2 USING (i, j)
+	WHERE d1.i ISNULL OR d2.i ISNULL;
+}
+teardown
+{
+	SELECT injection_points_detach('repack-concurrently-before-lock');
+}
+
+session s2
+# Change the existing data. UPDATE changes both key and non-key columns. Also
+# update one row twice to test whether tuple version generated by this session
+# can be found.
+step add_column
+{
+	alter table repack_deadlock add column noise text;
+}
+
+step wakeup_before_lock
+{
+	SELECT injection_points_wakeup('repack-concurrently-before-lock');
+}
+
+# Test if data changes introduced while one session is performing REPACK
+# CONCURRENTLY find their way into the table.
+permutation
+	wait_before_lock
+	add_column
+	wakeup_before_lock
+	check1
-- 
2.47.3


--fot5plqmb33ey33c--





^ permalink  raw  reply  [nested|flat] 89+ messages in thread

* [PATCH v44b 4/4] hack deadlock detector
@ 2026-03-25 19:35  Álvaro Herrera <[email protected]>
  0 siblings, 0 replies; 89+ messages in thread

From: Álvaro Herrera @ 2026-03-25 19:35 UTC (permalink / raw)

---
 src/backend/commands/cluster.c                | 10 +++
 src/backend/storage/lmgr/deadlock.c           | 12 +++
 src/include/storage/proc.h                    |  1 +
 src/test/modules/injection_points/Makefile    |  1 +
 .../expected/repack_deadlock.out              | 63 ++++++++++++++
 src/test/modules/injection_points/meson.build |  1 +
 .../specs/repack_deadlock.spec                | 83 +++++++++++++++++++
 7 files changed, 171 insertions(+)
 create mode 100644 src/test/modules/injection_points/expected/repack_deadlock.out
 create mode 100644 src/test/modules/injection_points/specs/repack_deadlock.spec

diff --git a/src/backend/commands/cluster.c b/src/backend/commands/cluster.c
index 2c3058ba10d..d5b1dfbff69 100644
--- a/src/backend/commands/cluster.c
+++ b/src/backend/commands/cluster.c
@@ -372,6 +372,16 @@ ExecRepack(ParseState *pstate, RepackStmt *stmt, bool isTopLevel)
 	/* Determine the lock mode to use. */
 	lockmode = RepackLockLevel((params.options & CLUOPT_CONCURRENT) != 0);
 
+	/*
+	 * If in concurrent mode, set the PROC_IN_CONCURRENT_REPACK flag.  This
+	 * makes the deadlock checker cause anyone that would conflict with us
+	 * to error out.  It's important to set this flag ahead of actually locking
+	 * the relation; it won't of course affect anyone until we do have a lock
+	 * that others can conflict with.
+	 */
+	if ((params.options & CLUOPT_CONCURRENT) != 0)
+		MyProc->statusFlags |= PROC_IN_CONCURRENT_REPACK;
+
 	/*
 	 * If a single relation is specified, process it and we're done ... unless
 	 * the relation is a partitioned table, in which case we fall through.
diff --git a/src/backend/storage/lmgr/deadlock.c b/src/backend/storage/lmgr/deadlock.c
index b8962d875b6..10266e384aa 100644
--- a/src/backend/storage/lmgr/deadlock.c
+++ b/src/backend/storage/lmgr/deadlock.c
@@ -620,6 +620,18 @@ FindLockCycleRecurseMember(PGPROC *checkProc,
 						proc->statusFlags & PROC_IS_AUTOVACUUM)
 						blocking_autovacuum_proc = proc;
 
+					/*
+					 * If we note that we're blocked by some process running
+					 * REPACK (CONCURRENTLY), just fail.  That process is
+					 * going to upgrade its lock at some point, and it would
+					 * be inappropriate for any other process to cause that
+					 * to fail.
+					 */
+					if (checkProc == MyProc &&
+						proc->statusFlags & PROC_IN_CONCURRENT_REPACK)
+						/* not a serious error message proposal */
+						elog(ERROR, "oops, we're blocked by REPACK, time to die");
+
 					/* We're done looking at this proclock */
 					break;
 				}
diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h
index 1dad125706e..4e340ead455 100644
--- a/src/include/storage/proc.h
+++ b/src/include/storage/proc.h
@@ -69,6 +69,7 @@ struct XidCache
 #define		PROC_AFFECTS_ALL_HORIZONS	0x20	/* this proc's xmin must be
 												 * included in vacuum horizons
 												 * in all databases */
+#define		PROC_IN_CONCURRENT_REPACK	0x40	/* REPACK (CONCURRENTLY) */
 
 /* flags reset at EOXact */
 #define		PROC_VACUUM_STATE_MASK \
diff --git a/src/test/modules/injection_points/Makefile b/src/test/modules/injection_points/Makefile
index 2cd7d87c533..f7663859fe2 100644
--- a/src/test/modules/injection_points/Makefile
+++ b/src/test/modules/injection_points/Makefile
@@ -15,6 +15,7 @@ REGRESS_OPTS = --dlpath=$(top_builddir)/src/test/regress
 ISOLATION = basic \
 	    inplace \
 	    repack \
+	    repack_deadlock \
 	    repack_toast \
 	    syscache-update-pruned \
 	    heap_lock_update
diff --git a/src/test/modules/injection_points/expected/repack_deadlock.out b/src/test/modules/injection_points/expected/repack_deadlock.out
new file mode 100644
index 00000000000..42505d10933
--- /dev/null
+++ b/src/test/modules/injection_points/expected/repack_deadlock.out
@@ -0,0 +1,63 @@
+Parsed test spec with 2 sessions
+
+starting permutation: wait_before_lock add_column wakeup_before_lock check1
+injection_points_attach
+-----------------------
+                       
+(1 row)
+
+step wait_before_lock: 
+	REPACK (CONCURRENTLY) repack_deadlock USING INDEX repack_deadlock_pkey;
+ <waiting ...>
+step add_column: 
+	alter table repack_deadlock add column noise text;
+ <waiting ...>
+step add_column: <... completed>
+ERROR:  oops, we're blocked by REPACK, time to die
+step wakeup_before_lock: 
+	SELECT injection_points_wakeup('repack-concurrently-before-lock');
+
+injection_points_wakeup
+-----------------------
+                       
+(1 row)
+
+step wait_before_lock: <... completed>
+step check1: 
+	INSERT INTO relfilenodes(node)
+	SELECT relfilenode FROM pg_class WHERE relname='repack_deadlock';
+
+	SELECT count(DISTINCT node) FROM relfilenodes;
+
+	SELECT i, j FROM repack_deadlock ORDER BY i, j;
+
+	INSERT INTO data_s1(i, j)
+	SELECT i, j FROM repack_deadlock;
+
+	SELECT count(*)
+	FROM data_s1 d1 FULL JOIN data_s2 d2 USING (i, j)
+	WHERE d1.i ISNULL OR d2.i ISNULL;
+
+count
+-----
+    1
+(1 row)
+
+i|j
+-+-
+1|1
+2|2
+3|3
+4|4
+(4 rows)
+
+count
+-----
+    4
+(1 row)
+
+injection_points_detach
+-----------------------
+                       
+(1 row)
+
diff --git a/src/test/modules/injection_points/meson.build b/src/test/modules/injection_points/meson.build
index a414abb924b..1cd88d6db65 100644
--- a/src/test/modules/injection_points/meson.build
+++ b/src/test/modules/injection_points/meson.build
@@ -46,6 +46,7 @@ tests += {
       'basic',
       'inplace',
       'repack',
+      'repack_deadlock',
       'repack_toast',
       'syscache-update-pruned',
       'heap_lock_update',
diff --git a/src/test/modules/injection_points/specs/repack_deadlock.spec b/src/test/modules/injection_points/specs/repack_deadlock.spec
new file mode 100644
index 00000000000..9d23a6588c2
--- /dev/null
+++ b/src/test/modules/injection_points/specs/repack_deadlock.spec
@@ -0,0 +1,83 @@
+# Test REPACK with a concurrent transaction that would cause a deadlock
+setup
+{
+	CREATE EXTENSION injection_points;
+
+	CREATE TABLE repack_deadlock(i int PRIMARY KEY, j int);
+	INSERT INTO repack_deadlock(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4);
+
+	CREATE TABLE relfilenodes(node oid);
+
+	CREATE TABLE data_s1(i int, j int);
+	CREATE TABLE data_s2(i int, j int);
+}
+
+teardown
+{
+	DROP TABLE repack_deadlock;
+	DROP EXTENSION injection_points;
+
+	DROP TABLE relfilenodes;
+	DROP TABLE data_s1;
+	DROP TABLE data_s2;
+}
+
+session s1
+setup
+{
+	SELECT injection_points_set_local();
+	SELECT injection_points_attach('repack-concurrently-before-lock', 'wait');
+}
+# Perform the initial load and wait for s2 to do some data changes.
+step wait_before_lock
+{
+	REPACK (CONCURRENTLY) repack_deadlock USING INDEX repack_deadlock_pkey;
+}
+# Check the table from the perspective of s1.
+#
+# Besides the contents, we also check that relfilenode has changed.
+
+# Have each session write the contents into a table and use FULL JOIN to check
+# if the outputs are identical.
+step check1
+{
+	INSERT INTO relfilenodes(node)
+	SELECT relfilenode FROM pg_class WHERE relname='repack_deadlock';
+
+	SELECT count(DISTINCT node) FROM relfilenodes;
+
+	SELECT i, j FROM repack_deadlock ORDER BY i, j;
+
+	INSERT INTO data_s1(i, j)
+	SELECT i, j FROM repack_deadlock;
+
+	SELECT count(*)
+	FROM data_s1 d1 FULL JOIN data_s2 d2 USING (i, j)
+	WHERE d1.i ISNULL OR d2.i ISNULL;
+}
+teardown
+{
+	SELECT injection_points_detach('repack-concurrently-before-lock');
+}
+
+session s2
+# Change the existing data. UPDATE changes both key and non-key columns. Also
+# update one row twice to test whether tuple version generated by this session
+# can be found.
+step add_column
+{
+	alter table repack_deadlock add column noise text;
+}
+
+step wakeup_before_lock
+{
+	SELECT injection_points_wakeup('repack-concurrently-before-lock');
+}
+
+# Test if data changes introduced while one session is performing REPACK
+# CONCURRENTLY find their way into the table.
+permutation
+	wait_before_lock
+	add_column
+	wakeup_before_lock
+	check1
-- 
2.47.3


--fot5plqmb33ey33c--





^ permalink  raw  reply  [nested|flat] 89+ messages in thread

* [PATCH v44b 4/4] hack deadlock detector
@ 2026-03-25 19:35  Álvaro Herrera <[email protected]>
  0 siblings, 0 replies; 89+ messages in thread

From: Álvaro Herrera @ 2026-03-25 19:35 UTC (permalink / raw)

---
 src/backend/commands/cluster.c                | 10 +++
 src/backend/storage/lmgr/deadlock.c           | 12 +++
 src/include/storage/proc.h                    |  1 +
 src/test/modules/injection_points/Makefile    |  1 +
 .../expected/repack_deadlock.out              | 63 ++++++++++++++
 src/test/modules/injection_points/meson.build |  1 +
 .../specs/repack_deadlock.spec                | 83 +++++++++++++++++++
 7 files changed, 171 insertions(+)
 create mode 100644 src/test/modules/injection_points/expected/repack_deadlock.out
 create mode 100644 src/test/modules/injection_points/specs/repack_deadlock.spec

diff --git a/src/backend/commands/cluster.c b/src/backend/commands/cluster.c
index 2c3058ba10d..d5b1dfbff69 100644
--- a/src/backend/commands/cluster.c
+++ b/src/backend/commands/cluster.c
@@ -372,6 +372,16 @@ ExecRepack(ParseState *pstate, RepackStmt *stmt, bool isTopLevel)
 	/* Determine the lock mode to use. */
 	lockmode = RepackLockLevel((params.options & CLUOPT_CONCURRENT) != 0);
 
+	/*
+	 * If in concurrent mode, set the PROC_IN_CONCURRENT_REPACK flag.  This
+	 * makes the deadlock checker cause anyone that would conflict with us
+	 * to error out.  It's important to set this flag ahead of actually locking
+	 * the relation; it won't of course affect anyone until we do have a lock
+	 * that others can conflict with.
+	 */
+	if ((params.options & CLUOPT_CONCURRENT) != 0)
+		MyProc->statusFlags |= PROC_IN_CONCURRENT_REPACK;
+
 	/*
 	 * If a single relation is specified, process it and we're done ... unless
 	 * the relation is a partitioned table, in which case we fall through.
diff --git a/src/backend/storage/lmgr/deadlock.c b/src/backend/storage/lmgr/deadlock.c
index b8962d875b6..10266e384aa 100644
--- a/src/backend/storage/lmgr/deadlock.c
+++ b/src/backend/storage/lmgr/deadlock.c
@@ -620,6 +620,18 @@ FindLockCycleRecurseMember(PGPROC *checkProc,
 						proc->statusFlags & PROC_IS_AUTOVACUUM)
 						blocking_autovacuum_proc = proc;
 
+					/*
+					 * If we note that we're blocked by some process running
+					 * REPACK (CONCURRENTLY), just fail.  That process is
+					 * going to upgrade its lock at some point, and it would
+					 * be inappropriate for any other process to cause that
+					 * to fail.
+					 */
+					if (checkProc == MyProc &&
+						proc->statusFlags & PROC_IN_CONCURRENT_REPACK)
+						/* not a serious error message proposal */
+						elog(ERROR, "oops, we're blocked by REPACK, time to die");
+
 					/* We're done looking at this proclock */
 					break;
 				}
diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h
index 1dad125706e..4e340ead455 100644
--- a/src/include/storage/proc.h
+++ b/src/include/storage/proc.h
@@ -69,6 +69,7 @@ struct XidCache
 #define		PROC_AFFECTS_ALL_HORIZONS	0x20	/* this proc's xmin must be
 												 * included in vacuum horizons
 												 * in all databases */
+#define		PROC_IN_CONCURRENT_REPACK	0x40	/* REPACK (CONCURRENTLY) */
 
 /* flags reset at EOXact */
 #define		PROC_VACUUM_STATE_MASK \
diff --git a/src/test/modules/injection_points/Makefile b/src/test/modules/injection_points/Makefile
index 2cd7d87c533..f7663859fe2 100644
--- a/src/test/modules/injection_points/Makefile
+++ b/src/test/modules/injection_points/Makefile
@@ -15,6 +15,7 @@ REGRESS_OPTS = --dlpath=$(top_builddir)/src/test/regress
 ISOLATION = basic \
 	    inplace \
 	    repack \
+	    repack_deadlock \
 	    repack_toast \
 	    syscache-update-pruned \
 	    heap_lock_update
diff --git a/src/test/modules/injection_points/expected/repack_deadlock.out b/src/test/modules/injection_points/expected/repack_deadlock.out
new file mode 100644
index 00000000000..42505d10933
--- /dev/null
+++ b/src/test/modules/injection_points/expected/repack_deadlock.out
@@ -0,0 +1,63 @@
+Parsed test spec with 2 sessions
+
+starting permutation: wait_before_lock add_column wakeup_before_lock check1
+injection_points_attach
+-----------------------
+                       
+(1 row)
+
+step wait_before_lock: 
+	REPACK (CONCURRENTLY) repack_deadlock USING INDEX repack_deadlock_pkey;
+ <waiting ...>
+step add_column: 
+	alter table repack_deadlock add column noise text;
+ <waiting ...>
+step add_column: <... completed>
+ERROR:  oops, we're blocked by REPACK, time to die
+step wakeup_before_lock: 
+	SELECT injection_points_wakeup('repack-concurrently-before-lock');
+
+injection_points_wakeup
+-----------------------
+                       
+(1 row)
+
+step wait_before_lock: <... completed>
+step check1: 
+	INSERT INTO relfilenodes(node)
+	SELECT relfilenode FROM pg_class WHERE relname='repack_deadlock';
+
+	SELECT count(DISTINCT node) FROM relfilenodes;
+
+	SELECT i, j FROM repack_deadlock ORDER BY i, j;
+
+	INSERT INTO data_s1(i, j)
+	SELECT i, j FROM repack_deadlock;
+
+	SELECT count(*)
+	FROM data_s1 d1 FULL JOIN data_s2 d2 USING (i, j)
+	WHERE d1.i ISNULL OR d2.i ISNULL;
+
+count
+-----
+    1
+(1 row)
+
+i|j
+-+-
+1|1
+2|2
+3|3
+4|4
+(4 rows)
+
+count
+-----
+    4
+(1 row)
+
+injection_points_detach
+-----------------------
+                       
+(1 row)
+
diff --git a/src/test/modules/injection_points/meson.build b/src/test/modules/injection_points/meson.build
index a414abb924b..1cd88d6db65 100644
--- a/src/test/modules/injection_points/meson.build
+++ b/src/test/modules/injection_points/meson.build
@@ -46,6 +46,7 @@ tests += {
       'basic',
       'inplace',
       'repack',
+      'repack_deadlock',
       'repack_toast',
       'syscache-update-pruned',
       'heap_lock_update',
diff --git a/src/test/modules/injection_points/specs/repack_deadlock.spec b/src/test/modules/injection_points/specs/repack_deadlock.spec
new file mode 100644
index 00000000000..9d23a6588c2
--- /dev/null
+++ b/src/test/modules/injection_points/specs/repack_deadlock.spec
@@ -0,0 +1,83 @@
+# Test REPACK with a concurrent transaction that would cause a deadlock
+setup
+{
+	CREATE EXTENSION injection_points;
+
+	CREATE TABLE repack_deadlock(i int PRIMARY KEY, j int);
+	INSERT INTO repack_deadlock(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4);
+
+	CREATE TABLE relfilenodes(node oid);
+
+	CREATE TABLE data_s1(i int, j int);
+	CREATE TABLE data_s2(i int, j int);
+}
+
+teardown
+{
+	DROP TABLE repack_deadlock;
+	DROP EXTENSION injection_points;
+
+	DROP TABLE relfilenodes;
+	DROP TABLE data_s1;
+	DROP TABLE data_s2;
+}
+
+session s1
+setup
+{
+	SELECT injection_points_set_local();
+	SELECT injection_points_attach('repack-concurrently-before-lock', 'wait');
+}
+# Perform the initial load and wait for s2 to do some data changes.
+step wait_before_lock
+{
+	REPACK (CONCURRENTLY) repack_deadlock USING INDEX repack_deadlock_pkey;
+}
+# Check the table from the perspective of s1.
+#
+# Besides the contents, we also check that relfilenode has changed.
+
+# Have each session write the contents into a table and use FULL JOIN to check
+# if the outputs are identical.
+step check1
+{
+	INSERT INTO relfilenodes(node)
+	SELECT relfilenode FROM pg_class WHERE relname='repack_deadlock';
+
+	SELECT count(DISTINCT node) FROM relfilenodes;
+
+	SELECT i, j FROM repack_deadlock ORDER BY i, j;
+
+	INSERT INTO data_s1(i, j)
+	SELECT i, j FROM repack_deadlock;
+
+	SELECT count(*)
+	FROM data_s1 d1 FULL JOIN data_s2 d2 USING (i, j)
+	WHERE d1.i ISNULL OR d2.i ISNULL;
+}
+teardown
+{
+	SELECT injection_points_detach('repack-concurrently-before-lock');
+}
+
+session s2
+# Change the existing data. UPDATE changes both key and non-key columns. Also
+# update one row twice to test whether tuple version generated by this session
+# can be found.
+step add_column
+{
+	alter table repack_deadlock add column noise text;
+}
+
+step wakeup_before_lock
+{
+	SELECT injection_points_wakeup('repack-concurrently-before-lock');
+}
+
+# Test if data changes introduced while one session is performing REPACK
+# CONCURRENTLY find their way into the table.
+permutation
+	wait_before_lock
+	add_column
+	wakeup_before_lock
+	check1
-- 
2.47.3


--fot5plqmb33ey33c--





^ permalink  raw  reply  [nested|flat] 89+ messages in thread

* [PATCH v44b 4/4] hack deadlock detector
@ 2026-03-25 19:35  Álvaro Herrera <[email protected]>
  0 siblings, 0 replies; 89+ messages in thread

From: Álvaro Herrera @ 2026-03-25 19:35 UTC (permalink / raw)

---
 src/backend/commands/cluster.c                | 10 +++
 src/backend/storage/lmgr/deadlock.c           | 12 +++
 src/include/storage/proc.h                    |  1 +
 src/test/modules/injection_points/Makefile    |  1 +
 .../expected/repack_deadlock.out              | 63 ++++++++++++++
 src/test/modules/injection_points/meson.build |  1 +
 .../specs/repack_deadlock.spec                | 83 +++++++++++++++++++
 7 files changed, 171 insertions(+)
 create mode 100644 src/test/modules/injection_points/expected/repack_deadlock.out
 create mode 100644 src/test/modules/injection_points/specs/repack_deadlock.spec

diff --git a/src/backend/commands/cluster.c b/src/backend/commands/cluster.c
index 2c3058ba10d..d5b1dfbff69 100644
--- a/src/backend/commands/cluster.c
+++ b/src/backend/commands/cluster.c
@@ -372,6 +372,16 @@ ExecRepack(ParseState *pstate, RepackStmt *stmt, bool isTopLevel)
 	/* Determine the lock mode to use. */
 	lockmode = RepackLockLevel((params.options & CLUOPT_CONCURRENT) != 0);
 
+	/*
+	 * If in concurrent mode, set the PROC_IN_CONCURRENT_REPACK flag.  This
+	 * makes the deadlock checker cause anyone that would conflict with us
+	 * to error out.  It's important to set this flag ahead of actually locking
+	 * the relation; it won't of course affect anyone until we do have a lock
+	 * that others can conflict with.
+	 */
+	if ((params.options & CLUOPT_CONCURRENT) != 0)
+		MyProc->statusFlags |= PROC_IN_CONCURRENT_REPACK;
+
 	/*
 	 * If a single relation is specified, process it and we're done ... unless
 	 * the relation is a partitioned table, in which case we fall through.
diff --git a/src/backend/storage/lmgr/deadlock.c b/src/backend/storage/lmgr/deadlock.c
index b8962d875b6..10266e384aa 100644
--- a/src/backend/storage/lmgr/deadlock.c
+++ b/src/backend/storage/lmgr/deadlock.c
@@ -620,6 +620,18 @@ FindLockCycleRecurseMember(PGPROC *checkProc,
 						proc->statusFlags & PROC_IS_AUTOVACUUM)
 						blocking_autovacuum_proc = proc;
 
+					/*
+					 * If we note that we're blocked by some process running
+					 * REPACK (CONCURRENTLY), just fail.  That process is
+					 * going to upgrade its lock at some point, and it would
+					 * be inappropriate for any other process to cause that
+					 * to fail.
+					 */
+					if (checkProc == MyProc &&
+						proc->statusFlags & PROC_IN_CONCURRENT_REPACK)
+						/* not a serious error message proposal */
+						elog(ERROR, "oops, we're blocked by REPACK, time to die");
+
 					/* We're done looking at this proclock */
 					break;
 				}
diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h
index 1dad125706e..4e340ead455 100644
--- a/src/include/storage/proc.h
+++ b/src/include/storage/proc.h
@@ -69,6 +69,7 @@ struct XidCache
 #define		PROC_AFFECTS_ALL_HORIZONS	0x20	/* this proc's xmin must be
 												 * included in vacuum horizons
 												 * in all databases */
+#define		PROC_IN_CONCURRENT_REPACK	0x40	/* REPACK (CONCURRENTLY) */
 
 /* flags reset at EOXact */
 #define		PROC_VACUUM_STATE_MASK \
diff --git a/src/test/modules/injection_points/Makefile b/src/test/modules/injection_points/Makefile
index 2cd7d87c533..f7663859fe2 100644
--- a/src/test/modules/injection_points/Makefile
+++ b/src/test/modules/injection_points/Makefile
@@ -15,6 +15,7 @@ REGRESS_OPTS = --dlpath=$(top_builddir)/src/test/regress
 ISOLATION = basic \
 	    inplace \
 	    repack \
+	    repack_deadlock \
 	    repack_toast \
 	    syscache-update-pruned \
 	    heap_lock_update
diff --git a/src/test/modules/injection_points/expected/repack_deadlock.out b/src/test/modules/injection_points/expected/repack_deadlock.out
new file mode 100644
index 00000000000..42505d10933
--- /dev/null
+++ b/src/test/modules/injection_points/expected/repack_deadlock.out
@@ -0,0 +1,63 @@
+Parsed test spec with 2 sessions
+
+starting permutation: wait_before_lock add_column wakeup_before_lock check1
+injection_points_attach
+-----------------------
+                       
+(1 row)
+
+step wait_before_lock: 
+	REPACK (CONCURRENTLY) repack_deadlock USING INDEX repack_deadlock_pkey;
+ <waiting ...>
+step add_column: 
+	alter table repack_deadlock add column noise text;
+ <waiting ...>
+step add_column: <... completed>
+ERROR:  oops, we're blocked by REPACK, time to die
+step wakeup_before_lock: 
+	SELECT injection_points_wakeup('repack-concurrently-before-lock');
+
+injection_points_wakeup
+-----------------------
+                       
+(1 row)
+
+step wait_before_lock: <... completed>
+step check1: 
+	INSERT INTO relfilenodes(node)
+	SELECT relfilenode FROM pg_class WHERE relname='repack_deadlock';
+
+	SELECT count(DISTINCT node) FROM relfilenodes;
+
+	SELECT i, j FROM repack_deadlock ORDER BY i, j;
+
+	INSERT INTO data_s1(i, j)
+	SELECT i, j FROM repack_deadlock;
+
+	SELECT count(*)
+	FROM data_s1 d1 FULL JOIN data_s2 d2 USING (i, j)
+	WHERE d1.i ISNULL OR d2.i ISNULL;
+
+count
+-----
+    1
+(1 row)
+
+i|j
+-+-
+1|1
+2|2
+3|3
+4|4
+(4 rows)
+
+count
+-----
+    4
+(1 row)
+
+injection_points_detach
+-----------------------
+                       
+(1 row)
+
diff --git a/src/test/modules/injection_points/meson.build b/src/test/modules/injection_points/meson.build
index a414abb924b..1cd88d6db65 100644
--- a/src/test/modules/injection_points/meson.build
+++ b/src/test/modules/injection_points/meson.build
@@ -46,6 +46,7 @@ tests += {
       'basic',
       'inplace',
       'repack',
+      'repack_deadlock',
       'repack_toast',
       'syscache-update-pruned',
       'heap_lock_update',
diff --git a/src/test/modules/injection_points/specs/repack_deadlock.spec b/src/test/modules/injection_points/specs/repack_deadlock.spec
new file mode 100644
index 00000000000..9d23a6588c2
--- /dev/null
+++ b/src/test/modules/injection_points/specs/repack_deadlock.spec
@@ -0,0 +1,83 @@
+# Test REPACK with a concurrent transaction that would cause a deadlock
+setup
+{
+	CREATE EXTENSION injection_points;
+
+	CREATE TABLE repack_deadlock(i int PRIMARY KEY, j int);
+	INSERT INTO repack_deadlock(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4);
+
+	CREATE TABLE relfilenodes(node oid);
+
+	CREATE TABLE data_s1(i int, j int);
+	CREATE TABLE data_s2(i int, j int);
+}
+
+teardown
+{
+	DROP TABLE repack_deadlock;
+	DROP EXTENSION injection_points;
+
+	DROP TABLE relfilenodes;
+	DROP TABLE data_s1;
+	DROP TABLE data_s2;
+}
+
+session s1
+setup
+{
+	SELECT injection_points_set_local();
+	SELECT injection_points_attach('repack-concurrently-before-lock', 'wait');
+}
+# Perform the initial load and wait for s2 to do some data changes.
+step wait_before_lock
+{
+	REPACK (CONCURRENTLY) repack_deadlock USING INDEX repack_deadlock_pkey;
+}
+# Check the table from the perspective of s1.
+#
+# Besides the contents, we also check that relfilenode has changed.
+
+# Have each session write the contents into a table and use FULL JOIN to check
+# if the outputs are identical.
+step check1
+{
+	INSERT INTO relfilenodes(node)
+	SELECT relfilenode FROM pg_class WHERE relname='repack_deadlock';
+
+	SELECT count(DISTINCT node) FROM relfilenodes;
+
+	SELECT i, j FROM repack_deadlock ORDER BY i, j;
+
+	INSERT INTO data_s1(i, j)
+	SELECT i, j FROM repack_deadlock;
+
+	SELECT count(*)
+	FROM data_s1 d1 FULL JOIN data_s2 d2 USING (i, j)
+	WHERE d1.i ISNULL OR d2.i ISNULL;
+}
+teardown
+{
+	SELECT injection_points_detach('repack-concurrently-before-lock');
+}
+
+session s2
+# Change the existing data. UPDATE changes both key and non-key columns. Also
+# update one row twice to test whether tuple version generated by this session
+# can be found.
+step add_column
+{
+	alter table repack_deadlock add column noise text;
+}
+
+step wakeup_before_lock
+{
+	SELECT injection_points_wakeup('repack-concurrently-before-lock');
+}
+
+# Test if data changes introduced while one session is performing REPACK
+# CONCURRENTLY find their way into the table.
+permutation
+	wait_before_lock
+	add_column
+	wakeup_before_lock
+	check1
-- 
2.47.3


--fot5plqmb33ey33c--





^ permalink  raw  reply  [nested|flat] 89+ messages in thread

* [PATCH v44b 4/4] hack deadlock detector
@ 2026-03-25 19:35  Álvaro Herrera <[email protected]>
  0 siblings, 0 replies; 89+ messages in thread

From: Álvaro Herrera @ 2026-03-25 19:35 UTC (permalink / raw)

---
 src/backend/commands/cluster.c                | 10 +++
 src/backend/storage/lmgr/deadlock.c           | 12 +++
 src/include/storage/proc.h                    |  1 +
 src/test/modules/injection_points/Makefile    |  1 +
 .../expected/repack_deadlock.out              | 63 ++++++++++++++
 src/test/modules/injection_points/meson.build |  1 +
 .../specs/repack_deadlock.spec                | 83 +++++++++++++++++++
 7 files changed, 171 insertions(+)
 create mode 100644 src/test/modules/injection_points/expected/repack_deadlock.out
 create mode 100644 src/test/modules/injection_points/specs/repack_deadlock.spec

diff --git a/src/backend/commands/cluster.c b/src/backend/commands/cluster.c
index 2c3058ba10d..d5b1dfbff69 100644
--- a/src/backend/commands/cluster.c
+++ b/src/backend/commands/cluster.c
@@ -372,6 +372,16 @@ ExecRepack(ParseState *pstate, RepackStmt *stmt, bool isTopLevel)
 	/* Determine the lock mode to use. */
 	lockmode = RepackLockLevel((params.options & CLUOPT_CONCURRENT) != 0);
 
+	/*
+	 * If in concurrent mode, set the PROC_IN_CONCURRENT_REPACK flag.  This
+	 * makes the deadlock checker cause anyone that would conflict with us
+	 * to error out.  It's important to set this flag ahead of actually locking
+	 * the relation; it won't of course affect anyone until we do have a lock
+	 * that others can conflict with.
+	 */
+	if ((params.options & CLUOPT_CONCURRENT) != 0)
+		MyProc->statusFlags |= PROC_IN_CONCURRENT_REPACK;
+
 	/*
 	 * If a single relation is specified, process it and we're done ... unless
 	 * the relation is a partitioned table, in which case we fall through.
diff --git a/src/backend/storage/lmgr/deadlock.c b/src/backend/storage/lmgr/deadlock.c
index b8962d875b6..10266e384aa 100644
--- a/src/backend/storage/lmgr/deadlock.c
+++ b/src/backend/storage/lmgr/deadlock.c
@@ -620,6 +620,18 @@ FindLockCycleRecurseMember(PGPROC *checkProc,
 						proc->statusFlags & PROC_IS_AUTOVACUUM)
 						blocking_autovacuum_proc = proc;
 
+					/*
+					 * If we note that we're blocked by some process running
+					 * REPACK (CONCURRENTLY), just fail.  That process is
+					 * going to upgrade its lock at some point, and it would
+					 * be inappropriate for any other process to cause that
+					 * to fail.
+					 */
+					if (checkProc == MyProc &&
+						proc->statusFlags & PROC_IN_CONCURRENT_REPACK)
+						/* not a serious error message proposal */
+						elog(ERROR, "oops, we're blocked by REPACK, time to die");
+
 					/* We're done looking at this proclock */
 					break;
 				}
diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h
index 1dad125706e..4e340ead455 100644
--- a/src/include/storage/proc.h
+++ b/src/include/storage/proc.h
@@ -69,6 +69,7 @@ struct XidCache
 #define		PROC_AFFECTS_ALL_HORIZONS	0x20	/* this proc's xmin must be
 												 * included in vacuum horizons
 												 * in all databases */
+#define		PROC_IN_CONCURRENT_REPACK	0x40	/* REPACK (CONCURRENTLY) */
 
 /* flags reset at EOXact */
 #define		PROC_VACUUM_STATE_MASK \
diff --git a/src/test/modules/injection_points/Makefile b/src/test/modules/injection_points/Makefile
index 2cd7d87c533..f7663859fe2 100644
--- a/src/test/modules/injection_points/Makefile
+++ b/src/test/modules/injection_points/Makefile
@@ -15,6 +15,7 @@ REGRESS_OPTS = --dlpath=$(top_builddir)/src/test/regress
 ISOLATION = basic \
 	    inplace \
 	    repack \
+	    repack_deadlock \
 	    repack_toast \
 	    syscache-update-pruned \
 	    heap_lock_update
diff --git a/src/test/modules/injection_points/expected/repack_deadlock.out b/src/test/modules/injection_points/expected/repack_deadlock.out
new file mode 100644
index 00000000000..42505d10933
--- /dev/null
+++ b/src/test/modules/injection_points/expected/repack_deadlock.out
@@ -0,0 +1,63 @@
+Parsed test spec with 2 sessions
+
+starting permutation: wait_before_lock add_column wakeup_before_lock check1
+injection_points_attach
+-----------------------
+                       
+(1 row)
+
+step wait_before_lock: 
+	REPACK (CONCURRENTLY) repack_deadlock USING INDEX repack_deadlock_pkey;
+ <waiting ...>
+step add_column: 
+	alter table repack_deadlock add column noise text;
+ <waiting ...>
+step add_column: <... completed>
+ERROR:  oops, we're blocked by REPACK, time to die
+step wakeup_before_lock: 
+	SELECT injection_points_wakeup('repack-concurrently-before-lock');
+
+injection_points_wakeup
+-----------------------
+                       
+(1 row)
+
+step wait_before_lock: <... completed>
+step check1: 
+	INSERT INTO relfilenodes(node)
+	SELECT relfilenode FROM pg_class WHERE relname='repack_deadlock';
+
+	SELECT count(DISTINCT node) FROM relfilenodes;
+
+	SELECT i, j FROM repack_deadlock ORDER BY i, j;
+
+	INSERT INTO data_s1(i, j)
+	SELECT i, j FROM repack_deadlock;
+
+	SELECT count(*)
+	FROM data_s1 d1 FULL JOIN data_s2 d2 USING (i, j)
+	WHERE d1.i ISNULL OR d2.i ISNULL;
+
+count
+-----
+    1
+(1 row)
+
+i|j
+-+-
+1|1
+2|2
+3|3
+4|4
+(4 rows)
+
+count
+-----
+    4
+(1 row)
+
+injection_points_detach
+-----------------------
+                       
+(1 row)
+
diff --git a/src/test/modules/injection_points/meson.build b/src/test/modules/injection_points/meson.build
index a414abb924b..1cd88d6db65 100644
--- a/src/test/modules/injection_points/meson.build
+++ b/src/test/modules/injection_points/meson.build
@@ -46,6 +46,7 @@ tests += {
       'basic',
       'inplace',
       'repack',
+      'repack_deadlock',
       'repack_toast',
       'syscache-update-pruned',
       'heap_lock_update',
diff --git a/src/test/modules/injection_points/specs/repack_deadlock.spec b/src/test/modules/injection_points/specs/repack_deadlock.spec
new file mode 100644
index 00000000000..9d23a6588c2
--- /dev/null
+++ b/src/test/modules/injection_points/specs/repack_deadlock.spec
@@ -0,0 +1,83 @@
+# Test REPACK with a concurrent transaction that would cause a deadlock
+setup
+{
+	CREATE EXTENSION injection_points;
+
+	CREATE TABLE repack_deadlock(i int PRIMARY KEY, j int);
+	INSERT INTO repack_deadlock(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4);
+
+	CREATE TABLE relfilenodes(node oid);
+
+	CREATE TABLE data_s1(i int, j int);
+	CREATE TABLE data_s2(i int, j int);
+}
+
+teardown
+{
+	DROP TABLE repack_deadlock;
+	DROP EXTENSION injection_points;
+
+	DROP TABLE relfilenodes;
+	DROP TABLE data_s1;
+	DROP TABLE data_s2;
+}
+
+session s1
+setup
+{
+	SELECT injection_points_set_local();
+	SELECT injection_points_attach('repack-concurrently-before-lock', 'wait');
+}
+# Perform the initial load and wait for s2 to do some data changes.
+step wait_before_lock
+{
+	REPACK (CONCURRENTLY) repack_deadlock USING INDEX repack_deadlock_pkey;
+}
+# Check the table from the perspective of s1.
+#
+# Besides the contents, we also check that relfilenode has changed.
+
+# Have each session write the contents into a table and use FULL JOIN to check
+# if the outputs are identical.
+step check1
+{
+	INSERT INTO relfilenodes(node)
+	SELECT relfilenode FROM pg_class WHERE relname='repack_deadlock';
+
+	SELECT count(DISTINCT node) FROM relfilenodes;
+
+	SELECT i, j FROM repack_deadlock ORDER BY i, j;
+
+	INSERT INTO data_s1(i, j)
+	SELECT i, j FROM repack_deadlock;
+
+	SELECT count(*)
+	FROM data_s1 d1 FULL JOIN data_s2 d2 USING (i, j)
+	WHERE d1.i ISNULL OR d2.i ISNULL;
+}
+teardown
+{
+	SELECT injection_points_detach('repack-concurrently-before-lock');
+}
+
+session s2
+# Change the existing data. UPDATE changes both key and non-key columns. Also
+# update one row twice to test whether tuple version generated by this session
+# can be found.
+step add_column
+{
+	alter table repack_deadlock add column noise text;
+}
+
+step wakeup_before_lock
+{
+	SELECT injection_points_wakeup('repack-concurrently-before-lock');
+}
+
+# Test if data changes introduced while one session is performing REPACK
+# CONCURRENTLY find their way into the table.
+permutation
+	wait_before_lock
+	add_column
+	wakeup_before_lock
+	check1
-- 
2.47.3


--fot5plqmb33ey33c--





^ permalink  raw  reply  [nested|flat] 89+ messages in thread

* [PATCH v44b 4/4] hack deadlock detector
@ 2026-03-25 19:35  Álvaro Herrera <[email protected]>
  0 siblings, 0 replies; 89+ messages in thread

From: Álvaro Herrera @ 2026-03-25 19:35 UTC (permalink / raw)

---
 src/backend/commands/cluster.c                | 10 +++
 src/backend/storage/lmgr/deadlock.c           | 12 +++
 src/include/storage/proc.h                    |  1 +
 src/test/modules/injection_points/Makefile    |  1 +
 .../expected/repack_deadlock.out              | 63 ++++++++++++++
 src/test/modules/injection_points/meson.build |  1 +
 .../specs/repack_deadlock.spec                | 83 +++++++++++++++++++
 7 files changed, 171 insertions(+)
 create mode 100644 src/test/modules/injection_points/expected/repack_deadlock.out
 create mode 100644 src/test/modules/injection_points/specs/repack_deadlock.spec

diff --git a/src/backend/commands/cluster.c b/src/backend/commands/cluster.c
index 2c3058ba10d..d5b1dfbff69 100644
--- a/src/backend/commands/cluster.c
+++ b/src/backend/commands/cluster.c
@@ -372,6 +372,16 @@ ExecRepack(ParseState *pstate, RepackStmt *stmt, bool isTopLevel)
 	/* Determine the lock mode to use. */
 	lockmode = RepackLockLevel((params.options & CLUOPT_CONCURRENT) != 0);
 
+	/*
+	 * If in concurrent mode, set the PROC_IN_CONCURRENT_REPACK flag.  This
+	 * makes the deadlock checker cause anyone that would conflict with us
+	 * to error out.  It's important to set this flag ahead of actually locking
+	 * the relation; it won't of course affect anyone until we do have a lock
+	 * that others can conflict with.
+	 */
+	if ((params.options & CLUOPT_CONCURRENT) != 0)
+		MyProc->statusFlags |= PROC_IN_CONCURRENT_REPACK;
+
 	/*
 	 * If a single relation is specified, process it and we're done ... unless
 	 * the relation is a partitioned table, in which case we fall through.
diff --git a/src/backend/storage/lmgr/deadlock.c b/src/backend/storage/lmgr/deadlock.c
index b8962d875b6..10266e384aa 100644
--- a/src/backend/storage/lmgr/deadlock.c
+++ b/src/backend/storage/lmgr/deadlock.c
@@ -620,6 +620,18 @@ FindLockCycleRecurseMember(PGPROC *checkProc,
 						proc->statusFlags & PROC_IS_AUTOVACUUM)
 						blocking_autovacuum_proc = proc;
 
+					/*
+					 * If we note that we're blocked by some process running
+					 * REPACK (CONCURRENTLY), just fail.  That process is
+					 * going to upgrade its lock at some point, and it would
+					 * be inappropriate for any other process to cause that
+					 * to fail.
+					 */
+					if (checkProc == MyProc &&
+						proc->statusFlags & PROC_IN_CONCURRENT_REPACK)
+						/* not a serious error message proposal */
+						elog(ERROR, "oops, we're blocked by REPACK, time to die");
+
 					/* We're done looking at this proclock */
 					break;
 				}
diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h
index 1dad125706e..4e340ead455 100644
--- a/src/include/storage/proc.h
+++ b/src/include/storage/proc.h
@@ -69,6 +69,7 @@ struct XidCache
 #define		PROC_AFFECTS_ALL_HORIZONS	0x20	/* this proc's xmin must be
 												 * included in vacuum horizons
 												 * in all databases */
+#define		PROC_IN_CONCURRENT_REPACK	0x40	/* REPACK (CONCURRENTLY) */
 
 /* flags reset at EOXact */
 #define		PROC_VACUUM_STATE_MASK \
diff --git a/src/test/modules/injection_points/Makefile b/src/test/modules/injection_points/Makefile
index 2cd7d87c533..f7663859fe2 100644
--- a/src/test/modules/injection_points/Makefile
+++ b/src/test/modules/injection_points/Makefile
@@ -15,6 +15,7 @@ REGRESS_OPTS = --dlpath=$(top_builddir)/src/test/regress
 ISOLATION = basic \
 	    inplace \
 	    repack \
+	    repack_deadlock \
 	    repack_toast \
 	    syscache-update-pruned \
 	    heap_lock_update
diff --git a/src/test/modules/injection_points/expected/repack_deadlock.out b/src/test/modules/injection_points/expected/repack_deadlock.out
new file mode 100644
index 00000000000..42505d10933
--- /dev/null
+++ b/src/test/modules/injection_points/expected/repack_deadlock.out
@@ -0,0 +1,63 @@
+Parsed test spec with 2 sessions
+
+starting permutation: wait_before_lock add_column wakeup_before_lock check1
+injection_points_attach
+-----------------------
+                       
+(1 row)
+
+step wait_before_lock: 
+	REPACK (CONCURRENTLY) repack_deadlock USING INDEX repack_deadlock_pkey;
+ <waiting ...>
+step add_column: 
+	alter table repack_deadlock add column noise text;
+ <waiting ...>
+step add_column: <... completed>
+ERROR:  oops, we're blocked by REPACK, time to die
+step wakeup_before_lock: 
+	SELECT injection_points_wakeup('repack-concurrently-before-lock');
+
+injection_points_wakeup
+-----------------------
+                       
+(1 row)
+
+step wait_before_lock: <... completed>
+step check1: 
+	INSERT INTO relfilenodes(node)
+	SELECT relfilenode FROM pg_class WHERE relname='repack_deadlock';
+
+	SELECT count(DISTINCT node) FROM relfilenodes;
+
+	SELECT i, j FROM repack_deadlock ORDER BY i, j;
+
+	INSERT INTO data_s1(i, j)
+	SELECT i, j FROM repack_deadlock;
+
+	SELECT count(*)
+	FROM data_s1 d1 FULL JOIN data_s2 d2 USING (i, j)
+	WHERE d1.i ISNULL OR d2.i ISNULL;
+
+count
+-----
+    1
+(1 row)
+
+i|j
+-+-
+1|1
+2|2
+3|3
+4|4
+(4 rows)
+
+count
+-----
+    4
+(1 row)
+
+injection_points_detach
+-----------------------
+                       
+(1 row)
+
diff --git a/src/test/modules/injection_points/meson.build b/src/test/modules/injection_points/meson.build
index a414abb924b..1cd88d6db65 100644
--- a/src/test/modules/injection_points/meson.build
+++ b/src/test/modules/injection_points/meson.build
@@ -46,6 +46,7 @@ tests += {
       'basic',
       'inplace',
       'repack',
+      'repack_deadlock',
       'repack_toast',
       'syscache-update-pruned',
       'heap_lock_update',
diff --git a/src/test/modules/injection_points/specs/repack_deadlock.spec b/src/test/modules/injection_points/specs/repack_deadlock.spec
new file mode 100644
index 00000000000..9d23a6588c2
--- /dev/null
+++ b/src/test/modules/injection_points/specs/repack_deadlock.spec
@@ -0,0 +1,83 @@
+# Test REPACK with a concurrent transaction that would cause a deadlock
+setup
+{
+	CREATE EXTENSION injection_points;
+
+	CREATE TABLE repack_deadlock(i int PRIMARY KEY, j int);
+	INSERT INTO repack_deadlock(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4);
+
+	CREATE TABLE relfilenodes(node oid);
+
+	CREATE TABLE data_s1(i int, j int);
+	CREATE TABLE data_s2(i int, j int);
+}
+
+teardown
+{
+	DROP TABLE repack_deadlock;
+	DROP EXTENSION injection_points;
+
+	DROP TABLE relfilenodes;
+	DROP TABLE data_s1;
+	DROP TABLE data_s2;
+}
+
+session s1
+setup
+{
+	SELECT injection_points_set_local();
+	SELECT injection_points_attach('repack-concurrently-before-lock', 'wait');
+}
+# Perform the initial load and wait for s2 to do some data changes.
+step wait_before_lock
+{
+	REPACK (CONCURRENTLY) repack_deadlock USING INDEX repack_deadlock_pkey;
+}
+# Check the table from the perspective of s1.
+#
+# Besides the contents, we also check that relfilenode has changed.
+
+# Have each session write the contents into a table and use FULL JOIN to check
+# if the outputs are identical.
+step check1
+{
+	INSERT INTO relfilenodes(node)
+	SELECT relfilenode FROM pg_class WHERE relname='repack_deadlock';
+
+	SELECT count(DISTINCT node) FROM relfilenodes;
+
+	SELECT i, j FROM repack_deadlock ORDER BY i, j;
+
+	INSERT INTO data_s1(i, j)
+	SELECT i, j FROM repack_deadlock;
+
+	SELECT count(*)
+	FROM data_s1 d1 FULL JOIN data_s2 d2 USING (i, j)
+	WHERE d1.i ISNULL OR d2.i ISNULL;
+}
+teardown
+{
+	SELECT injection_points_detach('repack-concurrently-before-lock');
+}
+
+session s2
+# Change the existing data. UPDATE changes both key and non-key columns. Also
+# update one row twice to test whether tuple version generated by this session
+# can be found.
+step add_column
+{
+	alter table repack_deadlock add column noise text;
+}
+
+step wakeup_before_lock
+{
+	SELECT injection_points_wakeup('repack-concurrently-before-lock');
+}
+
+# Test if data changes introduced while one session is performing REPACK
+# CONCURRENTLY find their way into the table.
+permutation
+	wait_before_lock
+	add_column
+	wakeup_before_lock
+	check1
-- 
2.47.3


--fot5plqmb33ey33c--





^ permalink  raw  reply  [nested|flat] 89+ messages in thread

* [PATCH v44b 4/4] hack deadlock detector
@ 2026-03-25 19:35  Álvaro Herrera <[email protected]>
  0 siblings, 0 replies; 89+ messages in thread

From: Álvaro Herrera @ 2026-03-25 19:35 UTC (permalink / raw)

---
 src/backend/commands/cluster.c                | 10 +++
 src/backend/storage/lmgr/deadlock.c           | 12 +++
 src/include/storage/proc.h                    |  1 +
 src/test/modules/injection_points/Makefile    |  1 +
 .../expected/repack_deadlock.out              | 63 ++++++++++++++
 src/test/modules/injection_points/meson.build |  1 +
 .../specs/repack_deadlock.spec                | 83 +++++++++++++++++++
 7 files changed, 171 insertions(+)
 create mode 100644 src/test/modules/injection_points/expected/repack_deadlock.out
 create mode 100644 src/test/modules/injection_points/specs/repack_deadlock.spec

diff --git a/src/backend/commands/cluster.c b/src/backend/commands/cluster.c
index 2c3058ba10d..d5b1dfbff69 100644
--- a/src/backend/commands/cluster.c
+++ b/src/backend/commands/cluster.c
@@ -372,6 +372,16 @@ ExecRepack(ParseState *pstate, RepackStmt *stmt, bool isTopLevel)
 	/* Determine the lock mode to use. */
 	lockmode = RepackLockLevel((params.options & CLUOPT_CONCURRENT) != 0);
 
+	/*
+	 * If in concurrent mode, set the PROC_IN_CONCURRENT_REPACK flag.  This
+	 * makes the deadlock checker cause anyone that would conflict with us
+	 * to error out.  It's important to set this flag ahead of actually locking
+	 * the relation; it won't of course affect anyone until we do have a lock
+	 * that others can conflict with.
+	 */
+	if ((params.options & CLUOPT_CONCURRENT) != 0)
+		MyProc->statusFlags |= PROC_IN_CONCURRENT_REPACK;
+
 	/*
 	 * If a single relation is specified, process it and we're done ... unless
 	 * the relation is a partitioned table, in which case we fall through.
diff --git a/src/backend/storage/lmgr/deadlock.c b/src/backend/storage/lmgr/deadlock.c
index b8962d875b6..10266e384aa 100644
--- a/src/backend/storage/lmgr/deadlock.c
+++ b/src/backend/storage/lmgr/deadlock.c
@@ -620,6 +620,18 @@ FindLockCycleRecurseMember(PGPROC *checkProc,
 						proc->statusFlags & PROC_IS_AUTOVACUUM)
 						blocking_autovacuum_proc = proc;
 
+					/*
+					 * If we note that we're blocked by some process running
+					 * REPACK (CONCURRENTLY), just fail.  That process is
+					 * going to upgrade its lock at some point, and it would
+					 * be inappropriate for any other process to cause that
+					 * to fail.
+					 */
+					if (checkProc == MyProc &&
+						proc->statusFlags & PROC_IN_CONCURRENT_REPACK)
+						/* not a serious error message proposal */
+						elog(ERROR, "oops, we're blocked by REPACK, time to die");
+
 					/* We're done looking at this proclock */
 					break;
 				}
diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h
index 1dad125706e..4e340ead455 100644
--- a/src/include/storage/proc.h
+++ b/src/include/storage/proc.h
@@ -69,6 +69,7 @@ struct XidCache
 #define		PROC_AFFECTS_ALL_HORIZONS	0x20	/* this proc's xmin must be
 												 * included in vacuum horizons
 												 * in all databases */
+#define		PROC_IN_CONCURRENT_REPACK	0x40	/* REPACK (CONCURRENTLY) */
 
 /* flags reset at EOXact */
 #define		PROC_VACUUM_STATE_MASK \
diff --git a/src/test/modules/injection_points/Makefile b/src/test/modules/injection_points/Makefile
index 2cd7d87c533..f7663859fe2 100644
--- a/src/test/modules/injection_points/Makefile
+++ b/src/test/modules/injection_points/Makefile
@@ -15,6 +15,7 @@ REGRESS_OPTS = --dlpath=$(top_builddir)/src/test/regress
 ISOLATION = basic \
 	    inplace \
 	    repack \
+	    repack_deadlock \
 	    repack_toast \
 	    syscache-update-pruned \
 	    heap_lock_update
diff --git a/src/test/modules/injection_points/expected/repack_deadlock.out b/src/test/modules/injection_points/expected/repack_deadlock.out
new file mode 100644
index 00000000000..42505d10933
--- /dev/null
+++ b/src/test/modules/injection_points/expected/repack_deadlock.out
@@ -0,0 +1,63 @@
+Parsed test spec with 2 sessions
+
+starting permutation: wait_before_lock add_column wakeup_before_lock check1
+injection_points_attach
+-----------------------
+                       
+(1 row)
+
+step wait_before_lock: 
+	REPACK (CONCURRENTLY) repack_deadlock USING INDEX repack_deadlock_pkey;
+ <waiting ...>
+step add_column: 
+	alter table repack_deadlock add column noise text;
+ <waiting ...>
+step add_column: <... completed>
+ERROR:  oops, we're blocked by REPACK, time to die
+step wakeup_before_lock: 
+	SELECT injection_points_wakeup('repack-concurrently-before-lock');
+
+injection_points_wakeup
+-----------------------
+                       
+(1 row)
+
+step wait_before_lock: <... completed>
+step check1: 
+	INSERT INTO relfilenodes(node)
+	SELECT relfilenode FROM pg_class WHERE relname='repack_deadlock';
+
+	SELECT count(DISTINCT node) FROM relfilenodes;
+
+	SELECT i, j FROM repack_deadlock ORDER BY i, j;
+
+	INSERT INTO data_s1(i, j)
+	SELECT i, j FROM repack_deadlock;
+
+	SELECT count(*)
+	FROM data_s1 d1 FULL JOIN data_s2 d2 USING (i, j)
+	WHERE d1.i ISNULL OR d2.i ISNULL;
+
+count
+-----
+    1
+(1 row)
+
+i|j
+-+-
+1|1
+2|2
+3|3
+4|4
+(4 rows)
+
+count
+-----
+    4
+(1 row)
+
+injection_points_detach
+-----------------------
+                       
+(1 row)
+
diff --git a/src/test/modules/injection_points/meson.build b/src/test/modules/injection_points/meson.build
index a414abb924b..1cd88d6db65 100644
--- a/src/test/modules/injection_points/meson.build
+++ b/src/test/modules/injection_points/meson.build
@@ -46,6 +46,7 @@ tests += {
       'basic',
       'inplace',
       'repack',
+      'repack_deadlock',
       'repack_toast',
       'syscache-update-pruned',
       'heap_lock_update',
diff --git a/src/test/modules/injection_points/specs/repack_deadlock.spec b/src/test/modules/injection_points/specs/repack_deadlock.spec
new file mode 100644
index 00000000000..9d23a6588c2
--- /dev/null
+++ b/src/test/modules/injection_points/specs/repack_deadlock.spec
@@ -0,0 +1,83 @@
+# Test REPACK with a concurrent transaction that would cause a deadlock
+setup
+{
+	CREATE EXTENSION injection_points;
+
+	CREATE TABLE repack_deadlock(i int PRIMARY KEY, j int);
+	INSERT INTO repack_deadlock(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4);
+
+	CREATE TABLE relfilenodes(node oid);
+
+	CREATE TABLE data_s1(i int, j int);
+	CREATE TABLE data_s2(i int, j int);
+}
+
+teardown
+{
+	DROP TABLE repack_deadlock;
+	DROP EXTENSION injection_points;
+
+	DROP TABLE relfilenodes;
+	DROP TABLE data_s1;
+	DROP TABLE data_s2;
+}
+
+session s1
+setup
+{
+	SELECT injection_points_set_local();
+	SELECT injection_points_attach('repack-concurrently-before-lock', 'wait');
+}
+# Perform the initial load and wait for s2 to do some data changes.
+step wait_before_lock
+{
+	REPACK (CONCURRENTLY) repack_deadlock USING INDEX repack_deadlock_pkey;
+}
+# Check the table from the perspective of s1.
+#
+# Besides the contents, we also check that relfilenode has changed.
+
+# Have each session write the contents into a table and use FULL JOIN to check
+# if the outputs are identical.
+step check1
+{
+	INSERT INTO relfilenodes(node)
+	SELECT relfilenode FROM pg_class WHERE relname='repack_deadlock';
+
+	SELECT count(DISTINCT node) FROM relfilenodes;
+
+	SELECT i, j FROM repack_deadlock ORDER BY i, j;
+
+	INSERT INTO data_s1(i, j)
+	SELECT i, j FROM repack_deadlock;
+
+	SELECT count(*)
+	FROM data_s1 d1 FULL JOIN data_s2 d2 USING (i, j)
+	WHERE d1.i ISNULL OR d2.i ISNULL;
+}
+teardown
+{
+	SELECT injection_points_detach('repack-concurrently-before-lock');
+}
+
+session s2
+# Change the existing data. UPDATE changes both key and non-key columns. Also
+# update one row twice to test whether tuple version generated by this session
+# can be found.
+step add_column
+{
+	alter table repack_deadlock add column noise text;
+}
+
+step wakeup_before_lock
+{
+	SELECT injection_points_wakeup('repack-concurrently-before-lock');
+}
+
+# Test if data changes introduced while one session is performing REPACK
+# CONCURRENTLY find their way into the table.
+permutation
+	wait_before_lock
+	add_column
+	wakeup_before_lock
+	check1
-- 
2.47.3


--fot5plqmb33ey33c--





^ permalink  raw  reply  [nested|flat] 89+ messages in thread

* [PATCH v44b 4/4] hack deadlock detector
@ 2026-03-25 19:35  Álvaro Herrera <[email protected]>
  0 siblings, 0 replies; 89+ messages in thread

From: Álvaro Herrera @ 2026-03-25 19:35 UTC (permalink / raw)

---
 src/backend/commands/cluster.c                | 10 +++
 src/backend/storage/lmgr/deadlock.c           | 12 +++
 src/include/storage/proc.h                    |  1 +
 src/test/modules/injection_points/Makefile    |  1 +
 .../expected/repack_deadlock.out              | 63 ++++++++++++++
 src/test/modules/injection_points/meson.build |  1 +
 .../specs/repack_deadlock.spec                | 83 +++++++++++++++++++
 7 files changed, 171 insertions(+)
 create mode 100644 src/test/modules/injection_points/expected/repack_deadlock.out
 create mode 100644 src/test/modules/injection_points/specs/repack_deadlock.spec

diff --git a/src/backend/commands/cluster.c b/src/backend/commands/cluster.c
index 2c3058ba10d..d5b1dfbff69 100644
--- a/src/backend/commands/cluster.c
+++ b/src/backend/commands/cluster.c
@@ -372,6 +372,16 @@ ExecRepack(ParseState *pstate, RepackStmt *stmt, bool isTopLevel)
 	/* Determine the lock mode to use. */
 	lockmode = RepackLockLevel((params.options & CLUOPT_CONCURRENT) != 0);
 
+	/*
+	 * If in concurrent mode, set the PROC_IN_CONCURRENT_REPACK flag.  This
+	 * makes the deadlock checker cause anyone that would conflict with us
+	 * to error out.  It's important to set this flag ahead of actually locking
+	 * the relation; it won't of course affect anyone until we do have a lock
+	 * that others can conflict with.
+	 */
+	if ((params.options & CLUOPT_CONCURRENT) != 0)
+		MyProc->statusFlags |= PROC_IN_CONCURRENT_REPACK;
+
 	/*
 	 * If a single relation is specified, process it and we're done ... unless
 	 * the relation is a partitioned table, in which case we fall through.
diff --git a/src/backend/storage/lmgr/deadlock.c b/src/backend/storage/lmgr/deadlock.c
index b8962d875b6..10266e384aa 100644
--- a/src/backend/storage/lmgr/deadlock.c
+++ b/src/backend/storage/lmgr/deadlock.c
@@ -620,6 +620,18 @@ FindLockCycleRecurseMember(PGPROC *checkProc,
 						proc->statusFlags & PROC_IS_AUTOVACUUM)
 						blocking_autovacuum_proc = proc;
 
+					/*
+					 * If we note that we're blocked by some process running
+					 * REPACK (CONCURRENTLY), just fail.  That process is
+					 * going to upgrade its lock at some point, and it would
+					 * be inappropriate for any other process to cause that
+					 * to fail.
+					 */
+					if (checkProc == MyProc &&
+						proc->statusFlags & PROC_IN_CONCURRENT_REPACK)
+						/* not a serious error message proposal */
+						elog(ERROR, "oops, we're blocked by REPACK, time to die");
+
 					/* We're done looking at this proclock */
 					break;
 				}
diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h
index 1dad125706e..4e340ead455 100644
--- a/src/include/storage/proc.h
+++ b/src/include/storage/proc.h
@@ -69,6 +69,7 @@ struct XidCache
 #define		PROC_AFFECTS_ALL_HORIZONS	0x20	/* this proc's xmin must be
 												 * included in vacuum horizons
 												 * in all databases */
+#define		PROC_IN_CONCURRENT_REPACK	0x40	/* REPACK (CONCURRENTLY) */
 
 /* flags reset at EOXact */
 #define		PROC_VACUUM_STATE_MASK \
diff --git a/src/test/modules/injection_points/Makefile b/src/test/modules/injection_points/Makefile
index 2cd7d87c533..f7663859fe2 100644
--- a/src/test/modules/injection_points/Makefile
+++ b/src/test/modules/injection_points/Makefile
@@ -15,6 +15,7 @@ REGRESS_OPTS = --dlpath=$(top_builddir)/src/test/regress
 ISOLATION = basic \
 	    inplace \
 	    repack \
+	    repack_deadlock \
 	    repack_toast \
 	    syscache-update-pruned \
 	    heap_lock_update
diff --git a/src/test/modules/injection_points/expected/repack_deadlock.out b/src/test/modules/injection_points/expected/repack_deadlock.out
new file mode 100644
index 00000000000..42505d10933
--- /dev/null
+++ b/src/test/modules/injection_points/expected/repack_deadlock.out
@@ -0,0 +1,63 @@
+Parsed test spec with 2 sessions
+
+starting permutation: wait_before_lock add_column wakeup_before_lock check1
+injection_points_attach
+-----------------------
+                       
+(1 row)
+
+step wait_before_lock: 
+	REPACK (CONCURRENTLY) repack_deadlock USING INDEX repack_deadlock_pkey;
+ <waiting ...>
+step add_column: 
+	alter table repack_deadlock add column noise text;
+ <waiting ...>
+step add_column: <... completed>
+ERROR:  oops, we're blocked by REPACK, time to die
+step wakeup_before_lock: 
+	SELECT injection_points_wakeup('repack-concurrently-before-lock');
+
+injection_points_wakeup
+-----------------------
+                       
+(1 row)
+
+step wait_before_lock: <... completed>
+step check1: 
+	INSERT INTO relfilenodes(node)
+	SELECT relfilenode FROM pg_class WHERE relname='repack_deadlock';
+
+	SELECT count(DISTINCT node) FROM relfilenodes;
+
+	SELECT i, j FROM repack_deadlock ORDER BY i, j;
+
+	INSERT INTO data_s1(i, j)
+	SELECT i, j FROM repack_deadlock;
+
+	SELECT count(*)
+	FROM data_s1 d1 FULL JOIN data_s2 d2 USING (i, j)
+	WHERE d1.i ISNULL OR d2.i ISNULL;
+
+count
+-----
+    1
+(1 row)
+
+i|j
+-+-
+1|1
+2|2
+3|3
+4|4
+(4 rows)
+
+count
+-----
+    4
+(1 row)
+
+injection_points_detach
+-----------------------
+                       
+(1 row)
+
diff --git a/src/test/modules/injection_points/meson.build b/src/test/modules/injection_points/meson.build
index a414abb924b..1cd88d6db65 100644
--- a/src/test/modules/injection_points/meson.build
+++ b/src/test/modules/injection_points/meson.build
@@ -46,6 +46,7 @@ tests += {
       'basic',
       'inplace',
       'repack',
+      'repack_deadlock',
       'repack_toast',
       'syscache-update-pruned',
       'heap_lock_update',
diff --git a/src/test/modules/injection_points/specs/repack_deadlock.spec b/src/test/modules/injection_points/specs/repack_deadlock.spec
new file mode 100644
index 00000000000..9d23a6588c2
--- /dev/null
+++ b/src/test/modules/injection_points/specs/repack_deadlock.spec
@@ -0,0 +1,83 @@
+# Test REPACK with a concurrent transaction that would cause a deadlock
+setup
+{
+	CREATE EXTENSION injection_points;
+
+	CREATE TABLE repack_deadlock(i int PRIMARY KEY, j int);
+	INSERT INTO repack_deadlock(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4);
+
+	CREATE TABLE relfilenodes(node oid);
+
+	CREATE TABLE data_s1(i int, j int);
+	CREATE TABLE data_s2(i int, j int);
+}
+
+teardown
+{
+	DROP TABLE repack_deadlock;
+	DROP EXTENSION injection_points;
+
+	DROP TABLE relfilenodes;
+	DROP TABLE data_s1;
+	DROP TABLE data_s2;
+}
+
+session s1
+setup
+{
+	SELECT injection_points_set_local();
+	SELECT injection_points_attach('repack-concurrently-before-lock', 'wait');
+}
+# Perform the initial load and wait for s2 to do some data changes.
+step wait_before_lock
+{
+	REPACK (CONCURRENTLY) repack_deadlock USING INDEX repack_deadlock_pkey;
+}
+# Check the table from the perspective of s1.
+#
+# Besides the contents, we also check that relfilenode has changed.
+
+# Have each session write the contents into a table and use FULL JOIN to check
+# if the outputs are identical.
+step check1
+{
+	INSERT INTO relfilenodes(node)
+	SELECT relfilenode FROM pg_class WHERE relname='repack_deadlock';
+
+	SELECT count(DISTINCT node) FROM relfilenodes;
+
+	SELECT i, j FROM repack_deadlock ORDER BY i, j;
+
+	INSERT INTO data_s1(i, j)
+	SELECT i, j FROM repack_deadlock;
+
+	SELECT count(*)
+	FROM data_s1 d1 FULL JOIN data_s2 d2 USING (i, j)
+	WHERE d1.i ISNULL OR d2.i ISNULL;
+}
+teardown
+{
+	SELECT injection_points_detach('repack-concurrently-before-lock');
+}
+
+session s2
+# Change the existing data. UPDATE changes both key and non-key columns. Also
+# update one row twice to test whether tuple version generated by this session
+# can be found.
+step add_column
+{
+	alter table repack_deadlock add column noise text;
+}
+
+step wakeup_before_lock
+{
+	SELECT injection_points_wakeup('repack-concurrently-before-lock');
+}
+
+# Test if data changes introduced while one session is performing REPACK
+# CONCURRENTLY find their way into the table.
+permutation
+	wait_before_lock
+	add_column
+	wakeup_before_lock
+	check1
-- 
2.47.3


--fot5plqmb33ey33c--





^ permalink  raw  reply  [nested|flat] 89+ messages in thread

* [PATCH v44b 4/4] hack deadlock detector
@ 2026-03-25 19:35  Álvaro Herrera <[email protected]>
  0 siblings, 0 replies; 89+ messages in thread

From: Álvaro Herrera @ 2026-03-25 19:35 UTC (permalink / raw)

---
 src/backend/commands/cluster.c                | 10 +++
 src/backend/storage/lmgr/deadlock.c           | 12 +++
 src/include/storage/proc.h                    |  1 +
 src/test/modules/injection_points/Makefile    |  1 +
 .../expected/repack_deadlock.out              | 63 ++++++++++++++
 src/test/modules/injection_points/meson.build |  1 +
 .../specs/repack_deadlock.spec                | 83 +++++++++++++++++++
 7 files changed, 171 insertions(+)
 create mode 100644 src/test/modules/injection_points/expected/repack_deadlock.out
 create mode 100644 src/test/modules/injection_points/specs/repack_deadlock.spec

diff --git a/src/backend/commands/cluster.c b/src/backend/commands/cluster.c
index 2c3058ba10d..d5b1dfbff69 100644
--- a/src/backend/commands/cluster.c
+++ b/src/backend/commands/cluster.c
@@ -372,6 +372,16 @@ ExecRepack(ParseState *pstate, RepackStmt *stmt, bool isTopLevel)
 	/* Determine the lock mode to use. */
 	lockmode = RepackLockLevel((params.options & CLUOPT_CONCURRENT) != 0);
 
+	/*
+	 * If in concurrent mode, set the PROC_IN_CONCURRENT_REPACK flag.  This
+	 * makes the deadlock checker cause anyone that would conflict with us
+	 * to error out.  It's important to set this flag ahead of actually locking
+	 * the relation; it won't of course affect anyone until we do have a lock
+	 * that others can conflict with.
+	 */
+	if ((params.options & CLUOPT_CONCURRENT) != 0)
+		MyProc->statusFlags |= PROC_IN_CONCURRENT_REPACK;
+
 	/*
 	 * If a single relation is specified, process it and we're done ... unless
 	 * the relation is a partitioned table, in which case we fall through.
diff --git a/src/backend/storage/lmgr/deadlock.c b/src/backend/storage/lmgr/deadlock.c
index b8962d875b6..10266e384aa 100644
--- a/src/backend/storage/lmgr/deadlock.c
+++ b/src/backend/storage/lmgr/deadlock.c
@@ -620,6 +620,18 @@ FindLockCycleRecurseMember(PGPROC *checkProc,
 						proc->statusFlags & PROC_IS_AUTOVACUUM)
 						blocking_autovacuum_proc = proc;
 
+					/*
+					 * If we note that we're blocked by some process running
+					 * REPACK (CONCURRENTLY), just fail.  That process is
+					 * going to upgrade its lock at some point, and it would
+					 * be inappropriate for any other process to cause that
+					 * to fail.
+					 */
+					if (checkProc == MyProc &&
+						proc->statusFlags & PROC_IN_CONCURRENT_REPACK)
+						/* not a serious error message proposal */
+						elog(ERROR, "oops, we're blocked by REPACK, time to die");
+
 					/* We're done looking at this proclock */
 					break;
 				}
diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h
index 1dad125706e..4e340ead455 100644
--- a/src/include/storage/proc.h
+++ b/src/include/storage/proc.h
@@ -69,6 +69,7 @@ struct XidCache
 #define		PROC_AFFECTS_ALL_HORIZONS	0x20	/* this proc's xmin must be
 												 * included in vacuum horizons
 												 * in all databases */
+#define		PROC_IN_CONCURRENT_REPACK	0x40	/* REPACK (CONCURRENTLY) */
 
 /* flags reset at EOXact */
 #define		PROC_VACUUM_STATE_MASK \
diff --git a/src/test/modules/injection_points/Makefile b/src/test/modules/injection_points/Makefile
index 2cd7d87c533..f7663859fe2 100644
--- a/src/test/modules/injection_points/Makefile
+++ b/src/test/modules/injection_points/Makefile
@@ -15,6 +15,7 @@ REGRESS_OPTS = --dlpath=$(top_builddir)/src/test/regress
 ISOLATION = basic \
 	    inplace \
 	    repack \
+	    repack_deadlock \
 	    repack_toast \
 	    syscache-update-pruned \
 	    heap_lock_update
diff --git a/src/test/modules/injection_points/expected/repack_deadlock.out b/src/test/modules/injection_points/expected/repack_deadlock.out
new file mode 100644
index 00000000000..42505d10933
--- /dev/null
+++ b/src/test/modules/injection_points/expected/repack_deadlock.out
@@ -0,0 +1,63 @@
+Parsed test spec with 2 sessions
+
+starting permutation: wait_before_lock add_column wakeup_before_lock check1
+injection_points_attach
+-----------------------
+                       
+(1 row)
+
+step wait_before_lock: 
+	REPACK (CONCURRENTLY) repack_deadlock USING INDEX repack_deadlock_pkey;
+ <waiting ...>
+step add_column: 
+	alter table repack_deadlock add column noise text;
+ <waiting ...>
+step add_column: <... completed>
+ERROR:  oops, we're blocked by REPACK, time to die
+step wakeup_before_lock: 
+	SELECT injection_points_wakeup('repack-concurrently-before-lock');
+
+injection_points_wakeup
+-----------------------
+                       
+(1 row)
+
+step wait_before_lock: <... completed>
+step check1: 
+	INSERT INTO relfilenodes(node)
+	SELECT relfilenode FROM pg_class WHERE relname='repack_deadlock';
+
+	SELECT count(DISTINCT node) FROM relfilenodes;
+
+	SELECT i, j FROM repack_deadlock ORDER BY i, j;
+
+	INSERT INTO data_s1(i, j)
+	SELECT i, j FROM repack_deadlock;
+
+	SELECT count(*)
+	FROM data_s1 d1 FULL JOIN data_s2 d2 USING (i, j)
+	WHERE d1.i ISNULL OR d2.i ISNULL;
+
+count
+-----
+    1
+(1 row)
+
+i|j
+-+-
+1|1
+2|2
+3|3
+4|4
+(4 rows)
+
+count
+-----
+    4
+(1 row)
+
+injection_points_detach
+-----------------------
+                       
+(1 row)
+
diff --git a/src/test/modules/injection_points/meson.build b/src/test/modules/injection_points/meson.build
index a414abb924b..1cd88d6db65 100644
--- a/src/test/modules/injection_points/meson.build
+++ b/src/test/modules/injection_points/meson.build
@@ -46,6 +46,7 @@ tests += {
       'basic',
       'inplace',
       'repack',
+      'repack_deadlock',
       'repack_toast',
       'syscache-update-pruned',
       'heap_lock_update',
diff --git a/src/test/modules/injection_points/specs/repack_deadlock.spec b/src/test/modules/injection_points/specs/repack_deadlock.spec
new file mode 100644
index 00000000000..9d23a6588c2
--- /dev/null
+++ b/src/test/modules/injection_points/specs/repack_deadlock.spec
@@ -0,0 +1,83 @@
+# Test REPACK with a concurrent transaction that would cause a deadlock
+setup
+{
+	CREATE EXTENSION injection_points;
+
+	CREATE TABLE repack_deadlock(i int PRIMARY KEY, j int);
+	INSERT INTO repack_deadlock(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4);
+
+	CREATE TABLE relfilenodes(node oid);
+
+	CREATE TABLE data_s1(i int, j int);
+	CREATE TABLE data_s2(i int, j int);
+}
+
+teardown
+{
+	DROP TABLE repack_deadlock;
+	DROP EXTENSION injection_points;
+
+	DROP TABLE relfilenodes;
+	DROP TABLE data_s1;
+	DROP TABLE data_s2;
+}
+
+session s1
+setup
+{
+	SELECT injection_points_set_local();
+	SELECT injection_points_attach('repack-concurrently-before-lock', 'wait');
+}
+# Perform the initial load and wait for s2 to do some data changes.
+step wait_before_lock
+{
+	REPACK (CONCURRENTLY) repack_deadlock USING INDEX repack_deadlock_pkey;
+}
+# Check the table from the perspective of s1.
+#
+# Besides the contents, we also check that relfilenode has changed.
+
+# Have each session write the contents into a table and use FULL JOIN to check
+# if the outputs are identical.
+step check1
+{
+	INSERT INTO relfilenodes(node)
+	SELECT relfilenode FROM pg_class WHERE relname='repack_deadlock';
+
+	SELECT count(DISTINCT node) FROM relfilenodes;
+
+	SELECT i, j FROM repack_deadlock ORDER BY i, j;
+
+	INSERT INTO data_s1(i, j)
+	SELECT i, j FROM repack_deadlock;
+
+	SELECT count(*)
+	FROM data_s1 d1 FULL JOIN data_s2 d2 USING (i, j)
+	WHERE d1.i ISNULL OR d2.i ISNULL;
+}
+teardown
+{
+	SELECT injection_points_detach('repack-concurrently-before-lock');
+}
+
+session s2
+# Change the existing data. UPDATE changes both key and non-key columns. Also
+# update one row twice to test whether tuple version generated by this session
+# can be found.
+step add_column
+{
+	alter table repack_deadlock add column noise text;
+}
+
+step wakeup_before_lock
+{
+	SELECT injection_points_wakeup('repack-concurrently-before-lock');
+}
+
+# Test if data changes introduced while one session is performing REPACK
+# CONCURRENTLY find their way into the table.
+permutation
+	wait_before_lock
+	add_column
+	wakeup_before_lock
+	check1
-- 
2.47.3


--fot5plqmb33ey33c--





^ permalink  raw  reply  [nested|flat] 89+ messages in thread

* [PATCH v44b 4/4] hack deadlock detector
@ 2026-03-25 19:35  Álvaro Herrera <[email protected]>
  0 siblings, 0 replies; 89+ messages in thread

From: Álvaro Herrera @ 2026-03-25 19:35 UTC (permalink / raw)

---
 src/backend/commands/cluster.c                | 10 +++
 src/backend/storage/lmgr/deadlock.c           | 12 +++
 src/include/storage/proc.h                    |  1 +
 src/test/modules/injection_points/Makefile    |  1 +
 .../expected/repack_deadlock.out              | 63 ++++++++++++++
 src/test/modules/injection_points/meson.build |  1 +
 .../specs/repack_deadlock.spec                | 83 +++++++++++++++++++
 7 files changed, 171 insertions(+)
 create mode 100644 src/test/modules/injection_points/expected/repack_deadlock.out
 create mode 100644 src/test/modules/injection_points/specs/repack_deadlock.spec

diff --git a/src/backend/commands/cluster.c b/src/backend/commands/cluster.c
index 2c3058ba10d..d5b1dfbff69 100644
--- a/src/backend/commands/cluster.c
+++ b/src/backend/commands/cluster.c
@@ -372,6 +372,16 @@ ExecRepack(ParseState *pstate, RepackStmt *stmt, bool isTopLevel)
 	/* Determine the lock mode to use. */
 	lockmode = RepackLockLevel((params.options & CLUOPT_CONCURRENT) != 0);
 
+	/*
+	 * If in concurrent mode, set the PROC_IN_CONCURRENT_REPACK flag.  This
+	 * makes the deadlock checker cause anyone that would conflict with us
+	 * to error out.  It's important to set this flag ahead of actually locking
+	 * the relation; it won't of course affect anyone until we do have a lock
+	 * that others can conflict with.
+	 */
+	if ((params.options & CLUOPT_CONCURRENT) != 0)
+		MyProc->statusFlags |= PROC_IN_CONCURRENT_REPACK;
+
 	/*
 	 * If a single relation is specified, process it and we're done ... unless
 	 * the relation is a partitioned table, in which case we fall through.
diff --git a/src/backend/storage/lmgr/deadlock.c b/src/backend/storage/lmgr/deadlock.c
index b8962d875b6..10266e384aa 100644
--- a/src/backend/storage/lmgr/deadlock.c
+++ b/src/backend/storage/lmgr/deadlock.c
@@ -620,6 +620,18 @@ FindLockCycleRecurseMember(PGPROC *checkProc,
 						proc->statusFlags & PROC_IS_AUTOVACUUM)
 						blocking_autovacuum_proc = proc;
 
+					/*
+					 * If we note that we're blocked by some process running
+					 * REPACK (CONCURRENTLY), just fail.  That process is
+					 * going to upgrade its lock at some point, and it would
+					 * be inappropriate for any other process to cause that
+					 * to fail.
+					 */
+					if (checkProc == MyProc &&
+						proc->statusFlags & PROC_IN_CONCURRENT_REPACK)
+						/* not a serious error message proposal */
+						elog(ERROR, "oops, we're blocked by REPACK, time to die");
+
 					/* We're done looking at this proclock */
 					break;
 				}
diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h
index 1dad125706e..4e340ead455 100644
--- a/src/include/storage/proc.h
+++ b/src/include/storage/proc.h
@@ -69,6 +69,7 @@ struct XidCache
 #define		PROC_AFFECTS_ALL_HORIZONS	0x20	/* this proc's xmin must be
 												 * included in vacuum horizons
 												 * in all databases */
+#define		PROC_IN_CONCURRENT_REPACK	0x40	/* REPACK (CONCURRENTLY) */
 
 /* flags reset at EOXact */
 #define		PROC_VACUUM_STATE_MASK \
diff --git a/src/test/modules/injection_points/Makefile b/src/test/modules/injection_points/Makefile
index 2cd7d87c533..f7663859fe2 100644
--- a/src/test/modules/injection_points/Makefile
+++ b/src/test/modules/injection_points/Makefile
@@ -15,6 +15,7 @@ REGRESS_OPTS = --dlpath=$(top_builddir)/src/test/regress
 ISOLATION = basic \
 	    inplace \
 	    repack \
+	    repack_deadlock \
 	    repack_toast \
 	    syscache-update-pruned \
 	    heap_lock_update
diff --git a/src/test/modules/injection_points/expected/repack_deadlock.out b/src/test/modules/injection_points/expected/repack_deadlock.out
new file mode 100644
index 00000000000..42505d10933
--- /dev/null
+++ b/src/test/modules/injection_points/expected/repack_deadlock.out
@@ -0,0 +1,63 @@
+Parsed test spec with 2 sessions
+
+starting permutation: wait_before_lock add_column wakeup_before_lock check1
+injection_points_attach
+-----------------------
+                       
+(1 row)
+
+step wait_before_lock: 
+	REPACK (CONCURRENTLY) repack_deadlock USING INDEX repack_deadlock_pkey;
+ <waiting ...>
+step add_column: 
+	alter table repack_deadlock add column noise text;
+ <waiting ...>
+step add_column: <... completed>
+ERROR:  oops, we're blocked by REPACK, time to die
+step wakeup_before_lock: 
+	SELECT injection_points_wakeup('repack-concurrently-before-lock');
+
+injection_points_wakeup
+-----------------------
+                       
+(1 row)
+
+step wait_before_lock: <... completed>
+step check1: 
+	INSERT INTO relfilenodes(node)
+	SELECT relfilenode FROM pg_class WHERE relname='repack_deadlock';
+
+	SELECT count(DISTINCT node) FROM relfilenodes;
+
+	SELECT i, j FROM repack_deadlock ORDER BY i, j;
+
+	INSERT INTO data_s1(i, j)
+	SELECT i, j FROM repack_deadlock;
+
+	SELECT count(*)
+	FROM data_s1 d1 FULL JOIN data_s2 d2 USING (i, j)
+	WHERE d1.i ISNULL OR d2.i ISNULL;
+
+count
+-----
+    1
+(1 row)
+
+i|j
+-+-
+1|1
+2|2
+3|3
+4|4
+(4 rows)
+
+count
+-----
+    4
+(1 row)
+
+injection_points_detach
+-----------------------
+                       
+(1 row)
+
diff --git a/src/test/modules/injection_points/meson.build b/src/test/modules/injection_points/meson.build
index a414abb924b..1cd88d6db65 100644
--- a/src/test/modules/injection_points/meson.build
+++ b/src/test/modules/injection_points/meson.build
@@ -46,6 +46,7 @@ tests += {
       'basic',
       'inplace',
       'repack',
+      'repack_deadlock',
       'repack_toast',
       'syscache-update-pruned',
       'heap_lock_update',
diff --git a/src/test/modules/injection_points/specs/repack_deadlock.spec b/src/test/modules/injection_points/specs/repack_deadlock.spec
new file mode 100644
index 00000000000..9d23a6588c2
--- /dev/null
+++ b/src/test/modules/injection_points/specs/repack_deadlock.spec
@@ -0,0 +1,83 @@
+# Test REPACK with a concurrent transaction that would cause a deadlock
+setup
+{
+	CREATE EXTENSION injection_points;
+
+	CREATE TABLE repack_deadlock(i int PRIMARY KEY, j int);
+	INSERT INTO repack_deadlock(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4);
+
+	CREATE TABLE relfilenodes(node oid);
+
+	CREATE TABLE data_s1(i int, j int);
+	CREATE TABLE data_s2(i int, j int);
+}
+
+teardown
+{
+	DROP TABLE repack_deadlock;
+	DROP EXTENSION injection_points;
+
+	DROP TABLE relfilenodes;
+	DROP TABLE data_s1;
+	DROP TABLE data_s2;
+}
+
+session s1
+setup
+{
+	SELECT injection_points_set_local();
+	SELECT injection_points_attach('repack-concurrently-before-lock', 'wait');
+}
+# Perform the initial load and wait for s2 to do some data changes.
+step wait_before_lock
+{
+	REPACK (CONCURRENTLY) repack_deadlock USING INDEX repack_deadlock_pkey;
+}
+# Check the table from the perspective of s1.
+#
+# Besides the contents, we also check that relfilenode has changed.
+
+# Have each session write the contents into a table and use FULL JOIN to check
+# if the outputs are identical.
+step check1
+{
+	INSERT INTO relfilenodes(node)
+	SELECT relfilenode FROM pg_class WHERE relname='repack_deadlock';
+
+	SELECT count(DISTINCT node) FROM relfilenodes;
+
+	SELECT i, j FROM repack_deadlock ORDER BY i, j;
+
+	INSERT INTO data_s1(i, j)
+	SELECT i, j FROM repack_deadlock;
+
+	SELECT count(*)
+	FROM data_s1 d1 FULL JOIN data_s2 d2 USING (i, j)
+	WHERE d1.i ISNULL OR d2.i ISNULL;
+}
+teardown
+{
+	SELECT injection_points_detach('repack-concurrently-before-lock');
+}
+
+session s2
+# Change the existing data. UPDATE changes both key and non-key columns. Also
+# update one row twice to test whether tuple version generated by this session
+# can be found.
+step add_column
+{
+	alter table repack_deadlock add column noise text;
+}
+
+step wakeup_before_lock
+{
+	SELECT injection_points_wakeup('repack-concurrently-before-lock');
+}
+
+# Test if data changes introduced while one session is performing REPACK
+# CONCURRENTLY find their way into the table.
+permutation
+	wait_before_lock
+	add_column
+	wakeup_before_lock
+	check1
-- 
2.47.3


--fot5plqmb33ey33c--





^ permalink  raw  reply  [nested|flat] 89+ messages in thread

* [PATCH v44b 4/4] hack deadlock detector
@ 2026-03-25 19:35  Álvaro Herrera <[email protected]>
  0 siblings, 0 replies; 89+ messages in thread

From: Álvaro Herrera @ 2026-03-25 19:35 UTC (permalink / raw)

---
 src/backend/commands/cluster.c                | 10 +++
 src/backend/storage/lmgr/deadlock.c           | 12 +++
 src/include/storage/proc.h                    |  1 +
 src/test/modules/injection_points/Makefile    |  1 +
 .../expected/repack_deadlock.out              | 63 ++++++++++++++
 src/test/modules/injection_points/meson.build |  1 +
 .../specs/repack_deadlock.spec                | 83 +++++++++++++++++++
 7 files changed, 171 insertions(+)
 create mode 100644 src/test/modules/injection_points/expected/repack_deadlock.out
 create mode 100644 src/test/modules/injection_points/specs/repack_deadlock.spec

diff --git a/src/backend/commands/cluster.c b/src/backend/commands/cluster.c
index 2c3058ba10d..d5b1dfbff69 100644
--- a/src/backend/commands/cluster.c
+++ b/src/backend/commands/cluster.c
@@ -372,6 +372,16 @@ ExecRepack(ParseState *pstate, RepackStmt *stmt, bool isTopLevel)
 	/* Determine the lock mode to use. */
 	lockmode = RepackLockLevel((params.options & CLUOPT_CONCURRENT) != 0);
 
+	/*
+	 * If in concurrent mode, set the PROC_IN_CONCURRENT_REPACK flag.  This
+	 * makes the deadlock checker cause anyone that would conflict with us
+	 * to error out.  It's important to set this flag ahead of actually locking
+	 * the relation; it won't of course affect anyone until we do have a lock
+	 * that others can conflict with.
+	 */
+	if ((params.options & CLUOPT_CONCURRENT) != 0)
+		MyProc->statusFlags |= PROC_IN_CONCURRENT_REPACK;
+
 	/*
 	 * If a single relation is specified, process it and we're done ... unless
 	 * the relation is a partitioned table, in which case we fall through.
diff --git a/src/backend/storage/lmgr/deadlock.c b/src/backend/storage/lmgr/deadlock.c
index b8962d875b6..10266e384aa 100644
--- a/src/backend/storage/lmgr/deadlock.c
+++ b/src/backend/storage/lmgr/deadlock.c
@@ -620,6 +620,18 @@ FindLockCycleRecurseMember(PGPROC *checkProc,
 						proc->statusFlags & PROC_IS_AUTOVACUUM)
 						blocking_autovacuum_proc = proc;
 
+					/*
+					 * If we note that we're blocked by some process running
+					 * REPACK (CONCURRENTLY), just fail.  That process is
+					 * going to upgrade its lock at some point, and it would
+					 * be inappropriate for any other process to cause that
+					 * to fail.
+					 */
+					if (checkProc == MyProc &&
+						proc->statusFlags & PROC_IN_CONCURRENT_REPACK)
+						/* not a serious error message proposal */
+						elog(ERROR, "oops, we're blocked by REPACK, time to die");
+
 					/* We're done looking at this proclock */
 					break;
 				}
diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h
index 1dad125706e..4e340ead455 100644
--- a/src/include/storage/proc.h
+++ b/src/include/storage/proc.h
@@ -69,6 +69,7 @@ struct XidCache
 #define		PROC_AFFECTS_ALL_HORIZONS	0x20	/* this proc's xmin must be
 												 * included in vacuum horizons
 												 * in all databases */
+#define		PROC_IN_CONCURRENT_REPACK	0x40	/* REPACK (CONCURRENTLY) */
 
 /* flags reset at EOXact */
 #define		PROC_VACUUM_STATE_MASK \
diff --git a/src/test/modules/injection_points/Makefile b/src/test/modules/injection_points/Makefile
index 2cd7d87c533..f7663859fe2 100644
--- a/src/test/modules/injection_points/Makefile
+++ b/src/test/modules/injection_points/Makefile
@@ -15,6 +15,7 @@ REGRESS_OPTS = --dlpath=$(top_builddir)/src/test/regress
 ISOLATION = basic \
 	    inplace \
 	    repack \
+	    repack_deadlock \
 	    repack_toast \
 	    syscache-update-pruned \
 	    heap_lock_update
diff --git a/src/test/modules/injection_points/expected/repack_deadlock.out b/src/test/modules/injection_points/expected/repack_deadlock.out
new file mode 100644
index 00000000000..42505d10933
--- /dev/null
+++ b/src/test/modules/injection_points/expected/repack_deadlock.out
@@ -0,0 +1,63 @@
+Parsed test spec with 2 sessions
+
+starting permutation: wait_before_lock add_column wakeup_before_lock check1
+injection_points_attach
+-----------------------
+                       
+(1 row)
+
+step wait_before_lock: 
+	REPACK (CONCURRENTLY) repack_deadlock USING INDEX repack_deadlock_pkey;
+ <waiting ...>
+step add_column: 
+	alter table repack_deadlock add column noise text;
+ <waiting ...>
+step add_column: <... completed>
+ERROR:  oops, we're blocked by REPACK, time to die
+step wakeup_before_lock: 
+	SELECT injection_points_wakeup('repack-concurrently-before-lock');
+
+injection_points_wakeup
+-----------------------
+                       
+(1 row)
+
+step wait_before_lock: <... completed>
+step check1: 
+	INSERT INTO relfilenodes(node)
+	SELECT relfilenode FROM pg_class WHERE relname='repack_deadlock';
+
+	SELECT count(DISTINCT node) FROM relfilenodes;
+
+	SELECT i, j FROM repack_deadlock ORDER BY i, j;
+
+	INSERT INTO data_s1(i, j)
+	SELECT i, j FROM repack_deadlock;
+
+	SELECT count(*)
+	FROM data_s1 d1 FULL JOIN data_s2 d2 USING (i, j)
+	WHERE d1.i ISNULL OR d2.i ISNULL;
+
+count
+-----
+    1
+(1 row)
+
+i|j
+-+-
+1|1
+2|2
+3|3
+4|4
+(4 rows)
+
+count
+-----
+    4
+(1 row)
+
+injection_points_detach
+-----------------------
+                       
+(1 row)
+
diff --git a/src/test/modules/injection_points/meson.build b/src/test/modules/injection_points/meson.build
index a414abb924b..1cd88d6db65 100644
--- a/src/test/modules/injection_points/meson.build
+++ b/src/test/modules/injection_points/meson.build
@@ -46,6 +46,7 @@ tests += {
       'basic',
       'inplace',
       'repack',
+      'repack_deadlock',
       'repack_toast',
       'syscache-update-pruned',
       'heap_lock_update',
diff --git a/src/test/modules/injection_points/specs/repack_deadlock.spec b/src/test/modules/injection_points/specs/repack_deadlock.spec
new file mode 100644
index 00000000000..9d23a6588c2
--- /dev/null
+++ b/src/test/modules/injection_points/specs/repack_deadlock.spec
@@ -0,0 +1,83 @@
+# Test REPACK with a concurrent transaction that would cause a deadlock
+setup
+{
+	CREATE EXTENSION injection_points;
+
+	CREATE TABLE repack_deadlock(i int PRIMARY KEY, j int);
+	INSERT INTO repack_deadlock(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4);
+
+	CREATE TABLE relfilenodes(node oid);
+
+	CREATE TABLE data_s1(i int, j int);
+	CREATE TABLE data_s2(i int, j int);
+}
+
+teardown
+{
+	DROP TABLE repack_deadlock;
+	DROP EXTENSION injection_points;
+
+	DROP TABLE relfilenodes;
+	DROP TABLE data_s1;
+	DROP TABLE data_s2;
+}
+
+session s1
+setup
+{
+	SELECT injection_points_set_local();
+	SELECT injection_points_attach('repack-concurrently-before-lock', 'wait');
+}
+# Perform the initial load and wait for s2 to do some data changes.
+step wait_before_lock
+{
+	REPACK (CONCURRENTLY) repack_deadlock USING INDEX repack_deadlock_pkey;
+}
+# Check the table from the perspective of s1.
+#
+# Besides the contents, we also check that relfilenode has changed.
+
+# Have each session write the contents into a table and use FULL JOIN to check
+# if the outputs are identical.
+step check1
+{
+	INSERT INTO relfilenodes(node)
+	SELECT relfilenode FROM pg_class WHERE relname='repack_deadlock';
+
+	SELECT count(DISTINCT node) FROM relfilenodes;
+
+	SELECT i, j FROM repack_deadlock ORDER BY i, j;
+
+	INSERT INTO data_s1(i, j)
+	SELECT i, j FROM repack_deadlock;
+
+	SELECT count(*)
+	FROM data_s1 d1 FULL JOIN data_s2 d2 USING (i, j)
+	WHERE d1.i ISNULL OR d2.i ISNULL;
+}
+teardown
+{
+	SELECT injection_points_detach('repack-concurrently-before-lock');
+}
+
+session s2
+# Change the existing data. UPDATE changes both key and non-key columns. Also
+# update one row twice to test whether tuple version generated by this session
+# can be found.
+step add_column
+{
+	alter table repack_deadlock add column noise text;
+}
+
+step wakeup_before_lock
+{
+	SELECT injection_points_wakeup('repack-concurrently-before-lock');
+}
+
+# Test if data changes introduced while one session is performing REPACK
+# CONCURRENTLY find their way into the table.
+permutation
+	wait_before_lock
+	add_column
+	wakeup_before_lock
+	check1
-- 
2.47.3


--fot5plqmb33ey33c--





^ permalink  raw  reply  [nested|flat] 89+ messages in thread

* [PATCH v44b 4/4] hack deadlock detector
@ 2026-03-25 19:35  Álvaro Herrera <[email protected]>
  0 siblings, 0 replies; 89+ messages in thread

From: Álvaro Herrera @ 2026-03-25 19:35 UTC (permalink / raw)

---
 src/backend/commands/cluster.c                | 10 +++
 src/backend/storage/lmgr/deadlock.c           | 12 +++
 src/include/storage/proc.h                    |  1 +
 src/test/modules/injection_points/Makefile    |  1 +
 .../expected/repack_deadlock.out              | 63 ++++++++++++++
 src/test/modules/injection_points/meson.build |  1 +
 .../specs/repack_deadlock.spec                | 83 +++++++++++++++++++
 7 files changed, 171 insertions(+)
 create mode 100644 src/test/modules/injection_points/expected/repack_deadlock.out
 create mode 100644 src/test/modules/injection_points/specs/repack_deadlock.spec

diff --git a/src/backend/commands/cluster.c b/src/backend/commands/cluster.c
index 2c3058ba10d..d5b1dfbff69 100644
--- a/src/backend/commands/cluster.c
+++ b/src/backend/commands/cluster.c
@@ -372,6 +372,16 @@ ExecRepack(ParseState *pstate, RepackStmt *stmt, bool isTopLevel)
 	/* Determine the lock mode to use. */
 	lockmode = RepackLockLevel((params.options & CLUOPT_CONCURRENT) != 0);
 
+	/*
+	 * If in concurrent mode, set the PROC_IN_CONCURRENT_REPACK flag.  This
+	 * makes the deadlock checker cause anyone that would conflict with us
+	 * to error out.  It's important to set this flag ahead of actually locking
+	 * the relation; it won't of course affect anyone until we do have a lock
+	 * that others can conflict with.
+	 */
+	if ((params.options & CLUOPT_CONCURRENT) != 0)
+		MyProc->statusFlags |= PROC_IN_CONCURRENT_REPACK;
+
 	/*
 	 * If a single relation is specified, process it and we're done ... unless
 	 * the relation is a partitioned table, in which case we fall through.
diff --git a/src/backend/storage/lmgr/deadlock.c b/src/backend/storage/lmgr/deadlock.c
index b8962d875b6..10266e384aa 100644
--- a/src/backend/storage/lmgr/deadlock.c
+++ b/src/backend/storage/lmgr/deadlock.c
@@ -620,6 +620,18 @@ FindLockCycleRecurseMember(PGPROC *checkProc,
 						proc->statusFlags & PROC_IS_AUTOVACUUM)
 						blocking_autovacuum_proc = proc;
 
+					/*
+					 * If we note that we're blocked by some process running
+					 * REPACK (CONCURRENTLY), just fail.  That process is
+					 * going to upgrade its lock at some point, and it would
+					 * be inappropriate for any other process to cause that
+					 * to fail.
+					 */
+					if (checkProc == MyProc &&
+						proc->statusFlags & PROC_IN_CONCURRENT_REPACK)
+						/* not a serious error message proposal */
+						elog(ERROR, "oops, we're blocked by REPACK, time to die");
+
 					/* We're done looking at this proclock */
 					break;
 				}
diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h
index 1dad125706e..4e340ead455 100644
--- a/src/include/storage/proc.h
+++ b/src/include/storage/proc.h
@@ -69,6 +69,7 @@ struct XidCache
 #define		PROC_AFFECTS_ALL_HORIZONS	0x20	/* this proc's xmin must be
 												 * included in vacuum horizons
 												 * in all databases */
+#define		PROC_IN_CONCURRENT_REPACK	0x40	/* REPACK (CONCURRENTLY) */
 
 /* flags reset at EOXact */
 #define		PROC_VACUUM_STATE_MASK \
diff --git a/src/test/modules/injection_points/Makefile b/src/test/modules/injection_points/Makefile
index 2cd7d87c533..f7663859fe2 100644
--- a/src/test/modules/injection_points/Makefile
+++ b/src/test/modules/injection_points/Makefile
@@ -15,6 +15,7 @@ REGRESS_OPTS = --dlpath=$(top_builddir)/src/test/regress
 ISOLATION = basic \
 	    inplace \
 	    repack \
+	    repack_deadlock \
 	    repack_toast \
 	    syscache-update-pruned \
 	    heap_lock_update
diff --git a/src/test/modules/injection_points/expected/repack_deadlock.out b/src/test/modules/injection_points/expected/repack_deadlock.out
new file mode 100644
index 00000000000..42505d10933
--- /dev/null
+++ b/src/test/modules/injection_points/expected/repack_deadlock.out
@@ -0,0 +1,63 @@
+Parsed test spec with 2 sessions
+
+starting permutation: wait_before_lock add_column wakeup_before_lock check1
+injection_points_attach
+-----------------------
+                       
+(1 row)
+
+step wait_before_lock: 
+	REPACK (CONCURRENTLY) repack_deadlock USING INDEX repack_deadlock_pkey;
+ <waiting ...>
+step add_column: 
+	alter table repack_deadlock add column noise text;
+ <waiting ...>
+step add_column: <... completed>
+ERROR:  oops, we're blocked by REPACK, time to die
+step wakeup_before_lock: 
+	SELECT injection_points_wakeup('repack-concurrently-before-lock');
+
+injection_points_wakeup
+-----------------------
+                       
+(1 row)
+
+step wait_before_lock: <... completed>
+step check1: 
+	INSERT INTO relfilenodes(node)
+	SELECT relfilenode FROM pg_class WHERE relname='repack_deadlock';
+
+	SELECT count(DISTINCT node) FROM relfilenodes;
+
+	SELECT i, j FROM repack_deadlock ORDER BY i, j;
+
+	INSERT INTO data_s1(i, j)
+	SELECT i, j FROM repack_deadlock;
+
+	SELECT count(*)
+	FROM data_s1 d1 FULL JOIN data_s2 d2 USING (i, j)
+	WHERE d1.i ISNULL OR d2.i ISNULL;
+
+count
+-----
+    1
+(1 row)
+
+i|j
+-+-
+1|1
+2|2
+3|3
+4|4
+(4 rows)
+
+count
+-----
+    4
+(1 row)
+
+injection_points_detach
+-----------------------
+                       
+(1 row)
+
diff --git a/src/test/modules/injection_points/meson.build b/src/test/modules/injection_points/meson.build
index a414abb924b..1cd88d6db65 100644
--- a/src/test/modules/injection_points/meson.build
+++ b/src/test/modules/injection_points/meson.build
@@ -46,6 +46,7 @@ tests += {
       'basic',
       'inplace',
       'repack',
+      'repack_deadlock',
       'repack_toast',
       'syscache-update-pruned',
       'heap_lock_update',
diff --git a/src/test/modules/injection_points/specs/repack_deadlock.spec b/src/test/modules/injection_points/specs/repack_deadlock.spec
new file mode 100644
index 00000000000..9d23a6588c2
--- /dev/null
+++ b/src/test/modules/injection_points/specs/repack_deadlock.spec
@@ -0,0 +1,83 @@
+# Test REPACK with a concurrent transaction that would cause a deadlock
+setup
+{
+	CREATE EXTENSION injection_points;
+
+	CREATE TABLE repack_deadlock(i int PRIMARY KEY, j int);
+	INSERT INTO repack_deadlock(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4);
+
+	CREATE TABLE relfilenodes(node oid);
+
+	CREATE TABLE data_s1(i int, j int);
+	CREATE TABLE data_s2(i int, j int);
+}
+
+teardown
+{
+	DROP TABLE repack_deadlock;
+	DROP EXTENSION injection_points;
+
+	DROP TABLE relfilenodes;
+	DROP TABLE data_s1;
+	DROP TABLE data_s2;
+}
+
+session s1
+setup
+{
+	SELECT injection_points_set_local();
+	SELECT injection_points_attach('repack-concurrently-before-lock', 'wait');
+}
+# Perform the initial load and wait for s2 to do some data changes.
+step wait_before_lock
+{
+	REPACK (CONCURRENTLY) repack_deadlock USING INDEX repack_deadlock_pkey;
+}
+# Check the table from the perspective of s1.
+#
+# Besides the contents, we also check that relfilenode has changed.
+
+# Have each session write the contents into a table and use FULL JOIN to check
+# if the outputs are identical.
+step check1
+{
+	INSERT INTO relfilenodes(node)
+	SELECT relfilenode FROM pg_class WHERE relname='repack_deadlock';
+
+	SELECT count(DISTINCT node) FROM relfilenodes;
+
+	SELECT i, j FROM repack_deadlock ORDER BY i, j;
+
+	INSERT INTO data_s1(i, j)
+	SELECT i, j FROM repack_deadlock;
+
+	SELECT count(*)
+	FROM data_s1 d1 FULL JOIN data_s2 d2 USING (i, j)
+	WHERE d1.i ISNULL OR d2.i ISNULL;
+}
+teardown
+{
+	SELECT injection_points_detach('repack-concurrently-before-lock');
+}
+
+session s2
+# Change the existing data. UPDATE changes both key and non-key columns. Also
+# update one row twice to test whether tuple version generated by this session
+# can be found.
+step add_column
+{
+	alter table repack_deadlock add column noise text;
+}
+
+step wakeup_before_lock
+{
+	SELECT injection_points_wakeup('repack-concurrently-before-lock');
+}
+
+# Test if data changes introduced while one session is performing REPACK
+# CONCURRENTLY find their way into the table.
+permutation
+	wait_before_lock
+	add_column
+	wakeup_before_lock
+	check1
-- 
2.47.3


--fot5plqmb33ey33c--





^ permalink  raw  reply  [nested|flat] 89+ messages in thread

* [PATCH v44b 4/4] hack deadlock detector
@ 2026-03-25 19:35  Álvaro Herrera <[email protected]>
  0 siblings, 0 replies; 89+ messages in thread

From: Álvaro Herrera @ 2026-03-25 19:35 UTC (permalink / raw)

---
 src/backend/commands/cluster.c                | 10 +++
 src/backend/storage/lmgr/deadlock.c           | 12 +++
 src/include/storage/proc.h                    |  1 +
 src/test/modules/injection_points/Makefile    |  1 +
 .../expected/repack_deadlock.out              | 63 ++++++++++++++
 src/test/modules/injection_points/meson.build |  1 +
 .../specs/repack_deadlock.spec                | 83 +++++++++++++++++++
 7 files changed, 171 insertions(+)
 create mode 100644 src/test/modules/injection_points/expected/repack_deadlock.out
 create mode 100644 src/test/modules/injection_points/specs/repack_deadlock.spec

diff --git a/src/backend/commands/cluster.c b/src/backend/commands/cluster.c
index 2c3058ba10d..d5b1dfbff69 100644
--- a/src/backend/commands/cluster.c
+++ b/src/backend/commands/cluster.c
@@ -372,6 +372,16 @@ ExecRepack(ParseState *pstate, RepackStmt *stmt, bool isTopLevel)
 	/* Determine the lock mode to use. */
 	lockmode = RepackLockLevel((params.options & CLUOPT_CONCURRENT) != 0);
 
+	/*
+	 * If in concurrent mode, set the PROC_IN_CONCURRENT_REPACK flag.  This
+	 * makes the deadlock checker cause anyone that would conflict with us
+	 * to error out.  It's important to set this flag ahead of actually locking
+	 * the relation; it won't of course affect anyone until we do have a lock
+	 * that others can conflict with.
+	 */
+	if ((params.options & CLUOPT_CONCURRENT) != 0)
+		MyProc->statusFlags |= PROC_IN_CONCURRENT_REPACK;
+
 	/*
 	 * If a single relation is specified, process it and we're done ... unless
 	 * the relation is a partitioned table, in which case we fall through.
diff --git a/src/backend/storage/lmgr/deadlock.c b/src/backend/storage/lmgr/deadlock.c
index b8962d875b6..10266e384aa 100644
--- a/src/backend/storage/lmgr/deadlock.c
+++ b/src/backend/storage/lmgr/deadlock.c
@@ -620,6 +620,18 @@ FindLockCycleRecurseMember(PGPROC *checkProc,
 						proc->statusFlags & PROC_IS_AUTOVACUUM)
 						blocking_autovacuum_proc = proc;
 
+					/*
+					 * If we note that we're blocked by some process running
+					 * REPACK (CONCURRENTLY), just fail.  That process is
+					 * going to upgrade its lock at some point, and it would
+					 * be inappropriate for any other process to cause that
+					 * to fail.
+					 */
+					if (checkProc == MyProc &&
+						proc->statusFlags & PROC_IN_CONCURRENT_REPACK)
+						/* not a serious error message proposal */
+						elog(ERROR, "oops, we're blocked by REPACK, time to die");
+
 					/* We're done looking at this proclock */
 					break;
 				}
diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h
index 1dad125706e..4e340ead455 100644
--- a/src/include/storage/proc.h
+++ b/src/include/storage/proc.h
@@ -69,6 +69,7 @@ struct XidCache
 #define		PROC_AFFECTS_ALL_HORIZONS	0x20	/* this proc's xmin must be
 												 * included in vacuum horizons
 												 * in all databases */
+#define		PROC_IN_CONCURRENT_REPACK	0x40	/* REPACK (CONCURRENTLY) */
 
 /* flags reset at EOXact */
 #define		PROC_VACUUM_STATE_MASK \
diff --git a/src/test/modules/injection_points/Makefile b/src/test/modules/injection_points/Makefile
index 2cd7d87c533..f7663859fe2 100644
--- a/src/test/modules/injection_points/Makefile
+++ b/src/test/modules/injection_points/Makefile
@@ -15,6 +15,7 @@ REGRESS_OPTS = --dlpath=$(top_builddir)/src/test/regress
 ISOLATION = basic \
 	    inplace \
 	    repack \
+	    repack_deadlock \
 	    repack_toast \
 	    syscache-update-pruned \
 	    heap_lock_update
diff --git a/src/test/modules/injection_points/expected/repack_deadlock.out b/src/test/modules/injection_points/expected/repack_deadlock.out
new file mode 100644
index 00000000000..42505d10933
--- /dev/null
+++ b/src/test/modules/injection_points/expected/repack_deadlock.out
@@ -0,0 +1,63 @@
+Parsed test spec with 2 sessions
+
+starting permutation: wait_before_lock add_column wakeup_before_lock check1
+injection_points_attach
+-----------------------
+                       
+(1 row)
+
+step wait_before_lock: 
+	REPACK (CONCURRENTLY) repack_deadlock USING INDEX repack_deadlock_pkey;
+ <waiting ...>
+step add_column: 
+	alter table repack_deadlock add column noise text;
+ <waiting ...>
+step add_column: <... completed>
+ERROR:  oops, we're blocked by REPACK, time to die
+step wakeup_before_lock: 
+	SELECT injection_points_wakeup('repack-concurrently-before-lock');
+
+injection_points_wakeup
+-----------------------
+                       
+(1 row)
+
+step wait_before_lock: <... completed>
+step check1: 
+	INSERT INTO relfilenodes(node)
+	SELECT relfilenode FROM pg_class WHERE relname='repack_deadlock';
+
+	SELECT count(DISTINCT node) FROM relfilenodes;
+
+	SELECT i, j FROM repack_deadlock ORDER BY i, j;
+
+	INSERT INTO data_s1(i, j)
+	SELECT i, j FROM repack_deadlock;
+
+	SELECT count(*)
+	FROM data_s1 d1 FULL JOIN data_s2 d2 USING (i, j)
+	WHERE d1.i ISNULL OR d2.i ISNULL;
+
+count
+-----
+    1
+(1 row)
+
+i|j
+-+-
+1|1
+2|2
+3|3
+4|4
+(4 rows)
+
+count
+-----
+    4
+(1 row)
+
+injection_points_detach
+-----------------------
+                       
+(1 row)
+
diff --git a/src/test/modules/injection_points/meson.build b/src/test/modules/injection_points/meson.build
index a414abb924b..1cd88d6db65 100644
--- a/src/test/modules/injection_points/meson.build
+++ b/src/test/modules/injection_points/meson.build
@@ -46,6 +46,7 @@ tests += {
       'basic',
       'inplace',
       'repack',
+      'repack_deadlock',
       'repack_toast',
       'syscache-update-pruned',
       'heap_lock_update',
diff --git a/src/test/modules/injection_points/specs/repack_deadlock.spec b/src/test/modules/injection_points/specs/repack_deadlock.spec
new file mode 100644
index 00000000000..9d23a6588c2
--- /dev/null
+++ b/src/test/modules/injection_points/specs/repack_deadlock.spec
@@ -0,0 +1,83 @@
+# Test REPACK with a concurrent transaction that would cause a deadlock
+setup
+{
+	CREATE EXTENSION injection_points;
+
+	CREATE TABLE repack_deadlock(i int PRIMARY KEY, j int);
+	INSERT INTO repack_deadlock(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4);
+
+	CREATE TABLE relfilenodes(node oid);
+
+	CREATE TABLE data_s1(i int, j int);
+	CREATE TABLE data_s2(i int, j int);
+}
+
+teardown
+{
+	DROP TABLE repack_deadlock;
+	DROP EXTENSION injection_points;
+
+	DROP TABLE relfilenodes;
+	DROP TABLE data_s1;
+	DROP TABLE data_s2;
+}
+
+session s1
+setup
+{
+	SELECT injection_points_set_local();
+	SELECT injection_points_attach('repack-concurrently-before-lock', 'wait');
+}
+# Perform the initial load and wait for s2 to do some data changes.
+step wait_before_lock
+{
+	REPACK (CONCURRENTLY) repack_deadlock USING INDEX repack_deadlock_pkey;
+}
+# Check the table from the perspective of s1.
+#
+# Besides the contents, we also check that relfilenode has changed.
+
+# Have each session write the contents into a table and use FULL JOIN to check
+# if the outputs are identical.
+step check1
+{
+	INSERT INTO relfilenodes(node)
+	SELECT relfilenode FROM pg_class WHERE relname='repack_deadlock';
+
+	SELECT count(DISTINCT node) FROM relfilenodes;
+
+	SELECT i, j FROM repack_deadlock ORDER BY i, j;
+
+	INSERT INTO data_s1(i, j)
+	SELECT i, j FROM repack_deadlock;
+
+	SELECT count(*)
+	FROM data_s1 d1 FULL JOIN data_s2 d2 USING (i, j)
+	WHERE d1.i ISNULL OR d2.i ISNULL;
+}
+teardown
+{
+	SELECT injection_points_detach('repack-concurrently-before-lock');
+}
+
+session s2
+# Change the existing data. UPDATE changes both key and non-key columns. Also
+# update one row twice to test whether tuple version generated by this session
+# can be found.
+step add_column
+{
+	alter table repack_deadlock add column noise text;
+}
+
+step wakeup_before_lock
+{
+	SELECT injection_points_wakeup('repack-concurrently-before-lock');
+}
+
+# Test if data changes introduced while one session is performing REPACK
+# CONCURRENTLY find their way into the table.
+permutation
+	wait_before_lock
+	add_column
+	wakeup_before_lock
+	check1
-- 
2.47.3


--fot5plqmb33ey33c--





^ permalink  raw  reply  [nested|flat] 89+ messages in thread

* [PATCH v44b 4/4] hack deadlock detector
@ 2026-03-25 19:35  Álvaro Herrera <[email protected]>
  0 siblings, 0 replies; 89+ messages in thread

From: Álvaro Herrera @ 2026-03-25 19:35 UTC (permalink / raw)

---
 src/backend/commands/cluster.c                | 10 +++
 src/backend/storage/lmgr/deadlock.c           | 12 +++
 src/include/storage/proc.h                    |  1 +
 src/test/modules/injection_points/Makefile    |  1 +
 .../expected/repack_deadlock.out              | 63 ++++++++++++++
 src/test/modules/injection_points/meson.build |  1 +
 .../specs/repack_deadlock.spec                | 83 +++++++++++++++++++
 7 files changed, 171 insertions(+)
 create mode 100644 src/test/modules/injection_points/expected/repack_deadlock.out
 create mode 100644 src/test/modules/injection_points/specs/repack_deadlock.spec

diff --git a/src/backend/commands/cluster.c b/src/backend/commands/cluster.c
index 2c3058ba10d..d5b1dfbff69 100644
--- a/src/backend/commands/cluster.c
+++ b/src/backend/commands/cluster.c
@@ -372,6 +372,16 @@ ExecRepack(ParseState *pstate, RepackStmt *stmt, bool isTopLevel)
 	/* Determine the lock mode to use. */
 	lockmode = RepackLockLevel((params.options & CLUOPT_CONCURRENT) != 0);
 
+	/*
+	 * If in concurrent mode, set the PROC_IN_CONCURRENT_REPACK flag.  This
+	 * makes the deadlock checker cause anyone that would conflict with us
+	 * to error out.  It's important to set this flag ahead of actually locking
+	 * the relation; it won't of course affect anyone until we do have a lock
+	 * that others can conflict with.
+	 */
+	if ((params.options & CLUOPT_CONCURRENT) != 0)
+		MyProc->statusFlags |= PROC_IN_CONCURRENT_REPACK;
+
 	/*
 	 * If a single relation is specified, process it and we're done ... unless
 	 * the relation is a partitioned table, in which case we fall through.
diff --git a/src/backend/storage/lmgr/deadlock.c b/src/backend/storage/lmgr/deadlock.c
index b8962d875b6..10266e384aa 100644
--- a/src/backend/storage/lmgr/deadlock.c
+++ b/src/backend/storage/lmgr/deadlock.c
@@ -620,6 +620,18 @@ FindLockCycleRecurseMember(PGPROC *checkProc,
 						proc->statusFlags & PROC_IS_AUTOVACUUM)
 						blocking_autovacuum_proc = proc;
 
+					/*
+					 * If we note that we're blocked by some process running
+					 * REPACK (CONCURRENTLY), just fail.  That process is
+					 * going to upgrade its lock at some point, and it would
+					 * be inappropriate for any other process to cause that
+					 * to fail.
+					 */
+					if (checkProc == MyProc &&
+						proc->statusFlags & PROC_IN_CONCURRENT_REPACK)
+						/* not a serious error message proposal */
+						elog(ERROR, "oops, we're blocked by REPACK, time to die");
+
 					/* We're done looking at this proclock */
 					break;
 				}
diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h
index 1dad125706e..4e340ead455 100644
--- a/src/include/storage/proc.h
+++ b/src/include/storage/proc.h
@@ -69,6 +69,7 @@ struct XidCache
 #define		PROC_AFFECTS_ALL_HORIZONS	0x20	/* this proc's xmin must be
 												 * included in vacuum horizons
 												 * in all databases */
+#define		PROC_IN_CONCURRENT_REPACK	0x40	/* REPACK (CONCURRENTLY) */
 
 /* flags reset at EOXact */
 #define		PROC_VACUUM_STATE_MASK \
diff --git a/src/test/modules/injection_points/Makefile b/src/test/modules/injection_points/Makefile
index 2cd7d87c533..f7663859fe2 100644
--- a/src/test/modules/injection_points/Makefile
+++ b/src/test/modules/injection_points/Makefile
@@ -15,6 +15,7 @@ REGRESS_OPTS = --dlpath=$(top_builddir)/src/test/regress
 ISOLATION = basic \
 	    inplace \
 	    repack \
+	    repack_deadlock \
 	    repack_toast \
 	    syscache-update-pruned \
 	    heap_lock_update
diff --git a/src/test/modules/injection_points/expected/repack_deadlock.out b/src/test/modules/injection_points/expected/repack_deadlock.out
new file mode 100644
index 00000000000..42505d10933
--- /dev/null
+++ b/src/test/modules/injection_points/expected/repack_deadlock.out
@@ -0,0 +1,63 @@
+Parsed test spec with 2 sessions
+
+starting permutation: wait_before_lock add_column wakeup_before_lock check1
+injection_points_attach
+-----------------------
+                       
+(1 row)
+
+step wait_before_lock: 
+	REPACK (CONCURRENTLY) repack_deadlock USING INDEX repack_deadlock_pkey;
+ <waiting ...>
+step add_column: 
+	alter table repack_deadlock add column noise text;
+ <waiting ...>
+step add_column: <... completed>
+ERROR:  oops, we're blocked by REPACK, time to die
+step wakeup_before_lock: 
+	SELECT injection_points_wakeup('repack-concurrently-before-lock');
+
+injection_points_wakeup
+-----------------------
+                       
+(1 row)
+
+step wait_before_lock: <... completed>
+step check1: 
+	INSERT INTO relfilenodes(node)
+	SELECT relfilenode FROM pg_class WHERE relname='repack_deadlock';
+
+	SELECT count(DISTINCT node) FROM relfilenodes;
+
+	SELECT i, j FROM repack_deadlock ORDER BY i, j;
+
+	INSERT INTO data_s1(i, j)
+	SELECT i, j FROM repack_deadlock;
+
+	SELECT count(*)
+	FROM data_s1 d1 FULL JOIN data_s2 d2 USING (i, j)
+	WHERE d1.i ISNULL OR d2.i ISNULL;
+
+count
+-----
+    1
+(1 row)
+
+i|j
+-+-
+1|1
+2|2
+3|3
+4|4
+(4 rows)
+
+count
+-----
+    4
+(1 row)
+
+injection_points_detach
+-----------------------
+                       
+(1 row)
+
diff --git a/src/test/modules/injection_points/meson.build b/src/test/modules/injection_points/meson.build
index a414abb924b..1cd88d6db65 100644
--- a/src/test/modules/injection_points/meson.build
+++ b/src/test/modules/injection_points/meson.build
@@ -46,6 +46,7 @@ tests += {
       'basic',
       'inplace',
       'repack',
+      'repack_deadlock',
       'repack_toast',
       'syscache-update-pruned',
       'heap_lock_update',
diff --git a/src/test/modules/injection_points/specs/repack_deadlock.spec b/src/test/modules/injection_points/specs/repack_deadlock.spec
new file mode 100644
index 00000000000..9d23a6588c2
--- /dev/null
+++ b/src/test/modules/injection_points/specs/repack_deadlock.spec
@@ -0,0 +1,83 @@
+# Test REPACK with a concurrent transaction that would cause a deadlock
+setup
+{
+	CREATE EXTENSION injection_points;
+
+	CREATE TABLE repack_deadlock(i int PRIMARY KEY, j int);
+	INSERT INTO repack_deadlock(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4);
+
+	CREATE TABLE relfilenodes(node oid);
+
+	CREATE TABLE data_s1(i int, j int);
+	CREATE TABLE data_s2(i int, j int);
+}
+
+teardown
+{
+	DROP TABLE repack_deadlock;
+	DROP EXTENSION injection_points;
+
+	DROP TABLE relfilenodes;
+	DROP TABLE data_s1;
+	DROP TABLE data_s2;
+}
+
+session s1
+setup
+{
+	SELECT injection_points_set_local();
+	SELECT injection_points_attach('repack-concurrently-before-lock', 'wait');
+}
+# Perform the initial load and wait for s2 to do some data changes.
+step wait_before_lock
+{
+	REPACK (CONCURRENTLY) repack_deadlock USING INDEX repack_deadlock_pkey;
+}
+# Check the table from the perspective of s1.
+#
+# Besides the contents, we also check that relfilenode has changed.
+
+# Have each session write the contents into a table and use FULL JOIN to check
+# if the outputs are identical.
+step check1
+{
+	INSERT INTO relfilenodes(node)
+	SELECT relfilenode FROM pg_class WHERE relname='repack_deadlock';
+
+	SELECT count(DISTINCT node) FROM relfilenodes;
+
+	SELECT i, j FROM repack_deadlock ORDER BY i, j;
+
+	INSERT INTO data_s1(i, j)
+	SELECT i, j FROM repack_deadlock;
+
+	SELECT count(*)
+	FROM data_s1 d1 FULL JOIN data_s2 d2 USING (i, j)
+	WHERE d1.i ISNULL OR d2.i ISNULL;
+}
+teardown
+{
+	SELECT injection_points_detach('repack-concurrently-before-lock');
+}
+
+session s2
+# Change the existing data. UPDATE changes both key and non-key columns. Also
+# update one row twice to test whether tuple version generated by this session
+# can be found.
+step add_column
+{
+	alter table repack_deadlock add column noise text;
+}
+
+step wakeup_before_lock
+{
+	SELECT injection_points_wakeup('repack-concurrently-before-lock');
+}
+
+# Test if data changes introduced while one session is performing REPACK
+# CONCURRENTLY find their way into the table.
+permutation
+	wait_before_lock
+	add_column
+	wakeup_before_lock
+	check1
-- 
2.47.3


--fot5plqmb33ey33c--





^ permalink  raw  reply  [nested|flat] 89+ messages in thread

* [PATCH v44b 4/4] hack deadlock detector
@ 2026-03-25 19:35  Álvaro Herrera <[email protected]>
  0 siblings, 0 replies; 89+ messages in thread

From: Álvaro Herrera @ 2026-03-25 19:35 UTC (permalink / raw)

---
 src/backend/commands/cluster.c                | 10 +++
 src/backend/storage/lmgr/deadlock.c           | 12 +++
 src/include/storage/proc.h                    |  1 +
 src/test/modules/injection_points/Makefile    |  1 +
 .../expected/repack_deadlock.out              | 63 ++++++++++++++
 src/test/modules/injection_points/meson.build |  1 +
 .../specs/repack_deadlock.spec                | 83 +++++++++++++++++++
 7 files changed, 171 insertions(+)
 create mode 100644 src/test/modules/injection_points/expected/repack_deadlock.out
 create mode 100644 src/test/modules/injection_points/specs/repack_deadlock.spec

diff --git a/src/backend/commands/cluster.c b/src/backend/commands/cluster.c
index 2c3058ba10d..d5b1dfbff69 100644
--- a/src/backend/commands/cluster.c
+++ b/src/backend/commands/cluster.c
@@ -372,6 +372,16 @@ ExecRepack(ParseState *pstate, RepackStmt *stmt, bool isTopLevel)
 	/* Determine the lock mode to use. */
 	lockmode = RepackLockLevel((params.options & CLUOPT_CONCURRENT) != 0);
 
+	/*
+	 * If in concurrent mode, set the PROC_IN_CONCURRENT_REPACK flag.  This
+	 * makes the deadlock checker cause anyone that would conflict with us
+	 * to error out.  It's important to set this flag ahead of actually locking
+	 * the relation; it won't of course affect anyone until we do have a lock
+	 * that others can conflict with.
+	 */
+	if ((params.options & CLUOPT_CONCURRENT) != 0)
+		MyProc->statusFlags |= PROC_IN_CONCURRENT_REPACK;
+
 	/*
 	 * If a single relation is specified, process it and we're done ... unless
 	 * the relation is a partitioned table, in which case we fall through.
diff --git a/src/backend/storage/lmgr/deadlock.c b/src/backend/storage/lmgr/deadlock.c
index b8962d875b6..10266e384aa 100644
--- a/src/backend/storage/lmgr/deadlock.c
+++ b/src/backend/storage/lmgr/deadlock.c
@@ -620,6 +620,18 @@ FindLockCycleRecurseMember(PGPROC *checkProc,
 						proc->statusFlags & PROC_IS_AUTOVACUUM)
 						blocking_autovacuum_proc = proc;
 
+					/*
+					 * If we note that we're blocked by some process running
+					 * REPACK (CONCURRENTLY), just fail.  That process is
+					 * going to upgrade its lock at some point, and it would
+					 * be inappropriate for any other process to cause that
+					 * to fail.
+					 */
+					if (checkProc == MyProc &&
+						proc->statusFlags & PROC_IN_CONCURRENT_REPACK)
+						/* not a serious error message proposal */
+						elog(ERROR, "oops, we're blocked by REPACK, time to die");
+
 					/* We're done looking at this proclock */
 					break;
 				}
diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h
index 1dad125706e..4e340ead455 100644
--- a/src/include/storage/proc.h
+++ b/src/include/storage/proc.h
@@ -69,6 +69,7 @@ struct XidCache
 #define		PROC_AFFECTS_ALL_HORIZONS	0x20	/* this proc's xmin must be
 												 * included in vacuum horizons
 												 * in all databases */
+#define		PROC_IN_CONCURRENT_REPACK	0x40	/* REPACK (CONCURRENTLY) */
 
 /* flags reset at EOXact */
 #define		PROC_VACUUM_STATE_MASK \
diff --git a/src/test/modules/injection_points/Makefile b/src/test/modules/injection_points/Makefile
index 2cd7d87c533..f7663859fe2 100644
--- a/src/test/modules/injection_points/Makefile
+++ b/src/test/modules/injection_points/Makefile
@@ -15,6 +15,7 @@ REGRESS_OPTS = --dlpath=$(top_builddir)/src/test/regress
 ISOLATION = basic \
 	    inplace \
 	    repack \
+	    repack_deadlock \
 	    repack_toast \
 	    syscache-update-pruned \
 	    heap_lock_update
diff --git a/src/test/modules/injection_points/expected/repack_deadlock.out b/src/test/modules/injection_points/expected/repack_deadlock.out
new file mode 100644
index 00000000000..42505d10933
--- /dev/null
+++ b/src/test/modules/injection_points/expected/repack_deadlock.out
@@ -0,0 +1,63 @@
+Parsed test spec with 2 sessions
+
+starting permutation: wait_before_lock add_column wakeup_before_lock check1
+injection_points_attach
+-----------------------
+                       
+(1 row)
+
+step wait_before_lock: 
+	REPACK (CONCURRENTLY) repack_deadlock USING INDEX repack_deadlock_pkey;
+ <waiting ...>
+step add_column: 
+	alter table repack_deadlock add column noise text;
+ <waiting ...>
+step add_column: <... completed>
+ERROR:  oops, we're blocked by REPACK, time to die
+step wakeup_before_lock: 
+	SELECT injection_points_wakeup('repack-concurrently-before-lock');
+
+injection_points_wakeup
+-----------------------
+                       
+(1 row)
+
+step wait_before_lock: <... completed>
+step check1: 
+	INSERT INTO relfilenodes(node)
+	SELECT relfilenode FROM pg_class WHERE relname='repack_deadlock';
+
+	SELECT count(DISTINCT node) FROM relfilenodes;
+
+	SELECT i, j FROM repack_deadlock ORDER BY i, j;
+
+	INSERT INTO data_s1(i, j)
+	SELECT i, j FROM repack_deadlock;
+
+	SELECT count(*)
+	FROM data_s1 d1 FULL JOIN data_s2 d2 USING (i, j)
+	WHERE d1.i ISNULL OR d2.i ISNULL;
+
+count
+-----
+    1
+(1 row)
+
+i|j
+-+-
+1|1
+2|2
+3|3
+4|4
+(4 rows)
+
+count
+-----
+    4
+(1 row)
+
+injection_points_detach
+-----------------------
+                       
+(1 row)
+
diff --git a/src/test/modules/injection_points/meson.build b/src/test/modules/injection_points/meson.build
index a414abb924b..1cd88d6db65 100644
--- a/src/test/modules/injection_points/meson.build
+++ b/src/test/modules/injection_points/meson.build
@@ -46,6 +46,7 @@ tests += {
       'basic',
       'inplace',
       'repack',
+      'repack_deadlock',
       'repack_toast',
       'syscache-update-pruned',
       'heap_lock_update',
diff --git a/src/test/modules/injection_points/specs/repack_deadlock.spec b/src/test/modules/injection_points/specs/repack_deadlock.spec
new file mode 100644
index 00000000000..9d23a6588c2
--- /dev/null
+++ b/src/test/modules/injection_points/specs/repack_deadlock.spec
@@ -0,0 +1,83 @@
+# Test REPACK with a concurrent transaction that would cause a deadlock
+setup
+{
+	CREATE EXTENSION injection_points;
+
+	CREATE TABLE repack_deadlock(i int PRIMARY KEY, j int);
+	INSERT INTO repack_deadlock(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4);
+
+	CREATE TABLE relfilenodes(node oid);
+
+	CREATE TABLE data_s1(i int, j int);
+	CREATE TABLE data_s2(i int, j int);
+}
+
+teardown
+{
+	DROP TABLE repack_deadlock;
+	DROP EXTENSION injection_points;
+
+	DROP TABLE relfilenodes;
+	DROP TABLE data_s1;
+	DROP TABLE data_s2;
+}
+
+session s1
+setup
+{
+	SELECT injection_points_set_local();
+	SELECT injection_points_attach('repack-concurrently-before-lock', 'wait');
+}
+# Perform the initial load and wait for s2 to do some data changes.
+step wait_before_lock
+{
+	REPACK (CONCURRENTLY) repack_deadlock USING INDEX repack_deadlock_pkey;
+}
+# Check the table from the perspective of s1.
+#
+# Besides the contents, we also check that relfilenode has changed.
+
+# Have each session write the contents into a table and use FULL JOIN to check
+# if the outputs are identical.
+step check1
+{
+	INSERT INTO relfilenodes(node)
+	SELECT relfilenode FROM pg_class WHERE relname='repack_deadlock';
+
+	SELECT count(DISTINCT node) FROM relfilenodes;
+
+	SELECT i, j FROM repack_deadlock ORDER BY i, j;
+
+	INSERT INTO data_s1(i, j)
+	SELECT i, j FROM repack_deadlock;
+
+	SELECT count(*)
+	FROM data_s1 d1 FULL JOIN data_s2 d2 USING (i, j)
+	WHERE d1.i ISNULL OR d2.i ISNULL;
+}
+teardown
+{
+	SELECT injection_points_detach('repack-concurrently-before-lock');
+}
+
+session s2
+# Change the existing data. UPDATE changes both key and non-key columns. Also
+# update one row twice to test whether tuple version generated by this session
+# can be found.
+step add_column
+{
+	alter table repack_deadlock add column noise text;
+}
+
+step wakeup_before_lock
+{
+	SELECT injection_points_wakeup('repack-concurrently-before-lock');
+}
+
+# Test if data changes introduced while one session is performing REPACK
+# CONCURRENTLY find their way into the table.
+permutation
+	wait_before_lock
+	add_column
+	wakeup_before_lock
+	check1
-- 
2.47.3


--fot5plqmb33ey33c--





^ permalink  raw  reply  [nested|flat] 89+ messages in thread

* [PATCH v44b 4/4] hack deadlock detector
@ 2026-03-25 19:35  Álvaro Herrera <[email protected]>
  0 siblings, 0 replies; 89+ messages in thread

From: Álvaro Herrera @ 2026-03-25 19:35 UTC (permalink / raw)

---
 src/backend/commands/cluster.c                | 10 +++
 src/backend/storage/lmgr/deadlock.c           | 12 +++
 src/include/storage/proc.h                    |  1 +
 src/test/modules/injection_points/Makefile    |  1 +
 .../expected/repack_deadlock.out              | 63 ++++++++++++++
 src/test/modules/injection_points/meson.build |  1 +
 .../specs/repack_deadlock.spec                | 83 +++++++++++++++++++
 7 files changed, 171 insertions(+)
 create mode 100644 src/test/modules/injection_points/expected/repack_deadlock.out
 create mode 100644 src/test/modules/injection_points/specs/repack_deadlock.spec

diff --git a/src/backend/commands/cluster.c b/src/backend/commands/cluster.c
index 2c3058ba10d..d5b1dfbff69 100644
--- a/src/backend/commands/cluster.c
+++ b/src/backend/commands/cluster.c
@@ -372,6 +372,16 @@ ExecRepack(ParseState *pstate, RepackStmt *stmt, bool isTopLevel)
 	/* Determine the lock mode to use. */
 	lockmode = RepackLockLevel((params.options & CLUOPT_CONCURRENT) != 0);
 
+	/*
+	 * If in concurrent mode, set the PROC_IN_CONCURRENT_REPACK flag.  This
+	 * makes the deadlock checker cause anyone that would conflict with us
+	 * to error out.  It's important to set this flag ahead of actually locking
+	 * the relation; it won't of course affect anyone until we do have a lock
+	 * that others can conflict with.
+	 */
+	if ((params.options & CLUOPT_CONCURRENT) != 0)
+		MyProc->statusFlags |= PROC_IN_CONCURRENT_REPACK;
+
 	/*
 	 * If a single relation is specified, process it and we're done ... unless
 	 * the relation is a partitioned table, in which case we fall through.
diff --git a/src/backend/storage/lmgr/deadlock.c b/src/backend/storage/lmgr/deadlock.c
index b8962d875b6..10266e384aa 100644
--- a/src/backend/storage/lmgr/deadlock.c
+++ b/src/backend/storage/lmgr/deadlock.c
@@ -620,6 +620,18 @@ FindLockCycleRecurseMember(PGPROC *checkProc,
 						proc->statusFlags & PROC_IS_AUTOVACUUM)
 						blocking_autovacuum_proc = proc;
 
+					/*
+					 * If we note that we're blocked by some process running
+					 * REPACK (CONCURRENTLY), just fail.  That process is
+					 * going to upgrade its lock at some point, and it would
+					 * be inappropriate for any other process to cause that
+					 * to fail.
+					 */
+					if (checkProc == MyProc &&
+						proc->statusFlags & PROC_IN_CONCURRENT_REPACK)
+						/* not a serious error message proposal */
+						elog(ERROR, "oops, we're blocked by REPACK, time to die");
+
 					/* We're done looking at this proclock */
 					break;
 				}
diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h
index 1dad125706e..4e340ead455 100644
--- a/src/include/storage/proc.h
+++ b/src/include/storage/proc.h
@@ -69,6 +69,7 @@ struct XidCache
 #define		PROC_AFFECTS_ALL_HORIZONS	0x20	/* this proc's xmin must be
 												 * included in vacuum horizons
 												 * in all databases */
+#define		PROC_IN_CONCURRENT_REPACK	0x40	/* REPACK (CONCURRENTLY) */
 
 /* flags reset at EOXact */
 #define		PROC_VACUUM_STATE_MASK \
diff --git a/src/test/modules/injection_points/Makefile b/src/test/modules/injection_points/Makefile
index 2cd7d87c533..f7663859fe2 100644
--- a/src/test/modules/injection_points/Makefile
+++ b/src/test/modules/injection_points/Makefile
@@ -15,6 +15,7 @@ REGRESS_OPTS = --dlpath=$(top_builddir)/src/test/regress
 ISOLATION = basic \
 	    inplace \
 	    repack \
+	    repack_deadlock \
 	    repack_toast \
 	    syscache-update-pruned \
 	    heap_lock_update
diff --git a/src/test/modules/injection_points/expected/repack_deadlock.out b/src/test/modules/injection_points/expected/repack_deadlock.out
new file mode 100644
index 00000000000..42505d10933
--- /dev/null
+++ b/src/test/modules/injection_points/expected/repack_deadlock.out
@@ -0,0 +1,63 @@
+Parsed test spec with 2 sessions
+
+starting permutation: wait_before_lock add_column wakeup_before_lock check1
+injection_points_attach
+-----------------------
+                       
+(1 row)
+
+step wait_before_lock: 
+	REPACK (CONCURRENTLY) repack_deadlock USING INDEX repack_deadlock_pkey;
+ <waiting ...>
+step add_column: 
+	alter table repack_deadlock add column noise text;
+ <waiting ...>
+step add_column: <... completed>
+ERROR:  oops, we're blocked by REPACK, time to die
+step wakeup_before_lock: 
+	SELECT injection_points_wakeup('repack-concurrently-before-lock');
+
+injection_points_wakeup
+-----------------------
+                       
+(1 row)
+
+step wait_before_lock: <... completed>
+step check1: 
+	INSERT INTO relfilenodes(node)
+	SELECT relfilenode FROM pg_class WHERE relname='repack_deadlock';
+
+	SELECT count(DISTINCT node) FROM relfilenodes;
+
+	SELECT i, j FROM repack_deadlock ORDER BY i, j;
+
+	INSERT INTO data_s1(i, j)
+	SELECT i, j FROM repack_deadlock;
+
+	SELECT count(*)
+	FROM data_s1 d1 FULL JOIN data_s2 d2 USING (i, j)
+	WHERE d1.i ISNULL OR d2.i ISNULL;
+
+count
+-----
+    1
+(1 row)
+
+i|j
+-+-
+1|1
+2|2
+3|3
+4|4
+(4 rows)
+
+count
+-----
+    4
+(1 row)
+
+injection_points_detach
+-----------------------
+                       
+(1 row)
+
diff --git a/src/test/modules/injection_points/meson.build b/src/test/modules/injection_points/meson.build
index a414abb924b..1cd88d6db65 100644
--- a/src/test/modules/injection_points/meson.build
+++ b/src/test/modules/injection_points/meson.build
@@ -46,6 +46,7 @@ tests += {
       'basic',
       'inplace',
       'repack',
+      'repack_deadlock',
       'repack_toast',
       'syscache-update-pruned',
       'heap_lock_update',
diff --git a/src/test/modules/injection_points/specs/repack_deadlock.spec b/src/test/modules/injection_points/specs/repack_deadlock.spec
new file mode 100644
index 00000000000..9d23a6588c2
--- /dev/null
+++ b/src/test/modules/injection_points/specs/repack_deadlock.spec
@@ -0,0 +1,83 @@
+# Test REPACK with a concurrent transaction that would cause a deadlock
+setup
+{
+	CREATE EXTENSION injection_points;
+
+	CREATE TABLE repack_deadlock(i int PRIMARY KEY, j int);
+	INSERT INTO repack_deadlock(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4);
+
+	CREATE TABLE relfilenodes(node oid);
+
+	CREATE TABLE data_s1(i int, j int);
+	CREATE TABLE data_s2(i int, j int);
+}
+
+teardown
+{
+	DROP TABLE repack_deadlock;
+	DROP EXTENSION injection_points;
+
+	DROP TABLE relfilenodes;
+	DROP TABLE data_s1;
+	DROP TABLE data_s2;
+}
+
+session s1
+setup
+{
+	SELECT injection_points_set_local();
+	SELECT injection_points_attach('repack-concurrently-before-lock', 'wait');
+}
+# Perform the initial load and wait for s2 to do some data changes.
+step wait_before_lock
+{
+	REPACK (CONCURRENTLY) repack_deadlock USING INDEX repack_deadlock_pkey;
+}
+# Check the table from the perspective of s1.
+#
+# Besides the contents, we also check that relfilenode has changed.
+
+# Have each session write the contents into a table and use FULL JOIN to check
+# if the outputs are identical.
+step check1
+{
+	INSERT INTO relfilenodes(node)
+	SELECT relfilenode FROM pg_class WHERE relname='repack_deadlock';
+
+	SELECT count(DISTINCT node) FROM relfilenodes;
+
+	SELECT i, j FROM repack_deadlock ORDER BY i, j;
+
+	INSERT INTO data_s1(i, j)
+	SELECT i, j FROM repack_deadlock;
+
+	SELECT count(*)
+	FROM data_s1 d1 FULL JOIN data_s2 d2 USING (i, j)
+	WHERE d1.i ISNULL OR d2.i ISNULL;
+}
+teardown
+{
+	SELECT injection_points_detach('repack-concurrently-before-lock');
+}
+
+session s2
+# Change the existing data. UPDATE changes both key and non-key columns. Also
+# update one row twice to test whether tuple version generated by this session
+# can be found.
+step add_column
+{
+	alter table repack_deadlock add column noise text;
+}
+
+step wakeup_before_lock
+{
+	SELECT injection_points_wakeup('repack-concurrently-before-lock');
+}
+
+# Test if data changes introduced while one session is performing REPACK
+# CONCURRENTLY find their way into the table.
+permutation
+	wait_before_lock
+	add_column
+	wakeup_before_lock
+	check1
-- 
2.47.3


--fot5plqmb33ey33c--





^ permalink  raw  reply  [nested|flat] 89+ messages in thread

* [PATCH v44b 4/4] hack deadlock detector
@ 2026-03-25 19:35  Álvaro Herrera <[email protected]>
  0 siblings, 0 replies; 89+ messages in thread

From: Álvaro Herrera @ 2026-03-25 19:35 UTC (permalink / raw)

---
 src/backend/commands/cluster.c                | 10 +++
 src/backend/storage/lmgr/deadlock.c           | 12 +++
 src/include/storage/proc.h                    |  1 +
 src/test/modules/injection_points/Makefile    |  1 +
 .../expected/repack_deadlock.out              | 63 ++++++++++++++
 src/test/modules/injection_points/meson.build |  1 +
 .../specs/repack_deadlock.spec                | 83 +++++++++++++++++++
 7 files changed, 171 insertions(+)
 create mode 100644 src/test/modules/injection_points/expected/repack_deadlock.out
 create mode 100644 src/test/modules/injection_points/specs/repack_deadlock.spec

diff --git a/src/backend/commands/cluster.c b/src/backend/commands/cluster.c
index 2c3058ba10d..d5b1dfbff69 100644
--- a/src/backend/commands/cluster.c
+++ b/src/backend/commands/cluster.c
@@ -372,6 +372,16 @@ ExecRepack(ParseState *pstate, RepackStmt *stmt, bool isTopLevel)
 	/* Determine the lock mode to use. */
 	lockmode = RepackLockLevel((params.options & CLUOPT_CONCURRENT) != 0);
 
+	/*
+	 * If in concurrent mode, set the PROC_IN_CONCURRENT_REPACK flag.  This
+	 * makes the deadlock checker cause anyone that would conflict with us
+	 * to error out.  It's important to set this flag ahead of actually locking
+	 * the relation; it won't of course affect anyone until we do have a lock
+	 * that others can conflict with.
+	 */
+	if ((params.options & CLUOPT_CONCURRENT) != 0)
+		MyProc->statusFlags |= PROC_IN_CONCURRENT_REPACK;
+
 	/*
 	 * If a single relation is specified, process it and we're done ... unless
 	 * the relation is a partitioned table, in which case we fall through.
diff --git a/src/backend/storage/lmgr/deadlock.c b/src/backend/storage/lmgr/deadlock.c
index b8962d875b6..10266e384aa 100644
--- a/src/backend/storage/lmgr/deadlock.c
+++ b/src/backend/storage/lmgr/deadlock.c
@@ -620,6 +620,18 @@ FindLockCycleRecurseMember(PGPROC *checkProc,
 						proc->statusFlags & PROC_IS_AUTOVACUUM)
 						blocking_autovacuum_proc = proc;
 
+					/*
+					 * If we note that we're blocked by some process running
+					 * REPACK (CONCURRENTLY), just fail.  That process is
+					 * going to upgrade its lock at some point, and it would
+					 * be inappropriate for any other process to cause that
+					 * to fail.
+					 */
+					if (checkProc == MyProc &&
+						proc->statusFlags & PROC_IN_CONCURRENT_REPACK)
+						/* not a serious error message proposal */
+						elog(ERROR, "oops, we're blocked by REPACK, time to die");
+
 					/* We're done looking at this proclock */
 					break;
 				}
diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h
index 1dad125706e..4e340ead455 100644
--- a/src/include/storage/proc.h
+++ b/src/include/storage/proc.h
@@ -69,6 +69,7 @@ struct XidCache
 #define		PROC_AFFECTS_ALL_HORIZONS	0x20	/* this proc's xmin must be
 												 * included in vacuum horizons
 												 * in all databases */
+#define		PROC_IN_CONCURRENT_REPACK	0x40	/* REPACK (CONCURRENTLY) */
 
 /* flags reset at EOXact */
 #define		PROC_VACUUM_STATE_MASK \
diff --git a/src/test/modules/injection_points/Makefile b/src/test/modules/injection_points/Makefile
index 2cd7d87c533..f7663859fe2 100644
--- a/src/test/modules/injection_points/Makefile
+++ b/src/test/modules/injection_points/Makefile
@@ -15,6 +15,7 @@ REGRESS_OPTS = --dlpath=$(top_builddir)/src/test/regress
 ISOLATION = basic \
 	    inplace \
 	    repack \
+	    repack_deadlock \
 	    repack_toast \
 	    syscache-update-pruned \
 	    heap_lock_update
diff --git a/src/test/modules/injection_points/expected/repack_deadlock.out b/src/test/modules/injection_points/expected/repack_deadlock.out
new file mode 100644
index 00000000000..42505d10933
--- /dev/null
+++ b/src/test/modules/injection_points/expected/repack_deadlock.out
@@ -0,0 +1,63 @@
+Parsed test spec with 2 sessions
+
+starting permutation: wait_before_lock add_column wakeup_before_lock check1
+injection_points_attach
+-----------------------
+                       
+(1 row)
+
+step wait_before_lock: 
+	REPACK (CONCURRENTLY) repack_deadlock USING INDEX repack_deadlock_pkey;
+ <waiting ...>
+step add_column: 
+	alter table repack_deadlock add column noise text;
+ <waiting ...>
+step add_column: <... completed>
+ERROR:  oops, we're blocked by REPACK, time to die
+step wakeup_before_lock: 
+	SELECT injection_points_wakeup('repack-concurrently-before-lock');
+
+injection_points_wakeup
+-----------------------
+                       
+(1 row)
+
+step wait_before_lock: <... completed>
+step check1: 
+	INSERT INTO relfilenodes(node)
+	SELECT relfilenode FROM pg_class WHERE relname='repack_deadlock';
+
+	SELECT count(DISTINCT node) FROM relfilenodes;
+
+	SELECT i, j FROM repack_deadlock ORDER BY i, j;
+
+	INSERT INTO data_s1(i, j)
+	SELECT i, j FROM repack_deadlock;
+
+	SELECT count(*)
+	FROM data_s1 d1 FULL JOIN data_s2 d2 USING (i, j)
+	WHERE d1.i ISNULL OR d2.i ISNULL;
+
+count
+-----
+    1
+(1 row)
+
+i|j
+-+-
+1|1
+2|2
+3|3
+4|4
+(4 rows)
+
+count
+-----
+    4
+(1 row)
+
+injection_points_detach
+-----------------------
+                       
+(1 row)
+
diff --git a/src/test/modules/injection_points/meson.build b/src/test/modules/injection_points/meson.build
index a414abb924b..1cd88d6db65 100644
--- a/src/test/modules/injection_points/meson.build
+++ b/src/test/modules/injection_points/meson.build
@@ -46,6 +46,7 @@ tests += {
       'basic',
       'inplace',
       'repack',
+      'repack_deadlock',
       'repack_toast',
       'syscache-update-pruned',
       'heap_lock_update',
diff --git a/src/test/modules/injection_points/specs/repack_deadlock.spec b/src/test/modules/injection_points/specs/repack_deadlock.spec
new file mode 100644
index 00000000000..9d23a6588c2
--- /dev/null
+++ b/src/test/modules/injection_points/specs/repack_deadlock.spec
@@ -0,0 +1,83 @@
+# Test REPACK with a concurrent transaction that would cause a deadlock
+setup
+{
+	CREATE EXTENSION injection_points;
+
+	CREATE TABLE repack_deadlock(i int PRIMARY KEY, j int);
+	INSERT INTO repack_deadlock(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4);
+
+	CREATE TABLE relfilenodes(node oid);
+
+	CREATE TABLE data_s1(i int, j int);
+	CREATE TABLE data_s2(i int, j int);
+}
+
+teardown
+{
+	DROP TABLE repack_deadlock;
+	DROP EXTENSION injection_points;
+
+	DROP TABLE relfilenodes;
+	DROP TABLE data_s1;
+	DROP TABLE data_s2;
+}
+
+session s1
+setup
+{
+	SELECT injection_points_set_local();
+	SELECT injection_points_attach('repack-concurrently-before-lock', 'wait');
+}
+# Perform the initial load and wait for s2 to do some data changes.
+step wait_before_lock
+{
+	REPACK (CONCURRENTLY) repack_deadlock USING INDEX repack_deadlock_pkey;
+}
+# Check the table from the perspective of s1.
+#
+# Besides the contents, we also check that relfilenode has changed.
+
+# Have each session write the contents into a table and use FULL JOIN to check
+# if the outputs are identical.
+step check1
+{
+	INSERT INTO relfilenodes(node)
+	SELECT relfilenode FROM pg_class WHERE relname='repack_deadlock';
+
+	SELECT count(DISTINCT node) FROM relfilenodes;
+
+	SELECT i, j FROM repack_deadlock ORDER BY i, j;
+
+	INSERT INTO data_s1(i, j)
+	SELECT i, j FROM repack_deadlock;
+
+	SELECT count(*)
+	FROM data_s1 d1 FULL JOIN data_s2 d2 USING (i, j)
+	WHERE d1.i ISNULL OR d2.i ISNULL;
+}
+teardown
+{
+	SELECT injection_points_detach('repack-concurrently-before-lock');
+}
+
+session s2
+# Change the existing data. UPDATE changes both key and non-key columns. Also
+# update one row twice to test whether tuple version generated by this session
+# can be found.
+step add_column
+{
+	alter table repack_deadlock add column noise text;
+}
+
+step wakeup_before_lock
+{
+	SELECT injection_points_wakeup('repack-concurrently-before-lock');
+}
+
+# Test if data changes introduced while one session is performing REPACK
+# CONCURRENTLY find their way into the table.
+permutation
+	wait_before_lock
+	add_column
+	wakeup_before_lock
+	check1
-- 
2.47.3


--fot5plqmb33ey33c--





^ permalink  raw  reply  [nested|flat] 89+ messages in thread

* [PATCH v44b 4/4] hack deadlock detector
@ 2026-03-25 19:35  Álvaro Herrera <[email protected]>
  0 siblings, 0 replies; 89+ messages in thread

From: Álvaro Herrera @ 2026-03-25 19:35 UTC (permalink / raw)

---
 src/backend/commands/cluster.c                | 10 +++
 src/backend/storage/lmgr/deadlock.c           | 12 +++
 src/include/storage/proc.h                    |  1 +
 src/test/modules/injection_points/Makefile    |  1 +
 .../expected/repack_deadlock.out              | 63 ++++++++++++++
 src/test/modules/injection_points/meson.build |  1 +
 .../specs/repack_deadlock.spec                | 83 +++++++++++++++++++
 7 files changed, 171 insertions(+)
 create mode 100644 src/test/modules/injection_points/expected/repack_deadlock.out
 create mode 100644 src/test/modules/injection_points/specs/repack_deadlock.spec

diff --git a/src/backend/commands/cluster.c b/src/backend/commands/cluster.c
index 2c3058ba10d..d5b1dfbff69 100644
--- a/src/backend/commands/cluster.c
+++ b/src/backend/commands/cluster.c
@@ -372,6 +372,16 @@ ExecRepack(ParseState *pstate, RepackStmt *stmt, bool isTopLevel)
 	/* Determine the lock mode to use. */
 	lockmode = RepackLockLevel((params.options & CLUOPT_CONCURRENT) != 0);
 
+	/*
+	 * If in concurrent mode, set the PROC_IN_CONCURRENT_REPACK flag.  This
+	 * makes the deadlock checker cause anyone that would conflict with us
+	 * to error out.  It's important to set this flag ahead of actually locking
+	 * the relation; it won't of course affect anyone until we do have a lock
+	 * that others can conflict with.
+	 */
+	if ((params.options & CLUOPT_CONCURRENT) != 0)
+		MyProc->statusFlags |= PROC_IN_CONCURRENT_REPACK;
+
 	/*
 	 * If a single relation is specified, process it and we're done ... unless
 	 * the relation is a partitioned table, in which case we fall through.
diff --git a/src/backend/storage/lmgr/deadlock.c b/src/backend/storage/lmgr/deadlock.c
index b8962d875b6..10266e384aa 100644
--- a/src/backend/storage/lmgr/deadlock.c
+++ b/src/backend/storage/lmgr/deadlock.c
@@ -620,6 +620,18 @@ FindLockCycleRecurseMember(PGPROC *checkProc,
 						proc->statusFlags & PROC_IS_AUTOVACUUM)
 						blocking_autovacuum_proc = proc;
 
+					/*
+					 * If we note that we're blocked by some process running
+					 * REPACK (CONCURRENTLY), just fail.  That process is
+					 * going to upgrade its lock at some point, and it would
+					 * be inappropriate for any other process to cause that
+					 * to fail.
+					 */
+					if (checkProc == MyProc &&
+						proc->statusFlags & PROC_IN_CONCURRENT_REPACK)
+						/* not a serious error message proposal */
+						elog(ERROR, "oops, we're blocked by REPACK, time to die");
+
 					/* We're done looking at this proclock */
 					break;
 				}
diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h
index 1dad125706e..4e340ead455 100644
--- a/src/include/storage/proc.h
+++ b/src/include/storage/proc.h
@@ -69,6 +69,7 @@ struct XidCache
 #define		PROC_AFFECTS_ALL_HORIZONS	0x20	/* this proc's xmin must be
 												 * included in vacuum horizons
 												 * in all databases */
+#define		PROC_IN_CONCURRENT_REPACK	0x40	/* REPACK (CONCURRENTLY) */
 
 /* flags reset at EOXact */
 #define		PROC_VACUUM_STATE_MASK \
diff --git a/src/test/modules/injection_points/Makefile b/src/test/modules/injection_points/Makefile
index 2cd7d87c533..f7663859fe2 100644
--- a/src/test/modules/injection_points/Makefile
+++ b/src/test/modules/injection_points/Makefile
@@ -15,6 +15,7 @@ REGRESS_OPTS = --dlpath=$(top_builddir)/src/test/regress
 ISOLATION = basic \
 	    inplace \
 	    repack \
+	    repack_deadlock \
 	    repack_toast \
 	    syscache-update-pruned \
 	    heap_lock_update
diff --git a/src/test/modules/injection_points/expected/repack_deadlock.out b/src/test/modules/injection_points/expected/repack_deadlock.out
new file mode 100644
index 00000000000..42505d10933
--- /dev/null
+++ b/src/test/modules/injection_points/expected/repack_deadlock.out
@@ -0,0 +1,63 @@
+Parsed test spec with 2 sessions
+
+starting permutation: wait_before_lock add_column wakeup_before_lock check1
+injection_points_attach
+-----------------------
+                       
+(1 row)
+
+step wait_before_lock: 
+	REPACK (CONCURRENTLY) repack_deadlock USING INDEX repack_deadlock_pkey;
+ <waiting ...>
+step add_column: 
+	alter table repack_deadlock add column noise text;
+ <waiting ...>
+step add_column: <... completed>
+ERROR:  oops, we're blocked by REPACK, time to die
+step wakeup_before_lock: 
+	SELECT injection_points_wakeup('repack-concurrently-before-lock');
+
+injection_points_wakeup
+-----------------------
+                       
+(1 row)
+
+step wait_before_lock: <... completed>
+step check1: 
+	INSERT INTO relfilenodes(node)
+	SELECT relfilenode FROM pg_class WHERE relname='repack_deadlock';
+
+	SELECT count(DISTINCT node) FROM relfilenodes;
+
+	SELECT i, j FROM repack_deadlock ORDER BY i, j;
+
+	INSERT INTO data_s1(i, j)
+	SELECT i, j FROM repack_deadlock;
+
+	SELECT count(*)
+	FROM data_s1 d1 FULL JOIN data_s2 d2 USING (i, j)
+	WHERE d1.i ISNULL OR d2.i ISNULL;
+
+count
+-----
+    1
+(1 row)
+
+i|j
+-+-
+1|1
+2|2
+3|3
+4|4
+(4 rows)
+
+count
+-----
+    4
+(1 row)
+
+injection_points_detach
+-----------------------
+                       
+(1 row)
+
diff --git a/src/test/modules/injection_points/meson.build b/src/test/modules/injection_points/meson.build
index a414abb924b..1cd88d6db65 100644
--- a/src/test/modules/injection_points/meson.build
+++ b/src/test/modules/injection_points/meson.build
@@ -46,6 +46,7 @@ tests += {
       'basic',
       'inplace',
       'repack',
+      'repack_deadlock',
       'repack_toast',
       'syscache-update-pruned',
       'heap_lock_update',
diff --git a/src/test/modules/injection_points/specs/repack_deadlock.spec b/src/test/modules/injection_points/specs/repack_deadlock.spec
new file mode 100644
index 00000000000..9d23a6588c2
--- /dev/null
+++ b/src/test/modules/injection_points/specs/repack_deadlock.spec
@@ -0,0 +1,83 @@
+# Test REPACK with a concurrent transaction that would cause a deadlock
+setup
+{
+	CREATE EXTENSION injection_points;
+
+	CREATE TABLE repack_deadlock(i int PRIMARY KEY, j int);
+	INSERT INTO repack_deadlock(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4);
+
+	CREATE TABLE relfilenodes(node oid);
+
+	CREATE TABLE data_s1(i int, j int);
+	CREATE TABLE data_s2(i int, j int);
+}
+
+teardown
+{
+	DROP TABLE repack_deadlock;
+	DROP EXTENSION injection_points;
+
+	DROP TABLE relfilenodes;
+	DROP TABLE data_s1;
+	DROP TABLE data_s2;
+}
+
+session s1
+setup
+{
+	SELECT injection_points_set_local();
+	SELECT injection_points_attach('repack-concurrently-before-lock', 'wait');
+}
+# Perform the initial load and wait for s2 to do some data changes.
+step wait_before_lock
+{
+	REPACK (CONCURRENTLY) repack_deadlock USING INDEX repack_deadlock_pkey;
+}
+# Check the table from the perspective of s1.
+#
+# Besides the contents, we also check that relfilenode has changed.
+
+# Have each session write the contents into a table and use FULL JOIN to check
+# if the outputs are identical.
+step check1
+{
+	INSERT INTO relfilenodes(node)
+	SELECT relfilenode FROM pg_class WHERE relname='repack_deadlock';
+
+	SELECT count(DISTINCT node) FROM relfilenodes;
+
+	SELECT i, j FROM repack_deadlock ORDER BY i, j;
+
+	INSERT INTO data_s1(i, j)
+	SELECT i, j FROM repack_deadlock;
+
+	SELECT count(*)
+	FROM data_s1 d1 FULL JOIN data_s2 d2 USING (i, j)
+	WHERE d1.i ISNULL OR d2.i ISNULL;
+}
+teardown
+{
+	SELECT injection_points_detach('repack-concurrently-before-lock');
+}
+
+session s2
+# Change the existing data. UPDATE changes both key and non-key columns. Also
+# update one row twice to test whether tuple version generated by this session
+# can be found.
+step add_column
+{
+	alter table repack_deadlock add column noise text;
+}
+
+step wakeup_before_lock
+{
+	SELECT injection_points_wakeup('repack-concurrently-before-lock');
+}
+
+# Test if data changes introduced while one session is performing REPACK
+# CONCURRENTLY find their way into the table.
+permutation
+	wait_before_lock
+	add_column
+	wakeup_before_lock
+	check1
-- 
2.47.3


--fot5plqmb33ey33c--





^ permalink  raw  reply  [nested|flat] 89+ messages in thread

* [PATCH v44b 4/4] hack deadlock detector
@ 2026-03-25 19:35  Álvaro Herrera <[email protected]>
  0 siblings, 0 replies; 89+ messages in thread

From: Álvaro Herrera @ 2026-03-25 19:35 UTC (permalink / raw)

---
 src/backend/commands/cluster.c                | 10 +++
 src/backend/storage/lmgr/deadlock.c           | 12 +++
 src/include/storage/proc.h                    |  1 +
 src/test/modules/injection_points/Makefile    |  1 +
 .../expected/repack_deadlock.out              | 63 ++++++++++++++
 src/test/modules/injection_points/meson.build |  1 +
 .../specs/repack_deadlock.spec                | 83 +++++++++++++++++++
 7 files changed, 171 insertions(+)
 create mode 100644 src/test/modules/injection_points/expected/repack_deadlock.out
 create mode 100644 src/test/modules/injection_points/specs/repack_deadlock.spec

diff --git a/src/backend/commands/cluster.c b/src/backend/commands/cluster.c
index 2c3058ba10d..d5b1dfbff69 100644
--- a/src/backend/commands/cluster.c
+++ b/src/backend/commands/cluster.c
@@ -372,6 +372,16 @@ ExecRepack(ParseState *pstate, RepackStmt *stmt, bool isTopLevel)
 	/* Determine the lock mode to use. */
 	lockmode = RepackLockLevel((params.options & CLUOPT_CONCURRENT) != 0);
 
+	/*
+	 * If in concurrent mode, set the PROC_IN_CONCURRENT_REPACK flag.  This
+	 * makes the deadlock checker cause anyone that would conflict with us
+	 * to error out.  It's important to set this flag ahead of actually locking
+	 * the relation; it won't of course affect anyone until we do have a lock
+	 * that others can conflict with.
+	 */
+	if ((params.options & CLUOPT_CONCURRENT) != 0)
+		MyProc->statusFlags |= PROC_IN_CONCURRENT_REPACK;
+
 	/*
 	 * If a single relation is specified, process it and we're done ... unless
 	 * the relation is a partitioned table, in which case we fall through.
diff --git a/src/backend/storage/lmgr/deadlock.c b/src/backend/storage/lmgr/deadlock.c
index b8962d875b6..10266e384aa 100644
--- a/src/backend/storage/lmgr/deadlock.c
+++ b/src/backend/storage/lmgr/deadlock.c
@@ -620,6 +620,18 @@ FindLockCycleRecurseMember(PGPROC *checkProc,
 						proc->statusFlags & PROC_IS_AUTOVACUUM)
 						blocking_autovacuum_proc = proc;
 
+					/*
+					 * If we note that we're blocked by some process running
+					 * REPACK (CONCURRENTLY), just fail.  That process is
+					 * going to upgrade its lock at some point, and it would
+					 * be inappropriate for any other process to cause that
+					 * to fail.
+					 */
+					if (checkProc == MyProc &&
+						proc->statusFlags & PROC_IN_CONCURRENT_REPACK)
+						/* not a serious error message proposal */
+						elog(ERROR, "oops, we're blocked by REPACK, time to die");
+
 					/* We're done looking at this proclock */
 					break;
 				}
diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h
index 1dad125706e..4e340ead455 100644
--- a/src/include/storage/proc.h
+++ b/src/include/storage/proc.h
@@ -69,6 +69,7 @@ struct XidCache
 #define		PROC_AFFECTS_ALL_HORIZONS	0x20	/* this proc's xmin must be
 												 * included in vacuum horizons
 												 * in all databases */
+#define		PROC_IN_CONCURRENT_REPACK	0x40	/* REPACK (CONCURRENTLY) */
 
 /* flags reset at EOXact */
 #define		PROC_VACUUM_STATE_MASK \
diff --git a/src/test/modules/injection_points/Makefile b/src/test/modules/injection_points/Makefile
index 2cd7d87c533..f7663859fe2 100644
--- a/src/test/modules/injection_points/Makefile
+++ b/src/test/modules/injection_points/Makefile
@@ -15,6 +15,7 @@ REGRESS_OPTS = --dlpath=$(top_builddir)/src/test/regress
 ISOLATION = basic \
 	    inplace \
 	    repack \
+	    repack_deadlock \
 	    repack_toast \
 	    syscache-update-pruned \
 	    heap_lock_update
diff --git a/src/test/modules/injection_points/expected/repack_deadlock.out b/src/test/modules/injection_points/expected/repack_deadlock.out
new file mode 100644
index 00000000000..42505d10933
--- /dev/null
+++ b/src/test/modules/injection_points/expected/repack_deadlock.out
@@ -0,0 +1,63 @@
+Parsed test spec with 2 sessions
+
+starting permutation: wait_before_lock add_column wakeup_before_lock check1
+injection_points_attach
+-----------------------
+                       
+(1 row)
+
+step wait_before_lock: 
+	REPACK (CONCURRENTLY) repack_deadlock USING INDEX repack_deadlock_pkey;
+ <waiting ...>
+step add_column: 
+	alter table repack_deadlock add column noise text;
+ <waiting ...>
+step add_column: <... completed>
+ERROR:  oops, we're blocked by REPACK, time to die
+step wakeup_before_lock: 
+	SELECT injection_points_wakeup('repack-concurrently-before-lock');
+
+injection_points_wakeup
+-----------------------
+                       
+(1 row)
+
+step wait_before_lock: <... completed>
+step check1: 
+	INSERT INTO relfilenodes(node)
+	SELECT relfilenode FROM pg_class WHERE relname='repack_deadlock';
+
+	SELECT count(DISTINCT node) FROM relfilenodes;
+
+	SELECT i, j FROM repack_deadlock ORDER BY i, j;
+
+	INSERT INTO data_s1(i, j)
+	SELECT i, j FROM repack_deadlock;
+
+	SELECT count(*)
+	FROM data_s1 d1 FULL JOIN data_s2 d2 USING (i, j)
+	WHERE d1.i ISNULL OR d2.i ISNULL;
+
+count
+-----
+    1
+(1 row)
+
+i|j
+-+-
+1|1
+2|2
+3|3
+4|4
+(4 rows)
+
+count
+-----
+    4
+(1 row)
+
+injection_points_detach
+-----------------------
+                       
+(1 row)
+
diff --git a/src/test/modules/injection_points/meson.build b/src/test/modules/injection_points/meson.build
index a414abb924b..1cd88d6db65 100644
--- a/src/test/modules/injection_points/meson.build
+++ b/src/test/modules/injection_points/meson.build
@@ -46,6 +46,7 @@ tests += {
       'basic',
       'inplace',
       'repack',
+      'repack_deadlock',
       'repack_toast',
       'syscache-update-pruned',
       'heap_lock_update',
diff --git a/src/test/modules/injection_points/specs/repack_deadlock.spec b/src/test/modules/injection_points/specs/repack_deadlock.spec
new file mode 100644
index 00000000000..9d23a6588c2
--- /dev/null
+++ b/src/test/modules/injection_points/specs/repack_deadlock.spec
@@ -0,0 +1,83 @@
+# Test REPACK with a concurrent transaction that would cause a deadlock
+setup
+{
+	CREATE EXTENSION injection_points;
+
+	CREATE TABLE repack_deadlock(i int PRIMARY KEY, j int);
+	INSERT INTO repack_deadlock(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4);
+
+	CREATE TABLE relfilenodes(node oid);
+
+	CREATE TABLE data_s1(i int, j int);
+	CREATE TABLE data_s2(i int, j int);
+}
+
+teardown
+{
+	DROP TABLE repack_deadlock;
+	DROP EXTENSION injection_points;
+
+	DROP TABLE relfilenodes;
+	DROP TABLE data_s1;
+	DROP TABLE data_s2;
+}
+
+session s1
+setup
+{
+	SELECT injection_points_set_local();
+	SELECT injection_points_attach('repack-concurrently-before-lock', 'wait');
+}
+# Perform the initial load and wait for s2 to do some data changes.
+step wait_before_lock
+{
+	REPACK (CONCURRENTLY) repack_deadlock USING INDEX repack_deadlock_pkey;
+}
+# Check the table from the perspective of s1.
+#
+# Besides the contents, we also check that relfilenode has changed.
+
+# Have each session write the contents into a table and use FULL JOIN to check
+# if the outputs are identical.
+step check1
+{
+	INSERT INTO relfilenodes(node)
+	SELECT relfilenode FROM pg_class WHERE relname='repack_deadlock';
+
+	SELECT count(DISTINCT node) FROM relfilenodes;
+
+	SELECT i, j FROM repack_deadlock ORDER BY i, j;
+
+	INSERT INTO data_s1(i, j)
+	SELECT i, j FROM repack_deadlock;
+
+	SELECT count(*)
+	FROM data_s1 d1 FULL JOIN data_s2 d2 USING (i, j)
+	WHERE d1.i ISNULL OR d2.i ISNULL;
+}
+teardown
+{
+	SELECT injection_points_detach('repack-concurrently-before-lock');
+}
+
+session s2
+# Change the existing data. UPDATE changes both key and non-key columns. Also
+# update one row twice to test whether tuple version generated by this session
+# can be found.
+step add_column
+{
+	alter table repack_deadlock add column noise text;
+}
+
+step wakeup_before_lock
+{
+	SELECT injection_points_wakeup('repack-concurrently-before-lock');
+}
+
+# Test if data changes introduced while one session is performing REPACK
+# CONCURRENTLY find their way into the table.
+permutation
+	wait_before_lock
+	add_column
+	wakeup_before_lock
+	check1
-- 
2.47.3


--fot5plqmb33ey33c--





^ permalink  raw  reply  [nested|flat] 89+ messages in thread

* [PATCH v44b 4/4] hack deadlock detector
@ 2026-03-25 19:35  Álvaro Herrera <[email protected]>
  0 siblings, 0 replies; 89+ messages in thread

From: Álvaro Herrera @ 2026-03-25 19:35 UTC (permalink / raw)

---
 src/backend/commands/cluster.c                | 10 +++
 src/backend/storage/lmgr/deadlock.c           | 12 +++
 src/include/storage/proc.h                    |  1 +
 src/test/modules/injection_points/Makefile    |  1 +
 .../expected/repack_deadlock.out              | 63 ++++++++++++++
 src/test/modules/injection_points/meson.build |  1 +
 .../specs/repack_deadlock.spec                | 83 +++++++++++++++++++
 7 files changed, 171 insertions(+)
 create mode 100644 src/test/modules/injection_points/expected/repack_deadlock.out
 create mode 100644 src/test/modules/injection_points/specs/repack_deadlock.spec

diff --git a/src/backend/commands/cluster.c b/src/backend/commands/cluster.c
index 2c3058ba10d..d5b1dfbff69 100644
--- a/src/backend/commands/cluster.c
+++ b/src/backend/commands/cluster.c
@@ -372,6 +372,16 @@ ExecRepack(ParseState *pstate, RepackStmt *stmt, bool isTopLevel)
 	/* Determine the lock mode to use. */
 	lockmode = RepackLockLevel((params.options & CLUOPT_CONCURRENT) != 0);
 
+	/*
+	 * If in concurrent mode, set the PROC_IN_CONCURRENT_REPACK flag.  This
+	 * makes the deadlock checker cause anyone that would conflict with us
+	 * to error out.  It's important to set this flag ahead of actually locking
+	 * the relation; it won't of course affect anyone until we do have a lock
+	 * that others can conflict with.
+	 */
+	if ((params.options & CLUOPT_CONCURRENT) != 0)
+		MyProc->statusFlags |= PROC_IN_CONCURRENT_REPACK;
+
 	/*
 	 * If a single relation is specified, process it and we're done ... unless
 	 * the relation is a partitioned table, in which case we fall through.
diff --git a/src/backend/storage/lmgr/deadlock.c b/src/backend/storage/lmgr/deadlock.c
index b8962d875b6..10266e384aa 100644
--- a/src/backend/storage/lmgr/deadlock.c
+++ b/src/backend/storage/lmgr/deadlock.c
@@ -620,6 +620,18 @@ FindLockCycleRecurseMember(PGPROC *checkProc,
 						proc->statusFlags & PROC_IS_AUTOVACUUM)
 						blocking_autovacuum_proc = proc;
 
+					/*
+					 * If we note that we're blocked by some process running
+					 * REPACK (CONCURRENTLY), just fail.  That process is
+					 * going to upgrade its lock at some point, and it would
+					 * be inappropriate for any other process to cause that
+					 * to fail.
+					 */
+					if (checkProc == MyProc &&
+						proc->statusFlags & PROC_IN_CONCURRENT_REPACK)
+						/* not a serious error message proposal */
+						elog(ERROR, "oops, we're blocked by REPACK, time to die");
+
 					/* We're done looking at this proclock */
 					break;
 				}
diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h
index 1dad125706e..4e340ead455 100644
--- a/src/include/storage/proc.h
+++ b/src/include/storage/proc.h
@@ -69,6 +69,7 @@ struct XidCache
 #define		PROC_AFFECTS_ALL_HORIZONS	0x20	/* this proc's xmin must be
 												 * included in vacuum horizons
 												 * in all databases */
+#define		PROC_IN_CONCURRENT_REPACK	0x40	/* REPACK (CONCURRENTLY) */
 
 /* flags reset at EOXact */
 #define		PROC_VACUUM_STATE_MASK \
diff --git a/src/test/modules/injection_points/Makefile b/src/test/modules/injection_points/Makefile
index 2cd7d87c533..f7663859fe2 100644
--- a/src/test/modules/injection_points/Makefile
+++ b/src/test/modules/injection_points/Makefile
@@ -15,6 +15,7 @@ REGRESS_OPTS = --dlpath=$(top_builddir)/src/test/regress
 ISOLATION = basic \
 	    inplace \
 	    repack \
+	    repack_deadlock \
 	    repack_toast \
 	    syscache-update-pruned \
 	    heap_lock_update
diff --git a/src/test/modules/injection_points/expected/repack_deadlock.out b/src/test/modules/injection_points/expected/repack_deadlock.out
new file mode 100644
index 00000000000..42505d10933
--- /dev/null
+++ b/src/test/modules/injection_points/expected/repack_deadlock.out
@@ -0,0 +1,63 @@
+Parsed test spec with 2 sessions
+
+starting permutation: wait_before_lock add_column wakeup_before_lock check1
+injection_points_attach
+-----------------------
+                       
+(1 row)
+
+step wait_before_lock: 
+	REPACK (CONCURRENTLY) repack_deadlock USING INDEX repack_deadlock_pkey;
+ <waiting ...>
+step add_column: 
+	alter table repack_deadlock add column noise text;
+ <waiting ...>
+step add_column: <... completed>
+ERROR:  oops, we're blocked by REPACK, time to die
+step wakeup_before_lock: 
+	SELECT injection_points_wakeup('repack-concurrently-before-lock');
+
+injection_points_wakeup
+-----------------------
+                       
+(1 row)
+
+step wait_before_lock: <... completed>
+step check1: 
+	INSERT INTO relfilenodes(node)
+	SELECT relfilenode FROM pg_class WHERE relname='repack_deadlock';
+
+	SELECT count(DISTINCT node) FROM relfilenodes;
+
+	SELECT i, j FROM repack_deadlock ORDER BY i, j;
+
+	INSERT INTO data_s1(i, j)
+	SELECT i, j FROM repack_deadlock;
+
+	SELECT count(*)
+	FROM data_s1 d1 FULL JOIN data_s2 d2 USING (i, j)
+	WHERE d1.i ISNULL OR d2.i ISNULL;
+
+count
+-----
+    1
+(1 row)
+
+i|j
+-+-
+1|1
+2|2
+3|3
+4|4
+(4 rows)
+
+count
+-----
+    4
+(1 row)
+
+injection_points_detach
+-----------------------
+                       
+(1 row)
+
diff --git a/src/test/modules/injection_points/meson.build b/src/test/modules/injection_points/meson.build
index a414abb924b..1cd88d6db65 100644
--- a/src/test/modules/injection_points/meson.build
+++ b/src/test/modules/injection_points/meson.build
@@ -46,6 +46,7 @@ tests += {
       'basic',
       'inplace',
       'repack',
+      'repack_deadlock',
       'repack_toast',
       'syscache-update-pruned',
       'heap_lock_update',
diff --git a/src/test/modules/injection_points/specs/repack_deadlock.spec b/src/test/modules/injection_points/specs/repack_deadlock.spec
new file mode 100644
index 00000000000..9d23a6588c2
--- /dev/null
+++ b/src/test/modules/injection_points/specs/repack_deadlock.spec
@@ -0,0 +1,83 @@
+# Test REPACK with a concurrent transaction that would cause a deadlock
+setup
+{
+	CREATE EXTENSION injection_points;
+
+	CREATE TABLE repack_deadlock(i int PRIMARY KEY, j int);
+	INSERT INTO repack_deadlock(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4);
+
+	CREATE TABLE relfilenodes(node oid);
+
+	CREATE TABLE data_s1(i int, j int);
+	CREATE TABLE data_s2(i int, j int);
+}
+
+teardown
+{
+	DROP TABLE repack_deadlock;
+	DROP EXTENSION injection_points;
+
+	DROP TABLE relfilenodes;
+	DROP TABLE data_s1;
+	DROP TABLE data_s2;
+}
+
+session s1
+setup
+{
+	SELECT injection_points_set_local();
+	SELECT injection_points_attach('repack-concurrently-before-lock', 'wait');
+}
+# Perform the initial load and wait for s2 to do some data changes.
+step wait_before_lock
+{
+	REPACK (CONCURRENTLY) repack_deadlock USING INDEX repack_deadlock_pkey;
+}
+# Check the table from the perspective of s1.
+#
+# Besides the contents, we also check that relfilenode has changed.
+
+# Have each session write the contents into a table and use FULL JOIN to check
+# if the outputs are identical.
+step check1
+{
+	INSERT INTO relfilenodes(node)
+	SELECT relfilenode FROM pg_class WHERE relname='repack_deadlock';
+
+	SELECT count(DISTINCT node) FROM relfilenodes;
+
+	SELECT i, j FROM repack_deadlock ORDER BY i, j;
+
+	INSERT INTO data_s1(i, j)
+	SELECT i, j FROM repack_deadlock;
+
+	SELECT count(*)
+	FROM data_s1 d1 FULL JOIN data_s2 d2 USING (i, j)
+	WHERE d1.i ISNULL OR d2.i ISNULL;
+}
+teardown
+{
+	SELECT injection_points_detach('repack-concurrently-before-lock');
+}
+
+session s2
+# Change the existing data. UPDATE changes both key and non-key columns. Also
+# update one row twice to test whether tuple version generated by this session
+# can be found.
+step add_column
+{
+	alter table repack_deadlock add column noise text;
+}
+
+step wakeup_before_lock
+{
+	SELECT injection_points_wakeup('repack-concurrently-before-lock');
+}
+
+# Test if data changes introduced while one session is performing REPACK
+# CONCURRENTLY find their way into the table.
+permutation
+	wait_before_lock
+	add_column
+	wakeup_before_lock
+	check1
-- 
2.47.3


--fot5plqmb33ey33c--





^ permalink  raw  reply  [nested|flat] 89+ messages in thread

* [PATCH v44b 4/4] hack deadlock detector
@ 2026-03-25 19:35  Álvaro Herrera <[email protected]>
  0 siblings, 0 replies; 89+ messages in thread

From: Álvaro Herrera @ 2026-03-25 19:35 UTC (permalink / raw)

---
 src/backend/commands/cluster.c                | 10 +++
 src/backend/storage/lmgr/deadlock.c           | 12 +++
 src/include/storage/proc.h                    |  1 +
 src/test/modules/injection_points/Makefile    |  1 +
 .../expected/repack_deadlock.out              | 63 ++++++++++++++
 src/test/modules/injection_points/meson.build |  1 +
 .../specs/repack_deadlock.spec                | 83 +++++++++++++++++++
 7 files changed, 171 insertions(+)
 create mode 100644 src/test/modules/injection_points/expected/repack_deadlock.out
 create mode 100644 src/test/modules/injection_points/specs/repack_deadlock.spec

diff --git a/src/backend/commands/cluster.c b/src/backend/commands/cluster.c
index 2c3058ba10d..d5b1dfbff69 100644
--- a/src/backend/commands/cluster.c
+++ b/src/backend/commands/cluster.c
@@ -372,6 +372,16 @@ ExecRepack(ParseState *pstate, RepackStmt *stmt, bool isTopLevel)
 	/* Determine the lock mode to use. */
 	lockmode = RepackLockLevel((params.options & CLUOPT_CONCURRENT) != 0);
 
+	/*
+	 * If in concurrent mode, set the PROC_IN_CONCURRENT_REPACK flag.  This
+	 * makes the deadlock checker cause anyone that would conflict with us
+	 * to error out.  It's important to set this flag ahead of actually locking
+	 * the relation; it won't of course affect anyone until we do have a lock
+	 * that others can conflict with.
+	 */
+	if ((params.options & CLUOPT_CONCURRENT) != 0)
+		MyProc->statusFlags |= PROC_IN_CONCURRENT_REPACK;
+
 	/*
 	 * If a single relation is specified, process it and we're done ... unless
 	 * the relation is a partitioned table, in which case we fall through.
diff --git a/src/backend/storage/lmgr/deadlock.c b/src/backend/storage/lmgr/deadlock.c
index b8962d875b6..10266e384aa 100644
--- a/src/backend/storage/lmgr/deadlock.c
+++ b/src/backend/storage/lmgr/deadlock.c
@@ -620,6 +620,18 @@ FindLockCycleRecurseMember(PGPROC *checkProc,
 						proc->statusFlags & PROC_IS_AUTOVACUUM)
 						blocking_autovacuum_proc = proc;
 
+					/*
+					 * If we note that we're blocked by some process running
+					 * REPACK (CONCURRENTLY), just fail.  That process is
+					 * going to upgrade its lock at some point, and it would
+					 * be inappropriate for any other process to cause that
+					 * to fail.
+					 */
+					if (checkProc == MyProc &&
+						proc->statusFlags & PROC_IN_CONCURRENT_REPACK)
+						/* not a serious error message proposal */
+						elog(ERROR, "oops, we're blocked by REPACK, time to die");
+
 					/* We're done looking at this proclock */
 					break;
 				}
diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h
index 1dad125706e..4e340ead455 100644
--- a/src/include/storage/proc.h
+++ b/src/include/storage/proc.h
@@ -69,6 +69,7 @@ struct XidCache
 #define		PROC_AFFECTS_ALL_HORIZONS	0x20	/* this proc's xmin must be
 												 * included in vacuum horizons
 												 * in all databases */
+#define		PROC_IN_CONCURRENT_REPACK	0x40	/* REPACK (CONCURRENTLY) */
 
 /* flags reset at EOXact */
 #define		PROC_VACUUM_STATE_MASK \
diff --git a/src/test/modules/injection_points/Makefile b/src/test/modules/injection_points/Makefile
index 2cd7d87c533..f7663859fe2 100644
--- a/src/test/modules/injection_points/Makefile
+++ b/src/test/modules/injection_points/Makefile
@@ -15,6 +15,7 @@ REGRESS_OPTS = --dlpath=$(top_builddir)/src/test/regress
 ISOLATION = basic \
 	    inplace \
 	    repack \
+	    repack_deadlock \
 	    repack_toast \
 	    syscache-update-pruned \
 	    heap_lock_update
diff --git a/src/test/modules/injection_points/expected/repack_deadlock.out b/src/test/modules/injection_points/expected/repack_deadlock.out
new file mode 100644
index 00000000000..42505d10933
--- /dev/null
+++ b/src/test/modules/injection_points/expected/repack_deadlock.out
@@ -0,0 +1,63 @@
+Parsed test spec with 2 sessions
+
+starting permutation: wait_before_lock add_column wakeup_before_lock check1
+injection_points_attach
+-----------------------
+                       
+(1 row)
+
+step wait_before_lock: 
+	REPACK (CONCURRENTLY) repack_deadlock USING INDEX repack_deadlock_pkey;
+ <waiting ...>
+step add_column: 
+	alter table repack_deadlock add column noise text;
+ <waiting ...>
+step add_column: <... completed>
+ERROR:  oops, we're blocked by REPACK, time to die
+step wakeup_before_lock: 
+	SELECT injection_points_wakeup('repack-concurrently-before-lock');
+
+injection_points_wakeup
+-----------------------
+                       
+(1 row)
+
+step wait_before_lock: <... completed>
+step check1: 
+	INSERT INTO relfilenodes(node)
+	SELECT relfilenode FROM pg_class WHERE relname='repack_deadlock';
+
+	SELECT count(DISTINCT node) FROM relfilenodes;
+
+	SELECT i, j FROM repack_deadlock ORDER BY i, j;
+
+	INSERT INTO data_s1(i, j)
+	SELECT i, j FROM repack_deadlock;
+
+	SELECT count(*)
+	FROM data_s1 d1 FULL JOIN data_s2 d2 USING (i, j)
+	WHERE d1.i ISNULL OR d2.i ISNULL;
+
+count
+-----
+    1
+(1 row)
+
+i|j
+-+-
+1|1
+2|2
+3|3
+4|4
+(4 rows)
+
+count
+-----
+    4
+(1 row)
+
+injection_points_detach
+-----------------------
+                       
+(1 row)
+
diff --git a/src/test/modules/injection_points/meson.build b/src/test/modules/injection_points/meson.build
index a414abb924b..1cd88d6db65 100644
--- a/src/test/modules/injection_points/meson.build
+++ b/src/test/modules/injection_points/meson.build
@@ -46,6 +46,7 @@ tests += {
       'basic',
       'inplace',
       'repack',
+      'repack_deadlock',
       'repack_toast',
       'syscache-update-pruned',
       'heap_lock_update',
diff --git a/src/test/modules/injection_points/specs/repack_deadlock.spec b/src/test/modules/injection_points/specs/repack_deadlock.spec
new file mode 100644
index 00000000000..9d23a6588c2
--- /dev/null
+++ b/src/test/modules/injection_points/specs/repack_deadlock.spec
@@ -0,0 +1,83 @@
+# Test REPACK with a concurrent transaction that would cause a deadlock
+setup
+{
+	CREATE EXTENSION injection_points;
+
+	CREATE TABLE repack_deadlock(i int PRIMARY KEY, j int);
+	INSERT INTO repack_deadlock(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4);
+
+	CREATE TABLE relfilenodes(node oid);
+
+	CREATE TABLE data_s1(i int, j int);
+	CREATE TABLE data_s2(i int, j int);
+}
+
+teardown
+{
+	DROP TABLE repack_deadlock;
+	DROP EXTENSION injection_points;
+
+	DROP TABLE relfilenodes;
+	DROP TABLE data_s1;
+	DROP TABLE data_s2;
+}
+
+session s1
+setup
+{
+	SELECT injection_points_set_local();
+	SELECT injection_points_attach('repack-concurrently-before-lock', 'wait');
+}
+# Perform the initial load and wait for s2 to do some data changes.
+step wait_before_lock
+{
+	REPACK (CONCURRENTLY) repack_deadlock USING INDEX repack_deadlock_pkey;
+}
+# Check the table from the perspective of s1.
+#
+# Besides the contents, we also check that relfilenode has changed.
+
+# Have each session write the contents into a table and use FULL JOIN to check
+# if the outputs are identical.
+step check1
+{
+	INSERT INTO relfilenodes(node)
+	SELECT relfilenode FROM pg_class WHERE relname='repack_deadlock';
+
+	SELECT count(DISTINCT node) FROM relfilenodes;
+
+	SELECT i, j FROM repack_deadlock ORDER BY i, j;
+
+	INSERT INTO data_s1(i, j)
+	SELECT i, j FROM repack_deadlock;
+
+	SELECT count(*)
+	FROM data_s1 d1 FULL JOIN data_s2 d2 USING (i, j)
+	WHERE d1.i ISNULL OR d2.i ISNULL;
+}
+teardown
+{
+	SELECT injection_points_detach('repack-concurrently-before-lock');
+}
+
+session s2
+# Change the existing data. UPDATE changes both key and non-key columns. Also
+# update one row twice to test whether tuple version generated by this session
+# can be found.
+step add_column
+{
+	alter table repack_deadlock add column noise text;
+}
+
+step wakeup_before_lock
+{
+	SELECT injection_points_wakeup('repack-concurrently-before-lock');
+}
+
+# Test if data changes introduced while one session is performing REPACK
+# CONCURRENTLY find their way into the table.
+permutation
+	wait_before_lock
+	add_column
+	wakeup_before_lock
+	check1
-- 
2.47.3


--fot5plqmb33ey33c--





^ permalink  raw  reply  [nested|flat] 89+ messages in thread

* [PATCH v44b 4/4] hack deadlock detector
@ 2026-03-25 19:35  Álvaro Herrera <[email protected]>
  0 siblings, 0 replies; 89+ messages in thread

From: Álvaro Herrera @ 2026-03-25 19:35 UTC (permalink / raw)

---
 src/backend/commands/cluster.c                | 10 +++
 src/backend/storage/lmgr/deadlock.c           | 12 +++
 src/include/storage/proc.h                    |  1 +
 src/test/modules/injection_points/Makefile    |  1 +
 .../expected/repack_deadlock.out              | 63 ++++++++++++++
 src/test/modules/injection_points/meson.build |  1 +
 .../specs/repack_deadlock.spec                | 83 +++++++++++++++++++
 7 files changed, 171 insertions(+)
 create mode 100644 src/test/modules/injection_points/expected/repack_deadlock.out
 create mode 100644 src/test/modules/injection_points/specs/repack_deadlock.spec

diff --git a/src/backend/commands/cluster.c b/src/backend/commands/cluster.c
index 2c3058ba10d..d5b1dfbff69 100644
--- a/src/backend/commands/cluster.c
+++ b/src/backend/commands/cluster.c
@@ -372,6 +372,16 @@ ExecRepack(ParseState *pstate, RepackStmt *stmt, bool isTopLevel)
 	/* Determine the lock mode to use. */
 	lockmode = RepackLockLevel((params.options & CLUOPT_CONCURRENT) != 0);
 
+	/*
+	 * If in concurrent mode, set the PROC_IN_CONCURRENT_REPACK flag.  This
+	 * makes the deadlock checker cause anyone that would conflict with us
+	 * to error out.  It's important to set this flag ahead of actually locking
+	 * the relation; it won't of course affect anyone until we do have a lock
+	 * that others can conflict with.
+	 */
+	if ((params.options & CLUOPT_CONCURRENT) != 0)
+		MyProc->statusFlags |= PROC_IN_CONCURRENT_REPACK;
+
 	/*
 	 * If a single relation is specified, process it and we're done ... unless
 	 * the relation is a partitioned table, in which case we fall through.
diff --git a/src/backend/storage/lmgr/deadlock.c b/src/backend/storage/lmgr/deadlock.c
index b8962d875b6..10266e384aa 100644
--- a/src/backend/storage/lmgr/deadlock.c
+++ b/src/backend/storage/lmgr/deadlock.c
@@ -620,6 +620,18 @@ FindLockCycleRecurseMember(PGPROC *checkProc,
 						proc->statusFlags & PROC_IS_AUTOVACUUM)
 						blocking_autovacuum_proc = proc;
 
+					/*
+					 * If we note that we're blocked by some process running
+					 * REPACK (CONCURRENTLY), just fail.  That process is
+					 * going to upgrade its lock at some point, and it would
+					 * be inappropriate for any other process to cause that
+					 * to fail.
+					 */
+					if (checkProc == MyProc &&
+						proc->statusFlags & PROC_IN_CONCURRENT_REPACK)
+						/* not a serious error message proposal */
+						elog(ERROR, "oops, we're blocked by REPACK, time to die");
+
 					/* We're done looking at this proclock */
 					break;
 				}
diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h
index 1dad125706e..4e340ead455 100644
--- a/src/include/storage/proc.h
+++ b/src/include/storage/proc.h
@@ -69,6 +69,7 @@ struct XidCache
 #define		PROC_AFFECTS_ALL_HORIZONS	0x20	/* this proc's xmin must be
 												 * included in vacuum horizons
 												 * in all databases */
+#define		PROC_IN_CONCURRENT_REPACK	0x40	/* REPACK (CONCURRENTLY) */
 
 /* flags reset at EOXact */
 #define		PROC_VACUUM_STATE_MASK \
diff --git a/src/test/modules/injection_points/Makefile b/src/test/modules/injection_points/Makefile
index 2cd7d87c533..f7663859fe2 100644
--- a/src/test/modules/injection_points/Makefile
+++ b/src/test/modules/injection_points/Makefile
@@ -15,6 +15,7 @@ REGRESS_OPTS = --dlpath=$(top_builddir)/src/test/regress
 ISOLATION = basic \
 	    inplace \
 	    repack \
+	    repack_deadlock \
 	    repack_toast \
 	    syscache-update-pruned \
 	    heap_lock_update
diff --git a/src/test/modules/injection_points/expected/repack_deadlock.out b/src/test/modules/injection_points/expected/repack_deadlock.out
new file mode 100644
index 00000000000..42505d10933
--- /dev/null
+++ b/src/test/modules/injection_points/expected/repack_deadlock.out
@@ -0,0 +1,63 @@
+Parsed test spec with 2 sessions
+
+starting permutation: wait_before_lock add_column wakeup_before_lock check1
+injection_points_attach
+-----------------------
+                       
+(1 row)
+
+step wait_before_lock: 
+	REPACK (CONCURRENTLY) repack_deadlock USING INDEX repack_deadlock_pkey;
+ <waiting ...>
+step add_column: 
+	alter table repack_deadlock add column noise text;
+ <waiting ...>
+step add_column: <... completed>
+ERROR:  oops, we're blocked by REPACK, time to die
+step wakeup_before_lock: 
+	SELECT injection_points_wakeup('repack-concurrently-before-lock');
+
+injection_points_wakeup
+-----------------------
+                       
+(1 row)
+
+step wait_before_lock: <... completed>
+step check1: 
+	INSERT INTO relfilenodes(node)
+	SELECT relfilenode FROM pg_class WHERE relname='repack_deadlock';
+
+	SELECT count(DISTINCT node) FROM relfilenodes;
+
+	SELECT i, j FROM repack_deadlock ORDER BY i, j;
+
+	INSERT INTO data_s1(i, j)
+	SELECT i, j FROM repack_deadlock;
+
+	SELECT count(*)
+	FROM data_s1 d1 FULL JOIN data_s2 d2 USING (i, j)
+	WHERE d1.i ISNULL OR d2.i ISNULL;
+
+count
+-----
+    1
+(1 row)
+
+i|j
+-+-
+1|1
+2|2
+3|3
+4|4
+(4 rows)
+
+count
+-----
+    4
+(1 row)
+
+injection_points_detach
+-----------------------
+                       
+(1 row)
+
diff --git a/src/test/modules/injection_points/meson.build b/src/test/modules/injection_points/meson.build
index a414abb924b..1cd88d6db65 100644
--- a/src/test/modules/injection_points/meson.build
+++ b/src/test/modules/injection_points/meson.build
@@ -46,6 +46,7 @@ tests += {
       'basic',
       'inplace',
       'repack',
+      'repack_deadlock',
       'repack_toast',
       'syscache-update-pruned',
       'heap_lock_update',
diff --git a/src/test/modules/injection_points/specs/repack_deadlock.spec b/src/test/modules/injection_points/specs/repack_deadlock.spec
new file mode 100644
index 00000000000..9d23a6588c2
--- /dev/null
+++ b/src/test/modules/injection_points/specs/repack_deadlock.spec
@@ -0,0 +1,83 @@
+# Test REPACK with a concurrent transaction that would cause a deadlock
+setup
+{
+	CREATE EXTENSION injection_points;
+
+	CREATE TABLE repack_deadlock(i int PRIMARY KEY, j int);
+	INSERT INTO repack_deadlock(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4);
+
+	CREATE TABLE relfilenodes(node oid);
+
+	CREATE TABLE data_s1(i int, j int);
+	CREATE TABLE data_s2(i int, j int);
+}
+
+teardown
+{
+	DROP TABLE repack_deadlock;
+	DROP EXTENSION injection_points;
+
+	DROP TABLE relfilenodes;
+	DROP TABLE data_s1;
+	DROP TABLE data_s2;
+}
+
+session s1
+setup
+{
+	SELECT injection_points_set_local();
+	SELECT injection_points_attach('repack-concurrently-before-lock', 'wait');
+}
+# Perform the initial load and wait for s2 to do some data changes.
+step wait_before_lock
+{
+	REPACK (CONCURRENTLY) repack_deadlock USING INDEX repack_deadlock_pkey;
+}
+# Check the table from the perspective of s1.
+#
+# Besides the contents, we also check that relfilenode has changed.
+
+# Have each session write the contents into a table and use FULL JOIN to check
+# if the outputs are identical.
+step check1
+{
+	INSERT INTO relfilenodes(node)
+	SELECT relfilenode FROM pg_class WHERE relname='repack_deadlock';
+
+	SELECT count(DISTINCT node) FROM relfilenodes;
+
+	SELECT i, j FROM repack_deadlock ORDER BY i, j;
+
+	INSERT INTO data_s1(i, j)
+	SELECT i, j FROM repack_deadlock;
+
+	SELECT count(*)
+	FROM data_s1 d1 FULL JOIN data_s2 d2 USING (i, j)
+	WHERE d1.i ISNULL OR d2.i ISNULL;
+}
+teardown
+{
+	SELECT injection_points_detach('repack-concurrently-before-lock');
+}
+
+session s2
+# Change the existing data. UPDATE changes both key and non-key columns. Also
+# update one row twice to test whether tuple version generated by this session
+# can be found.
+step add_column
+{
+	alter table repack_deadlock add column noise text;
+}
+
+step wakeup_before_lock
+{
+	SELECT injection_points_wakeup('repack-concurrently-before-lock');
+}
+
+# Test if data changes introduced while one session is performing REPACK
+# CONCURRENTLY find their way into the table.
+permutation
+	wait_before_lock
+	add_column
+	wakeup_before_lock
+	check1
-- 
2.47.3


--fot5plqmb33ey33c--





^ permalink  raw  reply  [nested|flat] 89+ messages in thread

* [PATCH v44b 4/4] hack deadlock detector
@ 2026-03-25 19:35  Álvaro Herrera <[email protected]>
  0 siblings, 0 replies; 89+ messages in thread

From: Álvaro Herrera @ 2026-03-25 19:35 UTC (permalink / raw)

---
 src/backend/commands/cluster.c                | 10 +++
 src/backend/storage/lmgr/deadlock.c           | 12 +++
 src/include/storage/proc.h                    |  1 +
 src/test/modules/injection_points/Makefile    |  1 +
 .../expected/repack_deadlock.out              | 63 ++++++++++++++
 src/test/modules/injection_points/meson.build |  1 +
 .../specs/repack_deadlock.spec                | 83 +++++++++++++++++++
 7 files changed, 171 insertions(+)
 create mode 100644 src/test/modules/injection_points/expected/repack_deadlock.out
 create mode 100644 src/test/modules/injection_points/specs/repack_deadlock.spec

diff --git a/src/backend/commands/cluster.c b/src/backend/commands/cluster.c
index 2c3058ba10d..d5b1dfbff69 100644
--- a/src/backend/commands/cluster.c
+++ b/src/backend/commands/cluster.c
@@ -372,6 +372,16 @@ ExecRepack(ParseState *pstate, RepackStmt *stmt, bool isTopLevel)
 	/* Determine the lock mode to use. */
 	lockmode = RepackLockLevel((params.options & CLUOPT_CONCURRENT) != 0);
 
+	/*
+	 * If in concurrent mode, set the PROC_IN_CONCURRENT_REPACK flag.  This
+	 * makes the deadlock checker cause anyone that would conflict with us
+	 * to error out.  It's important to set this flag ahead of actually locking
+	 * the relation; it won't of course affect anyone until we do have a lock
+	 * that others can conflict with.
+	 */
+	if ((params.options & CLUOPT_CONCURRENT) != 0)
+		MyProc->statusFlags |= PROC_IN_CONCURRENT_REPACK;
+
 	/*
 	 * If a single relation is specified, process it and we're done ... unless
 	 * the relation is a partitioned table, in which case we fall through.
diff --git a/src/backend/storage/lmgr/deadlock.c b/src/backend/storage/lmgr/deadlock.c
index b8962d875b6..10266e384aa 100644
--- a/src/backend/storage/lmgr/deadlock.c
+++ b/src/backend/storage/lmgr/deadlock.c
@@ -620,6 +620,18 @@ FindLockCycleRecurseMember(PGPROC *checkProc,
 						proc->statusFlags & PROC_IS_AUTOVACUUM)
 						blocking_autovacuum_proc = proc;
 
+					/*
+					 * If we note that we're blocked by some process running
+					 * REPACK (CONCURRENTLY), just fail.  That process is
+					 * going to upgrade its lock at some point, and it would
+					 * be inappropriate for any other process to cause that
+					 * to fail.
+					 */
+					if (checkProc == MyProc &&
+						proc->statusFlags & PROC_IN_CONCURRENT_REPACK)
+						/* not a serious error message proposal */
+						elog(ERROR, "oops, we're blocked by REPACK, time to die");
+
 					/* We're done looking at this proclock */
 					break;
 				}
diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h
index 1dad125706e..4e340ead455 100644
--- a/src/include/storage/proc.h
+++ b/src/include/storage/proc.h
@@ -69,6 +69,7 @@ struct XidCache
 #define		PROC_AFFECTS_ALL_HORIZONS	0x20	/* this proc's xmin must be
 												 * included in vacuum horizons
 												 * in all databases */
+#define		PROC_IN_CONCURRENT_REPACK	0x40	/* REPACK (CONCURRENTLY) */
 
 /* flags reset at EOXact */
 #define		PROC_VACUUM_STATE_MASK \
diff --git a/src/test/modules/injection_points/Makefile b/src/test/modules/injection_points/Makefile
index 2cd7d87c533..f7663859fe2 100644
--- a/src/test/modules/injection_points/Makefile
+++ b/src/test/modules/injection_points/Makefile
@@ -15,6 +15,7 @@ REGRESS_OPTS = --dlpath=$(top_builddir)/src/test/regress
 ISOLATION = basic \
 	    inplace \
 	    repack \
+	    repack_deadlock \
 	    repack_toast \
 	    syscache-update-pruned \
 	    heap_lock_update
diff --git a/src/test/modules/injection_points/expected/repack_deadlock.out b/src/test/modules/injection_points/expected/repack_deadlock.out
new file mode 100644
index 00000000000..42505d10933
--- /dev/null
+++ b/src/test/modules/injection_points/expected/repack_deadlock.out
@@ -0,0 +1,63 @@
+Parsed test spec with 2 sessions
+
+starting permutation: wait_before_lock add_column wakeup_before_lock check1
+injection_points_attach
+-----------------------
+                       
+(1 row)
+
+step wait_before_lock: 
+	REPACK (CONCURRENTLY) repack_deadlock USING INDEX repack_deadlock_pkey;
+ <waiting ...>
+step add_column: 
+	alter table repack_deadlock add column noise text;
+ <waiting ...>
+step add_column: <... completed>
+ERROR:  oops, we're blocked by REPACK, time to die
+step wakeup_before_lock: 
+	SELECT injection_points_wakeup('repack-concurrently-before-lock');
+
+injection_points_wakeup
+-----------------------
+                       
+(1 row)
+
+step wait_before_lock: <... completed>
+step check1: 
+	INSERT INTO relfilenodes(node)
+	SELECT relfilenode FROM pg_class WHERE relname='repack_deadlock';
+
+	SELECT count(DISTINCT node) FROM relfilenodes;
+
+	SELECT i, j FROM repack_deadlock ORDER BY i, j;
+
+	INSERT INTO data_s1(i, j)
+	SELECT i, j FROM repack_deadlock;
+
+	SELECT count(*)
+	FROM data_s1 d1 FULL JOIN data_s2 d2 USING (i, j)
+	WHERE d1.i ISNULL OR d2.i ISNULL;
+
+count
+-----
+    1
+(1 row)
+
+i|j
+-+-
+1|1
+2|2
+3|3
+4|4
+(4 rows)
+
+count
+-----
+    4
+(1 row)
+
+injection_points_detach
+-----------------------
+                       
+(1 row)
+
diff --git a/src/test/modules/injection_points/meson.build b/src/test/modules/injection_points/meson.build
index a414abb924b..1cd88d6db65 100644
--- a/src/test/modules/injection_points/meson.build
+++ b/src/test/modules/injection_points/meson.build
@@ -46,6 +46,7 @@ tests += {
       'basic',
       'inplace',
       'repack',
+      'repack_deadlock',
       'repack_toast',
       'syscache-update-pruned',
       'heap_lock_update',
diff --git a/src/test/modules/injection_points/specs/repack_deadlock.spec b/src/test/modules/injection_points/specs/repack_deadlock.spec
new file mode 100644
index 00000000000..9d23a6588c2
--- /dev/null
+++ b/src/test/modules/injection_points/specs/repack_deadlock.spec
@@ -0,0 +1,83 @@
+# Test REPACK with a concurrent transaction that would cause a deadlock
+setup
+{
+	CREATE EXTENSION injection_points;
+
+	CREATE TABLE repack_deadlock(i int PRIMARY KEY, j int);
+	INSERT INTO repack_deadlock(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4);
+
+	CREATE TABLE relfilenodes(node oid);
+
+	CREATE TABLE data_s1(i int, j int);
+	CREATE TABLE data_s2(i int, j int);
+}
+
+teardown
+{
+	DROP TABLE repack_deadlock;
+	DROP EXTENSION injection_points;
+
+	DROP TABLE relfilenodes;
+	DROP TABLE data_s1;
+	DROP TABLE data_s2;
+}
+
+session s1
+setup
+{
+	SELECT injection_points_set_local();
+	SELECT injection_points_attach('repack-concurrently-before-lock', 'wait');
+}
+# Perform the initial load and wait for s2 to do some data changes.
+step wait_before_lock
+{
+	REPACK (CONCURRENTLY) repack_deadlock USING INDEX repack_deadlock_pkey;
+}
+# Check the table from the perspective of s1.
+#
+# Besides the contents, we also check that relfilenode has changed.
+
+# Have each session write the contents into a table and use FULL JOIN to check
+# if the outputs are identical.
+step check1
+{
+	INSERT INTO relfilenodes(node)
+	SELECT relfilenode FROM pg_class WHERE relname='repack_deadlock';
+
+	SELECT count(DISTINCT node) FROM relfilenodes;
+
+	SELECT i, j FROM repack_deadlock ORDER BY i, j;
+
+	INSERT INTO data_s1(i, j)
+	SELECT i, j FROM repack_deadlock;
+
+	SELECT count(*)
+	FROM data_s1 d1 FULL JOIN data_s2 d2 USING (i, j)
+	WHERE d1.i ISNULL OR d2.i ISNULL;
+}
+teardown
+{
+	SELECT injection_points_detach('repack-concurrently-before-lock');
+}
+
+session s2
+# Change the existing data. UPDATE changes both key and non-key columns. Also
+# update one row twice to test whether tuple version generated by this session
+# can be found.
+step add_column
+{
+	alter table repack_deadlock add column noise text;
+}
+
+step wakeup_before_lock
+{
+	SELECT injection_points_wakeup('repack-concurrently-before-lock');
+}
+
+# Test if data changes introduced while one session is performing REPACK
+# CONCURRENTLY find their way into the table.
+permutation
+	wait_before_lock
+	add_column
+	wakeup_before_lock
+	check1
-- 
2.47.3


--fot5plqmb33ey33c--





^ permalink  raw  reply  [nested|flat] 89+ messages in thread

* [PATCH v44b 4/4] hack deadlock detector
@ 2026-03-25 19:35  Álvaro Herrera <[email protected]>
  0 siblings, 0 replies; 89+ messages in thread

From: Álvaro Herrera @ 2026-03-25 19:35 UTC (permalink / raw)

---
 src/backend/commands/cluster.c                | 10 +++
 src/backend/storage/lmgr/deadlock.c           | 12 +++
 src/include/storage/proc.h                    |  1 +
 src/test/modules/injection_points/Makefile    |  1 +
 .../expected/repack_deadlock.out              | 63 ++++++++++++++
 src/test/modules/injection_points/meson.build |  1 +
 .../specs/repack_deadlock.spec                | 83 +++++++++++++++++++
 7 files changed, 171 insertions(+)
 create mode 100644 src/test/modules/injection_points/expected/repack_deadlock.out
 create mode 100644 src/test/modules/injection_points/specs/repack_deadlock.spec

diff --git a/src/backend/commands/cluster.c b/src/backend/commands/cluster.c
index 2c3058ba10d..d5b1dfbff69 100644
--- a/src/backend/commands/cluster.c
+++ b/src/backend/commands/cluster.c
@@ -372,6 +372,16 @@ ExecRepack(ParseState *pstate, RepackStmt *stmt, bool isTopLevel)
 	/* Determine the lock mode to use. */
 	lockmode = RepackLockLevel((params.options & CLUOPT_CONCURRENT) != 0);
 
+	/*
+	 * If in concurrent mode, set the PROC_IN_CONCURRENT_REPACK flag.  This
+	 * makes the deadlock checker cause anyone that would conflict with us
+	 * to error out.  It's important to set this flag ahead of actually locking
+	 * the relation; it won't of course affect anyone until we do have a lock
+	 * that others can conflict with.
+	 */
+	if ((params.options & CLUOPT_CONCURRENT) != 0)
+		MyProc->statusFlags |= PROC_IN_CONCURRENT_REPACK;
+
 	/*
 	 * If a single relation is specified, process it and we're done ... unless
 	 * the relation is a partitioned table, in which case we fall through.
diff --git a/src/backend/storage/lmgr/deadlock.c b/src/backend/storage/lmgr/deadlock.c
index b8962d875b6..10266e384aa 100644
--- a/src/backend/storage/lmgr/deadlock.c
+++ b/src/backend/storage/lmgr/deadlock.c
@@ -620,6 +620,18 @@ FindLockCycleRecurseMember(PGPROC *checkProc,
 						proc->statusFlags & PROC_IS_AUTOVACUUM)
 						blocking_autovacuum_proc = proc;
 
+					/*
+					 * If we note that we're blocked by some process running
+					 * REPACK (CONCURRENTLY), just fail.  That process is
+					 * going to upgrade its lock at some point, and it would
+					 * be inappropriate for any other process to cause that
+					 * to fail.
+					 */
+					if (checkProc == MyProc &&
+						proc->statusFlags & PROC_IN_CONCURRENT_REPACK)
+						/* not a serious error message proposal */
+						elog(ERROR, "oops, we're blocked by REPACK, time to die");
+
 					/* We're done looking at this proclock */
 					break;
 				}
diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h
index 1dad125706e..4e340ead455 100644
--- a/src/include/storage/proc.h
+++ b/src/include/storage/proc.h
@@ -69,6 +69,7 @@ struct XidCache
 #define		PROC_AFFECTS_ALL_HORIZONS	0x20	/* this proc's xmin must be
 												 * included in vacuum horizons
 												 * in all databases */
+#define		PROC_IN_CONCURRENT_REPACK	0x40	/* REPACK (CONCURRENTLY) */
 
 /* flags reset at EOXact */
 #define		PROC_VACUUM_STATE_MASK \
diff --git a/src/test/modules/injection_points/Makefile b/src/test/modules/injection_points/Makefile
index 2cd7d87c533..f7663859fe2 100644
--- a/src/test/modules/injection_points/Makefile
+++ b/src/test/modules/injection_points/Makefile
@@ -15,6 +15,7 @@ REGRESS_OPTS = --dlpath=$(top_builddir)/src/test/regress
 ISOLATION = basic \
 	    inplace \
 	    repack \
+	    repack_deadlock \
 	    repack_toast \
 	    syscache-update-pruned \
 	    heap_lock_update
diff --git a/src/test/modules/injection_points/expected/repack_deadlock.out b/src/test/modules/injection_points/expected/repack_deadlock.out
new file mode 100644
index 00000000000..42505d10933
--- /dev/null
+++ b/src/test/modules/injection_points/expected/repack_deadlock.out
@@ -0,0 +1,63 @@
+Parsed test spec with 2 sessions
+
+starting permutation: wait_before_lock add_column wakeup_before_lock check1
+injection_points_attach
+-----------------------
+                       
+(1 row)
+
+step wait_before_lock: 
+	REPACK (CONCURRENTLY) repack_deadlock USING INDEX repack_deadlock_pkey;
+ <waiting ...>
+step add_column: 
+	alter table repack_deadlock add column noise text;
+ <waiting ...>
+step add_column: <... completed>
+ERROR:  oops, we're blocked by REPACK, time to die
+step wakeup_before_lock: 
+	SELECT injection_points_wakeup('repack-concurrently-before-lock');
+
+injection_points_wakeup
+-----------------------
+                       
+(1 row)
+
+step wait_before_lock: <... completed>
+step check1: 
+	INSERT INTO relfilenodes(node)
+	SELECT relfilenode FROM pg_class WHERE relname='repack_deadlock';
+
+	SELECT count(DISTINCT node) FROM relfilenodes;
+
+	SELECT i, j FROM repack_deadlock ORDER BY i, j;
+
+	INSERT INTO data_s1(i, j)
+	SELECT i, j FROM repack_deadlock;
+
+	SELECT count(*)
+	FROM data_s1 d1 FULL JOIN data_s2 d2 USING (i, j)
+	WHERE d1.i ISNULL OR d2.i ISNULL;
+
+count
+-----
+    1
+(1 row)
+
+i|j
+-+-
+1|1
+2|2
+3|3
+4|4
+(4 rows)
+
+count
+-----
+    4
+(1 row)
+
+injection_points_detach
+-----------------------
+                       
+(1 row)
+
diff --git a/src/test/modules/injection_points/meson.build b/src/test/modules/injection_points/meson.build
index a414abb924b..1cd88d6db65 100644
--- a/src/test/modules/injection_points/meson.build
+++ b/src/test/modules/injection_points/meson.build
@@ -46,6 +46,7 @@ tests += {
       'basic',
       'inplace',
       'repack',
+      'repack_deadlock',
       'repack_toast',
       'syscache-update-pruned',
       'heap_lock_update',
diff --git a/src/test/modules/injection_points/specs/repack_deadlock.spec b/src/test/modules/injection_points/specs/repack_deadlock.spec
new file mode 100644
index 00000000000..9d23a6588c2
--- /dev/null
+++ b/src/test/modules/injection_points/specs/repack_deadlock.spec
@@ -0,0 +1,83 @@
+# Test REPACK with a concurrent transaction that would cause a deadlock
+setup
+{
+	CREATE EXTENSION injection_points;
+
+	CREATE TABLE repack_deadlock(i int PRIMARY KEY, j int);
+	INSERT INTO repack_deadlock(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4);
+
+	CREATE TABLE relfilenodes(node oid);
+
+	CREATE TABLE data_s1(i int, j int);
+	CREATE TABLE data_s2(i int, j int);
+}
+
+teardown
+{
+	DROP TABLE repack_deadlock;
+	DROP EXTENSION injection_points;
+
+	DROP TABLE relfilenodes;
+	DROP TABLE data_s1;
+	DROP TABLE data_s2;
+}
+
+session s1
+setup
+{
+	SELECT injection_points_set_local();
+	SELECT injection_points_attach('repack-concurrently-before-lock', 'wait');
+}
+# Perform the initial load and wait for s2 to do some data changes.
+step wait_before_lock
+{
+	REPACK (CONCURRENTLY) repack_deadlock USING INDEX repack_deadlock_pkey;
+}
+# Check the table from the perspective of s1.
+#
+# Besides the contents, we also check that relfilenode has changed.
+
+# Have each session write the contents into a table and use FULL JOIN to check
+# if the outputs are identical.
+step check1
+{
+	INSERT INTO relfilenodes(node)
+	SELECT relfilenode FROM pg_class WHERE relname='repack_deadlock';
+
+	SELECT count(DISTINCT node) FROM relfilenodes;
+
+	SELECT i, j FROM repack_deadlock ORDER BY i, j;
+
+	INSERT INTO data_s1(i, j)
+	SELECT i, j FROM repack_deadlock;
+
+	SELECT count(*)
+	FROM data_s1 d1 FULL JOIN data_s2 d2 USING (i, j)
+	WHERE d1.i ISNULL OR d2.i ISNULL;
+}
+teardown
+{
+	SELECT injection_points_detach('repack-concurrently-before-lock');
+}
+
+session s2
+# Change the existing data. UPDATE changes both key and non-key columns. Also
+# update one row twice to test whether tuple version generated by this session
+# can be found.
+step add_column
+{
+	alter table repack_deadlock add column noise text;
+}
+
+step wakeup_before_lock
+{
+	SELECT injection_points_wakeup('repack-concurrently-before-lock');
+}
+
+# Test if data changes introduced while one session is performing REPACK
+# CONCURRENTLY find their way into the table.
+permutation
+	wait_before_lock
+	add_column
+	wakeup_before_lock
+	check1
-- 
2.47.3


--fot5plqmb33ey33c--





^ permalink  raw  reply  [nested|flat] 89+ messages in thread

* [PATCH v44b 4/4] hack deadlock detector
@ 2026-03-25 19:35  Álvaro Herrera <[email protected]>
  0 siblings, 0 replies; 89+ messages in thread

From: Álvaro Herrera @ 2026-03-25 19:35 UTC (permalink / raw)

---
 src/backend/commands/cluster.c                | 10 +++
 src/backend/storage/lmgr/deadlock.c           | 12 +++
 src/include/storage/proc.h                    |  1 +
 src/test/modules/injection_points/Makefile    |  1 +
 .../expected/repack_deadlock.out              | 63 ++++++++++++++
 src/test/modules/injection_points/meson.build |  1 +
 .../specs/repack_deadlock.spec                | 83 +++++++++++++++++++
 7 files changed, 171 insertions(+)
 create mode 100644 src/test/modules/injection_points/expected/repack_deadlock.out
 create mode 100644 src/test/modules/injection_points/specs/repack_deadlock.spec

diff --git a/src/backend/commands/cluster.c b/src/backend/commands/cluster.c
index 2c3058ba10d..d5b1dfbff69 100644
--- a/src/backend/commands/cluster.c
+++ b/src/backend/commands/cluster.c
@@ -372,6 +372,16 @@ ExecRepack(ParseState *pstate, RepackStmt *stmt, bool isTopLevel)
 	/* Determine the lock mode to use. */
 	lockmode = RepackLockLevel((params.options & CLUOPT_CONCURRENT) != 0);
 
+	/*
+	 * If in concurrent mode, set the PROC_IN_CONCURRENT_REPACK flag.  This
+	 * makes the deadlock checker cause anyone that would conflict with us
+	 * to error out.  It's important to set this flag ahead of actually locking
+	 * the relation; it won't of course affect anyone until we do have a lock
+	 * that others can conflict with.
+	 */
+	if ((params.options & CLUOPT_CONCURRENT) != 0)
+		MyProc->statusFlags |= PROC_IN_CONCURRENT_REPACK;
+
 	/*
 	 * If a single relation is specified, process it and we're done ... unless
 	 * the relation is a partitioned table, in which case we fall through.
diff --git a/src/backend/storage/lmgr/deadlock.c b/src/backend/storage/lmgr/deadlock.c
index b8962d875b6..10266e384aa 100644
--- a/src/backend/storage/lmgr/deadlock.c
+++ b/src/backend/storage/lmgr/deadlock.c
@@ -620,6 +620,18 @@ FindLockCycleRecurseMember(PGPROC *checkProc,
 						proc->statusFlags & PROC_IS_AUTOVACUUM)
 						blocking_autovacuum_proc = proc;
 
+					/*
+					 * If we note that we're blocked by some process running
+					 * REPACK (CONCURRENTLY), just fail.  That process is
+					 * going to upgrade its lock at some point, and it would
+					 * be inappropriate for any other process to cause that
+					 * to fail.
+					 */
+					if (checkProc == MyProc &&
+						proc->statusFlags & PROC_IN_CONCURRENT_REPACK)
+						/* not a serious error message proposal */
+						elog(ERROR, "oops, we're blocked by REPACK, time to die");
+
 					/* We're done looking at this proclock */
 					break;
 				}
diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h
index 1dad125706e..4e340ead455 100644
--- a/src/include/storage/proc.h
+++ b/src/include/storage/proc.h
@@ -69,6 +69,7 @@ struct XidCache
 #define		PROC_AFFECTS_ALL_HORIZONS	0x20	/* this proc's xmin must be
 												 * included in vacuum horizons
 												 * in all databases */
+#define		PROC_IN_CONCURRENT_REPACK	0x40	/* REPACK (CONCURRENTLY) */
 
 /* flags reset at EOXact */
 #define		PROC_VACUUM_STATE_MASK \
diff --git a/src/test/modules/injection_points/Makefile b/src/test/modules/injection_points/Makefile
index 2cd7d87c533..f7663859fe2 100644
--- a/src/test/modules/injection_points/Makefile
+++ b/src/test/modules/injection_points/Makefile
@@ -15,6 +15,7 @@ REGRESS_OPTS = --dlpath=$(top_builddir)/src/test/regress
 ISOLATION = basic \
 	    inplace \
 	    repack \
+	    repack_deadlock \
 	    repack_toast \
 	    syscache-update-pruned \
 	    heap_lock_update
diff --git a/src/test/modules/injection_points/expected/repack_deadlock.out b/src/test/modules/injection_points/expected/repack_deadlock.out
new file mode 100644
index 00000000000..42505d10933
--- /dev/null
+++ b/src/test/modules/injection_points/expected/repack_deadlock.out
@@ -0,0 +1,63 @@
+Parsed test spec with 2 sessions
+
+starting permutation: wait_before_lock add_column wakeup_before_lock check1
+injection_points_attach
+-----------------------
+                       
+(1 row)
+
+step wait_before_lock: 
+	REPACK (CONCURRENTLY) repack_deadlock USING INDEX repack_deadlock_pkey;
+ <waiting ...>
+step add_column: 
+	alter table repack_deadlock add column noise text;
+ <waiting ...>
+step add_column: <... completed>
+ERROR:  oops, we're blocked by REPACK, time to die
+step wakeup_before_lock: 
+	SELECT injection_points_wakeup('repack-concurrently-before-lock');
+
+injection_points_wakeup
+-----------------------
+                       
+(1 row)
+
+step wait_before_lock: <... completed>
+step check1: 
+	INSERT INTO relfilenodes(node)
+	SELECT relfilenode FROM pg_class WHERE relname='repack_deadlock';
+
+	SELECT count(DISTINCT node) FROM relfilenodes;
+
+	SELECT i, j FROM repack_deadlock ORDER BY i, j;
+
+	INSERT INTO data_s1(i, j)
+	SELECT i, j FROM repack_deadlock;
+
+	SELECT count(*)
+	FROM data_s1 d1 FULL JOIN data_s2 d2 USING (i, j)
+	WHERE d1.i ISNULL OR d2.i ISNULL;
+
+count
+-----
+    1
+(1 row)
+
+i|j
+-+-
+1|1
+2|2
+3|3
+4|4
+(4 rows)
+
+count
+-----
+    4
+(1 row)
+
+injection_points_detach
+-----------------------
+                       
+(1 row)
+
diff --git a/src/test/modules/injection_points/meson.build b/src/test/modules/injection_points/meson.build
index a414abb924b..1cd88d6db65 100644
--- a/src/test/modules/injection_points/meson.build
+++ b/src/test/modules/injection_points/meson.build
@@ -46,6 +46,7 @@ tests += {
       'basic',
       'inplace',
       'repack',
+      'repack_deadlock',
       'repack_toast',
       'syscache-update-pruned',
       'heap_lock_update',
diff --git a/src/test/modules/injection_points/specs/repack_deadlock.spec b/src/test/modules/injection_points/specs/repack_deadlock.spec
new file mode 100644
index 00000000000..9d23a6588c2
--- /dev/null
+++ b/src/test/modules/injection_points/specs/repack_deadlock.spec
@@ -0,0 +1,83 @@
+# Test REPACK with a concurrent transaction that would cause a deadlock
+setup
+{
+	CREATE EXTENSION injection_points;
+
+	CREATE TABLE repack_deadlock(i int PRIMARY KEY, j int);
+	INSERT INTO repack_deadlock(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4);
+
+	CREATE TABLE relfilenodes(node oid);
+
+	CREATE TABLE data_s1(i int, j int);
+	CREATE TABLE data_s2(i int, j int);
+}
+
+teardown
+{
+	DROP TABLE repack_deadlock;
+	DROP EXTENSION injection_points;
+
+	DROP TABLE relfilenodes;
+	DROP TABLE data_s1;
+	DROP TABLE data_s2;
+}
+
+session s1
+setup
+{
+	SELECT injection_points_set_local();
+	SELECT injection_points_attach('repack-concurrently-before-lock', 'wait');
+}
+# Perform the initial load and wait for s2 to do some data changes.
+step wait_before_lock
+{
+	REPACK (CONCURRENTLY) repack_deadlock USING INDEX repack_deadlock_pkey;
+}
+# Check the table from the perspective of s1.
+#
+# Besides the contents, we also check that relfilenode has changed.
+
+# Have each session write the contents into a table and use FULL JOIN to check
+# if the outputs are identical.
+step check1
+{
+	INSERT INTO relfilenodes(node)
+	SELECT relfilenode FROM pg_class WHERE relname='repack_deadlock';
+
+	SELECT count(DISTINCT node) FROM relfilenodes;
+
+	SELECT i, j FROM repack_deadlock ORDER BY i, j;
+
+	INSERT INTO data_s1(i, j)
+	SELECT i, j FROM repack_deadlock;
+
+	SELECT count(*)
+	FROM data_s1 d1 FULL JOIN data_s2 d2 USING (i, j)
+	WHERE d1.i ISNULL OR d2.i ISNULL;
+}
+teardown
+{
+	SELECT injection_points_detach('repack-concurrently-before-lock');
+}
+
+session s2
+# Change the existing data. UPDATE changes both key and non-key columns. Also
+# update one row twice to test whether tuple version generated by this session
+# can be found.
+step add_column
+{
+	alter table repack_deadlock add column noise text;
+}
+
+step wakeup_before_lock
+{
+	SELECT injection_points_wakeup('repack-concurrently-before-lock');
+}
+
+# Test if data changes introduced while one session is performing REPACK
+# CONCURRENTLY find their way into the table.
+permutation
+	wait_before_lock
+	add_column
+	wakeup_before_lock
+	check1
-- 
2.47.3


--fot5plqmb33ey33c--





^ permalink  raw  reply  [nested|flat] 89+ messages in thread

* [PATCH v44b 4/4] hack deadlock detector
@ 2026-03-25 19:35  Álvaro Herrera <[email protected]>
  0 siblings, 0 replies; 89+ messages in thread

From: Álvaro Herrera @ 2026-03-25 19:35 UTC (permalink / raw)

---
 src/backend/commands/cluster.c                | 10 +++
 src/backend/storage/lmgr/deadlock.c           | 12 +++
 src/include/storage/proc.h                    |  1 +
 src/test/modules/injection_points/Makefile    |  1 +
 .../expected/repack_deadlock.out              | 63 ++++++++++++++
 src/test/modules/injection_points/meson.build |  1 +
 .../specs/repack_deadlock.spec                | 83 +++++++++++++++++++
 7 files changed, 171 insertions(+)
 create mode 100644 src/test/modules/injection_points/expected/repack_deadlock.out
 create mode 100644 src/test/modules/injection_points/specs/repack_deadlock.spec

diff --git a/src/backend/commands/cluster.c b/src/backend/commands/cluster.c
index 2c3058ba10d..d5b1dfbff69 100644
--- a/src/backend/commands/cluster.c
+++ b/src/backend/commands/cluster.c
@@ -372,6 +372,16 @@ ExecRepack(ParseState *pstate, RepackStmt *stmt, bool isTopLevel)
 	/* Determine the lock mode to use. */
 	lockmode = RepackLockLevel((params.options & CLUOPT_CONCURRENT) != 0);
 
+	/*
+	 * If in concurrent mode, set the PROC_IN_CONCURRENT_REPACK flag.  This
+	 * makes the deadlock checker cause anyone that would conflict with us
+	 * to error out.  It's important to set this flag ahead of actually locking
+	 * the relation; it won't of course affect anyone until we do have a lock
+	 * that others can conflict with.
+	 */
+	if ((params.options & CLUOPT_CONCURRENT) != 0)
+		MyProc->statusFlags |= PROC_IN_CONCURRENT_REPACK;
+
 	/*
 	 * If a single relation is specified, process it and we're done ... unless
 	 * the relation is a partitioned table, in which case we fall through.
diff --git a/src/backend/storage/lmgr/deadlock.c b/src/backend/storage/lmgr/deadlock.c
index b8962d875b6..10266e384aa 100644
--- a/src/backend/storage/lmgr/deadlock.c
+++ b/src/backend/storage/lmgr/deadlock.c
@@ -620,6 +620,18 @@ FindLockCycleRecurseMember(PGPROC *checkProc,
 						proc->statusFlags & PROC_IS_AUTOVACUUM)
 						blocking_autovacuum_proc = proc;
 
+					/*
+					 * If we note that we're blocked by some process running
+					 * REPACK (CONCURRENTLY), just fail.  That process is
+					 * going to upgrade its lock at some point, and it would
+					 * be inappropriate for any other process to cause that
+					 * to fail.
+					 */
+					if (checkProc == MyProc &&
+						proc->statusFlags & PROC_IN_CONCURRENT_REPACK)
+						/* not a serious error message proposal */
+						elog(ERROR, "oops, we're blocked by REPACK, time to die");
+
 					/* We're done looking at this proclock */
 					break;
 				}
diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h
index 1dad125706e..4e340ead455 100644
--- a/src/include/storage/proc.h
+++ b/src/include/storage/proc.h
@@ -69,6 +69,7 @@ struct XidCache
 #define		PROC_AFFECTS_ALL_HORIZONS	0x20	/* this proc's xmin must be
 												 * included in vacuum horizons
 												 * in all databases */
+#define		PROC_IN_CONCURRENT_REPACK	0x40	/* REPACK (CONCURRENTLY) */
 
 /* flags reset at EOXact */
 #define		PROC_VACUUM_STATE_MASK \
diff --git a/src/test/modules/injection_points/Makefile b/src/test/modules/injection_points/Makefile
index 2cd7d87c533..f7663859fe2 100644
--- a/src/test/modules/injection_points/Makefile
+++ b/src/test/modules/injection_points/Makefile
@@ -15,6 +15,7 @@ REGRESS_OPTS = --dlpath=$(top_builddir)/src/test/regress
 ISOLATION = basic \
 	    inplace \
 	    repack \
+	    repack_deadlock \
 	    repack_toast \
 	    syscache-update-pruned \
 	    heap_lock_update
diff --git a/src/test/modules/injection_points/expected/repack_deadlock.out b/src/test/modules/injection_points/expected/repack_deadlock.out
new file mode 100644
index 00000000000..42505d10933
--- /dev/null
+++ b/src/test/modules/injection_points/expected/repack_deadlock.out
@@ -0,0 +1,63 @@
+Parsed test spec with 2 sessions
+
+starting permutation: wait_before_lock add_column wakeup_before_lock check1
+injection_points_attach
+-----------------------
+                       
+(1 row)
+
+step wait_before_lock: 
+	REPACK (CONCURRENTLY) repack_deadlock USING INDEX repack_deadlock_pkey;
+ <waiting ...>
+step add_column: 
+	alter table repack_deadlock add column noise text;
+ <waiting ...>
+step add_column: <... completed>
+ERROR:  oops, we're blocked by REPACK, time to die
+step wakeup_before_lock: 
+	SELECT injection_points_wakeup('repack-concurrently-before-lock');
+
+injection_points_wakeup
+-----------------------
+                       
+(1 row)
+
+step wait_before_lock: <... completed>
+step check1: 
+	INSERT INTO relfilenodes(node)
+	SELECT relfilenode FROM pg_class WHERE relname='repack_deadlock';
+
+	SELECT count(DISTINCT node) FROM relfilenodes;
+
+	SELECT i, j FROM repack_deadlock ORDER BY i, j;
+
+	INSERT INTO data_s1(i, j)
+	SELECT i, j FROM repack_deadlock;
+
+	SELECT count(*)
+	FROM data_s1 d1 FULL JOIN data_s2 d2 USING (i, j)
+	WHERE d1.i ISNULL OR d2.i ISNULL;
+
+count
+-----
+    1
+(1 row)
+
+i|j
+-+-
+1|1
+2|2
+3|3
+4|4
+(4 rows)
+
+count
+-----
+    4
+(1 row)
+
+injection_points_detach
+-----------------------
+                       
+(1 row)
+
diff --git a/src/test/modules/injection_points/meson.build b/src/test/modules/injection_points/meson.build
index a414abb924b..1cd88d6db65 100644
--- a/src/test/modules/injection_points/meson.build
+++ b/src/test/modules/injection_points/meson.build
@@ -46,6 +46,7 @@ tests += {
       'basic',
       'inplace',
       'repack',
+      'repack_deadlock',
       'repack_toast',
       'syscache-update-pruned',
       'heap_lock_update',
diff --git a/src/test/modules/injection_points/specs/repack_deadlock.spec b/src/test/modules/injection_points/specs/repack_deadlock.spec
new file mode 100644
index 00000000000..9d23a6588c2
--- /dev/null
+++ b/src/test/modules/injection_points/specs/repack_deadlock.spec
@@ -0,0 +1,83 @@
+# Test REPACK with a concurrent transaction that would cause a deadlock
+setup
+{
+	CREATE EXTENSION injection_points;
+
+	CREATE TABLE repack_deadlock(i int PRIMARY KEY, j int);
+	INSERT INTO repack_deadlock(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4);
+
+	CREATE TABLE relfilenodes(node oid);
+
+	CREATE TABLE data_s1(i int, j int);
+	CREATE TABLE data_s2(i int, j int);
+}
+
+teardown
+{
+	DROP TABLE repack_deadlock;
+	DROP EXTENSION injection_points;
+
+	DROP TABLE relfilenodes;
+	DROP TABLE data_s1;
+	DROP TABLE data_s2;
+}
+
+session s1
+setup
+{
+	SELECT injection_points_set_local();
+	SELECT injection_points_attach('repack-concurrently-before-lock', 'wait');
+}
+# Perform the initial load and wait for s2 to do some data changes.
+step wait_before_lock
+{
+	REPACK (CONCURRENTLY) repack_deadlock USING INDEX repack_deadlock_pkey;
+}
+# Check the table from the perspective of s1.
+#
+# Besides the contents, we also check that relfilenode has changed.
+
+# Have each session write the contents into a table and use FULL JOIN to check
+# if the outputs are identical.
+step check1
+{
+	INSERT INTO relfilenodes(node)
+	SELECT relfilenode FROM pg_class WHERE relname='repack_deadlock';
+
+	SELECT count(DISTINCT node) FROM relfilenodes;
+
+	SELECT i, j FROM repack_deadlock ORDER BY i, j;
+
+	INSERT INTO data_s1(i, j)
+	SELECT i, j FROM repack_deadlock;
+
+	SELECT count(*)
+	FROM data_s1 d1 FULL JOIN data_s2 d2 USING (i, j)
+	WHERE d1.i ISNULL OR d2.i ISNULL;
+}
+teardown
+{
+	SELECT injection_points_detach('repack-concurrently-before-lock');
+}
+
+session s2
+# Change the existing data. UPDATE changes both key and non-key columns. Also
+# update one row twice to test whether tuple version generated by this session
+# can be found.
+step add_column
+{
+	alter table repack_deadlock add column noise text;
+}
+
+step wakeup_before_lock
+{
+	SELECT injection_points_wakeup('repack-concurrently-before-lock');
+}
+
+# Test if data changes introduced while one session is performing REPACK
+# CONCURRENTLY find their way into the table.
+permutation
+	wait_before_lock
+	add_column
+	wakeup_before_lock
+	check1
-- 
2.47.3


--fot5plqmb33ey33c--





^ permalink  raw  reply  [nested|flat] 89+ messages in thread

* [PATCH v44b 4/4] hack deadlock detector
@ 2026-03-25 19:35  Álvaro Herrera <[email protected]>
  0 siblings, 0 replies; 89+ messages in thread

From: Álvaro Herrera @ 2026-03-25 19:35 UTC (permalink / raw)

---
 src/backend/commands/cluster.c                | 10 +++
 src/backend/storage/lmgr/deadlock.c           | 12 +++
 src/include/storage/proc.h                    |  1 +
 src/test/modules/injection_points/Makefile    |  1 +
 .../expected/repack_deadlock.out              | 63 ++++++++++++++
 src/test/modules/injection_points/meson.build |  1 +
 .../specs/repack_deadlock.spec                | 83 +++++++++++++++++++
 7 files changed, 171 insertions(+)
 create mode 100644 src/test/modules/injection_points/expected/repack_deadlock.out
 create mode 100644 src/test/modules/injection_points/specs/repack_deadlock.spec

diff --git a/src/backend/commands/cluster.c b/src/backend/commands/cluster.c
index 2c3058ba10d..d5b1dfbff69 100644
--- a/src/backend/commands/cluster.c
+++ b/src/backend/commands/cluster.c
@@ -372,6 +372,16 @@ ExecRepack(ParseState *pstate, RepackStmt *stmt, bool isTopLevel)
 	/* Determine the lock mode to use. */
 	lockmode = RepackLockLevel((params.options & CLUOPT_CONCURRENT) != 0);
 
+	/*
+	 * If in concurrent mode, set the PROC_IN_CONCURRENT_REPACK flag.  This
+	 * makes the deadlock checker cause anyone that would conflict with us
+	 * to error out.  It's important to set this flag ahead of actually locking
+	 * the relation; it won't of course affect anyone until we do have a lock
+	 * that others can conflict with.
+	 */
+	if ((params.options & CLUOPT_CONCURRENT) != 0)
+		MyProc->statusFlags |= PROC_IN_CONCURRENT_REPACK;
+
 	/*
 	 * If a single relation is specified, process it and we're done ... unless
 	 * the relation is a partitioned table, in which case we fall through.
diff --git a/src/backend/storage/lmgr/deadlock.c b/src/backend/storage/lmgr/deadlock.c
index b8962d875b6..10266e384aa 100644
--- a/src/backend/storage/lmgr/deadlock.c
+++ b/src/backend/storage/lmgr/deadlock.c
@@ -620,6 +620,18 @@ FindLockCycleRecurseMember(PGPROC *checkProc,
 						proc->statusFlags & PROC_IS_AUTOVACUUM)
 						blocking_autovacuum_proc = proc;
 
+					/*
+					 * If we note that we're blocked by some process running
+					 * REPACK (CONCURRENTLY), just fail.  That process is
+					 * going to upgrade its lock at some point, and it would
+					 * be inappropriate for any other process to cause that
+					 * to fail.
+					 */
+					if (checkProc == MyProc &&
+						proc->statusFlags & PROC_IN_CONCURRENT_REPACK)
+						/* not a serious error message proposal */
+						elog(ERROR, "oops, we're blocked by REPACK, time to die");
+
 					/* We're done looking at this proclock */
 					break;
 				}
diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h
index 1dad125706e..4e340ead455 100644
--- a/src/include/storage/proc.h
+++ b/src/include/storage/proc.h
@@ -69,6 +69,7 @@ struct XidCache
 #define		PROC_AFFECTS_ALL_HORIZONS	0x20	/* this proc's xmin must be
 												 * included in vacuum horizons
 												 * in all databases */
+#define		PROC_IN_CONCURRENT_REPACK	0x40	/* REPACK (CONCURRENTLY) */
 
 /* flags reset at EOXact */
 #define		PROC_VACUUM_STATE_MASK \
diff --git a/src/test/modules/injection_points/Makefile b/src/test/modules/injection_points/Makefile
index 2cd7d87c533..f7663859fe2 100644
--- a/src/test/modules/injection_points/Makefile
+++ b/src/test/modules/injection_points/Makefile
@@ -15,6 +15,7 @@ REGRESS_OPTS = --dlpath=$(top_builddir)/src/test/regress
 ISOLATION = basic \
 	    inplace \
 	    repack \
+	    repack_deadlock \
 	    repack_toast \
 	    syscache-update-pruned \
 	    heap_lock_update
diff --git a/src/test/modules/injection_points/expected/repack_deadlock.out b/src/test/modules/injection_points/expected/repack_deadlock.out
new file mode 100644
index 00000000000..42505d10933
--- /dev/null
+++ b/src/test/modules/injection_points/expected/repack_deadlock.out
@@ -0,0 +1,63 @@
+Parsed test spec with 2 sessions
+
+starting permutation: wait_before_lock add_column wakeup_before_lock check1
+injection_points_attach
+-----------------------
+                       
+(1 row)
+
+step wait_before_lock: 
+	REPACK (CONCURRENTLY) repack_deadlock USING INDEX repack_deadlock_pkey;
+ <waiting ...>
+step add_column: 
+	alter table repack_deadlock add column noise text;
+ <waiting ...>
+step add_column: <... completed>
+ERROR:  oops, we're blocked by REPACK, time to die
+step wakeup_before_lock: 
+	SELECT injection_points_wakeup('repack-concurrently-before-lock');
+
+injection_points_wakeup
+-----------------------
+                       
+(1 row)
+
+step wait_before_lock: <... completed>
+step check1: 
+	INSERT INTO relfilenodes(node)
+	SELECT relfilenode FROM pg_class WHERE relname='repack_deadlock';
+
+	SELECT count(DISTINCT node) FROM relfilenodes;
+
+	SELECT i, j FROM repack_deadlock ORDER BY i, j;
+
+	INSERT INTO data_s1(i, j)
+	SELECT i, j FROM repack_deadlock;
+
+	SELECT count(*)
+	FROM data_s1 d1 FULL JOIN data_s2 d2 USING (i, j)
+	WHERE d1.i ISNULL OR d2.i ISNULL;
+
+count
+-----
+    1
+(1 row)
+
+i|j
+-+-
+1|1
+2|2
+3|3
+4|4
+(4 rows)
+
+count
+-----
+    4
+(1 row)
+
+injection_points_detach
+-----------------------
+                       
+(1 row)
+
diff --git a/src/test/modules/injection_points/meson.build b/src/test/modules/injection_points/meson.build
index a414abb924b..1cd88d6db65 100644
--- a/src/test/modules/injection_points/meson.build
+++ b/src/test/modules/injection_points/meson.build
@@ -46,6 +46,7 @@ tests += {
       'basic',
       'inplace',
       'repack',
+      'repack_deadlock',
       'repack_toast',
       'syscache-update-pruned',
       'heap_lock_update',
diff --git a/src/test/modules/injection_points/specs/repack_deadlock.spec b/src/test/modules/injection_points/specs/repack_deadlock.spec
new file mode 100644
index 00000000000..9d23a6588c2
--- /dev/null
+++ b/src/test/modules/injection_points/specs/repack_deadlock.spec
@@ -0,0 +1,83 @@
+# Test REPACK with a concurrent transaction that would cause a deadlock
+setup
+{
+	CREATE EXTENSION injection_points;
+
+	CREATE TABLE repack_deadlock(i int PRIMARY KEY, j int);
+	INSERT INTO repack_deadlock(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4);
+
+	CREATE TABLE relfilenodes(node oid);
+
+	CREATE TABLE data_s1(i int, j int);
+	CREATE TABLE data_s2(i int, j int);
+}
+
+teardown
+{
+	DROP TABLE repack_deadlock;
+	DROP EXTENSION injection_points;
+
+	DROP TABLE relfilenodes;
+	DROP TABLE data_s1;
+	DROP TABLE data_s2;
+}
+
+session s1
+setup
+{
+	SELECT injection_points_set_local();
+	SELECT injection_points_attach('repack-concurrently-before-lock', 'wait');
+}
+# Perform the initial load and wait for s2 to do some data changes.
+step wait_before_lock
+{
+	REPACK (CONCURRENTLY) repack_deadlock USING INDEX repack_deadlock_pkey;
+}
+# Check the table from the perspective of s1.
+#
+# Besides the contents, we also check that relfilenode has changed.
+
+# Have each session write the contents into a table and use FULL JOIN to check
+# if the outputs are identical.
+step check1
+{
+	INSERT INTO relfilenodes(node)
+	SELECT relfilenode FROM pg_class WHERE relname='repack_deadlock';
+
+	SELECT count(DISTINCT node) FROM relfilenodes;
+
+	SELECT i, j FROM repack_deadlock ORDER BY i, j;
+
+	INSERT INTO data_s1(i, j)
+	SELECT i, j FROM repack_deadlock;
+
+	SELECT count(*)
+	FROM data_s1 d1 FULL JOIN data_s2 d2 USING (i, j)
+	WHERE d1.i ISNULL OR d2.i ISNULL;
+}
+teardown
+{
+	SELECT injection_points_detach('repack-concurrently-before-lock');
+}
+
+session s2
+# Change the existing data. UPDATE changes both key and non-key columns. Also
+# update one row twice to test whether tuple version generated by this session
+# can be found.
+step add_column
+{
+	alter table repack_deadlock add column noise text;
+}
+
+step wakeup_before_lock
+{
+	SELECT injection_points_wakeup('repack-concurrently-before-lock');
+}
+
+# Test if data changes introduced while one session is performing REPACK
+# CONCURRENTLY find their way into the table.
+permutation
+	wait_before_lock
+	add_column
+	wakeup_before_lock
+	check1
-- 
2.47.3


--fot5plqmb33ey33c--





^ permalink  raw  reply  [nested|flat] 89+ messages in thread

* [PATCH v44b 4/4] hack deadlock detector
@ 2026-03-25 19:35  Álvaro Herrera <[email protected]>
  0 siblings, 0 replies; 89+ messages in thread

From: Álvaro Herrera @ 2026-03-25 19:35 UTC (permalink / raw)

---
 src/backend/commands/cluster.c                | 10 +++
 src/backend/storage/lmgr/deadlock.c           | 12 +++
 src/include/storage/proc.h                    |  1 +
 src/test/modules/injection_points/Makefile    |  1 +
 .../expected/repack_deadlock.out              | 63 ++++++++++++++
 src/test/modules/injection_points/meson.build |  1 +
 .../specs/repack_deadlock.spec                | 83 +++++++++++++++++++
 7 files changed, 171 insertions(+)
 create mode 100644 src/test/modules/injection_points/expected/repack_deadlock.out
 create mode 100644 src/test/modules/injection_points/specs/repack_deadlock.spec

diff --git a/src/backend/commands/cluster.c b/src/backend/commands/cluster.c
index 2c3058ba10d..d5b1dfbff69 100644
--- a/src/backend/commands/cluster.c
+++ b/src/backend/commands/cluster.c
@@ -372,6 +372,16 @@ ExecRepack(ParseState *pstate, RepackStmt *stmt, bool isTopLevel)
 	/* Determine the lock mode to use. */
 	lockmode = RepackLockLevel((params.options & CLUOPT_CONCURRENT) != 0);
 
+	/*
+	 * If in concurrent mode, set the PROC_IN_CONCURRENT_REPACK flag.  This
+	 * makes the deadlock checker cause anyone that would conflict with us
+	 * to error out.  It's important to set this flag ahead of actually locking
+	 * the relation; it won't of course affect anyone until we do have a lock
+	 * that others can conflict with.
+	 */
+	if ((params.options & CLUOPT_CONCURRENT) != 0)
+		MyProc->statusFlags |= PROC_IN_CONCURRENT_REPACK;
+
 	/*
 	 * If a single relation is specified, process it and we're done ... unless
 	 * the relation is a partitioned table, in which case we fall through.
diff --git a/src/backend/storage/lmgr/deadlock.c b/src/backend/storage/lmgr/deadlock.c
index b8962d875b6..10266e384aa 100644
--- a/src/backend/storage/lmgr/deadlock.c
+++ b/src/backend/storage/lmgr/deadlock.c
@@ -620,6 +620,18 @@ FindLockCycleRecurseMember(PGPROC *checkProc,
 						proc->statusFlags & PROC_IS_AUTOVACUUM)
 						blocking_autovacuum_proc = proc;
 
+					/*
+					 * If we note that we're blocked by some process running
+					 * REPACK (CONCURRENTLY), just fail.  That process is
+					 * going to upgrade its lock at some point, and it would
+					 * be inappropriate for any other process to cause that
+					 * to fail.
+					 */
+					if (checkProc == MyProc &&
+						proc->statusFlags & PROC_IN_CONCURRENT_REPACK)
+						/* not a serious error message proposal */
+						elog(ERROR, "oops, we're blocked by REPACK, time to die");
+
 					/* We're done looking at this proclock */
 					break;
 				}
diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h
index 1dad125706e..4e340ead455 100644
--- a/src/include/storage/proc.h
+++ b/src/include/storage/proc.h
@@ -69,6 +69,7 @@ struct XidCache
 #define		PROC_AFFECTS_ALL_HORIZONS	0x20	/* this proc's xmin must be
 												 * included in vacuum horizons
 												 * in all databases */
+#define		PROC_IN_CONCURRENT_REPACK	0x40	/* REPACK (CONCURRENTLY) */
 
 /* flags reset at EOXact */
 #define		PROC_VACUUM_STATE_MASK \
diff --git a/src/test/modules/injection_points/Makefile b/src/test/modules/injection_points/Makefile
index 2cd7d87c533..f7663859fe2 100644
--- a/src/test/modules/injection_points/Makefile
+++ b/src/test/modules/injection_points/Makefile
@@ -15,6 +15,7 @@ REGRESS_OPTS = --dlpath=$(top_builddir)/src/test/regress
 ISOLATION = basic \
 	    inplace \
 	    repack \
+	    repack_deadlock \
 	    repack_toast \
 	    syscache-update-pruned \
 	    heap_lock_update
diff --git a/src/test/modules/injection_points/expected/repack_deadlock.out b/src/test/modules/injection_points/expected/repack_deadlock.out
new file mode 100644
index 00000000000..42505d10933
--- /dev/null
+++ b/src/test/modules/injection_points/expected/repack_deadlock.out
@@ -0,0 +1,63 @@
+Parsed test spec with 2 sessions
+
+starting permutation: wait_before_lock add_column wakeup_before_lock check1
+injection_points_attach
+-----------------------
+                       
+(1 row)
+
+step wait_before_lock: 
+	REPACK (CONCURRENTLY) repack_deadlock USING INDEX repack_deadlock_pkey;
+ <waiting ...>
+step add_column: 
+	alter table repack_deadlock add column noise text;
+ <waiting ...>
+step add_column: <... completed>
+ERROR:  oops, we're blocked by REPACK, time to die
+step wakeup_before_lock: 
+	SELECT injection_points_wakeup('repack-concurrently-before-lock');
+
+injection_points_wakeup
+-----------------------
+                       
+(1 row)
+
+step wait_before_lock: <... completed>
+step check1: 
+	INSERT INTO relfilenodes(node)
+	SELECT relfilenode FROM pg_class WHERE relname='repack_deadlock';
+
+	SELECT count(DISTINCT node) FROM relfilenodes;
+
+	SELECT i, j FROM repack_deadlock ORDER BY i, j;
+
+	INSERT INTO data_s1(i, j)
+	SELECT i, j FROM repack_deadlock;
+
+	SELECT count(*)
+	FROM data_s1 d1 FULL JOIN data_s2 d2 USING (i, j)
+	WHERE d1.i ISNULL OR d2.i ISNULL;
+
+count
+-----
+    1
+(1 row)
+
+i|j
+-+-
+1|1
+2|2
+3|3
+4|4
+(4 rows)
+
+count
+-----
+    4
+(1 row)
+
+injection_points_detach
+-----------------------
+                       
+(1 row)
+
diff --git a/src/test/modules/injection_points/meson.build b/src/test/modules/injection_points/meson.build
index a414abb924b..1cd88d6db65 100644
--- a/src/test/modules/injection_points/meson.build
+++ b/src/test/modules/injection_points/meson.build
@@ -46,6 +46,7 @@ tests += {
       'basic',
       'inplace',
       'repack',
+      'repack_deadlock',
       'repack_toast',
       'syscache-update-pruned',
       'heap_lock_update',
diff --git a/src/test/modules/injection_points/specs/repack_deadlock.spec b/src/test/modules/injection_points/specs/repack_deadlock.spec
new file mode 100644
index 00000000000..9d23a6588c2
--- /dev/null
+++ b/src/test/modules/injection_points/specs/repack_deadlock.spec
@@ -0,0 +1,83 @@
+# Test REPACK with a concurrent transaction that would cause a deadlock
+setup
+{
+	CREATE EXTENSION injection_points;
+
+	CREATE TABLE repack_deadlock(i int PRIMARY KEY, j int);
+	INSERT INTO repack_deadlock(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4);
+
+	CREATE TABLE relfilenodes(node oid);
+
+	CREATE TABLE data_s1(i int, j int);
+	CREATE TABLE data_s2(i int, j int);
+}
+
+teardown
+{
+	DROP TABLE repack_deadlock;
+	DROP EXTENSION injection_points;
+
+	DROP TABLE relfilenodes;
+	DROP TABLE data_s1;
+	DROP TABLE data_s2;
+}
+
+session s1
+setup
+{
+	SELECT injection_points_set_local();
+	SELECT injection_points_attach('repack-concurrently-before-lock', 'wait');
+}
+# Perform the initial load and wait for s2 to do some data changes.
+step wait_before_lock
+{
+	REPACK (CONCURRENTLY) repack_deadlock USING INDEX repack_deadlock_pkey;
+}
+# Check the table from the perspective of s1.
+#
+# Besides the contents, we also check that relfilenode has changed.
+
+# Have each session write the contents into a table and use FULL JOIN to check
+# if the outputs are identical.
+step check1
+{
+	INSERT INTO relfilenodes(node)
+	SELECT relfilenode FROM pg_class WHERE relname='repack_deadlock';
+
+	SELECT count(DISTINCT node) FROM relfilenodes;
+
+	SELECT i, j FROM repack_deadlock ORDER BY i, j;
+
+	INSERT INTO data_s1(i, j)
+	SELECT i, j FROM repack_deadlock;
+
+	SELECT count(*)
+	FROM data_s1 d1 FULL JOIN data_s2 d2 USING (i, j)
+	WHERE d1.i ISNULL OR d2.i ISNULL;
+}
+teardown
+{
+	SELECT injection_points_detach('repack-concurrently-before-lock');
+}
+
+session s2
+# Change the existing data. UPDATE changes both key and non-key columns. Also
+# update one row twice to test whether tuple version generated by this session
+# can be found.
+step add_column
+{
+	alter table repack_deadlock add column noise text;
+}
+
+step wakeup_before_lock
+{
+	SELECT injection_points_wakeup('repack-concurrently-before-lock');
+}
+
+# Test if data changes introduced while one session is performing REPACK
+# CONCURRENTLY find their way into the table.
+permutation
+	wait_before_lock
+	add_column
+	wakeup_before_lock
+	check1
-- 
2.47.3


--fot5plqmb33ey33c--





^ permalink  raw  reply  [nested|flat] 89+ messages in thread

* [PATCH v44b 4/4] hack deadlock detector
@ 2026-03-25 19:35  Álvaro Herrera <[email protected]>
  0 siblings, 0 replies; 89+ messages in thread

From: Álvaro Herrera @ 2026-03-25 19:35 UTC (permalink / raw)

---
 src/backend/commands/cluster.c                | 10 +++
 src/backend/storage/lmgr/deadlock.c           | 12 +++
 src/include/storage/proc.h                    |  1 +
 src/test/modules/injection_points/Makefile    |  1 +
 .../expected/repack_deadlock.out              | 63 ++++++++++++++
 src/test/modules/injection_points/meson.build |  1 +
 .../specs/repack_deadlock.spec                | 83 +++++++++++++++++++
 7 files changed, 171 insertions(+)
 create mode 100644 src/test/modules/injection_points/expected/repack_deadlock.out
 create mode 100644 src/test/modules/injection_points/specs/repack_deadlock.spec

diff --git a/src/backend/commands/cluster.c b/src/backend/commands/cluster.c
index 2c3058ba10d..d5b1dfbff69 100644
--- a/src/backend/commands/cluster.c
+++ b/src/backend/commands/cluster.c
@@ -372,6 +372,16 @@ ExecRepack(ParseState *pstate, RepackStmt *stmt, bool isTopLevel)
 	/* Determine the lock mode to use. */
 	lockmode = RepackLockLevel((params.options & CLUOPT_CONCURRENT) != 0);
 
+	/*
+	 * If in concurrent mode, set the PROC_IN_CONCURRENT_REPACK flag.  This
+	 * makes the deadlock checker cause anyone that would conflict with us
+	 * to error out.  It's important to set this flag ahead of actually locking
+	 * the relation; it won't of course affect anyone until we do have a lock
+	 * that others can conflict with.
+	 */
+	if ((params.options & CLUOPT_CONCURRENT) != 0)
+		MyProc->statusFlags |= PROC_IN_CONCURRENT_REPACK;
+
 	/*
 	 * If a single relation is specified, process it and we're done ... unless
 	 * the relation is a partitioned table, in which case we fall through.
diff --git a/src/backend/storage/lmgr/deadlock.c b/src/backend/storage/lmgr/deadlock.c
index b8962d875b6..10266e384aa 100644
--- a/src/backend/storage/lmgr/deadlock.c
+++ b/src/backend/storage/lmgr/deadlock.c
@@ -620,6 +620,18 @@ FindLockCycleRecurseMember(PGPROC *checkProc,
 						proc->statusFlags & PROC_IS_AUTOVACUUM)
 						blocking_autovacuum_proc = proc;
 
+					/*
+					 * If we note that we're blocked by some process running
+					 * REPACK (CONCURRENTLY), just fail.  That process is
+					 * going to upgrade its lock at some point, and it would
+					 * be inappropriate for any other process to cause that
+					 * to fail.
+					 */
+					if (checkProc == MyProc &&
+						proc->statusFlags & PROC_IN_CONCURRENT_REPACK)
+						/* not a serious error message proposal */
+						elog(ERROR, "oops, we're blocked by REPACK, time to die");
+
 					/* We're done looking at this proclock */
 					break;
 				}
diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h
index 1dad125706e..4e340ead455 100644
--- a/src/include/storage/proc.h
+++ b/src/include/storage/proc.h
@@ -69,6 +69,7 @@ struct XidCache
 #define		PROC_AFFECTS_ALL_HORIZONS	0x20	/* this proc's xmin must be
 												 * included in vacuum horizons
 												 * in all databases */
+#define		PROC_IN_CONCURRENT_REPACK	0x40	/* REPACK (CONCURRENTLY) */
 
 /* flags reset at EOXact */
 #define		PROC_VACUUM_STATE_MASK \
diff --git a/src/test/modules/injection_points/Makefile b/src/test/modules/injection_points/Makefile
index 2cd7d87c533..f7663859fe2 100644
--- a/src/test/modules/injection_points/Makefile
+++ b/src/test/modules/injection_points/Makefile
@@ -15,6 +15,7 @@ REGRESS_OPTS = --dlpath=$(top_builddir)/src/test/regress
 ISOLATION = basic \
 	    inplace \
 	    repack \
+	    repack_deadlock \
 	    repack_toast \
 	    syscache-update-pruned \
 	    heap_lock_update
diff --git a/src/test/modules/injection_points/expected/repack_deadlock.out b/src/test/modules/injection_points/expected/repack_deadlock.out
new file mode 100644
index 00000000000..42505d10933
--- /dev/null
+++ b/src/test/modules/injection_points/expected/repack_deadlock.out
@@ -0,0 +1,63 @@
+Parsed test spec with 2 sessions
+
+starting permutation: wait_before_lock add_column wakeup_before_lock check1
+injection_points_attach
+-----------------------
+                       
+(1 row)
+
+step wait_before_lock: 
+	REPACK (CONCURRENTLY) repack_deadlock USING INDEX repack_deadlock_pkey;
+ <waiting ...>
+step add_column: 
+	alter table repack_deadlock add column noise text;
+ <waiting ...>
+step add_column: <... completed>
+ERROR:  oops, we're blocked by REPACK, time to die
+step wakeup_before_lock: 
+	SELECT injection_points_wakeup('repack-concurrently-before-lock');
+
+injection_points_wakeup
+-----------------------
+                       
+(1 row)
+
+step wait_before_lock: <... completed>
+step check1: 
+	INSERT INTO relfilenodes(node)
+	SELECT relfilenode FROM pg_class WHERE relname='repack_deadlock';
+
+	SELECT count(DISTINCT node) FROM relfilenodes;
+
+	SELECT i, j FROM repack_deadlock ORDER BY i, j;
+
+	INSERT INTO data_s1(i, j)
+	SELECT i, j FROM repack_deadlock;
+
+	SELECT count(*)
+	FROM data_s1 d1 FULL JOIN data_s2 d2 USING (i, j)
+	WHERE d1.i ISNULL OR d2.i ISNULL;
+
+count
+-----
+    1
+(1 row)
+
+i|j
+-+-
+1|1
+2|2
+3|3
+4|4
+(4 rows)
+
+count
+-----
+    4
+(1 row)
+
+injection_points_detach
+-----------------------
+                       
+(1 row)
+
diff --git a/src/test/modules/injection_points/meson.build b/src/test/modules/injection_points/meson.build
index a414abb924b..1cd88d6db65 100644
--- a/src/test/modules/injection_points/meson.build
+++ b/src/test/modules/injection_points/meson.build
@@ -46,6 +46,7 @@ tests += {
       'basic',
       'inplace',
       'repack',
+      'repack_deadlock',
       'repack_toast',
       'syscache-update-pruned',
       'heap_lock_update',
diff --git a/src/test/modules/injection_points/specs/repack_deadlock.spec b/src/test/modules/injection_points/specs/repack_deadlock.spec
new file mode 100644
index 00000000000..9d23a6588c2
--- /dev/null
+++ b/src/test/modules/injection_points/specs/repack_deadlock.spec
@@ -0,0 +1,83 @@
+# Test REPACK with a concurrent transaction that would cause a deadlock
+setup
+{
+	CREATE EXTENSION injection_points;
+
+	CREATE TABLE repack_deadlock(i int PRIMARY KEY, j int);
+	INSERT INTO repack_deadlock(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4);
+
+	CREATE TABLE relfilenodes(node oid);
+
+	CREATE TABLE data_s1(i int, j int);
+	CREATE TABLE data_s2(i int, j int);
+}
+
+teardown
+{
+	DROP TABLE repack_deadlock;
+	DROP EXTENSION injection_points;
+
+	DROP TABLE relfilenodes;
+	DROP TABLE data_s1;
+	DROP TABLE data_s2;
+}
+
+session s1
+setup
+{
+	SELECT injection_points_set_local();
+	SELECT injection_points_attach('repack-concurrently-before-lock', 'wait');
+}
+# Perform the initial load and wait for s2 to do some data changes.
+step wait_before_lock
+{
+	REPACK (CONCURRENTLY) repack_deadlock USING INDEX repack_deadlock_pkey;
+}
+# Check the table from the perspective of s1.
+#
+# Besides the contents, we also check that relfilenode has changed.
+
+# Have each session write the contents into a table and use FULL JOIN to check
+# if the outputs are identical.
+step check1
+{
+	INSERT INTO relfilenodes(node)
+	SELECT relfilenode FROM pg_class WHERE relname='repack_deadlock';
+
+	SELECT count(DISTINCT node) FROM relfilenodes;
+
+	SELECT i, j FROM repack_deadlock ORDER BY i, j;
+
+	INSERT INTO data_s1(i, j)
+	SELECT i, j FROM repack_deadlock;
+
+	SELECT count(*)
+	FROM data_s1 d1 FULL JOIN data_s2 d2 USING (i, j)
+	WHERE d1.i ISNULL OR d2.i ISNULL;
+}
+teardown
+{
+	SELECT injection_points_detach('repack-concurrently-before-lock');
+}
+
+session s2
+# Change the existing data. UPDATE changes both key and non-key columns. Also
+# update one row twice to test whether tuple version generated by this session
+# can be found.
+step add_column
+{
+	alter table repack_deadlock add column noise text;
+}
+
+step wakeup_before_lock
+{
+	SELECT injection_points_wakeup('repack-concurrently-before-lock');
+}
+
+# Test if data changes introduced while one session is performing REPACK
+# CONCURRENTLY find their way into the table.
+permutation
+	wait_before_lock
+	add_column
+	wakeup_before_lock
+	check1
-- 
2.47.3


--fot5plqmb33ey33c--





^ permalink  raw  reply  [nested|flat] 89+ messages in thread

* [PATCH v44b 4/4] hack deadlock detector
@ 2026-03-25 19:35  Álvaro Herrera <[email protected]>
  0 siblings, 0 replies; 89+ messages in thread

From: Álvaro Herrera @ 2026-03-25 19:35 UTC (permalink / raw)

---
 src/backend/commands/cluster.c                | 10 +++
 src/backend/storage/lmgr/deadlock.c           | 12 +++
 src/include/storage/proc.h                    |  1 +
 src/test/modules/injection_points/Makefile    |  1 +
 .../expected/repack_deadlock.out              | 63 ++++++++++++++
 src/test/modules/injection_points/meson.build |  1 +
 .../specs/repack_deadlock.spec                | 83 +++++++++++++++++++
 7 files changed, 171 insertions(+)
 create mode 100644 src/test/modules/injection_points/expected/repack_deadlock.out
 create mode 100644 src/test/modules/injection_points/specs/repack_deadlock.spec

diff --git a/src/backend/commands/cluster.c b/src/backend/commands/cluster.c
index 2c3058ba10d..d5b1dfbff69 100644
--- a/src/backend/commands/cluster.c
+++ b/src/backend/commands/cluster.c
@@ -372,6 +372,16 @@ ExecRepack(ParseState *pstate, RepackStmt *stmt, bool isTopLevel)
 	/* Determine the lock mode to use. */
 	lockmode = RepackLockLevel((params.options & CLUOPT_CONCURRENT) != 0);
 
+	/*
+	 * If in concurrent mode, set the PROC_IN_CONCURRENT_REPACK flag.  This
+	 * makes the deadlock checker cause anyone that would conflict with us
+	 * to error out.  It's important to set this flag ahead of actually locking
+	 * the relation; it won't of course affect anyone until we do have a lock
+	 * that others can conflict with.
+	 */
+	if ((params.options & CLUOPT_CONCURRENT) != 0)
+		MyProc->statusFlags |= PROC_IN_CONCURRENT_REPACK;
+
 	/*
 	 * If a single relation is specified, process it and we're done ... unless
 	 * the relation is a partitioned table, in which case we fall through.
diff --git a/src/backend/storage/lmgr/deadlock.c b/src/backend/storage/lmgr/deadlock.c
index b8962d875b6..10266e384aa 100644
--- a/src/backend/storage/lmgr/deadlock.c
+++ b/src/backend/storage/lmgr/deadlock.c
@@ -620,6 +620,18 @@ FindLockCycleRecurseMember(PGPROC *checkProc,
 						proc->statusFlags & PROC_IS_AUTOVACUUM)
 						blocking_autovacuum_proc = proc;
 
+					/*
+					 * If we note that we're blocked by some process running
+					 * REPACK (CONCURRENTLY), just fail.  That process is
+					 * going to upgrade its lock at some point, and it would
+					 * be inappropriate for any other process to cause that
+					 * to fail.
+					 */
+					if (checkProc == MyProc &&
+						proc->statusFlags & PROC_IN_CONCURRENT_REPACK)
+						/* not a serious error message proposal */
+						elog(ERROR, "oops, we're blocked by REPACK, time to die");
+
 					/* We're done looking at this proclock */
 					break;
 				}
diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h
index 1dad125706e..4e340ead455 100644
--- a/src/include/storage/proc.h
+++ b/src/include/storage/proc.h
@@ -69,6 +69,7 @@ struct XidCache
 #define		PROC_AFFECTS_ALL_HORIZONS	0x20	/* this proc's xmin must be
 												 * included in vacuum horizons
 												 * in all databases */
+#define		PROC_IN_CONCURRENT_REPACK	0x40	/* REPACK (CONCURRENTLY) */
 
 /* flags reset at EOXact */
 #define		PROC_VACUUM_STATE_MASK \
diff --git a/src/test/modules/injection_points/Makefile b/src/test/modules/injection_points/Makefile
index 2cd7d87c533..f7663859fe2 100644
--- a/src/test/modules/injection_points/Makefile
+++ b/src/test/modules/injection_points/Makefile
@@ -15,6 +15,7 @@ REGRESS_OPTS = --dlpath=$(top_builddir)/src/test/regress
 ISOLATION = basic \
 	    inplace \
 	    repack \
+	    repack_deadlock \
 	    repack_toast \
 	    syscache-update-pruned \
 	    heap_lock_update
diff --git a/src/test/modules/injection_points/expected/repack_deadlock.out b/src/test/modules/injection_points/expected/repack_deadlock.out
new file mode 100644
index 00000000000..42505d10933
--- /dev/null
+++ b/src/test/modules/injection_points/expected/repack_deadlock.out
@@ -0,0 +1,63 @@
+Parsed test spec with 2 sessions
+
+starting permutation: wait_before_lock add_column wakeup_before_lock check1
+injection_points_attach
+-----------------------
+                       
+(1 row)
+
+step wait_before_lock: 
+	REPACK (CONCURRENTLY) repack_deadlock USING INDEX repack_deadlock_pkey;
+ <waiting ...>
+step add_column: 
+	alter table repack_deadlock add column noise text;
+ <waiting ...>
+step add_column: <... completed>
+ERROR:  oops, we're blocked by REPACK, time to die
+step wakeup_before_lock: 
+	SELECT injection_points_wakeup('repack-concurrently-before-lock');
+
+injection_points_wakeup
+-----------------------
+                       
+(1 row)
+
+step wait_before_lock: <... completed>
+step check1: 
+	INSERT INTO relfilenodes(node)
+	SELECT relfilenode FROM pg_class WHERE relname='repack_deadlock';
+
+	SELECT count(DISTINCT node) FROM relfilenodes;
+
+	SELECT i, j FROM repack_deadlock ORDER BY i, j;
+
+	INSERT INTO data_s1(i, j)
+	SELECT i, j FROM repack_deadlock;
+
+	SELECT count(*)
+	FROM data_s1 d1 FULL JOIN data_s2 d2 USING (i, j)
+	WHERE d1.i ISNULL OR d2.i ISNULL;
+
+count
+-----
+    1
+(1 row)
+
+i|j
+-+-
+1|1
+2|2
+3|3
+4|4
+(4 rows)
+
+count
+-----
+    4
+(1 row)
+
+injection_points_detach
+-----------------------
+                       
+(1 row)
+
diff --git a/src/test/modules/injection_points/meson.build b/src/test/modules/injection_points/meson.build
index a414abb924b..1cd88d6db65 100644
--- a/src/test/modules/injection_points/meson.build
+++ b/src/test/modules/injection_points/meson.build
@@ -46,6 +46,7 @@ tests += {
       'basic',
       'inplace',
       'repack',
+      'repack_deadlock',
       'repack_toast',
       'syscache-update-pruned',
       'heap_lock_update',
diff --git a/src/test/modules/injection_points/specs/repack_deadlock.spec b/src/test/modules/injection_points/specs/repack_deadlock.spec
new file mode 100644
index 00000000000..9d23a6588c2
--- /dev/null
+++ b/src/test/modules/injection_points/specs/repack_deadlock.spec
@@ -0,0 +1,83 @@
+# Test REPACK with a concurrent transaction that would cause a deadlock
+setup
+{
+	CREATE EXTENSION injection_points;
+
+	CREATE TABLE repack_deadlock(i int PRIMARY KEY, j int);
+	INSERT INTO repack_deadlock(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4);
+
+	CREATE TABLE relfilenodes(node oid);
+
+	CREATE TABLE data_s1(i int, j int);
+	CREATE TABLE data_s2(i int, j int);
+}
+
+teardown
+{
+	DROP TABLE repack_deadlock;
+	DROP EXTENSION injection_points;
+
+	DROP TABLE relfilenodes;
+	DROP TABLE data_s1;
+	DROP TABLE data_s2;
+}
+
+session s1
+setup
+{
+	SELECT injection_points_set_local();
+	SELECT injection_points_attach('repack-concurrently-before-lock', 'wait');
+}
+# Perform the initial load and wait for s2 to do some data changes.
+step wait_before_lock
+{
+	REPACK (CONCURRENTLY) repack_deadlock USING INDEX repack_deadlock_pkey;
+}
+# Check the table from the perspective of s1.
+#
+# Besides the contents, we also check that relfilenode has changed.
+
+# Have each session write the contents into a table and use FULL JOIN to check
+# if the outputs are identical.
+step check1
+{
+	INSERT INTO relfilenodes(node)
+	SELECT relfilenode FROM pg_class WHERE relname='repack_deadlock';
+
+	SELECT count(DISTINCT node) FROM relfilenodes;
+
+	SELECT i, j FROM repack_deadlock ORDER BY i, j;
+
+	INSERT INTO data_s1(i, j)
+	SELECT i, j FROM repack_deadlock;
+
+	SELECT count(*)
+	FROM data_s1 d1 FULL JOIN data_s2 d2 USING (i, j)
+	WHERE d1.i ISNULL OR d2.i ISNULL;
+}
+teardown
+{
+	SELECT injection_points_detach('repack-concurrently-before-lock');
+}
+
+session s2
+# Change the existing data. UPDATE changes both key and non-key columns. Also
+# update one row twice to test whether tuple version generated by this session
+# can be found.
+step add_column
+{
+	alter table repack_deadlock add column noise text;
+}
+
+step wakeup_before_lock
+{
+	SELECT injection_points_wakeup('repack-concurrently-before-lock');
+}
+
+# Test if data changes introduced while one session is performing REPACK
+# CONCURRENTLY find their way into the table.
+permutation
+	wait_before_lock
+	add_column
+	wakeup_before_lock
+	check1
-- 
2.47.3


--fot5plqmb33ey33c--





^ permalink  raw  reply  [nested|flat] 89+ messages in thread


end of thread, other threads:[~2026-03-25 19:35 UTC | newest]

Thread overview: 89+ messages (download: mbox mbox.gz follow: Atom feed)
-- links below jump to the message on this page --
2024-08-26 04:32 [PATCH v21 4/8] Row pattern recognition patch (planner). Tatsuo Ishii <[email protected]>
2026-03-25 19:35 [PATCH v44b 4/4] hack deadlock detector Álvaro Herrera <[email protected]>
2026-03-25 19:35 [PATCH v44b 4/4] hack deadlock detector Álvaro Herrera <[email protected]>
2026-03-25 19:35 [PATCH v44b 4/4] hack deadlock detector Álvaro Herrera <[email protected]>
2026-03-25 19:35 [PATCH v44b 4/4] hack deadlock detector Álvaro Herrera <[email protected]>
2026-03-25 19:35 [PATCH v44b 4/4] hack deadlock detector Álvaro Herrera <[email protected]>
2026-03-25 19:35 [PATCH v44b 4/4] hack deadlock detector Álvaro Herrera <[email protected]>
2026-03-25 19:35 [PATCH v44b 4/4] hack deadlock detector Álvaro Herrera <[email protected]>
2026-03-25 19:35 [PATCH v44b 4/4] hack deadlock detector Álvaro Herrera <[email protected]>
2026-03-25 19:35 [PATCH v44b 4/4] hack deadlock detector Álvaro Herrera <[email protected]>
2026-03-25 19:35 [PATCH v44b 4/4] hack deadlock detector Álvaro Herrera <[email protected]>
2026-03-25 19:35 [PATCH v44b 4/4] hack deadlock detector Álvaro Herrera <[email protected]>
2026-03-25 19:35 [PATCH v44b 4/4] hack deadlock detector Álvaro Herrera <[email protected]>
2026-03-25 19:35 [PATCH v44b 4/4] hack deadlock detector Álvaro Herrera <[email protected]>
2026-03-25 19:35 [PATCH v44b 4/4] hack deadlock detector Álvaro Herrera <[email protected]>
2026-03-25 19:35 [PATCH v44b 4/4] hack deadlock detector Álvaro Herrera <[email protected]>
2026-03-25 19:35 [PATCH v44b 4/4] hack deadlock detector Álvaro Herrera <[email protected]>
2026-03-25 19:35 [PATCH v44b 4/4] hack deadlock detector Álvaro Herrera <[email protected]>
2026-03-25 19:35 [PATCH v44b 4/4] hack deadlock detector Álvaro Herrera <[email protected]>
2026-03-25 19:35 [PATCH v44b 4/4] hack deadlock detector Álvaro Herrera <[email protected]>
2026-03-25 19:35 [PATCH v44b 4/4] hack deadlock detector Álvaro Herrera <[email protected]>
2026-03-25 19:35 [PATCH v44b 4/4] hack deadlock detector Álvaro Herrera <[email protected]>
2026-03-25 19:35 [PATCH v44b 4/4] hack deadlock detector Álvaro Herrera <[email protected]>
2026-03-25 19:35 [PATCH v44b 4/4] hack deadlock detector Álvaro Herrera <[email protected]>
2026-03-25 19:35 [PATCH v44b 4/4] hack deadlock detector Álvaro Herrera <[email protected]>
2026-03-25 19:35 [PATCH v44b 4/4] hack deadlock detector Álvaro Herrera <[email protected]>
2026-03-25 19:35 [PATCH v44b 4/4] hack deadlock detector Álvaro Herrera <[email protected]>
2026-03-25 19:35 [PATCH v44b 4/4] hack deadlock detector Álvaro Herrera <[email protected]>
2026-03-25 19:35 [PATCH v44b 4/4] hack deadlock detector Álvaro Herrera <[email protected]>
2026-03-25 19:35 [PATCH v44b 4/4] hack deadlock detector Álvaro Herrera <[email protected]>
2026-03-25 19:35 [PATCH v44b 4/4] hack deadlock detector Álvaro Herrera <[email protected]>
2026-03-25 19:35 [PATCH v44b 4/4] hack deadlock detector Álvaro Herrera <[email protected]>
2026-03-25 19:35 [PATCH v44b 4/4] hack deadlock detector Álvaro Herrera <[email protected]>
2026-03-25 19:35 [PATCH v44b 4/4] hack deadlock detector Álvaro Herrera <[email protected]>
2026-03-25 19:35 [PATCH v44b 4/4] hack deadlock detector Álvaro Herrera <[email protected]>
2026-03-25 19:35 [PATCH v44b 4/4] hack deadlock detector Álvaro Herrera <[email protected]>
2026-03-25 19:35 [PATCH v44b 4/4] hack deadlock detector Álvaro Herrera <[email protected]>
2026-03-25 19:35 [PATCH v44b 4/4] hack deadlock detector Álvaro Herrera <[email protected]>
2026-03-25 19:35 [PATCH v44b 4/4] hack deadlock detector Álvaro Herrera <[email protected]>
2026-03-25 19:35 [PATCH v44b 4/4] hack deadlock detector Álvaro Herrera <[email protected]>
2026-03-25 19:35 [PATCH v44b 4/4] hack deadlock detector Álvaro Herrera <[email protected]>
2026-03-25 19:35 [PATCH v44b 4/4] hack deadlock detector Álvaro Herrera <[email protected]>
2026-03-25 19:35 [PATCH v44b 4/4] hack deadlock detector Álvaro Herrera <[email protected]>
2026-03-25 19:35 [PATCH v44b 4/4] hack deadlock detector Álvaro Herrera <[email protected]>
2026-03-25 19:35 [PATCH v44b 4/4] hack deadlock detector Álvaro Herrera <[email protected]>
2026-03-25 19:35 [PATCH v44b 4/4] hack deadlock detector Álvaro Herrera <[email protected]>
2026-03-25 19:35 [PATCH v44b 4/4] hack deadlock detector Álvaro Herrera <[email protected]>
2026-03-25 19:35 [PATCH v44b 4/4] hack deadlock detector Álvaro Herrera <[email protected]>
2026-03-25 19:35 [PATCH v44b 4/4] hack deadlock detector Álvaro Herrera <[email protected]>
2026-03-25 19:35 [PATCH v44b 4/4] hack deadlock detector Álvaro Herrera <[email protected]>
2026-03-25 19:35 [PATCH v44b 4/4] hack deadlock detector Álvaro Herrera <[email protected]>
2026-03-25 19:35 [PATCH v44b 4/4] hack deadlock detector Álvaro Herrera <[email protected]>
2026-03-25 19:35 [PATCH v44b 4/4] hack deadlock detector Álvaro Herrera <[email protected]>
2026-03-25 19:35 [PATCH v44b 4/4] hack deadlock detector Álvaro Herrera <[email protected]>
2026-03-25 19:35 [PATCH v44b 4/4] hack deadlock detector Álvaro Herrera <[email protected]>
2026-03-25 19:35 [PATCH v44b 4/4] hack deadlock detector Álvaro Herrera <[email protected]>
2026-03-25 19:35 [PATCH v44b 4/4] hack deadlock detector Álvaro Herrera <[email protected]>
2026-03-25 19:35 [PATCH v44b 4/4] hack deadlock detector Álvaro Herrera <[email protected]>
2026-03-25 19:35 [PATCH v44b 4/4] hack deadlock detector Álvaro Herrera <[email protected]>
2026-03-25 19:35 [PATCH v44b 4/4] hack deadlock detector Álvaro Herrera <[email protected]>
2026-03-25 19:35 [PATCH v44b 4/4] hack deadlock detector Álvaro Herrera <[email protected]>
2026-03-25 19:35 [PATCH v44b 4/4] hack deadlock detector Álvaro Herrera <[email protected]>
2026-03-25 19:35 [PATCH v44b 4/4] hack deadlock detector Álvaro Herrera <[email protected]>
2026-03-25 19:35 [PATCH v44b 4/4] hack deadlock detector Álvaro Herrera <[email protected]>
2026-03-25 19:35 [PATCH v44b 4/4] hack deadlock detector Álvaro Herrera <[email protected]>
2026-03-25 19:35 [PATCH v44b 4/4] hack deadlock detector Álvaro Herrera <[email protected]>
2026-03-25 19:35 [PATCH v44b 4/4] hack deadlock detector Álvaro Herrera <[email protected]>
2026-03-25 19:35 [PATCH v44b 4/4] hack deadlock detector Álvaro Herrera <[email protected]>
2026-03-25 19:35 [PATCH v44b 4/4] hack deadlock detector Álvaro Herrera <[email protected]>
2026-03-25 19:35 [PATCH v44b 4/4] hack deadlock detector Álvaro Herrera <[email protected]>
2026-03-25 19:35 [PATCH v44b 4/4] hack deadlock detector Álvaro Herrera <[email protected]>
2026-03-25 19:35 [PATCH v44b 4/4] hack deadlock detector Álvaro Herrera <[email protected]>
2026-03-25 19:35 [PATCH v44b 4/4] hack deadlock detector Álvaro Herrera <[email protected]>
2026-03-25 19:35 [PATCH v44b 4/4] hack deadlock detector Álvaro Herrera <[email protected]>
2026-03-25 19:35 [PATCH v44b 4/4] hack deadlock detector Álvaro Herrera <[email protected]>
2026-03-25 19:35 [PATCH v44b 4/4] hack deadlock detector Álvaro Herrera <[email protected]>
2026-03-25 19:35 [PATCH v44b 4/4] hack deadlock detector Álvaro Herrera <[email protected]>
2026-03-25 19:35 [PATCH v44b 4/4] hack deadlock detector Álvaro Herrera <[email protected]>
2026-03-25 19:35 [PATCH v44b 4/4] hack deadlock detector Álvaro Herrera <[email protected]>
2026-03-25 19:35 [PATCH v44b 4/4] hack deadlock detector Álvaro Herrera <[email protected]>
2026-03-25 19:35 [PATCH v44b 4/4] hack deadlock detector Álvaro Herrera <[email protected]>
2026-03-25 19:35 [PATCH v44b 4/4] hack deadlock detector Álvaro Herrera <[email protected]>
2026-03-25 19:35 [PATCH v44b 4/4] hack deadlock detector Álvaro Herrera <[email protected]>
2026-03-25 19:35 [PATCH v44b 4/4] hack deadlock detector Álvaro Herrera <[email protected]>
2026-03-25 19:35 [PATCH v44b 4/4] hack deadlock detector Álvaro Herrera <[email protected]>
2026-03-25 19:35 [PATCH v44b 4/4] hack deadlock detector Álvaro Herrera <[email protected]>
2026-03-25 19:35 [PATCH v44b 4/4] hack deadlock detector Álvaro Herrera <[email protected]>
2026-03-25 19:35 [PATCH v44b 4/4] hack deadlock detector Álvaro Herrera <[email protected]>
2026-03-25 19:35 [PATCH v44b 4/4] hack deadlock detector Álvaro Herrera <[email protected]>

This inbox is served by agora; see mirroring instructions
for how to clone and mirror all data and code used for this inbox