Skip to content

Refuse to schedule a reconnect while an attempt is in flight - #23

Merged
calebtt merged 1 commit into
masterfrom
fix/reconnect-schedule-race
Sep 26, 2026
Merged

calebtt merged 1 commit into
masterfrom
fix/reconnect-schedule-race

Conversation

@calebtt

@calebtt calebtt commented Sep 26, 2026 •

Copy link
Copy Markdown
Owner

Summary

Closes #22. This is a follow-up to #19.

CI on master e7bedee failed RegistrationHealthCheckInFlightTests with more reconnect attempts than failures (4 for 3, and 4 for 2).

PerformHealthCheck reads _registrationState without the lock. If it read TemporaryFailure just before a scheduled attempt started, its ScheduleReconnection() call ran with that attempt already in flight and scheduled another one. That attempt had no failure behind it, and in extended mode the in-flight agent was stopped before it could report its own result. #19 closed this at the health-check level; the remaining window was between the unlocked read and the lock.

Changes

  • SipClient.ScheduleReconnection() also refuses while RegistrationState is Registering, checked under the lock. It is now internal so the race can be tested directly.
  • Tests, TestSetup: the test assembly raises the thread-pool minimum when it loads. SIPSorcery blocks thread-pool threads during each registration attempt, and with test classes running in parallel on a small runner, the pool grew by about one thread a second. Timers then fired ~1 s late, and the registration timing tests failed. This was reproduced by pinning the tests to one CPU with taskset. Test setup only; library behavior is unchanged.

Tests

  • New RegistrationScheduleRaceTests (deterministic): calling ScheduleReconnection() while an attempt is in flight starts nothing, and the attempt still reports its own failure, in both modes. Without the fix, both cases fail with Expected 0, Actual 1.
  • dotnet test: 97 passed.
  • Suite pinned to one CPU (taskset -c 0): with the thread-pool minimum, 5 of 5 runs passed (97/97 each). Without it, 4 of 7 runs had a timing failure (a gap of ~1 s where 100–800 ms was expected).

🤖 Generated with Claude Code

The health check reads the registration state without the lock. When it
read TemporaryFailure just before a scheduled attempt started, its
ScheduleReconnection() call ran with that attempt already in flight and
scheduled another one: an attempt no failure asked for, and in extended
mode the in-flight agent was stopped before it could report its result.
CI on master caught it as more reconnect attempts than failures.

ScheduleReconnection() now also refuses while RegistrationState is
Registering, checked under the lock. New deterministic test: calling it
during an in-flight attempt starts nothing and the attempt still reports
its failure (without the fix: 1 attempt, expected 0).

The registration timing tests also failed on a starved runner (reproduced
by pinning the tests to one CPU): SIPSorcery blocks thread-pool threads
during each attempt, and with test classes running in parallel the pool
grew by about one thread a second, so timers fired ~1 s late. The test
assembly now raises the thread-pool minimum when it loads.

Closes #22

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@calebtt
calebtt merged commit c55e8eb into master Sep 26, 2026
2 checks passed
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.

Health check can schedule a reconnect while a registration attempt is in flight (race)

1 participant