Skip to content

fix(polling): drop stale in-flight check results on cancel/resubscribe - #148

Open
mem-5514-tahara wants to merge 4 commits into
OutdatedGuy:mainfrom
mem-5514-tahara:fix/subscription-version-guard
Open

mem-5514-tahara wants to merge 4 commits into
OutdatedGuy:mainfrom
mem-5514-tahara:fix/subscription-version-guard

Conversation

@mem-5514-tahara

Copy link
Copy Markdown

Summary

Split out from #139 per review feedback, with _cancelGeneration renamed to _subscriptionVersion.

If a listener cancels its subscription while a connectivity check is still running, that check's result belongs to the old subscription. Without a guard:

  • it could still get emitted to a brand-new subscriber that started its own fresh check moments later, or
  • it could restart the polling timer even though nobody is listening anymore.

This adds a _subscriptionVersion counter, bumped only when a listener cancels. _maybeEmitStatusUpdate snapshots it before the check starts and skips both the emit and the next timer reschedule if it changed while the check was in-flight.

Test plan

  • dart analyze — no issues
  • dart format --set-exit-if-changed — clean
  • dart test — all tests pass, including two new regression tests:
    • cancelled in-flight check does not emit stale result to new subscriber
    • does not restart the timer if cancelled while a check is in-flight

If a listener cancels while a connectivity check is still running, that
check's result belonged to the old subscription. Without a guard, it
could still get emitted to a brand-new subscriber that started its own
fresh check moments later, or restart the polling timer even though
nobody is listening anymore.

Track a _subscriptionVersion counter, bumped only when a listener
cancels. _maybeEmitStatusUpdate snapshots it before the check starts and
skips the emit (and the next timer reschedule) if it changed while the
check was in-flight.

@OutdatedGuy OutdatedGuy left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Few changes

Comment thread lib/src/internet_connection.dart Resolved
Comment thread lib/src/internet_connection.dart Resolved
Comment thread lib/src/internet_connection.dart Resolved
mem-5514-tahara added a commit to mem-5514-tahara/internet_connection_checker_plus that referenced this pull request Aug 18, 2026
Removing _generation entirely (previous commit) turned out to be wrong:
unlike _cancelGeneration, this guard protects backoff-specific mutable
state (_currentBackoffDelay/_backoffNeedsReset), which only exists in
this branch. Without it, a setIntervalAndResetTimer call (or a cancel)
racing an in-flight failing check silently undoes the reset it just
applied — the next failure incorrectly grows the delay instead of
starting over at the fresh initial delay.

Confirmed via a reproduction: removing the guard makes the added
regression test fail (checkCount climbs from 3 to 5, and the delay
grows to 100ms instead of staying at the reset 50ms).

Renamed from _generation to _timerVersion with a plainer doc comment,
per review feedback. _cancelGeneration (renamed _subscriptionVersion)
remains split out as OutdatedGuy#148, since that one has no backoff dependency.
Move the hasListener check right after the await so isStale no longer
needs to recheck it, and drop comments already covered by the
_subscriptionVersion doc comment.

Copilot AI 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.

Pull request overview

Adds subscription-version tracking to discard connectivity results from cancelled subscriptions.

Changes:

  • Guards status emissions against stale in-flight checks.
  • Adds cancellation and resubscription regression tests.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

File Description
lib/src/internet_connection.dart Adds stale-result detection during polling.
test/internet_connection_test.dart Tests cancellation during in-flight checks.
Suppressed comments (1)

lib/src/internet_connection.dart:304

  • The version is advanced only after awaiting trigger-stream cleanup. If that subscription has asynchronous cancellation, a replacement listener can attach and the old connectivity check can finish while the version is still unchanged, allowing the stale result to be emitted to the replacement subscriber. Invalidate the generation synchronously before the first await.
  Future<void> _handleStatusChangeCancel() async {
    await _triggerSubscription?.cancel();
    _triggerSubscription = null;
    _subscriptionVersion++;

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread lib/src/internet_connection.dart Resolved
A stale check still reached the unconditional Timer(...) at the end of
_maybeEmitStatusUpdate, overwriting _timerHandle with an untracked
timer while the fresh subscription's own timer kept running
unreferenced. Return immediately on a version mismatch so stale work
skips both emission and rescheduling.

Also bump _subscriptionVersion synchronously before the first await in
_handleStatusChangeCancel, so a resubscribe that races the in-flight
trigger-subscription cancellation can't observe the stale version.
@mem-5514-tahara

Copy link
Copy Markdown
Author

Hi @OutdatedGuy, just a gentle follow-up on this PR as well!

The Copilot review feedback regarding early return on stale checks has been applied, and all tests/checks are green.

Whenever you have time to take a look at both #148 and #139, please let me know. Thanks!

Copilot AI 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.

🟢 Approval recommended

The reviewed changes are focused, tested, and have no unresolved issues.

Review details
  • Files reviewed: 2/2 changed files
  • Comments generated: 0 new
  • Review effort level: Balanced

Comment thread test/internet_connection_test.dart Resolved
Comment thread test/internet_connection_test.dart Resolved
The `option` local was referenced only once, in `customCheckOptions`.
The `option:` argument inside `customConnectivityCheck` comes from the
callback's own `opt` parameter, not this variable. Inline it to match
how every other test in the file builds its check options.

Copilot AI 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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@OutdatedGuy OutdatedGuy left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

LGTM!

Copilot AI 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.

🔵 Needs a closer look

The cancellation test passes even with the pre-fix implementation because it checks connectivity calls rather than timer creation, leaving the unintended polling-timer restart undetected.

No open issues 🎉

1 previously missed issue

In code that hasn't changed since last review

Medium severity Test does not detect polling timer restart after cancellation

test/​internet_connection_test.dart:357

This assertion also passes with the pre-fix implementation: the cancelled check schedules a timer, but its callback returns at if (!_statusController.hasListener) return; before invoking another connectivity check, so checkCount remains 1. The test therefore does not detect the timer restart it is intended to prevent. Track timer creation (for example, with ZoneSpecification.createTimer) and assert that completing the cancelled check schedules no polling timer.

Review effort: Balanced

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.

3 participants