Skip to content

Make listener teardown exactly-once and bound the accept loop - #12

Open
Atsika wants to merge 1 commit into
mainfrom
fix/listener-lifecycle
Open

Make listener teardown exactly-once and bound the accept loop#12
Atsika wants to merge 1 commit into
mainfrom
fix/listener-lifecycle

Conversation

@Atsika

@Atsika Atsika commented Aug 15, 2026

Copy link
Copy Markdown
Collaborator

Proxy half of the listener-lifecycle work. The transport half — reporting the Azure deletion window clearly on restart — is Atsika/aznet#6.

Zombie listener. When Accept failed persistently the loop spun with no delay, logging every iteration, while listener list still showed Running:true and no agent could arrive. Same pattern that was fixed in ReceiveLoop. It now backs off between failures (100ms doubling to 5s) and gives up after 20 consecutive ones, tearing the listener down and marking it stopped.

Lifecycle races. Letting the accept loop stop a listener means two goroutines can decide to shut one down, and Running/Listener/CancelFunc were plain fields written by StopListener while the CLI read them, with nothing making teardown happen once. Running is now an atomic.Bool claimed via CompareAndSwap, so exactly one caller owns the shutdown; listener/cancel moved behind a mutex; the shared work is factored into teardownListener. This also closes the pre-existing double-teardown and CLI-read races.

Two ordering choices worth flagging for review:

  • The claim happens after the 200ms FIN window, not before. Claiming first would report the listener as stopped while it is still up, which lets a restart bind a new listener under the same ID whose fresh agents the in-flight teardown would then disconnect.
  • Clearing the selected agent/listener stays in StopListener rather than moving into the shared helper. Selection is CLI-owned state, and having the accept goroutine write it would trade one race for another. monitorAgent already drops agents without touching selection, so this is consistent with what's there.

Verified: go build ./..., go vet ./..., go test ./... -race -count=1, and the wasm agent build, against both the published aznet and the local aznet#6 branch via go.work.

Two problems in the same lifecycle.

A listener whose Accept keeps failing spun forever: it logged on every
iteration with no delay while `listener list` still reported Running:true and
no agent could ever arrive -- the zombie pattern already fixed in ReceiveLoop.
The accept loop now backs off between failures and, after a bounded run of
them, tears the listener down and marks it stopped.

That gave a second goroutine the ability to stop a listener, and Running,
Listener, and CancelFunc were plain fields mutated by StopListener while the
CLI read them, with nothing making teardown happen once. Running is now an
atomic.Bool claimed with CompareAndSwap, so exactly one of the CLI and the
accept loop owns the shutdown, and listener/cancel move behind a mutex with the
teardown path factored into teardownListener.

Clearing the selected agent and listener stays in StopListener rather than
moving into the shared helper: selection is CLI-owned state, and the accept
goroutine writing it would trade one race for another. monitorAgent already
drops agents without touching selection, so this matches.
@Atsika
Atsika force-pushed the fix/listener-lifecycle branch from a210970 to 08e41e9 Compare August 15, 2026 17:03
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