Skip to content

fix(relay): deliver tracks whose group sequences do not start at 1 - #515

Merged
okdaichi merged 2 commits into
mainfrom
fix/funnel-late-subscriber
Oct 12, 2026
Merged

okdaichi merged 2 commits into
mainfrom
fix/funnel-late-subscriber

Conversation

@okdaichi

@okdaichi okdaichi commented Oct 12, 2026 •

Copy link
Copy Markdown
Collaborator

Found through foalk-inc/foalk#608.

The bug

The relay never delivered a track whose groups were not numbered 1, 2, 3, … as they reached it. The subscription got no answer and no group.

groupRing stores a group at its arrival count (pos.Add(1) % size) and head() returns that count. The egress loop walks 1 … head() and calls get with each. get took that number as a sequence and required cache.seq == seq, so a group was found only when its sequence equalled its arrival count.

That check came with 2b1fc9a (pooled caches, 2026-06-15) as a guard against reading a slot the ring had reused. Until then get returned whatever the slot held and any sequence worked. The guard needed the slot's position to compare against; the cache had no such field, and the argument was already named and typed as a sequence.

Publishers that break the assumption:

  • The funnel. It answers a subscription with the track's latest record under that record's sequence. With three records, the relay's upstream subscription starts at group 3; the ring stores it at position 1 and the lookup for 1 finds a cache whose seq is 3. Every track with more than one record was unreachable through the relay. The existing integration test records once before subscribing, the one case where the two numbers agree.
  • Any publisher with a gap in its sequences.

The fix

The ring is addressed by arrival order alone. groupCache gains pos, set in reserve, and get checks cache.pos == pos, which keeps the reuse guard. get, head and earliestAvailable use a ringPosition type so a sequence cannot be passed where a position is meant. A group still leaves the relay under the sequence its publisher gave it (OpenGroupAt(cache.seq)), unchanged.

Groups that arrive out of sequence order leave in the order they arrived, and a new subscriber starts at the group that arrived last. That is how the ring behaved before 2b1fc9a; the type's comment now says so.

The fell-behind debug log names its values requested_position, earliest_position and latest_position, since they are ring positions.

Tests

  • TestGroupRing_Get_SequencesNotStartingAtOneOrDense: sequences starting past 1, with gaps, a single late one, wrapping the ring, and out of order.
  • TestRelayChain_KeepsPublisherSequences (integration): through two relays, a subscriber receives sequences that start past 1, have gaps, or both.
  • TestFunnel_RelaySubscriberJoinsAfterSeveralRecords (integration): three records, then a subscriber through the relay receives group 3, then group 4. It timed out before the fix. The setup it shares with TestFunnel_PublishesThroughTheRelay is now startFunnelBehindRelay and subscribeChatAsViewer.

go test ./internal/relay/, go test -tags=integration ./internal/integration/, the new relay-chain test and golangci-lint run ./... pass locally.

Not in this change

The relay does not forward dropped-group reports, so a subscriber of a track with gaps learns of the track's end late. That is #516.

🤖 Generated with Claude Code

https://claude.ai/code/session_01RJgd6G5k5aezW5cs8JCY5g

The group ring stored a group at its arrival count but looked it up by
its sequence, so a group was found only when the two were equal. A track
whose first group reached the relay with another sequence, or whose
sequences had a gap, was never delivered. The funnel publishes such
tracks: it answers a subscription with the latest record under that
record's sequence.

The ring is now addressed by arrival order, with its own type; a group
keeps the sequence its publisher gave it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RJgd6G5k5aezW5cs8JCY5g
A relay-chain test sends sequences that start past 1 and have gaps
through two relays. The ring test covers out-of-order arrival and
releases the references it takes. The funnel tests share their relay
setup. The fell-behind log names its values as ring positions.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RJgd6G5k5aezW5cs8JCY5g
@okdaichi
okdaichi merged commit 2f692eb into main Oct 12, 2026
13 of 14 checks passed
@okdaichi
okdaichi deleted the fix/funnel-late-subscriber branch October 12, 2026 09:34
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.

1 participant