fix(streams): release AR connections after each dispatcher iteration#158
Conversation
The StreamEventDispatcher runs in a background fiber and acquires an ActiveRecord connection via StreamStat.record! (BusRecord.create!). After the INSERT, the fiber parks on @queue.pop — but the connection stays leased to the parked fiber. On the next Rails code reload, clear_reloadable_connections! blocks waiting for the leased connection, wedging the request thread for the full rack-timeout duration (10s). Under config.active_support.isolation_level = :fiber, AR connection leases are keyed to the owning Fiber. The dispatcher's fiber never dies and never calls clear_active_connections!, so the lease persists for the life of the worker. Fix: call BusRecord.connection_handler.clear_active_connections! in an ensure block at the end of each run_loop iteration. This releases any AR connections the dispatcher acquired during message handling before parking on the queue. The ensure guarantees cleanup even when handle() raises. Reported by Juraj (getzazu/app#2310).
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Repository UI (base), Organization UI (inherited) Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (2)
📝 WalkthroughWalkthroughStream event dispatcher now ensures ActiveRecord connections are released after each event loop iteration by adding a new ChangesConnection Release Mechanism
Estimated code review effort🎯 2 (Simple) | ⏱️ ~12 minutes Suggested labels
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
Summary
Fixes the AR connection leak in
StreamEventDispatcherthat causes 10s request wedges in development on every Rails code reload when the dashboard/SSE is open.The dispatcher runs in a background fiber (
Thread.new { run_loop }). WhenStreamStat.record!fires, it leases an AR connection from theBusRecordpool. After the INSERT, the fiber parks on@queue.pop— but the connection stays leased to the parked fiber.Under
config.active_support.isolation_level = :fiber(common in modern Rails apps), AR connection leases are keyed to the owning Fiber. The dispatcher's fiber never dies and never callsclear_active_connections!, so the lease persists for the life of the worker.On the next dev file save, the Rails reloader runs
clear_reloadable_connections!, which callsattempt_to_checkout_all_existing_connectionson every pool. The:pgbuspool has exactly one connection leased to the parked dispatcher fiber — so it blocks until rack-timeout fires (10s).Fix: call
BusRecord.connection_handler.clear_active_connections!in anensureblock at the end of eachrun_loopiteration. This releases any AR connections the dispatcher acquired during message handling before parking on the queue.The
ensureguarantees cleanup even whenhandle()raises. The method is guarded bydefined?(::ActiveRecord::Base)so non-Rails contexts aren't affected, and errors in the release itself are swallowed at debug level.Impact in prod: beyond fixing the dev wedge, this also reclaims the wasted connection-per-puma-worker that was permanently leased in production. One fewer idle connection per worker on the
:pgbuspool.Test plan
bundle exec rspec spec/pgbus/web/streamer/— 160 examples, 0 failuresclear_active_connections!is called after a normal messageensurepathbundle exec rubocop— cleanbin/devin a Zazu-like app → open dashboard (subscribes to SSE) → save files rapidly → no 10s wedgeSummary by CodeRabbit
Bug Fixes
Tests