Skip to content

harden session lifecycle and prevent double disconnects - #574

Merged
AngeloTadeucci merged 1 commit into
masterfrom
session-disconnect
Sep 28, 2025
Merged

harden session lifecycle and prevent double disconnects#574
AngeloTadeucci merged 1 commit into
masterfrom
session-disconnect

Conversation

@AngeloTadeucci

@AngeloTadeucci AngeloTadeucci commented Sep 28, 2025

Copy link
Copy Markdown
Collaborator

Summary by CodeRabbit

  • Bug Fixes

    • Prevented rare re-entrant disconnects that could cause errors.
    • Blocked outgoing messages during disconnect to avoid crashes.
    • Improved handling of network read/write failures for smoother recovery.
    • Ensured disconnect triggers in all failure paths to prevent stuck sessions.
  • Refactor

    • Hardened shutdown/teardown with safer disposal and clearer logging.
    • Made logout/teardown saves more reliable to preserve progress and group state.
    • Enhanced diagnostics to aid tracing of connection issues.

@coderabbitai

coderabbitai Bot commented Sep 28, 2025

Copy link
Copy Markdown
Contributor

Walkthrough

Refactors session disconnect and disposal flows. Adds reentrancy guard and caller-info logging to network Disconnect; exposes AccountId/CharacterId on core Session. Strengthens exception handling and send guards. GameSession replaces public identity properties with internal handling and introduces a dispose state machine with centralized, safe teardown and save logic.

Changes

Cohort / File(s) Summary
Network session disconnect/logging hardening
Maple2.Server.Core/Network/Session.cs
Added disconnect reentrancy guard and caller-info parameters to Disconnect; introduced AccountId/CharacterId public properties; wrapped Dispose/close/join in try/catch; improved pipeline fault logging; blocked sends during disconnect; refined read/write exception handling and logging; ensured Disconnect is invoked in error/finally paths.
Game session dispose refactor and public API removal
Maple2.Server.Game/Session/GameSession.cs
Replaced single dispose flag with a 3-state dispose state machine; removed public AccountId/CharacterId; reworked Dispose: early base dispose, safe component saves via local helpers, safe disposals for group managers, snapshot of field tick for SaveCacheConfig, unified error handling across teardown.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor Caller
  participant Session as Core Session
  participant Pipeline as Recv/Send Pipelines
  participant Client as Network Client

  Caller->>Session: Disconnect(caller,line,file)
  alt first-time disconnect
    Session->>Session: Set disconnecting flag
    Session->>Pipeline: Signal completion
    Pipeline-->>Session: Fault? log error
    Session->>Client: CloseClient()
    Session->>Session: Dispose (try/catch around join/close)
  else already disconnecting
    Session->>Caller: Return (no-op)
  end

  note over Session,Client: SendInternal/SendRaw check disconnecting flag and skip sends when set
Loading
sequenceDiagram
  autonumber
  actor Shutdown as Shutdown/GC
  participant GSession as GameSession
  participant Base as Base Session
  participant Managers as Guild/Buddy/Party/...
  participant Storage as Save/Config/Persistence

  Shutdown->>GSession: Dispose()
  alt state: active -> disposing
    GSession->>GSession: Capture fieldTickSnapshot
    GSession->>Base: Dispose()
    loop per component
      GSession->>Storage: TrySaveComponent(...)
      Storage-->>GSession: Success/Exception (logged)
    end
    par safe disposals
      GSession->>Managers: SafeDispose(...)
    and
      GSession->>GSession: SaveCacheConfig(fieldTickSnapshot)
    end
    GSession->>GSession: Set state -> disposed
  else disposing/disposed
    GSession->>Shutdown: Return (no-op)
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • Improve task management #414 — Also modifies GameSession Dispose logic, likely overlapping with the new dispose state machine and teardown changes.

Suggested reviewers

  • Zintixx
  • mettaursp

Poem

A bunny taps keys with a careful thump-thump,
Guarding disconnects from a double-bump.
Sessions now bow with orderly grace,
Dispose states march in a single-file pace.
IDs tucked neat, logs tell the tale—
Hippity-hop, no racing to fail. 🐇✨

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 10.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The title accurately and concisely summarizes the primary change of the pull request by highlighting enhancements to the session lifecycle and the prevention of double disconnects, matching the implemented reentrancy guard and lifecycle hardening described in the diff. It is specific enough for a reviewer to understand the focus without extra noise or vagueness.
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch session-disconnect

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
Maple2.Server.Game/Session/GameSession.cs (1)

