mj_controller/session_manager/
remote.rs1use super::*;
2
3pub(super) fn reconcile_action(
4 actor: Option<&RelaySessionTarget>,
5 desired: Option<&RelaySessionTarget>,
6) -> ReconcileAction {
7 match (actor, desired) {
8 (None, None) => ReconcileAction::Idle,
9 (None, Some(_)) => ReconcileAction::Spawn,
10 (Some(actor), Some(desired)) if actor == desired => ReconcileAction::Keep,
11 (Some(_), Some(_) | None) => ReconcileAction::Retire,
12 }
13}
14
15pub(super) fn target_map(targets: &[RelaySessionTarget]) -> BTreeMap<String, RelaySessionTarget> {
16 targets
17 .iter()
18 .cloned()
19 .map(|target| (target.session_id.clone(), target))
20 .collect()
21}
22
23pub(super) fn remove_actor_task(
24 actors: &mut BTreeMap<String, ActorRegistration>,
25 task_id: tokio::task::Id,
26) -> Option<String> {
27 let session_id = actors.iter().find_map(|(session_id, actor)| {
28 (actor.abort.id() == task_id).then(|| session_id.clone())
29 })?;
30 actors.remove(&session_id);
31 Some(session_id)
32}
33
34pub(super) fn reconcile_actors(
35 targets: &BTreeMap<String, RelaySessionTarget>,
36 actors: &mut BTreeMap<String, ActorRegistration>,
37 tasks: &mut tokio::task::JoinSet<String>,
38 updates: &CoalescedUpdateSender,
39) {
40 actors.retain(|session_id, actor| {
46 let live = !actor.commands.is_closed();
47 if !live {
48 tracing::warn!(session_id, "replacing stopped session relay actor");
49 }
50 live
51 });
52
53 for (session_id, actor) in actors.iter() {
54 let retiring = matches!(
55 reconcile_action(Some(&actor.target), targets.get(session_id)),
56 ReconcileAction::Retire
57 );
58 actor.retirement.send_replace(retiring);
59 }
60
61 for (session_id, target) in targets {
62 if !matches!(
63 reconcile_action(
64 actors.get(session_id).map(|actor| &actor.target),
65 Some(target)
66 ),
67 ReconcileAction::Spawn
68 ) {
69 continue;
70 }
71 let (actor_tx, actor_rx) = mpsc::channel(32);
72 let (release_tx, release_rx) = mpsc::unbounded_channel();
73 let (retirement_tx, retirement_rx) = watch::channel(false);
74 let (view_tx, view_rx) = watch::channel(ManagedSessionView::default());
75 let actor_updates = updates.clone();
76 let task_target = target.clone();
77 let task_id = session_id.clone();
78 let abort = tasks.spawn(async move {
79 run_session_actor(
80 task_target,
81 actor_rx,
82 release_rx,
83 retirement_rx,
84 view_tx,
85 actor_updates,
86 )
87 .await;
88 task_id
89 });
90 actors.insert(
91 session_id.clone(),
92 ActorRegistration {
93 target: target.clone(),
94 commands: actor_tx,
95 releases: release_tx,
96 retirement: retirement_tx,
97 view: view_rx,
98 abort,
99 },
100 );
101 }
102}
103
104pub(super) async fn run_remote_session_actor(
105 session_id: String,
106 mut commands: mpsc::Receiver<ActorCommand>,
107 requests: mpsc::Sender<RemoteSessionRequest>,
108) {
109 while let Some(command) = commands.recv().await {
110 let request = match command {
111 ActorCommand::Submit {
112 queued_at: _,
113 command_id,
114 command,
115 admission,
116 reply,
117 } => RemoteSessionRequest::Submit {
118 session_id: session_id.clone(),
119 command_id,
120 command,
121 admission,
122 reply,
123 },
124 ActorCommand::Sync { reply } => RemoteSessionRequest::Sync {
125 session_id: session_id.clone(),
126 reply,
127 },
128 ActorCommand::RespondElicitation {
129 elicitation_id,
130 response,
131 reply,
132 } => RemoteSessionRequest::RespondElicitation {
133 session_id: session_id.clone(),
134 elicitation_id,
135 response,
136 reply,
137 },
138 ActorCommand::StopBackgroundTask {
139 background_task_id,
140 reply,
141 } => RemoteSessionRequest::StopBackgroundTask {
142 session_id: session_id.clone(),
143 background_task_id,
144 reply,
145 },
146 ActorCommand::Reviewer {
147 role,
148 action,
149 reply,
150 } => RemoteSessionRequest::Reviewer {
151 session_id: session_id.clone(),
152 role,
153 action,
154 reply,
155 },
156 ActorCommand::InstallPromptContext { reply, .. } => {
157 let _ = reply.send(Err(
160 "prompt context can be installed only inside the controller daemon".into(),
161 ));
162 continue;
163 }
164 ActorCommand::Lease { reply } => {
165 let _ = reply.send(Err(anyhow::anyhow!(
166 "relay connection leases are available only inside the controller daemon"
167 )));
168 continue;
169 }
170 };
171 if let Err(error) = requests.send(request).await {
172 match error.0 {
173 RemoteSessionRequest::Submit { reply, .. } => {
174 let _ = reply.send(Err("controller daemon request bridge stopped".into()));
175 }
176 RemoteSessionRequest::Sync { reply, .. }
177 | RemoteSessionRequest::RespondElicitation { reply, .. }
178 | RemoteSessionRequest::StopBackgroundTask { reply, .. } => {
179 let _ = reply.send(Err("controller daemon request bridge stopped".into()));
180 }
181 RemoteSessionRequest::Reviewer { reply, .. } => {
182 let _ = reply.send(Err("controller daemon request bridge stopped".into()));
183 }
184 }
185 break;
186 }
187 }
188}
189
190pub(super) fn spawn_remote_actor(
191 session_id: String,
192 view: ManagedSessionView,
193 requests: &mpsc::Sender<RemoteSessionRequest>,
194 actors: &mut BTreeMap<String, RemoteActorRegistration>,
195 updates: &CoalescedUpdateSender,
196) {
197 let (actor_tx, actor_rx) = mpsc::channel(32);
198 let (release_tx, _release_rx) = mpsc::unbounded_channel();
199 let (view_tx, view_rx) = watch::channel(view.clone());
200 let abort = tokio::spawn(run_remote_session_actor(
201 session_id.clone(),
202 actor_rx,
203 requests.clone(),
204 ))
205 .abort_handle();
206 actors.insert(
207 session_id.clone(),
208 RemoteActorRegistration {
209 commands: actor_tx,
210 releases: release_tx,
211 view: view_rx,
212 view_tx,
213 abort,
214 },
215 );
216 updates.send(SessionManagerUpdate { session_id, view });
217}
218
219pub fn spawn_remote_session_manager() -> Result<RemoteSessionManagerChannels> {
223 let (targets_tx, mut targets_rx) = watch::channel(Vec::<RelaySessionTarget>::new());
224 let (commands_tx, mut commands_rx) = mpsc::channel(32);
225 let (updates_tx, updates_rx) = coalesced_update_channel();
226 let (published_tx, mut published_rx) = mpsc::unbounded_channel();
227 let (requests_tx, requests_rx) = mpsc::channel(64);
228 let (shutdown_tx, mut shutdown_rx) = oneshot::channel();
229 let task = tokio::spawn(async move {
230 let mut actors = BTreeMap::<String, RemoteActorRegistration>::new();
231 let mut latest = BTreeMap::<String, ManagedSessionView>::new();
232 let mut desired = BTreeMap::<String, RelaySessionTarget>::new();
233 loop {
234 tokio::select! {
235 _ = &mut shutdown_rx => break,
236 changed = targets_rx.changed() => {
237 if changed.is_err() {
238 break;
239 }
240 desired = target_map(&targets_rx.borrow_and_update());
241 actors.retain(|session_id, actor| {
242 if desired.contains_key(session_id) {
243 true
244 } else {
245 actor.abort.abort();
246 false
247 }
248 });
249 latest.retain(|session_id, _| desired.contains_key(session_id));
255 for session_id in desired.keys() {
256 if !actors.contains_key(session_id)
257 && let Some(view) = latest.get(session_id).cloned()
258 {
259 spawn_remote_actor(
260 session_id.clone(),
261 view,
262 &requests_tx,
263 &mut actors,
264 &updates_tx,
265 );
266 }
267 }
268 }
269 command = commands_rx.recv() => {
270 let Some(ManagerCommand::Session { session_id, reply }) = command else {
271 break;
272 };
273 let handle = actors.get(&session_id).map(|actor| ManagedSessionHandle {
274 session_id: session_id.clone(),
275 commands: actor.commands.clone(),
276 releases: actor.releases.clone(),
277 view: actor.view.clone(),
278 });
279 let _ = reply.send(handle);
280 }
281 published = published_rx.recv() => {
282 let Some(RemoteManagerUpdate::Publish { session_id, view }) = published else {
283 break;
284 };
285 latest.insert(session_id.clone(), view.clone());
286 if !desired.contains_key(&session_id) {
287 continue;
288 }
289 if let Some(actor) = actors.get(&session_id) {
290 publish_view(&session_id, view, &actor.view_tx, &updates_tx);
291 continue;
292 }
293 spawn_remote_actor(
294 session_id,
295 view,
296 &requests_tx,
297 &mut actors,
298 &updates_tx,
299 );
300 }
301 }
302 }
303 for actor in actors.into_values() {
304 actor.abort.abort();
305 }
306 });
307 Ok(RemoteSessionManagerChannels {
308 targets: targets_tx,
309 control: SessionManagerControl {
310 commands: commands_tx,
311 },
312 updates: updates_rx,
313 shutdown: SessionManagerShutdown {
314 signal: Some(shutdown_tx),
315 task: Some(task),
316 },
317 publisher: RemoteSessionPublisher {
318 updates: published_tx,
319 },
320 requests: RemoteSessionRequests {
321 requests: requests_rx,
322 },
323 })
324}