@opengeni/db 0.14.3 → 0.17.1
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/{chunk-Y5WZZVQK.js → chunk-SK7YYHBI.js} +5 -2
- package/dist/chunk-SK7YYHBI.js.map +1 -0
- package/dist/{chunk-ELMUCYZF.js → chunk-T6RSJT6C.js} +202 -43
- package/dist/chunk-T6RSJT6C.js.map +1 -0
- package/dist/chunk-TLAC622R.js +4990 -0
- package/dist/chunk-TLAC622R.js.map +1 -0
- package/dist/codex-token-resolver.d.ts +64 -0
- package/dist/connection-token-resolver.d.ts +90 -0
- package/dist/environment-crypto.d.ts +11 -0
- package/dist/event-payload-sanitizer.d.ts +32 -0
- package/dist/index.d.ts +6374 -10
- package/dist/index.js +8075 -2776
- package/dist/index.js.map +1 -1
- package/dist/insights.d.ts +157 -0
- package/dist/memory-domain.d.ts +44 -0
- package/dist/migrate.d.ts +5 -7
- package/dist/migrate.js +1 -1
- package/dist/new-session-drafts.d.ts +49 -0
- package/dist/persistence-errors.d.ts +69 -0
- package/dist/preference-registry-schema.d.ts +1147 -0
- package/dist/preference-registry.d.ts +878 -0
- package/dist/provision-roles.d.ts +4 -6489
- package/dist/provision-roles.js +1 -1
- package/dist/role-relationships.d.ts +35 -0
- package/dist/runtime-posture-cli.d.ts +1 -0
- package/dist/runtime-posture.d.ts +103 -0
- package/dist/schema.d.ts +25330 -3
- package/dist/schema.js +31 -1
- package/dist/session-control.d.ts +331 -0
- package/dist/session-queue-commands.d.ts +186 -0
- package/dist/session-tool-call-settlement.d.ts +32 -0
- package/dist/turn-initiator.d.ts +28 -0
- package/dist/workspace-artifacts.d.ts +62 -0
- package/dist/workspace-instruction-policies-schema.d.ts +735 -0
- package/dist/workspace-instruction-policies.d.ts +64 -0
- package/drizzle/0136_unified_session_tool_policy.sql +1 -1
- package/drizzle/0137_preference_registry.sql +1347 -0
- package/drizzle/0138_sandbox_checkpoint_artifacts_and_deadlines.sql +1074 -0
- package/drizzle/0139_codex_provider_artifact_invalidations.sql +51 -0
- package/drizzle/0140_sandbox_restore_and_reaper_fences.sql +276 -0
- package/drizzle/0142_sandbox_archive_capture_gate.sql +80 -0
- package/drizzle/0143_session_codex_compaction_mode.sql +20 -0
- package/drizzle/0144_sandbox_viewer_force_drain_gate.sql +95 -0
- package/drizzle/0145_model_call_facts.sql +96 -0
- package/drizzle/0146_slack_bot_delete_idempotency.sql +105 -0
- package/drizzle/0147_draft_latency_mode.sql +12 -0
- package/drizzle/0148_session_turn_latency_mode.sql +12 -0
- package/drizzle/0149_workspace_artifacts.sql +348 -0
- package/drizzle/0150_slack_task_interactions.sql +344 -0
- package/package.json +6 -6
- package/src/index.ts +5766 -275
- package/src/insights.ts +837 -0
- package/src/migrate.ts +9 -1
- package/src/new-session-drafts.ts +4 -0
- package/src/preference-registry-schema.ts +170 -0
- package/src/preference-registry.ts +1220 -0
- package/src/provision-roles.ts +95 -22
- package/src/role-relationships.ts +132 -0
- package/src/runtime-posture.ts +47 -27
- package/src/schema.ts +875 -19
- package/src/session-queue-commands.ts +39 -6
- package/src/workspace-artifacts.ts +751 -0
- package/dist/chunk-DW24CKCT.js +0 -4046
- package/dist/chunk-DW24CKCT.js.map +0 -1
- package/dist/chunk-ELMUCYZF.js.map +0 -1
- package/dist/chunk-Y5WZZVQK.js.map +0 -1
- package/dist/schema-DhkRhcuQ.d.ts +0 -22666
|
@@ -0,0 +1,1074 @@
|
|
|
1
|
+
-- deployment-mode: maintenance
|
|
2
|
+
-- Make provider snapshot ownership and finite sandbox lifetime first-class
|
|
3
|
+
-- durable state. This is a one-way protocol cutover: old application workers
|
|
4
|
+
-- neither stamp provider deadlines nor honor rotation fences, so every
|
|
5
|
+
-- opengeni_app session must be stopped before this migration runs.
|
|
6
|
+
|
|
7
|
+
SET LOCAL lock_timeout = '5s';
|
|
8
|
+
SET LOCAL statement_timeout = '30min';
|
|
9
|
+
|
|
10
|
+
-- Reject an obviously live old application before waiting on table locks. The
|
|
11
|
+
-- guard runs again after the locks to close the connect/write race.
|
|
12
|
+
DO $maintenance_preflight_guard$
|
|
13
|
+
BEGIN
|
|
14
|
+
IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'opengeni_app')
|
|
15
|
+
AND EXISTS (
|
|
16
|
+
SELECT 1
|
|
17
|
+
FROM pg_stat_activity
|
|
18
|
+
WHERE datname = current_database()
|
|
19
|
+
AND usename = 'opengeni_app'
|
|
20
|
+
AND pid <> pg_backend_pid()
|
|
21
|
+
)
|
|
22
|
+
THEN
|
|
23
|
+
RAISE EXCEPTION
|
|
24
|
+
'sandbox checkpoint/deadline activation requires all opengeni_app sessions to be stopped'
|
|
25
|
+
USING ERRCODE = '55000';
|
|
26
|
+
END IF;
|
|
27
|
+
END
|
|
28
|
+
$maintenance_preflight_guard$;
|
|
29
|
+
|
|
30
|
+
-- Serialize the cutover against every old lease lifecycle writer. The migration
|
|
31
|
+
-- runner wraps this file in one transaction, so any timeout/guard failure leaves
|
|
32
|
+
-- the old schema and protocol fully active.
|
|
33
|
+
LOCK TABLE sandbox_leases IN ACCESS EXCLUSIVE MODE;
|
|
34
|
+
LOCK TABLE sandbox_lease_holders IN ACCESS EXCLUSIVE MODE;
|
|
35
|
+
LOCK TABLE sandbox_pty_sessions IN ACCESS EXCLUSIVE MODE;
|
|
36
|
+
LOCK TABLE sandbox_retained_processes IN ACCESS EXCLUSIVE MODE;
|
|
37
|
+
LOCK TABLE sandbox_workspace_mutation_admissions IN ACCESS EXCLUSIVE MODE;
|
|
38
|
+
LOCK TABLE session_turn_attempts IN SHARE MODE;
|
|
39
|
+
LOCK TABLE session_turns IN SHARE MODE;
|
|
40
|
+
|
|
41
|
+
DO $maintenance_guard$
|
|
42
|
+
BEGIN
|
|
43
|
+
IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'opengeni_app')
|
|
44
|
+
AND EXISTS (
|
|
45
|
+
SELECT 1
|
|
46
|
+
FROM pg_stat_activity
|
|
47
|
+
WHERE datname = current_database()
|
|
48
|
+
AND usename = 'opengeni_app'
|
|
49
|
+
AND pid <> pg_backend_pid()
|
|
50
|
+
)
|
|
51
|
+
THEN
|
|
52
|
+
RAISE EXCEPTION
|
|
53
|
+
'sandbox checkpoint/deadline activation requires all opengeni_app sessions to be stopped'
|
|
54
|
+
USING ERRCODE = '55000';
|
|
55
|
+
END IF;
|
|
56
|
+
END
|
|
57
|
+
$maintenance_guard$;
|
|
58
|
+
|
|
59
|
+
-- A provider establish call can outlive an abandoned Temporal activity. Older
|
|
60
|
+
-- workers kept touching the resulting holder from a private warmup timer even
|
|
61
|
+
-- after the exact attempt had closed, so TTL cleanup could never observe it as
|
|
62
|
+
-- stale. The maintenance fence makes this a race-free one-time reconciliation:
|
|
63
|
+
-- retain only canonical turn holders whose complete attempt/turn ownership
|
|
64
|
+
-- chain is still current, then restore the lease counters from source rows.
|
|
65
|
+
WITH removed AS MATERIALIZED (
|
|
66
|
+
DELETE FROM sandbox_lease_holders holder
|
|
67
|
+
WHERE holder.kind = 'turn'
|
|
68
|
+
AND holder.holder_id
|
|
69
|
+
~* '^turn-attempt:[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$'
|
|
70
|
+
AND NOT EXISTS (
|
|
71
|
+
SELECT 1
|
|
72
|
+
FROM session_turn_attempts attempt
|
|
73
|
+
JOIN session_turns turn
|
|
74
|
+
ON turn.account_id = attempt.account_id
|
|
75
|
+
AND turn.workspace_id = attempt.workspace_id
|
|
76
|
+
AND turn.session_id = attempt.session_id
|
|
77
|
+
AND turn.id = attempt.turn_id
|
|
78
|
+
AND turn.execution_generation = attempt.execution_generation
|
|
79
|
+
AND turn.active_attempt_id = attempt.id
|
|
80
|
+
WHERE holder.holder_id = ('turn-attempt:' || attempt.id::text)
|
|
81
|
+
AND attempt.account_id = holder.account_id
|
|
82
|
+
AND attempt.workspace_id = holder.workspace_id
|
|
83
|
+
AND attempt.session_id = holder.subject_id
|
|
84
|
+
AND attempt.state IN ('claimed', 'running')
|
|
85
|
+
)
|
|
86
|
+
RETURNING holder.id AS holder_id, holder.lease_id
|
|
87
|
+
),
|
|
88
|
+
affected AS MATERIALIZED (
|
|
89
|
+
SELECT DISTINCT removed.lease_id AS id FROM removed
|
|
90
|
+
),
|
|
91
|
+
-- Data-modifying CTE siblings share one base-table snapshot in PostgreSQL.
|
|
92
|
+
-- The physical DELETE is therefore not visible to these counts; subtract its
|
|
93
|
+
-- exact RETURNING identities explicitly instead of silently retaining stale
|
|
94
|
+
-- denormalized counters.
|
|
95
|
+
counts AS MATERIALIZED (
|
|
96
|
+
SELECT affected.id,
|
|
97
|
+
(SELECT count(*) FROM sandbox_lease_holders holder
|
|
98
|
+
WHERE holder.lease_id = affected.id
|
|
99
|
+
AND NOT EXISTS (
|
|
100
|
+
SELECT 1 FROM removed WHERE removed.holder_id = holder.id
|
|
101
|
+
))::integer AS total,
|
|
102
|
+
(SELECT count(*) FROM sandbox_lease_holders holder
|
|
103
|
+
WHERE holder.lease_id = affected.id
|
|
104
|
+
AND holder.kind = 'turn'
|
|
105
|
+
AND NOT EXISTS (
|
|
106
|
+
SELECT 1 FROM removed WHERE removed.holder_id = holder.id
|
|
107
|
+
))::integer AS turns,
|
|
108
|
+
(SELECT count(*) FROM sandbox_lease_holders holder
|
|
109
|
+
WHERE holder.lease_id = affected.id
|
|
110
|
+
AND holder.kind = 'viewer'
|
|
111
|
+
AND NOT EXISTS (
|
|
112
|
+
SELECT 1 FROM removed WHERE removed.holder_id = holder.id
|
|
113
|
+
))::integer AS viewers
|
|
114
|
+
FROM affected
|
|
115
|
+
)
|
|
116
|
+
UPDATE sandbox_leases lease SET
|
|
117
|
+
refcount = counts.total,
|
|
118
|
+
turn_holders = counts.turns,
|
|
119
|
+
viewer_holders = counts.viewers,
|
|
120
|
+
liveness = CASE
|
|
121
|
+
WHEN lease.liveness = 'warm' AND counts.total = 0 THEN 'draining'
|
|
122
|
+
ELSE lease.liveness
|
|
123
|
+
END,
|
|
124
|
+
expires_at = CASE
|
|
125
|
+
WHEN lease.liveness = 'warm' AND counts.total = 0
|
|
126
|
+
THEN now() - interval '1 millisecond'
|
|
127
|
+
ELSE lease.expires_at
|
|
128
|
+
END,
|
|
129
|
+
updated_at = now()
|
|
130
|
+
FROM counts
|
|
131
|
+
WHERE lease.id = counts.id;
|
|
132
|
+
|
|
133
|
+
-- A provider instance may disappear after its lease row has already advanced
|
|
134
|
+
-- to a successor identity. The reconciliation worker can still prove the exact
|
|
135
|
+
-- historical Modal sandbox is terminal by id; persist that distinct proof
|
|
136
|
+
-- instead of retrying an impossible current-lease identity match forever.
|
|
137
|
+
ALTER TABLE sandbox_retained_processes
|
|
138
|
+
ADD COLUMN provider_binding_key text,
|
|
139
|
+
ADD COLUMN provider_binding jsonb,
|
|
140
|
+
DROP CONSTRAINT sandbox_retained_processes_reconcile_proof_check,
|
|
141
|
+
ADD CONSTRAINT sandbox_retained_processes_provider_binding_check
|
|
142
|
+
CHECK (
|
|
143
|
+
(
|
|
144
|
+
provider_binding_key IS NULL
|
|
145
|
+
AND provider_binding IS NULL
|
|
146
|
+
) OR (
|
|
147
|
+
provider_backend = 'modal'
|
|
148
|
+
AND octet_length(provider_binding_key) BETWEEN 1 AND 1024
|
|
149
|
+
AND jsonb_typeof(provider_binding) = 'object'
|
|
150
|
+
AND provider_binding_key::jsonb = provider_binding
|
|
151
|
+
AND provider_binding_key = format(
|
|
152
|
+
'{"version":1,"serverUrl":%s,"workspaceName":%s,"environment":%s}',
|
|
153
|
+
to_jsonb(provider_binding ->> 'serverUrl')::text,
|
|
154
|
+
to_jsonb(provider_binding ->> 'workspaceName')::text,
|
|
155
|
+
to_jsonb(provider_binding ->> 'environment')::text
|
|
156
|
+
)
|
|
157
|
+
AND provider_binding = jsonb_build_object(
|
|
158
|
+
'version', 1,
|
|
159
|
+
'serverUrl', provider_binding ->> 'serverUrl',
|
|
160
|
+
'workspaceName', provider_binding ->> 'workspaceName',
|
|
161
|
+
'environment', provider_binding ->> 'environment'
|
|
162
|
+
)
|
|
163
|
+
AND coalesce(octet_length(provider_binding ->> 'serverUrl'), 0) > 0
|
|
164
|
+
AND coalesce(octet_length(provider_binding ->> 'workspaceName'), 0) > 0
|
|
165
|
+
AND provider_binding ->> 'environment' IS NOT NULL
|
|
166
|
+
)
|
|
167
|
+
),
|
|
168
|
+
ADD CONSTRAINT sandbox_retained_processes_reconcile_proof_check
|
|
169
|
+
CHECK (
|
|
170
|
+
(
|
|
171
|
+
reconcile_proof_outcome IS NULL
|
|
172
|
+
AND reconcile_proof_exit_code IS NULL
|
|
173
|
+
AND reconcile_proof_reason IS NULL
|
|
174
|
+
AND reconcile_proof_observed_at IS NULL
|
|
175
|
+
) OR (
|
|
176
|
+
reconcile_proof_outcome = 'exited'
|
|
177
|
+
AND reconcile_proof_exit_code IS NOT NULL
|
|
178
|
+
AND reconcile_proof_reason = 'provider_exit_banner'
|
|
179
|
+
AND reconcile_proof_observed_at IS NOT NULL
|
|
180
|
+
) OR (
|
|
181
|
+
reconcile_proof_outcome = 'lost'
|
|
182
|
+
AND reconcile_proof_exit_code IS NULL
|
|
183
|
+
AND reconcile_proof_reason IN (
|
|
184
|
+
'provider_session_lost_banner',
|
|
185
|
+
'provider_instance_not_found',
|
|
186
|
+
'provider_instance_terminated'
|
|
187
|
+
)
|
|
188
|
+
AND reconcile_proof_observed_at IS NOT NULL
|
|
189
|
+
)
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
CREATE TABLE sandbox_checkpoint_artifacts (
|
|
193
|
+
id uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
|
194
|
+
account_id uuid NOT NULL,
|
|
195
|
+
workspace_id uuid NOT NULL,
|
|
196
|
+
sandbox_group_id uuid NOT NULL,
|
|
197
|
+
source_lease_id uuid NOT NULL,
|
|
198
|
+
source_lease_epoch integer NOT NULL,
|
|
199
|
+
source_instance_id text,
|
|
200
|
+
source_workspace_generation integer,
|
|
201
|
+
provenance text NOT NULL,
|
|
202
|
+
provider_backend text NOT NULL,
|
|
203
|
+
provider_binding_key text NOT NULL,
|
|
204
|
+
provider_binding jsonb NOT NULL,
|
|
205
|
+
object_kind text NOT NULL,
|
|
206
|
+
object_id text NOT NULL,
|
|
207
|
+
archive_base64 text NOT NULL,
|
|
208
|
+
archive_sha256 text NOT NULL,
|
|
209
|
+
archive_bytes integer NOT NULL,
|
|
210
|
+
descriptor jsonb NOT NULL,
|
|
211
|
+
descriptor_revision text NOT NULL,
|
|
212
|
+
state text NOT NULL DEFAULT 'candidate',
|
|
213
|
+
delete_after timestamptz,
|
|
214
|
+
delete_attempts integer NOT NULL DEFAULT 0,
|
|
215
|
+
delete_claim_id uuid,
|
|
216
|
+
delete_claimed_at timestamptz,
|
|
217
|
+
last_delete_error text,
|
|
218
|
+
created_at timestamptz NOT NULL DEFAULT now(),
|
|
219
|
+
published_at timestamptz,
|
|
220
|
+
deleted_at timestamptz,
|
|
221
|
+
updated_at timestamptz NOT NULL DEFAULT now(),
|
|
222
|
+
|
|
223
|
+
CONSTRAINT sandbox_checkpoint_artifacts_source_check CHECK (
|
|
224
|
+
source_lease_epoch >= 0
|
|
225
|
+
AND (
|
|
226
|
+
(
|
|
227
|
+
provenance = 'native_capture'
|
|
228
|
+
AND source_workspace_generation IS NOT NULL
|
|
229
|
+
AND source_workspace_generation >= 0
|
|
230
|
+
AND source_instance_id IS NOT NULL
|
|
231
|
+
AND octet_length(source_instance_id) BETWEEN 1 AND 512
|
|
232
|
+
)
|
|
233
|
+
OR (
|
|
234
|
+
provenance = 'legacy_provider_adopted'
|
|
235
|
+
AND source_workspace_generation IS NULL
|
|
236
|
+
AND source_instance_id IS NULL
|
|
237
|
+
)
|
|
238
|
+
)
|
|
239
|
+
),
|
|
240
|
+
CONSTRAINT sandbox_checkpoint_artifacts_provider_check CHECK (
|
|
241
|
+
provider_backend = 'modal'
|
|
242
|
+
AND octet_length(provider_binding_key) BETWEEN 1 AND 1024
|
|
243
|
+
AND jsonb_typeof(provider_binding) = 'object'
|
|
244
|
+
AND provider_binding_key::jsonb = provider_binding
|
|
245
|
+
AND provider_binding_key = format(
|
|
246
|
+
'{"version":1,"serverUrl":%s,"workspaceName":%s,"environment":%s}',
|
|
247
|
+
to_jsonb(provider_binding ->> 'serverUrl')::text,
|
|
248
|
+
to_jsonb(provider_binding ->> 'workspaceName')::text,
|
|
249
|
+
to_jsonb(provider_binding ->> 'environment')::text
|
|
250
|
+
)
|
|
251
|
+
AND provider_binding = jsonb_build_object(
|
|
252
|
+
'version', 1,
|
|
253
|
+
'serverUrl', provider_binding ->> 'serverUrl',
|
|
254
|
+
'workspaceName', provider_binding ->> 'workspaceName',
|
|
255
|
+
'environment', provider_binding ->> 'environment'
|
|
256
|
+
)
|
|
257
|
+
AND coalesce(octet_length(provider_binding ->> 'serverUrl'), 0) > 0
|
|
258
|
+
AND coalesce(octet_length(provider_binding ->> 'workspaceName'), 0) > 0
|
|
259
|
+
AND provider_binding ->> 'environment' IS NOT NULL
|
|
260
|
+
AND object_kind IN ('modal_filesystem_snapshot', 'modal_directory_snapshot')
|
|
261
|
+
AND octet_length(object_id) BETWEEN 1 AND 1024
|
|
262
|
+
),
|
|
263
|
+
CONSTRAINT sandbox_checkpoint_artifacts_archive_check CHECK (
|
|
264
|
+
archive_bytes > 0
|
|
265
|
+
AND octet_length(archive_base64) > 0
|
|
266
|
+
AND archive_sha256 ~ '^[0-9a-f]{64}$'
|
|
267
|
+
AND jsonb_typeof(descriptor) = 'object'
|
|
268
|
+
AND octet_length(descriptor_revision) BETWEEN 1 AND 256
|
|
269
|
+
AND translate(encode(decode(archive_base64, 'base64'), 'base64'), E'\n\r', '')
|
|
270
|
+
= archive_base64
|
|
271
|
+
AND octet_length(decode(archive_base64, 'base64')) = archive_bytes
|
|
272
|
+
AND encode(digest(decode(archive_base64, 'base64'), 'sha256'), 'hex')
|
|
273
|
+
= archive_sha256
|
|
274
|
+
AND descriptor ->> 'version' = '2'
|
|
275
|
+
AND descriptor ->> 'kind' = 'provider_snapshot'
|
|
276
|
+
AND descriptor ->> 'revision' = descriptor_revision
|
|
277
|
+
AND descriptor ->> 'archiveSha256' = archive_sha256
|
|
278
|
+
AND CASE
|
|
279
|
+
WHEN descriptor ->> 'archiveBytes' ~ '^[1-9][0-9]*$'
|
|
280
|
+
THEN (descriptor ->> 'archiveBytes')::bigint = archive_bytes
|
|
281
|
+
ELSE false
|
|
282
|
+
END
|
|
283
|
+
AND descriptor ->> 'snapshotId' = object_id
|
|
284
|
+
AND (
|
|
285
|
+
(
|
|
286
|
+
object_kind = 'modal_filesystem_snapshot'
|
|
287
|
+
AND descriptor ->> 'provider' = 'modal_snapshot_filesystem'
|
|
288
|
+
AND descriptor ->> 'workspacePersistence' = 'snapshot_filesystem'
|
|
289
|
+
)
|
|
290
|
+
OR (
|
|
291
|
+
object_kind = 'modal_directory_snapshot'
|
|
292
|
+
AND descriptor ->> 'provider' = 'modal_snapshot_directory'
|
|
293
|
+
AND descriptor ->> 'workspacePersistence' = 'snapshot_directory'
|
|
294
|
+
)
|
|
295
|
+
)
|
|
296
|
+
),
|
|
297
|
+
CONSTRAINT sandbox_checkpoint_artifacts_state_check CHECK (
|
|
298
|
+
state IN (
|
|
299
|
+
'candidate', 'current', 'previous', 'delete_pending',
|
|
300
|
+
'deleting', 'delete_failed', 'deleted'
|
|
301
|
+
)
|
|
302
|
+
),
|
|
303
|
+
CONSTRAINT sandbox_checkpoint_artifacts_delete_claim_check CHECK (
|
|
304
|
+
(state = 'deleting' AND delete_claim_id IS NOT NULL AND delete_claimed_at IS NOT NULL)
|
|
305
|
+
OR (state <> 'deleting' AND delete_claim_id IS NULL AND delete_claimed_at IS NULL)
|
|
306
|
+
)
|
|
307
|
+
);
|
|
308
|
+
|
|
309
|
+
CREATE UNIQUE INDEX sandbox_checkpoint_artifacts_provider_object_uq
|
|
310
|
+
ON sandbox_checkpoint_artifacts (provider_backend, provider_binding_key, object_id);
|
|
311
|
+
CREATE INDEX sandbox_checkpoint_artifacts_gc_idx
|
|
312
|
+
ON sandbox_checkpoint_artifacts (delete_after, created_at, id)
|
|
313
|
+
WHERE state IN (
|
|
314
|
+
'candidate', 'current', 'previous',
|
|
315
|
+
'delete_pending', 'delete_failed', 'deleting'
|
|
316
|
+
);
|
|
317
|
+
CREATE INDEX sandbox_checkpoint_artifacts_source_idx
|
|
318
|
+
ON sandbox_checkpoint_artifacts (workspace_id, sandbox_group_id, source_lease_id);
|
|
319
|
+
|
|
320
|
+
ALTER TABLE sandbox_leases
|
|
321
|
+
ADD COLUMN provider_created_at timestamptz,
|
|
322
|
+
ADD COLUMN provider_deadline_at timestamptz,
|
|
323
|
+
ADD COLUMN rotation_requested_at timestamptz,
|
|
324
|
+
ADD COLUMN rotation_reason text,
|
|
325
|
+
ADD COLUMN current_checkpoint_artifact_id uuid,
|
|
326
|
+
ADD COLUMN previous_checkpoint_artifact_id uuid;
|
|
327
|
+
|
|
328
|
+
ALTER TABLE sandbox_leases
|
|
329
|
+
ADD CONSTRAINT sandbox_leases_provider_deadline_check CHECK (
|
|
330
|
+
(provider_created_at IS NULL AND provider_deadline_at IS NULL)
|
|
331
|
+
OR (
|
|
332
|
+
provider_created_at IS NOT NULL
|
|
333
|
+
AND provider_deadline_at IS NOT NULL
|
|
334
|
+
AND provider_deadline_at > provider_created_at
|
|
335
|
+
)
|
|
336
|
+
),
|
|
337
|
+
ADD CONSTRAINT sandbox_leases_rotation_check CHECK (
|
|
338
|
+
(rotation_requested_at IS NULL AND rotation_reason IS NULL)
|
|
339
|
+
OR (
|
|
340
|
+
rotation_requested_at IS NOT NULL
|
|
341
|
+
AND rotation_reason IN ('provider_deadline', 'operator')
|
|
342
|
+
)
|
|
343
|
+
),
|
|
344
|
+
ADD CONSTRAINT sandbox_leases_checkpoint_distinct_check CHECK (
|
|
345
|
+
current_checkpoint_artifact_id IS NULL
|
|
346
|
+
OR previous_checkpoint_artifact_id IS NULL
|
|
347
|
+
OR current_checkpoint_artifact_id <> previous_checkpoint_artifact_id
|
|
348
|
+
),
|
|
349
|
+
ADD CONSTRAINT sandbox_leases_current_checkpoint_fk
|
|
350
|
+
FOREIGN KEY (current_checkpoint_artifact_id)
|
|
351
|
+
REFERENCES sandbox_checkpoint_artifacts (id)
|
|
352
|
+
DEFERRABLE INITIALLY IMMEDIATE,
|
|
353
|
+
ADD CONSTRAINT sandbox_leases_previous_checkpoint_fk
|
|
354
|
+
FOREIGN KEY (previous_checkpoint_artifact_id)
|
|
355
|
+
REFERENCES sandbox_checkpoint_artifacts (id)
|
|
356
|
+
DEFERRABLE INITIALLY IMMEDIATE;
|
|
357
|
+
|
|
358
|
+
CREATE INDEX sandbox_leases_provider_deadline_idx
|
|
359
|
+
ON sandbox_leases (provider_deadline_at, id)
|
|
360
|
+
WHERE backend = 'modal'
|
|
361
|
+
AND liveness IN ('warming', 'warm')
|
|
362
|
+
AND rotation_requested_at IS NULL;
|
|
363
|
+
|
|
364
|
+
-- Publication validation is deferred so one transaction can register a
|
|
365
|
+
-- candidate, rotate both lease references, and update artifact states in any
|
|
366
|
+
-- convenient statement order while still rejecting cross-scope references.
|
|
367
|
+
DO $checkpoint_ref_validator$
|
|
368
|
+
DECLARE data_schema text := current_schema();
|
|
369
|
+
BEGIN
|
|
370
|
+
EXECUTE format($create$
|
|
371
|
+
CREATE OR REPLACE FUNCTION opengeni_private.validate_sandbox_checkpoint_refs()
|
|
372
|
+
RETURNS trigger
|
|
373
|
+
LANGUAGE plpgsql
|
|
374
|
+
SECURITY DEFINER
|
|
375
|
+
SET search_path = pg_catalog
|
|
376
|
+
AS $function$
|
|
377
|
+
DECLARE checkpoint %1$I.sandbox_checkpoint_artifacts%%ROWTYPE;
|
|
378
|
+
BEGIN
|
|
379
|
+
IF NEW.current_checkpoint_artifact_id IS NOT NULL THEN
|
|
380
|
+
SELECT * INTO checkpoint
|
|
381
|
+
FROM %1$I.sandbox_checkpoint_artifacts
|
|
382
|
+
WHERE id = NEW.current_checkpoint_artifact_id;
|
|
383
|
+
IF NOT FOUND
|
|
384
|
+
OR checkpoint.account_id <> NEW.account_id
|
|
385
|
+
OR checkpoint.workspace_id <> NEW.workspace_id
|
|
386
|
+
OR checkpoint.sandbox_group_id <> NEW.sandbox_group_id
|
|
387
|
+
OR checkpoint.source_lease_id <> NEW.id
|
|
388
|
+
OR checkpoint.provider_backend <> NEW.backend
|
|
389
|
+
OR (
|
|
390
|
+
checkpoint.provenance = 'native_capture'
|
|
391
|
+
AND checkpoint.source_workspace_generation
|
|
392
|
+
IS DISTINCT FROM NEW.archive_generation
|
|
393
|
+
)
|
|
394
|
+
OR (
|
|
395
|
+
checkpoint.provenance = 'legacy_provider_adopted'
|
|
396
|
+
AND NEW.archive_generation IS DISTINCT FROM NEW.workspace_generation
|
|
397
|
+
)
|
|
398
|
+
OR checkpoint.archive_base64
|
|
399
|
+
IS DISTINCT FROM NEW.resume_state #>> '{sessionState,workspaceArchive}'
|
|
400
|
+
OR checkpoint.descriptor_revision
|
|
401
|
+
IS DISTINCT FROM NEW.resume_state #>> '{sessionState,workspaceArchiveMeta,revision}'
|
|
402
|
+
OR checkpoint.state <> 'current'
|
|
403
|
+
THEN
|
|
404
|
+
RAISE EXCEPTION 'current checkpoint artifact does not match its exact lease scope'
|
|
405
|
+
USING ERRCODE = '23514';
|
|
406
|
+
END IF;
|
|
407
|
+
END IF;
|
|
408
|
+
|
|
409
|
+
IF NEW.previous_checkpoint_artifact_id IS NOT NULL THEN
|
|
410
|
+
SELECT * INTO checkpoint
|
|
411
|
+
FROM %1$I.sandbox_checkpoint_artifacts
|
|
412
|
+
WHERE id = NEW.previous_checkpoint_artifact_id;
|
|
413
|
+
IF NOT FOUND
|
|
414
|
+
OR checkpoint.account_id <> NEW.account_id
|
|
415
|
+
OR checkpoint.workspace_id <> NEW.workspace_id
|
|
416
|
+
OR checkpoint.sandbox_group_id <> NEW.sandbox_group_id
|
|
417
|
+
OR checkpoint.source_lease_id <> NEW.id
|
|
418
|
+
OR checkpoint.provider_backend <> NEW.backend
|
|
419
|
+
OR checkpoint.archive_base64
|
|
420
|
+
IS DISTINCT FROM NEW.resume_state #>> '{sessionState,workspaceArchivePrev}'
|
|
421
|
+
OR checkpoint.descriptor_revision
|
|
422
|
+
IS DISTINCT FROM NEW.resume_state #>> '{sessionState,workspaceArchivePrevMeta,revision}'
|
|
423
|
+
OR checkpoint.state <> 'previous'
|
|
424
|
+
THEN
|
|
425
|
+
RAISE EXCEPTION 'previous checkpoint artifact does not match its exact lease scope'
|
|
426
|
+
USING ERRCODE = '23514';
|
|
427
|
+
END IF;
|
|
428
|
+
END IF;
|
|
429
|
+
RETURN NEW;
|
|
430
|
+
END;
|
|
431
|
+
$function$;
|
|
432
|
+
$create$, data_schema);
|
|
433
|
+
END
|
|
434
|
+
$checkpoint_ref_validator$;
|
|
435
|
+
|
|
436
|
+
DO $checkpoint_artifact_ref_validator$
|
|
437
|
+
DECLARE data_schema text := current_schema();
|
|
438
|
+
BEGIN
|
|
439
|
+
EXECUTE format($create$
|
|
440
|
+
CREATE OR REPLACE FUNCTION opengeni_private.validate_sandbox_checkpoint_artifact_refs()
|
|
441
|
+
RETURNS trigger
|
|
442
|
+
LANGUAGE plpgsql
|
|
443
|
+
SECURITY DEFINER
|
|
444
|
+
SET search_path = pg_catalog
|
|
445
|
+
AS $function$
|
|
446
|
+
DECLARE exact_current boolean;
|
|
447
|
+
DECLARE exact_previous boolean;
|
|
448
|
+
DECLARE any_reference boolean;
|
|
449
|
+
DECLARE final_state text;
|
|
450
|
+
BEGIN
|
|
451
|
+
SELECT artifact.state INTO final_state
|
|
452
|
+
FROM %1$I.sandbox_checkpoint_artifacts artifact
|
|
453
|
+
WHERE artifact.id = NEW.id;
|
|
454
|
+
IF NOT FOUND THEN
|
|
455
|
+
RAISE EXCEPTION 'checkpoint artifact vanished before reference validation'
|
|
456
|
+
USING ERRCODE = '23514';
|
|
457
|
+
END IF;
|
|
458
|
+
|
|
459
|
+
SELECT
|
|
460
|
+
pg_catalog.bool_or(
|
|
461
|
+
lease.current_checkpoint_artifact_id = NEW.id
|
|
462
|
+
AND lease.id = NEW.source_lease_id
|
|
463
|
+
AND lease.account_id = NEW.account_id
|
|
464
|
+
AND lease.workspace_id = NEW.workspace_id
|
|
465
|
+
AND lease.sandbox_group_id = NEW.sandbox_group_id
|
|
466
|
+
AND lease.backend = NEW.provider_backend
|
|
467
|
+
),
|
|
468
|
+
pg_catalog.bool_or(
|
|
469
|
+
lease.previous_checkpoint_artifact_id = NEW.id
|
|
470
|
+
AND lease.id = NEW.source_lease_id
|
|
471
|
+
AND lease.account_id = NEW.account_id
|
|
472
|
+
AND lease.workspace_id = NEW.workspace_id
|
|
473
|
+
AND lease.sandbox_group_id = NEW.sandbox_group_id
|
|
474
|
+
AND lease.backend = NEW.provider_backend
|
|
475
|
+
),
|
|
476
|
+
pg_catalog.bool_or(
|
|
477
|
+
lease.current_checkpoint_artifact_id = NEW.id
|
|
478
|
+
OR lease.previous_checkpoint_artifact_id = NEW.id
|
|
479
|
+
)
|
|
480
|
+
INTO exact_current, exact_previous, any_reference
|
|
481
|
+
FROM %1$I.sandbox_leases lease
|
|
482
|
+
WHERE lease.current_checkpoint_artifact_id = NEW.id
|
|
483
|
+
OR lease.previous_checkpoint_artifact_id = NEW.id;
|
|
484
|
+
|
|
485
|
+
exact_current := coalesce(exact_current, false);
|
|
486
|
+
exact_previous := coalesce(exact_previous, false);
|
|
487
|
+
any_reference := coalesce(any_reference, false);
|
|
488
|
+
|
|
489
|
+
IF (final_state = 'current' AND (NOT exact_current OR exact_previous))
|
|
490
|
+
OR (final_state = 'previous' AND (NOT exact_previous OR exact_current))
|
|
491
|
+
OR (final_state NOT IN ('current', 'previous') AND any_reference)
|
|
492
|
+
THEN
|
|
493
|
+
RAISE EXCEPTION 'checkpoint artifact state does not match its exact lease reference'
|
|
494
|
+
USING ERRCODE = '23514';
|
|
495
|
+
END IF;
|
|
496
|
+
RETURN NEW;
|
|
497
|
+
END;
|
|
498
|
+
$function$;
|
|
499
|
+
$create$, data_schema);
|
|
500
|
+
END
|
|
501
|
+
$checkpoint_artifact_ref_validator$;
|
|
502
|
+
|
|
503
|
+
CREATE CONSTRAINT TRIGGER sandbox_checkpoint_refs_guard
|
|
504
|
+
AFTER INSERT OR UPDATE OF
|
|
505
|
+
account_id, workspace_id, sandbox_group_id, backend,
|
|
506
|
+
workspace_generation, archive_generation, resume_state,
|
|
507
|
+
current_checkpoint_artifact_id, previous_checkpoint_artifact_id
|
|
508
|
+
ON sandbox_leases
|
|
509
|
+
DEFERRABLE INITIALLY DEFERRED
|
|
510
|
+
FOR EACH ROW EXECUTE FUNCTION opengeni_private.validate_sandbox_checkpoint_refs();
|
|
511
|
+
|
|
512
|
+
CREATE CONSTRAINT TRIGGER sandbox_checkpoint_artifact_refs_guard
|
|
513
|
+
AFTER INSERT OR UPDATE OF
|
|
514
|
+
account_id, workspace_id, sandbox_group_id, source_lease_id,
|
|
515
|
+
provider_backend, state
|
|
516
|
+
ON sandbox_checkpoint_artifacts
|
|
517
|
+
DEFERRABLE INITIALLY DEFERRED
|
|
518
|
+
FOR EACH ROW EXECUTE FUNCTION opengeni_private.validate_sandbox_checkpoint_artifact_refs();
|
|
519
|
+
|
|
520
|
+
CREATE OR REPLACE FUNCTION opengeni_private.enforce_sandbox_checkpoint_artifact_immutability()
|
|
521
|
+
RETURNS trigger
|
|
522
|
+
LANGUAGE plpgsql
|
|
523
|
+
SET search_path = pg_catalog
|
|
524
|
+
AS $function$
|
|
525
|
+
BEGIN
|
|
526
|
+
IF ROW(
|
|
527
|
+
NEW.id, NEW.account_id, NEW.workspace_id, NEW.sandbox_group_id,
|
|
528
|
+
NEW.source_lease_id, NEW.source_lease_epoch, NEW.source_instance_id,
|
|
529
|
+
NEW.source_workspace_generation, NEW.provenance, NEW.provider_backend,
|
|
530
|
+
NEW.provider_binding_key, NEW.provider_binding, NEW.object_kind,
|
|
531
|
+
NEW.object_id, NEW.archive_base64, NEW.archive_sha256, NEW.archive_bytes,
|
|
532
|
+
NEW.descriptor, NEW.descriptor_revision, NEW.created_at
|
|
533
|
+
) IS DISTINCT FROM ROW(
|
|
534
|
+
OLD.id, OLD.account_id, OLD.workspace_id, OLD.sandbox_group_id,
|
|
535
|
+
OLD.source_lease_id, OLD.source_lease_epoch, OLD.source_instance_id,
|
|
536
|
+
OLD.source_workspace_generation, OLD.provenance, OLD.provider_backend,
|
|
537
|
+
OLD.provider_binding_key, OLD.provider_binding, OLD.object_kind,
|
|
538
|
+
OLD.object_id, OLD.archive_base64, OLD.archive_sha256, OLD.archive_bytes,
|
|
539
|
+
OLD.descriptor, OLD.descriptor_revision, OLD.created_at
|
|
540
|
+
)
|
|
541
|
+
THEN
|
|
542
|
+
RAISE EXCEPTION 'checkpoint artifact identity and receipt fields are immutable'
|
|
543
|
+
USING ERRCODE = '55000';
|
|
544
|
+
END IF;
|
|
545
|
+
RETURN NEW;
|
|
546
|
+
END
|
|
547
|
+
$function$;
|
|
548
|
+
|
|
549
|
+
CREATE TRIGGER sandbox_checkpoint_artifact_immutability_guard
|
|
550
|
+
BEFORE UPDATE ON sandbox_checkpoint_artifacts
|
|
551
|
+
FOR EACH ROW
|
|
552
|
+
EXECUTE FUNCTION opengeni_private.enforce_sandbox_checkpoint_artifact_immutability();
|
|
553
|
+
|
|
554
|
+
CREATE TRIGGER sandbox_recovery_protocol_v2_checkpoint_guard
|
|
555
|
+
BEFORE INSERT OR UPDATE OR DELETE ON sandbox_checkpoint_artifacts
|
|
556
|
+
FOR EACH ROW EXECUTE FUNCTION opengeni_private.enforce_sandbox_recovery_protocol_v2();
|
|
557
|
+
|
|
558
|
+
ALTER TABLE sandbox_checkpoint_artifacts ENABLE ROW LEVEL SECURITY;
|
|
559
|
+
ALTER TABLE sandbox_checkpoint_artifacts FORCE ROW LEVEL SECURITY;
|
|
560
|
+
CREATE POLICY workspace_isolation ON sandbox_checkpoint_artifacts
|
|
561
|
+
USING (opengeni_private.workspace_rls_visible(account_id, workspace_id))
|
|
562
|
+
WITH CHECK (opengeni_private.workspace_rls_visible(account_id, workspace_id));
|
|
563
|
+
|
|
564
|
+
-- Cross-workspace claim. Candidates that were captured but never published are
|
|
565
|
+
-- collectible after a grace period. Referenced artifacts are never returned,
|
|
566
|
+
-- even if a corrupt/stale state label says otherwise.
|
|
567
|
+
DO $checkpoint_gc_functions$
|
|
568
|
+
DECLARE data_schema text := current_schema();
|
|
569
|
+
BEGIN
|
|
570
|
+
EXECUTE format($create$
|
|
571
|
+
CREATE OR REPLACE FUNCTION opengeni_private.claim_sandbox_checkpoint_artifacts(
|
|
572
|
+
p_claim_id uuid,
|
|
573
|
+
p_limit integer,
|
|
574
|
+
p_claim_ttl_ms bigint
|
|
575
|
+
)
|
|
576
|
+
RETURNS TABLE (
|
|
577
|
+
id uuid,
|
|
578
|
+
provider_backend text,
|
|
579
|
+
provider_binding_key text,
|
|
580
|
+
provider_binding jsonb,
|
|
581
|
+
object_kind text,
|
|
582
|
+
object_id text,
|
|
583
|
+
delete_attempts integer
|
|
584
|
+
)
|
|
585
|
+
LANGUAGE plpgsql
|
|
586
|
+
SECURITY DEFINER
|
|
587
|
+
SET search_path = pg_catalog
|
|
588
|
+
AS $function$
|
|
589
|
+
BEGIN
|
|
590
|
+
IF p_claim_id IS NULL THEN
|
|
591
|
+
RAISE EXCEPTION 'checkpoint artifact claim id is required'
|
|
592
|
+
USING ERRCODE = '22023';
|
|
593
|
+
END IF;
|
|
594
|
+
IF p_limit IS NULL OR p_limit < 1 OR p_limit > 500 THEN
|
|
595
|
+
RAISE EXCEPTION 'checkpoint artifact claim limit must be between 1 and 500'
|
|
596
|
+
USING ERRCODE = '22023';
|
|
597
|
+
END IF;
|
|
598
|
+
IF p_claim_ttl_ms IS NULL
|
|
599
|
+
OR p_claim_ttl_ms < 1000
|
|
600
|
+
OR p_claim_ttl_ms > 3600000
|
|
601
|
+
THEN
|
|
602
|
+
RAISE EXCEPTION 'checkpoint artifact claim TTL must be between 1s and 1h'
|
|
603
|
+
USING ERRCODE = '22023';
|
|
604
|
+
END IF;
|
|
605
|
+
|
|
606
|
+
PERFORM pg_catalog.set_config('opengeni.sandbox_recovery_protocol_v2', '1', true);
|
|
607
|
+
|
|
608
|
+
WITH stale_claims AS MATERIALIZED (
|
|
609
|
+
SELECT artifact.id
|
|
610
|
+
FROM %1$I.sandbox_checkpoint_artifacts artifact
|
|
611
|
+
WHERE artifact.state = 'deleting'
|
|
612
|
+
AND artifact.delete_claimed_at
|
|
613
|
+
< pg_catalog.now()
|
|
614
|
+
- pg_catalog.make_interval(secs => p_claim_ttl_ms / 1000.0)
|
|
615
|
+
ORDER BY artifact.delete_claimed_at, artifact.id
|
|
616
|
+
FOR UPDATE SKIP LOCKED
|
|
617
|
+
LIMIT p_limit
|
|
618
|
+
)
|
|
619
|
+
UPDATE %1$I.sandbox_checkpoint_artifacts artifact SET
|
|
620
|
+
state = 'delete_failed',
|
|
621
|
+
delete_after = pg_catalog.now(),
|
|
622
|
+
delete_claim_id = null,
|
|
623
|
+
delete_claimed_at = null,
|
|
624
|
+
last_delete_error = coalesce(
|
|
625
|
+
last_delete_error, 'stale delete claim recovered'
|
|
626
|
+
),
|
|
627
|
+
updated_at = pg_catalog.now()
|
|
628
|
+
FROM stale_claims
|
|
629
|
+
WHERE artifact.id = stale_claims.id;
|
|
630
|
+
|
|
631
|
+
RETURN QUERY
|
|
632
|
+
WITH candidates AS (
|
|
633
|
+
SELECT artifact.id
|
|
634
|
+
FROM %1$I.sandbox_checkpoint_artifacts artifact
|
|
635
|
+
WHERE (
|
|
636
|
+
(
|
|
637
|
+
artifact.state = 'candidate'
|
|
638
|
+
AND artifact.created_at < pg_catalog.now() - interval '15 minutes'
|
|
639
|
+
)
|
|
640
|
+
OR (
|
|
641
|
+
artifact.state IN ('current', 'previous')
|
|
642
|
+
AND artifact.created_at < pg_catalog.now() - interval '15 minutes'
|
|
643
|
+
)
|
|
644
|
+
OR (
|
|
645
|
+
artifact.state IN ('delete_pending', 'delete_failed')
|
|
646
|
+
AND coalesce(artifact.delete_after, artifact.created_at)
|
|
647
|
+
<= pg_catalog.now()
|
|
648
|
+
)
|
|
649
|
+
)
|
|
650
|
+
AND NOT EXISTS (
|
|
651
|
+
SELECT 1 FROM %1$I.sandbox_leases lease
|
|
652
|
+
WHERE lease.current_checkpoint_artifact_id = artifact.id
|
|
653
|
+
OR lease.previous_checkpoint_artifact_id = artifact.id
|
|
654
|
+
)
|
|
655
|
+
ORDER BY coalesce(artifact.delete_after, artifact.created_at), artifact.id
|
|
656
|
+
FOR UPDATE SKIP LOCKED
|
|
657
|
+
LIMIT p_limit
|
|
658
|
+
)
|
|
659
|
+
UPDATE %1$I.sandbox_checkpoint_artifacts artifact SET
|
|
660
|
+
state = 'deleting',
|
|
661
|
+
delete_attempts = artifact.delete_attempts + 1,
|
|
662
|
+
delete_claim_id = p_claim_id,
|
|
663
|
+
delete_claimed_at = pg_catalog.now(),
|
|
664
|
+
updated_at = pg_catalog.now()
|
|
665
|
+
FROM candidates
|
|
666
|
+
WHERE artifact.id = candidates.id
|
|
667
|
+
RETURNING artifact.id, artifact.provider_backend, artifact.provider_binding_key,
|
|
668
|
+
artifact.provider_binding, artifact.object_kind, artifact.object_id,
|
|
669
|
+
artifact.delete_attempts;
|
|
670
|
+
END;
|
|
671
|
+
$function$;
|
|
672
|
+
$create$, data_schema);
|
|
673
|
+
|
|
674
|
+
EXECUTE format($create$
|
|
675
|
+
CREATE OR REPLACE FUNCTION opengeni_private.settle_sandbox_checkpoint_artifact(
|
|
676
|
+
p_id uuid,
|
|
677
|
+
p_claim_id uuid,
|
|
678
|
+
p_deleted boolean,
|
|
679
|
+
p_error text,
|
|
680
|
+
p_retry_after_ms bigint
|
|
681
|
+
)
|
|
682
|
+
RETURNS boolean
|
|
683
|
+
LANGUAGE plpgsql
|
|
684
|
+
SECURITY DEFINER
|
|
685
|
+
SET search_path = pg_catalog
|
|
686
|
+
AS $function$
|
|
687
|
+
DECLARE changed integer;
|
|
688
|
+
BEGIN
|
|
689
|
+
IF p_id IS NULL OR p_claim_id IS NULL THEN
|
|
690
|
+
RAISE EXCEPTION 'checkpoint artifact and claim ids are required'
|
|
691
|
+
USING ERRCODE = '22023';
|
|
692
|
+
END IF;
|
|
693
|
+
IF p_deleted IS NULL THEN
|
|
694
|
+
RAISE EXCEPTION 'checkpoint artifact deletion outcome is required'
|
|
695
|
+
USING ERRCODE = '22023';
|
|
696
|
+
END IF;
|
|
697
|
+
IF p_retry_after_ms IS NULL
|
|
698
|
+
OR p_retry_after_ms < 0
|
|
699
|
+
OR p_retry_after_ms > 86400000
|
|
700
|
+
THEN
|
|
701
|
+
RAISE EXCEPTION 'checkpoint artifact retry delay must be between 0 and 24h'
|
|
702
|
+
USING ERRCODE = '22023';
|
|
703
|
+
END IF;
|
|
704
|
+
|
|
705
|
+
PERFORM pg_catalog.set_config('opengeni.sandbox_recovery_protocol_v2', '1', true);
|
|
706
|
+
UPDATE %1$I.sandbox_checkpoint_artifacts SET
|
|
707
|
+
state = CASE WHEN p_deleted THEN 'deleted' ELSE 'delete_failed' END,
|
|
708
|
+
deleted_at = CASE WHEN p_deleted THEN pg_catalog.now() ELSE null END,
|
|
709
|
+
delete_after = CASE
|
|
710
|
+
WHEN p_deleted THEN null
|
|
711
|
+
ELSE pg_catalog.now()
|
|
712
|
+
+ pg_catalog.make_interval(secs => greatest(1000, p_retry_after_ms) / 1000.0)
|
|
713
|
+
END,
|
|
714
|
+
delete_claim_id = null,
|
|
715
|
+
delete_claimed_at = null,
|
|
716
|
+
last_delete_error = CASE
|
|
717
|
+
WHEN p_deleted THEN null
|
|
718
|
+
ELSE pg_catalog.left(coalesce(p_error, 'unknown'), 4000)
|
|
719
|
+
END,
|
|
720
|
+
updated_at = pg_catalog.now()
|
|
721
|
+
WHERE id = p_id
|
|
722
|
+
AND state = 'deleting'
|
|
723
|
+
AND delete_claim_id = p_claim_id;
|
|
724
|
+
GET DIAGNOSTICS changed = ROW_COUNT;
|
|
725
|
+
RETURN changed = 1;
|
|
726
|
+
END;
|
|
727
|
+
$function$;
|
|
728
|
+
$create$, data_schema);
|
|
729
|
+
|
|
730
|
+
EXECUTE format($create$
|
|
731
|
+
CREATE OR REPLACE FUNCTION opengeni_private.prune_deleted_sandbox_checkpoint_artifacts(
|
|
732
|
+
p_retention_ms bigint,
|
|
733
|
+
p_limit integer
|
|
734
|
+
)
|
|
735
|
+
RETURNS integer
|
|
736
|
+
LANGUAGE plpgsql
|
|
737
|
+
SECURITY DEFINER
|
|
738
|
+
SET search_path = pg_catalog
|
|
739
|
+
AS $function$
|
|
740
|
+
DECLARE pruned integer;
|
|
741
|
+
BEGIN
|
|
742
|
+
IF p_retention_ms IS NULL
|
|
743
|
+
OR p_retention_ms < 86400000
|
|
744
|
+
OR p_retention_ms > 31536000000
|
|
745
|
+
THEN
|
|
746
|
+
RAISE EXCEPTION 'checkpoint tombstone retention must be between 1 and 365 days'
|
|
747
|
+
USING ERRCODE = '22023';
|
|
748
|
+
END IF;
|
|
749
|
+
IF p_limit IS NULL OR p_limit < 1 OR p_limit > 1000 THEN
|
|
750
|
+
RAISE EXCEPTION 'checkpoint tombstone prune limit must be between 1 and 1000'
|
|
751
|
+
USING ERRCODE = '22023';
|
|
752
|
+
END IF;
|
|
753
|
+
|
|
754
|
+
PERFORM pg_catalog.set_config('opengeni.sandbox_recovery_protocol_v2', '1', true);
|
|
755
|
+
WITH candidates AS MATERIALIZED (
|
|
756
|
+
SELECT artifact.id
|
|
757
|
+
FROM %1$I.sandbox_checkpoint_artifacts artifact
|
|
758
|
+
WHERE artifact.state = 'deleted'
|
|
759
|
+
AND artifact.deleted_at
|
|
760
|
+
< pg_catalog.now()
|
|
761
|
+
- pg_catalog.make_interval(secs => p_retention_ms / 1000.0)
|
|
762
|
+
AND NOT EXISTS (
|
|
763
|
+
SELECT 1 FROM %1$I.sandbox_leases lease
|
|
764
|
+
WHERE lease.current_checkpoint_artifact_id = artifact.id
|
|
765
|
+
OR lease.previous_checkpoint_artifact_id = artifact.id
|
|
766
|
+
)
|
|
767
|
+
ORDER BY artifact.deleted_at, artifact.id
|
|
768
|
+
FOR UPDATE SKIP LOCKED
|
|
769
|
+
LIMIT p_limit
|
|
770
|
+
),
|
|
771
|
+
removed AS (
|
|
772
|
+
DELETE FROM %1$I.sandbox_checkpoint_artifacts artifact
|
|
773
|
+
USING candidates
|
|
774
|
+
WHERE artifact.id = candidates.id
|
|
775
|
+
RETURNING artifact.id
|
|
776
|
+
)
|
|
777
|
+
SELECT pg_catalog.count(*)::integer INTO pruned FROM removed;
|
|
778
|
+
RETURN pruned;
|
|
779
|
+
END;
|
|
780
|
+
$function$;
|
|
781
|
+
$create$, data_schema);
|
|
782
|
+
END
|
|
783
|
+
$checkpoint_gc_functions$;
|
|
784
|
+
|
|
785
|
+
DO $checkpoint_legacy_and_rotation_functions$
|
|
786
|
+
DECLARE data_schema text := current_schema();
|
|
787
|
+
BEGIN
|
|
788
|
+
EXECUTE format($create$
|
|
789
|
+
CREATE OR REPLACE FUNCTION opengeni_private.list_legacy_modal_checkpoint_slots(
|
|
790
|
+
p_limit integer
|
|
791
|
+
)
|
|
792
|
+
RETURNS TABLE (
|
|
793
|
+
account_id uuid,
|
|
794
|
+
workspace_id uuid,
|
|
795
|
+
sandbox_group_id uuid,
|
|
796
|
+
lease_id uuid,
|
|
797
|
+
lease_epoch integer,
|
|
798
|
+
instance_id text,
|
|
799
|
+
workspace_generation integer,
|
|
800
|
+
slot text,
|
|
801
|
+
archive_base64 text,
|
|
802
|
+
descriptor jsonb
|
|
803
|
+
)
|
|
804
|
+
LANGUAGE plpgsql
|
|
805
|
+
SECURITY DEFINER
|
|
806
|
+
SET search_path = pg_catalog
|
|
807
|
+
AS $function$
|
|
808
|
+
BEGIN
|
|
809
|
+
IF p_limit IS NULL OR p_limit < 1 OR p_limit > 500 THEN
|
|
810
|
+
RAISE EXCEPTION 'legacy checkpoint slot limit must be between 1 and 500'
|
|
811
|
+
USING ERRCODE = '22023';
|
|
812
|
+
END IF;
|
|
813
|
+
|
|
814
|
+
RETURN QUERY
|
|
815
|
+
SELECT lease.account_id, lease.workspace_id, lease.sandbox_group_id, lease.id,
|
|
816
|
+
lease.lease_epoch, lease.instance_id,
|
|
817
|
+
lease.workspace_generation, candidate.slot, candidate.archive_base64,
|
|
818
|
+
candidate.descriptor
|
|
819
|
+
FROM %1$I.sandbox_leases lease
|
|
820
|
+
CROSS JOIN LATERAL (
|
|
821
|
+
VALUES
|
|
822
|
+
(
|
|
823
|
+
'current'::text,
|
|
824
|
+
lease.resume_state #>> '{sessionState,workspaceArchive}',
|
|
825
|
+
lease.resume_state #> '{sessionState,workspaceArchiveMeta}',
|
|
826
|
+
lease.current_checkpoint_artifact_id
|
|
827
|
+
),
|
|
828
|
+
(
|
|
829
|
+
'previous'::text,
|
|
830
|
+
lease.resume_state #>> '{sessionState,workspaceArchivePrev}',
|
|
831
|
+
lease.resume_state #> '{sessionState,workspaceArchivePrevMeta}',
|
|
832
|
+
lease.previous_checkpoint_artifact_id
|
|
833
|
+
)
|
|
834
|
+
) candidate(slot, archive_base64, descriptor, artifact_id)
|
|
835
|
+
WHERE candidate.artifact_id IS NULL
|
|
836
|
+
AND lease.backend = 'modal'
|
|
837
|
+
AND lease.liveness IN ('warming', 'warm', 'draining')
|
|
838
|
+
AND lease.instance_id IS NOT NULL
|
|
839
|
+
AND coalesce(candidate.archive_base64, '') <> ''
|
|
840
|
+
AND candidate.descriptor ->> 'version' = '2'
|
|
841
|
+
AND candidate.descriptor ->> 'provider'
|
|
842
|
+
IN ('modal_snapshot_filesystem', 'modal_snapshot_directory')
|
|
843
|
+
ORDER BY lease.updated_at, lease.id, candidate.slot
|
|
844
|
+
LIMIT p_limit;
|
|
845
|
+
END;
|
|
846
|
+
$function$;
|
|
847
|
+
$create$, data_schema);
|
|
848
|
+
|
|
849
|
+
-- Request controlled rotation before Modal's hard creation-time timeout.
|
|
850
|
+
-- Existing passive viewer holders are evicted because they cannot mutate
|
|
851
|
+
-- workspace state and may reconnect to the successor. Direct API operations
|
|
852
|
+
-- can mutate files/git/terminal state, so they remain blockers until their
|
|
853
|
+
-- normal release or the existing direct-holder TTL reaper proves them stale.
|
|
854
|
+
-- Turn/process holders likewise remain until their explicit settlement path.
|
|
855
|
+
EXECUTE format($create$
|
|
856
|
+
CREATE OR REPLACE FUNCTION opengeni_private.request_due_sandbox_rotations(
|
|
857
|
+
p_lead_ms bigint,
|
|
858
|
+
p_limit integer
|
|
859
|
+
)
|
|
860
|
+
RETURNS integer
|
|
861
|
+
LANGUAGE plpgsql
|
|
862
|
+
SECURITY DEFINER
|
|
863
|
+
SET search_path = pg_catalog
|
|
864
|
+
AS $function$
|
|
865
|
+
DECLARE
|
|
866
|
+
requested integer;
|
|
867
|
+
requested_ids uuid[];
|
|
868
|
+
BEGIN
|
|
869
|
+
IF p_lead_ms IS NULL OR p_lead_ms < 0 OR p_lead_ms > 86400000 THEN
|
|
870
|
+
RAISE EXCEPTION 'sandbox rotation lead must be between 0 and 24h'
|
|
871
|
+
USING ERRCODE = '22023';
|
|
872
|
+
END IF;
|
|
873
|
+
IF p_limit IS NULL OR p_limit < 1 OR p_limit > 500 THEN
|
|
874
|
+
RAISE EXCEPTION 'sandbox rotation batch limit must be between 1 and 500'
|
|
875
|
+
USING ERRCODE = '22023';
|
|
876
|
+
END IF;
|
|
877
|
+
|
|
878
|
+
PERFORM pg_catalog.set_config('opengeni.sandbox_recovery_protocol_v2', '1', true);
|
|
879
|
+
|
|
880
|
+
WITH candidates AS MATERIALIZED (
|
|
881
|
+
SELECT lease.id
|
|
882
|
+
FROM %1$I.sandbox_leases lease
|
|
883
|
+
WHERE lease.backend = 'modal'
|
|
884
|
+
AND lease.liveness IN ('warming', 'warm')
|
|
885
|
+
AND lease.provider_deadline_at IS NOT NULL
|
|
886
|
+
AND lease.provider_deadline_at
|
|
887
|
+
<= pg_catalog.now()
|
|
888
|
+
+ pg_catalog.make_interval(secs => p_lead_ms / 1000.0)
|
|
889
|
+
AND lease.rotation_requested_at IS NULL
|
|
890
|
+
ORDER BY lease.provider_deadline_at, lease.id
|
|
891
|
+
FOR UPDATE SKIP LOCKED
|
|
892
|
+
LIMIT p_limit
|
|
893
|
+
),
|
|
894
|
+
due AS (
|
|
895
|
+
UPDATE %1$I.sandbox_leases lease SET
|
|
896
|
+
rotation_requested_at = pg_catalog.now(),
|
|
897
|
+
rotation_reason = 'provider_deadline',
|
|
898
|
+
updated_at = pg_catalog.now()
|
|
899
|
+
FROM candidates
|
|
900
|
+
WHERE lease.id = candidates.id
|
|
901
|
+
RETURNING lease.id
|
|
902
|
+
)
|
|
903
|
+
SELECT
|
|
904
|
+
coalesce(pg_catalog.array_agg(due.id), ARRAY[]::uuid[]),
|
|
905
|
+
pg_catalog.count(*)::integer
|
|
906
|
+
INTO requested_ids, requested
|
|
907
|
+
FROM due;
|
|
908
|
+
|
|
909
|
+
DELETE FROM %1$I.sandbox_lease_holders holder
|
|
910
|
+
WHERE holder.lease_id = ANY(requested_ids)
|
|
911
|
+
AND holder.kind = 'viewer';
|
|
912
|
+
|
|
913
|
+
UPDATE %1$I.sandbox_leases lease SET
|
|
914
|
+
refcount = counts.total,
|
|
915
|
+
turn_holders = counts.turns,
|
|
916
|
+
viewer_holders = counts.viewers,
|
|
917
|
+
liveness = CASE
|
|
918
|
+
WHEN lease.liveness = 'warm' AND counts.total = 0 THEN 'draining'
|
|
919
|
+
ELSE lease.liveness
|
|
920
|
+
END,
|
|
921
|
+
expires_at = CASE
|
|
922
|
+
WHEN lease.liveness = 'warm' AND counts.total = 0
|
|
923
|
+
THEN pg_catalog.now() - interval '1 millisecond'
|
|
924
|
+
ELSE lease.expires_at
|
|
925
|
+
END,
|
|
926
|
+
updated_at = pg_catalog.now()
|
|
927
|
+
FROM (
|
|
928
|
+
SELECT candidate.id,
|
|
929
|
+
(SELECT pg_catalog.count(*) FROM %1$I.sandbox_lease_holders holder
|
|
930
|
+
WHERE holder.lease_id = candidate.id)::int AS total,
|
|
931
|
+
(SELECT pg_catalog.count(*) FROM %1$I.sandbox_lease_holders holder
|
|
932
|
+
WHERE holder.lease_id = candidate.id AND holder.kind = 'turn')::int AS turns,
|
|
933
|
+
(SELECT pg_catalog.count(*) FROM %1$I.sandbox_lease_holders holder
|
|
934
|
+
WHERE holder.lease_id = candidate.id AND holder.kind = 'viewer')::int AS viewers
|
|
935
|
+
FROM unnest(requested_ids) requested_id(id)
|
|
936
|
+
JOIN %1$I.sandbox_leases candidate ON candidate.id = requested_id.id
|
|
937
|
+
) counts
|
|
938
|
+
WHERE lease.id = counts.id;
|
|
939
|
+
|
|
940
|
+
RETURN requested;
|
|
941
|
+
END;
|
|
942
|
+
$function$;
|
|
943
|
+
$create$, data_schema);
|
|
944
|
+
|
|
945
|
+
EXECUTE format($create$
|
|
946
|
+
CREATE OR REPLACE FUNCTION opengeni_private.sandbox_checkpoint_artifact_inventory()
|
|
947
|
+
RETURNS TABLE (state text, count bigint)
|
|
948
|
+
LANGUAGE sql
|
|
949
|
+
SECURITY DEFINER
|
|
950
|
+
SET search_path = pg_catalog
|
|
951
|
+
AS $function$
|
|
952
|
+
SELECT artifact.state, pg_catalog.count(*)::bigint
|
|
953
|
+
FROM %1$I.sandbox_checkpoint_artifacts artifact
|
|
954
|
+
GROUP BY artifact.state
|
|
955
|
+
ORDER BY artifact.state;
|
|
956
|
+
$function$;
|
|
957
|
+
$create$, data_schema);
|
|
958
|
+
|
|
959
|
+
EXECUTE format($create$
|
|
960
|
+
CREATE OR REPLACE FUNCTION opengeni_private.sandbox_rotation_backlog()
|
|
961
|
+
RETURNS TABLE (
|
|
962
|
+
requested bigint,
|
|
963
|
+
overdue bigint,
|
|
964
|
+
turn_blocked bigint,
|
|
965
|
+
direct_blocked bigint,
|
|
966
|
+
process_blocked bigint
|
|
967
|
+
)
|
|
968
|
+
LANGUAGE sql
|
|
969
|
+
SECURITY DEFINER
|
|
970
|
+
SET search_path = pg_catalog
|
|
971
|
+
AS $function$
|
|
972
|
+
SELECT
|
|
973
|
+
pg_catalog.count(*) FILTER (
|
|
974
|
+
WHERE lease.rotation_requested_at IS NOT NULL
|
|
975
|
+
)::bigint,
|
|
976
|
+
pg_catalog.count(*) FILTER (
|
|
977
|
+
WHERE lease.rotation_requested_at IS NOT NULL
|
|
978
|
+
AND lease.provider_deadline_at <= pg_catalog.now()
|
|
979
|
+
)::bigint,
|
|
980
|
+
pg_catalog.count(*) FILTER (
|
|
981
|
+
WHERE lease.rotation_requested_at IS NOT NULL
|
|
982
|
+
AND EXISTS (
|
|
983
|
+
SELECT 1 FROM %1$I.sandbox_lease_holders holder
|
|
984
|
+
WHERE holder.lease_id = lease.id AND holder.kind = 'turn'
|
|
985
|
+
)
|
|
986
|
+
)::bigint,
|
|
987
|
+
pg_catalog.count(*) FILTER (
|
|
988
|
+
WHERE lease.rotation_requested_at IS NOT NULL
|
|
989
|
+
AND EXISTS (
|
|
990
|
+
SELECT 1 FROM %1$I.sandbox_lease_holders holder
|
|
991
|
+
WHERE holder.lease_id = lease.id AND holder.kind = 'direct'
|
|
992
|
+
)
|
|
993
|
+
)::bigint,
|
|
994
|
+
pg_catalog.count(*) FILTER (
|
|
995
|
+
WHERE lease.rotation_requested_at IS NOT NULL
|
|
996
|
+
AND EXISTS (
|
|
997
|
+
SELECT 1 FROM %1$I.sandbox_lease_holders holder
|
|
998
|
+
WHERE holder.lease_id = lease.id AND holder.kind = 'process'
|
|
999
|
+
)
|
|
1000
|
+
)::bigint
|
|
1001
|
+
FROM %1$I.sandbox_leases lease
|
|
1002
|
+
WHERE lease.backend = 'modal'
|
|
1003
|
+
AND lease.liveness IN ('warming', 'warm', 'draining');
|
|
1004
|
+
$function$;
|
|
1005
|
+
$create$, data_schema);
|
|
1006
|
+
END
|
|
1007
|
+
$checkpoint_legacy_and_rotation_functions$;
|
|
1008
|
+
|
|
1009
|
+
-- Existing live Modal leases predate durable provider creation clocks. Their
|
|
1010
|
+
-- true remaining lifetime is unknowable, so fail safe: make them immediately
|
|
1011
|
+
-- eligible for controlled rotation after the new worker starts.
|
|
1012
|
+
UPDATE sandbox_leases
|
|
1013
|
+
SET provider_created_at = now() - interval '1 millisecond',
|
|
1014
|
+
provider_deadline_at = now(),
|
|
1015
|
+
updated_at = now()
|
|
1016
|
+
WHERE backend = 'modal'
|
|
1017
|
+
AND liveness IN ('warming', 'warm', 'draining')
|
|
1018
|
+
AND instance_id IS NOT NULL
|
|
1019
|
+
AND provider_deadline_at IS NULL;
|
|
1020
|
+
|
|
1021
|
+
-- PostgreSQL grants new functions to PUBLIC by default. Every function below
|
|
1022
|
+
-- either bypasses workspace RLS or is an internal trigger implementation, so
|
|
1023
|
+
-- leave execution to the migration owner and explicitly granted application
|
|
1024
|
+
-- role only.
|
|
1025
|
+
REVOKE ALL ON FUNCTION opengeni_private.validate_sandbox_checkpoint_refs()
|
|
1026
|
+
FROM PUBLIC;
|
|
1027
|
+
REVOKE ALL ON FUNCTION opengeni_private.validate_sandbox_checkpoint_artifact_refs()
|
|
1028
|
+
FROM PUBLIC;
|
|
1029
|
+
REVOKE ALL ON FUNCTION opengeni_private.enforce_sandbox_checkpoint_artifact_immutability()
|
|
1030
|
+
FROM PUBLIC;
|
|
1031
|
+
REVOKE ALL ON FUNCTION opengeni_private.claim_sandbox_checkpoint_artifacts(uuid, integer, bigint)
|
|
1032
|
+
FROM PUBLIC;
|
|
1033
|
+
REVOKE ALL ON FUNCTION opengeni_private.settle_sandbox_checkpoint_artifact(uuid, uuid, boolean, text, bigint)
|
|
1034
|
+
FROM PUBLIC;
|
|
1035
|
+
REVOKE ALL ON FUNCTION opengeni_private.prune_deleted_sandbox_checkpoint_artifacts(bigint, integer)
|
|
1036
|
+
FROM PUBLIC;
|
|
1037
|
+
REVOKE ALL ON FUNCTION opengeni_private.list_legacy_modal_checkpoint_slots(integer)
|
|
1038
|
+
FROM PUBLIC;
|
|
1039
|
+
REVOKE ALL ON FUNCTION opengeni_private.request_due_sandbox_rotations(bigint, integer)
|
|
1040
|
+
FROM PUBLIC;
|
|
1041
|
+
REVOKE ALL ON FUNCTION opengeni_private.sandbox_checkpoint_artifact_inventory()
|
|
1042
|
+
FROM PUBLIC;
|
|
1043
|
+
REVOKE ALL ON FUNCTION opengeni_private.sandbox_rotation_backlog()
|
|
1044
|
+
FROM PUBLIC;
|
|
1045
|
+
|
|
1046
|
+
DO $grants$
|
|
1047
|
+
DECLARE data_schema text := current_schema();
|
|
1048
|
+
BEGIN
|
|
1049
|
+
IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'opengeni_app') THEN
|
|
1050
|
+
EXECUTE format(
|
|
1051
|
+
'GRANT SELECT, INSERT, UPDATE, DELETE ON TABLE %I.sandbox_checkpoint_artifacts TO opengeni_app',
|
|
1052
|
+
data_schema
|
|
1053
|
+
);
|
|
1054
|
+
EXECUTE format(
|
|
1055
|
+
'GRANT SELECT, UPDATE ON TABLE %I.sandbox_leases TO opengeni_app',
|
|
1056
|
+
data_schema
|
|
1057
|
+
);
|
|
1058
|
+
GRANT EXECUTE ON FUNCTION opengeni_private.claim_sandbox_checkpoint_artifacts(uuid, integer, bigint)
|
|
1059
|
+
TO opengeni_app;
|
|
1060
|
+
GRANT EXECUTE ON FUNCTION opengeni_private.settle_sandbox_checkpoint_artifact(uuid, uuid, boolean, text, bigint)
|
|
1061
|
+
TO opengeni_app;
|
|
1062
|
+
GRANT EXECUTE ON FUNCTION opengeni_private.prune_deleted_sandbox_checkpoint_artifacts(bigint, integer)
|
|
1063
|
+
TO opengeni_app;
|
|
1064
|
+
GRANT EXECUTE ON FUNCTION opengeni_private.list_legacy_modal_checkpoint_slots(integer)
|
|
1065
|
+
TO opengeni_app;
|
|
1066
|
+
GRANT EXECUTE ON FUNCTION opengeni_private.request_due_sandbox_rotations(bigint, integer)
|
|
1067
|
+
TO opengeni_app;
|
|
1068
|
+
GRANT EXECUTE ON FUNCTION opengeni_private.sandbox_checkpoint_artifact_inventory()
|
|
1069
|
+
TO opengeni_app;
|
|
1070
|
+
GRANT EXECUTE ON FUNCTION opengeni_private.sandbox_rotation_backlog()
|
|
1071
|
+
TO opengeni_app;
|
|
1072
|
+
END IF;
|
|
1073
|
+
END
|
|
1074
|
+
$grants$;
|