deepline 0.3.248 → 0.3.250
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/release.ts +1 -1
- package/dist/bundling-sources/shared_libs/play-runtime/child-execution-strategy.ts +62 -2
- package/dist/bundling-sources/shared_libs/play-runtime/context.ts +49 -3
- package/dist/bundling-sources/shared_libs/play-runtime/play-runs/terminal/job.ts +1 -0
- package/dist/bundling-sources/shared_libs/play-runtime/play-runs/terminal/ports.ts +1 -0
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres-progress.ts +50 -29
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres.ts +94 -26
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/runtime-event-search.ts +394 -93
- package/dist/bundling-sources/shared_libs/plays/static-pipeline.ts +4 -0
- package/dist/cli/index.js +1 -1
- package/dist/cli/index.mjs +1 -1
- package/dist/{compiler-manifest-DavmLa8y.d.mts → compiler-manifest-C4mdRJOP.d.mts} +4 -0
- package/dist/{compiler-manifest-DavmLa8y.d.ts → compiler-manifest-C4mdRJOP.d.ts} +4 -0
- 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/bundle-play-file.d.mts +2 -2
- package/dist/plays/bundle-play-file.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
|
@@ -12,7 +12,7 @@ const EVENTS_TABLE = 'runtime_observability.events';
|
|
|
12
12
|
// Migration readiness tracks this projection body independently of scheduler
|
|
13
13
|
// wire and schema versions. Bump when a changed trigger must be reinstalled.
|
|
14
14
|
export const RUNTIME_EVENT_SEARCH_OUTBOX_PROJECTION_MARKER =
|
|
15
|
-
'runtime-event-search-log-context-
|
|
15
|
+
'runtime-event-search-log-context-v8-terminal-progress-counts-compact-first-progress-channel-aliases';
|
|
16
16
|
export const RUNTIME_EVENTS_RETENTION_MS = 7 * 24 * 60 * 60 * 1000;
|
|
17
17
|
const MAX_SEARCH_WINDOW_MS = RUNTIME_EVENTS_RETENTION_MS;
|
|
18
18
|
const MAX_SEARCH_RESULTS = 200;
|
|
@@ -459,6 +459,7 @@ export async function getRuntimeRunLogPage(
|
|
|
459
459
|
FROM runtime_observability.events
|
|
460
460
|
WHERE runtime_scope = $1 AND org_id = $2 AND run_id = $3
|
|
461
461
|
AND kind = 'log.appended' AND log_seq IS NOT NULL
|
|
462
|
+
AND (log_body IS NOT NULL OR message IS NOT NULL)
|
|
462
463
|
AND ingested_at >= clock_timestamp() - interval '7 days'
|
|
463
464
|
), run_cursor AS (
|
|
464
465
|
SELECT last_seq
|
|
@@ -469,6 +470,7 @@ export async function getRuntimeRunLogPage(
|
|
|
469
470
|
FROM runtime_observability.events
|
|
470
471
|
WHERE runtime_scope = $1 AND org_id = $2 AND run_id = $3
|
|
471
472
|
AND kind = 'log.appended' AND log_seq IS NOT NULL
|
|
473
|
+
AND (log_body IS NOT NULL OR message IS NOT NULL)
|
|
472
474
|
AND ingested_at >= clock_timestamp() - interval '7 days'
|
|
473
475
|
${pageWhere}
|
|
474
476
|
ORDER BY log_seq ${pageOrder}
|
|
@@ -798,56 +800,60 @@ export async function attachRuntimeEventSearchTriggers(
|
|
|
798
800
|
END IF;
|
|
799
801
|
END $create$`);
|
|
800
802
|
await client.query(`
|
|
801
|
-
CREATE OR REPLACE FUNCTION ${schema}.
|
|
803
|
+
CREATE OR REPLACE FUNCTION ${schema}.project_outbox_event(observation ${schema}.outbox) RETURNS void
|
|
802
804
|
LANGUAGE plpgsql SECURITY DEFINER SET search_path = pg_catalog AS $function$
|
|
803
|
-
DECLARE run_org text; play text; item jsonb; line jsonb; event_batch jsonb;
|
|
805
|
+
DECLARE run_org text; play text; compact_logs boolean; run_status text; item jsonb; line jsonb; event_batch jsonb;
|
|
804
806
|
ordinal bigint; line_ordinal bigint; kind text; at_time timestamptz;
|
|
805
807
|
doc jsonb; line_doc jsonb; line_context jsonb;
|
|
806
|
-
eid text; existing_digest text; digest text; assigned_seq bigint;
|
|
808
|
+
eid text; existing_digest text; digest text; assigned_seq bigint; terminal_progress_identity boolean;
|
|
807
809
|
log_channel text; log_position bigint; matched_event_id text; matched_doc jsonb;
|
|
808
810
|
legacy_position_prefix text; log_alias jsonb; envelope_attempt jsonb;
|
|
809
811
|
line_count bigint; fresh_event_ids text[] := ARRAY[]::text[];
|
|
810
812
|
recent_log_keys jsonb := '{}'::jsonb; dedupe_key text;
|
|
811
813
|
terminal_overlap_counts jsonb := '{}'::jsonb;
|
|
812
|
-
recent_log_keys_loaded boolean := false;
|
|
814
|
+
recent_log_keys_loaded boolean := false; recent_log_attempt text;
|
|
813
815
|
BEGIN
|
|
814
|
-
--
|
|
815
|
-
--
|
|
816
|
-
|
|
817
|
-
|
|
816
|
+
-- Log ordering owns a transaction-scoped mutex, independent of the
|
|
817
|
+
-- scheduler's Run/attempt row-lock order. In particular, old completion
|
|
818
|
+
-- writers lock attempt before Run; this recorder must not invert that.
|
|
819
|
+
PERFORM pg_advisory_xact_lock(hashtext(${quoteLiteral(scope)}), hashtext(observation.run_id));
|
|
820
|
+
SELECT org_id, play_name, run_log_store = 'planet_scale', status
|
|
821
|
+
INTO run_org, play, compact_logs, run_status
|
|
822
|
+
FROM ${schema}.runs WHERE run_id = observation.run_id;
|
|
818
823
|
IF run_org IS NULL OR play IS NULL THEN
|
|
819
824
|
RAISE EXCEPTION 'Outbox search projection has no matching run';
|
|
820
825
|
END IF;
|
|
821
|
-
IF
|
|
822
|
-
event_batch :=
|
|
826
|
+
IF observation.event_type = 'run.events' THEN
|
|
827
|
+
event_batch := observation.payload_json->'events';
|
|
823
828
|
IF jsonb_typeof(event_batch) IS DISTINCT FROM 'array' THEN
|
|
824
829
|
RAISE EXCEPTION 'Search projection requires run.events array';
|
|
825
830
|
END IF;
|
|
826
|
-
IF jsonb_array_length(event_batch) = 0
|
|
831
|
+
IF jsonb_array_length(event_batch) = 0
|
|
832
|
+
AND observation.payload_json->>'logBodyStore' IS DISTINCT FROM 'planet_scale' THEN
|
|
827
833
|
RAISE EXCEPTION 'Search projection requires nonempty run.events array';
|
|
828
834
|
END IF;
|
|
829
|
-
ELSIF
|
|
830
|
-
AND
|
|
831
|
-
event_batch :=
|
|
835
|
+
ELSIF observation.event_type IN ('run.completed', 'run.failed', 'runner.logs')
|
|
836
|
+
AND observation.payload_json ? 'terminalProgressEvents' THEN
|
|
837
|
+
event_batch := observation.payload_json->'terminalProgressEvents';
|
|
832
838
|
IF jsonb_typeof(event_batch) IS DISTINCT FROM 'array' THEN
|
|
833
839
|
RAISE EXCEPTION 'Search projection requires terminalProgressEvents array';
|
|
834
840
|
END IF;
|
|
835
841
|
END IF;
|
|
836
842
|
-- Ordinary terminal SQL already binds the attempt in its counter
|
|
837
843
|
-- segment; compact carriers also name it at the top level.
|
|
838
|
-
envelope_attempt := coalesce(NULLIF(
|
|
839
|
-
|
|
844
|
+
envelope_attempt := coalesce(NULLIF(observation.payload_json->'producerAttempt', 'null'::jsonb),
|
|
845
|
+
observation.payload_json->'terminalWorkCounterSegment'->'attempt');
|
|
840
846
|
-- The retained envelope has its own position space: it can contain
|
|
841
847
|
-- diagnostics absent from both live delivery and ctx-only replay.
|
|
842
|
-
IF
|
|
843
|
-
AND
|
|
848
|
+
IF observation.event_type IN ('run.completed', 'run.failed', 'runner.logs')
|
|
849
|
+
AND observation.payload_json ? 'terminalLogLines'
|
|
844
850
|
AND (envelope_attempt #>> '{}') ~ '^[1-9][0-9]{0,5}$' THEN
|
|
845
851
|
event_batch := coalesce(event_batch, '[]'::jsonb) || jsonb_build_array(
|
|
846
852
|
jsonb_strip_nulls(jsonb_build_object(
|
|
847
|
-
'type', 'log.appended', 'runId',
|
|
853
|
+
'type', 'log.appended', 'runId', observation.run_id, 'source', 'terminal',
|
|
848
854
|
'producerAttempt', envelope_attempt, 'channelOffset', 0,
|
|
849
|
-
'lines',
|
|
850
|
-
'lineContexts',
|
|
855
|
+
'lines', observation.payload_json->'terminalLogLines',
|
|
856
|
+
'lineContexts', observation.payload_json->'terminalLogContexts'
|
|
851
857
|
))
|
|
852
858
|
);
|
|
853
859
|
END IF;
|
|
@@ -860,21 +866,21 @@ export async function attachRuntimeEventSearchTriggers(
|
|
|
860
866
|
UPDATE runtime_observability.events AS event SET search_doc = event.search_doc ||
|
|
861
867
|
jsonb_build_object('logContentDigest', event.source_digest) ||
|
|
862
868
|
CASE WHEN NOT (event.search_doc ? 'logPositions')
|
|
863
|
-
AND left(event.event_id, length('log:' ||
|
|
864
|
-
'log:' ||
|
|
865
|
-
AND substring(event.event_id FROM length('log:' ||
|
|
869
|
+
AND left(event.event_id, length('log:' || observation.run_id || ':' || (event.search_doc->>'producerAttempt') || ':')) =
|
|
870
|
+
'log:' || observation.run_id || ':' || (event.search_doc->>'producerAttempt') || ':'
|
|
871
|
+
AND substring(event.event_id FROM length('log:' || observation.run_id || ':' || (event.search_doc->>'producerAttempt') || ':') + 1) ~ '^[0-9]{1,12}$'
|
|
866
872
|
THEN jsonb_build_object('logPositions', jsonb_build_object(
|
|
867
873
|
CASE WHEN event.search_doc->>'source' = 'coordinator' THEN 'replay' ELSE 'worker' END,
|
|
868
|
-
substring(event.event_id FROM length('log:' ||
|
|
874
|
+
substring(event.event_id FROM length('log:' || observation.run_id || ':' || (event.search_doc->>'producerAttempt') || ':') + 1)::bigint))
|
|
869
875
|
ELSE '{}'::jsonb END
|
|
870
876
|
WHERE event.runtime_scope = ${quoteLiteral(scope)} AND event.org_id = run_org
|
|
871
|
-
AND event.run_id =
|
|
877
|
+
AND event.run_id = observation.run_id AND event.kind = 'log.appended'
|
|
872
878
|
AND event.source_digest IS NOT NULL AND NOT (event.search_doc ? 'logContentDigest')
|
|
873
879
|
AND (event.search_doc->>'producerAttempt') ~ '^[1-9][0-9]{0,5}$';
|
|
874
880
|
END IF;
|
|
875
881
|
FOR item, ordinal IN SELECT value, ordinality FROM jsonb_array_elements(event_batch) WITH ORDINALITY LOOP
|
|
876
882
|
IF jsonb_typeof(item) IS DISTINCT FROM 'object'
|
|
877
|
-
OR item->>'runId' IS DISTINCT FROM
|
|
883
|
+
OR item->>'runId' IS DISTINCT FROM observation.run_id THEN
|
|
878
884
|
RAISE EXCEPTION 'Search projection event has invalid run identity';
|
|
879
885
|
END IF;
|
|
880
886
|
kind := coalesce(item->>'type', item->>'key');
|
|
@@ -889,7 +895,7 @@ export async function attachRuntimeEventSearchTriggers(
|
|
|
889
895
|
END IF;
|
|
890
896
|
at_time := CASE WHEN (item->>'occurredAt') ~ '^[0-9]{12,14}$'
|
|
891
897
|
THEN to_timestamp((item->>'occurredAt')::double precision / 1000)
|
|
892
|
-
ELSE
|
|
898
|
+
ELSE observation.created_at END;
|
|
893
899
|
doc := jsonb_strip_nulls(jsonb_build_object(
|
|
894
900
|
'kind', kind, 'source', left(item->>'source', 64),
|
|
895
901
|
'producerAttempt', CASE WHEN (item->>'producerAttempt') ~ '^[0-9]{1,6}$'
|
|
@@ -920,19 +926,22 @@ export async function attachRuntimeEventSearchTriggers(
|
|
|
920
926
|
END IF;
|
|
921
927
|
IF (item->>'source' IS DISTINCT FROM 'coordinator')
|
|
922
928
|
AND NOT coalesce((item->>'channelOffset') ~ '^[0-9]{1,12}$', false) THEN
|
|
923
|
-
IF NOT recent_log_keys_loaded THEN
|
|
929
|
+
IF NOT recent_log_keys_loaded OR recent_log_attempt IS DISTINCT FROM item->>'producerAttempt' THEN
|
|
924
930
|
SELECT coalesce(jsonb_object_agg(recent.dedupe_key, true), '{}'::jsonb)
|
|
925
931
|
INTO recent_log_keys
|
|
926
932
|
FROM (
|
|
927
|
-
SELECT encode(sha256(convert_to(btrim(event.log_body), 'UTF8')), 'hex') AS dedupe_key
|
|
933
|
+
SELECT coalesce(event.log_text_digest, encode(sha256(convert_to(btrim(event.log_body), 'UTF8')), 'hex')) AS dedupe_key
|
|
928
934
|
FROM runtime_observability.events AS event
|
|
929
935
|
WHERE event.runtime_scope = ${quoteLiteral(scope)} AND event.org_id = run_org
|
|
930
|
-
AND event.run_id =
|
|
931
|
-
AND event.log_seq IS NOT NULL AND event.log_body IS NOT NULL
|
|
936
|
+
AND event.run_id = observation.run_id AND event.kind = 'log.appended'
|
|
937
|
+
AND event.log_seq IS NOT NULL AND (event.log_text_digest IS NOT NULL OR event.log_body IS NOT NULL)
|
|
938
|
+
AND (item->>'producerAttempt' IS NULL
|
|
939
|
+
OR event.search_doc->>'producerAttempt' = item->>'producerAttempt')
|
|
932
940
|
ORDER BY event.log_seq DESC
|
|
933
941
|
LIMIT 256
|
|
934
942
|
) AS recent;
|
|
935
943
|
recent_log_keys_loaded := true;
|
|
944
|
+
recent_log_attempt := item->>'producerAttempt';
|
|
936
945
|
END IF;
|
|
937
946
|
dedupe_key := encode(
|
|
938
947
|
sha256(convert_to(btrim(line #>> '{}'), 'UTF8')), 'hex'
|
|
@@ -973,9 +982,9 @@ export async function attachRuntimeEventSearchTriggers(
|
|
|
973
982
|
ELSE doc || jsonb_build_object('context', line_context) END;
|
|
974
983
|
eid := CASE WHEN (item->>'channelOffset') ~ '^[0-9]{1,12}$'
|
|
975
984
|
AND (item->>'producerAttempt') ~ '^[0-9]{1,6}$'
|
|
976
|
-
THEN 'log:' ||
|
|
985
|
+
THEN 'log:' || observation.run_id || ':' || (item->>'producerAttempt') || ':' ||
|
|
977
986
|
((item->>'channelOffset')::bigint + line_ordinal - 1)::text
|
|
978
|
-
ELSE
|
|
987
|
+
ELSE observation.event_id || ':' || ordinal || ':' || line_ordinal END;
|
|
979
988
|
digest := CASE WHEN line_context IS NULL
|
|
980
989
|
THEN encode(sha256(convert_to(line #>> '{}', 'UTF8')), 'hex')
|
|
981
990
|
ELSE encode(sha256(convert_to(jsonb_build_array(line #>> '{}', line_context)::text, 'UTF8')), 'hex') END;
|
|
@@ -987,9 +996,9 @@ export async function attachRuntimeEventSearchTriggers(
|
|
|
987
996
|
WHEN 'coordinator' THEN 'replay' WHEN 'terminal' THEN 'envelope'
|
|
988
997
|
ELSE 'worker' END;
|
|
989
998
|
log_position := (item->>'channelOffset')::bigint + line_ordinal - 1;
|
|
990
|
-
legacy_position_prefix := 'log:' ||
|
|
999
|
+
legacy_position_prefix := 'log:' || observation.run_id || ':' || (item->>'producerAttempt') || ':';
|
|
991
1000
|
eid := CASE WHEN log_channel = 'worker' THEN legacy_position_prefix
|
|
992
|
-
ELSE 'log-' || log_channel || ':' ||
|
|
1001
|
+
ELSE 'log-' || log_channel || ':' || observation.run_id || ':' || (item->>'producerAttempt') || ':' END
|
|
993
1002
|
|| log_position::text;
|
|
994
1003
|
-- Older coordinator replays used the worker-shaped primary
|
|
995
1004
|
-- key. Do not overwrite it when a delayed worker owns a
|
|
@@ -1000,7 +1009,7 @@ export async function attachRuntimeEventSearchTriggers(
|
|
|
1000
1009
|
AND search_doc->>'source' = 'coordinator'
|
|
1001
1010
|
AND NOT (coalesce(search_doc->'logPositions', '{}'::jsonb) ? 'worker')
|
|
1002
1011
|
) THEN
|
|
1003
|
-
eid := 'log-worker:' ||
|
|
1012
|
+
eid := 'log-worker:' || observation.run_id || ':' || (item->>'producerAttempt') || ':' || log_position::text;
|
|
1004
1013
|
END IF;
|
|
1005
1014
|
log_alias := jsonb_build_object('producerAttempt', (item->>'producerAttempt')::integer,
|
|
1006
1015
|
'logPositions', jsonb_build_object(log_channel, log_position));
|
|
@@ -1011,7 +1020,7 @@ export async function attachRuntimeEventSearchTriggers(
|
|
|
1011
1020
|
INTO matched_event_id, existing_digest, matched_doc
|
|
1012
1021
|
FROM runtime_observability.events AS event
|
|
1013
1022
|
WHERE event.runtime_scope = ${quoteLiteral(scope)} AND event.org_id = run_org
|
|
1014
|
-
AND event.run_id =
|
|
1023
|
+
AND event.run_id = observation.run_id AND event.kind = 'log.appended'
|
|
1015
1024
|
AND event.search_doc @> log_alias;
|
|
1016
1025
|
IF matched_event_id IS NOT NULL THEN
|
|
1017
1026
|
IF existing_digest IS DISTINCT FROM digest THEN
|
|
@@ -1026,7 +1035,7 @@ export async function attachRuntimeEventSearchTriggers(
|
|
|
1026
1035
|
INTO matched_event_id, matched_doc
|
|
1027
1036
|
FROM runtime_observability.events AS event
|
|
1028
1037
|
WHERE event.runtime_scope = ${quoteLiteral(scope)} AND event.org_id = run_org
|
|
1029
|
-
AND event.run_id =
|
|
1038
|
+
AND event.run_id = observation.run_id AND event.kind = 'log.appended'
|
|
1030
1039
|
AND event.search_doc @> jsonb_build_object(
|
|
1031
1040
|
'producerAttempt', (item->>'producerAttempt')::integer, 'logContentDigest', digest)
|
|
1032
1041
|
AND event.source_digest = digest
|
|
@@ -1043,12 +1052,24 @@ export async function attachRuntimeEventSearchTriggers(
|
|
|
1043
1052
|
END IF;
|
|
1044
1053
|
line_doc := line_doc || log_alias || jsonb_build_object('logContentDigest', digest);
|
|
1045
1054
|
END IF;
|
|
1055
|
+
-- Serialize with terminal acceptance on the log mutex. A late
|
|
1056
|
+
-- retry of an accepted position is harmless; a new position
|
|
1057
|
+
-- after terminal closure must never recreate expired history.
|
|
1058
|
+
IF compact_logs AND observation.event_type = 'run.events'
|
|
1059
|
+
AND run_status IN ('completed', 'failed', 'cancelled')
|
|
1060
|
+
AND NOT EXISTS (SELECT 1 FROM runtime_observability.events
|
|
1061
|
+
WHERE runtime_scope = ${quoteLiteral(scope)} AND org_id = run_org
|
|
1062
|
+
AND event_id = eid) THEN
|
|
1063
|
+
RAISE EXCEPTION 'Run log ingress is closed after terminal acceptance';
|
|
1064
|
+
END IF;
|
|
1046
1065
|
INSERT INTO runtime_observability.events (
|
|
1047
1066
|
runtime_scope, org_id, event_id, occurred_at, run_id, play_name,
|
|
1048
|
-
kind, level, message, log_body, source_digest, search_doc
|
|
1067
|
+
kind, level, message, log_body, source_digest, log_text_digest, search_doc
|
|
1049
1068
|
) VALUES (
|
|
1050
|
-
${quoteLiteral(scope)}, run_org, eid, at_time,
|
|
1051
|
-
'log.appended', NULL, left(line #>> '{}', 2048)
|
|
1069
|
+
${quoteLiteral(scope)}, run_org, eid, at_time, observation.run_id, play,
|
|
1070
|
+
'log.appended', NULL, CASE WHEN compact_logs THEN NULL ELSE left(line #>> '{}', 2048) END,
|
|
1071
|
+
line #>> '{}', digest,
|
|
1072
|
+
encode(sha256(convert_to(btrim(line #>> '{}'), 'UTF8')), 'hex'), line_doc
|
|
1052
1073
|
) ON CONFLICT DO NOTHING;
|
|
1053
1074
|
IF FOUND THEN
|
|
1054
1075
|
fresh_event_ids := array_append(fresh_event_ids, eid);
|
|
@@ -1062,45 +1083,64 @@ export async function attachRuntimeEventSearchTriggers(
|
|
|
1062
1083
|
END IF;
|
|
1063
1084
|
END LOOP;
|
|
1064
1085
|
ELSE
|
|
1086
|
+
-- Final progress may arrive in the first terminal receipt and
|
|
1087
|
+
-- again in completion. One attempt/ordinal owns one immutable fact.
|
|
1088
|
+
terminal_progress_identity := observation.event_type IN ('runner.logs', 'run.completed', 'run.failed')
|
|
1089
|
+
AND coalesce((observation.payload_json->>'producerAttempt') ~ '^[1-9][0-9]{0,5}$', false);
|
|
1090
|
+
eid := CASE WHEN terminal_progress_identity
|
|
1091
|
+
THEN 'terminal-progress:' || observation.run_id || ':' ||
|
|
1092
|
+
(observation.payload_json->>'producerAttempt') || ':' ||
|
|
1093
|
+
coalesce(item->>'_runtimeProgressOrdinal', ordinal::text)
|
|
1094
|
+
ELSE observation.event_id || ':' || ordinal END;
|
|
1095
|
+
digest := CASE WHEN terminal_progress_identity
|
|
1096
|
+
THEN encode(sha256(convert_to(item::text, 'UTF8')), 'hex') ELSE NULL END;
|
|
1065
1097
|
INSERT INTO runtime_observability.events (
|
|
1066
1098
|
runtime_scope, org_id, event_id, occurred_at, run_id, play_name,
|
|
1067
|
-
kind, search_doc
|
|
1099
|
+
kind, source_digest, search_doc
|
|
1068
1100
|
) VALUES (
|
|
1069
|
-
${quoteLiteral(scope)}, run_org,
|
|
1070
|
-
at_time,
|
|
1071
|
-
);
|
|
1101
|
+
${quoteLiteral(scope)}, run_org, eid,
|
|
1102
|
+
at_time, observation.run_id, play, kind, digest, doc
|
|
1103
|
+
) ON CONFLICT DO NOTHING;
|
|
1104
|
+
IF NOT FOUND AND terminal_progress_identity THEN
|
|
1105
|
+
SELECT source_digest INTO existing_digest FROM runtime_observability.events
|
|
1106
|
+
WHERE runtime_scope = ${quoteLiteral(scope)} AND org_id = run_org AND event_id = eid;
|
|
1107
|
+
IF existing_digest IS DISTINCT FROM digest THEN
|
|
1108
|
+
RAISE EXCEPTION 'Progress event identity changed its content';
|
|
1109
|
+
END IF;
|
|
1110
|
+
END IF;
|
|
1072
1111
|
END IF;
|
|
1073
1112
|
END LOOP;
|
|
1074
1113
|
END IF;
|
|
1075
|
-
IF
|
|
1114
|
+
IF observation.event_type IN (
|
|
1076
1115
|
'run.created', 'run.started', 'run.waiting', 'run.resumed',
|
|
1077
1116
|
'run.completed', 'run.failed', 'run.cancelled'
|
|
1078
|
-
) AND NOT (
|
|
1079
|
-
AND
|
|
1117
|
+
) AND NOT (observation.event_type IN ('run.waiting', 'run.resumed')
|
|
1118
|
+
AND observation.payload_json->>'kind' = 'detached_runner') THEN
|
|
1080
1119
|
INSERT INTO runtime_observability.events (
|
|
1081
1120
|
runtime_scope, org_id, event_id, occurred_at, run_id, play_name,
|
|
1082
1121
|
kind, search_doc
|
|
1083
1122
|
) VALUES (
|
|
1084
|
-
${quoteLiteral(scope)}, run_org,
|
|
1085
|
-
|
|
1086
|
-
);
|
|
1123
|
+
${quoteLiteral(scope)}, run_org, observation.event_id, observation.created_at,
|
|
1124
|
+
observation.run_id, play, observation.event_type, jsonb_build_object('kind', observation.event_type, 'source', 'system')
|
|
1125
|
+
) ON CONFLICT DO NOTHING;
|
|
1126
|
+
END IF;
|
|
1127
|
+
IF observation.event_type IN ('run.completed', 'run.failed', 'runner.logs') THEN
|
|
1087
1128
|
-- Detached runners deliver their retained ctx.log tail in the terminal
|
|
1088
1129
|
-- outbox fact rather than a run.events batch. Project it from that
|
|
1089
1130
|
-- authoritative write so search and the existing log stream agree.
|
|
1090
|
-
IF
|
|
1091
|
-
AND
|
|
1092
|
-
AND NOT (NEW.payload_json ? 'terminalLogLines') THEN
|
|
1131
|
+
IF observation.payload_json ? 'terminalLogContexts'
|
|
1132
|
+
AND NOT (observation.payload_json ? 'terminalLogLines') THEN
|
|
1093
1133
|
RAISE EXCEPTION 'Search projection requires terminalLogLines for terminalLogContexts';
|
|
1094
1134
|
END IF;
|
|
1095
|
-
IF
|
|
1096
|
-
AND
|
|
1135
|
+
IF observation.event_type IN ('run.completed', 'run.failed', 'runner.logs')
|
|
1136
|
+
AND observation.payload_json ? 'terminalLogLines'
|
|
1097
1137
|
AND NOT coalesce((envelope_attempt #>> '{}') ~ '^[1-9][0-9]{0,5}$', false) THEN
|
|
1098
|
-
IF jsonb_typeof(
|
|
1138
|
+
IF jsonb_typeof(observation.payload_json->'terminalLogLines') IS DISTINCT FROM 'array' THEN
|
|
1099
1139
|
RAISE EXCEPTION 'Search projection requires terminalLogLines array';
|
|
1100
1140
|
END IF;
|
|
1101
|
-
IF
|
|
1102
|
-
AND (jsonb_typeof(
|
|
1103
|
-
OR jsonb_array_length(
|
|
1141
|
+
IF observation.payload_json ? 'terminalLogContexts'
|
|
1142
|
+
AND (jsonb_typeof(observation.payload_json->'terminalLogContexts') IS DISTINCT FROM 'array'
|
|
1143
|
+
OR jsonb_array_length(observation.payload_json->'terminalLogContexts') <> jsonb_array_length(observation.payload_json->'terminalLogLines')) THEN
|
|
1104
1144
|
RAISE EXCEPTION 'Search projection requires terminalLogContexts aligned to terminalLogLines';
|
|
1105
1145
|
END IF;
|
|
1106
1146
|
-- Count matching bodies already projected, including progress lines
|
|
@@ -1115,30 +1155,34 @@ export async function attachRuntimeEventSearchTriggers(
|
|
|
1115
1155
|
FROM (
|
|
1116
1156
|
SELECT candidate.dedupe_key, count(*) AS occurrences
|
|
1117
1157
|
FROM (
|
|
1118
|
-
SELECT encode(sha256(convert_to(btrim(recent.log_body), 'UTF8')), 'hex') AS dedupe_key
|
|
1158
|
+
SELECT coalesce(recent.log_text_digest, encode(sha256(convert_to(btrim(recent.log_body), 'UTF8')), 'hex')) AS dedupe_key
|
|
1119
1159
|
FROM (
|
|
1120
|
-
SELECT event.log_body
|
|
1160
|
+
SELECT event.log_body, event.log_text_digest
|
|
1121
1161
|
FROM runtime_observability.events AS event
|
|
1122
1162
|
WHERE event.runtime_scope = ${quoteLiteral(scope)}
|
|
1123
|
-
AND event.org_id = run_org AND event.run_id =
|
|
1163
|
+
AND event.org_id = run_org AND event.run_id = observation.run_id
|
|
1124
1164
|
AND event.kind = 'log.appended'
|
|
1125
|
-
AND
|
|
1165
|
+
AND (observation.payload_json->>'producerAttempt' IS NULL
|
|
1166
|
+
OR event.search_doc->>'producerAttempt' = observation.payload_json->>'producerAttempt')
|
|
1167
|
+
AND event.log_seq IS NOT NULL AND (event.log_text_digest IS NOT NULL OR event.log_body IS NOT NULL)
|
|
1126
1168
|
ORDER BY event.log_seq DESC
|
|
1127
1169
|
LIMIT 256
|
|
1128
1170
|
) AS recent
|
|
1129
1171
|
UNION ALL
|
|
1130
|
-
SELECT encode(sha256(convert_to(btrim(event.log_body), 'UTF8')), 'hex')
|
|
1172
|
+
SELECT coalesce(event.log_text_digest, encode(sha256(convert_to(btrim(event.log_body), 'UTF8')), 'hex'))
|
|
1131
1173
|
FROM runtime_observability.events AS event
|
|
1132
1174
|
WHERE event.runtime_scope = ${quoteLiteral(scope)}
|
|
1133
|
-
AND event.org_id = run_org AND event.run_id =
|
|
1134
|
-
AND event.kind = 'log.appended' AND event.log_body IS NOT NULL
|
|
1175
|
+
AND event.org_id = run_org AND event.run_id = observation.run_id
|
|
1176
|
+
AND event.kind = 'log.appended' AND (event.log_text_digest IS NOT NULL OR event.log_body IS NOT NULL)
|
|
1135
1177
|
AND event.event_id = ANY(fresh_event_ids)
|
|
1178
|
+
AND (observation.payload_json->>'producerAttempt' IS NULL
|
|
1179
|
+
OR event.search_doc->>'producerAttempt' = observation.payload_json->>'producerAttempt')
|
|
1136
1180
|
) AS candidate
|
|
1137
1181
|
GROUP BY candidate.dedupe_key
|
|
1138
1182
|
) AS counted;
|
|
1139
1183
|
FOR line, line_ordinal IN
|
|
1140
1184
|
SELECT value, ordinality
|
|
1141
|
-
FROM jsonb_array_elements(
|
|
1185
|
+
FROM jsonb_array_elements(observation.payload_json->'terminalLogLines') WITH ORDINALITY
|
|
1142
1186
|
LOOP
|
|
1143
1187
|
IF jsonb_typeof(line) IS DISTINCT FROM 'string' THEN
|
|
1144
1188
|
RAISE EXCEPTION 'Search projection requires terminal log strings';
|
|
@@ -1154,8 +1198,8 @@ export async function attachRuntimeEventSearchTriggers(
|
|
|
1154
1198
|
CONTINUE;
|
|
1155
1199
|
END IF;
|
|
1156
1200
|
line_context := NULL;
|
|
1157
|
-
IF
|
|
1158
|
-
line_context :=
|
|
1201
|
+
IF observation.payload_json ? 'terminalLogContexts' THEN
|
|
1202
|
+
line_context := observation.payload_json->'terminalLogContexts'->((line_ordinal - 1)::int);
|
|
1159
1203
|
IF jsonb_typeof(line_context) = 'null' THEN
|
|
1160
1204
|
line_context := NULL;
|
|
1161
1205
|
ELSIF jsonb_typeof(line_context) IS DISTINCT FROM 'object' THEN
|
|
@@ -1179,28 +1223,32 @@ export async function attachRuntimeEventSearchTriggers(
|
|
|
1179
1223
|
RAISE EXCEPTION 'Search projection terminal log context has an unsupported key or value';
|
|
1180
1224
|
END IF;
|
|
1181
1225
|
END IF;
|
|
1182
|
-
line_doc := jsonb_build_object(
|
|
1226
|
+
line_doc := jsonb_strip_nulls(jsonb_build_object(
|
|
1227
|
+
'kind', 'log.appended', 'source', 'terminal',
|
|
1228
|
+
'producerAttempt', observation.payload_json->'producerAttempt'
|
|
1229
|
+
));
|
|
1183
1230
|
IF line_context IS NOT NULL THEN
|
|
1184
1231
|
line_doc := line_doc || jsonb_build_object('context', line_context);
|
|
1185
1232
|
END IF;
|
|
1186
1233
|
INSERT INTO runtime_observability.events (
|
|
1187
1234
|
runtime_scope, org_id, event_id, occurred_at, run_id, play_name,
|
|
1188
|
-
kind, level, message, log_body, source_digest, search_doc
|
|
1235
|
+
kind, level, message, log_body, source_digest, log_text_digest, search_doc
|
|
1189
1236
|
) VALUES (
|
|
1190
|
-
${quoteLiteral(scope)}, run_org,
|
|
1191
|
-
|
|
1192
|
-
left(regexp_replace(line #>> '{}',
|
|
1193
|
-
'^' || chr(1) || 'prov:[^' || chr(1) || ']*' || chr(1), ''), 2048),
|
|
1237
|
+
${quoteLiteral(scope)}, run_org, observation.event_id || ':log:' || line_ordinal,
|
|
1238
|
+
observation.created_at, observation.run_id, play, 'log.appended', NULL,
|
|
1239
|
+
CASE WHEN compact_logs THEN NULL ELSE left(regexp_replace(line #>> '{}',
|
|
1240
|
+
'^' || chr(1) || 'prov:[^' || chr(1) || ']*' || chr(1), ''), 2048) END,
|
|
1194
1241
|
line #>> '{}',
|
|
1195
1242
|
CASE WHEN line_context IS NULL
|
|
1196
1243
|
THEN encode(sha256(convert_to(line #>> '{}', 'UTF8')), 'hex')
|
|
1197
1244
|
ELSE encode(sha256(convert_to(jsonb_build_array(line #>> '{}', line_context)::text, 'UTF8')), 'hex') END,
|
|
1245
|
+
encode(sha256(convert_to(btrim(line #>> '{}'), 'UTF8')), 'hex'),
|
|
1198
1246
|
line_doc
|
|
1199
1247
|
) ON CONFLICT DO NOTHING;
|
|
1200
1248
|
IF FOUND THEN
|
|
1201
1249
|
fresh_event_ids := array_append(
|
|
1202
1250
|
fresh_event_ids,
|
|
1203
|
-
|
|
1251
|
+
observation.event_id || ':log:' || line_ordinal
|
|
1204
1252
|
);
|
|
1205
1253
|
END IF;
|
|
1206
1254
|
END LOOP;
|
|
@@ -1210,7 +1258,7 @@ export async function attachRuntimeEventSearchTriggers(
|
|
|
1210
1258
|
IF line_count > 0 THEN
|
|
1211
1259
|
INSERT INTO runtime_observability.run_log_cursors (
|
|
1212
1260
|
runtime_scope, org_id, run_id, last_seq
|
|
1213
|
-
) VALUES (${quoteLiteral(scope)}, run_org,
|
|
1261
|
+
) VALUES (${quoteLiteral(scope)}, run_org, observation.run_id, line_count)
|
|
1214
1262
|
ON CONFLICT (runtime_scope, org_id, run_id)
|
|
1215
1263
|
DO UPDATE SET last_seq = runtime_observability.run_log_cursors.last_seq + EXCLUDED.last_seq,
|
|
1216
1264
|
updated_at = clock_timestamp()
|
|
@@ -1222,20 +1270,168 @@ export async function attachRuntimeEventSearchTriggers(
|
|
|
1222
1270
|
AND event.org_id = run_org
|
|
1223
1271
|
AND event.event_id = fresh.event_id;
|
|
1224
1272
|
END IF;
|
|
1273
|
+
RETURN;
|
|
1274
|
+
END
|
|
1275
|
+
$function$
|
|
1276
|
+
`);
|
|
1277
|
+
await client.query(`
|
|
1278
|
+
CREATE OR REPLACE FUNCTION ${schema}.without_log_events(batch jsonb) RETURNS jsonb
|
|
1279
|
+
LANGUAGE sql IMMUTABLE SET search_path = pg_catalog AS $function$
|
|
1280
|
+
SELECT coalesce(jsonb_agg(item.value ORDER BY item.ordinality), '[]'::jsonb)
|
|
1281
|
+
FROM jsonb_array_elements(batch) WITH ORDINALITY AS item(value, ordinality)
|
|
1282
|
+
WHERE coalesce(item.value->>'type', item.value->>'key') IS DISTINCT FROM 'log.appended'
|
|
1283
|
+
$function$
|
|
1284
|
+
`);
|
|
1285
|
+
await client.query(`
|
|
1286
|
+
CREATE OR REPLACE FUNCTION ${schema}.without_runtime_log_tail(value jsonb) RETURNS jsonb
|
|
1287
|
+
LANGUAGE sql IMMUTABLE SET search_path = pg_catalog AS $function$
|
|
1288
|
+
SELECT CASE
|
|
1289
|
+
-- This reserved metadata shape is produced by the runtime. Authored
|
|
1290
|
+
-- output/checkpoint strings and unrelated metadata are left intact.
|
|
1291
|
+
WHEN jsonb_typeof(value#>'{_metadata,runLogTail,tail}') = 'array'
|
|
1292
|
+
AND jsonb_typeof(value#>'{_metadata,runLogTail,totalLogCount}') = 'number'
|
|
1293
|
+
THEN value #- '{_metadata,runLogTail}' ELSE value END
|
|
1294
|
+
$function$
|
|
1295
|
+
`);
|
|
1296
|
+
await client.query(`
|
|
1297
|
+
CREATE OR REPLACE FUNCTION ${schema}.compact_runner_log_envelope(value jsonb) RETURNS jsonb
|
|
1298
|
+
LANGUAGE sql IMMUTABLE SET search_path = pg_catalog AS $function$
|
|
1299
|
+
WITH envelope AS (
|
|
1300
|
+
SELECT CASE WHEN jsonb_typeof(value) = 'object' THEN
|
|
1301
|
+
(value - 'logContexts') || CASE WHEN value ? 'logs'
|
|
1302
|
+
THEN jsonb_build_object('logs', '[]'::jsonb) ELSE '{}'::jsonb END
|
|
1303
|
+
ELSE value END AS result
|
|
1304
|
+
)
|
|
1305
|
+
SELECT CASE WHEN result ? 'output' THEN jsonb_set(result, '{output}',
|
|
1306
|
+
${schema}.without_runtime_log_tail(result->'output')) ELSE result END
|
|
1307
|
+
FROM envelope
|
|
1308
|
+
$function$
|
|
1309
|
+
`);
|
|
1310
|
+
await client.query(`
|
|
1311
|
+
CREATE OR REPLACE FUNCTION ${schema}.search_outbox_event() RETURNS trigger
|
|
1312
|
+
LANGUAGE plpgsql SECURITY DEFINER SET search_path = pg_catalog AS $function$
|
|
1313
|
+
DECLARE compact_logs boolean;
|
|
1314
|
+
BEGIN
|
|
1315
|
+
-- Project the original validated observation before replacing its
|
|
1316
|
+
-- transport body. Failure rolls back the outbox and canonical rows.
|
|
1317
|
+
PERFORM ${schema}.project_outbox_event(NEW);
|
|
1318
|
+
SELECT run_log_store = 'planet_scale' INTO compact_logs
|
|
1319
|
+
FROM ${schema}.runs WHERE run_id = NEW.run_id;
|
|
1320
|
+
IF compact_logs THEN
|
|
1321
|
+
NEW.payload_json := NEW.payload_json - 'terminalLogLines' - 'terminalLogContexts';
|
|
1322
|
+
IF NEW.event_type = 'run.events' THEN
|
|
1323
|
+
NEW.payload_json := jsonb_set(NEW.payload_json, '{events}',
|
|
1324
|
+
${schema}.without_log_events(NEW.payload_json->'events'))
|
|
1325
|
+
|| jsonb_build_object('logBodyStore', 'planet_scale');
|
|
1326
|
+
END IF;
|
|
1327
|
+
IF NEW.payload_json ? 'terminalProgressEvents' THEN
|
|
1328
|
+
NEW.payload_json := jsonb_set(NEW.payload_json, '{terminalProgressEvents}',
|
|
1329
|
+
${schema}.without_log_events(NEW.payload_json->'terminalProgressEvents'));
|
|
1330
|
+
END IF;
|
|
1331
|
+
IF NEW.event_type = 'run.failed' AND NEW.payload_json ? 'result' THEN
|
|
1332
|
+
NEW.payload_json := jsonb_set(NEW.payload_json, '{result}',
|
|
1333
|
+
${schema}.compact_runner_log_envelope(NEW.payload_json->'result'));
|
|
1334
|
+
END IF;
|
|
1335
|
+
IF NEW.event_type = 'run.completed' AND NEW.payload_json ? 'output' THEN
|
|
1336
|
+
NEW.payload_json := jsonb_set(NEW.payload_json, '{output}',
|
|
1337
|
+
${schema}.without_runtime_log_tail(NEW.payload_json->'output'));
|
|
1338
|
+
END IF;
|
|
1339
|
+
END IF;
|
|
1225
1340
|
RETURN NEW;
|
|
1226
1341
|
END
|
|
1227
1342
|
$function$
|
|
1228
1343
|
`);
|
|
1229
|
-
await client.query(
|
|
1230
|
-
|
|
1231
|
-
|
|
1344
|
+
await client.query(`
|
|
1345
|
+
CREATE OR REPLACE FUNCTION ${schema}.accept_runner_log_payload() RETURNS trigger
|
|
1346
|
+
LANGUAGE plpgsql SECURITY DEFINER SET search_path = pg_catalog AS $function$
|
|
1347
|
+
DECLARE observation ${schema}.outbox; compact_logs boolean;
|
|
1348
|
+
BEGIN
|
|
1349
|
+
SELECT run_log_store = 'planet_scale' INTO compact_logs
|
|
1350
|
+
FROM ${schema}.runs WHERE run_id = NEW.run_id;
|
|
1351
|
+
IF NOT coalesce(compact_logs, false) OR NEW.runner_log_payload_json IS NULL THEN
|
|
1352
|
+
RETURN NEW;
|
|
1353
|
+
END IF;
|
|
1354
|
+
IF OLD.runner_result_json IS NULL THEN
|
|
1355
|
+
NEW.runner_result_digest := encode(
|
|
1356
|
+
sha256(convert_to(NEW.runner_result_json::text, 'UTF8')), 'hex');
|
|
1357
|
+
observation.event_id := 'runner-terminal-logs:' || NEW.run_id || ':' || NEW.attempt;
|
|
1358
|
+
observation.run_id := NEW.run_id;
|
|
1359
|
+
observation.event_type := 'runner.logs';
|
|
1360
|
+
observation.created_at := coalesce(NEW.runner_terminal_received_at, clock_timestamp());
|
|
1361
|
+
observation.payload_json := jsonb_set(NEW.runner_log_payload_json,
|
|
1362
|
+
'{terminalProgressEvents}', (
|
|
1363
|
+
SELECT coalesce(jsonb_agg(
|
|
1364
|
+
CASE WHEN event->>'type' = 'log.appended' THEN event
|
|
1365
|
+
ELSE event || jsonb_build_object('_runtimeProgressOrdinal', position) END
|
|
1366
|
+
ORDER BY position), '[]'::jsonb)
|
|
1367
|
+
FROM jsonb_array_elements(CASE
|
|
1368
|
+
WHEN jsonb_typeof(NEW.runner_log_payload_json->'terminalProgressEvents') = 'array'
|
|
1369
|
+
THEN NEW.runner_log_payload_json->'terminalProgressEvents'
|
|
1370
|
+
ELSE '[]'::jsonb END) WITH ORDINALITY AS progress(event, position)
|
|
1371
|
+
));
|
|
1372
|
+
PERFORM ${schema}.project_outbox_event(observation);
|
|
1373
|
+
NEW.runner_result_json := ${schema}.compact_runner_log_envelope(NEW.runner_result_json);
|
|
1374
|
+
IF jsonb_array_length(observation.payload_json->'recoveredRuntimeDiagnostics') > 0
|
|
1375
|
+
AND (NOT (NEW.runner_result_json ? 'runtimeDiagnostics')
|
|
1376
|
+
OR jsonb_typeof(NEW.runner_result_json->'runtimeDiagnostics') = 'array') THEN
|
|
1377
|
+
NEW.runner_result_json := jsonb_set(NEW.runner_result_json, '{runtimeDiagnostics}',
|
|
1378
|
+
coalesce(NEW.runner_result_json->'runtimeDiagnostics', '[]'::jsonb)
|
|
1379
|
+
|| (observation.payload_json->'recoveredRuntimeDiagnostics'));
|
|
1380
|
+
END IF;
|
|
1381
|
+
-- Retain only the first accepted non-log facts. Gateway retries
|
|
1382
|
+
-- regenerate transport timestamps, so completion must reuse these
|
|
1383
|
+
-- facts rather than compare newly generated progress with the first.
|
|
1384
|
+
NEW.runner_log_payload_json := jsonb_build_object(
|
|
1385
|
+
'terminalProgressEvents', ${schema}.without_log_events(
|
|
1386
|
+
observation.payload_json->'terminalProgressEvents'));
|
|
1387
|
+
ELSE
|
|
1388
|
+
NEW.runner_log_payload_json := OLD.runner_log_payload_json;
|
|
1389
|
+
END IF;
|
|
1390
|
+
RETURN NEW;
|
|
1391
|
+
END
|
|
1392
|
+
$function$
|
|
1393
|
+
`);
|
|
1394
|
+
await client.query(`
|
|
1395
|
+
CREATE OR REPLACE FUNCTION ${schema}.compact_run_log_result() RETURNS trigger
|
|
1396
|
+
LANGUAGE plpgsql SET search_path = pg_catalog AS $function$
|
|
1397
|
+
BEGIN
|
|
1398
|
+
IF NEW.run_log_store = 'planet_scale' THEN
|
|
1399
|
+
IF NEW.status = 'failed' THEN
|
|
1400
|
+
NEW.terminal_result_json := ${schema}.compact_runner_log_envelope(NEW.terminal_result_json);
|
|
1401
|
+
ELSIF NEW.status = 'completed' THEN
|
|
1402
|
+
NEW.terminal_result_json := ${schema}.without_runtime_log_tail(NEW.terminal_result_json);
|
|
1403
|
+
END IF;
|
|
1404
|
+
END IF;
|
|
1405
|
+
RETURN NEW;
|
|
1406
|
+
END
|
|
1407
|
+
$function$
|
|
1408
|
+
`);
|
|
1409
|
+
// Replacing AFTER with BEFORE is one DDL transaction, so live inserts cannot
|
|
1410
|
+
// observe an interval with no projector. All older unmarked runs stay full.
|
|
1232
1411
|
await client.query(`DO $create$ BEGIN
|
|
1233
|
-
IF
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1412
|
+
DROP TRIGGER IF EXISTS runtime_outbox_event_v1 ON ${schema}.outbox;
|
|
1413
|
+
CREATE TRIGGER runtime_outbox_event_v1 BEFORE INSERT ON ${schema}.outbox
|
|
1414
|
+
FOR EACH ROW EXECUTE FUNCTION ${schema}.search_outbox_event();
|
|
1415
|
+
IF NOT EXISTS (SELECT 1 FROM pg_trigger WHERE tgrelid = '${schema}.attempts'::regclass
|
|
1416
|
+
AND tgname = 'runtime_attempt_log_acceptance') THEN
|
|
1417
|
+
CREATE TRIGGER runtime_attempt_log_acceptance BEFORE UPDATE OF runner_result_json ON ${schema}.attempts
|
|
1418
|
+
FOR EACH ROW EXECUTE FUNCTION ${schema}.accept_runner_log_payload();
|
|
1419
|
+
END IF;
|
|
1420
|
+
IF NOT EXISTS (SELECT 1 FROM pg_trigger WHERE tgrelid = '${schema}.runs'::regclass
|
|
1421
|
+
AND tgname = 'runtime_run_log_result') THEN
|
|
1422
|
+
CREATE TRIGGER runtime_run_log_result BEFORE UPDATE OF terminal_result_json ON ${schema}.runs
|
|
1423
|
+
FOR EACH ROW EXECUTE FUNCTION ${schema}.compact_run_log_result();
|
|
1237
1424
|
END IF;
|
|
1238
1425
|
END $create$`);
|
|
1426
|
+
await client.query(`REVOKE ALL ON FUNCTION ${schema}.project_outbox_event(${schema}.outbox),
|
|
1427
|
+
${schema}.accept_runner_log_payload(), ${schema}.search_outbox_event(),
|
|
1428
|
+
${schema}.compact_run_log_result(), ${schema}.without_log_events(jsonb),
|
|
1429
|
+
${schema}.compact_runner_log_envelope(jsonb), ${schema}.without_runtime_log_tail(jsonb)
|
|
1430
|
+
FROM PUBLIC`);
|
|
1431
|
+
// Publish readiness last, after all acceptance/compaction functions exist.
|
|
1432
|
+
await client.query(
|
|
1433
|
+
`COMMENT ON FUNCTION ${schema}.search_outbox_event() IS ${quoteLiteral(RUNTIME_EVENT_SEARCH_OUTBOX_PROJECTION_MARKER)}`,
|
|
1434
|
+
);
|
|
1239
1435
|
}
|
|
1240
1436
|
|
|
1241
1437
|
export async function hasRuntimeEventSearchShape(
|
|
@@ -1252,6 +1448,9 @@ export async function hasRuntimeEventSearchShape(
|
|
|
1252
1448
|
AND EXISTS (SELECT 1 FROM pg_attribute
|
|
1253
1449
|
WHERE attrelid = to_regclass('runtime_observability.events')
|
|
1254
1450
|
AND attname = 'log_body' AND NOT attisdropped)
|
|
1451
|
+
AND EXISTS (SELECT 1 FROM pg_attribute
|
|
1452
|
+
WHERE attrelid = to_regclass('runtime_observability.events')
|
|
1453
|
+
AND attname = 'log_text_digest' AND NOT attisdropped)
|
|
1255
1454
|
AND EXISTS (SELECT 1 FROM pg_class
|
|
1256
1455
|
WHERE oid = to_regclass('runtime_observability.events') AND relrowsecurity)
|
|
1257
1456
|
AND EXISTS (SELECT 1 FROM pg_class
|
|
@@ -1259,7 +1458,12 @@ export async function hasRuntimeEventSearchShape(
|
|
|
1259
1458
|
AND EXISTS (SELECT 1 FROM pg_trigger WHERE tgrelid = '${schema}.receipts'::regclass
|
|
1260
1459
|
AND tgname = 'runtime_receipt_event_v1' AND NOT tgisinternal)
|
|
1261
1460
|
AND EXISTS (SELECT 1 FROM pg_trigger WHERE tgrelid = '${schema}.outbox'::regclass
|
|
1262
|
-
AND tgname = 'runtime_outbox_event_v1' AND NOT tgisinternal
|
|
1461
|
+
AND tgname = 'runtime_outbox_event_v1' AND NOT tgisinternal
|
|
1462
|
+
AND tgenabled <> 'D' AND (tgtype & 2) = 2)
|
|
1463
|
+
AND EXISTS (SELECT 1 FROM pg_trigger WHERE tgrelid = '${schema}.attempts'::regclass
|
|
1464
|
+
AND tgname = 'runtime_attempt_log_acceptance' AND NOT tgisinternal AND tgenabled <> 'D')
|
|
1465
|
+
AND EXISTS (SELECT 1 FROM pg_trigger WHERE tgrelid = '${schema}.runs'::regclass
|
|
1466
|
+
AND tgname = 'runtime_run_log_result' AND NOT tgisinternal AND tgenabled <> 'D')
|
|
1263
1467
|
AND EXISTS (SELECT 1 FROM pg_proc AS proc
|
|
1264
1468
|
JOIN pg_namespace AS namespace ON namespace.oid = proc.pronamespace
|
|
1265
1469
|
WHERE namespace.nspname = $1 AND proc.proname = 'search_outbox_event'
|
|
@@ -1271,6 +1475,83 @@ export async function hasRuntimeEventSearchShape(
|
|
|
1271
1475
|
return result.rows[0]?.ready === true;
|
|
1272
1476
|
}
|
|
1273
1477
|
|
|
1478
|
+
const RUNTIME_EVENT_BODY_RETENTION_PREDICATE = `kind <> 'log.appended' OR log_body IS NOT NULL
|
|
1479
|
+
OR message IS NOT NULL OR search_doc ? 'context'`;
|
|
1480
|
+
|
|
1481
|
+
// pg_get_expr adds grouping and text casts. Preserve quoted literals exactly;
|
|
1482
|
+
// this known OR-only predicate rejects other operators, keys and literal values.
|
|
1483
|
+
function normalizedRetentionPredicate(value: string): string {
|
|
1484
|
+
return (value.match(/'(?:''|[^'])*'|::text\b|[()\s]+|./g) ?? [])
|
|
1485
|
+
.filter((token) => !/^(?:::text|[()\s]+)$/.test(token))
|
|
1486
|
+
.join('');
|
|
1487
|
+
}
|
|
1488
|
+
|
|
1489
|
+
/** Operator-only concurrent index build; never part of runtime bootstrap. */
|
|
1490
|
+
export async function prepareRuntimeEventRetentionIndex(
|
|
1491
|
+
client: PostgresSchedulerQueryClient,
|
|
1492
|
+
): Promise<void> {
|
|
1493
|
+
type IndexShape = {
|
|
1494
|
+
valid: boolean;
|
|
1495
|
+
ready: boolean;
|
|
1496
|
+
is_unique: boolean;
|
|
1497
|
+
table_matches: boolean;
|
|
1498
|
+
key_count: number;
|
|
1499
|
+
attribute_count: number;
|
|
1500
|
+
method: string;
|
|
1501
|
+
key1: string | null;
|
|
1502
|
+
key2: string | null;
|
|
1503
|
+
predicate: string | null;
|
|
1504
|
+
};
|
|
1505
|
+
const inspect = () =>
|
|
1506
|
+
client.query<IndexShape>(
|
|
1507
|
+
`SELECT index.indisvalid AS valid, index.indisready AS ready, index.indisunique AS is_unique,
|
|
1508
|
+
index.indrelid = to_regclass('runtime_observability.events') AS table_matches,
|
|
1509
|
+
index.indnkeyatts AS key_count, index.indnatts AS attribute_count,
|
|
1510
|
+
method.amname AS method,
|
|
1511
|
+
pg_get_indexdef(index.indexrelid, 1, true) AS key1,
|
|
1512
|
+
pg_get_indexdef(index.indexrelid, 2, true) AS key2,
|
|
1513
|
+
pg_get_expr(index.indpred, index.indrelid) AS predicate
|
|
1514
|
+
FROM pg_index AS index
|
|
1515
|
+
JOIN pg_class AS relation ON relation.oid = index.indexrelid
|
|
1516
|
+
JOIN pg_am AS method ON method.oid = relation.relam
|
|
1517
|
+
WHERE index.indexrelid = to_regclass('runtime_observability.runtime_events_body_retention_idx')`,
|
|
1518
|
+
);
|
|
1519
|
+
const assertReady = (shape: IndexShape): void => {
|
|
1520
|
+
if (!shape.valid || !shape.ready) {
|
|
1521
|
+
throw new Error(
|
|
1522
|
+
'Runtime body retention index is invalid or incomplete. Inspect the failed build; repair it explicitly before retrying.',
|
|
1523
|
+
);
|
|
1524
|
+
}
|
|
1525
|
+
if (
|
|
1526
|
+
!shape.table_matches ||
|
|
1527
|
+
shape.is_unique ||
|
|
1528
|
+
shape.key_count !== 2 ||
|
|
1529
|
+
shape.attribute_count !== 2 ||
|
|
1530
|
+
shape.method !== 'btree' ||
|
|
1531
|
+
shape.key1 !== 'ingested_at' ||
|
|
1532
|
+
shape.key2 !== 'event_id' ||
|
|
1533
|
+
shape.predicate === null ||
|
|
1534
|
+
normalizedRetentionPredicate(shape.predicate) !==
|
|
1535
|
+
normalizedRetentionPredicate(RUNTIME_EVENT_BODY_RETENTION_PREDICATE)
|
|
1536
|
+
) {
|
|
1537
|
+
throw new Error(
|
|
1538
|
+
'Runtime body retention index definition differs from its expected table, uniqueness, keys or predicate. Inspect and repair it explicitly.',
|
|
1539
|
+
);
|
|
1540
|
+
}
|
|
1541
|
+
};
|
|
1542
|
+
const existing = (await inspect()).rows[0];
|
|
1543
|
+
if (existing) assertReady(existing);
|
|
1544
|
+
await client.query(`CREATE INDEX CONCURRENTLY IF NOT EXISTS runtime_events_body_retention_idx
|
|
1545
|
+
ON runtime_observability.events (ingested_at, event_id)
|
|
1546
|
+
WHERE ${RUNTIME_EVENT_BODY_RETENTION_PREDICATE}`);
|
|
1547
|
+
const built = (await inspect()).rows[0];
|
|
1548
|
+
if (!built)
|
|
1549
|
+
throw new Error(
|
|
1550
|
+
'Runtime body retention index was not found after preparation.',
|
|
1551
|
+
);
|
|
1552
|
+
assertReady(built);
|
|
1553
|
+
}
|
|
1554
|
+
|
|
1274
1555
|
/** One bounded operator-owned TTL pass. Never called by a customer request. */
|
|
1275
1556
|
export async function pruneRuntimeEvents(
|
|
1276
1557
|
client: PostgresSchedulerQueryClient,
|
|
@@ -1295,16 +1576,32 @@ export async function pruneRuntimeEvents(
|
|
|
1295
1576
|
`WITH candidates AS (
|
|
1296
1577
|
SELECT ctid FROM runtime_observability.events
|
|
1297
1578
|
WHERE ingested_at < $1::timestamptz
|
|
1579
|
+
AND (kind <> 'log.appended' OR log_body IS NOT NULL
|
|
1580
|
+
OR message IS NOT NULL OR search_doc ? 'context')
|
|
1298
1581
|
ORDER BY ingested_at, event_id
|
|
1299
1582
|
LIMIT $2::integer FOR UPDATE SKIP LOCKED
|
|
1583
|
+
), scrubbed AS (
|
|
1584
|
+
UPDATE runtime_observability.events AS event
|
|
1585
|
+
SET log_text_digest = coalesce(event.log_text_digest,
|
|
1586
|
+
encode(sha256(convert_to(btrim(coalesce(event.log_body, event.message)), 'UTF8')), 'hex')),
|
|
1587
|
+
log_body = NULL, message = NULL,
|
|
1588
|
+
search_doc = jsonb_strip_nulls(jsonb_build_object(
|
|
1589
|
+
'kind', 'log.appended', 'source', event.search_doc->'source',
|
|
1590
|
+
'producerAttempt', event.search_doc->'producerAttempt',
|
|
1591
|
+
'logPositions', event.search_doc->'logPositions',
|
|
1592
|
+
'logContentDigest', event.search_doc->'logContentDigest'))
|
|
1593
|
+
FROM candidates WHERE event.ctid = candidates.ctid AND event.kind = 'log.appended'
|
|
1594
|
+
RETURNING event.event_id
|
|
1595
|
+
), deleted AS (
|
|
1596
|
+
DELETE FROM runtime_observability.events AS event USING candidates
|
|
1597
|
+
WHERE event.ctid = candidates.ctid AND event.kind <> 'log.appended'
|
|
1598
|
+
RETURNING event.event_id
|
|
1300
1599
|
)
|
|
1301
|
-
|
|
1302
|
-
WHERE event.ctid = candidates.ctid
|
|
1303
|
-
RETURNING event.event_id`,
|
|
1600
|
+
SELECT event_id FROM scrubbed UNION ALL SELECT event_id FROM deleted`,
|
|
1304
1601
|
[input.before.toISOString(), input.limit],
|
|
1305
1602
|
);
|
|
1306
|
-
// Keep
|
|
1307
|
-
//
|
|
1603
|
+
// Keep identity/digests until fenced ingress and pending delivery drain. A
|
|
1604
|
+
// high-water mark alone cannot recognize a resent producer offset.
|
|
1308
1605
|
return result.rows.length;
|
|
1309
1606
|
}
|
|
1310
1607
|
|
|
@@ -1326,10 +1623,14 @@ export async function inspectRuntimeEventRetention(
|
|
|
1326
1623
|
}>(
|
|
1327
1624
|
`WITH oldest AS (
|
|
1328
1625
|
SELECT ingested_at FROM runtime_observability.events
|
|
1626
|
+
WHERE kind <> 'log.appended' OR log_body IS NOT NULL
|
|
1627
|
+
OR message IS NOT NULL OR search_doc ? 'context'
|
|
1329
1628
|
ORDER BY ingested_at, event_id LIMIT 1
|
|
1330
1629
|
), bounded AS (
|
|
1331
1630
|
SELECT 1 FROM runtime_observability.events
|
|
1332
1631
|
WHERE ingested_at < $1::timestamptz
|
|
1632
|
+
AND (kind <> 'log.appended' OR log_body IS NOT NULL
|
|
1633
|
+
OR message IS NOT NULL OR search_doc ? 'context')
|
|
1333
1634
|
ORDER BY ingested_at, event_id LIMIT 10001
|
|
1334
1635
|
)
|
|
1335
1636
|
SELECT (SELECT ingested_at FROM oldest) AS oldest,
|