Bump dependencies and implement fast ingest#24
Conversation
Signed-off-by: Tomasz Pietrek <tomasz@synadia.com>
Signed-off-by: Tomasz Pietrek <tomasz@synadia.com>
Previous commit 1571c83 declared `pub mod batch_publish_fast;` in lib.rs but did not include the module file itself, breaking CI with E0583. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Signed-off-by: Tomasz Pietrek <tomasz@synadia.com>
|
Claude finished @Jarema's task in 5m 18s —— View job PR Review: Bump dependencies and implement fast ingest
SummaryThis is a thorough and well-structured implementation of ADR-50 fast ingest. The architecture — inline state machine, lazily-created subscription, no background tasks, no shared locks — is the right call and mirrors the Go reference cleanly. The Issues found:
No correctness bugs found. The stall gate, reply-subject encoding, gap/flow-error handling, EOB commit, single-message fast path, ping recovery, and subscriber lifecycle are all correct. The |
|
|
||
| // --------------------------------------------------------------------------- | ||
| // Errors | ||
| // --------------------------------------------------------------------------- |
There was a problem hiding this comment.
FastPublishErrorKind is missing #[non_exhaustive], unlike BatchPublishErrorKind which was annotated in this same PR. Without it, adding a new variant in a future minor release is a breaking change for downstream users who exhaustively match on error kinds.
| serde_json::from_slice(payload) | ||
| .map_err(|e| FastPublishError::with_source(FastPublishErrorKind::Serialization, e))?; | ||
|
|
||
| Ok(match resp { |
There was a problem hiding this comment.
Dead code: an empty string has 0 dots and is already caught by matches('.').count() != 1 above — this branch is unreachable. The same logic issue means the function can be simplified:
pub(crate) fn validate_inbox_shape(inbox: &str) -> Result<(), FastPublishError> {
let mut parts = inbox.splitn(3, '.');
let a = parts.next().unwrap_or(""); // always has at least one part
let b = parts.next().unwrap_or("");
let no_third = parts.next().is_none();
if a.is_empty() || b.is_empty() || !no_third {
return Err(FastPublishError::new(FastPublishErrorKind::InvalidInboxShape));
}
Ok(())
}| for h in handles { | ||
| let ack = h.await.unwrap(); | ||
| assert_eq!(ack.batch_size, 101); | ||
| } |
There was a problem hiding this comment.
This 200 ms sleep is a timing dependency that makes the test fragile on slow CI runners. The test depends on the server delivering a BatchFlowErr within that window, but there's no guarantee under load. Consider polling batch.commit() with a short retry loop instead, or structuring the test so the error surfaces synchronously (e.g. by completing the stall gate cycle before committing, which forces a drain).
| /// reference implementation and avoids wasted subscriptions when a | ||
| /// publisher is built and then dropped unused. | ||
| /// | ||
| /// # Errors |
There was a problem hiding this comment.
The error returned for max_outstanding_acks outside 1..=3 is InvalidState, but at build() time no state exists yet — this is a configuration validation error. InvalidState implies a runtime-state violation (e.g. calling add on a closed publisher). A more descriptive kind or an adjustment to the error message would make the failure easier to diagnose.
| InvalidPattern, | ||
| /// 10207 — batch id exceeds 64 characters or is otherwise invalid. | ||
| InvalidBatchId, | ||
| /// 10208 — server has forgotten this batch (timed out, leader change in |
There was a problem hiding this comment.
Error codes are matched as raw integer literals. If the server-side error code definitions change (unlikely, but not impossible), this mapping silently diverges. Consider defining these as named constants alongside their mapping, similar to how async-nats exposes ErrorCode::ATOMIC_PUBLISH_DISABLED for the atomic-batch path, so the intent is searchable and auditable.
| serde_json::from_slice(payload) | ||
| .map_err(|e| FastPublishError::with_source(FastPublishErrorKind::Serialization, e))?; | ||
|
|
||
| Ok(match resp { |
There was a problem hiding this comment.
Dead code: an empty string has 0 dots, so it is already rejected by the != 1 count check above. This branch can never be reached.
// remove the unreachable check
if inbox.is_empty() {
return Err(FastPublishError::new(FastPublishErrorKind::InvalidInboxShape));
}The same applies to the subsequent split_once('.').unwrap() — it's safe (count==1 guarantees a dot exists), but an expect("exactly one dot confirmed above") would make the invariant explicit and avoid a silent panic if the logic changes.
Signed-off-by: Tomasz Pietrek <tomasz@synadia.com>
Signed-off-by: Tomasz Pietrek <tomasz@synadia.com>
Signed-off-by: Tomasz Pietrek <tomasz@synadia.com>
|
@claude review again |
|
Claude finished @Jarema's task in 3m 39s —— View job PR Review: Bump dependencies and implement fast ingest
SummaryAll five issues from the previous review have been addressed. No new issues found. The implementation is ready to merge. Previous issues — all resolved ✓1. Dead code in 2. 3. Timing-sensitive integration tests — ✅ Fixed
4. 5. Raw error code integers in Changes to
|
- New example `bench_normal_vs_fast` compares sync, async (bounded inflight), core+js, atomic batch, and fast batch publishing on an R1 stream (100k messages each). - `build_reply` now returns `Subject` instead of `String`; the publisher stamps the reply via `From<String> for Subject` which is zero-copy, saving one `Bytes::copy_from_slice` per fast-publish message. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Signed-off-by: Tomasz Pietrek <tomasz@synadia.com>
Sweeps `flow` × `max_outstanding_acks` × payload-size for fast batch publishing on a local R1 stream. On this machine `max_outstanding_acks=3` beats `=2` by 4-7%; flow size has minimal effect within a max_acks group. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Signed-off-by: Tomasz Pietrek <tomasz@synadia.com>
Signed-off-by: Tomasz Pietrek <tomasz@synadia.com>
| /// Per-message error sent when a batch message fails a server-side check | ||
| /// (e.g. expected-last-seq mismatch). Wire tag: `"type":"err"`. | ||
| #[derive(Debug, Clone, Deserialize)] | ||
| pub(crate) struct BatchFlowErr { |
There was a problem hiding this comment.
Super nitpick but, for consistency would be?
| pub(crate) struct BatchFlowErr { | |
| pub(crate) struct BatchFlowError { |
| async fn wait_for_flow_event_with_pings(&mut self) -> Result<(), FastPublishError> { | ||
| // Split timeout into 3 intervals for up to 2 pings before giving up. | ||
| // Clamp floor to 100ms so tiny ack_timeouts don't spin. | ||
| let ping_interval = (self.ack_timeout / 3).max(Duration::from_millis(100)); |
There was a problem hiding this comment.
Magic number? promote toa const maybe?
Signed-off-by: Tomasz Pietrek tomasz@synadia.com