From 207aae0f8d165c665b4489c8d64c67773fc07493 Mon Sep 17 00:00:00 2001 From: Jorben Date: Tue, 10 Feb 2026 23:30:14 +0800 Subject: [PATCH 1/2] =?UTF-8?q?fix(worker):=20=F0=9F=90=9B=20fix=20Convert?= =?UTF-8?q?erWorker=20not=20picking=20up=20pages=20after=20split?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Orphaned TaskDetail records (PENDING status) from tasks in terminal states (FAILED, CANCELLED, etc.) were causing ConverterWorker's claimPage() to exhaust its maxAttempts without finding valid work. - Refactor claimPage() to pre-filter active tasks (PROCESSING/COMPLETED) before searching for PENDING pages, eliminating orphan interference - Add cleanup step in WorkerOrchestrator.cleanupOrphanedWork() to mark PENDING pages from terminal tasks as FAILED on startup - Update CleanupResult interface with orphanedPendingPages field Co-Authored-By: Claude Opus 4.6 --- .../services/WorkerOrchestrator.ts | 45 ++++++++++++++- .../__tests__/WorkerOrchestrator.test.ts | 41 +++++++++++++- .../interfaces/IWorkerOrchestrator.ts | 1 + .../application/workers/ConverterWorker.ts | 43 +++++++------- .../workers/__tests__/ConverterWorker.test.ts | 56 +++++++++++-------- 5 files changed, 138 insertions(+), 48 deletions(-) diff --git a/src/core/application/services/WorkerOrchestrator.ts b/src/core/application/services/WorkerOrchestrator.ts index d970e81..1bd1e8d 100644 --- a/src/core/application/services/WorkerOrchestrator.ts +++ b/src/core/application/services/WorkerOrchestrator.ts @@ -241,11 +241,53 @@ export class WorkerOrchestrator implements IWorkerOrchestrator { console.log(`[WorkerOrchestrator] Reset ${orphanedMergingTasks.count} orphaned MERGING tasks to READY_TO_MERGE`); } + // Clean up orphaned TaskDetail records whose parent Task is in a terminal/non-processing state. + // These are pages left in PENDING state from tasks that have been FAILED, CANCELLED, etc. + // Without cleanup, these orphaned pages block ConverterWorker from finding valid work. + // Note: This step intentionally runs after the PROCESSING->PENDING reset above, + // so that any pages from terminal tasks that were both orphaned AND in PROCESSING state + // are first reset to PENDING, then caught here and marked as FAILED. + const terminalTaskStatuses = [ + TaskStatus.CREATED, + TaskStatus.FAILED, + TaskStatus.CANCELLED, + TaskStatus.COMPLETED, + TaskStatus.PARTIAL_FAILED, + ]; + + // Find tasks in terminal states that still have PENDING pages + const terminalTasks = await prisma.task.findMany({ + where: { + status: { in: terminalTaskStatuses }, + }, + select: { id: true }, + }); + + let orphanedPendingPages = 0; + if (terminalTasks.length > 0) { + const terminalTaskIds = terminalTasks.map((t) => t.id); + const result = await prisma.taskDetail.updateMany({ + where: { + task: { in: terminalTaskIds }, + status: PageStatus.PENDING, + }, + data: { + status: PageStatus.FAILED, + error: 'Orphaned: parent task no longer active', + }, + }); + orphanedPendingPages = result.count; + if (orphanedPendingPages > 0) { + console.log(`[WorkerOrchestrator] Marked ${orphanedPendingPages} orphaned PENDING pages as FAILED (parent task in terminal state)`); + } + } + const result: CleanupResult = { orphanedPages: orphanedPages.count, orphanedSplittingTasks: orphanedSplittingTasks.count, orphanedMergingTasks: orphanedMergingTasks.count, - total: orphanedPages.count + orphanedSplittingTasks.count + orphanedMergingTasks.count, + orphanedPendingPages, + total: orphanedPages.count + orphanedSplittingTasks.count + orphanedMergingTasks.count + orphanedPendingPages, }; if (result.total === 0) { @@ -262,6 +304,7 @@ export class WorkerOrchestrator implements IWorkerOrchestrator { orphanedPages: 0, orphanedSplittingTasks: 0, orphanedMergingTasks: 0, + orphanedPendingPages: 0, total: 0, }; } diff --git a/src/core/application/services/__tests__/WorkerOrchestrator.test.ts b/src/core/application/services/__tests__/WorkerOrchestrator.test.ts index e0a114a..96b510d 100644 --- a/src/core/application/services/__tests__/WorkerOrchestrator.test.ts +++ b/src/core/application/services/__tests__/WorkerOrchestrator.test.ts @@ -79,6 +79,7 @@ describe('WorkerOrchestrator', () => { // Default: no orphaned work prismaMock.taskDetail.updateMany.mockResolvedValue({ count: 0 } as any) prismaMock.task.updateMany.mockResolvedValue({ count: 0 } as any) + prismaMock.task.findMany.mockResolvedValue([] as any) }) afterEach(() => { @@ -250,9 +251,15 @@ describe('WorkerOrchestrator', () => { }) describe('cleanupOrphanedWork', () => { + // Helper to set up default mocks for cleanup (no terminal tasks with orphaned pages) + function setupDefaultCleanupMocks() { + prismaMock.task.findMany.mockResolvedValue([] as any) + } + it('should reset PROCESSING pages to PENDING', async () => { prismaMock.taskDetail.updateMany.mockResolvedValue({ count: 5 } as any) prismaMock.task.updateMany.mockResolvedValue({ count: 0 } as any) + setupDefaultCleanupMocks() const result = await orchestrator.cleanupOrphanedWork() @@ -275,6 +282,7 @@ describe('WorkerOrchestrator', () => { prismaMock.task.updateMany .mockResolvedValueOnce({ count: 2 } as any) // SPLITTING -> PENDING .mockResolvedValueOnce({ count: 0 } as any) // MERGING -> READY_TO_MERGE + setupDefaultCleanupMocks() const result = await orchestrator.cleanupOrphanedWork() @@ -296,6 +304,7 @@ describe('WorkerOrchestrator', () => { prismaMock.task.updateMany .mockResolvedValueOnce({ count: 0 } as any) // SPLITTING -> PENDING .mockResolvedValueOnce({ count: 3 } as any) // MERGING -> READY_TO_MERGE + setupDefaultCleanupMocks() const result = await orchestrator.cleanupOrphanedWork() @@ -312,9 +321,35 @@ describe('WorkerOrchestrator', () => { expect(result.orphanedMergingTasks).toBe(3) }) + it('should mark orphaned PENDING pages from terminal tasks as FAILED', async () => { + prismaMock.taskDetail.updateMany + .mockResolvedValueOnce({ count: 0 } as any) // PROCESSING -> PENDING (first call) + .mockResolvedValueOnce({ count: 4 } as any) // orphaned PENDING -> FAILED (second call) + prismaMock.task.updateMany.mockResolvedValue({ count: 0 } as any) + prismaMock.task.findMany.mockResolvedValue([ + { id: 'failed-task-1' }, + { id: 'cancelled-task-2' }, + ] as any) + + const result = await orchestrator.cleanupOrphanedWork() + + expect(prismaMock.taskDetail.updateMany).toHaveBeenCalledWith({ + where: { + task: { in: ['failed-task-1', 'cancelled-task-2'] }, + status: PageStatus.PENDING, + }, + data: { + status: PageStatus.FAILED, + error: 'Orphaned: parent task no longer active', + } + }) + expect(result.orphanedPendingPages).toBe(4) + }) + it('should return total=0 when no orphaned work', async () => { prismaMock.taskDetail.updateMany.mockResolvedValue({ count: 0 } as any) prismaMock.task.updateMany.mockResolvedValue({ count: 0 } as any) + setupDefaultCleanupMocks() const result = await orchestrator.cleanupOrphanedWork() @@ -322,6 +357,7 @@ describe('WorkerOrchestrator', () => { expect(result.orphanedPages).toBe(0) expect(result.orphanedSplittingTasks).toBe(0) expect(result.orphanedMergingTasks).toBe(0) + expect(result.orphanedPendingPages).toBe(0) }) it('should return sum of all orphaned items as total', async () => { @@ -329,10 +365,11 @@ describe('WorkerOrchestrator', () => { prismaMock.task.updateMany .mockResolvedValueOnce({ count: 2 } as any) .mockResolvedValueOnce({ count: 3 } as any) + setupDefaultCleanupMocks() const result = await orchestrator.cleanupOrphanedWork() - expect(result.total).toBe(10) // 5 + 2 + 3 + expect(result.total).toBe(10) // 5 + 2 + 3 + 0 (no terminal tasks) }) it('should return empty result on error without interrupting startup', async () => { @@ -344,6 +381,7 @@ describe('WorkerOrchestrator', () => { orphanedPages: 0, orphanedSplittingTasks: 0, orphanedMergingTasks: 0, + orphanedPendingPages: 0, total: 0 }) }) @@ -352,6 +390,7 @@ describe('WorkerOrchestrator', () => { const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {}) prismaMock.taskDetail.updateMany.mockResolvedValue({ count: 2 } as any) prismaMock.task.updateMany.mockResolvedValue({ count: 0 } as any) + setupDefaultCleanupMocks() await orchestrator.cleanupOrphanedWork() diff --git a/src/core/application/services/interfaces/IWorkerOrchestrator.ts b/src/core/application/services/interfaces/IWorkerOrchestrator.ts index a233204..e1ee5a8 100644 --- a/src/core/application/services/interfaces/IWorkerOrchestrator.ts +++ b/src/core/application/services/interfaces/IWorkerOrchestrator.ts @@ -26,6 +26,7 @@ export interface CleanupResult { orphanedPages: number; orphanedSplittingTasks: number; orphanedMergingTasks: number; + orphanedPendingPages: number; total: number; } diff --git a/src/core/application/workers/ConverterWorker.ts b/src/core/application/workers/ConverterWorker.ts index 5fbd215..1061838 100644 --- a/src/core/application/workers/ConverterWorker.ts +++ b/src/core/application/workers/ConverterWorker.ts @@ -159,27 +159,39 @@ export class ConverterWorker extends WorkerBase { /** * Claim a PENDING page for processing using optimistic locking. * - * Query conditions: - * - task.status = PROCESSING - * - task.status != CANCELLED - * - page.status = PENDING - * - page.worker_id = null + * Strategy: First find tasks in valid states (PROCESSING/COMPLETED), + * then find PENDING pages only within those tasks. This avoids the problem + * where orphaned PENDING pages from terminal tasks exhaust claim attempts. * * Order: retry_count ASC, page ASC (prioritize fresh pages) */ private async claimPage(): Promise { const maxAttempts = 5; - const checkedTaskIds: string[] = []; // Track tasks we've already checked and skipped + + // Pre-filter: find tasks in valid states for conversion. + // Done once before the retry loop since task states rarely change + // within the milliseconds between optimistic lock retries. + const activeTasks = await prisma.task.findMany({ + where: { + status: { in: [TaskStatus.PROCESSING, TaskStatus.COMPLETED] }, + }, + select: { id: true }, + }); + + if (activeTasks.length === 0) { + return null; + } + + const activeTaskIds = activeTasks.map((t) => t.id); for (let attempt = 0; attempt < maxAttempts; attempt++) { try { - // Step 1: Find a candidate page, excluding tasks we've already checked + // Step 1: Find a candidate page within active tasks only const candidate = await prisma.taskDetail.findFirst({ where: { status: PageStatus.PENDING, worker_id: null, - // Exclude pages from tasks we've already checked and found not in PROCESSING state - ...(checkedTaskIds.length > 0 && { task: { notIn: checkedTaskIds } }), + task: { in: activeTaskIds }, }, orderBy: [ { retry_count: 'asc' }, @@ -191,19 +203,6 @@ export class ConverterWorker extends WorkerBase { return null; } - // Step 2: Verify the parent task is in a valid state for processing - // Allow PROCESSING (normal flow) and COMPLETED (single page retry) - const task = await prisma.task.findUnique({ - where: { id: candidate.task }, - select: { status: true }, - }); - - if (!task || (task.status !== TaskStatus.PROCESSING && task.status !== TaskStatus.COMPLETED)) { - // Task not in correct state, remember it and try next - checkedTaskIds.push(candidate.task); - continue; - } - // Step 3: Try to claim using optimistic locking const result = await prisma.taskDetail.updateMany({ where: { diff --git a/src/core/application/workers/__tests__/ConverterWorker.test.ts b/src/core/application/workers/__tests__/ConverterWorker.test.ts index 08abbd8..04ef0ca 100644 --- a/src/core/application/workers/__tests__/ConverterWorker.test.ts +++ b/src/core/application/workers/__tests__/ConverterWorker.test.ts @@ -8,6 +8,7 @@ vi.mock('../../../infrastructure/db/index.js', () => ({ prisma: { $transaction: vi.fn(), task: { + findMany: vi.fn().mockResolvedValue([]), findUnique: vi.fn(), update: vi.fn(), }, @@ -91,8 +92,8 @@ describe('ConverterWorker', () => { describe('run()', () => { it('should set isRunning to true when started', async () => { - // Mock claimPage to return null (no pages) - vi.mocked(prisma.taskDetail.findFirst).mockResolvedValue(null); + // Mock claimPage to return null (no active tasks) + vi.mocked(prisma.task.findMany).mockResolvedValue([]); const runPromise = worker.run(); await new Promise((resolve) => setTimeout(resolve, 50)); @@ -104,7 +105,7 @@ describe('ConverterWorker', () => { }); it('should stop when stop() is called', async () => { - vi.mocked(prisma.taskDetail.findFirst).mockResolvedValue(null); + vi.mocked(prisma.task.findMany).mockResolvedValue([]); const runPromise = worker.run(); await new Promise((resolve) => setTimeout(resolve, 50)); @@ -118,12 +119,12 @@ describe('ConverterWorker', () => { it('should continue running after error in main loop', async () => { let callCount = 0; - vi.mocked(prisma.taskDetail.findFirst).mockImplementation(async () => { + vi.mocked(prisma.task.findMany).mockImplementation(async () => { callCount++; if (callCount === 1) { throw new Error('Transient error'); } - return null; + return []; }); const runPromise = worker.run(); @@ -159,8 +160,8 @@ describe('ConverterWorker', () => { updatedAt: new Date(), }; + vi.mocked(prisma.task.findMany).mockResolvedValue([{ id: 'task123' }] as any); vi.mocked(prisma.taskDetail.findFirst).mockResolvedValue(mockPage as any); - vi.mocked(prisma.task.findUnique).mockResolvedValue({ status: TaskStatus.PROCESSING } as any); vi.mocked(prisma.taskDetail.updateMany).mockResolvedValue({ count: 1 }); vi.mocked(prisma.taskDetail.findUnique).mockResolvedValue({ ...mockPage, @@ -174,29 +175,18 @@ describe('ConverterWorker', () => { expect(prisma.taskDetail.updateMany).toHaveBeenCalled(); }); - it('should return null if no PENDING pages exist', async () => { - vi.mocked(prisma.taskDetail.findFirst).mockResolvedValue(null); + it('should return null if no active tasks exist', async () => { + vi.mocked(prisma.task.findMany).mockResolvedValue([]); const result = await (worker as any).claimPage(); expect(result).toBeNull(); + expect(prisma.taskDetail.findFirst).not.toHaveBeenCalled(); }); - it('should skip pages from tasks not in PROCESSING state', async () => { - const mockPage = { - id: 1, - task: 'task123', - status: PageStatus.PENDING, - worker_id: null, - }; - - // First call returns a page, second call returns null - vi.mocked(prisma.taskDetail.findFirst) - .mockResolvedValueOnce(mockPage as any) - .mockResolvedValueOnce(null); - - // Task is not in PROCESSING state - vi.mocked(prisma.task.findUnique).mockResolvedValue({ status: TaskStatus.CANCELLED } as any); + it('should return null if no PENDING pages exist in active tasks', async () => { + vi.mocked(prisma.task.findMany).mockResolvedValue([{ id: 'task123' }] as any); + vi.mocked(prisma.taskDetail.findFirst).mockResolvedValue(null); const result = await (worker as any).claimPage(); @@ -211,10 +201,10 @@ describe('ConverterWorker', () => { worker_id: null, }; + vi.mocked(prisma.task.findMany).mockResolvedValue([{ id: 'task123' }] as any); vi.mocked(prisma.taskDetail.findFirst) .mockResolvedValueOnce(mockPage as any) .mockResolvedValueOnce(null); - vi.mocked(prisma.task.findUnique).mockResolvedValue({ status: TaskStatus.PROCESSING } as any); // First attempt fails (another worker claimed it) vi.mocked(prisma.taskDetail.updateMany).mockResolvedValueOnce({ count: 0 }); @@ -227,6 +217,7 @@ describe('ConverterWorker', () => { it('should prioritize pages with lower retry_count', async () => { let queryParams: any; + vi.mocked(prisma.task.findMany).mockResolvedValue([{ id: 'task123' }] as any); vi.mocked(prisma.taskDetail.findFirst).mockImplementation(async (params: any) => { queryParams = params; return null; @@ -236,6 +227,23 @@ describe('ConverterWorker', () => { expect(queryParams.orderBy).toEqual([{ retry_count: 'asc' }, { page: 'asc' }]); }); + + it('should only search within active task IDs', async () => { + let queryParams: any; + + vi.mocked(prisma.task.findMany).mockResolvedValue([ + { id: 'task-a' }, + { id: 'task-b' }, + ] as any); + vi.mocked(prisma.taskDetail.findFirst).mockImplementation(async (params: any) => { + queryParams = params; + return null; + }); + + await (worker as any).claimPage(); + + expect(queryParams.where.task).toEqual({ in: ['task-a', 'task-b'] }); + }); }); describe('convertPage()', () => { From 0a104817033a47f304bc002804792315b883e773 Mon Sep 17 00:00:00 2001 From: Jorben Date: Tue, 10 Feb 2026 23:32:35 +0800 Subject: [PATCH 2/2] =?UTF-8?q?fix(completion):=20=F0=9F=90=9B=20improve?= =?UTF-8?q?=20test=20connection=20with=20realistic=20image=20and=20token?= =?UTF-8?q?=20limit?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Use a meaningful 32x32 image instead of 1x1 pixel for connection test, add maxTokens cap to limit cost, and update prompt for vision testing. Co-Authored-By: Claude Opus 4.6 --- .../__tests__/completion.handler.test.ts | 2 +- src/main/ipc/handlers/completion.handler.ts | 21 ++++++++++++------- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/src/main/ipc/handlers/__tests__/completion.handler.test.ts b/src/main/ipc/handlers/__tests__/completion.handler.test.ts index d906b2f..8069a0e 100644 --- a/src/main/ipc/handlers/__tests__/completion.handler.test.ts +++ b/src/main/ipc/handlers/__tests__/completion.handler.test.ts @@ -177,7 +177,7 @@ describe('Completion Handler', () => { }), expect.objectContaining({ type: 'text', - text: 'Test connection.' + text: 'Please identify the largest letter in the image.' }) ]) }) diff --git a/src/main/ipc/handlers/completion.handler.ts b/src/main/ipc/handlers/completion.handler.ts index 852a2d6..1d54cb7 100644 --- a/src/main/ipc/handlers/completion.handler.ts +++ b/src/main/ipc/handlers/completion.handler.ts @@ -16,11 +16,14 @@ export function registerCompletionHandlers() { _, providerId: number, modelId: string, - url: string + url: string, ): Promise => { try { if (!providerId || !modelId || !url) { - return { success: false, error: "providerId, modelId, and url are required" }; + return { + success: false, + error: "providerId, modelId, and url are required", + }; } const result = await modelLogic.completion(providerId, { @@ -47,7 +50,7 @@ export function registerCompletionHandlers() { console.error("[IPC] completion:markImagedown error:", error); return { success: false, error: error.message }; } - } + }, ); /** @@ -58,15 +61,19 @@ export function registerCompletionHandlers() { async (_, providerId: number, modelId: string): Promise => { try { if (!providerId || !modelId) { - return { success: false, error: "providerId and modelId are required" }; + return { + success: false, + error: "providerId and modelId are required", + }; } // Use a simple base64 image for testing const testImageBase64 = - "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg=="; + "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"; const result = await modelLogic.completion(providerId, { model: modelId, + maxTokens: 8, messages: [ { role: "user", @@ -79,7 +86,7 @@ export function registerCompletionHandlers() { }, { type: "text", - text: "Test connection.", + text: "Please identify the largest letter in the image.", }, ], }, @@ -91,7 +98,7 @@ export function registerCompletionHandlers() { console.error("[IPC] completion:testConnection error:", error); return { success: false, error: error.message }; } - } + }, ); console.log("[IPC] Completion handlers registered");