Skip to content

Commit 062d4b4

Browse files
gayyappanclaude
andcommitted
Invalidate cached tenant tracker pointers per hypertable
The relcache callback used to hash_destroy the whole resolved-tracker cache. Callbacks run from AcceptInvalidationMessages(), which fires inside resolve_tenant_tracker()'s own catalog scans, so the function dereferenced the table it had just created and crashed. It was also tied to the hypertable proxy, so every hypertable catalog change in the database made every backend re-resolve every tracker. Disabling granular refresh now invalidates the raw hypertable's own relcache entry. The callback receives that relid and only marks the matching cache entry stale; the next resolve treats a stale entry as a miss and overwrites it in place. Nothing is freed in a callback. The freeing backend marks its own entry at commit, since it may never execute its own message locally. The drain keeps using cached pointers regardless of the flag: every hypertable in the buffer was resolved under a lock this backend holds to transaction end. Add the debug_discard_caches reproducer and a cross-backend stale pointer permutation. Filter the tracker map by database in TEST 7 so concurrent test databases no longer change its count. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
1 parent 6f20eec commit 062d4b4

13 files changed

Lines changed: 371 additions & 81 deletions

src/cache_invalidate.c

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -84,31 +84,34 @@ cache_invalidate_relcache_callback(Datum arg, Oid relid)
8484
if (!OidIsValid(relid))
8585
{
8686
cache_invalidate_relcache_all();
87-
ts_cm_functions->tenant_tracker_cache_invalidate();
87+
ts_cm_functions->tenant_tracker_cache_invalidate(InvalidOid);
8888
}
8989
else if (ts_extension_is_proxy_table_relid(relid))
9090
{
9191
ts_extension_invalidate();
9292
cache_invalidate_relcache_all();
93-
ts_cm_functions->tenant_tracker_cache_invalidate();
93+
ts_cm_functions->tenant_tracker_cache_invalidate(InvalidOid);
9494
ts_cache_invalidate_set_proxy_tables(InvalidOid, InvalidOid);
9595
}
9696
else if (relid == hypertable_proxy_table_oid)
9797
{
9898
ts_hypertable_cache_invalidate_callback();
99-
100-
/*
101-
* Deleting a hypertable's granular refresh settings frees its tenant
102-
* tracker at commit, so drop any tracker pointer this backend cached.
103-
* Deliberately not hooked into cache_invalidate_relcache_all(): that
104-
* also runs on every aborted transaction, where nothing was freed.
105-
*/
106-
ts_cm_functions->tenant_tracker_cache_invalidate();
10799
}
108100
else if (relid == bgw_proxy_table_oid)
109101
{
110102
ts_bgw_job_cache_invalidate_callback();
111103
}
104+
else
105+
{
106+
/*
107+
* Disabling granular refresh on a hypertable frees its tenant tracker
108+
* at commit and invalidates the hypertable's own relcache entry, so a
109+
* backend that cached a pointer to that tracker can drop just that one.
110+
* Not routed through cache_invalidate_relcache_all(): an aborted
111+
* transaction frees nothing.
112+
*/
113+
ts_cm_functions->tenant_tracker_cache_invalidate(relid);
114+
}
112115
}
113116

114117
TS_FUNCTION_INFO_V1(ts_timescaledb_invalidate_cache);

src/cross_module_fn.c

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -287,7 +287,7 @@ preprocess_query_tsl_default_fn_community(Query *parse, int *cursor_opts)
287287
}
288288

289289
static void
290-
tenant_tracker_cache_invalidate_default(void)
290+
tenant_tracker_cache_invalidate_default(Oid relid)
291291
{
292292
/* No op in community licensed code */
293293
}

src/cross_module_fn.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ typedef struct CrossModuleFunctions
106106
void (*continuous_agg_dml_invalidate)(int32 hypertable_id, Relation chunk_rel,
107107
HeapTuple chunk_tuple, HeapTuple chunk_newtuple,
108108
bool update);
109-
void (*tenant_tracker_cache_invalidate)(void);
109+
void (*tenant_tracker_cache_invalidate)(Oid relid);
110110
void (*continuous_agg_update_options)(ContinuousAgg *cagg,
111111
WithClauseResult *with_clause_options);
112112
void (*continuous_agg_add_column)(ContinuousAgg *cagg, AlterTableStmt *stmt);

src/ts_catalog/catalog.c

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -800,7 +800,6 @@ ts_catalog_invalidate_cache(Oid catalog_relid, CmdType operation)
800800
case DIMENSION:
801801
case CONTINUOUS_AGG:
802802
case CHUNK_COLUMN_STATS:
803-
case HYPERTABLE_CAGG_SETTINGS:
804803
relid = ts_catalog_get_cache_proxy_id(catalog, CACHE_TYPE_HYPERTABLE);
805804
CacheInvalidateRelcacheByRelid(relid);
806805
break;

