fix(workers): tear down a deleted source's worker after its FK nulls root_dir_id

This commit is contained in:
Daniel Salazar committed 2026-10-10 21:21:59 -04:00
1 parent ab7773a5d5
commit bd7e70edfd
2 files changed
+43 -5

No files matched your search

@@ -719,6 +719,37 @@ describe('WorkerDriver hot reload', () => {
expect(path).toContain(user.username);
});
it('tears the worker down when the delete nulls its root_dir_id', async () => {
// MySQL and Postgres null `subdomains.root_dir_id` when its fsentry row
// is deleted (FK ON DELETE SET NULL). SQLite's schema has no such FK,
// so a trigger stands in for it.
await server.clients.db.write(
'CREATE TRIGGER test_subdomain_root_set_null AFTER DELETE ON fsentries ' +
'BEGIN UPDATE subdomains SET root_dir_id = NULL WHERE root_dir_id = OLD.id; END',
);
try {
const { user, actor, path, name } = await deployWorker();
fetchSpy.mockClear();
const entry = await server.stores.fsEntry.getEntryByPath(path);
await inCtx(actor, () =>
server.services.fs.remove(user.id, { entry: entry! }),
);
await waitFor(
() => deleteCalls().length > 0,
'edge delete after source delete',
);
expect(deleteCalls().map((call) => call[0])).toContain(
`${SCRIPTS_BASE}/${name}/`,
);
} finally {
await server.clients.db.write(
'DROP TRIGGER IF EXISTS test_subdomain_root_set_null',
);
}
});
it('tears the worker down when its source file is moved to Trash', async () => {
const { user, actor, entry, name } = await deployWorker();
fetchSpy.mockClear();
+12 -5
View File
@@ -1207,7 +1207,9 @@ export class WorkerDriver extends PuterDriver {
const entry = this.#extractFsEntryFromEvent(data);
if (!entry || entry.isDir) return;
const matched = await this.#listWorkerRowsForEntry(entry);
const matched = await this.#listWorkerRowsForEntry(entry, {
deleted: true,
});
for (const row of matched) {
await this.#deleteWorkerForSourceRow(row, entry.userId);
}
@@ -1259,9 +1261,13 @@ export class WorkerDriver extends PuterDriver {
* The entry already names the worker rows pointing at it, so a file no
* worker is bound to (nearly every write) costs no query. The rows
* themselves come from the primary: one deleted by cascade can outlive its
* by-name cache entry.
* by-name cache entry. Once the entry is deleted, its FK has already set
* those rows' `root_dir_id` to NULL.
*/
async #listWorkerRowsForEntry(entry: FSEntry): Promise<SubdomainRow[]> {
async #listWorkerRowsForEntry(
entry: FSEntry,
{ deleted = false }: { deleted?: boolean } = {},
): Promise<SubdomainRow[]> {
const names = (entry.workers ?? []).map((worker) => worker.subdomain);
if (names.length === 0) return [];
const rowsByName = await this.stores.subdomain.getBySubdomains(names, {
@@ -1269,8 +1275,9 @@ export class WorkerDriver extends PuterDriver {
});
return [...rowsByName.values()].filter(
(row) =>
Number(row.root_dir_id) === entry.id &&
Number(row.user_id) === entry.userId,
Number(row.user_id) === entry.userId &&
(Number(row.root_dir_id) === entry.id ||
(deleted && row.root_dir_id == null)),
);
}