forked from microsoft/pg_durable
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy path03_wraparound_risk.sql
More file actions
213 lines (184 loc) · 8.07 KB
/
Copy path03_wraparound_risk.sql
File metadata and controls
213 lines (184 loc) · 8.07 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
-- =============================================================================
-- SCENARIO 3 – WRAPAROUND RISK
-- =============================================================================
-- PostgreSQL uses 32-bit transaction IDs (XIDs). When a database approaches
-- the ~2 billion XID limit without freezing old rows, it risks entering
-- emergency shutdown mode. This scenario helps identify and mitigate
-- wraparound risk proactively.
-- =============================================================================
-- STEP 1: Identify database transaction age
-- Check which databases are closest to the wraparound limit.
SELECT
datname,
age(datfrozenxid) AS dat_xid_age,
2000000000 - age(datfrozenxid) AS txids_remaining
FROM pg_database
WHERE datallowconn
ORDER BY dat_xid_age DESC;
-- STEP 2: Identify databases with remaining transactions < 1 billion
-- Any database with txids_remaining < 1,000,000,000 needs attention.
SELECT
datname,
age(datfrozenxid) AS dat_xid_age,
2000000000 - age(datfrozenxid) AS txids_remaining
FROM pg_database
WHERE datallowconn
AND 2000000000 - age(datfrozenxid) < 1000000000
ORDER BY txids_remaining ASC;
-- STEP 3: Identify tables that need freezing
-- Lists tables sorted by how close they are to the wraparound threshold.
SELECT
c.relnamespace::regnamespace AS schema_name,
c.relname AS table_name,
greatest(age(c.relfrozenxid), age(t.relfrozenxid)) AS txid_age,
2^31 - 3000000 - greatest(age(c.relfrozenxid), age(t.relfrozenxid)) AS txid_remaining
FROM pg_class c
LEFT JOIN pg_class t ON c.reltoastrelid = t.oid
WHERE c.relkind IN ('r', 'm')
ORDER BY txid_remaining ASC;
-- STEP 4: Vacuum freeze the most at-risk tables
-- Replace schema.table with the actual schema and table name from Step 3.
-- VACUUM (VERBOSE, FREEZE, ANALYZE) schema.table;
-- =============================================================================
-- PG_DURABLE VERSION – Wraparound Risk Mitigation as a Durable Function
-- =============================================================================
-- This version chains wraparound detection, at-risk table identification,
-- blocker remediation, and targeted VACUUM FREEZE into a durable function
-- graph. Uses ~> (sequence) to ensure each step completes before the next.
-- If the workflow fails (e.g., vacuum killed by OOM), duroxide resumes from
-- the last completed step on retry.
-- =============================================================================
-- Track wraparound diagnostics
DROP TABLE IF EXISTS wraparound_db_log;
CREATE TABLE wraparound_db_log (
id SERIAL PRIMARY KEY,
datname TEXT,
dat_xid_age BIGINT,
txids_remaining BIGINT,
detected_at TIMESTAMPTZ DEFAULT now()
);
DROP TABLE IF EXISTS wraparound_table_log;
CREATE TABLE wraparound_table_log (
id SERIAL PRIMARY KEY,
schema_name TEXT,
table_name TEXT,
txid_age BIGINT,
txid_remaining BIGINT,
detected_at TIMESTAMPTZ DEFAULT now()
);
DROP TABLE IF EXISTS wraparound_action_log;
CREATE TABLE wraparound_action_log (
id SERIAL PRIMARY KEY,
action TEXT,
result TEXT,
executed_at TIMESTAMPTZ DEFAULT now()
);
-- Start the durable function: detect DB risk → find tables → branch on blockers → freeze
CREATE TEMP TABLE _scenario3_state (instance_id TEXT);
INSERT INTO _scenario3_state SELECT df.start(
-- Step 1: Log database-level transaction ages
'INSERT INTO wraparound_db_log (datname, dat_xid_age, txids_remaining)
SELECT datname, age(datfrozenxid),
2000000000 - age(datfrozenxid)
FROM pg_database
WHERE datallowconn
ORDER BY age(datfrozenxid) DESC'
~>
-- Step 2: Log tables closest to wraparound (top 50 most at-risk)
'INSERT INTO wraparound_table_log (schema_name, table_name, txid_age, txid_remaining)
SELECT
c.relnamespace::regnamespace::text,
c.relname,
greatest(age(c.relfrozenxid), age(t.relfrozenxid)),
(2^31 - 3000000 - greatest(age(c.relfrozenxid), age(t.relfrozenxid)))::bigint
FROM pg_class c
LEFT JOIN pg_class t ON c.reltoastrelid = t.oid
WHERE c.relkind IN (''r'', ''m'')
ORDER BY greatest(age(c.relfrozenxid), age(t.relfrozenxid)) DESC
LIMIT 50'
~>
-- Step 3: Log autovacuum blockers
'INSERT INTO wraparound_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'', pid, state) 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'', gid)
FROM pg_prepared_xacts WHERE transaction IS NOT NULL
) blockers ORDER BY xmin_age DESC'
~>
-- Step 4: Branch — are there blockers?
-- YES → wait for user approval, remediate blockers, then VACUUM FREEZE
-- NO → VACUUM FREEZE immediately (no user interaction needed)
'SELECT EXISTS(
SELECT 1 FROM wraparound_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(<instance_id>, 'approve-wraparound-remediation').
df.wait_for_signal('approve-wraparound-remediation', 30)
~>
-- Terminate idle-in-transaction backends blocking vacuum
'INSERT INTO wraparound_action_log (action, result)
SELECT format(''terminated pid=%s'', pid),
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'''
~>
-- VACUUM FREEZE after blockers are cleared
'VACUUM (FREEZE, ANALYZE)'
)
!>
-- No blockers: VACUUM FREEZE immediately
'VACUUM (FREEZE, ANALYZE)'
~>
-- Step 5: Record completion with risk summary
'INSERT INTO wraparound_action_log (action, result)
SELECT ''wraparound_remediation_complete'',
format(''Databases at risk: %s, Most urgent table: %s.%s (remaining: %s txids)'',
(SELECT count(*) FROM wraparound_db_log WHERE txids_remaining < 1000000000),
(SELECT schema_name FROM wraparound_table_log ORDER BY txid_remaining ASC LIMIT 1),
(SELECT table_name FROM wraparound_table_log ORDER BY txid_remaining ASC LIMIT 1),
(SELECT txid_remaining FROM wraparound_table_log ORDER BY txid_remaining ASC LIMIT 1))',
'scenario3-wraparound-risk'
);
-- 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 _scenario3_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 3 FAILED: durable function status = %', status;
END IF;
RAISE NOTICE 'SCENARIO 3 COMPLETED: wraparound risk assessed and mitigated';
END $$;
-- Review results
SELECT * FROM wraparound_db_log ORDER BY dat_xid_age DESC;
SELECT * FROM wraparound_table_log ORDER BY txid_remaining ASC LIMIT 20;
SELECT * FROM wraparound_action_log ORDER BY id;
-- Cleanup
DROP TABLE _scenario3_state;
-- DROP TABLE wraparound_db_log;
-- DROP TABLE wraparound_table_log;
-- DROP TABLE wraparound_action_log;