Skip to content

Fix lost reconnects caused by unsynchronized Process state transitions - #294

Closed
AmiradelBeyg wants to merge 2 commits into
apache:masterfrom
AmiradelBeyg:fix-process-state-race
Closed

Fix lost reconnects caused by unsynchronized Process state transitions#294
AmiradelBeyg wants to merge 2 commits into
apache:masterfrom
AmiradelBeyg:fix-process-state-race

Conversation

@AmiradelBeyg

Copy link
Copy Markdown
Contributor

Motivation

Process.Handle is invoked synchronously on the caller's thread, so events race: ChannelConnected arrives from the connection response continuation while ChannelDisconnected arrives from SubProducer.MessageDispatcher when an in-flight send fails. The switch writes ChannelState based on the event, but CalculateState() reads the shared field afterwards — the two steps are not atomic.

A stale Connected write can land between a Disconnected write and its CalculateState() call, making both threads read Connected: the disconnect is handled but no reconnect is ever scheduled. Since the dispatcher stops itself after reporting the failure and the channel is already deregistered from the connection, no further event will arrive — the producer stays disconnected forever while State reports Connected. This is unrecoverable from the application side because the state is not final, so state monitoring never sees a terminal transition.

Modifications

  • Guard the state update and the resulting decision in Process with a per-process lock so every event's decision is made against that event's own state: every handled disconnect now schedules a reconnect.
  • The lock is on the lifecycle-event path only (a handful of events per channel transition) and never executes on the message send path.
  • Added a concurrency test firing 500 concurrent connected/disconnected pairs and asserting a final disconnect always leads to a re-established channel, for producer, consumer, and reader processes.

Verifying this change

  • Make sure that the change passes the CI checks.

All existing unit tests pass.

Note: this PR is stacked on #293 (its first commit); only the second commit is new here. Happy to rebase once #293 lands, or squash both into one PR if preferred.

AmiradelBeyg and others added 2 commits August 14, 2026 16:31
Every ChannelDisconnected event enqueued a new CloseChannel +
EstablishNewChannel action. When a second disconnected event arrived
while a reconnect was already in flight (e.g. a failed in-flight send
racing the connection-level disconnect), the stale action would later
close the freshly re-established channel, deregistering a healthy
producer/consumer from the broker and forcing another reconnect cycle.

ScheduleReconnect now runs at most one reconnect action at a time and
re-evaluates the channel state after the action completes, so a
disconnect arriving mid-reconnect is never lost and a replacement
channel is never closed by a stale action.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Handle is invoked synchronously on the caller's thread, so events race:
ChannelConnected arrives from the connection response continuation while
ChannelDisconnected arrives from the message dispatcher when an in-flight
send fails. The switch writes ChannelState based on the event, but
CalculateState reads the shared field afterwards - the two steps are not
atomic. A stale Connected write can land between a Disconnected write and
its CalculateState call, making both threads read Connected: the
disconnect is handled but no reconnect is ever scheduled. Since the
dispatcher stops itself after reporting the failure and the channel is
already deregistered, no further event arrives and the producer stays
disconnected forever while its state reports Connected.

Guard the state update and the resulting decision with a per-process
lock so every event's decision is made against that event's own state:
every handled disconnect now schedules a reconnect. The lock is on the
lifecycle-event path only (a handful of events per channel transition)
and never executes on the message send path.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@AmiradelBeyg

Copy link
Copy Markdown
Contributor Author

Consolidating into #293 for easier review - that PR now contains both commits (the reconnect dedup and this synchronization fix) as one coherent change set.

@AmiradelBeyg
AmiradelBeyg deleted the fix-process-state-race branch August 14, 2026 21:09
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