@masons/runtime-broker 0.2.2 → 0.2.6
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/broker/background-assignment-ledger.d.ts +27 -0
- package/dist/broker/background-assignment-ledger.d.ts.map +1 -0
- package/dist/broker/background-assignment-ledger.js +125 -0
- package/dist/broker/broker-daemon.d.ts +2 -1
- package/dist/broker/broker-daemon.d.ts.map +1 -1
- package/dist/broker/broker-daemon.js +429 -46
- package/dist/broker/control-event-types.d.ts +9 -4
- package/dist/broker/control-event-types.d.ts.map +1 -1
- package/dist/broker/endpoint-registry.d.ts +2 -0
- package/dist/broker/endpoint-registry.d.ts.map +1 -1
- package/dist/broker/endpoint-registry.js +1 -0
- package/dist/broker/entry.d.ts.map +1 -1
- package/dist/broker/entry.js +4 -1
- package/dist/broker/ipc-server.d.ts +13 -0
- package/dist/broker/ipc-server.d.ts.map +1 -1
- package/dist/broker/ipc-server.js +6 -0
- package/dist/broker/runtime-endpoint-port.d.ts +3 -2
- package/dist/broker/runtime-endpoint-port.d.ts.map +1 -1
- package/dist/broker/runtime-inbound-routed-emitter.d.ts +4 -2
- package/dist/broker/runtime-inbound-routed-emitter.d.ts.map +1 -1
- package/dist/broker/runtime-inbound-routed-emitter.js +18 -6
- package/dist/broker/runtime-inbound-routed-event-types.d.ts +8 -0
- package/dist/broker/runtime-inbound-routed-event-types.d.ts.map +1 -1
- package/dist/broker/runtime-processing-state-event-types.d.ts +9 -4
- package/dist/broker/runtime-processing-state-event-types.d.ts.map +1 -1
- package/dist/broker/version-handshake.d.ts +1 -0
- package/dist/broker/version-handshake.d.ts.map +1 -1
- package/dist/broker/version-handshake.js +1 -0
- package/dist/broker-client/broker-client.d.ts +25 -2
- package/dist/broker-client/broker-client.d.ts.map +1 -1
- package/dist/broker-client/broker-client.js +45 -0
- package/dist/broker-client/lazy-spawn.d.ts.map +1 -1
- package/dist/connector-client.d.ts +8 -0
- package/dist/connector-client.d.ts.map +1 -1
- package/dist/connector-client.js +17 -3
- package/dist/runtime-endpoint-client.d.ts +22 -1
- package/dist/runtime-endpoint-client.d.ts.map +1 -1
- package/dist/runtime-endpoint-client.js +47 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +13 -12
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { spawn as spawnChild } from "node:child_process";
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
3
|
import { basename } from "node:path";
|
|
4
|
+
import { ConnectorClientUnavailableError, ConnectorSendAckTimeoutError, } from "../connector-client.js";
|
|
4
5
|
import { createControlEventDispatcher, } from "./control-event-dispatcher.js";
|
|
5
6
|
import { readDeliveryCursorFile, writeDeliveryCursorFile, } from "./delivery-cursor-file.js";
|
|
6
7
|
import { deleteDiscoveryFile, mintBearerToken, writeDiscoveryFile, } from "./discovery-file.js";
|
|
@@ -23,13 +24,27 @@ import { createUndispatchedChangedEmitter, postUndispatchedChangedViaPort, } fro
|
|
|
23
24
|
import { createUndispatchedInbox, } from "./undispatched-inbox.js";
|
|
24
25
|
const REMOTE_SPAWN_CAPABILITY = "remote_spawn_v1";
|
|
25
26
|
const DEFAULT_RUNTIME_ASSIGNMENT_ACCEPTANCE_TIMEOUT_MS = 30_000;
|
|
27
|
+
const RUNTIME_ASSIGNMENT_REPLY_RECORD_TTL_MS = 24 * 60 * 60 * 1000;
|
|
26
28
|
const RUNTIME_PROCESSING_RETRY_INITIAL_MS = 1_000;
|
|
27
29
|
const RUNTIME_PROCESSING_RETRY_MAX_MS = 30_000;
|
|
28
30
|
function isBackgroundExchangeMode(value) {
|
|
29
31
|
return value === "resident_endpoint" || value === "adapter_managed_turn";
|
|
30
32
|
}
|
|
31
|
-
function
|
|
32
|
-
|
|
33
|
+
function buildLocalDispatchTargetRef(targetEndpointId, target) {
|
|
34
|
+
if (target?.background_exchange_mode === "adapter_managed_turn" &&
|
|
35
|
+
target.runtime_session_id) {
|
|
36
|
+
return {
|
|
37
|
+
kind: "projection_endpoint",
|
|
38
|
+
projection_endpoint_id: targetEndpointId,
|
|
39
|
+
runtime_session_id: target.runtime_session_id,
|
|
40
|
+
background_exchange_mode: "adapter_managed_turn",
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
return {
|
|
44
|
+
kind: "projection_endpoint",
|
|
45
|
+
projection_endpoint_id: targetEndpointId,
|
|
46
|
+
background_exchange_mode: "resident_endpoint",
|
|
47
|
+
};
|
|
33
48
|
}
|
|
34
49
|
function derivePublicDisplayLabel(body) {
|
|
35
50
|
const parts = [
|
|
@@ -76,6 +91,8 @@ export async function startBrokerDaemon(opts) {
|
|
|
76
91
|
const controlEventDispatcher = createControlEventDispatcher();
|
|
77
92
|
const runtimeAssignments = new Map();
|
|
78
93
|
const runtimeAssignmentByEndpointCorrelation = new Map();
|
|
94
|
+
const runtimeAssignmentReplyRecords = new Map();
|
|
95
|
+
const runtimeAssignmentReplyInFlight = new Map();
|
|
79
96
|
const spawnCorrelation = createSpawnCorrelationManager({
|
|
80
97
|
timeoutMs: opts.spawnCorrelationTimeoutMs,
|
|
81
98
|
});
|
|
@@ -135,6 +152,21 @@ export async function startBrokerDaemon(opts) {
|
|
|
135
152
|
maxRetries: opts.runtimeInboundRoutedMaxRetries,
|
|
136
153
|
});
|
|
137
154
|
const runtimeAssignmentKey = (sourceMessageId, assignmentId) => `${sourceMessageId}\u0000${assignmentId}`;
|
|
155
|
+
const runtimeAssignmentReplyKey = (sourceMessageId, assignmentId, replyRequestId) => `${sourceMessageId}\u0000${assignmentId}\u0000${replyRequestId}`;
|
|
156
|
+
const runtimeAssignmentReplyFingerprint = (content, contentType) => JSON.stringify([content, contentType ?? "text"]);
|
|
157
|
+
const pruneRuntimeAssignmentReplyRecords = (now = Date.now()) => {
|
|
158
|
+
for (const [key, record] of runtimeAssignmentReplyRecords.entries()) {
|
|
159
|
+
const [sourceMessageId, assignmentId] = key.split("\u0000");
|
|
160
|
+
if (sourceMessageId !== undefined &&
|
|
161
|
+
assignmentId !== undefined &&
|
|
162
|
+
runtimeAssignments.has(runtimeAssignmentKey(sourceMessageId, assignmentId))) {
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
if (record.expiresAt <= now) {
|
|
166
|
+
runtimeAssignmentReplyRecords.delete(key);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
};
|
|
138
170
|
const endpointCorrelationKey = (endpointId, correlationId) => `${endpointId}\u0000${correlationId}`;
|
|
139
171
|
const isBackgroundExchangeTarget = (entry) => Boolean(entry.runtime_capabilities?.includes("background_exchange_v1") &&
|
|
140
172
|
isBackgroundExchangeMode(entry.background_exchange_mode));
|
|
@@ -158,6 +190,11 @@ export async function startBrokerDaemon(opts) {
|
|
|
158
190
|
runtimeAssignmentByEndpointCorrelation.delete(correlationKey);
|
|
159
191
|
}
|
|
160
192
|
}
|
|
193
|
+
for (const replyKey of Array.from(runtimeAssignmentReplyInFlight.keys())) {
|
|
194
|
+
if (replyKey.startsWith(`${key}\u0000`)) {
|
|
195
|
+
runtimeAssignmentReplyInFlight.delete(replyKey);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
161
198
|
};
|
|
162
199
|
const scheduleAcceptanceTimeout = (key) => {
|
|
163
200
|
if (runtimeAssignmentAcceptanceTimeoutMs <= 0)
|
|
@@ -247,10 +284,45 @@ export async function startBrokerDaemon(opts) {
|
|
|
247
284
|
detail: outcome.detail,
|
|
248
285
|
});
|
|
249
286
|
if (!outcome.terminal && opts.queueOnTransientFailure !== false) {
|
|
250
|
-
runtimeInboundRoutedEmitter.enqueue(event
|
|
287
|
+
const queued = runtimeInboundRoutedEmitter.enqueue(event, {
|
|
288
|
+
onSuccess: opts.onQueuedSuccess,
|
|
289
|
+
});
|
|
290
|
+
return opts.allowQueuedAsSuccess === true ? queued : false;
|
|
251
291
|
}
|
|
252
292
|
return false;
|
|
253
293
|
};
|
|
294
|
+
const directClaimRuntimeInboundRoute = async (event) => {
|
|
295
|
+
const claim = opts.runtimeInboundRouteClaimPost ?? apiPort.claimRuntimeInboundRoute;
|
|
296
|
+
if (!claim) {
|
|
297
|
+
logger.warn("runtime_inbound_route_claim_unavailable", {
|
|
298
|
+
source_message_id: event.source_message_id,
|
|
299
|
+
routed_to_endpoint_id: event.routed_to_endpoint_id,
|
|
300
|
+
});
|
|
301
|
+
return "retry";
|
|
302
|
+
}
|
|
303
|
+
let outcome;
|
|
304
|
+
try {
|
|
305
|
+
outcome = await claim(event);
|
|
306
|
+
}
|
|
307
|
+
catch (err) {
|
|
308
|
+
outcome = {
|
|
309
|
+
ok: false,
|
|
310
|
+
terminal: false,
|
|
311
|
+
detail: err instanceof Error ? err.message : String(err),
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
if (outcome.ok) {
|
|
315
|
+
return outcome.action;
|
|
316
|
+
}
|
|
317
|
+
logger.warn("runtime_inbound_route_claim_failed", {
|
|
318
|
+
source_message_id: event.source_message_id,
|
|
319
|
+
routed_to_endpoint_id: event.routed_to_endpoint_id,
|
|
320
|
+
terminal: outcome.terminal,
|
|
321
|
+
status: outcome.status,
|
|
322
|
+
detail: outcome.detail,
|
|
323
|
+
});
|
|
324
|
+
return "retry";
|
|
325
|
+
};
|
|
254
326
|
const markDeliverySeqAccepted = (seq) => {
|
|
255
327
|
if (typeof seq !== "number")
|
|
256
328
|
return true;
|
|
@@ -327,6 +399,27 @@ export async function startBrokerDaemon(opts) {
|
|
|
327
399
|
}
|
|
328
400
|
return Promise.allSettled(tasks).then(() => undefined);
|
|
329
401
|
};
|
|
402
|
+
const restorePendingAssignmentBeforeAcceptance = (key, assignment, reason) => {
|
|
403
|
+
if (assignment.acceptanceTimer) {
|
|
404
|
+
clearTimeout(assignment.acceptanceTimer);
|
|
405
|
+
assignment.acceptanceTimer = undefined;
|
|
406
|
+
}
|
|
407
|
+
const readded = undispatchedInbox.tryAdd(assignment.message);
|
|
408
|
+
if (!readded) {
|
|
409
|
+
logger.error("runtime_assignment_reinsert_failed_at_capacity", {
|
|
410
|
+
undispatched_id: assignment.context.undispatchedId,
|
|
411
|
+
source_message_id: assignment.context.sourceMessageId,
|
|
412
|
+
target_endpoint_id: assignment.context.targetRef.projection_endpoint_id,
|
|
413
|
+
inbox_size: undispatchedInbox.size(),
|
|
414
|
+
inbox_capacity: undispatchedInbox.capacity(),
|
|
415
|
+
reason,
|
|
416
|
+
});
|
|
417
|
+
scheduleAcceptanceTimeout(key);
|
|
418
|
+
return false;
|
|
419
|
+
}
|
|
420
|
+
forgetRuntimeAssignment(key);
|
|
421
|
+
return true;
|
|
422
|
+
};
|
|
330
423
|
const emitReplyEmittedWithRetry = (assignmentKey, event, attempt = 0) => {
|
|
331
424
|
const assignment = runtimeAssignments.get(assignmentKey);
|
|
332
425
|
if (!assignment)
|
|
@@ -351,8 +444,10 @@ export async function startBrokerDaemon(opts) {
|
|
|
351
444
|
terminal: outcome.terminal,
|
|
352
445
|
detail: outcome.detail,
|
|
353
446
|
});
|
|
354
|
-
if (outcome.terminal)
|
|
447
|
+
if (outcome.terminal) {
|
|
448
|
+
forgetRuntimeAssignment(assignmentKey);
|
|
355
449
|
return;
|
|
450
|
+
}
|
|
356
451
|
const delay = Math.min(RUNTIME_PROCESSING_RETRY_INITIAL_MS * 2 ** attempt, RUNTIME_PROCESSING_RETRY_MAX_MS);
|
|
357
452
|
const timer = setTimeout(() => {
|
|
358
453
|
emitReplyEmittedWithRetry(assignmentKey, event, attempt + 1);
|
|
@@ -380,6 +475,9 @@ export async function startBrokerDaemon(opts) {
|
|
|
380
475
|
terminal: outcome.terminal,
|
|
381
476
|
detail: outcome.detail,
|
|
382
477
|
});
|
|
478
|
+
if (outcome.terminal) {
|
|
479
|
+
forgetRuntimeAssignment(key);
|
|
480
|
+
}
|
|
383
481
|
}
|
|
384
482
|
}
|
|
385
483
|
for (const endpointId of endpointIds) {
|
|
@@ -606,6 +704,12 @@ export async function startBrokerDaemon(opts) {
|
|
|
606
704
|
}
|
|
607
705
|
};
|
|
608
706
|
controlEventDispatcher.on("dispatch_undispatched", async (event) => {
|
|
707
|
+
if (event.broker_projection_token === undefined) {
|
|
708
|
+
return {
|
|
709
|
+
ok: false,
|
|
710
|
+
detail: "dispatch broker_projection_token missing",
|
|
711
|
+
};
|
|
712
|
+
}
|
|
609
713
|
const targetEndpointId = event.target_ref?.projection_endpoint_id ?? event.target_endpoint_id;
|
|
610
714
|
if (!targetEndpointId) {
|
|
611
715
|
return {
|
|
@@ -668,13 +772,31 @@ export async function startBrokerDaemon(opts) {
|
|
|
668
772
|
detail: `target endpoint not found: ${targetEndpointId}`,
|
|
669
773
|
};
|
|
670
774
|
}
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
775
|
+
let targetRef = event.target_ref;
|
|
776
|
+
if (!targetRef) {
|
|
777
|
+
if (target.background_exchange_mode === "resident_endpoint") {
|
|
778
|
+
targetRef = {
|
|
779
|
+
kind: "projection_endpoint",
|
|
780
|
+
projection_endpoint_id: targetEndpointId,
|
|
781
|
+
background_exchange_mode: "resident_endpoint",
|
|
782
|
+
};
|
|
783
|
+
}
|
|
784
|
+
else if (target.background_exchange_mode === "adapter_managed_turn" &&
|
|
785
|
+
target.runtime_session_id) {
|
|
786
|
+
targetRef = {
|
|
787
|
+
kind: "projection_endpoint",
|
|
788
|
+
projection_endpoint_id: targetEndpointId,
|
|
789
|
+
runtime_session_id: target.runtime_session_id,
|
|
790
|
+
background_exchange_mode: "adapter_managed_turn",
|
|
791
|
+
};
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
if (!targetRef ||
|
|
795
|
+
!target.runtime_capabilities?.includes("background_exchange_v1") ||
|
|
796
|
+
target.background_exchange_mode !==
|
|
797
|
+
targetRef.background_exchange_mode ||
|
|
798
|
+
(targetRef.background_exchange_mode === "adapter_managed_turn" &&
|
|
799
|
+
target.runtime_session_id !== targetRef.runtime_session_id)) {
|
|
678
800
|
undispatchedInbox.tryAdd(taken);
|
|
679
801
|
return {
|
|
680
802
|
ok: false,
|
|
@@ -693,6 +815,9 @@ export async function startBrokerDaemon(opts) {
|
|
|
693
815
|
state: "assigned_to_runtime_target",
|
|
694
816
|
occurred_at: assignedAt,
|
|
695
817
|
undispatched_id: event.undispatched_id,
|
|
818
|
+
...(event.broker_projection_token !== undefined && {
|
|
819
|
+
broker_projection_token: event.broker_projection_token,
|
|
820
|
+
}),
|
|
696
821
|
};
|
|
697
822
|
const processingResult = await emitRuntimeProcessingState(processingEvent);
|
|
698
823
|
if (!processingResult.ok) {
|
|
@@ -724,12 +849,20 @@ export async function startBrokerDaemon(opts) {
|
|
|
724
849
|
const storedAssignment = existingAssignment ?? {
|
|
725
850
|
context: runtimeAssignment,
|
|
726
851
|
message: taken,
|
|
852
|
+
...(event.broker_projection_token !== undefined && {
|
|
853
|
+
brokerProjectionToken: event.broker_projection_token,
|
|
854
|
+
}),
|
|
727
855
|
accepted: false,
|
|
728
856
|
assignedAt,
|
|
857
|
+
lastStateSequence: processingEvent.state_sequence,
|
|
729
858
|
};
|
|
730
859
|
storedAssignment.context = runtimeAssignment;
|
|
731
860
|
storedAssignment.message = taken;
|
|
861
|
+
if (event.broker_projection_token !== undefined) {
|
|
862
|
+
storedAssignment.brokerProjectionToken = event.broker_projection_token;
|
|
863
|
+
}
|
|
732
864
|
storedAssignment.assignedAt = assignedAt;
|
|
865
|
+
storedAssignment.lastStateSequence = Math.max(storedAssignment.lastStateSequence, processingEvent.state_sequence);
|
|
733
866
|
runtimeAssignments.set(assignmentKey, storedAssignment);
|
|
734
867
|
scheduleAcceptanceTimeout(assignmentKey);
|
|
735
868
|
rememberRuntimeAssignmentCorrelation(target.endpoint_id, taken.metadata.correlation_id, assignmentKey);
|
|
@@ -883,6 +1016,7 @@ export async function startBrokerDaemon(opts) {
|
|
|
883
1016
|
const runtimeCapabilities = runtimeCapabilitiesSet.size > 0
|
|
884
1017
|
? Array.from(runtimeCapabilitiesSet)
|
|
885
1018
|
: undefined;
|
|
1019
|
+
const runtimeSessionId = body.runtime_session_id?.trim() || undefined;
|
|
886
1020
|
const apiResp = await apiPort.register({
|
|
887
1021
|
runtime_kind: body.kind,
|
|
888
1022
|
endpoint_nonce: randomUUID(),
|
|
@@ -895,6 +1029,7 @@ export async function startBrokerDaemon(opts) {
|
|
|
895
1029
|
task_hint: body.task_hint,
|
|
896
1030
|
runtime_capabilities: runtimeCapabilities,
|
|
897
1031
|
execution_surface: body.execution_surface,
|
|
1032
|
+
runtime_session_id: runtimeSessionId,
|
|
898
1033
|
background_exchange_mode: body.background_exchange_mode,
|
|
899
1034
|
});
|
|
900
1035
|
registry.register({
|
|
@@ -904,6 +1039,7 @@ export async function startBrokerDaemon(opts) {
|
|
|
904
1039
|
ipc_ws: boundWs,
|
|
905
1040
|
runtime_capabilities: runtimeCapabilities,
|
|
906
1041
|
execution_surface: body.execution_surface,
|
|
1042
|
+
runtime_session_id: runtimeSessionId,
|
|
907
1043
|
background_exchange_mode: body.background_exchange_mode,
|
|
908
1044
|
display_metadata: {
|
|
909
1045
|
session_name: body.session_name,
|
|
@@ -1014,35 +1150,186 @@ export async function startBrokerDaemon(opts) {
|
|
|
1014
1150
|
}
|
|
1015
1151
|
catch (err) {
|
|
1016
1152
|
const msg = err instanceof Error ? err.message : String(err);
|
|
1017
|
-
if (
|
|
1153
|
+
if (err instanceof ConnectorClientUnavailableError) {
|
|
1018
1154
|
throw new BrokerHttpError(503, "connector_unavailable", msg, {
|
|
1019
1155
|
retryable: true,
|
|
1020
1156
|
});
|
|
1021
1157
|
}
|
|
1158
|
+
if (err instanceof ConnectorSendAckTimeoutError) {
|
|
1159
|
+
throw new BrokerHttpError(503, "connector_send_outcome_unknown", msg, { retryable: false });
|
|
1160
|
+
}
|
|
1022
1161
|
throw err;
|
|
1023
1162
|
}
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1163
|
+
return { messageId: ack.messageId, status: ack.status };
|
|
1164
|
+
},
|
|
1165
|
+
async sendRuntimeAssignmentReply(body) {
|
|
1166
|
+
const entry = registry.get(body.endpoint_id);
|
|
1167
|
+
if (!entry) {
|
|
1168
|
+
throw new BrokerHttpError(404, "unknown_endpoint", `unknown endpoint_id: ${body.endpoint_id}`);
|
|
1169
|
+
}
|
|
1170
|
+
if (entry.plugin_pid !== body.plugin_pid) {
|
|
1171
|
+
throw new BrokerHttpError(403, "endpoint_pid_mismatch", "plugin_pid does not own endpoint_id");
|
|
1172
|
+
}
|
|
1173
|
+
if (entry.state !== "active") {
|
|
1174
|
+
throw new BrokerHttpError(409, "endpoint_not_active", `endpoint ${body.endpoint_id} is ${entry.state}; assignment reply is only allowed from Active`);
|
|
1175
|
+
}
|
|
1176
|
+
if (!body.reply_request_id) {
|
|
1177
|
+
throw new BrokerHttpError(400, "reply_request_id_required", "reply_request_id is required for assignment replies");
|
|
1178
|
+
}
|
|
1179
|
+
const assignmentKey = runtimeAssignmentKey(body.source_message_id, body.assignment_id);
|
|
1180
|
+
const replyKey = runtimeAssignmentReplyKey(body.source_message_id, body.assignment_id, body.reply_request_id);
|
|
1181
|
+
pruneRuntimeAssignmentReplyRecords();
|
|
1182
|
+
const requestFingerprint = runtimeAssignmentReplyFingerprint(body.content, body.contentType);
|
|
1183
|
+
const assignment = runtimeAssignments.get(assignmentKey);
|
|
1184
|
+
const existingRecord = runtimeAssignmentReplyRecords.get(replyKey);
|
|
1185
|
+
const inFlight = runtimeAssignmentReplyInFlight.get(replyKey);
|
|
1186
|
+
if (assignment) {
|
|
1187
|
+
if (assignment.context.targetRef.projection_endpoint_id !==
|
|
1188
|
+
body.endpoint_id) {
|
|
1189
|
+
throw new BrokerHttpError(403, "runtime_assignment_target_mismatch", "endpoint_id does not match the assigned target");
|
|
1190
|
+
}
|
|
1191
|
+
if (!assignment.accepted) {
|
|
1192
|
+
throw new BrokerHttpError(409, "runtime_assignment_not_accepted", "assignment replies require adapter_accepted first");
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
else if (existingRecord || inFlight) {
|
|
1196
|
+
const owner = existingRecord ?? inFlight;
|
|
1197
|
+
if (owner?.endpointId !== body.endpoint_id ||
|
|
1198
|
+
owner.pluginPid !== body.plugin_pid) {
|
|
1199
|
+
throw new BrokerHttpError(403, "runtime_assignment_target_mismatch", "endpoint_id does not match the assigned target");
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
else {
|
|
1203
|
+
throw new BrokerHttpError(409, "runtime_assignment_unknown", "source assignment is not known to this broker");
|
|
1204
|
+
}
|
|
1205
|
+
if (existingRecord &&
|
|
1206
|
+
existingRecord.requestFingerprint !== requestFingerprint) {
|
|
1207
|
+
throw new BrokerHttpError(409, "runtime_assignment_reply_idempotency_conflict", "reply_request_id was already used with different reply content");
|
|
1208
|
+
}
|
|
1209
|
+
if (existingRecord?.outcomeUnknown) {
|
|
1210
|
+
throw new BrokerHttpError(503, "connector_send_outcome_unknown", existingRecord.outcomeUnknown.message, { retryable: false });
|
|
1211
|
+
}
|
|
1212
|
+
if (existingRecord?.result)
|
|
1213
|
+
return existingRecord.result;
|
|
1214
|
+
if (inFlight) {
|
|
1215
|
+
if (inFlight.requestFingerprint !== requestFingerprint) {
|
|
1216
|
+
throw new BrokerHttpError(409, "runtime_assignment_reply_idempotency_conflict", "reply_request_id was already used with different reply content");
|
|
1217
|
+
}
|
|
1218
|
+
return inFlight.task;
|
|
1219
|
+
}
|
|
1220
|
+
if (!assignment) {
|
|
1221
|
+
throw new BrokerHttpError(409, "runtime_assignment_unknown", "source assignment is not known to this broker");
|
|
1222
|
+
}
|
|
1223
|
+
if (assignment.brokerProjectionToken === undefined) {
|
|
1224
|
+
throw new BrokerHttpError(409, "runtime_assignment_projection_authority_missing", "runtime assignment reply requires broker projection authority");
|
|
1225
|
+
}
|
|
1226
|
+
const task = (async () => {
|
|
1227
|
+
let record = runtimeAssignmentReplyRecords.get(replyKey);
|
|
1228
|
+
if (!record) {
|
|
1229
|
+
const replyRequestedSequence = assignment.lastStateSequence + 1;
|
|
1230
|
+
const replyRequestedEvent = {
|
|
1031
1231
|
type: "runtime_processing_state",
|
|
1032
1232
|
version: 1,
|
|
1033
1233
|
source_message_id: assignment.context.sourceMessageId,
|
|
1034
1234
|
assignment_id: assignment.context.assignmentId,
|
|
1035
|
-
state_event_id: `${assignment.context.assignmentId}:
|
|
1036
|
-
state_sequence:
|
|
1235
|
+
state_event_id: `${assignment.context.assignmentId}:reply_requested:${body.reply_request_id}`,
|
|
1236
|
+
state_sequence: replyRequestedSequence,
|
|
1037
1237
|
target_ref: assignment.context.targetRef,
|
|
1038
|
-
state: "
|
|
1238
|
+
state: "reply_requested",
|
|
1039
1239
|
occurred_at: Date.now(),
|
|
1040
1240
|
undispatched_id: assignment.context.undispatchedId,
|
|
1041
|
-
|
|
1042
|
-
|
|
1241
|
+
reply_request_id: body.reply_request_id,
|
|
1242
|
+
...(assignment.brokerProjectionToken !== undefined && {
|
|
1243
|
+
broker_projection_token: assignment.brokerProjectionToken,
|
|
1244
|
+
}),
|
|
1245
|
+
};
|
|
1246
|
+
const replyRequested = await emitRuntimeProcessingState(replyRequestedEvent);
|
|
1247
|
+
if (!replyRequested.ok) {
|
|
1248
|
+
throw new BrokerHttpError(replyRequested.terminal ? 409 : 503, "runtime_reply_request_rejected", replyRequested.detail ?? "runtime reply request was not accepted");
|
|
1249
|
+
}
|
|
1250
|
+
record = {
|
|
1251
|
+
replyRequestedEvent,
|
|
1252
|
+
endpointId: body.endpoint_id,
|
|
1253
|
+
pluginPid: body.plugin_pid,
|
|
1254
|
+
requestFingerprint,
|
|
1255
|
+
wireMessageId: randomUUID(),
|
|
1256
|
+
expiresAt: Date.now() + RUNTIME_ASSIGNMENT_REPLY_RECORD_TTL_MS,
|
|
1257
|
+
};
|
|
1258
|
+
runtimeAssignmentReplyRecords.set(replyKey, record);
|
|
1259
|
+
assignment.lastStateSequence = replyRequestedSequence;
|
|
1043
1260
|
}
|
|
1261
|
+
const correlationId = typeof assignment.context.replyCorrelationId === "string" &&
|
|
1262
|
+
assignment.context.replyCorrelationId.length > 0
|
|
1263
|
+
? assignment.context.replyCorrelationId
|
|
1264
|
+
: randomUUID();
|
|
1265
|
+
entry.correlation_ring.add(correlationId);
|
|
1266
|
+
const remoteMessageId = assignment.message.metadata.message_id;
|
|
1267
|
+
const metadata = {
|
|
1268
|
+
message_id: record.wireMessageId,
|
|
1269
|
+
...(typeof remoteMessageId === "string" &&
|
|
1270
|
+
remoteMessageId.length > 0 && { in_reply_to: remoteMessageId }),
|
|
1271
|
+
correlation_id: correlationId,
|
|
1272
|
+
source_endpoint_id: body.endpoint_id,
|
|
1273
|
+
require_live: false,
|
|
1274
|
+
};
|
|
1275
|
+
let ack;
|
|
1276
|
+
try {
|
|
1277
|
+
ack = await connector.send(assignment.message.sender_address, body.content, body.contentType ?? "text", metadata);
|
|
1278
|
+
}
|
|
1279
|
+
catch (err) {
|
|
1280
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1281
|
+
if (err instanceof ConnectorClientUnavailableError) {
|
|
1282
|
+
throw new BrokerHttpError(503, "connector_unavailable", msg, {
|
|
1283
|
+
retryable: true,
|
|
1284
|
+
});
|
|
1285
|
+
}
|
|
1286
|
+
if (err instanceof ConnectorSendAckTimeoutError) {
|
|
1287
|
+
record.outcomeUnknown = { message: msg };
|
|
1288
|
+
record.expiresAt =
|
|
1289
|
+
Date.now() + RUNTIME_ASSIGNMENT_REPLY_RECORD_TTL_MS;
|
|
1290
|
+
throw new BrokerHttpError(503, "connector_send_outcome_unknown", msg, { retryable: false });
|
|
1291
|
+
}
|
|
1292
|
+
throw err;
|
|
1293
|
+
}
|
|
1294
|
+
const result = {
|
|
1295
|
+
messageId: record.wireMessageId,
|
|
1296
|
+
status: ack.status,
|
|
1297
|
+
};
|
|
1298
|
+
record.result = result;
|
|
1299
|
+
record.expiresAt = Date.now() + RUNTIME_ASSIGNMENT_REPLY_RECORD_TTL_MS;
|
|
1300
|
+
const replyEmittedSequence = assignment.lastStateSequence + 1;
|
|
1301
|
+
emitReplyEmittedWithRetry(assignmentKey, {
|
|
1302
|
+
type: "runtime_processing_state",
|
|
1303
|
+
version: 1,
|
|
1304
|
+
source_message_id: assignment.context.sourceMessageId,
|
|
1305
|
+
assignment_id: assignment.context.assignmentId,
|
|
1306
|
+
state_event_id: `${assignment.context.assignmentId}:reply_emitted:${body.reply_request_id}`,
|
|
1307
|
+
state_sequence: replyEmittedSequence,
|
|
1308
|
+
target_ref: assignment.context.targetRef,
|
|
1309
|
+
state: "reply_emitted",
|
|
1310
|
+
occurred_at: Date.now(),
|
|
1311
|
+
undispatched_id: assignment.context.undispatchedId,
|
|
1312
|
+
reply_request_id: body.reply_request_id,
|
|
1313
|
+
outbound_message_id: record.wireMessageId,
|
|
1314
|
+
...(assignment.brokerProjectionToken !== undefined && {
|
|
1315
|
+
broker_projection_token: assignment.brokerProjectionToken,
|
|
1316
|
+
}),
|
|
1317
|
+
});
|
|
1318
|
+
assignment.lastStateSequence = replyEmittedSequence;
|
|
1319
|
+
return result;
|
|
1320
|
+
})();
|
|
1321
|
+
runtimeAssignmentReplyInFlight.set(replyKey, {
|
|
1322
|
+
endpointId: body.endpoint_id,
|
|
1323
|
+
pluginPid: body.plugin_pid,
|
|
1324
|
+
requestFingerprint,
|
|
1325
|
+
task,
|
|
1326
|
+
});
|
|
1327
|
+
try {
|
|
1328
|
+
return await task;
|
|
1329
|
+
}
|
|
1330
|
+
finally {
|
|
1331
|
+
runtimeAssignmentReplyInFlight.delete(replyKey);
|
|
1044
1332
|
}
|
|
1045
|
-
return { messageId: ack.messageId, status: ack.status };
|
|
1046
1333
|
},
|
|
1047
1334
|
async reportProcessingState(body) {
|
|
1048
1335
|
const entry = registry.get(body.endpoint_id);
|
|
@@ -1060,8 +1347,10 @@ export async function startBrokerDaemon(opts) {
|
|
|
1060
1347
|
if (assignment.context.targetRef.projection_endpoint_id !== body.endpoint_id) {
|
|
1061
1348
|
throw new BrokerHttpError(403, "runtime_assignment_target_mismatch", "endpoint_id does not match the assigned target");
|
|
1062
1349
|
}
|
|
1063
|
-
if (body.state === "
|
|
1064
|
-
|
|
1350
|
+
if (body.state === "assigned_to_runtime_target" ||
|
|
1351
|
+
body.state === "reply_requested" ||
|
|
1352
|
+
body.state === "reply_emitted") {
|
|
1353
|
+
throw new BrokerHttpError(403, `${body.state}_broker_owned`, `${body.state} is emitted by the broker assignment lifecycle path`);
|
|
1065
1354
|
}
|
|
1066
1355
|
const event = {
|
|
1067
1356
|
type: "runtime_processing_state",
|
|
@@ -1090,6 +1379,7 @@ export async function startBrokerDaemon(opts) {
|
|
|
1090
1379
|
if (!outcome.ok) {
|
|
1091
1380
|
throw new BrokerHttpError(outcome.terminal ? 409 : 503, "runtime_processing_state_rejected", outcome.detail ?? "runtime processing state was not accepted");
|
|
1092
1381
|
}
|
|
1382
|
+
assignment.lastStateSequence = Math.max(assignment.lastStateSequence, body.state_sequence);
|
|
1093
1383
|
if (body.state === "adapter_accepted") {
|
|
1094
1384
|
assignment.accepted = true;
|
|
1095
1385
|
if (assignment.acceptanceTimer) {
|
|
@@ -1097,6 +1387,21 @@ export async function startBrokerDaemon(opts) {
|
|
|
1097
1387
|
assignment.acceptanceTimer = undefined;
|
|
1098
1388
|
}
|
|
1099
1389
|
}
|
|
1390
|
+
if (!assignment.accepted &&
|
|
1391
|
+
(body.state === "processing_failed" ||
|
|
1392
|
+
body.state === "waiting_for_runtime_capability" ||
|
|
1393
|
+
body.state === "target_lost_before_acceptance")) {
|
|
1394
|
+
if (body.state === "processing_failed" && body.retryable !== true) {
|
|
1395
|
+
if (assignment.acceptanceTimer) {
|
|
1396
|
+
clearTimeout(assignment.acceptanceTimer);
|
|
1397
|
+
assignment.acceptanceTimer = undefined;
|
|
1398
|
+
}
|
|
1399
|
+
forgetRuntimeAssignment(key);
|
|
1400
|
+
return;
|
|
1401
|
+
}
|
|
1402
|
+
restorePendingAssignmentBeforeAcceptance(key, assignment, body.state);
|
|
1403
|
+
return;
|
|
1404
|
+
}
|
|
1100
1405
|
if (body.state === "processing_failed" || body.state === "expired") {
|
|
1101
1406
|
forgetRuntimeAssignment(key);
|
|
1102
1407
|
}
|
|
@@ -1175,12 +1480,9 @@ export async function startBrokerDaemon(opts) {
|
|
|
1175
1480
|
emitted_at: Date.now(),
|
|
1176
1481
|
undispatched_id,
|
|
1177
1482
|
source_message_id: pending?.source_message_id ?? undispatched_id,
|
|
1178
|
-
target_ref:
|
|
1179
|
-
kind: "projection_endpoint",
|
|
1180
|
-
projection_endpoint_id: target_endpoint_id,
|
|
1181
|
-
background_exchange_mode: normalizeBackgroundExchangeMode(target?.background_exchange_mode),
|
|
1182
|
-
},
|
|
1483
|
+
target_ref: buildLocalDispatchTargetRef(target_endpoint_id, target),
|
|
1183
1484
|
delivery_intent: "external_handoff",
|
|
1485
|
+
broker_projection_token: randomUUID(),
|
|
1184
1486
|
});
|
|
1185
1487
|
if (!result.ok) {
|
|
1186
1488
|
throw new BrokerHttpError(400, "dispatch_failed", result.detail ?? "unknown");
|
|
@@ -1259,7 +1561,36 @@ export async function startBrokerDaemon(opts) {
|
|
|
1259
1561
|
});
|
|
1260
1562
|
const inboundProcessing = new Set();
|
|
1261
1563
|
const blockedInboundSeqs = new Set();
|
|
1564
|
+
const asyncAcceptedInboundSeqs = new Set();
|
|
1262
1565
|
let unsequencedInboundBlocked = false;
|
|
1566
|
+
let deferredDeliveryPendingUpTo;
|
|
1567
|
+
const deliveredInboundSourceRoutes = new Map();
|
|
1568
|
+
const deliveredInboundSourceRouteOrder = [];
|
|
1569
|
+
const DELIVERED_INBOUND_SOURCE_ROUTE_CAP = 4096;
|
|
1570
|
+
const maybeAckDeferredDeliveryPending = () => {
|
|
1571
|
+
if (deferredDeliveryPendingUpTo === undefined ||
|
|
1572
|
+
unsequencedInboundBlocked ||
|
|
1573
|
+
blockedInboundSeqs.size > 0) {
|
|
1574
|
+
return;
|
|
1575
|
+
}
|
|
1576
|
+
const upTo = deferredDeliveryPendingUpTo;
|
|
1577
|
+
deferredDeliveryPendingUpTo = undefined;
|
|
1578
|
+
connector.ackDelivery(upTo);
|
|
1579
|
+
};
|
|
1580
|
+
const rememberDeliveredInboundSourceRoute = (sourceMessageId, event) => {
|
|
1581
|
+
if (deliveredInboundSourceRoutes.has(sourceMessageId)) {
|
|
1582
|
+
deliveredInboundSourceRoutes.set(sourceMessageId, event);
|
|
1583
|
+
return;
|
|
1584
|
+
}
|
|
1585
|
+
deliveredInboundSourceRoutes.set(sourceMessageId, event);
|
|
1586
|
+
deliveredInboundSourceRouteOrder.push(sourceMessageId);
|
|
1587
|
+
while (deliveredInboundSourceRouteOrder.length >
|
|
1588
|
+
DELIVERED_INBOUND_SOURCE_ROUTE_CAP) {
|
|
1589
|
+
const evicted = deliveredInboundSourceRouteOrder.shift();
|
|
1590
|
+
if (evicted)
|
|
1591
|
+
deliveredInboundSourceRoutes.delete(evicted);
|
|
1592
|
+
}
|
|
1593
|
+
};
|
|
1263
1594
|
const handleInboundMessage = async (payload) => {
|
|
1264
1595
|
const meta = payload.metadata ?? {};
|
|
1265
1596
|
const target_endpoint_id = typeof meta.target_endpoint_id === "string"
|
|
@@ -1332,6 +1663,58 @@ export async function startBrokerDaemon(opts) {
|
|
|
1332
1663
|
originalCorrelationId: correlation_id,
|
|
1333
1664
|
});
|
|
1334
1665
|
}
|
|
1666
|
+
const routeKind = decision.entry.state === "active"
|
|
1667
|
+
? "active_endpoint"
|
|
1668
|
+
: "reconnecting_endpoint";
|
|
1669
|
+
const deliverySeq = payload.seq;
|
|
1670
|
+
const onQueuedFinalizationSuccess = typeof deliverySeq === "number"
|
|
1671
|
+
? () => {
|
|
1672
|
+
const wasAlreadyBlocked = blockedInboundSeqs.delete(deliverySeq);
|
|
1673
|
+
if (!wasAlreadyBlocked) {
|
|
1674
|
+
asyncAcceptedInboundSeqs.add(deliverySeq);
|
|
1675
|
+
}
|
|
1676
|
+
markDeliverySeqAccepted(deliverySeq);
|
|
1677
|
+
maybeAckDeferredDeliveryPending();
|
|
1678
|
+
}
|
|
1679
|
+
: undefined;
|
|
1680
|
+
if (payload.messageId !== undefined) {
|
|
1681
|
+
const delivered = deliveredInboundSourceRoutes.get(payload.messageId);
|
|
1682
|
+
if (delivered) {
|
|
1683
|
+
const posted = await directPostRuntimeInboundRouted(delivered, {
|
|
1684
|
+
allowQueuedAsSuccess: payload.seq === undefined,
|
|
1685
|
+
onQueuedSuccess: onQueuedFinalizationSuccess,
|
|
1686
|
+
});
|
|
1687
|
+
if (!posted)
|
|
1688
|
+
return false;
|
|
1689
|
+
const accepted = markDeliverySeqAccepted(payload.seq);
|
|
1690
|
+
if (accepted)
|
|
1691
|
+
maybeAckDeferredDeliveryPending();
|
|
1692
|
+
return accepted;
|
|
1693
|
+
}
|
|
1694
|
+
const claim = await directClaimRuntimeInboundRoute({
|
|
1695
|
+
type: "runtime_inbound_route_claim",
|
|
1696
|
+
version: 1,
|
|
1697
|
+
source_message_id: payload.messageId,
|
|
1698
|
+
routed_to_endpoint_id: decision.entry.endpoint_id,
|
|
1699
|
+
route_kind: routeKind,
|
|
1700
|
+
claimed_at: Date.now(),
|
|
1701
|
+
});
|
|
1702
|
+
if (claim === "retry")
|
|
1703
|
+
return false;
|
|
1704
|
+
if (claim === "skip")
|
|
1705
|
+
return markDeliverySeqAccepted(payload.seq);
|
|
1706
|
+
}
|
|
1707
|
+
let routedEvent;
|
|
1708
|
+
if (payload.messageId !== undefined) {
|
|
1709
|
+
routedEvent = {
|
|
1710
|
+
type: "runtime_inbound_routed",
|
|
1711
|
+
version: 1,
|
|
1712
|
+
source_message_id: payload.messageId,
|
|
1713
|
+
routed_to_endpoint_id: decision.entry.endpoint_id,
|
|
1714
|
+
route_kind: routeKind,
|
|
1715
|
+
routed_at: Date.now(),
|
|
1716
|
+
};
|
|
1717
|
+
}
|
|
1335
1718
|
recordInboundCorrelation(decision.entry.endpoint_id, meta);
|
|
1336
1719
|
if (decision.entry.state === "active") {
|
|
1337
1720
|
const pushed = pushToPlugin(decision.entry.ipc_ws, {
|
|
@@ -1376,18 +1759,13 @@ export async function startBrokerDaemon(opts) {
|
|
|
1376
1759
|
buffer_size: buf.size(),
|
|
1377
1760
|
});
|
|
1378
1761
|
}
|
|
1379
|
-
if (payload.messageId !== undefined) {
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
? "active_endpoint"
|
|
1387
|
-
: "reconnecting_endpoint",
|
|
1388
|
-
routed_at: Date.now(),
|
|
1389
|
-
}, { queueOnTransientFailure: typeof payload.seq !== "number" });
|
|
1390
|
-
if (!posted && typeof payload.seq === "number")
|
|
1762
|
+
if (routedEvent !== undefined && payload.messageId !== undefined) {
|
|
1763
|
+
rememberDeliveredInboundSourceRoute(payload.messageId, routedEvent);
|
|
1764
|
+
const posted = await directPostRuntimeInboundRouted(routedEvent, {
|
|
1765
|
+
allowQueuedAsSuccess: payload.seq === undefined,
|
|
1766
|
+
onQueuedSuccess: onQueuedFinalizationSuccess,
|
|
1767
|
+
});
|
|
1768
|
+
if (!posted)
|
|
1391
1769
|
return false;
|
|
1392
1770
|
}
|
|
1393
1771
|
return markDeliverySeqAccepted(payload.seq);
|
|
@@ -1420,10 +1798,13 @@ export async function startBrokerDaemon(opts) {
|
|
|
1420
1798
|
task = handleInboundMessage(payload)
|
|
1421
1799
|
.then((accepted) => {
|
|
1422
1800
|
if (typeof payload.seq === "number") {
|
|
1423
|
-
if (accepted)
|
|
1801
|
+
if (accepted || asyncAcceptedInboundSeqs.has(payload.seq)) {
|
|
1424
1802
|
blockedInboundSeqs.delete(payload.seq);
|
|
1425
|
-
|
|
1803
|
+
asyncAcceptedInboundSeqs.delete(payload.seq);
|
|
1804
|
+
}
|
|
1805
|
+
else {
|
|
1426
1806
|
blockedInboundSeqs.add(payload.seq);
|
|
1807
|
+
}
|
|
1427
1808
|
return;
|
|
1428
1809
|
}
|
|
1429
1810
|
if (accepted)
|
|
@@ -1454,6 +1835,7 @@ export async function startBrokerDaemon(opts) {
|
|
|
1454
1835
|
await Promise.allSettled([...inboundProcessing]);
|
|
1455
1836
|
}
|
|
1456
1837
|
if (unsequencedInboundBlocked || blockedInboundSeqs.size > 0) {
|
|
1838
|
+
deferredDeliveryPendingUpTo = payload.upTo;
|
|
1457
1839
|
logger.warn("delivery_ack_deferred_until_replay", {
|
|
1458
1840
|
upTo: payload.upTo,
|
|
1459
1841
|
count: payload.count,
|
|
@@ -1462,6 +1844,7 @@ export async function startBrokerDaemon(opts) {
|
|
|
1462
1844
|
});
|
|
1463
1845
|
return;
|
|
1464
1846
|
}
|
|
1847
|
+
deferredDeliveryPendingUpTo = undefined;
|
|
1465
1848
|
connector.ackDelivery(payload.upTo);
|
|
1466
1849
|
});
|
|
1467
1850
|
writeDiscoveryFile(paths.discoveryFile, {
|