Skip to content

feat(transaction): RFC 6026 Accepted state + Timer L/M for INVITE 200 OK retransmission absorption#128

Open
andrico21 wants to merge 4 commits into
restsend:mainfrom
andrico21:rfc6026-accepted-state-timer-l-m
Open

feat(transaction): RFC 6026 Accepted state + Timer L/M for INVITE 200 OK retransmission absorption#128
andrico21 wants to merge 4 commits into
restsend:mainfrom
andrico21:rfc6026-accepted-state-timer-l-m

Conversation

@andrico21

Copy link
Copy Markdown

Implements RFC 6026 §7.1–§7.2 to fix the long-standing issue where rsipstack's INVITE state machine terminates immediately on 2xx and cannot absorb retransmitted 200 OKs from the UAS, breaking glare resolution and forking scenarios.

Closes #127. Per maintainer @shenjinti's confirmation on the issue thread: "you're completely right ... A PR would be very welcome."

Branch is rebased onto current upstream/main (8928174).

Standards

  • RFC 6026 §7.1 (server INVITE 2xx → Accepted; Timer L = 64·T1; absorbs ACK retransmits + late forked 2xx)
  • RFC 6026 §7.2 (client INVITE 2xx → Accepted; Timer M = 64·T1; absorbs retransmitted 2xx and forwards each to TU; client does NOT auto-ACK in Accepted — caller TU/dialog layer is responsible per strict RFC)
  • RFC 3261 §17.1.1 / §17.2.1 (legacy Completed semantics preserved for 3xx-6xx)

Changes

Area File
Accepted state variant src/transaction/mod.rs
TimerL + TimerM variants src/transaction/mod.rs
can_transition matrix src/transaction/mod.rs
timer_l + timer_m fields src/transaction/transaction.rs
Accepted-entry handler src/transaction/transaction.rs
on_timer Accepted dispatch src/transaction/transaction.rs
Server-side 2xx routing src/transaction/transaction.rs::respond
Client-side 2xx routing src/transaction/transaction.rs::on_received_response
9 conformance tests src/transaction/tests/test_transaction_states.rs

Both TransactionState and TransactionTimer are now #[non_exhaustive] to allow future RFC additions without breakage. This is itself a breaking change — recommend targeting 0.6.0.

Backward compatibility

The pre-existing convenience that auto-ACKs client INVITE 2xx from on_received_response is retained for 0.5.x consumers (technically a deviation from RFC 3261 §17.1.1.3, which mandates the TU drive the 2xx ACK). Documented honestly in lib.rs::Standards Compliance. Removal is a candidate for a follow-up PR that migrates 2xx-ACK responsibility to the dialog layer.

Validation

  • cargo test --lib — 247 passed, 0 failed
  • cargo test --doc — 65 passed, 0 failed
  • cargo fmt --check — clean

Pre-existing baseline: 66 clippy errors on main (not introduced by this PR; touched files contribute zero new clippy regressions).

Pre-submit review

Two read-only architecture review passes were run before submission. Each finding was either fixed in an atomic follow-up commit or explicitly deferred with rationale.

First pass — 3 MUST-FIX + 4 RECOMMEND + 3 NIT:

  • F1: pass every 2xx in Accepted to TU (skip dup-suppression per §7.2)
  • F2: lib.rs auto-ACK retention documented honestly
  • F3: #[non_exhaustive] on both public enums
  • R2: log auto-ACK failures (was silent .ok())
  • R3: cancel-safety rustdoc on respond / send_ack / receive
  • R4: typed (transaction_type, timer) dispatch in Accepted entry handler
  • N1: explicit stray-timer list with warn! on mismatched pairing
  • N2: dropped internal commit-hash references from comments

