@parcha/pi-durable-disk 0.0.0-stage → 0.1.0-beta.13
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/CHANGELOG.md +329 -0
- package/LICENSE +202 -0
- package/NOTICE +2 -0
- package/README.md +695 -2
- package/bin/archil-scoped +165 -0
- package/dist/app.d.ts +40 -0
- package/dist/app.js +47 -0
- package/dist/claim.d.ts +222 -0
- package/dist/claim.js +722 -0
- package/dist/cli.d.ts +64 -0
- package/dist/cli.js +664 -0
- package/dist/commands.d.ts +67 -0
- package/dist/commands.js +299 -0
- package/dist/confine.d.ts +91 -0
- package/dist/confine.js +341 -0
- package/dist/env.d.ts +129 -0
- package/dist/env.js +608 -0
- package/dist/errors.d.ts +47 -0
- package/dist/errors.js +55 -0
- package/dist/fork.d.ts +41 -0
- package/dist/fork.js +181 -0
- package/dist/hosts/daytona-launch.d.ts +52 -0
- package/dist/hosts/daytona-launch.js +270 -0
- package/dist/hosts/daytona.d.ts +147 -0
- package/dist/hosts/daytona.js +368 -0
- package/dist/hosts/docker.d.ts +103 -0
- package/dist/hosts/docker.js +362 -0
- package/dist/hosts/local-host.d.ts +71 -0
- package/dist/hosts/local-host.js +356 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.js +17 -0
- package/dist/lease.d.ts +7 -0
- package/dist/lease.js +6 -0
- package/dist/local-run.d.ts +10 -0
- package/dist/local-run.js +63 -0
- package/dist/park.d.ts +92 -0
- package/dist/park.js +229 -0
- package/dist/run.d.ts +290 -0
- package/dist/run.js +785 -0
- package/dist/serve.d.ts +101 -0
- package/dist/serve.js +393 -0
- package/dist/status.d.ts +123 -0
- package/dist/status.js +264 -0
- package/dist/store.d.ts +103 -0
- package/dist/store.js +217 -0
- package/dist/supervise.d.ts +340 -0
- package/dist/supervise.js +528 -0
- package/dist/watchdog.d.ts +39 -0
- package/dist/watchdog.js +137 -0
- package/package.json +87 -4
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
import type { Delegation } from "disk";
|
|
2
|
+
import { type ArchilHost, type ControlApi, type PathlessResolver, type RunRef } from "./claim.js";
|
|
3
|
+
import { PdaError } from "./errors.js";
|
|
4
|
+
import { type RunRecord } from "./status.js";
|
|
5
|
+
export type Json = null | boolean | number | string | Json[] | {
|
|
6
|
+
[key: string]: Json;
|
|
7
|
+
};
|
|
8
|
+
/** What a host driver reports for an instance it started. `unknown`: the handle is not this driver's to judge. */
|
|
9
|
+
export type HostStatus = "running" | "stopped" | "failed" | "gone" | "unknown";
|
|
10
|
+
/** A driver's description of one instance; the instance writes it into `run.json` as `holder`. */
|
|
11
|
+
export type HostHandle = {
|
|
12
|
+
driver: string;
|
|
13
|
+
[key: string]: Json;
|
|
14
|
+
};
|
|
15
|
+
/** Which start this is: the generation the instance will open (run.json's generation + 1), also in the token's nickname. */
|
|
16
|
+
export type StartAttempt = {
|
|
17
|
+
attempt: number;
|
|
18
|
+
};
|
|
19
|
+
/** Compute is disposable and pluggable: a driver is these three calls. */
|
|
20
|
+
export interface HostDriver {
|
|
21
|
+
/**
|
|
22
|
+
* Start an instance of the run that mounts with `mountToken` (a reusable token, minted for this attempt). A driver may
|
|
23
|
+
* key the start on `attempt`, so a retry of the same attempt finds the instance it already started; it then returns
|
|
24
|
+
* that instance's handle with `adopted: true`, and the supervisor removes the token it minted for this start, which no
|
|
25
|
+
* instance used.
|
|
26
|
+
*/
|
|
27
|
+
start(ref: RunRef, mountToken: string, attempt?: StartAttempt): Promise<HostHandle>;
|
|
28
|
+
status(handle: HostHandle): Promise<HostStatus>;
|
|
29
|
+
/** Stop the instance and whatever it left on its host. A handle the driver cannot reach is a no-op. */
|
|
30
|
+
stop(handle: HostHandle): Promise<void>;
|
|
31
|
+
}
|
|
32
|
+
/** The control-plane calls the supervisor uses; an archil `Disk` satisfies it. */
|
|
33
|
+
export interface SupervisorControl extends ControlApi {
|
|
34
|
+
getObject(key: string): Promise<Uint8Array>;
|
|
35
|
+
headObject(key: string): Promise<unknown | null>;
|
|
36
|
+
}
|
|
37
|
+
export declare const CONTROL_TIMEOUT_MS = 10000;
|
|
38
|
+
/** The same control API with every call bounded: one that does not settle in `ms` fails as CONTROL_API_FAILED. */
|
|
39
|
+
export declare function withTimeouts<T extends object>(control: T, ms: number): T;
|
|
40
|
+
export type SuperviseErrorCode = "CONTROL_API_FAILED" | "HOST_START_FAILED" | "RUN_TREE_NOT_DELETED";
|
|
41
|
+
/** A supervisor step failed; nothing was started. Exit 1; the next tick tries again. */
|
|
42
|
+
export declare class SuperviseError extends PdaError {
|
|
43
|
+
constructor(code: SuperviseErrorCode, message: string, options?: {
|
|
44
|
+
cause?: unknown;
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* `run.json` over S3 GetObject, strongly consistent once the writer fsynced the file and its directory. Absent: null.
|
|
49
|
+
* status.ts's strict reader: an unknown status or a malformed field is RunRecordError (RUN_JSON_INVALID), which fails
|
|
50
|
+
* this run's decision and nothing else.
|
|
51
|
+
*/
|
|
52
|
+
export declare function readRunStatus(control: Pick<SupervisorControl, "getObject">, id: string): Promise<RunRecord | null>;
|
|
53
|
+
export declare const LEASE_EXPIRY_MS = 90000;
|
|
54
|
+
export declare const STONITH_TIMEOUT_MS = 30000;
|
|
55
|
+
/** Each start attempt's mount token lives one day (reusable: a single-use token dies at the client's 5 min refresh). */
|
|
56
|
+
export declare const TOKEN_TTL = "24h";
|
|
57
|
+
/** A token user younger than this may belong to a start still in flight (minted, not yet mounted): never removed. */
|
|
58
|
+
export declare const TOKEN_GRACE_MS: number;
|
|
59
|
+
/** The longest start grace a run whose starts keep failing gets. */
|
|
60
|
+
export declare const START_BACKOFF_MAX_MS: number;
|
|
61
|
+
export interface EnsureOptions {
|
|
62
|
+
control: SupervisorControl;
|
|
63
|
+
/**
|
|
64
|
+
* Attributes delegations the control API lists without a path (`pathlessResolver`). `superviseRuns` shares one across
|
|
65
|
+
* its pass, so a pass costs at most one exec however many runs it decides; default: one per decision.
|
|
66
|
+
*/
|
|
67
|
+
pathless?: PathlessResolver;
|
|
68
|
+
/** A held, not orphaned delegation whose `heartbeatAt` is older than this is a lost instance. Default 90 s. */
|
|
69
|
+
leaseExpiryMs?: number;
|
|
70
|
+
/** Bound on the best-effort stop of a lost holder (status plus stop). Default 30 s. */
|
|
71
|
+
stonithTimeoutMs?: number;
|
|
72
|
+
/** TTL of each mount token (default 24 h). An instance whose token expires is fenced at the next 5 min refresh. */
|
|
73
|
+
tokenTtl?: string;
|
|
74
|
+
/** Prefix of each mount token's nickname. */
|
|
75
|
+
tokenPrefix?: string;
|
|
76
|
+
/** The caller has work for the run now: start a paused run, or a sleeping one before its `wakeAt`. */
|
|
77
|
+
demand?: boolean;
|
|
78
|
+
/** Create `runs/<id>/` with this owner when it does not exist and nothing holds it. */
|
|
79
|
+
create?: {
|
|
80
|
+
uid: number;
|
|
81
|
+
gid: number;
|
|
82
|
+
mode?: number;
|
|
83
|
+
};
|
|
84
|
+
/** Bound on every control API call; one that does not answer fails this run's decision only. Default 10 s. */
|
|
85
|
+
controlTimeoutMs?: number;
|
|
86
|
+
/**
|
|
87
|
+
* After a start, how long a tick leaves the run to the instance it started (no revoke, no second start) until that
|
|
88
|
+
* instance writes run.json at its generation. Default: the lease expiry. 0 turns the grace, and the backoff, off.
|
|
89
|
+
* A start that follows failed ones gets this doubled per failure, up to `startBackoffMaxMs`.
|
|
90
|
+
*/
|
|
91
|
+
startGraceMs?: number;
|
|
92
|
+
/** The cap on a backed-off start grace. Default 10 min. */
|
|
93
|
+
startBackoffMaxMs?: number;
|
|
94
|
+
/** Who starts, written into the start mark (`<host>:<pid>` by default). */
|
|
95
|
+
supervisorId?: string;
|
|
96
|
+
now?: () => number;
|
|
97
|
+
}
|
|
98
|
+
export type StonithOutcome = {
|
|
99
|
+
outcome: "no-holder";
|
|
100
|
+
}
|
|
101
|
+
/** The driver cannot reach the holder (another host, another driver) or its host is gone. */
|
|
102
|
+
| {
|
|
103
|
+
outcome: "unreachable";
|
|
104
|
+
status: "unknown" | "gone";
|
|
105
|
+
}
|
|
106
|
+
/** Stopped, and whatever it left on its host cleaned; `status` is what the driver reported first. */
|
|
107
|
+
| {
|
|
108
|
+
outcome: "stopped";
|
|
109
|
+
status: HostStatus;
|
|
110
|
+
ms: number;
|
|
111
|
+
} | {
|
|
112
|
+
outcome: "timed-out";
|
|
113
|
+
ms: number;
|
|
114
|
+
} | {
|
|
115
|
+
outcome: "failed";
|
|
116
|
+
ms: number;
|
|
117
|
+
error: string;
|
|
118
|
+
};
|
|
119
|
+
export type StartReason = "none" | "orphaned" | "lease-expired";
|
|
120
|
+
export type EnsureResult =
|
|
121
|
+
/** `detail` is the instance's own word on why, e.g. `{ code: "STORE_BEHIND_SEAL", sealedSeq, head }`. */
|
|
122
|
+
{
|
|
123
|
+
action: "terminal";
|
|
124
|
+
status: "done" | "failed";
|
|
125
|
+
generation: number;
|
|
126
|
+
detail: Json;
|
|
127
|
+
} | {
|
|
128
|
+
action: "paused";
|
|
129
|
+
generation: number;
|
|
130
|
+
} | {
|
|
131
|
+
action: "sleeping";
|
|
132
|
+
wakeAt: string | null;
|
|
133
|
+
generation: number;
|
|
134
|
+
} | {
|
|
135
|
+
action: "pending";
|
|
136
|
+
delegations: number;
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* An instance this or another supervisor started is inside its start grace (it has not written run.json at its
|
|
140
|
+
* generation): nothing is revoked or started. `delegations` 0: not mounted yet. `failures` > 0: the start follows
|
|
141
|
+
* that many failed ones, and `graceMs` is its backed-off grace.
|
|
142
|
+
*/
|
|
143
|
+
| {
|
|
144
|
+
action: "starting";
|
|
145
|
+
generation: number;
|
|
146
|
+
sinceMs: number;
|
|
147
|
+
graceMs: number;
|
|
148
|
+
failures: number;
|
|
149
|
+
lastExit: string | null;
|
|
150
|
+
by: string | null;
|
|
151
|
+
delegations: number;
|
|
152
|
+
} | {
|
|
153
|
+
action: "healthy";
|
|
154
|
+
heartbeatAt: string;
|
|
155
|
+
ageMs: number;
|
|
156
|
+
generation: number;
|
|
157
|
+
} | {
|
|
158
|
+
action: "started";
|
|
159
|
+
reason: StartReason;
|
|
160
|
+
/** The run was sleeping and its `wakeAt` was due (or the caller demanded it). */
|
|
161
|
+
woke: boolean;
|
|
162
|
+
revoked: Pick<Delegation, "clientId" | "inodeId" | "path" | "isOrphaned">[];
|
|
163
|
+
stonith?: StonithOutcome;
|
|
164
|
+
/** The run directory did not exist and was created with `create`'s owner. */
|
|
165
|
+
created: boolean;
|
|
166
|
+
handle: HostHandle;
|
|
167
|
+
/** The token user minted for this start; with `adopted` it was not used and is already removed. */
|
|
168
|
+
token: {
|
|
169
|
+
identifier: string;
|
|
170
|
+
nickname: string;
|
|
171
|
+
};
|
|
172
|
+
/** The driver found the instance an earlier start of this attempt made (another supervisor, a retried call). */
|
|
173
|
+
adopted?: true;
|
|
174
|
+
/** Whether the start mark (runs/<id>/start.json) was written; without it the next tick has no start grace. */
|
|
175
|
+
startMark: boolean;
|
|
176
|
+
/** Failed starts right before this one (0 after a start that wrote run.json), and what was seen of the last one. */
|
|
177
|
+
failures: number;
|
|
178
|
+
lastExit: string | null;
|
|
179
|
+
/** The grace this start gets: the base, doubled per failure, capped. */
|
|
180
|
+
graceMs: number;
|
|
181
|
+
/** Milliseconds from after the revoke (or the decision, when nothing was revoked) to the driver's return. */
|
|
182
|
+
startMs: number;
|
|
183
|
+
};
|
|
184
|
+
/**
|
|
185
|
+
* Make sure the run has a live instance, or has no business having one:
|
|
186
|
+
* - `run.json` done or failed (a store behind its seal is failed, never restarted into): nothing. Paused, or sleeping
|
|
187
|
+
* with `wakeAt` later or null: nothing unless `demand`.
|
|
188
|
+
* - No delegation on `runs/<id>`: start. Every delegation orphaned (the holder's client is gone): revoke, start.
|
|
189
|
+
* - A delegation still checking out (`isPending`): someone is mounting; nothing.
|
|
190
|
+
* - Held and the lease fresh: healthy. Held and the lease expired (or no heartbeat at all): stop the holder if the
|
|
191
|
+
* driver can reach it (bounded), revoke, start. Revoking a live holder is safe: its next fsync fails.
|
|
192
|
+
* - The start grace: every start first writes `runs/<id>/start.json` (the generation the new instance will write, and
|
|
193
|
+
* when). While run.json is below that generation and the mark is younger than `startGraceMs`, a run with no
|
|
194
|
+
* delegation or with a lease that looks expired is `starting`: nothing is revoked or started, so a tick between a
|
|
195
|
+
* start and the instance's first run.json write never fences the instance it started. Past the grace, the start has
|
|
196
|
+
* failed and the rules above apply. Every delegation orphaned inside the grace waits too: the start's client is gone,
|
|
197
|
+
* which is a failed start like any other.
|
|
198
|
+
* - The backoff: a start whose generation run.json never reached failed (it ended before its first run.json write, 76
|
|
199
|
+
* or on MOUNT_FAILED, or ran out its grace; 65 and 70 mark run.json failed, which is terminal). The next start counts
|
|
200
|
+
* it in its mark (`failures`, `lastExit`) and gets the base grace doubled per consecutive failure, capped by
|
|
201
|
+
* `startBackoffMaxMs`, so a run whose starts keep failing is started a handful of times an hour, not once a tick. A
|
|
202
|
+
* start that writes run.json at its generation spends its mark, and the next start counts from 0.
|
|
203
|
+
* No wait follows a revoke: the fence is Archil's, enforced by the server at the old holder's next fsync. Two racing
|
|
204
|
+
* supervisors are safe: the mount admits one claimant and the other instance exits 76.
|
|
205
|
+
*/
|
|
206
|
+
export declare function ensureRunning(ref: RunRef, host: HostDriver, options: EnsureOptions): Promise<EnsureResult>;
|
|
207
|
+
/**
|
|
208
|
+
* The supervisor's record of its last start of a run: the generation the instance will write, when, by whom, and the
|
|
209
|
+
* failed starts right before it (`failures`; `lastExit`, what the decision saw of the last one).
|
|
210
|
+
*/
|
|
211
|
+
export declare const START_MARK = "start.json";
|
|
212
|
+
export type StartMark = {
|
|
213
|
+
generation: number;
|
|
214
|
+
at: string;
|
|
215
|
+
by: string | null;
|
|
216
|
+
failures: number;
|
|
217
|
+
lastExit: string | null;
|
|
218
|
+
};
|
|
219
|
+
/** The grace of a start that follows `failures` failed ones: the base doubled per failure, capped, never below the base. */
|
|
220
|
+
export declare function startGrace(baseMs: number, failures: number, maxMs?: number): number;
|
|
221
|
+
/**
|
|
222
|
+
* The start mark over S3, or null when absent or unreadable (an unreadable mark gives no grace). It lives in the run's
|
|
223
|
+
* directory, so it goes with the run (deleteRunTree removes it), and a later start overwrites it; a mark whose
|
|
224
|
+
* generation run.json has reached is spent.
|
|
225
|
+
*/
|
|
226
|
+
export declare function readStartMark(control: Pick<SupervisorControl, "getObject">, id: string): Promise<StartMark | null>;
|
|
227
|
+
/** One pass over many runs: each decision on its own, so a run whose calls fail or time out costs only its own line. */
|
|
228
|
+
export declare function superviseRuns(refs: RunRef[], host: HostDriver, opts: EnsureOptions): Promise<({
|
|
229
|
+
run: string;
|
|
230
|
+
ms: number;
|
|
231
|
+
} & (EnsureResult | {
|
|
232
|
+
action: "error";
|
|
233
|
+
error: string;
|
|
234
|
+
message: string;
|
|
235
|
+
cause?: string;
|
|
236
|
+
}))[]>;
|
|
237
|
+
export type TokenUser = {
|
|
238
|
+
identifier?: string;
|
|
239
|
+
nickname?: string;
|
|
240
|
+
status?: string;
|
|
241
|
+
expiresAt?: string;
|
|
242
|
+
createdAt?: string;
|
|
243
|
+
};
|
|
244
|
+
export interface TokenSweepOptions {
|
|
245
|
+
/** The disk's token users (`getDisk(id).authorizedUsers`; the list lags by seconds). */
|
|
246
|
+
listUsers(): Promise<TokenUser[]>;
|
|
247
|
+
control: Pick<SupervisorControl, "getObject" | "listDelegations" | "removeUser" | "exec">;
|
|
248
|
+
/** Only token users whose nickname starts with this (the supervisor's `tokenPrefix`). */
|
|
249
|
+
prefix: string;
|
|
250
|
+
/** The runs this pass may clean; undefined: every run a token nickname names. */
|
|
251
|
+
runs?: readonly string[];
|
|
252
|
+
/** Also remove expired token users of any run under the prefix (`supervise --sweep-tokens`). */
|
|
253
|
+
expired?: boolean;
|
|
254
|
+
/** Users younger than this are never removed (a start may be in flight). Default 15 min. */
|
|
255
|
+
graceMs?: number;
|
|
256
|
+
now?: () => number;
|
|
257
|
+
}
|
|
258
|
+
export type TokenSweep = {
|
|
259
|
+
removed: {
|
|
260
|
+
identifier: string;
|
|
261
|
+
run: string | null;
|
|
262
|
+
why: "released" | "expired";
|
|
263
|
+
}[];
|
|
264
|
+
failed: {
|
|
265
|
+
identifier: string;
|
|
266
|
+
error: string;
|
|
267
|
+
}[];
|
|
268
|
+
};
|
|
269
|
+
/**
|
|
270
|
+
* Remove mount-token users that no live mount needs. Removing a token user under a live mount makes that mount lose its
|
|
271
|
+
* claim at the client's next re-authentication, so a run's users go only when the run holds no delegation, its
|
|
272
|
+
* run.json says it is released (paused, sleeping, done, failed; never running, absent or unreadable), and the user is
|
|
273
|
+
* older than `graceMs` (a start mints before it mounts). With `expired`, users whose TTL has passed go too: an expired
|
|
274
|
+
* token already fences its mount at the next refresh.
|
|
275
|
+
*/
|
|
276
|
+
export declare function sweepTokens(opts: TokenSweepOptions): Promise<TokenSweep>;
|
|
277
|
+
export interface CheckControl extends ControlApi {
|
|
278
|
+
listObjects(prefix: string, options?: {
|
|
279
|
+
recursive?: boolean;
|
|
280
|
+
}): Promise<{
|
|
281
|
+
objects: {
|
|
282
|
+
key: string;
|
|
283
|
+
}[];
|
|
284
|
+
commonPrefixes: string[];
|
|
285
|
+
}>;
|
|
286
|
+
deleteObjects(keys: string[], options?: {
|
|
287
|
+
quiet?: boolean;
|
|
288
|
+
}): Promise<{
|
|
289
|
+
errors: unknown[];
|
|
290
|
+
}>;
|
|
291
|
+
}
|
|
292
|
+
export interface CheckOptions {
|
|
293
|
+
control: CheckControl;
|
|
294
|
+
disk: string;
|
|
295
|
+
region: string;
|
|
296
|
+
/** Where the probe's two mounts go: `<mountRoot>/runs/<probe>` and `<mountRoot>/.check-b/runs/<probe>`. */
|
|
297
|
+
mountRoot: string;
|
|
298
|
+
/** Owner of the probe directory (the instance's user). */
|
|
299
|
+
owner: {
|
|
300
|
+
uid: number;
|
|
301
|
+
gid: number;
|
|
302
|
+
};
|
|
303
|
+
host?: ArchilHost;
|
|
304
|
+
idPrefix?: string;
|
|
305
|
+
tokenPrefix?: string;
|
|
306
|
+
/** Bound on every control API call. Default 10 s. */
|
|
307
|
+
controlTimeoutMs?: number;
|
|
308
|
+
/** Called with each token user minted and each probe directory created, so a caller can keep a ledger. */
|
|
309
|
+
onResource?: (kind: "token" | "token-removed" | "subdir" | "subdir-deleted" | "mount" | "unmount", id: string, detail?: string) => void;
|
|
310
|
+
}
|
|
311
|
+
export type CheckStep = {
|
|
312
|
+
step: string;
|
|
313
|
+
ok: boolean;
|
|
314
|
+
ms: number;
|
|
315
|
+
detail?: string;
|
|
316
|
+
};
|
|
317
|
+
export type CheckReport = {
|
|
318
|
+
ok: boolean;
|
|
319
|
+
host: string;
|
|
320
|
+
run: string;
|
|
321
|
+
steps: CheckStep[];
|
|
322
|
+
cleanup: string[];
|
|
323
|
+
};
|
|
324
|
+
/**
|
|
325
|
+
* Prove on this host and client version that the claim's two server-side guarantees hold before serving runs: a second
|
|
326
|
+
* exclusive mount of a probe directory is refused while the first holds it, and a revoked mount's fsync fails (and its
|
|
327
|
+
* barrier fences). Then a third mount takes over. Every token, mount and the probe directory are removed.
|
|
328
|
+
*/
|
|
329
|
+
export declare function checkHost(options: CheckOptions): Promise<CheckReport>;
|
|
330
|
+
/**
|
|
331
|
+
* Delete a run's directory tree over S3. A tree with a delegation on it, even an orphaned one, is not deleted: S3
|
|
332
|
+
* DeleteObjects leaves its objects in place and reports no error, and the run id would later reopen with its old
|
|
333
|
+
* store. So: revoke every delegation on the subtree and its holders' private directories, delete the files, then the
|
|
334
|
+
* directory markers deepest first (a directory with children refuses), then list the prefix. Anything left is
|
|
335
|
+
* RUN_TREE_NOT_DELETED; a non-empty prefix is never reported as deleted.
|
|
336
|
+
*/
|
|
337
|
+
export declare function deleteRunTree(control: CheckControl, id: string): Promise<{
|
|
338
|
+
objects: number;
|
|
339
|
+
revoked: number;
|
|
340
|
+
}>;
|