Skip to content

Event feed: a post-confirmation welcome does not resubscribe, contrary to SPEC §23 #800

Description

@jeremy

Raised by daybreak on #705. Confirmed against the current head.

The rule

SPEC §23 "Cable Protocol Details" is unconditional:

Subscribe is sent on each welcome received.

Not "on the first welcome". The sentence directly above it anticipates exactly
this case — "The server absorbs identical retransmits and rejects different
ones" — which is why the subscribe command is built byte-identically on every
send.

What the connector does

handleLiveFrame's post-confirmation dispatch drops it
(go/pkg/basecamp/eventfeed/catchup.go, the default arm):

default:
    // welcome, ping, unknown types, and a post-confirmation confirm or
    // reject: liveness only — the pump already reset staleness.
    return cycleOutcome{}, false

So a welcome arriving in CatchingUp, Draining, or Streaming updates liveness
and nothing else. An Action Cable server that re-issues welcome on the same
socket — after its own connection-state reset — leaves the connector believing
it holds a subscription the server no longer has. The feed then goes silent
with a healthy socket underneath it: pings keep staleness satisfied, so nothing
tears it down, and only the repair poll's 60s cadence recovers any events at
all. Live delivery is dead until the socket happens to drop.

Why it is not a one-line fix

The obvious patch — write the subscribe frame from that arm — reintroduces a
defect this PR already fixed once. CableConn.WriteFrame may block, and the
handshake path deliberately writes subscribe on its own goroutine against a
deadline for that reason (loop.go, transition 8). A synchronous write here
runs on the run goroutine, inside frame dispatch, with no deadline and nothing
to cancel it: a peer whose receive window has shut would hang the feed until
Connector.Close, which is the "defeated deadline" failure the bounded write
exists to prevent.

So the fix needs the same bounded-write discipline in a place that currently
has none, and a decision the inventory does not answer: whether a
post-confirmation welcome also re-arms confirmation-deadline and returns
the state machine to AwaitingConfirmation (transition 8's shape), or only
retransmits while staying in its current state. The first is a new edge from
three states; the second is a write with no state change. §23 numbers neither.

Acceptance criteria

  1. A welcome in CatchingUp, Draining, and Streaming each results in the
    byte-identical subscribe command being written — one test per state, since
    the three reach handleLiveFrame by different paths.
  2. The write is bounded the way transition 8's is: a stalled WriteFrame must
    not hold the run goroutine past a deadline, and must be cancellable by
    teardown.
  3. The state/timer answer is written into §23 — either a numbered edge back to
    AwaitingConfirmation with confirmation-deadline re-armed, or an explicit
    statement that the retransmit changes neither state nor the timer set. The
    per-state exact-timer assertions make this observable, so it cannot be left
    implicit.
  4. A tier-2 fixture covers it, since this is cross-SDK behavior and the other
    five drivers will need the same case.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions