@team-semicolon/semicolony-cli 4.18.51 → 4.18.52
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/package.json +4 -2
|
@@ -0,0 +1,600 @@
|
|
|
1
|
+
-- @schema-retarget: off
|
|
2
|
+
-- Independent, side-effect-free Hive cron shadow cursors and bounded observations.
|
|
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
|
+
IF NOT role_state.rolcanlogin
|
|
19
|
+
OR role_state.rolsuper
|
|
20
|
+
OR role_state.rolcreatedb
|
|
21
|
+
OR role_state.rolcreaterole
|
|
22
|
+
OR role_state.rolinherit
|
|
23
|
+
OR role_state.rolreplication
|
|
24
|
+
OR role_state.rolbypassrls THEN
|
|
25
|
+
RAISE EXCEPTION 'sc_provider role attributes are unsafe';
|
|
26
|
+
END IF;
|
|
27
|
+
END $$;
|
|
28
|
+
|
|
29
|
+
CREATE FUNCTION semicolony.hive_cron_shadow_sha256(p_value text)
|
|
30
|
+
RETURNS text
|
|
31
|
+
LANGUAGE sql
|
|
32
|
+
IMMUTABLE
|
|
33
|
+
STRICT
|
|
34
|
+
SECURITY DEFINER
|
|
35
|
+
SET search_path = pg_catalog
|
|
36
|
+
SET TimeZone = 'UTC'
|
|
37
|
+
AS $$
|
|
38
|
+
SELECT 'sha256:' || pg_catalog.encode(
|
|
39
|
+
pg_catalog.sha256(pg_catalog.convert_to(p_value, 'UTF8')),
|
|
40
|
+
'hex'
|
|
41
|
+
);
|
|
42
|
+
$$;
|
|
43
|
+
|
|
44
|
+
REVOKE ALL ON FUNCTION semicolony.hive_cron_shadow_sha256(text)
|
|
45
|
+
FROM PUBLIC, sc_app, sc_provider;
|
|
46
|
+
|
|
47
|
+
CREATE TABLE semicolony.hive_cron_shadow_cursors (
|
|
48
|
+
bot_id TEXT NOT NULL CHECK (bot_id = 'semi'),
|
|
49
|
+
job_id TEXT NOT NULL CHECK (
|
|
50
|
+
(job_id COLLATE "C") ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$'
|
|
51
|
+
),
|
|
52
|
+
runtime_source TEXT NOT NULL CHECK (runtime_source = 'hive-cron-v2'),
|
|
53
|
+
cursor_version BIGINT NOT NULL CHECK (cursor_version > 0),
|
|
54
|
+
next_scheduled_for TIMESTAMPTZ NOT NULL,
|
|
55
|
+
schedule_digest TEXT NOT NULL CHECK (schedule_digest ~ '^sha256:[a-f0-9]{64}$'),
|
|
56
|
+
template_digest TEXT NOT NULL CHECK (template_digest ~ '^sha256:[a-f0-9]{64}$'),
|
|
57
|
+
binding_digest TEXT NOT NULL CHECK (binding_digest ~ '^sha256:[a-f0-9]{64}$'),
|
|
58
|
+
created_at TIMESTAMPTZ NOT NULL,
|
|
59
|
+
updated_at TIMESTAMPTZ NOT NULL,
|
|
60
|
+
PRIMARY KEY (bot_id, job_id, runtime_source),
|
|
61
|
+
FOREIGN KEY (bot_id, job_id, runtime_source)
|
|
62
|
+
REFERENCES semicolony.cron_capability_templates (bot_id, job_id, runtime_source)
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
COMMENT ON TABLE semicolony.hive_cron_shadow_cursors IS
|
|
66
|
+
'Independent Hive shadow schedule cursor; never copies or advances primary next_run.';
|
|
67
|
+
|
|
68
|
+
CREATE TABLE semicolony.hive_cron_shadow_observations (
|
|
69
|
+
observation_id TEXT PRIMARY KEY CHECK (
|
|
70
|
+
observation_id = 'shadow-' || pg_catalog.substr(
|
|
71
|
+
semicolony.hive_cron_shadow_sha256(
|
|
72
|
+
bot_id || E'\n' || job_id || E'\n' || runtime_source || E'\n' || after_cursor_version::text
|
|
73
|
+
),
|
|
74
|
+
8
|
|
75
|
+
)
|
|
76
|
+
),
|
|
77
|
+
bot_id TEXT NOT NULL CHECK (bot_id = 'semi'),
|
|
78
|
+
job_id TEXT NOT NULL CHECK (
|
|
79
|
+
(job_id COLLATE "C") ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$'
|
|
80
|
+
),
|
|
81
|
+
runtime_source TEXT NOT NULL CHECK (runtime_source = 'hive-cron-v2'),
|
|
82
|
+
transition TEXT NOT NULL CHECK (transition IN ('initialized', 'advanced')),
|
|
83
|
+
before_cursor_version BIGINT,
|
|
84
|
+
before_next_scheduled_for TIMESTAMPTZ,
|
|
85
|
+
after_cursor_version BIGINT NOT NULL CHECK (after_cursor_version > 0),
|
|
86
|
+
after_next_scheduled_for TIMESTAMPTZ NOT NULL,
|
|
87
|
+
observed_at TIMESTAMPTZ NOT NULL,
|
|
88
|
+
due_count INTEGER NOT NULL CHECK (due_count BETWEEN 0 AND 512),
|
|
89
|
+
skipped_count INTEGER NOT NULL CHECK (skipped_count BETWEEN 0 AND 511),
|
|
90
|
+
enabled BOOLEAN NOT NULL,
|
|
91
|
+
schedule_digest TEXT NOT NULL CHECK (schedule_digest ~ '^sha256:[a-f0-9]{64}$'),
|
|
92
|
+
template_digest TEXT NOT NULL CHECK (template_digest ~ '^sha256:[a-f0-9]{64}$'),
|
|
93
|
+
binding_digest TEXT NOT NULL CHECK (binding_digest ~ '^sha256:[a-f0-9]{64}$'),
|
|
94
|
+
occurrence_digest TEXT NOT NULL CHECK (occurrence_digest ~ '^sha256:[a-f0-9]{64}$'),
|
|
95
|
+
observation_digest TEXT NOT NULL CHECK (observation_digest ~ '^sha256:[a-f0-9]{64}$'),
|
|
96
|
+
UNIQUE (bot_id, job_id, runtime_source, after_cursor_version),
|
|
97
|
+
FOREIGN KEY (bot_id, job_id, runtime_source)
|
|
98
|
+
REFERENCES semicolony.cron_capability_templates (bot_id, job_id, runtime_source),
|
|
99
|
+
CHECK (after_next_scheduled_for > observed_at),
|
|
100
|
+
CHECK (
|
|
101
|
+
(
|
|
102
|
+
transition = 'initialized'
|
|
103
|
+
AND before_cursor_version IS NULL
|
|
104
|
+
AND before_next_scheduled_for IS NULL
|
|
105
|
+
AND after_cursor_version = 1
|
|
106
|
+
AND due_count = 0
|
|
107
|
+
AND skipped_count = 0
|
|
108
|
+
)
|
|
109
|
+
OR
|
|
110
|
+
(
|
|
111
|
+
transition = 'advanced'
|
|
112
|
+
AND before_cursor_version IS NOT NULL
|
|
113
|
+
AND before_cursor_version > 0
|
|
114
|
+
AND before_next_scheduled_for IS NOT NULL
|
|
115
|
+
AND before_next_scheduled_for <= observed_at
|
|
116
|
+
AND after_cursor_version = before_cursor_version + 1
|
|
117
|
+
AND due_count BETWEEN 1 AND 512
|
|
118
|
+
AND skipped_count = due_count - 1
|
|
119
|
+
)
|
|
120
|
+
)
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
COMMENT ON TABLE semicolony.hive_cron_shadow_observations IS
|
|
124
|
+
'Append-only bounded shadow cursor transitions without raw schedules, templates, or payloads.';
|
|
125
|
+
|
|
126
|
+
ALTER TABLE semicolony.hive_cron_shadow_cursors
|
|
127
|
+
ADD CONSTRAINT hive_cron_shadow_cursor_current_observation_fk
|
|
128
|
+
FOREIGN KEY (bot_id, job_id, runtime_source, cursor_version)
|
|
129
|
+
REFERENCES semicolony.hive_cron_shadow_observations
|
|
130
|
+
(bot_id, job_id, runtime_source, after_cursor_version)
|
|
131
|
+
DEFERRABLE INITIALLY DEFERRED;
|
|
132
|
+
|
|
133
|
+
CREATE FUNCTION semicolony.hive_cron_protect_shadow_observation()
|
|
134
|
+
RETURNS trigger
|
|
135
|
+
LANGUAGE plpgsql
|
|
136
|
+
SECURITY DEFINER
|
|
137
|
+
SET search_path = pg_catalog
|
|
138
|
+
SET TimeZone = 'UTC'
|
|
139
|
+
AS $$
|
|
140
|
+
BEGIN
|
|
141
|
+
RAISE EXCEPTION 'shadow_observation_immutable' USING ERRCODE = '55000';
|
|
142
|
+
END;
|
|
143
|
+
$$;
|
|
144
|
+
|
|
145
|
+
REVOKE ALL ON FUNCTION semicolony.hive_cron_protect_shadow_observation()
|
|
146
|
+
FROM PUBLIC, sc_app, sc_provider;
|
|
147
|
+
|
|
148
|
+
CREATE TRIGGER protect_hive_cron_shadow_observation
|
|
149
|
+
BEFORE UPDATE OR DELETE ON semicolony.hive_cron_shadow_observations
|
|
150
|
+
FOR EACH ROW EXECUTE FUNCTION semicolony.hive_cron_protect_shadow_observation();
|
|
151
|
+
|
|
152
|
+
CREATE FUNCTION semicolony.hive_cron_advance_shadow(p_now timestamptz)
|
|
153
|
+
RETURNS TABLE (
|
|
154
|
+
transition text,
|
|
155
|
+
observation_id text,
|
|
156
|
+
bot_id text,
|
|
157
|
+
job_id text,
|
|
158
|
+
runtime_source text,
|
|
159
|
+
before_cursor_version bigint,
|
|
160
|
+
before_next_scheduled_for timestamptz,
|
|
161
|
+
after_cursor_version bigint,
|
|
162
|
+
after_next_scheduled_for timestamptz,
|
|
163
|
+
observed_at timestamptz,
|
|
164
|
+
due_count integer,
|
|
165
|
+
skipped_count integer,
|
|
166
|
+
enabled boolean,
|
|
167
|
+
schedule_digest text,
|
|
168
|
+
template_digest text,
|
|
169
|
+
binding_digest text,
|
|
170
|
+
occurrence_digest text,
|
|
171
|
+
observation_digest text
|
|
172
|
+
)
|
|
173
|
+
LANGUAGE plpgsql
|
|
174
|
+
SECURITY DEFINER
|
|
175
|
+
SET search_path = pg_catalog
|
|
176
|
+
SET TimeZone = 'UTC'
|
|
177
|
+
AS $$
|
|
178
|
+
DECLARE
|
|
179
|
+
candidate record;
|
|
180
|
+
candidates jsonb;
|
|
181
|
+
cursor_state record;
|
|
182
|
+
db_now timestamptz;
|
|
183
|
+
first_future timestamptz;
|
|
184
|
+
second_future timestamptz;
|
|
185
|
+
occurrence timestamptz;
|
|
186
|
+
previous_occurrence timestamptz;
|
|
187
|
+
occurrence_lines text[];
|
|
188
|
+
transition_value text;
|
|
189
|
+
observation_id_value text;
|
|
190
|
+
before_cursor_version_value bigint;
|
|
191
|
+
before_next_scheduled_for_value timestamptz;
|
|
192
|
+
after_cursor_version_value bigint;
|
|
193
|
+
after_next_scheduled_for_value timestamptz;
|
|
194
|
+
due_count_value integer;
|
|
195
|
+
skipped_count_value integer;
|
|
196
|
+
occurrence_digest_value text;
|
|
197
|
+
observation_digest_value text;
|
|
198
|
+
changed integer;
|
|
199
|
+
BEGIN
|
|
200
|
+
IF session_user <> 'sc_provider'
|
|
201
|
+
OR public.is_provider() IS DISTINCT FROM true THEN
|
|
202
|
+
RAISE EXCEPTION 'provider_mode_required' USING ERRCODE = '42501';
|
|
203
|
+
END IF;
|
|
204
|
+
|
|
205
|
+
PERFORM pg_catalog.pg_advisory_xact_lock(
|
|
206
|
+
pg_catalog.hashtextextended('semicolony.hive_cron_shadow_cursor', 0)
|
|
207
|
+
);
|
|
208
|
+
db_now := pg_catalog.clock_timestamp();
|
|
209
|
+
IF p_now IS NULL
|
|
210
|
+
OR p_now < db_now - interval '30 seconds'
|
|
211
|
+
OR p_now > db_now + interval '30 seconds' THEN
|
|
212
|
+
RAISE EXCEPTION 'invalid_shadow_clock' USING ERRCODE = '22023';
|
|
213
|
+
END IF;
|
|
214
|
+
|
|
215
|
+
SELECT COALESCE(
|
|
216
|
+
pg_catalog.jsonb_agg(
|
|
217
|
+
pg_catalog.jsonb_build_object(
|
|
218
|
+
'bot_id', candidate_source.bot_id,
|
|
219
|
+
'job_id', candidate_source.job_id,
|
|
220
|
+
'runtime_source', candidate_source.runtime_source,
|
|
221
|
+
'job_runtime_source', candidate_source.job_runtime_source,
|
|
222
|
+
'schedule', candidate_source.schedule,
|
|
223
|
+
'enabled', candidate_source.enabled,
|
|
224
|
+
'schedule_digest', candidate_source.schedule_digest,
|
|
225
|
+
'template_digest', candidate_source.template_digest,
|
|
226
|
+
'binding_digest', semicolony.hive_cron_shadow_sha256(
|
|
227
|
+
candidate_source.schedule_digest || E'\n' ||
|
|
228
|
+
candidate_source.template_digest
|
|
229
|
+
)
|
|
230
|
+
) ORDER BY candidate_source.job_id COLLATE "C"
|
|
231
|
+
),
|
|
232
|
+
'[]'::jsonb
|
|
233
|
+
)
|
|
234
|
+
INTO candidates
|
|
235
|
+
FROM (
|
|
236
|
+
SELECT template.bot_id,
|
|
237
|
+
template.job_id,
|
|
238
|
+
template.runtime_source,
|
|
239
|
+
job.runtime_source AS job_runtime_source,
|
|
240
|
+
job.schedule,
|
|
241
|
+
job.enabled,
|
|
242
|
+
digests.schedule_digest,
|
|
243
|
+
digests.template_digest
|
|
244
|
+
FROM (
|
|
245
|
+
SELECT template_bound.*
|
|
246
|
+
FROM semicolony.cron_capability_templates AS template_bound
|
|
247
|
+
WHERE template_bound.bot_id = 'semi'
|
|
248
|
+
AND template_bound.runtime_source = 'hive-cron-v2'
|
|
249
|
+
ORDER BY template_bound.job_id COLLATE "C"
|
|
250
|
+
LIMIT 21
|
|
251
|
+
) AS template
|
|
252
|
+
LEFT JOIN semicolony.bot_cron_jobs AS job
|
|
253
|
+
ON job.bot_id = template.bot_id
|
|
254
|
+
AND job.job_id = template.job_id
|
|
255
|
+
CROSS JOIN LATERAL (
|
|
256
|
+
SELECT semicolony.hive_cron_shadow_sha256(job.schedule::text) AS schedule_digest,
|
|
257
|
+
semicolony.hive_cron_shadow_sha256(
|
|
258
|
+
pg_catalog.to_jsonb(template)::text
|
|
259
|
+
) AS template_digest
|
|
260
|
+
) AS digests
|
|
261
|
+
) AS candidate_source;
|
|
262
|
+
|
|
263
|
+
IF pg_catalog.jsonb_array_length(candidates) > 20 THEN
|
|
264
|
+
RAISE EXCEPTION 'shadow_candidate_limit_exceeded' USING ERRCODE = '54000';
|
|
265
|
+
END IF;
|
|
266
|
+
|
|
267
|
+
FOR candidate IN
|
|
268
|
+
SELECT candidate_row.*
|
|
269
|
+
FROM pg_catalog.jsonb_to_recordset(candidates) AS candidate_row(
|
|
270
|
+
bot_id text,
|
|
271
|
+
job_id text,
|
|
272
|
+
runtime_source text,
|
|
273
|
+
job_runtime_source text,
|
|
274
|
+
schedule jsonb,
|
|
275
|
+
enabled boolean,
|
|
276
|
+
schedule_digest text,
|
|
277
|
+
template_digest text,
|
|
278
|
+
binding_digest text
|
|
279
|
+
)
|
|
280
|
+
ORDER BY candidate_row.job_id COLLATE "C"
|
|
281
|
+
LOOP
|
|
282
|
+
IF candidate.job_runtime_source IS DISTINCT FROM candidate.runtime_source THEN
|
|
283
|
+
RAISE EXCEPTION 'shadow_binding_drift' USING ERRCODE = '40001';
|
|
284
|
+
END IF;
|
|
285
|
+
SELECT cursor.schedule_digest,
|
|
286
|
+
cursor.template_digest,
|
|
287
|
+
cursor.binding_digest
|
|
288
|
+
INTO cursor_state
|
|
289
|
+
FROM semicolony.hive_cron_shadow_cursors AS cursor
|
|
290
|
+
WHERE cursor.bot_id = candidate.bot_id
|
|
291
|
+
AND cursor.job_id = candidate.job_id
|
|
292
|
+
AND cursor.runtime_source = candidate.runtime_source;
|
|
293
|
+
IF FOUND AND (
|
|
294
|
+
cursor_state.schedule_digest IS DISTINCT FROM candidate.schedule_digest
|
|
295
|
+
OR cursor_state.template_digest IS DISTINCT FROM candidate.template_digest
|
|
296
|
+
OR cursor_state.binding_digest IS DISTINCT FROM candidate.binding_digest
|
|
297
|
+
) THEN
|
|
298
|
+
RAISE EXCEPTION 'shadow_binding_drift' USING ERRCODE = '40001';
|
|
299
|
+
END IF;
|
|
300
|
+
BEGIN
|
|
301
|
+
first_future := semicolony.hive_cron_derive_next_run(candidate.schedule, db_now);
|
|
302
|
+
second_future := semicolony.hive_cron_derive_next_run(candidate.schedule, first_future);
|
|
303
|
+
EXCEPTION
|
|
304
|
+
WHEN SQLSTATE '22023' THEN
|
|
305
|
+
RAISE EXCEPTION 'shadow_schedule_frequency_invalid' USING ERRCODE = '22023';
|
|
306
|
+
END;
|
|
307
|
+
IF first_future <= db_now
|
|
308
|
+
OR second_future <= first_future
|
|
309
|
+
OR second_future - first_future < interval '30 minutes' THEN
|
|
310
|
+
RAISE EXCEPTION 'shadow_schedule_frequency_invalid' USING ERRCODE = '22023';
|
|
311
|
+
END IF;
|
|
312
|
+
END LOOP;
|
|
313
|
+
|
|
314
|
+
FOR candidate IN
|
|
315
|
+
SELECT candidate_row.*
|
|
316
|
+
FROM pg_catalog.jsonb_to_recordset(candidates) AS candidate_row(
|
|
317
|
+
bot_id text,
|
|
318
|
+
job_id text,
|
|
319
|
+
runtime_source text,
|
|
320
|
+
job_runtime_source text,
|
|
321
|
+
schedule jsonb,
|
|
322
|
+
enabled boolean,
|
|
323
|
+
schedule_digest text,
|
|
324
|
+
template_digest text,
|
|
325
|
+
binding_digest text
|
|
326
|
+
)
|
|
327
|
+
ORDER BY candidate_row.job_id COLLATE "C"
|
|
328
|
+
LOOP
|
|
329
|
+
SELECT cursor.cursor_version,
|
|
330
|
+
cursor.next_scheduled_for,
|
|
331
|
+
cursor.schedule_digest,
|
|
332
|
+
cursor.template_digest,
|
|
333
|
+
cursor.binding_digest
|
|
334
|
+
INTO cursor_state
|
|
335
|
+
FROM semicolony.hive_cron_shadow_cursors AS cursor
|
|
336
|
+
WHERE cursor.bot_id = candidate.bot_id
|
|
337
|
+
AND cursor.job_id = candidate.job_id
|
|
338
|
+
AND cursor.runtime_source = candidate.runtime_source
|
|
339
|
+
FOR UPDATE;
|
|
340
|
+
|
|
341
|
+
occurrence_lines := ARRAY[]::text[];
|
|
342
|
+
due_count_value := 0;
|
|
343
|
+
skipped_count_value := 0;
|
|
344
|
+
before_cursor_version_value := NULL;
|
|
345
|
+
before_next_scheduled_for_value := NULL;
|
|
346
|
+
|
|
347
|
+
IF NOT FOUND THEN
|
|
348
|
+
transition_value := 'initialized';
|
|
349
|
+
after_cursor_version_value := 1;
|
|
350
|
+
after_next_scheduled_for_value := semicolony.hive_cron_derive_next_run(
|
|
351
|
+
candidate.schedule,
|
|
352
|
+
db_now
|
|
353
|
+
);
|
|
354
|
+
occurrence_digest_value := semicolony.hive_cron_shadow_sha256('');
|
|
355
|
+
|
|
356
|
+
INSERT INTO semicolony.hive_cron_shadow_cursors (
|
|
357
|
+
bot_id, job_id, runtime_source, cursor_version, next_scheduled_for,
|
|
358
|
+
schedule_digest, template_digest, binding_digest, created_at, updated_at
|
|
359
|
+
) VALUES (
|
|
360
|
+
candidate.bot_id, candidate.job_id, candidate.runtime_source,
|
|
361
|
+
after_cursor_version_value, after_next_scheduled_for_value,
|
|
362
|
+
candidate.schedule_digest, candidate.template_digest, candidate.binding_digest,
|
|
363
|
+
db_now, db_now
|
|
364
|
+
);
|
|
365
|
+
ELSE
|
|
366
|
+
IF cursor_state.schedule_digest IS DISTINCT FROM candidate.schedule_digest
|
|
367
|
+
OR cursor_state.template_digest IS DISTINCT FROM candidate.template_digest
|
|
368
|
+
OR cursor_state.binding_digest IS DISTINCT FROM candidate.binding_digest THEN
|
|
369
|
+
RAISE EXCEPTION 'shadow_binding_drift' USING ERRCODE = '40001';
|
|
370
|
+
END IF;
|
|
371
|
+
IF cursor_state.next_scheduled_for > db_now THEN
|
|
372
|
+
CONTINUE;
|
|
373
|
+
END IF;
|
|
374
|
+
IF db_now - cursor_state.next_scheduled_for > interval '8 days' THEN
|
|
375
|
+
RAISE EXCEPTION 'shadow_backlog_window_exceeded' USING ERRCODE = '54000';
|
|
376
|
+
END IF;
|
|
377
|
+
|
|
378
|
+
transition_value := 'advanced';
|
|
379
|
+
before_cursor_version_value := cursor_state.cursor_version;
|
|
380
|
+
before_next_scheduled_for_value := cursor_state.next_scheduled_for;
|
|
381
|
+
occurrence := cursor_state.next_scheduled_for;
|
|
382
|
+
WHILE occurrence <= db_now LOOP
|
|
383
|
+
due_count_value := due_count_value + 1;
|
|
384
|
+
IF due_count_value > 512 THEN
|
|
385
|
+
RAISE EXCEPTION 'shadow_occurrence_limit_exceeded' USING ERRCODE = '54000';
|
|
386
|
+
END IF;
|
|
387
|
+
occurrence_lines := pg_catalog.array_append(
|
|
388
|
+
occurrence_lines,
|
|
389
|
+
pg_catalog.to_char(
|
|
390
|
+
occurrence AT TIME ZONE 'UTC',
|
|
391
|
+
'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"'
|
|
392
|
+
)
|
|
393
|
+
);
|
|
394
|
+
previous_occurrence := occurrence;
|
|
395
|
+
occurrence := semicolony.hive_cron_derive_next_run(candidate.schedule, occurrence);
|
|
396
|
+
IF occurrence <= previous_occurrence THEN
|
|
397
|
+
RAISE EXCEPTION 'shadow_non_monotonic_schedule' USING ERRCODE = '22023';
|
|
398
|
+
END IF;
|
|
399
|
+
END LOOP;
|
|
400
|
+
|
|
401
|
+
skipped_count_value := due_count_value - 1;
|
|
402
|
+
after_cursor_version_value := cursor_state.cursor_version + 1;
|
|
403
|
+
after_next_scheduled_for_value := occurrence;
|
|
404
|
+
occurrence_digest_value := semicolony.hive_cron_shadow_sha256(
|
|
405
|
+
pg_catalog.array_to_string(occurrence_lines, E'\n')
|
|
406
|
+
);
|
|
407
|
+
|
|
408
|
+
UPDATE semicolony.hive_cron_shadow_cursors AS cursor
|
|
409
|
+
SET cursor_version = after_cursor_version_value,
|
|
410
|
+
next_scheduled_for = after_next_scheduled_for_value,
|
|
411
|
+
updated_at = db_now
|
|
412
|
+
WHERE cursor.bot_id = candidate.bot_id
|
|
413
|
+
AND cursor.job_id = candidate.job_id
|
|
414
|
+
AND cursor.runtime_source = candidate.runtime_source
|
|
415
|
+
AND cursor.cursor_version = before_cursor_version_value
|
|
416
|
+
AND cursor.next_scheduled_for = before_next_scheduled_for_value
|
|
417
|
+
AND cursor.schedule_digest = candidate.schedule_digest
|
|
418
|
+
AND cursor.template_digest = candidate.template_digest
|
|
419
|
+
AND cursor.binding_digest = candidate.binding_digest;
|
|
420
|
+
GET DIAGNOSTICS changed = ROW_COUNT;
|
|
421
|
+
IF changed <> 1 THEN
|
|
422
|
+
RAISE EXCEPTION 'shadow_cursor_concurrent_change' USING ERRCODE = '40001';
|
|
423
|
+
END IF;
|
|
424
|
+
END IF;
|
|
425
|
+
|
|
426
|
+
observation_id_value := 'shadow-' || pg_catalog.substr(
|
|
427
|
+
semicolony.hive_cron_shadow_sha256(
|
|
428
|
+
candidate.bot_id || E'\n' || candidate.job_id || E'\n' ||
|
|
429
|
+
candidate.runtime_source || E'\n' || after_cursor_version_value::text
|
|
430
|
+
),
|
|
431
|
+
8
|
|
432
|
+
);
|
|
433
|
+
observation_digest_value := semicolony.hive_cron_shadow_sha256(
|
|
434
|
+
transition_value || E'\n' ||
|
|
435
|
+
COALESCE(before_cursor_version_value::text, '') || E'\n' ||
|
|
436
|
+
COALESCE(pg_catalog.to_char(
|
|
437
|
+
before_next_scheduled_for_value AT TIME ZONE 'UTC',
|
|
438
|
+
'YYYY-MM-DD"T"HH24:MI:SS.US"Z"'
|
|
439
|
+
), '') || E'\n' ||
|
|
440
|
+
after_cursor_version_value::text || E'\n' ||
|
|
441
|
+
pg_catalog.to_char(
|
|
442
|
+
after_next_scheduled_for_value AT TIME ZONE 'UTC',
|
|
443
|
+
'YYYY-MM-DD"T"HH24:MI:SS.US"Z"'
|
|
444
|
+
) || E'\n' ||
|
|
445
|
+
pg_catalog.to_char(db_now AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.US"Z"') || E'\n' ||
|
|
446
|
+
due_count_value::text || E'\n' || skipped_count_value::text || E'\n' ||
|
|
447
|
+
candidate.enabled::text || E'\n' || candidate.schedule_digest || E'\n' ||
|
|
448
|
+
candidate.template_digest || E'\n' || candidate.binding_digest || E'\n' ||
|
|
449
|
+
occurrence_digest_value
|
|
450
|
+
);
|
|
451
|
+
|
|
452
|
+
INSERT INTO semicolony.hive_cron_shadow_observations (
|
|
453
|
+
observation_id, bot_id, job_id, runtime_source, transition,
|
|
454
|
+
before_cursor_version, before_next_scheduled_for,
|
|
455
|
+
after_cursor_version, after_next_scheduled_for, observed_at,
|
|
456
|
+
due_count, skipped_count, enabled, schedule_digest, template_digest,
|
|
457
|
+
binding_digest, occurrence_digest, observation_digest
|
|
458
|
+
) VALUES (
|
|
459
|
+
observation_id_value, candidate.bot_id, candidate.job_id, candidate.runtime_source,
|
|
460
|
+
transition_value, before_cursor_version_value, before_next_scheduled_for_value,
|
|
461
|
+
after_cursor_version_value, after_next_scheduled_for_value, db_now,
|
|
462
|
+
due_count_value, skipped_count_value, candidate.enabled,
|
|
463
|
+
candidate.schedule_digest, candidate.template_digest, candidate.binding_digest,
|
|
464
|
+
occurrence_digest_value, observation_digest_value
|
|
465
|
+
);
|
|
466
|
+
|
|
467
|
+
transition := transition_value;
|
|
468
|
+
observation_id := observation_id_value;
|
|
469
|
+
bot_id := candidate.bot_id;
|
|
470
|
+
job_id := candidate.job_id;
|
|
471
|
+
runtime_source := candidate.runtime_source;
|
|
472
|
+
before_cursor_version := before_cursor_version_value;
|
|
473
|
+
before_next_scheduled_for := before_next_scheduled_for_value;
|
|
474
|
+
after_cursor_version := after_cursor_version_value;
|
|
475
|
+
after_next_scheduled_for := after_next_scheduled_for_value;
|
|
476
|
+
observed_at := db_now;
|
|
477
|
+
due_count := due_count_value;
|
|
478
|
+
skipped_count := skipped_count_value;
|
|
479
|
+
enabled := candidate.enabled;
|
|
480
|
+
schedule_digest := candidate.schedule_digest;
|
|
481
|
+
template_digest := candidate.template_digest;
|
|
482
|
+
binding_digest := candidate.binding_digest;
|
|
483
|
+
occurrence_digest := occurrence_digest_value;
|
|
484
|
+
observation_digest := observation_digest_value;
|
|
485
|
+
RETURN NEXT;
|
|
486
|
+
END LOOP;
|
|
487
|
+
END;
|
|
488
|
+
$$;
|
|
489
|
+
|
|
490
|
+
CREATE FUNCTION semicolony.hive_cron_get_shadow_snapshot()
|
|
491
|
+
RETURNS TABLE (
|
|
492
|
+
bot_id text,
|
|
493
|
+
job_id text,
|
|
494
|
+
runtime_source text,
|
|
495
|
+
cursor_version bigint,
|
|
496
|
+
next_scheduled_for timestamptz,
|
|
497
|
+
cursor_schedule_digest text,
|
|
498
|
+
cursor_template_digest text,
|
|
499
|
+
cursor_binding_digest text,
|
|
500
|
+
created_at timestamptz,
|
|
501
|
+
updated_at timestamptz,
|
|
502
|
+
observation_id text,
|
|
503
|
+
transition text,
|
|
504
|
+
observation_before_cursor_version bigint,
|
|
505
|
+
observation_before_next_scheduled_for timestamptz,
|
|
506
|
+
observation_after_cursor_version bigint,
|
|
507
|
+
observation_after_next_scheduled_for timestamptz,
|
|
508
|
+
observed_at timestamptz,
|
|
509
|
+
due_count integer,
|
|
510
|
+
skipped_count integer,
|
|
511
|
+
enabled boolean,
|
|
512
|
+
observation_schedule_digest text,
|
|
513
|
+
observation_template_digest text,
|
|
514
|
+
observation_binding_digest text,
|
|
515
|
+
occurrence_digest text,
|
|
516
|
+
observation_digest text
|
|
517
|
+
)
|
|
518
|
+
LANGUAGE plpgsql
|
|
519
|
+
SECURITY DEFINER
|
|
520
|
+
SET search_path = pg_catalog
|
|
521
|
+
SET TimeZone = 'UTC'
|
|
522
|
+
AS $$
|
|
523
|
+
BEGIN
|
|
524
|
+
IF session_user <> 'sc_provider'
|
|
525
|
+
OR public.is_provider() IS DISTINCT FROM true THEN
|
|
526
|
+
RAISE EXCEPTION 'provider_mode_required' USING ERRCODE = '42501';
|
|
527
|
+
END IF;
|
|
528
|
+
|
|
529
|
+
PERFORM pg_catalog.pg_advisory_xact_lock(
|
|
530
|
+
pg_catalog.hashtextextended('semicolony.hive_cron_shadow_cursor', 0)
|
|
531
|
+
);
|
|
532
|
+
IF EXISTS (
|
|
533
|
+
SELECT 1
|
|
534
|
+
FROM semicolony.hive_cron_shadow_cursors AS cursor
|
|
535
|
+
LEFT JOIN semicolony.hive_cron_shadow_observations AS observation
|
|
536
|
+
ON observation.bot_id = cursor.bot_id
|
|
537
|
+
AND observation.job_id = cursor.job_id
|
|
538
|
+
AND observation.runtime_source = cursor.runtime_source
|
|
539
|
+
AND observation.after_cursor_version = cursor.cursor_version
|
|
540
|
+
WHERE observation.observation_id IS NULL
|
|
541
|
+
OR cursor.cursor_version IS DISTINCT FROM observation.after_cursor_version
|
|
542
|
+
OR cursor.next_scheduled_for IS DISTINCT FROM observation.after_next_scheduled_for
|
|
543
|
+
OR cursor.updated_at IS DISTINCT FROM observation.observed_at
|
|
544
|
+
OR cursor.schedule_digest IS DISTINCT FROM observation.schedule_digest
|
|
545
|
+
OR cursor.template_digest IS DISTINCT FROM observation.template_digest
|
|
546
|
+
OR cursor.binding_digest IS DISTINCT FROM observation.binding_digest
|
|
547
|
+
) THEN
|
|
548
|
+
RAISE EXCEPTION 'shadow_snapshot_incoherent' USING ERRCODE = '40001';
|
|
549
|
+
END IF;
|
|
550
|
+
|
|
551
|
+
RETURN QUERY
|
|
552
|
+
SELECT cursor.bot_id,
|
|
553
|
+
cursor.job_id,
|
|
554
|
+
cursor.runtime_source,
|
|
555
|
+
cursor.cursor_version,
|
|
556
|
+
cursor.next_scheduled_for,
|
|
557
|
+
cursor.schedule_digest,
|
|
558
|
+
cursor.template_digest,
|
|
559
|
+
cursor.binding_digest,
|
|
560
|
+
cursor.created_at,
|
|
561
|
+
cursor.updated_at,
|
|
562
|
+
observation.observation_id,
|
|
563
|
+
observation.transition,
|
|
564
|
+
observation.before_cursor_version,
|
|
565
|
+
observation.before_next_scheduled_for,
|
|
566
|
+
observation.after_cursor_version,
|
|
567
|
+
observation.after_next_scheduled_for,
|
|
568
|
+
observation.observed_at,
|
|
569
|
+
observation.due_count,
|
|
570
|
+
observation.skipped_count,
|
|
571
|
+
observation.enabled,
|
|
572
|
+
observation.schedule_digest,
|
|
573
|
+
observation.template_digest,
|
|
574
|
+
observation.binding_digest,
|
|
575
|
+
observation.occurrence_digest,
|
|
576
|
+
observation.observation_digest
|
|
577
|
+
FROM semicolony.hive_cron_shadow_cursors AS cursor
|
|
578
|
+
JOIN semicolony.hive_cron_shadow_observations AS observation
|
|
579
|
+
ON observation.bot_id = cursor.bot_id
|
|
580
|
+
AND observation.job_id = cursor.job_id
|
|
581
|
+
AND observation.runtime_source = cursor.runtime_source
|
|
582
|
+
AND observation.after_cursor_version = cursor.cursor_version
|
|
583
|
+
ORDER BY cursor.job_id COLLATE "C";
|
|
584
|
+
END;
|
|
585
|
+
$$;
|
|
586
|
+
|
|
587
|
+
REVOKE ALL PRIVILEGES ON TABLE semicolony.hive_cron_shadow_cursors
|
|
588
|
+
FROM PUBLIC, sc_app, sc_provider;
|
|
589
|
+
REVOKE ALL PRIVILEGES ON TABLE semicolony.hive_cron_shadow_observations
|
|
590
|
+
FROM PUBLIC, sc_app, sc_provider;
|
|
591
|
+
|
|
592
|
+
REVOKE ALL ON FUNCTION semicolony.hive_cron_advance_shadow(timestamptz)
|
|
593
|
+
FROM PUBLIC, sc_app, sc_provider;
|
|
594
|
+
REVOKE ALL ON FUNCTION semicolony.hive_cron_get_shadow_snapshot()
|
|
595
|
+
FROM PUBLIC, sc_app, sc_provider;
|
|
596
|
+
|
|
597
|
+
GRANT EXECUTE ON FUNCTION semicolony.hive_cron_advance_shadow(timestamptz)
|
|
598
|
+
TO sc_provider;
|
|
599
|
+
GRANT EXECUTE ON FUNCTION semicolony.hive_cron_get_shadow_snapshot()
|
|
600
|
+
TO sc_provider;
|