Skip to content

Commit 76abc65

Browse files
Add concurrent DML compaction isolation test
Tests that UPDATE or DELETE on compressed data during compaction triggers the serialization error. Uses a debug waitpoint after the overlap scan to allow DML before compaction proceeds. (cherry picked from commit 5bd0019)
1 parent dec1bc3 commit 76abc65

4 files changed

Lines changed: 332 additions & 2 deletions

File tree

tsl/src/compression/compression_dml.c

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2615,7 +2615,20 @@ report_error(TM_Result result)
26152615
*/
26162616
case TM_Updated:
26172617
{
2618-
elog(ERROR, "tuple concurrently updated");
2618+
if (IsolationUsesXactSnapshot())
2619+
{
2620+
ereport(ERROR,
2621+
(errcode(ERRCODE_T_R_SERIALIZATION_FAILURE),
2622+
errmsg("could not serialize access due to concurrent update")));
2623+
}
2624+
/*
2625+
* TODO: Gracefully handle these scenarios.
2626+
* Can happen with compaction or recompress segmentwise.
2627+
*/
2628+
ereport(ERROR,
2629+
(errcode(ERRCODE_T_R_SERIALIZATION_FAILURE),
2630+
errmsg("could not update/delete compressed data due to "
2631+
"concurrent modification")));
26192632
}
26202633
break;
26212634
case TM_Invisible:

