1use std::{
5 collections::{BTreeMap, BTreeSet},
6 sync::Arc,
7};
8
9use futures::{lock::Mutex, stream::StreamExt, FutureExt};
10use linera_base::{
11 data_types::TimeDelta,
12 identifiers::{ApplicationId, ChainId},
13};
14use linera_client::chain_listener::{ClientContext, ListenerCommand};
15use linera_core::{client::ChainClient, node::NotificationStream, worker::Reason};
16use linera_sdk::abis::controller::{LocalWorkerState, Operation, WorkerCommand};
17use serde_json::json;
18use tokio::{
19 select,
20 sync::mpsc::{self, UnboundedSender},
21};
22use tokio_util::sync::CancellationToken;
23use tracing::{debug, error, info};
24
25use crate::task_processor::{OperatorMap, TaskProcessor};
26
27#[derive(Debug)]
29pub struct Update {
30 pub application_ids: Vec<ApplicationId>,
32}
33
34struct ProcessorHandle {
35 update_sender: mpsc::UnboundedSender<Update>,
36}
37
38pub struct Controller<Ctx: ClientContext> {
40 chain_id: ChainId,
41 controller_id: ApplicationId,
42 context: Arc<Mutex<Ctx>>,
43 chain_client: ChainClient<Ctx::Environment>,
44 cancellation_token: CancellationToken,
45 notifications: NotificationStream,
46 operators: OperatorMap,
47 retry_delay: TimeDelta,
48 processors: BTreeMap<ChainId, ProcessorHandle>,
49 listened_local_chains: BTreeSet<ChainId>,
50 command_sender: UnboundedSender<ListenerCommand>,
51}
52
53impl<Ctx> Controller<Ctx>
54where
55 Ctx: ClientContext + Send + Sync + 'static,
56 Ctx::Environment: 'static,
57 <Ctx::Environment as linera_core::Environment>::Storage: Clone,
58{
59 #[expect(clippy::too_many_arguments)]
61 pub fn new(
62 chain_id: ChainId,
63 controller_id: ApplicationId,
64 context: Arc<Mutex<Ctx>>,
65 chain_client: ChainClient<Ctx::Environment>,
66 cancellation_token: CancellationToken,
67 operators: OperatorMap,
68 retry_delay: TimeDelta,
69 command_sender: UnboundedSender<ListenerCommand>,
70 ) -> Self {
71 let notifications = chain_client.subscribe().expect("client subscription");
72 Self {
73 chain_id,
74 controller_id,
75 context,
76 chain_client,
77 cancellation_token,
78 notifications,
79 operators,
80 retry_delay,
81 processors: BTreeMap::new(),
82 listened_local_chains: BTreeSet::new(),
83 command_sender,
84 }
85 }
86
87 pub async fn run(mut self) {
89 info!(
90 "Watching for notifications for controller chain {}",
91 self.chain_id
92 );
93 self.process_controller_state().await;
94 loop {
95 select! {
96 Some(notification) = self.notifications.next() => {
97 if let Reason::NewBlock { .. } = notification.reason {
98 debug!("Processing notification on controller chain {}", self.chain_id);
99 self.process_controller_state().await;
100 }
101 }
102 _ = self.cancellation_token.cancelled().fuse() => {
103 break;
104 }
105 }
106 }
107 debug!("Notification stream ended.");
108 }
109
110 async fn process_controller_state(&mut self) {
111 let state = match self.query_controller_state().await {
112 Ok(state) => state,
113 Err(error) => {
114 error!("Error reading controller state: {error}");
115 return;
116 }
117 };
118 let Some(worker) = state.local_worker else {
119 self.register_worker().await;
121 return;
122 };
123 assert_eq!(
124 worker.owner,
125 self.chain_client
126 .preferred_owner()
127 .expect("The current wallet should own the chain being watched"),
128 "We should be registered with the current account owner."
129 );
130
131 let mut chain_apps: BTreeMap<ChainId, Vec<ApplicationId>> = BTreeMap::new();
133 for service in &state.local_services {
134 chain_apps
135 .entry(service.chain_id)
136 .or_default()
137 .push(service.application_id);
138 }
139
140 let old_chains: BTreeSet<_> = self.processors.keys().cloned().collect();
141
142 for (service_chain_id, application_ids) in chain_apps {
144 if let Err(err) = self
145 .update_or_spawn_processor(service_chain_id, application_ids)
146 .await
147 {
148 error!("Error updating or spawning processor: {err}");
149 return;
150 }
151 }
152
153 let active_chains: std::collections::BTreeSet<_> =
156 state.local_services.iter().map(|s| s.chain_id).collect();
157 let stale_chains: BTreeSet<_> = self
158 .processors
159 .keys()
160 .filter(|chain_id| !active_chains.contains(chain_id))
161 .cloned()
162 .collect();
163 for chain_id in &stale_chains {
164 if let Some(handle) = self.processors.get(chain_id) {
165 let update = Update {
166 application_ids: Vec::new(),
167 };
168 if handle.update_sender.send(update).is_err() {
169 self.processors.remove(chain_id);
171 }
172 }
173 }
174
175 let local_chains: BTreeSet<_> = state.local_chains.iter().cloned().collect();
177
178 let old_listened: BTreeSet<_> = old_chains
180 .union(&self.listened_local_chains)
181 .cloned()
182 .collect();
183
184 let desired_listened: BTreeSet<_> = active_chains.union(&local_chains).cloned().collect();
186
187 let owner = worker.owner;
189 let new_chains: BTreeMap<_, _> = desired_listened
190 .difference(&old_listened)
191 .map(|chain_id| (*chain_id, Some(owner)))
192 .collect();
193
194 let chains_to_stop: BTreeSet<_> = old_listened
196 .difference(&desired_listened)
197 .cloned()
198 .collect();
199
200 self.listened_local_chains = local_chains.difference(&active_chains).cloned().collect();
203
204 if let Err(error) = self.command_sender.send(ListenerCommand::SetMessagePolicy(
205 state.local_message_policy,
206 )) {
207 error!(%error, "error sending a command to chain listener");
208 }
209 if let Err(error) = self
210 .command_sender
211 .send(ListenerCommand::Listen(new_chains))
212 {
213 error!(%error, "error sending a command to chain listener");
214 }
215 if let Err(error) = self
216 .command_sender
217 .send(ListenerCommand::StopListening(chains_to_stop))
218 {
219 error!(%error, "error sending a command to chain listener");
220 }
221 }
222
223 #[expect(clippy::needless_pass_by_ref_mut)]
224 async fn register_worker(&mut self) {
225 let capabilities = self.operators.keys().cloned().collect();
226 let command = WorkerCommand::RegisterWorker { capabilities };
227 let owner = self
228 .chain_client
229 .preferred_owner()
230 .expect("The current wallet should own the chain being watched");
231 let bytes =
232 bcs::to_bytes(&Operation::ExecuteWorkerCommand { owner, command }).expect("bcs bytes");
233 let operation = linera_execution::Operation::User {
234 application_id: self.controller_id,
235 bytes,
236 };
237 if let Err(e) = self
238 .chain_client
239 .execute_operations(vec![operation], vec![])
240 .await
241 {
242 error!("Failed to execute worker on-chain registration: {e}");
244 }
245 }
246
247 async fn update_or_spawn_processor(
248 &mut self,
249 service_chain_id: ChainId,
250 application_ids: Vec<ApplicationId>,
251 ) -> Result<(), anyhow::Error> {
252 if let Some(handle) = self.processors.get(&service_chain_id) {
253 let update = Update {
255 application_ids: application_ids.clone(),
256 };
257 if handle.update_sender.send(update).is_err() {
258 self.processors.remove(&service_chain_id);
260 self.spawn_processor(service_chain_id, application_ids)
261 .await?;
262 }
263 } else {
264 self.spawn_processor(service_chain_id, application_ids)
266 .await?;
267 }
268 Ok(())
269 }
270
271 async fn spawn_processor(
272 &mut self,
273 service_chain_id: ChainId,
274 application_ids: Vec<ApplicationId>,
275 ) -> Result<(), anyhow::Error> {
276 info!(
277 "Spawning TaskProcessor for chain {} with applications {:?}",
278 service_chain_id, application_ids
279 );
280
281 let (update_sender, update_receiver) = mpsc::unbounded_channel();
282
283 let mut chain_client = self
284 .context
285 .lock()
286 .await
287 .make_chain_client(service_chain_id)
288 .await?;
289 if let Some(owner) = self.chain_client.preferred_owner() {
292 chain_client.set_preferred_owner(owner);
293 }
294 let processor = TaskProcessor::new(
295 service_chain_id,
296 application_ids,
297 chain_client,
298 self.cancellation_token.child_token(),
299 self.operators.clone(),
300 self.retry_delay,
301 Some(update_receiver),
302 );
303
304 tokio::spawn(processor.run());
305
306 self.processors
307 .insert(service_chain_id, ProcessorHandle { update_sender });
308
309 Ok(())
310 }
311
312 async fn query_controller_state(&mut self) -> Result<LocalWorkerState, anyhow::Error> {
313 let query = "query { localWorkerState }";
314 let bytes = serde_json::to_vec(&json!({"query": query}))?;
315 let query = linera_execution::Query::User {
316 application_id: self.controller_id,
317 bytes,
318 };
319 let (
320 linera_execution::QueryOutcome {
321 response,
322 operations: _,
323 },
324 _,
325 ) = self.chain_client.query_application(query, None).await?;
326 let linera_execution::QueryResponse::User(response) = response else {
327 anyhow::bail!("cannot get a system response for a user query");
328 };
329 let mut response: serde_json::Value = serde_json::from_slice(&response)?;
330 let state = serde_json::from_value(response["data"]["localWorkerState"].take())?;
331 Ok(state)
332 }
333}