-- -- macavity: faults that actually fire. -- -- Only 'error' and 'delay' are exercised here. 'crash' cannot be tested -- under pg_regress -- an unclean backend exit makes the postmaster reset -- the cluster -- so it has its own harness in test/crash_test.sh. -- -- error at executor_start, next matching hit SELECT macavity_arm('executor_start', 'error'); macavity_arm -------------- 1 (1 row) SELECT 1 AS never_runs; ERROR: macavity: injected error at fault point "executor_start" -- the event is one-shot: it is completed, and no longer fires. The hit -- that fired it was recorded before the error was raised, so the counters -- are still reported: hits 1, remaining 0. SELECT * FROM macavity_status(); event_id | point | action | occurrence | hits | remaining | state ----------+----------------+--------+------------+------+-----------+----------- 1 | executor_start | error | 1 | 1 | 0 | completed (1 row) SELECT 1 AS runs_normally; runs_normally --------------- 1 (1 row) SELECT macavity_reset(); macavity_reset ---------------- 1 (1 row) -- occurrence counting: hits 1 and 2 pass through, hit 3 fails SELECT macavity_arm('executor_start', 'error', 3); macavity_arm -------------- 1 (1 row) SELECT 1 AS hit_one; hit_one --------- 1 (1 row) SELECT hits, remaining FROM macavity_status(); hits | remaining ------+----------- 2 | 1 (1 row) SELECT 3 AS hit_three_fails; ERROR: macavity: injected error at fault point "executor_start" SELECT state FROM macavity_status(); state ----------- completed (1 row) SELECT macavity_reset(); macavity_reset ---------------- 1 (1 row) -- error at executor_end with occurrence 1: the next statement's own -- ExecutorEnd is the matching hit SELECT macavity_arm('executor_end', 'error'); macavity_arm -------------- 1 (1 row) SELECT 1 AS statement_whose_end_fails; ERROR: macavity: injected error at fault point "executor_end" -- the hit was recorded before the error: completed, hits 1, remaining 0 SELECT event_id, point, action, occurrence, hits, remaining, state FROM macavity_status(); event_id | point | action | occurrence | hits | remaining | state ----------+--------------+--------+------------+------+-----------+----------- 1 | executor_end | error | 1 | 1 | 0 | completed (1 row) SELECT macavity_reset(); macavity_reset ---------------- 1 (1 row) -- before_commit inside an explicit transaction block: the user's own COMMIT -- is the matching hit, so the transaction is rolled back instead BEGIN; SELECT macavity_arm('before_commit', 'error'); macavity_arm -------------- 1 (1 row) CREATE TEMP TABLE macavity_commit_victim (i int); INSERT INTO macavity_commit_victim VALUES (1); COMMIT; ERROR: macavity: injected error at fault point "before_commit" SELECT state FROM macavity_status(); state ----------- completed (1 row) -- the table did not survive the failed commit SELECT count(*) FROM pg_class WHERE relname = 'macavity_commit_victim'; count ------- 0 (1 row) SELECT macavity_reset(); macavity_reset ---------------- 1 (1 row) -- before_commit in autocommit mode: the arming statement's own implicit -- commit is not counted, so the event survives to trip the next statement SELECT macavity_arm('before_commit', 'error'); macavity_arm -------------- 1 (1 row) SELECT 1 AS commits_and_then_fails; ERROR: macavity: injected error at fault point "before_commit" SELECT state FROM macavity_status(); state ----------- completed (1 row) SELECT macavity_reset(); macavity_reset ---------------- 1 (1 row) -- the same skip applies when a completed event is reinstated in autocommit -- mode: the reinstating statement's own commit is not counted SELECT macavity_arm_error('before_commit'); macavity_arm_error -------------------- 1 (1 row) SELECT 1 AS fails_at_commit; ERROR: macavity: injected error at fault point "before_commit" SELECT macavity_arm(1); macavity_arm -------------- t (1 row) SELECT 1 AS fails_again_at_commit; ERROR: macavity: injected error at fault point "before_commit" SELECT event_id, hits, state FROM macavity_status(); event_id | hits | state ----------+------+----------- 1 | 1 | completed (1 row) SELECT macavity_reset(); macavity_reset ---------------- 1 (1 row) -- before_abort during abort processing: the delay runs, then the original -- error is reported as usual SELECT macavity_arm('before_abort', 'delay'); macavity_arm -------------- 1 (1 row) DO $$ BEGIN RAISE EXCEPTION 'macavity test: forcing an abort'; END $$; ERROR: macavity test: forcing an abort CONTEXT: PL/pgSQL function inline_code_block line 1 at RAISE SELECT state FROM macavity_status(); state ----------- completed (1 row) SELECT macavity_reset(); macavity_reset ---------------- 1 (1 row) -- delay really delays: the statement observes at least most of the 1 s -- sleep between its start and the clock read during execution SELECT macavity_arm('executor_start', 'delay'); macavity_arm -------------- 1 (1 row) SELECT clock_timestamp() - statement_timestamp() >= interval '0.5 second' AS delayed; delayed --------- t (1 row) SELECT state FROM macavity_status(); state ----------- completed (1 row) SELECT macavity_reset(); macavity_reset ---------------- 1 (1 row) -- a disarmed event never fires, however many hits go by SELECT macavity_arm('executor_start', 'error', 2); macavity_arm -------------- 1 (1 row) SELECT macavity_disarm(1); macavity_disarm ----------------- t (1 row) SELECT 1 AS safe_one; safe_one ---------- 1 (1 row) SELECT 2 AS safe_two; safe_two ---------- 2 (1 row) SELECT 3 AS safe_three; safe_three ------------ 3 (1 row) SELECT * FROM macavity_status(); event_id | point | action | occurrence | hits | remaining | state ----------+----------------+--------+------------+------+-----------+---------- 1 | executor_start | error | 2 | 1 | 1 | disarmed (1 row) SELECT macavity_reset(); macavity_reset ---------------- 1 (1 row) -- The arming statement fails after arming executor_end. Its ExecutorEnd -- never runs, so the skip reserved for it must not survive and swallow a -- later hit: with occurrence 2 the event fires at stmt_b, not stmt_c. -- (random() * 0 keeps the division from being folded at plan time, which -- would fail the statement before macavity_arm() ran at all.) SELECT macavity_arm('executor_end', 'error', 2), 1 / (random() * 0)::int; ERROR: division by zero SELECT 'stmt_a' AS stmt; stmt -------- stmt_a (1 row) SELECT 'stmt_b' AS stmt; ERROR: macavity: injected error at fault point "executor_end" SELECT 'stmt_c' AS stmt; stmt -------- stmt_c (1 row) SELECT event_id, hits, state FROM macavity_status(); event_id | hits | state ----------+------+----------- 1 | 2 | completed (1 row) SELECT macavity_reset(); macavity_reset ---------------- 1 (1 row) -- same, inside an explicit transaction block that is then rolled back BEGIN; SELECT macavity_arm('executor_end', 'error', 2), 1 / (random() * 0)::int; ERROR: division by zero ROLLBACK; SELECT 'stmt_a' AS stmt; stmt -------- stmt_a (1 row) SELECT 'stmt_b' AS stmt; ERROR: macavity: injected error at fault point "executor_end" SELECT 'stmt_c' AS stmt; stmt -------- stmt_c (1 row) SELECT macavity_reset(); macavity_reset ---------------- 1 (1 row) -- same, inside a PL/pgSQL exception block: no transaction abort happens, -- only a subtransaction rollback, and the skip must still be dropped DO $$ BEGIN BEGIN PERFORM macavity_arm('executor_end', 'error', 2), 1 / (random() * 0)::int; EXCEPTION WHEN division_by_zero THEN NULL; END; PERFORM 'a'; RAISE NOTICE 'after stmt_a'; PERFORM 'b'; RAISE NOTICE 'after stmt_b: not reached'; END $$; NOTICE: after stmt_a ERROR: macavity: injected error at fault point "executor_end" CONTEXT: SQL statement "SELECT 'b'" PL/pgSQL function inline_code_block line 9 at PERFORM SELECT event_id, hits, state FROM macavity_status(); event_id | hits | state ----------+------+----------- 1 | 2 | completed (1 row) SELECT macavity_reset(); macavity_reset ---------------- 1 (1 row) -- but an error inside the arming statement that does not kill it keeps the -- skip: the outer SELECT armed the event, caught an error in a nested -- block, and finished normally, so its own ExecutorEnd is still skipped and -- the next statement is the matching hit CREATE FUNCTION macavity_test_arm_then_catch() RETURNS integer LANGUAGE plpgsql AS $$ DECLARE id integer; BEGIN BEGIN id := macavity_arm('executor_end', 'error'); PERFORM 1 / (random() * 0)::int; EXCEPTION WHEN division_by_zero THEN NULL; END; RETURN id; END $$; SELECT macavity_test_arm_then_catch(); macavity_test_arm_then_catch ------------------------------ 1 (1 row) SELECT 1 AS statement_whose_end_fails; ERROR: macavity: injected error at fault point "executor_end" SELECT event_id, hits, state FROM macavity_status(); event_id | hits | state ----------+------+----------- 1 | 1 | completed (1 row) DROP FUNCTION macavity_test_arm_then_catch(); SELECT macavity_reset(); macavity_reset ---------------- 1 (1 row) -- before_abort fires only when a whole transaction aborts. Rolling back a -- subtransaction -- ROLLBACK TO SAVEPOINT, or a PL/pgSQL exception block -- catching an error -- is not a matching hit; the top-level ROLLBACK is. BEGIN; SELECT macavity_arm('before_abort', 'delay'); macavity_arm -------------- 1 (1 row) SAVEPOINT sp; SELECT 1 / (random() * 0)::int; ERROR: division by zero ROLLBACK TO SAVEPOINT sp; SELECT hits, state FROM macavity_status(); hits | state ------+------- 0 | armed (1 row) DO $$ BEGIN PERFORM 1 / (random() * 0)::int; EXCEPTION WHEN division_by_zero THEN NULL; END $$; SELECT hits, state FROM macavity_status(); hits | state ------+------- 0 | armed (1 row) ROLLBACK; SELECT hits, state FROM macavity_status(); hits | state ------+----------- 1 | completed (1 row) SELECT macavity_reset(); macavity_reset ---------------- 1 (1 row)