1use 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
16pub struct BatchManager<S: StorageClient + Clone + 'static> {
18 tasks: Arc<DashMap<BatchId, BatchTask>>,
20 storage: Arc<RwLock<S>>,
22}
23
24impl<S: StorageClient + Clone + 'static> BatchManager<S> {
25 pub fn new(storage: Arc<RwLock<S>>) -> Self {
27 Self {
28 tasks: Arc::new(DashMap::new()),
29 storage,
30 }
31 }
32
33 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 pub fn get_task(&self, batch_id: &str) -> Option<BatchTask> {
50 self.tasks.get(batch_id).map(|t| t.clone())
51 }
52
53 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 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 {
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 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 let result = self.process_items(items, space_name).await;
105
106 {
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 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 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 async fn process_items(
173 &self,
174 items: Vec<BatchItem>,
175 space_name: &str,
176 ) -> CoreResult<BatchResultData> {
177 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 let config = BulkInsertConfig::new().with_continue_on_error(true);
238 let mut operation = BulkInsertOperation::new(config);
239 operation.add_items(insert_items);
240
241 let mut storage = self.storage.write();
243 let core_result = operation.execute_sync(&mut *storage, space_name)?;
244
245 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 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 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 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
393enum 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#[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 assert_eq!(
472 json_to_value(serde_json::Value::Null),
473 Some(Value::Null(linkrs_core::NullType::Null))
474 );
475
476 assert_eq!(
478 json_to_value(serde_json::Value::Bool(true)),
479 Some(Value::Bool(true))
480 );
481
482 assert_eq!(
484 json_to_value(serde_json::json!(42)),
485 Some(Value::BigInt(42))
486 );
487
488 assert_eq!(
490 json_to_value(serde_json::json!("hello")),
491 Some(Value::string("hello"))
492 );
493 }
494}