fix(nats): switch from push to pull consumer — pull is reliable, push had deliver_subject issues
Some checks failed
lint / lint (push) Has been cancelled
test / unit (push) Has been cancelled
test / integration (push) Has been cancelled
lint / lint (pull_request) Failing after 9m35s
test / unit (pull_request) Failing after 11m38s
test / integration (pull_request) Failing after 17m2s
Some checks failed
lint / lint (push) Has been cancelled
test / unit (push) Has been cancelled
test / integration (push) Has been cancelled
lint / lint (pull_request) Failing after 9m35s
test / unit (pull_request) Failing after 11m38s
test / integration (pull_request) Failing after 17m2s
This commit is contained in:
@@ -96,25 +96,28 @@ impl NatsMessageSource {
|
|||||||
|
|
||||||
impl MessageSource for NatsMessageSource {
|
impl MessageSource for NatsMessageSource {
|
||||||
fn messages(&self) -> BoxStream<'_, Result<RawMessage, DomainError>> {
|
fn messages(&self) -> BoxStream<'_, Result<RawMessage, DomainError>> {
|
||||||
|
use futures::stream;
|
||||||
|
use tokio::sync::{mpsc, Mutex as TokioMutex};
|
||||||
|
|
||||||
let js = self.jetstream.clone();
|
let js = self.jetstream.clone();
|
||||||
Box::pin(async_stream::try_stream! {
|
let (tx, rx) = mpsc::channel::<Result<RawMessage, DomainError>>(128);
|
||||||
// Ensure stream exists (idempotent).
|
|
||||||
js.get_or_create_stream(stream_config())
|
|
||||||
.await
|
|
||||||
.map_err(|e| DomainError::Internal(e.to_string()))?;
|
|
||||||
|
|
||||||
let stream = js
|
// Spawn the consumer loop in the background.
|
||||||
.get_stream(STREAM_NAME)
|
// Pull consumer: worker explicitly fetches from NATS rather than NATS pushing.
|
||||||
.await
|
tokio::spawn(async move {
|
||||||
.map_err(|e| DomainError::Internal(e.to_string()))?;
|
let stream = match js.get_stream(STREAM_NAME).await {
|
||||||
|
Ok(s) => s,
|
||||||
|
Err(e) => {
|
||||||
|
let _ = tx.send(Err(DomainError::Internal(e.to_string()))).await;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Durable push consumer — survives worker restarts.
|
let consumer = match stream
|
||||||
let consumer = stream
|
|
||||||
.get_or_create_consumer(
|
.get_or_create_consumer(
|
||||||
CONSUMER_NAME,
|
CONSUMER_NAME,
|
||||||
jetstream::consumer::push::Config {
|
jetstream::consumer::pull::Config {
|
||||||
durable_name: Some(CONSUMER_NAME.to_string()),
|
durable_name: Some(CONSUMER_NAME.to_string()),
|
||||||
deliver_subject: CONSUMER_NAME.to_string() + ".deliver",
|
|
||||||
ack_policy: jetstream::consumer::AckPolicy::Explicit,
|
ack_policy: jetstream::consumer::AckPolicy::Explicit,
|
||||||
ack_wait: std::time::Duration::from_secs(ACK_WAIT_SECS),
|
ack_wait: std::time::Duration::from_secs(ACK_WAIT_SECS),
|
||||||
max_deliver: MAX_DELIVER,
|
max_deliver: MAX_DELIVER,
|
||||||
@@ -122,24 +125,42 @@ impl MessageSource for NatsMessageSource {
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| DomainError::Internal(e.to_string()))?;
|
{
|
||||||
|
Ok(c) => c,
|
||||||
|
Err(e) => {
|
||||||
|
let _ = tx.send(Err(DomainError::Internal(e.to_string()))).await;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
let mut messages = consumer
|
tracing::info!("NATS pull consumer ready");
|
||||||
.messages()
|
|
||||||
.await
|
loop {
|
||||||
.map_err(|e| DomainError::Internal(e.to_string()))?;
|
let mut messages = match consumer.messages().await {
|
||||||
|
Ok(m) => m,
|
||||||
|
Err(e) => {
|
||||||
|
tracing::error!("NATS consumer.messages() failed: {e}");
|
||||||
|
let _ = tx.send(Err(DomainError::Internal(e.to_string()))).await;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
use futures::StreamExt;
|
use futures::StreamExt;
|
||||||
while let Some(result) = messages.next().await {
|
while let Some(result) = messages.next().await {
|
||||||
let msg = result.map_err(|e| DomainError::Internal(e.to_string()))?;
|
let msg = match result {
|
||||||
|
Ok(m) => m,
|
||||||
|
Err(e) => {
|
||||||
|
tracing::warn!("NATS message error: {e}");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
let subject = msg.subject.to_string();
|
let subject = msg.subject.to_string();
|
||||||
let payload = msg.payload.to_vec();
|
let payload = msg.payload.to_vec();
|
||||||
|
|
||||||
// Wrap in Arc so both closures can hold a reference.
|
|
||||||
let msg = Arc::new(msg);
|
let msg = Arc::new(msg);
|
||||||
let msg_nack = Arc::clone(&msg);
|
let msg_nack = Arc::clone(&msg);
|
||||||
|
|
||||||
yield RawMessage {
|
let raw = RawMessage {
|
||||||
subject,
|
subject,
|
||||||
payload,
|
payload,
|
||||||
ack: Box::new(move || {
|
ack: Box::new(move || {
|
||||||
@@ -159,8 +180,21 @@ impl MessageSource for NatsMessageSource {
|
|||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if tx.send(Ok(raw)).await.is_err() {
|
||||||
|
return; // receiver dropped — worker shutting down
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
|
// messages() stream ended (e.g. fetch timeout) — loop and restart
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Bridge the channel receiver into a BoxStream.
|
||||||
|
let rx = Arc::new(TokioMutex::new(rx));
|
||||||
|
Box::pin(stream::unfold(rx, |rx| async move {
|
||||||
|
let item = rx.lock().await.recv().await?;
|
||||||
|
Some((item, rx))
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user