-
Notifications
You must be signed in to change notification settings - Fork 2.2k
enhancement(mqtt source): support end-to-end acknowledgements #25666
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| The `mqtt` source now supports end-to-end acknowledgements. When `acknowledgements` is enabled, the QoS 1 `PUBACK` for an incoming publish is deferred until the resulting events have been delivered to all connected sinks. Because the source already uses `clean_session = false` and subscribes at QoS `AtLeastOnce`, an unacknowledged message is redelivered by the broker after a crash or reconnect, providing at-least-once delivery when the broker resumes the persistent session. A stable `client_id` must be configured when `acknowledgements` is enabled, so the session (and its unacknowledged messages) can be resumed after a restart. Publishes delivered by the broker with QoS 0 cannot be acknowledged end-to-end and are forwarded with a warning instead of being registered for deferred acknowledgement. The source also warns if the broker starts a new session while acknowledgements are enabled, since unacknowledged messages from any previous session for that client ID will not be redelivered. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -11,7 +11,7 @@ use crate::{ | |
| SourceSender, | ||
| common::mqtt::MqttCommonConfig, | ||
| config::{SourceConfig, SourceContext, log_schema}, | ||
| event::Event, | ||
| event::{Event, EventStatus}, | ||
| serde::OneOrMany, | ||
| sources::mqtt::MqttSourceConfig, | ||
| test_util::{ | ||
|
|
@@ -40,9 +40,21 @@ async fn send_test_events(client: &AsyncClient, topic: &str, messages: &Vec<Stri | |
| } | ||
| } | ||
|
|
||
| fn message_body(event: &Event) -> String { | ||
| event | ||
| .as_log() | ||
| .get(log_schema().message_key_target_path().unwrap()) | ||
| .unwrap() | ||
| .to_string_lossy() | ||
| .into_owned() | ||
| } | ||
|
|
||
| async fn get_mqtt_client() -> AsyncClient { | ||
| // Unique client ID per producer: brokers that strictly enforce client-ID | ||
| // uniqueness (e.g. RabbitMQ) otherwise kick a previous connection when tests | ||
| // run concurrently, which manifests as spurious publish timeouts. | ||
| let mut mqtt_options = MqttOptions::new( | ||
| "integration-test-producer", | ||
| format!("integration-test-producer-{}", random_string(6)), | ||
| mqtt_broker_address(), | ||
| mqtt_broker_port(), | ||
| ); | ||
|
|
@@ -118,6 +130,99 @@ async fn mqtt_one_topic_happy() { | |
| .await; | ||
| } | ||
|
|
||
| /// With end-to-end acknowledgements enabled, a message that is received but not | ||
| /// successfully delivered (the sink rejects it) must not be acknowledged to the | ||
| /// broker, so the broker redelivers it. This proves the at-least-once guarantee | ||
| /// added by the `acknowledgements` option: no data is lost when a downstream | ||
| /// failure or crash occurs before the write is confirmed. | ||
| #[tokio::test] | ||
| async fn mqtt_redelivers_unacknowledged_messages() { | ||
| trace_init(); | ||
|
|
||
| let topic = "source-redelivery-test"; | ||
| // A stable client ID so the second connection resumes the same persistent | ||
| // session (the source sets `clean_session = false`); the broker then | ||
| // redelivers any in-flight QoS 1 message that was never acknowledged. | ||
| let client_id = format!("sourceAckTest{}", random_string(6)); | ||
| let message = random_string(32); | ||
|
|
||
| let make_config = || MqttSourceConfig { | ||
| common: MqttCommonConfig { | ||
| host: mqtt_broker_address(), | ||
| port: mqtt_broker_port(), | ||
| client_id: Some(client_id.clone()), | ||
| ..Default::default() | ||
| }, | ||
| topic: OneOrMany::One(topic.to_owned()), | ||
| acknowledgements: true.into(), | ||
| ..MqttSourceConfig::default() | ||
| }; | ||
|
|
||
| // Phase 1: the first instance subscribes (creating the persistent session) | ||
| // and receives the message, but its sink rejects every event, so the source | ||
| // never sends a PUBACK. | ||
| let (tx1, mut rx1) = SourceSender::new_test_finalize(EventStatus::Rejected); | ||
| let config1 = make_config(); | ||
| let source1 = tokio::spawn(async move { | ||
| config1 | ||
| .build(SourceContext::new_test(tx1, None)) | ||
| .await | ||
| .unwrap() | ||
| .await | ||
| .unwrap() | ||
| }); | ||
|
|
||
| // Wait for the subscription to be established before publishing. | ||
| tokio::time::sleep(Duration::from_millis(500)).await; | ||
|
|
||
| let producer = get_mqtt_client().await; | ||
| producer | ||
| .publish(topic, QoS::AtLeastOnce, false, message.as_bytes()) | ||
| .await | ||
| .unwrap(); | ||
|
|
||
| // The first instance must actually receive (and reject) the message so that | ||
| // it remains unacknowledged in the broker. | ||
| let first = timeout(Duration::from_secs(5), rx1.next()) | ||
| .await | ||
| .expect("timed out waiting for first delivery") | ||
| .expect("source stream ended unexpectedly"); | ||
| assert_eq!(message_body(&first), message); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
In this test, Useful? React with 👍 / 👎.
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed in 3974323. While here, the deferred-ack handling is extracted into a small |
||
| drop(first); | ||
|
|
||
| // Give the source a moment to observe the rejected status (and therefore | ||
| // skip the ack), then drop the connection without acknowledging. | ||
| tokio::time::sleep(Duration::from_millis(200)).await; | ||
| source1.abort(); | ||
| let _ = source1.await; | ||
|
|
||
| // Phase 2: a new instance resumes the same session; the broker must | ||
| // redeliver the unacknowledged message. | ||
| let (tx2, mut rx2) = SourceSender::new_test(); | ||
| let config2 = make_config(); | ||
| let source2 = tokio::spawn(async move { | ||
| config2 | ||
| .build(SourceContext::new_test(tx2, None)) | ||
| .await | ||
| .unwrap() | ||
| .await | ||
| .unwrap() | ||
| }); | ||
|
|
||
| let redelivered = timeout(Duration::from_secs(10), rx2.next()) | ||
| .await | ||
| .expect("timed out waiting for redelivery: the message was lost") | ||
| .expect("source stream ended unexpectedly"); | ||
| assert_eq!( | ||
| message_body(&redelivered), | ||
| message, | ||
| "redelivered message did not match the original" | ||
| ); | ||
|
|
||
| source2.abort(); | ||
| let _ = source2.await; | ||
| } | ||
|
|
||
| #[tokio::test] | ||
| async fn mqtt_many_topics_happy() { | ||
| trace_init(); | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When acknowledgements are enabled but
common.client_idis omitted, the code above still creates a randomvectorSource...ID and this block turns on manual ACKs. MQTT persistent sessions/redelivery are keyed by client ID, so after Vector crashes or restarts before a PUBACK, the next process generates a different ID and won't resume the session that owns the unacknowledged QoS 1 publish. That breaks the advertised at-least-once behavior for the default config; require an explicit stable client ID, or avoid enabling manual ACKs, when acknowledgements are active.Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good catch — fixed in e1ec93b.
build_connectornow returns a configuration error whenacknowledgementsis enabled and noclient_idis set, since a generated ID would orphan the session's unacknowledged messages on restart. Added a unit test (acknowledgements_require_a_stable_client_id).