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 command_id,
113 command,
114 admission,
115 reply,
116 } => RemoteSessionRequest::Submit {
117 session_id: session_id.clone(),
118 command_id,
119 command,
120 admission,
121 reply,
122 },
123 ActorCommand::Sync { reply } => RemoteSessionRequest::Sync {
124 session_id: session_id.clone(),
125 reply,
126 },
127 ActorCommand::RespondElicitation {
128 elicitation_id,
129 response,
130 reply,
131 } => RemoteSessionRequest::RespondElicitation {
132 session_id: session_id.clone(),
133 elicitation_id,
134 response,
135 reply,
136 },
137 ActorCommand::StopBackgroundTask {
138 background_task_id,
139 reply,
140 } => RemoteSessionRequest::StopBackgroundTask {
141 session_id: session_id.clone(),
142 background_task_id,
143 reply,
144 },
145 ActorCommand::Reviewer {
146 role,
147 action,
148 reply,
149 } => RemoteSessionRequest::Reviewer {
150 session_id: session_id.clone(),
151 role,
152 action,
153 reply,
154 },
155 ActorCommand::InstallPromptContext { reply, .. } => {
156 let _ = reply.send(Err(
159 "prompt context can be installed only inside the controller daemon".into(),
160 ));
161 continue;
162 }
163 ActorCommand::Lease { reply } => {
164 let _ = reply.send(Err(anyhow::anyhow!(
165 "relay connection leases are available only inside the controller daemon"
166 )));
167 continue;
168 }
169 };
170 if let Err(error) = requests.send(request).await {
171 match error.0 {
172 RemoteSessionRequest::Submit { reply, .. } => {
173 let _ = reply.send(Err("controller daemon request bridge stopped".into()));
174 }
175 RemoteSessionRequest::Sync { reply, .. }
176 | RemoteSessionRequest::RespondElicitation { reply, .. }
177 | RemoteSessionRequest::StopBackgroundTask { reply, .. } => {
178 let _ = reply.send(Err("controller daemon request bridge stopped".into()));
179 }
180 RemoteSessionRequest::Reviewer { reply, .. } => {
181 let _ = reply.send(Err("controller daemon request bridge stopped".into()));
182 }
183 }
184 break;
185 }
186 }
187}
188
189pub(super) fn spawn_remote_actor(
190 session_id: String,
191 view: ManagedSessionView,
192 requests: &mpsc::Sender<RemoteSessionRequest>,
193 actors: &mut BTreeMap<String, RemoteActorRegistration>,
194 updates: &CoalescedUpdateSender,
195) {
196 let (actor_tx, actor_rx) = mpsc::channel(32);
197 let (release_tx, _release_rx) = mpsc::unbounded_channel();
198 let (view_tx, view_rx) = watch::channel(view.clone());
199 let abort = tokio::spawn(run_remote_session_actor(
200 session_id.clone(),
201 actor_rx,
202 requests.clone(),
203 ))
204 .abort_handle();
205 actors.insert(
206 session_id.clone(),
207 RemoteActorRegistration {
208 commands: actor_tx,
209 releases: release_tx,
210 view: view_rx,
211 view_tx,
212 abort,
213 },
214 );
215 updates.send(SessionManagerUpdate { session_id, view });
216}
217
218pub fn spawn_remote_session_manager() -> Result<RemoteSessionManagerChannels> {
222 let (targets_tx, mut targets_rx) = watch::channel(Vec::<RelaySessionTarget>::new());
223 let (commands_tx, mut commands_rx) = mpsc::channel(32);
224 let (updates_tx, updates_rx) = coalesced_update_channel();
225 let (published_tx, mut published_rx) = mpsc::unbounded_channel();
226 let (requests_tx, requests_rx) = mpsc::channel(64);
227 let (shutdown_tx, mut shutdown_rx) = oneshot::channel();
228 let task = tokio::spawn(async move {
229 let mut actors = BTreeMap::<String, RemoteActorRegistration>::new();
230 let mut latest = BTreeMap::<String, ManagedSessionView>::new();
231 let mut desired = BTreeMap::<String, RelaySessionTarget>::new();
232 loop {
233 tokio::select! {
234 _ = &mut shutdown_rx => break,
235 changed = targets_rx.changed() => {
236 if changed.is_err() {
237 break;
238 }
239 desired = target_map(&targets_rx.borrow_and_update());
240 actors.retain(|session_id, actor| {
241 if desired.contains_key(session_id) {
242 true
243 } else {
244 actor.abort.abort();
245 false
246 }
247 });
248 latest.retain(|session_id, _| desired.contains_key(session_id));
254 for session_id in desired.keys() {
255 if !actors.contains_key(session_id)
256 && let Some(view) = latest.get(session_id).cloned()
257 {
258 spawn_remote_actor(
259 session_id.clone(),
260 view,
261 &requests_tx,
262 &mut actors,
263 &updates_tx,
264 );
265 }
266 }
267 }
268 command = commands_rx.recv() => {
269 let Some(ManagerCommand::Session { session_id, reply }) = command else {
270 break;
271 };
272 let handle = actors.get(&session_id).map(|actor| ManagedSessionHandle {
273 session_id: session_id.clone(),
274 commands: actor.commands.clone(),
275 releases: actor.releases.clone(),
276 view: actor.view.clone(),
277 });
278 let _ = reply.send(handle);
279 }
280 published = published_rx.recv() => {
281 let Some(RemoteManagerUpdate::Publish { session_id, view }) = published else {
282 break;
283 };
284 latest.insert(session_id.clone(), view.clone());
285 if !desired.contains_key(&session_id) {
286 continue;
287 }
288 if let Some(actor) = actors.get(&session_id) {
289 publish_view(&session_id, view, &actor.view_tx, &updates_tx);
290 continue;
291 }
292 spawn_remote_actor(
293 session_id,
294 view,
295 &requests_tx,
296 &mut actors,
297 &updates_tx,
298 );
299 }
300 }
301 }
302 for actor in actors.into_values() {
303 actor.abort.abort();
304 }
305 });
306 Ok(RemoteSessionManagerChannels {
307 targets: targets_tx,
308 control: SessionManagerControl {
309 commands: commands_tx,
310 },
311 updates: updates_rx,
312 shutdown: SessionManagerShutdown {
313 signal: Some(shutdown_tx),
314 task: Some(task),
315 },
316 publisher: RemoteSessionPublisher {
317 updates: published_tx,
318 },
319 requests: RemoteSessionRequests {
320 requests: requests_rx,
321 },
322 })
323}