-- ============================================================================= -- SCENARIO 4 – TABLES NOT VACUUMED FOR X DAYS -- ============================================================================= -- Tables that have not been vacuumed (manually or by autovacuum) for an -- extended period accumulate dead tuples, leading to bloat and degraded -- query performance. This scenario identifies stale tables and ensures -- vacuum maintenance is current. -- ============================================================================= -- STEP 1: Identify tables not vacuumed / auto-vacuumed for X days -- Replace 'X' with the number of days threshold (e.g., 7, 30). SELECT schemaname, relname, last_vacuum, last_autovacuum, n_dead_tup FROM pg_stat_user_tables WHERE last_autovacuum IS NULL OR last_autovacuum < now() - interval '7 days' OR last_vacuum IS NULL OR last_vacuum < now() - interval '7 days' ORDER BY n_dead_tup DESC; -- STEP 2: Identify autovacuum blockers -- Run the common prerequisite query: -- \i examples/operational-scenarios/00_common_prerequisite.sql -- STEP 3: Resolve blockers -- Based on the blocker source, take the appropriate action: -- 3a. Terminate long-running backend sessions (if safe): -- SELECT pg_terminate_backend(); -- 3b. Drop unused replication slots: -- SELECT pg_drop_replication_slot(''); -- 3c. Resolve prepared transactions: -- COMMIT PREPARED ''; -- or -- ROLLBACK PREPARED ''; -- STEP 4: Run vacuum after blockers are resolved -- (commented out so sourcing this file with \i does not vacuum unexpectedly) -- VACUUM (ANALYZE); -- ============================================================================= -- PG_DURABLE VERSION – Stale Table Vacuum as a Durable Function -- ============================================================================= -- This version chains stale-table detection, blocker identification, -- remediation, and vacuum into a durable function graph. Uses ~> (sequence) -- to ensure each step completes before the next. Configurable day threshold. -- If the workflow fails mid-way, duroxide resumes from the last completed step. -- ============================================================================= -- Track stale tables and remediation DROP TABLE IF EXISTS stale_tables_log; CREATE TABLE stale_tables_log ( id SERIAL PRIMARY KEY, schema_name TEXT, table_name TEXT, last_vacuum TIMESTAMPTZ, last_autovacuum TIMESTAMPTZ, n_dead_tup BIGINT, days_since_vacuum NUMERIC, detected_at TIMESTAMPTZ DEFAULT now() ); DROP TABLE IF EXISTS stale_vacuum_action_log; CREATE TABLE stale_vacuum_action_log ( id SERIAL PRIMARY KEY, action TEXT, result TEXT, executed_at TIMESTAMPTZ DEFAULT now() ); -- Start the durable function: find stale tables → detect blockers → branch → vacuum CREATE TEMP TABLE _scenario4_state (instance_id TEXT); INSERT INTO _scenario4_state SELECT df.start( -- Step 1: Identify tables not vacuumed in the last 7 days -- (change the interval to match your threshold) 'INSERT INTO stale_tables_log (schema_name, table_name, last_vacuum, last_autovacuum, n_dead_tup, days_since_vacuum) SELECT schemaname, relname, last_vacuum, last_autovacuum, n_dead_tup, round(extract(epoch FROM now() - greatest( coalesce(last_vacuum, ''1970-01-01''::timestamptz), coalesce(last_autovacuum, ''1970-01-01''::timestamptz) )) / 86400, 1) FROM pg_stat_user_tables WHERE (last_autovacuum IS NULL OR last_autovacuum < now() - interval ''7 days'') AND (last_vacuum IS NULL OR last_vacuum < now() - interval ''7 days'') ORDER BY n_dead_tup DESC' ~> -- Step 2: Log autovacuum blockers 'INSERT INTO stale_vacuum_action_log (action, result) SELECT ''blocker_detected'', format(''source=%s, xmin_age=%s, details=%s'', source, xmin_age, details) FROM ( SELECT ''pg_stat_activity'' AS source, age(backend_xid) AS xmin_age, format(''pid=%s, state=%s, user=%s'', pid, state, usename) AS details FROM pg_stat_activity WHERE backend_xid IS NOT NULL UNION ALL SELECT ''pg_replication_slots'', age(catalog_xmin), format(''slot=%s, active=%s'', slot_name, active) FROM pg_replication_slots WHERE catalog_xmin IS NOT NULL UNION ALL SELECT ''pg_prepared_xacts'', age(transaction::xid), format(''gid=%s, db=%s'', gid, database) FROM pg_prepared_xacts WHERE transaction IS NOT NULL ) blockers ORDER BY xmin_age DESC' ~> -- Step 3: Branch — are there blockers? -- YES → wait for user approval, remediate, then vacuum -- NO → vacuum immediately (no user interaction needed) 'SELECT EXISTS( SELECT 1 FROM stale_vacuum_action_log WHERE action = ''blocker_detected'' )' ?> ( -- Blockers found: pause for user approval before remediation. -- Demo uses a timeout so the workflow auto-continues; in production -- omit it and approve with df.signal(, 'approve-stale-vacuum'). df.wait_for_signal('approve-stale-vacuum', 30) ~> -- Terminate idle-in-transaction backends older than 30 minutes 'INSERT INTO stale_vacuum_action_log (action, result) SELECT format(''terminated pid=%s (%s)'', pid, usename), pg_terminate_backend(pid)::text FROM pg_stat_activity WHERE state = ''idle in transaction'' AND backend_xid IS NOT NULL AND state_change < now() - interval ''30 minutes''' ~> -- Run VACUUM ANALYZE after blockers are cleared 'VACUUM (ANALYZE)' ) !> -- No blockers: vacuum immediately 'VACUUM (ANALYZE)' ~> -- Step 4: Record completion with summary 'INSERT INTO stale_vacuum_action_log (action, result) SELECT ''stale_vacuum_complete'', format(''Found %s stale tables, worst: %s.%s (%s dead tuples, %s days since vacuum)'', (SELECT count(*) FROM stale_tables_log), (SELECT schema_name FROM stale_tables_log ORDER BY n_dead_tup DESC LIMIT 1), (SELECT table_name FROM stale_tables_log ORDER BY n_dead_tup DESC LIMIT 1), (SELECT max(n_dead_tup) FROM stale_tables_log), (SELECT max(days_since_vacuum) FROM stale_tables_log))', 'scenario4-tables-not-vacuumed' ); -- Poll until the durable function completes (timeout ~60s) DO $$ DECLARE inst_id TEXT; status TEXT; attempts INT := 0; BEGIN SELECT instance_id INTO inst_id FROM _scenario4_state; LOOP SELECT s INTO status FROM df.status(inst_id) s; EXIT WHEN lower(status) IN ('completed', 'failed', 'cancelled') OR attempts > 600; PERFORM pg_sleep(0.1); attempts := attempts + 1; END LOOP; IF lower(status) != 'completed' THEN RAISE EXCEPTION 'SCENARIO 4 FAILED: durable function status = %', status; END IF; RAISE NOTICE 'SCENARIO 4 COMPLETED: stale table vacuum finished'; END $$; -- Review results SELECT * FROM stale_tables_log ORDER BY n_dead_tup DESC; SELECT * FROM stale_vacuum_action_log ORDER BY id; -- Cleanup DROP TABLE _scenario4_state; -- DROP TABLE stale_tables_log; -- DROP TABLE stale_vacuum_action_log;