Update iceState to DISCONNECTED in PeerIceModule.close() (fixes #59)#89
Update iceState to DISCONNECTED in PeerIceModule.close() (fixes #59)#89juerghegglin wants to merge 1 commit intoFAForever:masterfrom
Conversation
…ever#59) Issue FAForever#59 ("Socket closed, when state Connected") reports a state desync: the game shows no connection, the ICE debug window shows 'Connected', and the logs say the socket is closed. All three are true simultaneously. Root cause: PeerIceModule.close() shuts down the listener thread, the TURN refresh module, the connectivity checker, and frees the ice4j Agent, but it does not transition iceState to DISCONNECTED and does not notify the FAF client via rpcService.onConnected(false) or rpcService.onIceConnectionStateChanged("disconnected"). Whatever state was active when close() ran (typically CONNECTED, since close() is called from the RPC disconnect-from-peer path on a live peer or from GameSession.close() during game teardown) sticks. The companion method onConnectionLost() does the right thing already: it sets connected=false, fires onConnected(false), calls setState(DISCONNECTED), and nulls agent/mediaStream/component references after agent.free(). close() should mirror this so anyone reading the Peer's state after close() sees a coherent picture. Change: rewrite close() to follow the same cleanup-then-notify-then- free order that onConnectionLost() uses. The connected and iceState checks guard against double-firing notifications when close() runs after onConnectionLost has already updated state. Side benefit: turnRefreshModule, agent, mediaStream, and component are now nulled after their close/free, matching onConnectionLost. This avoids dangling references that the listener-thread NPE catch in PeerIceModule.listener() exists to defend against.
|
Warning Rate limit exceeded
To continue reviewing without waiting, purchase usage credits in the billing tab. ⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (1)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Problem (closes #59)
Issue #59 reports a state desync where three things are simultaneously true:
ConnectedSocketException: Socket closedThe original report includes stack traces from
Peer.javaandPeerIceModule.javalistener loops that fire after the underlying socket has been closed — but the adapter'siceStatefield is stillCONNECTED.Root cause
PeerIceModule.close()does the cleanup work (interrupts the listener thread, closes the TURN refresh module, frees the ice4jAgent, stops the connectivity checker) but does not:connected = falserpcService.onConnected(..., false)setState(DISCONNECTED)(which would notify both RPC and the telemetry/debug pipeline)agent,mediaStream, andcomponentreferences afteragent.free()So whatever
iceStatewas whenclose()ran — typicallyCONNECTEDbecauseclose()is invoked either from the RPC disconnect-from-peer path on a live peer or fromGameSession.close()during game teardown — stays put. The FAF client never gets a state-change notification matching the actual closed-socket reality, the telemetry server's UI shows the peer as still connected, and any code path readingpeer.getIce().getIceState()returnsCONNECTEDfor a peer whose socket is gone.The companion method
onConnectionLost()does this correctly already.close()is the outlier.Change
Rewrite
PeerIceModule.close()to mirroronConnectionLost()'s cleanup-then-notify-then-free flow:void close() { if (listenerThread != null) { listenerThread.interrupt(); listenerThread = null; } if (turnRefreshModule != null) { turnRefreshModule.close(); + turnRefreshModule = null; } + connectivityChecker.stop(); + + // Mirror the state notifications that onConnectionLost() sends so the FAF client + // and telemetry see this peer as fully disconnected. Without this, iceState stayed + // at whatever it was (typically CONNECTED) when the peer was closed via the RPC + // disconnect path or game-end teardown - the desync reported in issue #59 + // ("Socket closed, when state Connected"). + if (connected) { + connected = false; + rpcService.onConnected(IceAdapter.getId(), peer.getRemoteId(), false); + } + if (iceState != DISCONNECTED) { + setState(DISCONNECTED); + } + if (agent != null) { agent.free(); + agent = null; + mediaStream = null; + component = null; } - connectivityChecker.stop(); }The
connectedandiceStatechecks prevent firing duplicate notifications whenclose()runs afteronConnectionLost()has already updated state (e.g. peer drop followed by game-end cleanup).Risk
Low. The new behavior is exactly what
onConnectionLost()already does for the same fields. The state machine is unchanged for paths that go throughonConnectionLost(); only the close() path now reports its actual outcome.Side benefit
The dangling-reference cleanup (nulling
agent,mediaStream,componentafterfree(), plusturnRefreshModuleafter itsclose()) matches whatonConnectionLost()does and reduces the chance the listener thread sees half-freed state during shutdown — partially defusing the known race that thecatch (NullPointerException)inPeerIceModule.listener()exists to defend against.Verification
:ice-adapter:check(compile + spotlessCheck) passes afterspotlessApply.onConnectionLost's state notification) is identical to existing well-tested code paths.Closes #59.
Co-authored with Claude (Anthropic); reviewed and locally verified by me.