@opengeni/runtime 0.13.0 → 0.13.4

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.
@@ -0,0 +1,175 @@
1
+ /**
2
+ * Provider sandbox failure classification.
3
+ *
4
+ * This is deliberately structural and fail-closed. DNS failures often contain
5
+ * the word "not found" (`ENOTFOUND`) while gRPC status values are also used by
6
+ * unrelated SDKs and command routers. A bare numeric status (including gRPC's
7
+ * numeric 5) is therefore not enough to retire a provider instance. Only
8
+ * authoritative, non-transient evidence may retire a provider instance and
9
+ * license lease recovery.
10
+ */
11
+ export type ProviderSandboxFailureKind = "not_found" | "transient_transport" | "other";
12
+
13
+ export type ProviderSandboxFailure = {
14
+ kind: ProviderSandboxFailureKind;
15
+ /** Bounded, non-secret diagnostic suitable for logs and lifecycle events. */
16
+ diagnostic: string;
17
+ };
18
+
19
+ const GRPC_TRANSIENT = new Set([1, 2, 4, 13, 14]);
20
+ const TRANSIENT_CODES = new Set([
21
+ "CANCELLED",
22
+ "UNKNOWN",
23
+ "DEADLINE_EXCEEDED",
24
+ "INTERNAL",
25
+ "UNAVAILABLE",
26
+ "ECONNRESET",
27
+ "ECONNREFUSED",
28
+ "ETIMEDOUT",
29
+ "EAI_AGAIN",
30
+ "ENOTFOUND",
31
+ ]);
32
+ const NOT_FOUND_CODES = new Set(["404", "NOT_FOUND", "RESOURCE_NOT_FOUND", "SANDBOX_NOT_FOUND"]);
33
+ const SAFE_CODE = /^[A-Z0-9._:-]{1,64}$/;
34
+
35
+ type ErrorSignals = {
36
+ numbers: number[];
37
+ codes: string[];
38
+ messages: string[];
39
+ };
40
+
41
+ function safeRead(record: object, key: string): unknown {
42
+ try {
43
+ return Reflect.get(record, key);
44
+ } catch {
45
+ return undefined;
46
+ }
47
+ }
48
+
49
+ function collectSignals(value: unknown): ErrorSignals {
50
+ const out: ErrorSignals = { numbers: [], codes: [], messages: [] };
51
+ const seen = new Set<object>();
52
+ const visit = (current: unknown, depth: number): void => {
53
+ if (depth > 3 || current === null || current === undefined) return;
54
+ if (typeof current === "string") {
55
+ if (current.length <= 512) out.messages.push(current);
56
+ return;
57
+ }
58
+ if (typeof current !== "object" || seen.has(current)) return;
59
+ seen.add(current);
60
+ for (const key of [
61
+ "status",
62
+ "statusCode",
63
+ "httpStatus",
64
+ "httpStatusCode",
65
+ "responseStatus",
66
+ "errorCode",
67
+ "code",
68
+ ]) {
69
+ const signal = safeRead(current, key);
70
+ if (typeof signal === "number" && Number.isFinite(signal)) out.numbers.push(signal);
71
+ if (typeof signal === "string") {
72
+ const code = signal.trim().toUpperCase();
73
+ if (SAFE_CODE.test(code)) out.codes.push(code);
74
+ }
75
+ }
76
+ for (const key of ["name", "message", "details", "cause"]) {
77
+ const signal = safeRead(current, key);
78
+ if (typeof signal === "string" && signal.length <= 512) out.messages.push(signal);
79
+ else visit(signal, depth + 1);
80
+ }
81
+ visit(safeRead(current, "response"), depth + 1);
82
+ visit(safeRead(current, "error"), depth + 1);
83
+ };
84
+ visit(value, 0);
85
+ return out;
86
+ }
87
+
88
+ function safeDiagnostic(signals: ErrorSignals): string {
89
+ const code = signals.codes.find(Boolean);
90
+ const numeric = signals.numbers.find((value) => Number.isFinite(value));
91
+ return [code ? `code=${code}` : "", numeric !== undefined ? `status=${numeric}` : ""]
92
+ .filter(Boolean)
93
+ .join(" ");
94
+ }
95
+
96
+ function hasTransientSignal(signals: ErrorSignals): boolean {
97
+ return (
98
+ signals.codes.some((code) => TRANSIENT_CODES.has(code)) ||
99
+ signals.numbers.some((status) => GRPC_TRANSIENT.has(status))
100
+ );
101
+ }
102
+
103
+ function hasModalTerminalSandboxMessage(signals: ErrorSignals): boolean {
104
+ return signals.messages.some(
105
+ (message) =>
106
+ /^Modal sandbox [A-Za-z0-9_-]+ is no longer running\.$/.test(message) ||
107
+ /^Modal sandbox [A-Za-z0-9_-]+ not found \(has been terminated\)$/.test(message),
108
+ );
109
+ }
110
+
111
+ /** Typed transport evidence always dominates nested/string NotFound text. */
112
+ export function classifyProviderSandboxFailure(
113
+ backendId: string,
114
+ error: unknown,
115
+ ): ProviderSandboxFailure {
116
+ if (backendId === "selfhosted" || !error) {
117
+ return { kind: "other", diagnostic: "selfhosted-or-empty" };
118
+ }
119
+ const signals = collectSignals(error);
120
+ const diagnostic = safeDiagnostic(signals) || "unclassified_provider_failure";
121
+
122
+ if (hasTransientSignal(signals)) {
123
+ return { kind: "transient_transport", diagnostic };
124
+ }
125
+ if (
126
+ signals.numbers.some((status) => status === 404) ||
127
+ signals.codes.some((code) => NOT_FOUND_CODES.has(code))
128
+ ) {
129
+ return { kind: "not_found", diagnostic };
130
+ }
131
+
132
+ // Modal's SDK emits these exact terminal grammars after resolving one exact
133
+ // sandbox id. Generic "not found" prose is intentionally not authoritative.
134
+ if (backendId === "modal" && hasModalTerminalSandboxMessage(signals)) {
135
+ return {
136
+ kind: "not_found",
137
+ diagnostic:
138
+ diagnostic === "unclassified_provider_failure"
139
+ ? "modal_terminal_message"
140
+ : `${diagnostic} modal_terminal_message`,
141
+ };
142
+ }
143
+
144
+ return { kind: "other", diagnostic };
145
+ }
146
+
147
+ export function isProviderSandboxNotFoundError(backendId: string, error: unknown): boolean {
148
+ return classifyProviderSandboxFailure(backendId, error).kind === "not_found";
149
+ }
150
+
151
+ /**
152
+ * Whether a failure from an operation routed through an already-established
153
+ * sandbox proves that exact provider sandbox disappeared.
154
+ *
155
+ * Routed operations can legitimately return HTTP 404 / `NOT_FOUND` for paths,
156
+ * files, ports, processes, and other subresources. Those generic signals are
157
+ * sufficient for provider create/resume APIs, but they must never retire the
158
+ * live sandbox identity after an arbitrary operation. Only an explicit
159
+ * sandbox-scoped provider code or Modal's exact terminal sandbox grammar is
160
+ * authoritative on this path. Transient transport evidence still dominates.
161
+ */
162
+ export function isProviderSandboxGoneDuringRoutedOperation(
163
+ backendId: string,
164
+ error: unknown,
165
+ ): boolean {
166
+ if (backendId === "selfhosted" || !error) return false;
167
+ const signals = collectSignals(error);
168
+ if (hasTransientSignal(signals)) return false;
169
+ if (signals.codes.includes("SANDBOX_NOT_FOUND")) return true;
170
+ return backendId === "modal" && hasModalTerminalSandboxMessage(signals);
171
+ }
172
+
173
+ export function isProviderSandboxTransientError(backendId: string, error: unknown): boolean {
174
+ return classifyProviderSandboxFailure(backendId, error).kind === "transient_transport";
175
+ }
@@ -47,6 +47,18 @@ export interface ActiveBackendResolverDeps {
47
47
  defaultBackend: RoutableBackendSession;
48
48
  /** A label for the default backend (its backend id: "modal"/"selfhosted"/…). */
49
49
  defaultKind: string;
50
+ /**
51
+ * Re-resolve the session's HOME backend after the route cache is fenced.
52
+ *
53
+ * The normal path returns `defaultBackend` (the turn's already-established
54
+ * handle), but an API-side same-target repair can rematerialize that lease
55
+ * while a worker turn is still alive. Such a repair advances the route epoch
56
+ * without changing the null pointer; callers that can resume the current
57
+ * lease-owned identity may provide this callback so the next epoch does not
58
+ * keep dispatching to the dead pre-repair object. This callback must never
59
+ * create a provider or replay an operation.
60
+ */
61
+ resolveDefaultBackend?: (pointer: ActivePointer) => Promise<ResolvedActiveBackend>;
50
62
  /** Look up a first-class sandbox by id (the swap target). Returns null when the
51
63
  * id is unknown or not in this workspace (the caller 409s the swap). */
52
64
  getSandbox(sandboxId: string): Promise<RoutableSandbox | null>;
@@ -236,6 +248,9 @@ export function makeActiveBackendResolver(
236
248
  "the active sandbox was detached to the session default (home), but this turn started pinned to a machine and established no home box; the detach is accepted and takes effect on the next turn — this turn has no active home box",
237
249
  );
238
250
  }
251
+ if (deps.resolveDefaultBackend) {
252
+ return await deps.resolveDefaultBackend(pointer);
253
+ }
239
254
  return { session: deps.defaultBackend, sandboxId: null, kind: deps.defaultKind };
240
255
  }
241
256