{"record":{"id":"fd6f206162aeb883","repo":"overleaf/overleaf","slug":"refusing-to-delete-non-expired-project","errorCode":null,"errorMessage":"refusing to delete non-expired project","messagePattern":"refusing to delete non-expired project","errorType":"http","errorClass":"NotReadyToDelete","httpStatus":422,"severity":"warning","filePath":"services/history-v1/storage/lib/backupDeletion.mjs","lineNumber":51,"sourceCode":" * @return {Promise<void>}\n */\nasync function deleteProjectBackup(projectId) {\n  const deletedProject = await deletedProjectsCollection.findOne(\n    { 'deleterData.deletedProjectId': new ObjectId(projectId) },\n    {\n      projection: {\n        'deleterData.deletedProjectOverleafHistoryId': 1,\n        'deleterData.deletedAt': 1,\n      },\n    }\n  )\n  if (!deletedProject) {\n    throw new NotReadyToDelete('refusing to delete non-deleted project')\n  }\n  const expiresAt =\n    deletedProject.deleterData.deletedAt.getTime() + EXPIRE_PROJECTS_AFTER_MS\n  if (expiresAt > Date.now()) {\n    throw new NotReadyToDelete('refusing to delete non-expired project')\n  }\n\n  const historyId =\n    deletedProject.deleterData.deletedProjectOverleafHistoryId?.toString()\n  if (!historyId) {\n    throw new NotReadyToDelete(\n      'refusing to delete project with unknown historyId'\n    )\n  }\n\n  if (await projectHasLatestChunk(historyId)) {\n    throw new NotReadyToDelete(\n      'refusing to delete project with remaining chunks'\n    )\n  }\n\n  const prefix = projectKey.format(historyId) + '/'\n  await backupPersistor.deleteDirectory(chunksBucket, prefix)","sourceCodeStart":33,"sourceCodeEnd":69,"githubUrl":"https://github.com/overleaf/overleaf/blob/28ad3b03b71cb4311decdcb55c36b33ec10d72db/services/history-v1/storage/lib/backupDeletion.mjs#L33-L69","documentation":"deleteProjectBackup throws NotReadyToDelete when the project's deleterData.deletedAt plus EXPIRE_PROJECTS_AFTER_MS is in the future — the project is deleted but its grace/retention period has not elapsed. Deletion is intentionally deferred until the backup expires so users can still restore within the window.","triggerScenarios":"deleteProjectBackup is called for a deleted project whose deletedAt timestamp is less than EXPIRE_PROJECTS_AFTER_MS ago: `expiresAt > Date.now()`.","commonSituations":"Cron or script running backup deletion too aggressively after deletion; EXPIRE_PROJECTS_AFTER_MS raised (retention policy change) so previously 'due' projects are no longer expired; clock skew between app servers and stored timestamps.","solutions":["Wait until the retention window (deletedAt + EXPIRE_PROJECTS_AFTER_MS) has passed and let the next scheduled run pick it up","Skip the project gracefully — NotReadyToDelete here is expected during the retention window","Verify EXPIRE_PROJECTS_AFTER_MS matches your intended retention policy; adjust only deliberately","Check server clock synchronization (NTP) if projects seem expired but still throw"],"exampleFix":"// before\nawait deleteProjectBackup(projectId) // may throw NotReadyToDelete\n// after\ntry {\n  await deleteProjectBackup(projectId)\n} catch (err) {\n  if (err instanceof NotReadyToDelete) {\n    console.log(`deferred: ${projectId}`)\n    return\n  }\n  throw err\n}","handlingStrategy":"try-catch","validationCode":"const doc = await db.projects.findOne(\n  { _id: projectId },\n  { projection: { 'deleterData.deletedAt': 1 } }\n)\nif (doc && new Date(doc.deleterData.deletedAt.getTime() + EXPIRE_PROJECTS_AFTER_MS) > new Date()) {\n  throw new Skip(`project ${projectId} still within retention window`)\n}","typeGuard":null,"tryCatchPattern":"try {\n  await deleteProjectBackup(projectId)\n} catch (err) {\n  if (err instanceof NotReadyToDelete) {\n    scheduleRetry(projectId, EXPIRE_PROJECTS_AFTER_MS)\n    return\n  }\n  throw err\n}","preventionTips":["Schedule deletion jobs no earlier than deletedAt + retention period","Keep EXPIRE_PROJECTS_AFTER_MS consistent between scheduler and deleter","Sync server clocks (NTP) to avoid premature/late expiry"],"tags":["retention","grace-period","backup-deletion","not-ready"],"backgroundTag":"retention-period-not-elapsed","analyzedSha":"28ad3b03b71cb4311decdcb55c36b33ec10d72db","analyzedAt":"2026-09-03T02:10:22.807Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-10T07:17:11.731Z"}