#![forbid(unsafe_code)]
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::sync::{Mutex, MutexGuard, mpsc};
use std::thread::JoinHandle;
use std::time::{Duration, SystemTime};
use zerodds_dcps::runtime::{UserReaderConfig, UserSample, UserWriterConfig};
use zerodds_dcps::{
DataReader, DataReaderQos, DataWriter, DataWriterQos, DomainParticipant,
DomainParticipantFactory, DomainParticipantQos, Publisher, PublisherQos, RawBytes, Subscriber,
SubscriberQos, Topic, TopicQos,
};
use zerodds_durability_store::{Contract, DurabilitySample, DurabilityStore};
use zerodds_qos::policies::durability::DurabilityKind;
use zerodds_qos::policies::history::HistoryKind;
use zerodds_qos::policies::reliability::ReliabilityKind;
use zerodds_qos::policies::resource_limits::LENGTH_UNLIMITED;
use zerodds_qos::{
DeadlineQosPolicy, LifespanQosPolicy, LivelinessKind, LivelinessQosPolicy, OwnershipKind,
};
#[derive(Debug)]
pub enum ServiceError {
Dcps(zerodds_dcps::DdsError),
Store(zerodds_durability_store::StoreError),
Poisoned(&'static str),
}
impl core::fmt::Display for ServiceError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Self::Dcps(e) => write!(f, "durability service: dcps: {e}"),
Self::Store(e) => write!(f, "durability service: store: {e}"),
Self::Poisoned(w) => write!(f, "durability service: poisoned: {w}"),
}
}
}
impl core::error::Error for ServiceError {}
impl From<zerodds_dcps::DdsError> for ServiceError {
fn from(e: zerodds_dcps::DdsError) -> Self {
Self::Dcps(e)
}
}
impl From<zerodds_durability_store::StoreError> for ServiceError {
fn from(e: zerodds_durability_store::StoreError) -> Self {
Self::Store(e)
}
}
pub type Result<T> = core::result::Result<T, ServiceError>;
fn ingest_reader_qos() -> DataReaderQos {
let mut q = DataReaderQos::default();
q.reliability.kind = ReliabilityKind::Reliable;
q.durability.kind = DurabilityKind::TransientLocal;
q.history.kind = HistoryKind::KeepAll;
q.resource_limits.max_samples = LENGTH_UNLIMITED;
q
}
fn replay_writer_qos() -> DataWriterQos {
let mut q = DataWriterQos::default();
q.reliability.kind = ReliabilityKind::Reliable;
q.durability.kind = DurabilityKind::TransientLocal;
q.history.kind = HistoryKind::KeepAll;
q.resource_limits.max_samples = LENGTH_UNLIMITED;
q
}
struct Served {
name: String,
stop: Arc<AtomicBool>,
handle: Option<JoinHandle<()>>,
}
pub struct DurabilityService {
ingest: DomainParticipant,
replay: DomainParticipant,
publisher: Publisher,
subscriber: Subscriber,
store: Arc<dyn DurabilityStore>,
served: Mutex<Vec<Served>>,
}
impl DurabilityService {
pub fn start(domain: i32, store: Arc<dyn DurabilityStore>) -> Result<Self> {
let factory = DomainParticipantFactory::instance();
let ingest = factory.create_participant(domain, DomainParticipantQos::default())?;
let replay = factory.create_participant(domain, DomainParticipantQos::default())?;
let _ = ingest.ignore_participant(replay.participant_handle());
let _ = replay.ignore_participant(ingest.participant_handle());
let publisher = replay.create_publisher(PublisherQos::default());
let subscriber = ingest.create_subscriber(SubscriberQos::default());
Ok(Self {
ingest,
replay,
publisher,
subscriber,
store,
served: Mutex::new(Vec::new()),
})
}
fn served(&self) -> Result<MutexGuard<'_, Vec<Served>>> {
self.served
.lock()
.map_err(|_| ServiceError::Poisoned("served topics"))
}
pub fn serve(&self, topic_name: &str, contract: Contract) -> Result<()> {
self.store.set_contract(topic_name, contract)?;
let rtopic: Topic<RawBytes> = self
.ingest
.create_topic::<RawBytes>(topic_name, TopicQos::default())?;
let wtopic: Topic<RawBytes> = self
.replay
.create_topic::<RawBytes>(topic_name, TopicQos::default())?;
let writer = self
.publisher
.create_datawriter::<RawBytes>(&wtopic, replay_writer_qos())?;
let reader = self
.subscriber
.create_datareader::<RawBytes>(&rtopic, ingest_reader_qos())?;
for sample in self.store.replay_for_topic(topic_name)? {
writer.write(&RawBytes::new(sample.payload))?;
}
let stop = Arc::new(AtomicBool::new(false));
let pump_stop = Arc::clone(&stop);
let store = Arc::clone(&self.store);
let topic_owned = topic_name.to_string();
let own_pub = writer.instance_handle();
let seq = AtomicU64::new(self.store.stats(topic_name)?.samples as u64);
let handle = std::thread::Builder::new()
.name(format!("durability-pump-{topic_name}"))
.spawn(move || {
pump(
&pump_stop,
&reader,
&writer,
store.as_ref(),
&topic_owned,
own_pub,
&seq,
);
})
.map_err(|_| ServiceError::Poisoned("spawn pump"))?;
self.served()?.push(Served {
name: topic_name.to_string(),
stop,
handle: Some(handle),
});
Ok(())
}
pub fn serve_typed(
&self,
topic_name: &str,
type_name: &str,
keyed: bool,
contract: Contract,
) -> Result<()> {
self.store.set_contract(topic_name, contract)?;
let irt = self
.ingest
.runtime()
.ok_or(ServiceError::Poisoned("ingest runtime"))?
.clone();
let rrt = self
.replay
.runtime()
.ok_or(ServiceError::Poisoned("replay runtime"))?
.clone();
let weid = rrt
.register_user_writer_kind(user_writer_cfg(topic_name, type_name), keyed)
.map_err(|_| ServiceError::Poisoned("register replay writer"))?;
let (_reid, rx) = irt
.register_user_reader_kind(user_reader_cfg(topic_name, type_name), keyed)
.map_err(|_| ServiceError::Poisoned("register ingest reader"))?;
for sample in self.store.replay_for_topic(topic_name)? {
let off: i16 = if sample.representation == 1 { 2 } else { 0 };
let _ = rrt.set_user_writer_data_rep_override(weid, Some(vec![off]));
let _ = rrt.set_user_writer_byte_order_override(weid, sample.big_endian);
let _ = rrt.write_user_sample_borrowed(weid, &sample.payload);
}
let stop = Arc::new(AtomicBool::new(false));
let pump_stop = Arc::clone(&stop);
let store = Arc::clone(&self.store);
let topic_owned = topic_name.to_string();
let pump_rrt = Arc::clone(&rrt);
let seq = AtomicU64::new(self.store.stats(topic_name)?.samples as u64);
let handle = std::thread::Builder::new()
.name(format!("durability-pump-{topic_name}"))
.spawn(move || {
let mut last_rep: u8 = 255;
let mut last_be = false;
let mut seen: std::collections::HashSet<([u8; 16], i64)> = store
.replay_for_topic(&topic_owned)
.map(|v| {
v.into_iter()
.filter(|s| s.source_sequence >= 0)
.map(|s| (s.source_guid, s.source_sequence))
.collect()
})
.unwrap_or_default();
while !pump_stop.load(Ordering::Relaxed) {
let sample = match rx.recv_timeout(Duration::from_millis(200)) {
Ok(s) => s,
Err(mpsc::RecvTimeoutError::Timeout) => continue,
Err(mpsc::RecvTimeoutError::Disconnected) => break,
};
let (payload, representation, big_endian, writer_guid, source_seq) =
match sample {
UserSample::Alive {
payload,
representation,
big_endian,
writer_guid,
source_sequence_number,
..
} => (
payload.to_vec(),
representation,
big_endian,
writer_guid,
source_sequence_number,
),
UserSample::Lifecycle { .. } => continue,
};
if source_seq >= 0 && !seen.insert((writer_guid, source_seq)) {
continue;
}
if representation != last_rep {
let off: i16 = if representation == 1 { 2 } else { 0 };
let _ = pump_rrt.set_user_writer_data_rep_override(weid, Some(vec![off]));
last_rep = representation;
}
if big_endian != last_be {
let _ = pump_rrt.set_user_writer_byte_order_override(weid, big_endian);
last_be = big_endian;
}
let ds = DurabilitySample {
topic: topic_owned.clone(),
instance_key: [0u8; 16], sequence: seq.fetch_add(1, Ordering::Relaxed),
payload: payload.clone(),
representation,
big_endian,
created_at: SystemTime::now(),
source_guid: writer_guid,
source_sequence: source_seq,
};
if store.store(ds).is_ok() {
let _ = pump_rrt.write_user_sample_borrowed(weid, &payload);
}
}
})
.map_err(|_| ServiceError::Poisoned("spawn pump"))?;
self.served()?.push(Served {
name: topic_name.to_string(),
stop,
handle: Some(handle),
});
Ok(())
}
pub fn served_topics(&self) -> Result<Vec<String>> {
Ok(self.served()?.iter().map(|s| s.name.clone()).collect())
}
pub fn enable_auto_discovery(self: &Arc<Self>, default_contract: Contract) -> Result<()> {
let reader = self.ingest.get_builtin_subscriber().publication_reader();
let me = Arc::clone(self);
let stop = Arc::new(AtomicBool::new(false));
let pstop = Arc::clone(&stop);
let handle = std::thread::Builder::new()
.name("durability-autodiscover".to_string())
.spawn(move || {
while !pstop.load(Ordering::Relaxed) {
let _ = reader.wait_for_data(Duration::from_millis(300));
let Ok(pubs) = reader.take_with_info() else {
continue;
};
for s in pubs {
if !s.info.valid_data || s.data.durability < DurabilityKind::Transient {
continue;
}
let topic = s.data.topic_name;
let type_name = s.data.type_name;
let already = me
.served_topics()
.map(|v| v.iter().any(|t| t == &topic))
.unwrap_or(true);
if !already {
let _ = me.serve_typed(&topic, &type_name, false, default_contract);
}
}
}
})
.map_err(|_| ServiceError::Poisoned("spawn auto-discovery"))?;
self.served()?.push(Served {
name: "<auto-discovery>".to_string(),
stop,
handle: Some(handle),
});
Ok(())
}
pub fn shutdown(&self) {
let handles: Vec<JoinHandle<()>> = {
let Ok(mut served) = self.served.lock() else {
return;
};
for s in served.iter() {
s.stop.store(true, Ordering::Relaxed);
}
served.iter_mut().filter_map(|s| s.handle.take()).collect()
};
for h in handles {
let _ = h.join();
}
}
}
fn pump(
stop: &AtomicBool,
reader: &DataReader<RawBytes>,
writer: &DataWriter<RawBytes>,
store: &dyn DurabilityStore,
topic: &str,
own_pub: zerodds_dcps::InstanceHandle,
seq: &AtomicU64,
) {
while !stop.load(Ordering::Relaxed) {
let _ = reader.wait_for_data(Duration::from_millis(200));
let samples = match reader.take_with_info() {
Ok(s) => s,
Err(_) => continue,
};
for sample in samples {
if !sample.info.valid_data {
continue; }
if sample.info.publication_handle == own_pub {
continue;
}
let payload = sample.data.data;
let ds = DurabilitySample {
topic: topic.to_string(),
instance_key: [0u8; 16], sequence: seq.fetch_add(1, Ordering::Relaxed),
payload: payload.clone(),
representation: 1,
big_endian: false,
created_at: SystemTime::now(),
source_guid: [0u8; 16],
source_sequence: -1,
};
if store.store(ds).is_ok() {
let _ = writer.write(&RawBytes::new(payload));
}
}
}
}
fn user_reader_cfg(topic_name: &str, type_name: &str) -> UserReaderConfig {
UserReaderConfig {
topic_name: topic_name.to_string(),
type_name: type_name.to_string(),
reliable: true,
durability: DurabilityKind::Volatile,
deadline: DeadlineQosPolicy::default(),
liveliness: LivelinessQosPolicy {
kind: LivelinessKind::Automatic,
..Default::default()
},
ownership: OwnershipKind::Shared,
partition: Vec::new(),
user_data: Vec::new(),
topic_data: Vec::new(),
group_data: Vec::new(),
type_identifier: zerodds_types::TypeIdentifier::None,
type_consistency: zerodds_types::qos::TypeConsistencyEnforcement::default(),
data_representation_offer: Some(vec![0, 2]),
}
}
fn user_writer_cfg(topic_name: &str, type_name: &str) -> UserWriterConfig {
UserWriterConfig {
topic_name: topic_name.to_string(),
type_name: type_name.to_string(),
reliable: true,
durability: DurabilityKind::TransientLocal,
deadline: DeadlineQosPolicy::default(),
lifespan: LifespanQosPolicy::default(),
liveliness: LivelinessQosPolicy {
kind: LivelinessKind::Automatic,
..Default::default()
},
ownership: OwnershipKind::Shared,
ownership_strength: 0,
partition: Vec::new(),
user_data: Vec::new(),
topic_data: Vec::new(),
group_data: Vec::new(),
type_identifier: zerodds_types::TypeIdentifier::None,
data_representation_offer: None,
}
}