Skip to content

feat(sm): RFC 3539 watchdog for connections a StateMachine accepts - #77

Merged
gomaja merged 1 commit into
mainfrom
feat/i72-server-watchdog
Oct 10, 2026
Merged

gomaja merged 1 commit into
mainfrom
feat/i72-server-watchdog

Conversation

@gomaja

@gomaja gomaja commented Oct 10, 2026 •

Copy link
Copy Markdown
Owner

Watchdog supervision existed only on sm.Client. A server built from sm.New and diam.Server answered the DWRs it received, but it never sent one, and it never noticed a peer that went silent while its transport stayed up. RFC 6733 §5.5.3 requires every implementation to support the RFC 3539 algorithm, and RFC 3539 §3.4 runs that algorithm on all open connections.

Settings

Settings gains EnableWatchdog, WatchdogInterval, WatchdogStream and OnWatchdogConnEvent, with the same names sm.Client uses.

  • Interval: Twinit defaults to 30 s, with a 6 s minimum and ±2 s of jitter redrawn for every Tw (RFC 3539 §3.4.1).
  • Opt-in: supervision is off unless enabled. The docs advise conforming deployments to enable it.
  • peer.Manager: peer.New rejects these fields, because the Manager supervises its peers itself.

Behaviour (RFC 3539 Appendix A on an established connection)

  • Start: after the CEA is written and OnHandshake has returned. It never starts for a rejected or aborted CER, or on a connection already closed or Closing.
  • DWR: sent after Tw of silence, and never resent while one is pending.
  • SUSPECT: reached on the next expiry and reported as WatchdogSuspect. A responder has no request queue to fail over (§3.4.1 [3]).
  • DOWN: reached on the expiry after that, and the transport is closed.
  • Recovery: any valid received message resets Tw and returns a SUSPECT connection to OKAY. A valid DWA clears the pending DWR.
  • Closing (RFC 6733 §5.6): a peer's DPR or a local Disconnect stops supervision, including a Disconnect issued while the CEA is being written or OnHandshake runs. Once the stop returns:
    • no DWR is written and no DWA is credited;
    • no activity counts and no watchdog event is emitted;
    • the watchdog does not close the transport.
  • No dispatch stalls: counting received traffic takes no lock, so a blocked DWR write or a slow event callback cannot hold up inbound dispatch. Server.WriteTimeout bounds the DWR write.
  • Shared loop: Client and server share one supervision loop (watchdog.go). Client behaviour and its existing tests are unchanged.

Tests

  • Reproducer: the issue's reproducer at the default Twinit, in virtual time. Before the fix it failed with "accepted idle connection received no DWR by default Twinit + jitter".
  • Transports: TCP and SCTP in every dispatch mode. Over SCTP the DWR goes out on the configured stream, and a DWA is credited whichever stream it arrives on. SCTP tests ran with no skips.
  • Behaviour: jitter redrawn per expiry; recovery; DWA handling; answers credited per connection; no supervision before the handshake or after a rejected or aborted one.
  • Races: DPR and Disconnect, including a Disconnect racing the handshake or an expiry, and a stale DWA after the stop.
  • Robustness: cleanup on every close cause, a blocked DWR write, and callback panics.
  • Mutations: 60, all caught, including the six that survived an independent review's first pass: jitter wiring, CloseNotify discovery, the DWA drain at an expiry, the closed-connection guard, and Client event ordering.
  • Wire captures (tshark): none on an idle connection before this change; exactly one DWR and then FIN on a silent TCP peer; over SCTP, DWRs on stream 3 with DWAs on stream 5 and matching Hop-by-Hop and End-to-End IDs.

Validation

Under Go 1.27.2:

  • go build ./..., go test ./... -count=1, go test -race ./diam/... -count=1, go test -race -count=10 -run Watchdog ./diam/sm/ and go vet ./... pass;
  • staticcheck at the go-tools commit that reads Go 1.27.2 export data and golangci-lint run ./... report no issues, and gofmt -l . is empty;
  • the examples/middleware build, vet and test pass.

Fixes #72


Summary by cubic

Adds RFC 3539 watchdog supervision to connections accepted by a StateMachine server, previously only sm.Client had it. New opt-in sm.Settings fields (EnableWatchdog, WatchdogInterval, WatchdogStream, OnWatchdogConnEvent) supervise an accepted connection after a successful CEA and OnHandshake. A silent peer gets a DWR after Tw, SUSPECT at the next expiry, and DOWN with transport closure at the third; any received message resets Tw. Supervision stops on DPR or Disconnect (RFC 6733 §5.6). peer.Manager rejects these fields because it supervises peers itself. Fixes #72.

