magnetar/builders.rs
1// SPDX-License-Identifier: Apache-2.0
2
3//! Per-surface builders for [`crate::PulsarClient`] — extracted from
4//! `client.rs` so the central façade module stays focused on the
5//! `PulsarClient` surface, message types, interceptor traits, and the
6//! `Reader` impl. The builders here are
7//! [`ProducerBuilder`] / [`ConsumerBuilder`] / [`ReaderBuilder`],
8//! each carrying a phantom `E: Engine` parameter (default
9//! [`crate::TokioEngine`]) so `PulsarClient<E>::producer(...)` /
10//! `consumer(...)` / `reader(...)` dispatch through the
11//! engine-generic factory traits.
12//!
13//! **Engine-genericity.** The encryptor
14//! / decryptor storage is engine-typed via the per-engine
15//! [`crate::MessageEncryptorApi`] / [`crate::MessageDecryptorApi`]
16//! extension traits: tokio plugs in
17//! `Arc<dyn magnetar_runtime_tokio::MessageEncryptor>` and moonpool
18//! plugs in `Arc<dyn magnetar_runtime_moonpool::MessageEncryptor>`
19//! (both engines now ship the PIP-4 bridge). The chainable
20//! surface stays engine-agnostic — the `E: Engine` parameter only
21//! surfaces in the terminal `.create()` / `.subscribe()` dispatch
22//! through [`crate::CreateProducerApi`] / [`crate::SubscribeApi`], and
23//! in the per-engine `.create_with_encryption()` /
24//! `.subscribe_with_decryption()` specialisations.
25//!
26//! All three builders are re-exported from `magnetar::*` via the
27//! façade `lib.rs` so existing call sites keep working unchanged.
28
29use std::time::Duration;
30
31use magnetar_proto::conn::{CreateProducerRequest, SubscribeRequest};
32use magnetar_proto::pb;
33use magnetar_proto::types::CompressionKind;
34
35use crate::client::{PulsarClient, PulsarError, Reader};
36
37/// Result alias used inside this module, mirroring the one in
38/// `client.rs`.
39type Result<T, E = PulsarError> = std::result::Result<T, E>;
40
41/// Builder for a producer.
42///
43/// Phantom-generic over `E: Engine` per ADR-0026 §D1 — type
44/// parameter present (defaulting to [`crate::TokioEngine`]). Same lift
45/// pattern as [`ConsumerBuilder`]; the inherent impl methods stay
46/// tokio-bound until the [`crate::CreateProducerApi`] dispatch
47/// path lands (foundation traits added in commit `cc61d4d`).
48pub struct ProducerBuilder<'a, E: crate::Engine = crate::TokioEngine> {
49 client: &'a PulsarClient<E>,
50 req: CreateProducerRequest,
51 /// Engine-typed encryptor slot. Tokio resolves
52 /// `<TokioEngine as MessageEncryptorApi>::Encryptor` to
53 /// `Arc<dyn magnetar_runtime_tokio::MessageEncryptor>`; moonpool
54 /// resolves it to `Arc<dyn magnetar_runtime_moonpool::MessageEncryptor>`.
55 /// The generic `.create()` path **rejects** a configured encryptor — only
56 /// the per-engine `.create_with_encryption()` specialisations actually
57 /// open a PIP-4-encrypting producer.
58 ///
59 /// `MessageEncryptorApi` is a supertrait of [`crate::Engine`], so the
60 /// resolution is automatic — no extra bound needed at the use site.
61 encryptor: Option<<E as crate::MessageEncryptorApi>::Encryptor>,
62 /// Opt-in unique-suffix policy for [`Self::name`] (issue #406).
63 /// `false` — the default — keeps the pinned name the caller asked for.
64 unique_name_suffix: bool,
65}
66
67impl<E: crate::Engine> std::fmt::Debug for ProducerBuilder<'_, E> {
68 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
69 f.debug_struct("ProducerBuilder")
70 .field("topic", &self.req.topic)
71 .field("producer_name", &self.req.producer_name)
72 .finish_non_exhaustive()
73 }
74}
75
76impl<'a, E: crate::Engine> ProducerBuilder<'a, E> {
77 pub(crate) fn new(client: &'a PulsarClient<E>, topic: String) -> Self {
78 let req = CreateProducerRequest {
79 topic,
80 ..CreateProducerRequest::default()
81 };
82 Self {
83 client,
84 req,
85 encryptor: None,
86 unique_name_suffix: false,
87 }
88 }
89
90 /// Set the optional producer name.
91 ///
92 /// The name is advertised verbatim unless [`Self::unique_name_suffix`] is
93 /// enabled.
94 #[must_use]
95 pub fn name(mut self, name: impl Into<String>) -> Self {
96 self.req.producer_name = Some(name.into());
97 self
98 }
99
100 /// Append an engine-generated unique suffix to the name set by
101 /// [`Self::name`]. **Off by default** — a pinned producer name stays
102 /// pinned.
103 ///
104 /// A pinned name is what makes a leaked broker-side producer fatal
105 /// (issue #406): the broker rejects every later open under a name it still
106 /// holds with `ProducerBusy` / `NamingException`, and a client that died
107 /// mid-open — or behind a proxy that outlives it — cannot send the
108 /// `CommandCloseProducer` that would free it. Enabling this makes each open
109 /// claim its own name, so a stranded registration can never collide with
110 /// the next one.
111 ///
112 /// The cost is the reason it is opt-in: a unique name breaks every
113 /// behaviour keyed on producer identity — `Exclusive` /
114 /// `WaitForExclusive` access-mode fencing, broker-side sequence-id dedup
115 /// across a restart, and dashboard/metric continuity. Enable it only for
116 /// `Shared` producers whose identity is disposable.
117 ///
118 /// With no [`Self::name`] set this is a no-op: the broker already assigns a
119 /// unique name of its own.
120 ///
121 /// The suffix comes from
122 /// [`crate::Engine::random_subscription_suffix`] — the same seam
123 /// [`ReaderBuilder`] uses for auto-generated subscription names — so the
124 /// tokio engine draws a UUID and the moonpool engine a deterministic
125 /// counter. Randomness stays out of `magnetar-proto`
126 /// (`ARCHITECTURE.md` § known non-determinism leaks).
127 #[must_use]
128 pub fn unique_name_suffix(mut self, enable: bool) -> Self {
129 self.unique_name_suffix = enable;
130 self
131 }
132
133 /// Apply the [`Self::unique_name_suffix`] policy to the request just
134 /// before it leaves the builder. Shared by [`Self::create`] and both
135 /// engine-specialised `create_with_encryption` entries.
136 fn resolve_producer_name(&mut self) {
137 if self.unique_name_suffix
138 && let Some(name) = self.req.producer_name.as_mut()
139 {
140 name.push('-');
141 name.push_str(&E::random_subscription_suffix());
142 }
143 }
144
145 /// Enable batching with the given limits.
146 #[must_use]
147 pub fn batching(mut self, max_messages: usize, max_bytes: usize) -> Self {
148 self.req.enable_batching = true;
149 self.req.max_messages_in_batch = max_messages;
150 self.req.max_batch_size_bytes = max_bytes;
151 self
152 }
153
154 /// Enable chunking for oversize messages.
155 #[must_use]
156 pub fn chunking(mut self, enable: bool) -> Self {
157 self.req.enable_chunking = enable;
158 self
159 }
160
161 /// Set the compression codec.
162 #[must_use]
163 pub fn compression(mut self, kind: CompressionKind) -> Self {
164 self.req.compression = kind;
165 self
166 }
167
168 /// Advertise the given Pulsar schema on `CommandProducer.schema`. The broker stores it
169 /// and surfaces it on the dashboard; magnetar does not enforce serialisation on its own
170 /// — pair with a [`crate::TypedProducer`] for that.
171 #[must_use]
172 pub fn schema(mut self, schema: pb::Schema) -> Self {
173 self.req.schema = Some(schema);
174 self
175 }
176
177 /// Mirrors Java `ProducerBuilder#initialSequenceId`. The producer's first publish gets
178 /// the supplied sequence id; the next one gets `id + 1`, and so on. Useful for resuming
179 /// at-least-once delivery from a checkpoint (where the caller knows the last sequence
180 /// id the broker acknowledged for this producer name).
181 #[must_use]
182 pub fn initial_sequence_id(mut self, id: u64) -> Self {
183 self.req.initial_sequence_id = Some(id);
184 self
185 }
186
187 /// Mirrors Java `ProducerBuilder#accessMode`. Defaults to `Shared`; switch to
188 /// `Exclusive` for single-writer-per-topic patterns, `WaitForExclusive` to queue
189 /// behind the current writer, or `ExclusiveWithFencing` to evict it.
190 #[must_use]
191 pub fn access_mode(mut self, mode: pb::ProducerAccessMode) -> Self {
192 self.req.access_mode = mode;
193 self
194 }
195
196 /// Mirrors Java `ProducerBuilder#property`. Appends a `(key, value)` entry to the
197 /// producer metadata advertised on `CommandProducer.metadata`. Visible on the broker
198 /// dashboard alongside the producer.
199 #[must_use]
200 pub fn property(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
201 self.req.producer_metadata.push((key.into(), value.into()));
202 self
203 }
204
205 /// Mirrors Java `ProducerBuilder#sendTimeout`. In-flight sends past
206 /// `enqueued_at + timeout` resolve with a synthetic `SendError` carrying
207 /// `code=-1, message="send timeout"` on the next state-machine tick.
208 ///
209 /// The default is **30 s** (Java parity — `sendTimeoutMs = 30000`, ADR-0072),
210 /// so a send whose receipt is lost or corrupted in flight fails deterministically
211 /// rather than hanging forever. Call [`Self::disable_send_timeout`] for the
212 /// unbounded (never-times-out) behavior.
213 #[must_use]
214 pub fn send_timeout(mut self, timeout: Duration) -> Self {
215 self.req.send_timeout = Some(timeout);
216 self
217 }
218
219 /// Disable the send timeout: in-flight sends never resolve with a synthetic
220 /// timeout `SendError` — they wait indefinitely for the broker's receipt
221 /// (or a session-loss / terminal error). Mirrors Java
222 /// `ProducerBuilder#sendTimeout(0, …)`. Overrides the 30 s default (ADR-0072).
223 #[must_use]
224 pub fn disable_send_timeout(mut self) -> Self {
225 self.req.send_timeout = None;
226 self
227 }
228
229 /// Mirrors Java `ProducerBuilder#batchingMaxPublishDelay`. With batching enabled,
230 /// the state machine flushes any non-empty batch whose oldest message has been waiting
231 /// longer than this duration. Caps end-to-end latency for batched sends that would
232 /// otherwise sit until the batch fills up.
233 #[must_use]
234 pub fn batching_max_publish_delay(mut self, delay: Duration) -> Self {
235 self.req.batching_max_publish_delay = Some(delay);
236 self
237 }
238
239 /// Open the producer via the engine-generic
240 /// [`crate::CreateProducerApi`] trait. Returns the engine's
241 /// concrete `Producer` type.
242 ///
243 /// **PIP-4 encryption guardrail (BREAKING since the encryptor-storage lift).**
244 /// If [`Self::encryption`] was called on the per-engine specialisation,
245 /// `.create()` returns [`PulsarError::Other`] instead of silently opening
246 /// a plaintext producer. The engine-generic dispatch does not know how to
247 /// thread an engine-typed encryptor through `open_producer`, so the
248 /// previous "silently drop the encryptor" behaviour was a footgun.
249 /// Use [`Self::create_with_encryption`] on the tokio /
250 /// moonpool specialisation instead.
251 ///
252 /// # Errors
253 /// - [`PulsarError::Other`] if an encryptor was configured via [`Self::encryption`] — call
254 /// `create_with_encryption()` instead.
255 /// - [`PulsarError::Other`] (stringified) on broker rejection or wire failure.
256 pub async fn create(
257 mut self,
258 ) -> Result<<E::ClientState as crate::CreateProducerApi>::Producer, PulsarError>
259 where
260 E::ClientState: crate::BrokerMetadataApi + crate::CreateProducerApi,
261 {
262 self.resolve_producer_name();
263 if self.encryptor.is_some() {
264 return Err(PulsarError::Other(
265 "ProducerBuilder::create() refuses a configured encryptor — \
266 use create_with_encryption() on the engine-specific builder \
267 (PIP-4 encryptors are engine-typed and cannot dispatch \
268 through the engine-generic CreateProducerApi)"
269 .to_owned(),
270 ));
271 }
272 // Pre-check the partition metadata: if the topic happens to be
273 // partitioned, opening a bare-topic producer surfaces as the broker's
274 // `NotAllowedError(22) "Found partitioned metadata for non-partitioned
275 // topic"`. Catch that here and surface an actionable error pointing at
276 // `client.partitioned_producer(...)`, which fans out into one child
277 // producer per partition (the Java client's
278 // `PulsarClientImpl#createProducerAsync` does the same thing
279 // transparently — magnetar makes the routing explicit per ADR-0051).
280 //
281 // The per-partition fast path in `partitioned_topic_metadata`
282 // short-circuits `<base>-partition-<N>` suffixes to `N == 0` without
283 // a broker round-trip; for other topic names this is one
284 // `CommandPartitionedTopicMetadata` round-trip, the same cost the
285 // Java client pays.
286 let topic = self.req.topic.clone();
287 let mut deadline =
288 crate::CreateProducerApi::new_producer_operation_deadline(&self.client.inner);
289 let partitions = crate::BrokerMetadataApi::partitioned_topic_metadata_with_deadline(
290 &self.client.inner,
291 &topic,
292 &mut deadline,
293 )
294 .await
295 .map_err(|err| PulsarError::Other(format!("partitioned_topic_metadata: {err}")))?;
296 if partitions > 0 {
297 return Err(PulsarError::Other(format!(
298 "topic `{topic}` is partitioned (broker reports {partitions} partitions); \
299 call `client.partitioned_producer(\"{topic}\").create()` instead — \
300 a bare `client.producer(t).create()` would round-trip to a broker \
301 NotAllowedError(22) \"Found partitioned metadata for non-partitioned topic\"."
302 )));
303 }
304 crate::CreateProducerApi::open_producer_with_deadline(
305 &self.client.inner,
306 self.req,
307 &mut deadline,
308 )
309 .await
310 .map_err(|err| PulsarError::Other(format!("open_producer: {err}")))
311 }
312}
313
314/// Tokio-engine-specific `ProducerBuilder` methods that depend on the
315/// tokio `MessageEncryptor` extension. The moonpool equivalent lives in
316/// the `#[cfg(feature = "moonpool")]` block below (ADR-0044).
317impl ProducerBuilder<'_, crate::TokioEngine> {
318 /// Set the PIP-4 encryptor. The encryptor is consulted on every
319 /// `send()` to wrap the (post-compression) payload.
320 #[must_use]
321 pub fn encryption(
322 mut self,
323 encryptor: std::sync::Arc<dyn magnetar_runtime_tokio::MessageEncryptor>,
324 ) -> Self {
325 // `<TokioEngine as MessageEncryptorApi>::Encryptor` resolves
326 // exactly to `Arc<dyn MessageEncryptor>` so we store the arg
327 // directly into the engine-typed slot.
328 self.encryptor = Some(encryptor);
329 self
330 }
331
332 /// Open the producer honoring the configured encryptor (PIP-4).
333 /// Tokio-engine-only — use [`Self::create`] for the engine-generic
334 /// path that ignores the encryptor.
335 ///
336 /// # Errors
337 /// - [`PulsarError::Client`] on broker rejection or wire failure.
338 pub async fn create_with_encryption(mut self) -> Result<magnetar_runtime_tokio::Producer> {
339 self.resolve_producer_name();
340 Ok(self
341 .client
342 .inner
343 .open_producer_with(self.req, self.encryptor)
344 .await?)
345 }
346}
347
348/// Moonpool-engine-specific `ProducerBuilder` methods that depend on the
349/// moonpool `MessageEncryptor` extension (PIP-4). 1:1 mirror of the tokio
350/// specialisation above — the moonpool runtime now ships the same encryption
351/// hook surface, so the façade exposes the same `.encryption()` +
352/// `.create_with_encryption()` chain for the moonpool engine.
353#[cfg(feature = "moonpool")]
354impl<P: moonpool_core::Providers + Send + Sync + 'static>
355 ProducerBuilder<'_, crate::MoonpoolEngine<P>>
356{
357 /// Set the PIP-4 encryptor. The encryptor is consulted on every
358 /// `send()` to wrap the (post-compression) payload.
359 #[must_use]
360 pub fn encryption(
361 mut self,
362 encryptor: std::sync::Arc<dyn magnetar_runtime_moonpool::MessageEncryptor>,
363 ) -> Self {
364 // `<MoonpoolEngine<P> as MessageEncryptorApi>::Encryptor` resolves
365 // exactly to `Arc<dyn MessageEncryptor>` so we store the arg
366 // directly into the engine-typed slot.
367 self.encryptor = Some(encryptor);
368 self
369 }
370
371 /// Open the producer honoring the configured encryptor (PIP-4).
372 /// Moonpool-engine-only — use [`Self::create`] for the engine-generic
373 /// path that ignores the encryptor.
374 ///
375 /// # Errors
376 /// - [`PulsarError::Other`] (stringified) on broker rejection or wire failure.
377 pub async fn create_with_encryption(
378 mut self,
379 ) -> Result<magnetar_runtime_moonpool::Producer<P>> {
380 self.resolve_producer_name();
381 self.client
382 .inner
383 .open_producer_with(self.req, self.encryptor)
384 .await
385 .map_err(|err| PulsarError::Other(format!("open_producer: {err}")))
386 }
387}
388
389/// Builder for a consumer.
390///
391/// Engine-generic over `E: Engine` per ADR-0026 §D1 (default
392/// [`crate::TokioEngine`]). The base `subscribe()` dispatches through
393/// the [`crate::SubscribeApi`] extension trait implemented by both
394/// runtimes' `Client`; the per-engine PIP-4 decryption knobs live on the
395/// engine-specialised `impl ConsumerBuilder<TokioEngine>` /
396/// `#[cfg(feature = "moonpool")]` blocks (ADR-0044).
397pub struct ConsumerBuilder<'a, E: crate::Engine = crate::TokioEngine> {
398 client: &'a PulsarClient<E>,
399 req: SubscribeRequest,
400 /// Engine-typed decryptor slot. See
401 /// [`ProducerBuilder`] for the analogous tokio /
402 /// moonpool split; same per-engine
403 /// [`crate::MessageDecryptorApi`] resolution (supertrait of
404 /// [`crate::Engine`], so no extra bound needed at the use site).
405 ///
406 /// The generic `.subscribe()` path **rejects** a configured decryptor —
407 /// only the per-engine `.subscribe_with_decryption()` specialisations
408 /// actually open a PIP-4-decrypting consumer.
409 decryptor: Option<<E as crate::MessageDecryptorApi>::Decryptor>,
410 /// Optional push-delivery callback (Java `ConsumerBuilder#messageListener`).
411 /// Engine-agnostic — the façade [`crate::MessageListener`] takes the façade
412 /// [`crate::IncomingMessage`], which both engines produce. Set it via
413 /// [`Self::message_listener`] and subscribe via
414 /// [`Self::subscribe_with_listener`]; the plain [`Self::subscribe`] ignores
415 /// it and returns a pull-mode consumer.
416 listener: Option<crate::MessageListener>,
417 /// Optional Failover active/standby callback (Java
418 /// `ConsumerBuilder#consumerEventListener`, issue #348). Set via
419 /// [`Self::consumer_event_listener`] and subscribe via
420 /// [`Self::subscribe_with_event_listener`]; the plain [`Self::subscribe`]
421 /// (and [`Self::subscribe_with_listener`]) ignore it.
422 event_listener: Option<crate::ConsumerEventListener>,
423}
424
425impl<E: crate::Engine> std::fmt::Debug for ConsumerBuilder<'_, E> {
426 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
427 f.debug_struct("ConsumerBuilder")
428 .field("topic", &self.req.topic)
429 .field("subscription", &self.req.subscription)
430 .finish_non_exhaustive()
431 }
432}
433
434impl<'a, E: crate::Engine> ConsumerBuilder<'a, E> {
435 pub(crate) fn new(client: &'a PulsarClient<E>, topic: String) -> Self {
436 let req = SubscribeRequest {
437 topic,
438 ..SubscribeRequest::default()
439 };
440 Self {
441 client,
442 req,
443 decryptor: None,
444 listener: None,
445 event_listener: None,
446 }
447 }
448
449 /// Read-only snapshot of the [`SubscribeRequest`] this builder has assembled
450 /// so far. Test-support seam (`#[doc(hidden)]`, not part of the stable API):
451 /// lets the builder-surface guard test assert that each of the five consumer
452 /// builders seeds the bounded-chunk-reassembly knobs into the request that
453 /// seeds `ConsumerState`, without opening a real broker connection.
454 #[doc(hidden)]
455 #[must_use]
456 pub fn request_snapshot(&self) -> &SubscribeRequest {
457 &self.req
458 }
459
460 /// Test-support seam (`#[doc(hidden)]`, not part of the stable API):
461 /// `true` once a push-delivery listener has been set via
462 /// [`Self::message_listener`]. Lets the builder-surface guard test pin the
463 /// listener → field wiring without opening a real broker connection.
464 #[doc(hidden)]
465 #[must_use]
466 pub fn has_listener_for_test(&self) -> bool {
467 self.listener.is_some()
468 }
469
470 /// Test-support seam (`#[doc(hidden)]`, not part of the stable API):
471 /// `true` once a [`crate::ConsumerEventListener`] has been set via
472 /// [`Self::consumer_event_listener`]. Lets the builder-surface guard test
473 /// pin the listener → field wiring without opening a real broker
474 /// connection.
475 #[doc(hidden)]
476 #[must_use]
477 pub fn has_event_listener_for_test(&self) -> bool {
478 self.event_listener.is_some()
479 }
480
481 /// Required: set the subscription name.
482 #[must_use]
483 pub fn subscription(mut self, name: impl Into<String>) -> Self {
484 self.req.subscription = name.into();
485 self
486 }
487
488 /// Set the subscription type.
489 #[must_use]
490 pub fn subscription_type(mut self, sub_type: pb::command_subscribe::SubType) -> Self {
491 self.req.sub_type = sub_type;
492 self
493 }
494
495 /// Set the consumer name.
496 #[must_use]
497 pub fn name(mut self, name: impl Into<String>) -> Self {
498 self.req.consumer_name = Some(name.into());
499 self
500 }
501
502 /// Set the receiver queue size (the fixed permit count handed to the broker).
503 ///
504 /// This is sugar for [`Self::receiver_queue_policy`]`(`[`magnetar_proto::Fixed`]`(size))`:
505 /// it pins the queue to a constant size, the historical (and default)
506 /// behaviour. To let the queue self-tune under load, use
507 /// [`Self::receiver_queue_policy`] with [`magnetar_proto::Auto`] instead.
508 #[must_use]
509 pub fn receiver_queue_size(mut self, size: usize) -> Self {
510 self.req.receiver_queue_size = size;
511 // Clearing any previously-set policy keeps `receiver_queue_size` and
512 // `receiver_queue_policy` from disagreeing: the last setter wins.
513 self.req.receiver_queue_policy = None;
514 self.req.receiver_queue_adjust_interval = None;
515 self
516 }
517
518 /// Set a pluggable receiver-queue-size policy (issue #301, PIP-74
519 /// `autoScaledReceiverQueueSizeEnabled` parity).
520 ///
521 /// Pass [`magnetar_proto::Auto`] (wrapped in `Arc`) to let the queue grow
522 /// under starvation and shrink under memory pressure, or any custom
523 /// [`magnetar_proto::ReceiverQueuePolicy`]. The policy's `adjust` is called
524 /// from the connection's deterministic timeout tick, so it MUST be pure (no
525 /// clock, no RNG, no I/O) — see the trait docs.
526 ///
527 /// Enabling a policy turns on auto-adjust with a default 5-second tick;
528 /// override the cadence with [`Self::receiver_queue_adjust_interval`].
529 ///
530 /// # Example
531 ///
532 /// ```no_run
533 /// # use std::sync::Arc;
534 /// # use magnetar_proto::Auto;
535 /// # fn demo<'a>(b: magnetar::ConsumerBuilder<'a>) -> magnetar::ConsumerBuilder<'a> {
536 /// b.receiver_queue_policy(Arc::new(Auto::new(1_000, 128 * 1024 * 1024)))
537 /// # }
538 /// ```
539 #[must_use]
540 pub fn receiver_queue_policy(
541 mut self,
542 policy: std::sync::Arc<dyn magnetar_proto::ReceiverQueuePolicy>,
543 ) -> Self {
544 self.req.receiver_queue_policy = Some(policy);
545 // Default auto-adjust cadence; tunable via `receiver_queue_adjust_interval`.
546 if self.req.receiver_queue_adjust_interval.is_none() {
547 self.req.receiver_queue_adjust_interval = Some(Duration::from_secs(5));
548 }
549 self
550 }
551
552 /// Override the auto-adjust tick cadence used when a
553 /// [`Self::receiver_queue_policy`] is set. No effect with the default
554 /// [`magnetar_proto::Fixed`] policy. Mirrors how often Java re-evaluates its
555 /// auto-scaled receiver queue.
556 #[must_use]
557 pub fn receiver_queue_adjust_interval(mut self, interval: Duration) -> Self {
558 self.req.receiver_queue_adjust_interval = Some(interval);
559 self
560 }
561
562 /// Choose between a durable subscription (cursor persisted broker-side, the default)
563 /// and a non-durable one (used by [`Reader`] / streaming use cases).
564 #[must_use]
565 pub fn durable(mut self, durable: bool) -> Self {
566 self.req.durable = durable;
567 self
568 }
569
570 /// Set the initial position the broker dispatches from when the subscription is new.
571 #[must_use]
572 pub fn initial_position(mut self, position: pb::command_subscribe::InitialPosition) -> Self {
573 self.req.initial_position = position;
574 self
575 }
576
577 /// Read from the compacted (key-deduplicated) view of the topic. Required by
578 /// [`crate::TableView`] and by any "latest-value-per-key" workflow against compacted topics.
579 #[must_use]
580 pub fn read_compacted(mut self, on: bool) -> Self {
581 self.req.read_compacted = on;
582 self
583 }
584
585 /// Advertise the given Pulsar schema on `CommandSubscribe.schema`. The broker uses it
586 /// for schema-version negotiation; magnetar does not enforce deserialisation on its own
587 /// — pair with a [`crate::TypedConsumer`] for that.
588 #[must_use]
589 pub fn schema(mut self, schema: pb::Schema) -> Self {
590 self.req.schema = Some(schema);
591 self
592 }
593
594 /// Mirrors Java `ConsumerBuilder#priorityLevel`. The broker uses the value for Shared
595 /// / Failover dispatch ordering — higher-priority consumers receive messages first.
596 #[must_use]
597 pub fn priority_level(mut self, level: i32) -> Self {
598 self.req.priority_level = Some(level);
599 self
600 }
601
602 /// Append a (key, value) entry to the subscription properties advertised on
603 /// `CommandSubscribe.subscription_properties`. Mirrors Java
604 /// `ConsumerBuilder#subscriptionProperties` (one entry at a time).
605 #[must_use]
606 pub fn subscription_property(
607 mut self,
608 key: impl Into<String>,
609 value: impl Into<String>,
610 ) -> Self {
611 self.req
612 .subscription_properties
613 .push((key.into(), value.into()));
614 self
615 }
616
617 /// Mirrors Java `ConsumerBuilder#keySharedPolicy`. Only meaningful when
618 /// [`Self::subscription_type`] is `Key_Shared`. The broker rejects the subscribe if
619 /// the config is invalid (e.g. overlapping sticky ranges across consumers in the same
620 /// subscription).
621 #[must_use]
622 pub fn key_shared_policy(mut self, cfg: magnetar_proto::KeySharedConfig) -> Self {
623 self.req.key_shared = Some(cfg);
624 self
625 }
626
627 /// Mirrors Java `ConsumerBuilder#startMessageId`. Overrides the initial position with a
628 /// specific message id. Only honoured for fresh subscriptions — has no effect if the
629 /// subscription already has a persisted cursor.
630 #[must_use]
631 pub fn start_message_id(mut self, id: magnetar_proto::MessageId) -> Self {
632 self.req.start_message_id = Some(id);
633 self
634 }
635
636 /// Mirrors Java `ConsumerBuilder#replicateSubscriptionState`. When `true`, the broker
637 /// replicates this subscription's cursor across geo-replicated clusters.
638 #[must_use]
639 pub fn replicate_subscription_state(mut self, on: bool) -> Self {
640 self.req.replicate_subscription_state = Some(on);
641 self
642 }
643
644 /// Mirrors Java `ConsumerBuilder#enableTopicCreation`. When `false`, the broker fails
645 /// the subscribe if the topic doesn't already exist. Defaults to the broker default
646 /// (which is `true`).
647 #[must_use]
648 pub fn force_topic_creation(mut self, on: bool) -> Self {
649 self.req.force_topic_creation = Some(on);
650 self
651 }
652
653 /// Mirrors Java's `startMessageRollbackDuration` knob — rolls the subscription cursor
654 /// back by `seconds` at subscribe time so the consumer re-reads recent history. Useful
655 /// for "catch up on the last hour" patterns.
656 #[must_use]
657 pub fn start_message_rollback_duration(mut self, seconds: u64) -> Self {
658 self.req.start_message_rollback_duration_sec = Some(seconds);
659 self
660 }
661
662 /// Mirrors Java `ConsumerBuilder#property`. Appends a `(key, value)` entry to the
663 /// consumer metadata advertised on `CommandSubscribe.metadata`. Visible on the broker
664 /// dashboard alongside the consumer.
665 #[must_use]
666 pub fn property(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
667 self.req.consumer_metadata.push((key.into(), value.into()));
668 self
669 }
670
671 /// Mirrors Java `ConsumerBuilder#negativeAckRedeliveryDelay`. When set, the consumer
672 /// keeps nacked ids locally and defers the redelivery command until the delay has
673 /// elapsed. The state machine drives the timer on its existing keepalive tick.
674 #[must_use]
675 pub fn negative_ack_redelivery_delay(mut self, delay: Duration) -> Self {
676 self.req.negative_ack_redelivery_delay = Some(delay);
677 self
678 }
679
680 /// Mirrors Java `ConsumerBuilder#ackTimeout`. The consumer client-tracks every
681 /// delivered message and forces a redelivery if no positive ack arrives within
682 /// `timeout`. The state machine drives the tracker on its existing tick.
683 #[must_use]
684 pub fn ack_timeout(mut self, timeout: Duration) -> Self {
685 self.req.ack_timeout = Some(timeout);
686 self
687 }
688
689 /// Mirrors Java `ConsumerBuilder#ackTimeoutRedeliveryBackoff`. PIP-37 backoff applied to
690 /// the per-message ack-timeout deadline using the broker-reported `redelivery_count`.
691 /// Has no effect unless [`Self::ack_timeout`] is also set.
692 #[must_use]
693 pub fn ack_timeout_backoff(
694 mut self,
695 backoff: magnetar_proto::trackers::MultiplierRedeliveryBackoff,
696 ) -> Self {
697 self.req.ack_timeout_backoff = Some(backoff);
698 self
699 }
700
701 /// Mirrors Java `ConsumerBuilder#acknowledgmentGroupTime`. When set, calls to
702 /// [`magnetar_runtime_tokio::Consumer::ack_grouped`] (and
703 /// `ack_grouped_cumulative`) stage acks in an in-memory tracker and the state
704 /// machine flushes them as one coalesced `CommandAck` every `window`. Trades
705 /// broker-confirmation guarantees for lower ack bandwidth on high-throughput
706 /// consumers. Has no effect on the synchronous [`Self::ack_timeout`] or the
707 /// awaited `Consumer::ack` paths.
708 #[must_use]
709 pub fn ack_group_time(mut self, window: Duration) -> Self {
710 self.req.ack_group_time = Some(window);
711 self
712 }
713
714 /// Mirrors Java `ConsumerBuilder#cryptoFailureAction`. Controls what the consumer does
715 /// when payload decryption fails (PIP-4): `Fail` (default) propagates the error,
716 /// `Discard` silently drops the message, `Consume` returns the encrypted ciphertext
717 /// as-is. All three arms are honored by the
718 /// [`magnetar_runtime_tokio::Consumer`] receive path.
719 #[must_use]
720 pub fn crypto_failure_action(
721 mut self,
722 action: magnetar_proto::conn::CryptoFailureAction,
723 ) -> Self {
724 self.req.crypto_failure_action = action;
725 self
726 }
727
728 /// Mirrors Java `ConsumerBuilder#maxPendingChunkedMessage` (default `10`).
729 /// Bounds the number of distinct incomplete chunked messages the consumer
730 /// buffers; on breach the oldest incomplete message is evicted. `0` disables
731 /// the cap. Guards against a hostile/buggy broker streaming distinct-UUID
732 /// first chunks that never complete (unbounded `chunk_reassembly` growth →
733 /// OOM).
734 #[must_use]
735 pub fn max_pending_chunked_message(mut self, max: usize) -> Self {
736 self.req.max_pending_chunked_message = max;
737 self
738 }
739
740 /// Mirrors Java `ConsumerBuilder#autoAckOldestChunkedMessageOnQueueFull`
741 /// (default `false`). When `true`, an evicted/expired partial chunked
742 /// message's first-chunk id is acked before drop (the broker treats it as
743 /// consumed); when `false`, it is dropped without acking so the broker
744 /// redelivers the whole message.
745 #[must_use]
746 pub fn auto_ack_oldest_chunked_message_on_queue_full(mut self, auto_ack: bool) -> Self {
747 self.req.auto_ack_oldest_chunked_message_on_queue_full = auto_ack;
748 self
749 }
750
751 /// Mirrors Java `ConsumerBuilder#expireTimeOfIncompleteChunkedMessage`
752 /// (default `60s`). An incomplete chunked message older than this is swept
753 /// on the connection's existing timeout tick and dropped (or acked, per
754 /// [`Self::auto_ack_oldest_chunked_message_on_queue_full`]).
755 #[must_use]
756 pub fn expire_time_of_incomplete_chunked_message(mut self, expire: Duration) -> Self {
757 self.req.expire_time_of_incomplete_chunked_message = Some(expire);
758 self
759 }
760
761 /// Mirrors Java `ConsumerBuilder#deadLetterPolicy`. After `max_redeliver_count`
762 /// redeliveries, the consumer flags the message as dead-letter — drain via
763 /// [`magnetar_runtime_tokio::Consumer::drain_dead_letter`] and republish to
764 /// `dead_letter_topic` (or to the Java-default `<topic>-<subscription>-DLQ` when
765 /// `dead_letter_topic` is `None`).
766 ///
767 /// `0` disables DLQ routing (the default).
768 #[must_use]
769 pub fn dead_letter_policy(
770 mut self,
771 max_redeliver_count: u32,
772 dead_letter_topic: Option<String>,
773 ) -> Self {
774 self.req.max_redeliver_count = max_redeliver_count;
775 self.req.dead_letter_topic = dead_letter_topic;
776 self
777 }
778
779 /// Subscribe via the engine-generic [`crate::SubscribeApi`] trait.
780 /// Returns the engine's concrete `Consumer` type.
781 ///
782 /// **PIP-4 decryption guardrail (BREAKING since the decryptor-storage lift).**
783 /// If [`Self::encryption`] was called on the per-engine specialisation,
784 /// `.subscribe()` returns [`PulsarError::Other`] instead of silently opening
785 /// a plaintext consumer. The engine-generic dispatch cannot thread an
786 /// engine-typed decryptor through `subscribe`, so the previous "silently
787 /// drop the decryptor" behaviour was a footgun. Use
788 /// [`Self::subscribe_with_decryption`] on the tokio / moonpool
789 /// specialisation instead.
790 ///
791 /// # Errors
792 /// - [`PulsarError::Other`] if a decryptor was configured via [`Self::encryption`] — call
793 /// `subscribe_with_decryption()` instead.
794 /// - [`PulsarError::Other`] (stringified) on broker rejection or wire failure.
795 pub async fn subscribe(
796 self,
797 ) -> Result<<E::ClientState as crate::SubscribeApi>::Consumer, PulsarError>
798 where
799 E::ClientState: crate::SubscribeApi,
800 {
801 let mut deadline =
802 crate::SubscribeApi::new_subscribe_operation_deadline(&self.client.inner);
803 self.subscribe_with_deadline(&mut deadline).await
804 }
805
806 pub(crate) async fn subscribe_with_deadline(
807 self,
808 deadline: &mut crate::OperationDeadline,
809 ) -> Result<<E::ClientState as crate::SubscribeApi>::Consumer, PulsarError>
810 where
811 E::ClientState: crate::SubscribeApi,
812 {
813 if self.decryptor.is_some() {
814 return Err(PulsarError::Other(
815 "ConsumerBuilder::subscribe() refuses a configured decryptor — \
816 use subscribe_with_decryption() on the engine-specific builder \
817 (PIP-4 decryptors are engine-typed and cannot dispatch \
818 through the engine-generic SubscribeApi)"
819 .to_owned(),
820 ));
821 }
822 crate::SubscribeApi::subscribe_with_deadline(&self.client.inner, self.req, deadline)
823 .await
824 .map_err(|err| PulsarError::Other(format!("subscribe: {err}")))
825 }
826
827 /// Register a push-delivery callback (Java
828 /// `ConsumerBuilder#messageListener`). Once set, subscribe via
829 /// [`Self::subscribe_with_listener`] to start a background poller that
830 /// drives `receive()` and hands every message to `listener`, sequentially
831 /// and in order.
832 ///
833 /// The plain [`Self::subscribe`] does **not** consult the listener — it
834 /// returns a pull-mode consumer. Pull and push are mutually exclusive
835 /// (Java forbids `receive()` on a listener-backed consumer): use
836 /// `subscribe_with_listener` for push and never call `receive()`, or use
837 /// `subscribe` for pull and call `receive()` yourself.
838 ///
839 /// The callback **must ack explicitly** — the poller never auto-acks (Java
840 /// parity). Hold a clone of your consumer in the closure to ack.
841 #[must_use]
842 pub fn message_listener(mut self, listener: crate::MessageListener) -> Self {
843 self.listener = Some(listener);
844 self
845 }
846
847 /// Register a Failover active/standby callback (Java
848 /// `ConsumerBuilder#consumerEventListener`, issue #348). Subscribe via
849 /// [`Self::subscribe_with_event_listener`] to spawn the poller that
850 /// drives it; the plain [`Self::subscribe`] and
851 /// [`Self::subscribe_with_listener`] ignore it.
852 ///
853 /// The callback fires [`crate::ConsumerEvent::BecameActive`] /
854 /// [`crate::ConsumerEvent::BecameInactive`] once per broker-reported
855 /// `CommandActiveConsumerChange`, sequentially, from the detached poller
856 /// task — never under any lock.
857 #[must_use]
858 pub fn consumer_event_listener(mut self, listener: crate::ConsumerEventListener) -> Self {
859 self.event_listener = Some(listener);
860 self
861 }
862
863 /// Subscribe and start a push-delivery poller over the resulting consumer,
864 /// returning the owning [`crate::MessageListenerHandle`]. Mirrors Java's
865 /// `ConsumerBuilder#messageListener(...)` + `subscribe()` flow.
866 ///
867 /// The poller delivers messages sequentially and in order, does **not**
868 /// auto-ack (the callback acks explicitly), and stops cleanly when the
869 /// consumer is closed or the returned handle is dropped. Because the
870 /// consumer is moved into the poller, there is no handle left to call
871 /// `receive()` on — the listener owns delivery (matching Java's
872 /// "no `receive()` with a `messageListener`" rule).
873 ///
874 /// # Errors
875 /// - [`PulsarError::Config`] if no listener was set via [`Self::message_listener`].
876 /// - [`PulsarError::Other`] (stringified) on broker rejection or wire failure.
877 pub async fn subscribe_with_listener(self) -> Result<crate::MessageListenerHandle, PulsarError>
878 where
879 E::ClientState: crate::SubscribeApi,
880 <E::ClientState as crate::SubscribeApi>::Consumer: Clone,
881 {
882 let Some(listener) = self.listener.clone() else {
883 return Err(PulsarError::Config(
884 "subscribe_with_listener() requires a listener — \
885 call message_listener(...) first (or use subscribe() for pull mode)"
886 .to_owned(),
887 ));
888 };
889 let consumer = self.subscribe().await?;
890 Ok(crate::consumer_listener::spawn_message_listener(
891 consumer, listener,
892 ))
893 }
894
895 /// Subscribe and start a Failover active/standby poller over the
896 /// resulting consumer, returning the owning
897 /// [`crate::ConsumerEventListenerHandle`] (issue #348). Mirrors Java's
898 /// `ConsumerBuilder#consumerEventListener(...)` + `subscribe()` flow.
899 ///
900 /// The consumer is moved into the poller, so there is no handle left to
901 /// call on this terminal — hold a separate clone (subscribe pull-mode,
902 /// then use [`crate::spawn_consumer_event_listener`] directly on a
903 /// clone) if you also need to receive messages from the same consumer.
904 ///
905 /// # Errors
906 /// - [`PulsarError::Config`] if no listener was set via [`Self::consumer_event_listener`].
907 /// - [`PulsarError::Other`] (stringified) on broker rejection or wire failure.
908 pub async fn subscribe_with_event_listener(
909 self,
910 ) -> Result<crate::ConsumerEventListenerHandle, PulsarError>
911 where
912 E::ClientState: crate::SubscribeApi,
913 <E::ClientState as crate::SubscribeApi>::Consumer: Clone,
914 {
915 let Some(listener) = self.event_listener.clone() else {
916 return Err(PulsarError::Config(
917 "subscribe_with_event_listener() requires a listener — \
918 call consumer_event_listener(...) first (or use subscribe() for pull mode)"
919 .to_owned(),
920 ));
921 };
922 let consumer = self.subscribe().await?;
923 Ok(crate::consumer_listener::spawn_consumer_event_listener(
924 consumer, listener,
925 ))
926 }
927}
928
929/// Tokio-engine-specific `ConsumerBuilder` methods that depend on the
930/// tokio `MessageDecryptor` extension. The moonpool equivalent lives in
931/// the `#[cfg(feature = "moonpool")]` block below (ADR-0044).
932impl ConsumerBuilder<'_, crate::TokioEngine> {
933 /// Configure PIP-4 end-to-end decryption. The decryptor is consulted on every received
934 /// message whose `MessageMetadata.encryption_keys` is non-empty.
935 #[must_use]
936 pub fn encryption(
937 mut self,
938 decryptor: std::sync::Arc<dyn magnetar_runtime_tokio::MessageDecryptor>,
939 ) -> Self {
940 // `<TokioEngine as MessageDecryptorApi>::Decryptor` resolves
941 // exactly to `Arc<dyn MessageDecryptor>` so we store the arg
942 // directly into the engine-typed slot.
943 self.decryptor = Some(decryptor);
944 self
945 }
946
947 /// Subscribe with the configured decryptor (PIP-4). Tokio-engine-only.
948 /// Use [`Self::subscribe`] for the engine-generic path that ignores
949 /// the decryptor.
950 ///
951 /// # Errors
952 /// - [`PulsarError::Client`] on broker rejection or wire failure.
953 pub async fn subscribe_with_decryption(self) -> Result<magnetar_runtime_tokio::Consumer> {
954 Ok(self
955 .client
956 .inner
957 .subscribe_with(self.req, self.decryptor)
958 .await?)
959 }
960}
961
962/// Moonpool-engine-specific `ConsumerBuilder` methods that depend on the
963/// moonpool `MessageDecryptor` extension (PIP-4). 1:1 mirror of the tokio
964/// specialisation above.
965#[cfg(feature = "moonpool")]
966impl<P: moonpool_core::Providers + Send + Sync + 'static>
967 ConsumerBuilder<'_, crate::MoonpoolEngine<P>>
968{
969 /// Configure PIP-4 end-to-end decryption. The decryptor is consulted on every received
970 /// message whose `MessageMetadata.encryption_keys` is non-empty.
971 #[must_use]
972 pub fn encryption(
973 mut self,
974 decryptor: std::sync::Arc<dyn magnetar_runtime_moonpool::MessageDecryptor>,
975 ) -> Self {
976 // `<MoonpoolEngine<P> as MessageDecryptorApi>::Decryptor` resolves
977 // exactly to `Arc<dyn MessageDecryptor>` so we store the arg
978 // directly into the engine-typed slot.
979 self.decryptor = Some(decryptor);
980 self
981 }
982
983 /// Subscribe with the configured decryptor (PIP-4). Moonpool-engine-only.
984 /// Use [`Self::subscribe`] for the engine-generic path that ignores
985 /// the decryptor.
986 ///
987 /// # Errors
988 /// - [`PulsarError::Other`] (stringified) on broker rejection or wire failure.
989 pub async fn subscribe_with_decryption(self) -> Result<magnetar_runtime_moonpool::Consumer<P>> {
990 self.client
991 .inner
992 .subscribe_with(self.req, self.decryptor)
993 .await
994 .map_err(|err| PulsarError::Other(format!("subscribe: {err}")))
995 }
996}
997
998/// Builder for a [`Reader`].
999///
1000/// Mirrors `org.apache.pulsar.client.api.ReaderBuilder`. Internally a `Reader` is just a
1001/// non-durable `Exclusive` consumer with an auto-generated subscription name — there's no
1002/// dedicated wire command, so the protocol layer doesn't need any extra plumbing.
1003///
1004/// Phantom-generic over `E: Engine` (defaults to [`crate::TokioEngine`]).
1005/// Wraps a [`ConsumerBuilder<E>`]; the impl methods stay tokio-bound
1006/// until the `SubscribeApi` dispatch path lands in the Builder lift
1007/// sub-PR.
1008pub struct ReaderBuilder<'a, E: crate::Engine = crate::TokioEngine> {
1009 inner: ConsumerBuilder<'a, E>,
1010}
1011
1012impl<E: crate::Engine> std::fmt::Debug for ReaderBuilder<'_, E> {
1013 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1014 f.debug_struct("ReaderBuilder")
1015 .field("inner", &self.inner)
1016 .finish()
1017 }
1018}
1019
1020impl<'a, E: crate::Engine> ReaderBuilder<'a, E> {
1021 pub(crate) fn new(client: &'a PulsarClient<E>, topic: String) -> Self {
1022 let subscription = format!("reader-{}", E::random_subscription_suffix());
1023 let inner = ConsumerBuilder::new(client, topic)
1024 .subscription(subscription)
1025 .subscription_type(pb::command_subscribe::SubType::Exclusive)
1026 .durable(false);
1027 Self { inner }
1028 }
1029
1030 /// Override the auto-generated subscription name. Rarely needed — Reader subscriptions
1031 /// are not visible on the broker dashboard anyway.
1032 #[must_use]
1033 pub fn subscription_name(mut self, name: impl Into<String>) -> Self {
1034 self.inner = self.inner.subscription(name);
1035 self
1036 }
1037
1038 /// Set the receiver queue size.
1039 #[must_use]
1040 pub fn receiver_queue_size(mut self, size: usize) -> Self {
1041 self.inner = self.inner.receiver_queue_size(size);
1042 self
1043 }
1044
1045 /// Set the consumer name advertised to the broker.
1046 #[must_use]
1047 pub fn name(mut self, name: impl Into<String>) -> Self {
1048 self.inner = self.inner.name(name);
1049 self
1050 }
1051
1052 /// Choose where the reader starts when its non-durable subscription is fresh.
1053 /// Defaults to [`pb::command_subscribe::InitialPosition::Latest`].
1054 #[must_use]
1055 pub fn start_position(mut self, position: pb::command_subscribe::InitialPosition) -> Self {
1056 self.inner = self.inner.initial_position(position);
1057 self
1058 }
1059
1060 /// Read from the compacted (key-deduplicated) view of the topic. Mirrors Java
1061 /// `ReaderBuilder#readCompacted`. Required for compacted-topic readers.
1062 #[must_use]
1063 pub fn read_compacted(mut self, on: bool) -> Self {
1064 self.inner = self.inner.read_compacted(on);
1065 self
1066 }
1067
1068 /// Override the initial message id the reader starts from. Mirrors Java
1069 /// `ReaderBuilder#startMessageId`. Pass [`magnetar_proto::MessageId::EARLIEST`] /
1070 /// [`magnetar_proto::MessageId::LATEST`] for the sentinel positions.
1071 #[must_use]
1072 pub fn start_message_id(mut self, id: magnetar_proto::MessageId) -> Self {
1073 self.inner = self.inner.start_message_id(id);
1074 self
1075 }
1076
1077 /// Mirrors `ConsumerBuilder::property`. The reader's underlying consumer carries the
1078 /// (key, value) pair on its `CommandSubscribe.metadata`.
1079 #[must_use]
1080 pub fn property(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
1081 self.inner = self.inner.property(key, value);
1082 self
1083 }
1084
1085 /// Roll the reader cursor back by `seconds` at create time. Mirrors Java
1086 /// `ReaderBuilder#startMessageIdInclusive` rollback knob.
1087 #[must_use]
1088 pub fn start_message_rollback_duration(mut self, seconds: u64) -> Self {
1089 self.inner = self.inner.start_message_rollback_duration(seconds);
1090 self
1091 }
1092
1093 /// Create the reader via the engine-generic
1094 /// [`crate::SubscribeApi`] dispatch path. Returns
1095 /// `Reader<<E::ClientState as SubscribeApi>::Consumer>` —
1096 /// resolves to `Reader<magnetar_runtime_tokio::Consumer>` (the
1097 /// default `Reader<>` alias) under the default
1098 /// `E = TokioEngine`.
1099 ///
1100 /// # Errors
1101 /// - [`PulsarError::Other`] on broker rejection or wire failure.
1102 pub async fn create(
1103 self,
1104 ) -> Result<Reader<<E::ClientState as crate::SubscribeApi>::Consumer>, PulsarError>
1105 where
1106 E::ClientState: crate::SubscribeApi,
1107 {
1108 let consumer = self.inner.subscribe().await?;
1109 Ok(Reader {
1110 consumer,
1111 last_received: parking_lot::Mutex::new(None),
1112 })
1113 }
1114}
1115
1116/// Tokio-engine-specific `ReaderBuilder` methods that depend on the
1117/// tokio `MessageDecryptor` extension (PIP-4).
1118impl ReaderBuilder<'_, crate::TokioEngine> {
1119 /// Mirrors Java `ReaderBuilder#cryptoKeyReader` — supplies a PIP-4 decryptor for the
1120 /// reader's underlying subscription.
1121 #[must_use]
1122 pub fn encryption(
1123 mut self,
1124 decryptor: std::sync::Arc<dyn magnetar_runtime_tokio::MessageDecryptor>,
1125 ) -> Self {
1126 self.inner = self.inner.encryption(decryptor);
1127 self
1128 }
1129}