@decocms/sandbox-controller 0.8.1 → 0.8.3
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 +92 -2
- 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>;
|
|
@@ -3807,8 +3817,21 @@ declare class FreestyleSandboxProvider implements SandboxProvider {
|
|
|
3807
3817
|
* VM anyway, only later.
|
|
3808
3818
|
*/
|
|
3809
3819
|
releaseAfter(handle: string, graceMs: number): Promise<void>;
|
|
3820
|
+
/**
|
|
3821
|
+
* `renewTtl` cancels only this process's timer, so a turn that started
|
|
3822
|
+
* through another replica still has to be seen: the daemon's clock counts
|
|
3823
|
+
* its requests and streaming runs. A daemon that won't say is left to the
|
|
3824
|
+
* VM's idle timeout.
|
|
3825
|
+
*/
|
|
3826
|
+
private pauseIfIdle;
|
|
3810
3827
|
/** The VM's idle timeout already counts every request as activity. */
|
|
3811
3828
|
renewTtl(handle: string): Promise<void>;
|
|
3829
|
+
/**
|
|
3830
|
+
* Pause every running VM of ours whose daemon reports `idlePauseMs` without
|
|
3831
|
+
* use. The daemon's clock counts requests and streaming runs, not the dev
|
|
3832
|
+
* server's own traffic. Returns the handles paused; never throws.
|
|
3833
|
+
*/
|
|
3834
|
+
pauseIdle(): Promise<string[]>;
|
|
3812
3835
|
private cancelRelease;
|
|
3813
3836
|
close(): void;
|
|
3814
3837
|
}
|
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.
|
|
6762
|
+
version: "1.89.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) {
|
|
@@ -7318,23 +7328,88 @@ running`).digest("hex").slice(0, 16)}`;
|
|
|
7318
7328
|
this.cancelRelease(handle);
|
|
7319
7329
|
const timer = setTimeout(() => {
|
|
7320
7330
|
this.releaseTimers.delete(handle);
|
|
7321
|
-
void this.
|
|
7331
|
+
void this.pauseIfIdle(handle, graceMs).catch(
|
|
7322
7332
|
(err) => console.warn(`[${LOG_LABEL2}] pause ${handle} failed: ${errMsg2(err)}`)
|
|
7323
7333
|
);
|
|
7324
7334
|
}, graceMs);
|
|
7325
7335
|
timer.unref?.();
|
|
7326
7336
|
this.releaseTimers.set(handle, timer);
|
|
7327
7337
|
}
|
|
7338
|
+
/**
|
|
7339
|
+
* `renewTtl` cancels only this process's timer, so a turn that started
|
|
7340
|
+
* through another replica still has to be seen: the daemon's clock counts
|
|
7341
|
+
* its requests and streaming runs. A daemon that won't say is left to the
|
|
7342
|
+
* VM's idle timeout.
|
|
7343
|
+
*/
|
|
7344
|
+
async pauseIfIdle(handle, graceMs) {
|
|
7345
|
+
const found = await this.find(handle);
|
|
7346
|
+
if (!found) return;
|
|
7347
|
+
const idleMs = await daemonIdleMs(found.daemon.url);
|
|
7348
|
+
if (idleMs === null || idleMs < graceMs) return;
|
|
7349
|
+
await found.vm.pause();
|
|
7350
|
+
}
|
|
7328
7351
|
/** The VM's idle timeout already counts every request as activity. */
|
|
7329
7352
|
async renewTtl(handle) {
|
|
7330
7353
|
this.cancelRelease(handle);
|
|
7331
7354
|
}
|
|
7355
|
+
/**
|
|
7356
|
+
* Pause every running VM of ours whose daemon reports `idlePauseMs` without
|
|
7357
|
+
* use. The daemon's clock counts requests and streaming runs, not the dev
|
|
7358
|
+
* server's own traffic. Returns the handles paused; never throws.
|
|
7359
|
+
*/
|
|
7360
|
+
async pauseIdle() {
|
|
7361
|
+
if (this.sweeping) return [];
|
|
7362
|
+
this.sweeping = true;
|
|
7363
|
+
try {
|
|
7364
|
+
const running = [];
|
|
7365
|
+
for (let offset = 0; ; offset += IDLE_SWEEP_PAGE) {
|
|
7366
|
+
const { vms } = await this.freestyle.vms.list({
|
|
7367
|
+
state: "running",
|
|
7368
|
+
limit: IDLE_SWEEP_PAGE,
|
|
7369
|
+
offset
|
|
7370
|
+
});
|
|
7371
|
+
running.push(...vms);
|
|
7372
|
+
if (vms.length < IDLE_SWEEP_PAGE) break;
|
|
7373
|
+
}
|
|
7374
|
+
const paused = await Promise.all(
|
|
7375
|
+
running.map(async (data) => {
|
|
7376
|
+
const handle = data.slug;
|
|
7377
|
+
if (!handle || !data.metadata[TOKEN_KEY]) return null;
|
|
7378
|
+
if (Date.now() - Date.parse(data.createdAt) < this.idlePauseMs) {
|
|
7379
|
+
return null;
|
|
7380
|
+
}
|
|
7381
|
+
const idleMs = await daemonIdleMs(this.daemonUrl(handle));
|
|
7382
|
+
if (idleMs === null || idleMs < this.idlePauseMs) return null;
|
|
7383
|
+
try {
|
|
7384
|
+
await this.freestyle.vms.ref(data.id).pause();
|
|
7385
|
+
} catch (err) {
|
|
7386
|
+
console.warn(
|
|
7387
|
+
`[${LOG_LABEL2}] pause ${handle} failed: ${errMsg2(err)}`
|
|
7388
|
+
);
|
|
7389
|
+
return null;
|
|
7390
|
+
}
|
|
7391
|
+
this.cancelRelease(handle);
|
|
7392
|
+
console.log(
|
|
7393
|
+
`[${LOG_LABEL2}] paused ${handle}, daemon idle ${Math.round(idleMs / 6e4)}m`
|
|
7394
|
+
);
|
|
7395
|
+
return handle;
|
|
7396
|
+
})
|
|
7397
|
+
);
|
|
7398
|
+
return paused.filter((h) => h !== null);
|
|
7399
|
+
} catch (err) {
|
|
7400
|
+
console.warn(`[${LOG_LABEL2}] idle sweep failed: ${errMsg2(err)}`);
|
|
7401
|
+
return [];
|
|
7402
|
+
} finally {
|
|
7403
|
+
this.sweeping = false;
|
|
7404
|
+
}
|
|
7405
|
+
}
|
|
7332
7406
|
cancelRelease(handle) {
|
|
7333
7407
|
const timer = this.releaseTimers.get(handle);
|
|
7334
7408
|
if (timer) clearTimeout(timer);
|
|
7335
7409
|
this.releaseTimers.delete(handle);
|
|
7336
7410
|
}
|
|
7337
7411
|
close() {
|
|
7412
|
+
if (this.idleSweep) clearInterval(this.idleSweep);
|
|
7338
7413
|
for (const timer of this.releaseTimers.values()) clearTimeout(timer);
|
|
7339
7414
|
this.releaseTimers.clear();
|
|
7340
7415
|
this.lookups.clear();
|
|
@@ -7351,6 +7426,21 @@ async function daemonAnswers(url) {
|
|
|
7351
7426
|
return false;
|
|
7352
7427
|
}
|
|
7353
7428
|
}
|
|
7429
|
+
async function daemonIdleMs(url) {
|
|
7430
|
+
try {
|
|
7431
|
+
const res = await fetch(`${url}/_sandbox/idle`, {
|
|
7432
|
+
signal: AbortSignal.timeout(HEALTH_PROBE_TIMEOUT_MS2)
|
|
7433
|
+
});
|
|
7434
|
+
if (!res.ok) {
|
|
7435
|
+
await res.body?.cancel();
|
|
7436
|
+
return null;
|
|
7437
|
+
}
|
|
7438
|
+
const body = await res.json();
|
|
7439
|
+
return typeof body === "object" && body !== null && "idleMs" in body && typeof body.idleMs === "number" ? body.idleMs : null;
|
|
7440
|
+
} catch {
|
|
7441
|
+
return null;
|
|
7442
|
+
}
|
|
7443
|
+
}
|
|
7354
7444
|
function shellQuote(s) {
|
|
7355
7445
|
return `'${s.replaceAll("'", `'\\''`)}'`;
|
|
7356
7446
|
}
|