docs(plan): Revise implementation plan to reflect API simplifications and feature deferrals
This commit is contained in:
1 file changed
+74
-65
+74
-65
@@ -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.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user