use super::*;
use crate::server::drop_database_off_runtime;
use bacnet_objects::audit::AuditReporterObject;
use bacnet_objects::durable::{PendingWrite, StageStep};
use bacnet_objects::traits::BACnetObject;
use bacnet_services::device_mgmt::DeviceCommunicationControlRequest;
use bacnet_types::constructed::BACnetRecipient;
use bacnet_types::enums::EnableDisable;
use std::sync::Weak;
use std::sync::mpsc as std_mpsc;
use std::thread::ThreadId;
struct Reporting {
storage: Arc<ClassStorage>,
dropped: std_mpsc::Sender<ThreadId>,
}
impl NotificationClassPersistence for Reporting {
fn load(&self, _class: ObjectIdentifier) -> Result<Option<NotificationClassSnapshot>, Error> {
Ok(self.storage.load_saved())
}
fn save(
&self,
_class: ObjectIdentifier,
snapshot: &NotificationClassSnapshot,
) -> Result<(), Error> {
self.storage.store(snapshot)
}
}
impl Drop for Reporting {
fn drop(&mut self) {
let _ = self.dropped.send(std::thread::current().id());
}
}
fn reporting_class(
storage: &Arc<ClassStorage>,
) -> (NotificationClass, std_mpsc::Receiver<ThreadId>) {
let (dropped, dropped_on) = std_mpsc::channel();
let persistence = Arc::new(Reporting {
storage: Arc::clone(storage),
dropped,
});
let class = NotificationClass::with_persistence(
1,
"NC-1",
persistence as Arc<dyn NotificationClassPersistence>,
)
.unwrap();
(class, dropped_on)
}
async fn serving(
class: NotificationClass,
dcc_audit: bool,
) -> (BACnetServer<TestTransport>, mpsc::Sender<ReceivedNpdu>) {
let (transport, inbound) = TestTransport::inbound(4);
let mut db = ObjectDatabase::new();
let mut device = DeviceObject::new(DeviceConfig {
identity: bacnet_objects::DeviceIdentity::new(100, 0).expect("valid Device identity"),
..DeviceConfig::default()
})
.unwrap();
let mut builder = BACnetServer::generic_builder()
.device(bacnet_objects::DeviceIdentity::new(100, 0).unwrap())
.transport(transport)
.enable_event_enrollment(false);
if dcc_audit {
let logger = ObjectIdentifier::new(ObjectType::DEVICE, 200).unwrap();
device
.provision_audit_recipient(BACnetRecipient::Device(logger))
.unwrap();
let reporter = AuditReporterObject::new(1, "reporter").unwrap();
builder = builder
.audit_reporters(AuditReportersConfig {
reporters: vec![reporter.object_identifier()],
})
.dcc_policy(DccPolicy::LegacyPermissive)
.device_binding(DeviceBinding::local(logger, [2]).unwrap())
.unwrap();
db.add(Box::new(reporter)).unwrap();
}
db.add(Box::new(device)).unwrap();
db.add(Box::new(class)).unwrap();
let server = builder.database(db).build().await.unwrap();
(server, inbound)
}
async fn disable_initiation(
server: &BACnetServer<TestTransport>,
inbound: &mpsc::Sender<ReceivedNpdu>,
) {
let mut request = BytesMut::new();
DeviceCommunicationControlRequest {
time_duration: Some(60),
enable_disable: EnableDisable::DISABLE_INITIATION,
password: None,
}
.encode(&mut request)
.unwrap();
let service = ConfirmedServiceChoice::DEVICE_COMMUNICATION_CONTROL;
send(inbound, service, request.freeze()).await;
tokio::time::timeout(WAIT, async {
while server.comm_state() != DccState::DisableInitiation {
tokio::task::yield_now().await;
}
})
.await
.expect("the disable took effect");
}
async fn stage_held_save(
storage: &Arc<ClassStorage>,
inbound: &mpsc::Sender<ReceivedNpdu>,
) -> std_mpsc::Sender<()> {
let (started, go) = storage.hold();
let request = write_property(1, &[destination(11)]);
send(inbound, ConfirmedServiceChoice::WRITE_PROPERTY, request).await;
save_started(started).await;
go
}
fn watchdog(go: std_mpsc::Sender<()>) -> (std_mpsc::Sender<()>, std::thread::JoinHandle<bool>) {
let (progress, progressed) = std_mpsc::channel::<()>();
let watchdog = std::thread::spawn(move || {
let stalled = progressed.recv_timeout(WAIT).is_err();
drop(go);
stalled
});
(progress, watchdog)
}
async fn thread_was_free(
weak: Weak<RwLock<ObjectDatabase>>,
progress: std_mpsc::Sender<()>,
watchdog: std::thread::JoinHandle<bool>,
) -> bool {
tokio::time::timeout(WAIT, async {
while weak.strong_count() > 0 {
tokio::task::yield_now().await;
}
})
.await
.expect("every handle on the database went");
tokio::spawn(async move {
let _ = progress.send(());
})
.await
.unwrap();
let stalled = tokio::task::spawn_blocking(move || watchdog.join().unwrap())
.await
.unwrap();
!stalled
}
async fn dropped_off_the_runtime(
dropped_on: std_mpsc::Receiver<ThreadId>,
storage: &Arc<ClassStorage>,
) {
let thread = tokio::task::spawn_blocking(move || dropped_on.recv_timeout(WAIT))
.await
.unwrap()
.expect("the database was dropped");
assert_ne!(thread, std::thread::current().id());
assert_eq!(storage.load_saved(), Some(snapshot(&[destination(1)])));
}
#[tokio::test]
async fn a_server_dropped_without_stop_lets_its_database_go_off_the_runtime() {
let storage = holding(&[destination(1)]);
let (class, dropped_on) = reporting_class(&storage);
let (server, inbound) = serving(class, false).await;
let go = stage_held_save(&storage, &inbound).await;
let weak = Arc::downgrade(server.database());
let (progress, watchdog) = watchdog(go);
drop(server);
assert!(
thread_was_free(weak, progress, watchdog).await,
"dropping the server held the runtime's thread"
);
dropped_off_the_runtime(dropped_on, &storage).await;
}
#[tokio::test]
async fn an_application_handle_let_go_of_last_drops_the_database_off_the_runtime() {
let storage = holding(&[destination(1)]);
let (class, dropped_on) = reporting_class(&storage);
let (server, inbound) = serving(class, false).await;
let db = Arc::clone(server.database());
let go = stage_held_save(&storage, &inbound).await;
drop(server);
tokio::time::timeout(WAIT, async {
while Arc::strong_count(&db) > 1 {
tokio::task::yield_now().await;
}
})
.await
.expect("the server let go of the database");
let weak = Arc::downgrade(&db);
let (progress, watchdog) = watchdog(go);
let dropping = drop_database_off_runtime(db);
assert!(
thread_was_free(weak, progress, watchdog).await,
"letting go of the last handle held the runtime's thread"
);
dropping
.expect("the last handle went to the blocking pool")
.await
.unwrap();
dropped_off_the_runtime(dropped_on, &storage).await;
}
#[tokio::test]
async fn the_tasks_a_stop_leaves_let_the_database_go_off_the_runtime() {
let storage = holding(&[destination(1)]);
let (class, dropped_on) = reporting_class(&storage);
let (mut server, inbound) = serving(class, false).await;
let go = stage_held_save(&storage, &inbound).await;
let reading = Arc::clone(server.database()).read_owned().await;
let weak = Arc::downgrade(server.database());
server.stop().await.unwrap();
drop(server);
let (progress, watchdog) = watchdog(go);
drop(reading);
assert!(
thread_was_free(weak, progress, watchdog).await,
"a task stop() left held the runtime's thread"
);
dropped_off_the_runtime(dropped_on, &storage).await;
}
#[tokio::test]
async fn a_settle_task_that_ends_last_drops_the_database_off_the_runtime() {
let storage = holding(&[destination(1)]);
let (class, dropped_on) = reporting_class(&storage);
let mut objects = ObjectDatabase::new();
objects.add(Box::new(class)).unwrap();
let db = Arc::new(RwLock::new(objects));
let (started, go) = storage.hold();
let step = db
.write()
.await
.get_mut(&nc(1))
.and_then(|class| class.durable_writes_internal())
.expect("the class saves")
.stage_writes(&[PendingWrite {
property: RECIPIENT_LIST,
array_index: None,
value: PropertyValue::ApplicationData(encoded(&[destination(11)])),
}]);
assert!(matches!(step, StageStep::Staged(_)));
save_started(started).await;
let reading = Arc::clone(&db).read_owned().await;
crate::server::durable_writes::settle_forgotten(&db).await;
let weak = Arc::downgrade(&db);
drop(db);
let (progress, watchdog) = watchdog(go);
drop(reading);
assert!(
thread_was_free(weak, progress, watchdog).await,
"the settle task held the runtime's thread"
);
dropped_off_the_runtime(dropped_on, &storage).await;
}
#[tokio::test]
async fn a_dcc_timer_the_drop_cannot_take_lets_go_of_the_database() {
let storage = holding(&[destination(1)]);
let (class, dropped_on) = reporting_class(&storage);
let (server, inbound) = serving(class, true).await;
disable_initiation(&server, &inbound).await;
let go = stage_held_save(&storage, &inbound).await;
let weak = Arc::downgrade(server.database());
let slot = Arc::clone(&server.dcc_timer);
let held = slot.lock().await;
let (progress, watchdog) = watchdog(go);
drop(server);
drop(held);
drop(slot);
assert!(
thread_was_free(weak, progress, watchdog).await,
"the DCC timer held the database"
);
dropped_off_the_runtime(dropped_on, &storage).await;
}