1use 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
32pub 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 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 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 #[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 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 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}