From 64095607775ab4fd83a5ecea7ee047cb367f4ff4 Mon Sep 17 00:00:00 2001 From: Jan Nidzwetzki Date: Wed, 10 Apr 2024 12:13:12 +0200 Subject: [PATCH] Fix handling of chunks with no contraints When a catalog corruption occurs, and a chunk does not contain any dimension slices, we crash in ts_dimension_slice_cmp(). This patch adds a proper check and errors out before the code path is called. --- src/chunk_scan.c | 8 ++ test/expected/catalog_corruption.out | 150 +++++++++++++++++++++++++++ test/sql/CMakeLists.txt | 1 + test/sql/catalog_corruption.sql | 113 ++++++++++++++++++++ 4 files changed, 272 insertions(+) create mode 100644 test/expected/catalog_corruption.out create mode 100644 test/sql/catalog_corruption.sql diff --git a/src/chunk_scan.c b/src/chunk_scan.c index 3e2dbb9e60e..902bbc3b5ab 100644 --- a/src/chunk_scan.c +++ b/src/chunk_scan.c @@ -197,6 +197,14 @@ ts_chunk_scan_by_chunk_ids(const Hyperspace *hs, const List *chunk_ids, unsigned Assert(cube->capacity > cube->num_slices); cube->slices[cube->num_slices++] = slice_copy; } + + if (cube->num_slices == 0) + { + ereport(ERROR, + (errcode(ERRCODE_INTERNAL_ERROR), + errmsg("chunk %s has no dimension slices", get_rel_name(chunk->table_id)))); + } + ts_hypercube_slice_sort(cube); chunk->cube = cube; } diff --git a/test/expected/catalog_corruption.out b/test/expected/catalog_corruption.out new file mode 100644 index 00000000000..b5f3b8c7658 --- /dev/null +++ b/test/expected/catalog_corruption.out @@ -0,0 +1,150 @@ +-- This file and its contents are licensed under the Apache License 2.0. +-- Please see the included NOTICE for copyright information and +-- LICENSE-APACHE for a copy of the license. +-- Hypertables can break as a result of race conditions, but we should +-- still not crash when trying to truncate or delete the broken table. +\c :TEST_DBNAME :ROLE_SUPERUSER +CREATE VIEW missing_slices AS +SELECT DISTINCT + dimension_slice_id, + constraint_name, + attname AS column_name, + pg_get_expr(conbin, conrelid) AS constraint_expr +FROM + _timescaledb_catalog.chunk_constraint cc + JOIN _timescaledb_catalog.chunk ch ON cc.chunk_id = ch.id + JOIN pg_constraint ON conname = constraint_name + JOIN pg_namespace ns ON connamespace = ns.oid + AND ns.nspname = ch.schema_name + JOIN pg_attribute ON attnum = conkey[1] + AND attrelid = conrelid +WHERE + dimension_slice_id NOT IN (SELECT id FROM _timescaledb_catalog.dimension_slice); +-- To drop rows from dimension_slice table, we need to remove some +-- constraints. +ALTER TABLE _timescaledb_catalog.chunk_constraint + DROP CONSTRAINT chunk_constraint_dimension_slice_id_fkey; +CREATE TABLE chunk_test_int(time integer, temp float8, tag integer, color integer); +SELECT create_hypertable('chunk_test_int', 'time', 'tag', 2, chunk_time_interval => 3); +NOTICE: adding not-null constraint to column "time" + create_hypertable +----------------------------- + (1,public,chunk_test_int,t) +(1 row) + +INSERT INTO chunk_test_int VALUES + (4, 24.3, 1, 1), + (4, 24.3, 2, 1), + (10, 24.3, 2, 1); +SELECT * FROM _timescaledb_catalog.dimension_slice ORDER BY id; + id | dimension_id | range_start | range_end +----+--------------+----------------------+--------------------- + 1 | 1 | 3 | 6 + 2 | 2 | -9223372036854775808 | 1073741823 + 3 | 2 | 1073741823 | 9223372036854775807 + 4 | 1 | 9 | 12 +(4 rows) + +SELECT DISTINCT + chunk_id, + dimension_slice_id, + constraint_name, + pg_get_expr(conbin, conrelid) AS constraint_expr +FROM _timescaledb_catalog.chunk_constraint, + LATERAL ( + SELECT * + FROM pg_constraint JOIN pg_namespace ns ON connamespace = ns.oid + WHERE conname = constraint_name + ) AS con +ORDER BY chunk_id, dimension_slice_id; + chunk_id | dimension_slice_id | constraint_name | constraint_expr +----------+--------------------+-----------------+---------------------------------------------------------------- + 1 | 1 | constraint_1 | (("time" >= 3) AND ("time" < 6)) + 1 | 2 | constraint_2 | (_timescaledb_functions.get_partition_hash(tag) < 1073741823) + 2 | 1 | constraint_1 | (("time" >= 3) AND ("time" < 6)) + 2 | 3 | constraint_3 | (_timescaledb_functions.get_partition_hash(tag) >= 1073741823) + 3 | 3 | constraint_3 | (_timescaledb_functions.get_partition_hash(tag) >= 1073741823) + 3 | 4 | constraint_4 | (("time" >= 9) AND ("time" < 12)) +(6 rows) + +DELETE FROM _timescaledb_catalog.dimension_slice WHERE id = 1; +SELECT * FROM missing_slices; + dimension_slice_id | constraint_name | column_name | constraint_expr +--------------------+-----------------+-------------+---------------------------------- + 1 | constraint_1 | time | (("time" >= 3) AND ("time" < 6)) +(1 row) + +TRUNCATE TABLE chunk_test_int; +WARNING: unexpected state for chunk _timescaledb_internal._hyper_1_1_chunk, dropping anyway +WARNING: unexpected state for chunk _timescaledb_internal._hyper_1_2_chunk, dropping anyway +DROP TABLE chunk_test_int; +CREATE TABLE chunk_test_int(time integer, temp float8, tag integer, color integer); +SELECT create_hypertable('chunk_test_int', 'time', 'tag', 2, chunk_time_interval => 3); +NOTICE: adding not-null constraint to column "time" + create_hypertable +----------------------------- + (2,public,chunk_test_int,t) +(1 row) + +INSERT INTO chunk_test_int VALUES + (4, 24.3, 1, 1), + (4, 24.3, 2, 1), + (10, 24.3, 2, 1); +SELECT DISTINCT + chunk_id, + dimension_slice_id, + constraint_name, + pg_get_expr(conbin, conrelid) AS constraint_expr +FROM _timescaledb_catalog.chunk_constraint, + LATERAL ( + SELECT * + FROM pg_constraint JOIN pg_namespace ns ON connamespace = ns.oid + WHERE conname = constraint_name + ) AS con +ORDER BY chunk_id, dimension_slice_id; + chunk_id | dimension_slice_id | constraint_name | constraint_expr +----------+--------------------+-----------------+---------------------------------------------------------------- + 4 | 5 | constraint_5 | (("time" >= 3) AND ("time" < 6)) + 4 | 6 | constraint_6 | (_timescaledb_functions.get_partition_hash(tag) < 1073741823) + 5 | 5 | constraint_5 | (("time" >= 3) AND ("time" < 6)) + 5 | 7 | constraint_7 | (_timescaledb_functions.get_partition_hash(tag) >= 1073741823) + 6 | 7 | constraint_7 | (_timescaledb_functions.get_partition_hash(tag) >= 1073741823) + 6 | 8 | constraint_8 | (("time" >= 9) AND ("time" < 12)) +(6 rows) + +DELETE FROM _timescaledb_catalog.dimension_slice WHERE id = 5; +SELECT * FROM missing_slices; + dimension_slice_id | constraint_name | column_name | constraint_expr +--------------------+-----------------+-------------+---------------------------------- + 5 | constraint_5 | time | (("time" >= 3) AND ("time" < 6)) +(1 row) + +DROP TABLE chunk_test_int; +WARNING: unexpected state for chunk _timescaledb_internal._hyper_2_4_chunk, dropping anyway +WARNING: unexpected state for chunk _timescaledb_internal._hyper_2_5_chunk, dropping anyway +--- Test handling of missing dimension slices +CREATE TABLE dim_test(time TIMESTAMPTZ, device int); +SELECT create_hypertable('dim_test', 'time', chunk_time_interval => INTERVAL '1 day'); +NOTICE: adding not-null constraint to column "time" + create_hypertable +----------------------- + (3,public,dim_test,t) +(1 row) + +-- Create two chunks +INSERT INTO dim_test values('2000-01-01 00:00:00', 1); +INSERT INTO dim_test values('2020-01-01 00:00:00', 1); +SELECT id AS dim_slice_id FROM _timescaledb_catalog.dimension_slice + ORDER BY id DESC LIMIT 1 + \gset +-- Delete the dimension slice for the second chunk +DELETE FROM _timescaledb_catalog.chunk_constraint WHERE dimension_slice_id = :dim_slice_id; +\set ON_ERROR_STOP 0 +-- Select data +SELECT * FROM dim_test; +ERROR: chunk _hyper_3_8_chunk has no dimension slices +-- Select data using ordered append +SELECT * FROM dim_test ORDER BY time; +ERROR: chunk _hyper_3_8_chunk has no dimension slices +\set ON_ERROR_STOP 1 +DROP TABLE dim_test; diff --git a/test/sql/CMakeLists.txt b/test/sql/CMakeLists.txt index 85e3f9f7b64..55216e7ef09 100644 --- a/test/sql/CMakeLists.txt +++ b/test/sql/CMakeLists.txt @@ -5,6 +5,7 @@ set(TEST_FILES alter.sql alternate_users.sql baserel_cache.sql + catalog_corruption.sql chunks.sql chunk_adaptive.sql chunk_utils.sql diff --git a/test/sql/catalog_corruption.sql b/test/sql/catalog_corruption.sql new file mode 100644 index 00000000000..e825c9cc0b1 --- /dev/null +++ b/test/sql/catalog_corruption.sql @@ -0,0 +1,113 @@ +-- This file and its contents are licensed under the Apache License 2.0. +-- Please see the included NOTICE for copyright information and +-- LICENSE-APACHE for a copy of the license. + +-- Hypertables can break as a result of race conditions, but we should +-- still not crash when trying to truncate or delete the broken table. + +\c :TEST_DBNAME :ROLE_SUPERUSER + +CREATE VIEW missing_slices AS +SELECT DISTINCT + dimension_slice_id, + constraint_name, + attname AS column_name, + pg_get_expr(conbin, conrelid) AS constraint_expr +FROM + _timescaledb_catalog.chunk_constraint cc + JOIN _timescaledb_catalog.chunk ch ON cc.chunk_id = ch.id + JOIN pg_constraint ON conname = constraint_name + JOIN pg_namespace ns ON connamespace = ns.oid + AND ns.nspname = ch.schema_name + JOIN pg_attribute ON attnum = conkey[1] + AND attrelid = conrelid +WHERE + dimension_slice_id NOT IN (SELECT id FROM _timescaledb_catalog.dimension_slice); + +-- To drop rows from dimension_slice table, we need to remove some +-- constraints. +ALTER TABLE _timescaledb_catalog.chunk_constraint + DROP CONSTRAINT chunk_constraint_dimension_slice_id_fkey; + +CREATE TABLE chunk_test_int(time integer, temp float8, tag integer, color integer); +SELECT create_hypertable('chunk_test_int', 'time', 'tag', 2, chunk_time_interval => 3); + +INSERT INTO chunk_test_int VALUES + (4, 24.3, 1, 1), + (4, 24.3, 2, 1), + (10, 24.3, 2, 1); + +SELECT * FROM _timescaledb_catalog.dimension_slice ORDER BY id; + +SELECT DISTINCT + chunk_id, + dimension_slice_id, + constraint_name, + pg_get_expr(conbin, conrelid) AS constraint_expr +FROM _timescaledb_catalog.chunk_constraint, + LATERAL ( + SELECT * + FROM pg_constraint JOIN pg_namespace ns ON connamespace = ns.oid + WHERE conname = constraint_name + ) AS con +ORDER BY chunk_id, dimension_slice_id; + +DELETE FROM _timescaledb_catalog.dimension_slice WHERE id = 1; +SELECT * FROM missing_slices; + +TRUNCATE TABLE chunk_test_int; +DROP TABLE chunk_test_int; + +CREATE TABLE chunk_test_int(time integer, temp float8, tag integer, color integer); +SELECT create_hypertable('chunk_test_int', 'time', 'tag', 2, chunk_time_interval => 3); + +INSERT INTO chunk_test_int VALUES + (4, 24.3, 1, 1), + (4, 24.3, 2, 1), + (10, 24.3, 2, 1); + +SELECT DISTINCT + chunk_id, + dimension_slice_id, + constraint_name, + pg_get_expr(conbin, conrelid) AS constraint_expr +FROM _timescaledb_catalog.chunk_constraint, + LATERAL ( + SELECT * + FROM pg_constraint JOIN pg_namespace ns ON connamespace = ns.oid + WHERE conname = constraint_name + ) AS con +ORDER BY chunk_id, dimension_slice_id; + +DELETE FROM _timescaledb_catalog.dimension_slice WHERE id = 5; +SELECT * FROM missing_slices; + +DROP TABLE chunk_test_int; + +--- Test handling of missing dimension slices +CREATE TABLE dim_test(time TIMESTAMPTZ, device int); +SELECT create_hypertable('dim_test', 'time', chunk_time_interval => INTERVAL '1 day'); + +-- Create two chunks +INSERT INTO dim_test values('2000-01-01 00:00:00', 1); +INSERT INTO dim_test values('2020-01-01 00:00:00', 1); + +SELECT id AS dim_slice_id FROM _timescaledb_catalog.dimension_slice + ORDER BY id DESC LIMIT 1 + \gset + +-- Delete the dimension slice for the second chunk +DELETE FROM _timescaledb_catalog.chunk_constraint WHERE dimension_slice_id = :dim_slice_id; + +\set ON_ERROR_STOP 0 + +-- Select data +SELECT * FROM dim_test; + +-- Select data using ordered append +SELECT * FROM dim_test ORDER BY time; + +\set ON_ERROR_STOP 1 + +DROP TABLE dim_test; +