Skip to main content

liminal_server/agents/
retained.rs

1//! Retained metadata and live delivery share one lock. Each watcher has a
2//! real bounded library inbox, so its initial value is charged like a change.
3
4use std::collections::BTreeMap;
5use std::sync::atomic::{AtomicBool, Ordering};
6use std::sync::{Arc, Mutex, OnceLock, Weak};
7
8use liminal::channel::{
9    ChannelConfig, ChannelHandle, ChannelMode, ChannelSupervisor, ConnectionInboxBudget,
10    InboxInstall, Schema, SubscriptionHandle,
11};
12use liminal::envelope::PublisherId;
13use liminal::protocol::SchemaId;
14
15use super::config::AgentsConfig;
16use super::error::AgentError;
17use crate::server::connection::services::{ConnectionSubscription, SubscriptionResource};
18
19struct Watcher {
20    subject: String,
21    channel: ChannelHandle,
22    overflow: Arc<AtomicBool>,
23    wake: Option<liminal::channel::InboxNotifier>,
24}
25
26#[derive(Default)]
27struct Kept {
28    values: BTreeMap<String, serde_json::Value>,
29    watchers: BTreeMap<u64, Watcher>,
30}
31
32/// Holds only server metadata. No agent message or working stream reaches it.
33pub struct Retained {
34    bounds: AgentsConfig,
35    supervisor: ChannelSupervisor,
36    publisher: PublisherId,
37    kept: Mutex<Kept>,
38    console: OnceLock<Weak<crate::console::ConsoleHub>>,
39}
40
41impl std::fmt::Debug for Retained {
42    fn fmt(&self, out: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
43        out.debug_struct("Retained")
44            .field("bounds", &self.bounds)
45            .finish_non_exhaustive()
46    }
47}
48
49impl Retained {
50    /// Uses the existing console's signal, with no timer or second publisher.
51    ///
52    /// # Errors
53    /// A console was already installed.
54    pub fn bind_console(
55        &self,
56        console: &Arc<crate::console::ConsoleHub>,
57    ) -> Result<(), AgentError> {
58        self.console
59            .set(Arc::downgrade(console))
60            .map_err(|_| AgentError::Invalid("retained console already bound"))
61    }
62    /// Reads current metadata without consuming the watcher stream.
63    ///
64    /// # Errors
65    /// The retained lock is poisoned.
66    pub fn value(&self, subject: &str) -> Result<Option<serde_json::Value>, AgentError> {
67        Ok(self
68            .kept
69            .lock()
70            .map_err(|_| AgentError::Store("retained lock poisoned".into()))?
71            .values
72            .get(subject)
73            .cloned())
74    }
75    /// Shares the server's channel scheduler; creates no worker or timer.
76    #[must_use]
77    pub fn new(bounds: AgentsConfig, supervisor: ChannelSupervisor, device: &str) -> Arc<Self> {
78        Arc::new(Self {
79            bounds,
80            supervisor,
81            publisher: PublisherId::new(format!("liminal.{device}")),
82            kept: Mutex::new(Kept::default()),
83            console: OnceLock::new(),
84        })
85    }
86
87    /// Installs both presence and state atomically before delivering either.
88    ///
89    /// # Errors
90    /// Metadata is oversized, a lock is poisoned, or a watcher cannot receive.
91    /// A delivery failure does not erase the retained values.
92    pub fn publish(&self, values: &[(String, serde_json::Value)]) -> Result<(), AgentError> {
93        let mut encoded = BTreeMap::new();
94        for (subject, value) in values {
95            let bytes =
96                serde_json::to_vec(value).map_err(|_| AgentError::Invalid("metadata encoding"))?;
97            if bytes.len() > self.bounds.max_event_bytes {
98                return Err(AgentError::Bound {
99                    bound: "max_event_bytes",
100                    limit: self.bounds.max_event_bytes,
101                    count: bytes.len(),
102                });
103            }
104            encoded.insert(subject.as_str(), bytes);
105        }
106        let mut kept = self
107            .kept
108            .lock()
109            .map_err(|_| AgentError::Store("retained lock poisoned".into()))?;
110        let changed = values
111            .iter()
112            .any(|(subject, value)| kept.values.get(subject) != Some(value));
113        for (subject, value) in values {
114            kept.values.insert(subject.clone(), value.clone());
115        }
116        for (id, watcher) in &kept.watchers {
117            if values
118                .iter()
119                .any(|(subject, _)| subject == &watcher.subject)
120            {
121                let bytes = encoded
122                    .get(watcher.subject.as_str())
123                    .ok_or(AgentError::Invalid("retained encoding absent"))?;
124                let delivery =
125                    watcher
126                        .channel
127                        .publish_with_delivery(bytes, self.publisher.clone(), None);
128                if delivery.is_err() || delivery.is_ok_and(|delivery| !delivery.is_admitted()) {
129                    watcher.overflow.store(true, Ordering::Release);
130                    if let Some(wake) = &watcher.wake {
131                        wake();
132                    }
133                    tracing::warn!(watcher = id, "agent_retained_watcher_shed");
134                }
135            }
136        }
137        drop(kept);
138        if changed {
139            if let Some(console) = self.console.get().and_then(Weak::upgrade) {
140                console
141                    .agent_changed()
142                    .map_err(|_| AgentError::Delivery("agent topology signal failed"))?;
143            }
144        }
145        Ok(())
146    }
147
148    /// Subscribes under the same lock used by writers: the current value is
149    /// always queued first and every later change follows it. The supplied
150    /// wire inbox budget is installed before that first enqueue.
151    ///
152    /// # Errors
153    /// Unknown subject, too many watchers, a refused schema, or failed delivery.
154    pub fn subscribe(
155        self: &Arc<Self>,
156        id: u64,
157        subject: &str,
158        schemas: &[SchemaId],
159        install: Option<InboxInstall>,
160    ) -> Result<ConnectionSubscription, AgentError> {
161        let schema_bytes = b"{}";
162        let protocol_schema = SchemaId::from_schema_bytes(schema_bytes);
163        if !schemas.is_empty() && !schemas.contains(&protocol_schema) {
164            return Err(AgentError::Invalid("retained subscription schema"));
165        }
166        let mut kept = self
167            .kept
168            .lock()
169            .map_err(|_| AgentError::Store("retained lock poisoned".into()))?;
170        let value = kept
171            .values
172            .get(subject)
173            .ok_or_else(|| AgentError::Unknown(subject.into()))?
174            .clone();
175        if kept.watchers.len() >= self.bounds.max_watchers {
176            return Err(AgentError::Bound {
177                bound: "max_watchers",
178                limit: self.bounds.max_watchers,
179                count: kept.watchers.len(),
180            });
181        }
182        if kept.watchers.contains_key(&id) {
183            return Err(AgentError::Invalid("duplicate watcher id"));
184        }
185        let install = install.unwrap_or_else(|| InboxInstall {
186            budget: ConnectionInboxBudget::new(self.bounds.watcher_bytes),
187            depth_cap: self.bounds.watcher_depth,
188            notifier: None,
189            capacity: None,
190        });
191        let wake = install.notifier.clone();
192        let channel = ChannelHandle::with_supervisor(
193            ChannelConfig::new(
194                format!("agent-watch-{id}"),
195                Schema::new(serde_json::json!({}))
196                    .map_err(|_| AgentError::Invalid("metadata schema"))?,
197                ChannelMode::Ephemeral,
198            ),
199            self.supervisor.clone(),
200        );
201        let subscription = channel
202            .subscribe_with_install(install)
203            .map_err(|_| AgentError::Delivery("retained subscribe refused"))?;
204        let initial =
205            serde_json::to_vec(&value).map_err(|_| AgentError::Invalid("metadata encoding"))?;
206        let delivery = channel
207            .publish_with_delivery(&initial, self.publisher.clone(), None)
208            .map_err(|_| AgentError::Delivery("retained initial value refused"))?;
209        if !delivery.is_admitted() {
210            return Err(AgentError::Delivery("retained initial inbox full"));
211        }
212        let overflow = Arc::new(AtomicBool::new(false));
213        kept.watchers.insert(
214            id,
215            Watcher {
216                subject: subject.into(),
217                channel: channel.clone(),
218                overflow: Arc::clone(&overflow),
219                wake,
220            },
221        );
222        drop(kept);
223        Ok(ConnectionSubscription::new(
224            id,
225            protocol_schema,
226            Box::new(Resource {
227                id,
228                channel,
229                subscription,
230                owner: Arc::downgrade(self),
231                overflow,
232                closed: false,
233            }),
234        ))
235    }
236
237    fn detach(&self, id: u64) -> Result<(), AgentError> {
238        self.kept
239            .lock()
240            .map_err(|_| AgentError::Store("retained lock poisoned".into()))?
241            .watchers
242            .remove(&id);
243        Ok(())
244    }
245}
246
247struct Resource {
248    id: u64,
249    channel: ChannelHandle,
250    subscription: SubscriptionHandle,
251    owner: Weak<Retained>,
252    overflow: Arc<AtomicBool>,
253    closed: bool,
254}
255
256impl std::fmt::Debug for Resource {
257    fn fmt(&self, out: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
258        out.debug_struct("AgentWatch")
259            .field("id", &self.id)
260            .finish_non_exhaustive()
261    }
262}
263
264impl Resource {
265    fn release(&self) -> Result<(), crate::ServerError> {
266        if let Some(owner) = self.owner.upgrade() {
267            owner.detach(self.id)?;
268        }
269        Ok(())
270    }
271    fn finish(&mut self) -> Result<(), crate::ServerError> {
272        if self.closed {
273            return Ok(());
274        }
275        self.closed = true;
276        let unsubscribed = self.channel.unsubscribe(&self.subscription);
277        let closed = self.channel.close();
278        let released = self.release();
279        if unsubscribed.is_err() {
280            tracing::error!(watcher = self.id, "agent_watch_unsubscribe_failed");
281        }
282        if closed.is_err() {
283            tracing::error!(watcher = self.id, "agent_watch_actor_close_failed");
284        }
285        released?;
286        if unsubscribed.is_err() {
287            return Err(AgentError::Delivery("watch unsubscribe failed").into());
288        }
289        if closed.is_err() {
290            return Err(AgentError::Delivery("watch actor close failed").into());
291        }
292        Ok(())
293    }
294}
295
296impl SubscriptionResource for Resource {
297    fn unsubscribe(mut self: Box<Self>) -> Result<(), crate::ServerError> {
298        self.finish()
299    }
300    fn try_next(&mut self) -> Option<liminal::envelope::Envelope> {
301        if let Ok(value) = self.subscription.try_next() {
302            value
303        } else {
304            self.overflow.store(true, Ordering::Release);
305            tracing::error!(watcher = self.id, "agent_watch_read_failed");
306            None
307        }
308    }
309    fn try_next_positioned(
310        &mut self,
311    ) -> Result<Option<(liminal::envelope::Envelope, Option<u64>)>, crate::ServerError> {
312        self.subscription
313            .try_next_positioned()
314            .map_err(|_| AgentError::Delivery("watch read failed").into())
315    }
316    fn has_pending(&self) -> bool {
317        self.overflow.load(Ordering::Acquire) || self.subscription.has_pending()
318    }
319    fn is_overflowed(&self) -> bool {
320        self.overflow.load(Ordering::Acquire) || self.subscription.is_overflowed()
321    }
322    fn subscriber_pid(&self) -> Option<u64> {
323        Some(self.subscription.subscriber_pid())
324    }
325}
326
327impl Drop for Resource {
328    fn drop(&mut self) {
329        if self.finish().is_err() {
330            tracing::error!("agent_watch_release_failed");
331        }
332    }
333}