Skip to main content

linkrs_server/batch/
manager.rs

1//! Batch Task Manager
2
3use crate::batch::types::*;
4use crate::storage::StorageClient;
5use dashmap::DashMap;
6use linkrs_api::api_core::{
7    BulkInsertConfig, BulkInsertItem, BulkInsertItemType, BulkInsertOperation,
8};
9use linkrs_api::api_core::{CoreError, CoreResult};
10use linkrs_core::types::VertexId;
11use linkrs_core::{Edge, Value, Vertex};
12use parking_lot::RwLock;
13use std::sync::Arc;
14use uuid::Uuid;
15
16/// Batch Task Manager
17pub struct BatchManager<S: StorageClient + Clone + 'static> {
18    /// Store all batch jobs
19    tasks: Arc<DashMap<BatchId, BatchTask>>,
20    /// Storage Client
21    storage: Arc<RwLock<S>>,
22}
23
24impl<S: StorageClient + Clone + 'static> BatchManager<S> {
25    /// Creating a new batch task manager
26    pub fn new(storage: Arc<RwLock<S>>) -> Self {
27        Self {
28            tasks: Arc::new(DashMap::new()),
29            storage,
30        }
31    }
32
33    /// Creating Batch Tasks
34    pub fn create_task(
35        &self,
36        space_id: u64,
37        batch_type: BatchType,
38        batch_size: usize,
39    ) -> CoreResult<BatchTask> {
40        let batch_id = Uuid::new_v4().to_string();
41        let task = BatchTask::new(batch_id.clone(), space_id, batch_type, batch_size);
42
43        self.tasks.insert(batch_id.clone(), task.clone());
44
45        Ok(task)
46    }
47
48    /// Get Batch Tasks
49    pub fn get_task(&self, batch_id: &str) -> Option<BatchTask> {
50        self.tasks.get(batch_id).map(|t| t.clone())
51    }
52
53    /// Adding Batch Items
54    pub fn add_items(&self, batch_id: &str, items: Vec<BatchItem>) -> CoreResult<usize> {
55        let mut task = self.tasks.get_mut(batch_id).ok_or_else(|| {
56            CoreError::InvalidParameter(format!("Batch task does not exist: {}", batch_id))
57        })?;
58
59        if task.status != BatchStatus::Created {
60            return Err(CoreError::InvalidParameter(format!(
61                "Incorrect batch task status: {:?}",
62                task.status
63            )));
64        }
65
66        let count = task.add_items(items);
67        Ok(count)
68    }
69
70    /// Perform batch tasks
71    pub async fn execute_task(
72        &self,
73        batch_id: &str,
74        space_name: &str,
75    ) -> CoreResult<BatchResultData> {
76        let task = self.tasks.get(batch_id).ok_or_else(|| {
77            CoreError::InvalidParameter(format!("Batch task does not exist: {}", batch_id))
78        })?;
79
80        if task.status != BatchStatus::Created {
81            return Err(CoreError::InvalidParameter(format!(
82                "Incorrect batch task status: {:?}",
83                task.status
84            )));
85        }
86
87        // Update status to running
88        {
89            let mut task = self.tasks.get_mut(batch_id).ok_or_else(|| {
90                CoreError::InvalidParameter(format!("Batch task does not exist: {}", batch_id))
91            })?;
92            task.update_status(BatchStatus::Running);
93        }
94
95        // Get all buffered items
96        let items = {
97            let mut task = self.tasks.get_mut(batch_id).ok_or_else(|| {
98                CoreError::InvalidParameter(format!("Batch task does not exist: {}", batch_id))
99            })?;
100            task.take_buffered_items()
101        };
102
103        // Perform batch insertion using core API
104        let result = self.process_items(items, space_name).await;
105
106        // Update task status and results
107        {
108            let mut task = self.tasks.get_mut(batch_id).ok_or_else(|| {
109                CoreError::InvalidParameter(format!("Batch task does not exist: {}", batch_id))
110            })?;
111
112            match &result {
113                Ok(data) => {
114                    let status = if data.errors.is_empty() {
115                        BatchStatus::Completed
116                    } else {
117                        BatchStatus::Failed
118                    };
119                    task.update_status(status);
120                    task.set_result(data.clone());
121                }
122                Err(e) => {
123                    task.update_status(BatchStatus::Failed);
124                    task.set_result(BatchResultData {
125                        vertices_inserted: 0,
126                        edges_inserted: 0,
127                        vertices_updated: 0,
128                        edges_updated: 0,
129                        vertices_deleted: 0,
130                        edges_deleted: 0,
131                        errors: vec![BatchErrorData {
132                            index: 0,
133                            item_type: BatchItemType::Vertex,
134                            error: e.to_string(),
135                        }],
136                    });
137                }
138            }
139        }
140
141        result
142    }
143
144    /// Cancel Batch Tasks
145    pub fn cancel_task(&self, batch_id: &str) -> CoreResult<()> {
146        let mut task = self.tasks.get_mut(batch_id).ok_or_else(|| {
147            CoreError::InvalidParameter(format!("Batch task does not exist: {}", batch_id))
148        })?;
149
150        match task.status {
151            BatchStatus::Created | BatchStatus::Running => {
152                task.update_status(BatchStatus::Cancelled);
153                Ok(())
154            }
155            _ => Err(CoreError::InvalidParameter(format!(
156                "Unable to cancel tasks with status {:?}",
157                task.status
158            ))),
159        }
160    }
161
162    /// Delete Batch Tasks
163    pub fn remove_task(&self, batch_id: &str) -> CoreResult<()> {
164        self.tasks.remove(batch_id).ok_or_else(|| {
165            CoreError::InvalidParameter(format!("Batch task does not exist: {}", batch_id))
166        })?;
167        Ok(())
168    }
169
170    /// Processing of batch items using core API for inserts and direct
171    /// storage writer calls for updates/deletes (both continue on error).
172    async fn process_items(
173        &self,
174        items: Vec<BatchItem>,
175        space_name: &str,
176    ) -> CoreResult<BatchResultData> {
177        // Partition by kind, keeping original indices for error reporting.
178        let mut insert_items: Vec<BulkInsertItem> = Vec::new();
179        let mut mutations: Vec<(usize, Mutation)> = Vec::new();
180        for (index, item) in items.into_iter().enumerate() {
181            match item {
182                BatchItem::Vertex(data) => {
183                    if let Some(core) = self.convert_vertex_data(data).map(BulkInsertItem::Vertex) {
184                        insert_items.push(core);
185                    }
186                }
187                BatchItem::Edge(data) => {
188                    if let Some(core) = self.convert_edge_data(data).map(BulkInsertItem::Edge) {
189                        insert_items.push(core);
190                    }
191                }
192                BatchItem::UpdateVertex(data) => match self.convert_vertex_data(data) {
193                    Some(vertex) => mutations.push((index, Mutation::UpdateVertex(vertex))),
194                    None => mutations.push((
195                        index,
196                        Mutation::Invalid("update_vertex has an invalid vid or empty tag".into()),
197                    )),
198                },
199                BatchItem::UpdateEdgeData(data) => match self.convert_update_edge_data(&data) {
200                    Some(edge) => mutations.push((index, Mutation::UpdateEdge(edge))),
201                    None => mutations.push((
202                        index,
203                        Mutation::Invalid("update_edge has invalid endpoint ids".into()),
204                    )),
205                },
206                BatchItem::DeleteVertex(data) => match self.convert_delete_vertex_data(&data) {
207                    Some(ops) => {
208                        if ops.is_empty() {
209                            mutations.push((
210                                index,
211                                Mutation::Invalid(
212                                    "delete_vertex requires at least one tag_name".into(),
213                                ),
214                            ));
215                        } else {
216                            for op in ops {
217                                mutations.push((index, op));
218                            }
219                        }
220                    }
221                    None => mutations.push((
222                        index,
223                        Mutation::Invalid("delete_vertex has an invalid vid".into()),
224                    )),
225                },
226                BatchItem::DeleteEdgeData(data) => match self.convert_delete_edge_data(&data) {
227                    Some(op) => mutations.push((index, op)),
228                    None => mutations.push((
229                        index,
230                        Mutation::Invalid("delete_edge has invalid endpoint ids".into()),
231                    )),
232                },
233            }
234        }
235
236        // Inserts go through the core bulk insert path.
237        let config = BulkInsertConfig::new().with_continue_on_error(true);
238        let mut operation = BulkInsertOperation::new(config);
239        operation.add_items(insert_items);
240
241        // Execute batch operation
242        let mut storage = self.storage.write();
243        let core_result = operation.execute_sync(&mut *storage, space_name)?;
244
245        // Updates/deletes run against the same storage handle.
246        let mut mutations_result = MutationCounts::default();
247        let mut mutation_errors: Vec<BatchErrorData> = Vec::new();
248        for (index, mutation) in mutations {
249            let item_type = mutation.item_type();
250            if let Err(error) =
251                self.apply_mutation(&mut storage, space_name, mutation, &mut mutations_result)
252            {
253                mutation_errors.push(BatchErrorData {
254                    index,
255                    item_type,
256                    error,
257                });
258            }
259        }
260        drop(storage);
261
262        // Convert core result to server result
263        Ok(BatchResultData {
264            vertices_inserted: core_result.vertices_inserted,
265            edges_inserted: core_result.edges_inserted,
266            vertices_updated: mutations_result.vertices_updated,
267            edges_updated: mutations_result.edges_updated,
268            vertices_deleted: mutations_result.vertices_deleted,
269            edges_deleted: mutations_result.edges_deleted,
270            errors: core_result
271                .errors
272                .into_iter()
273                .map(|e| BatchErrorData {
274                    index: e.index,
275                    item_type: match e.item_type {
276                        BulkInsertItemType::Vertex => BatchItemType::Vertex,
277                        BulkInsertItemType::Edge => BatchItemType::Edge,
278                    },
279                    error: e.message,
280                })
281                .chain(mutation_errors)
282                .collect(),
283        })
284    }
285
286    /// Apply one update/delete mutation, bumping the matching counter.
287    fn apply_mutation(
288        &self,
289        storage: &mut S,
290        space_name: &str,
291        mutation: Mutation,
292        counts: &mut MutationCounts,
293    ) -> Result<(), String> {
294        match mutation {
295            Mutation::UpdateVertex(vertex) => storage
296                .update_vertex(space_name, vertex)
297                .map(|()| counts.vertices_updated += 1)
298                .map_err(|e| format!("update vertex failed: {e}")),
299            Mutation::UpdateEdge(edge) => storage
300                .update_edge(space_name, edge)
301                .map(|()| counts.edges_updated += 1)
302                .map_err(|e| format!("update edge failed: {e}")),
303            Mutation::DeleteVertex { tag, id } => storage
304                .delete_vertex_with_edges(space_name, &tag, &id)
305                .map(|()| counts.vertices_deleted += 1)
306                .map_err(|e| format!("delete vertex failed: {e}")),
307            Mutation::DeleteEdge {
308                edge_type,
309                src,
310                dst,
311                rank,
312            } => storage
313                .delete_edge(space_name, &src, &dst, &edge_type, rank)
314                .map(|()| counts.edges_deleted += 1)
315                .map_err(|e| format!("delete edge failed: {e}")),
316            Mutation::Invalid(reason) => Err(reason),
317        }
318    }
319
320    fn convert_vertex_data(&self, data: VertexData) -> Option<Vertex> {
321        let vid_value = json_to_value(data.vid)?;
322        let vid = value_to_vertex_id(&vid_value)?;
323
324        if data.tag.is_empty() {
325            return None;
326        }
327
328        let properties: std::collections::HashMap<Arc<str>, Value> = data
329            .properties
330            .into_iter()
331            .filter_map(|(k, v)| json_to_value(v).map(|val| (Arc::from(k.as_str()), val)))
332            .collect();
333
334        Some(Vertex::new(
335            vid,
336            linkrs_core::vertex_edge_path::Tag::new(data.tag, properties),
337        ))
338    }
339
340    fn convert_edge_data(&self, data: EdgeData) -> Option<Edge> {
341        let src_vid_value = json_to_value(data.src_vid)?;
342        let dst_vid_value = json_to_value(data.dst_vid)?;
343        let src_vid = value_to_vertex_id(&src_vid_value)?;
344        let dst_vid = value_to_vertex_id(&dst_vid_value)?;
345
346        let props: std::collections::HashMap<Arc<str>, Value> = data
347            .properties
348            .into_iter()
349            .filter_map(|(k, v)| json_to_value(v).map(|val| (Arc::from(k.as_str()), val)))
350            .collect();
351
352        Some(Edge::new(src_vid, dst_vid, data.edge_type, 0, props))
353    }
354
355    fn convert_update_edge_data(&self, data: &UpdateEdgeData) -> Option<Edge> {
356        let src_vid = value_to_vertex_id(&json_to_value(data.src_vid.clone())?)?;
357        let dst_vid = value_to_vertex_id(&json_to_value(data.dst_vid.clone())?)?;
358        let props: std::collections::HashMap<Arc<str>, Value> = data
359            .properties
360            .clone()
361            .into_iter()
362            .filter_map(|(k, v)| json_to_value(v).map(|val| (Arc::from(k.as_str()), val)))
363            .collect();
364        let mut edge = Edge::new(src_vid, dst_vid, data.edge_type.clone(), 0, props);
365        edge.ranking = data.rank;
366        Some(edge)
367    }
368
369    /// One delete-vertex item fans out to one mutation per tag.
370    fn convert_delete_vertex_data(&self, data: &DeleteVertexData) -> Option<Vec<Mutation>> {
371        let id = value_to_vertex_id(&json_to_value(data.vid.clone())?)?;
372        Some(
373            data.tag_names
374                .iter()
375                .cloned()
376                .map(|tag| Mutation::DeleteVertex { tag, id })
377                .collect(),
378        )
379    }
380
381    fn convert_delete_edge_data(&self, data: &DeleteEdgeData) -> Option<Mutation> {
382        let src = value_to_vertex_id(&json_to_value(data.src_vid.clone())?)?;
383        let dst = value_to_vertex_id(&json_to_value(data.dst_vid.clone())?)?;
384        Some(Mutation::DeleteEdge {
385            edge_type: data.edge_type.clone(),
386            src,
387            dst,
388            rank: data.rank,
389        })
390    }
391}
392
393/// One buffered update/delete item with its storage call arguments resolved.
394enum Mutation {
395    UpdateVertex(Vertex),
396    UpdateEdge(Edge),
397    DeleteVertex {
398        tag: String,
399        id: VertexId,
400    },
401    DeleteEdge {
402        edge_type: String,
403        src: VertexId,
404        dst: VertexId,
405        rank: i64,
406    },
407    Invalid(String),
408}
409
410impl Mutation {
411    fn item_type(&self) -> BatchItemType {
412        match self {
413            Mutation::UpdateVertex(_) | Mutation::UpdateEdge(_) => BatchItemType::Update,
414            Mutation::DeleteVertex { .. } | Mutation::DeleteEdge { .. } => BatchItemType::Delete,
415            Mutation::Invalid(reason) if reason.starts_with("delete") => BatchItemType::Delete,
416            Mutation::Invalid(_) => BatchItemType::Update,
417        }
418    }
419}
420
421/// Counters for applied update/delete mutations.
422#[derive(Default)]
423struct MutationCounts {
424    vertices_updated: usize,
425    edges_updated: usize,
426    vertices_deleted: usize,
427    edges_deleted: usize,
428}
429
430fn value_to_vertex_id(value: &Value) -> Option<VertexId> {
431    match value {
432        Value::Int(i) => VertexId::try_from_int64(*i as i64).ok(),
433        Value::BigInt(i) => VertexId::try_from_int64(*i).ok(),
434        Value::String(s) => VertexId::try_from_string(s.as_str()).ok(),
435        _ => None,
436    }
437}
438
439fn json_to_value(json: serde_json::Value) -> Option<Value> {
440    match json {
441        serde_json::Value::Null => Some(Value::Null(linkrs_core::NullType::Null)),
442        serde_json::Value::Bool(b) => Some(Value::Bool(b)),
443        serde_json::Value::Number(n) => {
444            if let Some(i) = n.as_i64() {
445                Some(Value::BigInt(i))
446            } else {
447                n.as_f64().map(Value::Double)
448            }
449        }
450        serde_json::Value::String(s) => Some(Value::string(s)),
451        serde_json::Value::Array(arr) => {
452            let values: Vec<Value> = arr.into_iter().filter_map(json_to_value).collect();
453            Some(Value::list(linkrs_core::value::List::from(values)))
454        }
455        serde_json::Value::Object(data) => {
456            let obj = serde_json::Value::Object(data);
457            linkrs_core::value::Json::parse(&obj.to_string())
458                .ok()
459                .map(|j| Value::Json(Box::new(j)))
460        }
461    }
462}
463
464#[cfg(test)]
465mod tests {
466    use super::*;
467
468    #[test]
469    fn test_json_to_value() {
470        // Test null
471        assert_eq!(
472            json_to_value(serde_json::Value::Null),
473            Some(Value::Null(linkrs_core::NullType::Null))
474        );
475
476        // Test bool
477        assert_eq!(
478            json_to_value(serde_json::Value::Bool(true)),
479            Some(Value::Bool(true))
480        );
481
482        // Test number
483        assert_eq!(
484            json_to_value(serde_json::json!(42)),
485            Some(Value::BigInt(42))
486        );
487
488        // Test string
489        assert_eq!(
490            json_to_value(serde_json::json!("hello")),
491            Some(Value::string("hello"))
492        );
493    }
494}