{"record":{"id":"21b453599cd2b0ba","repo":"agalwood/Motrix","slug":"plugin-lifecycle-deactivate-timeout","errorCode":"plugin.lifecycle.deactivate_timeout","errorMessage":"plugin \"${pluginId}\" deactivate budget exhausted before all handlers ran","messagePattern":"plugin \"(.+?)\" deactivate budget exhausted before all handlers ran","errorType":"exception","errorClass":"LifecycleError","httpStatus":null,"severity":"error","filePath":"src/core/plugin/capabilities/lifecycle.ts","lineNumber":100,"sourceCode":"   * sharing `totalBudgetMs`. Clears the handler list after the run (whether\n   * it succeeded or failed). Throws `LifecycleError` on timeout or handler\n   * throw.\n   */\n  async runDeactivate(pluginId: string): Promise<void> {\n    const list = this.handlers.get(pluginId) ?? []\n    // Snapshot so dispose() calls during run don't mutate the in-flight list.\n    const snapshot = list.slice()\n    // Clear immediately so subsequent calls start fresh.\n    this.handlers.delete(pluginId)\n\n    const startMs = Date.now()\n\n    for (const handler of snapshot) {\n      const elapsed = Date.now() - startMs\n      const remaining = this.totalBudgetMs - elapsed\n\n      if (remaining <= 0) {\n        throw new LifecycleError(\n          'plugin.lifecycle.deactivate_timeout',\n          `plugin \"${pluginId}\" deactivate budget exhausted before all handlers ran`\n        )\n      }\n\n      await this._raceWithTimeout(pluginId, handler, remaining)\n    }\n  }\n\n  /**\n   * Clear all registered handlers for `pluginId` without running them.\n   * Called on full plugin removal / uninstall.\n   */\n  reset(pluginId: string): void {\n    this.handlers.delete(pluginId)\n  }\n\n  /**","sourceCodeStart":82,"sourceCodeEnd":118,"githubUrl":"https://github.com/agalwood/Motrix/blob/1a708ee57746c434e2c67a44bbf0906a976afea4/src/core/plugin/capabilities/lifecycle.ts#L82-L118","documentation":"Deactivate handlers for one plugin run sequentially and share a single total budget (default 2000ms). Before each handler, remaining = totalBudgetMs - elapsed is computed; once it drops to zero the next handler is never run and the error fires. Handlers are also individually raced (_raceWithTimeout) so a slow handler can consume the whole budget.","triggerScenarios":"A deactivate handler does blocking I/O (sync fs, large writes); many handlers whose cumulative time exceeds 2000ms; a handler awaits a never-resolving promise that gets killed at the per-handler timeout, consuming most of the budget; cleanup logic that awaits external services.","commonSituations":"Plugin grew cleanup responsibilities over time; plugin closes DB connections or flushes buffers synchronously; third-party SDKs with slow teardown; CI machines slower than production so budget is tighter.","solutions":["Make deactivate handlers fast, async, and non-blocking; offload heavy cleanup to a separate background path.","Raise totalBudgetMs in LifecycleCapabilityHost options if legitimate cleanup needs more.","Reduce the number of registered deactivate handlers; consolidate.","Audit handlers for accidental awaits on never-resolving promises."],"exampleFix":"// before\nhost.onDeactivate('p', async () => { await fs.writeFileSync(...) ; await db.flush(); await db.close() })\n\n// after\nhost.onDeactivate('p', async () => { db.close(); }) // fire-and-forget heavy flush to a background task","handlingStrategy":"validation","validationCode":"// profile handlers in dev before shipping\nconst t0 = Date.now()\nawait handler()\nif (Date.now() - t0 > 200) console.warn('slow deactivate handler', handler.name)","typeGuard":null,"tryCatchPattern":"try {\n  await host.deactivate(pluginId)\n} catch (e) {\n  if (e instanceof LifecycleError && e.code === 'plugin.lifecycle.deactivate_timeout') {\n    // schedule remaining cleanup off the deactivate path (background task)\n  } else throw e\n}","preventionTips":["Keep deactivate handlers O(few ms): close handles, do not flush large buffers inline.","Raise totalBudgetMs only if legitimate cleanup needs it; do not paper over a slow handler.","Consolidate many small handlers into one fast batch teardown."],"tags":["lifecycle","plugin","timeout","cleanup","performance"],"backgroundTag":null,"analyzedSha":"1a708ee57746c434e2c67a44bbf0906a976afea4","analyzedAt":"2026-08-12T16:18:09.346Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}