Skip to content

Commit

Permalink
Fix handling of chunks with no contraints
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
jnidzwetzki committed Apr 10, 2024
1 parent 7ffdd07 commit 6409560
Show file tree
Hide file tree
Showing 4 changed files with 272 additions and 0 deletions.
8 changes: 8 additions & 0 deletions src/chunk_scan.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
150 changes: 150 additions & 0 deletions test/expected/catalog_corruption.out
Original file line number Diff line number Diff line change
@@ -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;
1 change: 1 addition & 0 deletions test/sql/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
113 changes: 113 additions & 0 deletions test/sql/catalog_corruption.sql
Original file line number Diff line number Diff line change
@@ -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;

0 comments on commit 6409560

Please sign in to comment.