Skip to content

feat(client): implement Producer & Consumer graceful shutdown, lightweight barrier synchronization#202

Merged
Migorithm merged 14 commits into
mainfrom
feat/188
Jul 25, 2026
Merged

feat(client): implement Producer & Consumer graceful shutdown, lightweight barrier synchronization#202
Migorithm merged 14 commits into
mainfrom
feat/188

Conversation

@Migorithm

@Migorithm Migorithm commented Jul 24, 2026

Copy link
Copy Markdown
Collaborator

closes #188

Summary of Changes

This PR enhances the client SDK (Producer and Consumer) with deterministic graceful shutdown protocols, lightweight dual-gate synchronization, cancellation resilience for shared batches, and CLI
configuration validation.

1. Producer Architecture & Synchronization

  • Dual-Gate Barrier Synchronization:
  • send_gate: RwLock<()>: Gates active application send() calls against flush() and close().
  • flush_gate: Arc<RwLock<()>>: Gates batch network publication against buffer draining, closing the linger-timer race window.
  • Documented Tokio's writer-preferring (fair) RwLock deadlock prevention rationale in Inner struct rustdoc.
  • Cancellation-Safe Shared Batch Spawning (spawn_flush):
  • Decoupled size-triggered batch flushes into detached tokio::spawn background tasks under flush_gate.read().
  • Cancelling an individual caller's send() future discards only that caller's ACK (oneshot::Receiver), allowing shared batch publication to complete durably for surviving senders.
  • Non-Cancellable Drains:
  • flush() and close() execute producer-owned background drains that continue to completion even if the caller drops the flush() / close() future.

2. Consumer Graceful Shutdown (Consumer::close)

  • 5-Stage Ordered Shutdown Protocol:
  1. Delivery Fencing: group.fence_delivery() prevents new records from reaching caller iterators.
  2. Range Fetcher Join: stop_all() dispatches StopRangeFetch with oneshot replies to all RangeFetchActors and awaits their exit.
  3. Acked Offset Commit: group.commit() persists only explicitly ack()ed consumer offsets.
  4. Revoke Ownership: group.clear_effective_ownership() clears local partition assignments.
  5. Group Leave: group.leave() sends SyncConsumerGroupAction::Leave to the coordinator, awaits confirmation, and sets quit = true.
  • Prefetch Buffer Purge: Purges leftover prefetched records from consumer_rx upon shutdown to guarantee immediate termination visibility across all consumer clones.
  • Drop Idempotency: ConsumerGroup::drop checks the quit atomic flag to make drop() a no-op when close().await was called, preserving a best-effort background leave fallback for un-closed drops.

3. Configuration & Error Handling

  • CLI & Environment Validation: Added clap value_parser = parse_nonzero_usize for max_batch_bytes and max_batch_records in BufferConfig, while maintaining runtime BufferConfig::validate() domain error validation.
  • Error Variants: Added ClientError::ConsumerClosed and enhanced client redirect error mapping.

- Multi-Stage Ordered Shutdown Protocol:
  - Implemented async Consumer::close().await:
          1. Fence Delivery: group.fence_delivery() prevents newly fetched records from reaching the consumer iterator.
          2. Stop & Await Fetchers: stop_all() dispatches StopRangeFetch with oneshot reply channels to all range fetcher actors and awaits their exit.
          3. Commit Acked Offsets: group.commit() persists only explicitly ack()ed consumer offsets to the cluster.
          4. Revoke Ownership: group.clear_effective_ownership() clears local assigned ranges.
          5. Leave Group: group.leave() sends SyncConsumerGroupAction::Leave to the coordinator, awaits confirmation, and sets quit = true.

- Drop Idempotency & Defense-in-Depth:
  - Updated ConsumerGroup::drop to check quit atomic flag: returns immediately as a zero-cost no-op if close() was called, while retaining a best-effort background task fallback for un-closed drops.
  - Added ClientError::ConsumerClosed error variant.
- Added ClientError::Server(ServerError).
- Actual terminal server failures now preserve their typed category.
- Wrong success variants and ClientResponse::Stop remain UnexpectedResponse.
- Existing special handling remains intact for topic existence, stale ranges, producer rejection, and consumer-generation fencing.
- Redirect and retry behavior is unchanged.
- Full batches are now producer-owned tasks, so canceling the triggering send() cannot cancel other records.
- flush() and close() still wait through flush_gate.
- Added deterministic e2e coverage for the cancellation case.
- Documented precisely when cancellation can still allow publication.
- Producer flush()/close() drains are now producer-owned tasks, so canceling the waiting future does not cancel the drain.
- The send gate now covers acceptance/buffer insertion, not acknowledgement waiting.

- Threshold batches reserve the flush gate before releasing acceptance, preventing flush() from overtaking detached publication.
@Migorithm Migorithm self-assigned this Jul 24, 2026
@Migorithm Migorithm added bug Something isn't working documentation Improvements or additions to documentation enhancement New feature or request feat labels Jul 24, 2026
  - Removed accidental Producer implementations of Deref, DerefMut, and From<Arc<Inner>>.
  - Made producer implementation state private.
  - Replaced the stale synchronization documentation with an accurate internal comment.
  - Kept Producer cloning and all intended methods unchanged.
@Migorithm
Migorithm merged commit 65dfffa into main Jul 25, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working documentation Improvements or additions to documentation enhancement New feature or request feat

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Stabilize the client SDK lifecycle and public error contract for 1.0

1 participant