tsl/src/continuous_aggs/insert.c

Lines changed: 87 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,8 @@ typedef struct TenantLocalKey
8888

8989
typedef struct TenantLocalEntry
9090
{
91-
TenantLocalKey key; /* must be first: dynahash key */
91+
TenantLocalKey key; /* must be first: dynahash key */
92+
Oid hypertable_relid; /* key into the resolved-tracker cache at drain */
9293
int64 min_ts;
9394
int64 max_ts;
9495
} TenantLocalEntry;
@@ -115,14 +116,21 @@ typedef struct ContinuousAggsCacheHyperInvalThresholdEntry
115116
* the DML path and avoids any additional lookups.
116117
* tracking == NULL means DISABLED, tracking = non NULL means we have a valid
117118
* entry in shared mem.
118-
* Both outcomes are stable for the backend's life: a tracker is
119-
* never moved or freed and its mapping is pinned, and a DISABLED marker persists
120-
* until restart . So entries never need invalidation. MyDatabaseId is fixed
121-
* per backend, so hypertable_id alone is a sufficient key.
119+
*
120+
* A tracker is freed only when granular refresh is disabled on its hypertable,
121+
* which invalidates the hypertable's relcache entry (see granular_refresh_disable).
122+
* The cache is keyed by the hypertable's relid so the relcache callback can find
123+
* the one entry the message is about and mark it stale; the next resolve treats
124+
* a stale entry as a miss and overwrites it in place. Entries are never removed
125+
* and the table is never destroyed, so the callback can run at any
126+
* AcceptInvalidationMessages() without pulling memory out from under a caller.
127+
* MyDatabaseId is fixed per backend, so the relid alone is a sufficient key.
122128
*/
123129
typedef struct TenantTrackerCacheEntry
124130
{
125-
int32 hypertable_id;
131+
Oid main_table_relid; /* must be first: dynahash key; what relcache messages carry */
132+
int32 hypertable_id; /* what the shared tracker map is keyed by */
133+
bool stale; /* set by the relcache callback; re-resolve on next use */
126134
TenantTracking *tracking;
127135
} TenantTrackerCacheEntry;
128136

@@ -160,7 +168,7 @@ static inline ContinuousAggsCacheInvalEntry *get_cache_inval_entry(int32 hyperta
160168
static void cache_inval_cleanup(void);
161169
static void cache_inval_htab_write(List *hypertable_seqnums);
162170
static HTAB *get_tenant_local_htab(void);
163-
static TenantTracking *resolve_tenant_tracker(int32 hypertable_id);
171+
static TenantTracking *resolve_tenant_tracker(int32 hypertable_id, Oid main_table_relid);
164172
static List *tenant_local_htab_write(void);
165173
static void continuous_agg_xact_invalidation_callback(XactEvent event, void *arg);
166174
static ScanTupleResult invalidation_tuple_found(TupleInfo *ti, void *min);
@@ -365,10 +373,10 @@ get_tenant_local_htab(void)
365373
*
366374
* Returns the tracker, or NULL when tracking is disabled for this hypertable
367375
* (negative-cache marker, loader absent, or a contained OOM). Both outcomes are
368-
* cached and stable for the backend's life.
376+
* cached until a relcache invalidation for the hypertable marks the entry stale.
369377
*/
370378
static TenantTracking *
371-
resolve_tenant_tracker(int32 hypertable_id)
379+
resolve_tenant_tracker(int32 hypertable_id, Oid main_table_relid)
372380
{
373381
TenantTrackerCacheEntry *ce;
374382
TenantTracking *volatile tracking = NULL;
@@ -380,7 +388,7 @@ resolve_tenant_tracker(int32 hypertable_id)
380388
HASHCTL ctl;
381389

382390
memset(&ctl, 0, sizeof(ctl));
383-
ctl.keysize = sizeof(int32);
391+
ctl.keysize = sizeof(Oid);
384392
ctl.entrysize = sizeof(TenantTrackerCacheEntry);
385393
ctl.hcxt = TopMemoryContext; /* backend lifetime: entries never expire */
386394
tenant_tracker_resolved_htab = hash_create("TS Tenant Tracker Resolved",
@@ -389,14 +397,15 @@ resolve_tenant_tracker(int32 hypertable_id)
389397
HASH_ELEM | HASH_BLOBS | HASH_CONTEXT);
390398
}
391399

392-
ce = hash_search(tenant_tracker_resolved_htab, &hypertable_id, HASH_FIND, NULL);
393-
if (ce != NULL)
400+
ce = hash_search(tenant_tracker_resolved_htab, &main_table_relid, HASH_FIND, NULL);
401+
if (ce != NULL && !ce->stale)
394402
{
395403
return ce->tracking; /* cached FOUND (ptr) or DISABLED (NULL) */
396404
}
397405

398406
/*
399-
* First time this backend touches this hypertable. Park here so the
407+
* First time this backend touches this hypertable, or the cached entry was
408+
* invalidated. Park here so the
400409
* first-touch race isolation test can line up two backends before the
401410
* get_or_attach below (which serializes on the dshash partition lock).
402411
*/
@@ -468,39 +477,60 @@ resolve_tenant_tracker(int32 hypertable_id)
468477
}
469478
PG_END_TRY();
470479