743-808: Reentrancy guard can strand the network socket when pre-base cleanup throws

gameDisposeState flips to 1 before we touch Scheduler.Stop(), LeaveField(), etc. If any of those steps throw before we hit base.Dispose(disposing);, we land in the catch, log the error, and then the finally block drives gameDisposeState straight to 2. Every future Dispose (including the finalizer) will now bail out at the guard, so the base Session.Dispose never runs—meaning the underlying socket thread (Complete(), thread.Join, CloseClient, see Maple2.Server.Core/Network/Session.cs:83-108) is left alive. Previously, a second dispose attempt would have finished the cleanup; the new guard prevents that safety net.

We should only mark the session as fully disposed once the base dispose succeeds, and reset the guard if we fail before that so another pass can retry. Something like:

-        if (Interlocked.CompareExchange(ref gameDisposeState, 1, 0) != 0) return;
-        // begin dispose
-
-        // Snapshot values needed after teardown
-        long fieldTickSnapshot = Field?.FieldTick ?? Environment.TickCount64;
+        if (Interlocked.CompareExchange(ref gameDisposeState, 1, 0) != 0) return;
+        bool baseDisposed = false;
+        long fieldTickSnapshot = Field?.FieldTick ?? Environment.TickCount64;
...
-            base.Dispose(disposing);
+            base.Dispose(disposing);
+            baseDisposed = true;
...
-        } catch (Exception ex) {
-            Logger.Error(ex, "Error during session cleanup for {Player}", PlayerName);
+        } catch (Exception ex) {
+            Logger.Error(ex, "Error during session cleanup for {Player}", PlayerName);
+            if (!baseDisposed) {
+                Interlocked.Exchange(ref gameDisposeState, 0);
+            }
         } finally {
...
-            Interlocked.Exchange(ref gameDisposeState, 2);
+            if (baseDisposed) {
+                Interlocked.Exchange(ref gameDisposeState, 2);
+            }
         }

This keeps the reentrancy guard while still guaranteeing the base resources are eventually torn down.

🧹 Nitpick comments (1)
Maple2.Server.Core/Network/Session.cs (1)

120-123: Log after winning the disconnect guard

Calling Logger.Information("Disconnected …") before the Interlocked.Exchange means every re-entrant call logs “Disconnected …” even when we immediately return because another thread already tore the session down. That makes production logs noisy and obscures who actually performed the teardown. Please move the log (or change its wording) so we only emit the Info-level entry once we win the guard; you can keep an extra Debug log for filtered duplicates if needed.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e023540 and 43ce44c.

📒 Files selected for processing (2)
  • Maple2.Server.Core/Network/Session.cs (11 hunks)
  • Maple2.Server.Game/Session/GameSession.cs (4 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
Maple2.Server.Core/Network/Session.cs (1)
Maple2.Server.Core/Network/QueuedPipeScheduler.cs (1)
  • Complete (14-14)
Maple2.Server.Game/Session/GameSession.cs (5)
Maple2.Tools/Scheduler/EventQueue.cs (1)
  • InvokeAll (84-113)
Maple2.Server.Game/GameServer.cs (1)
  • OnDisconnected (73-78)
Maple2.Server.Core/Network/Session.cs (2)
  • Dispose (84-87)
  • Dispose (89-109)
Maple2.Database/Storage/Game/DatabaseRequest.cs (4)
  • Dispose (34-37)
  • BeginTransaction (16-18)
  • Commit (20-28)
  • SaveChanges (30-32)
Maple2.Database/Storage/Game/GameStorage.cs (5)
  • GameStorage (11-80)
  • GameStorage (22-40)
  • Request (42-49)
  • Request (51-79)
  • Request (53-55)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: build
  • GitHub Check: build

Comment thread Maple2.Server.Core/Network/Session.cs
@AngeloTadeucci
AngeloTadeucci merged commit 8143d28 into master Sep 28, 2025
4 checks passed
@AngeloTadeucci
AngeloTadeucci deleted the session-disconnect branch September 28, 2025 00:21
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.

2 participants