Skip to content

Commit

Permalink
Add NAK and backoff support
Browse files Browse the repository at this point in the history
Signed-off-by: Tomasz Pietrek <tomasz@nats.io>
  • Loading branch information
Jarema committed Feb 24, 2023
1 parent 2b5c851 commit ffd41ac
Show file tree
Hide file tree
Showing 6 changed files with 117 additions and 7 deletions.
4 changes: 2 additions & 2 deletions async-nats/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ futures = "0.3.21"
nkeys = "0.2.0"
once_cell = "1.10.0"
regex = "1.5.5"
serde = { version = "1.0.136", features = ["derive"] }
serde = { version = "1.0.152", features = ["derive"] }
serde_json = "1.0.79"
serde_repr = "0.1.7"
http = "0.2.7"
Expand All @@ -31,7 +31,7 @@ url = "2"
tokio-rustls = "0.23"
rustls-pemfile = "1.0.1"
nuid = "0.3.2"
serde_nanos = "0.1.1"
serde_nanos = "0.1.3"
time = { version = "0.3.6", features = ["parsing", "formatting", "serde", "serde-well-known"] }
rustls-native-certs = "0.6.2"
tracing = "0.1"
Expand Down
5 changes: 4 additions & 1 deletion async-nats/src/jetstream/consumer/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -313,9 +313,12 @@ pub struct Config {
/// Force consumer to use memory storage.
#[serde(default, skip_serializing_if = "is_default", rename = "mem_storage")]
pub memory_storage: bool,
// Additional consumer metadata.
/// Additional consumer metadata.
#[serde(default, skip_serializing_if = "is_default")]
pub metadata: HashMap<String, String>,
/// Custom backoff for missed acknowledgments.
#[serde(default, skip_serializing_if = "is_default", with = "serde_nanos")]
pub backoff: Vec<Duration>,
}

impl From<&Config> for Config {
Expand Down
5 changes: 5 additions & 0 deletions async-nats/src/jetstream/consumer/pull.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1514,6 +1514,9 @@ pub struct Config {
// Additional consumer metadata.
#[serde(default, skip_serializing_if = "is_default")]
pub metadata: HashMap<String, String>,
/// Custom backoff for missed acknowledgments.
#[serde(default, skip_serializing_if = "is_default", with = "serde_nanos")]
pub backoff: Vec<Duration>,
}

impl IntoConsumerConfig for &Config {
Expand Down Expand Up @@ -1550,6 +1553,7 @@ impl IntoConsumerConfig for Config {
num_replicas: self.num_replicas,
memory_storage: self.memory_storage,
metadata: self.metadata,
backoff: self.backoff,
}
}
}
Expand Down Expand Up @@ -1583,6 +1587,7 @@ impl FromConsumer for Config {
num_replicas: config.num_replicas,
memory_storage: config.memory_storage,
metadata: config.metadata,
backoff: config.backoff,
})
}
}
6 changes: 6 additions & 0 deletions async-nats/src/jetstream/consumer/push.rs
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,9 @@ pub struct Config {
// Additional consumer metadata.
#[serde(default, skip_serializing_if = "is_default")]
pub metadata: HashMap<String, String>,
/// Custom backoff for missed acknowledgments.
#[serde(default, skip_serializing_if = "is_default", with = "serde_nanos")]
pub backoff: Vec<Duration>,
}

impl FromConsumer for Config {
Expand Down Expand Up @@ -257,6 +260,7 @@ impl FromConsumer for Config {
num_replicas: config.num_replicas,
memory_storage: config.memory_storage,
metadata: config.metadata,
backoff: config.backoff,
})
}
}
Expand Down Expand Up @@ -289,6 +293,7 @@ impl IntoConsumerConfig for Config {
num_replicas: self.num_replicas,
memory_storage: self.memory_storage,
metadata: self.metadata,
backoff: self.backoff,
}
}
}
Expand Down Expand Up @@ -399,6 +404,7 @@ impl IntoConsumerConfig for OrderedConfig {
num_replicas: 1,
memory_storage: true,
metadata: self.metadata,
backoff: Vec::new(),
}
}
}
Expand Down
9 changes: 6 additions & 3 deletions async-nats/src/jetstream/message.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,10 @@
//! A wrapped `crate::Message` with `JetStream` related methods.
use super::context::Context;
use crate::Error;

use bytes::Bytes;
use futures::future::TryFutureExt;
use futures::StreamExt;
use std::time::Duration;
use time::OffsetDateTime;

