deepline 0.3.289 → 0.3.291
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/bundling-sources/sdk/src/client.ts +8 -6
- package/dist/bundling-sources/sdk/src/release.ts +1 -1
- package/dist/bundling-sources/sdk/src/runs/overview.ts +16 -6
- package/dist/bundling-sources/sdk/src/types.ts +21 -2
- package/dist/bundling-sources/shared_libs/play-runtime/app-runtime-api.ts +41 -11
- package/dist/bundling-sources/shared_libs/play-runtime/context.ts +317 -63
- package/dist/bundling-sources/shared_libs/play-runtime/ctx-types.ts +10 -2
- package/dist/bundling-sources/shared_libs/play-runtime/fixture-behavior.ts +1 -0
- package/dist/bundling-sources/shared_libs/play-runtime/governor/rate-state-backend.ts +3 -0
- package/dist/bundling-sources/shared_libs/play-runtime/launch-identity.ts +1 -0
- package/dist/bundling-sources/shared_libs/play-runtime/log-context.ts +36 -0
- package/dist/bundling-sources/shared_libs/play-runtime/log-provenance.ts +19 -1
- package/dist/bundling-sources/shared_libs/play-runtime/play-runs/attempts/worker-terminal-projection.ts +6 -5
- package/dist/bundling-sources/shared_libs/play-runtime/play-runtime-batching-registry.ts +2 -0
- package/dist/bundling-sources/shared_libs/play-runtime/protocol.ts +12 -1
- package/dist/bundling-sources/shared_libs/play-runtime/run-event-catalog.ts +2 -0
- package/dist/bundling-sources/shared_libs/play-runtime/run-ledger-event-transport.ts +74 -33
- package/dist/bundling-sources/shared_libs/play-runtime/run-ledger.ts +73 -3
- package/dist/bundling-sources/shared_libs/play-runtime/run-log-buffer.ts +39 -2
- package/dist/bundling-sources/shared_libs/play-runtime/runner-app/index.ts +72 -9
- package/dist/bundling-sources/shared_libs/play-runtime/runner-app/runtime-progress-push.ts +2 -3
- package/dist/bundling-sources/shared_libs/play-runtime/runner-app/runtime-terminal-push.ts +77 -10
- package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona.ts +1 -1
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backend.ts +2 -0
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/absurd-pool-options.ts +145 -0
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/absurd-runtime-worker.ts +17 -9
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/absurd-shared.ts +8 -139
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres-pool.ts +19 -5
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres-progress.ts +176 -18
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres-rate-state.ts +7 -5
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres.ts +1404 -478
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/receipt-gateway.ts +55 -25
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/run-ledger-dispatch-authority.ts +97 -0
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/run-ledger-outbox-recovery.ts +34 -15
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/runtime-event-search.ts +558 -57
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/runtime-receipt-value-storage.ts +10 -1
- package/dist/bundling-sources/shared_libs/play-runtime/step-progress.ts +6 -1
- package/dist/bundling-sources/shared_libs/play-runtime/test-admission-batching.ts +103 -0
- package/dist/bundling-sources/shared_libs/play-runtime/tool-call/runtime-step-receipts-adapter.ts +22 -5
- package/dist/bundling-sources/shared_libs/play-runtime/tool-http-errors.ts +35 -0
- package/dist/bundling-sources/shared_libs/play-runtime/tool-result.ts +68 -8
- package/dist/bundling-sources/shared_libs/play-runtime/worker-api-types.ts +2 -0
- package/dist/bundling-sources/shared_libs/play-runtime/zerobounce-batching.ts +75 -28
- package/dist/bundling-sources/shared_libs/plays/tool-result-types.ts +2 -0
- package/dist/cli/index.js +195 -582
- package/dist/cli/index.mjs +195 -582
- package/dist/{client-FR_u0dxT.d.ts → client-Ct4jmvAg.d.ts} +29 -8
- package/dist/{client-phK1Ixcl.d.mts → client-Ct9GKrwu.d.mts} +29 -8
- package/dist/{compiler-manifest-9SQtUuKb.d.mts → compiler-manifest-qtTNYvBl.d.mts} +2 -0
- package/dist/{compiler-manifest-9SQtUuKb.d.ts → compiler-manifest-qtTNYvBl.d.ts} +2 -0
- package/dist/index.d.mts +4 -4
- package/dist/index.d.ts +4 -4
- package/dist/index.js +14 -5
- package/dist/index.mjs +14 -5
- package/dist/install-integrity.json +7 -4
- package/dist/plays/bundle-play-file.d.mts +2 -2
- package/dist/plays/bundle-play-file.d.ts +2 -2
- package/dist/plays/run-request.d.mts +2 -2
- package/dist/plays/run-request.d.ts +2 -2
- package/dist/release.d.mts +1 -1
- package/dist/release.d.ts +1 -1
- package/dist/release.js +1 -1
- package/dist/release.mjs +1 -1
- package/package.json +1 -1
package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/runtime-event-search.ts
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The customer-visible, append-only search projection of durable Play facts.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* After reader-first cutover, triggers persist raw input in the existing
|
|
5
|
+
* outbox. The leased dispatcher projects and compacts it before delivery.
|
|
6
|
+
* PlanetScale runtime_observability.events is the single log/event/receipt
|
|
7
|
+
* discovery surface. JSONPath runs natively against its indexed search_doc;
|
|
8
|
+
* exact identities use indexed columns on that same table. Full saved results
|
|
9
|
+
* resolve through the scoped fetch_receipt capability, never a second search
|
|
10
|
+
* store or a CLI-side scan. Receipt output remains authoritative in its store.
|
|
11
|
+
* The API uses only the fixed, tenant-scoped readers in this module.
|
|
7
12
|
*/
|
|
8
13
|
import { normalizeRuntimeSchedulerSchema } from '../runtime-scheduler-topology';
|
|
9
14
|
import type { PostgresSchedulerQueryClient } from './postgres';
|
|
@@ -56,7 +61,11 @@ const RUNTIME_EVENT_LOG_POSITION_EXPRESSION = `jsonb_set(
|
|
|
56
61
|
// Migration readiness tracks this projection body independently of scheduler
|
|
57
62
|
// wire and schema versions. Bump when a changed trigger must be reinstalled.
|
|
58
63
|
export const RUNTIME_EVENT_SEARCH_OUTBOX_PROJECTION_MARKER =
|
|
59
|
-
'runtime-event-search-
|
|
64
|
+
'runtime-event-search-origin-batches-log-metadata';
|
|
65
|
+
/** Trusted trigger-authored state; absent on already-projected legacy rows. */
|
|
66
|
+
export const RUNTIME_EVENT_DEFERRED_PROJECTION_FIELD =
|
|
67
|
+
'_runtimeSearchProjectionPending';
|
|
68
|
+
export const RUNTIME_EVENT_SEARCH_ONLY_FIELD = '_runtimeSearchOnly';
|
|
60
69
|
export const RUNTIME_EVENT_CANONICAL_RETENTION_INDEX_READY_MARKER =
|
|
61
70
|
'runtime-event-canonical-retention-v1';
|
|
62
71
|
export const RUNTIME_EVENTS_RETENTION_MS = 7 * 24 * 60 * 60 * 1000;
|
|
@@ -172,7 +181,7 @@ function parseRuntimeEventJsonpath(value: string): RuntimeEventPredicate[] {
|
|
|
172
181
|
);
|
|
173
182
|
}
|
|
174
183
|
const clause =
|
|
175
|
-
/\$\.(orgId|runId|playName|runtimeScope|producerAttempt|kind|status|source|stepId|stepKind|datasetId|phase|provider|operation|context\.[A-Za-z_][A-Za-z0-9_]{0,63})\s*==\s*("(?:\\.|[^"\\])*"|true|false|-?\d+(?:\.\d+)?)/y;
|
|
184
|
+
/\$\.(eventId|orgId|runId|playName|runtimeScope|producerAttempt|kind|status|source|level|stepId|stepKind|datasetId|phase|provider|operation|context\.[A-Za-z_][A-Za-z0-9_]{0,63})\s*==\s*("(?:\\.|[^"\\])*"|true|false|-?\d+(?:\.\d+)?)/y;
|
|
176
185
|
let position = 0;
|
|
177
186
|
let clauses = 0;
|
|
178
187
|
const predicates: RuntimeEventPredicate[] = [];
|
|
@@ -209,7 +218,9 @@ function parseRuntimeEventJsonpath(value: string): RuntimeEventPredicate[] {
|
|
|
209
218
|
);
|
|
210
219
|
}
|
|
211
220
|
if (
|
|
212
|
-
['orgId', 'runId', 'playName', 'runtimeScope'].includes(
|
|
221
|
+
['eventId', 'orgId', 'runId', 'playName', 'runtimeScope'].includes(
|
|
222
|
+
match[1],
|
|
223
|
+
) &&
|
|
213
224
|
typeof parsed !== 'string'
|
|
214
225
|
) {
|
|
215
226
|
throw new RuntimeEventInputError(
|
|
@@ -226,6 +237,14 @@ function parseRuntimeEventJsonpath(value: string): RuntimeEventPredicate[] {
|
|
|
226
237
|
'Runtime event producerAttempt requires a positive integer.',
|
|
227
238
|
);
|
|
228
239
|
}
|
|
240
|
+
if (
|
|
241
|
+
match[1] === 'level' &&
|
|
242
|
+
!['debug', 'info', 'warn', 'error'].includes(String(parsed))
|
|
243
|
+
) {
|
|
244
|
+
throw new RuntimeEventInputError(
|
|
245
|
+
'Runtime event level requires debug, info, warn or error.',
|
|
246
|
+
);
|
|
247
|
+
}
|
|
229
248
|
predicates.push({ path: match[1], literal: match[2], value: parsed });
|
|
230
249
|
if (
|
|
231
250
|
match[1].startsWith('context.') &&
|
|
@@ -343,6 +362,7 @@ export async function searchRuntimeEvents(
|
|
|
343
362
|
addExact('kind', input.kind);
|
|
344
363
|
if (input.jsonpath !== undefined) {
|
|
345
364
|
const identityColumns = {
|
|
365
|
+
eventId: 'event_id',
|
|
346
366
|
orgId: 'org_id',
|
|
347
367
|
runId: 'run_id',
|
|
348
368
|
playName: 'play_name',
|
|
@@ -950,11 +970,131 @@ export async function hasRuntimeEventReaderGrant(
|
|
|
950
970
|
return result.rows[0]?.ready === true;
|
|
951
971
|
}
|
|
952
972
|
|
|
973
|
+
/** Deployment-owned cutover; install readers everywhere before enabling. */
|
|
974
|
+
export async function setRuntimeEventDeferredProjection(
|
|
975
|
+
client: PostgresSchedulerQueryClient,
|
|
976
|
+
runtimeScope: string,
|
|
977
|
+
deferred: boolean,
|
|
978
|
+
independent?: boolean,
|
|
979
|
+
): Promise<void> {
|
|
980
|
+
const schema = quoteIdent(requiredScope(runtimeScope));
|
|
981
|
+
if (!(await hasRuntimeEventSearchShape(client, runtimeScope))) {
|
|
982
|
+
throw new Error(
|
|
983
|
+
'Install the deferred outbox reader before changing projection mode.',
|
|
984
|
+
);
|
|
985
|
+
}
|
|
986
|
+
if (deferred && !(await hasRuntimeEventLogPositionIndex(client))) {
|
|
987
|
+
throw new Error(
|
|
988
|
+
'Prepare the log-position indexes before enabling deferred projection.',
|
|
989
|
+
);
|
|
990
|
+
}
|
|
991
|
+
await client.query('BEGIN');
|
|
992
|
+
try {
|
|
993
|
+
await client.query("SELECT set_config('lock_timeout', '1000', true)");
|
|
994
|
+
// Fence trigger execution while changing mode. This operator-only lock
|
|
995
|
+
// never spans projection; normal producer writes acquire no such lock.
|
|
996
|
+
await client.query(
|
|
997
|
+
`LOCK TABLE ${schema}.outbox IN SHARE ROW EXCLUSIVE MODE`,
|
|
998
|
+
);
|
|
999
|
+
if (!deferred) {
|
|
1000
|
+
const pending = await client.query<{ event_id: string }>(
|
|
1001
|
+
`SELECT event_id FROM ${schema}.outbox
|
|
1002
|
+
WHERE delivered_at IS NULL
|
|
1003
|
+
AND payload_json->'${RUNTIME_EVENT_DEFERRED_PROJECTION_FIELD}' = 'true'::jsonb
|
|
1004
|
+
LIMIT 1`,
|
|
1005
|
+
);
|
|
1006
|
+
if (pending.rows.length > 0) {
|
|
1007
|
+
throw new Error(
|
|
1008
|
+
'Drain deferred outbox rows before returning to synchronous projection.',
|
|
1009
|
+
);
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
if (independent === false || !deferred) {
|
|
1013
|
+
const raw = await client.query<{ event_id: string }>(
|
|
1014
|
+
`SELECT event_id FROM ${schema}.outbox WHERE delivered_at IS NULL AND payload_json->'${RUNTIME_EVENT_SEARCH_ONLY_FIELD}' = 'true'::jsonb LIMIT 1`,
|
|
1015
|
+
);
|
|
1016
|
+
if (raw.rows.length)
|
|
1017
|
+
throw new Error(
|
|
1018
|
+
'Drain independent search rows before disabling independent projection.',
|
|
1019
|
+
);
|
|
1020
|
+
}
|
|
1021
|
+
await installRuntimeEventProjectionMode(client, schema, deferred);
|
|
1022
|
+
if (independent !== undefined || !deferred)
|
|
1023
|
+
await installIndependentProjectionMode(
|
|
1024
|
+
client,
|
|
1025
|
+
schema,
|
|
1026
|
+
deferred && independent === true,
|
|
1027
|
+
);
|
|
1028
|
+
await client.query('COMMIT');
|
|
1029
|
+
} catch (error) {
|
|
1030
|
+
await client.query('ROLLBACK');
|
|
1031
|
+
throw error;
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
/** Operator state uses the same durable pending set as the outbox projector. */
|
|
1036
|
+
export async function inspectRuntimeEventProjection(
|
|
1037
|
+
client: PostgresSchedulerQueryClient,
|
|
1038
|
+
runtimeScope: string,
|
|
1039
|
+
): Promise<{
|
|
1040
|
+
deferred: boolean;
|
|
1041
|
+
independent: boolean;
|
|
1042
|
+
pending: number;
|
|
1043
|
+
blocked: number;
|
|
1044
|
+
oldestPendingAt: string | null;
|
|
1045
|
+
}> {
|
|
1046
|
+
const schema = quoteIdent(requiredScope(runtimeScope));
|
|
1047
|
+
const result = await client.query<{
|
|
1048
|
+
deferred: boolean;
|
|
1049
|
+
independent: boolean;
|
|
1050
|
+
pending: string | number;
|
|
1051
|
+
blocked: string | number;
|
|
1052
|
+
oldest: Date | string | null;
|
|
1053
|
+
}>(`SELECT ${schema}.defer_runtime_event_projection() AS deferred, ${schema}.independent_runtime_event_projection() AS independent,
|
|
1054
|
+
count(*) AS pending, count(*) FILTER (WHERE failed_at IS NOT NULL) AS blocked,
|
|
1055
|
+
min(created_at) AS oldest
|
|
1056
|
+
FROM ${schema}.outbox
|
|
1057
|
+
WHERE delivered_at IS NULL
|
|
1058
|
+
AND payload_json->'${RUNTIME_EVENT_DEFERRED_PROJECTION_FIELD}' = 'true'::jsonb`);
|
|
1059
|
+
const state = result.rows[0]!;
|
|
1060
|
+
return {
|
|
1061
|
+
deferred: state.deferred,
|
|
1062
|
+
independent: state.independent,
|
|
1063
|
+
pending: Number(state.pending),
|
|
1064
|
+
blocked: Number(state.blocked),
|
|
1065
|
+
oldestPendingAt: state.oldest === null ? null : iso(state.oldest),
|
|
1066
|
+
};
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
async function installRuntimeEventProjectionMode(
|
|
1070
|
+
client: PostgresSchedulerQueryClient,
|
|
1071
|
+
schema: string,
|
|
1072
|
+
deferred: boolean,
|
|
1073
|
+
): Promise<void> {
|
|
1074
|
+
await client.query(`CREATE OR REPLACE FUNCTION ${schema}.defer_runtime_event_projection()
|
|
1075
|
+
RETURNS boolean LANGUAGE sql VOLATILE SET search_path = pg_catalog
|
|
1076
|
+
AS 'SELECT ${deferred ? 'true' : 'false'}'`);
|
|
1077
|
+
await client.query(
|
|
1078
|
+
`REVOKE ALL ON FUNCTION ${schema}.defer_runtime_event_projection() FROM PUBLIC`,
|
|
1079
|
+
);
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
async function installIndependentProjectionMode(
|
|
1083
|
+
client: PostgresSchedulerQueryClient,
|
|
1084
|
+
schema: string,
|
|
1085
|
+
enabled: boolean,
|
|
1086
|
+
): Promise<void> {
|
|
1087
|
+
await client.query(`CREATE OR REPLACE FUNCTION ${schema}.independent_runtime_event_projection()
|
|
1088
|
+
RETURNS boolean LANGUAGE sql VOLATILE SET search_path = pg_catalog AS 'SELECT ${enabled ? 'true' : 'false'}'`);
|
|
1089
|
+
await client.query(
|
|
1090
|
+
`REVOKE ALL ON FUNCTION ${schema}.independent_runtime_event_projection() FROM PUBLIC`,
|
|
1091
|
+
);
|
|
1092
|
+
}
|
|
1093
|
+
|
|
953
1094
|
/**
|
|
954
|
-
*
|
|
955
|
-
*
|
|
956
|
-
*
|
|
957
|
-
* corresponding search history after enablement.
|
|
1095
|
+
* Migration installs readers with synchronous production unchanged. After
|
|
1096
|
+
* cutover, the producer persists raw input and only the leased dispatcher
|
|
1097
|
+
* projects and compacts it, atomically, before acknowledging its outbox row.
|
|
958
1098
|
*/
|
|
959
1099
|
export async function attachRuntimeEventSearchTriggers(
|
|
960
1100
|
client: PostgresSchedulerQueryClient,
|
|
@@ -962,6 +1102,33 @@ export async function attachRuntimeEventSearchTriggers(
|
|
|
962
1102
|
): Promise<void> {
|
|
963
1103
|
const scope = requiredScope(runtimeScope);
|
|
964
1104
|
const schema = quoteIdent(scope);
|
|
1105
|
+
// Refreshing the reader must preserve an already-enabled producer cutover.
|
|
1106
|
+
const mode = await client.query<{ installed: boolean }>(
|
|
1107
|
+
`SELECT to_regprocedure($1) IS NOT NULL AS installed`,
|
|
1108
|
+
[`${schema}.defer_runtime_event_projection()`],
|
|
1109
|
+
);
|
|
1110
|
+
if (!mode.rows[0]?.installed) {
|
|
1111
|
+
await installRuntimeEventProjectionMode(client, schema, false);
|
|
1112
|
+
}
|
|
1113
|
+
const independentMode = await client.query<{ installed: boolean }>(
|
|
1114
|
+
`SELECT to_regprocedure($1) IS NOT NULL AS installed`,
|
|
1115
|
+
[`${schema}.independent_runtime_event_projection()`],
|
|
1116
|
+
);
|
|
1117
|
+
if (!independentMode.rows[0]?.installed)
|
|
1118
|
+
await installIndependentProjectionMode(client, schema, false);
|
|
1119
|
+
// Stamp new admissions once. Existing launches never acquire a producer pin
|
|
1120
|
+
// on replay or worker upgrade. Ignore a caller-supplied pin before cutover.
|
|
1121
|
+
await client.query(`CREATE OR REPLACE FUNCTION ${schema}.pin_runtime_log_identity() RETURNS trigger
|
|
1122
|
+
LANGUAGE plpgsql SECURITY DEFINER SET search_path = pg_catalog AS $function$
|
|
1123
|
+
BEGIN
|
|
1124
|
+
NEW.launch_json := NEW.launch_json - 'runtimeLogIdentity';
|
|
1125
|
+
NEW.admission_json := NEW.admission_json - 'runtimeLogIdentity';
|
|
1126
|
+
IF ${schema}.independent_runtime_event_projection() THEN
|
|
1127
|
+
NEW.launch_json := NEW.launch_json || '{"runtimeLogIdentity":"origin"}'::jsonb;
|
|
1128
|
+
NEW.admission_json := NEW.admission_json || '{"runtimeLogIdentity":"origin"}'::jsonb;
|
|
1129
|
+
END IF;
|
|
1130
|
+
RETURN NEW;
|
|
1131
|
+
END $function$`);
|
|
965
1132
|
await client.query(`
|
|
966
1133
|
CREATE OR REPLACE FUNCTION ${schema}.search_receipt_event() RETURNS trigger
|
|
967
1134
|
LANGUAGE plpgsql SECURITY DEFINER SET search_path = pg_catalog AS $function$
|
|
@@ -978,6 +1145,23 @@ export async function attachRuntimeEventSearchTriggers(
|
|
|
978
1145
|
IF NEW.run_id IS NULL OR NEW.org_id IS NULL THEN
|
|
979
1146
|
RAISE EXCEPTION 'Terminal receipt search projection requires run and organization';
|
|
980
1147
|
END IF;
|
|
1148
|
+
IF ${schema}.defer_runtime_event_projection() THEN
|
|
1149
|
+
IF NOT EXISTS (SELECT 1 FROM ${schema}.runs
|
|
1150
|
+
WHERE run_id = NEW.run_id AND org_id = NEW.org_id) THEN
|
|
1151
|
+
RAISE EXCEPTION 'Terminal receipt search projection has no matching run';
|
|
1152
|
+
END IF;
|
|
1153
|
+
ref := gen_random_uuid();
|
|
1154
|
+
INSERT INTO ${schema}.outbox (event_id, run_id, event_type, payload_json, created_at)
|
|
1155
|
+
VALUES ('receipt:' || ref::text, NEW.run_id,
|
|
1156
|
+
${RUN_SCHEDULER_OUTBOX_EVENT_TYPE_SQL_LITERAL.receiptEvent},
|
|
1157
|
+
jsonb_strip_nulls(jsonb_build_object(
|
|
1158
|
+
'orgId', NEW.org_id, 'receiptRef', ref, 'receiptKey', NEW.k,
|
|
1159
|
+
'status', NEW.status, 'occurredAt', NEW.updated_at,
|
|
1160
|
+
'producerAttempt', NEW.lease_owner_attempt)),
|
|
1161
|
+
GREATEST(clock_timestamp(), coalesce((SELECT max(created_at) + interval '1 microsecond'
|
|
1162
|
+
FROM ${schema}.outbox WHERE run_id = NEW.run_id), clock_timestamp())));
|
|
1163
|
+
RETURN NEW;
|
|
1164
|
+
END IF;
|
|
981
1165
|
-- Serialize event ordering with the same per-run lock as outbox facts.
|
|
982
1166
|
PERFORM pg_advisory_xact_lock(hashtext(${quoteLiteral(scope)}), hashtext(NEW.run_id));
|
|
983
1167
|
SELECT play_name, run_event_store = 'planet_scale'
|
|
@@ -1001,7 +1185,19 @@ export async function attachRuntimeEventSearchTriggers(
|
|
|
1001
1185
|
NEW.updated_at, NEW.run_id, play, 'receipt.' || NEW.status,
|
|
1002
1186
|
ref, NEW.k, jsonb_strip_nulls(jsonb_build_object(
|
|
1003
1187
|
'kind', 'receipt.' || NEW.status, 'status', NEW.status,
|
|
1004
|
-
'source', 'receipt', 'producerAttempt', NEW.lease_owner_attempt
|
|
1188
|
+
'source', 'receipt', 'producerAttempt', NEW.lease_owner_attempt,
|
|
1189
|
+
-- Only the reserved runtime diagnostic record supplies this flat
|
|
1190
|
+
-- context. Ordinary authored receipt outputs are never indexed.
|
|
1191
|
+
'context', CASE WHEN NEW.k LIKE 'tool-evidence:%'
|
|
1192
|
+
AND NEW.output_json->>'kind' = 'tool_call_evidence' THEN jsonb_strip_nulls(jsonb_build_object(
|
|
1193
|
+
'callKey', NEW.output_json#>'{context,callKey}',
|
|
1194
|
+
'toolId', NEW.output_json#>'{context,toolId}',
|
|
1195
|
+
'callStatus', NEW.output_json#>'{context,callStatus}',
|
|
1196
|
+
'stepId', NEW.output_json#>'{context,stepId}',
|
|
1197
|
+
'dataset', NEW.output_json#>'{context,dataset}',
|
|
1198
|
+
'column', NEW.output_json#>'{context,column}',
|
|
1199
|
+
'rowKey', NEW.output_json#>'{context,rowKey}'
|
|
1200
|
+
)) END
|
|
1005
1201
|
))
|
|
1006
1202
|
);
|
|
1007
1203
|
RETURN NEW;
|
|
@@ -1009,18 +1205,129 @@ export async function attachRuntimeEventSearchTriggers(
|
|
|
1009
1205
|
$function$
|
|
1010
1206
|
`);
|
|
1011
1207
|
await client.query(`DO $create$ BEGIN
|
|
1208
|
+
IF NOT EXISTS (SELECT 1 FROM pg_trigger WHERE tgrelid = '${schema}.runs'::regclass
|
|
1209
|
+
AND tgname = 'runtime_run_log_identity') THEN
|
|
1210
|
+
CREATE TRIGGER runtime_run_log_identity BEFORE INSERT ON ${schema}.runs
|
|
1211
|
+
FOR EACH ROW EXECUTE FUNCTION ${schema}.pin_runtime_log_identity();
|
|
1212
|
+
END IF;
|
|
1012
1213
|
IF NOT EXISTS (SELECT 1 FROM pg_trigger WHERE tgrelid = '${schema}.receipts'::regclass
|
|
1013
1214
|
AND tgname = 'runtime_receipt_event_v1') THEN
|
|
1014
1215
|
CREATE TRIGGER runtime_receipt_event_v1 AFTER INSERT OR UPDATE ON ${schema}.receipts
|
|
1015
1216
|
FOR EACH ROW EXECUTE FUNCTION ${schema}.search_receipt_event();
|
|
1016
1217
|
END IF;
|
|
1017
1218
|
END $create$`);
|
|
1219
|
+
await client.query(`
|
|
1220
|
+
CREATE OR REPLACE FUNCTION ${schema}.run_log_metadata(line text, metadata jsonb, fallback_at timestamptz) RETURNS jsonb
|
|
1221
|
+
LANGUAGE plpgsql IMMUTABLE SET search_path = pg_catalog AS $function$
|
|
1222
|
+
DECLARE clean text; message text; level text; at_time timestamptz := fallback_at;
|
|
1223
|
+
BEGIN
|
|
1224
|
+
clean := regexp_replace(line, '^' || chr(1) || 'prov:[^' || chr(1) || ']*' || chr(1), '');
|
|
1225
|
+
IF metadata IS NOT NULL AND metadata <> 'null'::jsonb THEN
|
|
1226
|
+
IF jsonb_typeof(metadata) IS DISTINCT FROM 'object'
|
|
1227
|
+
OR NOT coalesce(metadata->>'level' IN ('debug','info','warn','error'), false)
|
|
1228
|
+
OR jsonb_typeof(metadata->'occurredAt') IS DISTINCT FROM 'number'
|
|
1229
|
+
OR NOT coalesce(metadata->>'occurredAt' ~ '^[1-9][0-9]{0,15}$', false)
|
|
1230
|
+
OR (metadata ? 'stepId' AND (jsonb_typeof(metadata->'stepId') IS DISTINCT FROM 'string'
|
|
1231
|
+
OR length(metadata->>'stepId') NOT BETWEEN 1 AND 256)) THEN
|
|
1232
|
+
RAISE EXCEPTION 'Run log metadata is invalid';
|
|
1233
|
+
END IF;
|
|
1234
|
+
RETURN jsonb_strip_nulls(jsonb_build_object('level', metadata->'level',
|
|
1235
|
+
'occurredAt', metadata->'occurredAt', 'stepId', metadata->'stepId'));
|
|
1236
|
+
END IF;
|
|
1237
|
+
-- Historical lines already carry the original ISO time and severity.
|
|
1238
|
+
IF clean ~ '^\\[[-0-9]{10}T[0-9:.]+Z\\]' THEN
|
|
1239
|
+
BEGIN at_time := substring(clean from '^\\[([^]]+)\\]')::timestamptz;
|
|
1240
|
+
EXCEPTION WHEN invalid_datetime_format OR datetime_field_overflow THEN at_time := fallback_at; END;
|
|
1241
|
+
END IF;
|
|
1242
|
+
message := regexp_replace(clean, '^\\[[-0-9]{10}T[0-9:.]+Z\\][ ]*', '');
|
|
1243
|
+
level := CASE
|
|
1244
|
+
WHEN message ~* '^\\[(error|console.error)\\]' THEN 'error'
|
|
1245
|
+
WHEN message ~* '^\\[(warn|console.warn)\\]' THEN 'warn'
|
|
1246
|
+
WHEN message ~* '^\\[info\\]' THEN 'info'
|
|
1247
|
+
WHEN message ~* '^\\[(debug|console.debug|perf|runtime.(phase_summary|logs|log_transport))\\]'
|
|
1248
|
+
OR line ~ ('^' || chr(1) || 'prov:(infra|receipt|replay)' || chr(1)) THEN 'debug'
|
|
1249
|
+
ELSE 'info' END;
|
|
1250
|
+
RETURN jsonb_build_object('level', level, 'occurredAt', floor(extract(epoch FROM at_time) * 1000)::bigint);
|
|
1251
|
+
END
|
|
1252
|
+
$function$
|
|
1253
|
+
`);
|
|
1254
|
+
await client.query(`
|
|
1255
|
+
CREATE OR REPLACE FUNCTION ${schema}.project_origin_log_batch(observation ${schema}.outbox, item jsonb, run_org text, play text, compact_logs boolean, canonical_events boolean, at_time timestamptz, doc jsonb, closed boolean)
|
|
1256
|
+
RETURNS text[] LANGUAGE plpgsql SECURITY DEFINER SET search_path = pg_catalog AS $function$
|
|
1257
|
+
DECLARE fresh text[]; mismatches bigint;
|
|
1258
|
+
BEGIN
|
|
1259
|
+
IF jsonb_typeof(item->'lineIds') IS DISTINCT FROM 'array'
|
|
1260
|
+
OR jsonb_array_length(item->'lineIds') <> jsonb_array_length(item->'lines')
|
|
1261
|
+
OR EXISTS (SELECT 1 FROM jsonb_array_elements(item->'lineIds') id WHERE jsonb_typeof(id) <> 'string' OR (id #>> '{}') !~ '^[A-Za-z0-9:_-]{1,128}$')
|
|
1262
|
+
OR (SELECT count(DISTINCT id) FROM jsonb_array_elements(item->'lineIds') id) <> jsonb_array_length(item->'lineIds') THEN
|
|
1263
|
+
RAISE EXCEPTION 'Origin log identities must be distinct bounded strings aligned with lines';
|
|
1264
|
+
END IF;
|
|
1265
|
+
IF EXISTS (SELECT 1 FROM jsonb_array_elements(item->'lines') line WHERE jsonb_typeof(line) <> 'string') THEN
|
|
1266
|
+
RAISE EXCEPTION 'Search projection requires log line strings';
|
|
1267
|
+
END IF;
|
|
1268
|
+
IF item ? 'lineContexts' AND EXISTS (
|
|
1269
|
+
SELECT 1 FROM jsonb_array_elements(item->'lineContexts') ctx
|
|
1270
|
+
WHERE jsonb_typeof(ctx) NOT IN ('object', 'null')
|
|
1271
|
+
) THEN RAISE EXCEPTION 'Search projection log context must be an object'; END IF;
|
|
1272
|
+
IF item ? 'lineContexts' AND EXISTS (
|
|
1273
|
+
SELECT 1 FROM jsonb_array_elements(item->'lineContexts') ctx
|
|
1274
|
+
WHERE jsonb_typeof(ctx) = 'object' AND (
|
|
1275
|
+
(SELECT count(*) FROM jsonb_object_keys(ctx)) > 8 OR octet_length(ctx::text) > 2064
|
|
1276
|
+
OR EXISTS (SELECT 1 FROM jsonb_each(ctx) field WHERE
|
|
1277
|
+
field.key !~ '^[A-Za-z_][A-Za-z0-9_]{0,63}$' OR field.key IN ('__proto__', 'constructor', 'prototype')
|
|
1278
|
+
OR jsonb_typeof(field.value) NOT IN ('string', 'number', 'boolean')
|
|
1279
|
+
OR (jsonb_typeof(field.value) = 'string' AND octet_length(field.value #>> '{}') > 256)
|
|
1280
|
+
OR (jsonb_typeof(field.value) = 'number' AND octet_length(field.value #>> '{}') > 32))
|
|
1281
|
+
)
|
|
1282
|
+
) THEN RAISE EXCEPTION 'Search projection log context has an unsupported key or value'; END IF;
|
|
1283
|
+
WITH entries AS MATERIALIZED (
|
|
1284
|
+
SELECT value #>> '{}' AS body, ordinality AS ordinal,
|
|
1285
|
+
'log-origin:' || observation.run_id || ':' || (item->'lineIds'->>(ordinality-1)::int) AS eid,
|
|
1286
|
+
NULLIF(item->'lineContexts'->(ordinality-1)::int, 'null'::jsonb) AS ctx,
|
|
1287
|
+
${schema}.run_log_metadata(value #>> '{}', item->'lineMetadata'->(ordinality-1)::int, at_time) AS metadata
|
|
1288
|
+
FROM jsonb_array_elements(item->'lines') WITH ORDINALITY
|
|
1289
|
+
), inserted AS (
|
|
1290
|
+
INSERT INTO runtime_observability.events (
|
|
1291
|
+
runtime_scope, org_id, event_id, event_seq, event_payload, occurred_at,
|
|
1292
|
+
run_id, play_name, kind, level, message, log_body, source_digest, log_text_digest, search_doc
|
|
1293
|
+
) SELECT ${quoteLiteral(scope)}, run_org, eid,
|
|
1294
|
+
CASE WHEN canonical_events THEN nextval('runtime_observability.runtime_event_order_seq') END,
|
|
1295
|
+
CASE WHEN canonical_events THEN jsonb_strip_nulls((item - 'lines' - 'lineContexts' - 'lineIds' - 'lineMetadata') || jsonb_build_object(
|
|
1296
|
+
'type', ${RUN_EVENT_KIND_SQL_LITERAL.logAppended}, 'runId', observation.run_id,
|
|
1297
|
+
'occurredAt', metadata->'occurredAt', 'level', metadata->'level', 'stepId', metadata->'stepId', 'lineOrdinal', ordinal,
|
|
1298
|
+
'entryId', item->'lineIds'->(ordinal-1)::int, 'context', ctx)) END,
|
|
1299
|
+
to_timestamp((metadata->>'occurredAt')::double precision / 1000), observation.run_id, play, ${RUN_EVENT_KIND_SQL_LITERAL.logAppended}, metadata->>'level',
|
|
1300
|
+
CASE WHEN compact_logs THEN NULL ELSE left(body, 2048) END, body,
|
|
1301
|
+
CASE WHEN ctx IS NULL THEN encode(sha256(convert_to(body,'UTF8')),'hex')
|
|
1302
|
+
ELSE encode(sha256(convert_to(jsonb_build_array(body,ctx)::text,'UTF8')),'hex') END,
|
|
1303
|
+
encode(sha256(convert_to(btrim(body),'UTF8')),'hex'),
|
|
1304
|
+
doc || (metadata - 'occurredAt') || jsonb_strip_nulls(jsonb_build_object('entryId', item->'lineIds'->(ordinal-1)::int, 'context', ctx))
|
|
1305
|
+
FROM entries WHERE NOT closed ORDER BY ordinal
|
|
1306
|
+
ON CONFLICT (runtime_scope, org_id, event_id) DO NOTHING RETURNING event_id
|
|
1307
|
+
) SELECT coalesce(array_agg(inserted.event_id ORDER BY entries.ordinal), ARRAY[]::text[])
|
|
1308
|
+
INTO fresh FROM inserted JOIN entries ON entries.eid = inserted.event_id;
|
|
1309
|
+
-- A new READ COMMITTED statement sees a concurrent uniqueness winner.
|
|
1310
|
+
-- Tombstones retain digest/identity: retry never restores an expired body.
|
|
1311
|
+
SELECT count(*) INTO mismatches
|
|
1312
|
+
FROM jsonb_array_elements(item->'lines') WITH ORDINALITY AS entries(body, ordinal)
|
|
1313
|
+
LEFT JOIN runtime_observability.events event ON event.runtime_scope = ${quoteLiteral(scope)} AND event.org_id = run_org
|
|
1314
|
+
AND event.event_id = 'log-origin:' || observation.run_id || ':' || (item->'lineIds'->>(ordinal-1)::int)
|
|
1315
|
+
WHERE event.run_id IS DISTINCT FROM observation.run_id OR event.kind IS DISTINCT FROM ${RUN_EVENT_KIND_SQL_LITERAL.logAppended}
|
|
1316
|
+
OR event.source_digest IS DISTINCT FROM CASE WHEN NULLIF(item->'lineContexts'->(ordinal-1)::int, 'null'::jsonb) IS NULL
|
|
1317
|
+
THEN encode(sha256(convert_to(body #>> '{}','UTF8')),'hex')
|
|
1318
|
+
ELSE encode(sha256(convert_to(jsonb_build_array(body #>> '{}',item->'lineContexts'->(ordinal-1)::int)::text,'UTF8')),'hex') END;
|
|
1319
|
+
IF mismatches > 0 THEN
|
|
1320
|
+
RAISE EXCEPTION 'Origin log identity changed its content or ingress is closed after terminal acceptance';
|
|
1321
|
+
END IF;
|
|
1322
|
+
RETURN fresh;
|
|
1323
|
+
END $function$
|
|
1324
|
+
`);
|
|
1018
1325
|
await client.query(`
|
|
1019
1326
|
CREATE OR REPLACE FUNCTION ${schema}.project_outbox_event(observation ${schema}.outbox) RETURNS void
|
|
1020
1327
|
LANGUAGE plpgsql SECURITY DEFINER SET search_path = pg_catalog AS $function$
|
|
1021
1328
|
DECLARE run_org text; play text; compact_logs boolean; canonical_events boolean; run_status text; item jsonb; line jsonb; event_batch jsonb;
|
|
1022
1329
|
ordinal bigint; line_ordinal bigint; kind text; at_time timestamptz;
|
|
1023
|
-
doc jsonb; line_doc jsonb; line_context jsonb;
|
|
1330
|
+
doc jsonb; line_doc jsonb; line_context jsonb; line_metadata jsonb; line_at timestamptz;
|
|
1024
1331
|
eid text; existing_digest text; digest text; assigned_seq bigint; terminal_progress_identity boolean;
|
|
1025
1332
|
log_channel text; log_position bigint; matched_event_id text; matched_doc jsonb;
|
|
1026
1333
|
legacy_position_prefix text; position_event_ids text[];
|
|
@@ -1033,7 +1340,11 @@ export async function attachRuntimeEventSearchTriggers(
|
|
|
1033
1340
|
-- Log ordering owns a transaction-scoped mutex, independent of the
|
|
1034
1341
|
-- scheduler's Run/attempt row-lock order. In particular, old completion
|
|
1035
1342
|
-- writers lock attempt before Run; this recorder must not invert that.
|
|
1036
|
-
|
|
1343
|
+
-- Stable batches are ordered by the leased background prefix and cursor row.
|
|
1344
|
+
-- The legacy alias path still needs its old transaction mutex.
|
|
1345
|
+
IF NOT ${schema}.defer_runtime_event_projection() THEN
|
|
1346
|
+
PERFORM pg_advisory_xact_lock(hashtext(${quoteLiteral(scope)}), hashtext(observation.run_id));
|
|
1347
|
+
END IF;
|
|
1037
1348
|
SELECT org_id, play_name, run_log_store = 'planet_scale', status,
|
|
1038
1349
|
run_event_store = 'planet_scale'
|
|
1039
1350
|
INTO run_org, play, compact_logs, run_status, canonical_events
|
|
@@ -1041,6 +1352,35 @@ export async function attachRuntimeEventSearchTriggers(
|
|
|
1041
1352
|
IF run_org IS NULL OR play IS NULL THEN
|
|
1042
1353
|
RAISE EXCEPTION 'Outbox search projection has no matching run';
|
|
1043
1354
|
END IF;
|
|
1355
|
+
IF observation.event_type = ${RUN_SCHEDULER_OUTBOX_EVENT_TYPE_SQL_LITERAL.receiptEvent} THEN
|
|
1356
|
+
IF observation.payload_json->>'orgId' IS DISTINCT FROM run_org
|
|
1357
|
+
OR NOT coalesce(observation.payload_json->>'status' IN ('completed', 'failed', 'skipped'), false)
|
|
1358
|
+
OR nullif(observation.payload_json->>'receiptKey', '') IS NULL
|
|
1359
|
+
OR observation.payload_json->>'receiptRef' IS NULL
|
|
1360
|
+
OR observation.payload_json->>'occurredAt' IS NULL THEN
|
|
1361
|
+
RAISE EXCEPTION 'Deferred receipt event has invalid identity or status';
|
|
1362
|
+
END IF;
|
|
1363
|
+
INSERT INTO runtime_observability.events (
|
|
1364
|
+
runtime_scope, org_id, event_id, event_seq, event_payload,
|
|
1365
|
+
occurred_at, run_id, play_name, kind, receipt_ref, receipt_key, search_doc
|
|
1366
|
+
) VALUES (
|
|
1367
|
+
${quoteLiteral(scope)}, run_org, observation.event_id,
|
|
1368
|
+
CASE WHEN canonical_events THEN nextval('runtime_observability.runtime_event_order_seq') END,
|
|
1369
|
+
CASE WHEN canonical_events THEN jsonb_build_object(
|
|
1370
|
+
'type', 'receipt.' || (observation.payload_json->>'status'), 'runId', observation.run_id,
|
|
1371
|
+
'occurredAt', floor(extract(epoch FROM (observation.payload_json->>'occurredAt')::timestamptz) * 1000)::bigint,
|
|
1372
|
+
'source', 'receipt', 'status', observation.payload_json->>'status') END,
|
|
1373
|
+
(observation.payload_json->>'occurredAt')::timestamptz,
|
|
1374
|
+
observation.run_id, play, 'receipt.' || (observation.payload_json->>'status'),
|
|
1375
|
+
(observation.payload_json->>'receiptRef')::uuid,
|
|
1376
|
+
observation.payload_json->>'receiptKey',
|
|
1377
|
+
jsonb_strip_nulls(jsonb_build_object(
|
|
1378
|
+
'kind', 'receipt.' || (observation.payload_json->>'status'),
|
|
1379
|
+
'status', observation.payload_json->>'status', 'source', 'receipt',
|
|
1380
|
+
'producerAttempt', observation.payload_json->'producerAttempt'))
|
|
1381
|
+
) ON CONFLICT DO NOTHING;
|
|
1382
|
+
RETURN;
|
|
1383
|
+
END IF;
|
|
1044
1384
|
IF observation.event_type = ${RUN_SCHEDULER_OUTBOX_EVENT_TYPE_SQL_LITERAL.runEvents} THEN
|
|
1045
1385
|
event_batch := observation.payload_json->'events';
|
|
1046
1386
|
IF jsonb_typeof(event_batch) IS DISTINCT FROM 'array' THEN
|
|
@@ -1071,7 +1411,9 @@ export async function attachRuntimeEventSearchTriggers(
|
|
|
1071
1411
|
'type', ${RUN_EVENT_KIND_SQL_LITERAL.logAppended}, 'runId', observation.run_id, 'source', 'terminal',
|
|
1072
1412
|
'producerAttempt', envelope_attempt, 'channelOffset', 0,
|
|
1073
1413
|
'lines', observation.payload_json->'terminalLogLines',
|
|
1074
|
-
'lineContexts', observation.payload_json->'terminalLogContexts'
|
|
1414
|
+
'lineContexts', observation.payload_json->'terminalLogContexts',
|
|
1415
|
+
'lineMetadata', observation.payload_json->'terminalLogMetadata',
|
|
1416
|
+
'lineIds', NULLIF(observation.payload_json->'terminalLogIds', 'null'::jsonb)
|
|
1075
1417
|
))
|
|
1076
1418
|
);
|
|
1077
1419
|
END IF;
|
|
@@ -1117,11 +1459,14 @@ export async function attachRuntimeEventSearchTriggers(
|
|
|
1117
1459
|
));
|
|
1118
1460
|
END IF;
|
|
1119
1461
|
IF event_batch IS NOT NULL THEN
|
|
1462
|
+
IF ${schema}.defer_runtime_event_projection() AND EXISTS (SELECT 1 FROM jsonb_array_elements(event_batch) entry WHERE entry->>'type' = ${RUN_EVENT_KIND_SQL_LITERAL.logAppended} AND (NOT (entry ? 'lineIds') OR jsonb_typeof(entry->'lineIds') IS DISTINCT FROM 'array' OR EXISTS (SELECT 1 FROM jsonb_array_elements(CASE WHEN jsonb_typeof(entry->'lineIds') = 'array' THEN entry->'lineIds' ELSE '[]'::jsonb END) id WHERE id = 'null'::jsonb))) THEN
|
|
1463
|
+
PERFORM pg_advisory_xact_lock(hashtext(${quoteLiteral(scope)}), hashtext(observation.run_id));
|
|
1464
|
+
END IF;
|
|
1120
1465
|
-- Normalize old positional writers once per batch under the run mutex.
|
|
1121
1466
|
-- Per-line lookups must remain digest-selective: an OR fallback for
|
|
1122
1467
|
-- unmarked rows otherwise scans the whole attempt for every new line.
|
|
1123
1468
|
IF EXISTS (SELECT 1 FROM jsonb_array_elements(event_batch) AS entry
|
|
1124
|
-
WHERE entry->>'type' = ${RUN_EVENT_KIND_SQL_LITERAL.logAppended}) THEN
|
|
1469
|
+
WHERE entry->>'type' = ${RUN_EVENT_KIND_SQL_LITERAL.logAppended} AND (NOT (entry ? 'lineIds') OR EXISTS (SELECT 1 FROM jsonb_array_elements(CASE WHEN jsonb_typeof(entry->'lineIds') = 'array' THEN entry->'lineIds' ELSE '[]'::jsonb END) id WHERE id = 'null'::jsonb))) THEN
|
|
1125
1470
|
UPDATE runtime_observability.events AS event SET search_doc = event.search_doc ||
|
|
1126
1471
|
jsonb_build_object('logContentDigest', event.source_digest) ||
|
|
1127
1472
|
CASE WHEN NOT (event.search_doc ? 'logPositions')
|
|
@@ -1134,7 +1479,7 @@ export async function attachRuntimeEventSearchTriggers(
|
|
|
1134
1479
|
ELSE '{}'::jsonb END
|
|
1135
1480
|
WHERE event.runtime_scope = ${quoteLiteral(scope)} AND event.org_id = run_org
|
|
1136
1481
|
AND event.run_id = observation.run_id AND event.kind = ${RUN_EVENT_KIND_SQL_LITERAL.logAppended}
|
|
1137
|
-
AND event.source_digest IS NOT NULL AND NOT (event.search_doc ? 'logContentDigest')
|
|
1482
|
+
AND event.source_digest IS NOT NULL AND NOT (event.search_doc ? 'logContentDigest') AND NOT (event.search_doc ? 'entryId')
|
|
1138
1483
|
AND (event.search_doc->>'producerAttempt') ~ '^[1-9][0-9]{0,5}$';
|
|
1139
1484
|
END IF;
|
|
1140
1485
|
FOR item, ordinal IN SELECT value, ordinality FROM jsonb_array_elements(event_batch) WITH ORDINALITY LOOP
|
|
@@ -1172,12 +1517,28 @@ export async function attachRuntimeEventSearchTriggers(
|
|
|
1172
1517
|
OR jsonb_array_length(item->'lineContexts') <> jsonb_array_length(item->'lines')) THEN
|
|
1173
1518
|
RAISE EXCEPTION 'Search projection requires lineContexts aligned to log lines';
|
|
1174
1519
|
END IF;
|
|
1520
|
+
IF kind = ${RUN_EVENT_KIND_SQL_LITERAL.logAppended} AND item ? 'lineMetadata'
|
|
1521
|
+
AND (jsonb_typeof(item->'lineMetadata') IS DISTINCT FROM 'array'
|
|
1522
|
+
OR jsonb_array_length(item->'lineMetadata') <> jsonb_array_length(item->'lines')) THEN
|
|
1523
|
+
RAISE EXCEPTION 'Run log metadata must align with lines';
|
|
1524
|
+
END IF;
|
|
1525
|
+
IF kind = ${RUN_EVENT_KIND_SQL_LITERAL.logAppended} AND item ? 'lineIds' THEN
|
|
1526
|
+
IF jsonb_typeof(item->'lineIds') IS DISTINCT FROM 'array' OR jsonb_array_length(item->'lineIds') <> jsonb_array_length(item->'lines') THEN
|
|
1527
|
+
RAISE EXCEPTION 'Origin log identities must align with lines';
|
|
1528
|
+
END IF;
|
|
1529
|
+
IF EXISTS (SELECT 1 FROM jsonb_array_elements(item->'lineIds') id WHERE id <> 'null'::jsonb AND (jsonb_typeof(id) <> 'string' OR (id #>> '{}') !~ '^[A-Za-z0-9:_-]{1,128}$')) OR (SELECT count(id) FROM jsonb_array_elements(item->'lineIds') id WHERE id <> 'null'::jsonb) <> (SELECT count(DISTINCT id) FROM jsonb_array_elements(item->'lineIds') id WHERE id <> 'null'::jsonb) THEN RAISE EXCEPTION 'Origin log identities must be distinct bounded strings'; END IF;
|
|
1530
|
+
IF NOT EXISTS (SELECT 1 FROM jsonb_array_elements(item->'lineIds') id WHERE id = 'null'::jsonb) THEN
|
|
1531
|
+
fresh_event_ids := fresh_event_ids || ${schema}.project_origin_log_batch(observation, item, run_org, play, compact_logs, canonical_events, at_time, doc,
|
|
1532
|
+
compact_logs AND observation.event_type = ${RUN_SCHEDULER_OUTBOX_EVENT_TYPE_SQL_LITERAL.runEvents} AND coalesce((observation.payload_json->>'_runtimeSearchRunClosed')::boolean, run_status IN ('completed','failed','cancelled')));
|
|
1533
|
+
CONTINUE;
|
|
1534
|
+
END IF;
|
|
1535
|
+
END IF;
|
|
1175
1536
|
IF kind = ${RUN_EVENT_KIND_SQL_LITERAL.logAppended} THEN
|
|
1176
1537
|
FOR line, line_ordinal IN SELECT value, ordinality FROM jsonb_array_elements(item->'lines') WITH ORDINALITY LOOP
|
|
1177
1538
|
IF jsonb_typeof(line) IS DISTINCT FROM 'string' THEN
|
|
1178
1539
|
RAISE EXCEPTION 'Search projection requires log line strings';
|
|
1179
1540
|
END IF;
|
|
1180
|
-
IF (item->>'source' IS DISTINCT FROM 'coordinator')
|
|
1541
|
+
IF NULLIF(item->'lineIds'->((line_ordinal-1)::int), 'null'::jsonb) IS NULL AND (item->>'source' IS DISTINCT FROM 'coordinator')
|
|
1181
1542
|
AND NOT coalesce((item->>'channelOffset') ~ '^[0-9]{1,12}$', false) THEN
|
|
1182
1543
|
IF NOT recent_log_keys_loaded OR recent_log_attempt IS DISTINCT FROM item->>'producerAttempt' THEN
|
|
1183
1544
|
SELECT coalesce(jsonb_object_agg(recent.dedupe_key, true), '{}'::jsonb)
|
|
@@ -1187,6 +1548,7 @@ export async function attachRuntimeEventSearchTriggers(
|
|
|
1187
1548
|
FROM runtime_observability.events AS event
|
|
1188
1549
|
WHERE event.runtime_scope = ${quoteLiteral(scope)} AND event.org_id = run_org
|
|
1189
1550
|
AND event.run_id = observation.run_id AND event.kind = ${RUN_EVENT_KIND_SQL_LITERAL.logAppended}
|
|
1551
|
+
AND NOT (event.search_doc ? 'entryId')
|
|
1190
1552
|
AND event.log_seq IS NOT NULL AND (event.log_text_digest IS NOT NULL OR event.log_body IS NOT NULL)
|
|
1191
1553
|
AND (item->>'producerAttempt' IS NULL
|
|
1192
1554
|
OR event.search_doc->>'producerAttempt' = item->>'producerAttempt')
|
|
@@ -1231,8 +1593,10 @@ export async function attachRuntimeEventSearchTriggers(
|
|
|
1231
1593
|
RAISE EXCEPTION 'Search projection log context has an unsupported key or value';
|
|
1232
1594
|
END IF;
|
|
1233
1595
|
END IF;
|
|
1234
|
-
|
|
1235
|
-
|
|
1596
|
+
line_metadata := ${schema}.run_log_metadata(line #>> '{}', item->'lineMetadata'->((line_ordinal - 1)::int), at_time);
|
|
1597
|
+
line_at := to_timestamp((line_metadata->>'occurredAt')::double precision / 1000);
|
|
1598
|
+
line_doc := (CASE WHEN line_context IS NULL THEN doc
|
|
1599
|
+
ELSE doc || jsonb_build_object('context', line_context) END) || (line_metadata - 'occurredAt');
|
|
1236
1600
|
eid := CASE WHEN (item->>'channelOffset') ~ '^[0-9]{1,12}$'
|
|
1237
1601
|
AND (item->>'producerAttempt') ~ '^[0-9]{1,6}$'
|
|
1238
1602
|
THEN 'log:' || observation.run_id || ':' || (item->>'producerAttempt') || ':' ||
|
|
@@ -1241,7 +1605,11 @@ export async function attachRuntimeEventSearchTriggers(
|
|
|
1241
1605
|
digest := CASE WHEN line_context IS NULL
|
|
1242
1606
|
THEN encode(sha256(convert_to(line #>> '{}', 'UTF8')), 'hex')
|
|
1243
1607
|
ELSE encode(sha256(convert_to(jsonb_build_array(line #>> '{}', line_context)::text, 'UTF8')), 'hex') END;
|
|
1244
|
-
IF (item
|
|
1608
|
+
IF NULLIF(item->'lineIds'->((line_ordinal-1)::int), 'null'::jsonb) IS NOT NULL THEN
|
|
1609
|
+
IF jsonb_typeof(item->'lineIds'->((line_ordinal-1)::int)) <> 'string' OR (item->'lineIds'->>((line_ordinal-1)::int)) !~ '^[A-Za-z0-9:_-]{1,128}$' THEN RAISE EXCEPTION 'Origin log identity must be a bounded string'; END IF;
|
|
1610
|
+
eid := 'log-origin:' || observation.run_id || ':' || (item->'lineIds'->>((line_ordinal-1)::int));
|
|
1611
|
+
line_doc := line_doc || jsonb_build_object('entryId', item->'lineIds'->((line_ordinal-1)::int));
|
|
1612
|
+
ELSIF (item->>'channelOffset') ~ '^[0-9]{1,12}$'
|
|
1245
1613
|
AND (item->>'producerAttempt') ~ '^[1-9][0-9]{0,5}$' THEN
|
|
1246
1614
|
-- These transports retain different buffers. Their offsets
|
|
1247
1615
|
-- are immutable within a channel, never interchangeable.
|
|
@@ -1340,7 +1708,7 @@ export async function attachRuntimeEventSearchTriggers(
|
|
|
1340
1708
|
AND event.run_id = observation.run_id AND event.kind = ${RUN_EVENT_KIND_SQL_LITERAL.logAppended}
|
|
1341
1709
|
AND event.source_digest IS NOT NULL
|
|
1342
1710
|
AND event.search_doc ? 'producerAttempt'
|
|
1343
|
-
AND event.search_doc ? 'logContentDigest'
|
|
1711
|
+
AND event.search_doc ? 'logContentDigest' AND NOT (event.search_doc ? 'entryId')
|
|
1344
1712
|
AND event.search_doc->>'producerAttempt' = ((item->>'producerAttempt')::integer)::text
|
|
1345
1713
|
AND event.source_digest = digest
|
|
1346
1714
|
AND event.search_doc @> jsonb_build_object('logContentDigest', digest)
|
|
@@ -1361,7 +1729,8 @@ export async function attachRuntimeEventSearchTriggers(
|
|
|
1361
1729
|
-- retry of an accepted position is harmless; a new position
|
|
1362
1730
|
-- after terminal closure must never recreate expired history.
|
|
1363
1731
|
IF compact_logs AND observation.event_type = ${RUN_SCHEDULER_OUTBOX_EVENT_TYPE_SQL_LITERAL.runEvents}
|
|
1364
|
-
AND
|
|
1732
|
+
AND coalesce((observation.payload_json->>'_runtimeSearchRunClosed')::boolean,
|
|
1733
|
+
run_status IN ('completed', 'failed', 'cancelled'))
|
|
1365
1734
|
AND NOT EXISTS (SELECT 1 FROM runtime_observability.events
|
|
1366
1735
|
WHERE runtime_scope = ${quoteLiteral(scope)} AND org_id = run_org
|
|
1367
1736
|
AND event_id = eid) THEN
|
|
@@ -1375,14 +1744,14 @@ export async function attachRuntimeEventSearchTriggers(
|
|
|
1375
1744
|
${quoteLiteral(scope)}, run_org, eid,
|
|
1376
1745
|
CASE WHEN canonical_events THEN nextval('runtime_observability.runtime_event_order_seq') END,
|
|
1377
1746
|
CASE WHEN canonical_events THEN jsonb_strip_nulls(
|
|
1378
|
-
(item - 'lines' - 'lineContexts') || jsonb_build_object(
|
|
1747
|
+
(item - 'lines' - 'lineContexts' - 'lineMetadata' - 'lineIds') || jsonb_build_object(
|
|
1379
1748
|
'type', ${RUN_EVENT_KIND_SQL_LITERAL.logAppended}, 'runId', observation.run_id,
|
|
1380
|
-
'occurredAt',
|
|
1749
|
+
'occurredAt', line_metadata->'occurredAt', 'level', line_metadata->'level', 'stepId', line_metadata->'stepId',
|
|
1381
1750
|
'lineOrdinal', line_ordinal,
|
|
1382
1751
|
'context', line_context
|
|
1383
1752
|
)) END,
|
|
1384
|
-
|
|
1385
|
-
${RUN_EVENT_KIND_SQL_LITERAL.logAppended},
|
|
1753
|
+
line_at, observation.run_id, play,
|
|
1754
|
+
${RUN_EVENT_KIND_SQL_LITERAL.logAppended}, line_metadata->>'level', CASE WHEN compact_logs THEN NULL ELSE left(line #>> '{}', 2048) END,
|
|
1386
1755
|
line #>> '{}', digest,
|
|
1387
1756
|
encode(sha256(convert_to(btrim(line #>> '{}'), 'UTF8')), 'hex'), line_doc
|
|
1388
1757
|
) ON CONFLICT DO NOTHING;
|
|
@@ -1462,6 +1831,15 @@ export async function attachRuntimeEventSearchTriggers(
|
|
|
1462
1831
|
AND NOT (observation.payload_json ? 'terminalLogLines') THEN
|
|
1463
1832
|
RAISE EXCEPTION 'Search projection requires terminalLogLines for terminalLogContexts';
|
|
1464
1833
|
END IF;
|
|
1834
|
+
IF observation.payload_json ? 'terminalLogMetadata'
|
|
1835
|
+
AND NOT (observation.payload_json ? 'terminalLogLines') THEN
|
|
1836
|
+
RAISE EXCEPTION 'Search projection requires terminalLogLines for terminalLogMetadata';
|
|
1837
|
+
END IF;
|
|
1838
|
+
IF observation.payload_json ? 'terminalLogMetadata'
|
|
1839
|
+
AND (jsonb_typeof(observation.payload_json->'terminalLogMetadata') IS DISTINCT FROM 'array'
|
|
1840
|
+
OR jsonb_array_length(observation.payload_json->'terminalLogMetadata') <> jsonb_array_length(observation.payload_json->'terminalLogLines')) THEN
|
|
1841
|
+
RAISE EXCEPTION 'Search projection requires terminalLogMetadata aligned to terminalLogLines';
|
|
1842
|
+
END IF;
|
|
1465
1843
|
IF observation.event_type IN (${RUN_EVENT_TERMINAL_LOG_CARRIER_SQL_LIST})
|
|
1466
1844
|
AND observation.payload_json ? 'terminalLogLines'
|
|
1467
1845
|
AND NOT coalesce((envelope_attempt #>> '{}') ~ '^[1-9][0-9]{0,5}$', false) THEN
|
|
@@ -1494,6 +1872,7 @@ export async function attachRuntimeEventSearchTriggers(
|
|
|
1494
1872
|
AND event.kind = ${RUN_EVENT_KIND_SQL_LITERAL.logAppended}
|
|
1495
1873
|
AND (observation.payload_json->>'producerAttempt' IS NULL
|
|
1496
1874
|
OR event.search_doc->>'producerAttempt' = observation.payload_json->>'producerAttempt')
|
|
1875
|
+
AND NOT (event.search_doc ? 'entryId')
|
|
1497
1876
|
AND event.log_seq IS NOT NULL AND (event.log_text_digest IS NOT NULL OR event.log_body IS NOT NULL)
|
|
1498
1877
|
ORDER BY event.log_seq DESC
|
|
1499
1878
|
LIMIT 256
|
|
@@ -1503,7 +1882,7 @@ export async function attachRuntimeEventSearchTriggers(
|
|
|
1503
1882
|
FROM runtime_observability.events AS event
|
|
1504
1883
|
WHERE event.runtime_scope = ${quoteLiteral(scope)}
|
|
1505
1884
|
AND event.org_id = run_org AND event.run_id = observation.run_id
|
|
1506
|
-
AND event.kind = ${RUN_EVENT_KIND_SQL_LITERAL.logAppended} AND (event.log_text_digest IS NOT NULL OR event.log_body IS NOT NULL)
|
|
1885
|
+
AND event.kind = ${RUN_EVENT_KIND_SQL_LITERAL.logAppended} AND NOT (event.search_doc ? 'entryId') AND (event.log_text_digest IS NOT NULL OR event.log_body IS NOT NULL)
|
|
1507
1886
|
AND event.event_id = ANY(fresh_event_ids)
|
|
1508
1887
|
AND (observation.payload_json->>'producerAttempt' IS NULL
|
|
1509
1888
|
OR event.search_doc->>'producerAttempt' = observation.payload_json->>'producerAttempt')
|
|
@@ -1553,7 +1932,9 @@ export async function attachRuntimeEventSearchTriggers(
|
|
|
1553
1932
|
RAISE EXCEPTION 'Search projection terminal log context has an unsupported key or value';
|
|
1554
1933
|
END IF;
|
|
1555
1934
|
END IF;
|
|
1556
|
-
|
|
1935
|
+
line_metadata := ${schema}.run_log_metadata(line #>> '{}', observation.payload_json->'terminalLogMetadata'->((line_ordinal - 1)::int), observation.created_at);
|
|
1936
|
+
line_at := to_timestamp((line_metadata->>'occurredAt')::double precision / 1000);
|
|
1937
|
+
line_doc := (line_metadata - 'occurredAt') || jsonb_strip_nulls(jsonb_build_object(
|
|
1557
1938
|
'kind', ${RUN_EVENT_KIND_SQL_LITERAL.logAppended}, 'source', 'terminal',
|
|
1558
1939
|
'producerAttempt', observation.payload_json->'producerAttempt'
|
|
1559
1940
|
));
|
|
@@ -1569,11 +1950,11 @@ export async function attachRuntimeEventSearchTriggers(
|
|
|
1569
1950
|
CASE WHEN canonical_events THEN nextval('runtime_observability.runtime_event_order_seq') END,
|
|
1570
1951
|
CASE WHEN canonical_events THEN jsonb_strip_nulls(jsonb_build_object(
|
|
1571
1952
|
'type', ${RUN_EVENT_KIND_SQL_LITERAL.logAppended}, 'runId', observation.run_id,
|
|
1572
|
-
'occurredAt',
|
|
1953
|
+
'occurredAt', line_metadata->'occurredAt', 'level', line_metadata->'level', 'stepId', line_metadata->'stepId',
|
|
1573
1954
|
'source', 'terminal', 'producerAttempt', observation.payload_json->'producerAttempt',
|
|
1574
1955
|
'lineOrdinal', line_ordinal, 'context', line_context
|
|
1575
1956
|
)) END,
|
|
1576
|
-
|
|
1957
|
+
line_at, observation.run_id, play, ${RUN_EVENT_KIND_SQL_LITERAL.logAppended}, line_metadata->>'level',
|
|
1577
1958
|
CASE WHEN compact_logs THEN NULL ELSE left(regexp_replace(line #>> '{}',
|
|
1578
1959
|
'^' || chr(1) || 'prov:[^' || chr(1) || ']*' || chr(1), ''), 2048) END,
|
|
1579
1960
|
line #>> '{}',
|
|
@@ -1720,7 +2101,7 @@ export async function attachRuntimeEventSearchTriggers(
|
|
|
1720
2101
|
LANGUAGE sql IMMUTABLE SET search_path = pg_catalog AS $function$
|
|
1721
2102
|
WITH envelope AS (
|
|
1722
2103
|
SELECT CASE WHEN jsonb_typeof(value) = 'object' THEN
|
|
1723
|
-
(value - 'logContexts') || CASE WHEN value ? 'logs'
|
|
2104
|
+
(value - 'logContexts' - 'logMetadata' - 'logIds') || CASE WHEN value ? 'logs'
|
|
1724
2105
|
THEN jsonb_build_object('logs', '[]'::jsonb) ELSE '{}'::jsonb END
|
|
1725
2106
|
ELSE value END AS result
|
|
1726
2107
|
)
|
|
@@ -1730,50 +2111,150 @@ export async function attachRuntimeEventSearchTriggers(
|
|
|
1730
2111
|
$function$
|
|
1731
2112
|
`);
|
|
1732
2113
|
await client.query(`
|
|
1733
|
-
CREATE OR REPLACE FUNCTION ${schema}.
|
|
2114
|
+
CREATE OR REPLACE FUNCTION ${schema}.compact_runtime_outbox_payload(observation ${schema}.outbox) RETURNS jsonb
|
|
1734
2115
|
LANGUAGE plpgsql SECURITY DEFINER SET search_path = pg_catalog AS $function$
|
|
1735
2116
|
DECLARE compact_logs boolean; canonical_events boolean;
|
|
1736
2117
|
BEGIN
|
|
1737
|
-
-- Project the original validated observation before replacing its
|
|
1738
|
-
-- transport body. Failure rolls back the outbox and canonical rows.
|
|
1739
|
-
PERFORM ${schema}.project_outbox_event(NEW);
|
|
1740
2118
|
SELECT run_log_store = 'planet_scale', run_event_store = 'planet_scale'
|
|
1741
2119
|
INTO compact_logs, canonical_events
|
|
1742
|
-
FROM ${schema}.runs WHERE run_id =
|
|
2120
|
+
FROM ${schema}.runs WHERE run_id = observation.run_id;
|
|
1743
2121
|
IF compact_logs OR canonical_events THEN
|
|
1744
|
-
|
|
1745
|
-
IF
|
|
1746
|
-
|
|
1747
|
-
${schema}.without_log_events(
|
|
2122
|
+
observation.payload_json := observation.payload_json - 'terminalLogLines' - 'terminalLogContexts' - 'terminalLogIds' - 'terminalLogMetadata';
|
|
2123
|
+
IF observation.event_type = ${RUN_SCHEDULER_OUTBOX_EVENT_TYPE_SQL_LITERAL.runEvents} THEN
|
|
2124
|
+
observation.payload_json := jsonb_set(observation.payload_json, '{events}',
|
|
2125
|
+
${schema}.without_log_events(observation.payload_json->'events'))
|
|
1748
2126
|
|| jsonb_build_object('logBodyStore', 'planet_scale');
|
|
1749
2127
|
END IF;
|
|
1750
|
-
IF
|
|
1751
|
-
|
|
1752
|
-
${schema}.without_log_events(
|
|
2128
|
+
IF observation.payload_json ? 'terminalProgressEvents' THEN
|
|
2129
|
+
observation.payload_json := jsonb_set(observation.payload_json, '{terminalProgressEvents}',
|
|
2130
|
+
${schema}.without_log_events(observation.payload_json->'terminalProgressEvents'));
|
|
1753
2131
|
END IF;
|
|
1754
|
-
IF
|
|
1755
|
-
|
|
1756
|
-
${schema}.compact_runner_log_envelope(
|
|
2132
|
+
IF observation.event_type = ${RUN_EVENT_KIND_SQL_LITERAL.runFailed} AND observation.payload_json ? 'result' THEN
|
|
2133
|
+
observation.payload_json := jsonb_set(observation.payload_json, '{result}',
|
|
2134
|
+
${schema}.compact_runner_log_envelope(observation.payload_json->'result'));
|
|
1757
2135
|
END IF;
|
|
1758
|
-
IF
|
|
1759
|
-
|
|
1760
|
-
${schema}.without_runtime_log_tail(
|
|
2136
|
+
IF observation.event_type = ${RUN_EVENT_KIND_SQL_LITERAL.runCompleted} AND observation.payload_json ? 'output' THEN
|
|
2137
|
+
observation.payload_json := jsonb_set(observation.payload_json, '{output}',
|
|
2138
|
+
${schema}.without_runtime_log_tail(observation.payload_json->'output'));
|
|
1761
2139
|
END IF;
|
|
1762
2140
|
END IF;
|
|
1763
2141
|
IF canonical_events THEN
|
|
2142
|
+
IF observation.event_type = ${RUN_SCHEDULER_OUTBOX_EVENT_TYPE_SQL_LITERAL.runEvents} THEN
|
|
2143
|
+
observation.payload_json := jsonb_set(observation.payload_json, '{events}',
|
|
2144
|
+
${schema}.convex_projection_events(observation.payload_json->'events'));
|
|
2145
|
+
END IF;
|
|
2146
|
+
IF observation.payload_json ? 'terminalProgressEvents' THEN
|
|
2147
|
+
observation.payload_json := jsonb_set(observation.payload_json, '{terminalProgressEvents}',
|
|
2148
|
+
${schema}.convex_projection_events(observation.payload_json->'terminalProgressEvents'));
|
|
2149
|
+
END IF;
|
|
2150
|
+
END IF;
|
|
2151
|
+
RETURN observation.payload_json - '${RUNTIME_EVENT_DEFERRED_PROJECTION_FIELD}' - '_runtimeSearchRunClosed';
|
|
2152
|
+
END
|
|
2153
|
+
$function$
|
|
2154
|
+
`);
|
|
2155
|
+
await client.query(`
|
|
2156
|
+
CREATE OR REPLACE FUNCTION ${schema}.search_outbox_event() RETURNS trigger
|
|
2157
|
+
LANGUAGE plpgsql SECURITY DEFINER SET search_path = pg_catalog AS $function$
|
|
2158
|
+
DECLARE run_closed boolean; search_payload jsonb;
|
|
2159
|
+
BEGIN
|
|
2160
|
+
IF NEW.payload_json->'${RUNTIME_EVENT_SEARCH_ONLY_FIELD}' = 'true'::jsonb THEN RETURN NEW; END IF;
|
|
2161
|
+
IF ${schema}.defer_runtime_event_projection()
|
|
2162
|
+
AND NEW.event_type <> ${RUN_SCHEDULER_OUTBOX_EVENT_TYPE_SQL_LITERAL.runCreated} THEN
|
|
1764
2163
|
IF NEW.event_type = ${RUN_SCHEDULER_OUTBOX_EVENT_TYPE_SQL_LITERAL.runEvents} THEN
|
|
1765
|
-
|
|
1766
|
-
|
|
2164
|
+
-- One cheap Run fence captures whether ingress preceded terminal
|
|
2165
|
+
-- closure. No indexed log lookup or search mutex runs under it.
|
|
2166
|
+
SELECT status IN ('completed', 'failed', 'cancelled') INTO run_closed
|
|
2167
|
+
FROM ${schema}.runs WHERE run_id = NEW.run_id FOR SHARE;
|
|
2168
|
+
NEW.created_at := GREATEST(NEW.created_at, clock_timestamp(),
|
|
2169
|
+
coalesce((SELECT max(created_at) + interval '1 microsecond'
|
|
2170
|
+
FROM ${schema}.outbox WHERE run_id = NEW.run_id), clock_timestamp()));
|
|
2171
|
+
ELSE
|
|
2172
|
+
SELECT status IN ('completed', 'failed', 'cancelled') INTO run_closed
|
|
2173
|
+
FROM ${schema}.runs WHERE run_id = NEW.run_id;
|
|
1767
2174
|
END IF;
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
2175
|
+
NEW.payload_json := NEW.payload_json || jsonb_build_object(
|
|
2176
|
+
'${RUNTIME_EVENT_DEFERRED_PROJECTION_FIELD}', true,
|
|
2177
|
+
'_runtimeSearchRunClosed', coalesce(run_closed, false));
|
|
2178
|
+
IF ${schema}.independent_runtime_event_projection() THEN
|
|
2179
|
+
IF NEW.event_type IN (${RUN_SCHEDULER_OUTBOX_EVENT_TYPE_SQL_LITERAL.receiptEvent}, ${RUN_SCHEDULER_OUTBOX_EVENT_TYPE_SQL_LITERAL.runnerLogs}) OR (NEW.event_type = ${RUN_SCHEDULER_OUTBOX_EVENT_TYPE_SQL_LITERAL.runEvents} AND jsonb_typeof(NEW.payload_json->'events') = 'array' AND NOT EXISTS (SELECT 1 FROM jsonb_array_elements(NEW.payload_json->'events') item WHERE item->>'type' IS DISTINCT FROM ${RUN_EVENT_KIND_SQL_LITERAL.logAppended})) THEN
|
|
2180
|
+
NEW.payload_json := NEW.payload_json || jsonb_build_object('${RUNTIME_EVENT_SEARCH_ONLY_FIELD}', true);
|
|
2181
|
+
ELSE
|
|
2182
|
+
search_payload := NEW.payload_json || jsonb_build_object('${RUNTIME_EVENT_SEARCH_ONLY_FIELD}', true, '_runtimeSearchEventId', NEW.event_id);
|
|
2183
|
+
INSERT INTO ${schema}.outbox(event_id, run_id, event_type, payload_json, created_at)
|
|
2184
|
+
VALUES ('search:' || NEW.event_id, NEW.run_id, NEW.event_type, search_payload, NEW.created_at)
|
|
2185
|
+
ON CONFLICT (event_id) DO NOTHING;
|
|
2186
|
+
NEW.payload_json := ${schema}.compact_runtime_outbox_payload(NEW);
|
|
2187
|
+
END IF;
|
|
1771
2188
|
END IF;
|
|
2189
|
+
RETURN NEW;
|
|
2190
|
+
END IF;
|
|
2191
|
+
-- Legacy writers retain their atomic projection until explicit cutover.
|
|
2192
|
+
NEW.payload_json := NEW.payload_json - '${RUNTIME_EVENT_DEFERRED_PROJECTION_FIELD}' - '_runtimeSearchRunClosed';
|
|
2193
|
+
PERFORM ${schema}.project_outbox_event(NEW);
|
|
2194
|
+
NEW.payload_json := ${schema}.compact_runtime_outbox_payload(NEW);
|
|
2195
|
+
-- The search rows above are committed atomically with this outbox row.
|
|
2196
|
+
-- When compaction leaves no ledger event to project, settle the no-op
|
|
2197
|
+
-- receipt here so a delayed log-search fact cannot hold up run terminal
|
|
2198
|
+
-- projection behind the scheduler outbox.
|
|
2199
|
+
IF NEW.event_type = ${RUN_SCHEDULER_OUTBOX_EVENT_TYPE_SQL_LITERAL.runEvents}
|
|
2200
|
+
AND NEW.payload_json->>'logBodyStore' = 'planet_scale'
|
|
2201
|
+
AND jsonb_typeof(NEW.payload_json->'events') = 'array'
|
|
2202
|
+
AND jsonb_array_length(NEW.payload_json->'events') = 0 THEN
|
|
2203
|
+
NEW.delivered_at := coalesce(NEW.delivered_at, clock_timestamp());
|
|
1772
2204
|
END IF;
|
|
1773
2205
|
RETURN NEW;
|
|
1774
2206
|
END
|
|
1775
2207
|
$function$
|
|
1776
2208
|
`);
|
|
2209
|
+
await client.query(`
|
|
2210
|
+
CREATE OR REPLACE FUNCTION ${schema}.guard_runtime_outbox_ack() RETURNS trigger
|
|
2211
|
+
LANGUAGE plpgsql SET search_path = pg_catalog AS $function$
|
|
2212
|
+
BEGIN
|
|
2213
|
+
IF OLD.delivered_at IS NULL AND NEW.delivered_at IS NOT NULL
|
|
2214
|
+
AND OLD.payload_json->'${RUNTIME_EVENT_DEFERRED_PROJECTION_FIELD}' = 'true'::jsonb THEN
|
|
2215
|
+
RAISE EXCEPTION 'Deferred outbox input must be projected before acknowledgement' USING ERRCODE = '55000';
|
|
2216
|
+
END IF;
|
|
2217
|
+
RETURN NEW;
|
|
2218
|
+
END
|
|
2219
|
+
$function$
|
|
2220
|
+
`);
|
|
2221
|
+
await client.query(`DO $create$ BEGIN
|
|
2222
|
+
IF NOT EXISTS (SELECT 1 FROM pg_trigger WHERE tgrelid = '${schema}.outbox'::regclass
|
|
2223
|
+
AND tgname = 'runtime_outbox_projection_ack_v1') THEN
|
|
2224
|
+
CREATE TRIGGER runtime_outbox_projection_ack_v1 BEFORE UPDATE OF delivered_at ON ${schema}.outbox
|
|
2225
|
+
FOR EACH ROW EXECUTE FUNCTION ${schema}.guard_runtime_outbox_ack();
|
|
2226
|
+
END IF;
|
|
2227
|
+
END $create$`);
|
|
2228
|
+
await client.query(`
|
|
2229
|
+
CREATE OR REPLACE FUNCTION ${schema}.materialize_runtime_outbox_event(eid text, owner text) RETURNS jsonb
|
|
2230
|
+
LANGUAGE plpgsql SECURITY DEFINER SET search_path = pg_catalog AS $function$
|
|
2231
|
+
DECLARE observation ${schema}.outbox; projection_observation ${schema}.outbox;
|
|
2232
|
+
BEGIN
|
|
2233
|
+
SELECT * INTO observation FROM ${schema}.outbox
|
|
2234
|
+
WHERE event_id = eid AND lease_owner = owner AND lease_expires_at > clock_timestamp()
|
|
2235
|
+
AND delivered_at IS NULL AND failed_at IS NULL;
|
|
2236
|
+
IF NOT FOUND THEN
|
|
2237
|
+
RAISE EXCEPTION 'Deferred event projection lease is not current' USING ERRCODE = '40001';
|
|
2238
|
+
END IF;
|
|
2239
|
+
IF observation.payload_json->'${RUNTIME_EVENT_DEFERRED_PROJECTION_FIELD}' = 'true'::jsonb THEN
|
|
2240
|
+
projection_observation := observation;
|
|
2241
|
+
projection_observation.event_id := coalesce(observation.payload_json->>'_runtimeSearchEventId', observation.event_id);
|
|
2242
|
+
PERFORM ${schema}.project_outbox_event(projection_observation);
|
|
2243
|
+
observation.payload_json := ${schema}.compact_runtime_outbox_payload(observation);
|
|
2244
|
+
-- Project against immutable input without locking the outbox row.
|
|
2245
|
+
-- Cancellation may transfer its lease while indexing is in flight.
|
|
2246
|
+
-- This final fence rolls the journal back if ownership changed.
|
|
2247
|
+
UPDATE ${schema}.outbox SET payload_json = observation.payload_json
|
|
2248
|
+
WHERE event_id = eid AND lease_owner = owner AND lease_expires_at > clock_timestamp()
|
|
2249
|
+
AND delivered_at IS NULL AND failed_at IS NULL;
|
|
2250
|
+
IF NOT FOUND THEN
|
|
2251
|
+
RAISE EXCEPTION 'Deferred event projection lease is not current' USING ERRCODE = '40001';
|
|
2252
|
+
END IF;
|
|
2253
|
+
END IF;
|
|
2254
|
+
RETURN observation.payload_json;
|
|
2255
|
+
END
|
|
2256
|
+
$function$
|
|
2257
|
+
`);
|
|
1777
2258
|
await client.query(`
|
|
1778
2259
|
CREATE OR REPLACE FUNCTION ${schema}.accept_runner_log_payload() RETURNS trigger
|
|
1779
2260
|
LANGUAGE plpgsql SECURITY DEFINER SET search_path = pg_catalog AS $function$
|
|
@@ -1803,7 +2284,16 @@ export async function attachRuntimeEventSearchTriggers(
|
|
|
1803
2284
|
THEN NEW.runner_log_payload_json->'terminalProgressEvents'
|
|
1804
2285
|
ELSE '[]'::jsonb END) WITH ORDINALITY AS progress(event, position)
|
|
1805
2286
|
));
|
|
1806
|
-
|
|
2287
|
+
IF ${schema}.defer_runtime_event_projection() THEN
|
|
2288
|
+
INSERT INTO ${schema}.outbox (event_id, run_id, event_type, payload_json, created_at)
|
|
2289
|
+
VALUES (observation.event_id, observation.run_id, observation.event_type,
|
|
2290
|
+
observation.payload_json,
|
|
2291
|
+
GREATEST(clock_timestamp(), coalesce((SELECT max(created_at) + interval '1 microsecond'
|
|
2292
|
+
FROM ${schema}.outbox WHERE run_id = NEW.run_id), clock_timestamp())))
|
|
2293
|
+
ON CONFLICT (event_id) DO NOTHING;
|
|
2294
|
+
ELSE
|
|
2295
|
+
PERFORM ${schema}.project_outbox_event(observation);
|
|
2296
|
+
END IF;
|
|
1807
2297
|
NEW.runner_result_json := ${schema}.compact_runner_log_envelope(NEW.runner_result_json);
|
|
1808
2298
|
IF jsonb_array_length(observation.payload_json->'recoveredRuntimeDiagnostics') > 0
|
|
1809
2299
|
AND (NOT (NEW.runner_result_json ? 'runtimeDiagnostics')
|
|
@@ -1888,10 +2378,16 @@ export async function attachRuntimeEventSearchTriggers(
|
|
|
1888
2378
|
END IF;
|
|
1889
2379
|
END $create$`);
|
|
1890
2380
|
await client.query(`REVOKE ALL ON FUNCTION ${schema}.project_outbox_event(${schema}.outbox),
|
|
2381
|
+
${schema}.project_origin_log_batch(${schema}.outbox, jsonb, text, text, boolean, boolean, timestamptz, jsonb, boolean),
|
|
2382
|
+
${schema}.independent_runtime_event_projection(),
|
|
2383
|
+
${schema}.pin_runtime_log_identity(),
|
|
2384
|
+
${schema}.materialize_runtime_outbox_event(text, text),
|
|
2385
|
+
${schema}.guard_runtime_outbox_ack(),
|
|
2386
|
+
${schema}.compact_runtime_outbox_payload(${schema}.outbox),
|
|
1891
2387
|
${schema}.accept_runner_log_payload(), ${schema}.search_outbox_event(),
|
|
1892
2388
|
${schema}.compact_run_log_result(), ${schema}.without_log_events(jsonb),
|
|
1893
2389
|
${schema}.convex_projection_event(jsonb), ${schema}.convex_projection_events(jsonb),
|
|
1894
|
-
${schema}.compact_runner_log_envelope(jsonb), ${schema}.without_runtime_log_tail(jsonb)
|
|
2390
|
+
${schema}.run_log_metadata(text, jsonb, timestamptz), ${schema}.compact_runner_log_envelope(jsonb), ${schema}.without_runtime_log_tail(jsonb)
|
|
1895
2391
|
FROM PUBLIC`);
|
|
1896
2392
|
// Publish readiness last, after all acceptance/compaction functions exist.
|
|
1897
2393
|
await client.query(
|
|
@@ -1905,7 +2401,11 @@ export async function hasRuntimeEventSearchShape(
|
|
|
1905
2401
|
): Promise<boolean> {
|
|
1906
2402
|
const schema = quoteIdent(requiredScope(runtimeScope));
|
|
1907
2403
|
const result = await client.query<{ ready: boolean }>(
|
|
1908
|
-
`SELECT
|
|
2404
|
+
`SELECT to_regprocedure('${schema}.materialize_runtime_outbox_event(text,text)') IS NOT NULL
|
|
2405
|
+
AND EXISTS (SELECT 1 FROM pg_trigger WHERE tgrelid = to_regclass('${schema}.outbox')
|
|
2406
|
+
AND tgname = 'runtime_outbox_projection_ack_v1' AND tgenabled <> 'D')
|
|
2407
|
+
AND to_regprocedure('${schema}.defer_runtime_event_projection()') IS NOT NULL
|
|
2408
|
+
AND to_regclass('runtime_observability.events') IS NOT NULL
|
|
1909
2409
|
AND to_regclass('runtime_observability.run_log_cursors') IS NOT NULL
|
|
1910
2410
|
AND EXISTS (SELECT 1 FROM pg_attribute
|
|
1911
2411
|
WHERE attrelid = to_regclass('runtime_observability.events')
|
|
@@ -2345,6 +2845,7 @@ export async function pruneRuntimeEvents(
|
|
|
2345
2845
|
search_doc = jsonb_strip_nulls(jsonb_build_object(
|
|
2346
2846
|
'kind', ${RUN_EVENT_KIND_SQL_LITERAL.logAppended}, 'source', event.search_doc->'source',
|
|
2347
2847
|
'producerAttempt', event.search_doc->'producerAttempt',
|
|
2848
|
+
'entryId', event.search_doc->'entryId',
|
|
2348
2849
|
'logPositions', event.search_doc->'logPositions',
|
|
2349
2850
|
'logContentDigest', event.search_doc->'logContentDigest'))
|
|
2350
2851
|
FROM candidates WHERE event.ctid = candidates.ctid AND event.kind = ${RUN_EVENT_KIND_SQL_LITERAL.logAppended}
|