471-
ce = hash_search(tenant_tracker_resolved_htab, &hypertable_id, HASH_ENTER, NULL);
480+
/* Re-find: the catalog scans above can run relcache callbacks, but those only
481+
* flip the stale flag; the table itself is never freed under us. */
482+
ce = hash_search(tenant_tracker_resolved_htab, &main_table_relid, HASH_ENTER, NULL);
483+
ce->hypertable_id = hypertable_id;
472484
ce->tracking = tracking;
485+
ce->stale = false;
473486
return tracking;
474487
}
475488

476489
/*
477-
* Drop this backend's resolved-tracker cache.
490+
* Relcache invalidation callback: mark this backend's cached tracker entry for
491+
* the relation stale (all entries for InvalidOid, i.e. a sinval reset).
478492
*
479-
* Called from the hypertable-proxy relcache invalidation callback, which is how
480-
* a backend learns that some hypertable's granular refresh settings changed --
481-
* and therefore that its tenant tracker may have been freed at that
482-
* transaction's commit. The cached pointers are raw DSA addresses with no way
483-
* to tell a live tracker from a freed one, so the whole table goes; a re-resolve
484-
* is one dshash lookup.
493+
* Disabling granular refresh frees the hypertable's tracker at commit and
494+
* invalidates the hypertable's relcache entry, so this is how a backend learns
495+
* that its cached pointer may now dangle. The relation's ordinary relcache
496+
* events (ANALYZE, ALTER TABLE) land here too; they just cost one dshash
497+
* lookup on the next resolve.
485498
*
486-
* The proxy carries no hypertable id, so this cannot be selective. That also
487-
* clears the cached DISABLED (NULL) entries, which is harmless: the
488-
* negative-cache marker lives in the shared map entry, so a re-resolve for a
489-
* hypertable whose allocation once failed still returns NULL without retrying
490-
* the allocation.
499+
* Also called directly by the commit callback that frees a tracker: the freeing
500+
* backend's own sinval messages are only executed locally at
501+
* CommandCounterIncrement, so it cannot count on receiving the message it sent.
491502
*
492-
* Safe to run mid-transaction. A backend that has already buffered tenants
493-
* this transaction will find no entry in tenant_local_htab_write() and record
494-
* seqnum 0 for that hypertable, which makes the refresh fall back to the full
495-
* invalidation log -- an over-refresh, never a missed one.
503+
* Runs from AcceptInvalidationMessages(), possibly in the middle of
504+
* resolve_tenant_tracker(): it must not allocate, free or throw. Setting a
505+
* flag is all it does. The drain (tenant_local_htab_write) ignores the flag on
506+
* purpose -- see the comment there.
496507
*/
497508
void
498-
continuous_agg_tenant_tracker_cache_invalidate(void)
509+
continuous_agg_tenant_tracker_cache_invalidate(Oid relid)
499510
{
500-
if (tenant_tracker_resolved_htab != NULL)
511+
TenantTrackerCacheEntry *ce;
512+
513+
if (tenant_tracker_resolved_htab == NULL)
501514
{
502-
hash_destroy(tenant_tracker_resolved_htab);
503-
tenant_tracker_resolved_htab = NULL;
515+
return;
516+
}
517+
518+
if (!OidIsValid(relid))
519+
{
520+
HASH_SEQ_STATUS status;
521+
522+
hash_seq_init(&status, tenant_tracker_resolved_htab);
523+
while ((ce = hash_seq_search(&status)) != NULL)
524+
{
525+
ce->stale = true;
526+
}
527+
return;
528+
}
529+
530+
ce = hash_search(tenant_tracker_resolved_htab, &relid, HASH_FIND, NULL);
531+
if (ce != NULL)
532+
{
533+
ce->stale = true;
504534
}
505535
}
506536

