Skip to content

Commit 6c84541

Browse files
committed
Relax lock during CAgg invalidation log processing
When processing the continuous aggregate invalidation log, we currently take a ShareUpdateExclusiveLock on the materialized hypertable to prevent logs being cut by concurrent refreshes on the same continuous aggregate. However, this can also block other concurrent operations. We can instead take a RowExclusiveLock on the catalog table entry for the continuous aggregate. This has the same effect as earlier - serializing cagg log processing, without the need for the heavy lock that was taken previously.
1 parent c482ad3 commit 6c84541

10 files changed

Lines changed: 154 additions & 163 deletions

File tree

.unreleased/pr_9701

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Implements: #9701 Relax lock during CAgg invalidation log processing

src/hypertable.c

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -432,7 +432,7 @@ hypertable_scan_limit_internal(ScanKeyData *scankey, int num_scankeys, int index
432432
/* update the tuple at this tid. The assumption is that we already hold a
433433
* tuple exclusive lock and no other transaction can modify this tuple
434434
* The sequence of operations for any update is:
435-
* lock the tuple using lock_hypertable_tuple.
435+
* lock the tuple using ts_lock_hypertable_tuple.
436436
* then update the required fields
437437
* call hypertable_update_catalog_tuple to complete the update.
438438
* This ensures correct tuple locking and tuple updates in the presence of
@@ -457,7 +457,7 @@ hypertable_update_catalog_tuple(ItemPointer tid, FormData_hypertable *update)
457457
}
458458

459459
static bool
460-
lock_hypertable_tuple(int32 htid, ItemPointer tid, FormData_hypertable *form)
460+
ts_lock_hypertable_tuple(int32 htid, ItemPointer tid, FormData_hypertable *form)
461461
{
462462
bool success = false;
463463
ScanTupLock scantuplock = {
@@ -841,7 +841,7 @@ ts_hypertable_set_name(Hypertable *ht, const char *newname)
841841
FormData_hypertable form;
842842
ItemPointerData tid;
843843
/* lock the tuple entry in the catalog table */
844-
bool found = lock_hypertable_tuple(ht->fd.id, &tid, &form);
844+
bool found = ts_lock_hypertable_tuple(ht->fd.id, &tid, &form);
845845
Ensure(found, "hypertable id %d not found", ht->fd.id);
846846

847847
namestrcpy(&form.table_name, newname);
@@ -855,7 +855,7 @@ ts_hypertable_set_schema(Hypertable *ht, const char *newname)
855855
FormData_hypertable form;
856856
ItemPointerData tid;
857857
/* lock the tuple entry in the catalog table */
858-
bool found = lock_hypertable_tuple(ht->fd.id, &tid, &form);
858+
bool found = ts_lock_hypertable_tuple(ht->fd.id, &tid, &form);
859859
Ensure(found, "hypertable id %d not found", ht->fd.id);
860860

861861
namestrcpy(&form.schema_name, newname);
@@ -869,7 +869,7 @@ ts_hypertable_set_num_dimensions(Hypertable *ht, int16 num_dimensions)
869869
FormData_hypertable form;
870870
ItemPointerData tid;
871871
/* lock the tuple entry in the catalog table */
872-
bool found = lock_hypertable_tuple(ht->fd.id, &tid, &form);
872+
bool found = ts_lock_hypertable_tuple(ht->fd.id, &tid, &form);
873873
Ensure(found, "hypertable id %d not found", ht->fd.id);
874874

875875
Assert(num_dimensions > 0);
@@ -2219,7 +2219,7 @@ ts_hypertable_set_compressed(Hypertable *ht, int32 compressed_hypertable_id)
22192219
FormData_hypertable form;
22202220
ItemPointerData tid;
22212221
/* lock the tuple entry in the catalog table */
2222-
bool found = lock_hypertable_tuple(ht->fd.id, &tid, &form);
2222+
bool found = ts_lock_hypertable_tuple(ht->fd.id, &tid, &form);
22232223
Ensure(found, "hypertable id %d not found", ht->fd.id);
22242224

22252225
Assert(!TS_HYPERTABLE_IS_INTERNAL_COMPRESSION_TABLE(ht));
@@ -2238,7 +2238,7 @@ ts_hypertable_unset_compressed(Hypertable *ht)
22382238
FormData_hypertable form;
22392239
ItemPointerData tid;
22402240
/* lock the tuple entry in the catalog table */
2241-
bool found = lock_hypertable_tuple(ht->fd.id, &tid, &form);
2241+
bool found = ts_lock_hypertable_tuple(ht->fd.id, &tid, &form);
22422242
Ensure(found, "hypertable id %d not found", ht->fd.id);
22432243

22442244
Assert(!TS_HYPERTABLE_IS_INTERNAL_COMPRESSION_TABLE(ht));
@@ -2453,7 +2453,7 @@ ts_hypertable_update_status_osm(Hypertable *ht)
24532453
FormData_hypertable form;
24542454
ItemPointerData tid;
24552455
/* lock the tuple entry in the catalog table */
2456-
bool found = lock_hypertable_tuple(ht->fd.id, &tid, &form);
2456+
bool found = ts_lock_hypertable_tuple(ht->fd.id, &tid, &form);
24572457
Ensure(found, "hypertable id %d not found", ht->fd.id);
24582458

24592459
if (form.status != ht->fd.status)

src/hypertable.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,7 @@ extern bool ts_hypertable_has_tablespace(const Hypertable *ht, Oid tspc_oid);
127127
extern Tablespace *ts_hypertable_select_tablespace(const Hypertable *ht, const Chunk *chunk);
128128
extern const char *ts_hypertable_select_tablespace_name(const Hypertable *ht, const Chunk *chunk);
129129
extern Tablespace *ts_hypertable_get_tablespace_at_offset_from(int32 hypertable_id,
130+
130131
Oid tablespace_oid, int16 offset);
131132
extern TSDLLEXPORT bool ts_hypertable_has_chunks(Oid table_relid, LOCKMODE lockmode);
132133
extern void ts_hypertables_rename_schema_name(const char *old_name, const char *new_name);

src/ts_catalog/continuous_agg.c

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,56 @@ init_materialization_invalidation_log_scan_by_materialization_id(ScanIterator *i
138138
Int32GetDatum(materialization_id));
139139
}
140140

