deepline 0.3.306 → 0.3.308
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/README.md +2 -1
- package/dist/bundling-sources/sdk/src/client.ts +270 -11
- package/dist/bundling-sources/sdk/src/compat.ts +27 -9
- package/dist/bundling-sources/sdk/src/index.ts +35 -0
- package/dist/bundling-sources/sdk/src/release.ts +1 -1
- package/dist/bundling-sources/shared_libs/play-runtime/batch-runtime.ts +4 -0
- package/dist/bundling-sources/shared_libs/play-runtime/batching-types.ts +3 -0
- package/dist/bundling-sources/shared_libs/play-runtime/context.ts +46 -16
- package/dist/bundling-sources/shared_libs/play-runtime/fullenrich-batching.ts +24 -1
- package/dist/bundling-sources/shared_libs/play-runtime/run-event-query.ts +872 -0
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/absurd-shared.ts +10 -5
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres.ts +4 -1
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/runtime-event-search.ts +720 -19
- package/dist/cli/auth-LWNR7TQB.js +31 -0
- package/dist/cli/{auth-WTC7LCQ6.mjs → auth-ZWX4BF5K.mjs} +4 -4
- package/dist/cli/{billing-VQM65LVP.mjs → billing-CLV5QRFQ.mjs} +4 -4
- package/dist/cli/{billing-3DYTHO32.js → billing-H24FGAKE.js} +70 -70
- package/dist/cli/{chunk-WV64SNDV.mjs → chunk-3GIUCJAT.mjs} +1 -1
- package/dist/cli/{chunk-5V56XCQN.mjs → chunk-ABMGEW3C.mjs} +1 -1
- package/dist/cli/{chunk-UT3VLSLM.mjs → chunk-ASWZTTKT.mjs} +13 -5
- package/dist/cli/{chunk-FJ3DANI4.mjs → chunk-BPD7TR27.mjs} +1 -1
- package/dist/cli/{chunk-7XORS6WQ.mjs → chunk-CXU7ME5X.mjs} +2 -2
- package/dist/cli/{chunk-RODIZCGU.js → chunk-GBFIAI4L.js} +68 -60
- package/dist/cli/{chunk-OZIFVQRY.js → chunk-GONK2N7U.js} +1 -1
- package/dist/cli/{chunk-LLGF6V6I.mjs → chunk-HGI6RGHB.mjs} +653 -5
- package/dist/cli/{chunk-FZNO6B4W.js → chunk-IFIGQWHC.js} +6 -6
- package/dist/cli/{chunk-6CRUYVU7.mjs → chunk-JUAVGRD7.mjs} +4 -4
- package/dist/cli/{chunk-SZQPJW2L.js → chunk-O7NNH76S.js} +6 -6
- package/dist/cli/{chunk-2IKIG333.js → chunk-QJBJZVG6.js} +15 -4
- package/dist/cli/{chunk-M6HCRRV2.js → chunk-QTCFSRGC.js} +4 -4
- package/dist/cli/{chunk-W7ELTWDM.js → chunk-SRZ6XDX3.js} +26 -26
- package/dist/cli/{chunk-4PSSDZQB.mjs → chunk-T3P4NRZU.mjs} +1 -1
- package/dist/cli/{chunk-PHNAFCSB.js → chunk-TH3YBIJT.js} +852 -204
- package/dist/cli/{chunk-U37SW6IK.js → chunk-UGTYNMRX.js} +48 -48
- package/dist/cli/{chunk-2KR56MPF.js → chunk-WEZVPZPE.js} +2 -2
- package/dist/cli/{chunk-UABLQKTZ.mjs → chunk-WXLOW623.mjs} +11 -0
- package/dist/cli/{chunk-QDC4XTO3.mjs → chunk-XFYVL7EM.mjs} +2 -2
- package/dist/cli/{chunk-5HS5EEFE.mjs → chunk-XSDL3NHZ.mjs} +3 -3
- package/dist/cli/{chunk-MMNZ6GBT.js → chunk-YEJMVAD3.js} +6 -6
- package/dist/cli/{chunk-WWPI4GJI.mjs → chunk-YSZSERRZ.mjs} +1 -1
- package/dist/cli/{chunk-OF7QP2XX.js → chunk-ZHHF24O5.js} +15 -15
- package/dist/cli/client-3J76AMVE.js +20 -0
- package/dist/cli/{client-DCEK34D5.mjs → client-A5ILG5GT.mjs} +3 -3
- package/dist/cli/{csv-LXBF5UIV.mjs → csv-NZ7KAYEF.mjs} +3 -3
- package/dist/cli/{csv-H42JDJ6G.js → csv-VSSAAXFX.js} +12 -12
- package/dist/cli/{db-DP5GU4MU.js → db-ICLEBSNO.js} +20 -20
- package/dist/cli/{db-3O5CFLNP.mjs → db-YSAJUBLD.mjs} +4 -4
- package/dist/cli/{executions-QC2UZKIP.mjs → executions-MUJRAT5R.mjs} +4 -4
- package/dist/cli/{executions-5FFTPHEX.js → executions-XCAJB5TF.js} +6 -6
- package/dist/cli/{feedback-JCGE6JFW.mjs → feedback-6C7Y2SCJ.mjs} +3 -3
- package/dist/cli/{feedback-XAU3FI5S.js → feedback-DBZMBHZG.js} +6 -6
- package/dist/cli/git-2NSE4WIK.js +23 -0
- package/dist/cli/{git-S3Y3E2NL.mjs → git-EAH67ITA.mjs} +4 -4
- package/dist/cli/index.js +79 -77
- package/dist/cli/index.mjs +33 -31
- package/dist/cli/{legacy-noops-TXOUX4FX.js → legacy-noops-EJTOLTVO.js} +7 -7
- package/dist/cli/{legacy-noops-FQF2IRVS.mjs → legacy-noops-O5INQP6Q.mjs} +4 -4
- package/dist/cli/{monitors-4BQCO6B7.js → monitors-5OYO2DPJ.js} +85 -85
- package/dist/cli/{monitors-F3PELPSH.mjs → monitors-SBBBMUFY.mjs} +4 -4
- package/dist/cli/{org-VQM6OOLK.js → org-SJRPJG42.js} +60 -60
- package/dist/cli/{org-ER5J3ATG.mjs → org-Z7LTOL3H.mjs} +4 -4
- package/dist/cli/{play-B5H2RN4N.mjs → play-2JTIEOZM.mjs} +472 -55
- package/dist/cli/{play-WKZPAOW3.js → play-VQJ6ABZ2.js} +961 -544
- package/dist/cli/{providers-5TEA2FJG.js → providers-43MR5XOV.js} +7 -7
- package/dist/cli/{providers-6ODEAYC2.mjs → providers-MX4CV2EH.mjs} +4 -4
- package/dist/cli/{quickstart-GJYCNMFC.js → quickstart-A6W3HEE2.js} +5 -5
- package/dist/cli/{quickstart-BCYT7BCY.mjs → quickstart-Y4YLKRZ3.mjs} +3 -3
- package/dist/cli/{search-KKJ535OX.js → search-AMUFSAFP.js} +6 -6
- package/dist/cli/{search-RY547KOT.mjs → search-FRKDLJQG.mjs} +5 -5
- package/dist/cli/{secrets-U42ZEYIU.mjs → secrets-E4EX7O3J.mjs} +4 -4
- package/dist/cli/{secrets-RFGDXSEW.js → secrets-XITHKZW2.js} +13 -13
- package/dist/cli/{sessions-ZRTWHMIH.js → sessions-6HJFDIR2.js} +5 -5
- package/dist/cli/{sessions-YWEFB62D.mjs → sessions-6YXAUJII.mjs} +4 -4
- package/dist/cli/{settings-XUWFXIL7.js → settings-J5NRLCV2.js} +74 -74
- package/dist/cli/{settings-AS4U6NVP.mjs → settings-LBJSZIET.mjs} +7 -7
- package/dist/cli/{setup-2QPWP2FI.js → setup-2ES4CTO5.js} +41 -41
- package/dist/cli/{setup-2GUIV2EW.mjs → setup-75FENNVH.mjs} +6 -6
- package/dist/cli/skills-VLTAVA4K.js +28 -0
- package/dist/cli/{skills-5QS7ERVF.mjs → skills-XGZDUXW6.mjs} +4 -4
- package/dist/cli/{tools-PVWSGCZD.mjs → tools-2LEKXOGY.mjs} +58 -42
- package/dist/cli/{tools-J47LIO2H.js → tools-ZOQOGDKI.js} +111 -95
- package/dist/cli/update-5CZOVYQT.js +28 -0
- package/dist/cli/{update-KS4TRSZI.mjs → update-SNK3IN3I.mjs} +6 -6
- package/dist/cli/{workflow-QCLR7GVC.mjs → workflow-G7ZM3MAC.mjs} +5 -5
- package/dist/cli/{workflow-HQRUUTTL.js → workflow-W3AYTB3J.js} +15 -15
- package/dist/{client-Dj8MAkDc.d.mts → client-DpnkGBn-.d.mts} +324 -11
- package/dist/{client-DsrFGXJf.d.ts → client-ypdBvf1o.d.ts} +324 -11
- package/dist/index.d.mts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +8 -6
- package/dist/index.mjs +5 -3
- package/dist/install-integrity.json +73 -72
- 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 +2 -2
- package/dist/release.mjs +1 -1
- package/package.json +1 -1
- package/dist/cli/auth-ZT3UF33R.js +0 -31
- package/dist/cli/client-UQXVU2T6.js +0 -20
- package/dist/cli/git-CNO2UFZE.js +0 -23
- package/dist/cli/skills-4E5KYW2Y.js +0 -28
- package/dist/cli/update-GZKGOMKD.js +0 -28
package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/runtime-event-search.ts
CHANGED
|
@@ -11,6 +11,31 @@
|
|
|
11
11
|
* The API uses only the fixed, tenant-scoped readers in this module.
|
|
12
12
|
*/
|
|
13
13
|
import { normalizeRuntimeSchedulerSchema } from '../runtime-scheduler-topology';
|
|
14
|
+
import { createHash } from 'node:crypto';
|
|
15
|
+
import {
|
|
16
|
+
getRunEventQueryFields,
|
|
17
|
+
getRunEventQueryPredicate,
|
|
18
|
+
normalizeRunEventOperationRequest,
|
|
19
|
+
normalizeRunEventQuery,
|
|
20
|
+
normalizeRunEventQueryTimestamp,
|
|
21
|
+
RunEventQueryInputError,
|
|
22
|
+
RunEventQueryDataError,
|
|
23
|
+
RUN_EVENT_QUERY_RETENTION_MS,
|
|
24
|
+
RUN_EVENT_QUERY_MAX_CURSOR_BYTES,
|
|
25
|
+
RUN_EVENT_QUERY_MAX_REQUEST_BYTES,
|
|
26
|
+
type NormalizedRunEventQuery,
|
|
27
|
+
type RunEventOperationRequest,
|
|
28
|
+
type RunEventOperationResult,
|
|
29
|
+
type RunEventQueryEvent,
|
|
30
|
+
type RunEventField,
|
|
31
|
+
type RunEventPredicate,
|
|
32
|
+
type RunEventGroupValue,
|
|
33
|
+
type RunEventScalar,
|
|
34
|
+
type RunEventQueryPage,
|
|
35
|
+
type RunEventQueryRetention,
|
|
36
|
+
type RunEventSummaryResult,
|
|
37
|
+
} from '../run-event-query';
|
|
38
|
+
import { RUN_LOG_LEVELS, type RunLogLevel } from '../run-event-catalog';
|
|
14
39
|
import type { PostgresSchedulerQueryClient } from './postgres';
|
|
15
40
|
import {
|
|
16
41
|
RUN_EVENT_CONVEX_INTERNAL_KIND_SQL_LITERAL,
|
|
@@ -61,7 +86,7 @@ const RUNTIME_EVENT_LOG_POSITION_EXPRESSION = `jsonb_set(
|
|
|
61
86
|
// Migration readiness tracks this projection body independently of scheduler
|
|
62
87
|
// wire and schema versions. Bump when a changed trigger must be reinstalled.
|
|
63
88
|
export const RUNTIME_EVENT_SEARCH_OUTBOX_PROJECTION_MARKER =
|
|
64
|
-
'runtime-event-search-origin-batches-
|
|
89
|
+
'runtime-event-search-origin-batches-new-run-cutover-fresh-origin';
|
|
65
90
|
/** Trusted trigger-authored state; absent on already-projected legacy rows. */
|
|
66
91
|
export const RUNTIME_EVENT_DEFERRED_PROJECTION_FIELD =
|
|
67
92
|
'_runtimeSearchProjectionPending';
|
|
@@ -464,6 +489,576 @@ export async function searchRuntimeEvents(
|
|
|
464
489
|
};
|
|
465
490
|
}
|
|
466
491
|
|
|
492
|
+
type RuntimeRunEventReadEvent = RunEventQueryEvent & {
|
|
493
|
+
receiptRef: string | null;
|
|
494
|
+
};
|
|
495
|
+
type RunEventQueryRow = EventRow & { cursor_occurred_at: string };
|
|
496
|
+
export type RuntimeRunEventQueryReadResult =
|
|
497
|
+
| Exclude<RunEventOperationResult, { logs: unknown } | { events: unknown }>
|
|
498
|
+
| {
|
|
499
|
+
logs: RuntimeRunEventReadEvent[];
|
|
500
|
+
page: RunEventQueryPage;
|
|
501
|
+
query: NormalizedRunEventQuery;
|
|
502
|
+
retention: RunEventQueryRetention;
|
|
503
|
+
}
|
|
504
|
+
| {
|
|
505
|
+
events: RuntimeRunEventReadEvent[];
|
|
506
|
+
page: RunEventQueryPage;
|
|
507
|
+
query: NormalizedRunEventQuery;
|
|
508
|
+
retention: RunEventQueryRetention;
|
|
509
|
+
};
|
|
510
|
+
type QueryCursor = {
|
|
511
|
+
format: 'run-event-query';
|
|
512
|
+
operation: 'list' | 'summarize';
|
|
513
|
+
fingerprint: string;
|
|
514
|
+
query: NormalizedRunEventQuery;
|
|
515
|
+
position: { occurredAt: string; eventId: string } | RunEventGroupValue[];
|
|
516
|
+
};
|
|
517
|
+
function invalidQueryCursor(): never {
|
|
518
|
+
throw new RunEventQueryInputError(
|
|
519
|
+
'Run event cursor does not match this scope, query, resource or projection. Start a new query without cursor.',
|
|
520
|
+
'invalid_run_event_cursor',
|
|
521
|
+
);
|
|
522
|
+
}
|
|
523
|
+
function decodeQueryCursor(value: string): QueryCursor {
|
|
524
|
+
try {
|
|
525
|
+
if (
|
|
526
|
+
!/^[A-Za-z0-9_-]+$/.test(value) ||
|
|
527
|
+
value.length > RUN_EVENT_QUERY_MAX_CURSOR_BYTES
|
|
528
|
+
)
|
|
529
|
+
return invalidQueryCursor();
|
|
530
|
+
const decoded = JSON.parse(
|
|
531
|
+
Buffer.from(value, 'base64url').toString('utf8'),
|
|
532
|
+
) as QueryCursor;
|
|
533
|
+
if (
|
|
534
|
+
decoded.format !== 'run-event-query' ||
|
|
535
|
+
!['list', 'summarize'].includes(decoded.operation) ||
|
|
536
|
+
typeof decoded.fingerprint !== 'string' ||
|
|
537
|
+
!/^[a-f0-9]{64}$/.test(decoded.fingerprint) ||
|
|
538
|
+
!decoded.query ||
|
|
539
|
+
!decoded.position
|
|
540
|
+
)
|
|
541
|
+
return invalidQueryCursor();
|
|
542
|
+
decoded.query = normalizeRunEventQuery(decoded.query);
|
|
543
|
+
return decoded;
|
|
544
|
+
} catch {
|
|
545
|
+
return invalidQueryCursor();
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
function queryFingerprint(
|
|
549
|
+
scope: string,
|
|
550
|
+
org: string,
|
|
551
|
+
request: RunEventOperationRequest,
|
|
552
|
+
query: NormalizedRunEventQuery,
|
|
553
|
+
): string {
|
|
554
|
+
return createHash('sha256')
|
|
555
|
+
.update(
|
|
556
|
+
JSON.stringify({
|
|
557
|
+
scope,
|
|
558
|
+
org,
|
|
559
|
+
resource: request.resource,
|
|
560
|
+
runId: request.runId ?? query.runId ?? null,
|
|
561
|
+
operation: request.operation,
|
|
562
|
+
query,
|
|
563
|
+
order:
|
|
564
|
+
request.operation === 'summarize'
|
|
565
|
+
? 'group_key:asc'
|
|
566
|
+
: 'occurred_at:event_id:desc',
|
|
567
|
+
projection:
|
|
568
|
+
request.operation === 'list' && request.includePayloads === true,
|
|
569
|
+
groupBy:
|
|
570
|
+
request.operation === 'summarize' ? request.groupBy : undefined,
|
|
571
|
+
measures:
|
|
572
|
+
request.operation === 'summarize' ? request.measures : undefined,
|
|
573
|
+
}),
|
|
574
|
+
)
|
|
575
|
+
.digest('hex');
|
|
576
|
+
}
|
|
577
|
+
function queryFieldSql(field: RunEventField, params: unknown[]): string {
|
|
578
|
+
const columns: Record<string, string> = {
|
|
579
|
+
eventId: 'event_id',
|
|
580
|
+
orgId: 'org_id',
|
|
581
|
+
runtimeScope: 'runtime_scope',
|
|
582
|
+
runId: 'run_id',
|
|
583
|
+
playName: 'play_name',
|
|
584
|
+
kind: 'kind',
|
|
585
|
+
};
|
|
586
|
+
if (columns[field]) return `to_jsonb(${columns[field]})`;
|
|
587
|
+
if (field === 'occurredAt') return `to_jsonb(${queryTimestampSql()})`;
|
|
588
|
+
if (field === 'level') return 'to_jsonb(level)';
|
|
589
|
+
params.push(
|
|
590
|
+
field.startsWith('context.') ? ['context', field.slice(8)] : [field],
|
|
591
|
+
);
|
|
592
|
+
return `search_doc #> $${params.length}::text[]`;
|
|
593
|
+
}
|
|
594
|
+
function queryTimestampSql(): string {
|
|
595
|
+
return `to_char(occurred_at AT TIME ZONE 'UTC', CASE
|
|
596
|
+
WHEN mod(extract(microseconds FROM occurred_at)::bigint, 1000) = 0
|
|
597
|
+
THEN 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"'
|
|
598
|
+
ELSE 'YYYY-MM-DD"T"HH24:MI:SS.US"Z"' END)`;
|
|
599
|
+
}
|
|
600
|
+
const QUERY_DIRECT_COLUMNS: Partial<Record<RunEventField, string>> = {
|
|
601
|
+
eventId: 'event_id',
|
|
602
|
+
orgId: 'org_id',
|
|
603
|
+
runtimeScope: 'runtime_scope',
|
|
604
|
+
runId: 'run_id',
|
|
605
|
+
playName: 'play_name',
|
|
606
|
+
kind: 'kind',
|
|
607
|
+
occurredAt: 'occurred_at',
|
|
608
|
+
level: 'level',
|
|
609
|
+
};
|
|
610
|
+
function queryLogBodySql(): string {
|
|
611
|
+
return `CASE WHEN kind = ${RUN_EVENT_KIND_SQL_LITERAL.logAppended}
|
|
612
|
+
AND search_doc->>'source' = 'terminal' THEN
|
|
613
|
+
regexp_replace(coalesce(log_body, message),
|
|
614
|
+
'^' || chr(1) || 'prov:[^' || chr(1) || ']*' || chr(1), '')
|
|
615
|
+
ELSE coalesce(log_body, message) END`;
|
|
616
|
+
}
|
|
617
|
+
function queryPredicateSql(
|
|
618
|
+
predicate: RunEventPredicate,
|
|
619
|
+
params: unknown[],
|
|
620
|
+
): string {
|
|
621
|
+
if ('and' in predicate)
|
|
622
|
+
return `(${predicate.and.map((term) => queryPredicateSql(term, params)).join(' AND ')})`;
|
|
623
|
+
if ('or' in predicate)
|
|
624
|
+
return `(${predicate.or.map((term) => queryPredicateSql(term, params)).join(' OR ')})`;
|
|
625
|
+
const column = QUERY_DIRECT_COLUMNS[predicate.field];
|
|
626
|
+
if (column) {
|
|
627
|
+
if (
|
|
628
|
+
predicate.field === 'level' &&
|
|
629
|
+
['gt', 'gte', 'lt', 'lte'].includes(predicate.op)
|
|
630
|
+
) {
|
|
631
|
+
const index = RUN_LOG_LEVELS.indexOf(predicate.value as RunLogLevel);
|
|
632
|
+
const levels =
|
|
633
|
+
predicate.op === 'gt'
|
|
634
|
+
? RUN_LOG_LEVELS.slice(index + 1)
|
|
635
|
+
: predicate.op === 'gte'
|
|
636
|
+
? RUN_LOG_LEVELS.slice(index)
|
|
637
|
+
: predicate.op === 'lt'
|
|
638
|
+
? RUN_LOG_LEVELS.slice(0, index)
|
|
639
|
+
: RUN_LOG_LEVELS.slice(0, index + 1);
|
|
640
|
+
params.push(levels);
|
|
641
|
+
return `(${column} = ANY($${params.length}::text[]))`;
|
|
642
|
+
}
|
|
643
|
+
if (predicate.op === 'exists')
|
|
644
|
+
return `(${column} IS ${predicate.value ? 'NOT ' : ''}NULL)`;
|
|
645
|
+
const bind = (value: RunEventScalar) => {
|
|
646
|
+
params.push(value);
|
|
647
|
+
return `$${params.length}${predicate.field === 'occurredAt' ? '::timestamptz' : ''}`;
|
|
648
|
+
};
|
|
649
|
+
if (predicate.op === 'in') {
|
|
650
|
+
const values = predicate.value
|
|
651
|
+
.filter((value) => value !== null)
|
|
652
|
+
.map(bind);
|
|
653
|
+
const terms = values.length
|
|
654
|
+
? [`${column} IN (${values.join(', ')})`]
|
|
655
|
+
: [];
|
|
656
|
+
if (predicate.value.includes(null)) terms.push(`${column} IS NULL`);
|
|
657
|
+
return `(${terms.join(' OR ')})`;
|
|
658
|
+
}
|
|
659
|
+
if (predicate.value === null)
|
|
660
|
+
return `(${column} IS ${predicate.op === 'ne' ? 'NOT ' : ''}NULL)`;
|
|
661
|
+
const operator = {
|
|
662
|
+
eq: '=',
|
|
663
|
+
ne: '<>',
|
|
664
|
+
gt: '>',
|
|
665
|
+
gte: '>=',
|
|
666
|
+
lt: '<',
|
|
667
|
+
lte: '<=',
|
|
668
|
+
}[predicate.op];
|
|
669
|
+
return `(${column} ${operator} ${bind(predicate.value)})`;
|
|
670
|
+
}
|
|
671
|
+
const field = queryFieldSql(predicate.field, params);
|
|
672
|
+
if (predicate.op === 'exists')
|
|
673
|
+
return `(${field} IS ${predicate.value ? 'NOT ' : ''}NULL)`;
|
|
674
|
+
if (predicate.op === 'in') {
|
|
675
|
+
const values = predicate.value.map((value) => {
|
|
676
|
+
params.push(JSON.stringify(value));
|
|
677
|
+
return `$${params.length}::jsonb`;
|
|
678
|
+
});
|
|
679
|
+
return `(${field} IN (${values.join(', ')}))`;
|
|
680
|
+
}
|
|
681
|
+
params.push(JSON.stringify(predicate.value));
|
|
682
|
+
const value = `$${params.length}::jsonb`;
|
|
683
|
+
const operator = {
|
|
684
|
+
eq: '=',
|
|
685
|
+
ne: '<>',
|
|
686
|
+
gt: '>',
|
|
687
|
+
gte: '>=',
|
|
688
|
+
lt: '<',
|
|
689
|
+
lte: '<=',
|
|
690
|
+
}[predicate.op];
|
|
691
|
+
if (predicate.op === 'eq' || predicate.op === 'ne')
|
|
692
|
+
return `(${field} IS NOT NULL AND ${field} ${operator} ${value})`;
|
|
693
|
+
return `(jsonb_typeof(${field}) = '${typeof predicate.value === 'number' ? 'number' : 'string'}' AND ${field} ${operator} ${value})`;
|
|
694
|
+
}
|
|
695
|
+
function queryScopeSql(
|
|
696
|
+
scope: string,
|
|
697
|
+
org: string,
|
|
698
|
+
request: RunEventOperationRequest,
|
|
699
|
+
query: NormalizedRunEventQuery,
|
|
700
|
+
): { params: unknown[]; where: string } {
|
|
701
|
+
const params: unknown[] = [scope, org, query.since, query.until];
|
|
702
|
+
// Trusted admission, tenant, runtime and retention predicates always enclose
|
|
703
|
+
// the complete user expression. An OR can never widen these constraints.
|
|
704
|
+
const predicates = [
|
|
705
|
+
'runtime_scope = $1',
|
|
706
|
+
'org_id = $2',
|
|
707
|
+
'occurred_at >= $3::timestamptz',
|
|
708
|
+
'occurred_at < $4::timestamptz',
|
|
709
|
+
"ingested_at >= clock_timestamp() - interval '7 days'",
|
|
710
|
+
];
|
|
711
|
+
const runId = request.runId ?? query.runId;
|
|
712
|
+
if (runId !== undefined) {
|
|
713
|
+
params.push(runId);
|
|
714
|
+
predicates.push(`run_id = $${params.length}`);
|
|
715
|
+
}
|
|
716
|
+
if (request.resource === 'logs') {
|
|
717
|
+
predicates.push(`kind = ${RUN_EVENT_KIND_SQL_LITERAL.logAppended}`);
|
|
718
|
+
predicates.push('(log_body IS NOT NULL OR message IS NOT NULL)');
|
|
719
|
+
}
|
|
720
|
+
for (const [field, value] of [
|
|
721
|
+
['play_name', query.playName],
|
|
722
|
+
['kind', query.kind],
|
|
723
|
+
] as const) {
|
|
724
|
+
if (value !== undefined) {
|
|
725
|
+
params.push(value);
|
|
726
|
+
predicates.push(`${field} = $${params.length}`);
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
if (query.stepId !== undefined) {
|
|
730
|
+
params.push(query.stepId);
|
|
731
|
+
predicates.push(`search_doc->'stepId' = to_jsonb($${params.length}::text)`);
|
|
732
|
+
}
|
|
733
|
+
if (query.minLevel !== undefined) {
|
|
734
|
+
params.push(RUN_LOG_LEVELS.slice(RUN_LOG_LEVELS.indexOf(query.minLevel)));
|
|
735
|
+
predicates.push(`level = ANY($${params.length}::text[])`);
|
|
736
|
+
}
|
|
737
|
+
if (query.text !== undefined) {
|
|
738
|
+
// Escape LIKE metacharacters: this is a literal substring search, never a
|
|
739
|
+
// user-supplied wildcard, regex or a search over a shortened response tail.
|
|
740
|
+
params.push(`%${query.text.replace(/[\\%_]/g, '\\$&')}%`);
|
|
741
|
+
predicates.push(
|
|
742
|
+
`coalesce((${queryLogBodySql()}), '') ILIKE $${params.length} ESCAPE E'\\\\'`,
|
|
743
|
+
);
|
|
744
|
+
}
|
|
745
|
+
if (query.where)
|
|
746
|
+
predicates.push(
|
|
747
|
+
queryPredicateSql(getRunEventQueryPredicate(query.where), params),
|
|
748
|
+
);
|
|
749
|
+
return { params, where: predicates.join(' AND ') };
|
|
750
|
+
}
|
|
751
|
+
function exactQueryNumber(value: unknown, label: string): number {
|
|
752
|
+
const number = Number(value);
|
|
753
|
+
// SQL sums are exact numeric values. Public fractional results use JS number
|
|
754
|
+
// precision; integral results must never silently lose integer precision.
|
|
755
|
+
if (
|
|
756
|
+
!Number.isFinite(number) ||
|
|
757
|
+
(Number.isInteger(number) && !Number.isSafeInteger(number)) ||
|
|
758
|
+
(label === 'count' && (!Number.isSafeInteger(number) || number < 0))
|
|
759
|
+
)
|
|
760
|
+
throw new RunEventQueryDataError(
|
|
761
|
+
`Run event ${label} cannot be represented exactly in the response.`,
|
|
762
|
+
);
|
|
763
|
+
return number;
|
|
764
|
+
}
|
|
765
|
+
function flatQueryEvent(row: RunEventQueryRow): RuntimeRunEventReadEvent {
|
|
766
|
+
const document = row.search_doc;
|
|
767
|
+
const metadata: Record<string, unknown> = {};
|
|
768
|
+
for (const field of [
|
|
769
|
+
'source',
|
|
770
|
+
'stepId',
|
|
771
|
+
'status',
|
|
772
|
+
'stepKind',
|
|
773
|
+
'datasetId',
|
|
774
|
+
'phase',
|
|
775
|
+
'provider',
|
|
776
|
+
'operation',
|
|
777
|
+
] as const) {
|
|
778
|
+
if (typeof document[field] === 'string') metadata[field] = document[field];
|
|
779
|
+
}
|
|
780
|
+
if (typeof document.producerAttempt === 'number')
|
|
781
|
+
metadata.producerAttempt = document.producerAttempt;
|
|
782
|
+
if (document.context !== undefined) {
|
|
783
|
+
if (
|
|
784
|
+
document.context === null ||
|
|
785
|
+
typeof document.context !== 'object' ||
|
|
786
|
+
Array.isArray(document.context) ||
|
|
787
|
+
Object.values(document.context).some(
|
|
788
|
+
(value) =>
|
|
789
|
+
value !== null &&
|
|
790
|
+
!['string', 'number', 'boolean'].includes(typeof value),
|
|
791
|
+
)
|
|
792
|
+
)
|
|
793
|
+
throw new RunEventQueryDataError(
|
|
794
|
+
'Run event retained context is not a flat scalar object.',
|
|
795
|
+
);
|
|
796
|
+
metadata.context = document.context;
|
|
797
|
+
}
|
|
798
|
+
if (row.level !== null && !RUN_LOG_LEVELS.includes(row.level as RunLogLevel))
|
|
799
|
+
throw new RunEventQueryDataError(
|
|
800
|
+
'Run event retained level is unsupported.',
|
|
801
|
+
);
|
|
802
|
+
if (
|
|
803
|
+
row.event_payload !== null &&
|
|
804
|
+
row.event_payload !== undefined &&
|
|
805
|
+
(typeof row.event_payload !== 'object' || Array.isArray(row.event_payload))
|
|
806
|
+
)
|
|
807
|
+
throw new RunEventQueryDataError(
|
|
808
|
+
'Runtime canonical event payload is not a JSON object.',
|
|
809
|
+
);
|
|
810
|
+
return {
|
|
811
|
+
...metadata,
|
|
812
|
+
eventId: row.event_id,
|
|
813
|
+
occurredAt: normalizeRunEventQueryTimestamp(row.cursor_occurred_at),
|
|
814
|
+
ingestedAt: iso(row.ingested_at),
|
|
815
|
+
runId: row.run_id,
|
|
816
|
+
playName: row.play_name,
|
|
817
|
+
kind: row.kind,
|
|
818
|
+
level: row.level as RunLogLevel | null,
|
|
819
|
+
message: row.message,
|
|
820
|
+
receiptRef: row.receipt_ref,
|
|
821
|
+
...(row.event_payload == null
|
|
822
|
+
? {}
|
|
823
|
+
: { eventPayload: row.event_payload as Record<string, unknown> }),
|
|
824
|
+
};
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
/** Reads through the existing restricted reader transaction and 3000ms budget. */
|
|
828
|
+
export async function executeRuntimeEventQuery(
|
|
829
|
+
client: PostgresSchedulerQueryClient,
|
|
830
|
+
input: {
|
|
831
|
+
runtimeScope: string;
|
|
832
|
+
orgId: string;
|
|
833
|
+
request: RunEventOperationRequest;
|
|
834
|
+
},
|
|
835
|
+
): Promise<RuntimeRunEventQueryReadResult> {
|
|
836
|
+
const scope = requiredScope(input.runtimeScope);
|
|
837
|
+
const org = requiredOrg(input.orgId);
|
|
838
|
+
const request = normalizeRunEventOperationRequest(input.request);
|
|
839
|
+
if (request.operation === 'fields') return getRunEventQueryFields();
|
|
840
|
+
const cursor =
|
|
841
|
+
'cursor' in request && request.cursor
|
|
842
|
+
? decodeQueryCursor(request.cursor)
|
|
843
|
+
: undefined;
|
|
844
|
+
const criteria = Object.fromEntries(
|
|
845
|
+
Object.entries(request).filter(([key]) =>
|
|
846
|
+
[
|
|
847
|
+
'since',
|
|
848
|
+
'until',
|
|
849
|
+
'where',
|
|
850
|
+
'stepId',
|
|
851
|
+
'minLevel',
|
|
852
|
+
'text',
|
|
853
|
+
'playName',
|
|
854
|
+
'kind',
|
|
855
|
+
...(request.resource === 'events' ? ['runId'] : []),
|
|
856
|
+
].includes(key),
|
|
857
|
+
),
|
|
858
|
+
);
|
|
859
|
+
// All omitted criteria are inherited from the cursor's admitted query. Any
|
|
860
|
+
// supplied criterion must normalize to exactly the original canonical fact.
|
|
861
|
+
const query = normalizeRunEventQuery({ ...cursor?.query, ...criteria });
|
|
862
|
+
const fingerprint = queryFingerprint(scope, org, request, query);
|
|
863
|
+
if (
|
|
864
|
+
cursor &&
|
|
865
|
+
(cursor.operation !== request.operation ||
|
|
866
|
+
cursor.fingerprint !== fingerprint)
|
|
867
|
+
)
|
|
868
|
+
invalidQueryCursor();
|
|
869
|
+
const retention = {
|
|
870
|
+
retainedSince: new Date(
|
|
871
|
+
Date.now() - RUN_EVENT_QUERY_RETENTION_MS,
|
|
872
|
+
).toISOString(),
|
|
873
|
+
retentionDays: 7 as const,
|
|
874
|
+
};
|
|
875
|
+
const base = { query, retention };
|
|
876
|
+
const sql = queryScopeSql(scope, org, request, query);
|
|
877
|
+
if (request.operation === 'count') {
|
|
878
|
+
const result = await client.query<{ matched_count: string }>(
|
|
879
|
+
`SELECT count(*)::text AS matched_count FROM ${EVENTS_TABLE} WHERE ${sql.where}`,
|
|
880
|
+
sql.params,
|
|
881
|
+
);
|
|
882
|
+
return {
|
|
883
|
+
...base,
|
|
884
|
+
matchedCount: exactQueryNumber(result.rows[0]?.matched_count, 'count'),
|
|
885
|
+
exact: true,
|
|
886
|
+
};
|
|
887
|
+
}
|
|
888
|
+
const limit = request.limit!;
|
|
889
|
+
const encode = (position: QueryCursor['position']) => {
|
|
890
|
+
const encoded = Buffer.from(
|
|
891
|
+
JSON.stringify({
|
|
892
|
+
format: 'run-event-query',
|
|
893
|
+
operation: request.operation,
|
|
894
|
+
fingerprint,
|
|
895
|
+
query,
|
|
896
|
+
position,
|
|
897
|
+
}),
|
|
898
|
+
).toString('base64url');
|
|
899
|
+
const continuation = JSON.stringify({
|
|
900
|
+
...request,
|
|
901
|
+
...query,
|
|
902
|
+
cursor: encoded,
|
|
903
|
+
});
|
|
904
|
+
if (
|
|
905
|
+
encoded.length > RUN_EVENT_QUERY_MAX_CURSOR_BYTES ||
|
|
906
|
+
Buffer.byteLength(continuation, 'utf8') >
|
|
907
|
+
RUN_EVENT_QUERY_MAX_REQUEST_BYTES
|
|
908
|
+
) {
|
|
909
|
+
throw new RunEventQueryDataError(
|
|
910
|
+
'Run event retained pagination key exceeds the continuation contract.',
|
|
911
|
+
);
|
|
912
|
+
}
|
|
913
|
+
return encoded;
|
|
914
|
+
};
|
|
915
|
+
if (request.operation === 'list') {
|
|
916
|
+
if (cursor) {
|
|
917
|
+
const position = cursor.position as RuntimeSearchCursor;
|
|
918
|
+
if (
|
|
919
|
+
Array.isArray(position) ||
|
|
920
|
+
typeof position.occurredAt !== 'string' ||
|
|
921
|
+
typeof position.eventId !== 'string' ||
|
|
922
|
+
!position.eventId ||
|
|
923
|
+
position.eventId.length > 512 ||
|
|
924
|
+
!Number.isFinite(Date.parse(position.occurredAt))
|
|
925
|
+
)
|
|
926
|
+
invalidQueryCursor();
|
|
927
|
+
sql.params.push(position.occurredAt, position.eventId);
|
|
928
|
+
sql.where += ` AND (occurred_at, event_id) < ($${sql.params.length - 1}::timestamptz, $${sql.params.length})`;
|
|
929
|
+
}
|
|
930
|
+
sql.params.push(limit + 1, request.includePayloads === true);
|
|
931
|
+
const result = await client.query<RunEventQueryRow>(
|
|
932
|
+
`SELECT event_id, occurred_at, ingested_at, run_id, play_name, kind, level,
|
|
933
|
+
${queryTimestampSql()} AS cursor_occurred_at,
|
|
934
|
+
${queryLogBodySql()} AS message, receipt_ref, search_doc,
|
|
935
|
+
CASE WHEN $${sql.params.length}::boolean THEN event_payload END AS event_payload
|
|
936
|
+
FROM ${EVENTS_TABLE} WHERE ${sql.where}
|
|
937
|
+
ORDER BY occurred_at DESC, event_id DESC LIMIT $${sql.params.length - 1}::integer`,
|
|
938
|
+
sql.params,
|
|
939
|
+
);
|
|
940
|
+
const hasMore = result.rows.length > limit;
|
|
941
|
+
const events = result.rows.slice(0, limit).map(flatQueryEvent);
|
|
942
|
+
const last = result.rows[Math.min(result.rows.length, limit) - 1];
|
|
943
|
+
const page = {
|
|
944
|
+
returnedCount: events.length,
|
|
945
|
+
hasMore,
|
|
946
|
+
nextCursor:
|
|
947
|
+
hasMore && last
|
|
948
|
+
? encode({
|
|
949
|
+
occurredAt: last.cursor_occurred_at,
|
|
950
|
+
eventId: last.event_id,
|
|
951
|
+
})
|
|
952
|
+
: null,
|
|
953
|
+
};
|
|
954
|
+
return request.resource === 'logs'
|
|
955
|
+
? { ...base, logs: events, page }
|
|
956
|
+
: { ...base, events, page };
|
|
957
|
+
}
|
|
958
|
+
const fields = request.groupBy;
|
|
959
|
+
const measures = request.measures!;
|
|
960
|
+
const groupFields = fields.map(
|
|
961
|
+
(field) =>
|
|
962
|
+
`coalesce(${queryFieldSql(field, sql.params)}, '{"missing":true}'::jsonb)`,
|
|
963
|
+
);
|
|
964
|
+
const selected = [
|
|
965
|
+
`jsonb_build_array(${groupFields.join(', ')}) AS group_key`,
|
|
966
|
+
];
|
|
967
|
+
const invalid: string[] = [];
|
|
968
|
+
const aggregations = measures.map((measure, i) => {
|
|
969
|
+
if (measure.op === 'count') return `count(*)::text AS measure_${i}`;
|
|
970
|
+
const field = queryFieldSql(measure.field, sql.params);
|
|
971
|
+
selected.push(
|
|
972
|
+
`CASE WHEN jsonb_typeof(${field}) = 'number' THEN (${field} #>> '{}')::numeric END AS sum_${i}`,
|
|
973
|
+
);
|
|
974
|
+
invalid.push(
|
|
975
|
+
`(${field} IS NOT NULL AND jsonb_typeof(${field}) <> 'number')`,
|
|
976
|
+
);
|
|
977
|
+
return `coalesce(sum(sum_${i}), 0)::text AS measure_${i}`;
|
|
978
|
+
});
|
|
979
|
+
selected.push(
|
|
980
|
+
`${invalid.length ? invalid.join(' OR ') : 'false'} AS invalid_sum`,
|
|
981
|
+
);
|
|
982
|
+
let continuation = '';
|
|
983
|
+
if (cursor) {
|
|
984
|
+
if (
|
|
985
|
+
!Array.isArray(cursor.position) ||
|
|
986
|
+
cursor.position.length !== fields.length ||
|
|
987
|
+
cursor.position.some((value) => {
|
|
988
|
+
if (
|
|
989
|
+
value === null ||
|
|
990
|
+
typeof value === 'string' ||
|
|
991
|
+
typeof value === 'boolean'
|
|
992
|
+
)
|
|
993
|
+
return false;
|
|
994
|
+
if (typeof value === 'number') return !Number.isFinite(value);
|
|
995
|
+
return (
|
|
996
|
+
!value || value.missing !== true || Object.keys(value).length !== 1
|
|
997
|
+
);
|
|
998
|
+
})
|
|
999
|
+
)
|
|
1000
|
+
invalidQueryCursor();
|
|
1001
|
+
sql.params.push(JSON.stringify(cursor.position));
|
|
1002
|
+
continuation = `WHERE group_key > $${sql.params.length}::jsonb`;
|
|
1003
|
+
}
|
|
1004
|
+
sql.params.push(limit + 1);
|
|
1005
|
+
const result = await client.query<Record<string, unknown>>(
|
|
1006
|
+
`WITH matched AS MATERIALIZED (
|
|
1007
|
+
SELECT ${selected.join(', ')} FROM ${EVENTS_TABLE} WHERE ${sql.where}
|
|
1008
|
+
), grouped AS (
|
|
1009
|
+
SELECT ${fields.length ? 'group_key' : "'[]'::jsonb AS group_key"}, ${aggregations.join(', ')} FROM matched ${fields.length ? 'GROUP BY group_key' : ''}
|
|
1010
|
+
), validation AS (
|
|
1011
|
+
SELECT count(*) FILTER (WHERE invalid_sum)::text AS invalid_sum_count FROM matched
|
|
1012
|
+
), paged AS (
|
|
1013
|
+
SELECT * FROM grouped ${continuation} ORDER BY group_key ASC LIMIT $${sql.params.length}::integer
|
|
1014
|
+
) SELECT paged.*, validation.invalid_sum_count FROM validation LEFT JOIN paged ON true ORDER BY group_key ASC`,
|
|
1015
|
+
sql.params,
|
|
1016
|
+
);
|
|
1017
|
+
if (exactQueryNumber(result.rows[0]?.invalid_sum_count, 'count') > 0) {
|
|
1018
|
+
const sumFields = measures.flatMap((measure) =>
|
|
1019
|
+
measure.op === 'sum' ? [measure.field] : [],
|
|
1020
|
+
);
|
|
1021
|
+
throw new RunEventQueryDataError(
|
|
1022
|
+
`Run event sum selected a nonnumeric value in ${sumFields.join(', ')}. Filter to numeric values or choose another measure.`,
|
|
1023
|
+
'nonnumeric_run_event_sum',
|
|
1024
|
+
);
|
|
1025
|
+
}
|
|
1026
|
+
const rows = result.rows.filter(
|
|
1027
|
+
(row) => row.group_key !== null && row.group_key !== undefined,
|
|
1028
|
+
);
|
|
1029
|
+
const hasMore = rows.length > limit;
|
|
1030
|
+
const groups = rows.slice(0, limit).map((row) => {
|
|
1031
|
+
if (!Array.isArray(row.group_key) || row.group_key.length !== fields.length)
|
|
1032
|
+
throw new RunEventQueryDataError(
|
|
1033
|
+
'Run event group projection is invalid.',
|
|
1034
|
+
);
|
|
1035
|
+
return {
|
|
1036
|
+
keys: Object.fromEntries(
|
|
1037
|
+
fields.map((field, i) => [field, (row.group_key as unknown[])[i]]),
|
|
1038
|
+
),
|
|
1039
|
+
measures: Object.fromEntries(
|
|
1040
|
+
measures.map((measure, i) => [
|
|
1041
|
+
measure.as!,
|
|
1042
|
+
exactQueryNumber(row[`measure_${i}`], measure.op),
|
|
1043
|
+
]),
|
|
1044
|
+
),
|
|
1045
|
+
};
|
|
1046
|
+
});
|
|
1047
|
+
const last = rows[Math.min(rows.length, limit) - 1];
|
|
1048
|
+
return {
|
|
1049
|
+
...base,
|
|
1050
|
+
groupBy: fields,
|
|
1051
|
+
measures,
|
|
1052
|
+
groups: groups as RunEventSummaryResult['groups'],
|
|
1053
|
+
page: {
|
|
1054
|
+
returnedCount: groups.length,
|
|
1055
|
+
hasMore,
|
|
1056
|
+
nextCursor:
|
|
1057
|
+
hasMore && last ? encode(last.group_key as RunEventGroupValue[]) : null,
|
|
1058
|
+
},
|
|
1059
|
+
};
|
|
1060
|
+
}
|
|
1061
|
+
|
|
467
1062
|
/**
|
|
468
1063
|
* Reads a receipt through a narrow SECURITY DEFINER function. The reader has
|
|
469
1064
|
* no direct grant on any scheduler receipt table, and the function checks both
|
|
@@ -976,6 +1571,7 @@ export async function setRuntimeEventDeferredProjection(
|
|
|
976
1571
|
runtimeScope: string,
|
|
977
1572
|
deferred: boolean,
|
|
978
1573
|
independent?: boolean,
|
|
1574
|
+
newRunsOnly?: true,
|
|
979
1575
|
): Promise<void> {
|
|
980
1576
|
const schema = quoteIdent(requiredScope(runtimeScope));
|
|
981
1577
|
if (!(await hasRuntimeEventSearchShape(client, runtimeScope))) {
|
|
@@ -991,11 +1587,38 @@ export async function setRuntimeEventDeferredProjection(
|
|
|
991
1587
|
await client.query('BEGIN');
|
|
992
1588
|
try {
|
|
993
1589
|
await client.query("SELECT set_config('lock_timeout', '1000', true)");
|
|
1590
|
+
// Fence admission before outbox writes, in the normal Run -> outbox order.
|
|
1591
|
+
// The admission pin and producer flags change at one committed boundary.
|
|
1592
|
+
await client.query(`LOCK TABLE ${schema}.runs IN SHARE ROW EXCLUSIVE MODE`);
|
|
994
1593
|
// Fence trigger execution while changing mode. This operator-only lock
|
|
995
1594
|
// never spans projection; normal producer writes acquire no such lock.
|
|
996
1595
|
await client.query(
|
|
997
1596
|
`LOCK TABLE ${schema}.outbox IN SHARE ROW EXCLUSIVE MODE`,
|
|
998
1597
|
);
|
|
1598
|
+
const previous = await client.query<{
|
|
1599
|
+
deferred: boolean;
|
|
1600
|
+
new_runs_only: boolean;
|
|
1601
|
+
}>(`SELECT ${schema}.defer_runtime_event_projection() AS deferred,
|
|
1602
|
+
${schema}.new_runs_only_runtime_event_projection() AS new_runs_only`);
|
|
1603
|
+
const scoped =
|
|
1604
|
+
newRunsOnly === true || previous.rows[0]?.new_runs_only === true;
|
|
1605
|
+
if (deferred && scoped && independent === false) {
|
|
1606
|
+
throw new Error(
|
|
1607
|
+
'Scoped cutover requires independent projection; use disable to stop it.',
|
|
1608
|
+
);
|
|
1609
|
+
}
|
|
1610
|
+
if (newRunsOnly === true) {
|
|
1611
|
+
if (!deferred || independent !== true) {
|
|
1612
|
+
throw new Error(
|
|
1613
|
+
'New-run cutover requires independent deferred projection.',
|
|
1614
|
+
);
|
|
1615
|
+
}
|
|
1616
|
+
if (previous.rows[0]?.deferred && !previous.rows[0]?.new_runs_only) {
|
|
1617
|
+
throw new Error(
|
|
1618
|
+
'Drain and disable global deferred projection before a new-run cutover.',
|
|
1619
|
+
);
|
|
1620
|
+
}
|
|
1621
|
+
}
|
|
999
1622
|
if (!deferred) {
|
|
1000
1623
|
const pending = await client.query<{ event_id: string }>(
|
|
1001
1624
|
`SELECT event_id FROM ${schema}.outbox
|
|
@@ -1019,12 +1642,14 @@ export async function setRuntimeEventDeferredProjection(
|
|
|
1019
1642
|
);
|
|
1020
1643
|
}
|
|
1021
1644
|
await installRuntimeEventProjectionMode(client, schema, deferred);
|
|
1022
|
-
if (independent !== undefined || !deferred)
|
|
1645
|
+
if (independent !== undefined || !deferred || scoped)
|
|
1023
1646
|
await installIndependentProjectionMode(
|
|
1024
1647
|
client,
|
|
1025
1648
|
schema,
|
|
1026
|
-
deferred && independent === true,
|
|
1649
|
+
deferred && (independent === true || scoped),
|
|
1027
1650
|
);
|
|
1651
|
+
if (newRunsOnly !== undefined)
|
|
1652
|
+
await installNewRunProjectionMode(client, schema, newRunsOnly);
|
|
1028
1653
|
await client.query('COMMIT');
|
|
1029
1654
|
} catch (error) {
|
|
1030
1655
|
await client.query('ROLLBACK');
|
|
@@ -1036,10 +1661,12 @@ export type RuntimeEventProjectionModes = {
|
|
|
1036
1661
|
runtimeScope: string;
|
|
1037
1662
|
deferred: boolean | null;
|
|
1038
1663
|
independent: boolean | null;
|
|
1664
|
+
newRunsOnly?: boolean | null;
|
|
1039
1665
|
unavailableReason:
|
|
1040
1666
|
| 'projection_mode_readers_missing'
|
|
1041
1667
|
| 'deferred_projection_reader_missing'
|
|
1042
1668
|
| 'independent_projection_reader_missing'
|
|
1669
|
+
| 'new_run_projection_reader_missing'
|
|
1043
1670
|
| null;
|
|
1044
1671
|
};
|
|
1045
1672
|
|
|
@@ -1053,25 +1680,31 @@ export async function readRuntimeEventProjectionModes(
|
|
|
1053
1680
|
const installed = await client.query<{
|
|
1054
1681
|
deferred_installed: boolean;
|
|
1055
1682
|
independent_installed: boolean;
|
|
1683
|
+
new_runs_only_installed: boolean;
|
|
1056
1684
|
}>(
|
|
1057
1685
|
`SELECT to_regprocedure($1) IS NOT NULL AS deferred_installed,
|
|
1058
|
-
to_regprocedure($2) IS NOT NULL AS independent_installed
|
|
1686
|
+
to_regprocedure($2) IS NOT NULL AS independent_installed,
|
|
1687
|
+
to_regprocedure($3) IS NOT NULL AS new_runs_only_installed`,
|
|
1059
1688
|
[
|
|
1060
1689
|
`${schema}.defer_runtime_event_projection()`,
|
|
1061
1690
|
`${schema}.independent_runtime_event_projection()`,
|
|
1691
|
+
`${schema}.new_runs_only_runtime_event_projection()`,
|
|
1062
1692
|
],
|
|
1063
1693
|
);
|
|
1064
1694
|
const readers = installed.rows[0];
|
|
1065
1695
|
if (
|
|
1066
1696
|
!readers ||
|
|
1067
1697
|
typeof readers.deferred_installed !== 'boolean' ||
|
|
1068
|
-
typeof readers.independent_installed !== 'boolean'
|
|
1698
|
+
typeof readers.independent_installed !== 'boolean' ||
|
|
1699
|
+
typeof readers.new_runs_only_installed !== 'boolean'
|
|
1069
1700
|
) {
|
|
1070
1701
|
throw new Error('Runtime projection mode lookup returned no reader state.');
|
|
1071
1702
|
}
|
|
1072
1703
|
const unavailableReason = readers.deferred_installed
|
|
1073
1704
|
? readers.independent_installed
|
|
1074
|
-
?
|
|
1705
|
+
? readers.new_runs_only_installed
|
|
1706
|
+
? null
|
|
1707
|
+
: 'new_run_projection_reader_missing'
|
|
1075
1708
|
: 'independent_projection_reader_missing'
|
|
1076
1709
|
: readers.independent_installed
|
|
1077
1710
|
? 'deferred_projection_reader_missing'
|
|
@@ -1081,6 +1714,7 @@ export async function readRuntimeEventProjectionModes(
|
|
|
1081
1714
|
runtimeScope: scope,
|
|
1082
1715
|
deferred: null,
|
|
1083
1716
|
independent: null,
|
|
1717
|
+
newRunsOnly: null,
|
|
1084
1718
|
unavailableReason,
|
|
1085
1719
|
};
|
|
1086
1720
|
}
|
|
@@ -1091,18 +1725,25 @@ export async function readRuntimeEventProjectionModes(
|
|
|
1091
1725
|
const independentExpression = readers.independent_installed
|
|
1092
1726
|
? `${schema}.independent_runtime_event_projection()`
|
|
1093
1727
|
: 'NULL::boolean';
|
|
1728
|
+
const newRunsOnlyExpression = readers.new_runs_only_installed
|
|
1729
|
+
? `${schema}.new_runs_only_runtime_event_projection()`
|
|
1730
|
+
: 'NULL::boolean';
|
|
1094
1731
|
const result = await client.query<{
|
|
1095
1732
|
deferred: boolean | null;
|
|
1096
1733
|
independent: boolean | null;
|
|
1734
|
+
new_runs_only: boolean | null;
|
|
1097
1735
|
}>(
|
|
1098
1736
|
`SELECT ${deferredExpression} AS deferred,
|
|
1099
|
-
${independentExpression} AS independent
|
|
1737
|
+
${independentExpression} AS independent,
|
|
1738
|
+
${newRunsOnlyExpression} AS new_runs_only`,
|
|
1100
1739
|
);
|
|
1101
1740
|
const modes = result.rows[0];
|
|
1102
1741
|
if (
|
|
1103
1742
|
!modes ||
|
|
1104
1743
|
(readers.deferred_installed && typeof modes.deferred !== 'boolean') ||
|
|
1105
|
-
(readers.independent_installed && typeof modes.independent !== 'boolean')
|
|
1744
|
+
(readers.independent_installed && typeof modes.independent !== 'boolean') ||
|
|
1745
|
+
(readers.new_runs_only_installed &&
|
|
1746
|
+
typeof modes.new_runs_only !== 'boolean')
|
|
1106
1747
|
) {
|
|
1107
1748
|
throw new Error('Runtime projection mode lookup returned invalid values.');
|
|
1108
1749
|
}
|
|
@@ -1110,6 +1751,7 @@ export async function readRuntimeEventProjectionModes(
|
|
|
1110
1751
|
runtimeScope: scope,
|
|
1111
1752
|
deferred: modes.deferred,
|
|
1112
1753
|
independent: modes.independent,
|
|
1754
|
+
newRunsOnly: modes.new_runs_only,
|
|
1113
1755
|
unavailableReason,
|
|
1114
1756
|
};
|
|
1115
1757
|
}
|
|
@@ -1121,27 +1763,32 @@ export async function inspectRuntimeEventProjection(
|
|
|
1121
1763
|
): Promise<{
|
|
1122
1764
|
deferred: boolean;
|
|
1123
1765
|
independent: boolean;
|
|
1766
|
+
newRunsOnly: boolean | null;
|
|
1124
1767
|
pending: number;
|
|
1125
1768
|
blocked: number;
|
|
1126
1769
|
oldestPendingAt: string | null;
|
|
1127
1770
|
}> {
|
|
1128
1771
|
const schema = quoteIdent(requiredScope(runtimeScope));
|
|
1772
|
+
const modes = await readRuntimeEventProjectionModes(client, runtimeScope);
|
|
1773
|
+
if (modes.deferred === null || modes.independent === null) {
|
|
1774
|
+
throw new Error(
|
|
1775
|
+
'Install the deferred projection readers before inspecting the outbox.',
|
|
1776
|
+
);
|
|
1777
|
+
}
|
|
1129
1778
|
const result = await client.query<{
|
|
1130
|
-
deferred: boolean;
|
|
1131
|
-
independent: boolean;
|
|
1132
1779
|
pending: string | number;
|
|
1133
1780
|
blocked: string | number;
|
|
1134
1781
|
oldest: Date | string | null;
|
|
1135
|
-
}>(`SELECT
|
|
1136
|
-
count(*) AS pending, count(*) FILTER (WHERE failed_at IS NOT NULL) AS blocked,
|
|
1782
|
+
}>(`SELECT count(*) AS pending, count(*) FILTER (WHERE failed_at IS NOT NULL) AS blocked,
|
|
1137
1783
|
min(created_at) AS oldest
|
|
1138
1784
|
FROM ${schema}.outbox
|
|
1139
1785
|
WHERE delivered_at IS NULL
|
|
1140
1786
|
AND payload_json->'${RUNTIME_EVENT_DEFERRED_PROJECTION_FIELD}' = 'true'::jsonb`);
|
|
1141
1787
|
const state = result.rows[0]!;
|
|
1142
1788
|
return {
|
|
1143
|
-
deferred:
|
|
1144
|
-
independent:
|
|
1789
|
+
deferred: modes.deferred,
|
|
1790
|
+
independent: modes.independent,
|
|
1791
|
+
newRunsOnly: modes.newRunsOnly ?? null,
|
|
1145
1792
|
pending: Number(state.pending),
|
|
1146
1793
|
blocked: Number(state.blocked),
|
|
1147
1794
|
oldestPendingAt: state.oldest === null ? null : iso(state.oldest),
|
|
@@ -1173,6 +1820,18 @@ async function installIndependentProjectionMode(
|
|
|
1173
1820
|
);
|
|
1174
1821
|
}
|
|
1175
1822
|
|
|
1823
|
+
async function installNewRunProjectionMode(
|
|
1824
|
+
client: PostgresSchedulerQueryClient,
|
|
1825
|
+
schema: string,
|
|
1826
|
+
enabled: boolean,
|
|
1827
|
+
): Promise<void> {
|
|
1828
|
+
await client.query(`CREATE OR REPLACE FUNCTION ${schema}.new_runs_only_runtime_event_projection()
|
|
1829
|
+
RETURNS boolean LANGUAGE sql VOLATILE SET search_path = pg_catalog AS 'SELECT ${enabled ? 'true' : 'false'}'`);
|
|
1830
|
+
await client.query(
|
|
1831
|
+
`REVOKE ALL ON FUNCTION ${schema}.new_runs_only_runtime_event_projection() FROM PUBLIC`,
|
|
1832
|
+
);
|
|
1833
|
+
}
|
|
1834
|
+
|
|
1176
1835
|
/**
|
|
1177
1836
|
* Migration installs readers with synchronous production unchanged. After
|
|
1178
1837
|
* cutover, the producer persists raw input and only the leased dispatcher
|
|
@@ -1198,6 +1857,19 @@ export async function attachRuntimeEventSearchTriggers(
|
|
|
1198
1857
|
);
|
|
1199
1858
|
if (!independentMode.rows[0]?.installed)
|
|
1200
1859
|
await installIndependentProjectionMode(client, schema, false);
|
|
1860
|
+
const newRunMode = await client.query<{ installed: boolean }>(
|
|
1861
|
+
`SELECT to_regprocedure($1) IS NOT NULL AS installed`,
|
|
1862
|
+
[`${schema}.new_runs_only_runtime_event_projection()`],
|
|
1863
|
+
);
|
|
1864
|
+
if (!newRunMode.rows[0]?.installed)
|
|
1865
|
+
await installNewRunProjectionMode(client, schema, false);
|
|
1866
|
+
await client.query(`CREATE OR REPLACE FUNCTION ${schema}.defer_runtime_event_projection_for_run(rid text)
|
|
1867
|
+
RETURNS boolean LANGUAGE sql VOLATILE SECURITY DEFINER SET search_path = pg_catalog AS $function$
|
|
1868
|
+
SELECT ${schema}.defer_runtime_event_projection() AND
|
|
1869
|
+
(NOT ${schema}.new_runs_only_runtime_event_projection() OR coalesce(
|
|
1870
|
+
(SELECT admission_json->>'runtimeEventProjection' = 'independent'
|
|
1871
|
+
FROM ${schema}.runs WHERE run_id = rid), false))
|
|
1872
|
+
$function$`);
|
|
1201
1873
|
// Stamp new admissions once. Existing launches never acquire a producer pin
|
|
1202
1874
|
// on replay or worker upgrade. Ignore a caller-supplied pin before cutover.
|
|
1203
1875
|
await client.query(`CREATE OR REPLACE FUNCTION ${schema}.pin_runtime_log_identity() RETURNS trigger
|
|
@@ -1205,6 +1877,25 @@ export async function attachRuntimeEventSearchTriggers(
|
|
|
1205
1877
|
BEGIN
|
|
1206
1878
|
NEW.launch_json := NEW.launch_json - 'runtimeLogIdentity';
|
|
1207
1879
|
NEW.admission_json := NEW.admission_json - 'runtimeLogIdentity';
|
|
1880
|
+
NEW.launch_json := NEW.launch_json - 'runtimeEventProjection';
|
|
1881
|
+
NEW.admission_json := NEW.admission_json - 'runtimeEventProjection';
|
|
1882
|
+
IF ${schema}.new_runs_only_runtime_event_projection()
|
|
1883
|
+
AND ${schema}.defer_runtime_event_projection()
|
|
1884
|
+
AND ${schema}.independent_runtime_event_projection() THEN
|
|
1885
|
+
-- Older writers may omit admission_json. Preserve their authority
|
|
1886
|
+
-- using the same transport-free projection as normal admission.
|
|
1887
|
+
NEW.admission_json := coalesce(NEW.admission_json,
|
|
1888
|
+
NEW.launch_json - ARRAY[
|
|
1889
|
+
'executorToken',
|
|
1890
|
+
'preloadedDbSessionUnwrapKey',
|
|
1891
|
+
'preloadedDbSessions',
|
|
1892
|
+
'submittedAtMs',
|
|
1893
|
+
'runtimeDeployVersion',
|
|
1894
|
+
'coordinatorInternalToken',
|
|
1895
|
+
'vercelProtectionBypassToken'
|
|
1896
|
+
]::text[])
|
|
1897
|
+
|| '{"runtimeEventProjection":"independent"}'::jsonb;
|
|
1898
|
+
END IF;
|
|
1208
1899
|
IF ${schema}.independent_runtime_event_projection() THEN
|
|
1209
1900
|
NEW.launch_json := NEW.launch_json || '{"runtimeLogIdentity":"origin"}'::jsonb;
|
|
1210
1901
|
NEW.admission_json := NEW.admission_json || '{"runtimeLogIdentity":"origin"}'::jsonb;
|
|
@@ -1227,7 +1918,7 @@ export async function attachRuntimeEventSearchTriggers(
|
|
|
1227
1918
|
IF NEW.run_id IS NULL OR NEW.org_id IS NULL THEN
|
|
1228
1919
|
RAISE EXCEPTION 'Terminal receipt search projection requires run and organization';
|
|
1229
1920
|
END IF;
|
|
1230
|
-
IF ${schema}.
|
|
1921
|
+
IF ${schema}.defer_runtime_event_projection_for_run(NEW.run_id) THEN
|
|
1231
1922
|
IF NOT EXISTS (SELECT 1 FROM ${schema}.runs
|
|
1232
1923
|
WHERE run_id = NEW.run_id AND org_id = NEW.org_id) THEN
|
|
1233
1924
|
RAISE EXCEPTION 'Terminal receipt search projection has no matching run';
|
|
@@ -1388,6 +2079,12 @@ export async function attachRuntimeEventSearchTriggers(
|
|
|
1388
2079
|
ON CONFLICT (runtime_scope, org_id, event_id) DO NOTHING RETURNING event_id
|
|
1389
2080
|
) SELECT coalesce(array_agg(inserted.event_id ORDER BY entries.ordinal), ARRAY[]::text[])
|
|
1390
2081
|
INTO fresh FROM inserted JOIN entries ON entries.eid = inserted.event_id;
|
|
2082
|
+
-- The INSERT already proves the content of every fresh identity. Only
|
|
2083
|
+
-- retries or a concurrent uniqueness winner need a journal read-back.
|
|
2084
|
+
-- This fast path serves both inline and background origin projection.
|
|
2085
|
+
IF cardinality(fresh) = jsonb_array_length(item->'lines') THEN
|
|
2086
|
+
RETURN fresh;
|
|
2087
|
+
END IF;
|
|
1391
2088
|
-- A new READ COMMITTED statement sees a concurrent uniqueness winner.
|
|
1392
2089
|
-- Tombstones retain digest/identity: retry never restores an expired body.
|
|
1393
2090
|
SELECT count(*) INTO mismatches
|
|
@@ -1432,7 +2129,7 @@ export async function attachRuntimeEventSearchTriggers(
|
|
|
1432
2129
|
-- writers lock attempt before Run; this recorder must not invert that.
|
|
1433
2130
|
-- Stable batches are ordered by the leased background prefix and cursor row.
|
|
1434
2131
|
-- The legacy alias path still needs its old transaction mutex.
|
|
1435
|
-
IF NOT ${schema}.
|
|
2132
|
+
IF NOT ${schema}.defer_runtime_event_projection_for_run(observation.run_id) THEN
|
|
1436
2133
|
PERFORM pg_advisory_xact_lock(hashtext(${quoteLiteral(scope)}), hashtext(observation.run_id));
|
|
1437
2134
|
END IF;
|
|
1438
2135
|
SELECT org_id, play_name, run_log_store = 'planet_scale', status,
|
|
@@ -1549,7 +2246,7 @@ export async function attachRuntimeEventSearchTriggers(
|
|
|
1549
2246
|
));
|
|
1550
2247
|
END IF;
|
|
1551
2248
|
IF event_batch IS NOT NULL THEN
|
|
1552
|
-
IF ${schema}.
|
|
2249
|
+
IF ${schema}.defer_runtime_event_projection_for_run(observation.run_id) 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
|
|
1553
2250
|
PERFORM pg_advisory_xact_lock(hashtext(${quoteLiteral(scope)}), hashtext(observation.run_id));
|
|
1554
2251
|
END IF;
|
|
1555
2252
|
-- Normalize old positional writers once per batch under the run mutex.
|
|
@@ -2252,7 +2949,7 @@ export async function attachRuntimeEventSearchTriggers(
|
|
|
2252
2949
|
DECLARE run_closed boolean; search_payload jsonb;
|
|
2253
2950
|
BEGIN
|
|
2254
2951
|
IF NEW.payload_json->'${RUNTIME_EVENT_SEARCH_ONLY_FIELD}' = 'true'::jsonb THEN RETURN NEW; END IF;
|
|
2255
|
-
IF ${schema}.
|
|
2952
|
+
IF ${schema}.defer_runtime_event_projection_for_run(NEW.run_id)
|
|
2256
2953
|
AND NEW.event_type <> ${RUN_SCHEDULER_OUTBOX_EVENT_TYPE_SQL_LITERAL.runCreated} THEN
|
|
2257
2954
|
IF NEW.event_type = ${RUN_SCHEDULER_OUTBOX_EVENT_TYPE_SQL_LITERAL.runEvents} THEN
|
|
2258
2955
|
-- One cheap Run fence captures whether ingress preceded terminal
|
|
@@ -2378,7 +3075,7 @@ export async function attachRuntimeEventSearchTriggers(
|
|
|
2378
3075
|
THEN NEW.runner_log_payload_json->'terminalProgressEvents'
|
|
2379
3076
|
ELSE '[]'::jsonb END) WITH ORDINALITY AS progress(event, position)
|
|
2380
3077
|
));
|
|
2381
|
-
IF ${schema}.
|
|
3078
|
+
IF ${schema}.defer_runtime_event_projection_for_run(NEW.run_id) THEN
|
|
2382
3079
|
INSERT INTO ${schema}.outbox (event_id, run_id, event_type, payload_json, created_at)
|
|
2383
3080
|
VALUES (observation.event_id, observation.run_id, observation.event_type,
|
|
2384
3081
|
observation.payload_json,
|
|
@@ -2474,6 +3171,8 @@ export async function attachRuntimeEventSearchTriggers(
|
|
|
2474
3171
|
await client.query(`REVOKE ALL ON FUNCTION ${schema}.project_outbox_event(${schema}.outbox),
|
|
2475
3172
|
${schema}.project_origin_log_batch(${schema}.outbox, jsonb, text, text, boolean, boolean, timestamptz, jsonb, boolean),
|
|
2476
3173
|
${schema}.independent_runtime_event_projection(),
|
|
3174
|
+
${schema}.new_runs_only_runtime_event_projection(),
|
|
3175
|
+
${schema}.defer_runtime_event_projection_for_run(text),
|
|
2477
3176
|
${schema}.pin_runtime_log_identity(),
|
|
2478
3177
|
${schema}.materialize_runtime_outbox_event(text, text),
|
|
2479
3178
|
${schema}.guard_runtime_outbox_ack(),
|
|
@@ -2499,6 +3198,8 @@ export async function hasRuntimeEventSearchShape(
|
|
|
2499
3198
|
AND EXISTS (SELECT 1 FROM pg_trigger WHERE tgrelid = to_regclass('${schema}.outbox')
|
|
2500
3199
|
AND tgname = 'runtime_outbox_projection_ack_v1' AND tgenabled <> 'D')
|
|
2501
3200
|
AND to_regprocedure('${schema}.defer_runtime_event_projection()') IS NOT NULL
|
|
3201
|
+
AND to_regprocedure('${schema}.new_runs_only_runtime_event_projection()') IS NOT NULL
|
|
3202
|
+
AND to_regprocedure('${schema}.defer_runtime_event_projection_for_run(text)') IS NOT NULL
|
|
2502
3203
|
AND to_regclass('runtime_observability.events') IS NOT NULL
|
|
2503
3204
|
AND to_regclass('runtime_observability.run_log_cursors') IS NOT NULL
|
|
2504
3205
|
AND EXISTS (SELECT 1 FROM pg_attribute
|