feat(clone): background clone jobs that survive a reload and a server restart
Both clone POSTs take background: true: every check answers synchronously,
then 202 {job}. git runs in a detached runner (scripts/clone-runner.js, its
own process group, stderr to a file — no SIGPIPE when the server restarts);
the server tails it for progress and runs the after-step (workspace / extra
folder) once when it exits. Jobs persist in <data dir>/runtime/clone-jobs.json
(0600, atomic, throttled progress writes); on start a live runner is adopted
(counted against the clone limit), an ended one finished, a queued one
requeued (interrupted if it carried a typed login), the rest interrupted.
GET /clone/jobs, DELETE /clone/jobs/:id, POST /clone/jobs/:id/cancel (queued:
dequeued; running: SIGTERM to the runner's group -> cancelled, no after-step)
and /retry (new job from the stored request, never a credential). The
held-open path is unchanged; worktree.gitCommand builds the runner's argv
and git-identity.cloneFailure the shared error classification.
This commit is contained in:
@@ -9,6 +9,8 @@
|
||||
* least 1). A clone the browser named with a token (`track`) reports its
|
||||
* place in the queue, then git's own `--progress` lines (`report`), to
|
||||
* `GET /api/lanes/clone/progress/:token` — only to the user who started it.
|
||||
* A background clone (`server/lib/clone-jobs.js`) passes its own job record,
|
||||
* whose `onChange` hears each state change and progress line.
|
||||
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
||||
*/
|
||||
|
||||
@@ -46,15 +48,18 @@ function next() {
|
||||
while (running < concurrency() && waiting.length) {
|
||||
running++;
|
||||
const { start, job } = waiting.shift();
|
||||
if (job) job.state = "running";
|
||||
if (job) {
|
||||
job.state = "running";
|
||||
job.onChange?.("state");
|
||||
}
|
||||
start();
|
||||
}
|
||||
}
|
||||
|
||||
/** Runs `fn` once a clone slot is free; resolves or rejects with it. */
|
||||
async function withCloneSlot(fn, job = null) {
|
||||
await new Promise((start) => {
|
||||
waiting.push({ start, job });
|
||||
await new Promise((start, reject) => {
|
||||
waiting.push({ start, reject, job });
|
||||
next();
|
||||
});
|
||||
try {
|
||||
@@ -136,6 +141,7 @@ function report(job, chunk) {
|
||||
.replace(/,\s*done\.?$/, "")
|
||||
.replace(/^,\s*/, "")
|
||||
.trim() || null;
|
||||
job.onChange?.("progress");
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -149,10 +155,9 @@ function progress(token, owner) {
|
||||
sweep();
|
||||
const job = jobs.get(token);
|
||||
if (!job || job.owner !== owner) return null;
|
||||
const position = waiting.findIndex((w) => w.job === job);
|
||||
return {
|
||||
state: job.state,
|
||||
position: position >= 0 ? position + 1 : null,
|
||||
position: position(job),
|
||||
...cloneQueueState(),
|
||||
phase: job.phase,
|
||||
label: job.label,
|
||||
@@ -161,6 +166,40 @@ function progress(token, owner) {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Counts a clone this process did not start — a background clone's runner
|
||||
* adopted after a restart (clone-jobs.js) — against the limit, at once.
|
||||
* Returns its release.
|
||||
*/
|
||||
function occupy() {
|
||||
running++;
|
||||
let released = false;
|
||||
return () => {
|
||||
if (released) return;
|
||||
released = true;
|
||||
running--;
|
||||
next();
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes a still-waiting clone out of the queue: its `withCloneSlot` rejects
|
||||
* with `ECANCELLED` and it never starts. False when it is not waiting.
|
||||
*/
|
||||
function dequeue(job) {
|
||||
const i = waiting.findIndex((w) => w.job === job);
|
||||
if (i < 0) return false;
|
||||
const [w] = waiting.splice(i, 1);
|
||||
w.reject(Object.assign(new Error("Clone cancelled"), { code: "ECANCELLED" }));
|
||||
return true;
|
||||
}
|
||||
|
||||
/** A queued clone's 1-based place among the waiting ones, else null. */
|
||||
function position(job) {
|
||||
const i = waiting.findIndex((w) => w.job === job);
|
||||
return i >= 0 ? i + 1 : null;
|
||||
}
|
||||
|
||||
/** A progress-free tail of git's stderr for an error message. */
|
||||
function stripProgress(stderr) {
|
||||
return String(stderr || "")
|
||||
@@ -178,5 +217,8 @@ module.exports = {
|
||||
finish,
|
||||
report,
|
||||
progress,
|
||||
position,
|
||||
dequeue,
|
||||
occupy,
|
||||
stripProgress,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user