From e6d2a0baee7fd66dcee25a76b605fd7c80f2d375 Mon Sep 17 00:00:00 2001 From: 777genius Date: Sun, 31 May 2026 19:01:02 +0300 Subject: [PATCH] fix(team): avoid oversized task projection cache loops --- src/main/workers/team-fs-worker.ts | 9 +++- .../team/TeamFsWorker.integration.test.ts | 47 +++++++++++++++++++ 2 files changed, 55 insertions(+), 1 deletion(-) diff --git a/src/main/workers/team-fs-worker.ts b/src/main/workers/team-fs-worker.ts index fd13815e..bd7f9fa6 100644 --- a/src/main/workers/team-fs-worker.ts +++ b/src/main/workers/team-fs-worker.ts @@ -831,6 +831,7 @@ async function readPersistentTaskProjectionCache( const stat = await fs.promises.stat(cachePath); if (!stat.isFile() || stat.size > MAX_PERSISTENT_TASK_PROJECTION_CACHE_BYTES) { taskDiag.persistentCacheReadFailures++; + await fs.promises.rm(cachePath, { force: true }).catch(() => undefined); return null; } const raw = await fs.promises.readFile(cachePath, 'utf8'); @@ -898,13 +899,19 @@ async function writePersistentTaskProjectionCache( writtenAt: nowMs(), entries: Object.fromEntries(entries), }; + const raw = JSON.stringify(body); + if (Buffer.byteLength(raw, 'utf8') > MAX_PERSISTENT_TASK_PROJECTION_CACHE_BYTES) { + taskDiag.persistentCacheWriteFailures++; + await fs.promises.rm(cachePath, { force: true }).catch(() => undefined); + return; + } const tmpPath = `${cachePath}.${process.pid}.${Date.now()}.${Math.random() .toString(36) .slice(2)}.tmp`; try { await fs.promises.mkdir(path.dirname(cachePath), { recursive: true }); - await fs.promises.writeFile(tmpPath, JSON.stringify(body), 'utf8'); + await fs.promises.writeFile(tmpPath, raw, 'utf8'); await fs.promises.rename(tmpPath, cachePath); taskDiag.persistentCacheWrites++; } catch { diff --git a/test/main/services/team/TeamFsWorker.integration.test.ts b/test/main/services/team/TeamFsWorker.integration.test.ts index c92465cf..0c20c3ae 100644 --- a/test/main/services/team/TeamFsWorker.integration.test.ts +++ b/test/main/services/team/TeamFsWorker.integration.test.ts @@ -710,6 +710,53 @@ describe('team-fs-worker integration', () => { } }); + it('replaces oversized persisted task projection caches instead of repeatedly reusing them', async () => { + const workerPath = await getWorkerPath(); + tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'team-fs-worker-')); + const tasksBase = path.join(tempDir, 'tasks'); + const projectionCacheBase = path.join(tempDir, 'projection-cache'); + const teamName = 'oversized-persistent-cache-team'; + const tasksDir = path.join(tasksBase, teamName); + await fs.mkdir(tasksDir, { recursive: true }); + await fs.writeFile( + path.join(tasksDir, '1.json'), + JSON.stringify({ + id: '1', + subject: 'Small subject', + status: 'pending', + createdAt: '2026-05-02T12:00:00.000Z', + }), + 'utf8' + ); + + const firstWorker = createWorker(workerPath); + try { + const first = await callGetAllTasks(firstWorker, tasksBase, projectionCacheBase); + expect(first.tasks[0]).toMatchObject({ teamName, subject: 'Small subject' }); + expect(first.diag?.persistentCacheWrites).toBe(1); + } finally { + await firstWorker.terminate(); + } + + const cacheFiles = await fs.readdir(path.join(projectionCacheBase, 'v1')); + const cachePath = path.join(projectionCacheBase, 'v1', cacheFiles[0]); + const oversizedBytes = 16 * 1024 * 1024 + 1; + await fs.writeFile(cachePath, Buffer.alloc(oversizedBytes, 120)); + + const secondWorker = createWorker(workerPath); + try { + const second = await callGetAllTasks(secondWorker, tasksBase, projectionCacheBase); + expect(second.tasks[0]).toMatchObject({ teamName, subject: 'Small subject' }); + expect(second.diag?.persistentCacheReadFailures).toBe(1); + expect(second.diag?.cacheMisses).toBe(1); + expect(second.diag?.persistentCacheWrites).toBe(1); + const repairedStat = await fs.stat(cachePath); + expect(repairedStat.size).toBeLessThan(oversizedBytes); + } finally { + await secondWorker.terminate(); + } + }); + it('rejects persisted task projections that contain deleted tasks as task records', async () => { const workerPath = await getWorkerPath(); tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'team-fs-worker-'));