Behavior

  • DWR is sent after Tw of silence and never resent while one is pending.
  • SUSPECT is reported as a WatchdogSuspect event; DOWN closes the transport.
  • A valid DWA clears the pending DWR; incoming traffic resets Tw and recovers from SUSPECT.
  • Supervision never starts for rejected/aborted CERs or after a stop.

Written for commit 6b69c32. Summary will update on new commits.

View guided diff Turn on auto-fix

Watchdog supervision existed only on sm.Client. A server built from
sm.New and diam.Server answered the DWRs it received, but never sent one
and never noticed a peer that went silent while its transport stayed up,
although RFC 6733 §5.5.3 requires every implementation to support the
RFC 3539 algorithm, which runs on all open connections (§3.4).

- Settings gains EnableWatchdog, WatchdogInterval (Twinit, default 30s,
  minimum 6s, ±2s jitter per Tw, RFC 3539 §3.4.1), WatchdogStream and
  OnWatchdogConnEvent, with the names sm.Client uses. Supervision is
  opt-in; conforming deployments should enable it.
- Supervision starts once the CEA is written and OnHandshake has
  returned, never for a rejected or aborted CER. It follows Appendix A on
  the established connection: a DWR after Tw of silence, never resent
  while one is pending; SUSPECT at the next expiry, reported as
  WatchdogSuspect (a responder has no queue to fail over); DOWN and close
  at the expiry after that. Any valid received message resets Tw and
  recovers a SUSPECT connection; a valid DWA clears the pending DWR.
- A peer's DPR and a local Disconnect stop supervision before the
  Closing exchange (RFC 6733 §5.6), including a Disconnect issued while
  the CEA is being written or OnHandshake runs. Once the stop returns, no
  DWR is written, no DWA is credited, no activity counts, no watchdog
  event is emitted and the watchdog does not close the transport.
- Counting received traffic takes no lock, so a blocked DWR write or a
  slow event callback cannot stall inbound dispatch; Server.WriteTimeout
  bounds the write itself.
- Client and server share one supervision loop (watchdog.go); Client's
  behaviour and tests are unchanged.
- peer.Manager rejects the new Settings fields, because it supervises its
  peers itself.

Tests cover the issue's reproducer at the default Twinit in virtual time
and the per-expiry jitter;
TCP and SCTP (DWR on the configured stream, DWA credited from any
stream) in every dispatch mode; recovery, DWA handling, per-connection
answers, no supervision before or after a rejected handshake, DPR and
Disconnect racing the handshake and the expiry, cleanup on every close
cause, a blocked DWR write, and callback panics.

Fixes #72
Copilot AI balanced review requested due to automatic review settings October 10, 2026 06:44

Copilot AI left a comment

Copy link
Copy Markdown

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.

@coderabbitai

coderabbitai Bot commented Oct 10, 2026

Copy link
Copy Markdown

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration
  • Configuration used: defaults
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 92c495a6-ebc2-4e33-a683-dcbb09f1aad9

  • Autofix · 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.

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 10, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-10T06:48:02.776981Z 6b69c32 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@gomaja

gomaja commented Oct 10, 2026

Copy link
Copy Markdown
Owner Author

The lint failure is not from this change: staticcheck 2026.2.1 cannot read the export data of Go 1.27.2, the stable release CI now installs, so it stopped before analysing any package. Under Go 1.27.2, a staticcheck built from go-tools master (f1838cc3, which carries the fix) and golangci-lint both report no issues on this branch. The workflow pin is updated in a separate change.

@gomaja
gomaja merged commit a4beda7 into main Oct 10, 2026
15 of 16 checks passed
@gomaja
gomaja deleted the feat/i72-server-watchdog branch October 10, 2026 06:46

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6b69c321fe

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

observe: func(event WatchdogEvent) { w.observe(c, event) },
timeout: func() { w.terminate(nil, slog.LevelWarn, "sm: watchdog timeout; closing connection", nil) },
}
dwa := handleDWA(sm, w.signals.dwac, func(_ diam.Conn, event WatchdogEvent) { w.emit(event) })

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Reject mismatched DWAs before clearing Pending

The accepted supervisor passes every syntactically successful DWA to handleDWA, but it never records the DWR's Hop-by-Hop/End-to-End IDs, and handleDWA therefore clears Pending for unsolicited, duplicated, or stale answers. For example, a delayed DWA from an earlier probe arriving while a newer DWR is outstanding credits the newer request and postpones or prevents the SUSPECT/DOWN transition. The managed-peer watchdog already enforces this correlation in diam/peer/watchdog.go; this path should likewise retain the outstanding IDs and reject answers that do not match.

Useful? React with 👍 / 👎.

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.

Connections accepted by sm.StateMachine get no RFC 3539 watchdog supervision

2 participants