use super::request_admission::{Admission, Class, Rejection};
use super::{RequestAdmissionCounters, RequestAdmissionPolicy};
use crate::command_lists::Unfinished;
use bacnet_types::primitives::ObjectIdentifier;
use std::future::{Future, poll_fn};
use std::pin::Pin;
use std::sync::{Arc, Mutex, Weak};
use std::time::Duration;
use tokio::task::{JoinError, JoinSet};
use tokio::time::{Instant, Sleep};
const REAP_IDLE: Duration = Duration::from_millis(100);
pub(super) struct IdleReap(Pin<Box<Sleep>>);
impl IdleReap {
pub(super) fn new() -> Self {
Self(Box::pin(tokio::time::sleep(REAP_IDLE)))
}
pub(super) async fn tick(&mut self) {
self.0.as_mut().await;
self.0.as_mut().reset(Instant::now() + REAP_IDLE);
}
}
pub(super) struct RequestTasks(
Mutex<State>,
Admission,
Option<super::dcc_disable_rate::Bucket>,
Mutex<Vec<Unfinished>>,
);
impl Default for RequestTasks {
fn default() -> Self {
Self::new(RequestAdmissionPolicy::default()).expect("valid defaults")
}
}
#[derive(Default)]
struct State {
audit_owner: Option<Weak<bacnet_objects::database::AuditOwnership>>,
closed: bool,
tasks: JoinSet<()>,
}
#[derive(Clone)]
pub(super) struct RequestTaskSpawner(Weak<RequestTasks>);
impl RequestTaskSpawner {
pub(super) fn admit_dcc_disable(&self) -> bool {
self.0.upgrade().is_some_and(|owner| {
owner
.2
.as_ref()
.is_none_or(super::dcc_disable_rate::Bucket::admit)
})
}
pub(super) fn spawn(&self, task: impl Future<Output = ()> + Send + 'static) {
if let Some(owner) = self.0.upgrade() {
owner.spawn(task);
}
}
pub(super) fn strand(&self, left: Unfinished) -> Result<(), Unfinished> {
match self.0.upgrade() {
Some(owner) => {
owner.3.lock().unwrap().push(left);
Ok(())
}
None => Err(left),
}
}
pub(super) fn unstrand(&self, source: ObjectIdentifier, generation: u64) -> Option<Unfinished> {
let owner = self.0.upgrade()?;
let mut stranded = owner.3.lock().unwrap();
let at = stranded
.iter()
.position(|left| left.is(source, generation))?;
Some(stranded.swap_remove(at))
}
}
impl RequestTasks {
#[cfg(test)]
pub(super) fn peer_entries(&self) -> [usize; 3] {
self.1.peer_entries()
}
pub(super) fn for_server(
config: &super::ServerConfig,
) -> Result<Arc<Self>, bacnet_types::error::Error> {
let mut tasks = Self::new(config.request_admission_policy)?;
config.read_property_multiple_budget.validate()?;
config.validate_dcc_config()?;
config.time_sync_policy.validate()?;
config.get_alarm_summary_budget.validate()?;
config.get_enrollment_summary_budget.validate()?;
config.atomic_read_file_budget.validate()?;
config.atomic_write_file_budget.validate()?;
config.read_range_budget.validate()?;
config.get_event_information_budget.validate()?;
config.cov_policy.validate()?;
tasks.2 = config
.dcc_disable_rate_limit
.map(super::dcc_disable_rate::Bucket::new)
.transpose()?;
Ok(Arc::new(tasks))
}
pub(super) fn new(policy: RequestAdmissionPolicy) -> Result<Self, bacnet_types::error::Error> {
Ok(Self(
Mutex::new(State::default()),
Admission::new(policy)?,
None,
Mutex::default(),
))
}
pub(super) fn take_stranded(&self) -> Vec<Unfinished> {
std::mem::take(&mut *self.3.lock().unwrap())
}
pub(super) fn counters(&self) -> RequestAdmissionCounters {
self.1.snapshot()
}
pub(super) fn try_spawn<F: Future<Output = ()> + Send + 'static>(
&self,
class: Class,
peer: super::request_peer::CanonicalRequester,
make: impl FnOnce() -> F,
) -> Result<(), Rejection> {
let mut state = self.0.lock().unwrap();
let guard = self.1.try_enter(class, peer, state.closed)?;
let task = Box::pin(make());
let owner = state.audit_owner.as_ref().and_then(Weak::upgrade);
state.tasks.spawn(async move {
let _owner = owner;
let _guard = guard;
task.await;
});
Ok(())
}
pub(super) fn spawner(self: &Arc<Self>) -> RequestTaskSpawner {
RequestTaskSpawner(Arc::downgrade(self))
}
pub(super) fn set_audit_owner(&self, owner: &Arc<bacnet_objects::database::AuditOwnership>) {
self.0.lock().unwrap().audit_owner = Some(Arc::downgrade(owner));
}
pub(super) fn spawn(&self, task: impl Future<Output = ()> + Send + 'static) {
let mut state = self.0.lock().unwrap();
if !state.closed {
let owner = state.audit_owner.as_ref().and_then(Weak::upgrade);
state.tasks.spawn(async move {
let _owner = owner;
task.await;
});
}
}
pub(super) fn spawn_on(
&self,
task: impl Future<Output = ()> + Send + 'static,
handle: &tokio::runtime::Handle,
) {
let mut state = self.0.lock().unwrap();
if !state.closed {
let owner = state.audit_owner.as_ref().and_then(Weak::upgrade);
state.tasks.spawn_on(
async move {
let _owner = owner;
task.await;
},
handle,
);
}
}
pub(super) fn close(&self) {
let mut state = self.0.lock().unwrap();
state.closed = true;
state.tasks.abort_all();
}
pub(super) fn is_empty(&self) -> bool {
self.0.lock().unwrap().tasks.is_empty()
}
pub(super) fn reap_finished(&self) {
loop {
let Some(result) = self.0.lock().unwrap().tasks.try_join_next() else {
return;
};
Self::observe(Some(result));
}
}
pub(super) async fn join_next(&self) -> Option<Result<(), JoinError>> {
poll_fn(|cx| self.0.lock().unwrap().tasks.poll_join_next(cx)).await
}
pub(super) fn observe(result: Option<Result<(), JoinError>>) {
if let Some(Err(error)) = result
&& !error.is_cancelled()
{
tracing::warn!(%error, "Inbound request handler failed");
}
}
}
#[cfg(test)]
mod audit_lifetime_tests {
use super::*;
#[tokio::test]
async fn admitted_request_retains_audit_membership_until_its_frame_is_destroyed() {
use bacnet_objects::database::AuditOwnership;
use bacnet_types::{enums::ObjectType, primitives::ObjectIdentifier};
let owner = AuditOwnership::for_source(
ObjectIdentifier::new(ObjectType::DEVICE, 10).unwrap(),
ObjectIdentifier::new(ObjectType::AUDIT_REPORTER, 1).unwrap(),
);
let weak = Arc::downgrade(&owner);
let requests = RequestTasks::default();
requests.set_audit_owner(&owner);
requests
.try_spawn(
Class::Confirmed,
super::super::request_peer::CanonicalRequester::Direct(
bacnet_types::MacAddr::from_slice(&[1]),
),
std::future::pending::<()>,
)
.unwrap();
owner.seal();
requests.close();
drop(owner);
assert!(
weak.upgrade().is_some(),
"abort request is not task-frame quiescence"
);
while requests.join_next().await.is_some() {}
assert!(weak.upgrade().is_none());
}
}