use std::future::Future;
use std::sync::Arc;
use std::time::Duration;
use bacnet_objects::command::{CommandRun, RunPlan, WriteFailure};
use bacnet_objects::database::ObjectDatabase;
use bacnet_objects::traits::BACnetObject;
use bacnet_types::constructed::{BACnetActionCommand, BACnetDeviceObjectPropertyReference};
use bacnet_types::error::Error;
use bacnet_types::primitives::{ObjectIdentifier, PropertyValue};
use tokio::sync::RwLock;
use tracing::{debug, warn};
use crate::server::RemoteRequestError;
mod chain;
mod channel;
mod remote_slots;
mod taken;
mod target;
mod unattached;
pub(crate) use chain::admit;
pub(crate) use remote_slots::RemoteSlots;
pub(crate) use taken::TakenRuns;
pub(crate) use unattached::run_unattached;
pub(crate) trait RunHost: Sync {
fn database(&self) -> &Arc<RwLock<ObjectDatabase>>;
fn write(
&self,
run: &CommandRun,
command: &BACnetActionCommand,
) -> impl Future<Output = Result<(), Error>> + Send;
fn write_remote(
&self,
device: ObjectIdentifier,
command: &BACnetActionCommand,
) -> impl Future<Output = Result<(), RemoteRequestError>> + Send;
fn read_remote(
&self,
device: ObjectIdentifier,
reference: &BACnetDeviceObjectPropertyReference,
) -> impl Future<Output = Result<PropertyValue, RemoteRequestError>> + Send;
fn remote_slots(&self) -> Option<&RemoteSlots>;
fn committed(
&self,
db: &ObjectDatabase,
source: ObjectIdentifier,
) -> impl Future<Output = ()> + Send;
fn report(&self, source: ObjectIdentifier) -> impl Future<Output = ()> + Send;
fn abandoned(&self, left: Unfinished);
}
#[derive(Debug, Clone, Copy)]
pub(crate) struct Unfinished {
source: ObjectIdentifier,
generation: u64,
next: usize,
len: usize,
outcome: Result<(), WriteFailure>,
}
impl Unfinished {
pub(crate) fn start(run: &CommandRun) -> Self {
Self {
source: run.source,
generation: run.generation,
next: 0,
len: match &run.plan {
RunPlan::Actions(commands) => commands.len(),
RunPlan::Channel(distribution) => distribution.members.len(),
},
outcome: Ok(()),
}
}
pub(crate) fn source(&self) -> ObjectIdentifier {
self.source
}
pub(crate) fn is(&self, source: ObjectIdentifier, generation: u64) -> bool {
self.source == source && self.generation == generation
}
pub(crate) fn end(self, db: &mut ObjectDatabase) -> bool {
let outcome = if self.next < self.len {
self.outcome.and(Err(WriteFailure::Process))
} else {
self.outcome
};
db.get_mut(&self.source).is_some_and(|object| {
for index in self.next..self.len {
object.record_command_write_internal(self.generation, index, false);
}
object.complete_command_run_internal(self.generation, outcome)
})
}
}
pub(crate) fn end_when_free(db: &Arc<RwLock<ObjectDatabase>>, left: Unfinished) {
when_free(db, move |db| {
left.end(db);
});
}
pub(crate) fn end_unowned(db: &Arc<RwLock<ObjectDatabase>>, stranded: Vec<Unfinished>) {
when_free(db, move |db| {
for left in stranded {
left.end(db);
}
end_ownerless(db);
});
}
fn when_free(
db: &Arc<RwLock<ObjectDatabase>>,
end: impl FnOnce(&mut ObjectDatabase) + Send + 'static,
) {
if let Ok(mut db) = db.try_write() {
end(&mut db);
return;
}
match tokio::runtime::Handle::try_current() {
Ok(runtime) => {
let db = Arc::clone(db);
runtime.spawn(async move {
let mut waiting = Waiting(Some(
"runtime dropped the task ending runs let go of; their objects stay busy",
));
end(&mut *db.write().await);
waiting.0 = None;
drop(crate::server::drop_database_off_runtime(db));
});
}
Err(_) => warn!("runs let go of outside a runtime; their objects stay busy"),
}
}
pub(crate) struct Waiting(pub(crate) Option<&'static str>);
impl Drop for Waiting {
fn drop(&mut self) {
if let Some(undone) = self.0 {
warn!("{undone}");
}
}
}
fn end_ownerless(db: &mut ObjectDatabase) {
db.for_each_object_mut(|oid, object| {
if end_ownerless_object(object) {
debug!(source = %oid, "Ending a run nothing owns any more");
}
});
}
fn end_ownerless_object(object: &mut dyn BACnetObject) -> bool {
drop(object.take_command_run_internal());
let Some(generation) = object.command_generation_internal() else {
return false;
};
let mut index = 0;
while object.record_command_write_internal(generation, index, false) {
index += 1;
}
object.complete_command_run_internal(generation, Err(WriteFailure::Process))
}
fn take_queued(db: &mut ObjectDatabase, oids: &[ObjectIdentifier]) -> Vec<CommandRun> {
oids.iter()
.filter_map(|oid| {
db.get_mut(oid)
.and_then(|object| object.take_command_run_internal())
})
.collect()
}
pub(crate) fn end_unmade(db: &mut ObjectDatabase, oids: &[ObjectIdentifier]) {
for run in take_queued(db, oids) {
debug!(
source = %run.source,
"Ending a run without its writes: this path can't make them"
);
Unfinished::start(&run).end(db);
}
}
pub(crate) fn execute<H: RunHost>(
host: &H,
run: CommandRun,
) -> impl Future<Output = ()> + Send + use<'_, H> {
let mut owner = Owner {
host,
left: Some(Unfinished::start(&run)),
};
async move {
let ended = match &run.plan {
RunPlan::Actions(commands) => run_actions(host, &run, commands, &mut owner).await,
RunPlan::Channel(distribution) => {
channel::distribute(host, &run, distribution, &mut owner).await
}
};
if let Some(outcome) = ended {
complete(host, run.source, run.generation, outcome).await;
}
owner.release();
}
}
async fn run_actions<H: RunHost>(
host: &H,
run: &CommandRun,
commands: &[BACnetActionCommand],
owner: &mut Owner<'_, H>,
) -> Option<Result<(), WriteFailure>> {
let mut outcome = Ok(());
for (index, command) in commands.iter().enumerate() {
let made = make(host, run, index, command).await?;
outcome = outcome.and(made);
owner.progress(index + 1, outcome);
if let Some(delay) = command.post_delay {
tokio::time::sleep(Duration::from_secs(u64::from(delay))).await;
}
if made.is_err() && command.quit_on_failure {
break;
}
}
Some(outcome)
}
struct Owner<'h, H: RunHost> {
host: &'h H,
left: Option<Unfinished>,
}
impl<H: RunHost> Owner<'_, H> {
fn progress(&mut self, next: usize, outcome: Result<(), WriteFailure>) {
if let Some(left) = &mut self.left {
left.next = next;
left.outcome = outcome;
}
}
fn release(&mut self) {
self.left = None;
}
}
impl<H: RunHost> Drop for Owner<'_, H> {
fn drop(&mut self) {
if let Some(left) = self.left.take() {
self.host.abandoned(left);
}
}
}
async fn make<H: RunHost>(
host: &H,
run: &CommandRun,
index: usize,
command: &BACnetActionCommand,
) -> Option<Result<(), WriteFailure>> {
let remote = {
let db = host.database().read().await;
if db
.get(&run.source)
.and_then(|object| object.command_generation_internal())
!= Some(run.generation)
{
return None;
}
if db.local_device().is_local(command.device_identifier) {
None
} else {
command.device_identifier
}
};
let _slot = match (remote, host.remote_slots()) {
(Some(device), Some(slots)) => Some(slots.acquire(device).await),
_ => None,
};
let made = target::write(host, run, remote, command)
.await
.map_err(|failed| failed.failure);
let recorded = {
let mut db = host.database().write().await;
let recorded = db.get_mut(&run.source).is_some_and(|object| {
object.record_command_write_internal(run.generation, index, made.is_ok())
});
if recorded {
host.committed(&db, run.source).await;
}
recorded
};
if !recorded {
return None;
}
host.report(run.source).await;
Some(made)
}
pub(crate) async fn complete<H: RunHost>(
host: &H,
source: ObjectIdentifier,
generation: u64,
outcome: Result<(), WriteFailure>,
) -> bool {
let completed = {
let mut db = host.database().write().await;
let completed = db
.get_mut(&source)
.is_some_and(|object| object.complete_command_run_internal(generation, outcome));
if completed {
host.committed(&db, source).await;
}
completed
};
if completed {
host.report(source).await;
}
completed
}