omp-conductor 0.7.1 → 0.8.0
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/README.md +16 -10
- package/package.json +1 -1
- package/src/briefs/orchestrator.md +13 -2
- package/src/cli.ts +73 -24
- package/src/config.ts +11 -0
- package/src/daemon.ts +73 -8
- package/src/decisions.ts +67 -7
- package/src/fleet.ts +39 -6
- package/src/lifecycle.ts +8 -1
- package/src/orchestrator-tick.ts +150 -0
- package/src/plugin.ts +1 -1
- package/src/setup.ts +1 -1
- package/src/tracker/github.ts +234 -73
- package/src/types.ts +10 -0
- package/src/unblock.ts +98 -13
- package/src/upgrade.ts +29 -3
- package/src/worktree.ts +23 -4
package/src/upgrade.ts
CHANGED
|
@@ -68,7 +68,7 @@ async function runCommand(command: string, args: readonly string[]): Promise<Upg
|
|
|
68
68
|
}
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
-
const DEFAULT_DEPS: UpgradeDeps = {
|
|
71
|
+
export const DEFAULT_DEPS: UpgradeDeps = {
|
|
72
72
|
run: runCommand,
|
|
73
73
|
snapshot: statusSnapshot,
|
|
74
74
|
layers: fleetLayers,
|
|
@@ -84,7 +84,7 @@ const DEFAULT_DEPS: UpgradeDeps = {
|
|
|
84
84
|
const daemon = livingDaemon();
|
|
85
85
|
return daemon === undefined ? { running: false } : { running: true, project: daemon.project };
|
|
86
86
|
},
|
|
87
|
-
setPaused,
|
|
87
|
+
setPaused: (v) => setPaused(v, { source: "upgrade", reason: "upgrade, draining" }),
|
|
88
88
|
restartDaemon: async () => {
|
|
89
89
|
await restartDaemon({});
|
|
90
90
|
},
|
|
@@ -213,17 +213,43 @@ function previousHerdrInstall(source: string): readonly [string, readonly string
|
|
|
213
213
|
];
|
|
214
214
|
}
|
|
215
215
|
|
|
216
|
-
async function waitForDrain(deps: UpgradeDeps, project?: string): Promise<void> {
|
|
216
|
+
async function waitForDrain(deps: UpgradeDeps, project?: string, deadlineAt?: number): Promise<void> {
|
|
217
217
|
let last = -1;
|
|
218
218
|
while (true) {
|
|
219
219
|
const workers = deps.snapshot(project).liveWorkers;
|
|
220
220
|
if (workers === 0) return;
|
|
221
|
+
if (deadlineAt !== undefined && Date.now() >= deadlineAt) {
|
|
222
|
+
throw new Error(
|
|
223
|
+
`drain timed out with ${workers} live worker(s) still running — nothing was restarted; dispatch remains paused (omp-conductor resume to lift it, or re-run restart to keep waiting)`,
|
|
224
|
+
);
|
|
225
|
+
}
|
|
221
226
|
if (workers !== last) deps.log(`waiting for ${workers} live worker(s) to finish`);
|
|
222
227
|
last = workers;
|
|
223
228
|
await deps.sleep(DRAIN_POLL_MS);
|
|
224
229
|
}
|
|
225
230
|
}
|
|
226
231
|
|
|
232
|
+
/**
|
|
233
|
+
* Pause, drain, restart, restore — the trusted restart transaction, minus the
|
|
234
|
+
* install/verify steps of {@link upgradeConductor}.
|
|
235
|
+
*
|
|
236
|
+
* Mirrors the upgrade's fail-closed posture: on ANY throw after the pause the
|
|
237
|
+
* fleet stays paused (no resume in a catch) and the error rethrows, so a
|
|
238
|
+
* systemd-owned restart that fails or a drain that outlives `timeoutMs` cannot
|
|
239
|
+
* silently resume dispatch over a wedged daemon. The caller surfaces the message
|
|
240
|
+
* and exits nonzero.
|
|
241
|
+
*/
|
|
242
|
+
export async function drainAndRestart(
|
|
243
|
+
deps: UpgradeDeps,
|
|
244
|
+
o: { project?: string; timeoutMs: number },
|
|
245
|
+
): Promise<void> {
|
|
246
|
+
const initial = deps.layers(o.project);
|
|
247
|
+
if (!initial.paused) deps.setPaused(true);
|
|
248
|
+
await waitForDrain(deps, o.project, Date.now() + o.timeoutMs);
|
|
249
|
+
await deps.restartDaemon();
|
|
250
|
+
if (!initial.paused) deps.setPaused(false);
|
|
251
|
+
}
|
|
252
|
+
|
|
227
253
|
function recoveryProblem(
|
|
228
254
|
layers: FleetLayers,
|
|
229
255
|
initial: FleetLayers,
|
package/src/worktree.ts
CHANGED
|
@@ -263,14 +263,22 @@ function refreshManagedExclude(worktree: string): void {
|
|
|
263
263
|
}
|
|
264
264
|
}
|
|
265
265
|
|
|
266
|
+
/**
|
|
267
|
+
* Serializes concurrent {@link ensureMirror} calls per mirror path so two
|
|
268
|
+
* dispatches cannot collide on git's ref locks for the same repo (#186). A
|
|
269
|
+
* rejected (failed) call does not poison the chain: the next caller waits on a
|
|
270
|
+
* settled promise.
|
|
271
|
+
*/
|
|
272
|
+
const mirrorLocks = new Map<string, Promise<unknown>>();
|
|
273
|
+
|
|
266
274
|
/**
|
|
267
275
|
* Returns the path of the bare mirror for `repo`, cloning it on first use and
|
|
268
276
|
* refreshing it otherwise.
|
|
269
277
|
*
|
|
270
|
-
* ponytail:
|
|
271
|
-
*
|
|
272
|
-
* throw; the run is retried rather than corrupted. Upgrade path is a
|
|
273
|
-
*
|
|
278
|
+
* ponytail: the lock is per process, not a cross-process lockfile in
|
|
279
|
+
* `mirrorRoot`. Two *dispatch loops* can still collide on git's ref locks and
|
|
280
|
+
* one will throw; the run is retried rather than corrupted. Upgrade path is a
|
|
281
|
+
* lockfile keyed by repo name, for a future multi-daemon host.
|
|
274
282
|
*
|
|
275
283
|
* ponytail: if `repo.cloneUrl` embeds credentials, `git clone` persists them in
|
|
276
284
|
* the mirror's config, exactly as it would for a hand-run clone. Prefer an SSH
|
|
@@ -280,6 +288,17 @@ export async function ensureMirror(
|
|
|
280
288
|
repo: RepoTarget,
|
|
281
289
|
mirrorRoot: string,
|
|
282
290
|
): Promise<string> {
|
|
291
|
+
const key = mirrorPathFor(repo, mirrorRoot);
|
|
292
|
+
const prev = mirrorLocks.get(key) ?? Promise.resolve();
|
|
293
|
+
const next = prev.catch(() => {}).then(() => ensureMirrorUnlocked(repo, mirrorRoot));
|
|
294
|
+
mirrorLocks.set(key, next);
|
|
295
|
+
void next.catch(() => {}).finally(() => {
|
|
296
|
+
if (mirrorLocks.get(key) === next) mirrorLocks.delete(key);
|
|
297
|
+
});
|
|
298
|
+
return next;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
async function ensureMirrorUnlocked(repo: RepoTarget, mirrorRoot: string): Promise<string> {
|
|
283
302
|
mkdirSync(mirrorRoot, { recursive: true });
|
|
284
303
|
const mirrorPath = mirrorPathFor(repo, mirrorRoot);
|
|
285
304
|
|