Skip to content

fix(client): исключение из OnConnected больше не убивает клиент навсегда (10.10.1.0) - #72

Open
Platonenkov wants to merge 5 commits into
devfrom
claude/onconnected-disconnect-analysis-85435b
Open

fix(client): исключение из OnConnected больше не убивает клиент навсегда (10.10.1.0)#72
Platonenkov wants to merge 5 commits into
devfrom
claude/onconnected-disconnect-analysis-85435b

Conversation

@Platonenkov

@Platonenkov Platonenkov commented Aug 3, 2026

Copy link
Copy Markdown
Collaborator

Проблема

Connection.OnceOpen ловил любое исключение потребительского обработчика OnConnected и звал Disconnect() — то есть пользовательский путь отключения: _permanentlyDisconnected = true плюс ClearReconnectState().

catch (Exception error)
{
    connectionManager.RejectAllAwaiting(error);
    await Disconnect();   // <-- ПОСТОЯННОЕ отключение
    return;
}

После этого клиент мёртв навсегда: реконнект-цикл не перезапускается, новых сокетов не открывается, OnConnected больше не поднимается, а любой запрос падает с NotConnectedException("Client has been disconnected. Call Connect() to reconnect."). Наружу не выходило ничего — ни события, ни лога.

Триггер — самый штатный сценарий: перезапуск ноды. OnConnected — типовое место восстановления подписок (SDK их после реконнекта не восстанавливает), а нода принимает TCP раньше, чем начинает отвечать на запросы. Первый же subscribe уходит в RequestTimeout (40 с) и падает. Потребитель, который «упал громко, пусть SDK переподключится», получал ровно обратное — тихую смерть клиента. В проде так встал флот ботов: по четыре часа тишины после обновления ноды, один из них похоронил себя за 69 секунд до того, как нода стала доступна.

Что сделано

  • Сбой обработчика трактуется как отказ соединения, а не как воля пользователя: сокет сносится, дальше работает обычный реконнект с экспоненциальным бэкоффом. Флаг постоянного отключения на этом пути не ставится.
  • Защита от вечного цикла. OnceOpen чистит реконнект-состояние до вызова обработчика, поэтому счётчик попыток самого цикла обнуляется на каждом успешном TCP-коннекте и сойтись не может. Подряд идущие сбои обработчика считаются отдельно (_connectHandlerFailures; сбрасывается при успешном прогоне обработчика, в Connect() и в ChangeServer()). При достижении MaxReconnectAttempts с StopAfterMaxAttempts клиент осознанно сдаётся — сразу понятный NotConnectedException вместо пятиминутного молчания; Connect() обнуляет счётчик, так что восстановление остаётся возможным. С StopAfterMaxAttempts = false попытки продолжаются — это ровно то, о чём просит опция.
  • Наблюдаемость. Исключение поднимается через OnError с errorMessage = "connectHandlerError" (та же форма, что уже используется для сбоев stream-обработчиков) и через OnConnectionStatus. Раньше причина смерти не сообщалась нигде.
  • Попутно: WebSocketClient.SendMessageAsync (async void, вызывается без await из Connection.WebsocketSendAsync) больше не глотает ошибку отправки молча — она трассируется. Поведение не меняется (запрос по-прежнему ограничен RequestTimeout), но причина перестала быть невидимой при диагностике.

Совместимость

