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},
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/// An update message sent to a TaskProcessor to change its set of applications.
28#[derive(Debug)]
29pub struct Update {
30    /// The new set of applications the processor should handle.
31    pub application_ids: Vec<ApplicationId>,
32}
33
34struct ProcessorHandle {
35    update_sender: mpsc::UnboundedSender<Update>,
36}
37
38/// Watches a controller chain and spawns task processors for its managed services.
39pub 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    /// Creates a new controller for the given controller chain.
60    #[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    /// Runs the controller, watching for notifications until cancelled.
88    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            // Worker needs to be registered.
120            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        // Build a map of ChainId -> Vec<ApplicationId> from local_services
132        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        // Update or spawn processors for each chain
143        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        // Send empty updates to processors for chains no longer in the state
154        // This effectively tells them to stop processing applications
155        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                    // Processor has stopped, remove it
170                    self.processors.remove(chain_id);
171                }
172            }
173        }
174
175        // Collect local_chains from state
176        let local_chains: BTreeSet<_> = state.local_chains.iter().cloned().collect();
177
178        // Compute all chains we were listening to (processors + local_chains)
179        let old_listened: BTreeSet<_> = old_chains
180            .union(&self.listened_local_chains)
181            .cloned()
182            .collect();
183
184        // Compute all chains we want to listen to (active services + local_chains)
185        let desired_listened: BTreeSet<_> = active_chains.union(&local_chains).cloned().collect();
186
187        // New chains to listen (neither had processor nor were in listened_local_chains)
188        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        // Chains to stop listening (were listened but no longer needed)
195        let chains_to_stop: BTreeSet<_> = old_listened
196            .difference(&desired_listened)
197            .cloned()
198            .collect();
199
200        // Update listened_local_chains for next iteration
201        // These are local_chains that don't have services (not in active_chains)
202        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            // TODO: handle leader timeouts
243            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            // Processor exists, send update
254            let update = Update {
255                application_ids: application_ids.clone(),
256            };
257            if handle.update_sender.send(update).is_err() {
258                // Processor has stopped, remove and respawn
259                self.processors.remove(&service_chain_id);
260                self.spawn_processor(service_chain_id, application_ids)
261                    .await?;
262            }
263        } else {
264            // No processor for this chain, spawn one
265            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        // The processor may need to propose blocks with task results - for that, it will
290        // need the chain client to be configured with a preferred owner.
291        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}