magnetar/engine/mod.rs
1// SPDX-License-Identifier: Apache-2.0
2
3//! `Engine` trait — the abstraction the public [`crate::PulsarClient`] is
4//! generic over.
5//!
6//! `Engine` is a marker trait with a single associated type
7//! ([`Engine::ClientState`]) that selects the per-engine storage backing
8//! [`crate::PulsarClient<E>`]. Today the two implementations are
9//! [`TokioEngine`] (production, default) and [`MoonpoolEngine<P>`]
10//! (deterministic simulation; `P` is the
11//! [`moonpool_core::Providers`](moonpool_core::Providers) bundle).
12//!
13//! Engine-specific methods (`producer`, `consumer`, partitioned, …) live in
14//! dedicated `impl PulsarClient<ConcreteEngine>` blocks rather than on the
15//! trait — production engines have wildly different connect signatures
16//! (tokio takes a URL, moonpool takes `host:port` + a `Providers` bundle)
17//! and trying to surface those through a single trait would either lose
18//! typing or reintroduce the per-engine façade duplication
19//! [ADR-0019](../../specs/adr/0019-engine-scope-and-moonpool-parity.md)
20//! rejected as Option B.
21//!
22//! Instead, moonpool callers that reach for a tokio-only method get a
23//! clean trait-bound error rather than a silent fallback — exactly the
24//! ADR-0019 §Decision contract.
25//!
26//! See ADR-0019 gate (e) — "Option A: generic `PulsarClient<E: Engine>`
27//! with default `E = TokioEngine`" — for the rationale.
28//!
29//! # Module layout
30//!
31//! - `mod.rs` (this file) — the [`Engine`] trait, the per-surface extension traits
32//! (`TransactionApi`, `ProducerApi`, `ConsumerApi`, `BrokerMetadataApi`, `SubscribeApi`,
33//! `CreateProducerApi`), the shared type aliases (`SubscribeFut`, `ReceiveOptFut`,
34//! `ReceiveBatchFut`, `WatchTopicListFut`, `OpenProducerFut`), and the [`TopicListChange`] data
35//! struct.
36//! - [`tokio`] — the [`TokioEngine`] marker + every `impl … for magnetar_runtime_tokio::*` block.
37//! - [`moonpool`] — the [`MoonpoolEngine`] marker + every `impl<P> … for
38//! magnetar_runtime_moonpool::*` block.
39
40use std::fmt::Debug;
41use std::future::Future;
42use std::pin::Pin;
43use std::time::Duration;
44
45#[cfg(feature = "moonpool")]
46pub(crate) mod moonpool;
47#[cfg(feature = "tokio")]
48pub(crate) mod tokio;
49
50#[cfg(feature = "moonpool")]
51pub use moonpool::MoonpoolEngine;
52#[cfg(feature = "tokio")]
53pub use tokio::TokioEngine;
54
55/// Marker trait labelling a runtime engine. Implementations select the
56/// concrete storage type ([`Self::ClientState`]) that backs the engine's
57/// branch of [`crate::PulsarClient<E>`].
58///
59/// `'static + Send + Sync` mirrors what we already require of producers and
60/// consumers; downstream users that hand `PulsarClient<E>` to a tokio
61/// `spawn` (or moonpool `spawn`) need at least that.
62///
63/// # Task and timer primitives (ADR-0025 phase 1)
64///
65/// The associated [`Self::TaskHandle`] and [`Self::Interval`] types plus the
66/// [`Self::spawn`] / [`Self::abort_task`] / [`Self::new_interval`] /
67/// [`Self::interval_tick`] methods give the façade an engine-agnostic way to
68/// spawn background tasks and drive periodic timers. They are the
69/// prerequisite for moving `PartitionedProducer::health_loop`,
70/// `TableView::drain_task`, `MultiTopicsConsumer::auto_update`, and the
71/// other surface lifts off `impl PulsarClient<TokioEngine>`. See
72/// [ADR-0025](../../specs/adr/0025-engine-trait-task-and-timer-primitives.md).
73pub trait Engine:
74 'static + Send + Sync + Debug + MessageEncryptorApi + MessageDecryptorApi
75{
76 /// Per-engine state stored inside [`crate::PulsarClient<E>`]. The tokio
77 /// engine plugs in [`magnetar_runtime_tokio::Client`]; the moonpool
78 /// engine plugs in `(Arc<moonpool::ConnectionShared>,
79 /// moonpool::DriverHandle)`. Both bundles are `'static + Send + Sync`
80 /// so the façade can be moved across spawn boundaries unchanged.
81 type ClientState: 'static + Send + Sync;
82
83 /// Opaque, cancel-safe handle to a background task spawned via
84 /// [`Self::spawn`]. Dropping the handle aborts the task on the tokio
85 /// engine; explicit [`Self::abort_task`] is the happens-before-Drop
86 /// path the façade uses on shutdown.
87 type TaskHandle: 'static + Send;
88
89 /// Opaque periodic timer created via [`Self::new_interval`]. The
90 /// façade drives ticks via [`Self::interval_tick`].
91 type Interval: 'static + Send;
92
93 /// Human-readable engine name, surfaced in logs / panics / errors.
94 /// Default returns the Rust type name — engines override to e.g.
95 /// `"tokio"` / `"moonpool"`.
96 fn name() -> &'static str
97 where
98 Self: Sized,
99 {
100 std::any::type_name::<Self>()
101 }
102
103 /// Spawn an async future on the engine's executor. Returns a cancel-
104 /// safe [`Self::TaskHandle`]. Tokio wraps [`::tokio::spawn`]; moonpool
105 /// delegates through its `Providers::TaskProvider` (`moonpool_core`).
106 fn spawn<F>(fut: F) -> Self::TaskHandle
107 where
108 F: Future<Output = ()> + Send + 'static;
109
110 /// Abort a spawned task. Idempotent: calling on an already-completed
111 /// or already-aborted handle is a no-op.
112 fn abort_task(handle: &mut Self::TaskHandle);
113
114 /// Create a periodic timer with `period` between ticks. The first
115 /// tick fires immediately (matches `tokio::time::interval`).
116 fn new_interval(period: Duration) -> Self::Interval;
117
118 /// Await the next tick. The returned future is `Send` and boxed so
119 /// the caller can `.await` from a generic context without exposing
120 /// the engine-specific timer shape.
121 fn interval_tick<'a>(
122 interval: &'a mut Self::Interval,
123 ) -> Pin<Box<dyn Future<Output = ()> + Send + 'a>>;
124
125 /// Engine-injected id provider for the façade's auto-generated
126 /// subscription names (`Reader`, `TableView`) and for the opt-in
127 /// [`ProducerBuilder::unique_name_suffix`](crate::ProducerBuilder::unique_name_suffix)
128 /// policy (issue #406). Tokio plugs in
129 /// `Uuid::new_v4().simple()` (RFC 4122 random); moonpool plugs in
130 /// a process-global atomic counter so deterministic-simulation runs
131 /// produce stable, reproducible names. Callers that need fully
132 /// deterministic names across processes should always pass an
133 /// explicit subscription / reader / producer name through the builder and
134 /// leave the suffix policy off.
135 fn random_subscription_suffix() -> String
136 where
137 Self: Sized;
138
139 /// Engine-provided `OAuth2` [`magnetar_auth_oauth2::Clock`]. Used by
140 /// callers that build a `ClientCredentialsFlow` from generic-engine
141 /// code so the `OAuth2` cache deadlines flow through the same clock
142 /// the engine uses everywhere else, instead of always landing on
143 /// `Arc::new(SystemClock)` at the `OAuth2` builder boundary.
144 ///
145 /// Default is `Arc::new(magnetar_auth_oauth2::SystemClock)` —
146 /// matches the `OAuth2` builder's own default. Engines wired into a
147 /// virtual-time substrate (e.g. moonpool with `SimProviders`)
148 /// override this to return a clock that reads the simulated time.
149 #[cfg(feature = "auth-oauth2")]
150 fn oauth2_clock() -> std::sync::Arc<dyn magnetar_auth_oauth2::Clock>
151 where
152 Self: Sized,
153 {
154 std::sync::Arc::new(magnetar_auth_oauth2::SystemClock)
155 }
156}
157
158// ---------------------------------------------------------------------------
159// Per-surface extension traits — ADR-0026 §D1.
160//
161// The Engine trait stays at ADR-0025 phase 1 (task + timer primitives).
162// Each Pulsar surface family (transactions, reader, typed schemas, …)
163// instead defines its own extension trait implemented by each runtime
164// on its `Client` type. The façade then writes
165// `impl<E: Engine> PulsarClient<E> where E::ClientState: TransactionApi`
166// and dispatches via `<E::ClientState as TransactionApi>::method(...)`.
167//
168// Why an extension trait, not a method on `Engine`:
169// - Engine primitives are bounded (spawn / timer / clock).
170// - Surface families grow with each PIP — putting them on `Engine` would mean every engine grows
171// with the Pulsar wire surface.
172// - Each engine implements only the families it supports. Moonpool can land Transaction before
173// TableView without the trait fattening.
174//
175// Sans-io: every trait method here returns a `Future` that resolves into
176// a broker round-trip; the I/O lives in the runtime crates that
177// implement these traits. `magnetar-proto` carries no `TransactionApi`
178// dep — the protocol-level handshakes (`CommandNewTxn` →
179// `CommandNewTxnResponse`, etc.) already live on `Connection` and are
180// called via `shared.inner.lock(); conn.new_txn(...)` from inside the
181// runtime impl. The trait surface stays free of tokio / mio / socket
182// types. See [ADR-0004](../../specs/adr/0004-sans-io-protocol-core.md).
183// ---------------------------------------------------------------------------
184
185/// Pulsar transactions (PIP-31) — implemented by each runtime on its
186/// `Client` type. Phase 1 of the D1 lift train.
187///
188/// The façade's [`crate::PulsarClient::new_transaction`] +
189/// `commit_transaction` / `abort_transaction` + the two `register_*`
190/// methods dispatch through this trait once
191/// [`crate::PulsarClient<E>`]'s impl block carries the
192/// `where E::ClientState: TransactionApi` bound. Subsequent surface
193/// lifts (`Reader`, `TypedSchemas`, `TableView`, …) follow the same
194/// template — one extension trait per family.
195///
196/// **Sans-io.** Methods are `async fn` returning `impl Future + Send +
197/// '_`; no tokio / mio / socket types appear in the trait surface. The
198/// runtime impl is responsible for driving the
199/// [`magnetar_proto::Connection`] state machine and waking its driver.
200///
201/// See [ADR-0026](../../specs/adr/0026-design-decisions-d1-d4-from-fdb-pulsar-codex-review.md)
202/// §D1 for the rationale (concrete-generic surfaces over GATs).
203pub trait TransactionApi {
204 /// Error surfaced by the runtime when a TC round-trip fails.
205 /// Each runtime maps this onto its own client-error variant.
206 type Error: std::error::Error + Send + Sync + 'static;
207
208 /// Open a new transaction at the broker-side transaction coordinator
209 /// (`CommandNewTxn` → `CommandNewTxnResponse`). Returns the TC-assigned
210 /// [`magnetar_proto::TxnId`] on success.
211 fn new_txn(
212 &self,
213 timeout: Duration,
214 ) -> Pin<Box<dyn Future<Output = Result<magnetar_proto::TxnId, Self::Error>> + Send + '_>>;
215
216 /// Register a partition that the given transaction will write to
217 /// (`CommandAddPartitionToTxn` → `CommandAddPartitionToTxnResponse`).
218 fn add_partition_to_txn(
219 &self,
220 txn: magnetar_proto::TxnId,
221 topic: String,
222 ) -> Pin<Box<dyn Future<Output = Result<(), Self::Error>> + Send + '_>>;
223
224 /// Register a subscription that the given transaction will
225 /// acknowledge on
226 /// (`CommandAddSubscriptionToTxn` → `CommandAddSubscriptionToTxnResponse`).
227 fn add_subscription_to_txn(
228 &self,
229 txn: magnetar_proto::TxnId,
230 topic: String,
231 subscription: String,
232 ) -> Pin<Box<dyn Future<Output = Result<(), Self::Error>> + Send + '_>>;
233
234 /// Commit or abort an open transaction
235 /// (`CommandEndTxn` → `CommandEndTxnResponse`). Returns the final
236 /// transaction state reported by the TC.
237 fn end_txn(
238 &self,
239 txn: magnetar_proto::TxnId,
240 action: magnetar_proto::TxnAction,
241 ) -> Pin<Box<dyn Future<Output = Result<magnetar_proto::TxnState, Self::Error>> + Send + '_>>;
242}
243
244// `OutgoingMessage` + `IncomingMessage` currently live in
245// `client.rs` (tokio-gated). Until those move to a feature-
246// independent module, the `ProducerApi` / `ConsumerApi` traits also
247// gate on the same set of features. Phase 4 of the façade lift will
248// move the message types out of `client.rs` to drop this gate.
249
250/// Pulsar producer wire surface — implemented by each runtime on its
251/// `Producer` type. Foundational for the seven dependent façade lifts
252/// (`Reader`, `TypedSchemas`, `MultiTopicsConsumer`, `PartitionedProducer`,
253/// `PartitionedConsumer`, `PatternConsumer`, `TableView`) per ADR-0026 §D1.
254///
255/// **Sans-io.** Async methods return `Pin<Box<dyn Future + Send + '_>>`;
256/// no tokio / mio / socket types appear in the surface. Each impl drives
257/// the [`magnetar_proto::Connection`] state machine and wakes its
258/// runtime-specific driver.
259///
260/// The method set here is **wire-level**: `send` (the only wire-bound
261/// publish path), `flush` (drain pending), `is_closed`, `topic`, `name`,
262/// `last_sequence_id`. Higher-level helpers (`send_bytes`, `stats`,
263/// `batch_len`, `pending_count`, `get_schema`, `access_mode`) stay
264/// engine-specific until a façade caller needs them — extending this
265/// trait is a non-breaking change so the additive growth pattern is
266/// safe.
267#[cfg(feature = "tokio")]
268pub trait ProducerApi: 'static + Send + Sync {
269 /// Per-runtime client error type used by the wire calls.
270 type Error: std::error::Error + Send + Sync + 'static;
271
272 /// Send a message. Resolves with the broker-assigned
273 /// [`magnetar_proto::MessageId`].
274 fn send(
275 &self,
276 msg: crate::OutgoingMessage,
277 ) -> Pin<Box<dyn Future<Output = Result<magnetar_proto::MessageId, Self::Error>> + Send + '_>>;
278
279 /// Wait for every previously-queued send to be acknowledged or
280 /// fail. Mirrors Java `Producer#flush()`.
281 fn flush(&self) -> Pin<Box<dyn Future<Output = Result<(), Self::Error>> + Send + '_>>;
282
283 /// `true` once the producer has entered a terminal state.
284 fn is_closed(&self) -> bool;
285
286 /// `true` while the broker connection is up. Mirrors Java
287 /// `Producer#isConnected`.
288 fn is_connected(&self) -> bool;
289
290 /// Topic this producer publishes to.
291 fn topic(&self) -> String;
292
293 /// Producer name advertised to the broker (broker-assigned if
294 /// the user didn't set one).
295 fn name(&self) -> String;
296
297 /// Latest sequence id the producer assigned. Mirrors Java
298 /// `Producer#getLastSequenceId`.
299 fn last_sequence_id(&self) -> i64;
300
301 /// Look up the broker-registered schema for the producer's topic
302 /// (PIP-87). Used by
303 /// `magnetar_proto::schema::AutoProduceBytesSchema` to warm its
304 /// cache on first send. `version = None` asks for the current
305 /// schema; pass `Some(schema_version_bytes)` to re-resolve.
306 fn get_schema(
307 &self,
308 version: Option<bytes::Bytes>,
309 ) -> Pin<Box<dyn Future<Output = Result<magnetar_proto::pb::Schema, Self::Error>> + Send + '_>>;
310
311 /// Cumulative producer-side counters. Mirrors Java
312 /// `Producer#getStats`. Returns a zeroed snapshot if the
313 /// producer handle is no longer registered.
314 fn stats(&self) -> magnetar_proto::producer::ProducerStats;
315
316 /// Clone of this producer's live send-latency histogram (issue #347).
317 /// `None` if the producer handle is no longer registered or the
318 /// histogram was never initialised. Used by
319 /// [`crate::PartitionedProducer::aggregate_stats`] to merge several
320 /// producers' distributions via
321 /// [`magnetar_proto::producer::ProducerStats::fold`] — `stats()` alone
322 /// only carries the three pre-computed percentiles, not the
323 /// distribution a sound merge needs.
324 fn send_latency_histogram(&self) -> Option<hdrhistogram::Histogram<u64>>;
325
326 /// Consume the producer and tear down the broker-side resource
327 /// (`CommandCloseProducer`). Mirrors Java `Producer#close`.
328 /// Both runtime types implement close by consuming `self`; the
329 /// trait exposes the same shape so generic façade surfaces (e.g.
330 /// `PartitionedProducer<P>::close`) can fan out closes over a
331 /// `Vec<P>`.
332 fn close_owned(self) -> Pin<Box<dyn Future<Output = Result<(), Self::Error>> + Send>>
333 where
334 Self: Sized;
335
336 /// Wall-clock timestamp of the last broker disconnection observed
337 /// by this producer's connection, or `None` if no disconnect has
338 /// happened yet. Mirrors Java
339 /// `Producer#getLastDisconnectedTimestamp`.
340 fn last_disconnected_timestamp(&self) -> Option<std::time::SystemTime>;
341
342 /// Compression codec this producer was opened with. Mirrors Java
343 /// `ProducerImpl#conf.getCompressionType()`. Returns
344 /// `CompressionKind::None` when the producer was opened without
345 /// explicit compression.
346 fn compression(&self) -> magnetar_proto::types::CompressionKind;
347
348 /// Last sequence id the broker has acknowledged via
349 /// `CommandSendReceipt`. Returns `-1` if no sends have been acked
350 /// yet. Mirrors Java
351 /// `Producer#getLastSequenceIdPublished`. Useful for
352 /// resume-from-checkpoint flows.
353 fn last_sequence_id_published(&self) -> i64;
354
355 /// Number of in-flight sends (queued and not yet acked by the
356 /// broker). Mirrors the un-batched view of Java
357 /// `ProducerStats#getPendingQueueSize`. Equivalent to
358 /// `self.stats().pending_queue_size as usize` but spares the full
359 /// stats snapshot.
360 fn pending_count(&self) -> usize;
361
362 /// Number of messages currently buffered in the batch container,
363 /// waiting for the next flush cycle. Returns `0` when batching is
364 /// disabled or the batch is empty.
365 fn batch_len(&self) -> usize;
366
367 /// Sum of payload bytes currently buffered in the batch container.
368 fn batch_bytes(&self) -> usize;
369}
370
371/// Pulsar consumer wire surface — implemented by each runtime on its
372/// `Consumer` type. Foundational alongside [`ProducerApi`] per
373/// ADR-0026 §D1.
374///
375/// Same sans-io contract as [`ProducerApi`]. The method set covers
376/// the wire-level subscription lifecycle: `receive`, the ack family
377/// (`ack`, `ack_cumulative`, `ack_with_txn`, `ack_cumulative_with_txn`),
378/// `negative_ack`, plus topic / subscription / `is_closed` accessors.
379///
380/// Pass-2 of the `MultiTopicsConsumer` / `PatternConsumer` lift extends
381/// this trait with the queue/permits getters (`available_in_queue`,
382/// `available_permits`, `has_received_any_message`, `has_reached_end_of_topic`,
383/// `is_paused`, `is_inactive`), the DLQ helpers (`drain_dead_letter`,
384/// `republish_dead_letters`, `reconsume_later`,
385/// `reconsume_later_with_properties`), the receive-batch family
386/// (`receive_with_timeout`, `receive_batch`, `receive_batch_with_bytes_cap`),
387/// flow control (`pause`, `resume`), and the remaining seek primitives
388/// (`seek_to_message`, `seek_to_timestamp`). The `unsubscribe` method now
389/// carries the PIP-313 `force: bool` flag so the trait matches the runtime
390/// signatures verbatim.
391///
392/// The associated [`Self::Producer`] type ties each engine's `Consumer` to
393/// its matching `Producer`, letting [`Self::republish_dead_letters`] and the
394/// [`Self::reconsume_later`] family accept a runtime-typed producer reference
395/// at the trait level without re-introducing a tokio-only carve-out.
396#[cfg(feature = "tokio")]
397pub trait ConsumerApi: 'static + Send + Sync {
398 /// Per-runtime client error type used by the wire calls.
399 type Error: std::error::Error + Send + Sync + 'static;
400
401 /// Matched runtime producer used by the DLQ + retry helpers.
402 /// Each runtime ties this to its own `Producer` (tokio →
403 /// [`magnetar_runtime_tokio::Producer`]; moonpool →
404 /// `magnetar_runtime_moonpool::Producer<P>`) so
405 /// [`Self::republish_dead_letters`] /
406 /// [`Self::reconsume_later`] /
407 /// [`Self::reconsume_later_with_properties`] dispatch through the
408 /// trait without a tokio-only carve-out.
409 type Producer: ProducerApi<Error = Self::Error>;
410
411 /// Receive the next message. Resolves once the broker has
412 /// delivered an entry. Returns the
413 /// [`magnetar_proto::IncomingMessage`] surfaced by the state
414 /// machine; callers that prefer the façade-side
415 /// [`crate::IncomingMessage`] (with computed accessors) can call
416 /// `.into()` on the result.
417 fn receive(
418 &self,
419 ) -> Pin<
420 Box<dyn Future<Output = Result<magnetar_proto::IncomingMessage, Self::Error>> + Send + '_>,
421 >;
422
423 /// Acknowledge `message_id` individually. Mirrors Java
424 /// `Consumer#acknowledge(MessageId)`.
425 fn ack(
426 &self,
427 message_id: magnetar_proto::MessageId,
428 ) -> Pin<Box<dyn Future<Output = Result<(), Self::Error>> + Send + '_>>;
429
430 /// Acknowledge all messages up to and including `message_id`.
431 /// Mirrors Java `Consumer#acknowledgeCumulative(MessageId)`.
432 fn ack_cumulative(
433 &self,
434 message_id: magnetar_proto::MessageId,
435 ) -> Pin<Box<dyn Future<Output = Result<(), Self::Error>> + Send + '_>>;
436
437 /// Negatively acknowledge `message_id`. Triggers a redelivery
438 /// after the configured `nackRedeliveryBackoff`. Mirrors Java
439 /// `Consumer#negativeAcknowledge`.
440 fn negative_ack(&self, message_id: magnetar_proto::MessageId);
441
442 /// Ask the broker for the topic's last-published message id.
443 /// Mirrors Java `Consumer#getLastMessageId`.
444 fn last_message_id(
445 &self,
446 ) -> Pin<Box<dyn Future<Output = Result<magnetar_proto::MessageId, Self::Error>> + Send + '_>>;
447
448 /// `true` if the broker has at least one message strictly past
449 /// `cursor`. Mirrors Java `Consumer#hasMessageAvailable` (with a
450 /// caller-supplied cursor variant).
451 fn has_message_after(
452 &self,
453 cursor: magnetar_proto::MessageId,
454 ) -> Pin<Box<dyn Future<Output = Result<bool, Self::Error>> + Send + '_>>;
455
456 /// Look up the broker-registered schema for the consumer's topic
457 /// (PIP-87). Used by
458 /// `magnetar_proto::schema::AutoConsumeSchema` to warm its cache
459 /// on first receive. `version = None` asks for the current schema;
460 /// pass `Some(schema_version_bytes)` to re-resolve.
461 fn get_schema(
462 &self,
463 version: Option<bytes::Bytes>,
464 ) -> Pin<Box<dyn Future<Output = Result<magnetar_proto::pb::Schema, Self::Error>> + Send + '_>>;
465
466 /// Topic this consumer is subscribed to.
467 fn topic(&self) -> String;
468
469 /// Subscription name this consumer holds.
470 fn subscription(&self) -> String;
471
472 /// Broker-assigned consumer name. Empty string when not yet known.
473 /// Mirrors Java `Consumer#getConsumerName`.
474 fn name(&self) -> String;
475
476 /// `true` once the consumer has entered a terminal state.
477 fn is_closed(&self) -> bool;
478
479 /// `true` while the broker connection is up. Mirrors Java
480 /// `Consumer#isConnected`.
481 fn is_connected(&self) -> bool;
482
483 /// Cumulative consumer-side counters. Mirrors Java
484 /// `Consumer#getStats`. Returns a zeroed snapshot if the consumer
485 /// handle is no longer registered.
486 fn stats(&self) -> magnetar_proto::consumer::ConsumerStats;
487
488 /// Clone of this consumer's live receive-latency histogram (issue
489 /// #347). `None` if the consumer handle is no longer registered or the
490 /// histogram was never initialised. Used by
491 /// [`crate::MultiTopicsConsumer::aggregate_stats`] (and, via the
492 /// `PartitionedConsumer` alias, `PartitionedConsumer::aggregate_stats`)
493 /// to merge several consumers' distributions via
494 /// [`magnetar_proto::consumer::ConsumerStats::fold`] — `stats()` alone
495 /// only carries the three pre-computed percentiles, not the
496 /// distribution a sound merge needs.
497 fn receive_latency_histogram(&self) -> Option<hdrhistogram::Histogram<u64>>;
498
499 /// Last broker-reported Failover active/standby state (issue #348).
500 /// `None` until the first `CommandActiveConsumerChange` lands for this
501 /// consumer (e.g. a `Shared` / `Exclusive` subscription never receives
502 /// it). Mirrors the implicit state Java's `ConsumerEventListener`
503 /// callbacks track.
504 fn is_active(&self) -> Option<bool>;
505
506 /// Resolve the next not-yet-observed Failover active/standby transition.
507 /// Backs [`crate::spawn_consumer_event_listener`] — the poller awaits
508 /// this in a loop and turns each `Ok(bool)` into a
509 /// [`crate::ConsumerEvent::BecameActive`] /
510 /// [`crate::ConsumerEvent::BecameInactive`] callback. Resolves the same
511 /// error [`Self::receive`] does once the consumer reaches a terminal
512 /// state with no unobserved transition buffered.
513 fn next_active_change(
514 &self,
515 ) -> Pin<Box<dyn Future<Output = Result<bool, Self::Error>> + Send + '_>>;
516
517 /// Wall-clock timestamp of the last broker disconnection observed
518 /// by this consumer's connection, or `None` if no disconnect has
519 /// happened yet. Mirrors Java
520 /// `Consumer#getLastDisconnectedTimestamp`.
521 fn last_disconnected_timestamp(&self) -> Option<std::time::SystemTime>;
522
523 /// Ask the broker to redeliver every unacknowledged message on
524 /// this consumer. Mirrors Java
525 /// `Consumer#redeliverUnacknowledgedMessages`.
526 fn redeliver_unacked(&self);
527
528 /// Negatively acknowledge a single message with an explicit
529 /// per-message redelivery delay. PIP-37 backoff variant.
530 fn negative_ack_with_delay(
531 &self,
532 message_id: magnetar_proto::MessageId,
533 delay: std::time::Duration,
534 );
535
536 /// Tear down this consumer's subscription on the broker. Mirrors
537 /// Java `Consumer#unsubscribe`. `force=true` selects the PIP-313
538 /// destructive variant that detaches every other attached consumer
539 /// on the same subscription; `force=false` (Java default) keeps the
540 /// cursor in place when other consumers are still attached.
541 fn unsubscribe(
542 &self,
543 force: bool,
544 ) -> Pin<Box<dyn Future<Output = Result<(), Self::Error>> + Send + '_>>;
545
546 /// Seek to the earliest available message. Mirrors Java
547 /// `Consumer#seek(MessageId.earliest)`.
548 fn seek_to_earliest(
549 &self,
550 ) -> Pin<Box<dyn Future<Output = Result<(), Self::Error>> + Send + '_>>;
551
552 /// Seek to the latest available message. Mirrors Java
553 /// `Consumer#seek(MessageId.latest)`.
554 fn seek_to_latest(&self) -> Pin<Box<dyn Future<Output = Result<(), Self::Error>> + Send + '_>>;
555
556 /// Seek to an explicit message id. Mirrors Java
557 /// `Consumer#seek(MessageId)`.
558 fn seek_to_message(
559 &self,
560 message_id: magnetar_proto::MessageId,
561 ) -> Pin<Box<dyn Future<Output = Result<(), Self::Error>> + Send + '_>>;
562
563 /// Seek to a publish-time deadline (broker-side wall clock, ms
564 /// since epoch). Mirrors Java `Consumer#seek(long)`.
565 fn seek_to_timestamp(
566 &self,
567 publish_time_ms: u64,
568 ) -> Pin<Box<dyn Future<Output = Result<(), Self::Error>> + Send + '_>>;
569
570 /// Stop automatic flow refills. Mirrors Java `Consumer#pause` —
571 /// already-issued permits keep draining, no new FLOW frames are
572 /// emitted until [`Self::resume`].
573 fn pause(&self);
574
575 /// Re-enable automatic flow refills. Mirrors Java `Consumer#resume`.
576 fn resume(&self);
577
578 /// Number of messages currently buffered in the per-consumer
579 /// receiver queue, waiting for a `receive()` call. Mirrors Java
580 /// `Consumer#getNumMessagesInQueue`.
581 fn available_in_queue(&self) -> usize;
582
583 /// Outstanding dispatch permits the broker still holds un-spent for this
584 /// consumer — the grants it has been given, minus one per dispatch unit
585 /// that has actually arrived. Mirrors Java
586 /// `ConsumerBase#getAvailablePermits`.
587 ///
588 /// Issue #414 re-pointed this from the purely-additive grant mirror to
589 /// the real decrementing balance, so a value pinned high while messages
590 /// stop arriving is now a usable stall signal (ADR-0101 amending
591 /// ADR-0082).
592 fn available_permits(&self) -> u32;
593
594 /// `true` once the consumer has received at least one message since
595 /// opening. Mirrors Java `Consumer#hasReceivedAnyMessage`.
596 fn has_received_any_message(&self) -> bool;
597
598 /// `true` once the broker has indicated end-of-topic for this
599 /// consumer (no more messages will be dispatched). Mirrors Java
600 /// `Consumer#hasReachedEndOfTopic`.
601 fn has_reached_end_of_topic(&self) -> bool;
602
603 /// `true` while [`Self::pause`] has flipped the consumer's flow
604 /// refills off. Mirrors Java `Consumer#isPaused`.
605 fn is_paused(&self) -> bool;
606
607 /// Mirrors Java `Consumer#isInactive`. Returns `true` once the
608 /// consumer has reached end-of-topic (no more messages will be
609 /// dispatched). Note: a closed consumer is not represented as
610 /// "inactive" here.
611 fn is_inactive(&self) -> bool;
612
613 /// Drain every message the state machine has flagged as dead-letter
614 /// (redelivery count greater than the configured
615 /// `max_redeliver_count`). The caller is responsible for
616 /// republishing them to the DLQ topic (or using
617 /// [`Self::republish_dead_letters`] for the transparent path).
618 fn drain_dead_letter(&self) -> Vec<magnetar_proto::IncomingMessage>;
619
620 /// Receive the next message bounded by `timeout`. Resolves with
621 /// `Ok(None)` when the deadline elapses with no message. Mirrors
622 /// Java `Consumer#receive(int, TimeUnit)`.
623 fn receive_with_timeout(&self, timeout: Duration) -> ReceiveOptFut<'_, Self>;
624
625 /// Receive up to `max_messages` messages in one call. Waits up to
626 /// `max_wait` for the first message, then drains additional
627 /// already-buffered messages without further waiting. Mirrors Java
628 /// `Consumer#batchReceive`.
629 fn receive_batch(&self, max_messages: usize, max_wait: Duration) -> ReceiveBatchFut<'_, Self>;
630
631 /// Same as [`Self::receive_batch`] but stops once the accumulated
632 /// payload size would exceed `max_bytes`. Mirrors Java
633 /// `BatchReceivePolicy` with all three caps (count, bytes, wait).
634 fn receive_batch_with_bytes_cap(
635 &self,
636 max_messages: usize,
637 max_bytes: usize,
638 max_wait: Duration,
639 ) -> ReceiveBatchFut<'_, Self>;
640
641 /// Drain the per-consumer dead-letter queue and republish every
642 /// entry via `dlq_producer`, preserving the message's metadata.
643 /// Acks each original after a successful republish. Returns the
644 /// number of messages republished.
645 fn republish_dead_letters<'a>(
646 &'a self,
647 dlq_producer: &'a Self::Producer,
648 ) -> Pin<Box<dyn Future<Output = Result<usize, Self::Error>> + Send + 'a>>;
649
650 /// Republish a single message via `retry_producer` with a
651 /// `delay`-bounded deadline, then ack the original. Mirrors Java
652 /// `Consumer#reconsumeLater(Message, long, TimeUnit)`.
653 fn reconsume_later<'a>(
654 &'a self,
655 retry_producer: &'a Self::Producer,
656 msg: magnetar_proto::IncomingMessage,
657 delay: Duration,
658 ) -> Pin<Box<dyn Future<Output = Result<(), Self::Error>> + Send + 'a>>;
659
660 /// Same as [`Self::reconsume_later`] but lets the caller stamp
661 /// additional custom properties on the republished message. Mirrors
662 /// Java's properties-aware reconsumeLater overload.
663 fn reconsume_later_with_properties<'a>(
664 &'a self,
665 retry_producer: &'a Self::Producer,
666 msg: magnetar_proto::IncomingMessage,
667 custom_properties: Vec<(String, String)>,
668 delay: Duration,
669 ) -> Pin<Box<dyn Future<Output = Result<(), Self::Error>> + Send + 'a>>;
670
671 /// Consume the consumer and reliably tear down the broker-side resource
672 /// (`CommandCloseConsumer`). Mirrors Java `Consumer#close`: both runtime
673 /// implementations await the broker acknowledgement and return close
674 /// errors through the future.
675 ///
676 /// Dropping the final runtime consumer clone is a distinct best-effort
677 /// safety net that cannot report acknowledgement or failure. Generic
678 /// code that requires confirmed release must call and await this method.
679 fn close_owned(self) -> Pin<Box<dyn Future<Output = Result<(), Self::Error>> + Send>>
680 where
681 Self: Sized;
682
683 /// Fire-and-forget individual ack into the consumer's
684 /// ack-grouping tracker (opt-in via
685 /// `ConsumerBuilder::ack_group_time`). The state machine flushes
686 /// the tracker after `ack_group_time` elapses, emitting one
687 /// coalesced `CommandAck`. With no tracker configured, the proto
688 /// layer falls back to a synchronous immediate `CommandAck` so the
689 /// message is never silently dropped. Mirrors Java's
690 /// `acknowledgmentGroupTime` path.
691 fn ack_grouped(&self, message_id: magnetar_proto::MessageId);
692
693 /// Fire-and-forget cumulative ack into the consumer's ack-grouping
694 /// tracker. See [`Self::ack_grouped`] for the semantics.
695 fn ack_grouped_cumulative(&self, message_id: magnetar_proto::MessageId);
696
697 /// Acknowledge `message_id` as part of a Pulsar transaction
698 /// (PIP-31). The ack only takes effect once the transaction
699 /// commits. Mirrors Java
700 /// `Consumer#acknowledgeAsync(MessageId, Transaction)`.
701 fn ack_with_txn(
702 &self,
703 message_id: magnetar_proto::MessageId,
704 txn_id: magnetar_proto::TxnId,
705 ) -> Pin<Box<dyn Future<Output = Result<(), Self::Error>> + Send + '_>>;
706
707 /// Cumulative ack as part of a Pulsar transaction (PIP-31).
708 /// Mirrors Java
709 /// `Consumer#acknowledgeCumulativeAsync(MessageId, Transaction)`.
710 fn ack_cumulative_with_txn(
711 &self,
712 message_id: magnetar_proto::MessageId,
713 txn_id: magnetar_proto::TxnId,
714 ) -> Pin<Box<dyn Future<Output = Result<(), Self::Error>> + Send + '_>>;
715}
716
717/// PIP-145 `TopicListChanged` delta surfaced through
718/// [`BrokerMetadataApi::poll_topic_list_change`]. Façade-side analogue
719/// of the per-runtime `TopicListChange` structs — each runtime impl
720/// converts its own delta into this engine-agnostic shape so generic
721/// surfaces (`PatternConsumer<C>::update`) can reconcile without
722/// touching runtime-specific types.
723#[cfg(feature = "tokio")]
724#[derive(Debug, Clone)]
725pub struct TopicListChange {
726 /// Topics that newly match the pattern.
727 pub added: Vec<String>,
728 /// Topics that no longer match the pattern.
729 pub removed: Vec<String>,
730}
731
732/// One setup-operation context shared across every caller-visible stage.
733///
734/// The pinned timer enforces the total deadline, while `last_broker_error`
735/// preserves the newest broker diagnostic across metadata, lookup, routing,
736/// and attachment so a later timeout does not replace it with a generic error.
737#[cfg(feature = "tokio")]
738#[doc(hidden)]
739pub struct OperationDeadline {
740 timer: Pin<Box<dyn Future<Output = ()> + Send + 'static>>,
741 last_broker_error: Option<(i32, String)>,
742}
743
744type OperationDeadlineParts<'a> = (
745 Pin<&'a mut (dyn Future<Output = ()> + Send)>,
746 &'a mut Option<(i32, String)>,
747);
748
749#[cfg(feature = "tokio")]
750impl core::fmt::Debug for OperationDeadline {
751 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
752 f.debug_struct("OperationDeadline").finish_non_exhaustive()
753 }
754}
755
756#[cfg(feature = "tokio")]
757impl OperationDeadline {
758 /// Build a façade deadline from an engine-provided timer.
759 #[doc(hidden)]
760 pub fn new(timer: Pin<Box<dyn Future<Output = ()> + Send + 'static>>) -> Self {
761 Self {
762 timer,
763 last_broker_error: None,
764 }
765 }
766
767 fn never() -> Self {
768 Self::new(Box::pin(std::future::pending()))
769 }
770
771 /// Reborrow the pinned engine timer.
772 #[doc(hidden)]
773 pub fn timer(&mut self) -> Pin<&mut (dyn Future<Output = ()> + Send)> {
774 self.timer.as_mut()
775 }
776
777 /// Reborrow both parts for a built-in runtime operation.
778 pub(crate) fn parts(&mut self) -> OperationDeadlineParts<'_> {
779 (self.timer.as_mut(), &mut self.last_broker_error)
780 }
781}
782
783/// Engine-side broker metadata lookups used by
784/// [`crate::PartitionedConsumerBuilder`] and
785/// [`crate::PatternConsumerBuilder`] (alongside other partition-aware
786/// surfaces). Each runtime implements this on its concrete `Client`
787/// type.
788///
789/// Same sans-io contract as [`SubscribeApi`] — async methods return
790/// `Pin<Box<dyn Future + Send + '_>>`; the impl drives the
791/// `magnetar_proto::Connection` state machine.
792#[cfg(feature = "tokio")]
793pub trait BrokerMetadataApi: 'static + Send + Sync {
794 /// Per-runtime client error type.
795 type Error: std::error::Error + Send + Sync + 'static;
796
797 /// Query the broker for the partition count of `topic`. Returns
798 /// `0` for non-partitioned topics. Mirrors Java
799 /// `PulsarClient#getPartitionsForTopic`.
800 fn partitioned_topic_metadata<'a>(
801 &'a self,
802 topic: &'a str,
803 ) -> Pin<Box<dyn Future<Output = Result<u32, Self::Error>> + Send + 'a>>;
804
805 /// Create a provider-correct setup timer.
806 ///
807 /// The default preserves compatibility for downstream custom engines:
808 /// their established operation method remains authoritative until they
809 /// opt into the deadline-aware companion below.
810 #[doc(hidden)]
811 fn new_metadata_operation_deadline(&self) -> OperationDeadline {
812 OperationDeadline::never()
813 }
814
815 /// Deadline-aware metadata lookup used by built-in composite builders.
816 #[doc(hidden)]
817 fn partitioned_topic_metadata_with_deadline<'a>(
818 &'a self,
819 topic: &'a str,
820 _deadline: &'a mut OperationDeadline,
821 ) -> Pin<Box<dyn Future<Output = Result<u32, Self::Error>> + Send + 'a>> {
822 self.partitioned_topic_metadata(topic)
823 }
824
825 /// Subscribe to a topic-list watcher and return the initial topic
826 /// snapshot for the given namespace + regex pattern (PIP-145).
827 fn watch_topic_list<'a>(
828 &'a self,
829 namespace: &'a str,
830 pattern: &'a str,
831 ) -> WatchTopicListFut<'a, Self>;
832
833 /// Deadline-aware topic-list snapshot used by built-in composite builders.
834 #[doc(hidden)]
835 fn watch_topic_list_with_deadline<'a>(
836 &'a self,
837 namespace: &'a str,
838 pattern: &'a str,
839 _deadline: &'a mut OperationDeadline,
840 ) -> WatchTopicListFut<'a, Self> {
841 self.watch_topic_list(namespace, pattern)
842 }
843
844 /// Drain the next pending `TopicListChanged` delta from the
845 /// connection's PIP-145 buffer, if any. Returns `None` when no
846 /// deltas are pending. Used by `PatternConsumer::update` to
847 /// reconcile its child set.
848 fn poll_topic_list_change(&self) -> Option<TopicListChange>;
849}
850
851/// Engine-side subscribe surface used by `ConsumerBuilder<E>` and the
852/// other consumer-spawning façade surfaces (`MultiTopicsConsumer`,
853/// `PatternConsumer`, `Reader`). Each runtime implements this on its
854/// concrete `Client` type with the runtime-specific `Consumer` type
855/// surfaced via the associated `Consumer` type.
856///
857/// Per ADR-0026 §D1: this is the next sub-PR after the per-surface
858/// lifts. Lifting `ConsumerBuilder<E>` to dispatch through this
859/// trait unblocks the impl-body lifts on the four phantom-lifted
860/// surfaces (`TypedSchemas`, `MultiTopicsConsumer` /
861/// `PartitionedConsumer`, `PatternConsumer`).
862#[cfg(feature = "tokio")]
863pub trait SubscribeApi: 'static + Send + Sync {
864 /// Concrete consumer type each runtime returns. Required to
865 /// implement [`ConsumerApi`] so generic surfaces can dispatch
866 /// further methods through that trait.
867 type Consumer: ConsumerApi;
868 /// Runtime client error.
869 type Error: std::error::Error + Send + Sync + 'static;
870
871 /// Issue a `CommandSubscribe` and resolve with the broker-side
872 /// `CommandSuccess` correlated with the request id (subscribe
873 /// ack). After this resolves the state machine has a fresh
874 /// per-consumer queue and the initial FLOW frame has been queued
875 /// for the driver.
876 fn subscribe(&self, req: magnetar_proto::SubscribeRequest) -> SubscribeFut<'_, Self>;
877
878 /// Create a provider-correct setup timer.
879 #[doc(hidden)]
880 fn new_subscribe_operation_deadline(&self) -> OperationDeadline {
881 OperationDeadline::never()
882 }
883
884 /// Deadline-aware subscribe used by built-in composite builders.
885 #[doc(hidden)]
886 fn subscribe_with_deadline<'a>(
887 &'a self,
888 req: magnetar_proto::SubscribeRequest,
889 _deadline: &'a mut OperationDeadline,
890 ) -> SubscribeFut<'a, Self> {
891 self.subscribe(req)
892 }
893}
894
895/// Helper alias: `SubscribeApi::subscribe` future return type.
896#[cfg(feature = "tokio")]
897pub type SubscribeFut<'a, S> = Pin<
898 Box<
899 dyn Future<Output = Result<<S as SubscribeApi>::Consumer, <S as SubscribeApi>::Error>>
900 + Send
901 + 'a,
902 >,
903>;
904
905/// Helper alias: `ConsumerApi::receive_with_timeout` future return type.
906#[cfg(feature = "tokio")]
907pub type ReceiveOptFut<'a, C> = Pin<
908 Box<
909 dyn Future<
910 Output = Result<Option<magnetar_proto::IncomingMessage>, <C as ConsumerApi>::Error>,
911 > + Send
912 + 'a,
913 >,
914>;
915
916/// Helper alias: `ConsumerApi::receive_batch` / `receive_batch_with_bytes_cap`
917/// future return type.
918#[cfg(feature = "tokio")]
919pub type ReceiveBatchFut<'a, C> = Pin<
920 Box<
921 dyn Future<Output = Result<Vec<magnetar_proto::IncomingMessage>, <C as ConsumerApi>::Error>>
922 + Send
923 + 'a,
924 >,
925>;
926
927/// Helper alias: `BrokerMetadataApi::watch_topic_list` future return type.
928#[cfg(feature = "tokio")]
929pub type WatchTopicListFut<'a, B> =
930 Pin<Box<dyn Future<Output = Result<Vec<String>, <B as BrokerMetadataApi>::Error>> + Send + 'a>>;
931
932/// Engine-side producer-creation surface used by `ProducerBuilder<E>`
933/// and `PartitionedProducer<E>`. Same shape as [`SubscribeApi`] for
934/// the producer side.
935#[cfg(feature = "tokio")]
936pub trait CreateProducerApi: 'static + Send + Sync {
937 /// Concrete producer type each runtime returns.
938 type Producer: ProducerApi;
939 /// Runtime client error.
940 type Error: std::error::Error + Send + Sync + 'static;
941
942 /// Issue a `CommandProducer` and resolve with
943 /// `CommandProducerSuccess` correlated with the request id.
944 fn open_producer(
945 &self,
946 req: magnetar_proto::CreateProducerRequest,
947 ) -> OpenProducerFut<'_, Self>;
948
949 /// Create a provider-correct setup timer.
950 #[doc(hidden)]
951 fn new_producer_operation_deadline(&self) -> OperationDeadline {
952 OperationDeadline::never()
953 }
954
955 /// Deadline-aware producer-open used by built-in composite builders.
956 #[doc(hidden)]
957 fn open_producer_with_deadline<'a>(
958 &'a self,
959 req: magnetar_proto::CreateProducerRequest,
960 _deadline: &'a mut OperationDeadline,
961 ) -> OpenProducerFut<'a, Self> {
962 self.open_producer(req)
963 }
964}
965
966/// Helper alias: `CreateProducerApi::open_producer` future return type.
967#[cfg(feature = "tokio")]
968pub type OpenProducerFut<'a, P> = Pin<
969 Box<
970 dyn Future<
971 Output = Result<
972 <P as CreateProducerApi>::Producer,
973 <P as CreateProducerApi>::Error,
974 >,
975 > + Send
976 + 'a,
977 >,
978>;
979
980// ---------------------------------------------------------------------------
981// PIP-4 per-engine encryption extension traits.
982//
983// Tokio defines `magnetar_runtime_tokio::MessageEncryptor` and
984// `magnetar_runtime_tokio::MessageDecryptor` for its own producer / consumer
985// surfaces. The façade builders historically stored
986// `Option<Arc<dyn magnetar_runtime_tokio::MessageEncryptor>>` directly,
987// hard-locking them to the tokio engine. The two extension traits below
988// lift that storage off tokio: each engine declares its own concrete
989// encryptor / decryptor type, the façade stores
990// `Option<<E as MessageEncryptorApi>::Encryptor>` instead. Both runtime
991// engines now ship the PIP-4 bridge, so each resolves the associated type
992// to its own `Arc<dyn …MessageEncryptor>` / `…MessageDecryptor`. The
993// zero-sized [`NoEncryption`] stub remains for any future engine that
994// genuinely cannot drive on-the-wire encryption.
995//
996// The traits live on the engine marker (not on `Client`) because the
997// encryptor identity is engine-global config rather than per-connection
998// state. The associated `Encryptor` / `Decryptor` types are `Clone +
999// Send + Sync + 'static` so the builders can pass them to the runtime's
1000// `open_producer_with` / `subscribe_with` without further bounds churn.
1001//
1002// Sans-io: the traits define types only. Real encryption happens in the
1003// runtime crates that supply the concrete types (`magnetar-runtime-tokio`
1004// and `magnetar-runtime-moonpool`).
1005// ---------------------------------------------------------------------------
1006
1007/// Engine-side message-encryptor selection. Each engine declares its own
1008/// concrete encryptor type; the façade's `ProducerBuilder` stores
1009/// `Option<E::Encryptor>` (engine-typed) instead of an
1010/// `Arc<dyn magnetar_runtime_tokio::MessageEncryptor>` (tokio-locked).
1011///
1012/// Implemented on the engine marker ([`TokioEngine`] / [`MoonpoolEngine<P>`]).
1013/// Tokio plugs in `Arc<dyn magnetar_runtime_tokio::MessageEncryptor>`;
1014/// moonpool plugs in `Arc<dyn magnetar_runtime_moonpool::MessageEncryptor>`.
1015/// The choice of `Encryptor: Clone` lets the façade fan out the encryptor
1016/// across child producers in `PartitionedProducer`.
1017pub trait MessageEncryptorApi {
1018 /// Concrete per-engine encryptor type. `Clone + Send + Sync + 'static`
1019 /// so it survives spawn boundaries and fan-out into child producers.
1020 type Encryptor: Clone + Send + Sync + 'static;
1021}
1022
1023/// Engine-side message-decryptor selection. Mirror of
1024/// [`MessageEncryptorApi`] for the consume path. Implemented on the
1025/// engine marker.
1026pub trait MessageDecryptorApi {
1027 /// Concrete per-engine decryptor type. `Clone + Send + Sync + 'static`.
1028 type Decryptor: Clone + Send + Sync + 'static;
1029}
1030
1031// ---------------------------------------------------------------------------
1032// PIP-460 scalable topics (ADR-0093, experimental). The `ScalableTopicsApi`
1033// extension trait follows the same ADR-0026 §D1 pattern as `TransactionApi`:
1034// defined here, implemented by each runtime on its `Client` type (which is the
1035// engine's `ClientState`), dispatched through
1036// `impl<E: Engine> PulsarClient<E> where E::ClientState: ScalableTopicsApi`.
1037// Gated on `feature = "scalable-topics"` so the default surface is unchanged.
1038// ---------------------------------------------------------------------------
1039
1040/// **Experimental** (PIP-460, ADR-0093). Engine-side scalable-topic hooks —
1041/// implemented by each runtime on its `Client` type. The façade's
1042/// [`crate::scalable::StreamConsumer`] dispatches through this trait once
1043/// [`crate::PulsarClient<E>`] carries the
1044/// `where E::ClientState: ScalableTopicsApi` bound.
1045///
1046/// **Sans-io.** Async methods return `Pin<Box<dyn Future + Send + '_>>`; no
1047/// tokio / mio / socket types appear in the surface. Each impl drives the
1048/// [`magnetar_proto::Connection`] scalable entries
1049/// (`open_scalable_topic_session`, `close_scalable_topic_session`) and reads the
1050/// driver-drained events.
1051#[cfg(all(feature = "tokio", feature = "scalable-topics"))]
1052pub trait ScalableTopicsApi: 'static + Send + Sync {
1053 /// Per-runtime client error type.
1054 type Error: std::error::Error + Send + Sync + 'static;
1055
1056 /// Open a scalable-topic session and await its first layout. The session
1057 /// stays open, pushing later layouts through
1058 /// [`Self::next_scalable_event`], until it is closed.
1059 fn scalable_topic_lookup<'a>(
1060 &'a self,
1061 topic: &'a str,
1062 ) -> Pin<Box<dyn Future<Output = Result<ScalableLookup, Self::Error>> + Send + 'a>>;
1063
1064 /// Whether the connected broker advertised the PIP-460 capability.
1065 /// `false` against a Pulsar 4.x peer.
1066 fn broker_supports_scalable_topics(&self) -> bool;
1067
1068 /// Register as a scalable consumer with the controller leader and await the
1069 /// initial assignment — the `segment://` topics this consumer owns.
1070 fn scalable_topic_subscribe<'a>(
1071 &'a self,
1072 topic: &'a str,
1073 subscription: &'a str,
1074 consumer_name: &'a str,
1075 consumer_id: u64,
1076 consumer_type: magnetar_proto::ScalableConsumerType,
1077 ) -> Pin<
1078 Box<
1079 dyn Future<Output = Result<magnetar_proto::ConsumerAssignment, Self::Error>>
1080 + Send
1081 + 'a,
1082 >,
1083 >;
1084
1085 /// Open a namespace-level watch over the scalable topics matching
1086 /// `property_filters` (empty = every scalable topic in the namespace).
1087 fn watch_scalable_topics(
1088 &self,
1089 namespace: &str,
1090 property_filters: Vec<(String, String)>,
1091 ) -> Result<u64, Self::Error>;
1092
1093 /// Close a namespace-level scalable-topics watch.
1094 fn close_scalable_topics_watch(&self, watch_id: u64);
1095
1096 /// The current matching topic set for a namespace watch.
1097 fn scalable_topics_snapshot(&self, watch_id: u64) -> Option<Vec<String>>;
1098
1099 /// Whether the broker advertised metadata-driven transaction-coordinator
1100 /// discovery. Independent of `supports_scalable_topics` upstream.
1101 fn broker_supports_tc_metadata_discovery(&self) -> bool;
1102
1103 /// Open a transaction-coordinator discovery watch.
1104 fn watch_tc_assignments(&self) -> Result<u64, Self::Error>;
1105
1106 /// Close a transaction-coordinator discovery watch.
1107 fn close_tc_assignments_watch(&self, watch_id: u64);
1108
1109 /// Close a scalable-topic session.
1110 fn close_scalable_topic_session(&self, session_id: u64);
1111
1112 /// Await the next scalable-topic event (DAG update / drop-on-change /
1113 /// close). Resolves `None` once the connection closes.
1114 fn next_scalable_event(
1115 &self,
1116 ) -> Pin<Box<dyn Future<Output = Option<ScalableEvent>> + Send + '_>>;
1117}
1118
1119/// **Experimental** (PIP-460, ADR-0093). Engine-agnostic resolved
1120/// scalable-topic lookup surfaced through [`ScalableTopicsApi`]. Façade-side
1121/// analogue of each runtime's `ScalableLookup`.
1122#[cfg(all(feature = "tokio", feature = "scalable-topics"))]
1123#[derive(Debug, Clone)]
1124pub struct ScalableLookup {
1125 /// Client-allocated session id; the session stays open until closed.
1126 pub session_id: u64,
1127 /// Canonical `topic://...` identity the broker resolved the request to.
1128 pub resolved_topic_name: Option<String>,
1129 /// Controller broker serving this topic's layout, when advertised.
1130 pub controller_broker_url: Option<String>,
1131 /// Initial DAG snapshot for the topic.
1132 pub segments: Vec<magnetar_proto::SegmentDescriptor>,
1133 /// Layout epoch the snapshot was stamped with.
1134 pub epoch: u64,
1135}
1136
1137/// **Experimental** (PIP-460, ADR-0093). Engine-agnostic scalable-topic event
1138/// surfaced through [`ScalableTopicsApi::next_scalable_event`]. Façade-side
1139/// analogue of each runtime's `ScalableEvent`.
1140#[cfg(all(feature = "tokio", feature = "scalable-topics"))]
1141#[derive(Debug, Clone)]
1142pub enum ScalableEvent {
1143 /// A scalable-topic session resolved: its first layout landed.
1144 LookupResolved {
1145 /// Client-allocated session id.
1146 session_id: u64,
1147 /// Canonical `topic://...` identity the broker resolved to.
1148 resolved_topic_name: Option<String>,
1149 /// Controller broker serving this topic's layout, when advertised.
1150 controller_broker_url: Option<String>,
1151 /// Initial DAG snapshot.
1152 segments: Vec<magnetar_proto::SegmentDescriptor>,
1153 /// Layout epoch the snapshot was stamped with.
1154 epoch: u64,
1155 },
1156 /// An open session applied a subsequent layout.
1157 DagUpdated {
1158 /// Session id.
1159 session_id: u64,
1160 /// The applied delta.
1161 delta: magnetar_proto::DagDelta,
1162 },
1163 /// The segment DAG changed under a live consumer (drop-on-change).
1164 DagChangedDuringConsume {
1165 /// Session id whose DAG changed.
1166 session_id: u64,
1167 /// Why the DAG changed.
1168 reason: magnetar_proto::DagChangeReason,
1169 },
1170 /// The scalable-topic session closed.
1171 DagWatchClosed {
1172 /// Session id that closed.
1173 session_id: u64,
1174 /// Optional close reason.
1175 reason: Option<String>,
1176 },
1177 /// A scalable consumer's registration resolved with its initial share.
1178 ConsumerAssigned {
1179 /// Consumer id that registered.
1180 consumer_id: u64,
1181 /// The `segment://` topics this consumer owns.
1182 assignment: magnetar_proto::ConsumerAssignment,
1183 },
1184 /// The controller leader rebalanced a registered consumer's share.
1185 AssignmentChanged {
1186 /// Consumer id whose share changed.
1187 consumer_id: u64,
1188 /// What to attach to and detach from.
1189 delta: magnetar_proto::AssignmentDelta,
1190 },
1191 /// A scalable consumer's registration was rejected.
1192 ConsumerRejected {
1193 /// Consumer id whose registration failed.
1194 consumer_id: u64,
1195 /// Why the broker rejected it.
1196 reason: String,
1197 },
1198 /// A namespace-level scalable-topics watch delivered a snapshot or a diff.
1199 TopicsChanged {
1200 /// Watch id the update belongs to.
1201 watch_id: u64,
1202 /// The snapshot or diff the broker sent.
1203 change: magnetar_proto::TopicsChange,
1204 },
1205 /// A namespace-level scalable-topics watch ended.
1206 TopicsWatchClosed {
1207 /// Watch id that closed.
1208 watch_id: u64,
1209 /// Optional close reason.
1210 reason: Option<String>,
1211 },
1212 /// The metadata-driven transaction-coordinator assignment set changed.
1213 TcAssignmentsChanged {
1214 /// Watch id the update belongs to.
1215 watch_id: u64,
1216 /// Number of transaction-coordinator partitions.
1217 parallelism: u32,
1218 /// Which broker serves each coordinator.
1219 assignments: Vec<magnetar_proto::TcAssignment>,
1220 },
1221 /// A transaction-coordinator discovery watch ended.
1222 TcAssignmentsWatchClosed {
1223 /// Watch id that closed.
1224 watch_id: u64,
1225 /// Optional close reason.
1226 reason: Option<String>,
1227 },
1228}
1229
1230/// Zero-sized stub for any future engine that genuinely cannot wire real
1231/// encryption. Both shipped engines ([`TokioEngine`] and
1232/// [`MoonpoolEngine`]) now resolve their `MessageEncryptorApi::Encryptor`
1233/// / `MessageDecryptorApi::Decryptor` to their own runtime's
1234/// `Arc<dyn …MessageEncryptor>` / `…MessageDecryptor`, so `NoEncryption`
1235/// is no longer used by either. It is retained as the documented opt-out
1236/// type an engine can hand to the façade to signal "encryption not
1237/// supported on this engine" — the builders' generic `.create()` /
1238/// `.subscribe()` paths ignore the encryptor field regardless.
1239#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
1240pub struct NoEncryption;
1241
1242// Per-engine storage for [`crate::PulsarClient<MoonpoolEngine<P>>`] is
1243// [`magnetar_runtime_moonpool::Client<P>`] directly — see
1244// `Engine::ClientState` above. This mirrors the tokio engine
1245// (`type ClientState = magnetar_runtime_tokio::Client`) so the existing
1246// `SubscribeApi` / `CreateProducerApi` / `ConsumerApi` / `ProducerApi`
1247// impls on the runtime `Client<P>` automatically satisfy the trait
1248// bounds the façade builders dispatch through, without a parallel
1249// state struct.
1250
1251#[cfg(test)]
1252mod tests {
1253 // NOTE: We import the trait + marker types explicitly rather than
1254 // via `use super::*;`. The parent module exposes sibling `tokio` /
1255 // `moonpool` submodules whose names would shadow the external
1256 // `tokio` crate inside this test scope and break the
1257 // `#[::tokio::test]` macro expansions below.
1258 use super::Engine;
1259 #[cfg(feature = "moonpool")]
1260 use super::MoonpoolEngine;
1261 #[cfg(feature = "tokio")]
1262 use super::TokioEngine;
1263
1264 #[cfg(feature = "tokio")]
1265 #[test]
1266 fn tokio_engine_implements_engine() {
1267 fn takes_engine<E: Engine>() -> &'static str {
1268 E::name()
1269 }
1270 assert_eq!(takes_engine::<TokioEngine>(), "tokio");
1271 }
1272
1273 #[cfg(feature = "tokio")]
1274 #[test]
1275 fn tokio_engine_is_send_sync() {
1276 fn assert_send_sync<T: Send + Sync>() {}
1277 assert_send_sync::<TokioEngine>();
1278 }
1279
1280 #[cfg(feature = "moonpool")]
1281 #[test]
1282 fn moonpool_engine_implements_engine() {
1283 use moonpool_core::TokioProviders;
1284 fn takes_engine<E: Engine>() -> &'static str {
1285 E::name()
1286 }
1287 assert_eq!(takes_engine::<MoonpoolEngine<TokioProviders>>(), "moonpool");
1288 }
1289
1290 #[cfg(feature = "moonpool")]
1291 #[test]
1292 fn moonpool_engine_is_send_sync() {
1293 use moonpool_core::TokioProviders;
1294 fn assert_send_sync<T: Send + Sync>() {}
1295 assert_send_sync::<MoonpoolEngine<TokioProviders>>();
1296 }
1297
1298 #[cfg(all(feature = "tokio", feature = "auth-oauth2"))]
1299 #[test]
1300 fn tokio_engine_oauth2_clock_is_monotonic() {
1301 let clock = <TokioEngine as Engine>::oauth2_clock();
1302 let a = clock.now();
1303 let b = clock.now();
1304 assert!(b >= a, "OAuth2 clock must be monotonic");
1305 }
1306
1307 #[cfg(all(feature = "moonpool", feature = "auth-oauth2"))]
1308 #[test]
1309 fn moonpool_engine_oauth2_clock_is_monotonic() {
1310 use moonpool_core::TokioProviders;
1311 let clock = <MoonpoolEngine<TokioProviders> as Engine>::oauth2_clock();
1312 let a = clock.now();
1313 let b = clock.now();
1314 assert!(b >= a, "OAuth2 clock must be monotonic");
1315 }
1316
1317 // -------------------------------------------------------------
1318 // ADR-0025 phase 1: task + timer primitive smoke tests. One pair
1319 // per engine — keeps the per-engine test count balanced even
1320 // though the new primitives don't yet have façade callers.
1321
1322 // Note: the tests below reference the external `tokio` crate via the
1323 // absolute `::tokio::` path because this module has a sibling
1324 // `tokio` submodule (carrying the `TokioEngine` impl) — the
1325 // unqualified `tokio` identifier would otherwise resolve to that
1326 // submodule rather than to the crate.
1327
1328 #[cfg(feature = "tokio")]
1329 #[::tokio::test(flavor = "current_thread", start_paused = true)]
1330 async fn tokio_engine_spawn_and_abort_round_trip() {
1331 use std::sync::Arc;
1332 use std::sync::atomic::{AtomicUsize, Ordering};
1333
1334 let counter = Arc::new(AtomicUsize::new(0));
1335 let c = counter.clone();
1336 let handle = <TokioEngine as Engine>::spawn(async move {
1337 c.fetch_add(1, Ordering::SeqCst);
1338 });
1339 // Drive the spawned task once.
1340 ::tokio::task::yield_now().await;
1341 // Awaiting the JoinHandle works on a non-aborted task.
1342 handle.await.expect("spawned task ran to completion");
1343 assert_eq!(counter.load(Ordering::SeqCst), 1);
1344
1345 // Spawn a second task that we abort before it can increment.
1346 let c2 = counter.clone();
1347 let mut handle2 = <TokioEngine as Engine>::spawn(async move {
1348 // Sleep forever — abort wins.
1349 ::tokio::time::sleep(std::time::Duration::from_hours(1)).await;
1350 c2.fetch_add(1, Ordering::SeqCst);
1351 });
1352 <TokioEngine as Engine>::abort_task(&mut handle2);
1353 // Second abort is a no-op.
1354 <TokioEngine as Engine>::abort_task(&mut handle2);
1355 assert_eq!(
1356 counter.load(Ordering::SeqCst),
1357 1,
1358 "aborted task must not run its body",
1359 );
1360 }
1361
1362 #[cfg(feature = "tokio")]
1363 #[::tokio::test(flavor = "current_thread", start_paused = true)]
1364 async fn tokio_engine_interval_first_tick_is_immediate() {
1365 use std::time::Duration;
1366
1367 let mut interval = <TokioEngine as Engine>::new_interval(Duration::from_secs(10));
1368 let start = ::tokio::time::Instant::now();
1369 <TokioEngine as Engine>::interval_tick(&mut interval).await;
1370 // First tick fires immediately per the tokio interval contract.
1371 assert_eq!(
1372 ::tokio::time::Instant::now().duration_since(start),
1373 Duration::ZERO,
1374 "first interval tick must fire immediately on tokio",
1375 );
1376 // Second tick waits for the period.
1377 <TokioEngine as Engine>::interval_tick(&mut interval).await;
1378 assert!(
1379 ::tokio::time::Instant::now().duration_since(start) >= Duration::from_secs(10),
1380 "second tick must wait one period",
1381 );
1382 }
1383
1384 #[cfg(feature = "moonpool")]
1385 #[::tokio::test(flavor = "current_thread", start_paused = true)]
1386 async fn moonpool_engine_spawn_and_abort_round_trip() {
1387 use std::sync::Arc;
1388 use std::sync::atomic::{AtomicUsize, Ordering};
1389
1390 use moonpool_core::TokioProviders;
1391
1392 type E = MoonpoolEngine<TokioProviders>;
1393
1394 let counter = Arc::new(AtomicUsize::new(0));
1395 let c = counter.clone();
1396 let handle = <E as Engine>::spawn(async move {
1397 c.fetch_add(1, Ordering::SeqCst);
1398 });
1399 ::tokio::task::yield_now().await;
1400 handle.await.expect("spawned task ran to completion");
1401 assert_eq!(counter.load(Ordering::SeqCst), 1);
1402
1403 let c2 = counter.clone();
1404 let mut handle2 = <E as Engine>::spawn(async move {
1405 ::tokio::time::sleep(std::time::Duration::from_hours(1)).await;
1406 c2.fetch_add(1, Ordering::SeqCst);
1407 });
1408 <E as Engine>::abort_task(&mut handle2);
1409 <E as Engine>::abort_task(&mut handle2);
1410 assert_eq!(
1411 counter.load(Ordering::SeqCst),
1412 1,
1413 "aborted task must not run its body",
1414 );
1415 }
1416
1417 #[cfg(feature = "moonpool")]
1418 #[::tokio::test(flavor = "current_thread", start_paused = true)]
1419 async fn moonpool_engine_interval_first_tick_is_immediate() {
1420 use std::time::Duration;
1421
1422 use moonpool_core::TokioProviders;
1423
1424 type E = MoonpoolEngine<TokioProviders>;
1425
1426 let mut interval = <E as Engine>::new_interval(Duration::from_secs(10));
1427 let start = ::tokio::time::Instant::now();
1428 <E as Engine>::interval_tick(&mut interval).await;
1429 assert_eq!(
1430 ::tokio::time::Instant::now().duration_since(start),
1431 Duration::ZERO,
1432 "first interval tick must fire immediately on moonpool",
1433 );
1434 <E as Engine>::interval_tick(&mut interval).await;
1435 assert!(
1436 ::tokio::time::Instant::now().duration_since(start) >= Duration::from_secs(10),
1437 "second tick must wait one period",
1438 );
1439 }
1440}