Потребители, сознательно бросавшие из OnConnected ради «жёсткой остановки», получат другое поведение. Судя по комментарию в коде (Don't start ping timer if connection failed), задумка была «не считать соединение установленным», а не «убить клиент». Жёсткая остановка при этом сохранена как бэкстоп: с дефолтными StopAfterMaxAttempts = true / MaxReconnectAttempts = 5 вечно падающий обработчик всё равно приводит к терминальному состоянию, только после 5 попыток и с внятным сообщением, а не с первой.

Проверка

Дефект сначала воспроизведён тестом на текущем dev — падал ровно с той ошибкой из прода:

Client never reconnected after OnConnected threw
(invocations: 1, connect error: Client has been disconnected. Call Connect() to reconnect.)

TestUOnConnectedHandlerFailure (mock rippled) закрывает четыре свойства:

Тест Что проверяет
TestTransientOnConnectedFailureRecovers разовый сбой обработчика → клиент переподключается, OnConnected поднимается снова
TestClientIsUsableAfterOnConnectedFailure после восстановления запрос проходит (флаг постоянного отключения не выставлен)
TestPermanentlyFailingOnConnectedHandlerStops вечно падающий обработчик не крутится бесконечно и останавливается в пределах MaxReconnectAttempts
TestOnConnectedFailureIsReportedThroughOnError причина доезжает до OnError с connectHandlerError

Локально, до и после:

  • юнит-тесты по всему решению — 1015 / 1015 зелёные (dotnet test --filter "TestU");
  • интеграционные против стенда .ci-config/docker-compose.ci.yml (xrpld 3.2.0 в Docker) — 219 пройдено, 40 пропущено, 0 упало (dotnet test --filter "TestI"), прогон повторён на финальной сборке.

Версия: 10.10.0.010.10.1.0.

Summary by CodeRabbit

  • Bug Fixes

    • Improved connection recovery when connection-established handlers fail.
    • Clients now retry transient failures, report errors through status callbacks, and stop after the configured retry limit.
    • Connection waits now fail immediately when reconnection is no longer possible.
    • Improved recovery when switching to an unavailable server.
    • WebSocket send failures are now traceable while preserving existing timeout behavior.
  • Tests

    • Added coverage for recovery, server switching, client usability, error reporting, and retry termination.
  • Documentation

    • Added release notes for version 10.10.1.0.

…гда (10.10.1.0)

Connection.OnceOpen ловил любое исключение потребительского обработчика
OnConnected и звал Disconnect() - пользовательский путь отключения:
_permanentlyDisconnected = true плюс ClearReconnectState(). После этого
клиент мёртв навсегда: реконнект-цикл не перезапускается, новых сокетов
не открывается, OnConnected больше не поднимается, а любой запрос падает
с NotConnectedException. Наружу при этом не выходило ничего - ни события,
ни лога.

Триггер - самый штатный сценарий: перезапуск ноды. OnConnected - типовое
место восстановления подписок (SDK их после реконнекта не восстанавливает),
а нода принимает TCP раньше, чем начинает отвечать на запросы, поэтому
первый subscribe уходит в RequestTimeout и падает. В проде так встал флот
ботов - по четыре часа тишины после обновления ноды.

Теперь сбой обработчика трактуется как отказ СОЕДИНЕНИЯ, а не как воля
пользователя: сокет сносится, дальше работает обычный реконнект с
экспоненциальным бэкоффом, флаг постоянного отключения не ставится.

Защита от вечного цикла: OnceOpen чистит реконнект-состояние до вызова
обработчика, поэтому счётчик попыток самого цикла обнуляется на каждом
успешном TCP-коннекте и сойтись не может. Подряд идущие сбои обработчика
считаются отдельно (_connectHandlerFailures, сбрасывается при успешном
прогоне обработчика, в Connect() и в ChangeServer()); при достижении
MaxReconnectAttempts с StopAfterMaxAttempts клиент осознанно сдаётся -
сразу понятный NotConnectedException вместо пятиминутного молчания.

Причина теперь наблюдаема: исключение поднимается через OnError с
errorMessage = "connectHandlerError" и через OnConnectionStatus.

Попутно: WebSocketClient.SendMessageAsync (async void, вызывается без
await) больше не глотает ошибку отправки молча - она трассируется.
Поведение не меняется, запрос по-прежнему ограничен RequestTimeout, но
причина перестала быть невидимой при диагностике.

TestUOnConnectedHandlerFailure закрывает все четыре свойства против
mock rippled: восстановление после разового сбоя, работоспособность
клиента после восстановления, отчёт через OnError и остановка вечно
падающего обработчика.
@coderabbitai

coderabbitai Bot commented Aug 3, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

You’ve reached a temporary PR review limit under our Fair Usage Limits Policy.

Your recent review volume is higher than typical usage, so adaptive limits are currently applied.

Next review available in: 42 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: c86db589-9d40-483f-a654-28d631a9a77f

📥 Commits

Reviewing files that changed from the base of the PR and between 120a5ba and e31edeb.

📒 Files selected for processing (1)
  • Tests/Xrpl.Tests/CreateMockRippled.cs
📝 Walkthrough

Walkthrough

The client now bounds OnConnected handler failures, reports them, and protects reconnect-loop ownership. ChangeServer recovery and connection-wait errors are covered by tests. WebSocket send failures now produce debug traces and OnError events. The package version is 10.10.1.0.

Changes

Connection recovery

Layer / File(s) Summary
Bounded connection failure flow
Xrpl/Client/connection.cs
The client tracks consecutive OnConnected failures, reports errors, retries through the reconnect loop, detects permanent disconnection during waits, and disconnects after the configured limit.
Server switching and reconnect ownership
Xrpl/Client/connection.cs
ChangeServer resets stale connection state. Reconnect loops prevent retired loops from modifying active reconnect state.
Recovery regression coverage
Tests/Xrpl.Tests/Client/TestUOnConnectedHandlerFailure.cs, Tests/Xrpl.Tests/Client/TestUChangeServerFailure.cs, Tests/Xrpl.Tests/CreateMockRippled.cs
Tests cover handler recovery, request usability, retry termination, error reporting, delayed server recovery, and recovery after user disconnect. Mock servers now support explicit stopping.
Send diagnostics and release metadata
Xrpl/Client/WebSocketClient.cs, CHANGES.md, Xrpl/Xrpl.csproj
WebSocket send exceptions produce debug output and invoke OnError without triggering reconnection. Release notes and package metadata use version 10.10.1.0.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant OnConnected
  participant ReconnectLoopAsync
  participant NewServer
  Client->>OnConnected: Invoke after connection
  OnConnected-->>Client: Throw exception
  Client->>ReconnectLoopAsync: Start bounded reconnect
  ReconnectLoopAsync->>NewServer: Attempt connection
  NewServer-->>ReconnectLoopAsync: Accept or reject connection
  ReconnectLoopAsync-->>Client: Restore connection or report permanent failure
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 40.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly identifies the main fix: preventing an OnConnected exception from permanently shutting down the client.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/onconnected-disconnect-analysis-85435b

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

@Platonenkov

Copy link
Copy Markdown
Collaborator Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 3, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@Platonenkov

Copy link
Copy Markdown
Collaborator Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Aug 3, 2026

Copy link
Copy Markdown
✅ Action performed

Full review finished.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@Xrpl/Client/connection.cs`:
- Around line 1690-1704: The give-up path loses its detailed notification and
does not promptly unblock waiting callers. In the giveUp branch of the
connection failure handler, call SetConnectionState with the detailed
disconnected message before await Disconnect(), then update
WaitForConnectionAsync to check _permanentlyDisconnected on every loop iteration
and throw NotConnectedException immediately; preserve the existing
reconnect-attempt guard and messages for other termination cases.
- Around line 1716-1721: The failed-connection cleanup must preserve the
original socket identity instead of potentially closing a newly connected
socket. In the cleanup block, use failedSocket as the socketToClose value and
clear ws only when it still references failedSocket; update the logic around the
disconnect lock without altering unrelated reconnect behavior.

In `@Xrpl/Client/WebSocketClient.cs`:
- Around line 311-314: Replace the DEBUG-only Debug.WriteLine in the WebSocket
send failure handler with a production-visible error path. Route the exception
through the WebSocket error callback and connection.OnError, or the project’s
established production logger, while preserving the existing diagnostic message
and ensuring fire-and-forget send failures are reported immediately.

In `@Xrpl/Xrpl.csproj`:
- Line 17: Update the PackageVersion property in the Xrpl.AddressCodec,
Xrpl.BinaryCodec, and Xrpl.Keypairs project files from 10.9.0.0 to 10.10.1.0,
matching the release version already declared in Xrpl.csproj.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 631b8cf1-fb0a-4e15-8417-218a5e5e9302

📥 Commits

Reviewing files that changed from the base of the PR and between f17fc11 and 9fb35be.

📒 Files selected for processing (5)
  • CHANGES.md
  • Tests/Xrpl.Tests/Client/TestUOnConnectedHandlerFailure.cs
  • Xrpl/Client/WebSocketClient.cs
  • Xrpl/Client/connection.cs
  • Xrpl/Xrpl.csproj

Comment thread Xrpl/Client/connection.cs
Comment thread Xrpl/Client/connection.cs Outdated
Comment thread Xrpl/Client/WebSocketClient.cs Outdated
Comment thread Xrpl/Xrpl.csproj
1. Детальная причина отказа терялась. SetConnectionState после Disconnect()
   - no-op: Disconnect() уже перевёл состояние в Disconnected, а
   SetConnectionState уведомляет только при смене состояния. Подписчик видел
   "Disconnected by user request." вместо причины. Уведомление перенесено
   перед Disconnect().

2. WaitForConnectionAsync проверял _permanentlyDisconnected один раз на входе
   и никогда - внутри цикла ожидания. Ждущий в Connect() вызывающий досиживал
   весь ConnectionAcquisitionTimeout (по умолчанию 5 минут) и получал общий
   TimeoutException вместо реальной причины. Проверка перенесена в цикл.

3. Личность сокета при разборе. WebSocketClient.Connect вызывает OnConnect без
   await, поэтому connect-лок отпускается, пока обработчик ещё выполняется, и
   к моменту разбора ws может указывать уже на новый сокет. Закрывается ровно
   тот сокет, для которого падал обработчик; ws обнуляется, только если всё
   ещё ссылается на него; если сокет уже не текущий - реконнект-состояние
   принадлежит новому соединению и не трогается.

   При этом тест на вечно падающий обработчик вскрыл гонку в самом фиксе:
   проверка "цикл реконнекта уже работает" гонится с выходом этого цикла - он
   прерывается сразу, как сокет отрапортовал Open, то есть ДО того, как
   обработчик успел упасть. Проигрыш гонки оставлял клиента без реконнекта -
   ровно тот клин, который правится. Вместо проверки путь теперь безусловно
   забирает владение: гасит текущий цикл и запускает новый, а пришедший позже
   OnceClose видит живой цикл и корректно отступает. Побочно: восстановление
   стало занимать сотни миллисекунд вместо десятков секунд.

4. Ошибка отправки: Debug.WriteLine вырезается без DEBUG, которого нет в
   релизной сборке. Мёртвый колбэк ошибки WebSocketClient (его никто не звал и
   не подписывал) теперь доносит исключение до Connection.OnError с
   errorMessage = "socketSendError". Только уведомление: неудачная отправка
   сама по себе не означает потерю соединения, реконнект не запускается,
   запрос по-прежнему ограничен RequestTimeout.

Отклонено: подъём версий Xrpl.AddressCodec / Xrpl.BinaryCodec / Xrpl.Keypairs
до 10.10.1.0. Эти пакеты в этом PR не менялись, версионируются независимо и
уже расходятся с основным на NuGet осознанно (Xrpl 10.10.0 против base 10.9.0;
так же было в 10.9.1.0 и 10.10.0.0). Публикация идёт с --skip-duplicate, так
что неизменные пакеты просто пропускаются, а подъём выложил бы побайтово те же
артефакты под новым номером.

Тест TestPermanentlyFailingOnConnectedHandlerStops усилен: теперь проверяет,
что ожидающий вызывающий разблокируется именно NotConnectedException.
@Platonenkov

Copy link
Copy Markdown
Collaborator Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 3, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@Platonenkov

Copy link
Copy Markdown
Collaborator Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Aug 3, 2026

Copy link
Copy Markdown
✅ Action performed

Full review finished.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (1)
Xrpl/Client/connection.cs (1)

1657-1674: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Consider narrowing the try block to the consumer OnConnected invocation.

The try block also covers SetConnectionState at Line 1666. SetConnectionState invokes the consumer OnConnectionStatus event. If an OnConnectionStatus subscriber throws, the catch classifies the failure as an OnConnected handler failure. The client then increments _connectHandlerFailures, reports connectHandlerError, and tears down a healthy socket. The reported reason is then wrong, and a broken status subscriber can drive the bounded retry to the give-up state.

♻️ Proposed narrowing
         try
         {
             connectionManager.ResolveAllAwaiting();
             if (OnConnected is not null)
             {
                 await OnConnected?.Invoke();
             }
-
-            Interlocked.Exchange(ref _connectHandlerFailures, value: 0);
-            SetConnectionState(XrpConnectionState.Connected, message: $"Connected {url}");
         }
         catch (Exception error)
         {
             connectionManager.RejectAllAwaiting(error);
             await OnConnectHandlerFailedAsync(connectedSocket, error);
             return; // Don't start ping timer if connection failed
         }
+
+        Interlocked.Exchange(ref _connectHandlerFailures, value: 0);
+        SetConnectionState(XrpConnectionState.Connected, message: $"Connected {url}");
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Xrpl/Client/connection.cs` around lines 1657 - 1674, Restrict the try/catch
in the connection-success flow to ResolveAllAwaiting and the OnConnected
invocation only. Move resetting _connectHandlerFailures and SetConnectionState
outside that catch so exceptions from OnConnectionStatus are not handled by
OnConnectHandlerFailedAsync or used to tear down the healthy socket; preserve
the existing failure handling for OnConnected errors.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@Tests/Xrpl.Tests/Client/TestUOnConnectedHandlerFailure.cs`:
- Around line 25-59: Add teardown support for the mock server created in
MyTestInitialize by retaining its Server instance and exposing a method that
calls Server.Stop(). Update MyTestCleanup to invoke this teardown after
disconnecting and clearing _client, ensuring the listener is stopped for every
test.

In `@Xrpl/Client/connection.cs`:
- Around line 1775-1783: Guard reconnect-loop cleanup so a retired loop cannot
modify the session created by StartReconnectLoop. In the reconnect-loop worker
and its tail cleanup, capture the loop-owned reconnect state and only update
_reconnectMode, _reconnectAttempts, or dispose _reconnectCts when _reconnectLoop
still identifies that same active loop; otherwise leave the replacement session
untouched. Preserve StopReconnectLoop’s cancellation behavior while making
teardown ownership-aware.

---

Nitpick comments:
In `@Xrpl/Client/connection.cs`:
- Around line 1657-1674: Restrict the try/catch in the connection-success flow
to ResolveAllAwaiting and the OnConnected invocation only. Move resetting
_connectHandlerFailures and SetConnectionState outside that catch so exceptions
from OnConnectionStatus are not handled by OnConnectHandlerFailedAsync or used
to tear down the healthy socket; preserve the existing failure handling for
OnConnected errors.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: febcc39f-611c-4d57-8d6e-d4776825ddf3

📥 Commits

Reviewing files that changed from the base of the PR and between f17fc11 and cbc7e7a.

📒 Files selected for processing (5)
  • CHANGES.md
  • Tests/Xrpl.Tests/Client/TestUOnConnectedHandlerFailure.cs
  • Xrpl/Client/WebSocketClient.cs
  • Xrpl/Client/connection.cs
  • Xrpl/Xrpl.csproj

Comment thread Tests/Xrpl.Tests/Client/TestUOnConnectedHandlerFailure.cs
Comment thread Xrpl/Client/connection.cs
1. Реконнект-цикл писал в чужую сессию. StopReconnectLoop() отменяет токен, но
   не дожидается цикла, поэтому снятый цикл мог добраться до тела или хвоста
   уже после того, как установлена замена, и обнулить _reconnectMode живого
   цикла, сбросить его _reconnectAttempts или выбросить его _reconnectCts.
   Дефект существовал и раньше (RetireCurrentSessionAndReconnectAsync снимает
   циклы ровно так же), но путь сбоя OnConnected делает его гораздо более
   достижимым. ReconnectLoopAsync теперь принимает CancellationTokenSource,
   которым владеет, и трогает общее состояние, только пока этот источник
   остаётся активным.

2. Mock-сервер в тестах не останавливался: CreateMockRippled.Start() создавал
   Server (его конструктор поднимает слушателя) и терял ссылку. Добавлен
   Stop(), TestUOnConnectedHandlerFailure зовёт его в TestCleanup.
@Platonenkov

Copy link
Copy Markdown
Collaborator Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 3, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@Platonenkov

Copy link
Copy Markdown
Collaborator Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Aug 3, 2026

Copy link
Copy Markdown
✅ Action performed

Full review finished.


Your included review limit is currently reached under our Fair Usage Limits Policy. Your recent PR review activity is in the 95th percentile or higher among CodeRabbit users, so adaptive limits apply. This review may still proceed through usage-based billing if eligible. Your next included review will be available in 26 minutes.

Тот же класс клина, что и основной фикс PR, но другой путь. Найдено при
прогоне Blazor-демо: переключение селектора сети на выключенную ноду.

ChangeServer ставил ГЛОБАЛЬНЫЙ _isIntentionalDisconnect = true, чтобы
отфильтровать поздние колбэки уходящего сокета, а сбрасывался этот флаг
только в OnceOpen. Если новый сервер не поднимался, OnceOpen не выполнялся
никогда: OnConnectionFailed читал отказ НОВОГО соединения как отключение по
воле пользователя, писал "Connection closed permanently.", не запускал
реконнект-цикл, и дальше всё - включая сам ChangeServer - падало с
вводящим в заблуждение "No connection attempt in progress. Call Connect()
first.". Поднятие сервера потом ничего не меняло: клиент был мёртв.

Поздние колбэки теперь фильтруются исключительно по-сокетному отслеживанием,
которое здесь и так уже было (_userInitiatedSockets плюс собственный флаг
сокета, выставляемый в RetireOldSessionAsync) - ровно так же, как всегда
делал путь ping-timeout / network-drop; в его коде даже висит комментарий,
предостерегающий от глобального флага именно по этой причине. Дополнительно
флаг явно гасится на входе, чтобы ChangeServer после пользовательского
Disconnect() не подавлялся оставшимся от него значением.

Дефект существовал до этого PR - проверено дважды: ни одна из строк этого
пути им не менялась, и одинаковый диагностический тест на чистом dev
(f17fc11) даёт байт-в-байт тот же вывод.

TestUChangeServerFailure закрывает оба случая: клиент доходит до нового
сервера, когда тот появляется, с предшествующим Disconnect() и без него.
Оба теста падают на dev и проходят с фиксом.
@Platonenkov

Copy link
Copy Markdown
Collaborator Author

Добавлен второй фикс того же класса — 120a5ba.

Как нашёлся. Прогон Blazor-демо (Tests/TestsClients/Blazor-WebAssembly): переключение селектора сети на выключенную локальную ноду. В логе — Connection closed permanently. и Failed to change server: No connection attempt in progress. Call Connect() first., повторных попыток нет, поднятие ноды потом ничего не меняет.

Причина. ChangeServer ставил глобальный _isIntentionalDisconnect = true, чтобы отфильтровать поздние колбэки уходящего сокета, а сбрасывался этот флаг только в OnceOpen. Если новый сервер не поднимался, OnceOpen не выполнялся никогда: OnConnectionFailed читал отказ нового соединения как отключение по воле пользователя, реконнект-цикл не стартовал, и дальше всё падало с вводящим в заблуждение No connection attempt in progress.

Исправление. Поздние колбэки фильтруются только по-сокетным отслеживанием, которое здесь и так уже было (_userInitiatedSockets + собственный флаг сокета из RetireOldSessionAsync) — ровно как в пути ping-timeout / network-drop, где в коде даже висит комментарий, предостерегающий от глобального флага именно по этой причине. Плюс флаг явно гасится на входе, чтобы ChangeServer после пользовательского Disconnect() не подавлялся его остатком.

Дефект существовал до этого PR, проверено дважды: ни одна строка этого пути PR-ом не менялась, и одинаковый диагностический тест на чистом dev (f17fc11) даёт байт-в-байт тот же вывод.

Проверка. TestUChangeServerFailure — оба теста падают на dev, проходят с фиксом. Юнит-тесты 1017/1017, интеграционные против стенда (xrpld 3.2.0 в Docker) 219 пройдено / 40 пропущено / 0 упало. В самом демо сценарий теперь проходит целиком:

[09:51:48.311] [Connecting] ChangeServer: Switching to ws://localhost:6006...
[09:51:52.692] [RestoringConnection] Reconnecting in 4,8 seconds... (attempt #1)
[09:51:59.882] [RestoringConnection] Reconnecting in 6,9 seconds... (attempt #2)
[09:52:09.119] [RestoringConnection] Reconnecting in 5,1 seconds... (attempt #3)
   <- нода поднята
[09:52:14.574] Server changed successfully to: ws://localhost:6006
[09:52:14.577] [Connected] Connected ws://localhost:6006

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 3, 2026

Copy link
Copy Markdown

@Platonenkov Принял контекст второго исправления. Проверю commit 120a5ba, включая пути ChangeServer, OnConnectionFailed, RetireOldSessionAsync и тесты TestUChangeServerFailure.


✏️ Learnings added
Learnt from: Platonenkov
URL: https://github.com/StaticBit-io/XrplCSharp/pull/72

Timestamp: 2026-08-03T12:53:22.201Z
Learning: In the C# connection client, `ChangeServer` must not use the global `_isIntentionalDisconnect` flag to ignore callbacks from the retired socket. A failed connection to the new server can leave that flag set and suppress normal reconnect handling. Use per-socket retirement tracking (`_userInitiatedSockets` and the socket-specific flag from `RetireOldSessionAsync`) for retired-socket callbacks, and clear `_isIntentionalDisconnect` when `ChangeServer` starts.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@Platonenkov

Copy link
Copy Markdown
Collaborator Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 3, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

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)
Xrpl/Client/connection.cs (1)

1784-1792: 🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

Make reconnect-loop ownership checks atomic with state changes.

ReferenceEquals(_reconnectCts, ownCts) is only a time-of-check. A replacement loop can start after Line 2015, Line 2104, or Line 2142 and before the following write.

A retired loop can then reset the replacement loop’s attempt counter or clear its reconnect mode. It can also retire the replacement session and clear its ws reference in the later session-cleanup block.

Serialize loop replacement and loop-owned state changes with one reconnect lifecycle lock. Check ownership and mutate _reconnectAttempts, _reconnectMode, _reconnectCts, _activeSession, and ws in that protected operation.

Based on learnings: ReconnectLoopAsync can be replaced while its prior task is still completing, so ownership must prevent the retired loop from changing replacement-session state.

Also applies to: 2015-2020, 2104-2108, 2139-2158

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Xrpl/Client/connection.cs` around lines 1784 - 1792, Protect reconnect-loop
replacement and all loop-owned state transitions with a single reconnect
lifecycle lock. In ReconnectLoopAsync and the related cleanup paths around the
referenced ownership checks, perform the ReferenceEquals validation and
mutations of _reconnectAttempts, _reconnectMode, _reconnectCts, _activeSession,
and ws atomically under that lock, so retired loops cannot alter
replacement-loop or replacement-session state. Apply the same synchronization to
the replacement flow using StopReconnectLoop, _reconnectLoop, and
StartReconnectLoop.

Source: Learnings

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@Tests/Xrpl.Tests/CreateMockRippled.cs`:
- Around line 71-96: Synchronize CreateMockRippled startup and shutdown by
protecting _server access with a shared lock and recording that Stop() has begun
before checking the current server. Update Start() so a server created after
shutdown is immediately stopped rather than assigned to _server, while
preserving the existing cleanup and exception behavior.

---

Outside diff comments:
In `@Xrpl/Client/connection.cs`:
- Around line 1784-1792: Protect reconnect-loop replacement and all loop-owned
state transitions with a single reconnect lifecycle lock. In ReconnectLoopAsync
and the related cleanup paths around the referenced ownership checks, perform
the ReferenceEquals validation and mutations of _reconnectAttempts,
_reconnectMode, _reconnectCts, _activeSession, and ws atomically under that
lock, so retired loops cannot alter replacement-loop or replacement-session
state. Apply the same synchronization to the replacement flow using
StopReconnectLoop, _reconnectLoop, and StartReconnectLoop.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 2df686f3-4407-459e-87df-eac4524def91

📥 Commits

Reviewing files that changed from the base of the PR and between cbc7e7a and 120a5ba.

📒 Files selected for processing (5)
  • CHANGES.md
  • Tests/Xrpl.Tests/Client/TestUChangeServerFailure.cs
  • Tests/Xrpl.Tests/Client/TestUOnConnectedHandlerFailure.cs
  • Tests/Xrpl.Tests/CreateMockRippled.cs
  • Xrpl/Client/connection.cs
🚧 Files skipped from review as they are similar to previous changes (1)
  • Tests/Xrpl.Tests/Client/TestUOnConnectedHandlerFailure.cs

Comment thread Tests/Xrpl.Tests/CreateMockRippled.cs
Start() выполняется на фоновом потоке, поэтому Stop() из TestCleanup мог
увидеть _server == null до присваивания и оставить живого слушателя.
Останов теперь фиксируется под локом: старт, завершившийся после него,
гасит собственный Server вместо того, чтобы его сохранить.
@Platonenkov

Copy link
Copy Markdown
Collaborator Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 3, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

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