@@ -88,7 +88,8 @@ typedef struct TenantLocalKey
8888
8989typedef 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 */
123129typedef 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
160168static void cache_inval_cleanup (void );
161169static void cache_inval_htab_write (List * hypertable_seqnums );
162170static 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 );
164172static List * tenant_local_htab_write (void );
165173static void continuous_agg_xact_invalidation_callback (XactEvent event , void * arg );
166174static 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 */
370378static 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 */
497508void
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
0 commit comments