1use std::{
5 collections::{BTreeMap, BTreeSet, HashMap},
6 sync::Arc,
7};
8
9use futures::{lock::Mutex, stream::StreamExt, FutureExt};
10use linera_base::{
11 data_types::{MessagePolicy, TimeDelta},
12 identifiers::{ApplicationId, ChainId, GenericApplicationId},
13};
14use linera_client::chain_listener::{ClientContext, ListenerCommand};
15use linera_core::{
16 client::ChainClient,
17 node::NotificationStream,
18 worker::{Notification, Reason},
19};
20use linera_sdk::abis::controller::{
21 LocalWorkerState, ManagedServiceId, Operation, PendingService, WorkerCommand,
22};
23use serde_json::json;
24use tokio::{
25 select,
26 sync::mpsc::{self, UnboundedSender},
27};
28use tokio_util::sync::CancellationToken;
29use tracing::{debug, error, info};
30
31use crate::task_processor::{OperatorMap, TaskProcessor};
32
33#[derive(Debug)]
35pub struct Update {
36 pub application_ids: Vec<ApplicationId>,
38}
39
40struct ProcessorHandle {
41 update_sender: mpsc::UnboundedSender<Update>,
42}
43
44pub struct Controller<Ctx: ClientContext> {
46 chain_id: ChainId,
47 controller_id: ApplicationId,
48 context: Arc<Mutex<Ctx>>,
49 chain_client: ChainClient<Ctx::Environment>,
50 cancellation_token: CancellationToken,
51 notifications: NotificationStream,
52 operators: OperatorMap,
53 retry_delay: TimeDelta,
54 processors: BTreeMap<ChainId, ProcessorHandle>,
55 listened_local_chains: BTreeSet<ChainId>,
56 current_message_policies: BTreeMap<ChainId, MessagePolicy>,
57 command_sender: UnboundedSender<ListenerCommand>,
58 pending_services_notifications: BTreeMap<
59 ChainId,
60 (
61 HashMap<ManagedServiceId, PendingService>,
62 NotificationStream,
63 ),
64 >,
65}
66
67impl<Ctx> Controller<Ctx>
68where
69 Ctx: ClientContext + Send + Sync + 'static,
70 Ctx::Environment: 'static,
71 <Ctx::Environment as linera_core::Environment>::Storage: Clone,
72{
73 #[expect(clippy::too_many_arguments)]
75 pub fn new(
76 chain_id: ChainId,
77 controller_id: ApplicationId,
78 context: Arc<Mutex<Ctx>>,
79 chain_client: ChainClient<Ctx::Environment>,
80 cancellation_token: CancellationToken,
81 operators: OperatorMap,
82 retry_delay: TimeDelta,
83 command_sender: UnboundedSender<ListenerCommand>,
84 ) -> Self {
85 let notifications = chain_client.subscribe().expect("client subscription");
86 Self {
87 chain_id,
88 controller_id,
89 context,
90 chain_client,
91 cancellation_token,
92 notifications,
93 operators,
94 retry_delay,
95 processors: BTreeMap::new(),
96 listened_local_chains: BTreeSet::new(),
97 current_message_policies: BTreeMap::new(),
98 command_sender,
99 pending_services_notifications: BTreeMap::new(),
100 }
101 }
102
103 pub async fn run(mut self) {
105 info!(
106 "Watching for notifications for controller chain {}",
107 self.chain_id
108 );
109 self.process_controller_state().await;
110 loop {
111 let pending_services_notifications: std::pin::Pin<
112 Box<dyn futures::Future<Output = (ChainId, Option<Notification>)> + Send>,
113 > = if !self.pending_services_notifications.is_empty() {
114 Box::pin(
115 futures::future::select_all(
116 self.pending_services_notifications.iter_mut().map(
117 |(chain_id, (_, notifications))| {
118 notifications.next().map(|result| (*chain_id, result))
119 },
120 ),
121 )
122 .map(|((chain_id, maybe_notification), _, _)| (chain_id, maybe_notification)),
123 )
124 } else {
125 Box::pin(futures::future::pending())
126 };
127 select! {
128 Some(notification) = self.notifications.next() => {
129 if let Reason::NewBlock { .. } = notification.reason {
130 debug!("Processing notification on controller chain {}", self.chain_id);
131 self.process_controller_state().await;
132 }
133 }
134 (chain_id, Some(notification)) = pending_services_notifications => {
135 self.process_pending_service_notification(chain_id, notification).await;
136 }
137 _ = self.cancellation_token.cancelled().fuse() => {
138 break;
139 }
140 }
141 }
142 debug!("Notification stream ended.");
143 }
144
145 async fn process_pending_service_notification(
146 &mut self,
147 chain_id: ChainId,
148 notification: Notification,
149 ) {
150 debug!(
151 "Processing notification on pending service chain {}",
152 chain_id
153 );
154 if let Reason::NewBlock { height, .. } = notification.reason {
155 let pending_services = &mut self
156 .pending_services_notifications
157 .get_mut(&chain_id)
158 .expect("the entry should exist")
159 .0;
160 for (service_id, pending_service) in &*pending_services {
161 if pending_service.start_block_height <= height {
162 let bytes = bcs::to_bytes(&Operation::StartLocalService {
163 service_id: *service_id,
164 })
165 .expect("bcs bytes");
166 let operation = linera_execution::Operation::User {
167 application_id: self.controller_id,
168 bytes,
169 };
170 if let Err(e) = self
171 .chain_client
172 .execute_operations(vec![operation], vec![])
173 .await
174 {
175 error!("Failed to execute worker on-chain registration: {e}");
177 }
178 }
179 }
180 pending_services
181 .retain(|_, pending_service| pending_service.start_block_height > height);
182 if pending_services.is_empty() {
183 self.pending_services_notifications.remove(&chain_id);
184 }
185 }
186 }
187
188 async fn process_controller_state(&mut self) {
189 let state = match self.query_controller_state().await {
190 Ok(state) => state,
191 Err(error) => {
192 error!("Error reading controller state: {error}");
193 return;
194 }
195 };
196 let Some(worker) = state.local_worker else {
197 self.register_worker().await;
199 return;
200 };
201 assert_eq!(
202 worker.owner,
203 self.chain_client
204 .preferred_owner()
205 .expect("The current wallet should own the chain being watched"),
206 "We should be registered with the current account owner."
207 );
208
209 for (managed_service_id, (chain_id, pending_service)) in &state.local_pending_services {
212 if self.pending_services_notifications.contains_key(chain_id) {
214 continue;
215 }
216 let service_notifications = self
217 .chain_client
218 .subscribe_to(*chain_id)
219 .expect("client subscription");
220 self.pending_services_notifications
221 .entry(*chain_id)
222 .or_insert_with(|| (HashMap::new(), service_notifications))
223 .0
224 .insert(*managed_service_id, pending_service.clone());
225 }
226
227 let mut chain_apps: BTreeMap<ChainId, Vec<ApplicationId>> = BTreeMap::new();
229 for service in &state.local_services {
230 chain_apps
231 .entry(service.chain_id)
232 .or_default()
233 .push(service.application_id);
234 }
235
236 let mut message_policies: BTreeMap<_, _> = chain_apps
239 .iter()
240 .map(|(chain_id, apps)| {
241 let message_policy = MessagePolicy {
242 reject_message_bundles_without_application_ids: Some(
243 apps.iter()
244 .map(|app_id| GenericApplicationId::User(*app_id))
245 .chain(std::iter::once(GenericApplicationId::User(
246 self.controller_id,
247 )))
248 .chain(std::iter::once(GenericApplicationId::System))
249 .collect(),
250 ),
251 ..Default::default()
252 };
253 (*chain_id, message_policy)
254 })
255 .collect();
256 message_policies.extend(state.local_message_policy);
257
258 let message_policies_to_update: BTreeMap<_, _> = message_policies
259 .iter()
260 .filter(|(chain_id, message_policy)| {
261 self.current_message_policies.get(chain_id) != Some(*message_policy)
262 })
263 .map(|(chain_id, message_policy)| (*chain_id, message_policy.clone()))
264 .collect();
265
266 let old_chains: BTreeSet<_> = self.processors.keys().cloned().collect();
267
268 for (service_chain_id, application_ids) in chain_apps {
270 if let Err(err) = self
271 .update_or_spawn_processor(service_chain_id, application_ids)
272 .await
273 {
274 error!("Error updating or spawning processor: {err}");
275 return;
276 }
277 }
278
279 let active_chains: std::collections::BTreeSet<_> =
282 state.local_services.iter().map(|s| s.chain_id).collect();
283 let stale_chains: BTreeSet<_> = self
284 .processors
285 .keys()
286 .filter(|chain_id| !active_chains.contains(chain_id))
287 .cloned()
288 .collect();
289 for chain_id in &stale_chains {
290 if let Some(handle) = self.processors.get(chain_id) {
291 let update = Update {
292 application_ids: Vec::new(),
293 };
294 if handle.update_sender.send(update).is_err() {
295 self.processors.remove(chain_id);
297 }
298 }
299 }
300
301 let local_chains: BTreeSet<_> = state.local_chains.iter().cloned().collect();
303
304 let old_listened: BTreeSet<_> = old_chains
306 .union(&self.listened_local_chains)
307 .cloned()
308 .collect();
309
310 let desired_listened: BTreeSet<_> = active_chains.union(&local_chains).cloned().collect();
312
313 let owner = worker.owner;
315 let mut new_chains: BTreeMap<_, _> = desired_listened
316 .difference(&old_listened)
317 .map(|chain_id| (*chain_id, Some(owner)))
318 .collect();
319
320 new_chains.extend(
322 state
323 .local_pending_services
324 .iter()
325 .map(|(_, (chain_id, _))| *chain_id)
326 .collect::<BTreeSet<_>>()
327 .difference(&old_listened)
328 .map(|chain_id| (*chain_id, None)),
329 );
330
331 let chains_to_stop: BTreeSet<_> = old_listened
333 .difference(&desired_listened)
334 .cloned()
335 .collect();
336
337 self.listened_local_chains = local_chains.difference(&active_chains).cloned().collect();
340
341 if let Err(error) = self
342 .command_sender
343 .send(ListenerCommand::Listen(new_chains))
344 {
345 error!(%error, "error sending a command to chain listener");
346 }
347 if let Err(error) = self
348 .command_sender
349 .send(ListenerCommand::StopListening(chains_to_stop))
350 {
351 error!(%error, "error sending a command to chain listener");
352 }
353 if let Err(error) = self.command_sender.send(ListenerCommand::SetMessagePolicy(
356 message_policies_to_update,
357 )) {
358 error!(%error, "error sending a command to chain listener");
359 }
360 self.current_message_policies = message_policies;
361 }
362
363 #[expect(clippy::needless_pass_by_ref_mut)]
366 async fn register_worker(&mut self) {
367 let capabilities = self.operators.keys().cloned().collect();
368 let command = WorkerCommand::RegisterWorker { capabilities };
369 let owner = self
370 .chain_client
371 .preferred_owner()
372 .expect("The current wallet should own the chain being watched");
373 let bytes =
374 bcs::to_bytes(&Operation::ExecuteWorkerCommand { owner, command }).expect("bcs bytes");
375 let operation = linera_execution::Operation::User {
376 application_id: self.controller_id,
377 bytes,
378 };
379 if let Err(e) = self
380 .chain_client
381 .execute_operations(vec![operation], vec![])
382 .await
383 {
384 error!("Failed to execute worker on-chain registration: {e}");
386 }
387 }
388
389 async fn update_or_spawn_processor(
390 &mut self,
391 service_chain_id: ChainId,
392 application_ids: Vec<ApplicationId>,
393 ) -> Result<(), anyhow::Error> {
394 if let Some(handle) = self.processors.get(&service_chain_id) {
395 let update = Update {
397 application_ids: application_ids.clone(),
398 };
399 if handle.update_sender.send(update).is_err() {
400 self.processors.remove(&service_chain_id);
402 self.spawn_processor(service_chain_id, application_ids)
403 .await?;
404 }
405 } else {
406 self.spawn_processor(service_chain_id, application_ids)
408 .await?;
409 }
410 Ok(())
411 }
412
413 async fn spawn_processor(
414 &mut self,
415 service_chain_id: ChainId,
416 application_ids: Vec<ApplicationId>,
417 ) -> Result<(), anyhow::Error> {
418 info!(
419 "Spawning TaskProcessor for chain {} with applications {:?}",
420 service_chain_id, application_ids
421 );
422
423 let (update_sender, update_receiver) = mpsc::unbounded_channel();
424
425 let mut chain_client = self
426 .context
427 .lock()
428 .await
429 .make_chain_client(service_chain_id)
430 .await?;
431 if let Some(owner) = self.chain_client.preferred_owner() {
434 chain_client.set_preferred_owner(owner);
435 }
436 let processor = TaskProcessor::new(
437 service_chain_id,
438 application_ids,
439 chain_client,
440 self.cancellation_token.child_token(),
441 self.operators.clone(),
442 self.retry_delay,
443 Some(update_receiver),
444 );
445
446 tokio::spawn(processor.run());
447
448 self.processors
449 .insert(service_chain_id, ProcessorHandle { update_sender });
450
451 Ok(())
452 }
453
454 async fn query_controller_state(&mut self) -> Result<LocalWorkerState, anyhow::Error> {
457 let query = "query { localWorkerState }";
458 let bytes = serde_json::to_vec(&json!({"query": query}))?;
459 let query = linera_execution::Query::User {
460 application_id: self.controller_id,
461 bytes,
462 };
463 let (
464 linera_execution::QueryOutcome {
465 response,
466 operations: _,
467 },
468 _,
469 ) = self.chain_client.query_application(query, None).await?;
470 let linera_execution::QueryResponse::User(response) = response else {
471 anyhow::bail!("cannot get a system response for a user query");
472 };
473 let mut response: serde_json::Value = serde_json::from_slice(&response)?;
474 let state = serde_json::from_value(response["data"]["localWorkerState"].take())?;
475 Ok(state)
476 }
477}