docs(plan): Revise implementation plan to reflect API simplifications and feature deferrals

This commit is contained in:
Nils Andreas Svee committed 2026-04-28 15:56:05 +07:00
1 parent 208341a84d
commit dd8b55ad9f
1 file changed
+74 -65
+74 -65
View File
@@ -139,10 +139,10 @@ The intended architecture from `RUST_DESIGN.md` is confirmed with no structural
**Main public types:** **Main public types:**
- `ClientConfig`, `SocketConfig` - `ClientConfig`, `SocketConfig`
- `Client`, `SessionState` - `Client`, `OpenOutcome`
- `ManagedClient`, `ManagedClientSession` - `ManagedClient`, `ManagedClientSession`
- `EventHub`, `EventSubscription`, `SubscriberConfig`, `SubscriberOverflow` - `EventHub`, `EventSubscription`, `SubscriberConfig`, `SubscriberOverflow`
- `ClientEvent`, `RttSample`, `OneWayDelaySample`, `ServerTiming` - `ClientEvent`, `RttSample`, `ServerTiming`
- `ReceivedStatsSample`, `PacketMeta`, `LossKind` - `ReceivedStatsSample`, `PacketMeta`, `LossKind`
- `ClientWarning`, `SessionOutcome`, `SessionEndReason` - `ClientWarning`, `SessionOutcome`, `SessionEndReason`
- `NegotiatedParams`, `NegotiationPolicy`, `ValidationPolicy` - `NegotiatedParams`, `NegotiationPolicy`, `ValidationPolicy`
@@ -489,8 +489,8 @@ Internal struct tracking:
logical_seq: u64 logical_seq: u64
wire_seq: u32 wire_seq: u32
sent_at_mono: Instant sent_at_mono: Instant
sent_at_wall: SystemTime sent_at_wall: Option<SystemTime>
replied: bool timeout_at: Instant
``` ```
### `BoundedPendingMap` ### `BoundedPendingMap`
@@ -507,6 +507,24 @@ A bounded map from `u32` (wire seq) to `PendingProbe`. Capacity limited to preve
### `Client::open` ### `Client::open`
Returns `Result<OpenOutcome, ClientError>` (not `Vec<ClientEvent>`).
```rust
pub enum OpenOutcome {
Started {
session_id: SessionId,
remote: SocketAddr,
negotiated: NegotiatedParams,
event: ClientEvent,
},
NoTestCompleted {
remote: SocketAddr,
event: ClientEvent,
},
}
```
Steps:
1. Encode open request with proposed params 1. Encode open request with proposed params
2. Send open request 2. Send open request
3. Set read timeout to first timeout value 3. Set read timeout to first timeout value
@@ -515,13 +533,13 @@ A bounded map from `u32` (wire seq) to `PendingProbe`. Capacity limited to preve
6. On all timeouts exhausted: return `ClientError::OpenTimeout` 6. On all timeouts exhausted: return `ClientError::OpenTimeout`
7. On valid reply: 7. On valid reply:
- If Close flag and client didn't request close: return `ClientError::ServerRejected` - If Close flag and client didn't request close: return `ClientError::ServerRejected`
- If Close flag and client requested close (no-test): return `SessionEnded` event - If Close flag and client requested close (no-test): return `OpenOutcome::NoTestCompleted`
- Extract connection token, negotiated params - Extract connection token, negotiated params
- Apply negotiation policy (reject or accept restrictions) - Apply negotiation policy (reject or accept restrictions)
- Compute minimum packet length from negotiated params - Compute minimum packet length from negotiated params
- Transition to Active phase - Transition to Active phase
- Set DSCP on socket (only now, for echo phase) - Set DSCP on socket (only now, for echo phase)
- Return `SessionStarted` event - Return `OpenOutcome::Started`
### `Client::send_probe` ### `Client::send_probe`
@@ -546,7 +564,7 @@ A bounded map from `u32` (wire seq) to `PendingProbe`. Capacity limited to preve
8. Compute `OneWayDelaySample` if wall timestamps available 8. Compute `OneWayDelaySample` if wall timestamps available
9. Extract `ReceivedStatsSample` if negotiated 9. Extract `ReceivedStatsSample` if negotiated
10. Emit `EchoReply` event 10. Emit `EchoReply` event
11. Return `Ok(Some(events))` or `Ok(None)` on timeout/would-block 11. Return `Ok(events)` — empty Vec on timeout/would-block
### `Client::recv_available` ### `Client::recv_available`
@@ -582,9 +600,8 @@ Loop calling `recv_once` up to a budget limit (max packets or max duration). Col
### Negotiation Policy ### Negotiation Policy
- `AcceptServerRestrictions`: accept any server-returned params, emit warnings if changed - `Strict` (default): reject if any param was changed by server
- `RequireExact`: reject if any param was changed by server - `Loose`: accept server-returned params, emit warnings if changed
- `RequireWithin(LocalRequirements)`: reject if params exceed caller-specified bounds
### Validation Policy ### Validation Policy
@@ -671,9 +688,10 @@ When coordinator emits an event:
2. Attempt send according to subscriber's overflow policy: 2. Attempt send according to subscriber's overflow policy:
- `Block`: blocking send (only if subscriber is trusted to keep up) - `Block`: blocking send (only if subscriber is trusted to keep up)
- `DropNewest`: try_send, increment drops on failure - `DropNewest`: try_send, increment drops on failure
- `DropOldest`: complex — not trivially supported by flume; implement by draining one and retrying, or use a ring-buffer channel
- `Disconnect`: try_send, on failure remove subscriber - `Disconnect`: try_send, on failure remove subscriber
`DropOldest` is deferred from MVP. It is useful for SQM/live-control freshness, but `flume` does not provide it natively and it is not worth blocking the managed runner. A future implementation may use a drain-one-and-retry workaround or a ring-buffer channel.
**Critical invariant:** Public subscriber backpressure must NOT delay the sender/pacer thread. The sender never touches the event hub. Only the coordinator touches the event hub, and the coordinator is not on the timing-critical path. **Critical invariant:** Public subscriber backpressure must NOT delay the sender/pacer thread. The sender never touches the event hub. Only the coordinator touches the event hub, and the coordinator is not on the timing-critical path.
### Subscriber Configuration ### Subscriber Configuration
@@ -688,7 +706,7 @@ SubscriberConfig {
Preset profiles: Preset profiles:
- `SubscriberConfig::stats()`: Block, large capacity, all events - `SubscriberConfig::stats()`: Block, large capacity, all events
- `SubscriberConfig::live()`: DropOldest, small capacity (e.g., 4), EchoReply/EchoLoss only - `SubscriberConfig::live()`: DropNewest, small capacity (e.g., 4), EchoReply/EchoLoss only (future: DropOldest when available)
- `SubscriberConfig::cli()`: DropNewest, moderate capacity (e.g., 1024), all events - `SubscriberConfig::cli()`: DropNewest, moderate capacity (e.g., 1024), all events
- `SubscriberConfig::debug()`: DropNewest, small capacity, all events - `SubscriberConfig::debug()`: DropNewest, small capacity, all events
@@ -739,6 +757,8 @@ It does NOT reuse `Client` directly (the managed runner has different ownership
### `ClientEvent` ### `ClientEvent`
Do **not** define or emit `EchoSent` in MVP. It is debug/instrumentation noise that can be added later behind a tracing or debug event option.
```rust ```rust
pub enum ClientEvent { pub enum ClientEvent {
SessionStarted { SessionStarted {
@@ -786,11 +806,13 @@ pub enum ClientEvent {
}, },
FatalError { FatalError {
session_id: Option<SessionId>, session_id: Option<SessionId>,
error: ClientErrorKind, error: ClientError,
}, },
} }
``` ```
**Change from initial design:** `FatalError` uses `ClientError` directly instead of a separate `ClientErrorKind` enum.
### `RttSample` ### `RttSample`
```rust ```rust
@@ -922,7 +944,7 @@ pub enum SessionEndReason {
TestComplete, TestComplete,
Cancelled, Cancelled,
ServerClosed, ServerClosed,
Error(ClientErrorKind), Error(ClientError),
} }
``` ```
@@ -1357,23 +1379,7 @@ pub enum ClientError {
} }
``` ```
### `ClientErrorKind` `ClientError` is used directly in `ClientEvent::FatalError` and `SessionEndReason::Error`. No separate `ClientErrorKind` is needed — `ClientError` should derive `Clone` where possible, or the event variants should use `Arc<ClientError>` for cheap cloning if `io::Error` prevents `Clone`.
A clonable, non-source-chaining variant of error info for embedding in `ClientEvent::FatalError`:
```rust
#[derive(Debug, Clone)]
pub enum ClientErrorKind {
OpenTimeout,
ServerRejected,
ProtocolVersionMismatch { expected: i64, got: i64 },
SendFailed(String),
RecvFailed(String),
MalformedSessionPacket,
ThreadPanic,
ChannelDisconnected,
}
```
--- ---
@@ -1889,18 +1895,9 @@ Require `irtt` server binary in PATH. Gated behind `--features interop`.
**Recommended behavior:** Follow the design document exactly. `stats` is default on for irtt-cli. All others are opt-in. **Recommended behavior:** Follow the design document exactly. `stats` is default on for irtt-cli. All others are opt-in.
### Q7: DropOldest Subscriber Overflow with Flume ### Q7: DropOldest Subscriber Overflow with Flume — RESOLVED
**Why it matters:** `flume` doesn't natively support drop-oldest semantics. **Decision:** Deferred from MVP. Initial overflow policies are `DropNewest`, `Block`, and `Disconnect`. `DropOldest` may be added post-MVP using a drain-one-and-retry approach or a ring-buffer channel.
**Blocks implementation:** No, but requires a design decision.
**Options:**
1. Implement DropOldest by draining one item from the receiver before retrying send (requires access to both ends)
2. Use a different channel type for DropOldest subscribers (e.g., a ring buffer)
3. Defer DropOldest and only implement DropNewest/Block/Disconnect initially
**Recommended temporary behavior:** Implement DropOldest as: try_send fails → recv one from the same channel → try_send again. This requires the EventHub to hold both sender and receiver for DropOldest subscribers, which is slightly unusual but workable.
--- ---
@@ -1921,7 +1918,7 @@ Require `irtt` server binary in PATH. Gated behind `--features interop`.
| CLI format instability | Medium — downstream breakage | Low | Machine format fields are positional and documented. New fields added at end only. | irtt-cli | | CLI format instability | Medium — downstream breakage | Low | Machine format fields are positional and documented. New fields added at end only. | irtt-cli |
| IPv6 Traffic Class socket option | Low — IPv6 DSCP may not work | Medium | Use socket2 if available. Fall back to libc setsockopt. Test on target platforms. | irtt-client | | IPv6 Traffic Class socket option | Low — IPv6 DSCP may not work | Medium | Use socket2 if available. Fall back to libc setsockopt. Test on target platforms. | irtt-client |
| Server behavior divergence across versions | Low — untested server behavior | Low | Target irtt 0.9.1 only. Black-box tests as regression suite. | all | | Server behavior divergence across versions | Low — untested server behavior | Low | Target irtt 0.9.1 only. Black-box tests as regression suite. | all |
| flume DropOldest not native | Low — subscriber policy gap | Medium | Workaround with drain-and-retry. Or defer DropOldest initially. | irtt-client (managed) | | flume DropOldest not native | Low — subscriber policy gap | Medium | Deferred from MVP. DropNewest/Block/Disconnect available. | irtt-client (managed) |
--- ---
@@ -1979,49 +1976,61 @@ Require `irtt` server binary in PATH. Gated behind `--features interop`.
- One commit per logical unit of work (e.g., "implement varint encoding" or "add open request encoding") - One commit per logical unit of work (e.g., "implement varint encoding" or "add open request encoding")
- Tag milestone completion with a clear commit message (e.g., "milestone 1: irtt-proto complete") - Tag milestone completion with a clear commit message (e.g., "milestone 1: irtt-proto complete")
- Ensure `cargo test` passes at every commit - At every commit, ensure:
- Ensure `cargo clippy` is clean at every commit - `cargo fmt --check --workspace`
- Run `cargo test --features interop` at milestone boundaries (2, 3, 4, 5, 8) - `cargo clippy --workspace --all-targets`
- `cargo test --workspace`
- Run `cargo test --workspace --features interop` at milestone boundaries (2, 3, 4, 5, 8)
### First work order
The first implementation agent task should be:
Implement Milestone 0 and Milestone 1 only. Create the Cargo workspace and complete `irtt-proto`. Do not implement sockets, client runtime, stats, CLI behavior, managed runner, serde, tracing, ancillary networking, or interop tests yet.
Success means:
- `cargo check --workspace` passes
- `cargo test -p irtt-proto` passes
- All 14 packet vectors pass
- No upstream source or tests are present
--- ---
## 18. Design Brief Amendments ## 18. Design Brief Amendments
### Amendment 1: `irtt-stats` dependency on `irtt-client` The following amendments to `RUST_DESIGN.md` have been incorporated into this plan. They are listed here for traceability.
The design lists `irtt-client` as a dependency of `irtt-stats`. This is correct because `irtt-stats` needs `ClientEvent` types. However, consider whether `ClientEvent` and related types should be in a separate `irtt-client-types` crate or re-exported from `irtt-proto` to avoid pulling in socket/thread code. ### Incorporated: `Client::open` return type
**Recommendation:** Keep as-is for now. The dependency is on the types, not the runtime. Rust's dead-code elimination handles this. If binary size becomes an issue, extract types later. Changed from `Result<Vec<ClientEvent>, ClientError>` to `Result<OpenOutcome, ClientError>`. See Section 5.
### Amendment 2: `DropOldest` subscriber overflow ### Incorporated: `recv_once` return type
The design specifies `DropOldest` as a subscriber overflow policy. Flume doesn't natively support this. Changed from `Result<Option<Vec<ClientEvent>>, ClientError>` to `Result<Vec<ClientEvent>, ClientError>` with empty Vec meaning "no data available." See Section 5.
**Recommendation:** Implement `DropOldest` in MVP using the drain-one-and-retry approach (EventHub holds both Sender and a cloned Receiver for DropOldest subscribers). If this proves problematic, defer DropOldest to post-MVP and document the limitation. ### Incorporated: `EchoSent` event omitted
### Amendment 3: `Client::open` return type `EchoSent` is not defined in MVP. Can be added later behind a debug/tracing option.
The design shows `Client::open` returning `Result<Vec<ClientEvent>, ClientError>`. For the manual API, returning a typed result struct might be cleaner than a Vec: ### Incorporated: `DropOldest` deferred
**Recommendation:** Use `Result<OpenOutcome, ClientError>` where `OpenOutcome` contains the `SessionStarted` event and negotiated params. The Vec<ClientEvent> pattern is better suited for `send_probe`/`recv_once` which may produce multiple events. `DropOldest` subscriber overflow is deferred from MVP. Initial overflow policies: `DropNewest`, `Block`, `Disconnect`. See Section 6.
### Amendment 4: `recv_once` return type ### Incorporated: `NegotiationPolicy` simplified
`recv_once` currently returns `Result<Option<Vec<ClientEvent>>, ClientError>`. This is a triple-nested result (Result → Option → Vec). Consider simplifying to `Result<SmallVec<[ClientEvent; 2]>, ClientError>` where an empty SmallVec means "no data available." Changed from three-variant (`AcceptServerRestrictions`/`RequireExact`/`RequireWithin`) to two-variant (`Strict`/`Loose`). See Section 5.
**Recommendation:** Use `Result<Vec<ClientEvent>, ClientError>` with empty Vec meaning "no data." This is simpler and the allocation cost is negligible per-recv. Or use a custom `RecvResult` enum: `Data(Vec<ClientEvent>)`, `WouldBlock`, `Timeout`. ### Incorporated: `ClientErrorKind` removed
### Amendment 5: `EchoSent` event `FatalError` and `SessionEndReason::Error` use `ClientError` directly. No separate `ClientErrorKind` enum.
The design says `EchoSent` should not be emitted by default. Consider not defining it at all in MVP, to avoid dead code. ### Remaining: `irtt-stats` dependency on `irtt-client`
**Recommendation:** Omit `EchoSent` entirely from MVP. Add it later if needed for debugging. Keep as-is. The dependency is on the types, not the runtime. Rust's dead-code elimination handles this. If binary size becomes an issue, extract types later.
### Amendment 6: Clock value 0 ### Remaining: Clock value 0
The spec defines Clock enum values as 1=Wall, 2=Monotonic, 3=Both. Value 0 is not defined. The implementation should treat Clock=0 as equivalent to "no timestamps" (the StampAt=None case makes Clock irrelevant). The spec defines Clock enum values as 1=Wall, 2=Monotonic, 3=Both. Value 0 is not defined. If StampAt is None, ignore Clock value. If StampAt is non-None and Clock is 0, treat as an error or default to Wall. Document the decision.
**Recommendation:** If StampAt is None, ignore Clock value. If StampAt is non-None and Clock is 0, treat as an error or default to Wall. Document the decision.
--- ---