diff --git a/src/cleanup.js b/src/cleanup.js index 7889e0b..b44078e 100644 --- a/src/cleanup.js +++ b/src/cleanup.js @@ -7,13 +7,19 @@ import { findExpiredJobs, markCleaned } from './jobs/jobRepository.js'; export async function runCleanup(prisma, config) { const expiredJobs = await findExpiredJobs(prisma); + let cleanedCount = 0; for (const job of expiredJobs) { - await deleteIfExists(uploadPath(config, job.uuid, job.sourceFormat)); - await deleteIfExists(outputPath(config, job.uuid, job.targetFormat)); - await markCleaned(prisma, job.id); + try { + await deleteIfExists(uploadPath(config, job.uuid, job.sourceFormat)); + await deleteIfExists(outputPath(config, job.uuid, job.targetFormat)); + await markCleaned(prisma, job.id); + cleanedCount += 1; + } catch (err) { + console.error(`Cleanup failed for job ${job.uuid}:`, err); + } } - return expiredJobs.length; + return cleanedCount; } async function main() { diff --git a/src/jobs/jobRepository.js b/src/jobs/jobRepository.js index 65f4f5d..49bc13d 100644 --- a/src/jobs/jobRepository.js +++ b/src/jobs/jobRepository.js @@ -54,7 +54,13 @@ export async function markProcessing(prisma, id) { export async function markDone(prisma, id, { outputPath, outputMimeType, outputSizeBytes, conversionDurationSeconds }) { await prisma.conversionJob.update({ where: { id }, - data: { status: 'done', outputPath, outputMimeType, outputSizeBytes, conversionDurationSeconds }, + data: { + status: 'done', + outputPath: outputPath ?? null, + outputMimeType: outputMimeType ?? null, + outputSizeBytes: outputSizeBytes ?? null, + conversionDurationSeconds: conversionDurationSeconds ?? null, + }, }); } diff --git a/test/cleanup.test.js b/test/cleanup.test.js index 32dbd53..6051f0a 100644 --- a/test/cleanup.test.js +++ b/test/cleanup.test.js @@ -105,6 +105,55 @@ describe('runCleanup', () => { expect(job.cleanedAt).toBeNull(); }); + it('isolates one job’s failure so later expired jobs in the same batch still get cleaned', async () => { + const failingUuid = '14141414-1414-4414-8414-141414141414'; + const okUuid = '15151515-1515-4515-8515-151515151515'; + await createJob(prisma, { + uuid: failingUuid, + family: 'image', + sourceFormat: 'png', + targetFormat: 'webp', + originalFilename: 'photo.png', + inputPath: `${failingUuid}.png`, + inputMimeType: 'image/png', + inputSizeBytes: 11, + expiresAt: new Date(Date.now() - 1000), + }); + await createJob(prisma, { + uuid: okUuid, + family: 'image', + sourceFormat: 'png', + targetFormat: 'webp', + originalFilename: 'photo.png', + inputPath: `${okUuid}.png`, + inputMimeType: 'image/png', + inputSizeBytes: 11, + expiresAt: new Date(Date.now() - 1000), + }); + const failingJob = await getJobByUuid(prisma, failingUuid); + + const originalUpdate = prisma.conversionJob.update.bind(prisma.conversionJob); + prisma.conversionJob.update = (args) => { + if (args.where?.id === failingJob.id) { + return Promise.reject(new Error('simulated markCleaned failure')); + } + return originalUpdate(args); + }; + + let cleanedCount; + try { + cleanedCount = await runCleanup(prisma, config); + } finally { + prisma.conversionJob.update = originalUpdate; + } + + expect(cleanedCount).toBe(1); + const failing = await getJobByUuid(prisma, failingUuid); + expect(failing.cleanedAt).toBeNull(); + const ok = await getJobByUuid(prisma, okUuid); + expect(ok.cleanedAt).not.toBeNull(); + }); + it('does not re-process an already cleaned expired job', async () => { const uuid = '13131313-1313-4313-8313-131313131313'; await createJob(prisma, {