Skip to main content

mai_sdk_core/
task_queue.rs

1use anyhow::Result;
2
3pub type TaskId = String;
4
5/// A task that can be run and return an output
6pub trait Runnable<Event, State> {
7    /// a unique identifier for the task, should be unique across all tasks, nodes and plugins
8    fn id(&self) -> TaskId;
9
10    /// run the task and return the output
11    /// if this method is called multiple times, it should return the same output
12    fn run(&self, state: State) -> impl std::future::Future<Output = Result<Event>> + Send;
13}
14
15use std::{collections::HashMap, sync::Arc};
16
17use async_channel::Sender;
18use serde::{de::DeserializeOwned, Deserialize, Serialize};
19use slog::{debug, error, info, warn, Logger};
20use tokio::sync::RwLock;
21
22use crate::{
23    bridge::{EventBridge, PublishEvents},
24    handler::Startable,
25    network::{NetworkMessage, PeerId},
26    storage::{OwnedTasks, RemoteTasks, TaskAssignments},
27};
28
29const BID_ACCEPTANCE: &str = "bid_acceptance";
30const REQUEST_FOR_BIDS: &str = "request_for_bids";
31const TASK_ERROR: &str = "task_error";
32const TASK_COMPLETE: &str = "task_complete";
33const BID: &str = "bid";
34
35#[derive(Debug, Clone, Serialize, Deserialize)]
36pub struct Initialize {
37    pub task: Vec<u8>,
38}
39
40#[derive(Debug, Clone, Serialize, Deserialize)]
41struct RequestForBid {
42    task_id: TaskId,
43    peer_id: PeerId,
44    task: Vec<u8>,
45}
46
47#[derive(Debug, Clone, Serialize, Deserialize)]
48struct Bid {
49    task_id: TaskId,
50    peer_id: PeerId,
51    bid: f64,
52}
53
54#[derive(Debug, Clone, Serialize, Deserialize)]
55struct BidAcceptance {
56    task_id: TaskId,
57    peer_id: PeerId,
58}
59
60#[derive(Debug, Clone, Serialize, Deserialize)]
61struct TaskComplete {
62    task_id: TaskId,
63    peer_id: PeerId,
64    output: Vec<u8>,
65}
66
67#[derive(Debug, Clone, Serialize, Deserialize)]
68struct TaskProgress {
69    task_id: TaskId,
70    peer_id: PeerId,
71    output: Vec<u8>,
72}
73
74#[derive(Debug, Clone, Serialize, Deserialize)]
75struct TaskError {
76    task_id: TaskId,
77    peer_id: PeerId,
78    error: String,
79}
80
81/// A distributed task queue.
82/// Executes tasks across 1 or more nodes, the behaviour of this implementation is at least once.
83#[derive(Debug, Clone)]
84pub struct DistributedTaskQueue<TTask, TTaskOutput, RunnableState> {
85    /// Logger
86    logger: Logger,
87
88    /// An identifier for the local node
89    local_peer_id: PeerId,
90
91    /// Tasks that are owned by the local node, these are tasks that have been submitted to the queue
92    owned_tasks: OwnedTasks<TTask, TTaskOutput>,
93
94    /// Tasks that are owned by remote nodes, these are tasks that have been submitted to the queue
95    remote_tasks: RemoteTasks<TTask>,
96
97    /// A map of task assignments, this is used to track which node is responsible for executing a task
98    task_assignments: TaskAssignments,
99
100    /// State that is passed to the runnable tasks
101    runnable_state: RunnableState,
102
103    /// Event bridge
104    bridge: EventBridge,
105}
106
107impl<
108        TTask: Clone + Runnable<TTaskOutput, TRunnableState> + Serialize,
109        TTaskOutput: Clone,
110        TRunnableState: Clone,
111    > DistributedTaskQueue<TTask, TTaskOutput, TRunnableState>
112{
113    pub fn new(
114        logger: &Logger,
115        local_peer_id: &PeerId,
116        runnable_state: &TRunnableState,
117        bridge: &EventBridge,
118    ) -> Self {
119        Self {
120            logger: logger.clone(),
121            local_peer_id: local_peer_id.clone(),
122            owned_tasks: Arc::new(RwLock::new(HashMap::new())),
123            remote_tasks: Arc::new(RwLock::new(HashMap::new())),
124            task_assignments: Arc::new(RwLock::new(HashMap::new())),
125            runnable_state: runnable_state.clone(),
126            bridge: bridge.clone(),
127        }
128    }
129
130    pub async fn submit_task(&self, task: TTask, tx: Sender<TTaskOutput>) -> Result<()> {
131        // Store the task in the owned_tasks map
132        let mut owned_tasks = self.owned_tasks.write().await;
133        owned_tasks.insert(task.id(), (task.clone(), tx.clone()));
134
135        // Emit a request for bids event
136        let request_for_bids = RequestForBid {
137            task_id: task.id(),
138            peer_id: self.local_peer_id.clone(),
139            task: bincode::serialize(&task)?,
140        };
141        self.bridge
142            .publish(PublishEvents::NetworkMessage(NetworkMessage {
143                message_type: REQUEST_FOR_BIDS.to_string(),
144                payload: bincode::serialize(&request_for_bids)?,
145            }))
146            .await?;
147
148        Ok::<_, anyhow::Error>(())
149    }
150}
151
152impl<
153        TTask: Runnable<TTaskOutput, TRunnableState>
154            + Send
155            + Sync
156            + DeserializeOwned
157            + Serialize
158            + Clone
159            + 'static,
160        TTaskOutput: std::fmt::Debug + Clone + Send + Sync + DeserializeOwned + Serialize + 'static,
161        TRunnableState: Clone + Send + Sync + 'static,
162    > Startable for DistributedTaskQueue<TTask, TTaskOutput, TRunnableState>
163{
164    async fn start(&self) -> Result<()> {
165        info!(self.logger, "starting distributed task queue");
166        let handler_rx = self.bridge.subscribe_to_handler().await;
167        loop {
168            // Wait for the next handler event
169            let event = handler_rx.recv().await;
170            match event {
171                Ok(event) => {
172                    let message = event.message();
173                    let logger = self.logger.clone();
174                    let bridge = self.bridge.clone();
175                    let local_peer_id = self.local_peer_id.clone();
176                    let remote_tasks = self.remote_tasks.clone();
177                    let owned_tasks = self.owned_tasks.clone();
178                    let task_assignments = self.task_assignments.clone();
179                    let runnable_state = self.runnable_state.clone();
180                    tokio::spawn(async move {
181                        /*
182                         * The happy path flow for a job is the following:
183                         * 1. A node sends a request for bids to all other nodes
184                         * 2. All other nodes that can execute the task respond with a bid
185                         * 3. The node that requested the bid selects the best bid and sends a bid acceptance to the winning node
186                         * 4. The winning node executes the task and sends a task complete message to the requesting node
187                         * 5. The requesting node receives the task complete message and stores the output
188                         */
189                        let message_type = message.message_type.as_str();
190                        info!(logger, "received message"; "message_type" => message_type);
191                        match message_type {
192                            REQUEST_FOR_BIDS => {
193                                // Deserialize the request
194                                let request_for_bid: RequestForBid =
195                                    bincode::deserialize(&message.payload)?;
196                                let task = bincode::deserialize::<TTask>(&request_for_bid.task)?;
197
198                                // Store the task in the remote_tasks map for future retrieval
199                                {
200                                    let mut remote_tasks = remote_tasks.write().await;
201                                    remote_tasks
202                                        .insert(request_for_bid.task_id.clone(), task.clone());
203                                }
204
205                                // Respond with a bid for the task
206                                let bid = Bid {
207                                    task_id: request_for_bid.task_id.clone(),
208                                    peer_id: local_peer_id.clone(),
209                                    bid: 1.0,
210                                };
211                                bridge
212                                    .publish(crate::bridge::PublishEvents::NetworkMessage(
213                                        NetworkMessage {
214                                            message_type: BID.to_string(),
215                                            payload: bincode::serialize(&bid)?,
216                                        },
217                                    ))
218                                    .await?;
219
220                                Ok::<_, anyhow::Error>(())
221                            }
222                            BID => {
223                                // Deserialize the bid
224                                let bid: Bid = bincode::deserialize(&message.payload)?;
225
226                                // Check if we have knowledge of the task
227                                if owned_tasks.read().await.get(&bid.task_id).is_none() {
228                                    debug!(logger, "received bid for unknown task"; "task_id" => bid.task_id);
229                                    return Ok(());
230                                }
231
232                                // Check if we have already assigned the task
233                                if task_assignments.read().await.contains_key(&bid.task_id) {
234                                    debug!(logger, "received bid for already assigned task"; "task_id" => bid.task_id);
235                                    return Ok(());
236                                }
237
238                                // Store the task assignment
239                                {
240                                    let mut task_assignments = task_assignments.write().await;
241                                    task_assignments
242                                        .insert(bid.task_id.clone(), bid.peer_id.clone());
243                                }
244
245                                // Respond with a bid acceptance event
246                                if let Err(e) = bridge
247                                    .publish(crate::bridge::PublishEvents::NetworkMessage(
248                                        NetworkMessage {
249                                            message_type: BID_ACCEPTANCE.to_string(),
250                                            payload: bincode::serialize(&BidAcceptance {
251                                                task_id: bid.task_id.clone(),
252                                                peer_id: bid.peer_id.clone(),
253                                            })?,
254                                        },
255                                    ))
256                                    .await
257                                {
258                                    warn!(logger, "failed to emit bid acceptance event"; "error" => e.to_string());
259                                    return Ok(());
260                                };
261
262                                Ok(())
263                            }
264                            BID_ACCEPTANCE => {
265                                let bid_acceptance: BidAcceptance =
266                                    bincode::deserialize(&message.payload)?;
267
268                                // Only act on the event if we are the accepted node
269                                if bid_acceptance.peer_id != local_peer_id {
270                                    debug!(logger, "received bid acceptance for another node"; "peer_id" => bid_acceptance.peer_id);
271                                    return Ok(());
272                                }
273
274                                // Get the task from the remote tasks
275                                let task = {
276                                    let mut remote_tasks = remote_tasks.write().await;
277                                    remote_tasks.remove(&bid_acceptance.task_id)
278                                };
279
280                                // TODO: emit error if task is not found and we were assigned
281                                if task.is_none() {
282                                    error!(logger, "task not found for bid acceptance"; "task_id" => bid_acceptance.task_id);
283                                    return Ok(());
284                                }
285                                let task = task.unwrap();
286
287                                // Execute the task & the sender thread to intercept any progress messages
288                                let output = task.run(runnable_state.clone()).await?;
289                                info!(logger, "task complete"; "task_id" => task.id());
290
291                                // Handle output
292                                bridge
293                                    .publish(crate::bridge::PublishEvents::NetworkMessage(
294                                        NetworkMessage {
295                                            message_type: TASK_COMPLETE.to_string(),
296                                            payload: bincode::serialize(&TaskComplete {
297                                                task_id: task.id(),
298                                                peer_id: local_peer_id.clone(),
299                                                output: bincode::serialize(&output)?,
300                                            })?,
301                                        },
302                                    ))
303                                    .await?;
304
305                                Ok(())
306                            }
307                            TASK_COMPLETE => {
308                                // Deserialize the task complete event
309                                let task_complete: TaskComplete =
310                                    bincode::deserialize(&message.payload)?;
311
312                                // Lookup the task from the owned tasks map
313                                let task = {
314                                    let mut owned_tasks = owned_tasks.write().await;
315                                    owned_tasks.remove(&task_complete.task_id)
316                                };
317                                if task.is_none() {
318                                    debug!(logger, "task not found for task complete"; "task_id" => task_complete.task_id);
319                                    return Ok(());
320                                }
321
322                                // Notify the initializer of the output
323                                let (_, tx) = task.unwrap();
324                                tx.send(bincode::deserialize(&task_complete.output)?)
325                                    .await?;
326
327                                // Remove the task assignment
328                                {
329                                    let mut task_assignments = task_assignments.write().await;
330                                    task_assignments.remove(&task_complete.task_id);
331                                }
332
333                                Ok(())
334                            }
335                            TASK_ERROR => {
336                                let task_error =
337                                    bincode::deserialize::<TaskError>(&message.payload)?;
338                                if owned_tasks.read().await.get(&task_error.task_id).is_none() {
339                                    debug!(logger, "received task error for unknown task"; "task_id" => task_error.task_id);
340                                    return Ok(());
341                                }
342                                Ok(())
343                            }
344                            _ => {
345                                debug!(logger, "received unknown message type"; "message_type" => message_type);
346                                Ok(())
347                            }
348                        }
349                    })
350                }
351                Err(e) => {
352                    error!(self.logger, "failed to receive handler event"; "error" => e.to_string());
353                    return Ok(());
354                }
355            };
356        }
357    }
358}