141+
TSDLLEXPORT bool
142+
ts_lock_continuous_agg_tuple(int32 mat_hypertable_id)
143+
{
144+
bool success = false;
145+
ScanTupLock scantuplock = {
146+
.waitpolicy = LockWaitBlock,
147+
.lockmode = LockTupleExclusive,
148+
};
149+
ScanIterator iterator =
150+
ts_scan_iterator_create(CONTINUOUS_AGG, RowShareLock, CurrentMemoryContext);
151+
init_scan_by_mat_hypertable_id(&iterator, mat_hypertable_id);
152+
iterator.ctx.tuplock = &scantuplock;
153+
iterator.ctx.flags = SCANNER_F_KEEPLOCK;
154+
155+
/* see table_tuple_lock for details about flags that are set in TupleExclusive mode */
156+
scantuplock.lockflags = TUPLE_LOCK_FLAG_LOCK_UPDATE_IN_PROGRESS;
157+
if (!IsolationUsesXactSnapshot())
158+
{
159+
/* in read committed mode, we follow all updates to this tuple */
160+
scantuplock.lockflags |= TUPLE_LOCK_FLAG_FIND_LAST_VERSION;
161+
}
162+
163+
ts_scanner_foreach(&iterator)
164+
{
165+
TupleInfo *ti = ts_scan_iterator_tuple_info(&iterator);
166+
if (ti->lockresult != TM_Ok)
167+
{
168+
if (IsolationUsesXactSnapshot())
169+
{
170+
ereport(ERROR,
171+
(errcode(ERRCODE_T_R_SERIALIZATION_FAILURE),
172+
errmsg("could not serialize access due to concurrent update")));
173+
}
174+
else
175+
{
176+
ereport(ERROR,
177+
(errcode(ERRCODE_INTERNAL_ERROR),
178+
errmsg("unable to lock continuous aggregate catalog tuple, lock result "
179+
"is %d for mat_hypertable_id (%d)",
180+
ti->lockresult,
181+
mat_hypertable_id)));
182+
}
183+
}
184+
success = true;
185+
break;
186+
}
187+
ts_scan_iterator_close(&iterator);
188+
return success;
189+
}
190+
141191
static int32
142192
number_of_continuous_aggs_attached(int32 raw_hypertable_id)
143193
{

src/ts_catalog/continuous_agg.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,8 @@ typedef struct ContinuousAggPolicyOffset
145145
const char *name;
146146
} ContinuousAggPolicyOffset;
147147

