#![allow(dead_code)]
use mq_bridge::endpoints::object_store::{ObjectStoreConsumer, ObjectStorePublisher};
use mq_bridge::models::{FileFormat, ObjectStoreConfig};
use mq_bridge::test_utils::{run_pipeline_test, run_test_with_docker, setup_logging};
use mq_bridge::traits::{MessageConsumer, MessagePublisher};
use mq_bridge::CanonicalMessage;
const BUCKET: &str = "mqb-object-store-test";
const ENDPOINT: &str = "http://localhost:4566";
fn set_s3_env() {
std::env::set_var("AWS_ACCESS_KEY_ID", "test");
std::env::set_var("AWS_SECRET_ACCESS_KEY", "test");
std::env::set_var("AWS_REGION", "us-east-1");
std::env::set_var("AWS_ENDPOINT", ENDPOINT);
std::env::set_var("AWS_ALLOW_HTTP", "true");
}
async fn ensure_bucket() {
let client = reqwest::Client::new();
client
.put(format!("{ENDPOINT}/{BUCKET}"))
.send()
.await
.expect("create bucket request")
.error_for_status()
.expect("create bucket response");
}
fn config(prefix: &str, checkpoint_store: Option<String>) -> ObjectStoreConfig {
ObjectStoreConfig {
url: format!("s3://{BUCKET}/{prefix}"),
format: FileFormat::Normal,
cursor_id: checkpoint_store.as_ref().map(|_| "resume-test".to_string()),
checkpoint_store,
polling_interval_ms: Some(100),
..Default::default()
}
}
fn json_msg(n: i64) -> CanonicalMessage {
CanonicalMessage::new(
serde_json::to_vec(&serde_json::json!({ "n": n })).unwrap(),
None,
)
}
const CONFIG_YAML: &str = r#"
routes:
memory_to_object_store:
concurrency: 4
batch_size: 64
input:
memory: { topic: "obj-test-in" }
output:
object_store:
url: "s3://mqb-object-store-test/{prefix}"
format: normal
object_store_to_memory:
concurrency: 1
batch_size: 64
input:
object_store:
url: "s3://mqb-object-store-test/{prefix}"
format: normal
polling_interval_ms: 100
output:
memory: { topic: "obj-test-out", capacity: {out_capacity} }
"#;
pub async fn test_object_store_pipeline() {
setup_logging();
run_test_with_docker(
"tests/integration/docker-compose/object_store.yml",
|| async {
set_s3_env();
ensure_bucket().await;
let prefix = format!("pipeline/{}", fast_uuid_v7::gen_id_str());
let config_yaml = CONFIG_YAML
.replace("{prefix}", &prefix)
.replace("{out_capacity}", "10000");
run_pipeline_test("object_store", &config_yaml).await;
},
)
.await;
}
pub async fn test_object_store_resume() {
setup_logging();
run_test_with_docker(
"tests/integration/docker-compose/object_store.yml",
|| async {
set_s3_env();
ensure_bucket().await;
let prefix = format!("resume/{}", fast_uuid_v7::gen_id_str());
let ckpt_dir =
std::env::temp_dir().join(format!("mqb_obj_ckpt_{}", fast_uuid_v7::gen_id_str()));
let ckpt = format!("file://{}/cursor.json", ckpt_dir.display());
let publisher = ObjectStorePublisher::new(&config(&prefix, None))
.await
.unwrap();
for n in 0..3 {
publisher.send_batch(vec![json_msg(n)]).await.unwrap();
}
{
let mut consumer = ObjectStoreConsumer::new(&config(&prefix, Some(ckpt.clone())))
.await
.unwrap();
let batch = wait_for_batch(&mut consumer).await;
assert_eq!(batch.messages.len(), 1, "one record per object");
let first_payload = batch.messages[0].payload.clone();
(batch.commit)(vec![mq_bridge::traits::MessageDisposition::Ack; 1])
.await
.unwrap();
let mut resumed = ObjectStoreConsumer::new(&config(&prefix, Some(ckpt.clone())))
.await
.unwrap();
let next = wait_for_batch(&mut resumed).await;
assert_eq!(next.messages.len(), 1);
assert_ne!(
next.messages[0].payload, first_payload,
"resumed consumer must not re-read the acked object"
);
}
tokio::fs::remove_dir_all(&ckpt_dir).await.ok();
},
)
.await;
}
async fn wait_for_batch(consumer: &mut ObjectStoreConsumer) -> mq_bridge::traits::ReceivedBatch {
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(30);
loop {
let batch = consumer.receive_batch(64).await.unwrap();
if !batch.messages.is_empty() {
return batch;
}
assert!(
std::time::Instant::now() < deadline,
"timed out waiting for a non-empty batch"
);
}
}