@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.
- package/dist/bundle.js +1524 -1046
- package/migrations/149_hive_cron_capability_templates.sql +44 -0
- package/migrations/150_hive_cron_action_contract.sql +38 -0
- package/migrations/151_hive_cron_occurrence_leases.sql +150 -0
- package/migrations/152_hive_cron_relay_outbox.sql +243 -0
- package/migrations/153_hive_cron_evidence_uniqueness.sql +9 -0
- package/migrations/154_hive_cron_evidence_provider_grants.sql +816 -0
- package/migrations/155_hive_cron_occurrence_lease_provider_grants.sql +44 -0
- package/migrations/156_hive_cron_relay_outbox_provider_grants.sql +47 -0
- package/migrations/157_hive_cron_trusted_config_revisions.sql +243 -0
- package/migrations/158_hive_cron_typed_dispatch_outbox.sql +1406 -0
- package/migrations/159_hive_cron_shadow_cursor.sql +600 -0
- package/migrations/160_hive_cron_failure_alert_outbox.sql +406 -0
- package/migrations/161_skill_governance.sql +529 -0
- package/migrations/161_workspace_projection_grants.sql +39 -0
- package/migrations/162_sc_app_kb_history_trigger_hardening.sql +96 -0
- package/migrations/163_workspace_projection_grants_sc_app_read.sql +21 -0
- package/package.json +4 -2
|
@@ -0,0 +1,1406 @@
|
|
|
1
|
+
-- @schema-retarget: off
|
|
2
|
+
-- Durable, credential-free dispatch identities for Hive cron typed read actions.
|
|
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_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 FUNCTION semicolony.hive_cron_resource_scopes_are_canonical(p_scopes text[])
|
|
31
|
+
RETURNS boolean
|
|
32
|
+
LANGUAGE sql
|
|
33
|
+
IMMUTABLE
|
|
34
|
+
SECURITY DEFINER
|
|
35
|
+
SET search_path = pg_catalog
|
|
36
|
+
SET TimeZone = 'UTC'
|
|
37
|
+
AS $$
|
|
38
|
+
SELECT COALESCE(
|
|
39
|
+
p_scopes IS NOT NULL
|
|
40
|
+
AND pg_catalog.cardinality(p_scopes) BETWEEN 1 AND 32
|
|
41
|
+
AND pg_catalog.array_position(p_scopes, NULL) IS NULL
|
|
42
|
+
AND p_scopes = ARRAY(
|
|
43
|
+
SELECT scope
|
|
44
|
+
FROM pg_catalog.unnest(p_scopes) AS scope
|
|
45
|
+
ORDER BY scope COLLATE "C"
|
|
46
|
+
)
|
|
47
|
+
AND pg_catalog.cardinality(p_scopes) = (
|
|
48
|
+
SELECT pg_catalog.count(DISTINCT scope COLLATE "C")::integer
|
|
49
|
+
FROM pg_catalog.unnest(p_scopes) AS scope
|
|
50
|
+
)
|
|
51
|
+
AND NOT EXISTS (
|
|
52
|
+
SELECT 1
|
|
53
|
+
FROM pg_catalog.unnest(p_scopes) AS scope
|
|
54
|
+
WHERE (scope COLLATE "C") !~ '^[!-~]+$'
|
|
55
|
+
OR pg_catalog.char_length(scope) NOT BETWEEN 1 AND 256
|
|
56
|
+
OR pg_catalog.strpos(scope, '*') > 0
|
|
57
|
+
),
|
|
58
|
+
false
|
|
59
|
+
);
|
|
60
|
+
$$;
|
|
61
|
+
|
|
62
|
+
REVOKE ALL ON FUNCTION semicolony.hive_cron_resource_scopes_are_canonical(text[])
|
|
63
|
+
FROM PUBLIC, sc_app, sc_provider;
|
|
64
|
+
|
|
65
|
+
CREATE TABLE semicolony.cron_read_action_dispatch_outbox (
|
|
66
|
+
bot_id TEXT NOT NULL CHECK (bot_id = 'semi'),
|
|
67
|
+
job_id TEXT NOT NULL CHECK (
|
|
68
|
+
(job_id COLLATE "C") ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$'
|
|
69
|
+
),
|
|
70
|
+
runtime_source TEXT NOT NULL CHECK (runtime_source = 'hive-cron-v2'),
|
|
71
|
+
scheduled_for TIMESTAMPTZ NOT NULL,
|
|
72
|
+
idempotency_key TEXT NOT NULL,
|
|
73
|
+
capability_template_ref TEXT NOT NULL,
|
|
74
|
+
template_updated_at TIMESTAMPTZ NOT NULL,
|
|
75
|
+
permission TEXT NOT NULL CHECK (permission = 'read'),
|
|
76
|
+
resource_scopes TEXT[] NOT NULL CHECK (
|
|
77
|
+
semicolony.hive_cron_resource_scopes_are_canonical(resource_scopes)
|
|
78
|
+
),
|
|
79
|
+
destination TEXT NOT NULL CHECK (
|
|
80
|
+
pg_catalog.char_length(destination) BETWEEN 1 AND 256
|
|
81
|
+
),
|
|
82
|
+
timeout_seconds INTEGER NOT NULL CHECK (timeout_seconds BETWEEN 1 AND 3600),
|
|
83
|
+
expected_outcome_digest TEXT NOT NULL CHECK (
|
|
84
|
+
expected_outcome_digest ~ '^sha256:[a-f0-9]{64}$'
|
|
85
|
+
),
|
|
86
|
+
source_hash TEXT NOT NULL CHECK (source_hash ~ '^sha256:[a-f0-9]{64}$'),
|
|
87
|
+
action_kind TEXT NOT NULL CHECK (
|
|
88
|
+
pg_catalog.char_length(action_kind) BETWEEN 1 AND 256
|
|
89
|
+
),
|
|
90
|
+
action_version INTEGER NOT NULL CHECK (action_version > 0),
|
|
91
|
+
action_contract_digest TEXT NOT NULL CHECK (
|
|
92
|
+
action_contract_digest ~ '^[a-f0-9]{64}$'
|
|
93
|
+
),
|
|
94
|
+
trusted_config_revision INTEGER NOT NULL CHECK (trusted_config_revision > 0),
|
|
95
|
+
trusted_config_ref TEXT NOT NULL,
|
|
96
|
+
trusted_config_digest TEXT NOT NULL CHECK (
|
|
97
|
+
trusted_config_digest ~ '^sha256:[a-f0-9]{64}$'
|
|
98
|
+
),
|
|
99
|
+
occurrence_claim_id TEXT NOT NULL CHECK (
|
|
100
|
+
occurrence_claim_id ~ '^scheduled-[a-f0-9]{64}$'
|
|
101
|
+
),
|
|
102
|
+
status TEXT NOT NULL DEFAULT 'pending'
|
|
103
|
+
CHECK (status IN ('pending', 'claimed', 'completed', 'failed', 'blocked')),
|
|
104
|
+
claim_id TEXT CHECK (
|
|
105
|
+
claim_id IS NULL
|
|
106
|
+
OR (claim_id COLLATE "C") ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$'
|
|
107
|
+
),
|
|
108
|
+
claimed_at TIMESTAMPTZ,
|
|
109
|
+
claim_expires_at TIMESTAMPTZ,
|
|
110
|
+
attempt_count INTEGER NOT NULL DEFAULT 0 CHECK (attempt_count BETWEEN 0 AND 3),
|
|
111
|
+
next_attempt_at TIMESTAMPTZ,
|
|
112
|
+
result_code TEXT CHECK (
|
|
113
|
+
result_code IS NULL
|
|
114
|
+
OR result_code IN (
|
|
115
|
+
'completed',
|
|
116
|
+
'released_failed',
|
|
117
|
+
'retry_exhausted',
|
|
118
|
+
'ambiguous_authority_state',
|
|
119
|
+
'stale_binding'
|
|
120
|
+
)
|
|
121
|
+
),
|
|
122
|
+
resolved_at TIMESTAMPTZ,
|
|
123
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
124
|
+
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
125
|
+
PRIMARY KEY (idempotency_key),
|
|
126
|
+
UNIQUE (job_id, scheduled_for),
|
|
127
|
+
UNIQUE (claim_id),
|
|
128
|
+
UNIQUE (occurrence_claim_id),
|
|
129
|
+
FOREIGN KEY (bot_id, job_id, runtime_source)
|
|
130
|
+
REFERENCES semicolony.cron_capability_templates (bot_id, job_id, runtime_source),
|
|
131
|
+
FOREIGN KEY (bot_id, job_id, runtime_source, trusted_config_revision)
|
|
132
|
+
REFERENCES semicolony.cron_read_action_trusted_config_revisions
|
|
133
|
+
(bot_id, job_id, runtime_source, config_revision),
|
|
134
|
+
CHECK (idempotency_key = 'cron:' || job_id || ':' ||
|
|
135
|
+
to_char(scheduled_for AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')),
|
|
136
|
+
CHECK (occurrence_claim_id = 'scheduled-' || pg_catalog.encode(
|
|
137
|
+
pg_catalog.sha256(pg_catalog.convert_to(idempotency_key, 'UTF8')),
|
|
138
|
+
'hex'
|
|
139
|
+
)),
|
|
140
|
+
CHECK (capability_template_ref = 'semi:' || job_id || ':hive-cron-v2'),
|
|
141
|
+
CHECK (trusted_config_ref = 'semi:' || job_id || ':hive-cron-v2:config:r' ||
|
|
142
|
+
trusted_config_revision::text),
|
|
143
|
+
CHECK (
|
|
144
|
+
(
|
|
145
|
+
status = 'pending'
|
|
146
|
+
AND claim_id IS NULL
|
|
147
|
+
AND claimed_at IS NULL
|
|
148
|
+
AND claim_expires_at IS NULL
|
|
149
|
+
AND next_attempt_at IS NOT NULL
|
|
150
|
+
AND result_code IS NULL
|
|
151
|
+
AND resolved_at IS NULL
|
|
152
|
+
)
|
|
153
|
+
OR
|
|
154
|
+
(
|
|
155
|
+
status = 'claimed'
|
|
156
|
+
AND claim_id IS NOT NULL
|
|
157
|
+
AND claimed_at IS NOT NULL
|
|
158
|
+
AND claim_expires_at IS NOT NULL
|
|
159
|
+
AND claim_expires_at > claimed_at
|
|
160
|
+
AND next_attempt_at IS NULL
|
|
161
|
+
AND result_code IS NULL
|
|
162
|
+
AND resolved_at IS NULL
|
|
163
|
+
)
|
|
164
|
+
OR
|
|
165
|
+
(
|
|
166
|
+
status IN ('completed', 'failed', 'blocked')
|
|
167
|
+
AND claim_id IS NULL
|
|
168
|
+
AND claimed_at IS NULL
|
|
169
|
+
AND claim_expires_at IS NULL
|
|
170
|
+
AND next_attempt_at IS NULL
|
|
171
|
+
AND result_code IS NOT NULL
|
|
172
|
+
AND resolved_at IS NOT NULL
|
|
173
|
+
AND (
|
|
174
|
+
(status = 'completed' AND result_code = 'completed')
|
|
175
|
+
OR
|
|
176
|
+
(status = 'failed' AND result_code IN ('released_failed', 'retry_exhausted', 'stale_binding'))
|
|
177
|
+
OR
|
|
178
|
+
(status = 'blocked' AND result_code = 'ambiguous_authority_state')
|
|
179
|
+
)
|
|
180
|
+
)
|
|
181
|
+
)
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
CREATE INDEX idx_cron_read_action_dispatch_pending
|
|
185
|
+
ON semicolony.cron_read_action_dispatch_outbox
|
|
186
|
+
(next_attempt_at, scheduled_for, idempotency_key)
|
|
187
|
+
WHERE status = 'pending';
|
|
188
|
+
|
|
189
|
+
CREATE INDEX idx_cron_read_action_dispatch_abandoned
|
|
190
|
+
ON semicolony.cron_read_action_dispatch_outbox (claim_expires_at, idempotency_key)
|
|
191
|
+
WHERE status = 'claimed';
|
|
192
|
+
|
|
193
|
+
CREATE FUNCTION semicolony.hive_cron_protect_terminal_dispatch()
|
|
194
|
+
RETURNS trigger
|
|
195
|
+
LANGUAGE plpgsql
|
|
196
|
+
SECURITY DEFINER
|
|
197
|
+
SET search_path = pg_catalog
|
|
198
|
+
SET TimeZone = 'UTC'
|
|
199
|
+
AS $$
|
|
200
|
+
BEGIN
|
|
201
|
+
IF session_user <> 'sc_provider'
|
|
202
|
+
OR public.is_provider() IS DISTINCT FROM true THEN
|
|
203
|
+
RAISE EXCEPTION 'provider_mode_required' USING ERRCODE = '42501';
|
|
204
|
+
END IF;
|
|
205
|
+
IF OLD.status IN ('completed', 'failed', 'blocked') THEN
|
|
206
|
+
RAISE EXCEPTION 'terminal_dispatch_immutable' USING ERRCODE = '55000';
|
|
207
|
+
END IF;
|
|
208
|
+
IF TG_OP = 'DELETE' THEN
|
|
209
|
+
RETURN OLD;
|
|
210
|
+
END IF;
|
|
211
|
+
RETURN NEW;
|
|
212
|
+
END;
|
|
213
|
+
$$;
|
|
214
|
+
|
|
215
|
+
CREATE TRIGGER protect_terminal_cron_read_action_dispatch
|
|
216
|
+
BEFORE UPDATE OR DELETE ON semicolony.cron_read_action_dispatch_outbox
|
|
217
|
+
FOR EACH ROW EXECUTE FUNCTION semicolony.hive_cron_protect_terminal_dispatch();
|
|
218
|
+
|
|
219
|
+
CREATE FUNCTION semicolony.hive_cron_derive_next_run(
|
|
220
|
+
p_schedule jsonb,
|
|
221
|
+
p_observed_at timestamptz
|
|
222
|
+
)
|
|
223
|
+
RETURNS timestamptz
|
|
224
|
+
LANGUAGE plpgsql
|
|
225
|
+
STABLE
|
|
226
|
+
SECURITY DEFINER
|
|
227
|
+
SET search_path = pg_catalog
|
|
228
|
+
SET TimeZone = 'UTC'
|
|
229
|
+
AS $$
|
|
230
|
+
DECLARE
|
|
231
|
+
schedule_keys text[];
|
|
232
|
+
cron_expression text;
|
|
233
|
+
fields text[];
|
|
234
|
+
minute_field text;
|
|
235
|
+
hour_field text;
|
|
236
|
+
day_of_week_field text;
|
|
237
|
+
minute_value integer;
|
|
238
|
+
minute_step integer;
|
|
239
|
+
hour_value integer;
|
|
240
|
+
hour_step integer;
|
|
241
|
+
day_of_week_value integer;
|
|
242
|
+
candidate timestamptz;
|
|
243
|
+
local_candidate timestamp;
|
|
244
|
+
BEGIN
|
|
245
|
+
IF p_schedule IS NULL
|
|
246
|
+
OR p_observed_at IS NULL
|
|
247
|
+
OR pg_catalog.jsonb_typeof(p_schedule) <> 'object' THEN
|
|
248
|
+
RAISE EXCEPTION 'unsupported_hive_cron_schedule' USING ERRCODE = '22023';
|
|
249
|
+
END IF;
|
|
250
|
+
|
|
251
|
+
SELECT pg_catalog.array_agg(key ORDER BY key)
|
|
252
|
+
INTO schedule_keys
|
|
253
|
+
FROM pg_catalog.jsonb_object_keys(p_schedule) AS key;
|
|
254
|
+
IF schedule_keys IS DISTINCT FROM ARRAY['cron', 'tz']::text[]
|
|
255
|
+
OR pg_catalog.jsonb_typeof(p_schedule->'cron') <> 'string'
|
|
256
|
+
OR pg_catalog.jsonb_typeof(p_schedule->'tz') <> 'string'
|
|
257
|
+
OR p_schedule->>'tz' <> 'Asia/Seoul' THEN
|
|
258
|
+
RAISE EXCEPTION 'unsupported_hive_cron_schedule' USING ERRCODE = '22023';
|
|
259
|
+
END IF;
|
|
260
|
+
|
|
261
|
+
cron_expression := p_schedule->>'cron';
|
|
262
|
+
fields := pg_catalog.string_to_array(cron_expression, ' ');
|
|
263
|
+
IF pg_catalog.cardinality(fields) <> 5
|
|
264
|
+
OR pg_catalog.array_to_string(fields, ' ') <> cron_expression THEN
|
|
265
|
+
RAISE EXCEPTION 'unsupported_hive_cron_schedule' USING ERRCODE = '22023';
|
|
266
|
+
END IF;
|
|
267
|
+
|
|
268
|
+
minute_field := fields[1];
|
|
269
|
+
hour_field := fields[2];
|
|
270
|
+
day_of_week_field := fields[5];
|
|
271
|
+
|
|
272
|
+
IF (minute_field COLLATE "C") ~ '^(0|[1-9]|[1-5][0-9])$' THEN
|
|
273
|
+
minute_value := minute_field::integer;
|
|
274
|
+
ELSIF (minute_field COLLATE "C") ~ '^\*/([1-9]|[1-5][0-9])$' THEN
|
|
275
|
+
minute_step := pg_catalog.substr(minute_field, 3)::integer;
|
|
276
|
+
ELSE
|
|
277
|
+
RAISE EXCEPTION 'unsupported_hive_cron_schedule' USING ERRCODE = '22023';
|
|
278
|
+
END IF;
|
|
279
|
+
|
|
280
|
+
IF hour_field = '*' THEN
|
|
281
|
+
NULL;
|
|
282
|
+
ELSIF (hour_field COLLATE "C") ~ '^(0|[1-9]|1[0-9]|2[0-3])$' THEN
|
|
283
|
+
hour_value := hour_field::integer;
|
|
284
|
+
ELSIF (hour_field COLLATE "C") ~ '^\*/([1-9]|1[0-9]|2[0-3])$' THEN
|
|
285
|
+
hour_step := pg_catalog.substr(hour_field, 3)::integer;
|
|
286
|
+
ELSE
|
|
287
|
+
RAISE EXCEPTION 'unsupported_hive_cron_schedule' USING ERRCODE = '22023';
|
|
288
|
+
END IF;
|
|
289
|
+
|
|
290
|
+
IF fields[3] <> '*' OR fields[4] <> '*' THEN
|
|
291
|
+
RAISE EXCEPTION 'unsupported_hive_cron_schedule' USING ERRCODE = '22023';
|
|
292
|
+
END IF;
|
|
293
|
+
|
|
294
|
+
IF day_of_week_field = '*' THEN
|
|
295
|
+
NULL;
|
|
296
|
+
ELSIF (day_of_week_field COLLATE "C") ~ '^[0-7]$' THEN
|
|
297
|
+
day_of_week_value := day_of_week_field::integer;
|
|
298
|
+
IF day_of_week_value = 7 THEN
|
|
299
|
+
day_of_week_value := 0;
|
|
300
|
+
END IF;
|
|
301
|
+
ELSE
|
|
302
|
+
RAISE EXCEPTION 'unsupported_hive_cron_schedule' USING ERRCODE = '22023';
|
|
303
|
+
END IF;
|
|
304
|
+
|
|
305
|
+
FOR candidate IN
|
|
306
|
+
SELECT series
|
|
307
|
+
FROM pg_catalog.generate_series(
|
|
308
|
+
pg_catalog.date_trunc('minute', p_observed_at) + interval '1 minute',
|
|
309
|
+
pg_catalog.date_trunc('minute', p_observed_at) + interval '8 days',
|
|
310
|
+
interval '1 minute'
|
|
311
|
+
) AS series
|
|
312
|
+
LOOP
|
|
313
|
+
local_candidate := candidate AT TIME ZONE 'Asia/Seoul';
|
|
314
|
+
IF (minute_value IS NULL OR pg_catalog.date_part('minute', local_candidate)::integer = minute_value)
|
|
315
|
+
AND (minute_step IS NULL OR
|
|
316
|
+
pg_catalog.mod(pg_catalog.date_part('minute', local_candidate)::integer, minute_step) = 0)
|
|
317
|
+
AND (hour_value IS NULL OR pg_catalog.date_part('hour', local_candidate)::integer = hour_value)
|
|
318
|
+
AND (hour_step IS NULL OR
|
|
319
|
+
pg_catalog.mod(pg_catalog.date_part('hour', local_candidate)::integer, hour_step) = 0)
|
|
320
|
+
AND (day_of_week_value IS NULL OR
|
|
321
|
+
pg_catalog.date_part('dow', local_candidate)::integer = day_of_week_value) THEN
|
|
322
|
+
RETURN candidate;
|
|
323
|
+
END IF;
|
|
324
|
+
END LOOP;
|
|
325
|
+
|
|
326
|
+
RAISE EXCEPTION 'unsupported_hive_cron_schedule' USING ERRCODE = '22023';
|
|
327
|
+
END;
|
|
328
|
+
$$;
|
|
329
|
+
|
|
330
|
+
REVOKE ALL ON FUNCTION semicolony.hive_cron_derive_next_run(jsonb, timestamptz)
|
|
331
|
+
FROM PUBLIC, sc_app, sc_provider;
|
|
332
|
+
|
|
333
|
+
CREATE FUNCTION semicolony.hive_cron_preview_due_dispatches(
|
|
334
|
+
p_now timestamptz,
|
|
335
|
+
p_limit integer
|
|
336
|
+
)
|
|
337
|
+
RETURNS TABLE (
|
|
338
|
+
job_id text,
|
|
339
|
+
schedule jsonb,
|
|
340
|
+
scheduled_for timestamptz,
|
|
341
|
+
observed_at timestamptz,
|
|
342
|
+
idempotency_key text,
|
|
343
|
+
capability_template_ref text,
|
|
344
|
+
template_updated_at timestamptz,
|
|
345
|
+
permission text,
|
|
346
|
+
resource_scopes text[],
|
|
347
|
+
destination text,
|
|
348
|
+
timeout_seconds integer,
|
|
349
|
+
expected_outcome_digest text,
|
|
350
|
+
source_hash text,
|
|
351
|
+
action_kind text,
|
|
352
|
+
action_version integer,
|
|
353
|
+
action_contract_digest text,
|
|
354
|
+
trusted_config_ref text,
|
|
355
|
+
trusted_config_revision integer,
|
|
356
|
+
trusted_config_digest text
|
|
357
|
+
)
|
|
358
|
+
LANGUAGE plpgsql
|
|
359
|
+
SECURITY DEFINER
|
|
360
|
+
SET search_path = pg_catalog
|
|
361
|
+
SET TimeZone = 'UTC'
|
|
362
|
+
AS $$
|
|
363
|
+
DECLARE
|
|
364
|
+
db_now timestamptz;
|
|
365
|
+
BEGIN
|
|
366
|
+
IF session_user <> 'sc_provider'
|
|
367
|
+
OR public.is_provider() IS DISTINCT FROM true THEN
|
|
368
|
+
RAISE EXCEPTION 'provider_mode_required' USING ERRCODE = '42501';
|
|
369
|
+
END IF;
|
|
370
|
+
db_now := pg_catalog.clock_timestamp();
|
|
371
|
+
IF p_now IS NULL
|
|
372
|
+
OR p_now < db_now - interval '30 seconds'
|
|
373
|
+
OR p_now > db_now + interval '30 seconds'
|
|
374
|
+
OR p_limit IS NULL OR p_limit < 1 OR p_limit > 20 THEN
|
|
375
|
+
RAISE EXCEPTION 'invalid_dispatch_preview_input' USING ERRCODE = '22023';
|
|
376
|
+
END IF;
|
|
377
|
+
|
|
378
|
+
RETURN QUERY
|
|
379
|
+
WITH candidates AS MATERIALIZED (
|
|
380
|
+
SELECT job.job_id,
|
|
381
|
+
job.schedule,
|
|
382
|
+
job.next_run AT TIME ZONE 'UTC' AS scheduled_for,
|
|
383
|
+
db_now AS observed_at,
|
|
384
|
+
'cron:' || job.job_id || ':' ||
|
|
385
|
+
to_char(job.next_run AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"') AS idempotency_key,
|
|
386
|
+
'semi:' || job.job_id || ':hive-cron-v2' AS capability_template_ref,
|
|
387
|
+
template.updated_at AS template_updated_at,
|
|
388
|
+
template.permission,
|
|
389
|
+
canonical.resource_scopes,
|
|
390
|
+
template.destination,
|
|
391
|
+
template.timeout_seconds,
|
|
392
|
+
template.expected_outcome_digest,
|
|
393
|
+
template.source_hash,
|
|
394
|
+
template.action_kind,
|
|
395
|
+
template.action_version,
|
|
396
|
+
template.action_contract_digest,
|
|
397
|
+
'semi:' || job.job_id || ':hive-cron-v2:config:r' ||
|
|
398
|
+
config.config_revision::text AS trusted_config_ref,
|
|
399
|
+
config.config_revision AS trusted_config_revision,
|
|
400
|
+
config.config_digest AS trusted_config_digest
|
|
401
|
+
FROM semicolony.bot_cron_jobs AS job
|
|
402
|
+
JOIN semicolony.cron_capability_templates AS template
|
|
403
|
+
ON template.bot_id = job.bot_id
|
|
404
|
+
AND template.job_id = job.job_id
|
|
405
|
+
AND template.runtime_source = job.runtime_source
|
|
406
|
+
JOIN LATERAL (
|
|
407
|
+
SELECT pg_catalog.array_agg(scope ORDER BY scope COLLATE "C") AS resource_scopes
|
|
408
|
+
FROM (
|
|
409
|
+
SELECT DISTINCT scope
|
|
410
|
+
FROM pg_catalog.unnest(template.resource_scopes) AS scope
|
|
411
|
+
) AS unique_scope
|
|
412
|
+
) AS canonical ON canonical.resource_scopes IS NOT NULL
|
|
413
|
+
JOIN LATERAL (
|
|
414
|
+
SELECT revision.config_revision,
|
|
415
|
+
revision.config_digest,
|
|
416
|
+
revision.action_kind,
|
|
417
|
+
revision.action_version,
|
|
418
|
+
revision.action_contract_digest
|
|
419
|
+
FROM semicolony.cron_read_action_trusted_config_revisions AS revision
|
|
420
|
+
WHERE revision.bot_id = template.bot_id
|
|
421
|
+
AND revision.job_id = template.job_id
|
|
422
|
+
AND revision.runtime_source = template.runtime_source
|
|
423
|
+
ORDER BY revision.config_revision DESC
|
|
424
|
+
LIMIT 1
|
|
425
|
+
) AS config ON true
|
|
426
|
+
WHERE job.bot_id = 'semi'
|
|
427
|
+
AND job.runtime_source = 'hive-cron-v2'
|
|
428
|
+
AND job.enabled = true
|
|
429
|
+
AND job.next_run IS NOT NULL
|
|
430
|
+
AND job.next_run AT TIME ZONE 'UTC' <= db_now
|
|
431
|
+
AND template.permission = 'read'
|
|
432
|
+
AND pg_catalog.char_length(template.destination) BETWEEN 1 AND 256
|
|
433
|
+
AND template.timeout_seconds BETWEEN 1 AND 3600
|
|
434
|
+
AND template.expected_outcome_digest ~ '^sha256:[a-f0-9]{64}$'
|
|
435
|
+
AND template.source_hash ~ '^sha256:[a-f0-9]{64}$'
|
|
436
|
+
AND pg_catalog.char_length(template.action_kind) BETWEEN 1 AND 256
|
|
437
|
+
AND template.action_version > 0
|
|
438
|
+
AND template.action_contract_digest ~ '^[a-f0-9]{64}$'
|
|
439
|
+
AND pg_catalog.cardinality(template.resource_scopes) BETWEEN 1 AND 32
|
|
440
|
+
AND pg_catalog.array_position(template.resource_scopes, NULL) IS NULL
|
|
441
|
+
AND pg_catalog.cardinality(template.resource_scopes) =
|
|
442
|
+
pg_catalog.cardinality(canonical.resource_scopes)
|
|
443
|
+
AND NOT EXISTS (
|
|
444
|
+
SELECT 1
|
|
445
|
+
FROM pg_catalog.unnest(template.resource_scopes) AS scope
|
|
446
|
+
WHERE (scope COLLATE "C") !~ '^[!-~]+$'
|
|
447
|
+
OR pg_catalog.char_length(scope) NOT BETWEEN 1 AND 256
|
|
448
|
+
OR pg_catalog.strpos(scope, '*') > 0
|
|
449
|
+
)
|
|
450
|
+
AND template.action_kind IS NOT NULL
|
|
451
|
+
AND template.action_version IS NOT NULL
|
|
452
|
+
AND template.action_contract_digest IS NOT NULL
|
|
453
|
+
AND config.action_kind = template.action_kind
|
|
454
|
+
AND config.action_version = template.action_version
|
|
455
|
+
AND config.action_contract_digest = template.action_contract_digest
|
|
456
|
+
ORDER BY job.next_run AT TIME ZONE 'UTC', job.job_id
|
|
457
|
+
LIMIT p_limit
|
|
458
|
+
), validated AS MATERIALIZED (
|
|
459
|
+
SELECT candidate.*,
|
|
460
|
+
semicolony.hive_cron_derive_next_run(candidate.schedule, db_now) AS derived_next_run
|
|
461
|
+
FROM candidates AS candidate
|
|
462
|
+
)
|
|
463
|
+
SELECT validated.job_id,
|
|
464
|
+
validated.schedule,
|
|
465
|
+
validated.scheduled_for,
|
|
466
|
+
validated.observed_at,
|
|
467
|
+
validated.idempotency_key,
|
|
468
|
+
validated.capability_template_ref,
|
|
469
|
+
validated.template_updated_at,
|
|
470
|
+
validated.permission,
|
|
471
|
+
validated.resource_scopes,
|
|
472
|
+
validated.destination,
|
|
473
|
+
validated.timeout_seconds,
|
|
474
|
+
validated.expected_outcome_digest,
|
|
475
|
+
validated.source_hash,
|
|
476
|
+
validated.action_kind,
|
|
477
|
+
validated.action_version,
|
|
478
|
+
validated.action_contract_digest,
|
|
479
|
+
validated.trusted_config_ref,
|
|
480
|
+
validated.trusted_config_revision,
|
|
481
|
+
validated.trusted_config_digest
|
|
482
|
+
FROM validated
|
|
483
|
+
WHERE validated.derived_next_run > db_now
|
|
484
|
+
ORDER BY validated.scheduled_for, validated.job_id;
|
|
485
|
+
END;
|
|
486
|
+
$$;
|
|
487
|
+
|
|
488
|
+
CREATE FUNCTION semicolony.hive_cron_claim_dispatch_batch(
|
|
489
|
+
p_now timestamptz,
|
|
490
|
+
p_batch jsonb
|
|
491
|
+
)
|
|
492
|
+
RETURNS TABLE (
|
|
493
|
+
bot_id text,
|
|
494
|
+
job_id text,
|
|
495
|
+
runtime_source text,
|
|
496
|
+
scheduled_for timestamptz,
|
|
497
|
+
idempotency_key text,
|
|
498
|
+
capability_template_ref text,
|
|
499
|
+
template_updated_at timestamptz,
|
|
500
|
+
permission text,
|
|
501
|
+
resource_scopes text[],
|
|
502
|
+
destination text,
|
|
503
|
+
timeout_seconds integer,
|
|
504
|
+
expected_outcome_digest text,
|
|
505
|
+
source_hash text,
|
|
506
|
+
action_kind text,
|
|
507
|
+
action_version integer,
|
|
508
|
+
action_contract_digest text,
|
|
509
|
+
trusted_config_ref text,
|
|
510
|
+
trusted_config_revision integer,
|
|
511
|
+
trusted_config_digest text,
|
|
512
|
+
occurrence_claim_id text,
|
|
513
|
+
status text,
|
|
514
|
+
claim_id text,
|
|
515
|
+
claimed_at timestamptz,
|
|
516
|
+
claim_expires_at timestamptz,
|
|
517
|
+
attempt_count integer,
|
|
518
|
+
next_attempt_at timestamptz,
|
|
519
|
+
result_code text,
|
|
520
|
+
resolved_at timestamptz,
|
|
521
|
+
created_at timestamptz
|
|
522
|
+
)
|
|
523
|
+
LANGUAGE plpgsql
|
|
524
|
+
SECURITY DEFINER
|
|
525
|
+
SET search_path = pg_catalog
|
|
526
|
+
SET TimeZone = 'UTC'
|
|
527
|
+
AS $$
|
|
528
|
+
DECLARE
|
|
529
|
+
item jsonb;
|
|
530
|
+
item_keys text[];
|
|
531
|
+
expected_keys constant text[] := ARRAY[
|
|
532
|
+
'actionContractDigest', 'actionKind', 'actionVersion', 'capabilityTemplateRef',
|
|
533
|
+
'destination', 'expectedOutcomeDigest', 'idempotencyKey', 'jobId',
|
|
534
|
+
'observedAt', 'occurrenceClaimId', 'permission', 'resourceScopes', 'schedule', 'scheduledFor',
|
|
535
|
+
'sourceHash', 'templateUpdatedAt', 'timeoutSeconds', 'trustedConfigDigest',
|
|
536
|
+
'trustedConfigRef', 'trustedConfigRevision'
|
|
537
|
+
];
|
|
538
|
+
due record;
|
|
539
|
+
input_scheduled_for timestamptz;
|
|
540
|
+
input_observed_at timestamptz;
|
|
541
|
+
expected_scheduled_for text;
|
|
542
|
+
changed integer;
|
|
543
|
+
db_now timestamptz;
|
|
544
|
+
BEGIN
|
|
545
|
+
IF session_user <> 'sc_provider'
|
|
546
|
+
OR public.is_provider() IS DISTINCT FROM true THEN
|
|
547
|
+
RAISE EXCEPTION 'provider_mode_required' USING ERRCODE = '42501';
|
|
548
|
+
END IF;
|
|
549
|
+
db_now := pg_catalog.clock_timestamp();
|
|
550
|
+
IF p_now IS NULL
|
|
551
|
+
OR p_now < db_now - interval '30 seconds'
|
|
552
|
+
OR p_now > db_now + interval '30 seconds'
|
|
553
|
+
OR p_batch IS NULL
|
|
554
|
+
OR pg_catalog.jsonb_typeof(p_batch) <> 'array'
|
|
555
|
+
OR pg_catalog.octet_length(p_batch::text) > 262144
|
|
556
|
+
OR pg_catalog.jsonb_array_length(p_batch) < 1
|
|
557
|
+
OR pg_catalog.jsonb_array_length(p_batch) > 20 THEN
|
|
558
|
+
RAISE EXCEPTION 'invalid_dispatch_batch' USING ERRCODE = '22023';
|
|
559
|
+
END IF;
|
|
560
|
+
|
|
561
|
+
FOR item IN SELECT value FROM pg_catalog.jsonb_array_elements(p_batch)
|
|
562
|
+
LOOP
|
|
563
|
+
IF pg_catalog.jsonb_typeof(item) <> 'object' THEN
|
|
564
|
+
RAISE EXCEPTION 'invalid_dispatch_batch' USING ERRCODE = '22023';
|
|
565
|
+
END IF;
|
|
566
|
+
SELECT pg_catalog.array_agg(key ORDER BY key)
|
|
567
|
+
INTO item_keys
|
|
568
|
+
FROM pg_catalog.jsonb_object_keys(item) AS key;
|
|
569
|
+
IF item_keys IS DISTINCT FROM expected_keys
|
|
570
|
+
OR pg_catalog.jsonb_typeof(item->'jobId') <> 'string'
|
|
571
|
+
OR pg_catalog.jsonb_typeof(item->'scheduledFor') <> 'string'
|
|
572
|
+
OR pg_catalog.jsonb_typeof(item->'observedAt') <> 'string'
|
|
573
|
+
OR pg_catalog.jsonb_typeof(item->'schedule') <> 'object'
|
|
574
|
+
OR pg_catalog.jsonb_typeof(item->'idempotencyKey') <> 'string'
|
|
575
|
+
OR pg_catalog.jsonb_typeof(item->'capabilityTemplateRef') <> 'string'
|
|
576
|
+
OR pg_catalog.jsonb_typeof(item->'templateUpdatedAt') <> 'string'
|
|
577
|
+
OR pg_catalog.jsonb_typeof(item->'permission') <> 'string'
|
|
578
|
+
OR pg_catalog.jsonb_typeof(item->'resourceScopes') <> 'array'
|
|
579
|
+
OR pg_catalog.jsonb_typeof(item->'destination') <> 'string'
|
|
580
|
+
OR pg_catalog.jsonb_typeof(item->'timeoutSeconds') <> 'number'
|
|
581
|
+
OR pg_catalog.jsonb_typeof(item->'expectedOutcomeDigest') <> 'string'
|
|
582
|
+
OR pg_catalog.jsonb_typeof(item->'sourceHash') <> 'string'
|
|
583
|
+
OR pg_catalog.jsonb_typeof(item->'actionKind') <> 'string'
|
|
584
|
+
OR pg_catalog.jsonb_typeof(item->'actionVersion') <> 'number'
|
|
585
|
+
OR pg_catalog.jsonb_typeof(item->'actionContractDigest') <> 'string'
|
|
586
|
+
OR pg_catalog.jsonb_typeof(item->'trustedConfigRef') <> 'string'
|
|
587
|
+
OR pg_catalog.jsonb_typeof(item->'trustedConfigRevision') <> 'number'
|
|
588
|
+
OR pg_catalog.jsonb_typeof(item->'trustedConfigDigest') <> 'string'
|
|
589
|
+
OR pg_catalog.jsonb_typeof(item->'occurrenceClaimId') <> 'string'
|
|
590
|
+
OR ((item->>'jobId') COLLATE "C") !~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$'
|
|
591
|
+
OR ((item->>'scheduledFor') COLLATE "C") !~
|
|
592
|
+
'^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}\.[0-9]{3}Z$'
|
|
593
|
+
OR ((item->>'observedAt') COLLATE "C") !~
|
|
594
|
+
'^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}\.[0-9]{3}Z$'
|
|
595
|
+
OR ((item->>'templateUpdatedAt') COLLATE "C") !~
|
|
596
|
+
'^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}\.[0-9]{3}Z$'
|
|
597
|
+
OR (item->>'permission') <> 'read'
|
|
598
|
+
OR pg_catalog.jsonb_array_length(item->'resourceScopes') < 1
|
|
599
|
+
OR pg_catalog.jsonb_array_length(item->'resourceScopes') > 32
|
|
600
|
+
OR pg_catalog.char_length(item->>'destination') NOT BETWEEN 1 AND 256
|
|
601
|
+
OR (item->>'timeoutSeconds') !~ '^[1-9][0-9]*$'
|
|
602
|
+
OR ((item->>'expectedOutcomeDigest')) !~ '^sha256:[a-f0-9]{64}$'
|
|
603
|
+
OR ((item->>'sourceHash')) !~ '^sha256:[a-f0-9]{64}$'
|
|
604
|
+
OR pg_catalog.char_length(item->>'actionKind') NOT BETWEEN 1 AND 256
|
|
605
|
+
OR (item->>'actionVersion') !~ '^[1-9][0-9]*$'
|
|
606
|
+
OR ((item->>'actionContractDigest')) !~ '^[a-f0-9]{64}$'
|
|
607
|
+
OR ((item->>'trustedConfigDigest')) !~ '^sha256:[a-f0-9]{64}$' THEN
|
|
608
|
+
RAISE EXCEPTION 'invalid_dispatch_batch' USING ERRCODE = '22023';
|
|
609
|
+
END IF;
|
|
610
|
+
|
|
611
|
+
input_scheduled_for := (item->>'scheduledFor')::timestamptz;
|
|
612
|
+
input_observed_at := (item->>'observedAt')::timestamptz;
|
|
613
|
+
IF input_scheduled_for > input_observed_at
|
|
614
|
+
OR input_observed_at < db_now - interval '30 seconds'
|
|
615
|
+
OR input_observed_at > db_now THEN
|
|
616
|
+
RAISE EXCEPTION 'invalid_dispatch_batch' USING ERRCODE = '22023';
|
|
617
|
+
END IF;
|
|
618
|
+
|
|
619
|
+
SELECT job.next_run AT TIME ZONE 'UTC' AS scheduled_for_utc,
|
|
620
|
+
job.schedule AS schedule,
|
|
621
|
+
NULL::timestamptz AS next_run,
|
|
622
|
+
template.updated_at AS template_updated_at,
|
|
623
|
+
template.permission,
|
|
624
|
+
canonical.resource_scopes,
|
|
625
|
+
template.destination,
|
|
626
|
+
template.timeout_seconds,
|
|
627
|
+
template.expected_outcome_digest,
|
|
628
|
+
template.source_hash,
|
|
629
|
+
template.action_kind,
|
|
630
|
+
template.action_version,
|
|
631
|
+
template.action_contract_digest,
|
|
632
|
+
config.config_revision,
|
|
633
|
+
config.config_digest
|
|
634
|
+
INTO due
|
|
635
|
+
FROM semicolony.bot_cron_jobs AS job
|
|
636
|
+
JOIN semicolony.cron_capability_templates AS template
|
|
637
|
+
ON template.bot_id = job.bot_id
|
|
638
|
+
AND template.job_id = job.job_id
|
|
639
|
+
AND template.runtime_source = job.runtime_source
|
|
640
|
+
JOIN LATERAL (
|
|
641
|
+
SELECT pg_catalog.array_agg(scope ORDER BY scope COLLATE "C") AS resource_scopes
|
|
642
|
+
FROM (
|
|
643
|
+
SELECT DISTINCT scope
|
|
644
|
+
FROM pg_catalog.unnest(template.resource_scopes) AS scope
|
|
645
|
+
) AS unique_scope
|
|
646
|
+
) AS canonical ON canonical.resource_scopes IS NOT NULL
|
|
647
|
+
JOIN LATERAL (
|
|
648
|
+
SELECT revision.config_revision,
|
|
649
|
+
revision.config_digest,
|
|
650
|
+
revision.action_kind,
|
|
651
|
+
revision.action_version,
|
|
652
|
+
revision.action_contract_digest
|
|
653
|
+
FROM semicolony.cron_read_action_trusted_config_revisions AS revision
|
|
654
|
+
WHERE revision.bot_id = template.bot_id
|
|
655
|
+
AND revision.job_id = template.job_id
|
|
656
|
+
AND revision.runtime_source = template.runtime_source
|
|
657
|
+
ORDER BY revision.config_revision DESC
|
|
658
|
+
LIMIT 1
|
|
659
|
+
) AS config ON true
|
|
660
|
+
WHERE job.bot_id = 'semi'
|
|
661
|
+
AND job.job_id = (item->>'jobId')
|
|
662
|
+
AND job.runtime_source = 'hive-cron-v2'
|
|
663
|
+
AND job.enabled = true
|
|
664
|
+
AND job.next_run IS NOT NULL
|
|
665
|
+
AND job.next_run AT TIME ZONE 'UTC' = input_scheduled_for
|
|
666
|
+
AND job.next_run AT TIME ZONE 'UTC' <= db_now
|
|
667
|
+
AND job.schedule = item->'schedule'
|
|
668
|
+
AND template.permission = 'read'
|
|
669
|
+
AND pg_catalog.char_length(template.destination) BETWEEN 1 AND 256
|
|
670
|
+
AND template.timeout_seconds BETWEEN 1 AND 3600
|
|
671
|
+
AND template.expected_outcome_digest ~ '^sha256:[a-f0-9]{64}$'
|
|
672
|
+
AND template.source_hash ~ '^sha256:[a-f0-9]{64}$'
|
|
673
|
+
AND pg_catalog.char_length(template.action_kind) BETWEEN 1 AND 256
|
|
674
|
+
AND template.action_version > 0
|
|
675
|
+
AND template.action_contract_digest ~ '^[a-f0-9]{64}$'
|
|
676
|
+
AND pg_catalog.cardinality(template.resource_scopes) BETWEEN 1 AND 32
|
|
677
|
+
AND pg_catalog.array_position(template.resource_scopes, NULL) IS NULL
|
|
678
|
+
AND pg_catalog.cardinality(template.resource_scopes) =
|
|
679
|
+
pg_catalog.cardinality(canonical.resource_scopes)
|
|
680
|
+
AND NOT EXISTS (
|
|
681
|
+
SELECT 1
|
|
682
|
+
FROM pg_catalog.unnest(template.resource_scopes) AS scope
|
|
683
|
+
WHERE (scope COLLATE "C") !~ '^[!-~]+$'
|
|
684
|
+
OR pg_catalog.char_length(scope) NOT BETWEEN 1 AND 256
|
|
685
|
+
OR pg_catalog.strpos(scope, '*') > 0
|
|
686
|
+
)
|
|
687
|
+
AND template.action_kind IS NOT NULL
|
|
688
|
+
AND template.action_version IS NOT NULL
|
|
689
|
+
AND template.action_contract_digest IS NOT NULL
|
|
690
|
+
AND config.action_kind = template.action_kind
|
|
691
|
+
AND config.action_version = template.action_version
|
|
692
|
+
AND config.action_contract_digest = template.action_contract_digest
|
|
693
|
+
FOR UPDATE OF job SKIP LOCKED;
|
|
694
|
+
|
|
695
|
+
IF NOT FOUND THEN
|
|
696
|
+
RAISE EXCEPTION 'dispatch_batch_stale_or_locked' USING ERRCODE = '40001';
|
|
697
|
+
END IF;
|
|
698
|
+
|
|
699
|
+
due.next_run := semicolony.hive_cron_derive_next_run(due.schedule, db_now);
|
|
700
|
+
IF due.next_run <= db_now THEN
|
|
701
|
+
RAISE EXCEPTION 'unsupported_hive_cron_schedule' USING ERRCODE = '22023';
|
|
702
|
+
END IF;
|
|
703
|
+
|
|
704
|
+
expected_scheduled_for := to_char(
|
|
705
|
+
input_scheduled_for AT TIME ZONE 'UTC',
|
|
706
|
+
'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"'
|
|
707
|
+
);
|
|
708
|
+
IF (item->>'scheduledFor') <> expected_scheduled_for
|
|
709
|
+
OR (item->>'idempotencyKey') <>
|
|
710
|
+
('cron:' || (item->>'jobId') || ':' || expected_scheduled_for)
|
|
711
|
+
OR (item->>'capabilityTemplateRef') <>
|
|
712
|
+
('semi:' || (item->>'jobId') || ':hive-cron-v2')
|
|
713
|
+
OR (item->>'templateUpdatedAt') <> to_char(
|
|
714
|
+
due.template_updated_at AT TIME ZONE 'UTC',
|
|
715
|
+
'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"'
|
|
716
|
+
)
|
|
717
|
+
OR (item->>'permission') <> due.permission
|
|
718
|
+
OR item->'resourceScopes' IS DISTINCT FROM pg_catalog.to_jsonb(due.resource_scopes)
|
|
719
|
+
OR (item->>'destination') <> due.destination
|
|
720
|
+
OR (item->>'timeoutSeconds')::integer <> due.timeout_seconds
|
|
721
|
+
OR (item->>'expectedOutcomeDigest') <> due.expected_outcome_digest
|
|
722
|
+
OR (item->>'sourceHash') <> due.source_hash
|
|
723
|
+
OR (item->>'actionKind') <> due.action_kind
|
|
724
|
+
OR (item->>'actionVersion')::integer <> due.action_version
|
|
725
|
+
OR (item->>'actionContractDigest') <> due.action_contract_digest
|
|
726
|
+
OR (item->>'trustedConfigRef') <>
|
|
727
|
+
('semi:' || (item->>'jobId') || ':hive-cron-v2:config:r' || due.config_revision::text)
|
|
728
|
+
OR (item->>'trustedConfigRevision') !~ '^[1-9][0-9]*$'
|
|
729
|
+
OR (item->>'trustedConfigRevision')::integer <> due.config_revision
|
|
730
|
+
OR (item->>'trustedConfigDigest') <> due.config_digest THEN
|
|
731
|
+
RAISE EXCEPTION 'dispatch_batch_stale_or_locked' USING ERRCODE = '40001';
|
|
732
|
+
END IF;
|
|
733
|
+
IF ((item->>'occurrenceClaimId') COLLATE "C") !~ '^scheduled-[a-f0-9]{64}$'
|
|
734
|
+
OR (item->>'occurrenceClaimId') <> 'scheduled-' || pg_catalog.encode(
|
|
735
|
+
pg_catalog.sha256(pg_catalog.convert_to(item->>'idempotencyKey', 'UTF8')),
|
|
736
|
+
'hex'
|
|
737
|
+
) THEN
|
|
738
|
+
RAISE EXCEPTION 'invalid_dispatch_batch' USING ERRCODE = '22023';
|
|
739
|
+
END IF;
|
|
740
|
+
|
|
741
|
+
INSERT INTO semicolony.cron_read_action_dispatch_outbox (
|
|
742
|
+
bot_id, job_id, runtime_source, scheduled_for, idempotency_key,
|
|
743
|
+
capability_template_ref, template_updated_at, permission, resource_scopes,
|
|
744
|
+
destination, timeout_seconds, expected_outcome_digest, source_hash,
|
|
745
|
+
action_kind, action_version, action_contract_digest,
|
|
746
|
+
trusted_config_ref, trusted_config_revision, trusted_config_digest, occurrence_claim_id,
|
|
747
|
+
status, attempt_count, next_attempt_at, created_at, updated_at
|
|
748
|
+
) VALUES (
|
|
749
|
+
'semi', (item->>'jobId'), 'hive-cron-v2', input_scheduled_for,
|
|
750
|
+
(item->>'idempotencyKey'), (item->>'capabilityTemplateRef'),
|
|
751
|
+
due.template_updated_at, due.permission, due.resource_scopes,
|
|
752
|
+
due.destination, due.timeout_seconds, due.expected_outcome_digest, due.source_hash,
|
|
753
|
+
due.action_kind, due.action_version, due.action_contract_digest,
|
|
754
|
+
(item->>'trustedConfigRef'), (item->>'trustedConfigRevision')::integer,
|
|
755
|
+
(item->>'trustedConfigDigest'), (item->>'occurrenceClaimId'),
|
|
756
|
+
'pending', 0, db_now, db_now, db_now
|
|
757
|
+
)
|
|
758
|
+
RETURNING
|
|
759
|
+
cron_read_action_dispatch_outbox.bot_id,
|
|
760
|
+
cron_read_action_dispatch_outbox.job_id,
|
|
761
|
+
cron_read_action_dispatch_outbox.runtime_source,
|
|
762
|
+
cron_read_action_dispatch_outbox.scheduled_for,
|
|
763
|
+
cron_read_action_dispatch_outbox.idempotency_key,
|
|
764
|
+
cron_read_action_dispatch_outbox.capability_template_ref,
|
|
765
|
+
cron_read_action_dispatch_outbox.template_updated_at,
|
|
766
|
+
cron_read_action_dispatch_outbox.permission,
|
|
767
|
+
cron_read_action_dispatch_outbox.resource_scopes,
|
|
768
|
+
cron_read_action_dispatch_outbox.destination,
|
|
769
|
+
cron_read_action_dispatch_outbox.timeout_seconds,
|
|
770
|
+
cron_read_action_dispatch_outbox.expected_outcome_digest,
|
|
771
|
+
cron_read_action_dispatch_outbox.source_hash,
|
|
772
|
+
cron_read_action_dispatch_outbox.action_kind,
|
|
773
|
+
cron_read_action_dispatch_outbox.action_version,
|
|
774
|
+
cron_read_action_dispatch_outbox.action_contract_digest,
|
|
775
|
+
cron_read_action_dispatch_outbox.trusted_config_ref,
|
|
776
|
+
cron_read_action_dispatch_outbox.trusted_config_revision,
|
|
777
|
+
cron_read_action_dispatch_outbox.trusted_config_digest,
|
|
778
|
+
cron_read_action_dispatch_outbox.occurrence_claim_id,
|
|
779
|
+
cron_read_action_dispatch_outbox.status,
|
|
780
|
+
cron_read_action_dispatch_outbox.claim_id,
|
|
781
|
+
cron_read_action_dispatch_outbox.claimed_at,
|
|
782
|
+
cron_read_action_dispatch_outbox.claim_expires_at,
|
|
783
|
+
cron_read_action_dispatch_outbox.attempt_count,
|
|
784
|
+
cron_read_action_dispatch_outbox.next_attempt_at,
|
|
785
|
+
cron_read_action_dispatch_outbox.result_code,
|
|
786
|
+
cron_read_action_dispatch_outbox.resolved_at,
|
|
787
|
+
cron_read_action_dispatch_outbox.created_at
|
|
788
|
+
INTO bot_id, job_id, runtime_source, scheduled_for, idempotency_key,
|
|
789
|
+
capability_template_ref, template_updated_at, permission, resource_scopes,
|
|
790
|
+
destination, timeout_seconds, expected_outcome_digest, source_hash,
|
|
791
|
+
action_kind, action_version, action_contract_digest,
|
|
792
|
+
trusted_config_ref, trusted_config_revision, trusted_config_digest, occurrence_claim_id,
|
|
793
|
+
status, claim_id, claimed_at, claim_expires_at, attempt_count,
|
|
794
|
+
next_attempt_at, result_code, resolved_at, created_at;
|
|
795
|
+
|
|
796
|
+
UPDATE semicolony.bot_cron_jobs AS job
|
|
797
|
+
SET next_run = due.next_run AT TIME ZONE 'UTC'
|
|
798
|
+
WHERE job.bot_id = 'semi'
|
|
799
|
+
AND job.job_id = (item->>'jobId')
|
|
800
|
+
AND job.runtime_source = 'hive-cron-v2'
|
|
801
|
+
AND job.next_run AT TIME ZONE 'UTC' = input_scheduled_for;
|
|
802
|
+
GET DIAGNOSTICS changed = ROW_COUNT;
|
|
803
|
+
IF changed <> 1 THEN
|
|
804
|
+
RAISE EXCEPTION 'dispatch_batch_stale_or_locked' USING ERRCODE = '40001';
|
|
805
|
+
END IF;
|
|
806
|
+
|
|
807
|
+
RETURN NEXT;
|
|
808
|
+
END LOOP;
|
|
809
|
+
END;
|
|
810
|
+
$$;
|
|
811
|
+
|
|
812
|
+
CREATE FUNCTION semicolony.hive_cron_take_dispatch(
|
|
813
|
+
p_now timestamptz,
|
|
814
|
+
p_claim_id text,
|
|
815
|
+
p_lease_seconds integer
|
|
816
|
+
)
|
|
817
|
+
RETURNS TABLE (
|
|
818
|
+
bot_id text,
|
|
819
|
+
job_id text,
|
|
820
|
+
runtime_source text,
|
|
821
|
+
scheduled_for timestamptz,
|
|
822
|
+
idempotency_key text,
|
|
823
|
+
capability_template_ref text,
|
|
824
|
+
template_updated_at timestamptz,
|
|
825
|
+
permission text,
|
|
826
|
+
resource_scopes text[],
|
|
827
|
+
destination text,
|
|
828
|
+
timeout_seconds integer,
|
|
829
|
+
expected_outcome_digest text,
|
|
830
|
+
source_hash text,
|
|
831
|
+
action_kind text,
|
|
832
|
+
action_version integer,
|
|
833
|
+
action_contract_digest text,
|
|
834
|
+
trusted_config_ref text,
|
|
835
|
+
trusted_config_revision integer,
|
|
836
|
+
trusted_config_digest text,
|
|
837
|
+
occurrence_claim_id text,
|
|
838
|
+
status text,
|
|
839
|
+
claim_id text,
|
|
840
|
+
claimed_at timestamptz,
|
|
841
|
+
claim_expires_at timestamptz,
|
|
842
|
+
attempt_count integer,
|
|
843
|
+
next_attempt_at timestamptz,
|
|
844
|
+
result_code text,
|
|
845
|
+
resolved_at timestamptz,
|
|
846
|
+
created_at timestamptz
|
|
847
|
+
)
|
|
848
|
+
LANGUAGE plpgsql
|
|
849
|
+
SECURITY DEFINER
|
|
850
|
+
SET search_path = pg_catalog
|
|
851
|
+
SET TimeZone = 'UTC'
|
|
852
|
+
AS $$
|
|
853
|
+
DECLARE
|
|
854
|
+
db_now timestamptz;
|
|
855
|
+
BEGIN
|
|
856
|
+
IF session_user <> 'sc_provider'
|
|
857
|
+
OR public.is_provider() IS DISTINCT FROM true THEN
|
|
858
|
+
RAISE EXCEPTION 'provider_mode_required' USING ERRCODE = '42501';
|
|
859
|
+
END IF;
|
|
860
|
+
db_now := pg_catalog.clock_timestamp();
|
|
861
|
+
IF p_now IS NULL
|
|
862
|
+
OR p_now < db_now - interval '30 seconds'
|
|
863
|
+
OR p_now > db_now + interval '30 seconds'
|
|
864
|
+
OR p_claim_id IS NULL
|
|
865
|
+
OR (p_claim_id COLLATE "C") !~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$'
|
|
866
|
+
OR p_lease_seconds IS NULL
|
|
867
|
+
OR p_lease_seconds < 1
|
|
868
|
+
OR p_lease_seconds > 3600 THEN
|
|
869
|
+
RAISE EXCEPTION 'invalid_dispatch_take_input' USING ERRCODE = '22023';
|
|
870
|
+
END IF;
|
|
871
|
+
|
|
872
|
+
RETURN QUERY
|
|
873
|
+
WITH candidate AS (
|
|
874
|
+
SELECT dispatch.idempotency_key
|
|
875
|
+
FROM semicolony.cron_read_action_dispatch_outbox AS dispatch
|
|
876
|
+
WHERE dispatch.status = 'pending'
|
|
877
|
+
AND dispatch.next_attempt_at <= db_now
|
|
878
|
+
AND dispatch.attempt_count < 3
|
|
879
|
+
ORDER BY dispatch.next_attempt_at, dispatch.scheduled_for, dispatch.idempotency_key
|
|
880
|
+
FOR UPDATE SKIP LOCKED
|
|
881
|
+
LIMIT 1
|
|
882
|
+
)
|
|
883
|
+
UPDATE semicolony.cron_read_action_dispatch_outbox AS dispatch
|
|
884
|
+
SET status = 'claimed',
|
|
885
|
+
claim_id = p_claim_id,
|
|
886
|
+
claimed_at = db_now,
|
|
887
|
+
claim_expires_at = db_now + pg_catalog.make_interval(secs => p_lease_seconds),
|
|
888
|
+
attempt_count = dispatch.attempt_count + 1,
|
|
889
|
+
next_attempt_at = NULL,
|
|
890
|
+
updated_at = db_now
|
|
891
|
+
FROM candidate
|
|
892
|
+
WHERE dispatch.idempotency_key = candidate.idempotency_key
|
|
893
|
+
RETURNING dispatch.bot_id,
|
|
894
|
+
dispatch.job_id,
|
|
895
|
+
dispatch.runtime_source,
|
|
896
|
+
dispatch.scheduled_for,
|
|
897
|
+
dispatch.idempotency_key,
|
|
898
|
+
dispatch.capability_template_ref,
|
|
899
|
+
dispatch.template_updated_at,
|
|
900
|
+
dispatch.permission,
|
|
901
|
+
dispatch.resource_scopes,
|
|
902
|
+
dispatch.destination,
|
|
903
|
+
dispatch.timeout_seconds,
|
|
904
|
+
dispatch.expected_outcome_digest,
|
|
905
|
+
dispatch.source_hash,
|
|
906
|
+
dispatch.action_kind,
|
|
907
|
+
dispatch.action_version,
|
|
908
|
+
dispatch.action_contract_digest,
|
|
909
|
+
dispatch.trusted_config_ref,
|
|
910
|
+
dispatch.trusted_config_revision,
|
|
911
|
+
dispatch.trusted_config_digest,
|
|
912
|
+
dispatch.occurrence_claim_id,
|
|
913
|
+
dispatch.status,
|
|
914
|
+
dispatch.claim_id,
|
|
915
|
+
dispatch.claimed_at,
|
|
916
|
+
dispatch.claim_expires_at,
|
|
917
|
+
dispatch.attempt_count,
|
|
918
|
+
dispatch.next_attempt_at,
|
|
919
|
+
dispatch.result_code,
|
|
920
|
+
dispatch.resolved_at,
|
|
921
|
+
dispatch.created_at;
|
|
922
|
+
END;
|
|
923
|
+
$$;
|
|
924
|
+
|
|
925
|
+
CREATE FUNCTION semicolony.hive_cron_list_abandoned_dispatches(
|
|
926
|
+
p_now timestamptz,
|
|
927
|
+
p_limit integer
|
|
928
|
+
)
|
|
929
|
+
RETURNS TABLE (
|
|
930
|
+
bot_id text,
|
|
931
|
+
job_id text,
|
|
932
|
+
runtime_source text,
|
|
933
|
+
scheduled_for timestamptz,
|
|
934
|
+
idempotency_key text,
|
|
935
|
+
capability_template_ref text,
|
|
936
|
+
template_updated_at timestamptz,
|
|
937
|
+
permission text,
|
|
938
|
+
resource_scopes text[],
|
|
939
|
+
destination text,
|
|
940
|
+
timeout_seconds integer,
|
|
941
|
+
expected_outcome_digest text,
|
|
942
|
+
source_hash text,
|
|
943
|
+
action_kind text,
|
|
944
|
+
action_version integer,
|
|
945
|
+
action_contract_digest text,
|
|
946
|
+
trusted_config_ref text,
|
|
947
|
+
trusted_config_revision integer,
|
|
948
|
+
trusted_config_digest text,
|
|
949
|
+
occurrence_claim_id text,
|
|
950
|
+
status text,
|
|
951
|
+
claim_id text,
|
|
952
|
+
claimed_at timestamptz,
|
|
953
|
+
claim_expires_at timestamptz,
|
|
954
|
+
attempt_count integer,
|
|
955
|
+
next_attempt_at timestamptz,
|
|
956
|
+
result_code text,
|
|
957
|
+
resolved_at timestamptz,
|
|
958
|
+
created_at timestamptz
|
|
959
|
+
)
|
|
960
|
+
LANGUAGE plpgsql
|
|
961
|
+
SECURITY DEFINER
|
|
962
|
+
SET search_path = pg_catalog
|
|
963
|
+
SET TimeZone = 'UTC'
|
|
964
|
+
AS $$
|
|
965
|
+
DECLARE
|
|
966
|
+
db_now timestamptz;
|
|
967
|
+
BEGIN
|
|
968
|
+
IF session_user <> 'sc_provider'
|
|
969
|
+
OR public.is_provider() IS DISTINCT FROM true THEN
|
|
970
|
+
RAISE EXCEPTION 'provider_mode_required' USING ERRCODE = '42501';
|
|
971
|
+
END IF;
|
|
972
|
+
db_now := pg_catalog.clock_timestamp();
|
|
973
|
+
IF p_now IS NULL
|
|
974
|
+
OR p_now < db_now - interval '30 seconds'
|
|
975
|
+
OR p_now > db_now + interval '30 seconds'
|
|
976
|
+
OR p_limit IS NULL OR p_limit < 1 OR p_limit > 20 THEN
|
|
977
|
+
RAISE EXCEPTION 'invalid_dispatch_list_input' USING ERRCODE = '22023';
|
|
978
|
+
END IF;
|
|
979
|
+
|
|
980
|
+
RETURN QUERY
|
|
981
|
+
SELECT dispatch.bot_id,
|
|
982
|
+
dispatch.job_id,
|
|
983
|
+
dispatch.runtime_source,
|
|
984
|
+
dispatch.scheduled_for,
|
|
985
|
+
dispatch.idempotency_key,
|
|
986
|
+
dispatch.capability_template_ref,
|
|
987
|
+
dispatch.template_updated_at,
|
|
988
|
+
dispatch.permission,
|
|
989
|
+
dispatch.resource_scopes,
|
|
990
|
+
dispatch.destination,
|
|
991
|
+
dispatch.timeout_seconds,
|
|
992
|
+
dispatch.expected_outcome_digest,
|
|
993
|
+
dispatch.source_hash,
|
|
994
|
+
dispatch.action_kind,
|
|
995
|
+
dispatch.action_version,
|
|
996
|
+
dispatch.action_contract_digest,
|
|
997
|
+
dispatch.trusted_config_ref,
|
|
998
|
+
dispatch.trusted_config_revision,
|
|
999
|
+
dispatch.trusted_config_digest,
|
|
1000
|
+
dispatch.occurrence_claim_id,
|
|
1001
|
+
dispatch.status,
|
|
1002
|
+
dispatch.claim_id,
|
|
1003
|
+
dispatch.claimed_at,
|
|
1004
|
+
dispatch.claim_expires_at,
|
|
1005
|
+
dispatch.attempt_count,
|
|
1006
|
+
dispatch.next_attempt_at,
|
|
1007
|
+
dispatch.result_code,
|
|
1008
|
+
dispatch.resolved_at,
|
|
1009
|
+
dispatch.created_at
|
|
1010
|
+
FROM semicolony.cron_read_action_dispatch_outbox AS dispatch
|
|
1011
|
+
WHERE dispatch.status = 'claimed'
|
|
1012
|
+
AND dispatch.claim_expires_at <= db_now
|
|
1013
|
+
ORDER BY dispatch.claim_expires_at, dispatch.idempotency_key
|
|
1014
|
+
LIMIT p_limit;
|
|
1015
|
+
END;
|
|
1016
|
+
$$;
|
|
1017
|
+
|
|
1018
|
+
CREATE FUNCTION semicolony.hive_cron_resolve_dispatch(
|
|
1019
|
+
p_job_id text,
|
|
1020
|
+
p_idempotency_key text,
|
|
1021
|
+
p_claim_id text,
|
|
1022
|
+
p_observed_at timestamptz
|
|
1023
|
+
)
|
|
1024
|
+
RETURNS TABLE (outcome text)
|
|
1025
|
+
LANGUAGE plpgsql
|
|
1026
|
+
SECURITY DEFINER
|
|
1027
|
+
SET search_path = pg_catalog
|
|
1028
|
+
SET TimeZone = 'UTC'
|
|
1029
|
+
AS $$
|
|
1030
|
+
DECLARE
|
|
1031
|
+
dispatch record;
|
|
1032
|
+
reconciliation record;
|
|
1033
|
+
db_now timestamptz;
|
|
1034
|
+
BEGIN
|
|
1035
|
+
IF session_user <> 'sc_provider'
|
|
1036
|
+
OR public.is_provider() IS DISTINCT FROM true THEN
|
|
1037
|
+
RAISE EXCEPTION 'provider_mode_required' USING ERRCODE = '42501';
|
|
1038
|
+
END IF;
|
|
1039
|
+
db_now := pg_catalog.clock_timestamp();
|
|
1040
|
+
IF p_job_id IS NULL
|
|
1041
|
+
OR (p_job_id COLLATE "C") !~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$'
|
|
1042
|
+
OR p_idempotency_key IS NULL
|
|
1043
|
+
OR p_idempotency_key <> 'cron:' || p_job_id || ':' || pg_catalog.right(p_idempotency_key, 24)
|
|
1044
|
+
OR pg_catalog.right(p_idempotency_key, 24) !~
|
|
1045
|
+
'^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}\.[0-9]{3}Z$'
|
|
1046
|
+
OR p_claim_id IS NULL
|
|
1047
|
+
OR (p_claim_id COLLATE "C") !~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$'
|
|
1048
|
+
OR p_observed_at IS NULL
|
|
1049
|
+
OR p_observed_at < db_now - interval '30 seconds'
|
|
1050
|
+
OR p_observed_at > db_now + interval '30 seconds' THEN
|
|
1051
|
+
RAISE EXCEPTION 'invalid_dispatch_resolution' USING ERRCODE = '22023';
|
|
1052
|
+
END IF;
|
|
1053
|
+
|
|
1054
|
+
SELECT current_dispatch.status,
|
|
1055
|
+
current_dispatch.claim_id,
|
|
1056
|
+
current_dispatch.claimed_at,
|
|
1057
|
+
current_dispatch.claim_expires_at,
|
|
1058
|
+
current_dispatch.attempt_count,
|
|
1059
|
+
current_dispatch.occurrence_claim_id
|
|
1060
|
+
INTO dispatch
|
|
1061
|
+
FROM semicolony.cron_read_action_dispatch_outbox AS current_dispatch
|
|
1062
|
+
WHERE current_dispatch.job_id = p_job_id
|
|
1063
|
+
AND current_dispatch.idempotency_key = p_idempotency_key
|
|
1064
|
+
FOR UPDATE;
|
|
1065
|
+
IF NOT FOUND THEN
|
|
1066
|
+
outcome := 'not_found';
|
|
1067
|
+
RETURN NEXT;
|
|
1068
|
+
RETURN;
|
|
1069
|
+
END IF;
|
|
1070
|
+
IF dispatch.status <> 'claimed' OR dispatch.claim_id <> p_claim_id THEN
|
|
1071
|
+
outcome := 'claim_mismatch';
|
|
1072
|
+
RETURN NEXT;
|
|
1073
|
+
RETURN;
|
|
1074
|
+
END IF;
|
|
1075
|
+
SELECT *
|
|
1076
|
+
INTO reconciliation
|
|
1077
|
+
FROM semicolony.hive_cron_reconcile(p_job_id, p_idempotency_key);
|
|
1078
|
+
|
|
1079
|
+
IF NOT FOUND THEN
|
|
1080
|
+
UPDATE semicolony.cron_read_action_dispatch_outbox AS current_dispatch
|
|
1081
|
+
SET status = 'blocked',
|
|
1082
|
+
claim_id = NULL,
|
|
1083
|
+
claimed_at = NULL,
|
|
1084
|
+
claim_expires_at = NULL,
|
|
1085
|
+
result_code = 'ambiguous_authority_state',
|
|
1086
|
+
resolved_at = db_now,
|
|
1087
|
+
updated_at = db_now
|
|
1088
|
+
WHERE current_dispatch.idempotency_key = p_idempotency_key;
|
|
1089
|
+
outcome := 'blocked';
|
|
1090
|
+
RETURN NEXT;
|
|
1091
|
+
RETURN;
|
|
1092
|
+
END IF;
|
|
1093
|
+
|
|
1094
|
+
IF reconciliation.lease_count = 0
|
|
1095
|
+
AND reconciliation.outbox_count = 0
|
|
1096
|
+
AND reconciliation.evidence_count = 0
|
|
1097
|
+
AND reconciliation.event_count = 0
|
|
1098
|
+
AND dispatch.claim_expires_at > db_now THEN
|
|
1099
|
+
outcome := 'claim_active';
|
|
1100
|
+
ELSIF reconciliation.lease_count = 0
|
|
1101
|
+
AND reconciliation.outbox_count = 0
|
|
1102
|
+
AND reconciliation.evidence_count = 0
|
|
1103
|
+
AND reconciliation.event_count = 0
|
|
1104
|
+
AND dispatch.attempt_count < 3 THEN
|
|
1105
|
+
UPDATE semicolony.cron_read_action_dispatch_outbox AS current_dispatch
|
|
1106
|
+
SET status = 'pending',
|
|
1107
|
+
claim_id = NULL,
|
|
1108
|
+
claimed_at = NULL,
|
|
1109
|
+
claim_expires_at = NULL,
|
|
1110
|
+
next_attempt_at = db_now,
|
|
1111
|
+
updated_at = db_now
|
|
1112
|
+
WHERE current_dispatch.idempotency_key = p_idempotency_key;
|
|
1113
|
+
outcome := 'safe_to_retry_pre_effect';
|
|
1114
|
+
ELSIF reconciliation.lease_count = 0
|
|
1115
|
+
AND reconciliation.outbox_count = 0
|
|
1116
|
+
AND reconciliation.evidence_count = 0
|
|
1117
|
+
AND reconciliation.event_count = 0 THEN
|
|
1118
|
+
UPDATE semicolony.cron_read_action_dispatch_outbox AS current_dispatch
|
|
1119
|
+
SET status = 'failed',
|
|
1120
|
+
claim_id = NULL,
|
|
1121
|
+
claimed_at = NULL,
|
|
1122
|
+
claim_expires_at = NULL,
|
|
1123
|
+
result_code = 'retry_exhausted',
|
|
1124
|
+
resolved_at = db_now,
|
|
1125
|
+
updated_at = db_now
|
|
1126
|
+
WHERE current_dispatch.idempotency_key = p_idempotency_key;
|
|
1127
|
+
outcome := 'failed';
|
|
1128
|
+
ELSIF reconciliation.lease_count = 1
|
|
1129
|
+
AND reconciliation.lease_claim_id = dispatch.occurrence_claim_id
|
|
1130
|
+
AND reconciliation.lease_state = 'completed'
|
|
1131
|
+
AND reconciliation.lease_completed_at IS NOT NULL
|
|
1132
|
+
AND reconciliation.evidence_count = 1
|
|
1133
|
+
AND reconciliation.evidence_claim_id = dispatch.occurrence_claim_id
|
|
1134
|
+
AND reconciliation.evidence_stage = 'scheduled_primary'
|
|
1135
|
+
AND reconciliation.evidence_run_status = 'success'
|
|
1136
|
+
AND reconciliation.evidence_completed_at = reconciliation.lease_completed_at
|
|
1137
|
+
AND reconciliation.event_count = 1
|
|
1138
|
+
AND reconciliation.event_commitment_id = reconciliation.evidence_commitment_id
|
|
1139
|
+
AND reconciliation.event_type = 'cron_run_recorded'
|
|
1140
|
+
AND reconciliation.event_run_status = reconciliation.evidence_run_status
|
|
1141
|
+
AND reconciliation.event_output_digest = reconciliation.evidence_output_digest
|
|
1142
|
+
AND reconciliation.event_occurred_at = reconciliation.evidence_completed_at
|
|
1143
|
+
AND (
|
|
1144
|
+
(
|
|
1145
|
+
reconciliation.evidence_relay_required = 'true'::jsonb
|
|
1146
|
+
AND reconciliation.evidence_relay_outcome = 'delivered'
|
|
1147
|
+
AND reconciliation.evidence_relay_request_digest = reconciliation.outbox_request_digest
|
|
1148
|
+
AND reconciliation.outbox_count = 1
|
|
1149
|
+
AND reconciliation.outbox_state = 'completed'
|
|
1150
|
+
AND reconciliation.outbox_outcome_status = 'delivered'
|
|
1151
|
+
AND reconciliation.outbox_error_code IS NULL
|
|
1152
|
+
AND reconciliation.outbox_completed_at IS NOT NULL
|
|
1153
|
+
)
|
|
1154
|
+
OR
|
|
1155
|
+
(
|
|
1156
|
+
reconciliation.evidence_relay_required = 'false'::jsonb
|
|
1157
|
+
AND reconciliation.evidence_relay_outcome = 'suppressed'
|
|
1158
|
+
AND reconciliation.evidence_relay_request_digest IS NULL
|
|
1159
|
+
AND reconciliation.outbox_count = 0
|
|
1160
|
+
)
|
|
1161
|
+
) THEN
|
|
1162
|
+
UPDATE semicolony.cron_read_action_dispatch_outbox AS current_dispatch
|
|
1163
|
+
SET status = 'completed',
|
|
1164
|
+
claim_id = NULL,
|
|
1165
|
+
claimed_at = NULL,
|
|
1166
|
+
claim_expires_at = NULL,
|
|
1167
|
+
result_code = 'completed',
|
|
1168
|
+
resolved_at = db_now,
|
|
1169
|
+
updated_at = db_now
|
|
1170
|
+
WHERE current_dispatch.idempotency_key = p_idempotency_key;
|
|
1171
|
+
outcome := 'completed';
|
|
1172
|
+
ELSIF reconciliation.lease_count = 1
|
|
1173
|
+
AND reconciliation.lease_claim_id = dispatch.occurrence_claim_id
|
|
1174
|
+
AND reconciliation.lease_state = 'released_failed'
|
|
1175
|
+
AND reconciliation.lease_completed_at IS NOT NULL
|
|
1176
|
+
AND reconciliation.outbox_count = 0
|
|
1177
|
+
AND reconciliation.evidence_count = 0
|
|
1178
|
+
AND reconciliation.event_count = 0 THEN
|
|
1179
|
+
UPDATE semicolony.cron_read_action_dispatch_outbox AS current_dispatch
|
|
1180
|
+
SET status = 'failed',
|
|
1181
|
+
claim_id = NULL,
|
|
1182
|
+
claimed_at = NULL,
|
|
1183
|
+
claim_expires_at = NULL,
|
|
1184
|
+
result_code = 'released_failed',
|
|
1185
|
+
resolved_at = db_now,
|
|
1186
|
+
updated_at = db_now
|
|
1187
|
+
WHERE current_dispatch.idempotency_key = p_idempotency_key;
|
|
1188
|
+
outcome := 'failed';
|
|
1189
|
+
ELSE
|
|
1190
|
+
UPDATE semicolony.cron_read_action_dispatch_outbox AS current_dispatch
|
|
1191
|
+
SET status = 'blocked',
|
|
1192
|
+
claim_id = NULL,
|
|
1193
|
+
claimed_at = NULL,
|
|
1194
|
+
claim_expires_at = NULL,
|
|
1195
|
+
result_code = 'ambiguous_authority_state',
|
|
1196
|
+
resolved_at = db_now,
|
|
1197
|
+
updated_at = db_now
|
|
1198
|
+
WHERE current_dispatch.idempotency_key = p_idempotency_key;
|
|
1199
|
+
outcome := 'blocked';
|
|
1200
|
+
END IF;
|
|
1201
|
+
RETURN NEXT;
|
|
1202
|
+
END;
|
|
1203
|
+
$$;
|
|
1204
|
+
|
|
1205
|
+
CREATE FUNCTION semicolony.hive_cron_mark_dispatch_skipped_stale(
|
|
1206
|
+
p_job_id text,
|
|
1207
|
+
p_idempotency_key text,
|
|
1208
|
+
p_claim_id text,
|
|
1209
|
+
p_observed_at timestamptz
|
|
1210
|
+
)
|
|
1211
|
+
RETURNS TABLE (outcome text)
|
|
1212
|
+
LANGUAGE plpgsql
|
|
1213
|
+
SECURITY DEFINER
|
|
1214
|
+
SET search_path = pg_catalog
|
|
1215
|
+
SET TimeZone = 'UTC'
|
|
1216
|
+
AS $$
|
|
1217
|
+
DECLARE
|
|
1218
|
+
dispatch record;
|
|
1219
|
+
current_binding record;
|
|
1220
|
+
db_now timestamptz;
|
|
1221
|
+
BEGIN
|
|
1222
|
+
IF session_user <> 'sc_provider'
|
|
1223
|
+
OR public.is_provider() IS DISTINCT FROM true THEN
|
|
1224
|
+
RAISE EXCEPTION 'provider_mode_required' USING ERRCODE = '42501';
|
|
1225
|
+
END IF;
|
|
1226
|
+
db_now := pg_catalog.clock_timestamp();
|
|
1227
|
+
IF p_job_id IS NULL
|
|
1228
|
+
OR (p_job_id COLLATE "C") !~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$'
|
|
1229
|
+
OR p_idempotency_key IS NULL
|
|
1230
|
+
OR p_idempotency_key <> 'cron:' || p_job_id || ':' || pg_catalog.right(p_idempotency_key, 24)
|
|
1231
|
+
OR pg_catalog.right(p_idempotency_key, 24) !~
|
|
1232
|
+
'^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}\.[0-9]{3}Z$'
|
|
1233
|
+
OR p_claim_id IS NULL
|
|
1234
|
+
OR (p_claim_id COLLATE "C") !~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$'
|
|
1235
|
+
OR p_observed_at IS NULL
|
|
1236
|
+
OR p_observed_at < db_now - interval '30 seconds'
|
|
1237
|
+
OR p_observed_at > db_now + interval '30 seconds' THEN
|
|
1238
|
+
RAISE EXCEPTION 'invalid_dispatch_stale_resolution' USING ERRCODE = '22023';
|
|
1239
|
+
END IF;
|
|
1240
|
+
|
|
1241
|
+
SELECT current_dispatch.job_id,
|
|
1242
|
+
current_dispatch.status,
|
|
1243
|
+
current_dispatch.claim_id,
|
|
1244
|
+
current_dispatch.claimed_at,
|
|
1245
|
+
current_dispatch.capability_template_ref,
|
|
1246
|
+
current_dispatch.template_updated_at,
|
|
1247
|
+
current_dispatch.permission,
|
|
1248
|
+
current_dispatch.resource_scopes,
|
|
1249
|
+
current_dispatch.destination,
|
|
1250
|
+
current_dispatch.timeout_seconds,
|
|
1251
|
+
current_dispatch.expected_outcome_digest,
|
|
1252
|
+
current_dispatch.source_hash,
|
|
1253
|
+
current_dispatch.action_kind,
|
|
1254
|
+
current_dispatch.action_version,
|
|
1255
|
+
current_dispatch.action_contract_digest,
|
|
1256
|
+
current_dispatch.trusted_config_ref,
|
|
1257
|
+
current_dispatch.trusted_config_revision,
|
|
1258
|
+
current_dispatch.trusted_config_digest
|
|
1259
|
+
INTO dispatch
|
|
1260
|
+
FROM semicolony.cron_read_action_dispatch_outbox AS current_dispatch
|
|
1261
|
+
WHERE current_dispatch.job_id = p_job_id
|
|
1262
|
+
AND current_dispatch.idempotency_key = p_idempotency_key
|
|
1263
|
+
FOR UPDATE;
|
|
1264
|
+
IF NOT FOUND THEN
|
|
1265
|
+
outcome := 'not_found';
|
|
1266
|
+
RETURN NEXT;
|
|
1267
|
+
RETURN;
|
|
1268
|
+
END IF;
|
|
1269
|
+
IF dispatch.status <> 'claimed' OR dispatch.claim_id <> p_claim_id THEN
|
|
1270
|
+
outcome := 'claim_mismatch';
|
|
1271
|
+
RETURN NEXT;
|
|
1272
|
+
RETURN;
|
|
1273
|
+
END IF;
|
|
1274
|
+
SELECT job.enabled,
|
|
1275
|
+
job.runtime_source,
|
|
1276
|
+
template.updated_at AS template_updated_at,
|
|
1277
|
+
template.permission,
|
|
1278
|
+
template.resource_scopes AS raw_resource_scopes,
|
|
1279
|
+
canonical.resource_scopes,
|
|
1280
|
+
template.destination,
|
|
1281
|
+
template.timeout_seconds,
|
|
1282
|
+
template.expected_outcome_digest,
|
|
1283
|
+
template.source_hash,
|
|
1284
|
+
template.action_kind,
|
|
1285
|
+
template.action_version,
|
|
1286
|
+
template.action_contract_digest,
|
|
1287
|
+
config.config_revision,
|
|
1288
|
+
config.config_digest,
|
|
1289
|
+
config.action_kind AS config_action_kind,
|
|
1290
|
+
config.action_version AS config_action_version,
|
|
1291
|
+
config.action_contract_digest AS config_action_contract_digest
|
|
1292
|
+
INTO current_binding
|
|
1293
|
+
FROM semicolony.bot_cron_jobs AS job
|
|
1294
|
+
LEFT JOIN semicolony.cron_capability_templates AS template
|
|
1295
|
+
ON template.bot_id = job.bot_id
|
|
1296
|
+
AND template.job_id = job.job_id
|
|
1297
|
+
AND template.runtime_source = 'hive-cron-v2'
|
|
1298
|
+
LEFT JOIN LATERAL (
|
|
1299
|
+
SELECT pg_catalog.array_agg(scope ORDER BY scope COLLATE "C") AS resource_scopes
|
|
1300
|
+
FROM (
|
|
1301
|
+
SELECT DISTINCT scope
|
|
1302
|
+
FROM pg_catalog.unnest(template.resource_scopes) AS scope
|
|
1303
|
+
) AS unique_scope
|
|
1304
|
+
) AS canonical ON true
|
|
1305
|
+
LEFT JOIN LATERAL (
|
|
1306
|
+
SELECT revision.config_revision,
|
|
1307
|
+
revision.config_digest,
|
|
1308
|
+
revision.action_kind,
|
|
1309
|
+
revision.action_version,
|
|
1310
|
+
revision.action_contract_digest
|
|
1311
|
+
FROM semicolony.cron_read_action_trusted_config_revisions AS revision
|
|
1312
|
+
WHERE revision.bot_id = 'semi'
|
|
1313
|
+
AND revision.job_id = job.job_id
|
|
1314
|
+
AND revision.runtime_source = 'hive-cron-v2'
|
|
1315
|
+
ORDER BY revision.config_revision DESC
|
|
1316
|
+
LIMIT 1
|
|
1317
|
+
) AS config ON true
|
|
1318
|
+
WHERE job.bot_id = 'semi'
|
|
1319
|
+
AND job.job_id = p_job_id;
|
|
1320
|
+
|
|
1321
|
+
IF FOUND
|
|
1322
|
+
AND current_binding.enabled = true
|
|
1323
|
+
AND current_binding.runtime_source = 'hive-cron-v2'
|
|
1324
|
+
AND current_binding.action_kind IS NOT NULL
|
|
1325
|
+
AND current_binding.action_version IS NOT NULL
|
|
1326
|
+
AND current_binding.action_contract_digest IS NOT NULL
|
|
1327
|
+
AND pg_catalog.cardinality(current_binding.raw_resource_scopes) BETWEEN 1 AND 32
|
|
1328
|
+
AND pg_catalog.array_position(current_binding.raw_resource_scopes, NULL) IS NULL
|
|
1329
|
+
AND pg_catalog.cardinality(current_binding.raw_resource_scopes) =
|
|
1330
|
+
pg_catalog.cardinality(current_binding.resource_scopes)
|
|
1331
|
+
AND NOT EXISTS (
|
|
1332
|
+
SELECT 1
|
|
1333
|
+
FROM pg_catalog.unnest(current_binding.raw_resource_scopes) AS scope
|
|
1334
|
+
WHERE (scope COLLATE "C") !~ '^[!-~]+$'
|
|
1335
|
+
OR pg_catalog.char_length(scope) NOT BETWEEN 1 AND 256
|
|
1336
|
+
OR pg_catalog.strpos(scope, '*') > 0
|
|
1337
|
+
)
|
|
1338
|
+
AND current_binding.config_revision IS NOT NULL
|
|
1339
|
+
AND current_binding.config_action_kind = current_binding.action_kind
|
|
1340
|
+
AND current_binding.config_action_version = current_binding.action_version
|
|
1341
|
+
AND current_binding.config_action_contract_digest = current_binding.action_contract_digest
|
|
1342
|
+
AND dispatch.capability_template_ref = 'semi:' || p_job_id || ':hive-cron-v2'
|
|
1343
|
+
AND dispatch.template_updated_at = current_binding.template_updated_at
|
|
1344
|
+
AND dispatch.permission = current_binding.permission
|
|
1345
|
+
AND dispatch.resource_scopes = current_binding.resource_scopes
|
|
1346
|
+
AND dispatch.destination = current_binding.destination
|
|
1347
|
+
AND dispatch.timeout_seconds = current_binding.timeout_seconds
|
|
1348
|
+
AND dispatch.expected_outcome_digest = current_binding.expected_outcome_digest
|
|
1349
|
+
AND dispatch.source_hash = current_binding.source_hash
|
|
1350
|
+
AND dispatch.action_kind = current_binding.action_kind
|
|
1351
|
+
AND dispatch.action_version = current_binding.action_version
|
|
1352
|
+
AND dispatch.action_contract_digest = current_binding.action_contract_digest
|
|
1353
|
+
AND dispatch.trusted_config_ref =
|
|
1354
|
+
'semi:' || p_job_id || ':hive-cron-v2:config:r' || current_binding.config_revision::text
|
|
1355
|
+
AND dispatch.trusted_config_revision = current_binding.config_revision
|
|
1356
|
+
AND dispatch.trusted_config_digest = current_binding.config_digest THEN
|
|
1357
|
+
outcome := 'not_stale';
|
|
1358
|
+
RETURN NEXT;
|
|
1359
|
+
RETURN;
|
|
1360
|
+
END IF;
|
|
1361
|
+
|
|
1362
|
+
UPDATE semicolony.cron_read_action_dispatch_outbox AS current_dispatch
|
|
1363
|
+
SET status = 'failed',
|
|
1364
|
+
claim_id = NULL,
|
|
1365
|
+
claimed_at = NULL,
|
|
1366
|
+
claim_expires_at = NULL,
|
|
1367
|
+
result_code = 'stale_binding',
|
|
1368
|
+
resolved_at = db_now,
|
|
1369
|
+
updated_at = db_now
|
|
1370
|
+
WHERE current_dispatch.idempotency_key = p_idempotency_key;
|
|
1371
|
+
outcome := 'failed';
|
|
1372
|
+
RETURN NEXT;
|
|
1373
|
+
END;
|
|
1374
|
+
$$;
|
|
1375
|
+
|
|
1376
|
+
REVOKE ALL PRIVILEGES ON TABLE
|
|
1377
|
+
semicolony.cron_read_action_dispatch_outbox
|
|
1378
|
+
FROM PUBLIC, sc_app, sc_provider;
|
|
1379
|
+
|
|
1380
|
+
REVOKE ALL ON FUNCTION semicolony.hive_cron_protect_terminal_dispatch()
|
|
1381
|
+
FROM PUBLIC, sc_app, sc_provider;
|
|
1382
|
+
REVOKE ALL ON FUNCTION semicolony.hive_cron_preview_due_dispatches(timestamptz, integer)
|
|
1383
|
+
FROM PUBLIC, sc_app, sc_provider;
|
|
1384
|
+
REVOKE ALL ON FUNCTION semicolony.hive_cron_claim_dispatch_batch(timestamptz, jsonb)
|
|
1385
|
+
FROM PUBLIC, sc_app, sc_provider;
|
|
1386
|
+
REVOKE ALL ON FUNCTION semicolony.hive_cron_take_dispatch(timestamptz, text, integer)
|
|
1387
|
+
FROM PUBLIC, sc_app, sc_provider;
|
|
1388
|
+
REVOKE ALL ON FUNCTION semicolony.hive_cron_list_abandoned_dispatches(timestamptz, integer)
|
|
1389
|
+
FROM PUBLIC, sc_app, sc_provider;
|
|
1390
|
+
REVOKE ALL ON FUNCTION semicolony.hive_cron_resolve_dispatch(text, text, text, timestamptz)
|
|
1391
|
+
FROM PUBLIC, sc_app, sc_provider;
|
|
1392
|
+
REVOKE ALL ON FUNCTION semicolony.hive_cron_mark_dispatch_skipped_stale(text, text, text, timestamptz)
|
|
1393
|
+
FROM PUBLIC, sc_app, sc_provider;
|
|
1394
|
+
|
|
1395
|
+
GRANT EXECUTE ON FUNCTION semicolony.hive_cron_preview_due_dispatches(timestamptz, integer)
|
|
1396
|
+
TO sc_provider;
|
|
1397
|
+
GRANT EXECUTE ON FUNCTION semicolony.hive_cron_claim_dispatch_batch(timestamptz, jsonb)
|
|
1398
|
+
TO sc_provider;
|
|
1399
|
+
GRANT EXECUTE ON FUNCTION semicolony.hive_cron_take_dispatch(timestamptz, text, integer)
|
|
1400
|
+
TO sc_provider;
|
|
1401
|
+
GRANT EXECUTE ON FUNCTION semicolony.hive_cron_list_abandoned_dispatches(timestamptz, integer)
|
|
1402
|
+
TO sc_provider;
|
|
1403
|
+
GRANT EXECUTE ON FUNCTION semicolony.hive_cron_resolve_dispatch(text, text, text, timestamptz)
|
|
1404
|
+
TO sc_provider;
|
|
1405
|
+
GRANT EXECUTE ON FUNCTION semicolony.hive_cron_mark_dispatch_skipped_stale(text, text, text, timestamptz)
|
|
1406
|
+
TO sc_provider;
|