148+
extern TSDLLEXPORT bool ts_lock_continuous_agg_tuple(int32 mat_hypertable_id);
149+
148150
extern TSDLLEXPORT Oid ts_cagg_permissions_check(Oid cagg_oid, Oid userid);
149151

150152
extern TSDLLEXPORT ContinuousAggInfo ts_continuous_agg_get_all_caggs_info(int32 raw_hypertable_id);

tsl/src/continuous_aggs/refresh.c

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -703,15 +703,13 @@ process_cagg_invalidations_and_refresh(const ContinuousAgg *cagg,
703703
const ContinuousAggRefreshContext context,
704704
bool bucketing_refresh_window, bool force)
705705
{
706-
Oid hyper_relid = ts_hypertable_id_to_relid(cagg->data.mat_hypertable_id, false);
707-
708-
/* Lock the continuous aggregate's materialized hypertable to protect against
709-
* concurrent invalidation log processing.
710-
*
711-
* This is supposed to be a short transaction and in the future we can consider
712-
* relaxing this lock.
706+
/* Lock the continuous aggregate's catalog table entry to protect against concurrent refreshes
707+
* on the same cagg processing the cagg invalidation logs for that CAgg.
713708
*/
714-
LockRelationOid(hyper_relid, ShareUpdateExclusiveLock);
709+
bool found = ts_lock_continuous_agg_tuple(cagg->data.mat_hypertable_id);
710+
Ensure(found,
711+
"continuous aggregate with mat_hypertable_id %d not found",
712+
cagg->data.mat_hypertable_id);
715713
invalidation_process_cagg_log(cagg, refresh_window);
716714

717715
DEBUG_ERROR_INJECTION("cagg_refresh_fail_in_txn2");

tsl/test/isolation/expected/cagg_hierarchical_concurrent_refresh.out

Lines changed: 33 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -463,7 +463,7 @@ Fri Jan 02 00:00:00 2026 UTC|32.1666666666667|32.1666666666667| 288|
463463
Sat Jan 03 00:00:00 2026 UTC| 31.5| 31.5| 96| 96|t |t
464464

465465

466-
starting permutation: L1_refresh_full chk_1d_consistency WP_before_enable chk_hyper_invals L2_refresh_jan1 insert_ht L1_refresh_full lock_L2_source L2b_refresh_jan1_2 WP_before_release unlock chk_cagg_1d chk_1d_consistency
466+
starting permutation: L1_refresh_full chk_1d_consistency WP_before_enable chk_hyper_invals lock_L2_source L2_refresh_jan1 insert_ht L1_refresh_full L2b_refresh_jan1_2 WP_before_release unlock chk_cagg_1d chk_1d_consistency
467467
step L1_refresh_full:
468468
CALL refresh_continuous_aggregate('cagg_6h', '2026-01-01', '2026-01-04');
469469

@@ -511,6 +511,16 @@ source |lowest |greatest
511511
-------+----------------------------+----------------------------
512512
cagg_6h|Thu Jan 01 00:00:00 2026 UTC|Sat Jan 03 18:00:00 2026 UTC
513513

514+
step lock_L2_source:
515+
BEGIN;
516+
DO $$
517+
BEGIN
518+
PERFORM 1 FROM _timescaledb_catalog.continuous_agg
519+
WHERE user_view_name = 'cagg_1d'
520+
FOR UPDATE;
521+
END;
522+
$$;
523+
514524
step L2_refresh_jan1:
515525
CALL refresh_continuous_aggregate('cagg_1d', '2026-01-01', '2026-01-02');
516526
<waiting ...>
@@ -523,20 +533,6 @@ step insert_ht:
523533
step L1_refresh_full:
524534
CALL refresh_continuous_aggregate('cagg_6h', '2026-01-01', '2026-01-04');
525535
<waiting ...>
526-
step lock_L2_source:
527-
BEGIN;
528-
DO $$
529-
DECLARE
530-
mat_ht text;
531-
BEGIN
532-
SELECT format('%I.%I', h.schema_name, h.table_name) INTO mat_ht
533-
FROM _timescaledb_catalog.continuous_agg ca
534-
JOIN _timescaledb_catalog.hypertable h ON h.id = ca.mat_hypertable_id
535-
WHERE ca.user_view_name = 'cagg_1d';
536-
EXECUTE format('LOCK TABLE %s IN ACCESS EXCLUSIVE MODE', mat_ht);
537-
END;
538-
$$;
539-
<waiting ...>
540536
step L2b_refresh_jan1_2:
541537
CALL refresh_continuous_aggregate('cagg_1d', '2026-01-01', '2026-01-03');
542538

@@ -549,7 +545,6 @@ debug_waitpoint_release
549545

550546

551547
step L1_refresh_full: <... completed>
552-
step lock_L2_source: <... completed>
553548
step unlock:
554549
ROLLBACK;
555550

@@ -590,7 +585,7 @@ Fri Jan 02 00:00:00 2026 UTC| 31.5|32.1666666666667| 96|
590585
Sat Jan 03 00:00:00 2026 UTC| 31.5| 40.5| 96| 288|f |f
591586

592587

593-
starting permutation: L1_refresh_full chk_1d_consistency WP_before_enable chk_hyper_invals L2b_refresh_jan1_2 insert_ht L1_refresh_full lock_L2_source L2_refresh_jan1 WP_before_release unlock chk_cagg_1d chk_1d_consistency
588+
starting permutation: L1_refresh_full chk_1d_consistency WP_before_enable chk_hyper_invals lock_L2_source L2b_refresh_jan1_2 insert_ht L1_refresh_full L2_refresh_jan1 WP_before_release unlock chk_cagg_1d chk_1d_consistency
594589
step L1_refresh_full:
595590
CALL refresh_continuous_aggregate('cagg_6h', '2026-01-01', '2026-01-04');
596591

@@ -638,6 +633,16 @@ source |lowest |greatest
638633
-------+----------------------------+----------------------------
639634
cagg_6h|Thu Jan 01 00:00:00 2026 UTC|Sat Jan 03 18:00:00 2026 UTC
640635

636+
step lock_L2_source:
637+
BEGIN;
638+
DO $$
639+
BEGIN
640+
PERFORM 1 FROM _timescaledb_catalog.continuous_agg
641+
WHERE user_view_name = 'cagg_1d'
642+
FOR UPDATE;
643+
END;
644+
$$;
645+
641646
step L2b_refresh_jan1_2:
642647
CALL refresh_continuous_aggregate('cagg_1d', '2026-01-01', '2026-01-03');
643648
<waiting ...>
@@ -650,20 +655,6 @@ step insert_ht:
650655
step L1_refresh_full:
651656
CALL refresh_continuous_aggregate('cagg_6h', '2026-01-01', '2026-01-04');
652657
<waiting ...>
653-
step lock_L2_source:
654-
BEGIN;
655-
DO $$
656-
DECLARE
657-
mat_ht text;
658-
BEGIN
659-
SELECT format('%I.%I', h.schema_name, h.table_name) INTO mat_ht
660-
FROM _timescaledb_catalog.continuous_agg ca
661-
JOIN _timescaledb_catalog.hypertable h ON h.id = ca.mat_hypertable_id
662-
WHERE ca.user_view_name = 'cagg_1d';
663-
EXECUTE format('LOCK TABLE %s IN ACCESS EXCLUSIVE MODE', mat_ht);
664-
END;
665-
$$;
666-
<waiting ...>
667658
step L2_refresh_jan1:
668659
CALL refresh_continuous_aggregate('cagg_1d', '2026-01-01', '2026-01-02');
669660

@@ -676,7 +667,6 @@ debug_waitpoint_release
676667

677668

678669
step L1_refresh_full: <... completed>
679-
step lock_L2_source: <... completed>
680670
step unlock:
681671
ROLLBACK;
682672

@@ -872,7 +862,7 @@ Fri Jan 02 00:00:00 2026 UTC| 31.5| 31.5| 96|
872862
Sat Jan 03 00:00:00 2026 UTC| 40.5| 40.5| 288| 288|t |t
873863

874864

875-
starting permutation: L1_refresh_full chk_1d_consistency WP_before_enable chk_hyper_invals L2_refresh_jan1 insert_ht L1_refresh_full lock_L2_source L2b_refresh_jan3 WP_before_release unlock chk_cagg_1d chk_1d_consistency
865+
starting permutation: L1_refresh_full chk_1d_consistency WP_before_enable chk_hyper_invals lock_L2_source L2_refresh_jan1 insert_ht L1_refresh_full L2b_refresh_jan3 WP_before_release unlock chk_cagg_1d chk_1d_consistency
876866
step L1_refresh_full:
877867
CALL refresh_continuous_aggregate('cagg_6h', '2026-01-01', '2026-01-04');
878868

@@ -920,6 +910,16 @@ source |lowest |greatest
920910
-------+----------------------------+----------------------------
921911
cagg_6h|Thu Jan 01 00:00:00 2026 UTC|Sat Jan 03 18:00:00 2026 UTC
922912

913+
step lock_L2_source:
914+
BEGIN;
915+
DO $$
916+
BEGIN
917+
PERFORM 1 FROM _timescaledb_catalog.continuous_agg
918+
WHERE user_view_name = 'cagg_1d'
919+
FOR UPDATE;
920+
END;
921+
$$;
922+
923923
step L2_refresh_jan1:
924924
CALL refresh_continuous_aggregate('cagg_1d', '2026-01-01', '2026-01-02');
925925
<waiting ...>
@@ -932,20 +932,6 @@ step insert_ht:
932932
step L1_refresh_full:
933933
CALL refresh_continuous_aggregate('cagg_6h', '2026-01-01', '2026-01-04');
934934
<waiting ...>
935-
step lock_L2_source:
936-
BEGIN;
937-
DO $$
938-
DECLARE
939-
mat_ht text;
940-
BEGIN
941-
SELECT format('%I.%I', h.schema_name, h.table_name) INTO mat_ht
942-
FROM _timescaledb_catalog.continuous_agg ca
943-
JOIN _timescaledb_catalog.hypertable h ON h.id = ca.mat_hypertable_id
944-
WHERE ca.user_view_name = 'cagg_1d';
945-
EXECUTE format('LOCK TABLE %s IN ACCESS EXCLUSIVE MODE', mat_ht);
946-
END;
947-
$$;
948-
<waiting ...>
949935
step L2b_refresh_jan3:
950936
CALL refresh_continuous_aggregate('cagg_1d', '2026-01-03', '2026-01-04');
951937
<waiting ...>
@@ -957,7 +943,6 @@ debug_waitpoint_release
957943

958944

959945
step L1_refresh_full: <... completed>
960-
step lock_L2_source: <... completed>
961946
step unlock:
962947
ROLLBACK;
963948

0 commit comments

Comments
 (0)