Skip to main content

linera_service/
controller.rs

1// Copyright (c) Zefchain Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4use 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/// An update message sent to a TaskProcessor to change its set of applications.
34#[derive(Debug)]
35pub struct Update {
36    /// The new set of applications the processor should handle.
37    pub application_ids: Vec<ApplicationId>,
38}
39
40struct ProcessorHandle {
41    update_sender: mpsc::UnboundedSender<Update>,
42}
43
44/// Watches a controller chain and spawns task processors for its managed services.
45pub 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    /// Creates a new controller for the given controller chain.
74    #[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    /// Runs the controller, watching for notifications until cancelled.
104    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                        // TODO: handle leader timeouts
176                        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            // Worker needs to be registered.
198            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        // Subscribe to notifications on pending services chains - we need to know when
210        // they sync their blocks.
211        for (managed_service_id, (chain_id, pending_service)) in &state.local_pending_services {
212            // No need to subscribe twice.
213            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        // Build a map of ChainId -> Vec<ApplicationId> from local_services
228        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        // The service chains should reject bundles that contain no messages sent by relevant apps
237        // and no system messages.
238        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        // Update or spawn processors for each chain
269        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        // Send empty updates to processors for chains no longer in the state
280        // This effectively tells them to stop processing applications
281        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                    // Processor has stopped, remove it
296                    self.processors.remove(chain_id);
297                }
298            }
299        }
300
301        // Collect local_chains from state
302        let local_chains: BTreeSet<_> = state.local_chains.iter().cloned().collect();
303
304        // Compute all chains we were listening to (processors + local_chains)
305        let old_listened: BTreeSet<_> = old_chains
306            .union(&self.listened_local_chains)
307            .cloned()
308            .collect();
309
310        // Compute all chains we want to listen to (active services + local_chains)
311        let desired_listened: BTreeSet<_> = active_chains.union(&local_chains).cloned().collect();
312
313        // New chains to listen (neither had processor nor were in listened_local_chains)
314        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        // Follow the chains for pending services, so that they are synced.
321        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        // Chains to stop listening (were listened but no longer needed)
332        let chains_to_stop: BTreeSet<_> = old_listened
333            .difference(&desired_listened)
334            .cloned()
335            .collect();
336
337        // Update listened_local_chains for next iteration
338        // These are local_chains that don't have services (not in active_chains)
339        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        // This may affect chains we are just starting to listen to, so this command has to be sent
354        // last.
355        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    // Keeping `&mut self` avoids borrowing `Controller` through `&self` across `.await`,
364    // which would make `controller.run()` fail the `Send` bound required by `tokio::spawn`.
365    #[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            // TODO: handle leader timeouts
385            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            // Processor exists, send update
396            let update = Update {
397                application_ids: application_ids.clone(),
398            };
399            if handle.update_sender.send(update).is_err() {
400                // Processor has stopped, remove and respawn
401                self.processors.remove(&service_chain_id);
402                self.spawn_processor(service_chain_id, application_ids)
403                    .await?;
404            }
405        } else {
406            // No processor for this chain, spawn one
407            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        // The processor may need to propose blocks with task results - for that, it will
432        // need the chain client to be configured with a preferred owner.
433        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    // Keeping `&mut self` avoids borrowing `Controller` through `&self` across `.await`,
455    // which would make `controller.run()` fail the `Send` bound required by `tokio::spawn`.
456    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}