Skip to main content

mj_controller/session_manager/
channels.rs

1use super::*;
2
3#[derive(Debug, Clone)]
4pub struct SessionManagerUpdate {
5    pub session_id: String,
6    pub view: ManagedSessionView,
7}
8
9pub struct SessionManagerChannels {
10    pub targets: watch::Sender<Vec<RelaySessionTarget>>,
11    pub control: SessionManagerControl,
12    pub updates: SessionManagerUpdates,
13    pub shutdown: SessionManagerShutdown,
14}
15
16/// Client-side half of a remotely owned session manager.
17///
18/// The daemon remains the only process with relay connections. A control
19/// surface publishes the daemon's latest views here and forwards requests from
20/// [`RemoteSessionRequests`] over its authenticated transport.
21pub struct RemoteSessionManagerChannels {
22    pub targets: watch::Sender<Vec<RelaySessionTarget>>,
23    pub control: SessionManagerControl,
24    pub updates: SessionManagerUpdates,
25    pub shutdown: SessionManagerShutdown,
26    pub publisher: RemoteSessionPublisher,
27    pub requests: RemoteSessionRequests,
28}
29
30#[derive(Clone)]
31pub struct RemoteSessionPublisher {
32    pub(super) updates: mpsc::UnboundedSender<RemoteManagerUpdate>,
33}
34
35impl RemoteSessionPublisher {
36    pub async fn publish(&self, session_id: String, view: ManagedSessionView) -> Result<()> {
37        self.updates
38            .send(RemoteManagerUpdate::Publish { session_id, view })
39            .context("remote session manager stopped")
40    }
41
42    pub fn try_publish(&self, session_id: String, view: ManagedSessionView) -> Result<()> {
43        self.updates
44            .send(RemoteManagerUpdate::Publish { session_id, view })
45            .context("remote session manager update queue is unavailable")
46    }
47}
48
49pub struct RemoteSessionRequests {
50    pub(super) requests: mpsc::Receiver<RemoteSessionRequest>,
51}
52
53impl RemoteSessionRequests {
54    pub async fn recv(&mut self) -> Option<RemoteSessionRequest> {
55        self.requests.recv().await
56    }
57}
58
59pub enum RemoteSessionRequest {
60    Submit {
61        session_id: String,
62        command_id: String,
63        command: RelayCommand,
64        admission: Option<ReviewDeliveryAdmission>,
65        reply: oneshot::Sender<std::result::Result<u64, mj_client::session::SubmitFailure>>,
66    },
67    Sync {
68        session_id: String,
69        reply: oneshot::Sender<std::result::Result<(), String>>,
70    },
71    RespondElicitation {
72        session_id: String,
73        elicitation_id: String,
74        response: ElicitationResponse,
75        reply: oneshot::Sender<std::result::Result<(), String>>,
76    },
77    StopBackgroundTask {
78        session_id: String,
79        background_task_id: String,
80        reply: oneshot::Sender<std::result::Result<(), String>>,
81    },
82    Reviewer {
83        session_id: String,
84        /// Which reviewing role the action drives; `None` is the default one.
85        role: Option<String>,
86        action: ReviewerAction,
87        reply: oneshot::Sender<std::result::Result<ReviewerOutcome, String>>,
88    },
89}
90
91impl RemoteSessionRequest {
92    /// The session this request acts on. Requests for one session have to be
93    /// carried out in the order they were made.
94    pub fn session_id(&self) -> &str {
95        match self {
96            Self::Submit { session_id, .. }
97            | Self::Sync { session_id, .. }
98            | Self::RespondElicitation { session_id, .. }
99            | Self::StopBackgroundTask { session_id, .. }
100            | Self::Reviewer { session_id, .. } => session_id,
101        }
102    }
103}
104
105/// Admission bounds include running work, queued work, and dispatcher messages.
106const REQUESTS_PER_STREAM: usize = 32;
107const REQUESTS_TOTAL: usize = 256;
108type ForwardRequest = std::pin::Pin<Box<dyn std::future::Future<Output = ()> + Send>>;
109type RequestBudgets = Arc<Mutex<std::collections::HashMap<SessionRequestStream, usize>>>;
110
111/// One supervisor drains accepted requests even when its bridge disconnects.
112/// Each primary/reviewer stream runs in order, independently of other streams.
113/// Dropping the bridge closes admission but leaves its supervisor draining:
114/// disconnecting a caller cannot cancel a mutation that may already be accepted.
115/// Owners that can await shutdown use `drain` to observe that completion.
116pub struct SessionRequestOrder {
117    sender: mpsc::Sender<OrderedRequest>,
118    budgets: RequestBudgets,
119    supervisor: tokio::task::JoinHandle<()>,
120}
121
122#[derive(Debug, Clone, PartialEq, Eq, Hash)]
123pub(super) enum SessionRequestStream {
124    Primary(String),
125    Reviewer(String, Option<String>),
126}
127
128struct RequestPermit {
129    budgets: RequestBudgets,
130    stream: SessionRequestStream,
131}
132
133impl Drop for RequestPermit {
134    fn drop(&mut self) {
135        let mut budgets = self.budgets.lock().expect("request budgets poisoned");
136        let count = budgets.get_mut(&self.stream).expect("admitted request");
137        *count -= 1;
138        if *count == 0 {
139            budgets.remove(&self.stream);
140        }
141    }
142}
143
144struct OrderedRequest {
145    permit: RequestPermit,
146    forward: ForwardRequest,
147}
148
149impl RemoteSessionRequest {
150    pub(crate) fn reject(self, message: &str) {
151        match self {
152            Self::Submit { reply, .. } => {
153                let _ = reply.send(Err(message.to_owned().into()));
154            }
155            Self::Sync { reply, .. }
156            | Self::RespondElicitation { reply, .. }
157            | Self::StopBackgroundTask { reply, .. } => {
158                let _ = reply.send(Err(message.to_owned()));
159            }
160            Self::Reviewer { reply, .. } => {
161                let _ = reply.send(Err(message.to_owned()));
162            }
163        }
164    }
165}
166
167impl Default for SessionRequestOrder {
168    fn default() -> Self {
169        Self::new()
170    }
171}
172
173impl SessionRequestOrder {
174    #[must_use]
175    pub fn new() -> Self {
176        let (sender, receiver) = mpsc::channel(REQUESTS_TOTAL);
177        Self {
178            sender,
179            budgets: Default::default(),
180            supervisor: tokio::spawn(supervise_requests(receiver)),
181        }
182    }
183
184    /// Overload is an explicit non-admission, never ambiguous delivery.
185    pub fn dispatch<F, Fut>(&mut self, request: RemoteSessionRequest, forward: F)
186    where
187        F: FnOnce(RemoteSessionRequest) -> Fut + Send + 'static,
188        Fut: std::future::Future<Output = ()> + Send + 'static,
189    {
190        let stream = match &request {
191            RemoteSessionRequest::Reviewer {
192                session_id, role, ..
193            } => SessionRequestStream::Reviewer(session_id.clone(), role.clone()),
194            _ => SessionRequestStream::Primary(request.session_id().to_owned()),
195        };
196        let mut budgets = self.budgets.lock().expect("request budgets poisoned");
197        if budgets.get(&stream).copied().unwrap_or(0) >= REQUESTS_PER_STREAM
198            || budgets.values().sum::<usize>() >= REQUESTS_TOTAL
199        {
200            drop(budgets);
201            request.reject("session request queue is full; request was not accepted");
202            return;
203        }
204        // Reserve the bounded channel before constructing the forwarded future,
205        // so even a failed supervisor can refuse with a definite non-delivery.
206        let Ok(slot) = self.sender.try_reserve() else {
207            drop(budgets);
208            request.reject("session request dispatcher is unavailable; request was not accepted");
209            return;
210        };
211        *budgets.entry(stream.clone()).or_default() += 1;
212        drop(budgets);
213        slot.send(OrderedRequest {
214            permit: RequestPermit {
215                budgets: self.budgets.clone(),
216                stream,
217            },
218            forward: Box::pin(async move { forward(request).await }),
219        });
220    }
221
222    /// Close admission and wait for all admitted work, including queued work.
223    pub async fn drain(self) -> Result<()> {
224        drop(self.sender);
225        self.supervisor
226            .await
227            .context("session request supervisor failed")
228    }
229}
230
231async fn supervise_requests(mut requests: mpsc::Receiver<OrderedRequest>) {
232    let mut tasks = tokio::task::JoinSet::new();
233    let mut running = std::collections::HashMap::new();
234    let mut pending =
235        std::collections::HashMap::<SessionRequestStream, VecDeque<OrderedRequest>>::new();
236    let mut closed = false;
237    loop {
238        tokio::select! {
239            // Reap ready tasks before accepting another message; completed task
240            // records cannot accumulate behind a continuously busy producer.
241            biased;
242            completed = tasks.join_next_with_id(), if !tasks.is_empty() => {
243                let task_id = match completed.expect("nonempty request tasks") {
244                    Ok((id, ())) => id,
245                    Err(error) => {
246                        tracing::error!(stream = ?running.get(&error.id()), %error, "session request task failed");
247                        error.id()
248                    }
249                };
250                let stream = running.remove(&task_id).expect("registered request task");
251                if let Some(queue) = pending.get_mut(&stream) {
252                    if let Some(request) = queue.pop_front() {
253                        let handle = tasks.spawn(async move {
254                            let _permit = request.permit;
255                            request.forward.await;
256                        });
257                        running.insert(handle.id(), stream.clone());
258                    }
259                    if queue.is_empty() { pending.remove(&stream); }
260                }
261            }
262            request = requests.recv(), if !closed => {
263                match request {
264                    Some(request) => {
265                        let stream = request.permit.stream.clone();
266                        if running.values().any(|active| active == &stream) {
267                            pending.entry(stream).or_default().push_back(request);
268                        } else {
269                            let handle = tasks.spawn(async move {
270                                let _permit = request.permit;
271                                request.forward.await;
272                            });
273                            running.insert(handle.id(), stream);
274                        }
275                    }
276                    None => closed = true,
277                }
278            }
279        }
280        if closed && tasks.is_empty() {
281            break;
282        }
283    }
284}
285
286/// Exclusive owner of the manager task and every relay actor below it.
287///
288/// Long-running control surfaces explicitly await [`Self::shutdown`] before
289/// their Tokio runtime goes away. Drop remains an aborting fallback for tests
290/// and early-return paths that cannot await.
291pub struct SessionManagerShutdown {
292    pub(super) signal: Option<oneshot::Sender<()>>,
293    pub(super) task: Option<tokio::task::JoinHandle<()>>,
294}
295
296impl SessionManagerShutdown {
297    pub async fn shutdown(mut self) -> Result<()> {
298        if let Some(signal) = self.signal.take() {
299            let _ = signal.send(());
300        }
301        if let Some(task) = self.task.take() {
302            task.await.context("session manager shutdown task failed")?;
303        }
304        Ok(())
305    }
306}
307
308impl Drop for SessionManagerShutdown {
309    fn drop(&mut self) {
310        if let Some(signal) = self.signal.take() {
311            let _ = signal.send(());
312        }
313        if let Some(task) = self.task.take() {
314            task.abort();
315        }
316    }
317}
318
319#[derive(Clone)]
320pub(crate) struct CoalescedUpdateSender {
321    producers: Arc<Mutex<BTreeMap<String, Arc<()>>>>,
322    producer: Option<Arc<UpdateProducer>>,
323    pub(super) delegation: Option<DelegationSender>,
324    pub(super) observer: Option<Arc<DelegationPublisher>>,
325    pub(super) mailbox: Arc<Mutex<UpdateMailbox>>,
326    pub(super) wake: mpsc::Sender<()>,
327}
328
329struct UpdateProducer {
330    registry: Arc<Mutex<BTreeMap<String, Arc<()>>>>,
331    session_id: String,
332    identity: Arc<()>,
333}
334
335impl Drop for UpdateProducer {
336    fn drop(&mut self) {
337        let mut registry = self
338            .registry
339            .lock()
340            .expect("session producer registry poisoned");
341        if registry
342            .get(&self.session_id)
343            .is_some_and(|current| Arc::ptr_eq(current, &self.identity))
344        {
345            registry.remove(&self.session_id);
346        }
347    }
348}
349
350/// Bounded latest-state feed for the dashboard. At most one snapshot per
351/// session is retained, and sessions are served in first-pending order.
352pub struct SessionManagerUpdates {
353    pub(super) mailbox: Arc<Mutex<UpdateMailbox>>,
354    pub(super) wake: mpsc::Receiver<()>,
355    // Keep the update owned until the consumer asks for another one. Its
356    // completion edge can schedule a review or continuation in the meantime.
357    delivered_work: Option<crate::upgrade::Work>,
358}
359
360pub(super) struct PendingUpdate {
361    update: SessionManagerUpdate,
362    work: Option<crate::upgrade::Work>,
363}
364
365#[derive(Default)]
366pub(super) struct UpdateMailbox {
367    pub(super) pending: BTreeMap<String, PendingUpdate>,
368    ready: VecDeque<String>,
369}
370
371impl UpdateMailbox {
372    fn enqueue(&mut self, pending: PendingUpdate) {
373        let session_id = pending.update.session_id.clone();
374        // Replacing a pending view keeps its place; a delivered session's
375        // next view joins the tail so it cannot overtake waiting sessions.
376        if self.pending.insert(session_id.clone(), pending).is_none() {
377            self.ready.push_back(session_id);
378        }
379    }
380
381    fn remove(&mut self, session_id: &str) {
382        if self.pending.remove(session_id).is_some() {
383            self.ready.retain(|queued| queued != session_id);
384        }
385    }
386
387    fn pop(&mut self) -> Option<PendingUpdate> {
388        let session_id = self.ready.pop_front()?;
389        Some(
390            self.pending
391                .remove(&session_id)
392                .expect("queued session update"),
393        )
394    }
395}
396
397impl CoalescedUpdateSender {
398    pub(super) fn for_actor(&self, session_id: &str) -> Self {
399        assert!(
400            self.producer.is_none(),
401            "only the manager registers producers"
402        );
403        let identity = Arc::new(());
404        let mut registry = self
405            .producers
406            .lock()
407            .expect("session producer registry poisoned");
408        registry.insert(session_id.to_owned(), identity.clone());
409        // Replacing a producer also invalidates its undelivered observation.
410        self.mailbox
411            .lock()
412            .expect("session update coalescer poisoned")
413            .remove(session_id);
414        let mut sender = self.clone();
415        sender.producer = Some(Arc::new(UpdateProducer {
416            registry: self.producers.clone(),
417            session_id: session_id.to_owned(),
418            identity,
419        }));
420        sender
421    }
422
423    pub(crate) fn send(&self, update: SessionManagerUpdate) {
424        let registry = self
425            .producers
426            .lock()
427            .expect("session producer registry poisoned");
428        if let Some(producer) = &self.producer {
429            assert_eq!(producer.session_id, update.session_id);
430            if !registry
431                .get(&update.session_id)
432                .is_some_and(|current| Arc::ptr_eq(current, &producer.identity))
433            {
434                return;
435            }
436        }
437        if let Some(observer) = &self.observer {
438            observer.publish(&update.view);
439        }
440        if self.wake.is_closed() {
441            return;
442        }
443        self.mailbox
444            .lock()
445            .expect("session update coalescer poisoned")
446            .enqueue(PendingUpdate {
447                update,
448                work: crate::upgrade::activity("session update").ok(),
449            });
450        let _ = self.wake.try_send(());
451    }
452}
453
454impl SessionManagerUpdates {
455    pub(super) fn pop_pending(&mut self) -> Option<SessionManagerUpdate> {
456        self.delivered_work = None;
457        let pending = self
458            .mailbox
459            .lock()
460            .expect("session update coalescer poisoned")
461            .pop()?;
462        self.delivered_work = pending.work;
463        Some(pending.update)
464    }
465
466    pub async fn recv(&mut self) -> Option<SessionManagerUpdate> {
467        loop {
468            if let Some(update) = self.pop_pending() {
469                return Some(update);
470            }
471            self.wake.recv().await?;
472        }
473    }
474
475    pub fn try_recv(
476        &mut self,
477    ) -> std::result::Result<SessionManagerUpdate, mpsc::error::TryRecvError> {
478        if let Some(update) = self.pop_pending() {
479            return Ok(update);
480        }
481        self.wake.try_recv()?;
482        self.pop_pending().ok_or(mpsc::error::TryRecvError::Empty)
483    }
484}
485
486pub(crate) fn coalesced_update_channel() -> (CoalescedUpdateSender, SessionManagerUpdates) {
487    let mailbox = Arc::new(Mutex::new(UpdateMailbox::default()));
488    let (wake_tx, wake_rx) = mpsc::channel(1);
489    (
490        CoalescedUpdateSender {
491            producers: Default::default(),
492            producer: None,
493            delegation: None,
494            observer: None,
495            mailbox: mailbox.clone(),
496            wake: wake_tx,
497        },
498        SessionManagerUpdates {
499            mailbox,
500            wake: wake_rx,
501            delivered_work: None,
502        },
503    )
504}