#[derive(Debug)]
Expand Down Expand Up @@ -314,7 +314,7 @@ pub enum AckKind {
/// Signals that the message will not be processed now
/// and processing can move onto the next message, NAK'd
/// message will be retried.
Nak,
Nak(Option<Duration>),
/// When sent before the AckWait period indicates that
/// work is ongoing and the period should be extended by
/// another equal to AckWait.
Expand All @@ -333,7 +333,10 @@ impl From<AckKind> for Bytes {
use AckKind::*;
match kind {
Ack => Bytes::from_static(b"+ACK"),
Nak => Bytes::from_static(b"-NAK"),
Nak(maybe_duration) => match maybe_duration {
None => Bytes::from_static(b"-NAK"),
Some(duration) => format!("-NAK {{\"delay\":{}}}", duration.as_nanos()).into(),
},
Progress => Bytes::from_static(b"+WPI"),
Next => Bytes::from_static(b"+NXT"),
Term => Bytes::from_static(b"+TERM"),
Expand Down
95 changes: 94 additions & 1 deletion async-nats/tests/jetstream_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ mod jetstream {
use async_nats::jetstream::context::Publish;
use async_nats::jetstream::response::Response;
use async_nats::jetstream::stream::{self, DiscardPolicy, StorageType};
use async_nats::jetstream::AckKind;
use async_nats::ConnectOptions;
use bytes::Bytes;
use futures::stream::{StreamExt, TryStreamExt};
Expand Down Expand Up @@ -2014,7 +2015,7 @@ mod jetstream {
if let Some(message) = iter.next().await {
message
.unwrap()
.ack_with(async_nats::jetstream::AckKind::Nak)
.ack_with(async_nats::jetstream::AckKind::Nak(None))
.await
.unwrap();
}
Expand Down Expand Up @@ -2729,4 +2730,96 @@ mod jetstream {

assert_eq!(consumer.info().await.unwrap().config.metadata, metadata);
}

#[tokio::test]
async fn backoff() {
let server = nats_server::run_server("tests/configs/jetstream.conf");
let client = async_nats::connect(server.client_url()).await.unwrap();
let context = async_nats::jetstream::new(client.clone());

let stream = context
.create_stream(async_nats::jetstream::stream::Config {
name: "stream".to_string(),
..Default::default()
})
.await
.unwrap();

let mut consumer = stream
.create_consumer(async_nats::jetstream::consumer::Config {
max_deliver: 10,
backoff: vec![
std::time::Duration::from_secs(1),
std::time::Duration::from_secs(5),
],
..Default::default()
})
.await
.unwrap();

let info = consumer.info().await.unwrap();

assert_eq!(info.config.backoff[0], Duration::from_secs(1));
assert_eq!(info.config.backoff[1], Duration::from_secs(5));
}

#[tokio::test]
async fn nak_with_delay() {
let server = nats_server::run_server("tests/configs/jetstream.conf");
let client = async_nats::connect(server.client_url()).await.unwrap();
let context = async_nats::jetstream::new(client.clone());

let stream = context
.create_stream(async_nats::jetstream::stream::Config {
name: "stream".to_string(),
..Default::default()
})
.await
.unwrap();

let consumer = stream
.create_consumer(async_nats::jetstream::consumer::push::Config {
deliver_subject: "deliver".to_string(),
max_deliver: 30,
ack_policy: AckPolicy::Explicit,
ack_wait: Duration::from_secs(5),
..Default::default()
})
.await
.unwrap();

context
.publish("stream".to_string(), "data".into())
.await
.unwrap()
.await
.unwrap();

let mut messages = consumer.messages().await.unwrap();
let message = messages.next().await.unwrap().unwrap();

// Send NAK with much shorter duration.
message
.ack_with(AckKind::Nak(Some(Duration::from_millis(1000))))
.await
.unwrap();

// Check if we get a redelivery in that shortened duration.
let message = tokio::time::timeout(Duration::from_secs(3), messages.next())
.await
.unwrap()
.unwrap()
.unwrap();

// Send NAK with duration longer than `ack_wait` set on the consumer.
message
.ack_with(AckKind::Nak(Some(Duration::from_secs(10))))
.await
.unwrap();

// Expect it to timeout, but in time longer than consumer `ack_wait`.
tokio::time::timeout(Duration::from_secs(7), messages.next())
.await
.unwrap_err();
}
}

0 comments on commit ffd41ac

Please sign in to comment.