tsl/src/compression/recompress.c

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1254,6 +1254,8 @@ compact_chunk_recompress_overlapping_batches(
12541254
found_overlaps = true;
12551255
CommandCounterIncrement();
12561256

1257+
DEBUG_WAITPOINT("compact_chunk_after_batch_delete");
1258+
12571259
/* The overlapping batch becomes the predecessor for the scan loop. */
12581260
ItemPointerCopy(&state->first_overlap_tid, &state->previous_tid);
12591261
update_current_segment(recompress_ctx->current_segment,
@@ -1513,6 +1515,8 @@ compact_chunk_impl(Chunk *uncompressed_chunk, int max_batches)
15131515
recompress_ctx,
15141516
state);
15151517

1518+
DEBUG_WAITPOINT("compact_chunk_after_find_overlaps");
1519+
15161520
if (found_overlaps)
15171521
{
15181522
/* Recompress the overlaps */

tsl/test/isolation/expected/compact_chunk_concurrent.out

Lines changed: 273 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
Parsed test spec with 2 sessions
1+
Parsed test spec with 3 sessions
22

33
starting permutation: s2_begin s2_select s1_compact s2_commit s1_show_status s1_count
44
step s2_begin:
@@ -244,3 +244,275 @@ count
244244
-----
245245
4100
246246

247+
248+
starting permutation: s3_wp_enable s1_compact s2_update s3_wp_release s1_show_status s1_count
249+
step s3_wp_enable:
250+
SELECT debug_waitpoint_enable('compact_chunk_after_find_overlaps');
251+
252+
debug_waitpoint_enable
253+
----------------------
254+
255+
256+
step s1_compact:
257+
SELECT count(_timescaledb_functions.compact_chunk(chunk)) AS compact
258+
FROM show_chunks('metrics') chunk;
259+
<waiting ...>
260+
step s2_update:
261+
UPDATE metrics SET value = -1.0 WHERE value = 1.0;
262+
263+
step s3_wp_release:
264+
SELECT debug_waitpoint_release('compact_chunk_after_find_overlaps');
265+
266+
debug_waitpoint_release
267+
-----------------------
268+
269+
270+
step s1_compact: <... completed>
271+
ERROR: aborting compaction due to concurrent updates on compressed data, retrying with next policy run
272+
step s1_show_status:
273+
SELECT _timescaledb_functions.chunk_status_text(chunk) AS status
274+
FROM show_chunks('metrics') chunk;
275+
276+
status
277+
------------------------------
278+
{COMPRESSED,UNORDERED,PARTIAL}
279+
280+
step s1_count:
281+
SELECT count(*) FROM metrics;
282+
283+
count
284+
-----
285+
4000
286+
287+
288+
starting permutation: s3_wp_enable s1_compact s2_delete s3_wp_release s1_show_status s1_count
289+
step s3_wp_enable:
290+
SELECT debug_waitpoint_enable('compact_chunk_after_find_overlaps');
291+
292+
debug_waitpoint_enable
293+
----------------------
294+
295+
296+
step s1_compact:
297+
SELECT count(_timescaledb_functions.compact_chunk(chunk)) AS compact
298+
FROM show_chunks('metrics') chunk;
299+
<waiting ...>
300+
step s2_delete:
301+
DELETE FROM metrics WHERE value = 1.0;
302+
303+
step s3_wp_release:
304+
SELECT debug_waitpoint_release('compact_chunk_after_find_overlaps');
305+
306+
debug_waitpoint_release
307+
-----------------------
308+
309+
310+
step s1_compact: <... completed>
311+
ERROR: aborting compaction due to concurrent updates on compressed data, retrying with next policy run
312+
step s1_show_status:
313+
SELECT _timescaledb_functions.chunk_status_text(chunk) AS status
314+
FROM show_chunks('metrics') chunk;
315+
316+
status
317+
------------------------------
318+
{COMPRESSED,UNORDERED,PARTIAL}
319+
320+
step s1_count:
321+
SELECT count(*) FROM metrics;
322+
323+
count
324+
-----
325+
3999
326+
327+
328+
starting permutation: s3_wp_enable_after_delete s1_compact s2_update s3_wp_release_after_delete s1_show_status s1_count
329+
step s3_wp_enable_after_delete:
330+
SELECT debug_waitpoint_enable('compact_chunk_after_batch_delete');
331+
332+
debug_waitpoint_enable
333+
----------------------
334+
335+
336+
step s1_compact:
337+
SELECT count(_timescaledb_functions.compact_chunk(chunk)) AS compact
338+
FROM show_chunks('metrics') chunk;
339+
<waiting ...>
340+
step s2_update:
341+
UPDATE metrics SET value = -1.0 WHERE value = 1.0;
342+
<waiting ...>
343+
step s3_wp_release_after_delete:
344+
SELECT debug_waitpoint_release('compact_chunk_after_batch_delete');
345+
346+
debug_waitpoint_release
347+
-----------------------
348+
349+
350+
step s1_compact: <... completed>
351+
compact
352+
-------
353+
1
354+
355+
step s2_update: <... completed>
356+
ERROR: could not update/delete compressed data due to concurrent modification
357+
step s1_show_status:
358+
SELECT _timescaledb_functions.chunk_status_text(chunk) AS status
359+
FROM show_chunks('metrics') chunk;
360+
361+
status
362+
----------------------
363+
{COMPRESSED,UNORDERED}
364+
365+
step s1_count:
366+
SELECT count(*) FROM metrics;
367+
368+
count
369+
-----
370+
4000
371+
372+
373+
starting permutation: s3_wp_enable_after_delete s1_compact s2_delete s3_wp_release_after_delete s1_show_status s1_count
374+
step s3_wp_enable_after_delete:
375+
SELECT debug_waitpoint_enable('compact_chunk_after_batch_delete');
376+
377+
debug_waitpoint_enable
378+
----------------------
379+
380+
381+
step s1_compact:
382+
SELECT count(_timescaledb_functions.compact_chunk(chunk)) AS compact
383+
FROM show_chunks('metrics') chunk;
384+
<waiting ...>
385+
step s2_delete:
386+
DELETE FROM metrics WHERE value = 1.0;
387+
<waiting ...>
388+
step s3_wp_release_after_delete:
389+
SELECT debug_waitpoint_release('compact_chunk_after_batch_delete');
390+
391+
debug_waitpoint_release
392+
-----------------------
393+
394+
395+
step s1_compact: <... completed>
396+
compact
397+
-------
398+
1
399+
400+
step s2_delete: <... completed>
401+
ERROR: could not update/delete compressed data due to concurrent modification
402+
step s1_show_status:
403+
SELECT _timescaledb_functions.chunk_status_text(chunk) AS status
404+
FROM show_chunks('metrics') chunk;
405+
406+
status
407+
----------------------
408+
{COMPRESSED,UNORDERED}
409+
410+
step s1_count:
411+
SELECT count(*) FROM metrics;
412+
413+
count
414+
-----
415+
4000
416+
417+
418+
starting permutation: s3_wp_enable_after_delete s1_compact s2_begin_rr s2_update s3_wp_release_after_delete s2_commit s1_show_status s1_count
419+
step s3_wp_enable_after_delete:
420+
SELECT debug_waitpoint_enable('compact_chunk_after_batch_delete');
421+
422+
debug_waitpoint_enable
423+
----------------------
424+
425+
426+
step s1_compact:
427+
SELECT count(_timescaledb_functions.compact_chunk(chunk)) AS compact
428+
FROM show_chunks('metrics') chunk;
429+
<waiting ...>
430+
step s2_begin_rr:
431+
BEGIN ISOLATION LEVEL REPEATABLE READ;
432+
433+
step s2_update:
434+
UPDATE metrics SET value = -1.0 WHERE value = 1.0;
435+
<waiting ...>
436+
step s3_wp_release_after_delete:
437+
SELECT debug_waitpoint_release('compact_chunk_after_batch_delete');
438+
439+
debug_waitpoint_release
440+
-----------------------
441+
442+
443+
step s1_compact: <... completed>
444+
compact
445+
-------
446+
1
447+
448+
step s2_update: <... completed>
449+
ERROR: could not serialize access due to concurrent update
450+
step s2_commit:
451+
COMMIT;
452+
453+
step s1_show_status:
454+
SELECT _timescaledb_functions.chunk_status_text(chunk) AS status
455+
FROM show_chunks('metrics') chunk;
456+
457+
status
458+
----------------------
459+
{COMPRESSED,UNORDERED}
460+
461+
step s1_count:
462+
SELECT count(*) FROM metrics;
463+
464+
count
465+
-----
466+
4000
467+
468+
469+
starting permutation: s3_wp_enable_after_delete s1_compact s2_begin_rr s2_delete s3_wp_release_after_delete s2_commit s1_show_status s1_count
470+
step s3_wp_enable_after_delete:
471+
SELECT debug_waitpoint_enable('compact_chunk_after_batch_delete');
472+
473+
debug_waitpoint_enable
474+
----------------------
475+
476+
477+
step s1_compact:
478+
SELECT count(_timescaledb_functions.compact_chunk(chunk)) AS compact
479+
FROM show_chunks('metrics') chunk;
480+
<waiting ...>
481+
step s2_begin_rr:
482+
BEGIN ISOLATION LEVEL REPEATABLE READ;
483+
484+
step s2_delete:
485+
DELETE FROM metrics WHERE value = 1.0;
486+
<waiting ...>
487+
step s3_wp_release_after_delete:
488+
SELECT debug_waitpoint_release('compact_chunk_after_batch_delete');
489+
490+
debug_waitpoint_release
491+
-----------------------
492+
493+
494+
step s1_compact: <... completed>
495+
compact
496+
-------
497+
1
498+
499+
step s2_delete: <... completed>
500+
ERROR: could not serialize access due to concurrent update
501+
step s2_commit:
502+
COMMIT;
503+
504+
step s1_show_status:
505+
SELECT _timescaledb_functions.chunk_status_text(chunk) AS status
506+
FROM show_chunks('metrics') chunk;
507+
508+
status
509+
----------------------
510+
{COMPRESSED,UNORDERED}
511+
512+
step s1_count:
513+
SELECT count(*) FROM metrics;
514+
515+
count
516+
-----
517+
4000
518+

tsl/test/isolation/specs/compact_chunk_concurrent.spec

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,10 @@ step "s2_begin" {
4646
BEGIN;
4747
}
4848

49+
step "s2_begin_rr" {
50+
BEGIN ISOLATION LEVEL REPEATABLE READ;
51+
}
52+
4953
step "s2_insert" {
5054
INSERT INTO metrics VALUES ('2025-01-02 12:00', 'd1', -1.0);
5155
}
@@ -77,6 +81,24 @@ step "s2_rollback" {
7781
ROLLBACK;
7882
}
7983

84+
session "s3"
85+
step "s3_wp_enable" {
86+
SELECT debug_waitpoint_enable('compact_chunk_after_find_overlaps');
87+
}
88+
89+
step "s3_wp_release" {
90+
SELECT debug_waitpoint_release('compact_chunk_after_find_overlaps');
91+
}
92+
93+
step "s3_wp_enable_after_delete" {
94+
SELECT debug_waitpoint_enable('compact_chunk_after_batch_delete');
95+
}
96+
97+
step "s3_wp_release_after_delete" {
98+
SELECT debug_waitpoint_release('compact_chunk_after_batch_delete');
99+
}
100+
101+
80102
# compact_chunk should not block concurrent reads
81103
permutation "s2_begin" "s2_select" "s1_compact" "s2_commit" "s1_show_status" "s1_count"
82104

@@ -97,3 +119,22 @@ permutation "s2_begin" "s2_insert" "s2_rollback" "s1_compact" "s1_show_status" "
97119

98120
# compact_chunk should succeed after committed direct compress insert (chunk stays fully compressed)
99121
permutation "s2_begin" "s2_direct_insert" "s2_commit" "s1_compact" "s1_show_status" "s1_count"
122+
123+
# concurrent update triggers serialization error:
124+
permutation "s3_wp_enable" "s1_compact" "s2_update" "s3_wp_release" "s1_show_status" "s1_count"
125+
126+
# concurrent delete triggers serialization error
127+
permutation "s3_wp_enable" "s1_compact" "s2_delete" "s3_wp_release" "s1_show_status" "s1_count"
128+
129+
# concurrent update after batch delete triggers serialization error
130+
permutation "s3_wp_enable_after_delete" "s1_compact" "s2_update" "s3_wp_release_after_delete" "s1_show_status" "s1_count"
131+
132+
# concurrent delete after batch delete triggers serialization error
133+
permutation "s3_wp_enable_after_delete" "s1_compact" "s2_delete" "s3_wp_release_after_delete" "s1_show_status" "s1_count"
134+
135+
# Repeatable Read DML: concurrent update after batch delete.
136+
# s2 runs in Repeatable Read and should get a serialization error.
137+
permutation "s3_wp_enable_after_delete" "s1_compact" "s2_begin_rr" "s2_update" "s3_wp_release_after_delete" "s2_commit" "s1_show_status" "s1_count"
138+
139+
# Repeatable Read DML: concurrent delete after batch delete.
140+
permutation "s3_wp_enable_after_delete" "s1_compact" "s2_begin_rr" "s2_delete" "s3_wp_release_after_delete" "s2_commit" "s1_show_status" "s1_count"

0 commit comments

Comments
 (0)