use super::durable_write_wire_tests::{HeldStorage, WAIT, destination};
use super::mutation_tests::wpm;
use super::*;
use crate::server::durable_writes::{SLOW_SAVE_REPEAT, SLOW_SAVE_WARNING, slow_save_warnings};
use crate::server::test_transport::TestTransport;
use bacnet_encoding::constructed::encode_destination_list;
use bacnet_encoding::npdu::{Npdu, encode_npdu};
use bacnet_objects::device::{DeviceConfig, DeviceObject};
use bacnet_objects::notification_class::{
NotificationClass, NotificationClassPersistence, NotificationClassSnapshot,
};
use bacnet_services::common::BACnetPropertyValue;
use bacnet_services::wpm::WriteAccessSpecification;
use bacnet_services::write_property::WritePropertyRequest;
use bacnet_transport::port::{ReceivedNpdu, TransportProvenance};
use bacnet_types::constructed::BACnetDestination;
use std::time::Duration;
use tokio::sync::mpsc;
type ClassStorage = HeldStorage<NotificationClassSnapshot>;
const RECIPIENT_LIST: PropertyIdentifier = PropertyIdentifier::RECIPIENT_LIST;
fn snapshot(list: &[BACnetDestination]) -> NotificationClassSnapshot {
NotificationClassSnapshot {
recipient_list: Some(list.to_vec()),
}
}
fn holding(list: &[BACnetDestination]) -> Arc<ClassStorage> {
let storage = Arc::new(ClassStorage::default());
*storage.saved.lock().unwrap() = Some(snapshot(list));
storage
}
fn nc(instance: u32) -> ObjectIdentifier {
ObjectIdentifier::new(ObjectType::NOTIFICATION_CLASS, instance).unwrap()
}
fn class(instance: u32, storage: &Arc<ClassStorage>) -> NotificationClass {
NotificationClass::with_persistence(
instance,
format!("NC-{instance}"),
Arc::clone(storage) as Arc<dyn NotificationClassPersistence>,
)
.unwrap()
}
async fn server(
storages: &[&Arc<ClassStorage>],
) -> (BACnetServer<TestTransport>, mpsc::Sender<ReceivedNpdu>) {
let (transport, inbound) = TestTransport::inbound(4);
let mut db = ObjectDatabase::new();
db.add(Box::new(
DeviceObject::new(DeviceConfig {
identity: bacnet_objects::DeviceIdentity::new(100, 0).expect("valid Device identity"),
..DeviceConfig::default()
})
.unwrap(),
))
.unwrap();
for (instance, storage) in (1..).zip(storages) {
db.add(Box::new(class(instance, storage))).unwrap();
}
let server = BACnetServer::generic_builder()
.device(bacnet_objects::DeviceIdentity::new(100, 0).unwrap())
.transport(transport)
.database(db)
.enable_event_enrollment(false)
.build()
.await
.unwrap();
(server, inbound)
}
pub(super) async fn send(
inbound: &mpsc::Sender<ReceivedNpdu>,
service: ConfirmedServiceChoice,
request: Bytes,
) {
let mut apdu = BytesMut::new();
encode_apdu(
&mut apdu,
&Apdu::ConfirmedRequest(ConfirmedRequestPdu {
segmented: false,
more_follows: false,
segmented_response_accepted: false,
max_segments: None,
max_apdu_length: 1476,
invoke_id: 1,
sequence_number: None,
proposed_window_size: None,
service_choice: service,
service_request: request,
}),
)
.unwrap();
let mut npdu = BytesMut::new();
encode_npdu(
&mut npdu,
&Npdu {
payload: apdu.freeze(),
..Npdu::default()
},
)
.unwrap();
inbound
.send(ReceivedNpdu {
direct_response: None,
npdu: npdu.freeze(),
source_mac: MacAddr::from_slice(&[1]),
link_layer_group: false,
data_attributes: Vec::new(),
provenance: TransportProvenance::unverified(),
reply_tx: None,
})
.await
.unwrap();
}
fn encoded(list: &[BACnetDestination]) -> Vec<u8> {
let mut buf = BytesMut::new();
encode_destination_list(&mut buf, list).unwrap();
buf.to_vec()
}
fn write_property(instance: u32, list: &[BACnetDestination]) -> Bytes {
let mut request = BytesMut::new();
WritePropertyRequest {
object_identifier: nc(instance),
property_identifier: RECIPIENT_LIST,
property_array_index: None,
property_value: encoded(list),
priority: None,
}
.encode(&mut request)
.unwrap();
request.freeze()
}
fn list_write(instance: u32, list: &[BACnetDestination]) -> WriteAccessSpecification {
WriteAccessSpecification {
object_identifier: nc(instance),
list_of_properties: vec![BACnetPropertyValue {
property_identifier: RECIPIENT_LIST,
property_array_index: None,
value: encoded(list),
priority: None,
}],
}
}
async fn save_started(started: std::sync::mpsc::Receiver<()>) {
tokio::task::spawn_blocking(move || started.recv_timeout(WAIT))
.await
.unwrap()
.expect("the save started");
}
async fn comes_to_hold(storage: &Arc<ClassStorage>, expected: NotificationClassSnapshot) -> bool {
let storage = Arc::clone(storage);
tokio::task::spawn_blocking(move || {
let deadline = std::time::Instant::now() + WAIT;
while std::time::Instant::now() < deadline {
if storage.load_saved().as_ref() == Some(&expected) {
return true;
}
std::thread::sleep(Duration::from_millis(5));
}
false
})
.await
.unwrap()
}
pub(super) async fn stop_then_release(
mut server: BACnetServer<TestTransport>,
go: std::sync::mpsc::Sender<()>,
) -> BACnetServer<TestTransport> {
let requests = Arc::clone(&server.request_tasks);
let stopping = tokio::spawn(async move {
server.stop().await.unwrap();
server
});
tokio::time::timeout(WAIT, async {
while !requests.is_empty() {
tokio::task::yield_now().await;
}
})
.await
.expect("stop() joined the request");
assert!(
!stopping.is_finished(),
"stop() returned before the save ran"
);
drop(go);
stopping.await.unwrap()
}
async fn served(db: &Arc<RwLock<ObjectDatabase>>, instance: u32) -> PropertyValue {
db.read()
.await
.get(&nc(instance))
.unwrap()
.read_property(RECIPIENT_LIST, None)
.unwrap()
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn a_server_stopped_mid_request_leaves_storage_with_the_served_lists() {
let landed = holding(&[destination(1)]);
let held = holding(&[destination(2)]);
let (server, inbound) = server(&[&landed, &held]).await;
let (started, go) = held.hold();
let request = wpm(vec![
list_write(1, &[destination(11)]),
list_write(2, &[destination(12)]),
]);
send(
&inbound,
ConfirmedServiceChoice::WRITE_PROPERTY_MULTIPLE,
request,
)
.await;
save_started(started).await;
assert_eq!(landed.load_saved(), Some(snapshot(&[destination(11)])));
let server = stop_then_release(server, go).await;
assert_eq!(landed.load_saved(), Some(snapshot(&[destination(1)])));
assert_eq!(held.load_saved(), Some(snapshot(&[destination(2)])));
let db = Arc::clone(server.database());
let framed = |list: &[BACnetDestination]| PropertyValue::ApplicationData(encoded(list));
assert_eq!(served(&db, 1).await, framed(&[destination(1)]));
assert_eq!(served(&db, 2).await, framed(&[destination(2)]));
drop(db);
drop(server);
assert_eq!(class(1, &landed).recipient_list(), [destination(1)]);
assert_eq!(class(2, &held).recipient_list(), [destination(2)]);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn a_stop_while_the_application_holds_the_database_puts_storage_back_once_it_lets_go() {
let storage = holding(&[destination(1)]);
let (mut server, inbound) = server(&[&storage]).await;
let (started, go) = storage.hold();
send(
&inbound,
ConfirmedServiceChoice::WRITE_PROPERTY,
write_property(1, &[destination(11)]),
)
.await;
save_started(started).await;
let db = Arc::clone(server.database());
let reading = db.read().await;
drop(go);
assert!(comes_to_hold(&storage, snapshot(&[destination(11)])).await);
server.stop().await.unwrap();
assert_eq!(storage.load_saved(), Some(snapshot(&[destination(11)])));
drop(reading);
assert!(
comes_to_hold(&storage, snapshot(&[destination(1)])).await,
"storage kept a list the class never served"
);
assert_eq!(
served(&db, 1).await,
PropertyValue::ApplicationData(encoded(&[destination(1)]))
);
}
async fn until_warned(db: &Arc<RwLock<ObjectDatabase>>, count: usize) -> Duration {
let mut waited = Duration::ZERO;
while slow_save_warnings(db) < count {
assert!(waited < Duration::from_secs(60), "stop() never warned");
tokio::time::advance(Duration::from_secs(1)).await;
waited += Duration::from_secs(1);
}
waited
}
#[tokio::test(start_paused = true)]
async fn stop_warns_while_storage_holds_a_save_and_waits_for_it() {
let storage = holding(&[destination(1)]);
let (server, inbound) = server(&[&storage]).await;
let (started, go) = storage.hold();
send(
&inbound,
ConfirmedServiceChoice::WRITE_PROPERTY,
write_property(1, &[destination(11)]),
)
.await;
save_started(started).await;
let db = Arc::clone(server.database());
let stopping = tokio::spawn(async move {
let mut server = server;
server.stop().await.unwrap();
server
});
assert!(until_warned(&db, 1).await >= SLOW_SAVE_WARNING);
let again = until_warned(&db, 2).await;
assert!(again >= SLOW_SAVE_REPEAT - Duration::from_secs(1));
assert!(!stopping.is_finished());
drop(go);
let server = stopping.await.unwrap();
assert_eq!(storage.load_saved(), Some(snapshot(&[destination(1)])));
drop(server);
}
#[path = "durable_drop_tests.rs"]
mod drop_tests;