Repository navigation
Cancel the running watchdog timer when it is started again - #150
Merged
Merged
Conversation
Watchdog.start() replaced its timer without cancelling the old one. When a device ends the alert stream normally, alert_stream() reconnects and starts the watchdog again, so the previous timer was orphaned: pet() and stop() could no longer reach it, and 300s later it reset a healthy stream, dropping the packet in flight and taking the stream down for 10s. start() now cancels the running timer first. Each timer also carries a token, and an expiry is ignored unless it comes from the current timer, since cancel() cannot stop a timer that has already fired. A reentrant lock covers start, stop and expiry, which run on different threads. alert_stream() now logs at debug level when the device ends the stream. Fixes #149 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Contributor
|
The handler HikCamera passes is instant, but Watchdog is a A slow handler can held up pet(), start() and stop() on the stream Is this a better approach? |
baylanger
reviewed
Nov 6, 2026
| def test_pet_postpones_expiry(self): | ||
| self.watchdog.start() | ||
| for _ in range(3): | ||
| time.sleep(TIMEOUT / 2) |
Contributor
There was a problem hiding this comment.
TIMEOUT / 2 leaves a small margin of 0.1s before the 0.2s timer fires... a slow CI could make the test fail by chance, perhaps changing to TIMEOUT / 4 which leaves 0.15s makes sense but ... maybe CI runner are never "overloaded" ?!
A slow handler held the lock, so pet(), start() and stop() on the stream thread waited until it returned. Watchdog is a public class, so it should not impose that on handlers it does not control. The stale-expiry check and the clearing of the current timer stay under the lock; only the handler call moves out. With the handler no longer running under the lock, a plain Lock is enough. Also give test_pet_postpones_expiry more margin on a slow runner. Suggested by baylanger. Co-Authored-By: Pierre Belanger <baylanger@users.noreply.github.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Owner
Author
|
Thanks @baylanger, both good catches. Pushed in 8fdc1dc:
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #149.
Problem
Watchdog.start()replaced its timer without cancelling the one already running. When a device ends the alert stream normally,alert_stream()reconnects right away and starts the watchdog again. The previous timer was orphaned:pet()andstop()could no longer reach it, so it fired 300 s later and reset a healthy stream. The packet in flight was dropped and the stream was down for 10 s. The reporter's NVR (iDS-7608NXI-I2/8P/X) ended the stream 20 times in one hour, and each time a forced reset followed exactly 300 s later.Changes
Watchdog.start()cancels the running timer before starting a new one.pet()is now juststart().Timer.cancel()cannot stop a timer that has already fired, so without this, a timer replaced or stopped at the moment it expired could still reset the new connection.start(),stop()and the expiry check, which run on the stream thread and timer threads. The handler itself is called outside the lock, so a slow handler cannot hold uppet()andstop(), and the handler may call them itself (HikCamera.watchdog_handler()callsstop()).alert_stream()logs at debug level when the device ends the stream. These reconnects were completely silent before.Tests
test/test_watchdog.py: restarting replaces the running timer, stop after a restart cancels every timer, pet postpones expiry, expiries from replaced or stopped timers are ignored, the handler can callstop()without deadlocking, and a slow handler does not blockpet()andstop().StreamReliabilityTestCase: two connections that the device ends normally leave every watchdog timer cancelled.The new tests fail against the previous
watchdog.pyand pass with this change. The full suite passes (238 tests, 1 skipped). The timing-based watchdog tests passed 15 runs in a row.🤖 Generated with Claude Code