@cortexkit/aft-bridge 0.57.2 → 0.58.0
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/bash-hints.d.ts +22 -3
- package/dist/bash-hints.d.ts.map +1 -1
- package/dist/bash-hints.js +28 -9
- package/dist/bash-hints.js.map +1 -1
- package/dist/bash-host-fallback.js +1 -1
- package/dist/bash-host-fallback.js.map +1 -1
- package/dist/binary-identity.d.ts +109 -0
- package/dist/binary-identity.d.ts.map +1 -0
- package/dist/binary-identity.js +295 -0
- package/dist/binary-identity.js.map +1 -0
- package/dist/bridge.d.ts +10 -1
- package/dist/bridge.d.ts.map +1 -1
- package/dist/bridge.js +50 -14
- package/dist/bridge.js.map +1 -1
- package/dist/child-process.d.ts +37 -0
- package/dist/child-process.d.ts.map +1 -0
- package/dist/child-process.js +62 -0
- package/dist/child-process.js.map +1 -0
- package/dist/coerce.d.ts +2 -0
- package/dist/coerce.d.ts.map +1 -1
- package/dist/coerce.js +29 -0
- package/dist/coerce.js.map +1 -1
- package/dist/config-error.d.ts +71 -0
- package/dist/config-error.d.ts.map +1 -0
- package/dist/config-error.js +112 -0
- package/dist/config-error.js.map +1 -0
- package/dist/downloader.d.ts +4 -2
- package/dist/downloader.d.ts.map +1 -1
- package/dist/downloader.js +41 -20
- package/dist/downloader.js.map +1 -1
- package/dist/durable-log.d.ts +7 -0
- package/dist/durable-log.d.ts.map +1 -1
- package/dist/durable-log.js +47 -4
- package/dist/durable-log.js.map +1 -1
- package/dist/feature-config.d.ts +192 -0
- package/dist/feature-config.d.ts.map +1 -0
- package/dist/feature-config.js +657 -0
- package/dist/feature-config.js.map +1 -0
- package/dist/index.d.ts +17 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +17 -6
- package/dist/index.js.map +1 -1
- package/dist/message-detach.d.ts +25 -0
- package/dist/message-detach.d.ts.map +1 -0
- package/dist/message-detach.js +34 -0
- package/dist/message-detach.js.map +1 -0
- package/dist/migration-notices.d.ts +29 -0
- package/dist/migration-notices.d.ts.map +1 -0
- package/dist/migration-notices.js +114 -0
- package/dist/migration-notices.js.map +1 -0
- package/dist/migration.d.ts +1 -1
- package/dist/migration.d.ts.map +1 -1
- package/dist/migration.js +1 -1
- package/dist/migration.js.map +1 -1
- package/dist/npm-resolver.d.ts +1 -1
- package/dist/npm-resolver.d.ts.map +1 -1
- package/dist/npm-resolver.js +1 -1
- package/dist/npm-resolver.js.map +1 -1
- package/dist/onnx-probe.d.ts +44 -0
- package/dist/onnx-probe.d.ts.map +1 -0
- package/dist/onnx-probe.js +167 -0
- package/dist/onnx-probe.js.map +1 -0
- package/dist/onnx-runtime.d.ts +19 -4
- package/dist/onnx-runtime.d.ts.map +1 -1
- package/dist/onnx-runtime.js +146 -34
- package/dist/onnx-runtime.js.map +1 -1
- package/dist/path-lookup.d.ts +22 -0
- package/dist/path-lookup.d.ts.map +1 -0
- package/dist/path-lookup.js +51 -0
- package/dist/path-lookup.js.map +1 -0
- package/dist/resolver.d.ts +47 -21
- package/dist/resolver.d.ts.map +1 -1
- package/dist/resolver.js +254 -159
- package/dist/resolver.js.map +1 -1
- package/dist/revivable-transport.d.ts.map +1 -1
- package/dist/revivable-transport.js +12 -0
- package/dist/revivable-transport.js.map +1 -1
- package/dist/semantic-status.d.ts +10 -1
- package/dist/semantic-status.d.ts.map +1 -1
- package/dist/semantic-status.js +50 -2
- package/dist/semantic-status.js.map +1 -1
- package/dist/storage-paths.d.ts +9 -0
- package/dist/storage-paths.d.ts.map +1 -1
- package/dist/storage-paths.js +18 -1
- package/dist/storage-paths.js.map +1 -1
- package/dist/subc-transport.d.ts +58 -1
- package/dist/subc-transport.d.ts.map +1 -1
- package/dist/subc-transport.js +291 -35
- package/dist/subc-transport.js.map +1 -1
- package/dist/tar-executable.js +1 -1
- package/dist/tar-executable.js.map +1 -1
- package/dist/transport-factory.d.ts +14 -2
- package/dist/transport-factory.d.ts.map +1 -1
- package/dist/transport-factory.js +24 -16
- package/dist/transport-factory.js.map +1 -1
- package/dist/version-probe.d.ts +33 -0
- package/dist/version-probe.d.ts.map +1 -0
- package/dist/version-probe.js +108 -0
- package/dist/version-probe.js.map +1 -0
- package/package.json +1 -1
- package/dist/bash-timeout.d.ts +0 -19
- package/dist/bash-timeout.d.ts.map +0 -1
- package/dist/bash-timeout.js +0 -26
- package/dist/bash-timeout.js.map +0 -1
package/dist/subc-transport.js
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
*/
|
|
19
19
|
import { existsSync, statSync } from "node:fs";
|
|
20
20
|
import { connectionFileExists, isConsumerReconnectTransient, StaleRouteHandleError, SubcCallError, SubcClient, SubcError, } from "@cortexkit/subc-client";
|
|
21
|
-
import { log } from "./active-logger.js";
|
|
21
|
+
import { log, warn } from "./active-logger.js";
|
|
22
22
|
import { isRouteOpenReloadWindowError, isRouteRequestReloadRefusal } from "./error-contract.js";
|
|
23
23
|
import { asCanonicalRootPath, asRootGeneration, LifecycleRegistry, } from "./lifecycle-registry.js";
|
|
24
24
|
import { canonicalizeProjectRoot } from "./project-identity.js";
|
|
@@ -33,20 +33,102 @@ export class SubcTransportShuttingDownError extends SubcCallError {
|
|
|
33
33
|
export function isSubcClientClosedError(error) {
|
|
34
34
|
return error instanceof SubcError && error.message === "client closed";
|
|
35
35
|
}
|
|
36
|
-
|
|
36
|
+
/**
|
|
37
|
+
* True only when an error is evidence that the shared socket itself is dead, so
|
|
38
|
+
* every route on it is gone and the client must be replaced.
|
|
39
|
+
*
|
|
40
|
+
* subc-client's `isConsumerReconnectTransient` answers a different question
|
|
41
|
+
* ("may a managed call reconnect and retry?") and also accepts a managed
|
|
42
|
+
* `SubcCallError` of kind `not_sent` / `outcome_unknown`. Those kinds are the
|
|
43
|
+
* verdict on ONE call: a call whose deadline passed while the socket stayed up
|
|
44
|
+
* is `outcome_unknown` too. So a `SubcCallError` counts as socket death only when
|
|
45
|
+
* the error it wraps does. Everything else defers to the client's classifier,
|
|
46
|
+
* whose remaining members are all socket-level: the socket closed or reset, a
|
|
47
|
+
* write that could not complete, a handshake/auth failure while reconnecting,
|
|
48
|
+
* ECONN and EPIPE errno codes. The client's own "client closed" rejection means
|
|
49
|
+
* the connection has already been torn down locally.
|
|
50
|
+
*/
|
|
51
|
+
export function isSocketDeathError(error, depth = 0) {
|
|
52
|
+
if (error instanceof SubcCallError) {
|
|
53
|
+
const cause = error.cause;
|
|
54
|
+
return depth < 4 && cause !== undefined && cause !== error
|
|
55
|
+
? isSocketDeathError(cause, depth + 1)
|
|
56
|
+
: false;
|
|
57
|
+
}
|
|
37
58
|
return isConsumerReconnectTransient(error) || isSubcClientClosedError(error);
|
|
38
59
|
}
|
|
60
|
+
/**
|
|
61
|
+
* A call that never got an answer on a socket that may still be alive: the
|
|
62
|
+
* client's own request deadline (raw `request`/`routeOpen` reject with code
|
|
63
|
+
* `request_deadline`) or a managed verdict of `not_sent` / `outcome_unknown`
|
|
64
|
+
* that is not backed by socket death. Several of these in a row are grounds to
|
|
65
|
+
* check the connection, never to close it on their own.
|
|
66
|
+
*/
|
|
67
|
+
function isUnansweredCallError(error) {
|
|
68
|
+
if (isSocketDeathError(error))
|
|
69
|
+
return false;
|
|
70
|
+
if (error instanceof SubcCallError)
|
|
71
|
+
return error.kind !== "terminal";
|
|
72
|
+
return error instanceof SubcError && error.code === REQUEST_DEADLINE_CODE;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* A rejection that the daemon itself sent back (a coded wire Error frame, such as
|
|
76
|
+
* a refused route.open), which proves the socket still carries replies. The
|
|
77
|
+
* client's local deadline marker is also a code, so it is excluded.
|
|
78
|
+
*/
|
|
79
|
+
function isDaemonAnswerError(error) {
|
|
80
|
+
return (error instanceof SubcError &&
|
|
81
|
+
typeof error.code === "string" &&
|
|
82
|
+
error.code !== REQUEST_DEADLINE_CODE &&
|
|
83
|
+
!isSubcClientClosedError(error));
|
|
84
|
+
}
|
|
85
|
+
/** Short `Class/kind/code` label for a drop log line. */
|
|
86
|
+
function describeFailure(error) {
|
|
87
|
+
if (!(error instanceof Error))
|
|
88
|
+
return `non-error:${String(error)}`;
|
|
89
|
+
const className = error.name && error.name !== "Error" ? error.name : (error.constructor?.name ?? "Error");
|
|
90
|
+
const parts = [className];
|
|
91
|
+
if (error instanceof SubcCallError)
|
|
92
|
+
parts.push(error.kind);
|
|
93
|
+
const code = error.code;
|
|
94
|
+
if (typeof code === "string" && code.length > 0)
|
|
95
|
+
parts.push(code);
|
|
96
|
+
return parts.join("/");
|
|
97
|
+
}
|
|
98
|
+
function routeLabel(route) {
|
|
99
|
+
return route ? `${route.channel}@${route.epoch}` : undefined;
|
|
100
|
+
}
|
|
101
|
+
/** The liveness probe saw no reply on the shared connection within its window. */
|
|
102
|
+
export class SubcLivenessProbeTimeoutError extends Error {
|
|
103
|
+
code = "liveness_probe_timeout";
|
|
104
|
+
constructor(windowMs) {
|
|
105
|
+
super(`no reply to a channel-0 catalog.list probe within ${windowMs}ms`);
|
|
106
|
+
this.name = "SubcLivenessProbeTimeoutError";
|
|
107
|
+
}
|
|
108
|
+
}
|
|
39
109
|
/** The subc module id AFT registers under (matches the daemon manifest). */
|
|
40
110
|
const AFT_MODULE_ID = "aft";
|
|
41
111
|
/**
|
|
42
|
-
* A run of consecutive
|
|
43
|
-
* on the SAME client
|
|
44
|
-
* succeed, no response ever arrives
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
112
|
+
* A run of consecutive per-call failures that got no answer (timeout / route
|
|
113
|
+
* GOODBYE / refusal) on the SAME client raises the suspicion of a dead half-open
|
|
114
|
+
* connection: local writes succeed, no response ever arrives. The run alone never
|
|
115
|
+
* closes the client, because the calls may belong to different sessions that are
|
|
116
|
+
* each merely slow. It starts a liveness probe on the same connection, and the
|
|
117
|
+
* client is dropped only if that probe also goes unanswered. The counter resets
|
|
118
|
+
* on any successful request. Tool-level errors never count: they return
|
|
119
|
+
* `success:false`, they do not throw. (Audit B-#4.)
|
|
48
120
|
*/
|
|
49
121
|
const MAX_CONSECUTIVE_TRANSPORT_FAILURES = 3;
|
|
122
|
+
/**
|
|
123
|
+
* How long the liveness probe waits for its reply. subc-client documents that
|
|
124
|
+
* the daemon handles a connection's channel-0 frames in order and can hold one
|
|
125
|
+
* route.open for its whole bind relay (about 12s in production), so the probe
|
|
126
|
+
* may queue behind a legitimate bind. A shorter window would convict a busy but
|
|
127
|
+
* healthy connection.
|
|
128
|
+
*/
|
|
129
|
+
const LIVENESS_PROBE_TIMEOUT_MS = 15_000;
|
|
130
|
+
/** Code subc-client puts on a raw request or route.open that hit its own deadline. */
|
|
131
|
+
const REQUEST_DEADLINE_CODE = "request_deadline";
|
|
50
132
|
/** Every transport reconnect waits at least one event-loop turn and caps repeated failures. */
|
|
51
133
|
const RECONNECT_RETRY_FLOOR_MS = 100;
|
|
52
134
|
const RECONNECT_RETRY_CAP_MS = 2_000;
|
|
@@ -174,13 +256,21 @@ class BgSubscription {
|
|
|
174
256
|
dispatchProbeIntervalMs;
|
|
175
257
|
nudgeRef;
|
|
176
258
|
isCurrent;
|
|
259
|
+
noteClientAlive;
|
|
260
|
+
noteUnansweredCall;
|
|
177
261
|
stopped = false;
|
|
178
262
|
/** The live subscription handle, read by stop() to wake the loop's `await closed`. */
|
|
179
263
|
current = null;
|
|
264
|
+
/** Client carrying the live subscription route, counted when that client is dropped. */
|
|
265
|
+
attached = null;
|
|
180
266
|
loop;
|
|
181
267
|
lifecycleLogState = new Map();
|
|
182
268
|
nudgeReceiptLogState = null;
|
|
183
|
-
constructor(identity, acquireClient, dropClient, consumerIdentity, onNudge, sleep, canAttach, onRootAttachFailure, onDormant, dispatchProbeIntervalMs, nudgeRef, isCurrent = () => true
|
|
269
|
+
constructor(identity, acquireClient, dropClient, consumerIdentity, onNudge, sleep, canAttach, onRootAttachFailure, onDormant, dispatchProbeIntervalMs, nudgeRef, isCurrent = () => true,
|
|
270
|
+
/** Reports a reply seen on the client, which proves its socket still delivers. */
|
|
271
|
+
noteClientAlive = () => undefined,
|
|
272
|
+
/** Reports a call on the client that got no answer; enough in a row start a liveness probe. */
|
|
273
|
+
noteUnansweredCall = () => undefined) {
|
|
184
274
|
this.identity = identity;
|
|
185
275
|
this.acquireClient = acquireClient;
|
|
186
276
|
this.dropClient = dropClient;
|
|
@@ -193,8 +283,14 @@ class BgSubscription {
|
|
|
193
283
|
this.dispatchProbeIntervalMs = dispatchProbeIntervalMs;
|
|
194
284
|
this.nudgeRef = nudgeRef;
|
|
195
285
|
this.isCurrent = isCurrent;
|
|
286
|
+
this.noteClientAlive = noteClientAlive;
|
|
287
|
+
this.noteUnansweredCall = noteUnansweredCall;
|
|
196
288
|
this.loop = this.run();
|
|
197
289
|
}
|
|
290
|
+
/** The client whose route currently carries this subscription, if any. */
|
|
291
|
+
get attachedClient() {
|
|
292
|
+
return this.attached;
|
|
293
|
+
}
|
|
198
294
|
async stop() {
|
|
199
295
|
this.stopped = true;
|
|
200
296
|
// Wake a live `await sub.closed`; the loop's `finally` remains the sole
|
|
@@ -325,8 +421,23 @@ class BgSubscription {
|
|
|
325
421
|
this.onDormant();
|
|
326
422
|
return;
|
|
327
423
|
}
|
|
328
|
-
if (
|
|
329
|
-
this.dropClient(client
|
|
424
|
+
if (isSocketDeathError(err)) {
|
|
425
|
+
this.dropClient(client, {
|
|
426
|
+
error: err,
|
|
427
|
+
trigger: "bg_events route.open",
|
|
428
|
+
session: this.identity.session,
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
else if (isDaemonAnswerError(err)) {
|
|
432
|
+
this.noteClientAlive(client);
|
|
433
|
+
}
|
|
434
|
+
else if (isUnansweredCallError(err)) {
|
|
435
|
+
this.noteUnansweredCall(client, {
|
|
436
|
+
error: err,
|
|
437
|
+
trigger: "bg_events route.open",
|
|
438
|
+
session: this.identity.session,
|
|
439
|
+
});
|
|
440
|
+
}
|
|
330
441
|
if (!reconnecting)
|
|
331
442
|
beginReconnect();
|
|
332
443
|
this.info("reconnect-error", `reconnect error attempt=${reconnectAttempt} error=${this.errorText(err)}`);
|
|
@@ -341,6 +452,7 @@ class BgSubscription {
|
|
|
341
452
|
giveUp(this.stopped ? "stopped" : "stale-session");
|
|
342
453
|
return;
|
|
343
454
|
}
|
|
455
|
+
this.noteClientAlive(client);
|
|
344
456
|
const subscribedAt = Date.now();
|
|
345
457
|
const routeId = this.routeId(route);
|
|
346
458
|
let stopDispatchProbe = () => undefined;
|
|
@@ -351,12 +463,14 @@ class BgSubscription {
|
|
|
351
463
|
return;
|
|
352
464
|
}
|
|
353
465
|
this.recordNudgeReceipt(routeId);
|
|
466
|
+
this.noteClientAlive(client);
|
|
354
467
|
if (!this.isCurrent()) {
|
|
355
468
|
this.info("nudge-stale-carrier", `nudge carried by stale subscription; checking current session channel=${routeId}`);
|
|
356
469
|
}
|
|
357
470
|
this.onNudge();
|
|
358
471
|
});
|
|
359
472
|
this.current = sub;
|
|
473
|
+
this.attached = client;
|
|
360
474
|
stopDispatchProbe = this.startDispatchProbe(client, routeId);
|
|
361
475
|
this.info("subscription-open", `subscription open channel=${routeId}`);
|
|
362
476
|
if (reconnecting) {
|
|
@@ -391,8 +505,14 @@ class BgSubscription {
|
|
|
391
505
|
giveUp("stopped");
|
|
392
506
|
return;
|
|
393
507
|
}
|
|
394
|
-
if (
|
|
395
|
-
this.dropClient(client
|
|
508
|
+
if (isSocketDeathError(err)) {
|
|
509
|
+
this.dropClient(client, {
|
|
510
|
+
error: err,
|
|
511
|
+
trigger: "bg_events stream",
|
|
512
|
+
session: this.identity.session,
|
|
513
|
+
route: routeId,
|
|
514
|
+
});
|
|
515
|
+
}
|
|
396
516
|
if (Date.now() - subscribedAt >= BG_STABLE_MS)
|
|
397
517
|
backoffAttempt = 0;
|
|
398
518
|
beginReconnect();
|
|
@@ -400,6 +520,7 @@ class BgSubscription {
|
|
|
400
520
|
finally {
|
|
401
521
|
stopDispatchProbe();
|
|
402
522
|
this.current = null;
|
|
523
|
+
this.attached = null;
|
|
403
524
|
safeCloseRoute(client, route);
|
|
404
525
|
}
|
|
405
526
|
await this.backoff(backoffAttempt++);
|
|
@@ -617,6 +738,7 @@ export class SubcTransportPool {
|
|
|
617
738
|
bgBackoffSleep;
|
|
618
739
|
routeRetrySleep;
|
|
619
740
|
bgDispatchProbeIntervalMs;
|
|
741
|
+
livenessProbeTimeoutMs;
|
|
620
742
|
lifecycleDemandCheck;
|
|
621
743
|
onLifecycleEvent;
|
|
622
744
|
onBgNudgeRejected;
|
|
@@ -642,8 +764,15 @@ export class SubcTransportPool {
|
|
|
642
764
|
rootIndex = new Map();
|
|
643
765
|
/** Roots whose route binds must stay dormant until their directories return. */
|
|
644
766
|
dormantRoots = new Map();
|
|
645
|
-
/** Consecutive
|
|
767
|
+
/** Consecutive unanswered calls on the current pool-local client. */
|
|
646
768
|
transportFailures = 0;
|
|
769
|
+
/** The in-flight liveness probe of the current client; at most one runs at a time. */
|
|
770
|
+
livenessProbe = null;
|
|
771
|
+
/**
|
|
772
|
+
* Bumped whenever the current client delivers any reply. A liveness probe that
|
|
773
|
+
* times out while this moved has been outrun by other traffic, not by a dead socket.
|
|
774
|
+
*/
|
|
775
|
+
clientLivenessMark = 0;
|
|
647
776
|
/** Concrete per-root facades, including their captured root generation. */
|
|
648
777
|
transports = new Map();
|
|
649
778
|
generationRejections = new Set();
|
|
@@ -666,6 +795,7 @@ export class SubcTransportPool {
|
|
|
666
795
|
options.routeRetrySleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
667
796
|
this.bgDispatchProbeIntervalMs =
|
|
668
797
|
options.bgDispatchProbeIntervalMs ?? BG_DISPATCH_PROBE_INTERVAL_MS;
|
|
798
|
+
this.livenessProbeTimeoutMs = options.livenessProbeTimeoutMs ?? LIVENESS_PROBE_TIMEOUT_MS;
|
|
669
799
|
const lifecycle = options.lifecycle;
|
|
670
800
|
const demandCheck = options.lifecycleDemandCheck ?? options.demandCheck ?? lifecycle?.demandCheck;
|
|
671
801
|
this.lifecycleDemandCheck = demandCheck;
|
|
@@ -1170,10 +1300,25 @@ export class SubcTransportPool {
|
|
|
1170
1300
|
throw this.annotateReapError(error, record);
|
|
1171
1301
|
if (error instanceof RouteTornDownError)
|
|
1172
1302
|
throw error;
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
this.client
|
|
1176
|
-
|
|
1303
|
+
const ownsClient = this.isCurrentSession(key, record) && this.client === client;
|
|
1304
|
+
if (ownsClient && isSocketDeathError(error)) {
|
|
1305
|
+
this.dropClient(client, {
|
|
1306
|
+
error,
|
|
1307
|
+
trigger: "route.open",
|
|
1308
|
+
session: identity.session,
|
|
1309
|
+
});
|
|
1310
|
+
}
|
|
1311
|
+
else if (isDaemonAnswerError(error)) {
|
|
1312
|
+
// A refusal (cap reached, target unavailable, ...) fails this route
|
|
1313
|
+
// only, and proves the shared socket still delivers replies.
|
|
1314
|
+
this.noteClientAlive(client);
|
|
1315
|
+
}
|
|
1316
|
+
else if (ownsClient && isUnansweredCallError(error)) {
|
|
1317
|
+
this.noteUnansweredCall(client, {
|
|
1318
|
+
error,
|
|
1319
|
+
trigger: "route.open",
|
|
1320
|
+
session: identity.session,
|
|
1321
|
+
});
|
|
1177
1322
|
}
|
|
1178
1323
|
throw error;
|
|
1179
1324
|
}
|
|
@@ -1215,23 +1360,33 @@ export class SubcTransportPool {
|
|
|
1215
1360
|
safeCloseRoute(entry.client, entry.handle);
|
|
1216
1361
|
};
|
|
1217
1362
|
const handleRequestFailure = (error, entry) => {
|
|
1363
|
+
const aborted = error instanceof Error && error.name === "AbortError";
|
|
1364
|
+
const ownsClient = this.isCurrentSession(key, record) &&
|
|
1365
|
+
this.client === client &&
|
|
1366
|
+
!this.isReapInduced(record);
|
|
1367
|
+
if (!aborted && ownsClient && isSocketDeathError(error)) {
|
|
1368
|
+
// Dropped before the route is cleared so the drop log counts it.
|
|
1369
|
+
this.dropClient(client, {
|
|
1370
|
+
error,
|
|
1371
|
+
trigger: "request",
|
|
1372
|
+
session: identity.session,
|
|
1373
|
+
route: routeLabel(entry.handle),
|
|
1374
|
+
});
|
|
1375
|
+
}
|
|
1218
1376
|
clearRouteEntry(entry);
|
|
1219
|
-
if (
|
|
1377
|
+
if (aborted || !ownsClient || this.client !== client)
|
|
1220
1378
|
return;
|
|
1221
|
-
if (
|
|
1222
|
-
this.client !== client ||
|
|
1223
|
-
this.isReapInduced(record))
|
|
1379
|
+
if (isRouteProvenAbsentError(error))
|
|
1224
1380
|
return;
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
}
|
|
1381
|
+
// Anything else is this call's own outcome: a timeout, a GOODBYE, a
|
|
1382
|
+
// refusal, or a managed not_sent/outcome_unknown verdict. It fails this
|
|
1383
|
+
// call and route only; a run of them just checks the connection.
|
|
1384
|
+
this.noteUnansweredCall(client, {
|
|
1385
|
+
error,
|
|
1386
|
+
trigger: "request",
|
|
1387
|
+
session: identity.session,
|
|
1388
|
+
route: routeLabel(entry.handle),
|
|
1389
|
+
});
|
|
1235
1390
|
};
|
|
1236
1391
|
const requestOnRoute = async (route, entry) => {
|
|
1237
1392
|
this.assertRecordLive(record);
|
|
@@ -1262,6 +1417,7 @@ export class SubcTransportPool {
|
|
|
1262
1417
|
this.assertGeneration(record.canonicalRoot, record.generation, "request_completion");
|
|
1263
1418
|
if (this.client === client)
|
|
1264
1419
|
this.transportFailures = 0;
|
|
1420
|
+
this.noteClientAlive(client);
|
|
1265
1421
|
this.ensureBgSubscription(identity, record);
|
|
1266
1422
|
return reply;
|
|
1267
1423
|
}
|
|
@@ -1425,6 +1581,7 @@ export class SubcTransportPool {
|
|
|
1425
1581
|
}
|
|
1426
1582
|
entry.handle = route;
|
|
1427
1583
|
entry.opening = null;
|
|
1584
|
+
this.noteClientAlive(client);
|
|
1428
1585
|
return route;
|
|
1429
1586
|
})
|
|
1430
1587
|
.catch((error) => {
|
|
@@ -1487,13 +1644,13 @@ export class SubcTransportPool {
|
|
|
1487
1644
|
if (sub && record.bgSub === sub)
|
|
1488
1645
|
record.bgSub = null;
|
|
1489
1646
|
};
|
|
1490
|
-
sub = new BgSubscription(identity, () => this.ensureClient(), (client) => this.dropClient(client), this.consumerIdentity, onNudge, this.bgBackoffSleep, () => this.rootCanAttach(record.canonicalRoot), (error) => {
|
|
1647
|
+
sub = new BgSubscription(identity, () => this.ensureClient(), (client, cause) => this.dropClient(client, cause), this.consumerIdentity, onNudge, this.bgBackoffSleep, () => this.rootCanAttach(record.canonicalRoot), (error) => {
|
|
1491
1648
|
if (!isAbsentRootRouteError(error))
|
|
1492
1649
|
return false;
|
|
1493
1650
|
this.markRootDormant(record.canonicalRoot, error);
|
|
1494
1651
|
return true;
|
|
1495
1652
|
}, clearDormantSubscription, this.bgDispatchProbeIntervalMs, nudgeRef, () => this.isCurrentSession(record.identityKey, record) &&
|
|
1496
|
-
this.isCurrentLiveGeneration(record.canonicalRoot, record.generation));
|
|
1653
|
+
this.isCurrentLiveGeneration(record.canonicalRoot, record.generation), (client) => this.noteClientAlive(client), (client, cause) => this.noteUnansweredCall(client, cause));
|
|
1497
1654
|
record.bgSub = sub;
|
|
1498
1655
|
if (!this.rootCanAttach(record.canonicalRoot)) {
|
|
1499
1656
|
record.bgSub = null;
|
|
@@ -1503,10 +1660,30 @@ export class SubcTransportPool {
|
|
|
1503
1660
|
/**
|
|
1504
1661
|
* Invalidate only routes owned by a dead client. Sessions remain indexed so a
|
|
1505
1662
|
* replacement client can reconnect the same identity and its bg subscription.
|
|
1663
|
+
*
|
|
1664
|
+
* Only socket death reaches here (see {@link isSocketDeathError}), or a run of
|
|
1665
|
+
* unanswered calls that a liveness probe then confirmed. Every drop takes all
|
|
1666
|
+
* sessions' routes down with it, so each one logs its cause and its reach.
|
|
1506
1667
|
*/
|
|
1507
|
-
dropClient(client) {
|
|
1668
|
+
dropClient(client, cause) {
|
|
1508
1669
|
if (this.client !== client)
|
|
1509
1670
|
return;
|
|
1671
|
+
let sessions = 0;
|
|
1672
|
+
let routes = 0;
|
|
1673
|
+
for (const record of this.sessions.values()) {
|
|
1674
|
+
const toolRoute = record.routeEntry?.client === client ? 1 : 0;
|
|
1675
|
+
const bgRoute = record.bgSub?.attachedClient === client ? 1 : 0;
|
|
1676
|
+
if (toolRoute + bgRoute > 0)
|
|
1677
|
+
sessions += 1;
|
|
1678
|
+
routes += toolRoute + bgRoute;
|
|
1679
|
+
}
|
|
1680
|
+
const message = cause.error instanceof Error ? cause.error.message : String(cause.error);
|
|
1681
|
+
warn(`subc pool: dropping shared client cause=${describeFailure(cause.error)} trigger=${cause.trigger}` +
|
|
1682
|
+
(cause.session ? ` session=${cause.session}` : "") +
|
|
1683
|
+
(cause.route ? ` route=${cause.route}` : "") +
|
|
1684
|
+
` sessions=${sessions} routes=${routes}` +
|
|
1685
|
+
(cause.detail ? ` ${cause.detail}` : "") +
|
|
1686
|
+
` message=${JSON.stringify(message)}`);
|
|
1510
1687
|
this.client = null;
|
|
1511
1688
|
for (const record of this.sessions.values()) {
|
|
1512
1689
|
const entry = record.routeEntry;
|
|
@@ -1524,6 +1701,85 @@ export class SubcTransportPool {
|
|
|
1524
1701
|
// A dead socket is already released by the peer.
|
|
1525
1702
|
}
|
|
1526
1703
|
}
|
|
1704
|
+
/** Record that the current client delivered a reply, which proves its socket is alive. */
|
|
1705
|
+
noteClientAlive(client) {
|
|
1706
|
+
if (this.client === client)
|
|
1707
|
+
this.clientLivenessMark += 1;
|
|
1708
|
+
}
|
|
1709
|
+
/**
|
|
1710
|
+
* Count a call on `client` that failed without proving the socket dead. After
|
|
1711
|
+
* MAX_CONSECUTIVE_TRANSPORT_FAILURES in a row the connection is checked with a
|
|
1712
|
+
* liveness probe; the count alone never closes it.
|
|
1713
|
+
*/
|
|
1714
|
+
noteUnansweredCall(client, cause) {
|
|
1715
|
+
if (this.client !== client)
|
|
1716
|
+
return;
|
|
1717
|
+
if (++this.transportFailures < MAX_CONSECUTIVE_TRANSPORT_FAILURES)
|
|
1718
|
+
return;
|
|
1719
|
+
this.transportFailures = 0;
|
|
1720
|
+
this.probeClientLiveness(client, cause);
|
|
1721
|
+
}
|
|
1722
|
+
/**
|
|
1723
|
+
* Decide whether a connection that stopped answering calls is dead. A channel-0
|
|
1724
|
+
* catalog.list round trip is sent on the same connection; the client is dropped
|
|
1725
|
+
* only when that probe fails with socket death, or goes unanswered for the
|
|
1726
|
+
* probe window while no other reply arrived on the connection either. Any
|
|
1727
|
+
* reply, including a refusal, keeps the client.
|
|
1728
|
+
*/
|
|
1729
|
+
probeClientLiveness(client, suspicion) {
|
|
1730
|
+
if (this.livenessProbe || this.client !== client)
|
|
1731
|
+
return;
|
|
1732
|
+
const suspicionText = `after ${MAX_CONSECUTIVE_TRANSPORT_FAILURES} consecutive unanswered calls; last ${suspicion.trigger} failed with ${describeFailure(suspicion.error)}`;
|
|
1733
|
+
if (typeof client.catalogList !== "function") {
|
|
1734
|
+
log(`subc pool: keeping shared client ${suspicionText}; the client offers no liveness probe`);
|
|
1735
|
+
return;
|
|
1736
|
+
}
|
|
1737
|
+
const mark = this.clientLivenessMark;
|
|
1738
|
+
const windowMs = this.livenessProbeTimeoutMs;
|
|
1739
|
+
let timer;
|
|
1740
|
+
const timedOut = new Promise((resolve) => {
|
|
1741
|
+
timer = setTimeout(() => resolve("timeout"), windowMs);
|
|
1742
|
+
timer.unref?.();
|
|
1743
|
+
});
|
|
1744
|
+
const probe = (async () => {
|
|
1745
|
+
let deathEvidence = null;
|
|
1746
|
+
try {
|
|
1747
|
+
const reply = Promise.resolve().then(() => client.catalogList?.(AFT_MODULE_ID));
|
|
1748
|
+
const outcome = await Promise.race([reply.then(() => "answered"), timedOut]);
|
|
1749
|
+
if (outcome === "timeout")
|
|
1750
|
+
deathEvidence = new SubcLivenessProbeTimeoutError(windowMs);
|
|
1751
|
+
}
|
|
1752
|
+
catch (error) {
|
|
1753
|
+
// Only socket death or the probe's own deadline convicts. Any other
|
|
1754
|
+
// rejection came back over the connection, which is proof it works.
|
|
1755
|
+
if (isSocketDeathError(error) ||
|
|
1756
|
+
(error instanceof SubcError && error.code === REQUEST_DEADLINE_CODE)) {
|
|
1757
|
+
deathEvidence = error;
|
|
1758
|
+
}
|
|
1759
|
+
}
|
|
1760
|
+
finally {
|
|
1761
|
+
clearTimeout(timer);
|
|
1762
|
+
}
|
|
1763
|
+
if (this.client !== client)
|
|
1764
|
+
return;
|
|
1765
|
+
if (deathEvidence !== null && this.clientLivenessMark === mark) {
|
|
1766
|
+
this.dropClient(client, {
|
|
1767
|
+
error: deathEvidence,
|
|
1768
|
+
trigger: "liveness probe",
|
|
1769
|
+
session: suspicion.session,
|
|
1770
|
+
route: suspicion.route,
|
|
1771
|
+
detail: suspicionText,
|
|
1772
|
+
});
|
|
1773
|
+
return;
|
|
1774
|
+
}
|
|
1775
|
+
this.transportFailures = 0;
|
|
1776
|
+
log(`subc pool: keeping shared client ${suspicionText}; the connection answered`);
|
|
1777
|
+
})().finally(() => {
|
|
1778
|
+
if (this.livenessProbe === probe)
|
|
1779
|
+
this.livenessProbe = null;
|
|
1780
|
+
});
|
|
1781
|
+
this.livenessProbe = probe;
|
|
1782
|
+
}
|
|
1527
1783
|
/** Synchronous concrete-facade eviction used by the registry coordinator. */
|
|
1528
1784
|
evictConcreteFacade(root, generation) {
|
|
1529
1785
|
const facade = this.transports.get(root);
|