@superblocksteam/sdk 2.0.132 → 2.0.133
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/.turbo/turbo-build.log +1 -1
- package/dist/dev-utils/dev-server-metrics.d.mts +25 -0
- package/dist/dev-utils/dev-server-metrics.d.mts.map +1 -1
- package/dist/dev-utils/dev-server-metrics.mjs +50 -0
- package/dist/dev-utils/dev-server-metrics.mjs.map +1 -1
- package/dist/dev-utils/dev-server-metrics.test.mjs +21 -1
- package/dist/dev-utils/dev-server-metrics.test.mjs.map +1 -1
- package/dist/dev-utils/dev-server.d.mts.map +1 -1
- package/dist/dev-utils/dev-server.mjs +90 -27
- package/dist/dev-utils/dev-server.mjs.map +1 -1
- package/dist/dev-utils/fatal-exit.d.mts +1 -0
- package/dist/dev-utils/fatal-exit.d.mts.map +1 -1
- package/dist/dev-utils/fatal-exit.mjs +49 -0
- package/dist/dev-utils/fatal-exit.mjs.map +1 -1
- package/dist/dev-utils/fatal-exit.test.mjs +38 -1
- package/dist/dev-utils/fatal-exit.test.mjs.map +1 -1
- package/package.json +6 -6
- package/src/dev-utils/dev-server-metrics.mts +67 -0
- package/src/dev-utils/dev-server-metrics.test.mts +26 -0
- package/src/dev-utils/dev-server.mts +109 -28
- package/src/dev-utils/fatal-exit.mts +51 -0
- package/src/dev-utils/fatal-exit.test.mts +48 -0
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, expect, it } from "vitest";
|
|
2
|
-
import { buildFatalExitLog, FATAL_EXIT_EVENT, parseIsWarm, } from "./fatal-exit.mjs";
|
|
2
|
+
import { buildFatalExitLog, FATAL_EXIT_EVENT, isRecoverableShutdownError, parseIsWarm, } from "./fatal-exit.mjs";
|
|
3
3
|
describe("parseIsWarm", () => {
|
|
4
4
|
it("treats the same truthy spellings as the entrypoint shell", () => {
|
|
5
5
|
for (const value of ["true", "1", "yes", "on", "TRUE", "Yes", " on "]) {
|
|
@@ -65,4 +65,41 @@ describe("buildFatalExitLog", () => {
|
|
|
65
65
|
expect(line).toBe("Dev server fatal exit event=dev_server_fatal_exit handler=uncaughtException exit_code=1 signal=none is_warm=false");
|
|
66
66
|
});
|
|
67
67
|
});
|
|
68
|
+
describe("isRecoverableShutdownError", () => {
|
|
69
|
+
it("matches the connection-closed SocketError codes (8 and 10)", () => {
|
|
70
|
+
// The exact object socket.ts rejects pending requests with on an abnormal
|
|
71
|
+
// close, plus the SocketErrorException shape for a request-after-close.
|
|
72
|
+
expect(isRecoverableShutdownError({
|
|
73
|
+
code: 8,
|
|
74
|
+
message: "[internal] Connection closed",
|
|
75
|
+
})).toBe(true);
|
|
76
|
+
expect(isRecoverableShutdownError({
|
|
77
|
+
code: 10,
|
|
78
|
+
message: "[internal] Connection closed",
|
|
79
|
+
})).toBe(true);
|
|
80
|
+
});
|
|
81
|
+
it("matches Vite's ERR_CLOSED_SERVER (a string code, raced against close)", () => {
|
|
82
|
+
expect(isRecoverableShutdownError({
|
|
83
|
+
code: "ERR_CLOSED_SERVER",
|
|
84
|
+
message: "The server is being restarted or closed.",
|
|
85
|
+
})).toBe(true);
|
|
86
|
+
});
|
|
87
|
+
it("does not match other socket error codes (those are genuine faults)", () => {
|
|
88
|
+
// 2 = unknown method, 3 = handler exception, 7 = no-response timeout.
|
|
89
|
+
for (const code of [2, 3, 7]) {
|
|
90
|
+
expect(isRecoverableShutdownError({ code, message: "boom" })).toBe(false);
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
it("does not match plain errors or non-error shapes", () => {
|
|
94
|
+
expect(isRecoverableShutdownError(new Error("boom"))).toBe(false);
|
|
95
|
+
expect(isRecoverableShutdownError(undefined)).toBe(false);
|
|
96
|
+
expect(isRecoverableShutdownError(null)).toBe(false);
|
|
97
|
+
expect(isRecoverableShutdownError("Connection closed")).toBe(false);
|
|
98
|
+
expect(isRecoverableShutdownError({ message: "no code" })).toBe(false);
|
|
99
|
+
// A numeric code as a string ("8") is not our socket code, and an unrelated
|
|
100
|
+
// string code is not Vite's — neither is recoverable.
|
|
101
|
+
expect(isRecoverableShutdownError({ code: "8" })).toBe(false);
|
|
102
|
+
expect(isRecoverableShutdownError({ code: "ERR_OTHER" })).toBe(false);
|
|
103
|
+
});
|
|
104
|
+
});
|
|
68
105
|
//# sourceMappingURL=fatal-exit.test.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fatal-exit.test.mjs","sourceRoot":"","sources":["../../src/dev-utils/fatal-exit.test.mts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"fatal-exit.test.mjs","sourceRoot":"","sources":["../../src/dev-utils/fatal-exit.test.mts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@superblocksteam/sdk",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.133",
|
|
4
4
|
"description": "Superblocks JS SDK",
|
|
5
5
|
"homepage": "https://www.superblocks.com",
|
|
6
6
|
"license": "Superblocks Community Software License",
|
|
@@ -48,11 +48,11 @@
|
|
|
48
48
|
"vite-tsconfig-paths": "^6.0.4",
|
|
49
49
|
"winston": "^3.17.0",
|
|
50
50
|
"yaml": "^2.7.1",
|
|
51
|
-
"@superblocksteam/library-shared": "2.0.
|
|
52
|
-
"@superblocksteam/shared": "0.
|
|
53
|
-
"@superblocksteam/telemetry": "2.0.
|
|
54
|
-
"@superblocksteam/vite-plugin-file-sync": "2.0.
|
|
55
|
-
"@superblocksteam/util": "2.0.
|
|
51
|
+
"@superblocksteam/library-shared": "2.0.133",
|
|
52
|
+
"@superblocksteam/shared": "0.9594.0",
|
|
53
|
+
"@superblocksteam/telemetry": "2.0.133",
|
|
54
|
+
"@superblocksteam/vite-plugin-file-sync": "2.0.133",
|
|
55
|
+
"@superblocksteam/util": "2.0.133"
|
|
56
56
|
},
|
|
57
57
|
"devDependencies": {
|
|
58
58
|
"@eslint/js": "^9.39.2",
|
|
@@ -96,6 +96,40 @@ export function normalizeBootKindTag(
|
|
|
96
96
|
: "other";
|
|
97
97
|
}
|
|
98
98
|
|
|
99
|
+
/**
|
|
100
|
+
* Where a recoverable shutdown error was caught and suppressed instead of
|
|
101
|
+
* exiting the process. A closed enum so the `handler` label on
|
|
102
|
+
* `dev_server_recoverable_shutdown_error_total` stays low-cardinality.
|
|
103
|
+
*/
|
|
104
|
+
export type RecoverableShutdownHandler =
|
|
105
|
+
| "uncaughtException"
|
|
106
|
+
| "unhandledRejection"
|
|
107
|
+
| "vite_teardown";
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Allowlist of the close codes `isRecoverableShutdownError` matches: socket
|
|
111
|
+
* codes 8/10 (`packages/shared/src/socket/socket.ts`) and Vite's
|
|
112
|
+
* `ERR_CLOSED_SERVER`. Bounds the `code` metric label so the series count stays
|
|
113
|
+
* fixed even if the predicate ever lets an unexpected code through.
|
|
114
|
+
*/
|
|
115
|
+
const RECOVERABLE_SHUTDOWN_CODE_TAGS = new Set<string>([
|
|
116
|
+
"8",
|
|
117
|
+
"10",
|
|
118
|
+
"ERR_CLOSED_SERVER",
|
|
119
|
+
]);
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Normalize a recoverable-shutdown error's `code` into a bounded metric-label
|
|
123
|
+
* value: known close codes pass through (keeping the diagnostic facet),
|
|
124
|
+
* anything else collapses to `"other"`, and a missing/non-primitive code maps
|
|
125
|
+
* to `"none"`.
|
|
126
|
+
*/
|
|
127
|
+
export function normalizeRecoverableShutdownCodeTag(code: unknown): string {
|
|
128
|
+
if (typeof code !== "number" && typeof code !== "string") return "none";
|
|
129
|
+
const tag = String(code);
|
|
130
|
+
return RECOVERABLE_SHUTDOWN_CODE_TAGS.has(tag) ? tag : "other";
|
|
131
|
+
}
|
|
132
|
+
|
|
99
133
|
/** Dev-server endpoints that produce per-request metrics. */
|
|
100
134
|
export type DevServerEndpoint =
|
|
101
135
|
| "_sb_health"
|
|
@@ -408,6 +442,39 @@ class DevServerMetrics {
|
|
|
408
442
|
});
|
|
409
443
|
}
|
|
410
444
|
|
|
445
|
+
/**
|
|
446
|
+
* Records a recoverable shutdown error that was suppressed (logged at warn,
|
|
447
|
+
* process left alive) instead of triggering a fatal exit. This is the
|
|
448
|
+
* alertable replacement for the `dev_server_fatal_exit` log token the old
|
|
449
|
+
* fatal path emitted: without it, the only way to see these errors (or notice
|
|
450
|
+
* their rate regressing) is a log search.
|
|
451
|
+
*
|
|
452
|
+
* Both labels are low-cardinality: `handler` is a closed enum, and the error's
|
|
453
|
+
* `code` is normalized to the known socket / Vite close codes (or
|
|
454
|
+
* `other`/`none`).
|
|
455
|
+
*/
|
|
456
|
+
recordRecoverableShutdownError(
|
|
457
|
+
handler: RecoverableShutdownHandler,
|
|
458
|
+
reason: unknown,
|
|
459
|
+
): void {
|
|
460
|
+
const code =
|
|
461
|
+
typeof reason === "object" && reason !== null && "code" in reason
|
|
462
|
+
? (reason as { code: unknown }).code
|
|
463
|
+
: undefined;
|
|
464
|
+
const labels = {
|
|
465
|
+
handler,
|
|
466
|
+
code: normalizeRecoverableShutdownCodeTag(code),
|
|
467
|
+
};
|
|
468
|
+
this.record(() => {
|
|
469
|
+
getMeter()
|
|
470
|
+
.createCounter("dev_server_recoverable_shutdown_error_total", {
|
|
471
|
+
description:
|
|
472
|
+
"Count of recoverable shutdown errors suppressed instead of exiting, by handler and close code.",
|
|
473
|
+
})
|
|
474
|
+
.add(1, labels);
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
|
|
411
478
|
/**
|
|
412
479
|
* Records an ENOSPC disk-recovery attempt and its outcome. Lets dashboards
|
|
413
480
|
* and alerts answer "how often is the live-edit PVC filling up, and is
|
|
@@ -3,6 +3,7 @@ import { describe, it, expect } from "vitest";
|
|
|
3
3
|
import {
|
|
4
4
|
normalizeBootKindTag,
|
|
5
5
|
normalizeNpmErrorCodeTag,
|
|
6
|
+
normalizeRecoverableShutdownCodeTag,
|
|
6
7
|
} from "./dev-server-metrics.mjs";
|
|
7
8
|
|
|
8
9
|
describe("normalizeBootKindTag (metric cardinality bound)", () => {
|
|
@@ -59,3 +60,28 @@ describe("normalizeNpmErrorCodeTag (metric cardinality bound)", () => {
|
|
|
59
60
|
expect(normalizeNpmErrorCodeTag("")).toBe("none");
|
|
60
61
|
});
|
|
61
62
|
});
|
|
63
|
+
|
|
64
|
+
describe("normalizeRecoverableShutdownCodeTag (metric cardinality bound)", () => {
|
|
65
|
+
it("passes through the known socket / Vite close codes", () => {
|
|
66
|
+
// Numeric socket codes and Vite's string code are stringified for the label.
|
|
67
|
+
expect(normalizeRecoverableShutdownCodeTag(8)).toBe("8");
|
|
68
|
+
expect(normalizeRecoverableShutdownCodeTag(10)).toBe("10");
|
|
69
|
+
expect(normalizeRecoverableShutdownCodeTag("ERR_CLOSED_SERVER")).toBe(
|
|
70
|
+
"ERR_CLOSED_SERVER",
|
|
71
|
+
);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("maps any other code to 'other' (cardinality guard)", () => {
|
|
75
|
+
// Should the predicate ever let an unexpected code through, the label must
|
|
76
|
+
// not mint a new time series per distinct value.
|
|
77
|
+
expect(normalizeRecoverableShutdownCodeTag(7)).toBe("other");
|
|
78
|
+
expect(normalizeRecoverableShutdownCodeTag("ECONNRESET")).toBe("other");
|
|
79
|
+
expect(normalizeRecoverableShutdownCodeTag("`;rm -rf /#")).toBe("other");
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("maps a missing or non-primitive code to 'none'", () => {
|
|
83
|
+
expect(normalizeRecoverableShutdownCodeTag(undefined)).toBe("none");
|
|
84
|
+
expect(normalizeRecoverableShutdownCodeTag(null)).toBe("none");
|
|
85
|
+
expect(normalizeRecoverableShutdownCodeTag({})).toBe("none");
|
|
86
|
+
});
|
|
87
|
+
});
|
|
@@ -65,7 +65,11 @@ import {
|
|
|
65
65
|
type DevServerFailureType,
|
|
66
66
|
devServerMetrics,
|
|
67
67
|
} from "./dev-server-metrics.mjs";
|
|
68
|
-
import {
|
|
68
|
+
import {
|
|
69
|
+
buildFatalExitLog,
|
|
70
|
+
isRecoverableShutdownError,
|
|
71
|
+
parseIsWarm,
|
|
72
|
+
} from "./fatal-exit.mjs";
|
|
69
73
|
import {
|
|
70
74
|
formatViteDevServerStartedLog,
|
|
71
75
|
logViteBuildError,
|
|
@@ -626,7 +630,7 @@ export async function createDevServer({
|
|
|
626
630
|
let isViteServerInitialized = false;
|
|
627
631
|
let viteResolve: (value: void | PromiseLike<void>) => void;
|
|
628
632
|
let viteReject: (reason?: any) => void;
|
|
629
|
-
|
|
633
|
+
const vitePromise = new Promise((resolve, reject) => {
|
|
630
634
|
viteResolve = resolve;
|
|
631
635
|
viteReject = reject;
|
|
632
636
|
});
|
|
@@ -661,6 +665,68 @@ export async function createDevServer({
|
|
|
661
665
|
},
|
|
662
666
|
});
|
|
663
667
|
|
|
668
|
+
// Single authoritative Vite teardown. Both the graceful-shutdown path and the
|
|
669
|
+
// /_sb_disconnect restart path go through here so the ordering that prevents
|
|
670
|
+
// ERR_CLOSED_SERVER / socket-close races lives in one place: stop new work
|
|
671
|
+
// FIRST (flip the initialized flag so the request gate rejects, drop the Vite
|
|
672
|
+
// middleware from the router, cancel pending sync), THEN close. Nothing calls
|
|
673
|
+
// into Vite after close() that way, so it cannot throw "request after close".
|
|
674
|
+
// A recoverable close error is logged, not rethrown — the global fatal handler
|
|
675
|
+
// treats the same shapes as non-fatal, but swallowing here keeps a benign
|
|
676
|
+
// teardown race off the error path entirely (APPS-4797).
|
|
677
|
+
//
|
|
678
|
+
// Single-flight guard: a SIGTERM graceful shutdown can land while a
|
|
679
|
+
// /_sb_disconnect restart is already awaiting close(), and both reach the
|
|
680
|
+
// worker with viteCreationResults still set (it is nulled only after close()
|
|
681
|
+
// resolves). Without the guard both would close() and dispose() the same Vite
|
|
682
|
+
// instance: the second close() throws ERR_CLOSED_SERVER and dispose() runs
|
|
683
|
+
// twice. Sharing one in-flight promise makes the second caller await the first
|
|
684
|
+
// teardown instead of starting its own.
|
|
685
|
+
let viteClosePromise: Promise<void> | null = null;
|
|
686
|
+
function closeViteServer(): Promise<void> {
|
|
687
|
+
if (!viteClosePromise) {
|
|
688
|
+
viteClosePromise = teardownViteServer().finally(() => {
|
|
689
|
+
viteClosePromise = null;
|
|
690
|
+
});
|
|
691
|
+
}
|
|
692
|
+
return viteClosePromise;
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
async function teardownViteServer(): Promise<void> {
|
|
696
|
+
if (!viteCreationResults) {
|
|
697
|
+
return;
|
|
698
|
+
}
|
|
699
|
+
const { viteServer, dispose, routeIndex } = viteCreationResults;
|
|
700
|
+
isViteServerInitialized = false;
|
|
701
|
+
if (routeIndex !== -1) {
|
|
702
|
+
// Remove the Vite middleware so express stops handing requests to a server
|
|
703
|
+
// we are about to close. `app._router` is untyped express internals; this
|
|
704
|
+
// cast matches the existing usage in this file.
|
|
705
|
+
(app._router as IRouter).stack.splice(routeIndex, 1);
|
|
706
|
+
}
|
|
707
|
+
// Cancel pending file sync as part of teardown. This only runs when Vite is
|
|
708
|
+
// up (the guard above early-returns otherwise). The restart path used to call
|
|
709
|
+
// this unconditionally, but it exits the process immediately afterward, so an
|
|
710
|
+
// uncancelled in-flight sync cannot persist; when viteCreationResults is
|
|
711
|
+
// already null the skipped call would have been a no-op anyway.
|
|
712
|
+
syncService?.cancelPendingSync();
|
|
713
|
+
try {
|
|
714
|
+
await viteServer.close();
|
|
715
|
+
} catch (e) {
|
|
716
|
+
if (!isRecoverableShutdownError(e)) {
|
|
717
|
+
throw e;
|
|
718
|
+
}
|
|
719
|
+
logger.warn(
|
|
720
|
+
"Ignoring recoverable error during Vite teardown",
|
|
721
|
+
getErrorMeta(e),
|
|
722
|
+
);
|
|
723
|
+
devServerMetrics.recordRecoverableShutdownError("vite_teardown", e);
|
|
724
|
+
} finally {
|
|
725
|
+
dispose();
|
|
726
|
+
viteCreationResults = null;
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
|
|
664
730
|
async function runGracefulShutdown({
|
|
665
731
|
logger,
|
|
666
732
|
serverInitiated,
|
|
@@ -688,13 +754,7 @@ export async function createDevServer({
|
|
|
688
754
|
});
|
|
689
755
|
// Cleanup AI service cache
|
|
690
756
|
await aiService?.removeIntegrationCache();
|
|
691
|
-
|
|
692
|
-
if (viteCreationResults) {
|
|
693
|
-
await viteCreationResults.viteServer?.close();
|
|
694
|
-
viteCreationResults.dispose();
|
|
695
|
-
viteCreationResults = null;
|
|
696
|
-
isViteServerInitialized = false;
|
|
697
|
-
}
|
|
757
|
+
await closeViteServer();
|
|
698
758
|
} catch (e) {
|
|
699
759
|
logger.error(`Error during graceful shutdown: ${e}`);
|
|
700
760
|
} finally {
|
|
@@ -960,28 +1020,13 @@ export async function createDevServer({
|
|
|
960
1020
|
if (isRestart) {
|
|
961
1021
|
logger.info("Pausing dev server while restarting");
|
|
962
1022
|
lockService?.setIsRestarting();
|
|
963
|
-
syncService?.cancelPendingSync();
|
|
964
1023
|
// Invalidate chat session cache to reload fresh chat messages after checkpoint restore
|
|
965
1024
|
aiService?.chatSessionStore.invalidateCache();
|
|
966
|
-
|
|
967
|
-
const index = viteCreationResults.routeIndex;
|
|
968
|
-
if (index !== -1) {
|
|
969
|
-
// Remove the vite server middleware from the express app - this is a workaround to avoid
|
|
970
|
-
// the vite server middleware being called after the dev server is closed
|
|
971
|
-
(app._router as IRouter).stack.splice(index, 1);
|
|
972
|
-
}
|
|
973
|
-
|
|
974
|
-
await viteCreationResults.viteServer?.close();
|
|
975
|
-
viteCreationResults.dispose();
|
|
976
|
-
viteCreationResults = null;
|
|
977
|
-
vitePromise = new Promise((resolve, reject) => {
|
|
978
|
-
viteResolve = resolve;
|
|
979
|
-
viteReject = reject;
|
|
980
|
-
});
|
|
981
|
-
vitePromise.catch(() => {});
|
|
982
|
-
isViteServerInitialized = false;
|
|
983
|
-
}
|
|
1025
|
+
await closeViteServer();
|
|
984
1026
|
res.send("ok");
|
|
1027
|
+
// RESTART_EXIT_CODE tells the supervisor to respawn, and this runs
|
|
1028
|
+
// synchronously below (no await), so the next /_sb_connect is served by
|
|
1029
|
+
// the fresh process. There is nothing to re-arm in this dying one.
|
|
985
1030
|
process.exit(RESTART_EXIT_CODE);
|
|
986
1031
|
} else {
|
|
987
1032
|
logger.info("Shutting down dev server");
|
|
@@ -1159,6 +1204,26 @@ export async function createDevServer({
|
|
|
1159
1204
|
if (fatalExitHandled) {
|
|
1160
1205
|
return;
|
|
1161
1206
|
}
|
|
1207
|
+
// A connection or server we depend on closing mid-flight (socket code 8/10,
|
|
1208
|
+
// or Vite's ERR_CLOSED_SERVER from a call racing an in-place restart) is
|
|
1209
|
+
// expected and recoverable, so it must not kill the pod. See
|
|
1210
|
+
// isRecoverableShutdownError for the shapes and why their owning paths
|
|
1211
|
+
// already treat them as non-fatal.
|
|
1212
|
+
if (isRecoverableShutdownError(error)) {
|
|
1213
|
+
logger.warn(
|
|
1214
|
+
"Ignoring recoverable shutdown error (uncaughtException)",
|
|
1215
|
+
getErrorMeta(error),
|
|
1216
|
+
);
|
|
1217
|
+
// Keep an alertable signal. Suppressing the fatal exit drops the
|
|
1218
|
+
// dev_server_fatal_exit log token, so this counter is the only
|
|
1219
|
+
// dashboard-queryable way to see these (and catch a regression in their
|
|
1220
|
+
// rate) without a log search.
|
|
1221
|
+
devServerMetrics.recordRecoverableShutdownError(
|
|
1222
|
+
"uncaughtException",
|
|
1223
|
+
error,
|
|
1224
|
+
);
|
|
1225
|
+
return;
|
|
1226
|
+
}
|
|
1162
1227
|
fatalExitHandled = true;
|
|
1163
1228
|
// Write the fatal-exit line first, and synchronously, so the reason reaches
|
|
1164
1229
|
// the logs before anything else runs. Shutdown below may finish first and
|
|
@@ -1193,6 +1258,22 @@ export async function createDevServer({
|
|
|
1193
1258
|
if (fatalExitHandled) {
|
|
1194
1259
|
return;
|
|
1195
1260
|
}
|
|
1261
|
+
// See the uncaughtException handler above: a connection/server close racing
|
|
1262
|
+
// an in-place restart (socket code 8/10 or Vite's ERR_CLOSED_SERVER) is
|
|
1263
|
+
// recoverable and must not wedge the pod (APPS-4797).
|
|
1264
|
+
if (isRecoverableShutdownError(reason)) {
|
|
1265
|
+
logger.warn(
|
|
1266
|
+
"Ignoring recoverable shutdown rejection (unhandledRejection)",
|
|
1267
|
+
getErrorMeta(reason),
|
|
1268
|
+
);
|
|
1269
|
+
// Alertable signal in place of the suppressed dev_server_fatal_exit log
|
|
1270
|
+
// token; see the uncaughtException handler above.
|
|
1271
|
+
devServerMetrics.recordRecoverableShutdownError(
|
|
1272
|
+
"unhandledRejection",
|
|
1273
|
+
reason,
|
|
1274
|
+
);
|
|
1275
|
+
return;
|
|
1276
|
+
}
|
|
1196
1277
|
fatalExitHandled = true;
|
|
1197
1278
|
logger.error(
|
|
1198
1279
|
buildFatalExitLog({ handler: "unhandledRejection", exitCode: 1, isWarm }),
|
|
@@ -111,3 +111,54 @@ export function buildFatalExitLog(fields: FatalExitFields): string {
|
|
|
111
111
|
`is_warm=${fields.isWarm}`,
|
|
112
112
|
].join(" ");
|
|
113
113
|
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Error `code`s that mean "a connection or server we depend on was torn down
|
|
117
|
+
* while a request was in flight", not "the process is broken". They are
|
|
118
|
+
* expected during an in-place dev-server restart / reconnect and are
|
|
119
|
+
* recoverable, so the process-level fatal handler must not kill the pod for
|
|
120
|
+
* them. Two layers throw two different shapes, both benign here:
|
|
121
|
+
*
|
|
122
|
+
* 8 `packages/shared/src/socket/socket.ts` rejects every
|
|
123
|
+
* pending request with `{ code: 8, message: '[internal]
|
|
124
|
+
* Connection closed' }` when the server WebSocket closes
|
|
125
|
+
* unexpectedly (server-side teardown, abnormal 1006).
|
|
126
|
+
* 10 a socket `request()` issued after the close handshake.
|
|
127
|
+
* "ERR_CLOSED_SERVER" Vite throws this from `transformRequest` / module-graph
|
|
128
|
+
* work that lands after `ViteDevServer.close()`. Our
|
|
129
|
+
* in-place restart closes Vite, so an in-flight
|
|
130
|
+
* file-sync/HMR call can race the close. Note the string
|
|
131
|
+
* code: it comes from Vite, not from our socket layer.
|
|
132
|
+
*
|
|
133
|
+
* The editor/library socket path and Vite's own request path already treat
|
|
134
|
+
* these as expected; the fire-and-forget callers that reach the process-level
|
|
135
|
+
* handler do not, which is why this predicate exists (APPS-4797 / APPS-4667).
|
|
136
|
+
*
|
|
137
|
+
* Load-bearing assumption: this is a purely structural `code` match, so it
|
|
138
|
+
* trusts that the numeric codes 8 and 10 are produced ONLY by our socket layer
|
|
139
|
+
* (`packages/shared/src/socket/socket.ts`) and are not reused by another source
|
|
140
|
+
* for an unrelated, genuinely-fatal error. libuv/Node system error codes are
|
|
141
|
+
* negative or string (`EPIPE`, `-32`), so they never collide. gRPC status codes
|
|
142
|
+
* DO collide numerically (8 = RESOURCE_EXHAUSTED, 10 = ABORTED), but this
|
|
143
|
+
* process runs only the socket client and Vite, with no gRPC rejection path
|
|
144
|
+
* reaching the process-level handler, so the match is safe today. If a gRPC
|
|
145
|
+
* client is ever added here, gate the numeric codes on a discriminating field
|
|
146
|
+
* (e.g. the `'[internal]'` message prefix) instead of widening the set. The
|
|
147
|
+
* string `ERR_CLOSED_SERVER` is already Vite-specific.
|
|
148
|
+
*/
|
|
149
|
+
const RECOVERABLE_SHUTDOWN_CODES = new Set<number | string>([
|
|
150
|
+
8,
|
|
151
|
+
10,
|
|
152
|
+
"ERR_CLOSED_SERVER",
|
|
153
|
+
]);
|
|
154
|
+
|
|
155
|
+
export function isRecoverableShutdownError(reason: unknown): boolean {
|
|
156
|
+
if (typeof reason !== "object" || reason === null || !("code" in reason)) {
|
|
157
|
+
return false;
|
|
158
|
+
}
|
|
159
|
+
const { code } = reason as { code: unknown };
|
|
160
|
+
if (typeof code !== "number" && typeof code !== "string") {
|
|
161
|
+
return false;
|
|
162
|
+
}
|
|
163
|
+
return RECOVERABLE_SHUTDOWN_CODES.has(code);
|
|
164
|
+
}
|
|
@@ -3,6 +3,7 @@ import { describe, expect, it } from "vitest";
|
|
|
3
3
|
import {
|
|
4
4
|
buildFatalExitLog,
|
|
5
5
|
FATAL_EXIT_EVENT,
|
|
6
|
+
isRecoverableShutdownError,
|
|
6
7
|
parseIsWarm,
|
|
7
8
|
} from "./fatal-exit.mjs";
|
|
8
9
|
|
|
@@ -79,3 +80,50 @@ describe("buildFatalExitLog", () => {
|
|
|
79
80
|
);
|
|
80
81
|
});
|
|
81
82
|
});
|
|
83
|
+
|
|
84
|
+
describe("isRecoverableShutdownError", () => {
|
|
85
|
+
it("matches the connection-closed SocketError codes (8 and 10)", () => {
|
|
86
|
+
// The exact object socket.ts rejects pending requests with on an abnormal
|
|
87
|
+
// close, plus the SocketErrorException shape for a request-after-close.
|
|
88
|
+
expect(
|
|
89
|
+
isRecoverableShutdownError({
|
|
90
|
+
code: 8,
|
|
91
|
+
message: "[internal] Connection closed",
|
|
92
|
+
}),
|
|
93
|
+
).toBe(true);
|
|
94
|
+
expect(
|
|
95
|
+
isRecoverableShutdownError({
|
|
96
|
+
code: 10,
|
|
97
|
+
message: "[internal] Connection closed",
|
|
98
|
+
}),
|
|
99
|
+
).toBe(true);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("matches Vite's ERR_CLOSED_SERVER (a string code, raced against close)", () => {
|
|
103
|
+
expect(
|
|
104
|
+
isRecoverableShutdownError({
|
|
105
|
+
code: "ERR_CLOSED_SERVER",
|
|
106
|
+
message: "The server is being restarted or closed.",
|
|
107
|
+
}),
|
|
108
|
+
).toBe(true);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("does not match other socket error codes (those are genuine faults)", () => {
|
|
112
|
+
// 2 = unknown method, 3 = handler exception, 7 = no-response timeout.
|
|
113
|
+
for (const code of [2, 3, 7]) {
|
|
114
|
+
expect(isRecoverableShutdownError({ code, message: "boom" })).toBe(false);
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it("does not match plain errors or non-error shapes", () => {
|
|
119
|
+
expect(isRecoverableShutdownError(new Error("boom"))).toBe(false);
|
|
120
|
+
expect(isRecoverableShutdownError(undefined)).toBe(false);
|
|
121
|
+
expect(isRecoverableShutdownError(null)).toBe(false);
|
|
122
|
+
expect(isRecoverableShutdownError("Connection closed")).toBe(false);
|
|
123
|
+
expect(isRecoverableShutdownError({ message: "no code" })).toBe(false);
|
|
124
|
+
// A numeric code as a string ("8") is not our socket code, and an unrelated
|
|
125
|
+
// string code is not Vite's — neither is recoverable.
|
|
126
|
+
expect(isRecoverableShutdownError({ code: "8" })).toBe(false);
|
|
127
|
+
expect(isRecoverableShutdownError({ code: "ERR_OTHER" })).toBe(false);
|
|
128
|
+
});
|
|
129
|
+
});
|