@decocms/sandbox-controller 0.15.3 → 0.15.5
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/index.d.ts +23 -0
- package/dist/index.js +71 -10
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -3750,6 +3750,8 @@ interface FreestyleSandboxProviderOptions {
|
|
|
3750
3750
|
meter?: Meter;
|
|
3751
3751
|
/** Replaces the API client, for tests. */
|
|
3752
3752
|
client?: Freestyle;
|
|
3753
|
+
/** How long a started VM's daemon gets to answer /health, for tests. */
|
|
3754
|
+
daemonBootTimeoutMs?: number;
|
|
3753
3755
|
}
|
|
3754
3756
|
declare class FreestyleSandboxProvider implements SandboxProvider {
|
|
3755
3757
|
private readonly apiKey;
|
|
@@ -3761,6 +3763,7 @@ declare class FreestyleSandboxProvider implements SandboxProvider {
|
|
|
3761
3763
|
private readonly idleTimeoutSeconds;
|
|
3762
3764
|
private readonly idlePauseMs;
|
|
3763
3765
|
private readonly autoDeleteSeconds;
|
|
3766
|
+
private readonly daemonBootTimeoutMs;
|
|
3764
3767
|
private readonly domainSuffix;
|
|
3765
3768
|
private readonly timeDaemonRequest;
|
|
3766
3769
|
private readonly inflight;
|
|
@@ -3867,7 +3870,19 @@ declare class FreestyleSandboxProvider implements SandboxProvider {
|
|
|
3867
3870
|
private createVm;
|
|
3868
3871
|
private configPayload;
|
|
3869
3872
|
private waitForDaemon;
|
|
3873
|
+
/**
|
|
3874
|
+
* Deletes the VM only once its shutdown sync is confirmed. Otherwise the
|
|
3875
|
+
* user's unpushed work may still be on its disk, so it is detached instead:
|
|
3876
|
+
* the handle is free for a new sandbox, and the VM stays recoverable by id
|
|
3877
|
+
* until Freestyle's auto-delete.
|
|
3878
|
+
*/
|
|
3870
3879
|
delete(handle: string): Promise<void>;
|
|
3880
|
+
/**
|
|
3881
|
+
* Stops the daemon, which publishes unsaved work to git on SIGTERM, then the
|
|
3882
|
+
* sidecar, which flushes its mounts. The daemon exits 0 even when that
|
|
3883
|
+
* publish fails, so its log decides.
|
|
3884
|
+
*/
|
|
3885
|
+
private syncedOnShutdown;
|
|
3871
3886
|
lastTermination(_handle: string): Promise<PodTermination | null>;
|
|
3872
3887
|
/** A paused VM is alive: the next request resumes it. */
|
|
3873
3888
|
alive(handle: string): Promise<boolean>;
|
|
@@ -3954,6 +3969,13 @@ declare class SandboxProviderRouter implements SandboxProvider {
|
|
|
3954
3969
|
* reclaims a stray paused VM anyway, only later.
|
|
3955
3970
|
*/
|
|
3956
3971
|
private readonly strays;
|
|
3972
|
+
/**
|
|
3973
|
+
* The latest ensure per handle. Placement reads what exists, so two at once
|
|
3974
|
+
* each found nothing and put one sandbox on each provider.
|
|
3975
|
+
*
|
|
3976
|
+
* ponytail: in-process; replicas would need the state store's lock.
|
|
3977
|
+
*/
|
|
3978
|
+
private readonly ensuring;
|
|
3957
3979
|
private readonly freestyleShare;
|
|
3958
3980
|
constructor(providers: SandboxProviders, opts?: SandboxProviderRouterOptions);
|
|
3959
3981
|
private get;
|
|
@@ -3964,6 +3986,7 @@ declare class SandboxProviderRouter implements SandboxProvider {
|
|
|
3964
3986
|
/** Where `ensure` puts this sandbox, and why. */
|
|
3965
3987
|
place(handle: string, opts: EnsureOptions): Promise<Placement>;
|
|
3966
3988
|
ensure(id: SandboxId, opts?: EnsureOptions): Promise<Sandbox>;
|
|
3989
|
+
private ensureOne;
|
|
3967
3990
|
/** Delete the sandbox a fallback left behind on `kind`, in the background. */
|
|
3968
3991
|
private retire;
|
|
3969
3992
|
delete(handle: string): Promise<void>;
|
package/dist/index.js
CHANGED
|
@@ -6804,7 +6804,7 @@ var Freestyle = class {
|
|
|
6804
6804
|
// ../sandbox/package.json
|
|
6805
6805
|
var package_default = {
|
|
6806
6806
|
name: "@decocms/sandbox",
|
|
6807
|
-
version: "1.95.
|
|
6807
|
+
version: "1.95.5",
|
|
6808
6808
|
private: true,
|
|
6809
6809
|
type: "module",
|
|
6810
6810
|
description: "Sandbox runtime for isolated Studio tool execution",
|
|
@@ -6926,6 +6926,7 @@ var FreestyleSandboxProvider = class {
|
|
|
6926
6926
|
idleTimeoutSeconds;
|
|
6927
6927
|
idlePauseMs;
|
|
6928
6928
|
autoDeleteSeconds;
|
|
6929
|
+
daemonBootTimeoutMs;
|
|
6929
6930
|
domainSuffix;
|
|
6930
6931
|
timeDaemonRequest;
|
|
6931
6932
|
inflight = new Inflight();
|
|
@@ -6966,6 +6967,7 @@ var FreestyleSandboxProvider = class {
|
|
|
6966
6967
|
this.idleTimeoutSeconds = opts.idleTimeoutSeconds ?? 5 * 60;
|
|
6967
6968
|
this.idlePauseMs = opts.idlePauseMs ?? 10 * 6e4;
|
|
6968
6969
|
this.autoDeleteSeconds = opts.autoDeleteSeconds ?? 24 * 60 * 60;
|
|
6970
|
+
this.daemonBootTimeoutMs = opts.daemonBootTimeoutMs ?? DAEMON_BOOT_TIMEOUT_MS;
|
|
6969
6971
|
this.domainSuffix = opts.domainSuffix ?? "style.dev";
|
|
6970
6972
|
this.timeDaemonRequest = daemonProxyTimer(opts.meter);
|
|
6971
6973
|
if (this.idlePauseMs > 0) {
|
|
@@ -7300,7 +7302,12 @@ ${head}`).digest("hex").slice(0, 16)}`;
|
|
|
7300
7302
|
async resume(handle, found, opts) {
|
|
7301
7303
|
const { state, sourceSnapshotSlugAtCreate, metadata } = await found.vm.data();
|
|
7302
7304
|
if (state !== "running") await found.vm.start();
|
|
7303
|
-
await this.waitForDaemon(found.daemon.url)
|
|
7305
|
+
await this.waitForDaemon(found.daemon.url).catch(async (err) => {
|
|
7306
|
+
console.warn(`[${LOG_LABEL2}] ${handle}: ${errMsg2(err)}; rebooting`);
|
|
7307
|
+
await found.vm.exec({ command: "reboot -f", timeoutMs: 1e4 }).catch(() => {
|
|
7308
|
+
});
|
|
7309
|
+
await this.waitForDaemon(found.daemon.url);
|
|
7310
|
+
});
|
|
7304
7311
|
const sentinelSlug = metadata[BASE_KEY] ?? sourceSnapshotSlugAtCreate;
|
|
7305
7312
|
if (sentinelSlug) {
|
|
7306
7313
|
await this.reclaim(found, sentinelSlug, opts);
|
|
@@ -7539,26 +7546,63 @@ ${head}`).digest("hex").slice(0, 16)}`;
|
|
|
7539
7546
|
});
|
|
7540
7547
|
}
|
|
7541
7548
|
async waitForDaemon(url) {
|
|
7542
|
-
const deadline = Date.now() +
|
|
7549
|
+
const deadline = Date.now() + this.daemonBootTimeoutMs;
|
|
7543
7550
|
while (Date.now() < deadline) {
|
|
7544
7551
|
if (await daemonAnswers(url)) return;
|
|
7545
7552
|
await delay(HEALTH_POLL_MS);
|
|
7546
7553
|
}
|
|
7547
7554
|
throw new Error(
|
|
7548
|
-
`sandbox daemon at ${url} did not answer /health within ${
|
|
7555
|
+
`sandbox daemon at ${url} did not answer /health within ${this.daemonBootTimeoutMs / 1e3}s`
|
|
7549
7556
|
);
|
|
7550
7557
|
}
|
|
7558
|
+
/**
|
|
7559
|
+
* Deletes the VM only once its shutdown sync is confirmed. Otherwise the
|
|
7560
|
+
* user's unpushed work may still be on its disk, so it is detached instead:
|
|
7561
|
+
* the handle is free for a new sandbox, and the VM stays recoverable by id
|
|
7562
|
+
* until Freestyle's auto-delete.
|
|
7563
|
+
*/
|
|
7551
7564
|
async delete(handle) {
|
|
7552
7565
|
this.cancelRelease(handle);
|
|
7553
7566
|
const found = await this.find(handle, { refresh: true });
|
|
7554
7567
|
this.lookups.delete(handle);
|
|
7555
7568
|
if (!found) return;
|
|
7556
|
-
await found.vm
|
|
7557
|
-
|
|
7558
|
-
|
|
7559
|
-
}
|
|
7569
|
+
if (await this.syncedOnShutdown(found.vm)) {
|
|
7570
|
+
await found.vm.delete();
|
|
7571
|
+
return;
|
|
7572
|
+
}
|
|
7573
|
+
const { rules } = await this.freestyle.tls.rules.list({
|
|
7574
|
+
vmId: found.vm.id
|
|
7560
7575
|
});
|
|
7561
|
-
await
|
|
7576
|
+
for (const rule of rules) await this.freestyle.tls.rules.delete(rule.id);
|
|
7577
|
+
await found.vm.update({ slug: "" });
|
|
7578
|
+
this.appIngress.delete(handle);
|
|
7579
|
+
console.warn(
|
|
7580
|
+
`[${LOG_LABEL2}] ${handle}: shutdown sync not confirmed; detached VM ${found.vm.id} instead of deleting it`
|
|
7581
|
+
);
|
|
7582
|
+
}
|
|
7583
|
+
/**
|
|
7584
|
+
* Stops the daemon, which publishes unsaved work to git on SIGTERM, then the
|
|
7585
|
+
* sidecar, which flushes its mounts. The daemon exits 0 even when that
|
|
7586
|
+
* publish fails, so its log decides.
|
|
7587
|
+
*/
|
|
7588
|
+
async syncedOnShutdown(vm) {
|
|
7589
|
+
try {
|
|
7590
|
+
const { state } = await vm.data();
|
|
7591
|
+
if (state !== "running") await vm.start();
|
|
7592
|
+
const res = await vm.exec({
|
|
7593
|
+
command: [
|
|
7594
|
+
"since=$(date +%s)",
|
|
7595
|
+
`docker stop -t 30 ${CONTAINER} >/dev/null`,
|
|
7596
|
+
`code=$(docker inspect -f '{{.State.ExitCode}}' ${CONTAINER})`,
|
|
7597
|
+
`docker stop -t 10 ${SIDECAR} >/dev/null`,
|
|
7598
|
+
`[ "$code" = 0 ] && ! docker logs --since "$since" ${CONTAINER} 2>&1 | grep -q "shutdown publish failed"`
|
|
7599
|
+
].join("; "),
|
|
7600
|
+
timeoutMs: 6e4
|
|
7601
|
+
});
|
|
7602
|
+
return res.statusCode === 0;
|
|
7603
|
+
} catch {
|
|
7604
|
+
return false;
|
|
7605
|
+
}
|
|
7562
7606
|
}
|
|
7563
7607
|
async lastTermination(_handle) {
|
|
7564
7608
|
return null;
|
|
@@ -7861,6 +7905,13 @@ var SandboxProviderRouter = class {
|
|
|
7861
7905
|
* reclaims a stray paused VM anyway, only later.
|
|
7862
7906
|
*/
|
|
7863
7907
|
strays = /* @__PURE__ */ new Map();
|
|
7908
|
+
/**
|
|
7909
|
+
* The latest ensure per handle. Placement reads what exists, so two at once
|
|
7910
|
+
* each found nothing and put one sandbox on each provider.
|
|
7911
|
+
*
|
|
7912
|
+
* ponytail: in-process; replicas would need the state store's lock.
|
|
7913
|
+
*/
|
|
7914
|
+
ensuring = /* @__PURE__ */ new Map();
|
|
7864
7915
|
freestyleShare;
|
|
7865
7916
|
get(kind) {
|
|
7866
7917
|
const provider = this.providers[kind];
|
|
@@ -7919,8 +7970,18 @@ var SandboxProviderRouter = class {
|
|
|
7919
7970
|
return roomy ? { kind: "kubernetes", reason: "split" } : { kind: "freestyle", reason: "capacity" };
|
|
7920
7971
|
}
|
|
7921
7972
|
async ensure(id2, opts = {}) {
|
|
7922
|
-
const { provider: _requested, ...rest } = opts;
|
|
7923
7973
|
const handle = computeHandle(id2);
|
|
7974
|
+
const run = (this.ensuring.get(handle) ?? Promise.resolve()).catch(() => {
|
|
7975
|
+
}).then(() => this.ensureOne(handle, id2, opts));
|
|
7976
|
+
this.ensuring.set(handle, run);
|
|
7977
|
+
try {
|
|
7978
|
+
return await run;
|
|
7979
|
+
} finally {
|
|
7980
|
+
if (this.ensuring.get(handle) === run) this.ensuring.delete(handle);
|
|
7981
|
+
}
|
|
7982
|
+
}
|
|
7983
|
+
async ensureOne(handle, id2, opts) {
|
|
7984
|
+
const { provider: _requested, ...rest } = opts;
|
|
7924
7985
|
const placed = await this.place(handle, opts);
|
|
7925
7986
|
this.remember(handle, placed.kind);
|
|
7926
7987
|
if (this.strays.get(handle) === placed.kind) this.strays.delete(handle);
|