mirror of
https://github.com/getpaseo/paseo.git
synced 2026-11-02 22:52:58 +00:00
fix(server): finish Git refreshes before daemon shutdown (#8579)
Retain admitted refresh promises until they settle, including after the last workspace subscriber leaves. Disposal closes admission and waits for those commands before callers can remove their working directories. Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com>
This commit is contained in:
@@ -6,7 +6,11 @@ import { tmpdir } from "node:os";
|
||||
import path from "node:path";
|
||||
import pino from "pino";
|
||||
import { afterEach, expect, test, vi } from "vitest";
|
||||
import type { CheckoutSnapshotFacts, CheckoutStatusGit } from "../utils/checkout-git.js";
|
||||
import {
|
||||
getCheckoutStatus as readCheckoutStatus,
|
||||
type CheckoutSnapshotFacts,
|
||||
type CheckoutStatusGit,
|
||||
} from "../utils/checkout-git.js";
|
||||
import { CheckoutDiffManager } from "./checkout-diff-manager.js";
|
||||
import { createFileObserver } from "./file-observer/index.js";
|
||||
import { WorkspaceGitServiceImpl } from "./workspace-git-service.js";
|
||||
@@ -60,6 +64,49 @@ afterEach(async () => {
|
||||
}
|
||||
});
|
||||
|
||||
test.each([false, true])(
|
||||
"disposal waits for an admitted Git refresh after observation ends (unsubscribed: %s)",
|
||||
async (unsubscribeBeforeDisposal) => {
|
||||
const artifacts = path.resolve(".artifacts");
|
||||
mkdirSync(artifacts, { recursive: true });
|
||||
const root = realpathSync(mkdtempSync(path.join(artifacts, "git-refresh-shutdown-")));
|
||||
execFileSync("git", ["init", "-b", "main"], { cwd: root, stdio: "pipe" });
|
||||
const entered = Promise.withResolvers<void>();
|
||||
const release = Promise.withResolvers<void>();
|
||||
const service = new WorkspaceGitServiceImpl({
|
||||
logger: pino({ enabled: false }),
|
||||
paseoHome: path.join(root, "home"),
|
||||
deps: {
|
||||
getCheckoutStatus: async (...args) => {
|
||||
entered.resolve();
|
||||
await release.promise;
|
||||
return readCheckoutStatus(...args);
|
||||
},
|
||||
},
|
||||
});
|
||||
const subscription = service.registerWorkspace({ cwd: root }, () => {});
|
||||
try {
|
||||
await entered.promise;
|
||||
if (unsubscribeBeforeDisposal) subscription.unsubscribe();
|
||||
let disposed = false;
|
||||
const closing = service.dispose().then(() => {
|
||||
disposed = true;
|
||||
return undefined;
|
||||
});
|
||||
await new Promise<void>((resolve) => setImmediate(resolve));
|
||||
expect(disposed).toBe(false);
|
||||
release.resolve();
|
||||
await closing;
|
||||
expect(disposed).toBe(true);
|
||||
} finally {
|
||||
release.resolve();
|
||||
subscription.unsubscribe();
|
||||
await service.dispose();
|
||||
rmSync(root, { recursive: true, force: true });
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
test("a fresh checkout read makes a newly initialized repository available to cached readers", async () => {
|
||||
const root = realpathSync(mkdtempSync(path.join(tmpdir(), "paseo-checkout-init-")));
|
||||
const repo = path.join(root, "repo");
|
||||
|
||||
@@ -614,6 +614,7 @@ export class WorkspaceGitServiceImpl implements WorkspaceGitService {
|
||||
private readonly disposeController = new AbortController();
|
||||
private disposed = false;
|
||||
private disposePromise: Promise<void> | null = null;
|
||||
private readonly pendingRefreshes = new Set<Promise<WorkspaceGitRuntimeSnapshot>>();
|
||||
private readonly snapshotUpdatedListeners = new Set<WorkspaceGitSnapshotUpdatedListener>();
|
||||
private readonly workspaceTargets = new Map<string, WorkspaceGitTarget>();
|
||||
private readonly repoTargets = new Map<string, RepoGitTarget>();
|
||||
@@ -1092,7 +1093,12 @@ export class WorkspaceGitServiceImpl implements WorkspaceGitService {
|
||||
this.workingTreeWatchResolutions.clear();
|
||||
this.workingTreeWatchAliases.clear();
|
||||
this.snapshotUpdatedListeners.clear();
|
||||
this.disposePromise = this.fileObserver.close();
|
||||
// Unsubscribing can remove a target while its Git command still owns the cwd.
|
||||
// Stop admission first, then drain those refreshes before releasing the service.
|
||||
this.disposePromise = Promise.all([
|
||||
this.fileObserver.close(),
|
||||
Promise.allSettled(this.pendingRefreshes),
|
||||
]).then(() => undefined);
|
||||
return this.disposePromise;
|
||||
}
|
||||
|
||||
@@ -3212,11 +3218,13 @@ export class WorkspaceGitServiceImpl implements WorkspaceGitService {
|
||||
}
|
||||
|
||||
const promise = this.runWorkspaceRefreshLoop(target, request).finally(() => {
|
||||
this.pendingRefreshes.delete(promise);
|
||||
const state = target.refreshState;
|
||||
if (state.status === "in-flight" && state.promise === promise) {
|
||||
target.refreshState = { status: "idle" };
|
||||
}
|
||||
});
|
||||
this.pendingRefreshes.add(promise);
|
||||
target.refreshState = {
|
||||
status: "in-flight",
|
||||
promise,
|
||||
|
||||
Reference in New Issue
Block a user