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, String>>,
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/// Keeps each session's relay requests in the order they were made, while
106/// letting different sessions overlap.
107///
108/// A bridge that spawns every request concurrently loses the order the caller
109/// submitted them in, and the order is load-bearing: `/effort` followed by a
110/// prompt has to reach the relay that way round, or the prompt runs under the
111/// old setting. Awaiting each request inline would restore the order but would
112/// also make one slow session block every other one, so instead each request
113/// waits on its own session's previous request and nothing else.
114#[derive(Default)]
115pub struct SessionRequestOrder {
116    pub(super) latest: std::collections::HashMap<SessionRequestStream, tokio::task::JoinHandle<()>>,
117}
118
119#[derive(Debug, PartialEq, Eq, Hash)]
120pub(super) enum SessionRequestStream {
121    Primary(String),
122    Reviewer(String, Option<String>),
123}
124
125impl SessionRequestOrder {
126    #[must_use]
127    pub fn new() -> Self {
128        Self::default()
129    }
130
131    /// Runs `forward` for `request` after everything already queued for the
132    /// same primary or reviewer role has finished. Independent reviewers
133    /// must not delay primary controls or one another.
134    pub fn dispatch<F, Fut>(&mut self, request: RemoteSessionRequest, forward: F)
135    where
136        F: FnOnce(RemoteSessionRequest) -> Fut + Send + 'static,
137        Fut: std::future::Future<Output = ()> + Send,
138    {
139        // Sessions that have gone quiet leave a finished handle behind; drop
140        // them here so the map tracks live work rather than every session the
141        // bridge has ever served.
142        self.latest.retain(|_, handle| !handle.is_finished());
143        let stream = match &request {
144            RemoteSessionRequest::Reviewer {
145                session_id, role, ..
146            } => SessionRequestStream::Reviewer(session_id.clone(), role.clone()),
147            _ => SessionRequestStream::Primary(request.session_id().to_owned()),
148        };
149        let previous = self.latest.remove(&stream);
150        let handle = tokio::spawn(async move {
151            if let Some(previous) = previous {
152                // A panicked predecessor still releases its successor: the
153                // request behind it is the user's, and dropping it silently
154                // would be worse than running it late.
155                if let Err(error) = previous.await {
156                    tracing::error!(%error, "previous session request task failed");
157                }
158            }
159            forward(request).await;
160        });
161        self.latest.insert(stream, handle);
162    }
163}
164
165/// Exclusive owner of the manager task and every relay actor below it.
166///
167/// Long-running control surfaces explicitly await [`Self::shutdown`] before
168/// their Tokio runtime goes away. Drop remains an aborting fallback for tests
169/// and early-return paths that cannot await.
170pub struct SessionManagerShutdown {
171    pub(super) signal: Option<oneshot::Sender<()>>,
172    pub(super) task: Option<tokio::task::JoinHandle<()>>,
173}
174
175impl SessionManagerShutdown {
176    pub async fn shutdown(mut self) -> Result<()> {
177        if let Some(signal) = self.signal.take() {
178            let _ = signal.send(());
179        }
180        if let Some(task) = self.task.take() {
181            task.await.context("session manager shutdown task failed")?;
182        }
183        Ok(())
184    }
185}
186
187impl Drop for SessionManagerShutdown {
188    fn drop(&mut self) {
189        if let Some(signal) = self.signal.take() {
190            let _ = signal.send(());
191        }
192        if let Some(task) = self.task.take() {
193            task.abort();
194        }
195    }
196}
197
198#[derive(Clone)]
199pub(super) struct CoalescedUpdateSender {
200    pub(super) pending: Arc<Mutex<BTreeMap<String, SessionManagerUpdate>>>,
201    pub(super) wake: mpsc::Sender<()>,
202}
203
204/// Bounded latest-state feed for the dashboard. At most one snapshot per
205/// session is retained while the consumer is busy.
206pub struct SessionManagerUpdates {
207    pub(super) pending: Arc<Mutex<BTreeMap<String, SessionManagerUpdate>>>,
208    pub(super) wake: mpsc::Receiver<()>,
209}
210
211impl CoalescedUpdateSender {
212    pub(super) fn send(&self, update: SessionManagerUpdate) {
213        if self.wake.is_closed() {
214            return;
215        }
216        self.pending
217            .lock()
218            .expect("session update coalescer poisoned")
219            .insert(update.session_id.clone(), update);
220        let _ = self.wake.try_send(());
221    }
222}
223
224impl SessionManagerUpdates {
225    pub(super) fn pop_pending(&self) -> Option<SessionManagerUpdate> {
226        self.pending
227            .lock()
228            .expect("session update coalescer poisoned")
229            .pop_first()
230            .map(|(_, update)| update)
231    }
232
233    pub async fn recv(&mut self) -> Option<SessionManagerUpdate> {
234        loop {
235            if let Some(update) = self.pop_pending() {
236                return Some(update);
237            }
238            self.wake.recv().await?;
239        }
240    }
241
242    pub fn try_recv(
243        &mut self,
244    ) -> std::result::Result<SessionManagerUpdate, mpsc::error::TryRecvError> {
245        if let Some(update) = self.pop_pending() {
246            return Ok(update);
247        }
248        self.wake.try_recv()?;
249        self.pop_pending().ok_or(mpsc::error::TryRecvError::Empty)
250    }
251}
252
253pub(super) fn coalesced_update_channel() -> (CoalescedUpdateSender, SessionManagerUpdates) {
254    let pending = Arc::new(Mutex::new(BTreeMap::new()));
255    let (wake_tx, wake_rx) = mpsc::channel(1);
256    (
257        CoalescedUpdateSender {
258            pending: pending.clone(),
259            wake: wake_tx,
260        },
261        SessionManagerUpdates {
262            pending,
263            wake: wake_rx,
264        },
265    )
266}