Second pass — 4 SHOULD-FIX (run after rebase onto current main, evaluated against an internal Rust style guide):

  • D1: respond cancel-safety rustdoc had inverted mutation order — fixed to describe actual store last_response → await send → transition order
  • D2: send_ack rustdoc claimed a nonexistent self.connection fallback — rewritten to describe actual two-source resolution and silent no-send fallback when neither input arg nor 2xx-Contact lookup yields a connection
  • D3: inline comment in client Accepted-state entry contradicted the retained auto-ACK gate — rewritten to match the lib.rs disclaimer
  • T1: dead wildcard arm in send_ack post-send dispatch replaced with if/else + debug_assert_eq! invariant (catches future #[non_exhaustive] additions)

Follow-up out of scope for this PR

  • R1: client-side fast Timer L/M tests via T1 override knob (~100 LOC of test-endpoint instrumentation; happy to ship as a follow-up if requested)
  • 2xx-ACK migration to dialog layer (removes the §17.1.1.3 deviation)

Commit list (atomic, 20 commits)

  1. feat(transaction): add Accepted state for RFC 6026 §7.1/§7.2
  2. feat(transaction): add TimerL + TimerM variants for RFC 6026
  3. feat(transaction): allow RFC 6026 edges in can_transition matrix
  4. feat(transaction): add timer_l + timer_m fields on Transaction
  5. feat(transaction): Accepted-state entry handler per RFC 6026 §7.1/§7.2
  6. feat(transaction): on_timer Accepted dispatch + Completed 2xx guard
  7. feat(transaction): server-side RFC 6026 2xx routing + ACK handling
  8. feat(transaction): client-side RFC 6026 2xx routing + send_ack split
  9. test(transaction): RFC 6026 conformance tests for Accepted state
  10. docs(lib): expand RFC 6026 standards-compliance claim
  11. style: cargo fmt --all cleanup
  12. fix(transaction): pass every 2xx in Accepted to TU per RFC 6026 §7.2 [F1]
  13. docs(lib): clarify RFC 6026 client-side auto-ACK retention [F2]
  14. feat(transaction): mark TransactionState + TransactionTimer #[non_exhaustive] [F3]
  15. refactor(transaction): tighten Accepted timer dispatch [R4 + N1 + N2]
  16. fix(transaction): log auto-ACK failures instead of silent .ok() [R2]
  17. docs(transaction): cancel-safety rustdoc on async public APIs [R3]
  18. docs(transaction): correct rustdoc and comment accuracy on touched APIs [D1 + D2 + D3]
  19. refactor(transaction): drop dead wildcard arm in send_ack post-send dispatch [T1]
  20. style: cargo fmt cleanup on rebased F1 hunk

@shenjinti

Copy link
Copy Markdown
Contributor

20 commits is too granular for a single feature. Please squash into ~3-5 logical commits (e.g. feat, docs, test). The style: cargo fmt cleanup commits and (N/N) suffix noise can be folded into their parent commits.

The PR body mentions auto-ACK retention as a known RFC deviation. Consider adding an EndpointBuilder option (e.g. .auto_ack_invite_2xx(bool), default true) so users can opt into strict RFC 3261 §17.1.1.3 behavior without a breaking change. This can be a follow-up, not a blocker for this PR.

The on_timer → Accepted → Terminated path has no integration test (deferred as R1). Consider adding a t1_override test knob so this can be tested without waiting 32s.

@andrico21 andrico21 force-pushed the rfc6026-accepted-state-timer-l-m branch from 5dd5ea1 to bbdc4a6 Compare June 23, 2026 11:28
@andrico21

Copy link
Copy Markdown
Author

Squashed to 4 commits per your spec. Split the breaking #[non_exhaustive] change out as its own commit for bisect precision; everything else folded as you asked. Force-pushed:

  • feat(transaction): RFC 6026 Accepted state + Timer L/M for INVITE 2xx absorption
  • feat(transaction)!: mark TransactionState + TransactionTimer #[non_exhaustive]
  • test(transaction): RFC 6026 conformance tests for Accepted state
  • docs(transaction): RFC 6026 standards-compliance + cancel-safety + rustdoc accuracy

(N/N) suffixes + style fmt commits folded into parents. Final state matches the previous PR HEAD: 247 lib tests pass, fmt clean.

On .auto_ack_invite_2xx(bool) builder option: agreed - will land as a follow-up PR after this merges.

On the t1_override test knob: can do as follow-up too. Want it folded in before merge or its own PR?

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.

lib.rs claims RFC 6026 but transaction layer skips Accepted/Timer L/Timer M (§4, §7.1, §7.2, §8.5)

2 participants