@team-semicolon/semicolony-cli 4.18.51 → 4.18.53

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,406 @@
1
+ -- @schema-retarget: off
2
+ -- Durable, credential-free Hive cron failure-alert authority.
3
+ -- The migration runner supplies the transaction boundary; do not add one here.
4
+
5
+ DO $$
6
+ DECLARE
7
+ role_state record;
8
+ BEGIN
9
+ SELECT rolcanlogin, rolsuper, rolcreatedb, rolcreaterole,
10
+ rolinherit, rolreplication, rolbypassrls
11
+ INTO role_state
12
+ FROM pg_catalog.pg_roles
13
+ WHERE rolname = 'sc_provider';
14
+
15
+ IF NOT FOUND THEN
16
+ RAISE EXCEPTION 'sc_provider role missing; migration 135 required';
17
+ END IF;
18
+
19
+ IF NOT role_state.rolcanlogin
20
+ OR role_state.rolsuper
21
+ OR role_state.rolcreatedb
22
+ OR role_state.rolcreaterole
23
+ OR role_state.rolinherit
24
+ OR role_state.rolreplication
25
+ OR role_state.rolbypassrls THEN
26
+ RAISE EXCEPTION 'sc_provider role attributes are unsafe';
27
+ END IF;
28
+ END $$;
29
+
30
+ CREATE TABLE semicolony.cron_read_action_failure_alert_outbox (
31
+ occurrence_key TEXT PRIMARY KEY,
32
+ failure_code TEXT NOT NULL CHECK (
33
+ failure_code IN ('released_failed', 'retry_exhausted', 'stale_binding')
34
+ ),
35
+ state TEXT NOT NULL DEFAULT 'pending' CHECK (
36
+ state IN ('pending', 'claimed', 'sent', 'failed', 'ambiguous')
37
+ ),
38
+ claim_id TEXT UNIQUE CHECK (
39
+ claim_id IS NULL
40
+ OR (claim_id COLLATE "C") ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$'
41
+ ),
42
+ claimed_at TIMESTAMPTZ,
43
+ claim_expires_at TIMESTAMPTZ,
44
+ resolved_at TIMESTAMPTZ,
45
+ created_at TIMESTAMPTZ NOT NULL DEFAULT pg_catalog.clock_timestamp(),
46
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT pg_catalog.clock_timestamp(),
47
+ FOREIGN KEY (occurrence_key)
48
+ REFERENCES semicolony.cron_read_action_dispatch_outbox (idempotency_key),
49
+ CHECK (
50
+ (
51
+ state = 'pending'
52
+ AND claim_id IS NULL
53
+ AND claimed_at IS NULL
54
+ AND claim_expires_at IS NULL
55
+ AND resolved_at IS NULL
56
+ )
57
+ OR
58
+ (
59
+ state = 'claimed'
60
+ AND claim_id IS NOT NULL
61
+ AND claimed_at IS NOT NULL
62
+ AND claim_expires_at IS NOT NULL
63
+ AND claim_expires_at > claimed_at
64
+ AND resolved_at IS NULL
65
+ )
66
+ OR
67
+ (
68
+ state IN ('sent', 'failed', 'ambiguous')
69
+ AND claim_id IS NOT NULL
70
+ AND claimed_at IS NOT NULL
71
+ AND claim_expires_at IS NOT NULL
72
+ AND claim_expires_at > claimed_at
73
+ AND resolved_at IS NOT NULL
74
+ )
75
+ )
76
+ );
77
+
78
+ CREATE INDEX idx_cron_read_action_failure_alert_pending
79
+ ON semicolony.cron_read_action_failure_alert_outbox (created_at, occurrence_key)
80
+ WHERE state = 'pending';
81
+
82
+ CREATE INDEX idx_cron_read_action_failure_alert_expired
83
+ ON semicolony.cron_read_action_failure_alert_outbox (claim_expires_at, occurrence_key)
84
+ WHERE state = 'claimed';
85
+
86
+ CREATE FUNCTION semicolony.hive_cron_protect_failure_alert()
87
+ RETURNS trigger
88
+ LANGUAGE plpgsql
89
+ SECURITY DEFINER
90
+ SET search_path = pg_catalog
91
+ SET TimeZone = 'UTC'
92
+ AS $$
93
+ BEGIN
94
+ IF TG_OP = 'DELETE' THEN
95
+ RAISE EXCEPTION 'failure_alert_delete_denied' USING ERRCODE = '55000';
96
+ END IF;
97
+
98
+ IF NEW.occurrence_key IS DISTINCT FROM OLD.occurrence_key
99
+ OR NEW.failure_code IS DISTINCT FROM OLD.failure_code
100
+ OR NEW.created_at IS DISTINCT FROM OLD.created_at THEN
101
+ RAISE EXCEPTION 'failure_alert_identity_immutable' USING ERRCODE = '55000';
102
+ END IF;
103
+
104
+ IF OLD.state IN ('sent', 'failed', 'ambiguous') THEN
105
+ RAISE EXCEPTION 'failure_alert_terminal_immutable' USING ERRCODE = '55000';
106
+ END IF;
107
+
108
+ IF OLD.state = 'pending' AND NEW.state <> 'claimed' THEN
109
+ RAISE EXCEPTION 'invalid_failure_alert_transition' USING ERRCODE = '55000';
110
+ END IF;
111
+
112
+ IF OLD.state = 'claimed' AND NEW.state NOT IN ('sent', 'failed', 'ambiguous') THEN
113
+ RAISE EXCEPTION 'invalid_failure_alert_transition' USING ERRCODE = '55000';
114
+ END IF;
115
+
116
+ RETURN NEW;
117
+ END;
118
+ $$;
119
+
120
+ CREATE TRIGGER protect_cron_read_action_failure_alert
121
+ BEFORE UPDATE OR DELETE ON semicolony.cron_read_action_failure_alert_outbox
122
+ FOR EACH ROW EXECUTE FUNCTION semicolony.hive_cron_protect_failure_alert();
123
+
124
+ CREATE FUNCTION semicolony.hive_cron_record_failure_alert()
125
+ RETURNS trigger
126
+ LANGUAGE plpgsql
127
+ SECURITY DEFINER
128
+ SET search_path = pg_catalog
129
+ SET TimeZone = 'UTC'
130
+ AS $$
131
+ DECLARE
132
+ failure_count integer;
133
+ BEGIN
134
+ IF OLD.status IS DISTINCT FROM 'failed'
135
+ AND NEW.status = 'failed'
136
+ AND NEW.result_code IN ('released_failed', 'retry_exhausted', 'stale_binding') THEN
137
+ UPDATE semicolony.bot_cron_jobs AS job
138
+ SET consecutive_failures = job.consecutive_failures + 1,
139
+ last_status = 'failure',
140
+ last_error = NEW.result_code,
141
+ last_run = NEW.resolved_at
142
+ WHERE job.bot_id = 'semi'
143
+ AND job.job_id = NEW.job_id
144
+ AND job.runtime_source = 'hive-cron-v2'
145
+ RETURNING job.consecutive_failures INTO failure_count;
146
+
147
+ IF NOT FOUND THEN
148
+ RAISE EXCEPTION 'failure_alert_job_authority_missing' USING ERRCODE = 'P0001';
149
+ END IF;
150
+
151
+ IF failure_count = 3 THEN
152
+ INSERT INTO semicolony.cron_read_action_failure_alert_outbox (
153
+ occurrence_key, failure_code
154
+ ) VALUES (
155
+ NEW.idempotency_key, NEW.result_code
156
+ );
157
+ END IF;
158
+ END IF;
159
+
160
+ RETURN NEW;
161
+ END;
162
+ $$;
163
+
164
+ CREATE TRIGGER record_hive_cron_failure_alert
165
+ AFTER UPDATE ON semicolony.cron_read_action_dispatch_outbox
166
+ FOR EACH ROW
167
+ WHEN (
168
+ OLD.status IS DISTINCT FROM 'failed'
169
+ AND NEW.status = 'failed'
170
+ AND NEW.result_code IN ('released_failed', 'retry_exhausted', 'stale_binding')
171
+ )
172
+ EXECUTE FUNCTION semicolony.hive_cron_record_failure_alert();
173
+
174
+ CREATE FUNCTION semicolony.hive_cron_claim_failure_alert(
175
+ p_claim_id text,
176
+ p_lease_seconds integer
177
+ )
178
+ RETURNS TABLE (
179
+ outcome text,
180
+ occurrence_key text,
181
+ failure_code text,
182
+ claim_id text,
183
+ claimed_at timestamptz,
184
+ claim_expires_at timestamptz,
185
+ resolved_at timestamptz
186
+ )
187
+ LANGUAGE plpgsql
188
+ SECURITY DEFINER
189
+ SET search_path = pg_catalog
190
+ SET TimeZone = 'UTC'
191
+ AS $$
192
+ DECLARE
193
+ alert record;
194
+ db_now timestamptz;
195
+ BEGIN
196
+ IF session_user <> 'sc_provider'
197
+ OR public.is_provider() IS DISTINCT FROM true THEN
198
+ RAISE EXCEPTION 'provider_mode_required' USING ERRCODE = '42501';
199
+ END IF;
200
+
201
+ IF p_claim_id IS NULL
202
+ OR (p_claim_id COLLATE "C") !~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$'
203
+ OR p_lease_seconds IS NULL
204
+ OR p_lease_seconds NOT BETWEEN 1 AND 3600 THEN
205
+ RAISE EXCEPTION 'invalid_failure_alert_claim' USING ERRCODE = '22023';
206
+ END IF;
207
+
208
+ PERFORM pg_catalog.pg_advisory_xact_lock(
209
+ pg_catalog.hashtextextended(p_claim_id, 0::bigint)
210
+ );
211
+ db_now := pg_catalog.clock_timestamp();
212
+
213
+ SELECT current_alert.occurrence_key,
214
+ current_alert.failure_code,
215
+ current_alert.state,
216
+ current_alert.claim_id,
217
+ current_alert.claimed_at,
218
+ current_alert.claim_expires_at,
219
+ current_alert.resolved_at
220
+ INTO alert
221
+ FROM semicolony.cron_read_action_failure_alert_outbox AS current_alert
222
+ WHERE current_alert.claim_id = p_claim_id
223
+ FOR UPDATE;
224
+
225
+ IF FOUND THEN
226
+ IF alert.state = 'claimed' AND alert.claim_expires_at <= db_now THEN
227
+ UPDATE semicolony.cron_read_action_failure_alert_outbox AS current_alert
228
+ SET state = 'ambiguous',
229
+ resolved_at = db_now,
230
+ updated_at = db_now
231
+ WHERE current_alert.occurrence_key = alert.occurrence_key;
232
+
233
+ RETURN QUERY SELECT
234
+ 'recovered_ambiguous'::text,
235
+ alert.occurrence_key,
236
+ alert.failure_code,
237
+ alert.claim_id,
238
+ alert.claimed_at,
239
+ alert.claim_expires_at,
240
+ db_now;
241
+ ELSE
242
+ RETURN QUERY SELECT
243
+ 'claim_exists'::text,
244
+ alert.occurrence_key,
245
+ alert.failure_code,
246
+ alert.claim_id,
247
+ alert.claimed_at,
248
+ alert.claim_expires_at,
249
+ alert.resolved_at;
250
+ END IF;
251
+ RETURN;
252
+ END IF;
253
+
254
+ SELECT current_alert.occurrence_key,
255
+ current_alert.failure_code,
256
+ current_alert.claim_id,
257
+ current_alert.claimed_at,
258
+ current_alert.claim_expires_at
259
+ INTO alert
260
+ FROM semicolony.cron_read_action_failure_alert_outbox AS current_alert
261
+ WHERE current_alert.state = 'claimed'
262
+ AND current_alert.claim_expires_at <= db_now
263
+ ORDER BY current_alert.claim_expires_at, current_alert.occurrence_key
264
+ LIMIT 1
265
+ FOR UPDATE;
266
+
267
+ IF FOUND THEN
268
+ UPDATE semicolony.cron_read_action_failure_alert_outbox AS current_alert
269
+ SET state = 'ambiguous',
270
+ resolved_at = db_now,
271
+ updated_at = db_now
272
+ WHERE current_alert.occurrence_key = alert.occurrence_key;
273
+
274
+ RETURN QUERY SELECT
275
+ 'recovered_ambiguous'::text,
276
+ alert.occurrence_key,
277
+ alert.failure_code,
278
+ alert.claim_id,
279
+ alert.claimed_at,
280
+ alert.claim_expires_at,
281
+ db_now;
282
+ RETURN;
283
+ END IF;
284
+
285
+ SELECT current_alert.occurrence_key,
286
+ current_alert.failure_code
287
+ INTO alert
288
+ FROM semicolony.cron_read_action_failure_alert_outbox AS current_alert
289
+ WHERE current_alert.state = 'pending'
290
+ ORDER BY current_alert.created_at, current_alert.occurrence_key
291
+ LIMIT 1
292
+ FOR UPDATE SKIP LOCKED;
293
+
294
+ IF NOT FOUND THEN
295
+ RETURN;
296
+ END IF;
297
+
298
+ UPDATE semicolony.cron_read_action_failure_alert_outbox AS current_alert
299
+ SET state = 'claimed',
300
+ claim_id = p_claim_id,
301
+ claimed_at = db_now,
302
+ claim_expires_at = db_now + pg_catalog.make_interval(secs => p_lease_seconds),
303
+ updated_at = db_now
304
+ WHERE current_alert.occurrence_key = alert.occurrence_key;
305
+
306
+ RETURN QUERY SELECT
307
+ 'claimed'::text,
308
+ alert.occurrence_key,
309
+ alert.failure_code,
310
+ p_claim_id,
311
+ db_now,
312
+ db_now + pg_catalog.make_interval(secs => p_lease_seconds),
313
+ NULL::timestamptz;
314
+ END;
315
+ $$;
316
+
317
+ CREATE FUNCTION semicolony.hive_cron_complete_failure_alert(
318
+ p_occurrence_key text,
319
+ p_claim_id text,
320
+ p_outcome text
321
+ )
322
+ RETURNS TABLE (outcome text)
323
+ LANGUAGE plpgsql
324
+ SECURITY DEFINER
325
+ SET search_path = pg_catalog
326
+ SET TimeZone = 'UTC'
327
+ AS $$
328
+ DECLARE
329
+ alert record;
330
+ db_now timestamptz;
331
+ BEGIN
332
+ IF session_user <> 'sc_provider'
333
+ OR public.is_provider() IS DISTINCT FROM true THEN
334
+ RAISE EXCEPTION 'provider_mode_required' USING ERRCODE = '42501';
335
+ END IF;
336
+
337
+ IF p_occurrence_key IS NULL
338
+ OR (p_occurrence_key COLLATE "C") !~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,255}$'
339
+ OR p_claim_id IS NULL
340
+ OR (p_claim_id COLLATE "C") !~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$'
341
+ OR p_outcome IS NULL
342
+ OR p_outcome NOT IN ('sent', 'failed', 'ambiguous') THEN
343
+ RAISE EXCEPTION 'invalid_failure_alert_completion' USING ERRCODE = '22023';
344
+ END IF;
345
+
346
+ db_now := pg_catalog.clock_timestamp();
347
+
348
+ SELECT current_alert.state,
349
+ current_alert.claim_id
350
+ INTO alert
351
+ FROM semicolony.cron_read_action_failure_alert_outbox AS current_alert
352
+ WHERE current_alert.occurrence_key = p_occurrence_key
353
+ FOR UPDATE;
354
+
355
+ IF NOT FOUND THEN
356
+ outcome := 'not_found';
357
+ RETURN NEXT;
358
+ RETURN;
359
+ END IF;
360
+
361
+ IF alert.claim_id IS DISTINCT FROM p_claim_id THEN
362
+ outcome := 'claim_mismatch';
363
+ RETURN NEXT;
364
+ RETURN;
365
+ END IF;
366
+
367
+ IF alert.state IN ('sent', 'failed', 'ambiguous') THEN
368
+ outcome := CASE WHEN alert.state = p_outcome THEN 'existing' ELSE 'outcome_mismatch' END;
369
+ RETURN NEXT;
370
+ RETURN;
371
+ END IF;
372
+
373
+ IF alert.state <> 'claimed' THEN
374
+ outcome := 'claim_mismatch';
375
+ RETURN NEXT;
376
+ RETURN;
377
+ END IF;
378
+
379
+ UPDATE semicolony.cron_read_action_failure_alert_outbox AS current_alert
380
+ SET state = p_outcome,
381
+ resolved_at = db_now,
382
+ updated_at = db_now
383
+ WHERE current_alert.occurrence_key = p_occurrence_key;
384
+
385
+ outcome := 'completed';
386
+ RETURN NEXT;
387
+ END;
388
+ $$;
389
+
390
+ REVOKE ALL PRIVILEGES ON TABLE
391
+ semicolony.cron_read_action_failure_alert_outbox
392
+ FROM PUBLIC, sc_app, sc_provider;
393
+
394
+ REVOKE ALL ON FUNCTION semicolony.hive_cron_protect_failure_alert()
395
+ FROM PUBLIC, sc_app, sc_provider;
396
+ REVOKE ALL ON FUNCTION semicolony.hive_cron_record_failure_alert()
397
+ FROM PUBLIC, sc_app, sc_provider;
398
+ REVOKE ALL ON FUNCTION semicolony.hive_cron_claim_failure_alert(text, integer)
399
+ FROM PUBLIC, sc_app, sc_provider;
400
+ REVOKE ALL ON FUNCTION semicolony.hive_cron_complete_failure_alert(text, text, text)
401
+ FROM PUBLIC, sc_app, sc_provider;
402
+
403
+ GRANT EXECUTE ON FUNCTION semicolony.hive_cron_claim_failure_alert(text, integer)
404
+ TO sc_provider;
405
+ GRANT EXECUTE ON FUNCTION semicolony.hive_cron_complete_failure_alert(text, text, text)
406
+ TO sc_provider;