Skip to content

fix(storage): never hold a read transaction across a StreamActivities send - #1743

Merged
Dumbris merged 1 commit into
mainfrom
claude/issue-1742-fix-91b393
Oct 23, 2026
Merged

Dumbris merged 1 commit into
mainfrom
claude/issue-1742-fix-91b393

Conversation

@Dumbris

@Dumbris Dumbris commented Oct 23, 2026

Copy link
Copy Markdown
Member

closes #1742

Hazard

storage.Manager.StreamActivities walked the activity bucket inside one BBolt read transaction and sent each record into a buffered channel. #1737 made the send cancel-aware, but a consumer that stops reading without cancelling (an activity export to a client that keeps the connection open but no longer reads) still parked the producer inside db.View. The open read transaction pinned the mmap: the next write that had to grow the file waited on the remap lock for ever, and every later reader (agent-token lookups included) queued behind that pending writer. Result: all authenticated requests hang while /healthz keeps answering — the #1742 signature.

Fix

The producer never holds a read transaction across a channel send. Each short db.View collects at most 100 matching records (examining at most 1000 keys), leaves the transaction, then sends; the next batch resumes strictly before the last examined key (Seek + Prev, robust to that key being pruned). Offset, limit and newest-first order are unchanged. Records written after the stream starts are not guaranteed to appear, and records pruned between batches are skipped.

Tests

  • TestStreamActivities_StalledConsumerHoldsNoTransaction (replaces the test that asserted the leak as the contract): a consumer that stops reading without cancelling leaves OpenTxN == 0.
  • TestStreamActivities_StalledConsumerDoesNotBlockRemap: with a stalled stream, a 16 MiB write (forces an mmap remap) completes, and the stream then delivers all 300 records.
  • TestStreamActivities_BatchBoundaries: order, offset/limit across batches, and a sparse filter past the per-transaction scan cap.

Both stalled-consumer tests fail on main (the remap test times out at 5 s) and pass here. storage, runtime, httpapi pass with -race; golangci-lint v2 clean (bare and -tags server).

Live verification

Scratch core with a seeded DB (12k activity records, ~1 MB below the mmap boundary), a stalled export client, then a loop of writes + authenticated reads:

  • main: writes and authenticated reads hang after ~200–290 writes; the goroutine dump matches the issue (StreamActivities.func1.1 parked in View, a writer in (*DB).mmap on RWMutex.Lock, readers in beginTx RLock).
  • this branch: 400 writes, file grew 133 → 150 MB (remap happened), reads steady at ~76 ms; a full export returned all 12,000 records ordered with no duplicates.

Cross-reviewed by opencode gpt-6.1-sol: CLEAN.

… send

StreamActivities walked the activity bucket inside one BBolt read
transaction and sent each record into a buffered channel. #1737 made the
send cancel-aware, but a consumer that stops reading without cancelling
(an activity export to a client that keeps the connection open but no
longer reads) still parked the producer inside db.View. The open read
transaction pinned the mmap: the next write that had to grow the file
waited on the remap lock for ever, and every later reader — agent-token
lookups included — queued behind that writer, hanging all authenticated
requests while /healthz kept answering.

The producer now collects a bounded batch (at most 100 matches, at most
1000 keys examined) inside a short db.View, leaves the transaction, then
sends, and resumes the walk strictly before the last examined key in a
new transaction. Offset, limit and newest-first order are unchanged;
records written after the stream starts are not guaranteed to appear,
and records pruned between batches are skipped.

Tests: the no-cancel test that asserted the leak as the contract is
replaced by one that asserts a stalled consumer holds no transaction, a
remap test (a 16 MiB write must not block behind a stalled stream), and
batch-boundary coverage for offset/limit/order and a sparse filter past
the per-transaction scan cap.
@Dumbris
Dumbris enabled auto-merge (squash) October 23, 2026 05:17
@cloudflare-workers-and-pages

Copy link
Copy Markdown

Deploying mcpproxy-docs with  Cloudflare Pages  Cloudflare Pages

Latest commit: aa34be8
Status: ✅  Deploy successful!
Preview URL: https://fe3fc6cc.mcpproxy-docs.pages.dev
Branch Preview URL: https://claude-issue-1742-fix-91b393.mcpproxy-docs.pages.dev

View logs

@github-actions

Copy link
Copy Markdown
Contributor

📦 Build Artifacts

Workflow Run: View Run
Branch: claude/issue-1742-fix-91b393

Available Artifacts

  • archive-darwin-amd64 (32 MB)
  • archive-darwin-arm64 (29 MB)
  • archive-linux-amd64 (19 MB)
  • archive-linux-arm64 (17 MB)
  • archive-windows-amd64 (32 MB)
  • archive-windows-arm64 (28 MB)
  • frontend-dist-pr (0 MB)
  • installer-dmg-darwin-amd64 (28 MB)
  • installer-dmg-darwin-arm64 (25 MB)
  • smart-mcp-proxymcpproxy-goMUHJPO.dockerbuild (0 MB)

How to Download

Option 1: GitHub Web UI (easiest)

  1. Go to the workflow run page linked above
  2. Scroll to the bottom "Artifacts" section
  3. Click on the artifact you want to download

Option 2: GitHub CLI

gh run download 39904914281 --repo smart-mcp-proxy/mcpproxy-go

Note: Artifacts expire in 14 days.

@codecov-commenter

Copy link
Copy Markdown

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

❌ Patch coverage is 81.48148% with 10 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
internal/storage/activity.go 81.48% 6 Missing and 4 partials ⚠️

📢 Thoughts on this report? Let us know!

@Dumbris
Dumbris merged commit 272d5e8 into main Oct 23, 2026
45 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

v0.70.0: an abandoned StreamActivities read transaction blocks a bbolt remap and, with it, every agent-token lookup

2 participants