use std::collections::VecDeque;
use std::sync::Arc;
use std::time::Duration;
use bytes::{Bytes, BytesMut};
use magnetar_proto::{
ConnectionEvent, ConsumerHandle, DriverRetry, OpOutcome, PendingOpKey, ProducerHandle,
RequestId,
};
use moonpool_core::{Providers, TaskProvider, TimeProvider};
use parking_lot::Mutex;
use tokio::sync::Notify;
use crate::dns::DnsResolver;
use crate::transport::Transport;
use crate::{ConnectionShared, EngineError, ObservedReplicatedSubscriptionMarker, TopicListChange};
const READ_BUFFER_CAPACITY: usize = 64 * 1024;
const DRIVER_WRITE_BUDGET_BYTES: usize = 256 * 1024;
#[derive(Debug, Clone, Copy)]
enum RetryRequest {
Producer(ProducerHandle, RequestId),
Consumer(ConsumerHandle, RequestId),
}
fn handle_pending_events(
shared: &Arc<ConnectionShared>,
retries: &mut Vec<RetryRequest>,
) -> Result<(), EngineError> {
while let Some(retry) = shared.inner.lock().poll_driver_retry() {
match retry {
DriverRetry::Producer {
handle,
failed_request_id,
code,
message,
} => {
tracing::warn!(
?handle,
code,
message = crate::log_fields::truncate_broker_str(&message),
"producer-open transient error; scheduling lookup + retry"
);
retries.push(RetryRequest::Producer(handle, failed_request_id));
}
DriverRetry::Consumer {
handle,
failed_request_id,
code,
message,
} => {
tracing::warn!(
?handle,
code,
message = crate::log_fields::truncate_broker_str(&message),
"consumer-subscribe transient error; scheduling lookup + retry"
);
retries.push(RetryRequest::Consumer(handle, failed_request_id));
}
_ => {}
}
}
loop {
let event = shared.inner.lock().poll_event_if(|ev| {
#[cfg(feature = "scalable-topics")]
if matches!(
ev,
ConnectionEvent::ScalableTopicLookupResolved { .. }
| ConnectionEvent::SegmentDagUpdated { .. }
| ConnectionEvent::DagChangedDuringConsume { .. }
| ConnectionEvent::DagWatchClosed { .. }
) {
return true;
}
matches!(
ev,
ConnectionEvent::AuthChallenge { .. }
| ConnectionEvent::TopicListChanged { .. }
| ConnectionEvent::TopicMigrated { .. }
| ConnectionEvent::RedirectUrlRejected { .. }
| ConnectionEvent::ReplicatedSubscriptionMarkerObserved { .. }
| ConnectionEvent::ChecksumMismatch { .. }
| ConnectionEvent::ActiveConsumerChanged { .. }
| ConnectionEvent::LookupResponse {
result: magnetar_proto::LookupOutcome::Redirected { .. },
..
}
)
});
let Some(event) = event else {
return Ok(());
};
match event {
ConnectionEvent::AuthChallenge { method, challenge } => {
let Some(provider) = shared.auth_provider.clone() else {
tracing::warn!(
auth_method = method
.as_deref()
.map_or("none", crate::log_fields::truncate_broker_str),
"broker requested in-band auth refresh but no AuthProvider configured; \
the connection will be reset"
);
return Err(EngineError::Config(
"broker requested AUTH_CHALLENGE but client has no auth provider"
.to_owned(),
));
};
let bytes = challenge.unwrap_or_default();
tracing::debug!(
auth_method = %provider.method(),
"auth challenge received; refreshing credentials"
);
let refreshed = match provider.respond_to_challenge(&bytes) {
Ok(refreshed) => refreshed,
Err(err) => {
tracing::warn!(
auth_method = %provider.method(),
error_class = "auth_refresh_failed",
"in-band auth refresh failed; the connection will be reset"
);
return Err(EngineError::Config(format!("auth refresh failed: {err}")));
}
};
let method = provider.method().to_owned();
shared
.inner
.lock()
.submit_auth_response(refreshed, Some(method));
shared.driver_waker.notify_one();
}
ConnectionEvent::TopicListChanged { added, removed } => {
shared
.topic_list_changes
.lock()
.push_back(TopicListChange { added, removed });
shared.topic_list_notify.notify_waiters();
}
ConnectionEvent::ReplicatedSubscriptionMarkerObserved { handle, marker } => {
shared
.replicated_subscription_markers
.lock()
.push_back(ObservedReplicatedSubscriptionMarker { handle, marker });
shared
.replicated_subscription_marker_notify
.notify_waiters();
}
ConnectionEvent::RedirectUrlRejected {
source,
broker_service_url,
broker_service_url_tls,
} => {
tracing::warn!(
source,
rejected_url = broker_service_url
.as_deref()
.map(crate::log_fields::truncate_broker_str),
rejected_url_tls = broker_service_url_tls
.as_deref()
.map(crate::log_fields::truncate_broker_str),
"broker-advertised redirect URL rejected by redirect_url_allow_list; \
ignoring the hint (auth provider NOT replayed against the unverified host)",
);
}
ConnectionEvent::TopicMigrated {
producer,
consumer,
broker_service_url,
broker_service_url_tls,
} => {
tracing::info!(
?producer,
?consumer,
new_url = broker_service_url
.as_deref()
.map(crate::log_fields::truncate_broker_str),
new_url_tls = broker_service_url_tls
.as_deref()
.map(crate::log_fields::truncate_broker_str),
"broker requested PIP-188 topic migration; supervised reconnect will fire"
);
return Err(EngineError::Config(
"PIP-188: broker requested topic migration; resetting connection".to_owned(),
));
}
#[cfg(feature = "scalable-topics")]
ConnectionEvent::ScalableTopicLookupResolved {
request_id,
controller_broker_url,
segments,
lookup_token,
} => {
shared
.scalable_events
.lock()
.push_back(crate::ScalableEvent::LookupResolved {
request_id,
controller_broker_url,
segments,
lookup_token,
});
shared.scalable_notify.notify_waiters();
}
#[cfg(feature = "scalable-topics")]
ConnectionEvent::SegmentDagUpdated {
watch_session_id,
delta,
} => {
shared
.scalable_events
.lock()
.push_back(crate::ScalableEvent::DagUpdated {
watch_session_id,
delta,
});
shared.scalable_notify.notify_waiters();
}
#[cfg(feature = "scalable-topics")]
ConnectionEvent::DagChangedDuringConsume {
watch_session_id,
reason,
} => {
shared.scalable_events.lock().push_back(
crate::ScalableEvent::DagChangedDuringConsume {
watch_session_id,
reason,
},
);
shared.scalable_notify.notify_waiters();
}
#[cfg(feature = "scalable-topics")]
ConnectionEvent::DagWatchClosed {
watch_session_id,
reason,
} => {
shared
.scalable_events
.lock()
.push_back(crate::ScalableEvent::DagWatchClosed {
watch_session_id,
reason,
});
shared.scalable_notify.notify_waiters();
}
ConnectionEvent::ChecksumMismatch { .. } => {}
ConnectionEvent::ActiveConsumerChanged { .. } => {}
ConnectionEvent::LookupResponse { .. } => {}
_ => {}
}
}
}
fn spawn_retry_leg<P>(
shared: &Arc<ConnectionShared>,
time: &P::Time,
task: &P::Task,
req: RetryRequest,
) where
P: Providers,
{
let shared = shared.clone();
let time = time.clone();
let _detached = task.spawn_task("magnetar-moonpool-transient-retry", async move {
let delay = {
let conn = shared.inner.lock();
let failures = match req {
RetryRequest::Producer(handle, _) => conn.producer_transient_open_attempts(handle),
RetryRequest::Consumer(handle, _) => {
conn.consumer_transient_subscribe_attempts(handle)
}
};
conn.operation_retry_config().delay_after_failure(failures)
};
if !wait_retry_delay(&shared, &time, req, delay).await {
return;
}
let topic = retry_request_topic(&shared.inner.lock(), req);
let Some(topic) = topic else { return };
if !lookup_then(&shared, &time, &topic, req).await {
return;
}
let request_id = {
let mut conn = shared.inner.lock();
match req {
RetryRequest::Producer(handle, failed_request_id) => {
conn.retry_producer_open_if_current(handle, failed_request_id)
}
RetryRequest::Consumer(handle, failed_request_id) => {
conn.retry_consumer_subscribe_if_current(handle, failed_request_id)
}
}
};
if request_id.is_some() {
shared.driver_waker.notify_one();
}
});
}
fn retry_request_topic(conn: &magnetar_proto::Connection, req: RetryRequest) -> Option<String> {
if !conn.is_connected() {
return None;
}
match req {
RetryRequest::Producer(handle, failed_request_id)
if conn.producer_open_retry_is_current(handle, failed_request_id) =>
{
conn.producer_topic(handle).map(str::to_owned)
}
RetryRequest::Consumer(handle, failed_request_id)
if conn.consumer_subscribe_retry_is_current(handle, failed_request_id) =>
{
conn.consumer_topic(handle).map(str::to_owned)
}
RetryRequest::Producer(_, _) | RetryRequest::Consumer(_, _) => None,
}
}
pub(crate) fn notify_retry_generation_replaced(shared: &Arc<ConnectionShared>) {
shared.operation_cancel_notify.notify_waiters();
shared.driver_waker.notify_one();
}
struct PendingDriverWrite {
segments: VecDeque<Bytes>,
front_offset: usize,
}
impl PendingDriverWrite {
fn new() -> Self {
Self {
segments: VecDeque::new(),
front_offset: 0,
}
}
#[cfg(test)]
fn from_transmit(transmit: magnetar_proto::TransmitOwned) -> Self {
let mut pending = Self::new();
pending.push_transmit(transmit);
pending
}
fn push_transmit(&mut self, transmit: magnetar_proto::TransmitOwned) {
debug_assert!(
self.is_empty(),
"new transmit must only be pulled after the pending write queue drains"
);
match transmit {
magnetar_proto::TransmitOwned::Contiguous(buf) => {
if !buf.is_empty() {
self.segments.push_back(buf);
}
}
magnetar_proto::TransmitOwned::Vectored(segs) => {
self.segments
.extend(segs.into_iter().filter(|s| !s.is_empty()));
}
}
self.front_offset = 0;
}
fn is_empty(&self) -> bool {
self.segments.is_empty()
}
fn pop_budgeted(&mut self, budget: usize) -> Vec<Bytes> {
let mut chunks = Vec::new();
let mut remaining = budget;
while remaining > 0 {
let Some(front) = self.segments.front() else {
break;
};
let available = front.len().saturating_sub(self.front_offset);
if available == 0 {
let _ = self.segments.pop_front();
self.front_offset = 0;
continue;
}
let n = available.min(remaining);
chunks.push(front.slice(self.front_offset..self.front_offset + n));
self.front_offset += n;
remaining -= n;
if self.front_offset == front.len() {
let _ = self.segments.pop_front();
self.front_offset = 0;
}
}
chunks
}
async fn write_budgeted<P>(
&mut self,
transport: &mut Transport<P>,
budget: usize,
) -> std::io::Result<usize>
where
P: Providers,
{
let chunks = self.pop_budgeted(budget);
let mut written = 0usize;
for chunk in chunks {
written += chunk.len();
transport.write_all(&chunk).await?;
}
Ok(written)
}
}
async fn lookup_then<T: TimeProvider>(
shared: &Arc<ConnectionShared>,
time: &T,
topic: &str,
req: RetryRequest,
) -> bool {
let retry_config = shared.inner.lock().operation_retry_config().clone();
let mut failures = 0_u32;
loop {
let request_id = {
let mut conn = shared.inner.lock();
if retry_request_topic(&conn, req).is_none() {
return false;
}
conn.lookup(topic, false)
};
shared.driver_waker.notify_one();
let outcome_fut = LookupRetryFut {
shared: shared.clone(),
key: PendingOpKey::Request(request_id),
};
tokio::pin!(outcome_fut);
let outcome = loop {
let cancelled = shared.operation_cancel_notify.notified();
tokio::pin!(cancelled);
cancelled.as_mut().enable();
if retry_request_topic(&shared.inner.lock(), req).is_none() {
return false;
}
moonpool_core::select! {
biased;
() = cancelled.as_mut() => {}
outcome = outcome_fut.as_mut() => break outcome,
}
};
if retry_request_topic(&shared.inner.lock(), req).is_none() {
return false;
}
match outcome {
OpOutcome::LookupResponse {
outcome: magnetar_proto::LookupOutcome::Connect { .. },
..
} => {
tracing::debug!(%topic, "retry-path lookup resolved");
return true;
}
OpOutcome::LookupResponse {
outcome: magnetar_proto::LookupOutcome::Failed { code, message },
..
}
| OpOutcome::Error { code, message, .. } => {
failures = failures.saturating_add(1);
if magnetar_proto::is_retryable_broker_error(
magnetar_proto::OperationKind::Lookup,
code,
) && retry_config.should_retry_after_failure(failures)
{
let delay = retry_config.delay_after_failure(failures);
tracing::debug!(
%topic,
code,
failures,
?delay,
"retry-path lookup rejected transiently; re-issuing"
);
if !wait_retry_delay(shared, time, req, delay).await {
return false;
}
continue;
}
terminalize_retry_request(shared, req, code, &message);
return false;
}
OpOutcome::LookupResponse {
outcome: magnetar_proto::LookupOutcome::Redirected { .. },
..
} => {
terminalize_retry_request(
shared,
req,
magnetar_proto::pb::ServerError::MetadataError as i32,
"retry-path lookup redirected to another broker",
);
return false;
}
other => {
tracing::warn!(?other, %topic, "retry-path lookup landed unexpected outcome");
return false;
}
}
}
}
async fn wait_retry_delay<T: TimeProvider>(
shared: &Arc<ConnectionShared>,
time: &T,
req: RetryRequest,
delay: std::time::Duration,
) -> bool {
let sleep = time.sleep(delay);
tokio::pin!(sleep);
loop {
let cancelled = shared.operation_cancel_notify.notified();
tokio::pin!(cancelled);
cancelled.as_mut().enable();
if retry_request_topic(&shared.inner.lock(), req).is_none() {
return false;
}
moonpool_core::select! {
biased;
() = cancelled.as_mut() => {}
_ = sleep.as_mut() => {
return retry_request_topic(&shared.inner.lock(), req).is_some();
}
}
}
}
fn terminalize_retry_request(
shared: &Arc<ConnectionShared>,
req: RetryRequest,
code: i32,
message: &str,
) {
let terminalized = {
let mut conn = shared.inner.lock();
if retry_request_topic(&conn, req).is_none() {
false
} else {
match req {
RetryRequest::Producer(handle, _) => {
conn.fail_producer_open_with_broker_error(handle, code, message);
}
RetryRequest::Consumer(handle, _) => {
conn.fail_consumer_subscribe_with_broker_error(handle, code, message);
}
}
true
}
};
if terminalized {
shared.driver_waker.notify_one();
}
}
struct LookupRetryFut {
shared: Arc<ConnectionShared>,
key: PendingOpKey,
}
impl core::future::Future for LookupRetryFut {
type Output = OpOutcome;
fn poll(
self: core::pin::Pin<&mut Self>,
cx: &mut core::task::Context<'_>,
) -> core::task::Poll<Self::Output> {
let mut conn = self.shared.inner.lock();
if let Some(outcome) = conn.take_outcome(self.key) {
return core::task::Poll::Ready(outcome);
}
conn.register_waker(self.key, cx.waker().clone());
core::task::Poll::Pending
}
}
impl Drop for LookupRetryFut {
fn drop(&mut self) {
if let PendingOpKey::Request(request_id) = self.key {
self.shared.inner.lock().cancel_request(request_id);
} else {
self.shared.inner.lock().unregister_waker(self.key);
}
}
}
struct DriverResult {
result: Mutex<Option<Result<(), EngineError>>>,
done: Notify,
}
pub struct DriverHandle {
_join: Box<dyn core::any::Any + Send>,
result: Arc<DriverResult>,
shared: Arc<ConnectionShared>,
}
impl std::fmt::Debug for DriverHandle {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("DriverHandle").finish_non_exhaustive()
}
}
impl DriverHandle {
pub async fn join(self) -> Result<(), EngineError> {
loop {
if let Some(res) = self.result.result.lock().take() {
return res;
}
self.result.done.notified().await;
}
}
pub fn abort(&self) {
{
let mut conn = self.shared.inner.lock();
conn.close();
}
self.shared.driver_waker.notify_one();
}
}
#[derive(Clone)]
pub(crate) struct ReconnectContext {
pub(crate) host_port: String,
pub(crate) service_url_provider: Option<Arc<dyn magnetar_proto::ServiceUrlProvider>>,
pub(crate) dns_resolver: Option<Arc<dyn DnsResolver>>,
}
impl std::fmt::Debug for ReconnectContext {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ReconnectContext")
.field("host_port", &self.host_port)
.field(
"has_service_url_provider",
&self.service_url_provider.is_some(),
)
.field("has_dns_resolver", &self.dns_resolver.is_some())
.finish()
}
}
pub(crate) fn spawn<P>(
shared: Arc<ConnectionShared>,
transport: Transport<P>,
time: P::Time,
task: &P::Task,
) -> DriverHandle
where
P: Providers,
{
let result = Arc::new(DriverResult {
result: Mutex::new(None),
done: Notify::new(),
});
let result_for_task = result.clone();
let shared_for_handle = shared.clone();
let task_for_loop = task.clone();
let join = task.spawn_task("magnetar-moonpool-driver", async move {
let driver_shared = shared.clone();
let outcome = driver_loop_inner::<P>(shared, transport, time, task_for_loop).await;
{
let reason = match &outcome {
Ok(()) => "connection closed".to_owned(),
Err(err) => err.to_string(),
};
driver_shared.inner.lock().fail_all_pending(&reason);
}
driver_shared.mark_no_driver();
driver_shared.event_waker.notify_waiters();
driver_shared.driver_waker.notify_waiters();
*result_for_task.result.lock() = Some(outcome);
result_for_task.done.notify_one();
});
DriverHandle {
_join: Box::new(join),
result,
shared: shared_for_handle,
}
}
pub(crate) fn spawn_supervised<P>(
shared: Arc<ConnectionShared>,
transport: Transport<P>,
reconnect_ctx: ReconnectContext,
providers: P,
) -> DriverHandle
where
P: Providers,
{
let result = Arc::new(DriverResult {
result: Mutex::new(None),
done: Notify::new(),
});
let result_for_task = result.clone();
let shared_for_handle = shared.clone();
let time = providers.time().clone();
let task = providers.task().clone();
let join = task.spawn_task("magnetar-moonpool-driver-supervised", async move {
let driver_shared = shared.clone();
let outcome =
supervised_driver_loop::<P>(shared, transport, reconnect_ctx, providers, time).await;
{
let reason = match &outcome {
Ok(()) => "connection closed".to_owned(),
Err(err) => err.to_string(),
};
driver_shared.inner.lock().fail_all_pending(&reason);
}
driver_shared.mark_no_driver();
driver_shared.event_waker.notify_waiters();
driver_shared.driver_waker.notify_waiters();
*result_for_task.result.lock() = Some(outcome);
result_for_task.done.notify_one();
});
DriverHandle {
_join: Box::new(join),
result,
shared: shared_for_handle,
}
}
async fn supervised_driver_loop<P>(
shared: Arc<ConnectionShared>,
mut transport: Transport<P>,
reconnect_ctx: ReconnectContext,
providers: P,
time: P::Time,
) -> Result<(), EngineError>
where
P: Providers,
{
let seed: u64 = Arc::as_ptr(&shared) as usize as u64;
let mut backoff: Option<magnetar_proto::Backoff> = None;
let mut give_up_attempts: u32 = 0;
let task = providers.task().clone();
let mut socket_alive_since = shared.now_instant();
let mut last_inner_result =
driver_loop_inner::<P>(shared.clone(), transport, time.clone(), task.clone()).await;
loop {
if shared.inner.lock().is_user_closed() {
return last_inner_result;
}
let supervisor_cfg = shared.inner.lock().supervisor_config().cloned();
let connect_timeout = shared.inner.lock().connect_timeout();
let Some(cfg) = supervisor_cfg else {
return last_inner_result;
};
if cfg.anti_thrash_threshold.is_some() {
let now = shared.now_instant();
let should_record = {
let conn = shared.inner.lock();
conn.anti_thrash_state()
.last_reattach_at()
.is_some_and(|t| now.saturating_duration_since(t) <= cfg.drop_grace)
};
if should_record {
shared.inner.lock().record_reattach_outcome(
now,
magnetar_proto::ReAttachHandle::Producer(magnetar_proto::ProducerHandle(0)),
magnetar_proto::ReAttachOutcomeKind::TcpDropAfterReAttach,
);
}
}
let cooldown_until = {
let conn = shared.inner.lock();
match conn.anti_thrash_tick(shared.now_instant()) {
magnetar_proto::AntiThrashDisposition::Cooldown { until } => Some(until),
magnetar_proto::AntiThrashDisposition::Normal => None,
}
};
if let Some(until) = cooldown_until {
let now = shared.now_instant();
if until > now {
let dur = until.saturating_duration_since(now);
tracing::warn!(
cooldown_ms = u64::try_from(dur.as_millis()).unwrap_or(u64::MAX),
"supervisor: anti-thrash cooldown engaged; sleeping before next redial"
);
let _ = time.sleep(dur).await;
}
shared.inner.lock().anti_thrash_state_mut().clear_cooldown();
}
let backoff = backoff.get_or_insert_with(|| cfg.build_backoff(seed));
let socket_lifetime = shared
.now_instant()
.saturating_duration_since(socket_alive_since);
if cfg.should_reset_backoff(socket_lifetime) {
backoff.reset();
give_up_attempts = 0;
}
let new_transport = loop {
let delay = backoff.next();
let _ = time.sleep(delay).await;
give_up_attempts = give_up_attempts.saturating_add(1);
if cfg.should_give_up(give_up_attempts) {
tracing::warn!(
attempt = give_up_attempts,
max_attempts = cfg.max_attempts.unwrap_or(0),
"supervisor: gave up; reconnect attempt budget exhausted"
);
return last_inner_result;
}
let attempt = give_up_attempts;
if shared.inner.lock().is_user_closed() {
return last_inner_result;
}
let target_host_port: String =
if let Some(provider) = reconnect_ctx.service_url_provider.as_ref() {
strip_url_to_host_port(&provider.get_service_url()).unwrap_or_else(|| {
tracing::warn!(
attempt,
"supervisor: service-url provider returned an unparseable URL; \
falling back to the cached host:port"
);
reconnect_ctx.host_port.clone()
})
} else {
reconnect_ctx.host_port.clone()
};
let resolver = reconnect_ctx.dns_resolver.as_deref();
match Transport::<P>::connect_with_resolver(
providers.network(),
&target_host_port,
resolver,
providers.time(),
connect_timeout,
)
.await
{
Ok(t) => {
tracing::info!(
attempt,
target = %target_host_port,
"supervisor: TCP connected; handshaking"
);
break t;
}
Err(err) => {
tracing::warn!(
attempt,
target = %target_host_port,
error = %err,
"supervisor: reconnect attempt failed; will retry"
);
}
}
};
{
let mut conn = shared.inner.lock();
conn.reset();
if let Err(err) = conn.begin_handshake() {
tracing::error!(error = %err, "supervisor: begin_handshake after reset failed");
return Err(EngineError::Protocol(err));
}
}
shared
.pending_rebuild
.store(true, std::sync::atomic::Ordering::SeqCst);
notify_retry_generation_replaced(&shared);
transport = new_transport;
socket_alive_since = shared.now_instant();
last_inner_result =
driver_loop_inner::<P>(shared.clone(), transport, time.clone(), task.clone()).await;
}
}
fn strip_url_to_host_port(raw: &str) -> Option<String> {
let rest = raw
.strip_prefix("pulsar://")
.or_else(|| raw.strip_prefix("pulsar+ssl://"))?;
let rest = rest.split(['/', '?', '#']).next().unwrap_or(rest);
if rest.is_empty() {
return None;
}
if rest.contains(':') {
Some(rest.to_owned())
} else {
let default_port = if raw.starts_with("pulsar+ssl://") {
6651
} else {
6650
};
Some(format!("{rest}:{default_port}"))
}
}
pub(crate) async fn driver_loop_inner<P>(
shared: Arc<ConnectionShared>,
mut transport: Transport<P>,
time: P::Time,
task: P::Task,
) -> Result<(), EngineError>
where
P: Providers,
{
let mut read_buf = BytesMut::with_capacity(READ_BUFFER_CAPACITY);
let mut pending_write = PendingDriverWrite::new();
let mut close_after_write = false;
loop {
{
let elapsed_ms = time.now().as_millis();
let now_ms = shared
.wall_clock_base_ms
.saturating_add(u64::try_from(elapsed_ms).unwrap_or(u64::MAX));
shared
.wall_clock_ms
.store(now_ms, std::sync::atomic::Ordering::Relaxed);
}
let (out, deadline, should_close) = if pending_write.is_empty() {
let mut conn = shared.inner.lock();
let out = conn.poll_transmit_owned();
let dl = conn.poll_timeout();
let closing = matches!(
conn.state(),
magnetar_proto::HandshakeState::Closing
| magnetar_proto::HandshakeState::Closed
| magnetar_proto::HandshakeState::Failed
);
(out, dl, closing)
} else {
let conn = shared.inner.lock();
let dl = conn.poll_timeout();
let closing = matches!(
conn.state(),
magnetar_proto::HandshakeState::Closing
| magnetar_proto::HandshakeState::Closed
| magnetar_proto::HandshakeState::Failed
);
(
magnetar_proto::TransmitOwned::Contiguous(Bytes::new()),
dl,
closing,
)
};
close_after_write |= should_close;
if pending_write.is_empty() {
pending_write.push_transmit(out);
}
if !pending_write.is_empty() {
match pending_write
.write_budgeted(&mut transport, DRIVER_WRITE_BUDGET_BYTES)
.await
{
Ok(bytes) => tracing::trace!(bytes, "writing outbound bytes"),
Err(err) => {
shared.inner.lock().mark_disconnected();
return Err(err.into());
}
}
if let Err(err) = transport.flush().await {
shared.inner.lock().mark_disconnected();
return Err(err.into());
}
}
if pending_write.is_empty() && close_after_write {
let _ = transport.shutdown().await;
return Ok(());
}
let sleep_dur = deadline.map(|t| t.saturating_duration_since(shared.now_instant()));
moonpool_core::select! {
biased;
r = transport.read_buf(&mut read_buf) => {
let n = match r {
Ok(n) => n,
Err(err) => {
shared.inner.lock().mark_disconnected();
return Err(err.into());
}
};
if n == 0 {
{
let mut conn = shared.inner.lock();
conn.mark_disconnected();
debug_assert!(
!conn.is_connected(),
"mark_disconnected() must clear is_connected() (ADR-0038)"
);
}
return Err(EngineError::PeerClosed);
}
let chunk = read_buf.split();
debug_assert_eq!(
chunk.len(),
n,
"read chunk length must equal the byte count just read"
);
let now = shared.now_instant();
let handle_result = shared.inner.lock().handle_bytes_owned(now, chunk);
if let Err(err) = handle_result {
shared.inner.lock().mark_disconnected();
return Err(err.into());
}
if shared
.pending_rebuild
.load(std::sync::atomic::Ordering::SeqCst)
{
let connected = shared.inner.lock().is_connected();
if connected
&& shared
.pending_rebuild
.compare_exchange(
true,
false,
std::sync::atomic::Ordering::SeqCst,
std::sync::atomic::Ordering::SeqCst,
)
.is_ok()
{
let (n_p, n_c) = {
let mut conn = shared.inner.lock();
let producers = conn.rebuild_producers();
let consumers = conn.rebuild_consumers();
(producers.len(), consumers.len())
};
notify_retry_generation_replaced(&shared);
tracing::info!(
producers = n_p,
consumers = n_c,
"supervisor: reconnected to broker; handshake complete, replayed \
producer + consumer state"
);
shared.driver_waker.notify_one();
}
}
let mut retries: Vec<RetryRequest> = Vec::new();
handle_pending_events(&shared, &mut retries)?;
for req in retries {
spawn_retry_leg::<P>(&shared, &time, &task, req);
}
shared.event_waker.notify_waiters();
shared.driver_waker.notify_waiters();
}
() = shared.driver_waker.notified() => {
}
() = async {
if pending_write.is_empty() {
std::future::pending::<()>().await;
}
} => {
}
() = sleep_or_pending::<P>(&time, sleep_dur) => {
shared.inner.lock().handle_timeout(shared.now_instant());
}
}
}
}
async fn sleep_or_pending<P: Providers>(time: &P::Time, dur: Option<Duration>) {
match dur {
Some(d) => {
let _ = time.sleep(d).await;
}
None => std::future::pending::<()>().await,
}
}
#[cfg(test)]
mod tests {
use std::future::Future as _;
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::task::{Poll, Wake, Waker};
use std::time::{Duration, Instant};
use bytes::{Bytes, BytesMut};
use magnetar_proto::types::CompressionKind;
use magnetar_proto::{
ConnectionConfig, ConnectionEvent, CreateProducerRequest, OpOutcome, OperationRetryConfig,
PendingOpKey, ProducerHandle, SubscribeRequest, decode_one, encode_command, pb,
};
use moonpool_core::{Providers as _, TokioProviders};
use super::{
DRIVER_WRITE_BUDGET_BYTES, PendingDriverWrite, RetryRequest, handle_pending_events,
lookup_then, notify_retry_generation_replaced, spawn_retry_leg, strip_url_to_host_port,
terminalize_retry_request,
};
use crate::producer::Producer;
use crate::{ConnectionShared, EngineError};
#[test]
fn permanent_reattachment_errors_wake_established_operation_waiters() {
struct CountingWake(AtomicUsize);
impl Wake for CountingWake {
fn wake(self: Arc<Self>) {
self.0.fetch_add(1, Ordering::SeqCst);
}
fn wake_by_ref(self: &Arc<Self>) {
self.0.fetch_add(1, Ordering::SeqCst);
}
}
let shared = ConnectionShared::new(ConnectionConfig::default());
let mut conn = shared.inner.lock();
conn.begin_handshake().expect("handshake");
let connected = pb::BaseCommand {
r#type: pb::base_command::Type::Connected as i32,
connected: Some(pb::CommandConnected {
server_version: "magnetar-test".to_owned(),
protocol_version: Some(21),
max_message_size: Some(5 * 1024 * 1024),
feature_flags: Some(pb::FeatureFlags::default()),
}),
..Default::default()
};
let mut frame = BytesMut::new();
encode_command(&mut frame, &connected).expect("encode CommandConnected");
conn.handle_bytes(Instant::now(), &frame)
.expect("complete handshake");
while conn.poll_event().is_some() {}
let producer_request_id = conn.peek_next_request_id_for_test();
let producer = conn.create_producer(CreateProducerRequest {
topic: "persistent://public/default/permanent-reattach-producer".to_owned(),
..Default::default()
});
let producer_success = pb::BaseCommand {
r#type: pb::base_command::Type::ProducerSuccess as i32,
producer_success: Some(pb::CommandProducerSuccess {
request_id: producer_request_id,
producer_name: "producer".to_owned(),
last_sequence_id: Some(-1),
producer_ready: Some(true),
..Default::default()
}),
..Default::default()
};
let mut frame = BytesMut::new();
encode_command(&mut frame, &producer_success).expect("encode ProducerSuccess");
conn.handle_bytes(Instant::now(), &frame)
.expect("establish producer");
while conn.poll_event().is_some() {}
let consumer_request_id = conn.peek_next_request_id_for_test();
let consumer = conn.subscribe(SubscribeRequest {
topic: "persistent://public/default/permanent-reattach-consumer".to_owned(),
subscription: "permanent-reattach".to_owned(),
sub_type: pb::command_subscribe::SubType::Exclusive,
..Default::default()
});
let subscribe_success = pb::BaseCommand {
r#type: pb::base_command::Type::Success as i32,
success: Some(pb::CommandSuccess {
request_id: consumer_request_id,
..Default::default()
}),
..Default::default()
};
let mut frame = BytesMut::new();
encode_command(&mut frame, &subscribe_success).expect("encode CommandSuccess");
conn.handle_bytes(Instant::now(), &frame)
.expect("establish consumer");
while conn.poll_event().is_some() {}
let snapshot_sequence_id = conn
.send(
producer,
magnetar_proto::producer::OutgoingMessage {
payload: Bytes::from_static(b"before-reset"),
metadata: pb::MessageMetadata::default(),
uncompressed_size: 12,
num_messages: 1,
txn_id: None,
source_message_id: None,
},
0,
Instant::now(),
)
.expect("queue send before reset");
let reset_counter = Arc::new(CountingWake(AtomicUsize::new(0)));
let reset_waker: Waker = Arc::clone(&reset_counter).into();
conn.register_waker(
PendingOpKey::Send(producer, snapshot_sequence_id),
reset_waker,
);
conn.reset();
assert_eq!(reset_counter.0.load(Ordering::SeqCst), 1);
let snapshot_terminal_counter = Arc::new(CountingWake(AtomicUsize::new(0)));
let snapshot_terminal_waker: Waker = Arc::clone(&snapshot_terminal_counter).into();
conn.register_waker(
PendingOpKey::Send(producer, snapshot_sequence_id),
snapshot_terminal_waker,
);
conn.begin_handshake().expect("re-handshake");
let mut frame = BytesMut::new();
encode_command(&mut frame, &connected).expect("encode CommandConnected");
conn.handle_bytes(Instant::now(), &frame)
.expect("complete reconnect handshake");
while conn.poll_event().is_some() {}
let producer_retry = conn.rebuild_producers()[0];
let consumer_retry = conn.rebuild_consumers()[0];
let sequence_id = conn
.send(
producer,
magnetar_proto::producer::OutgoingMessage {
payload: Bytes::from_static(b"pending"),
metadata: pb::MessageMetadata::default(),
uncompressed_size: 7,
num_messages: 1,
txn_id: None,
source_message_id: None,
},
0,
Instant::now(),
)
.expect("queue send during producer reattachment");
let send_counter = Arc::new(CountingWake(AtomicUsize::new(0)));
let send_waker: Waker = Arc::clone(&send_counter).into();
conn.register_waker(PendingOpKey::Send(producer, sequence_id), send_waker);
let receive_counter = Arc::new(CountingWake(AtomicUsize::new(0)));
let receive_waker: Waker = Arc::clone(&receive_counter).into();
conn.register_consumer_receive_waker(consumer, receive_waker)
.expect("register receive waker during consumer reattachment");
for request_id in [producer_retry, consumer_retry] {
let error = pb::BaseCommand {
r#type: pb::base_command::Type::Error as i32,
error: Some(pb::CommandError {
request_id: request_id.0,
error: pb::ServerError::TopicNotFound as i32,
message: "topic was deleted".to_owned(),
}),
..Default::default()
};
let mut frame = BytesMut::new();
encode_command(&mut frame, &error).expect("encode CommandError");
conn.handle_bytes(Instant::now(), &frame)
.expect("handle terminal reattachment error");
}
assert_eq!(send_counter.0.load(Ordering::SeqCst), 1);
assert!(matches!(
conn.take_outcome(PendingOpKey::Send(producer, sequence_id)),
Some(OpOutcome::Terminal { .. })
));
assert_eq!(snapshot_terminal_counter.0.load(Ordering::SeqCst), 1);
assert!(matches!(
conn.take_outcome(PendingOpKey::Send(producer, snapshot_sequence_id)),
Some(OpOutcome::Terminal { .. })
));
assert_eq!(receive_counter.0.load(Ordering::SeqCst), 1);
assert!(conn.consumer(consumer).is_some());
assert!(conn.consumer_handle_is_terminal(consumer));
}
#[tokio::test(flavor = "current_thread")]
async fn dropping_established_handle_cancels_blackholed_retry_lookup() {
let shared = ConnectionShared::new(ConnectionConfig::default());
let (handle, slot, request_id) = {
let mut conn = shared.inner.lock();
conn.begin_handshake().expect("begin handshake");
let frame = handshake_response_bytes();
conn.handle_bytes(Instant::now(), &frame)
.expect("complete handshake");
let request_id = magnetar_proto::RequestId(conn.peek_next_request_id_for_test());
let handle = conn.create_producer(CreateProducerRequest {
topic: "persistent://public/default/blackholed-retry-drop".to_owned(),
..Default::default()
});
let slot = conn.producer(handle).expect("producer slot").clone();
(handle, slot, request_id)
};
let producer = Producer::<TokioProviders>::assemble(
shared.clone(),
handle,
slot,
CompressionKind::None,
None,
);
let providers = TokioProviders::new();
let time = providers.time().clone();
let mut lookup = Box::pin(lookup_then(
&shared,
&time,
"persistent://public/default/blackholed-retry-drop",
RetryRequest::Producer(handle, request_id),
));
std::future::poll_fn(|cx| {
assert!(lookup.as_mut().poll(cx).is_pending());
Poll::Ready(())
})
.await;
drop(producer);
let completed = tokio::time::timeout(Duration::from_millis(100), lookup)
.await
.expect("dropping the handle must wake the blackholed lookup");
assert!(!completed, "a closed handle must cancel its retry lookup");
}
#[tokio::test(flavor = "current_thread")]
async fn dropping_established_handle_cancels_initial_retry_backoff() {
let shared = ConnectionShared::new(ConnectionConfig::default());
let (handle, slot, failed_request_id) = {
let mut conn = shared.inner.lock();
conn.set_operation_retry_config(OperationRetryConfig {
initial_backoff: Duration::from_secs(30),
max_backoff: Duration::from_secs(30),
max_retries: Some(1),
});
conn.begin_handshake().expect("begin handshake");
let connected = pb::BaseCommand {
r#type: pb::base_command::Type::Connected as i32,
connected: Some(pb::CommandConnected {
server_version: "magnetar-test".to_owned(),
protocol_version: Some(21),
max_message_size: Some(5 * 1024 * 1024),
feature_flags: Some(pb::FeatureFlags::default()),
}),
..Default::default()
};
let mut frame = BytesMut::new();
encode_command(&mut frame, &connected).expect("encode CommandConnected");
conn.handle_bytes(Instant::now(), &frame)
.expect("complete handshake");
let producer_request_id = conn.peek_next_request_id_for_test();
let handle = conn.create_producer(CreateProducerRequest {
topic: "persistent://public/default/retry-backoff-drop".to_owned(),
..Default::default()
});
let slot = conn.producer(handle).expect("producer slot").clone();
let producer_success = pb::BaseCommand {
r#type: pb::base_command::Type::ProducerSuccess as i32,
producer_success: Some(pb::CommandProducerSuccess {
request_id: producer_request_id,
producer_name: "producer".to_owned(),
last_sequence_id: Some(-1),
producer_ready: Some(true),
..Default::default()
}),
..Default::default()
};
let mut frame = BytesMut::new();
encode_command(&mut frame, &producer_success).expect("encode ProducerSuccess");
conn.handle_bytes(Instant::now(), &frame)
.expect("establish producer");
while conn.poll_event().is_some() {}
conn.reset();
conn.begin_handshake().expect("restart handshake");
let mut frame = BytesMut::new();
encode_command(&mut frame, &connected).expect("encode CommandConnected");
conn.handle_bytes(Instant::now(), &frame)
.expect("complete reconnect handshake");
while conn.poll_event().is_some() {}
let failed_request_id = conn.rebuild_producers()[0];
let transient_error = pb::BaseCommand {
r#type: pb::base_command::Type::Error as i32,
error: Some(pb::CommandError {
request_id: failed_request_id.0,
error: pb::ServerError::ProducerBusy as i32,
message: "retry later".to_owned(),
}),
..Default::default()
};
let mut frame = BytesMut::new();
encode_command(&mut frame, &transient_error).expect("encode transient error");
conn.handle_bytes(Instant::now(), &frame)
.expect("schedule established retry");
(handle, slot, failed_request_id)
};
let producer = Producer::<TokioProviders>::assemble(
shared.clone(),
handle,
slot,
CompressionKind::None,
None,
);
let providers = TokioProviders::new();
let time = providers.time().clone();
let task = providers.task().clone();
let weak = Arc::downgrade(&shared);
spawn_retry_leg::<TokioProviders>(
&shared,
&time,
&task,
RetryRequest::Producer(handle, failed_request_id),
);
drop(producer);
drop(shared);
tokio::time::timeout(Duration::from_millis(100), async {
while weak.upgrade().is_some() {
tokio::task::yield_now().await;
}
})
.await
.expect("dropping the handle must cancel the initial retry backoff");
}
#[tokio::test(flavor = "current_thread")]
async fn retry_lookup_does_not_emit_before_reconnect_handshake_completes() {
let shared = ConnectionShared::new(ConnectionConfig::default());
let (handle, request_id) = {
let mut conn = shared.inner.lock();
conn.begin_handshake().expect("begin handshake");
let frame = handshake_response_bytes();
conn.handle_bytes(Instant::now(), &frame)
.expect("complete handshake");
let request_id = magnetar_proto::RequestId(conn.peek_next_request_id_for_test());
let handle = conn.subscribe(SubscribeRequest {
topic: "persistent://public/default/retry-before-reconnect-handshake".to_owned(),
subscription: "retry-before-reconnect-handshake".to_owned(),
..Default::default()
});
conn.reset();
conn.begin_handshake().expect("restart handshake");
(handle, request_id)
};
let providers = TokioProviders::new();
let time = providers.time().clone();
let completed = tokio::time::timeout(
Duration::from_millis(100),
lookup_then(
&shared,
&time,
"persistent://public/default/retry-before-reconnect-handshake",
RetryRequest::Consumer(handle, request_id),
),
)
.await
.expect("a pre-handshake retry lookup must cancel instead of parking");
assert!(!completed);
let mut staged = shared.inner.lock().poll_transmit();
while !staged.is_empty() {
let frame = decode_one(&mut staged).expect("staged frame must decode");
assert_ne!(
frame.command.r#type,
pb::base_command::Type::Lookup as i32,
"no data-plane lookup may precede CommandConnected"
);
}
}
#[tokio::test(flavor = "current_thread")]
async fn superseding_consumer_generation_cancels_blackholed_retry_lookup() {
let shared = ConnectionShared::new(ConnectionConfig::default());
let (handle, request_id) = {
let mut conn = shared.inner.lock();
conn.begin_handshake().expect("begin handshake");
let frame = handshake_response_bytes();
conn.handle_bytes(Instant::now(), &frame)
.expect("complete handshake");
let request_id = magnetar_proto::RequestId(conn.peek_next_request_id_for_test());
let handle = conn.subscribe(SubscribeRequest {
topic: "persistent://public/default/superseded-blackholed-retry".to_owned(),
subscription: "superseded-blackholed-retry".to_owned(),
..Default::default()
});
(handle, request_id)
};
let providers = TokioProviders::new();
let time = providers.time().clone();
let lookup_request_id =
magnetar_proto::RequestId(shared.inner.lock().peek_next_request_id_for_test());
let mut lookup = Box::pin(lookup_then(
&shared,
&time,
"persistent://public/default/superseded-blackholed-retry",
RetryRequest::Consumer(handle, request_id),
));
std::future::poll_fn(|cx| {
assert!(lookup.as_mut().poll(cx).is_pending());
Poll::Ready(())
})
.await;
assert!(
shared
.inner
.lock()
.has_pending_request_for_test(lookup_request_id)
);
shared
.inner
.lock()
.resubscribe_consumer_after_seek(handle)
.expect("replacement subscribe generation");
notify_retry_generation_replaced(&shared);
let completed = tokio::time::timeout(Duration::from_millis(100), lookup)
.await
.expect("generation replacement must wake the blackholed lookup");
assert!(!completed);
assert!(
!shared
.inner
.lock()
.has_pending_request_for_test(lookup_request_id),
"cancelled retry lookup must unregister its pending request"
);
}
#[test]
fn superseded_consumer_retry_lookup_cannot_terminalize_current_generation() {
let shared = ConnectionShared::new(ConnectionConfig::default());
let (handle, superseded_request_id, current_request_id) = {
let mut conn = shared.inner.lock();
conn.begin_handshake().expect("begin handshake");
let connected = pb::BaseCommand {
r#type: pb::base_command::Type::Connected as i32,
connected: Some(pb::CommandConnected {
server_version: "magnetar-test".to_owned(),
protocol_version: Some(21),
max_message_size: Some(5 * 1024 * 1024),
feature_flags: Some(pb::FeatureFlags::default()),
}),
..Default::default()
};
let mut frame = BytesMut::new();
encode_command(&mut frame, &connected).expect("encode CommandConnected");
conn.handle_bytes(Instant::now(), &frame)
.expect("complete handshake");
let initial_request_id = conn.peek_next_request_id_for_test();
let handle = conn.subscribe(SubscribeRequest {
topic: "persistent://public/default/superseded-retry-lookup".to_owned(),
subscription: "superseded-retry-lookup".to_owned(),
..Default::default()
});
let success = pb::BaseCommand {
r#type: pb::base_command::Type::Success as i32,
success: Some(pb::CommandSuccess {
request_id: initial_request_id,
..Default::default()
}),
..Default::default()
};
let mut frame = BytesMut::new();
encode_command(&mut frame, &success).expect("encode initial success");
conn.handle_bytes(Instant::now(), &frame)
.expect("establish consumer");
let superseded_request_id = conn
.resubscribe_consumer_after_seek(handle)
.expect("superseded subscribe generation");
let current_request_id = conn
.resubscribe_consumer_after_seek(handle)
.expect("current subscribe generation");
(handle, superseded_request_id, current_request_id)
};
terminalize_retry_request(
&shared,
RetryRequest::Consumer(handle, superseded_request_id),
pb::ServerError::AuthorizationError as i32,
"stale retry lookup denied",
);
let mut conn = shared.inner.lock();
assert!(
!conn.consumer_handle_is_terminal(handle),
"a terminal lookup from the superseded retry must not kill the current generation"
);
assert!(
conn.retry_consumer_subscribe_if_current(handle, current_request_id)
.is_some(),
"the current generation must remain retryable"
);
}
#[test]
fn superseded_producer_retry_lookup_cannot_terminalize_current_generation() {
let shared = ConnectionShared::new(ConnectionConfig::default());
let (handle, superseded_request_id, current_request_id) = {
let mut conn = shared.inner.lock();
conn.begin_handshake().expect("begin handshake");
let frame = handshake_response_bytes();
conn.handle_bytes(Instant::now(), &frame)
.expect("complete handshake");
let request_id = conn.peek_next_request_id_for_test();
let handle = conn.create_producer(CreateProducerRequest {
topic: "persistent://public/default/superseded-producer-retry".to_owned(),
..Default::default()
});
let success = pb::BaseCommand {
r#type: pb::base_command::Type::ProducerSuccess as i32,
producer_success: Some(pb::CommandProducerSuccess {
request_id,
producer_name: "producer".to_owned(),
last_sequence_id: Some(-1),
..Default::default()
}),
..Default::default()
};
let mut frame = BytesMut::new();
encode_command(&mut frame, &success).expect("encode ProducerSuccess");
conn.handle_bytes(Instant::now(), &frame)
.expect("establish producer");
let superseded_request_id = conn
.rebuild_producers()
.into_iter()
.next()
.expect("superseded producer generation");
let current_request_id = conn
.rebuild_producers()
.into_iter()
.next()
.expect("current producer generation");
(handle, superseded_request_id, current_request_id)
};
terminalize_retry_request(
&shared,
RetryRequest::Producer(handle, superseded_request_id),
pb::ServerError::AuthorizationError as i32,
"stale producer retry lookup denied",
);
let mut conn = shared.inner.lock();
assert!(
!conn.producer_is_closed(handle),
"a terminal lookup from the superseded retry must not kill the current producer generation"
);
assert!(
conn.retry_producer_open_if_current(handle, current_request_id)
.is_some(),
"the current producer generation must remain retryable"
);
}
#[tokio::test(flavor = "current_thread")]
async fn superseding_producer_generation_cancels_blackholed_retry_lookup() {
let shared = ConnectionShared::new(ConnectionConfig::default());
let (handle, request_id) = {
let mut conn = shared.inner.lock();
conn.begin_handshake().expect("begin handshake");
let frame = handshake_response_bytes();
conn.handle_bytes(Instant::now(), &frame)
.expect("complete handshake");
let request_id = magnetar_proto::RequestId(conn.peek_next_request_id_for_test());
let handle = conn.create_producer(CreateProducerRequest {
topic: "persistent://public/default/superseded-producer-blackhole".to_owned(),
..Default::default()
});
(handle, request_id)
};
let providers = TokioProviders::new();
let time = providers.time().clone();
let lookup_request_id =
magnetar_proto::RequestId(shared.inner.lock().peek_next_request_id_for_test());
let mut lookup = Box::pin(lookup_then(
&shared,
&time,
"persistent://public/default/superseded-producer-blackhole",
RetryRequest::Producer(handle, request_id),
));
std::future::poll_fn(|cx| {
assert!(lookup.as_mut().poll(cx).is_pending());
Poll::Ready(())
})
.await;
shared.inner.lock().rebuild_producers();
notify_retry_generation_replaced(&shared);
let completed = tokio::time::timeout(Duration::from_millis(100), lookup)
.await
.expect("producer generation replacement must wake the blackholed lookup");
assert!(!completed);
assert!(
!shared
.inner
.lock()
.has_pending_request_for_test(lookup_request_id),
"cancelled producer retry lookup must unregister its pending request"
);
}
fn handshake_response_bytes() -> BytesMut {
let cmd = pb::BaseCommand {
r#type: pb::base_command::Type::Connected as i32,
connected: Some(pb::CommandConnected {
server_version: "magnetar-test".to_owned(),
protocol_version: Some(21),
max_message_size: Some(5 * 1024 * 1024),
feature_flags: Some(pb::FeatureFlags::default()),
}),
..Default::default()
};
let mut buf = BytesMut::new();
encode_command(&mut buf, &cmd).expect("encode CommandConnected");
buf
}
#[test]
fn topic_migrated_triggers_recoverable_error() {
let shared = ConnectionShared::new(ConnectionConfig::default());
{
let mut conn = shared.inner.lock();
conn.begin_handshake().expect("handshake");
let frame = handshake_response_bytes();
conn.handle_bytes(Instant::now(), &frame)
.expect("connected");
match conn.poll_event() {
Some(ConnectionEvent::Connected { .. }) => {}
other => panic!("expected Connected, got {other:?}"),
}
let migrated = pb::BaseCommand {
r#type: pb::base_command::Type::TopicMigrated as i32,
topic_migrated: Some(pb::CommandTopicMigrated {
resource_id: 42,
resource_type: pb::command_topic_migrated::ResourceType::Producer as i32,
broker_service_url: Some("pulsar://new-broker:6650".to_owned()),
broker_service_url_tls: None,
}),
..Default::default()
};
let mut buf = BytesMut::new();
encode_command(&mut buf, &migrated).expect("encode CommandTopicMigrated");
conn.handle_bytes(Instant::now(), &buf)
.expect("handle migration");
}
let mut retries = Vec::new();
let err = handle_pending_events(&shared, &mut retries).expect_err("migration must error");
assert!(
retries.is_empty(),
"a topic-migration event must not enqueue a transient retry"
);
let msg = format!("{err}");
assert!(
matches!(err, EngineError::Config(_)) && msg.contains("PIP-188"),
"expected PIP-188 config error, got {err:?}"
);
assert_eq!(ProducerHandle(42), ProducerHandle(42));
}
#[test]
fn driver_write_budget_leaves_tail_for_next_tick() {
let mut pending =
PendingDriverWrite::from_transmit(magnetar_proto::TransmitOwned::Contiguous(
Bytes::from_static(b"abcdefghijklmnopqrstuvwxyz"),
));
let first = pending.pop_budgeted(8);
assert_eq!(first, vec![Bytes::from_static(b"abcdefgh")]);
assert!(
!pending.is_empty(),
"the driver must keep unwritten bytes so it can read before continuing writes"
);
let rest = pending.pop_budgeted(DRIVER_WRITE_BUDGET_BYTES);
let mut observed = Vec::new();
for chunk in rest {
observed.extend_from_slice(&chunk);
}
assert_eq!(&observed, b"ijklmnopqrstuvwxyz");
assert!(pending.is_empty());
}
#[test]
fn strip_url_to_host_port_handles_plain() {
assert_eq!(
strip_url_to_host_port("pulsar://broker:6650").as_deref(),
Some("broker:6650")
);
}
#[test]
fn strip_url_to_host_port_handles_tls() {
assert_eq!(
strip_url_to_host_port("pulsar+ssl://broker.example.com:6651").as_deref(),
Some("broker.example.com:6651")
);
}
#[test]
fn strip_url_to_host_port_defaults_plain_port() {
assert_eq!(
strip_url_to_host_port("pulsar://broker").as_deref(),
Some("broker:6650")
);
}
#[test]
fn strip_url_to_host_port_defaults_tls_port() {
assert_eq!(
strip_url_to_host_port("pulsar+ssl://broker").as_deref(),
Some("broker:6651")
);
}
#[test]
fn strip_url_to_host_port_strips_path() {
assert_eq!(
strip_url_to_host_port("pulsar://broker:6650/admin").as_deref(),
Some("broker:6650")
);
}
#[test]
fn strip_url_to_host_port_rejects_unknown_scheme() {
assert!(strip_url_to_host_port("http://broker:6650").is_none());
}
}