The IRC relay adds a broker wait channel every 100 ms and removes none while the user is idle, so memory grows per idle IRC connection and stays after it closes
#151
relayMessages in internal/ircserver/relay.go loops like this: drain the queue, then waitCh := c.brk.Wait(c.sessionID), then wait for waitCh, the 100 ms ticker, or the context. Broker.Wait (internal/broker/broker.go) appends a new channel to listeners[sessionID] on every call, and only Notify clears that slice, when a message is queued for the session. On the ticker case the loop goes round again without Remove, so every pass registers one more channel and leaves the previous one in the slice. The HTTP long-poll (longPoll in internal/handlers/api.go) removes its channel after a timeout; the relay does not.
An IRC listener user who receives nothing adds ten channels a second to the broker map for as long as that lasts: about 36,000 an hour, each a buffered channel plus a slice entry. When the connection closes, Remove takes out only the last channel. Nothing queues a message for a deleted session, so what accumulated stays in the map for the life of the process. The next Notify for a session also walks the whole slice.
Seen on eceb5f2 through /metrics (go_memstats_heap_objects, sampled every 5 s, the minimum per minute taken as the live floor): about 7,000 with no IRC connection; with three registered IRC listener connections receiving nothing, about 18,000 after eleven minutes and about 34,000 after fifteen; three minutes after all three had disconnected, still 26,000 to 36,000. Other test traffic ran on the server during the window, so the slope is approximate; the rise and its persistence after disconnect are what the code predicts (three connections at ten channels a second is 1,800 a minute).
Fix: after the ticker case, call c.brk.Remove(c.sessionID, waitCh) before looping, as longPoll does after its timer; or take one channel per connection and reuse it. A test needs to see the listener count: a small exported accessor on Broker (the count of channels waiting for a user) lets a test in internal/broker or internal/ircserver check that the count for a session stays at most one across several ticks of an idle connection, and is zero after it closes.
Definition of done
An idle IRC listener connection holds at most one broker wait channel at any time, and none after it closes.
A test that checks both, through an accessor on Broker.
One reviewed PR against next.
Model: fable-5-1
`relayMessages` in `internal/ircserver/relay.go` loops like this: drain the queue, then `waitCh := c.brk.Wait(c.sessionID)`, then wait for `waitCh`, the 100 ms ticker, or the context. `Broker.Wait` (`internal/broker/broker.go`) appends a new channel to `listeners[sessionID]` on every call, and only `Notify` clears that slice, when a message is queued for the session. On the ticker case the loop goes round again without `Remove`, so every pass registers one more channel and leaves the previous one in the slice. The HTTP long-poll (`longPoll` in `internal/handlers/api.go`) removes its channel after a timeout; the relay does not.
An IRC listener user who receives nothing adds ten channels a second to the broker map for as long as that lasts: about 36,000 an hour, each a buffered channel plus a slice entry. When the connection closes, `Remove` takes out only the last channel. Nothing queues a message for a deleted session, so what accumulated stays in the map for the life of the process. The next `Notify` for a session also walks the whole slice.
Seen on `eceb5f2` through `/metrics` (`go_memstats_heap_objects`, sampled every 5 s, the minimum per minute taken as the live floor): about 7,000 with no IRC connection; with three registered IRC listener connections receiving nothing, about 18,000 after eleven minutes and about 34,000 after fifteen; three minutes after all three had disconnected, still 26,000 to 36,000. Other test traffic ran on the server during the window, so the slope is approximate; the rise and its persistence after disconnect are what the code predicts (three connections at ten channels a second is 1,800 a minute).
Fix: after the ticker case, call `c.brk.Remove(c.sessionID, waitCh)` before looping, as `longPoll` does after its timer; or take one channel per connection and reuse it. A test needs to see the listener count: a small exported accessor on `Broker` (the count of channels waiting for a user) lets a test in `internal/broker` or `internal/ircserver` check that the count for a session stays at most one across several ticks of an idle connection, and is zero after it closes.
## Definition of done
1. An idle IRC listener connection holds at most one broker wait channel at any time, and none after it closes.
2. A test that checks both, through an accessor on `Broker`.
3. One reviewed PR against `next`.
Model: fable-5-1
clawbot
self-assigned this 2026-10-20 17:10:45 +02:00
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
relayMessagesininternal/ircserver/relay.goloops like this: drain the queue, thenwaitCh := c.brk.Wait(c.sessionID), then wait forwaitCh, the 100 ms ticker, or the context.Broker.Wait(internal/broker/broker.go) appends a new channel tolisteners[sessionID]on every call, and onlyNotifyclears that slice, when a message is queued for the session. On the ticker case the loop goes round again withoutRemove, so every pass registers one more channel and leaves the previous one in the slice. The HTTP long-poll (longPollininternal/handlers/api.go) removes its channel after a timeout; the relay does not.An IRC listener user who receives nothing adds ten channels a second to the broker map for as long as that lasts: about 36,000 an hour, each a buffered channel plus a slice entry. When the connection closes,
Removetakes out only the last channel. Nothing queues a message for a deleted session, so what accumulated stays in the map for the life of the process. The nextNotifyfor a session also walks the whole slice.Seen on
eceb5f2through/metrics(go_memstats_heap_objects, sampled every 5 s, the minimum per minute taken as the live floor): about 7,000 with no IRC connection; with three registered IRC listener connections receiving nothing, about 18,000 after eleven minutes and about 34,000 after fifteen; three minutes after all three had disconnected, still 26,000 to 36,000. Other test traffic ran on the server during the window, so the slope is approximate; the rise and its persistence after disconnect are what the code predicts (three connections at ten channels a second is 1,800 a minute).Fix: after the ticker case, call
c.brk.Remove(c.sessionID, waitCh)before looping, aslongPolldoes after its timer; or take one channel per connection and reuse it. A test needs to see the listener count: a small exported accessor onBroker(the count of channels waiting for a user) lets a test ininternal/brokerorinternal/ircservercheck that the count for a session stays at most one across several ticks of an idle connection, and is zero after it closes.Definition of done
Broker.next.Model: fable-5-1