-
Notifications
You must be signed in to change notification settings - Fork 1
Fix deadlock when ddl gets triggered during autovacuum #126
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
harshil-goel
wants to merge
1
commit into
main
Choose a base branch
from
harshil/ddl-autovacuum-bug
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -10,9 +10,16 @@ | |
| * | ||
| * Never opens the target relation. The replaying transaction holds | ||
| * AccessExclusiveLock on it and standby lock replay is driven by the startup | ||
| * process, so relation_open would block against recovery. Catalogs only, and | ||
| * standby lock replay records AccessExclusiveLocks alone, so AccessShareLock | ||
| * on a catalog never conflicts with the DDL in flight. | ||
| * process, so relation_open would block against recovery. Catalogs only. | ||
| * | ||
| * AccessShareLock on a catalog does not conflict with the DDL in flight, but | ||
| * that is not the only lock replay can be holding: any unrelated source | ||
| * transaction that took AccessExclusiveLock on the catalog itself — VACUUM | ||
| * truncating a bloated pg_type is the one seen in practice — leaves the | ||
| * startup process holding it until that transaction's commit record is | ||
| * replayed, and the caller is withholding exactly that record. A pinned scan | ||
| * therefore takes the lock only if it is free, and reads without it otherwise; | ||
| * see `ws_overlay_scan`. | ||
| * | ||
| * An invalid top xid asks the same scan for the committed view, which is what | ||
| * the daemon captures at a catalog commit. Same projections, same assembly on | ||
|
|
@@ -36,6 +43,7 @@ | |
| #include "catalog/pg_namespace.h" | ||
| #include "catalog/pg_type.h" | ||
| #include "libpq/pqformat.h" | ||
| #include "storage/lmgr.h" | ||
| #include "storage/procarray.h" | ||
| #include "utils/fmgroids.h" | ||
| #include "utils/fmgrprotos.h" | ||
|
|
@@ -412,12 +420,78 @@ ws_scan_emit(Relation rel, const WsCatalogPlan *plan, Oid key, | |
| systable_endscan(scan); | ||
| } | ||
|
|
||
| /* qsort/bsearch comparator over Oid */ | ||
| static int | ||
| ws_oid_cmp(const void *a, const void *b) | ||
| { | ||
| Oid x = *(const Oid *) a; | ||
| Oid y = *(const Oid *) b; | ||
|
|
||
| if (x < y) | ||
| return -1; | ||
| return (x > y) ? 1 : 0; | ||
| } | ||
|
|
||
| /* | ||
| * One heap pass, emitting rows whose key attribute is in `sorted` (or every | ||
| * row when `nsorted` is 0). Used only when the catalog's lock was unavailable: | ||
| * `systable_beginscan` with an index would `index_open` it under | ||
| * AccessShareLock, which is the wait this path exists to avoid, so the index | ||
| * is skipped and the oid list is applied to the heap tuple instead. | ||
| * | ||
| * `scanned` counts rows that passed the oid filter, so it stays comparable to | ||
| * the indexed path. | ||
| */ | ||
| static void | ||
| ws_scan_emit_lockfree(Relation rel, const WsCatalogPlan *plan, | ||
| const Oid *sorted, int nsorted, TransactionId top, | ||
| StringInfo out, WsScanStats *stats) | ||
| { | ||
| SysScanDesc scan; | ||
| ScanKeyData skey[1]; | ||
| int nkeys = 0; | ||
| HeapTuple tup; | ||
| TupleDesc desc = RelationGetDescr(rel); | ||
| bool scoped = nsorted > 0; | ||
|
|
||
| if (plan->min_attnum != 0) | ||
| ScanKeyInit(&skey[nkeys++], Anum_pg_attribute_attnum, | ||
| BTGreaterEqualStrategyNumber, F_INT2GE, | ||
| Int16GetDatum(plan->min_attnum)); | ||
|
|
||
| scan = systable_beginscan(rel, InvalidOid, false, SnapshotAny, nkeys, skey); | ||
| while (HeapTupleIsValid(tup = systable_getnext(scan))) | ||
| { | ||
| if (scoped) | ||
| { | ||
| Datum key; | ||
| bool isnull; | ||
| Oid keyoid; | ||
|
|
||
| key = heap_getattr(tup, plan->keyattno, desc, &isnull); | ||
| if (isnull) | ||
| continue; | ||
| keyoid = DatumGetObjectId(key); | ||
|
Comment on lines
+467
to
+474
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. let's get the C style of mixing declaration & assignment |
||
| if (bsearch(&keyoid, sorted, (size_t) nsorted, sizeof(Oid), | ||
| ws_oid_cmp) == NULL) | ||
| continue; | ||
| } | ||
| stats->scanned++; | ||
| if (!ws_tuple_visible(tup->t_data, top, scoped, stats)) | ||
| continue; | ||
| stats->emitted++; | ||
| plan->emit(out, tup, desc); | ||
| } | ||
| systable_endscan(scan); | ||
| } | ||
|
|
||
| void | ||
| ws_overlay_scan(WsCatalog cat, TransactionId top, const Oid *oids, int noids, | ||
| StringInfo out, WsScanStats *stats) | ||
| WsScanLock lock, StringInfo out, WsScanStats *stats) | ||
| { | ||
| WsCatalogPlan plan; | ||
| bool rel_scoped; | ||
| bool locked; | ||
| Relation rel; | ||
|
|
||
| if (!ws_catalog_plan(cat, &plan)) | ||
|
|
@@ -429,9 +503,50 @@ ws_overlay_scan(WsCatalog cat, TransactionId top, const Oid *oids, int noids, | |
| * pg_namespace and pg_type have. The lock argument comes with the list, so | ||
| * losing the list loses the argument too */ | ||
| rel_scoped = AttributeNumberIsValid(plan.keyattno) && noids > 0; | ||
| rel = table_open(plan.relid, AccessShareLock); | ||
|
|
||
| if (rel_scoped) | ||
| /* | ||
| * See WsScanLock. A caller that named a replay position never waits here: | ||
| * the release for a lock replay is holding can be in the WAL that caller | ||
| * is withholding, so waiting deadlocks the daemon against its own shadow. | ||
| * Reading without the lock is licensed only once that position is known to | ||
| * be where replay is, which is what holds these pages still — together | ||
| * with the shadow being read-only, so no local backend can write either. | ||
| */ | ||
| if (lock == WS_SCAN_LOCK_WAIT) | ||
| { | ||
| LockRelationOid(plan.relid, AccessShareLock); | ||
| locked = true; | ||
| } | ||
| else | ||
| { | ||
| locked = ConditionalLockRelationOid(plan.relid, AccessShareLock); | ||
| if (!locked && lock == WS_SCAN_LOCK_NOWAIT) | ||
| ereport(ERROR, | ||
| (errcode(ERRCODE_LOCK_NOT_AVAILABLE), | ||
| errmsg("walshadow: %u is locked and replay is not at the position the scan named", | ||
| plan.relid), | ||
| errdetail("Reading without the lock needs that position to hold; waiting for it can deadlock against withheld WAL."))); | ||
| if (!locked) | ||
| elog(DEBUG1, | ||
| "walshadow: pinned scan of %u read without AccessShareLock", | ||
| plan.relid); | ||
| } | ||
| rel = table_open(plan.relid, NoLock); | ||
|
|
||
| if (!locked) | ||
| { | ||
| Oid *sorted = NULL; | ||
|
|
||
| if (rel_scoped) | ||
| { | ||
| sorted = palloc_array(Oid, noids); | ||
| memcpy(sorted, oids, sizeof(Oid) * (size_t) noids); | ||
| qsort(sorted, (size_t) noids, sizeof(Oid), ws_oid_cmp); | ||
| } | ||
| ws_scan_emit_lockfree(rel, &plan, sorted, rel_scoped ? noids : 0, | ||
| top, out, stats); | ||
| } | ||
| else if (rel_scoped) | ||
| { | ||
| int i; | ||
|
|
||
|
|
@@ -441,5 +556,7 @@ ws_overlay_scan(WsCatalog cat, TransactionId top, const Oid *oids, int noids, | |
| else | ||
| ws_scan_emit(rel, &plan, InvalidOid, top, false, out, stats); | ||
|
|
||
| table_close(rel, AccessShareLock); | ||
| table_close(rel, NoLock); | ||
| if (locked) | ||
| UnlockRelationOid(plan.relid, AccessShareLock); | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| # Test-only loadable module; see wstest.c. Built by the integration tests | ||
| # that need VACUUM's lock lifetime without waiting on autovacuum. | ||
| # | ||
| # make -C pgext/test | ||
| MODULES = wstest | ||
| PG_CONFIG ?= pg_config | ||
| PGXS := $(shell $(PG_CONFIG) --pgxs) | ||
| include $(PGXS) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,30 @@ | ||
| /* | ||
| * Test-only helper. Not part of the walshadow module: built and loaded only | ||
| * by the daemon's integration tests, never by a deployed shadow. | ||
| * | ||
| * Reproduces VACUUM's lock lifetime (vacuumlazy.c, lazy_truncate_heap): | ||
| * acquire AccessExclusiveLock on a relation, then release it source-side | ||
| * while the surrounding transaction stays open. The acquire emits | ||
| * XLOG_STANDBY_LOCK; the release emits nothing. A standby therefore keeps | ||
| * the replayed lock until the transaction's commit record arrives, which is | ||
| * the asymmetry tests need to drive deterministically. | ||
| */ | ||
| #include "postgres.h" | ||
|
|
||
| #include "fmgr.h" | ||
| #include "storage/lmgr.h" | ||
|
|
||
| PG_MODULE_MAGIC; | ||
|
|
||
| PG_FUNCTION_INFO_V1(ws_test_lock_unlock_relation); | ||
|
|
||
| Datum | ||
| ws_test_lock_unlock_relation(PG_FUNCTION_ARGS) | ||
| { | ||
| Oid relid = PG_GETARG_OID(0); | ||
|
|
||
| LockRelationOid(relid, AccessExclusiveLock); | ||
| UnlockRelationOid(relid, AccessExclusiveLock); | ||
|
|
||
| PG_RETURN_VOID(); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.