@@ -582,11 +612,13 @@ record_tenant_invalidation_values(const ContinuousAggsCacheInvalEntry *cache_ent
582612
// TODO: try to see if there's a way to avoid allocate and free memory for every key.
583613
pfree(key);
584614

585-
resolve_tenant_tracker(cache_entry->hypertable_id);
615+
resolve_tenant_tracker(cache_entry->hypertable_id,
616+
cache_entry->hypertable_open_dimension.main_table_relid);
586617

587618
entry = (TenantLocalEntry *) hash_search(get_tenant_local_htab(), &lookup, HASH_ENTER, &found);
588619
if (!found)
589620
{
621+
entry->hypertable_relid = cache_entry->hypertable_open_dimension.main_table_relid;
590622
entry->min_ts = timeval;
591623
entry->max_ts = timeval;
592624
}
@@ -695,26 +727,33 @@ tenant_local_htab_write(void)
695727
HASH_SEQ_STATUS hash_seq;
696728
TenantLocalEntry *entry;
697729
List *hypertable_ids = NIL;
730+
List *hypertable_relids = NIL;
698731
List *hypertable_seqnums = NIL;
699-
ListCell *lc;
732+
ListCell *lc, *lc_relid;
700733

701734
if (tenant_local_htab == NULL)
702735
{
703736
return NIL;
704737
}
705738

706-
/* Distinct hypertables present in the buffer (usually just one). */
739+
/* Distinct hypertables present in the buffer (usually just one), with the
740+
* relid each was resolved under -- that is the resolved cache's key. */
707741
hash_seq_init(&hash_seq, tenant_local_htab);
708742
while ((entry = hash_seq_search(&hash_seq)) != NULL)
709743
{
710-
hypertable_ids = list_append_unique_int(hypertable_ids, entry->key.hypertable_id);
744+
if (!list_member_int(hypertable_ids, entry->key.hypertable_id))
745+
{
746+
hypertable_ids = lappend_int(hypertable_ids, entry->key.hypertable_id);
747+
hypertable_relids = lappend_oid(hypertable_relids, entry->hypertable_relid);
748+
}
711749
}
712750

713751
/* Drain each hypertable's tenants into its own tracker (one generation pin
714752
* per hypertable). */
715-
foreach (lc, hypertable_ids)
753+
forboth (lc, hypertable_ids, lc_relid, hypertable_relids)
716754
{
717755
int32 hypertable_id = lfirst_int(lc);
756+
Oid hypertable_relid = lfirst_oid(lc_relid);
718757
TenantTracking *tracking;
719758
TenantGeneration *generation;
720759
int32 seqnum = 0;
@@ -743,9 +782,17 @@ tenant_local_htab_write(void)
743782
* relying on this staying throw-free. A NULL entry means tracking is
744783
* disabled for this hypertable; a missing entry means nothing was buffered
745784
* for it (shouldn't happen), both -> skip (untracked, seqnum 0).
785+
*
786+
* The stale flag is deliberately ignored here. Every hypertable in the
787+
* buffer was resolved in this transaction while this backend held a lock
788+
* on it, and that lock is held to transaction end, so the free (which
789+
* needs AccessExclusiveLock) cannot have happened. A stale mark seen
790+
* here comes from a non-conflicting relcache event on the same relation
791+
* (e.g. ANALYZE); honoring it would only force a needless seqnum-0
792+
* fallback, and re-resolving would put dshash locks on this path.
746793
*/
747794
ce = (tenant_tracker_resolved_htab != NULL) ?
748-
hash_search(tenant_tracker_resolved_htab, &hypertable_id, HASH_FIND, NULL) :
795+
hash_search(tenant_tracker_resolved_htab, &hypertable_relid, HASH_FIND, NULL) :
749796
NULL;
750797
tracking = (ce != NULL) ? ce->tracking : NULL;
751798

tsl/src/continuous_aggs/insert.h

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
#include <postgres.h>
99
#include <executor/tuptable.h>
10+
#include <utils/rel.h>
1011

1112
extern void _continuous_aggs_cache_inval_init(void);
1213
extern void _continuous_aggs_cache_inval_fini(void);
@@ -17,4 +18,4 @@ extern void continuous_agg_dml_invalidate(int32 hypertable_id, Relation chunk_re
1718
bool update);
1819
extern void continuous_agg_record_tenant_from_slot(int32 hypertable_id, Oid chunk_relid,
1920
TupleTableSlot *slot);
20-
extern void continuous_agg_tenant_tracker_cache_invalidate(void);
21+
extern void continuous_agg_tenant_tracker_cache_invalidate(Oid relid);

0 commit comments

Comments
 (0)