diff --git a/.unreleased/pr_9964 b/.unreleased/pr_9964 new file mode 100644 index 00000000000..a6a550fce9b --- /dev/null +++ b/.unreleased/pr_9964 @@ -0,0 +1 @@ +Implements: #9964 Add a function to lock OSM chunk's dimension slice diff --git a/sql/osm_api.sql b/sql/osm_api.sql index b0f7bdcc643..795736a2031 100644 --- a/sql/osm_api.sql +++ b/sql/osm_api.sql @@ -12,3 +12,12 @@ CREATE OR REPLACE FUNCTION _timescaledb_functions.hypertable_osm_range_update( empty BOOL = false ) RETURNS BOOL AS '@MODULE_PATHNAME@', 'ts_hypertable_osm_range_update' LANGUAGE C VOLATILE; + +-- Acquires a FOR UPDATE row lock on the dimension slice tuple belonging to the +-- OSM chunk of the given hypertable. There is exactly one OSM chunk per +-- hypertable, so this locks its single dimension_slice entry. The lock is held +-- until the end of the current transaction; nothing is returned. +CREATE OR REPLACE FUNCTION _timescaledb_functions.lock_osm_chunk_dimension_slice( + htoid REGCLASS +) RETURNS VOID AS '@MODULE_PATHNAME@', +'ts_lock_osm_chunk_dimension_slice' LANGUAGE C VOLATILE; diff --git a/sql/updates/reverse-dev.sql b/sql/updates/reverse-dev.sql index 42a9b3de89e..1f3d27a95c3 100644 --- a/sql/updates/reverse-dev.sql +++ b/sql/updates/reverse-dev.sql @@ -20,6 +20,8 @@ $$; DROP VIEW IF EXISTS _timescaledb_catalog.chunk_constraint; DROP FUNCTION IF EXISTS _timescaledb_functions.chunk_constraint_add_table_constraint( integer, name, name); +DROP FUNCTION IF EXISTS _timescaledb_functions.lock_osm_chunk_dimension_slice(regclass); + ALTER TABLE _timescaledb_catalog.hypertable RESET (user_catalog_table); ALTER TABLE _timescaledb_catalog.chunk RESET (user_catalog_table); diff --git a/src/hypertable.c b/src/hypertable.c index 545c11b15c7..25d68756a1c 100644 --- a/src/hypertable.c +++ b/src/hypertable.c @@ -2690,6 +2690,71 @@ ts_hypertable_osm_range_update(PG_FUNCTION_ARGS) PG_RETURN_BOOL(overlap); } +/* + * lock_osm_chunk_dimension_slice + * 0 hypertable REGCLASS + * + * Acquires a FOR UPDATE row lock on the dimension slice tuple belonging to the + * OSM chunk of the given hypertable. There is exactly one OSM chunk per + * hypertable, so this locks its single dimension slice entry. The lock is held + * until the end of the current transaction. Returns void. + * + * Like hypertable_osm_range_update this is meant to be used by OSM to + * coordinate access to the OSM chunk's dimension slice; it is not meant to run + * on a read-only secondary. + */ +TS_FUNCTION_INFO_V1(ts_lock_osm_chunk_dimension_slice); +Datum +ts_lock_osm_chunk_dimension_slice(PG_FUNCTION_ARGS) +{ + Oid relid = PG_ARGISNULL(0) ? InvalidOid : PG_GETARG_OID(0); + Hypertable *ht; + const Dimension *time_dim; + Cache *hcache; + + Assert(!RecoveryInProgress()); + + hcache = ts_hypertable_cache_pin(); + ht = ts_resolve_hypertable_from_table_or_cagg(hcache, relid, true); + Assert(ht != NULL); + time_dim = hyperspace_get_open_dimension(ht->space, 0); + + Ensure(time_dim != NULL, + "could not find time dimension for hypertable %s.%s", + quote_identifier(NameStr(ht->fd.schema_name)), + quote_identifier(NameStr(ht->fd.table_name))); + + int32 osm_chunk_id = ts_chunk_get_osm_chunk_id(ht->fd.id); + if (osm_chunk_id == INVALID_CHUNK_ID) + { + ereport(ERROR, + errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), + errmsg("no OSM chunk found for hypertable %s.%s", + quote_identifier(NameStr(ht->fd.schema_name)), + quote_identifier(NameStr(ht->fd.table_name)))); + } + + /* + * Lock the OSM chunk's dimension slice tuple FOR UPDATE. The row lock is + * held until the end of the current transaction. + */ + DimensionSlice *slice = ts_chunk_get_osm_slice_and_lock(osm_chunk_id, + time_dim->fd.id, + LockTupleExclusive, + RowShareLock); + + if (!slice) + { + ereport(ERROR, + errcode(ERRCODE_INTERNAL_ERROR), + errmsg("could not find time dimension slice for chunk %d", osm_chunk_id)); + } + + ts_cache_release(&hcache); + + PG_RETURN_VOID(); +} + TSDLLEXPORT bool ts_hypertable_has_continuous_aggregates(int32 hypertable_id) { diff --git a/tsl/test/expected/chunk_utils_internal.out b/tsl/test/expected/chunk_utils_internal.out index 8594fd1e37c..13bfa50ccb6 100644 --- a/tsl/test/expected/chunk_utils_internal.out +++ b/tsl/test/expected/chunk_utils_internal.out @@ -498,11 +498,18 @@ SELECT status FROM _timescaledb_catalog.hypertable WHERE table_name = 'ht_try'; 3 -- must also update the range since the created chunk contains data +BEGIN; +SELECT _timescaledb_functions.lock_osm_chunk_dimension_slice('ht_try'); + lock_osm_chunk_dimension_slice +-------------------------------- + + SELECT _timescaledb_functions.hypertable_osm_range_update('ht_try', '2020-01-01'::timestamptz, '2020-01-02'); hypertable_osm_range_update ----------------------------- f +COMMIT; -- OSM chunk is not visible in chunks view SELECT chunk_name, range_start, range_end FROM timescaledb_information.chunks @@ -659,6 +666,11 @@ SELECT * FROM hypertable_approximate_size('ht_try'); ----------------------------- 32768 +\set ON_ERROR_STOP 0 +-- Error for a hypertable that has no OSM chunk +SELECT _timescaledb_functions.lock_osm_chunk_dimension_slice('test1.hyper1'); +ERROR: no OSM chunk found for hypertable test1.hyper1 +\set ON_ERROR_STOP 1 --TEST GUC variable to enable/disable OSM chunk SET timescaledb.enable_tiered_reads=false; :EXPLAIN SELECT * from ht_try; diff --git a/tsl/test/isolation/expected/osm_range_updates_iso.out b/tsl/test/isolation/expected/osm_range_updates_iso.out index 0c7a0820fa6..01982de018b 100644 --- a/tsl/test/isolation/expected/osm_range_updates_iso.out +++ b/tsl/test/isolation/expected/osm_range_updates_iso.out @@ -1,19 +1,34 @@ -Parsed test spec with 10 sessions +Parsed test spec with 11 sessions + +starting permutation: LockDimSliceTuple LockDimSliceTuple2 UnlockDimSliceTuple UnlockDimSliceTuple2 +step LockDimSliceTuple: + BEGIN; + SELECT _timescaledb_functions.lock_osm_chunk_dimension_slice('osm_test'); + +lock_osm_chunk_dimension_slice +------------------------------ + + +step LockDimSliceTuple2: + BEGIN; + SELECT _timescaledb_functions.lock_osm_chunk_dimension_slice('osm_test'); + +step UnlockDimSliceTuple: ROLLBACK; +step LockDimSliceTuple2: <... completed> +lock_osm_chunk_dimension_slice +------------------------------ + + +step UnlockDimSliceTuple2: ROLLBACK; starting permutation: LockDimSliceTuple UR1b UR1u UR2b UR2u UnlockDimSliceTuple UR1c UR2c step LockDimSliceTuple: BEGIN; - SELECT range_start, range_end FROM _timescaledb_catalog.dimension_slice - WHERE id IN ( SELECT ds.id FROM - _timescaledb_catalog.chunk ch, - _timescaledb_catalog.dimension_slice ds, _timescaledb_catalog.hypertable ht - WHERE ht.table_name like 'osm_test' AND ds.chunk_id = ch.id AND ht.id = ch.hypertable_id - AND ch.osm_chunk = true - ) FOR UPDATE; - - range_start| range_end --------------------+------------------- -9223372036854775806|9223372036854775807 + SELECT _timescaledb_functions.lock_osm_chunk_dimension_slice('osm_test'); + +lock_osm_chunk_dimension_slice +------------------------------ + step UR1b: BEGIN; step UR1u: SELECT _timescaledb_functions.hypertable_osm_range_update('osm_test', 0, 10); @@ -36,17 +51,11 @@ step UR2c: COMMIT; starting permutation: LockDimSliceTuple DTb UR1b DropOsmChunk UR1u UnlockDimSliceTuple DTc UR1c step LockDimSliceTuple: BEGIN; - SELECT range_start, range_end FROM _timescaledb_catalog.dimension_slice - WHERE id IN ( SELECT ds.id FROM - _timescaledb_catalog.chunk ch, - _timescaledb_catalog.dimension_slice ds, _timescaledb_catalog.hypertable ht - WHERE ht.table_name like 'osm_test' AND ds.chunk_id = ch.id AND ht.id = ch.hypertable_id - AND ch.osm_chunk = true - ) FOR UPDATE; - - range_start| range_end --------------------+------------------- -9223372036854775806|9223372036854775807 + SELECT _timescaledb_functions.lock_osm_chunk_dimension_slice('osm_test'); + +lock_osm_chunk_dimension_slice +------------------------------ + step DTb: BEGIN; step UR1b: BEGIN; @@ -73,17 +82,11 @@ step UR1c: COMMIT; starting permutation: LockDimSliceTuple DTb UR1b UR1u DropOsmChunk UnlockDimSliceTuple UR1c DTc step LockDimSliceTuple: BEGIN; - SELECT range_start, range_end FROM _timescaledb_catalog.dimension_slice - WHERE id IN ( SELECT ds.id FROM - _timescaledb_catalog.chunk ch, - _timescaledb_catalog.dimension_slice ds, _timescaledb_catalog.hypertable ht - WHERE ht.table_name like 'osm_test' AND ds.chunk_id = ch.id AND ht.id = ch.hypertable_id - AND ch.osm_chunk = true - ) FOR UPDATE; - - range_start| range_end --------------------+------------------- -9223372036854775806|9223372036854775807 + SELECT _timescaledb_functions.lock_osm_chunk_dimension_slice('osm_test'); + +lock_osm_chunk_dimension_slice +------------------------------ + step DTb: BEGIN; step UR1b: BEGIN; diff --git a/tsl/test/isolation/specs/osm_range_updates_iso.spec b/tsl/test/isolation/specs/osm_range_updates_iso.spec index d5584d27bfa..271fba0eeb9 100644 --- a/tsl/test/isolation/specs/osm_range_updates_iso.spec +++ b/tsl/test/isolation/specs/osm_range_updates_iso.spec @@ -45,16 +45,18 @@ step "UR2c" { COMMIT; } session "LDST" step "LockDimSliceTuple" { BEGIN; - SELECT range_start, range_end FROM _timescaledb_catalog.dimension_slice - WHERE id IN ( SELECT ds.id FROM - _timescaledb_catalog.chunk ch, - _timescaledb_catalog.dimension_slice ds, _timescaledb_catalog.hypertable ht - WHERE ht.table_name like 'osm_test' AND ds.chunk_id = ch.id AND ht.id = ch.hypertable_id - AND ch.osm_chunk = true - ) FOR UPDATE; + SELECT _timescaledb_functions.lock_osm_chunk_dimension_slice('osm_test'); } step "UnlockDimSliceTuple" { ROLLBACK; } +# second session that locks the same dimension_slice tuple +session "LDST2" +step "LockDimSliceTuple2" { + BEGIN; + SELECT _timescaledb_functions.lock_osm_chunk_dimension_slice('osm_test'); + } +step "UnlockDimSliceTuple2" { ROLLBACK; } + session "DT" step "DTb" { BEGIN; } step "DropOsmChunk" { @@ -102,6 +104,10 @@ step DR2b { BEGIN; } step DR2drop { SELECT _timescaledb_functions.drop_osm_chunk('test_drop'); } step DR2c { COMMIT; } +# Two concurrent locks on the same OSM chunk dimension_slice tuple block one +# another. The second lock can only be acquired after the first session rolls +# back and releases its lock. +permutation "LockDimSliceTuple" "LockDimSliceTuple2" "UnlockDimSliceTuple" "UnlockDimSliceTuple2" # Concurrent updates will block one another # this previously deadlocked one of the two transactions permutation "LockDimSliceTuple" "UR1b" "UR1u" "UR2b" "UR2u" "UnlockDimSliceTuple" "UR1c" "UR2c" diff --git a/tsl/test/shared/expected/extension.out b/tsl/test/shared/expected/extension.out index 3de00eeb879..699831d0382 100644 --- a/tsl/test/shared/expected/extension.out +++ b/tsl/test/shared/expected/extension.out @@ -91,6 +91,7 @@ ORDER BY pronamespace::regnamespace::text COLLATE "C", p.oid::regprocedure::text _timescaledb_functions.jsonb_get_matching_index_entry(jsonb,text,text) _timescaledb_functions.last_combinefunc(internal,internal) _timescaledb_functions.last_sfunc(internal,anyelement,"any") + _timescaledb_functions.lock_osm_chunk_dimension_slice(regclass) _timescaledb_functions.make_multirange_from_internal_time(tsrange,bigint,bigint) _timescaledb_functions.make_multirange_from_internal_time(tstzrange,bigint,bigint) _timescaledb_functions.make_range_from_internal_time(anyrange,anyelement,anyelement) diff --git a/tsl/test/sql/chunk_utils_internal.sql b/tsl/test/sql/chunk_utils_internal.sql index a515849e324..ffcf3a03588 100644 --- a/tsl/test/sql/chunk_utils_internal.sql +++ b/tsl/test/sql/chunk_utils_internal.sql @@ -339,7 +339,10 @@ SELECT _timescaledb_functions.attach_osm_table_chunk('ht_try', 'child_fdw_table' -- check hypertable status SELECT status FROM _timescaledb_catalog.hypertable WHERE table_name = 'ht_try'; -- must also update the range since the created chunk contains data +BEGIN; +SELECT _timescaledb_functions.lock_osm_chunk_dimension_slice('ht_try'); SELECT _timescaledb_functions.hypertable_osm_range_update('ht_try', '2020-01-01'::timestamptz, '2020-01-02'); +COMMIT; -- OSM chunk is not visible in chunks view SELECT chunk_name, range_start, range_end @@ -406,6 +409,11 @@ SELECT _timescaledb_functions.hypertable_osm_range_update('ht_try', '2022-05-05 -- test that approximate size function works when a osm chunk is present SELECT * FROM hypertable_approximate_size('ht_try'); +\set ON_ERROR_STOP 0 +-- Error for a hypertable that has no OSM chunk +SELECT _timescaledb_functions.lock_osm_chunk_dimension_slice('test1.hyper1'); +\set ON_ERROR_STOP 1 + --TEST GUC variable to enable/disable OSM chunk SET timescaledb.enable_tiered_reads=false; :EXPLAIN SELECT * from ht_try;