basecamp / basecamp/basecamp-sdk
Event feed: a post-confirmation welcome does not resubscribe, contrary to SPEC §23
- Dominant language
- Go
- Stars
- 49
- Forks
- 12
- Avg merge
- 20h 47m
- Merged PRs (30d)
- 89
Description
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):
```go
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.
Contributor guide
Research direction
Start in go/pkg/basecamp/eventfeed/catchup.go at handleLiveFrame's post-confirmation default arm, then read loop.go's transition 8 for the existing bounded subscribe write. Review the per-state event-feed tests and tier-2 fixture, and use SPEC §23 to resolve the state/timer behavior. Done means bounded, cancellable, byte-identical resubscription in CatchingUp, Draining, and Streaming with the documented timer semantics.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- go
- Domain
- backend, networking
- Issue type
- Bug
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 42/100