Skip to content

fix(logs): prevent stack overflow flushing large DuckDB write buffers - #316

Merged
grahaml merged 1 commit into
mainfrom
graham/fix-duckdb-log-flush-stack-overflow
Jun 24, 2026
Merged

fix(logs): prevent stack overflow flushing large DuckDB write buffers#316
grahaml merged 1 commit into
mainfrom
graham/fix-duckdb-log-flush-stack-overflow

Conversation

@grahaml

@grahaml grahaml commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

Problem

DuckDBStorage.flushWriteBuffer() flushes the entire write buffer in a single INSERT, and on failure requeues it with unshift(...toFlush). Both spread a potentially huge array as function arguments:

  • the INSERT builds one statement with ~7 bound params per row, spread into db.run(sql, ...params), and
  • the failure path spreads the whole batch into Array.prototype.unshift.

Once the buffer grows past V8's argument limit (~100k elements), this throws RangeError: Maximum call stack size exceeded. That error masks the original write failure, and because clear() calls flushWriteBuffer() before its DELETE, a wedged flush can stop the table from ever draining — so the buffer/table keeps growing.

Observed in production as a steady stream of DuckDB flush error: RangeError: Maximum call stack size exceeded, alongside Parquet-export OOMs on the same oversized tables.

Fix

  • Chunked insertswriteBatchInternal now inserts in fixed-size chunks (WRITE_CHUNK_SIZE = 1000) via a new writeChunk helper, instead of one statement spanning the whole buffer. Each statement is bounded regardless of buffer size.
  • Safe requeue — on flush failure, requeue with this.writeBuffer = toFlush.concat(this.writeBuffer) instead of unshift(...toFlush), so requeue can't overflow the stack and the original error surfaces.

No behavior change on the happy path; purely bounds the per-statement size and makes the failure path safe.

Testing

Type-checks clean (tsc --noEmit, 0 errors). The fix is localized to duckdb-storage.ts.

🤖 Generated with Claude Code

flushWriteBuffer() flushes the entire write buffer in a single INSERT and, on
failure, requeues it with `unshift(...toFlush)`. Both spread a potentially huge
array as call arguments: the INSERT builds a statement with ~7 bound params per
row (spread into `db.run(sql, ...params)`), and the requeue spreads the whole
batch into `unshift`. Once the buffer grows past V8's argument limit (~100k),
this throws "RangeError: Maximum call stack size exceeded". That masks the real
write failure and, because clear() flushes before its DELETE, can stop the
table from ever draining.

- Insert in fixed-size chunks (WRITE_CHUNK_SIZE = 1000) instead of one statement
  spanning the whole buffer.
- Requeue on failure with `toFlush.concat(this.writeBuffer)` instead of
  `unshift(...toFlush)`, so requeue can't overflow.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@grahaml
grahaml merged commit 5a26b95 into main Jun 24, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants