mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-06 21:33:14 +00:00
73bde843d6
* refactor(common): introduce rustfs-data-usage core crate * refactor(concurrency): migrate workers crate into concurrency * refactor(crypto): migrate appauth token APIs into crypto * fix docs urls * remove unused crate * refactor(data-usage): switch consumers to rustfs-data-usage * chore(fmt): apply cargo fmt and lockfile sync * refactor(common): remove data_usage compatibility re-export * refactor(capacity): move capacity_scope to object-capacity * refactor(io-metrics): relocate internode metrics from common * refactor(common): decouple scanner report from madmin * chore(fmt): normalize import ordering after pre-commit * refactor(s3): split s3 types and ops crates * refactor(s3): centralize event version and safe parsing * refactor(s3): add op-event compatibility guardrails * refactor(s3): add runtime op-event mismatch observability * refactor(s3): extract delete event mapping helper * refactor(s3): extract put event mapping helper * refactor(s3): consolidate remaining event semantic helpers * refactor(s3): add op-event coverage checks and observability alerts * refactor(s3-ops): consolidate op-event semantic mapping * refactor(scanner): remove last_minute wrapper module * refactor(scanner): consolidate duplicated data usage models
222 lines
8.0 KiB
Rust
222 lines
8.0 KiB
Rust
// Copyright 2024 RustFS Team
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
//! Integration tests for the AMQP notification target.
|
|
//!
|
|
//! These tests are ignored because they require a running RabbitMQ-compatible
|
|
//! AMQP 0-9-1 broker. To run locally:
|
|
//!
|
|
//! ```bash
|
|
//! docker run -d --name rustfs-rabbitmq -p 5672:5672 rabbitmq:3
|
|
//! cargo test -p rustfs-targets --test amqp_integration -- --ignored
|
|
//! ```
|
|
//!
|
|
//! Override the broker URL with `RUSTFS_TEST_AMQP_URL`.
|
|
|
|
use lapin::{
|
|
BasicProperties, Connection, ConnectionProperties,
|
|
options::{BasicAckOptions, BasicGetOptions, QueueBindOptions, QueueDeclareOptions, QueueDeleteOptions},
|
|
types::FieldTable,
|
|
};
|
|
use rustfs_s3_types::EventName;
|
|
use rustfs_targets::Target;
|
|
use rustfs_targets::check_amqp_broker_available;
|
|
use rustfs_targets::target::EntityTarget;
|
|
use rustfs_targets::target::TargetType;
|
|
use rustfs_targets::target::amqp::{AMQPArgs, AMQPTarget};
|
|
use serde_json::Value;
|
|
use std::sync::Arc;
|
|
use uuid::Uuid;
|
|
|
|
fn broker_url() -> String {
|
|
std::env::var("RUSTFS_TEST_AMQP_URL").unwrap_or_else(|_| "amqp://guest:guest@127.0.0.1:5672/%2f".to_string())
|
|
}
|
|
|
|
fn test_args(routing_key: &str) -> AMQPArgs {
|
|
AMQPArgs {
|
|
enable: true,
|
|
url: broker_url().parse().expect("valid AMQP URL"),
|
|
exchange: "amq.topic".to_string(),
|
|
routing_key: routing_key.to_string(),
|
|
mandatory: true,
|
|
persistent: true,
|
|
username: String::new(),
|
|
password: String::new(),
|
|
tls_ca: String::new(),
|
|
tls_client_cert: String::new(),
|
|
tls_client_key: String::new(),
|
|
queue_dir: String::new(),
|
|
queue_limit: 100_000,
|
|
target_type: TargetType::NotifyEvent,
|
|
}
|
|
}
|
|
|
|
fn entity_for(bucket: &str, object: &str) -> Arc<EntityTarget<serde_json::Value>> {
|
|
Arc::new(EntityTarget {
|
|
bucket_name: bucket.to_string(),
|
|
object_name: object.to_string(),
|
|
event_name: EventName::ObjectCreatedPut,
|
|
data: serde_json::json!({"bucket": bucket, "object": object}),
|
|
})
|
|
}
|
|
|
|
async fn bind_queue(queue: &str, routing_key: &str) -> lapin::Channel {
|
|
let conn = Connection::connect(&broker_url(), ConnectionProperties::default())
|
|
.await
|
|
.expect("connect to AMQP broker");
|
|
let channel = conn.create_channel().await.expect("create channel");
|
|
channel
|
|
.queue_declare(
|
|
queue.into(),
|
|
QueueDeclareOptions {
|
|
durable: false,
|
|
exclusive: true,
|
|
auto_delete: true,
|
|
..QueueDeclareOptions::default()
|
|
},
|
|
FieldTable::default(),
|
|
)
|
|
.await
|
|
.expect("declare queue");
|
|
channel
|
|
.queue_bind(
|
|
queue.into(),
|
|
"amq.topic".into(),
|
|
routing_key.into(),
|
|
QueueBindOptions::default(),
|
|
FieldTable::default(),
|
|
)
|
|
.await
|
|
.expect("bind queue");
|
|
channel
|
|
}
|
|
|
|
async fn read_one(channel: &lapin::Channel, queue: &str) -> (Value, BasicProperties) {
|
|
let msg = tokio::time::timeout(std::time::Duration::from_secs(5), async {
|
|
loop {
|
|
if let Some(msg) = channel
|
|
.basic_get(queue.into(), BasicGetOptions::default())
|
|
.await
|
|
.expect("basic_get")
|
|
{
|
|
msg.ack(BasicAckOptions::default()).await.expect("ack message");
|
|
break msg;
|
|
}
|
|
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
|
|
}
|
|
})
|
|
.await
|
|
.expect("message should arrive");
|
|
|
|
let properties = msg.properties.clone();
|
|
let payload = serde_json::from_slice(&msg.data).expect("message payload should be JSON");
|
|
(payload, properties)
|
|
}
|
|
|
|
#[tokio::test]
|
|
#[ignore = "requires running RabbitMQ-compatible AMQP broker"]
|
|
async fn test_check_amqp_broker_available() {
|
|
check_amqp_broker_available(&test_args("rustfs.check"))
|
|
.await
|
|
.expect("broker check should succeed");
|
|
}
|
|
|
|
#[tokio::test]
|
|
#[ignore = "requires running RabbitMQ-compatible AMQP broker"]
|
|
async fn test_direct_publish_delivers_json_payload() {
|
|
let routing_key = format!("rustfs.test.{}", Uuid::new_v4().simple());
|
|
let queue = format!("rustfs-test-{}", Uuid::new_v4().simple());
|
|
let channel = bind_queue(&queue, &routing_key).await;
|
|
let target = AMQPTarget::new("direct".to_string(), test_args(&routing_key)).expect("construct AMQP target");
|
|
|
|
target
|
|
.save(entity_for("bucket1", "object-A"))
|
|
.await
|
|
.expect("publish should succeed");
|
|
|
|
let (payload, properties) = read_one(&channel, &queue).await;
|
|
assert_eq!(payload["Key"], "bucket1/object-A");
|
|
assert_eq!(payload["Records"][0]["data"]["bucket"], "bucket1");
|
|
assert_eq!(properties.content_type().as_ref().map(|s| s.as_str()), Some("application/json"));
|
|
assert_eq!(*properties.delivery_mode(), Some(2));
|
|
|
|
channel
|
|
.queue_delete(queue.into(), QueueDeleteOptions::default())
|
|
.await
|
|
.expect("delete queue");
|
|
}
|
|
|
|
#[tokio::test]
|
|
#[ignore = "requires running RabbitMQ-compatible AMQP broker"]
|
|
async fn test_publish_reconnects_after_close() {
|
|
let routing_key = format!("rustfs.reconnect.{}", Uuid::new_v4().simple());
|
|
let queue = format!("rustfs-test-{}", Uuid::new_v4().simple());
|
|
let channel = bind_queue(&queue, &routing_key).await;
|
|
let target = AMQPTarget::new("reconnect".to_string(), test_args(&routing_key)).expect("construct AMQP target");
|
|
|
|
target
|
|
.save(entity_for("bucket1", "object-before-close"))
|
|
.await
|
|
.expect("initial publish should succeed");
|
|
let (payload, _) = read_one(&channel, &queue).await;
|
|
assert_eq!(payload["Key"], "bucket1/object-before-close");
|
|
|
|
target.close().await.expect("close cached AMQP connection");
|
|
|
|
target
|
|
.save(entity_for("bucket1", "object-after-close"))
|
|
.await
|
|
.expect("publish should reconnect after close");
|
|
let (payload, _) = read_one(&channel, &queue).await;
|
|
assert_eq!(payload["Key"], "bucket1/object-after-close");
|
|
|
|
channel
|
|
.queue_delete(queue.into(), QueueDeleteOptions::default())
|
|
.await
|
|
.expect("delete queue");
|
|
}
|
|
|
|
#[tokio::test]
|
|
#[ignore = "requires running RabbitMQ-compatible AMQP broker"]
|
|
async fn test_queue_replay_delivers_and_removes_stored_payload() {
|
|
let routing_key = format!("rustfs.replay.{}", Uuid::new_v4().simple());
|
|
let queue = format!("rustfs-test-{}", Uuid::new_v4().simple());
|
|
let channel = bind_queue(&queue, &routing_key).await;
|
|
let queue_dir = std::env::temp_dir().join(format!("rustfs-amqp-integration-{}", Uuid::new_v4()));
|
|
let mut args = test_args(&routing_key);
|
|
args.queue_dir = queue_dir.to_string_lossy().to_string();
|
|
let target = AMQPTarget::new("queued".to_string(), args.clone()).expect("construct AMQP target");
|
|
|
|
target
|
|
.save(entity_for("bucket1", "object-B"))
|
|
.await
|
|
.expect("store-backed save should queue");
|
|
assert_eq!(target.delivery_snapshot().queue_length, 1);
|
|
|
|
let key = target.store().expect("store configured").list()[0].clone();
|
|
target.send_from_store(key).await.expect("replay should publish and delete");
|
|
|
|
let (payload, properties) = read_one(&channel, &queue).await;
|
|
assert_eq!(payload["Key"], "bucket1/object-B");
|
|
assert_eq!(properties.content_type().as_ref().map(|s| s.as_str()), Some("application/json"));
|
|
assert_eq!(*properties.delivery_mode(), Some(2));
|
|
assert_eq!(target.delivery_snapshot().queue_length, 0);
|
|
|
|
channel
|
|
.queue_delete(queue.into(), QueueDeleteOptions::default())
|
|
.await
|
|
.expect("delete queue");
|
|
let _ = std::fs::remove_dir_all(args.queue_dir);
|
|
}
|