{"record":{"id":"d69296c26fd876f8","repo":"agalwood/Motrix","slug":"plugin-fs-too-many-readers","errorCode":"plugin.fs.too_many_readers","errorMessage":"plugin.fs.too_many_readers: max ${this.maxConcurrentReaders} concurrent readers","messagePattern":"plugin\\.fs\\.too_many_readers: max (.+?) concurrent readers","errorType":"exception","errorClass":"FsTaskError","httpStatus":null,"severity":"error","filePath":"src/core/plugin/capabilities/fs-task.ts","lineNumber":129,"sourceCode":"  // exists\n  // -------------------------------------------------------------------------\n\n  async exists(): Promise<boolean> {\n    try {\n      await fs.access(this._filePath)\n      return true\n    } catch {\n      return false\n    }\n  }\n\n  // -------------------------------------------------------------------------\n  // openReader\n  // -------------------------------------------------------------------------\n\n  openReader(opts: { offset?: number; length?: number }): FsTaskReader {\n    if (this.activeReaders.size >= this.maxConcurrentReaders) {\n      throw new FsTaskError(\n        'plugin.fs.too_many_readers',\n        `plugin.fs.too_many_readers: max ${this.maxConcurrentReaders} concurrent readers`\n      )\n    }\n\n    const offset = opts.offset ?? 0\n    const maxLength = opts.length ?? Infinity\n\n    const state: ReaderState = {\n      handle: null as unknown as Awaited<ReturnType<typeof fs.open>>,\n      position: offset,\n      closed: false,\n      idleTimer: null,\n    }\n\n    // Lazily opened — we open on first read to keep openReader() sync\n    let openPromise: Promise<void> | null = null\n    let bytesDelivered = 0","sourceCodeStart":111,"sourceCodeEnd":147,"githubUrl":"https://github.com/agalwood/Motrix/blob/1a708ee57746c434e2c67a44bbf0906a976afea4/src/core/plugin/capabilities/fs-task.ts#L111-L147","documentation":"Thrown by FsTask.openReader() when `activeReaders.size >= maxConcurrentReaders` (default 3 per DEFAULT_MAX_READERS). Each open reader holds a file handle and an idle timer; the cap prevents handle exhaustion. The check is synchronous and throws before allocating any state. Code is `plugin.fs.too_many_readers`.","triggerScenarios":"Opening more than maxConcurrentReaders readers on the same FsTask without closing prior ones; raising the count via fan-out parallelism without raising the cap; forgetting to call reader.close() so the slot never frees.","commonSituations":"Parallel chunked reads spawned by a Promise.all without a concurrency limiter; leaked readers from early-return/throw paths that skip close(); test fan-out exceeding the default 3.","solutions":["Always close readers in a finally block so slots are returned even on error.","Raise maxConcurrentReaders in the FsTask options when parallelism is intended and the OS handle limit allows it.","Gate openReader() calls with a semaphore/p-limit sized to the configured cap.","Audit for leaked readers by checking activeReaders before opening a new one."],"exampleFix":"// before\nconst readers = await Promise.all(\n  ranges.map(r => task.openReader(r))\n) // >3 ranges -> throws\n\n// after — pool with a limiter + always close\nconst pool = pLimit(3)\nawait Promise.all(ranges.map(r => pool(async () => {\n  const rd = task.openReader(r)\n  try { /* read loop */ } finally { await rd.close() }\n})))","handlingStrategy":"validation","validationCode":"function openReaderBounded(task: FsTask, opts: { offset?: number; length?: number }, max: number) {\n  // caller-side semaphore sized to the task's maxConcurrentReaders\n  if (activeCount >= max) throw new Error('reader cap reached; queue instead')\n  return task.openReader(opts)\n}","typeGuard":"function isTooManyReaders(e: unknown): boolean {\n  return e instanceof Error && (e as FsTaskError).code === 'plugin.fs.too_many_readers'\n}","tryCatchPattern":"try {\n  const rd = task.openReader(opts)\n  try { /* read loop */ } finally { await rd.close() }\n} catch (e) {\n  if (isTooManyReaders(e)) { /* queue and retry after a reader closes */ }\n  else throw e\n}","preventionTips":["Always close readers in a finally block.","Gate openReader() with a semaphore sized to the cap (default 3).","Raise maxConcurrentReaders in opts when intentional parallelism needs it."],"tags":["fs","task","concurrency","resource-limits","readers"],"backgroundTag":null,"analyzedSha":"1a708ee57746c434e2c67a44bbf0906a976afea4","analyzedAt":"2026-08-12T16:18:09.346Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}