@decocms/sandbox-controller 0.8.1 → 0.8.2
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 +16 -0
- package/dist/index.js +78 -1
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -3717,6 +3717,13 @@ interface FreestyleSandboxProviderOptions {
|
|
|
3717
3717
|
orgFsSidecarImage?: string;
|
|
3718
3718
|
/** Pause a VM after this long without network activity. Default 5 min. */
|
|
3719
3719
|
idleTimeoutSeconds?: number;
|
|
3720
|
+
/**
|
|
3721
|
+
* Also pause a running VM once its daemon has gone this long unused. The
|
|
3722
|
+
* idle timeout counts any traffic, so a dev server holding an outbound
|
|
3723
|
+
* connection open keeps the VM awake without it. Checked every minute;
|
|
3724
|
+
* 0 turns it off. Default 10 min.
|
|
3725
|
+
*/
|
|
3726
|
+
idlePauseMs?: number;
|
|
3720
3727
|
/**
|
|
3721
3728
|
* Delete a VM after this long without running. Default 1 day. A plan
|
|
3722
3729
|
* that caps it lower gets its cap.
|
|
@@ -3736,6 +3743,7 @@ declare class FreestyleSandboxProvider implements SandboxProvider {
|
|
|
3736
3743
|
private readonly image;
|
|
3737
3744
|
private readonly sidecarImage;
|
|
3738
3745
|
private readonly idleTimeoutSeconds;
|
|
3746
|
+
private readonly idlePauseMs;
|
|
3739
3747
|
private readonly autoDeleteSeconds;
|
|
3740
3748
|
private readonly domainSuffix;
|
|
3741
3749
|
private readonly timeDaemonRequest;
|
|
@@ -3745,6 +3753,8 @@ declare class FreestyleSandboxProvider implements SandboxProvider {
|
|
|
3745
3753
|
private readonly knownSnapshots;
|
|
3746
3754
|
private readonly lookups;
|
|
3747
3755
|
private readonly releaseTimers;
|
|
3756
|
+
private readonly idleSweep;
|
|
3757
|
+
private sweeping;
|
|
3748
3758
|
constructor(opts: FreestyleSandboxProviderOptions);
|
|
3749
3759
|
/** Whether this handle is a Freestyle sandbox. Cached; see `MISS_TTL_MS`. */
|
|
3750
3760
|
owns(handle: string): Promise<boolean>;
|
|
@@ -3809,6 +3819,12 @@ declare class FreestyleSandboxProvider implements SandboxProvider {
|
|
|
3809
3819
|
releaseAfter(handle: string, graceMs: number): Promise<void>;
|
|
3810
3820
|
/** The VM's idle timeout already counts every request as activity. */
|
|
3811
3821
|
renewTtl(handle: string): Promise<void>;
|
|
3822
|
+
/**
|
|
3823
|
+
* Pause every running VM of ours whose daemon reports `idlePauseMs` without
|
|
3824
|
+
* use. The daemon's clock counts requests and streaming runs, not the dev
|
|
3825
|
+
* server's own traffic. Returns the handles paused; never throws.
|
|
3826
|
+
*/
|
|
3827
|
+
pauseIdle(): Promise<string[]>;
|
|
3812
3828
|
private cancelRelease;
|
|
3813
3829
|
close(): void;
|
|
3814
3830
|
}
|
package/dist/index.js
CHANGED
|
@@ -6759,7 +6759,7 @@ var Freestyle = class {
|
|
|
6759
6759
|
// ../sandbox/package.json
|
|
6760
6760
|
var package_default = {
|
|
6761
6761
|
name: "@decocms/sandbox",
|
|
6762
|
-
version: "1.85.
|
|
6762
|
+
version: "1.85.2",
|
|
6763
6763
|
private: true,
|
|
6764
6764
|
type: "module",
|
|
6765
6765
|
description: "Sandbox runtime for isolated Studio tool execution",
|
|
@@ -6861,6 +6861,8 @@ var MISS_TTL_MS = 1e4;
|
|
|
6861
6861
|
var LOOKUP_MAX = 5e3;
|
|
6862
6862
|
var WATCH_TIMEOUT_MS = 5 * 6e4;
|
|
6863
6863
|
var LOOKUP_TIMEOUT_MS = 3e3;
|
|
6864
|
+
var IDLE_SWEEP_MS = 6e4;
|
|
6865
|
+
var IDLE_SWEEP_PAGE = 100;
|
|
6864
6866
|
var FreestyleSandboxProvider = class {
|
|
6865
6867
|
apiKey;
|
|
6866
6868
|
freestyle;
|
|
@@ -6868,6 +6870,7 @@ var FreestyleSandboxProvider = class {
|
|
|
6868
6870
|
image;
|
|
6869
6871
|
sidecarImage;
|
|
6870
6872
|
idleTimeoutSeconds;
|
|
6873
|
+
idlePauseMs;
|
|
6871
6874
|
autoDeleteSeconds;
|
|
6872
6875
|
domainSuffix;
|
|
6873
6876
|
timeDaemonRequest;
|
|
@@ -6877,6 +6880,8 @@ var FreestyleSandboxProvider = class {
|
|
|
6877
6880
|
knownSnapshots = /* @__PURE__ */ new Set();
|
|
6878
6881
|
lookups = /* @__PURE__ */ new Map();
|
|
6879
6882
|
releaseTimers = /* @__PURE__ */ new Map();
|
|
6883
|
+
idleSweep = null;
|
|
6884
|
+
sweeping = false;
|
|
6880
6885
|
constructor(opts) {
|
|
6881
6886
|
this.apiKey = opts.apiKey;
|
|
6882
6887
|
this.freestyle = opts.client ?? new Freestyle({ apiKey: opts.apiKey });
|
|
@@ -6896,9 +6901,14 @@ var FreestyleSandboxProvider = class {
|
|
|
6896
6901
|
this.image = opts.image ?? `ghcr.io/decocms/studio/studio-sandbox-go:${package_default.version}`;
|
|
6897
6902
|
this.sidecarImage = opts.orgFsSidecarImage ?? DEFAULT_SIDECAR_IMAGE;
|
|
6898
6903
|
this.idleTimeoutSeconds = opts.idleTimeoutSeconds ?? 5 * 60;
|
|
6904
|
+
this.idlePauseMs = opts.idlePauseMs ?? 10 * 6e4;
|
|
6899
6905
|
this.autoDeleteSeconds = opts.autoDeleteSeconds ?? 24 * 60 * 60;
|
|
6900
6906
|
this.domainSuffix = opts.domainSuffix ?? "style.dev";
|
|
6901
6907
|
this.timeDaemonRequest = daemonProxyTimer(opts.meter);
|
|
6908
|
+
if (this.idlePauseMs > 0) {
|
|
6909
|
+
this.idleSweep = setInterval(() => void this.pauseIdle(), IDLE_SWEEP_MS);
|
|
6910
|
+
this.idleSweep.unref?.();
|
|
6911
|
+
}
|
|
6902
6912
|
}
|
|
6903
6913
|
/** Whether this handle is a Freestyle sandbox. Cached; see `MISS_TTL_MS`. */
|
|
6904
6914
|
async owns(handle) {
|
|
@@ -7329,12 +7339,64 @@ running`).digest("hex").slice(0, 16)}`;
|
|
|
7329
7339
|
async renewTtl(handle) {
|
|
7330
7340
|
this.cancelRelease(handle);
|
|
7331
7341
|
}
|
|
7342
|
+
/**
|
|
7343
|
+
* Pause every running VM of ours whose daemon reports `idlePauseMs` without
|
|
7344
|
+
* use. The daemon's clock counts requests and streaming runs, not the dev
|
|
7345
|
+
* server's own traffic. Returns the handles paused; never throws.
|
|
7346
|
+
*/
|
|
7347
|
+
async pauseIdle() {
|
|
7348
|
+
if (this.sweeping) return [];
|
|
7349
|
+
this.sweeping = true;
|
|
7350
|
+
try {
|
|
7351
|
+
const running = [];
|
|
7352
|
+
for (let offset = 0; ; offset += IDLE_SWEEP_PAGE) {
|
|
7353
|
+
const { vms } = await this.freestyle.vms.list({
|
|
7354
|
+
state: "running",
|
|
7355
|
+
limit: IDLE_SWEEP_PAGE,
|
|
7356
|
+
offset
|
|
7357
|
+
});
|
|
7358
|
+
running.push(...vms);
|
|
7359
|
+
if (vms.length < IDLE_SWEEP_PAGE) break;
|
|
7360
|
+
}
|
|
7361
|
+
const paused = await Promise.all(
|
|
7362
|
+
running.map(async (data) => {
|
|
7363
|
+
const handle = data.slug;
|
|
7364
|
+
if (!handle || !data.metadata[TOKEN_KEY]) return null;
|
|
7365
|
+
if (Date.now() - Date.parse(data.createdAt) < this.idlePauseMs) {
|
|
7366
|
+
return null;
|
|
7367
|
+
}
|
|
7368
|
+
const idleMs = await daemonIdleMs(this.daemonUrl(handle));
|
|
7369
|
+
if (idleMs === null || idleMs < this.idlePauseMs) return null;
|
|
7370
|
+
try {
|
|
7371
|
+
await this.freestyle.vms.ref(data.id).pause();
|
|
7372
|
+
} catch (err) {
|
|
7373
|
+
console.warn(
|
|
7374
|
+
`[${LOG_LABEL2}] pause ${handle} failed: ${errMsg2(err)}`
|
|
7375
|
+
);
|
|
7376
|
+
return null;
|
|
7377
|
+
}
|
|
7378
|
+
this.cancelRelease(handle);
|
|
7379
|
+
console.log(
|
|
7380
|
+
`[${LOG_LABEL2}] paused ${handle}, daemon idle ${Math.round(idleMs / 6e4)}m`
|
|
7381
|
+
);
|
|
7382
|
+
return handle;
|
|
7383
|
+
})
|
|
7384
|
+
);
|
|
7385
|
+
return paused.filter((h) => h !== null);
|
|
7386
|
+
} catch (err) {
|
|
7387
|
+
console.warn(`[${LOG_LABEL2}] idle sweep failed: ${errMsg2(err)}`);
|
|
7388
|
+
return [];
|
|
7389
|
+
} finally {
|
|
7390
|
+
this.sweeping = false;
|
|
7391
|
+
}
|
|
7392
|
+
}
|
|
7332
7393
|
cancelRelease(handle) {
|
|
7333
7394
|
const timer = this.releaseTimers.get(handle);
|
|
7334
7395
|
if (timer) clearTimeout(timer);
|
|
7335
7396
|
this.releaseTimers.delete(handle);
|
|
7336
7397
|
}
|
|
7337
7398
|
close() {
|
|
7399
|
+
if (this.idleSweep) clearInterval(this.idleSweep);
|
|
7338
7400
|
for (const timer of this.releaseTimers.values()) clearTimeout(timer);
|
|
7339
7401
|
this.releaseTimers.clear();
|
|
7340
7402
|
this.lookups.clear();
|
|
@@ -7351,6 +7413,21 @@ async function daemonAnswers(url) {
|
|
|
7351
7413
|
return false;
|
|
7352
7414
|
}
|
|
7353
7415
|
}
|
|
7416
|
+
async function daemonIdleMs(url) {
|
|
7417
|
+
try {
|
|
7418
|
+
const res = await fetch(`${url}/_sandbox/idle`, {
|
|
7419
|
+
signal: AbortSignal.timeout(HEALTH_PROBE_TIMEOUT_MS2)
|
|
7420
|
+
});
|
|
7421
|
+
if (!res.ok) {
|
|
7422
|
+
await res.body?.cancel();
|
|
7423
|
+
return null;
|
|
7424
|
+
}
|
|
7425
|
+
const body = await res.json();
|
|
7426
|
+
return typeof body === "object" && body !== null && "idleMs" in body && typeof body.idleMs === "number" ? body.idleMs : null;
|
|
7427
|
+
} catch {
|
|
7428
|
+
return null;
|
|
7429
|
+
}
|
|
7430
|
+
}
|
|
7354
7431
|
function shellQuote(s) {
|
|
7355
7432
|
return `'${s.replaceAll("'", `'\\''`)}'`;
|
|
7356
7433
|
}
|