1use std::{
10 cmp::Reverse,
11 collections::{BTreeMap, BTreeSet, BinaryHeap},
12 path::PathBuf,
13 sync::Arc,
14};
15
16use async_graphql::InputType as _;
17use futures::{stream::StreamExt, FutureExt};
18use linera_base::{
19 data_types::{TimeDelta, Timestamp},
20 identifiers::{ApplicationId, ChainId},
21 task_processor::{ProcessorActions, Task, TaskOutcome},
22};
23use linera_core::{
24 client::ChainClient, data_types::ClientOutcome, node::NotificationStream, worker::Reason,
25};
26use serde_json::json;
27use tokio::{io::AsyncWriteExt, process::Command, select, sync::mpsc};
28use tokio_util::sync::CancellationToken;
29use tracing::{debug, error, info};
30
31use crate::controller::Update;
32
33pub type OperatorMap = Arc<BTreeMap<String, PathBuf>>;
35
36pub fn parse_operator(s: &str) -> Result<(String, PathBuf), String> {
39 if let Some((name, path)) = s.split_once('=') {
40 Ok((name.to_string(), PathBuf::from(path)))
41 } else {
42 Ok((s.to_string(), PathBuf::from(s)))
43 }
44}
45
46type Deadline = Reverse<(Timestamp, Option<ApplicationId>)>;
47
48struct BatchResult {
50 application_id: ApplicationId,
51 retry_at: Option<Timestamp>,
53}
54
55pub struct TaskProcessor<Env: linera_core::Environment> {
57 chain_id: ChainId,
58 application_ids: Vec<ApplicationId>,
59 cursors: BTreeMap<ApplicationId, String>,
60 chain_client: ChainClient<Env>,
61 cancellation_token: CancellationToken,
62 notifications: NotificationStream,
63 batch_sender: mpsc::UnboundedSender<BatchResult>,
64 batch_receiver: mpsc::UnboundedReceiver<BatchResult>,
65 update_receiver: mpsc::UnboundedReceiver<Update>,
66 deadlines: BinaryHeap<Deadline>,
67 operators: OperatorMap,
68 retry_delay: TimeDelta,
69 in_flight_apps: BTreeSet<ApplicationId>,
70}
71
72impl<Env: linera_core::Environment> TaskProcessor<Env> {
73 pub fn new(
75 chain_id: ChainId,
76 application_ids: Vec<ApplicationId>,
77 chain_client: ChainClient<Env>,
78 cancellation_token: CancellationToken,
79 operators: OperatorMap,
80 retry_delay: TimeDelta,
81 update_receiver: Option<mpsc::UnboundedReceiver<Update>>,
82 ) -> Self {
83 let notifications = chain_client.subscribe().expect("client subscription");
84 let (batch_sender, batch_receiver) = mpsc::unbounded_channel();
85 let update_receiver = update_receiver.unwrap_or_else(|| mpsc::unbounded_channel().1);
86 Self {
87 chain_id,
88 application_ids,
89 cursors: BTreeMap::new(),
90 chain_client,
91 cancellation_token,
92 notifications,
93 batch_sender,
94 batch_receiver,
95 update_receiver,
96 deadlines: BinaryHeap::new(),
97 operators,
98 retry_delay,
99 in_flight_apps: BTreeSet::new(),
100 }
101 }
102
103 pub async fn run(mut self) {
105 info!("Watching for notifications for chain {}", self.chain_id);
106 self.process_actions(self.application_ids.clone()).await;
107 loop {
108 select! {
109 Some(notification) = self.notifications.next() => {
110 if let Reason::NewBlock { .. } = notification.reason {
111 debug!(%self.chain_id, "Processing notification");
112 self.process_actions(self.application_ids.clone()).await;
113 }
114 }
115 _ = tokio::time::sleep(Self::duration_until_next_deadline(&self.deadlines)) => {
116 debug!("Processing event");
117 let application_ids = self.process_events();
118 self.process_actions(application_ids).await;
119 }
120 Some(result) = self.batch_receiver.recv() => {
121 self.in_flight_apps.remove(&result.application_id);
122 if self.application_ids.contains(&result.application_id) {
125 if let Some(retry_at) = result.retry_at {
126 self.deadlines.push(Reverse((
127 retry_at,
128 Some(result.application_id),
129 )));
130 } else {
131 self.process_actions(vec![result.application_id]).await;
133 }
134 }
135 }
136 Some(update) = self.update_receiver.recv() => {
137 self.apply_update(update).await;
138 }
139 _ = self.cancellation_token.cancelled().fuse() => {
140 break;
141 }
142 }
143 }
144 debug!("Notification stream ended.");
145 }
146
147 fn duration_until_next_deadline(deadlines: &BinaryHeap<Deadline>) -> tokio::time::Duration {
148 deadlines
149 .peek()
150 .map_or(tokio::time::Duration::MAX, |Reverse((x, _))| {
151 x.delta_since(Timestamp::now()).as_duration()
152 })
153 }
154
155 async fn apply_update(&mut self, update: Update) {
156 info!(
157 "Applying update for chain {}: {:?}",
158 self.chain_id, update.application_ids
159 );
160
161 let new_app_set: BTreeSet<_> = update.application_ids.iter().cloned().collect();
162 let old_app_set: BTreeSet<_> = self.application_ids.iter().cloned().collect();
163
164 self.cursors
165 .retain(|app_id, _| new_app_set.contains(app_id));
166 self.in_flight_apps
167 .retain(|app_id| new_app_set.contains(app_id));
168
169 self.application_ids = update.application_ids;
171
172 let new_apps = self
174 .application_ids
175 .iter()
176 .filter(|app_id| !old_app_set.contains(app_id))
177 .cloned()
178 .collect::<Vec<_>>();
179 if !new_apps.is_empty() {
180 self.process_actions(new_apps).await;
181 }
182 }
183
184 fn process_events(&mut self) -> Vec<ApplicationId> {
185 let now = Timestamp::now();
186 let mut application_ids = Vec::new();
187 while let Some(deadline) = self.deadlines.pop() {
188 if let Reverse((_, Some(id))) = deadline {
189 application_ids.push(id);
190 }
191 let Some(Reverse((ts, _))) = self.deadlines.peek() else {
192 break;
193 };
194 if *ts > now {
195 break;
196 }
197 }
198 application_ids
199 }
200
201 async fn process_actions(&mut self, application_ids: Vec<ApplicationId>) {
202 for application_id in application_ids {
203 if !self.application_ids.contains(&application_id) {
204 debug!("Skipping {application_id}: it's no longer assigned to this processor");
205 continue;
206 }
207 if self.in_flight_apps.contains(&application_id) {
208 debug!("Skipping {application_id}: tasks already in flight");
209 continue;
210 }
211 debug!("Processing actions for {application_id}");
212 let now = Timestamp::now();
213 let app_cursor = self.cursors.get(&application_id).cloned();
214 let actions = match self.query_actions(application_id, app_cursor, now).await {
215 Ok(actions) => actions,
216 Err(error) => {
217 error!(%application_id, %error, "Error reading application actions");
218 self.deadlines.push(Reverse((
220 now.saturating_add(TimeDelta::from_secs(60)),
221 Some(application_id),
222 )));
223 continue;
224 }
225 };
226 if let Some(timestamp) = actions.request_callback {
227 self.deadlines
228 .push(Reverse((timestamp, Some(application_id))));
229 }
230 if let Some(cursor) = actions.set_cursor {
231 self.cursors.insert(application_id, cursor);
232 }
233 if !actions.execute_tasks.is_empty() {
234 self.in_flight_apps.insert(application_id);
235 let chain_client = self.chain_client.clone();
236 let batch_sender = self.batch_sender.clone();
237 let retry_delay = self.retry_delay;
238 let operators = self.operators.clone();
239 tokio::spawn(async move {
240 let mut handles = Vec::new();
243 for (group, tasks) in group_tasks(actions.execute_tasks) {
244 handles.push((
245 group.clone(),
246 tokio::spawn(Self::process_group(
247 application_id,
248 group,
249 tasks,
250 chain_client.clone(),
251 operators.clone(),
252 retry_delay,
253 )),
254 ));
255 }
256 let mut retry_at = None;
260 for (group, handle) in handles {
261 retry_at = retry_at.max(handle.await.unwrap_or_else(|error| {
262 error!(%application_id, ?group, %error, "Task group panicked");
263 Some(Timestamp::now().saturating_add(retry_delay))
264 }));
265 }
266 if batch_sender
267 .send(BatchResult {
268 application_id,
269 retry_at,
270 })
271 .is_err()
272 {
273 error!(%application_id, "Batch receiver dropped");
274 }
275 });
276 }
277 }
278 }
279
280 async fn process_group(
291 application_id: ApplicationId,
292 group: Option<String>,
293 tasks: Vec<Task>,
294 chain_client: ChainClient<Env>,
295 operators: OperatorMap,
296 retry_delay: TimeDelta,
297 ) -> Option<Timestamp> {
298 let mut handles = Vec::with_capacity(tasks.len());
299 for task in tasks {
300 handles.push(tokio::spawn(Self::execute_task(
301 application_id,
302 task,
303 operators.clone(),
304 )));
305 }
306 for handle in handles {
307 let outcome = match handle.await {
308 Ok(Ok(outcome)) => outcome,
309 Ok(Err(error)) => {
310 error!(%application_id, ?group, %error, "Error executing task");
311 return Some(Timestamp::now().saturating_add(retry_delay));
312 }
313 Err(error) => {
314 error!(%application_id, ?group, %error, "Task panicked");
315 return Some(Timestamp::now().saturating_add(retry_delay));
316 }
317 };
318 if let Err(timestamp) =
319 Self::submit_task_outcome(&chain_client, application_id, &outcome, retry_delay)
320 .await
321 {
322 return Some(timestamp);
323 }
324 }
325 None
326 }
327
328 async fn execute_task(
329 application_id: ApplicationId,
330 task: Task,
331 operators: OperatorMap,
332 ) -> Result<TaskOutcome, anyhow::Error> {
333 let Task {
334 id,
335 operator,
336 input,
337 } = task;
338 let binary_path = operators
339 .get(&operator)
340 .ok_or_else(|| anyhow::anyhow!("unsupported operator: {operator}"))?;
341 debug!("Executing task {operator} ({binary_path:?}) for {application_id}");
342 let mut child = Command::new(binary_path)
343 .stdin(std::process::Stdio::piped())
344 .stdout(std::process::Stdio::piped())
345 .spawn()?;
346
347 let mut stdin = child.stdin.take().expect("stdin should be configured");
348 stdin.write_all(input.as_bytes()).await?;
349 drop(stdin);
350
351 let output = child.wait_with_output().await?;
352 anyhow::ensure!(
353 output.status.success(),
354 "operator {} exited with status: {}",
355 operator,
356 output.status
357 );
358 let outcome = TaskOutcome {
359 id,
360 operator,
361 output: String::from_utf8_lossy(&output.stdout).into(),
362 };
363 debug!("Done executing task for {application_id}");
364 Ok(outcome)
365 }
366
367 #[expect(clippy::needless_pass_by_ref_mut)]
370 async fn query_actions(
371 &mut self,
372 application_id: ApplicationId,
373 cursor: Option<String>,
374 now: Timestamp,
375 ) -> Result<ProcessorActions, anyhow::Error> {
376 let query = format!(
377 "query {{ nextActions(cursor: {}, now: {}) }}",
378 cursor.to_value(),
379 now.to_value(),
380 );
381 let bytes = serde_json::to_vec(&json!({"query": query}))?;
382 let query = linera_execution::Query::User {
383 application_id,
384 bytes,
385 };
386 let (
387 linera_execution::QueryOutcome {
388 response,
389 operations: _,
390 },
391 _,
392 ) = self.chain_client.query_application(query, None).await?;
393 let linera_execution::QueryResponse::User(response) = response else {
394 anyhow::bail!("cannot get a system response for a user query");
395 };
396 let mut response: serde_json::Value = serde_json::from_slice(&response)?;
397 let actions: ProcessorActions =
398 serde_json::from_value(response["data"]["nextActions"].take())?;
399 Ok(actions)
400 }
401
402 async fn submit_task_outcome(
405 chain_client: &ChainClient<Env>,
406 application_id: ApplicationId,
407 task_outcome: &TaskOutcome,
408 retry_delay: TimeDelta,
409 ) -> Result<(), Timestamp> {
410 info!("Submitting task outcome for {application_id}: {task_outcome:?}");
411 let group = &task_outcome.id;
413 let retry_with_delay = || Timestamp::now().saturating_add(retry_delay);
414 let query = task_outcome_query(task_outcome);
415 let bytes = serde_json::to_vec(&json!({"query": query})).map_err(|error| {
416 error!(%application_id, ?group, %error, "Error serializing task outcome query");
417 retry_with_delay()
418 })?;
419 let query = linera_execution::Query::User {
420 application_id,
421 bytes,
422 };
423 let (
424 linera_execution::QueryOutcome {
425 response: _,
426 operations,
427 },
428 _,
429 ) = chain_client
430 .query_application(query, None)
431 .await
432 .map_err(|error| {
433 error!(%application_id, ?group, %error, "Error querying application");
434 retry_with_delay()
435 })?;
436 if !operations.is_empty() {
437 match chain_client
438 .execute_operations(operations, vec![])
439 .await
440 .map_err(|error| {
441 error!(%application_id, ?group, %error, "Error executing operations");
442 retry_with_delay()
443 })? {
444 ClientOutcome::Committed(_) => {}
445 ClientOutcome::WaitForTimeout(timeout) => {
446 error!(%application_id, ?group, "Not the round leader, retrying after {}", timeout.timestamp);
447 return Err(timeout.timestamp);
448 }
449 ClientOutcome::Conflict(_) => {
450 debug!(%application_id, ?group, "Block conflict, retrying immediately");
451 return Err(Timestamp::now());
452 }
453 }
454 }
455 Ok(())
456 }
457}
458
459fn group_tasks(tasks: Vec<Task>) -> Vec<(Option<String>, Vec<Task>)> {
464 let mut groups = BTreeMap::<Option<String>, Vec<Task>>::new();
465 for task in tasks {
466 groups.entry(task.id.clone()).or_default().push(task);
467 }
468 groups.into_iter().collect()
469}
470
471fn task_outcome_query(task_outcome: &TaskOutcome) -> String {
473 format!(
474 "query {{ processTaskOutcome(outcome: {}) }}",
475 task_outcome.to_value()
476 )
477}
478
479#[cfg(test)]
480mod tests {
481 use super::*;
482
483 fn outcome(id: Option<&str>, output: &str) -> TaskOutcome {
484 TaskOutcome {
485 id: id.map(str::to_string),
486 operator: "echo".to_string(),
487 output: output.to_string(),
488 }
489 }
490
491 fn task(id: Option<&str>, input: &str) -> Task {
492 Task {
493 id: id.map(str::to_string),
494 operator: "echo".to_string(),
495 input: input.to_string(),
496 }
497 }
498
499 fn inputs(groups: Vec<(Option<String>, Vec<Task>)>) -> Vec<(Option<String>, Vec<String>)> {
501 groups
502 .into_iter()
503 .map(|(group, tasks)| (group, tasks.into_iter().map(|task| task.input).collect()))
504 .collect()
505 }
506
507 fn group(id: Option<&str>, inputs: &[&str]) -> (Option<String>, Vec<String>) {
508 (
509 id.map(str::to_string),
510 inputs.iter().copied().map(str::to_string).collect(),
511 )
512 }
513
514 #[test]
515 fn test_group_tasks_keeps_distinctly_identified_tasks_apart() {
516 let tasks = vec![task(Some("1"), "first"), task(Some("2"), "second")];
517 assert_eq!(
518 inputs(group_tasks(tasks)),
519 vec![group(Some("1"), &["first"]), group(Some("2"), &["second"])]
520 );
521 }
522
523 #[test]
524 fn test_group_tasks_gathers_the_unidentified_ones() {
525 let tasks = vec![
526 task(None, "first"),
527 task(Some("1"), "second"),
528 task(None, "third"),
529 ];
530 assert_eq!(
531 inputs(group_tasks(tasks)),
532 vec![
533 group(None, &["first", "third"]),
534 group(Some("1"), &["second"])
535 ]
536 );
537 }
538
539 #[test]
540 fn test_group_tasks_gathers_the_ones_sharing_an_id() {
541 let tasks = vec![
542 task(Some("dup"), "first"),
543 task(Some("other"), "second"),
544 task(Some("dup"), "third"),
545 ];
546 assert_eq!(
547 inputs(group_tasks(tasks)),
548 vec![
549 group(Some("dup"), &["first", "third"]),
550 group(Some("other"), &["second"])
551 ]
552 );
553 }
554
555 #[test]
556 fn test_task_outcome_query() {
557 assert_eq!(
558 task_outcome_query(&outcome(None, "hello")),
559 r#"query { processTaskOutcome(outcome: {operator: "echo", output: "hello"}) }"#
560 );
561 assert_eq!(
562 task_outcome_query(&outcome(Some("42"), "hello")),
563 r#"query { processTaskOutcome(outcome: {id: "42", operator: "echo", output: "hello"}) }"#
564 );
565 }
566}