deepline 0.3.275 → 0.3.277
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 -8
- package/dist/bundling-sources/sdk/src/release.ts +1 -1
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres.ts +65 -12
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/runtime-event-search.ts +147 -106
- package/dist/cli/index.js +92 -232
- package/dist/cli/index.mjs +92 -232
- package/dist/{client-CbNvJLgV.d.mts → client-C0EhEIQO.d.mts} +8 -8
- package/dist/{client-5zY7LsId.d.ts → client-HxPrGPbv.d.ts} +8 -8
- package/dist/index.d.mts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/index.mjs +1 -1
- package/dist/install-integrity.json +2 -2
- package/dist/plays/run-request.d.mts +1 -1
- package/dist/plays/run-request.d.ts +1 -1
- 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
|
@@ -841,7 +841,7 @@ export type RunsLogsResult = {
|
|
|
841
841
|
logsTruncated?: boolean;
|
|
842
842
|
};
|
|
843
843
|
|
|
844
|
-
/**
|
|
844
|
+
/** Search existing events in your organization, newest first. Discover kinds and filters with `deepline runs logs --help`. */
|
|
845
845
|
export type RunsEventSearchOptions = {
|
|
846
846
|
/** Inclusive UTC timestamp; defaults to one hour before the request. */
|
|
847
847
|
since?: string;
|
|
@@ -853,9 +853,9 @@ export type RunsEventSearchOptions = {
|
|
|
853
853
|
playName?: string;
|
|
854
854
|
/** Exact dotted event kind, such as `step.failed`. The catalog is discoverable with `runs logs --help`; future stored kinds are accepted too. */
|
|
855
855
|
kind?: string;
|
|
856
|
-
/**
|
|
856
|
+
/** JSONPath equality joined with `&&`, at most eight predicates and 512 bytes. See `runs logs --help` for fields and examples. Missing fields do not match; status is not inferred from kind. HTTP `jsonpath`, CLI `--where`. */
|
|
857
857
|
jsonpath?: string;
|
|
858
|
-
/**
|
|
858
|
+
/** Include available `eventPayload` details and receipt `payload` results; maximum ten events and 16 MiB. HTTP `payloads=true`, CLI `--payloads`. */
|
|
859
859
|
includePayloads?: boolean;
|
|
860
860
|
/** Page size: default 100, maximum 200; with payloads, default and maximum ten. */
|
|
861
861
|
limit?: number;
|
|
@@ -865,7 +865,7 @@ export type RunsEventSearchOptions = {
|
|
|
865
865
|
runtime?: PlayRuntimeSelection;
|
|
866
866
|
};
|
|
867
867
|
|
|
868
|
-
/** One
|
|
868
|
+
/** One matching event. Optional fields and details are present only when recorded. */
|
|
869
869
|
export type RunsEventSearchEntry = {
|
|
870
870
|
/** Stable event identity; also breaks ties in newest-first pagination. */
|
|
871
871
|
eventId: string;
|
|
@@ -877,15 +877,15 @@ export type RunsEventSearchEntry = {
|
|
|
877
877
|
runId: string;
|
|
878
878
|
/** Recorded Play name. */
|
|
879
879
|
playName: string;
|
|
880
|
-
/** Exact
|
|
880
|
+
/** Exact dotted event kind, such as `step.failed`. */
|
|
881
881
|
kind: string;
|
|
882
882
|
/** Recorded log severity, or null; not a JSONPath filter field. */
|
|
883
883
|
level: string | null;
|
|
884
884
|
/** Log message preview of at most 2,048 characters, or null. Use text logs/export for the full retained line. */
|
|
885
885
|
message: string | null;
|
|
886
|
-
/**
|
|
886
|
+
/** Recorded filter fields. SDK/API keep this nested; the CLI places these fields directly on each event. */
|
|
887
887
|
searchDoc: Record<string, unknown>;
|
|
888
|
-
/**
|
|
888
|
+
/** Available event details when requested with `includePayloads`; dataset events contain metadata, not dataset rows. */
|
|
889
889
|
eventPayload?: Record<string, unknown>;
|
|
890
890
|
/** Retained work result for a receipt match when payloads are requested; null if unavailable. Other events need not carry this field. */
|
|
891
891
|
payload?: RunsEventPayload | null;
|
|
@@ -903,7 +903,7 @@ export type RunsEventSearchResult = {
|
|
|
903
903
|
until: string;
|
|
904
904
|
};
|
|
905
905
|
|
|
906
|
-
/**
|
|
906
|
+
/** Saved work result returned for a receipt event. Reading it does not execute the work again. */
|
|
907
907
|
export type RunsEventPayload = {
|
|
908
908
|
/** Recorded terminal work outcome. */
|
|
909
909
|
status: 'completed' | 'failed' | 'skipped';
|
|
@@ -200,7 +200,7 @@ export const SDK_RELEASE = {
|
|
|
200
200
|
// getters keep their established compatibility behavior.
|
|
201
201
|
// 0.3.1 deprecated the legacy `deepline enrich` command in favor of Plays.
|
|
202
202
|
// 0.3.90 is the first deliberately versioned SDK release for API v3.
|
|
203
|
-
version: '0.3.
|
|
203
|
+
version: '0.3.277',
|
|
204
204
|
updateSummary:
|
|
205
205
|
'Play grep now returns concise results with the matched fields and terms. Automatic CLI updates are enabled by default; use `deepline settings autoupdate off` to opt out, `deepline settings autoupdate on` to re-enable updates, or `deepline settings autoupdate pin <version>` to hold an exact release.',
|
|
206
206
|
packageCapabilities: {
|
|
@@ -17391,6 +17391,22 @@ function runLedgerOrderSql(alias: string): string {
|
|
|
17391
17391
|
return `CASE ${alias}.event_type WHEN ${RUN_EVENT_KIND_SQL_LITERAL.runCreated} THEN 0 WHEN ${RUN_EVENT_KIND_SQL_LITERAL.runStarted} THEN 1 ELSE 2 END, ${alias}.created_at, ${alias}.event_id`;
|
|
17392
17392
|
}
|
|
17393
17393
|
|
|
17394
|
+
/** Prioritize a terminal's causal head without skipping its predecessors. */
|
|
17395
|
+
function runLedgerTerminalPendingSql(
|
|
17396
|
+
alias: string,
|
|
17397
|
+
options?: Pick<PostgresSchedulerOptions, 'schema'>,
|
|
17398
|
+
): string {
|
|
17399
|
+
return `EXISTS (
|
|
17400
|
+
SELECT 1 FROM ${tableName('outbox', options)} AS terminal_event
|
|
17401
|
+
WHERE terminal_event.event_id = 'run-terminal:' || ${alias}.run_id
|
|
17402
|
+
AND terminal_event.run_id = ${alias}.run_id
|
|
17403
|
+
AND terminal_event.event_type IN (${RUN_EVENT_KIND_SQL_LITERAL.runCompleted}, ${RUN_EVENT_KIND_SQL_LITERAL.runFailed}, ${RUN_EVENT_KIND_SQL_LITERAL.runCancelled})
|
|
17404
|
+
AND terminal_event.delivered_at IS NULL
|
|
17405
|
+
AND terminal_event.failed_at IS NULL
|
|
17406
|
+
AND coalesce(terminal_event.next_attempt_at, terminal_event.created_at) <= now()
|
|
17407
|
+
)`;
|
|
17408
|
+
}
|
|
17409
|
+
|
|
17394
17410
|
/**
|
|
17395
17411
|
* Claim a causal prefix, preserving the v33 adapter while the additive batch
|
|
17396
17412
|
* marker is absent. An enclosing transaction uses a savepoint so the missing
|
|
@@ -17463,16 +17479,14 @@ export async function readPendingPostgresSchedulerOutboxEvents(
|
|
|
17463
17479
|
-- terminal facts. Project it first so a lifecycle backlog cannot make
|
|
17464
17480
|
-- an accepted run invisible; per-run predecessor checks above retain
|
|
17465
17481
|
-- strict ordering within each run.
|
|
17466
|
-
-- A
|
|
17467
|
-
--
|
|
17468
|
-
--
|
|
17469
|
-
--
|
|
17470
|
-
-- runs' progress for seconds (live-run-observation-e2e: 4.9 s from
|
|
17471
|
-
-- commit to first claim while the projector drained continuously).
|
|
17482
|
+
-- A run with a pending terminal comes next, including its causal
|
|
17483
|
+
-- predecessors. Giving priority only to a terminal head leaves a
|
|
17484
|
+
-- finished run's earlier progress behind unrelated progress. The
|
|
17485
|
+
-- predecessor checks still forbid skipping any fact within the run.
|
|
17472
17486
|
ORDER BY
|
|
17473
17487
|
CASE
|
|
17474
17488
|
WHEN candidate.event_type = ${RUN_EVENT_KIND_SQL_LITERAL.runCreated} THEN 0
|
|
17475
|
-
WHEN candidate
|
|
17489
|
+
WHEN ${runLedgerTerminalPendingSql('candidate', options)} THEN 1
|
|
17476
17490
|
ELSE 2
|
|
17477
17491
|
END,
|
|
17478
17492
|
coalesce(candidate.next_attempt_at, candidate.created_at),
|
|
@@ -18892,6 +18906,7 @@ export async function dispatchRunLedgerOutboxToLedgerBatchOnceWithClientScope(
|
|
|
18892
18906
|
input: RunLedgerOutboxDispatchInput & { waitForRunLock?: boolean },
|
|
18893
18907
|
options?: Pick<PostgresSchedulerOptions, 'schema'>,
|
|
18894
18908
|
): Promise<PostgresSchedulerOutboxProjectionResult> {
|
|
18909
|
+
const dispatchStartedAtMs = Date.now();
|
|
18895
18910
|
const leaseOwner =
|
|
18896
18911
|
input.leaseOwner?.trim() || `run-ledger-outbox-dispatch:${randomUUID()}`;
|
|
18897
18912
|
const events = await withClient(async (client) => {
|
|
@@ -18986,16 +19001,16 @@ export async function dispatchRunLedgerOutboxToLedgerBatchOnceWithClientScope(
|
|
|
18986
19001
|
AND active.lease_owner IS NOT NULL
|
|
18987
19002
|
AND active.lease_expires_at > now()
|
|
18988
19003
|
)
|
|
18989
|
-
-- admission_first: admission, then
|
|
18990
|
-
-- terminal
|
|
18991
|
-
--
|
|
18992
|
-
--
|
|
19004
|
+
-- admission_first: admission, then the causal head of a run with
|
|
19005
|
+
-- a pending terminal, then FIFO progress. oldest_ready keeps its
|
|
19006
|
+
-- pure age order so continuous admissions/terminals cannot starve
|
|
19007
|
+
-- ordinary progress.
|
|
18993
19008
|
ORDER BY ${
|
|
18994
19009
|
input.headPriority === 'oldest_ready'
|
|
18995
19010
|
? ''
|
|
18996
19011
|
: `CASE
|
|
18997
19012
|
WHEN head.event_type = ${RUN_EVENT_KIND_SQL_LITERAL.runCreated} THEN 0
|
|
18998
|
-
WHEN head
|
|
19013
|
+
WHEN ${runLedgerTerminalPendingSql('head', options)} THEN 1
|
|
18999
19014
|
ELSE 2
|
|
19000
19015
|
END,`
|
|
19001
19016
|
}
|
|
@@ -19035,6 +19050,10 @@ export async function dispatchRunLedgerOutboxToLedgerBatchOnceWithClientScope(
|
|
|
19035
19050
|
};
|
|
19036
19051
|
}
|
|
19037
19052
|
|
|
19053
|
+
const claimedAtMs = Date.now();
|
|
19054
|
+
let authorityReadyAtMs: number | null = null;
|
|
19055
|
+
let appendFinishedAtMs: number | null = null;
|
|
19056
|
+
let acknowledgedAtMs: number | null = null;
|
|
19038
19057
|
try {
|
|
19039
19058
|
const first = events[0]!;
|
|
19040
19059
|
const tokenAuthority = input.tokenAuthority ?? 'local_mint';
|
|
@@ -19091,6 +19110,7 @@ export async function dispatchRunLedgerOutboxToLedgerBatchOnceWithClientScope(
|
|
|
19091
19110
|
// The caller-owned admission deadline (see `signal` above) wins.
|
|
19092
19111
|
...(input.signal ? { signal: input.signal } : {}),
|
|
19093
19112
|
};
|
|
19113
|
+
authorityReadyAtMs = Date.now();
|
|
19094
19114
|
const renewDelivery = () =>
|
|
19095
19115
|
withClient((client) =>
|
|
19096
19116
|
renewPostgresRunLedgerDelivery(
|
|
@@ -19155,6 +19175,7 @@ export async function dispatchRunLedgerOutboxToLedgerBatchOnceWithClientScope(
|
|
|
19155
19175
|
});
|
|
19156
19176
|
}
|
|
19157
19177
|
}
|
|
19178
|
+
appendFinishedAtMs = Date.now();
|
|
19158
19179
|
const deliveredRows = await withClient((client) =>
|
|
19159
19180
|
markPostgresSchedulerOutboxDeliveredRows(
|
|
19160
19181
|
client,
|
|
@@ -19162,6 +19183,7 @@ export async function dispatchRunLedgerOutboxToLedgerBatchOnceWithClientScope(
|
|
|
19162
19183
|
{ ...options, leaseOwner },
|
|
19163
19184
|
),
|
|
19164
19185
|
);
|
|
19186
|
+
acknowledgedAtMs = Date.now();
|
|
19165
19187
|
const markedDelivered = deliveredRows.length;
|
|
19166
19188
|
if (input.onDelivered && deliveredRows.length > 0) {
|
|
19167
19189
|
try {
|
|
@@ -19253,6 +19275,37 @@ export async function dispatchRunLedgerOutboxToLedgerBatchOnceWithClientScope(
|
|
|
19253
19275
|
retrying: settlement.retrying.length,
|
|
19254
19276
|
retryingEventIds: settlement.retrying,
|
|
19255
19277
|
};
|
|
19278
|
+
} finally {
|
|
19279
|
+
const settledAtMs = Date.now();
|
|
19280
|
+
const headAgeAtClaimMs = Math.max(
|
|
19281
|
+
0,
|
|
19282
|
+
claimedAtMs - events[0]!.createdAt.getTime(),
|
|
19283
|
+
);
|
|
19284
|
+
if (
|
|
19285
|
+
settledAtMs - dispatchStartedAtMs >= 1_000 ||
|
|
19286
|
+
headAgeAtClaimMs >= 1_000
|
|
19287
|
+
) {
|
|
19288
|
+
console.info('[run-ledger-outbox.slow_delivery]', {
|
|
19289
|
+
runId: events[0]!.runId,
|
|
19290
|
+
headEventId: events[0]!.eventId,
|
|
19291
|
+
eventCount: events.length,
|
|
19292
|
+
headAgeAtClaimMs,
|
|
19293
|
+
claimMs: claimedAtMs - dispatchStartedAtMs,
|
|
19294
|
+
authorityMs:
|
|
19295
|
+
authorityReadyAtMs === null ? null : authorityReadyAtMs - claimedAtMs,
|
|
19296
|
+
appendMs:
|
|
19297
|
+
appendFinishedAtMs === null || authorityReadyAtMs === null
|
|
19298
|
+
? null
|
|
19299
|
+
: appendFinishedAtMs - authorityReadyAtMs,
|
|
19300
|
+
acknowledgeMs:
|
|
19301
|
+
acknowledgedAtMs === null || appendFinishedAtMs === null
|
|
19302
|
+
? null
|
|
19303
|
+
: acknowledgedAtMs - appendFinishedAtMs,
|
|
19304
|
+
continuationMs:
|
|
19305
|
+
acknowledgedAtMs === null ? null : settledAtMs - acknowledgedAtMs,
|
|
19306
|
+
durationMs: settledAtMs - dispatchStartedAtMs,
|
|
19307
|
+
});
|
|
19308
|
+
}
|
|
19256
19309
|
}
|
|
19257
19310
|
}
|
|
19258
19311
|
|
package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/runtime-event-search.ts
CHANGED
|
@@ -51,7 +51,7 @@ const RUNTIME_EVENT_LOG_POSITION_EXPRESSION = `jsonb_set(
|
|
|
51
51
|
// Migration readiness tracks this projection body independently of scheduler
|
|
52
52
|
// wire and schema versions. Bump when a changed trigger must be reinstalled.
|
|
53
53
|
export const RUNTIME_EVENT_SEARCH_OUTBOX_PROJECTION_MARKER =
|
|
54
|
-
'runtime-event-search-log-context-
|
|
54
|
+
'runtime-event-search-log-context-v13-index-first-position';
|
|
55
55
|
export const RUNTIME_EVENT_CANONICAL_RETENTION_INDEX_READY_MARKER =
|
|
56
56
|
'runtime-event-canonical-retention-v1';
|
|
57
57
|
export const RUNTIME_EVENTS_RETENTION_MS = 7 * 24 * 60 * 60 * 1000;
|
|
@@ -1018,7 +1018,6 @@ export async function attachRuntimeEventSearchTriggers(
|
|
|
1018
1018
|
doc jsonb; line_doc jsonb; line_context jsonb;
|
|
1019
1019
|
eid text; existing_digest text; digest text; assigned_seq bigint; terminal_progress_identity boolean;
|
|
1020
1020
|
log_channel text; log_position bigint; matched_event_id text; matched_doc jsonb;
|
|
1021
|
-
previous_plan_cache_mode text;
|
|
1022
1021
|
legacy_position_prefix text; position_event_ids text[];
|
|
1023
1022
|
log_alias jsonb; position_alias jsonb; envelope_attempt jsonb;
|
|
1024
1023
|
line_count bigint; fresh_event_ids text[] := ARRAY[]::text[];
|
|
@@ -1297,23 +1296,29 @@ export async function attachRuntimeEventSearchTriggers(
|
|
|
1297
1296
|
-- A transport may already be aliased onto an event whose
|
|
1298
1297
|
-- primary key belongs to another channel. Find that alias
|
|
1299
1298
|
-- independently of content so changed bytes still fail the
|
|
1300
|
-
-- immutable-position check below.
|
|
1301
|
-
--
|
|
1302
|
-
--
|
|
1303
|
-
--
|
|
1304
|
-
|
|
1305
|
-
|
|
1299
|
+
-- immutable-position check below. Materialize the scoped
|
|
1300
|
+
-- position lookup before applying run filters: otherwise
|
|
1301
|
+
-- the planner can choose the run-history index and inspect
|
|
1302
|
+
-- every old log for each incoming position, even with a
|
|
1303
|
+
-- custom plan. The indexed document already binds scope,
|
|
1304
|
+
-- org, run, attempt and channel position; retain the outer
|
|
1305
|
+
-- identity checks as defense in depth.
|
|
1306
|
+
WITH position_matches AS MATERIALIZED (
|
|
1307
|
+
SELECT event.event_id, event.source_digest, event.search_doc,
|
|
1308
|
+
event.runtime_scope, event.org_id, event.run_id, event.log_seq
|
|
1309
|
+
FROM runtime_observability.events AS event
|
|
1310
|
+
WHERE event.kind = ${RUN_EVENT_KIND_SQL_LITERAL.logAppended}
|
|
1311
|
+
AND event.search_doc ? 'producerAttempt'
|
|
1312
|
+
AND event.search_doc ? 'logPositions'
|
|
1313
|
+
AND (${RUNTIME_EVENT_LOG_POSITION_EXPRESSION.replaceAll('search_doc', 'event.search_doc')}) @> position_alias
|
|
1314
|
+
)
|
|
1306
1315
|
SELECT event.event_id, event.source_digest, event.search_doc
|
|
1307
1316
|
INTO matched_event_id, existing_digest, matched_doc
|
|
1308
|
-
FROM
|
|
1317
|
+
FROM position_matches AS event
|
|
1309
1318
|
WHERE event.runtime_scope = ${quoteLiteral(scope)} AND event.org_id = run_org
|
|
1310
|
-
AND event.run_id = observation.run_id
|
|
1311
|
-
AND event.search_doc ? 'producerAttempt'
|
|
1312
|
-
AND event.search_doc ? 'logPositions'
|
|
1313
|
-
AND (${RUNTIME_EVENT_LOG_POSITION_EXPRESSION.replaceAll('search_doc', 'event.search_doc')}) @> position_alias
|
|
1319
|
+
AND event.run_id = observation.run_id
|
|
1314
1320
|
ORDER BY event.log_seq NULLS LAST, event.event_id
|
|
1315
1321
|
LIMIT 1;
|
|
1316
|
-
PERFORM set_config('plan_cache_mode', previous_plan_cache_mode, true);
|
|
1317
1322
|
END IF;
|
|
1318
1323
|
IF matched_event_id IS NOT NULL THEN
|
|
1319
1324
|
IF existing_digest IS DISTINCT FROM digest THEN
|
|
@@ -2015,20 +2020,22 @@ type RuntimeEventIndexExpectation = {
|
|
|
2015
2020
|
opclasses?: string[];
|
|
2016
2021
|
};
|
|
2017
2022
|
|
|
2018
|
-
const
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
createSql: `CREATE INDEX CONCURRENTLY IF NOT EXISTS ${RUNTIME_EVENT_LOG_POSITION_INDEX}
|
|
2023
|
+
const RUNTIME_EVENT_LOG_POSITION_INDEX_SPEC: RuntimeEventIndexExpectation = {
|
|
2024
|
+
name: RUNTIME_EVENT_LOG_POSITION_INDEX,
|
|
2025
|
+
method: 'gin',
|
|
2026
|
+
opclasses: ['jsonb_path_ops'],
|
|
2027
|
+
keys: [
|
|
2028
|
+
"jsonb_set(jsonb_set(jsonb_set(jsonb_set(COALESCE(search_doc -> 'logPositions'::text, '{}'::jsonb), ARRAY['producerAttempt'::text], search_doc -> 'producerAttempt'::text, true), ARRAY['_runtimeScope:'::text || runtime_scope], 'true'::jsonb, true), ARRAY['_orgId:'::text || org_id], 'true'::jsonb, true), ARRAY['_runId:'::text || run_id], 'true'::jsonb, true)",
|
|
2029
|
+
],
|
|
2030
|
+
predicate: RUNTIME_EVENT_LOG_POSITION_PREDICATE,
|
|
2031
|
+
createSql: `CREATE INDEX CONCURRENTLY IF NOT EXISTS ${RUNTIME_EVENT_LOG_POSITION_INDEX}
|
|
2028
2032
|
ON runtime_observability.events USING gin
|
|
2029
2033
|
((${RUNTIME_EVENT_LOG_POSITION_EXPRESSION}) jsonb_path_ops)
|
|
2030
2034
|
WHERE ${RUNTIME_EVENT_LOG_POSITION_PREDICATE}`,
|
|
2031
|
-
|
|
2035
|
+
};
|
|
2036
|
+
|
|
2037
|
+
const RUNTIME_EVENT_QUERY_INDEXES: RuntimeEventIndexExpectation[] = [
|
|
2038
|
+
RUNTIME_EVENT_LOG_POSITION_INDEX_SPEC,
|
|
2032
2039
|
{
|
|
2033
2040
|
name: RUNTIME_EVENT_LOG_DEDUPE_INDEX,
|
|
2034
2041
|
keys: [
|
|
@@ -2062,92 +2069,126 @@ const RUNTIME_EVENT_QUERY_INDEXES: RuntimeEventIndexExpectation[] = [
|
|
|
2062
2069
|
},
|
|
2063
2070
|
];
|
|
2064
2071
|
|
|
2065
|
-
|
|
2066
|
-
export async function prepareRuntimeEventQueryIndexes(
|
|
2072
|
+
async function hasRuntimeEventQueryIndex(
|
|
2067
2073
|
client: PostgresSchedulerQueryClient,
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2074
|
+
expected: RuntimeEventIndexExpectation,
|
|
2075
|
+
): Promise<boolean> {
|
|
2076
|
+
type IndexShape = {
|
|
2077
|
+
valid: boolean;
|
|
2078
|
+
ready: boolean;
|
|
2079
|
+
is_unique: boolean;
|
|
2080
|
+
table_matches: boolean;
|
|
2081
|
+
key_count: number;
|
|
2082
|
+
attribute_count: number;
|
|
2083
|
+
method: string;
|
|
2084
|
+
keys: string[];
|
|
2085
|
+
opclasses: string[];
|
|
2086
|
+
predicate: string | null;
|
|
2087
|
+
};
|
|
2088
|
+
const inspect = () =>
|
|
2089
|
+
client.query<IndexShape>(
|
|
2090
|
+
`SELECT index_row.indisvalid AS valid,
|
|
2091
|
+
index_row.indisready AS ready,
|
|
2092
|
+
index_row.indisunique AS is_unique,
|
|
2093
|
+
index_row.indrelid = to_regclass('runtime_observability.events') AS table_matches,
|
|
2094
|
+
index_row.indnkeyatts AS key_count,
|
|
2095
|
+
index_row.indnatts AS attribute_count,
|
|
2096
|
+
method.amname AS method,
|
|
2097
|
+
ARRAY(
|
|
2098
|
+
SELECT pg_get_indexdef(index_row.indexrelid, key_number.position, true)
|
|
2099
|
+
FROM generate_series(1, index_row.indnkeyatts) AS key_number(position)
|
|
2100
|
+
ORDER BY key_number.position
|
|
2101
|
+
) AS keys,
|
|
2102
|
+
(
|
|
2103
|
+
SELECT coalesce(json_agg(operator_class.opcname ORDER BY classes.position), '[]'::json)
|
|
2104
|
+
FROM unnest(index_row.indclass) WITH ORDINALITY AS classes(opclass_oid, position)
|
|
2105
|
+
JOIN pg_opclass AS operator_class ON operator_class.oid = classes.opclass_oid
|
|
2106
|
+
) AS opclasses,
|
|
2107
|
+
pg_get_expr(index_row.indpred, index_row.indrelid) AS predicate
|
|
2108
|
+
FROM pg_index AS index_row
|
|
2109
|
+
JOIN pg_class AS index_class ON index_class.oid = index_row.indexrelid
|
|
2110
|
+
JOIN pg_am AS method ON method.oid = index_class.relam
|
|
2111
|
+
WHERE index_class.oid = to_regclass($1)`,
|
|
2112
|
+
[`runtime_observability.${expected.name}`],
|
|
2113
|
+
);
|
|
2114
|
+
const assertShape = (shape: IndexShape): void => {
|
|
2115
|
+
if (!shape.valid || !shape.ready) {
|
|
2116
|
+
throw new Error(
|
|
2117
|
+
`Runtime event index ${expected.name} is invalid or incomplete; inspect and repair the failed build explicitly.`,
|
|
2107
2118
|
);
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
) ||
|
|
2131
|
-
shape.predicate === null ||
|
|
2132
|
-
normalizedQueryIndexPredicate(shape.predicate) !==
|
|
2133
|
-
normalizedQueryIndexPredicate(expected.predicate)
|
|
2134
|
-
) {
|
|
2135
|
-
throw new Error(
|
|
2136
|
-
`Runtime event index ${expected.name} differs from its expected table, keys, method, uniqueness or predicate. Actual catalog shape: ${JSON.stringify(shape)}. Inspect and repair it explicitly.`,
|
|
2137
|
-
);
|
|
2138
|
-
}
|
|
2139
|
-
};
|
|
2140
|
-
|
|
2141
|
-
const existing = (await inspect()).rows[0];
|
|
2142
|
-
if (existing) assertShape(existing);
|
|
2143
|
-
await client.query(expected.createSql);
|
|
2144
|
-
const built = (await inspect()).rows[0];
|
|
2145
|
-
if (!built) {
|
|
2119
|
+
}
|
|
2120
|
+
if (
|
|
2121
|
+
!shape.table_matches ||
|
|
2122
|
+
shape.is_unique ||
|
|
2123
|
+
shape.method !== (expected.method ?? 'btree') ||
|
|
2124
|
+
(expected.opclasses !== undefined &&
|
|
2125
|
+
(shape.opclasses.length !== expected.opclasses.length ||
|
|
2126
|
+
shape.opclasses.some(
|
|
2127
|
+
(name, index) => name !== expected.opclasses![index],
|
|
2128
|
+
))) ||
|
|
2129
|
+
shape.key_count !== expected.keys.length ||
|
|
2130
|
+
shape.attribute_count !== expected.keys.length ||
|
|
2131
|
+
shape.keys.length !== expected.keys.length ||
|
|
2132
|
+
shape.keys.some(
|
|
2133
|
+
(key, index) =>
|
|
2134
|
+
normalizedRetentionPredicate(key) !==
|
|
2135
|
+
normalizedRetentionPredicate(expected.keys[index]!),
|
|
2136
|
+
) ||
|
|
2137
|
+
shape.predicate === null ||
|
|
2138
|
+
normalizedQueryIndexPredicate(shape.predicate) !==
|
|
2139
|
+
normalizedQueryIndexPredicate(expected.predicate)
|
|
2140
|
+
) {
|
|
2146
2141
|
throw new Error(
|
|
2147
|
-
`Runtime event index ${expected.name}
|
|
2142
|
+
`Runtime event index ${expected.name} differs from its expected table, keys, method, uniqueness or predicate. Actual catalog shape: ${JSON.stringify(shape)}. Inspect and repair it explicitly.`,
|
|
2148
2143
|
);
|
|
2149
2144
|
}
|
|
2150
|
-
|
|
2145
|
+
};
|
|
2146
|
+
|
|
2147
|
+
const existing = (await inspect()).rows[0];
|
|
2148
|
+
if (!existing) return false;
|
|
2149
|
+
assertShape(existing);
|
|
2150
|
+
return true;
|
|
2151
|
+
}
|
|
2152
|
+
|
|
2153
|
+
async function prepareRuntimeEventQueryIndex(
|
|
2154
|
+
client: PostgresSchedulerQueryClient,
|
|
2155
|
+
expected: RuntimeEventIndexExpectation,
|
|
2156
|
+
): Promise<void> {
|
|
2157
|
+
if (await hasRuntimeEventQueryIndex(client, expected)) return;
|
|
2158
|
+
await client.query(expected.createSql);
|
|
2159
|
+
if (!(await hasRuntimeEventQueryIndex(client, expected))) {
|
|
2160
|
+
throw new Error(
|
|
2161
|
+
`Runtime event index ${expected.name} was not found after preparation.`,
|
|
2162
|
+
);
|
|
2163
|
+
}
|
|
2164
|
+
}
|
|
2165
|
+
|
|
2166
|
+
/** Deployment-only check; does not gate requests or worker admission. */
|
|
2167
|
+
export async function hasRuntimeEventLogPositionIndex(
|
|
2168
|
+
client: PostgresSchedulerQueryClient,
|
|
2169
|
+
): Promise<boolean> {
|
|
2170
|
+
return hasRuntimeEventQueryIndex(
|
|
2171
|
+
client,
|
|
2172
|
+
RUNTIME_EVENT_LOG_POSITION_INDEX_SPEC,
|
|
2173
|
+
);
|
|
2174
|
+
}
|
|
2175
|
+
|
|
2176
|
+
/** Prepare the existing position index before replacing the projection body. */
|
|
2177
|
+
export async function prepareRuntimeEventLogPositionIndex(
|
|
2178
|
+
client: PostgresSchedulerQueryClient,
|
|
2179
|
+
): Promise<void> {
|
|
2180
|
+
await prepareRuntimeEventQueryIndex(
|
|
2181
|
+
client,
|
|
2182
|
+
RUNTIME_EVENT_LOG_POSITION_INDEX_SPEC,
|
|
2183
|
+
);
|
|
2184
|
+
}
|
|
2185
|
+
|
|
2186
|
+
/** Build only operator-owned indexes, concurrently and outside a transaction. */
|
|
2187
|
+
export async function prepareRuntimeEventQueryIndexes(
|
|
2188
|
+
client: PostgresSchedulerQueryClient,
|
|
2189
|
+
): Promise<void> {
|
|
2190
|
+
for (const expected of RUNTIME_EVENT_QUERY_INDEXES) {
|
|
2191
|
+
await prepareRuntimeEventQueryIndex(client, expected);
|
|
2151
2192
|
}
|
|
2152
2193
|
}
|
|
2153
2194
|
|