@superblocksteam/sdk 2.0.157-next.5 → 2.0.157
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/cli-replacement/automatic-upgrades.d.ts.map +1 -1
- package/dist/cli-replacement/automatic-upgrades.js +8 -4
- package/dist/cli-replacement/automatic-upgrades.js.map +1 -1
- package/dist/cli-replacement/automatic-upgrades.test.js +1 -2
- package/dist/cli-replacement/automatic-upgrades.test.js.map +1 -1
- package/dist/cli-replacement/dev-s3-restore.test.mjs +2 -6
- package/dist/cli-replacement/dev-s3-restore.test.mjs.map +1 -1
- package/dist/cli-replacement/dev-startup-git-before-dbfs-order.test.mjs +2 -6
- package/dist/cli-replacement/dev-startup-git-before-dbfs-order.test.mjs.map +1 -1
- package/dist/cli-replacement/dev.d.mts +6 -6
- package/dist/cli-replacement/dev.d.mts.map +1 -1
- package/dist/cli-replacement/dev.interception.test.mjs +12 -12
- package/dist/cli-replacement/dev.interception.test.mjs.map +1 -1
- package/dist/cli-replacement/dev.mjs +42 -100
- package/dist/cli-replacement/dev.mjs.map +1 -1
- package/dist/dev-utils/dev-server.d.mts +1 -36
- package/dist/dev-utils/dev-server.d.mts.map +1 -1
- package/dist/dev-utils/dev-server.mjs +73 -80
- package/dist/dev-utils/dev-server.mjs.map +1 -1
- package/dist/telemetry/logging.js +1 -1
- package/dist/telemetry/logging.js.map +1 -1
- package/dist/telemetry/safe-stringify.d.ts.map +1 -1
- package/dist/telemetry/safe-stringify.js +9 -25
- package/dist/telemetry/safe-stringify.js.map +1 -1
- package/package.json +6 -10
- package/src/cli-replacement/automatic-upgrades.test.ts +1 -2
- package/src/cli-replacement/automatic-upgrades.ts +11 -4
- package/src/cli-replacement/dev-s3-restore.test.mts +2 -6
- package/src/cli-replacement/dev-startup-git-before-dbfs-order.test.mts +2 -6
- package/src/cli-replacement/dev.interception.test.mts +12 -12
- package/src/cli-replacement/dev.mts +56 -112
- package/src/dev-utils/dev-server.mts +93 -110
- package/src/telemetry/logging.ts +1 -1
- package/src/telemetry/safe-stringify.ts +10 -25
- package/test/safe-stringify.test.mts +0 -25
- package/tsconfig.tsbuildinfo +1 -1
- package/dist/cli-replacement/dev-startup-lock-acquisition.test.d.mts +0 -2
- package/dist/cli-replacement/dev-startup-lock-acquisition.test.d.mts.map +0 -1
- package/dist/cli-replacement/dev-startup-lock-acquisition.test.mjs +0 -487
- package/dist/cli-replacement/dev-startup-lock-acquisition.test.mjs.map +0 -1
- package/dist/dev-utils/dev-server-json-parser.test.d.mts +0 -2
- package/dist/dev-utils/dev-server-json-parser.test.d.mts.map +0 -1
- package/dist/dev-utils/dev-server-json-parser.test.mjs +0 -60
- package/dist/dev-utils/dev-server-json-parser.test.mjs.map +0 -1
- package/dist/dev-utils/dev-server.shutdown-lock.test.d.mts +0 -2
- package/dist/dev-utils/dev-server.shutdown-lock.test.d.mts.map +0 -1
- package/dist/dev-utils/dev-server.shutdown-lock.test.mjs +0 -48
- package/dist/dev-utils/dev-server.shutdown-lock.test.mjs.map +0 -1
- package/dist/dev-utils/fatal-process-barrier.d.mts +0 -71
- package/dist/dev-utils/fatal-process-barrier.d.mts.map +0 -1
- package/dist/dev-utils/fatal-process-barrier.mjs +0 -161
- package/dist/dev-utils/fatal-process-barrier.mjs.map +0 -1
- package/dist/dev-utils/fatal-process-barrier.spawn-fixture.d.mts +0 -2
- package/dist/dev-utils/fatal-process-barrier.spawn-fixture.d.mts.map +0 -1
- package/dist/dev-utils/fatal-process-barrier.spawn-fixture.mjs +0 -9
- package/dist/dev-utils/fatal-process-barrier.spawn-fixture.mjs.map +0 -1
- package/dist/dev-utils/fatal-process-barrier.test.d.mts +0 -2
- package/dist/dev-utils/fatal-process-barrier.test.d.mts.map +0 -1
- package/dist/dev-utils/fatal-process-barrier.test.mjs +0 -281
- package/dist/dev-utils/fatal-process-barrier.test.mjs.map +0 -1
- package/src/cli-replacement/dev-startup-lock-acquisition.test.mts +0 -573
- package/src/dev-utils/dev-server-json-parser.test.mts +0 -89
- package/src/dev-utils/dev-server.shutdown-lock.test.mts +0 -58
- package/src/dev-utils/fatal-process-barrier.mts +0 -262
- package/src/dev-utils/fatal-process-barrier.spawn-fixture.mts +0 -12
- package/src/dev-utils/fatal-process-barrier.test.mts +0 -346
|
@@ -16,10 +16,10 @@ import type {
|
|
|
16
16
|
DependencyUpgradeError,
|
|
17
17
|
ServerError,
|
|
18
18
|
} from "@superblocksteam/library-shared/types";
|
|
19
|
-
import type { ConflictError } from "@superblocksteam/shared";
|
|
20
19
|
import {
|
|
21
20
|
buildGithubSuperblocksSyncWorkflow,
|
|
22
21
|
buildGithubSuperblocksSyncWorkflowFromBaseUrl,
|
|
22
|
+
ConflictError,
|
|
23
23
|
DEFAULT_NON_GIT_BRANCH,
|
|
24
24
|
isGitAuthErrorMessage,
|
|
25
25
|
isGitHubRemoteUrl,
|
|
@@ -176,19 +176,6 @@ async function handleInactiveLockTakeover(
|
|
|
176
176
|
throw new Error("Lock takeover prompt shown");
|
|
177
177
|
}
|
|
178
178
|
|
|
179
|
-
// Only ask if somebody can answer. `promptUser` reads stdin, which on a
|
|
180
|
-
// sandbox nothing is attached to — it would wait forever for a keystroke
|
|
181
|
-
// that cannot come. Reachable now that conflicts surface when a client
|
|
182
|
-
// attaches rather than only during a CLI startup.
|
|
183
|
-
if (!process.stdin.isTTY) {
|
|
184
|
-
logger.warn(`${warningMessage} Run with --force-takeover to take it.`);
|
|
185
|
-
span.setStatus({
|
|
186
|
-
code: SpanStatusCode.ERROR,
|
|
187
|
-
message: "Lock held by an inactive user, nobody available to prompt",
|
|
188
|
-
});
|
|
189
|
-
throw new Error(warningMessage);
|
|
190
|
-
}
|
|
191
|
-
|
|
192
179
|
// Prompt user to force takeover (CLI only)
|
|
193
180
|
logger.warn(warningMessage);
|
|
194
181
|
const shouldTakeover = await getBooleanUserResponse(prompt);
|
|
@@ -1032,12 +1019,12 @@ export async function flushTelemetryWithTimeout(): Promise<boolean> {
|
|
|
1032
1019
|
* MUST BE CALLED LAST in any shutdown sequence. This does not merely flush: it
|
|
1033
1020
|
* awaits `shutdownTelemetry()`, which tears the provider down, so every signal
|
|
1034
1021
|
* emitted after it goes into a dead pipeline. It can also block for up to
|
|
1035
|
-
* `TELEMETRY_FLUSH_TIMEOUT_MS`, and delaying `lockService.
|
|
1036
|
-
*
|
|
1022
|
+
* `TELEMETRY_FLUSH_TIMEOUT_MS`, and delaying `lockService.shutdown()` by that
|
|
1023
|
+
* much delays the lock release that lets a replacement sandbox take over —
|
|
1037
1024
|
* which is the entire recovery story this exit path exists to enable.
|
|
1038
1025
|
*
|
|
1039
1026
|
* "Last" is only reachable because the callers drive the lock service with
|
|
1040
|
-
* `
|
|
1027
|
+
* `shutdown()` rather than `shutdownAndExit()`. The latter ends in an
|
|
1041
1028
|
* unconditional `finally { process.exit(1) }`, and `process.exit` is
|
|
1042
1029
|
* synchronous, so anything sequenced after it never runs at all. See the two
|
|
1043
1030
|
* fatal-exit blocks in `dev()`.
|
|
@@ -1088,8 +1075,8 @@ const FATAL_EXIT_STEP_TIMEOUT_MS = 5000;
|
|
|
1088
1075
|
|
|
1089
1076
|
/**
|
|
1090
1077
|
* One teardown step. Returning `undefined` is allowed so callers can pass the
|
|
1091
|
-
* optional-chained call directly (`() => lockService?.
|
|
1092
|
-
*
|
|
1078
|
+
* optional-chained call directly (`() => lockService?.shutdown(...)`) without
|
|
1079
|
+
* having to re-narrow a mutable binding at the call site.
|
|
1093
1080
|
*/
|
|
1094
1081
|
type FatalExitStep = () => Promise<unknown> | undefined;
|
|
1095
1082
|
|
|
@@ -1143,7 +1130,7 @@ async function runFatalExitStep(
|
|
|
1143
1130
|
*
|
|
1144
1131
|
* Order is load-bearing:
|
|
1145
1132
|
* 1. `removeIntegrationCache` — ai-service state that must not outlive us.
|
|
1146
|
-
* 2. `
|
|
1133
|
+
* 2. `shutdownLockService` — releases the AppBranchLock. Recovery-critical:
|
|
1147
1134
|
* the heartbeat dies with the process, so skipping this makes the
|
|
1148
1135
|
* replacement sandbox's `acquireLock()` fail with a ConflictError until
|
|
1149
1136
|
* the server-side TTL expires, and the replacement is the entire point of
|
|
@@ -1158,15 +1145,15 @@ async function runFatalExitStep(
|
|
|
1158
1145
|
*
|
|
1159
1146
|
* This does NOT exit. The caller owns `process.exit(1)` from a `finally`, so
|
|
1160
1147
|
* the exit still happens even if this function is somehow bypassed. That is
|
|
1161
|
-
* also why callers must drive the lock service through `
|
|
1162
|
-
*
|
|
1148
|
+
* also why callers must drive the lock service through `shutdown()` and not
|
|
1149
|
+
* `shutdownAndExit()`: the latter ends in its own unconditional
|
|
1163
1150
|
* `finally { process.exit(1) }`, and `process.exit` is synchronous, so
|
|
1164
1151
|
* everything sequenced after it — including the flush — silently never ran.
|
|
1165
1152
|
*/
|
|
1166
1153
|
export async function runFatalExitShutdown(steps: {
|
|
1167
1154
|
logger: Logger;
|
|
1168
1155
|
removeIntegrationCache?: FatalExitStep;
|
|
1169
|
-
|
|
1156
|
+
shutdownLockService?: FatalExitStep;
|
|
1170
1157
|
}): Promise<void> {
|
|
1171
1158
|
const { logger } = steps;
|
|
1172
1159
|
await runFatalExitStep(
|
|
@@ -1174,7 +1161,11 @@ export async function runFatalExitShutdown(steps: {
|
|
|
1174
1161
|
"ai-service integration cache removal",
|
|
1175
1162
|
steps.removeIntegrationCache,
|
|
1176
1163
|
);
|
|
1177
|
-
await runFatalExitStep(
|
|
1164
|
+
await runFatalExitStep(
|
|
1165
|
+
logger,
|
|
1166
|
+
"lock service shutdown",
|
|
1167
|
+
steps.shutdownLockService,
|
|
1168
|
+
);
|
|
1178
1169
|
await flushTelemetryBeforeExit(logger);
|
|
1179
1170
|
}
|
|
1180
1171
|
|
|
@@ -1644,84 +1635,35 @@ export async function dev(options: {
|
|
|
1644
1635
|
|
|
1645
1636
|
logger.info("Checking if local files are synced with the server");
|
|
1646
1637
|
|
|
1647
|
-
const lockSvc = lockService;
|
|
1648
|
-
|
|
1649
|
-
// Nobody takes the lock to boot: nothing startup does is gated on
|
|
1650
|
-
// holding one, and a lock taken before any client exists is held on
|
|
1651
|
-
// behalf of nobody. `LockService` takes it when the first client
|
|
1652
|
-
// attaches, and tells us here if somebody else already has it —
|
|
1653
|
-
// this is the `ConflictError` catch that used to wrap the boot-time
|
|
1654
|
-
// acquire, moved to where acquisition actually happens (ENG-6090).
|
|
1655
|
-
lockSvc.setOnLockConflict(async (error) => {
|
|
1656
|
-
let stopForExtension = false;
|
|
1657
|
-
await tracer.startActiveSpan("handlingLockConflict", async (span) => {
|
|
1658
|
-
try {
|
|
1659
|
-
await handleLockConflict(error, lockSvc, span, logger);
|
|
1660
|
-
} catch (conflictError) {
|
|
1661
|
-
// Reporting is all this owes the caller. The holder is someone
|
|
1662
|
-
// else's live session and the pod is still serving preview, so
|
|
1663
|
-
// there is nothing to tear down — the lock does not decide how
|
|
1664
|
-
// long this process lives (ENG-6032).
|
|
1665
|
-
logger.warn(
|
|
1666
|
-
`Could not take the live-edit lock: ${conflictError instanceof Error ? conflictError.message : String(conflictError)}`,
|
|
1667
|
-
);
|
|
1668
|
-
// Except under the VS Code extension, which reads the `cliprompt`
|
|
1669
|
-
// above, asks the user, and answers by restarting this command
|
|
1670
|
-
// with `--force-takeover`. That hand-off has always ended with
|
|
1671
|
-
// this process gone: it used to abort startup before anything
|
|
1672
|
-
// bound a port. Staying up now would leave the replacement
|
|
1673
|
-
// fighting us for :5173.
|
|
1674
|
-
if (process.env.SUPERBLOCKS_VSCODE === "true") {
|
|
1675
|
-
logger.info(
|
|
1676
|
-
"Stopping so the extension can restart with --force-takeover",
|
|
1677
|
-
);
|
|
1678
|
-
stopForExtension = true;
|
|
1679
|
-
}
|
|
1680
|
-
} finally {
|
|
1681
|
-
span.end();
|
|
1682
|
-
}
|
|
1683
|
-
});
|
|
1684
|
-
if (!stopForExtension) {
|
|
1685
|
-
return;
|
|
1686
|
-
}
|
|
1687
|
-
// Outside the span, so `span.end()` above has already run and the
|
|
1688
|
-
// flush below can carry it. This is the same teardown the two
|
|
1689
|
-
// fatal-exit blocks in `dev()` use, and for the same reason: the
|
|
1690
|
-
// `shutdownAndExit()` that used to be inside the `catch` exits
|
|
1691
|
-
// synchronously from its own `finally`, which made both the
|
|
1692
|
-
// `span.end()` and the telemetry flush unreachable — the trace for
|
|
1693
|
-
// the one event this handler exists to record never left the process.
|
|
1694
|
-
try {
|
|
1695
|
-
await runFatalExitShutdown({
|
|
1696
|
-
logger,
|
|
1697
|
-
removeIntegrationCache: () => aiService?.removeIntegrationCache(),
|
|
1698
|
-
relinquishLock: () => lockSvc.relinquishLock(),
|
|
1699
|
-
});
|
|
1700
|
-
} finally {
|
|
1701
|
-
process.exit(1);
|
|
1702
|
-
}
|
|
1703
|
-
});
|
|
1704
|
-
|
|
1705
1638
|
try {
|
|
1706
1639
|
await maskUnixSignals(async () => {
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
// helping itself to a lock nobody asked it to hold.
|
|
1710
|
-
if (options.forceTakeover) {
|
|
1640
|
+
if (lockService) {
|
|
1641
|
+
const lockSvc = lockService;
|
|
1711
1642
|
await tracer.startActiveSpan(
|
|
1712
|
-
"
|
|
1643
|
+
"acquiringInitialLock",
|
|
1713
1644
|
async (span) => {
|
|
1714
1645
|
try {
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1646
|
+
// If --force-takeover flag is set, immediately force takeover instead of acquiring
|
|
1647
|
+
if (options.forceTakeover) {
|
|
1648
|
+
logger.info(
|
|
1649
|
+
"Force takeover requested, taking over lock...",
|
|
1650
|
+
);
|
|
1651
|
+
await lockSvc.forceTakeover();
|
|
1652
|
+
logger.info(
|
|
1653
|
+
"Lock acquired successfully via force takeover.",
|
|
1654
|
+
);
|
|
1655
|
+
} else {
|
|
1656
|
+
await lockSvc.acquireLock();
|
|
1657
|
+
}
|
|
1722
1658
|
} catch (error) {
|
|
1659
|
+
// If we got a conflict error (409), inspect the lock for more information
|
|
1660
|
+
if (error instanceof ConflictError) {
|
|
1661
|
+
await handleLockConflict(error, lockSvc, span, logger);
|
|
1662
|
+
return;
|
|
1663
|
+
}
|
|
1664
|
+
|
|
1723
1665
|
logger.error(
|
|
1724
|
-
"Failed to
|
|
1666
|
+
"Failed to acquire lock on application",
|
|
1725
1667
|
getErrorMeta(error),
|
|
1726
1668
|
);
|
|
1727
1669
|
span.setStatus({
|
|
@@ -2395,7 +2337,7 @@ export async function dev(options: {
|
|
|
2395
2337
|
try {
|
|
2396
2338
|
logger.info("Releasing lock before restarting the dev server");
|
|
2397
2339
|
await aiService?.removeIntegrationCache();
|
|
2398
|
-
await lockService?.
|
|
2340
|
+
await lockService?.shutdown({ serverInitiated: false });
|
|
2399
2341
|
} catch (e) {
|
|
2400
2342
|
logger.error(
|
|
2401
2343
|
"Error releasing lock before restarting the dev server",
|
|
@@ -2472,13 +2414,13 @@ export async function dev(options: {
|
|
|
2472
2414
|
}),
|
|
2473
2415
|
);
|
|
2474
2416
|
try {
|
|
2475
|
-
// `
|
|
2476
|
-
//
|
|
2477
|
-
//
|
|
2417
|
+
// `shutdown()`, NOT `shutdownAndExit()`. The latter ends in an
|
|
2418
|
+
// unconditional `finally { process.exit(1) }`; `process.exit` is
|
|
2419
|
+
// synchronous, so every statement below it would be dead code
|
|
2478
2420
|
// whenever `lockService` is set — which is the normal state in a
|
|
2479
2421
|
// SABS pod, exactly the population this exit path targets. The
|
|
2480
|
-
// telemetry flush was silently never running. `
|
|
2481
|
-
//
|
|
2422
|
+
// telemetry flush was silently never running. `shutdown()` does
|
|
2423
|
+
// the same teardown without owning the exit, matching how
|
|
2482
2424
|
// `runGracefulShutdown` in dev-server.mts drives it; the
|
|
2483
2425
|
// `finally` below performs the exit instead.
|
|
2484
2426
|
//
|
|
@@ -2489,7 +2431,8 @@ export async function dev(options: {
|
|
|
2489
2431
|
logger,
|
|
2490
2432
|
removeIntegrationCache: () =>
|
|
2491
2433
|
aiService?.removeIntegrationCache(),
|
|
2492
|
-
|
|
2434
|
+
shutdownLockService: () =>
|
|
2435
|
+
lockService?.shutdown({ serverInitiated: false }),
|
|
2493
2436
|
});
|
|
2494
2437
|
} finally {
|
|
2495
2438
|
// The only exit on this path now that the lock service no longer
|
|
@@ -2546,14 +2489,15 @@ export async function dev(options: {
|
|
|
2546
2489
|
}),
|
|
2547
2490
|
);
|
|
2548
2491
|
try {
|
|
2549
|
-
// `
|
|
2492
|
+
// `shutdown()`, NOT `shutdownAndExit()` — same reason as the
|
|
2550
2493
|
// sync/lock catch above: `shutdownAndExit()` exits the process
|
|
2551
2494
|
// synchronously in its own `finally`, which made everything
|
|
2552
2495
|
// sequenced after it unreachable.
|
|
2553
2496
|
await runFatalExitShutdown({
|
|
2554
2497
|
logger,
|
|
2555
2498
|
removeIntegrationCache: () => aiService?.removeIntegrationCache(),
|
|
2556
|
-
|
|
2499
|
+
shutdownLockService: () =>
|
|
2500
|
+
lockService?.shutdown({ serverInitiated: false }),
|
|
2557
2501
|
});
|
|
2558
2502
|
} finally {
|
|
2559
2503
|
// The only exit on this path; here so a thrown shutdown path can't
|
|
@@ -2751,8 +2695,7 @@ export async function dev(options: {
|
|
|
2751
2695
|
logger.warn(`Error removing integration cache: ${error}`);
|
|
2752
2696
|
});
|
|
2753
2697
|
lockService?.shutdownAndExit().catch(() => {
|
|
2754
|
-
//
|
|
2755
|
-
// kept so the process still goes away if that ever stops being true.
|
|
2698
|
+
// this is redundant, but it's here to make sure the lock service is shutdown and the process exits
|
|
2756
2699
|
process.exit(1);
|
|
2757
2700
|
});
|
|
2758
2701
|
});
|
|
@@ -3151,7 +3094,7 @@ async function ensureRuntimeDbfsBranchConsistency({
|
|
|
3151
3094
|
currentBranchName: string;
|
|
3152
3095
|
}): Promise<string> {
|
|
3153
3096
|
interface BranchSwitchingLockService {
|
|
3154
|
-
|
|
3097
|
+
isLocked: boolean;
|
|
3155
3098
|
switchBranch(
|
|
3156
3099
|
nextBranchName: string,
|
|
3157
3100
|
options?: { reacquireLock?: boolean },
|
|
@@ -3181,8 +3124,7 @@ async function ensureRuntimeDbfsBranchConsistency({
|
|
|
3181
3124
|
|
|
3182
3125
|
const lockSvc = lockService as unknown as BranchSwitchingLockService;
|
|
3183
3126
|
const syncSvc = syncService as unknown as BranchSwitchingSyncService;
|
|
3184
|
-
|
|
3185
|
-
const wasLocked = await lockSvc.isLockedWhenSettled();
|
|
3127
|
+
const wasLocked = lockSvc.isLocked;
|
|
3186
3128
|
logger.info(
|
|
3187
3129
|
`Switching runtime DBFS branch context from '${currentBranchName}' to '${targetBranchName}'`,
|
|
3188
3130
|
);
|
|
@@ -3190,12 +3132,14 @@ async function ensureRuntimeDbfsBranchConsistency({
|
|
|
3190
3132
|
try {
|
|
3191
3133
|
syncSvc.setBranchName(targetBranchName);
|
|
3192
3134
|
try {
|
|
3193
|
-
await lockSvc.switchBranch(targetBranchName
|
|
3135
|
+
await lockSvc.switchBranch(targetBranchName, {
|
|
3136
|
+
reacquireLock: wasLocked,
|
|
3137
|
+
});
|
|
3194
3138
|
} catch (switchError) {
|
|
3195
3139
|
// Disconnecting native git can invalidate the previous live branch
|
|
3196
3140
|
// context. Fall back to explicit release/reacquire on the target branch.
|
|
3197
3141
|
logger.warn(
|
|
3198
|
-
`Lock branch transfer failed for '${currentBranchName}' -> '${targetBranchName}', performing
|
|
3142
|
+
`Lock branch transfer failed for '${currentBranchName}' -> '${targetBranchName}', performing shutdown-style lock reset: ${switchError instanceof Error ? switchError.message : String(switchError)}`,
|
|
3199
3143
|
);
|
|
3200
3144
|
await lockSvc.switchBranch(targetBranchName, { reacquireLock: false });
|
|
3201
3145
|
if (wasLocked) {
|
|
@@ -3212,7 +3156,7 @@ async function ensureRuntimeDbfsBranchConsistency({
|
|
|
3212
3156
|
syncSvc.setBranchName(currentBranchName);
|
|
3213
3157
|
try {
|
|
3214
3158
|
await lockSvc.switchBranch(currentBranchName, {
|
|
3215
|
-
reacquireLock:
|
|
3159
|
+
reacquireLock: lockSvc.isLocked || wasLocked,
|
|
3216
3160
|
});
|
|
3217
3161
|
} catch (rollbackError) {
|
|
3218
3162
|
logger.warn(
|
|
@@ -69,12 +69,11 @@ import {
|
|
|
69
69
|
type DevServerFailureType,
|
|
70
70
|
devServerMetrics,
|
|
71
71
|
} from "./dev-server-metrics.mjs";
|
|
72
|
-
import { isRecoverableShutdownError, parseIsWarm } from "./fatal-exit.mjs";
|
|
73
72
|
import {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
} from "./fatal-
|
|
73
|
+
buildFatalExitLog,
|
|
74
|
+
isRecoverableShutdownError,
|
|
75
|
+
parseIsWarm,
|
|
76
|
+
} from "./fatal-exit.mjs";
|
|
78
77
|
import { OPTIMIZE_DEPS_CONFIG } from "./optimize-deps-config.mjs";
|
|
79
78
|
import { prepareInPlaceRestart } from "./prepare-in-place-restart.mjs";
|
|
80
79
|
import { handleRecoverableBuildError } from "./recoverable-build-error.mjs";
|
|
@@ -547,7 +546,7 @@ export function buildStatusPayload<T extends object>(
|
|
|
547
546
|
return { ...base, serverErrors: devServerStatus?.serverErrors ?? [] };
|
|
548
547
|
}
|
|
549
548
|
|
|
550
|
-
type ProcessErrorHandler =
|
|
549
|
+
type ProcessErrorHandler = "uncaughtException" | "unhandledRejection";
|
|
551
550
|
|
|
552
551
|
// Both listeners log a recoverable shutdown, but with different wording, and
|
|
553
552
|
// these lines are what on-call greps when a pod stays up instead of restarting.
|
|
@@ -558,59 +557,6 @@ const RECOVERABLE_SHUTDOWN_LOG_MESSAGE: Record<ProcessErrorHandler, string> = {
|
|
|
558
557
|
"Ignoring recoverable shutdown rejection (unhandledRejection)",
|
|
559
558
|
};
|
|
560
559
|
|
|
561
|
-
/**
|
|
562
|
-
* What the dev server needs of the lock service on its way out. Structural so a
|
|
563
|
-
* test does not have to stand up a real one.
|
|
564
|
-
*/
|
|
565
|
-
type ExitingLockService = Pick<
|
|
566
|
-
LockService,
|
|
567
|
-
"notifyClosedByServer" | "relinquishLock"
|
|
568
|
-
>;
|
|
569
|
-
|
|
570
|
-
/**
|
|
571
|
-
* Give the AppBranchLock to whoever should have it next, then let the caller
|
|
572
|
-
* exit.
|
|
573
|
-
*
|
|
574
|
-
* Which of the two things to do is the dev server's knowledge, not the lock
|
|
575
|
-
* service's — it used to be a `serverInitiated` flag passed down into a
|
|
576
|
-
* `shutdown()` that branched on it, which is how a lock service came to have an
|
|
577
|
-
* opinion about why a process was stopping.
|
|
578
|
-
*
|
|
579
|
-
* Normally this process is the one stopping, so the lock goes back: the
|
|
580
|
-
* heartbeat dies with the process, and anything not released here makes the
|
|
581
|
-
* replacement sandbox's `acquireLock()` fail until the server-side TTL expires
|
|
582
|
-
* — and the replacement is the entire point of exiting.
|
|
583
|
-
*
|
|
584
|
-
* When the server closed us, the lock is already gone. `/_sb_disconnect` is
|
|
585
|
-
* usually a session moving across local↔cloud, so releasing would take the lock
|
|
586
|
-
* from whoever is taking over. The editor is told instead, because it has to
|
|
587
|
-
* distinguish "the server closed you" from a socket dropping.
|
|
588
|
-
*
|
|
589
|
-
* Extracted so both branches are testable: `runGracefulShutdown` is a closure
|
|
590
|
-
* over a booted dev server, so a unit test cannot otherwise reach either one.
|
|
591
|
-
*/
|
|
592
|
-
export async function handOverLockOnExit(
|
|
593
|
-
lockService: ExitingLockService | undefined,
|
|
594
|
-
{
|
|
595
|
-
serverInitiated,
|
|
596
|
-
switchingTo,
|
|
597
|
-
initiatedByEmail,
|
|
598
|
-
}: {
|
|
599
|
-
serverInitiated: boolean;
|
|
600
|
-
switchingTo?: "local" | "cloud" | "none";
|
|
601
|
-
initiatedByEmail?: string;
|
|
602
|
-
},
|
|
603
|
-
): Promise<void> {
|
|
604
|
-
if (!lockService) {
|
|
605
|
-
return;
|
|
606
|
-
}
|
|
607
|
-
if (serverInitiated) {
|
|
608
|
-
await lockService.notifyClosedByServer({ switchingTo, initiatedByEmail });
|
|
609
|
-
return;
|
|
610
|
-
}
|
|
611
|
-
await lockService.relinquishLock();
|
|
612
|
-
}
|
|
613
|
-
|
|
614
560
|
/**
|
|
615
561
|
* Decides whether a process-level error keeps the pod alive, and performs the
|
|
616
562
|
* recording for the cases that do. Returns `true` when the caller must return
|
|
@@ -677,22 +623,6 @@ export function handleNonFatalProcessError(params: {
|
|
|
677
623
|
export const DEV_SERVER_BOOT_ID = randomUUID();
|
|
678
624
|
export const DEV_SERVER_STARTED_AT = new Date().toISOString();
|
|
679
625
|
|
|
680
|
-
export function mountDevServerJsonParsers(app: IRouter): void {
|
|
681
|
-
app.use(
|
|
682
|
-
"/_sb_policy_gates",
|
|
683
|
-
express.json({
|
|
684
|
-
limit: "1mb",
|
|
685
|
-
strict: true,
|
|
686
|
-
}),
|
|
687
|
-
);
|
|
688
|
-
app.use(
|
|
689
|
-
express.json({
|
|
690
|
-
limit: "100kb",
|
|
691
|
-
strict: true,
|
|
692
|
-
}),
|
|
693
|
-
);
|
|
694
|
-
}
|
|
695
|
-
|
|
696
626
|
/**
|
|
697
627
|
* Builds the `/_sb_health` payload, including the per-process boot identity.
|
|
698
628
|
* Extracted as a pure function so the boot-identity contract can be unit
|
|
@@ -941,7 +871,7 @@ export async function createDevServer({
|
|
|
941
871
|
}): Promise<void> {
|
|
942
872
|
try {
|
|
943
873
|
stopMemoryMetrics();
|
|
944
|
-
await
|
|
874
|
+
await lockService?.shutdown({
|
|
945
875
|
serverInitiated,
|
|
946
876
|
switchingTo,
|
|
947
877
|
initiatedByEmail,
|
|
@@ -952,23 +882,16 @@ export async function createDevServer({
|
|
|
952
882
|
} catch (e) {
|
|
953
883
|
logger.error(`Error during graceful shutdown: ${e}`);
|
|
954
884
|
} finally {
|
|
955
|
-
// The statement about this process, made by the thing that owns its exit.
|
|
956
|
-
// It used to be the last line of `LockService.shutdown()`, prefixed
|
|
957
|
-
// `[lock-service]` — a lock service announcing a process exit, which is
|
|
958
|
-
// the tangle this change removes.
|
|
959
|
-
//
|
|
960
|
-
// Load bearing: cli-system-tests waits on this exact substring.
|
|
961
|
-
logger.info("[dev-server] shutting down and exiting dev server");
|
|
962
885
|
const closeTimeout = setTimeout(() => {
|
|
963
886
|
logger.info("Dev server closed");
|
|
964
|
-
process.exit(
|
|
887
|
+
process.exit(0);
|
|
965
888
|
}, 1000); // Fallback to exit if the HTTP server doesn't close within 1 second
|
|
966
889
|
|
|
967
890
|
// Then close the HTTP server
|
|
968
891
|
httpServer.close(() => {
|
|
969
892
|
logger.info("Dev server closed");
|
|
970
893
|
clearTimeout(closeTimeout);
|
|
971
|
-
process.exit(
|
|
894
|
+
process.exit(0);
|
|
972
895
|
});
|
|
973
896
|
}
|
|
974
897
|
}
|
|
@@ -979,7 +902,11 @@ export async function createDevServer({
|
|
|
979
902
|
credentials: true,
|
|
980
903
|
}),
|
|
981
904
|
);
|
|
982
|
-
|
|
905
|
+
app.use(
|
|
906
|
+
express.json({
|
|
907
|
+
strict: true,
|
|
908
|
+
}),
|
|
909
|
+
);
|
|
983
910
|
|
|
984
911
|
// Per-request metrics for dev-server endpoints. Identify the endpoint by URL
|
|
985
912
|
// pattern, time the request, record duration + status + coarse failure type
|
|
@@ -1339,11 +1266,18 @@ export async function createDevServer({
|
|
|
1339
1266
|
// even if the pod is activated later.
|
|
1340
1267
|
const isWarm = parseIsWarm(process.env.SUPERBLOCKS_WARM_STANDBY);
|
|
1341
1268
|
|
|
1269
|
+
// Only the first fatal event writes a fatal-exit line and starts shutdown.
|
|
1270
|
+
// Without this, one crash could write several lines: an unhandled rejection
|
|
1271
|
+
// fires once per rejected promise, so a burst of failures (say a dead DB pool
|
|
1272
|
+
// rejecting every in-flight query) would each write a line and each start
|
|
1273
|
+
// shutdown, making one crash look like many in the logs and the crash count.
|
|
1274
|
+
let fatalExitHandled = false;
|
|
1275
|
+
|
|
1342
1276
|
// Signal handlers attach `.catch` so a synchronous throw in
|
|
1343
1277
|
// `runGracefulShutdown` (e.g. logger init or lockService shutdown throwing
|
|
1344
1278
|
// before the first `await`) is logged rather than surfacing as an
|
|
1345
1279
|
// unhandled rejection across N handlers (one per fired signal). Symmetric
|
|
1346
|
-
// with the
|
|
1280
|
+
// with the `uncaughtException` handler below.
|
|
1347
1281
|
process.on("SIGINT", () => {
|
|
1348
1282
|
logger.info("SIGINT received");
|
|
1349
1283
|
gracefulShutdown({
|
|
@@ -1377,37 +1311,86 @@ export async function createDevServer({
|
|
|
1377
1311
|
);
|
|
1378
1312
|
});
|
|
1379
1313
|
|
|
1380
|
-
//
|
|
1381
|
-
//
|
|
1382
|
-
//
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1314
|
+
// The `.finally` always ends the process once shutdown settles. If an earlier
|
|
1315
|
+
// signal already ran shutdown, this handler still needs to exit; without the
|
|
1316
|
+
// final `process.exit` the process could hang instead of stopping.
|
|
1317
|
+
process.on("uncaughtException", (error) => {
|
|
1318
|
+
if (fatalExitHandled) {
|
|
1319
|
+
return;
|
|
1320
|
+
}
|
|
1321
|
+
if (
|
|
1386
1322
|
handleNonFatalProcessError({
|
|
1323
|
+
devServerStatus,
|
|
1387
1324
|
devServerMetrics,
|
|
1325
|
+
handler: "uncaughtException",
|
|
1326
|
+
logger,
|
|
1327
|
+
reason: error,
|
|
1328
|
+
})
|
|
1329
|
+
) {
|
|
1330
|
+
return;
|
|
1331
|
+
}
|
|
1332
|
+
fatalExitHandled = true;
|
|
1333
|
+
// Write the fatal-exit line first, and synchronously, so the reason reaches
|
|
1334
|
+
// the logs before anything else runs. Shutdown below may finish first and
|
|
1335
|
+
// call `process.exit(0)`, so the container's real exit code can be 0 even
|
|
1336
|
+
// though this was a crash. That is why `exit_code=1` here is the code this
|
|
1337
|
+
// handler means to use, not a promise of what the container reports: for the
|
|
1338
|
+
// JS-handler lines, trust that the line exists (and the `signal` field) over
|
|
1339
|
+
// its `exit_code` when comparing against the pod's exit status.
|
|
1340
|
+
logger.error(
|
|
1341
|
+
buildFatalExitLog({ handler: "uncaughtException", exitCode: 1, isWarm }),
|
|
1342
|
+
getErrorMeta(error),
|
|
1343
|
+
);
|
|
1344
|
+
gracefulShutdown({
|
|
1345
|
+
logger,
|
|
1346
|
+
serverInitiated: false,
|
|
1347
|
+
source: "uncaughtException",
|
|
1348
|
+
})
|
|
1349
|
+
.catch((shutdownError) => {
|
|
1350
|
+
logger.error(
|
|
1351
|
+
"Error during shutdown after uncaught exception:",
|
|
1352
|
+
getErrorMeta(shutdownError),
|
|
1353
|
+
);
|
|
1354
|
+
})
|
|
1355
|
+
.finally(() => process.exit(1));
|
|
1356
|
+
});
|
|
1357
|
+
|
|
1358
|
+
// Without its own listener, Node turns an unhandled promise rejection into an
|
|
1359
|
+
// uncaught exception, which would log it under the wrong cause. Handle it here
|
|
1360
|
+
// so a rejection is labelled as a rejection, then exit through the same
|
|
1361
|
+
// shutdown path as an uncaught exception.
|
|
1362
|
+
process.on("unhandledRejection", (reason) => {
|
|
1363
|
+
if (fatalExitHandled) {
|
|
1364
|
+
return;
|
|
1365
|
+
}
|
|
1366
|
+
if (
|
|
1367
|
+
handleNonFatalProcessError({
|
|
1388
1368
|
devServerStatus,
|
|
1389
|
-
|
|
1369
|
+
devServerMetrics,
|
|
1370
|
+
handler: "unhandledRejection",
|
|
1390
1371
|
logger,
|
|
1391
1372
|
reason,
|
|
1392
|
-
})
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1373
|
+
})
|
|
1374
|
+
) {
|
|
1375
|
+
return;
|
|
1376
|
+
}
|
|
1377
|
+
fatalExitHandled = true;
|
|
1378
|
+
logger.error(
|
|
1379
|
+
buildFatalExitLog({ handler: "unhandledRejection", exitCode: 1, isWarm }),
|
|
1380
|
+
getErrorMeta(reason),
|
|
1381
|
+
);
|
|
1382
|
+
gracefulShutdown({
|
|
1383
|
+
logger,
|
|
1384
|
+
serverInitiated: false,
|
|
1385
|
+
source: "unhandledRejection",
|
|
1386
|
+
})
|
|
1387
|
+
.catch((shutdownError) => {
|
|
1403
1388
|
logger.error(
|
|
1404
|
-
|
|
1405
|
-
? "Error during shutdown after uncaught exception:"
|
|
1406
|
-
: "Error during shutdown after unhandled rejection:",
|
|
1389
|
+
"Error during shutdown after unhandled rejection:",
|
|
1407
1390
|
getErrorMeta(shutdownError),
|
|
1408
1391
|
);
|
|
1409
|
-
}
|
|
1410
|
-
|
|
1392
|
+
})
|
|
1393
|
+
.finally(() => process.exit(1));
|
|
1411
1394
|
});
|
|
1412
1395
|
|
|
1413
1396
|
if (existingServer) {
|
package/src/telemetry/logging.ts
CHANGED
|
@@ -1,11 +1,3 @@
|
|
|
1
|
-
function fallbackString(value: unknown): string {
|
|
2
|
-
try {
|
|
3
|
-
return String(value);
|
|
4
|
-
} catch {
|
|
5
|
-
return "[unserializable]";
|
|
6
|
-
}
|
|
7
|
-
}
|
|
8
|
-
|
|
9
1
|
export function safeStringify(obj: unknown): string {
|
|
10
2
|
if (obj === null || obj === undefined) {
|
|
11
3
|
return String(obj);
|
|
@@ -17,22 +9,15 @@ export function safeStringify(obj: unknown): string {
|
|
|
17
9
|
return String(obj);
|
|
18
10
|
}
|
|
19
11
|
|
|
20
|
-
const seen = new WeakSet
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
if (typeof value === "bigint") {
|
|
24
|
-
return value.toString();
|
|
25
|
-
}
|
|
26
|
-
if (value === null || typeof value !== "object") {
|
|
27
|
-
return value;
|
|
28
|
-
}
|
|
29
|
-
if (seen.has(value)) {
|
|
30
|
-
return "[Circular]";
|
|
31
|
-
}
|
|
32
|
-
seen.add(value);
|
|
12
|
+
const seen = new WeakSet();
|
|
13
|
+
return JSON.stringify(obj, (_key, value) => {
|
|
14
|
+
if (value === null || typeof value !== "object") {
|
|
33
15
|
return value;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
16
|
+
}
|
|
17
|
+
if (seen.has(value)) {
|
|
18
|
+
return "[Circular]";
|
|
19
|
+
}
|
|
20
|
+
seen.add(value);
|
|
21
|
+
return value;
|
|
22
|
+
});
|
|
38
23
|
}
|