refactor(targets): unify queue/connectivity handling and coverage (#2953)

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: marshawcoco <marshawcoco@gmail.com>
This commit is contained in:
houseme
2026-05-14 12:31:23 +08:00
committed by GitHub
parent bdb98598d2
commit 81754d80b3
64 changed files with 9613 additions and 2800 deletions
+19 -50
View File
@@ -19,13 +19,14 @@
//! body through `send_raw_from_store`.
use crate::{
StoreError, Target, TargetLog,
StoreError, Target,
arn::TargetID,
error::TargetError,
store::{Key, QueueStore, Store},
store::{Key, Store},
target::{
ChannelTargetType, EntityTarget, QueuedPayload, QueuedPayloadMeta, TargetDeliveryCounters, TargetDeliverySnapshot,
TargetType, queue_store_subdir_name,
TargetType, build_queued_payload_with_records, is_connectivity_error, open_target_queue_store,
persist_queued_payload_to_store,
},
};
use async_trait::async_trait;
@@ -39,11 +40,11 @@ use rustfs_config::{AMQP_TLS_CA, AMQP_TLS_CLIENT_CERT, AMQP_TLS_CLIENT_KEY};
use serde::Serialize;
use serde::de::DeserializeOwned;
use std::fmt;
use std::path::{Path, PathBuf};
use std::path::Path;
use std::sync::Arc;
use std::time::Duration;
use tokio::sync::Mutex as AsyncMutex;
use tracing::{error, info, instrument, warn};
use tracing::{info, instrument, warn};
use url::Url;
#[derive(Clone)]
@@ -315,22 +316,14 @@ where
pub fn new(id: String, args: AMQPArgs) -> Result<Self, TargetError> {
args.validate()?;
let target_id = TargetID::new(id, ChannelTargetType::Amqp.as_str().to_string());
let queue_store = if !args.queue_dir.is_empty() {
let base_path = PathBuf::from(&args.queue_dir);
let specific_queue_path = base_path.join(queue_store_subdir_name(ChannelTargetType::Amqp.as_str(), &target_id.id));
let extension = match args.target_type {
TargetType::AuditLog => rustfs_config::audit::AUDIT_STORE_EXTENSION,
TargetType::NotifyEvent => rustfs_config::notify::NOTIFY_STORE_EXTENSION,
};
let store = QueueStore::<QueuedPayload>::new(specific_queue_path, args.queue_limit, extension);
if let Err(e) = store.open() {
error!(target_id = %target_id, error = %e, "Failed to open store for AMQP target");
return Err(TargetError::Storage(format!("{e}")));
}
Some(Box::new(store) as Box<dyn Store<QueuedPayload, Error = StoreError, Key = Key> + Send + Sync>)
} else {
None
};
let queue_store = open_target_queue_store(
&args.queue_dir,
args.queue_limit,
args.target_type,
ChannelTargetType::Amqp.as_str(),
&target_id,
"Failed to open store for AMQP target",
)?;
Ok(Self {
id: target_id,
@@ -344,22 +337,7 @@ where
}
fn build_queued_payload(&self, event: &EntityTarget<E>) -> Result<QueuedPayload, TargetError> {
let object_name = crate::target::decode_object_name(&event.object_name)?;
let key = format!("{}/{}", event.bucket_name, object_name);
let log = TargetLog {
event_name: event.event_name,
key,
records: vec![event.clone()],
};
let body = serde_json::to_vec(&log).map_err(|e| TargetError::Serialization(format!("Failed to serialize event: {e}")))?;
let meta = QueuedPayloadMeta::new(
event.event_name,
event.bucket_name.clone(),
event.object_name.clone(),
"application/json",
body.len(),
);
Ok(QueuedPayload::new(meta, body))
build_queued_payload_with_records(event, vec![event.clone()])
}
async fn get_or_connect(&self) -> Result<Arc<AMQPConnection>, TargetError> {
@@ -453,16 +431,9 @@ where
};
if let Some(store) = &self.store {
let encoded = match queued.encode() {
Ok(encoded) => encoded,
Err(err) => {
self.delivery_counters.record_final_failure();
return Err(TargetError::Storage(format!("Failed to encode queued payload: {err}")));
}
};
if let Err(e) = store.put_raw(&encoded) {
if let Err(e) = persist_queued_payload_to_store(store.as_ref(), &queued) {
self.delivery_counters.record_final_failure();
return Err(TargetError::Storage(format!("Failed to save event to store: {e}")));
return Err(e);
}
Ok(())
} else {
@@ -505,10 +476,7 @@ where
}
match self.get_or_connect().await {
Ok(_) => Ok(()),
Err(err)
if self.store.is_some()
&& matches!(err, TargetError::Network(_) | TargetError::Timeout(_) | TargetError::NotConnected) =>
{
Err(err) if self.store.is_some() && is_connectivity_error(&err) => {
warn!(target_id = %self.id, error = %err, "AMQP init failed; events will buffer in store");
Ok(())
}
@@ -535,6 +503,7 @@ mod tests {
use super::*;
use rustfs_s3_common::EventName;
use serde_json::json;
use std::path::PathBuf;
use std::sync::Arc;
use uuid::Uuid;
+17 -61
View File
@@ -13,23 +13,22 @@
// limitations under the License.
use crate::{
StoreError, Target, TargetLog,
StoreError, Target,
arn::TargetID,
error::TargetError,
store::{Key, QueueStore, Store},
store::{Key, Store},
target::{
ChannelTargetType, EntityTarget, QueuedPayload, QueuedPayloadMeta, TargetDeliveryCounters, TargetDeliverySnapshot,
TargetType, queue_store_subdir_name,
TargetType, build_queued_payload, invalidate_cache_on_connectivity_error, open_target_queue_store,
persist_queued_payload_to_store,
},
};
use async_trait::async_trait;
use rustfs_config::audit::AUDIT_STORE_EXTENSION;
use rustfs_config::notify::NOTIFY_STORE_EXTENSION;
use rustfs_kafka_async::error::{ConnectionError, Error as KafkaError};
use rustfs_kafka_async::{AsyncProducer, AsyncProducerConfig, Record, RequiredAcks, SecurityConfig};
use serde::Serialize;
use serde::de::DeserializeOwned;
use std::{marker::PhantomData, path::PathBuf, sync::Arc, time::Duration};
use std::{marker::PhantomData, sync::Arc, time::Duration};
use tokio::sync::Mutex;
use tracing::{debug, error, info, instrument, warn};
@@ -123,10 +122,6 @@ where
}
}
fn is_connection_error(err: &TargetError) -> bool {
matches!(err, TargetError::NotConnected | TargetError::Timeout(_) | TargetError::Network(_))
}
/// Creates a new KafkaTarget
#[instrument(skip(args), fields(target_id = %id))]
pub fn new(id: String, args: KafkaArgs) -> Result<Self, TargetError> {
@@ -134,25 +129,14 @@ where
let target_id = TargetID::new(id, ChannelTargetType::Kafka.as_str().to_string());
let queue_store = if !args.queue_dir.is_empty() {
let queue_dir =
PathBuf::from(&args.queue_dir).join(queue_store_subdir_name(ChannelTargetType::Kafka.as_str(), &target_id.id));
let extension = match args.target_type {
TargetType::AuditLog => AUDIT_STORE_EXTENSION,
TargetType::NotifyEvent => NOTIFY_STORE_EXTENSION,
};
let store = QueueStore::<QueuedPayload>::new(queue_dir, args.queue_limit, extension);
if let Err(e) = store.open() {
error!("Failed to open store for Kafka target {}: {}", target_id.id, e);
return Err(TargetError::Storage(format!("{e}")));
}
Some(Box::new(store) as Box<dyn Store<QueuedPayload, Error = StoreError, Key = Key> + Send + Sync>)
} else {
None
};
let queue_store = open_target_queue_store(
&args.queue_dir,
args.queue_limit,
args.target_type,
ChannelTargetType::Kafka.as_str(),
&target_id,
"Failed to open store for Kafka target",
)?;
info!(target_id = %target_id.id, "Kafka target created");
Ok(KafkaTarget {
@@ -211,26 +195,7 @@ where
/// Serializes the event and builds a QueuedPayload
fn build_queued_payload(&self, event: &EntityTarget<E>) -> Result<QueuedPayload, TargetError> {
let object_name = crate::target::decode_object_name(&event.object_name)?;
let key = format!("{}/{}", event.bucket_name, object_name);
let log = TargetLog {
event_name: event.event_name,
key,
records: vec![event.data.clone()],
};
let body = serde_json::to_vec(&log).map_err(|e| TargetError::Serialization(format!("Failed to serialize event: {e}")))?;
let meta = QueuedPayloadMeta::new(
event.event_name,
event.bucket_name.clone(),
event.object_name.clone(),
"application/json",
body.len(),
);
Ok(QueuedPayload::new(meta, body))
build_queued_payload(event)
}
/// Sends the raw body to Kafka
@@ -249,9 +214,7 @@ where
if let Err(err) = producer.send(&Record::from_value(&self.args.topic, body.as_slice())).await {
let mapped = Self::map_kafka_error(err, "Failed to send message to Kafka");
if Self::is_connection_error(&mapped) {
self.invalidate_cached_producer().await;
}
invalidate_cache_on_connectivity_error(&mapped, || self.invalidate_cached_producer()).await;
return Err(mapped);
}
@@ -297,16 +260,9 @@ where
};
if let Some(store) = &self.store {
let encoded = match queued.encode() {
Ok(encoded) => encoded,
Err(err) => {
self.delivery_counters.record_final_failure();
return Err(TargetError::Storage(format!("Failed to encode queued payload: {err}")));
}
};
if let Err(e) = store.put_raw(&encoded) {
if let Err(e) = persist_queued_payload_to_store(store.as_ref(), &queued) {
self.delivery_counters.record_final_failure();
return Err(TargetError::Storage(format!("Failed to save event to store: {e}")));
return Err(e);
}
debug!("Event saved to store for Kafka target: {}", self.id);
Ok(())
+297 -3
View File
@@ -13,15 +13,17 @@
// limitations under the License.
use crate::arn::TargetID;
use crate::store::{Key, Store};
use crate::store::{Key, QueueStore, Store};
use crate::{StoreError, TargetError, TargetLog};
use async_trait::async_trait;
use rustfs_s3_common::EventName;
use serde::de::DeserializeOwned;
use serde::{Deserialize, Serialize};
use std::fmt::Formatter;
use std::future::Future;
use std::path::PathBuf;
use std::sync::Arc;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::time::{SystemTime, UNIX_EPOCH};
use tracing::{debug, warn};
@@ -50,6 +52,8 @@ pub struct TargetDeliveryCounters {
total_messages: AtomicU64,
}
pub(crate) type BoxedQueuedStore = Box<dyn Store<QueuedPayload, Error = StoreError, Key = Key> + Send + Sync>;
impl TargetDeliveryCounters {
#[inline]
pub fn record_success(&self) {
@@ -408,6 +412,17 @@ pub fn decode_object_name(encoded: &str) -> Result<String, TargetError> {
pub(crate) fn build_queued_payload<E>(event: &EntityTarget<E>) -> Result<QueuedPayload, TargetError>
where
E: Send + Sync + 'static + Clone + Serialize + DeserializeOwned,
{
build_queued_payload_with_records(event, vec![event.data.clone()])
}
pub(crate) fn build_queued_payload_with_records<E, R>(
event: &EntityTarget<E>,
records: Vec<R>,
) -> Result<QueuedPayload, TargetError>
where
E: Send + Sync + 'static + Clone + Serialize + DeserializeOwned,
R: Serialize,
{
let object_name = decode_object_name(&event.object_name)?;
let key = format!("{}/{}", event.bucket_name, object_name);
@@ -415,7 +430,7 @@ where
let log = TargetLog {
event_name: event.event_name,
key,
records: vec![event.data.clone()],
records,
};
let body = serde_json::to_vec(&log).map_err(|err| TargetError::Serialization(format!("Failed to serialize event: {err}")))?;
@@ -430,6 +445,68 @@ where
Ok(QueuedPayload::new(meta, body))
}
pub(crate) fn open_target_queue_store(
queue_dir: &str,
queue_limit: u64,
target_type: TargetType,
target_type_label: &str,
target_id: &TargetID,
open_context: &str,
) -> Result<Option<BoxedQueuedStore>, TargetError> {
fn boxed_queue_store(store: QueueStore<QueuedPayload>) -> BoxedQueuedStore {
Box::new(store)
}
if queue_dir.is_empty() {
return Ok(None);
}
let queue_dir = PathBuf::from(queue_dir).join(queue_store_subdir_name(target_type_label, &target_id.id));
let extension = match target_type {
TargetType::AuditLog => rustfs_config::audit::AUDIT_STORE_EXTENSION,
TargetType::NotifyEvent => rustfs_config::notify::NOTIFY_STORE_EXTENSION,
};
let store = QueueStore::<QueuedPayload>::new(queue_dir, queue_limit, extension);
store
.open()
.map_err(|err| TargetError::Storage(format!("{open_context}: {err}")))?;
Ok(Some(boxed_queue_store(store)))
}
pub(crate) fn persist_queued_payload_to_store(
store: &(dyn Store<QueuedPayload, Error = StoreError, Key = Key> + Send + Sync),
queued: &QueuedPayload,
) -> Result<(), TargetError> {
let encoded = queued
.encode()
.map_err(|err| TargetError::Storage(format!("Failed to encode queued payload: {err}")))?;
store
.put_raw(&encoded)
.map(|_| ())
.map_err(|err| TargetError::Storage(format!("Failed to save event to store: {err}")))
}
pub(crate) fn is_connectivity_error(err: &TargetError) -> bool {
matches!(err, TargetError::NotConnected | TargetError::Timeout(_) | TargetError::Network(_))
}
pub(crate) async fn invalidate_cache_on_connectivity_error<F, Fut>(err: &TargetError, invalidate: F)
where
F: FnOnce() -> Fut,
Fut: Future<Output = ()>,
{
if is_connectivity_error(err) {
invalidate().await;
}
}
pub(crate) fn mark_target_disconnected_on_connectivity_error(connected: &AtomicBool, err: &TargetError) {
if is_connectivity_error(err) {
connected.store(false, Ordering::SeqCst);
}
}
pub(crate) fn delete_stored_payload(
store: &(dyn Store<QueuedPayload, Error = StoreError, Key = Key> + Send + Sync),
key: &Key,
@@ -457,6 +534,90 @@ pub(crate) fn ensure_rustls_provider_installed() {
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use std::sync::Mutex;
use uuid::Uuid;
#[derive(Clone)]
struct MockQueuedStore {
fail_put_raw: bool,
writes: Arc<Mutex<Vec<Vec<u8>>>>,
}
impl MockQueuedStore {
fn new(fail_put_raw: bool) -> Self {
Self {
fail_put_raw,
writes: Arc::new(Mutex::new(Vec::new())),
}
}
}
impl Store<QueuedPayload> for MockQueuedStore {
type Error = StoreError;
type Key = Key;
fn open(&self) -> Result<(), Self::Error> {
Ok(())
}
fn put(&self, _item: Arc<QueuedPayload>) -> Result<Self::Key, Self::Error> {
Err(StoreError::Internal("not implemented in mock".to_string()))
}
fn put_multiple(&self, _items: Vec<QueuedPayload>) -> Result<Self::Key, Self::Error> {
Err(StoreError::Internal("not implemented in mock".to_string()))
}
fn put_raw(&self, data: &[u8]) -> Result<Self::Key, Self::Error> {
if self.fail_put_raw {
return Err(StoreError::Internal("mock put_raw failed".to_string()));
}
self.writes.lock().expect("mock writes lock poisoned").push(data.to_vec());
Ok(Key {
name: "mock".to_string(),
extension: ".json".to_string(),
item_count: 1,
compress: false,
})
}
fn get(&self, _key: &Self::Key) -> Result<QueuedPayload, Self::Error> {
Err(StoreError::Internal("not implemented in mock".to_string()))
}
fn get_multiple(&self, _key: &Self::Key) -> Result<Vec<QueuedPayload>, Self::Error> {
Err(StoreError::Internal("not implemented in mock".to_string()))
}
fn get_raw(&self, _key: &Self::Key) -> Result<Vec<u8>, Self::Error> {
Err(StoreError::Internal("not implemented in mock".to_string()))
}
fn del(&self, _key: &Self::Key) -> Result<(), Self::Error> {
Err(StoreError::Internal("not implemented in mock".to_string()))
}
fn delete(&self) -> Result<(), Self::Error> {
Err(StoreError::Internal("not implemented in mock".to_string()))
}
fn list(&self) -> Vec<Self::Key> {
Vec::new()
}
fn len(&self) -> usize {
0
}
fn is_empty(&self) -> bool {
true
}
fn boxed_clone(&self) -> Box<dyn Store<QueuedPayload, Error = Self::Error, Key = Self::Key> + Send + Sync> {
Box::new(self.clone())
}
}
#[test]
fn channel_target_type_amqp_uses_runtime_name() {
@@ -501,6 +662,139 @@ mod tests {
assert_eq!(value["Records"][0], "payload-data");
}
#[test]
fn build_queued_payload_with_records_preserves_custom_record_shape() {
let event = EntityTarget {
object_name: "object.txt".to_string(),
bucket_name: "bucket-a".to_string(),
event_name: EventName::ObjectCreatedPut,
data: "ignored".to_string(),
};
let payload = build_queued_payload_with_records(&event, vec![event.clone()]).unwrap();
let value: serde_json::Value = serde_json::from_slice(&payload.body).unwrap();
assert_eq!(value["Records"][0]["bucket_name"], "bucket-a");
assert_eq!(value["Records"][0]["object_name"], "object.txt");
assert_eq!(value["Records"][0]["data"], "ignored");
}
#[test]
fn open_target_queue_store_returns_none_when_queue_dir_empty() {
let target_id = TargetID::new("target-a".to_string(), ChannelTargetType::Webhook.as_str().to_string());
let store = open_target_queue_store(
"",
100,
TargetType::NotifyEvent,
ChannelTargetType::Webhook.as_str(),
&target_id,
"open failed",
)
.unwrap();
assert!(store.is_none());
}
#[test]
fn open_target_queue_store_adds_context_on_open_error() {
let base = std::env::temp_dir().join(format!("rustfs-target-store-file-{}", Uuid::new_v4()));
fs::write(&base, b"not-a-directory").expect("failed to create file base");
let target_id = TargetID::new("target-a".to_string(), ChannelTargetType::Kafka.as_str().to_string());
let result = open_target_queue_store(
base.to_str().unwrap(),
100,
TargetType::NotifyEvent,
ChannelTargetType::Kafka.as_str(),
&target_id,
"custom open context",
);
match result {
Ok(_) => panic!("expected open_target_queue_store to fail on file base path"),
Err(err) => assert!(err.to_string().contains("custom open context")),
}
let _ = fs::remove_file(base);
}
#[test]
fn persist_queued_payload_to_store_writes_encoded_payload() {
let store = MockQueuedStore::new(false);
let meta = QueuedPayloadMeta::new(
EventName::ObjectCreatedPut,
"bucket-a".to_string(),
"obj.txt".to_string(),
"application/json",
7,
);
let queued = QueuedPayload::new(meta, br#"{"x":1}"#.to_vec());
persist_queued_payload_to_store(&store, &queued).unwrap();
let writes = store.writes.lock().expect("mock writes lock poisoned");
assert_eq!(writes.len(), 1);
let decoded = QueuedPayload::decode(&writes[0]).unwrap();
assert_eq!(decoded.body, br#"{"x":1}"#);
}
#[test]
fn persist_queued_payload_to_store_maps_store_error() {
let store = MockQueuedStore::new(true);
let meta = QueuedPayloadMeta::new(
EventName::ObjectCreatedPut,
"bucket-a".to_string(),
"obj.txt".to_string(),
"application/json",
7,
);
let queued = QueuedPayload::new(meta, br#"{"x":1}"#.to_vec());
let err = persist_queued_payload_to_store(&store, &queued).expect_err("expected put_raw failure");
assert!(err.to_string().contains("Failed to save event to store"));
}
#[test]
fn is_connectivity_error_classifies_target_errors() {
assert!(is_connectivity_error(&TargetError::NotConnected));
assert!(is_connectivity_error(&TargetError::Timeout("timeout".to_string())));
assert!(is_connectivity_error(&TargetError::Network("network".to_string())));
assert!(!is_connectivity_error(&TargetError::Storage("storage".to_string())));
assert!(!is_connectivity_error(&TargetError::Serialization("serialization".to_string())));
}
#[tokio::test]
async fn invalidate_cache_on_connectivity_error_only_runs_for_connectivity_failures() {
let marker = Arc::new(AtomicBool::new(false));
invalidate_cache_on_connectivity_error(&TargetError::NotConnected, {
let marker = Arc::clone(&marker);
move || async move {
marker.store(true, Ordering::SeqCst);
}
})
.await;
assert!(marker.load(Ordering::SeqCst));
marker.store(false, Ordering::SeqCst);
invalidate_cache_on_connectivity_error(&TargetError::Request("request failed".to_string()), {
let marker = Arc::clone(&marker);
move || async move {
marker.store(true, Ordering::SeqCst);
}
})
.await;
assert!(!marker.load(Ordering::SeqCst));
}
#[test]
fn mark_target_disconnected_on_connectivity_error_only_marks_connectivity_failures() {
let connected = AtomicBool::new(true);
mark_target_disconnected_on_connectivity_error(&connected, &TargetError::Timeout("timeout".to_string()));
assert!(!connected.load(Ordering::SeqCst));
connected.store(true, Ordering::SeqCst);
mark_target_disconnected_on_connectivity_error(&connected, &TargetError::Request("request failed".to_string()));
assert!(connected.load(Ordering::SeqCst));
}
#[test]
fn queued_payload_decode_rejects_invalid_magic() {
let err = QueuedPayload::decode(b"bad-payload").unwrap_err();
+19 -58
View File
@@ -13,13 +13,14 @@
// limitations under the License.
use crate::{
StoreError, Target, TargetLog,
StoreError, Target,
arn::TargetID,
error::TargetError,
store::{Key, QueueStore, Store},
store::{Key, Store},
target::{
ChannelTargetType, EntityTarget, QueuedPayload, QueuedPayloadMeta, TargetDeliveryCounters, TargetDeliverySnapshot,
TargetType, queue_store_subdir_name,
TargetType, build_queued_payload_with_records, mark_target_disconnected_on_connectivity_error, open_target_queue_store,
persist_queued_payload_to_store,
},
};
use async_trait::async_trait;
@@ -37,7 +38,7 @@ use serde::de::DeserializeOwned;
use std::sync::Arc;
use std::{
marker::PhantomData,
path::{Path, PathBuf},
path::Path,
sync::atomic::{AtomicBool, Ordering},
time::Duration,
};
@@ -502,30 +503,14 @@ where
pub fn new(id: String, args: MQTTArgs) -> Result<Self, TargetError> {
args.validate()?;
let target_id = TargetID::new(id, ChannelTargetType::Mqtt.as_str().to_string());
let queue_store = if !args.queue_dir.is_empty() {
let base_path = PathBuf::from(&args.queue_dir);
let unique_dir_name = queue_store_subdir_name(ChannelTargetType::Mqtt.as_str(), &target_id.id);
// Ensure the directory name is valid for filesystem
let specific_queue_path = base_path.join(unique_dir_name);
debug!(target_id = %target_id, path = %specific_queue_path.display(), "Initializing queue store for MQTT target");
let extension = match args.target_type {
TargetType::AuditLog => rustfs_config::audit::AUDIT_STORE_EXTENSION,
TargetType::NotifyEvent => rustfs_config::notify::NOTIFY_STORE_EXTENSION,
};
let store = QueueStore::<QueuedPayload>::new(specific_queue_path, args.queue_limit, extension);
if let Err(e) = store.open() {
error!(
target_id = %target_id,
error = %e,
"Failed to open store for MQTT target"
);
return Err(TargetError::Storage(format!("{e}")));
}
Some(Box::new(store) as Box<dyn Store<QueuedPayload, Error = StoreError, Key = Key> + Send + Sync>)
} else {
None
};
let queue_store = open_target_queue_store(
&args.queue_dir,
args.queue_limit,
args.target_type,
ChannelTargetType::Mqtt.as_str(),
&target_id,
"Failed to open store for MQTT target",
)?;
let (cancel_tx, cancel_rx) = mpsc::channel(1);
let bg_task_manager = Arc::new(BgTaskManager {
@@ -631,25 +616,7 @@ where
}
fn build_queued_payload(&self, event: &EntityTarget<E>) -> Result<QueuedPayload, TargetError> {
let object_name = crate::target::decode_object_name(&event.object_name)?;
let key = format!("{}/{}", event.bucket_name, object_name);
let log = TargetLog {
event_name: event.event_name,
key,
records: vec![event.clone()],
};
let body = serde_json::to_vec(&log).map_err(|e| TargetError::Serialization(format!("Failed to serialize event: {e}")))?;
let meta = QueuedPayloadMeta::new(
event.event_name,
event.bucket_name.clone(),
event.object_name.clone(),
"application/json",
body.len(),
);
Ok(QueuedPayload::new(meta, body))
build_queued_payload_with_records(event, vec![event.clone()])
}
#[instrument(skip(self, body, meta), fields(target_id = %self.id))]
@@ -673,9 +640,10 @@ where
.await
.map_err(|e| {
if e.to_string().contains("Connection") || e.to_string().contains("Timeout") {
self.connected.store(false, Ordering::SeqCst);
warn!(target_id = %self.id, error = %e, "Publish failed due to connection issue, marking as not connected.");
TargetError::NotConnected
let err = TargetError::NotConnected;
mark_target_disconnected_on_connectivity_error(&self.connected, &err);
err
} else {
TargetError::Request(format!("Failed to publish message: {e}"))
}
@@ -899,14 +867,7 @@ where
if let Some(store) = &self.store {
debug!(target_id = %self.id, "Event saved to store start");
let encoded = match queued.encode() {
Ok(encoded) => encoded,
Err(err) => {
self.delivery_counters.record_final_failure();
return Err(TargetError::Storage(format!("Failed to encode queued payload: {err}")));
}
};
match store.put_raw(&encoded) {
match persist_queued_payload_to_store(store.as_ref(), &queued) {
Ok(_) => {
debug!(target_id = %self.id, "Event saved to store for MQTT target successfully.");
Ok(())
@@ -914,7 +875,7 @@ where
Err(e) => {
error!(target_id = %self.id, error = %e, "Failed to save event to store");
self.delivery_counters.record_final_failure();
Err(TargetError::Storage(format!("Failed to save event to store: {e}")))
Err(e)
}
}
} else {
+14 -38
View File
@@ -16,15 +16,15 @@ use crate::{
StoreError, Target,
arn::TargetID,
error::TargetError,
store::{Key, QueueStore, Store},
store::{Key, Store},
target::{
ChannelTargetType, EntityTarget, QueuedPayload, QueuedPayloadMeta, TargetDeliveryCounters, TargetDeliverySnapshot,
TargetType, build_queued_payload, delete_stored_payload, queue_store_subdir_name,
TargetType, build_queued_payload, delete_stored_payload, is_connectivity_error, open_target_queue_store,
persist_queued_payload_to_store,
},
};
use async_trait::async_trait;
use mysql_async::{Conn, Opts, OptsBuilder, Pool, PoolConstraints, PoolOpts, SslOpts, prelude::Queryable};
use rustfs_config::notify::NOTIFY_STORE_EXTENSION;
use rustfs_config::{MYSQL_TLS_CA, MYSQL_TLS_CLIENT_CERT, MYSQL_TLS_CLIENT_KEY};
use serde::Serialize;
use serde::de::DeserializeOwned;
@@ -494,25 +494,14 @@ where
let target_id = TargetID::new(id, ChannelTargetType::MySql.as_str().to_string());
// If `queue_dir` is non-empty, a `QueueStore` is created for persistent at-least-once delivery.
let queue_store = if !args.queue_dir.is_empty() {
let queue_dir =
PathBuf::from(&args.queue_dir).join(queue_store_subdir_name(ChannelTargetType::MySql.as_str(), &target_id.id));
let extension = match args.target_type {
TargetType::AuditLog => rustfs_config::audit::AUDIT_STORE_EXTENSION,
TargetType::NotifyEvent => NOTIFY_STORE_EXTENSION,
};
let store = QueueStore::<QueuedPayload>::new(queue_dir, args.queue_limit, extension);
if let Err(e) = store.open() {
return Err(TargetError::Storage(format!("Failed to open MySQL queue store: {e}")));
}
Some(Box::new(store) as Box<dyn Store<QueuedPayload, Error = StoreError, Key = Key> + Send + Sync>)
} else {
None
};
let queue_store = open_target_queue_store(
&args.queue_dir,
args.queue_limit,
args.target_type,
ChannelTargetType::MySql.as_str(),
&target_id,
"Failed to open MySQL queue store",
)?;
info!(target_id = %target_id.id, table = %args.table, "MySQL target created");
@@ -733,18 +722,9 @@ where
};
if let Some(store) = &self.store {
// persist the event to a local queue before attempting to insert into MySQL. This will allow us to guarantee at-least-once delivery even if the database is temporarily unreachable or if the process crashes after acknowledging receipt but before writing to the database.
let encoded = match queued.encode() {
Ok(encoded) => encoded,
Err(err) => {
self.delivery_counters.record_final_failure();
return Err(TargetError::Storage(format!("Failed to encode queued payload: {err}")));
}
};
if let Err(e) = store.put_raw(&encoded) {
if let Err(e) = persist_queued_payload_to_store(store.as_ref(), &queued) {
self.delivery_counters.record_final_failure();
return Err(TargetError::Storage(format!("Failed to save event to store: {e}")));
return Err(e);
}
debug!("Event saved to queue store for MySQL target: {}", self.id);
@@ -789,12 +769,8 @@ where
}
if let Err(e) = self.insert_event(&body, &meta).await {
if matches!(e, TargetError::NotConnected) {
if is_connectivity_error(&e) {
warn!(target_id = %self.id, "MySQL not reachable, event remains in queue store");
return Err(TargetError::NotConnected);
}
if matches!(e, TargetError::Timeout(_)) {
warn!(target_id = %self.id, "MySQL timeout, event remains in queue store");
return Err(e);
}
error!(target_id = %self.id, error = %e, "Failed to send event from store");
+15 -45
View File
@@ -13,13 +13,13 @@
// limitations under the License.
use crate::{
StoreError, Target, TargetLog,
StoreError, Target,
arn::TargetID,
error::TargetError,
store::{Key, QueueStore, Store},
store::{Key, Store},
target::{
ChannelTargetType, EntityTarget, QueuedPayload, QueuedPayloadMeta, TargetDeliveryCounters, TargetDeliverySnapshot,
TargetType, queue_store_subdir_name,
TargetType, build_queued_payload_with_records, open_target_queue_store, persist_queued_payload_to_store,
},
};
use async_trait::async_trait;
@@ -30,7 +30,7 @@ use std::path::{Path, PathBuf};
use std::str::FromStr;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex};
use tracing::{error, info, instrument};
use tracing::{info, instrument};
#[derive(Debug, Clone)]
pub struct NATSArgs {
@@ -195,22 +195,14 @@ where
pub fn new(id: String, args: NATSArgs) -> Result<Self, TargetError> {
args.validate()?;
let target_id = TargetID::new(id, ChannelTargetType::Nats.as_str().to_string());
let queue_store = if !args.queue_dir.is_empty() {
let base_path = PathBuf::from(&args.queue_dir);
let specific_queue_path = base_path.join(queue_store_subdir_name(ChannelTargetType::Nats.as_str(), &target_id.id));
let extension = match args.target_type {
TargetType::AuditLog => rustfs_config::audit::AUDIT_STORE_EXTENSION,
TargetType::NotifyEvent => rustfs_config::notify::NOTIFY_STORE_EXTENSION,
};
let store = QueueStore::<QueuedPayload>::new(specific_queue_path, args.queue_limit, extension);
if let Err(e) = store.open() {
error!(target_id = %target_id, error = %e, "Failed to open store for NATS target");
return Err(TargetError::Storage(format!("{e}")));
}
Some(Box::new(store) as Box<dyn Store<QueuedPayload, Error = StoreError, Key = Key> + Send + Sync>)
} else {
None
};
let queue_store = open_target_queue_store(
&args.queue_dir,
args.queue_limit,
args.target_type,
ChannelTargetType::Nats.as_str(),
&target_id,
"Failed to open store for NATS target",
)?;
Ok(Self {
id: target_id,
@@ -241,22 +233,7 @@ where
}
fn build_queued_payload(&self, event: &EntityTarget<E>) -> Result<QueuedPayload, TargetError> {
let object_name = crate::target::decode_object_name(&event.object_name)?;
let key = format!("{}/{}", event.bucket_name, object_name);
let log = TargetLog {
event_name: event.event_name,
key,
records: vec![event.clone()],
};
let body = serde_json::to_vec(&log).map_err(|e| TargetError::Serialization(format!("Failed to serialize event: {e}")))?;
let meta = QueuedPayloadMeta::new(
event.event_name,
event.bucket_name.clone(),
event.object_name.clone(),
"application/json",
body.len(),
);
Ok(QueuedPayload::new(meta, body))
build_queued_payload_with_records(event, vec![event.clone()])
}
async fn send_body(&self, body: Vec<u8>) -> Result<(), TargetError> {
@@ -298,16 +275,9 @@ where
};
if let Some(store) = &self.store {
let encoded = match queued.encode() {
Ok(encoded) => encoded,
Err(err) => {
self.delivery_counters.record_final_failure();
return Err(TargetError::Storage(format!("Failed to encode queued payload: {err}")));
}
};
if let Err(e) = store.put_raw(&encoded) {
if let Err(e) = persist_queued_payload_to_store(store.as_ref(), &queued) {
self.delivery_counters.record_final_failure();
return Err(TargetError::Storage(format!("Failed to save event to store: {e}")));
return Err(e);
}
Ok(())
} else {
+14 -30
View File
@@ -29,10 +29,10 @@ use crate::{
StoreError, Target,
arn::TargetID,
error::TargetError,
store::{Key, QueueStore, Store},
store::{Key, Store},
target::{
ChannelTargetType, EntityTarget, QueuedPayload, QueuedPayloadMeta, TargetDeliveryCounters, TargetDeliverySnapshot,
TargetType, build_queued_payload, queue_store_subdir_name,
TargetType, build_queued_payload, open_target_queue_store, persist_queued_payload_to_store,
},
};
use async_trait::async_trait;
@@ -44,11 +44,11 @@ use serde::Serialize;
use serde::de::DeserializeOwned;
use std::fmt;
use std::io::BufReader;
use std::path::{Path, PathBuf};
use std::path::Path;
use std::sync::Arc;
use tokio_postgres::Config;
use tokio_postgres_rustls::MakeRustlsConnect;
use tracing::{error, info, instrument, warn};
use tracing::{info, instrument, warn};
use url::Url;
use uuid::Uuid;
@@ -585,23 +585,14 @@ where
let target_id = TargetID::new(id, ChannelTargetType::Postgres.as_str().to_string());
let pool = build_pool(&args)?;
let queue_store = if !args.queue_dir.is_empty() {
let base_path = PathBuf::from(&args.queue_dir);
let specific_queue_path =
base_path.join(queue_store_subdir_name(ChannelTargetType::Postgres.as_str(), &target_id.id));
let extension = match args.target_type {
TargetType::AuditLog => rustfs_config::audit::AUDIT_STORE_EXTENSION,
TargetType::NotifyEvent => rustfs_config::notify::NOTIFY_STORE_EXTENSION,
};
let store = QueueStore::<QueuedPayload>::new(specific_queue_path, args.queue_limit, extension);
if let Err(e) = store.open() {
error!(target_id = %target_id, error = %e, "Failed to open store for PostgreSQL target");
return Err(TargetError::Storage(format!("{e}")));
}
Some(Box::new(store) as Box<dyn Store<QueuedPayload, Error = StoreError, Key = Key> + Send + Sync>)
} else {
None
};
let queue_store = open_target_queue_store(
&args.queue_dir,
args.queue_limit,
args.target_type,
ChannelTargetType::Postgres.as_str(),
&target_id,
"Failed to open store for PostgreSQL target",
)?;
Ok(Self {
id: target_id,
@@ -712,16 +703,9 @@ where
};
if let Some(store) = &self.store {
let encoded = match queued.encode() {
Ok(encoded) => encoded,
Err(err) => {
self.delivery_counters.record_final_failure();
return Err(TargetError::Storage(format!("Failed to encode queued payload: {err}")));
}
};
if let Err(e) = store.put_raw(&encoded) {
if let Err(e) = persist_queued_payload_to_store(store.as_ref(), &queued) {
self.delivery_counters.record_final_failure();
return Err(TargetError::Storage(format!("Failed to save event to store: {e}")));
return Err(e);
}
Ok(())
} else {
+16 -46
View File
@@ -13,24 +13,24 @@
// limitations under the License.
use crate::{
StoreError, Target, TargetLog,
StoreError, Target,
arn::TargetID,
error::TargetError,
store::{Key, QueueStore, Store},
store::{Key, Store},
target::{
ChannelTargetType, EntityTarget, QueuedPayload, QueuedPayloadMeta, TargetDeliveryCounters, TargetDeliverySnapshot,
TargetType, queue_store_subdir_name,
TargetType, build_queued_payload_with_records, open_target_queue_store, persist_queued_payload_to_store,
},
};
use async_trait::async_trait;
use pulsar::{Authentication, Producer, Pulsar, TokioExecutor};
use serde::Serialize;
use serde::de::DeserializeOwned;
use std::path::{Path, PathBuf};
use std::path::Path;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex};
use tokio::sync::Mutex as AsyncMutex;
use tracing::{error, info, instrument};
use tracing::{info, instrument};
use url::Url;
#[derive(Debug, Clone)]
@@ -186,22 +186,14 @@ where
pub fn new(id: String, args: PulsarArgs) -> Result<Self, TargetError> {
args.validate()?;
let target_id = TargetID::new(id, ChannelTargetType::Pulsar.as_str().to_string());
let queue_store = if !args.queue_dir.is_empty() {
let base_path = PathBuf::from(&args.queue_dir);
let specific_queue_path = base_path.join(queue_store_subdir_name(ChannelTargetType::Pulsar.as_str(), &target_id.id));
let extension = match args.target_type {
TargetType::AuditLog => rustfs_config::audit::AUDIT_STORE_EXTENSION,
TargetType::NotifyEvent => rustfs_config::notify::NOTIFY_STORE_EXTENSION,
};
let store = QueueStore::<QueuedPayload>::new(specific_queue_path, args.queue_limit, extension);
if let Err(e) = store.open() {
error!(target_id = %target_id, error = %e, "Failed to open store for Pulsar target");
return Err(TargetError::Storage(format!("{e}")));
}
Some(Box::new(store) as Box<dyn Store<QueuedPayload, Error = StoreError, Key = Key> + Send + Sync>)
} else {
None
};
let queue_store = open_target_queue_store(
&args.queue_dir,
args.queue_limit,
args.target_type,
ChannelTargetType::Pulsar.as_str(),
&target_id,
"Failed to open store for Pulsar target",
)?;
Ok(Self {
id: target_id,
@@ -249,22 +241,7 @@ where
}
fn build_queued_payload(&self, event: &EntityTarget<E>) -> Result<QueuedPayload, TargetError> {
let object_name = crate::target::decode_object_name(&event.object_name)?;
let key = format!("{}/{}", event.bucket_name, object_name);
let log = TargetLog {
event_name: event.event_name,
key,
records: vec![event.clone()],
};
let body = serde_json::to_vec(&log).map_err(|e| TargetError::Serialization(format!("Failed to serialize event: {e}")))?;
let meta = QueuedPayloadMeta::new(
event.event_name,
event.bucket_name.clone(),
event.object_name.clone(),
"application/json",
body.len(),
);
Ok(QueuedPayload::new(meta, body))
build_queued_payload_with_records(event, vec![event.clone()])
}
async fn send_body(&self, body: Vec<u8>) -> Result<(), TargetError> {
@@ -317,16 +294,9 @@ where
};
if let Some(store) = &self.store {
let encoded = match queued.encode() {
Ok(encoded) => encoded,
Err(err) => {
self.delivery_counters.record_final_failure();
return Err(TargetError::Storage(format!("Failed to encode queued payload: {err}")));
}
};
if let Err(e) = store.put_raw(&encoded) {
if let Err(e) = persist_queued_payload_to_store(store.as_ref(), &queued) {
self.delivery_counters.record_final_failure();
return Err(TargetError::Storage(format!("Failed to save event to store: {e}")));
return Err(e);
}
Ok(())
} else {
+26 -48
View File
@@ -16,10 +16,11 @@ use crate::{
StoreError, Target,
arn::TargetID,
error::TargetError,
store::{Key, QueueStore, Store},
store::{Key, Store},
target::{
ChannelTargetType, EntityTarget, QueuedPayload, QueuedPayloadMeta, TargetDeliveryCounters, TargetDeliverySnapshot,
TargetType, build_queued_payload, queue_store_subdir_name,
TargetType, build_queued_payload, invalidate_cache_on_connectivity_error, is_connectivity_error,
mark_target_disconnected_on_connectivity_error, open_target_queue_store, persist_queued_payload_to_store,
},
};
use async_trait::async_trait;
@@ -33,12 +34,12 @@ use rustfs_config::{REDIS_TLS_CA, REDIS_TLS_CLIENT_CERT, REDIS_TLS_CLIENT_KEY, R
use serde::Serialize;
use serde::de::DeserializeOwned;
use std::fmt;
use std::path::{Path, PathBuf};
use std::path::Path;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::time::Duration;
use tokio::sync::Mutex;
use tracing::{debug, error, info, instrument, warn};
use tracing::{debug, info, instrument, warn};
use url::Url;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
@@ -320,22 +321,14 @@ where
let target_id = TargetID::new(id, ChannelTargetType::Redis.as_str().to_string());
let publisher_client = build_redis_client(&args)?;
let queue_store = if !args.queue_dir.is_empty() {
let base_path = PathBuf::from(&args.queue_dir);
let specific_queue_path = base_path.join(queue_store_subdir_name(ChannelTargetType::Redis.as_str(), &target_id.id));
let extension = match args.target_type {
TargetType::AuditLog => rustfs_config::audit::AUDIT_STORE_EXTENSION,
TargetType::NotifyEvent => rustfs_config::notify::NOTIFY_STORE_EXTENSION,
};
let store = QueueStore::<QueuedPayload>::new(specific_queue_path, args.queue_limit, extension);
if let Err(e) = store.open() {
error!(target_id = %target_id, error = %e, "Failed to open store for Redis target");
return Err(TargetError::Storage(format!("{e}")));
}
Some(Box::new(store) as Box<dyn Store<QueuedPayload, Error = StoreError, Key = Key> + Send + Sync>)
} else {
None
};
let queue_store = open_target_queue_store(
&args.queue_dir,
args.queue_limit,
args.target_type,
ChannelTargetType::Redis.as_str(),
&target_id,
"Failed to open store for Redis target",
)?;
info!(target_id = %target_id, "Redis target created");
Ok(Self {
@@ -391,9 +384,7 @@ where
Ok(_) => Ok(()),
Err(err) => {
let mapped = map_redis_error(err);
if is_retryable_target_error(&mapped) {
self.invalidate_cached_publisher().await;
}
invalidate_cache_on_connectivity_error(&mapped, || self.invalidate_cached_publisher()).await;
Err(mapped)
}
}
@@ -437,9 +428,7 @@ where
}
Err(err) => {
let mapped = map_redis_error(err);
if is_retryable_target_error(&mapped) {
self.invalidate_cached_publisher().await;
}
invalidate_cache_on_connectivity_error(&mapped, || self.invalidate_cached_publisher()).await;
warn!(
target_id = %self.id,
@@ -450,7 +439,7 @@ where
"Redis publish attempt failed"
);
if !is_retryable_target_error(&mapped) || attempt >= self.args.max_retry_attempts {
if !is_connectivity_error(&mapped) || attempt >= self.args.max_retry_attempts {
last_error = Some(mapped);
break;
}
@@ -492,14 +481,15 @@ where
Ok(true)
}
Ok(Err(err)) => {
self.invalidate_cached_publisher().await;
self.connected.store(false, Ordering::SeqCst);
invalidate_cache_on_connectivity_error(&err, || self.invalidate_cached_publisher()).await;
mark_target_disconnected_on_connectivity_error(&self.connected, &err);
Err(err)
}
Err(_) => {
self.invalidate_cached_publisher().await;
self.connected.store(false, Ordering::SeqCst);
Err(TargetError::Timeout("Redis connection timed out".to_string()))
let timeout_err = TargetError::Timeout("Redis connection timed out".to_string());
invalidate_cache_on_connectivity_error(&timeout_err, || self.invalidate_cached_publisher()).await;
mark_target_disconnected_on_connectivity_error(&self.connected, &timeout_err);
Err(timeout_err)
}
}
}
@@ -514,17 +504,9 @@ where
};
if let Some(store) = &self.store {
let encoded = match queued.encode() {
Ok(encoded) => encoded,
Err(err) => {
self.delivery_counters.record_final_failure();
return Err(TargetError::Storage(format!("Failed to encode queued payload: {err}")));
}
};
if let Err(e) = store.put_raw(&encoded) {
if let Err(e) = persist_queued_payload_to_store(store.as_ref(), &queued) {
self.delivery_counters.record_final_failure();
return Err(TargetError::Storage(format!("Failed to save event to store: {e}")));
return Err(e);
}
debug!(target_id = %self.id, "Event saved to store for Redis target");
@@ -556,14 +538,14 @@ where
}
if let Err(err) = self.init_inner().await {
if matches!(err, TargetError::NotConnected | TargetError::Timeout(_) | TargetError::Network(_)) {
if is_connectivity_error(&err) {
warn!(target_id = %self.id, error = %err, "Redis target not ready; queued event remains in store");
}
return Err(err);
}
if let Err(err) = self.send_body(body, &meta).await {
if matches!(err, TargetError::NotConnected | TargetError::Timeout(_) | TargetError::Network(_)) {
if is_connectivity_error(&err) {
warn!(target_id = %self.id, error = %err, "Failed to send Redis event from store: target not connected. Event remains queued.");
}
return Err(err);
@@ -734,10 +716,6 @@ fn map_redis_error(err: RedisError) -> TargetError {
}
}
fn is_retryable_target_error(err: &TargetError) -> bool {
matches!(err, TargetError::NotConnected | TargetError::Timeout(_) | TargetError::Network(_))
}
fn compute_retry_delay(attempt: usize, min_delay: Duration, max_delay: Duration) -> Duration {
let shift = attempt.saturating_sub(1).min(16) as u32;
let factor = 1u32 << shift;
+14 -53
View File
@@ -13,24 +13,21 @@
// limitations under the License.
use crate::{
StoreError, Target, TargetLog,
StoreError, Target,
arn::TargetID,
error::TargetError,
store::{Key, QueueStore, Store},
store::{Key, Store},
target::{
ChannelTargetType, EntityTarget, QueuedPayload, QueuedPayloadMeta, TargetDeliveryCounters, TargetDeliverySnapshot,
TargetType, queue_store_subdir_name,
TargetType, build_queued_payload, open_target_queue_store, persist_queued_payload_to_store,
},
};
use async_trait::async_trait;
use reqwest::{Client, StatusCode, Url};
use rustfs_config::audit::AUDIT_STORE_EXTENSION;
use rustfs_config::notify::NOTIFY_STORE_EXTENSION;
use serde::Serialize;
use serde::de::DeserializeOwned;
use std::{
marker::PhantomData,
path::PathBuf,
sync::{
Arc,
atomic::{AtomicBool, Ordering},
@@ -151,28 +148,14 @@ where
// Build HTTP client using the helper function
let http_client = Arc::new(Self::build_http_client(&args)?);
// Build storage
let queue_store = if !args.queue_dir.is_empty() {
let queue_dir =
PathBuf::from(&args.queue_dir).join(queue_store_subdir_name(ChannelTargetType::Webhook.as_str(), &target_id.id));
let extension = match args.target_type {
TargetType::AuditLog => AUDIT_STORE_EXTENSION,
TargetType::NotifyEvent => NOTIFY_STORE_EXTENSION,
};
let store = QueueStore::<QueuedPayload>::new(queue_dir, args.queue_limit, extension);
if let Err(e) = store.open() {
error!("Failed to open store for Webhook target {}: {}", target_id.id, e);
return Err(TargetError::Storage(format!("{e}")));
}
// Make sure that the Store trait implemented by QueueStore matches the expected error type
Some(Box::new(store) as Box<dyn Store<QueuedPayload, Error = StoreError, Key = Key> + Send + Sync>)
} else {
None
};
let queue_store = open_target_queue_store(
&args.queue_dir,
args.queue_limit,
args.target_type,
ChannelTargetType::Webhook.as_str(),
&target_id,
"Failed to open store for Webhook target",
)?;
// Create a cancel channel
let (cancel_sender, _) = mpsc::channel(1);
@@ -302,22 +285,7 @@ where
}
fn build_queued_payload(&self, event: &EntityTarget<E>) -> Result<QueuedPayload, TargetError> {
let object_name = crate::target::decode_object_name(&event.object_name)?;
let key = format!("{}/{}", event.bucket_name, object_name);
let log = TargetLog {
event_name: event.event_name,
key,
records: vec![event.data.clone()],
};
let body = serde_json::to_vec(&log).map_err(|e| TargetError::Serialization(format!("Failed to serialize event: {e}")))?;
let meta = QueuedPayloadMeta::new(
event.event_name,
event.bucket_name.clone(),
event.object_name.clone(),
"application/json",
body.len(),
);
Ok(QueuedPayload::new(meta, body))
build_queued_payload(event)
}
async fn send_body(&self, body: Vec<u8>, meta: &QueuedPayloadMeta) -> Result<(), TargetError> {
@@ -408,16 +376,9 @@ where
};
if let Some(store) = &self.store {
let encoded = match queued.encode() {
Ok(encoded) => encoded,
Err(err) => {
self.delivery_counters.record_final_failure();
return Err(TargetError::Storage(format!("Failed to encode queued payload: {err}")));
}
};
if let Err(e) = store.put_raw(&encoded) {
if let Err(e) = persist_queued_payload_to_store(store.as_ref(), &queued) {
self.delivery_counters.record_final_failure();
return Err(TargetError::Storage(format!("Failed to save event to store: {e}")));
return Err(e);
}
debug!("Event saved to store for target: {}", self.id);
Ok(())