perf: replace poll-driven queue flush and MCP child watcher with an event-driven drainable worker #489
Labels
No labels
backend
bug
chore
duplication
effort:complex
effort:medium
effort:trivial
enhancement
follow-up
frontend
fullstack
priority:high
ready-for-agent
refactor
security
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
dries/ocman#489
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Ten background loops are started unconditionally in one block (
internal/server/server.go:395-410), several of them polling for work that an event could have told them about, and their tests wait on sleeps because there is no way to ask "is this worker done".The two that are polling as a substitute for an event:
internal/server/queue.go:20—queueSweepInterval = 15 * time.Second. Runs aDISTINCTquery plus a per-session status check every 15 s. Its own comment acknowledges the cost.internal/server/mcp_watcher.go:19—childSessionWatchInterval = 5 * time.Second. Scansstate.dbfor non-terminal children and calls the platform per child, every 5 s, to notice a transition the event stream already carries.And one that polls hard for no reason:
internal/platforms/opencode/models_cache.go:310-336re-runs the full unfilteredGetSessions("", 0)every 4 s forever (sessionsRefreshInterval = 4 * time.Second), which is the heaviest query in the codebase.Why it matters
Two costs.
Latency and load. A queued message can wait up to 15 s past the idle edge; a finished MCP child can wait up to 5 s before its parent hears. Both intervals are pure overhead when the triggering event is already available. Meanwhile the 4 s session refresher burns the expensive correlated-subquery scan whether or not anything changed.
Untestable timing. Nothing exposes "the queue is empty and the in-flight item finished", so tests either sleep or poll. That is also why the sweep exists: it is the only way to be sure a stranded row eventually drains.
Suggested fix
Add a small
internal/workerpackage: a queue-backed serial worker with aDrain(ctx)that blocks until the queue is empty and the item currently being processed has finished.Shape (Go, ~60 lines, no new dependency):
The correctness detail worth getting right:
outstandingmust be bumped inside the same critical section as the append, and decremented only after the handler returns, otherwiseDraincan observe a false zero between take and process.Then:
session.idleedge; keeprunQueueSweepbut raise the interval and re-document it as a crash-recovery backstop, not the primary path.Drain, and add one test for the hard case: work enqueued while an item is in flight must not letDrainreturn early.Acceptance criteria
internal/workerexists with aDrainthat provably waits for in-flight work; test enqueues during processing and assertsDrainhas not returned.time.Sleepremains in the tests for the two converted paths.AGENTS.mdis updated where it describes the 15 s sweep and the 5 s child poll as the mechanism.Effort: L.