Skip to content

fix(tests): мок-сервер переставал принимать соединения после одного обрыва - #84

Merged
Platonenkov merged 2 commits into
devfrom
claude/mock-accept-loop-flake-b734b6
Aug 12, 2026
Merged

fix(tests): мок-сервер переставал принимать соединения после одного обрыва#84
Platonenkov merged 2 commits into
devfrom
claude/mock-accept-loop-flake-b734b6

Conversation

@Platonenkov

Copy link
Copy Markdown
Collaborator

Summary

Чинит флейк TestConcurrentChangeServerKeepsClientRecoverable, из-за которого unit упал на push-прогоне dev (run 31613419252), тогда как тот же коммит в PR-прогоне прошёл. Это единственная красная галка, оставшаяся на релизном PR #76.

Виноват не клиент, а мок-сервер.

Причина

Server.connectionCallback перезапускал приём соединений последней строкой try-блока:

OnClientConnected(this, new OnClientConnectedHandler(client));

// Start to accept incomming connections again
GetSocket().BeginAccept(connectionCallback, null);   // ← только если выше ничего не бросило
}
catch (Exception Exception)
{
    Debug.WriteLine("An error has occured while trying to accept a connecting client.\n\n{0}", Exception.Message);
}

Любое исключение выше по телу колбэка — и BeginAccept не вызывается уже никогда: приём соединений обрывается навсегда. Пир, рвущий соединение во время handshake, попадает ровно туда — clientSocket.Receive бросает Connection reset by peer, а конкурентные ChangeServer/Disconnect в тесте такие обрывы и порождают.

Почему это читалось как баг клиента: слушающий сокет остаётся связанным. Порт числится занятым, TCP-соединения ядро принимает в backlog, connect со стороны клиента успешен — и дальше тишина. Клиент ждёт handshake до своего таймаута и ретраится, пока не выйдет 30-секундное окно теста. Отсюда 2m06s у упавшего прогона против 10s у обычного и сообщение, обвиняющее клиент в том, что он «не дошёл до живого сервера», — при том что сервер к этому моменту оглох.

Строка Connection reset by peer: An error has occured while trying to accept a connecting client в CI-логе — это она и есть.

Доказательство

Локально флейк не воспроизвёлся ни разу за 12 прогонов подряд, поэтому доказательство построено не на статистике, а на прямом тесте дефекта: соединение рвётся на handshake через LingerOption(true, 0) (тот самый RST), после чего обычный клиент обязан подключиться.

до фикса после
TestAbortedHandshakeLeavesTheMockAccepting ❌ 8s
TestRepeatedAbortedHandshakesLeaveTheMockAccepting ❌ 2s

Оба падения — с той же строкой в выводе, что в CI. После фикса оба проходят за 0.6s суммарно.

Фикс

  • BeginAccept перезапускается безусловно после колбэка, чем бы тот ни кончился
  • ObjectDisposedException обрабатывается отдельно и без перезапуска: он означает, что Stop() закрыл listener, и повторная попытка только бросит снова
  • сокет, чей handshake упал, закрывается, а не течёт до конца процесса

Чтобы это не повторилось диагностически

TestUtils.MockCompletesHandshake пробует настоящий WS-upgrade и проверяет ответ 101; упавший reconnect-тест теперь вызывает её и пишет результат в сообщение — глухой мок больше не будет прочитан как баг клиента.

Обычный TCP-connect для такой пробы не годится: против связанного, но не принимающего сокета он успешен и не доказывает ничего — ровно в эту ловушку всё и провалилось. Сама проба покрыта тремя случаями: живой мок, свободный порт и слушающий сокет, который никогда не принимает.

Проверка

  • 905 юнит-тестов зелёные
  • TestUReconnectSessionRaces — 10 прогонов подряд без падений
  • новые тесты падают на старом колбэке и проходят на новом

Затронута только тестовая инфраструктура — код SDK не менялся.

…брыва

Причина флейка TestConcurrentChangeServerKeepsClientRecoverable: unit упал на
push-прогоне dev, тогда как тот же коммит в PR-прогоне прошёл.

Server.connectionCallback вызывал BeginAccept последней строкой try-блока,
поэтому любое исключение выше по телу навсегда обрывало приём соединений. Пир,
рвущий соединение во время handshake — ровно то, что порождают конкурентные
ChangeServer/Disconnect — глушил мок именно так. Со стороны всё выглядело
исправно: слушающий сокет оставался связан, порт числился занятым, TCP-соединения
устанавливались ядром, и клиент видел сервер, который принял подключение и молчит.
Отсюда 2m06s у упавшего прогона против 10s у обычного и сообщение, обвиняющее
клиент в том, что он «не дошёл до живого сервера».

- BeginAccept перезапускается безусловно после колбэка, чем бы тот ни кончился;
  ObjectDisposedException обрабатывается отдельно и без перезапуска — он означает,
  что Stop() закрыл listener
- сокет, чей handshake упал, закрывается, а не течёт до конца процесса
- TestUMockRippledAcceptLoop фиксирует поведение: обрыв на handshake (RST через
  LingerOption(true, 0)) поодиночке и десять подряд, затем обычный клиент, который
  обязан подключиться. На старом колбэке оба падают, после фикса проходят за 0.6s
- TestUtils.MockCompletesHandshake: проба реальным WS-upgrade. Упавший
  reconnect-тест теперь сообщает, отвечает ли мок, — глухой сервер больше не
  будет прочитан как баг клиента. Проба покрыта на живом моке, свободном порту и
  слушающем сокете, который никогда не принимает
@coderabbitai

coderabbitai Bot commented Aug 12, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 8473dfdd-e139-4a7d-afe3-b4dbb50bc6da

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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

Правка чисто в тестовой инфраструктуре: код SDK не менялся, потребителю
пакета изменение не видно, поэтому в changelog релиза ему места нет.
Разбор причины остаётся в описании PR и в комментариях к самому коду.
@Platonenkov
Platonenkov enabled auto-merge August 12, 2026 16:06
@Platonenkov
Platonenkov added this pull request to the merge queue Aug 12, 2026
Merged via the queue into dev with commit a11ae93 Aug 12, 2026
4 checks passed
@Platonenkov
Platonenkov deleted the claude/mock-accept-loop-flake-b734b6 branch August 14, 2026 00:13
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