Skip to content

Join a subscription's handler before releasing the caller's reference - #181

Merged
lalinsky merged 1 commit into
mainfrom
fix/autounsubscribe-handler-self-join
Oct 5, 2026
Merged

lalinsky merged 1 commit into
mainfrom
fix/autounsubscribe-handler-self-join

Conversation

@lalinsky

@lalinsky lalinsky commented Oct 5, 2026

Copy link
Copy Markdown
Owner

A smaller fix for the autounsubscribe async basic functionality flake. It replaces #179.

The bug

An async subscription that reaches its autounsubscribe limit calls removeSubscriptionInternal on its own handler task, which drops the connection's reference. If the caller has already called sub.deinit(), that reference is the last one. The release runs destroy(), which joins the handler task group from inside that same task, so it never returns. Connection.deinit() then panics with 1 live subscription(s).

The fix

Subscription.deinit() closes the queue and joins the handler before it unsubscribes and drops the caller's reference. The handler can then never be the one that drops the last reference. destroy() still runs the same close and cancel, which do nothing on the second call.

Unlike #179, this leaves live_subscriptions and the panic for unreleased subscriptions unchanged. The other library code that drops subscription references runs on connection tasks that close() joins before that check.

Calling sub.deinit() from the subscription's own handler is documented as not allowed. It was already a self-join in most cases.

Testing

  • autounsubscribe tests: 7 failures in 10 runs on main, 0 in 50 runs with this change.
  • Full ./check.sh passes.

An async subscription that reaches its autounsubscribe limit removes
itself from the connection on its own handler task. If the caller had
already released its reference, that removal dropped the last one, and
destroy() then joined the handler task from inside that same task, which
never returns. Connection.deinit() later reported it as a live
subscription the caller had not released.

deinit() now stops the handler while the caller's reference still keeps
the subscription alive, so the handler can never drop the last one.
@coderabbitai

coderabbitai Bot commented Oct 5, 2026

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

Note

Currently processing new changes in this PR. This may take a few minutes, please wait...

⚙️ Run configuration
  • Configuration used: Repository: lalinsky/nats.zig/.coderabbit.yaml
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: fe47b203-a323-4762-b94a-75369eaac221
📥 Commits

Reviewing files that changed from the base of the PR and between d4cd40d and 0bc3153.

📒 Files selected for processing (1)
  • src/subscription.zig
 _________________________________________
< Schrodinbug: works until I stare at it. >
 -----------------------------------------
  \
   \   (\__/)
       (•ㅅ•)
       /   づ
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@lalinsky
lalinsky merged commit c3c1871 into main Oct 5, 2026
1 of 2 checks passed
@lalinsky
lalinsky deleted the fix/autounsubscribe-handler-self-join branch October 5, 2026 07:56
lalinsky added a commit that referenced this pull request Oct 6, 2026
…#181)

An async subscription that reaches its autounsubscribe limit removes
itself from the connection on its own handler task. If the caller had
already released its reference, that removal dropped the last one, and
destroy() then joined the handler task from inside that same task, which
never returns. Connection.deinit() later reported it as a live
subscription the caller had not released.

deinit() now stops the handler while the caller's reference still keeps
the subscription alive, so the handler can never drop the last one.
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