use std::collections::{HashMap, HashSet};
use std::sync::{Arc, Mutex, MutexGuard, PoisonError, Weak};
use std::time::Duration;
use bacnet_ops::watch::{Changes, Lease, Point, WatchError};
use bacnet_transport::port::TransportPort;
use rmcp::ErrorData;
use rmcp::service::SubscriptionContext;
use tokio::sync::watch;
use tokio::time::Instant;
use crate::context::{Ctx, Listening};
use crate::errors::ToolError;
use crate::follow::{Follow, Wake};
use crate::resources::uri::{self, Uri};
pub(crate) const SHUTTING_DOWN: &str = "shutting-down";
const UNCLAIMED: Duration = Duration::from_secs(30);
const MAX_PENDING: usize = 16;
pub(crate) const MAX_STREAMS: usize = 4;
pub(crate) const MAX_DEVICE_PAGES: usize = 16;
pub(crate) const TOOL_IDLE: Duration = Duration::from_secs(600);
pub(crate) const ALARMS_EVERY: Duration = Duration::from_secs(60);
pub(crate) struct Subs {
state: Mutex<SubsState>,
legacy_changed: watch::Sender<u64>,
}
impl Default for Subs {
fn default() -> Self {
Self {
state: Mutex::default(),
legacy_changed: watch::Sender::new(0),
}
}
}
#[derive(Default)]
struct SubsState {
held: HashMap<Point, usize>,
pending: Vec<Pending>,
tool: HashMap<Point, (Instant, Lease)>,
legacy: HashMap<String, Option<Hold>>,
alarm_uris: usize,
closed: bool,
}
pub(crate) struct Hold {
subs: Weak<Subs>,
point: Point,
lease: Lease,
}
impl Drop for Hold {
fn drop(&mut self) {
if let Some(subs) = self.subs.upgrade() {
subs.release(&[self.point]);
}
}
}
struct Pending {
at: Instant,
uris: Vec<String>,
claim: Claim,
}
pub(crate) struct Claim {
subs: Arc<Subs>,
points: Vec<Point>,
leases: Vec<(String, Lease)>,
alarms: usize,
}
impl Drop for Claim {
fn drop(&mut self) {
let mut state = self.subs.lock();
state.release(&self.points);
state.alarm_uris = state.alarm_uris.saturating_sub(self.alarms);
}
}
impl SubsState {
fn release(&mut self, points: &[Point]) {
for point in points {
if let Some(count) = self.held.get_mut(point) {
*count -= 1;
if *count == 0 {
self.held.remove(point);
}
}
}
}
}
impl Subs {
fn lock(&self) -> MutexGuard<'_, SubsState> {
self.state.lock().unwrap_or_else(PoisonError::into_inner)
}
pub(crate) fn held(&self) -> usize {
self.lock().held.len()
}
fn release(&self, points: &[Point]) {
self.lock().release(points);
}
pub(crate) fn take<T: TransportPort + 'static>(
&self,
ctx: &Ctx<T>,
point: Point,
) -> Result<Lease, &'static str> {
let hub = ctx.hub.as_ref().ok_or("passive")?;
let mut state = self.lock();
if state.closed {
return Err(SHUTTING_DOWN);
}
if !state.held.contains_key(&point) && state.held.len() >= ctx.policy.caps.watches {
return Err("principal-cap");
}
let lease = hub.watch(point).map_err(|e| match e {
WatchError::Shut => SHUTTING_DOWN,
WatchError::Full { .. } => "process-cap",
})?;
*state.held.entry(point).or_default() += 1;
Ok(lease)
}
pub(crate) fn hold<T: TransportPort + 'static>(
self: &Arc<Self>,
ctx: &Ctx<T>,
point: Point,
) -> Result<Hold, &'static str> {
let lease = self.take(ctx, point)?;
Ok(Hold {
subs: Arc::downgrade(self),
point,
lease,
})
}
pub(crate) fn legacy_has(&self, uri: &str) -> bool {
self.lock().legacy.contains_key(uri)
}
pub(crate) fn legacy_add(
&self,
uri: &str,
hold: Option<Hold>,
max: usize,
) -> Result<(), &'static str> {
let mut state = self.lock();
let added = if state.closed {
Err(SHUTTING_DOWN)
} else if state.legacy.contains_key(uri) {
Ok(false)
} else if state.legacy.len() + state.alarm_uris >= max {
Err("principal-cap")
} else {
state.legacy.insert(uri.to_string(), hold);
Ok(true)
};
drop(state);
if added? {
self.legacy_changed.send_modify(|n| *n = n.wrapping_add(1));
}
Ok(())
}
pub(crate) fn legacy_remove(&self, uri: &str) {
let removed = self.lock().legacy.remove(uri);
if removed.is_some() {
drop(removed);
self.legacy_changed.send_modify(|n| *n = n.wrapping_add(1));
}
}
pub(crate) fn legacy_snapshot(&self) -> Vec<(String, Option<Changes>)> {
self.lock()
.legacy
.iter()
.map(|(uri, hold)| (uri.clone(), hold.as_ref().map(|h| h.lease.changes())))
.collect()
}
pub(crate) fn legacy_count(&self) -> usize {
self.lock().legacy.len()
}
pub(crate) fn uris(&self) -> usize {
let state = self.lock();
state.legacy.len() + state.alarm_uris
}
fn take_alarm_uri(&self, max: usize) -> bool {
let mut state = self.lock();
let room = !state.closed && state.legacy.len() + state.alarm_uris < max;
if room {
state.alarm_uris += 1;
}
room
}
pub(crate) fn legacy_watch(&self) -> watch::Receiver<u64> {
self.legacy_changed.subscribe()
}
pub(crate) fn expire_tool(&self, idle: Duration) {
let expired: Vec<(Point, Lease)> = {
let mut state = self.lock();
let now = Instant::now();
let old: Vec<Point> = state
.tool
.iter()
.filter(|(_, (at, _))| now.saturating_duration_since(*at) >= idle)
.map(|(point, _)| *point)
.collect();
old.into_iter()
.filter_map(|point| state.tool.remove(&point).map(|(_, l)| (point, l)))
.collect()
};
let points: Vec<Point> = expired.iter().map(|(p, _)| *p).collect();
self.release(&points);
}
pub(crate) fn tool_watch<T: TransportPort + 'static>(
&self,
ctx: &Ctx<T>,
point: Point,
) -> Result<(), &'static str> {
let now = Instant::now();
if let Some((at, _)) = self.lock().tool.get_mut(&point) {
*at = now;
return Ok(());
}
let lease = self.take(ctx, point)?;
let mut state = self.lock();
if state.closed {
drop(state);
self.release(&[point]);
return Err(SHUTTING_DOWN);
}
if let Some((at, _)) = state.tool.get_mut(&point) {
*at = now;
drop(state);
self.release(&[point]);
return Ok(());
}
state.tool.insert(point, (now, lease));
Ok(())
}
pub(crate) fn reserve<T: TransportPort + 'static>(
self: &Arc<Self>,
ctx: &Ctx<T>,
requested: &[String],
) -> Vec<String> {
self.prune();
if *ctx.listening.borrow() >= MAX_STREAMS {
return Vec::new();
}
let max_uris = crate::legacy::max_uris(ctx);
let mut accepted = Vec::new();
let mut claim = Claim {
subs: Arc::clone(self),
points: Vec::new(),
leases: Vec::new(),
alarms: 0,
};
let mut seen = HashSet::new();
let mut pages = 0;
for text in requested {
if !seen.insert(text.as_str()) {
continue;
}
let honoured = match uri::parse(text) {
Ok(Uri::Devices | Uri::DevicesAfter(_)) if ctx.hub.is_some() => {
pages += 1;
pages <= MAX_DEVICE_PAGES
}
Ok(Uri::Point(point)) if ctx.reach_known(point.device) => {
match self.take(ctx, point) {
Ok(lease) => {
claim.points.push(point);
claim.leases.push((text.clone(), lease));
true
}
Err(_) => false,
}
}
Ok(Uri::Alarms) => ctx.hub.is_some(),
Ok(Uri::DeviceAlarms(device)) => {
let taken = ctx.hub.is_some()
&& ctx.reach_known(device)
&& self.take_alarm_uri(max_uris);
claim.alarms += usize::from(taken);
taken
}
Ok(Uri::Session | Uri::Ledger) => true,
Ok(Uri::Device(_) | Uri::Point(_) | Uri::Devices | Uri::DevicesAfter(_))
| Err(_) => false,
};
if honoured {
accepted.push(text.clone());
}
}
if !accepted.is_empty() {
let mut state = self.lock();
if state.closed {
drop(state);
drop(claim);
return Vec::new();
}
let mut oldest = Vec::new();
while state.pending.len() >= MAX_PENDING {
oldest.push(state.pending.remove(0));
}
state.pending.push(Pending {
at: Instant::now(),
uris: accepted.clone(),
claim,
});
drop(state);
drop(oldest);
}
accepted
}
pub(crate) fn claim(&self, uris: &[String]) -> Option<Claim> {
let mut state = self.lock();
let at = state.pending.iter().position(|p| p.uris == uris)?;
Some(state.pending.remove(at).claim)
}
pub(crate) fn prune(&self) {
let stale: Vec<Pending> = {
let mut state = self.lock();
let now = Instant::now();
let (old, kept) = std::mem::take(&mut state.pending)
.into_iter()
.partition(|p| now.saturating_duration_since(p.at) >= UNCLAIMED);
state.pending = kept;
old
};
drop(stale);
}
pub(crate) fn clear(&self) {
let (pending, legacy, tool) = {
let mut state = self.lock();
state.closed = true;
(
std::mem::take(&mut state.pending),
std::mem::take(&mut state.legacy),
std::mem::take(&mut state.tool),
)
};
drop(pending);
if !legacy.is_empty() {
drop(legacy);
self.legacy_changed.send_modify(|n| *n = n.wrapping_add(1));
}
let points: Vec<Point> = tool.keys().copied().collect();
drop(tool);
self.release(&points);
}
}
fn accepted(context: &SubscriptionContext) -> Vec<String> {
let mut seen = HashSet::new();
context
.accepted()
.resource_subscriptions
.iter()
.flatten()
.filter(|uri| seen.insert(uri.as_str()))
.cloned()
.collect()
}
pub(crate) fn open<'a, T: TransportPort + 'static>(
ctx: &'a Ctx<T>,
uris: &[String],
) -> Result<(Listening<'a>, Option<Claim>), ErrorData> {
let Some(listening) = ctx.listening(MAX_STREAMS) else {
drop(ctx.subs.claim(uris));
let error = ToolError::refused(
"stream-cap",
format!(
"this session runs {MAX_STREAMS} listen streams, as many as it may; \
cancel one first"
),
);
return Err(ErrorData::invalid_params(
error.summary(),
Some(error.json()),
));
};
Ok((listening, ctx.subs.claim(uris)))
}
pub(crate) async fn run<T: TransportPort + 'static>(
ctx: &Ctx<T>,
context: SubscriptionContext,
) -> Result<(), ErrorData> {
let uris = accepted(&context);
let (_listening, claim) = open(ctx, &uris)?;
if claim.is_none() && !uris.is_empty() {
tracing::warn!(
"a listen stream found no watches set aside for it; its points won't update"
);
}
let leases = claim.as_ref().map_or(&[][..], |c| &c.leases[..]);
let mut follow = Follow::new();
follow.set(
ctx.engine.as_deref(),
uris.iter()
.map(|name| {
let changes = leases
.iter()
.find(|(held, _)| held == name)
.map(|(_, lease)| lease.changes());
(name.clone(), changes)
})
.collect(),
);
let sink = context.sink().clone();
tracing::debug!(
streams = uris.len(),
quiet = follow.quiet(),
"listen stream opened"
);
loop {
let wake = tokio::select! {
biased;
() = context.cancelled() => return Ok(()),
() = ctx.closing.cancelled() => return Ok(()),
wake = follow.next() => wake,
};
match wake {
Wake::Updated(name) => {
if sink.notify_resource_updated(name).await.is_err() {
return Ok(());
}
}
Wake::Devices => {
for name in follow.device_uris() {
if sink.notify_resource_updated(name.as_str()).await.is_err() {
return Ok(());
}
}
}
Wake::Alarms => {
for name in follow.alarm_uris() {
if crate::resources::refresh_alarms(ctx, name).await
&& sink.notify_resource_updated(name.as_str()).await.is_err()
{
return Ok(());
}
}
}
}
}
}