1use std::sync::{Arc, Mutex};
2
3use serde::{Deserialize, Serialize};
4use tokio::{
5 sync::mpsc,
6 task::JoinHandle,
7 time::{Duration, sleep},
8};
9use tracing::{debug, error};
10
11use crate::{
12 Error, OsClient,
13 bulk::{BulkAction, CreateAction, DeleteAction, IndexAction, UpdateAction, UpdateActionBody},
14};
15
16#[derive(Debug, Clone, Default, Serialize, Deserialize)]
17pub struct BulkerStatistic {
18 pub delete_actions: u64,
20 pub create_actions: u64,
22 pub update_actions: u64,
24 pub index_actions: u64,
26 pub queue_size: usize,
28 pub running_reqwest_calls: usize,
30 pub total_reqwest_calls: usize,
32 pub finished_reqwest_calls: usize,
34 pub error_reqwest_calls: usize,
36 pub success_actions: usize,
38 pub error_actions: usize,
40 pub error_create_actions: usize,
42}
43
44#[derive(Debug, Clone)]
45struct Action {
46 action: BulkAction,
47 document: Option<String>,
48}
49#[derive(Clone)]
51pub struct BulkerBuilder {
52 os_client: Arc<OsClient>,
54 bulk_size: u32,
56 max_concurrent_connections: u32,
58}
59
60impl BulkerBuilder {
61 pub fn new(os_client: Arc<OsClient>, bulk_size: u32) -> Self {
63 BulkerBuilder {
64 os_client,
65 bulk_size,
66 max_concurrent_connections: 10,
67 }
68 }
69
70 pub fn bulk_size(mut self, bulk_size: u32) -> Self {
72 self.bulk_size = bulk_size;
73 self
74 }
75
76 pub fn max_concurrent_connections(mut self, max_concurrent_connections: u32) -> Self {
78 self.max_concurrent_connections = max_concurrent_connections;
79 self
80 }
81
82 pub fn build(self) -> Bulker {
84 let (handle, bulker) = Bulker::new(
85 self.os_client,
86 self.bulk_size,
87 self.max_concurrent_connections,
88 );
89 tokio::spawn(async move {
90 handle.await.unwrap();
91 });
92 bulker
93 }
94}
95
96#[derive(Clone)]
98pub struct Bulker {
99 sender: mpsc::Sender<Action>,
100 queue: Arc<Mutex<Vec<Action>>>,
102 os_client: Arc<OsClient>,
104 #[allow(dead_code)]
106 bulk_size: u32,
107 statistics: Arc<Mutex<BulkerStatistic>>,
111}
112
113impl Bulker {
114 pub fn new(
117 os_client: Arc<OsClient>,
118 bulk_size: u32,
119 max_concurrent_connections: u32,
120 ) -> (JoinHandle<()>, Bulker) {
121 let (sender, receiver) =
122 mpsc::channel::<Action>((bulk_size * (max_concurrent_connections + 1)) as usize);
123 let statistics = Arc::new(Mutex::new(BulkerStatistic::default()));
124 let service = Bulker {
125 bulk_size,
126 sender,
128 os_client: os_client.clone(),
129 queue: Arc::new(Mutex::new(Vec::new())),
130 statistics: statistics.clone(),
131 };
132 let queue = service.queue.clone();
133 let o_client = os_client.clone();
134 let handle = tokio::spawn(async move {
136 process_queue(
137 queue.clone(),
138 receiver,
139 o_client,
140 bulk_size as usize,
141 max_concurrent_connections as usize,
142 statistics.clone(),
143 )
144 .await
145 .unwrap();
146 });
147
148 (handle, service)
149 }
150
151 pub fn statistics(&self) -> BulkerStatistic {
152 self.statistics.lock().unwrap().clone()
153 }
154
155 pub async fn index<T: Serialize>(
168 &self,
169 index: &str,
170 body: &T,
171 id: Option<String>,
172 ) -> Result<(), Error> {
173 let action = BulkAction::Index(IndexAction {
174 index: index.to_owned(),
175 id: id.clone(),
176 pipeline: None,
177 });
178 self.sender
179 .send(Action {
180 action,
181 document: Some(serde_json::to_string(&body)?),
182 })
183 .await
184 .map_err(|e| Error::InternalError(format!("{}", e)))?;
185
186 self.statistics.lock().unwrap().index_actions += 1;
187 Ok(())
188 }
189
190 pub async fn create<T: Serialize>(&self, index: &str, id: &str, body: &T) -> Result<(), Error> {
204 let action = BulkAction::Create(CreateAction {
205 index: index.to_owned(),
206 id: id.to_owned(),
207 ..Default::default()
208 });
209 self.sender
210 .send(Action {
211 action,
212 document: Some(serde_json::to_string(&body)?),
213 })
214 .await
215 .map_err(|e| Error::InternalError(format!("{}", e)))?;
216 self.statistics.lock().unwrap().create_actions += 1;
217 Ok(())
218 }
219
220 pub async fn delete<T: Serialize>(&self, index: &str, id: &str) -> Result<(), Error> {
232 let action = BulkAction::Delete(DeleteAction {
233 index: index.to_owned(),
234 id: id.to_owned(),
235 });
236 self.sender
237 .send(Action {
238 action,
239 document: None,
240 })
241 .await
242 .map_err(|e| Error::InternalError(format!("{}", e)))?;
243 self.statistics.lock().unwrap().delete_actions += 1;
244 Ok(())
245 }
246
247 pub async fn update(
261 &self,
262 index: &str,
263 id: &str,
264 body: &UpdateActionBody,
265 ) -> Result<(), Error> {
266 let action = BulkAction::Update(UpdateAction {
267 index: index.to_owned(),
268 id: id.to_owned(),
269 ..Default::default()
270 });
271 self.sender
272 .send(Action {
273 action,
274 document: Some(serde_json::to_string(&body)?),
275 })
276 .await
277 .map_err(|e| Error::InternalError(format!("{}", e)))?;
278 self.statistics.lock().unwrap().update_actions += 1;
279 Ok(())
280 }
281
282 pub async fn flush(&self) {
284 loop {
285 self.refresh_queue_size();
286 let (status, done) = {
287 let statistics = self.statistics.lock().unwrap();
288 let status = format!(
289 "Bulker: Finished reqwest calls: {}, Total reqwest calls: {}, Queue size: {}, Running reqwest calls: {}, Error reqwest calls: {}, Success actions: {}, Error actions: {}, Error create actions: {}",
290 statistics.finished_reqwest_calls,
291 statistics.total_reqwest_calls,
292 statistics.queue_size,
293 statistics.running_reqwest_calls,
294 statistics.error_reqwest_calls,
295 statistics.success_actions,
296 statistics.error_actions,
297 statistics.error_create_actions
298 );
299 let done = statistics.finished_reqwest_calls == statistics.total_reqwest_calls
300 && statistics.queue_size == 0;
301 (status, done)
302 };
304 println!("{}", status);
305 if done {
306 break;
307 }
308 sleep(Duration::from_secs(1)).await;
309 }
310 }
311
312 fn refresh_queue_size(&self) {
313 let mut statistics = self.statistics.lock().unwrap();
315 statistics.queue_size = self.queue.lock().unwrap().len();
316 }
317}
318
319impl Drop for Bulker {
320 fn drop(&mut self) {
321 tokio::task::block_in_place(|| {
322 tokio::runtime::Handle::current().block_on(async {
323 let records_to_process: Vec<Action> = self.queue.lock().unwrap().clone();
325 if !records_to_process.is_empty() {
326 debug!(
327 "Bulker: Processing remaining records: {:?}",
328 records_to_process.len()
329 );
330 make_reqwest_calls(
331 self.os_client.clone(),
332 records_to_process,
333 self.statistics.clone(),
334 )
335 .await;
336 }
337 self.queue.lock().unwrap().clear();
339 });
340 });
341 }
342}
343
344async fn process_queue(
345 queue: Arc<Mutex<Vec<Action>>>,
346 mut receiver: mpsc::Receiver<Action>,
347 os_client: Arc<OsClient>,
348 bulk_size: usize,
349 max_concurrent_connections: usize,
350 statistics: Arc<Mutex<BulkerStatistic>>,
351) -> Result<(), Error> {
352 let mut reqwest_calls: Vec<tokio::task::JoinHandle<()>> = Vec::new();
353 let mut start = std::time::Instant::now();
354 loop {
355 tokio::select! {
356 Some(json_record) = receiver.recv() => {
358 queue.lock().unwrap().push(json_record.clone());
360
361 let queue_size = queue.lock().unwrap().len();
363 let running_reqwest_calls = reqwest_calls.iter().filter(|task| !task.is_finished()).count();
364 {
365 let mut statistics = statistics.lock().unwrap();
366 statistics.queue_size = queue_size;
367 statistics.running_reqwest_calls = running_reqwest_calls;
368 }
369 let end= std::time::Instant::now();
370
371 if (queue_size >= bulk_size && running_reqwest_calls <= max_concurrent_connections) || (queue_size > 0 && end.duration_since(start).as_secs() > 1 && running_reqwest_calls <= max_concurrent_connections) {
372 let records_to_process: Vec<Action> = queue.lock().unwrap().clone();
374
375 queue.lock().unwrap().clear();
377 {
378 let mut statistics = statistics.lock().unwrap();
379 statistics.total_reqwest_calls += 1;
380 statistics.queue_size = 0;
381 }
382
383 reqwest_calls.push(tokio::spawn(make_reqwest_calls(os_client.clone(), records_to_process, statistics.clone())));
385 start= std::time::Instant::now();
386 }
387 }
388 _ = sleep(Duration::from_secs(1)) => {
390 reqwest_calls.retain(|task| !task.is_finished());
391 }
392 }
393 {
395 let end = std::time::Instant::now();
396 let queue_size = queue.lock().unwrap().len();
397 let running_reqwest_calls = reqwest_calls
398 .iter()
399 .filter(|task| !task.is_finished())
400 .count();
401 if (queue_size >= bulk_size && running_reqwest_calls <= max_concurrent_connections)
402 || (queue_size > 0
403 && end.duration_since(start).as_secs() > 1
404 && running_reqwest_calls <= max_concurrent_connections)
405 {
406 let records_to_process: Vec<Action> = queue.lock().unwrap().clone();
408
409 queue.lock().unwrap().clear();
411 {
412 let mut statistics = statistics.lock().unwrap();
413 statistics.total_reqwest_calls += 1;
414 statistics.queue_size = 0;
415 }
416
417 reqwest_calls.push(tokio::spawn(make_reqwest_calls(
419 os_client.clone(),
420 records_to_process,
421 statistics.clone(),
422 )));
423 start = std::time::Instant::now();
424 }
425 }
426 }
427}
428
429async fn make_reqwest_calls(
430 os_client: Arc<OsClient>,
431 records: Vec<Action>,
432 statistics: Arc<Mutex<BulkerStatistic>>,
433) {
434 let mut bulker = String::new();
435 let total = &records.len();
436
437 for record in records {
438 let j = serde_json::to_string(&record.action).unwrap();
439 bulker.push_str(j.as_str());
440 bulker.push('\n');
441 if let Some(document) = record.document {
442 bulker.push_str(document.as_str());
443 bulker.push('\n');
444 }
445 }
446
447 match os_client.bulk().body(bulker).call().await {
448 Ok(bulk_response) => {
449 let mut statistics = statistics.lock().unwrap();
450 statistics.finished_reqwest_calls += 1;
451 statistics.success_actions += bulk_response.count_ok();
452 statistics.error_actions += bulk_response.count_errors();
453 statistics.error_create_actions += bulk_response.count_create_errors();
454 debug!(
455 "Request successful for record: {:?}",
456 &bulk_response.items.len()
457 );
458 }
459 Err(e) => {
460 let mut statistics = statistics.lock().unwrap();
461 statistics.total_reqwest_calls += 1;
462 statistics.finished_reqwest_calls += 1;
463 statistics.error_reqwest_calls += 1;
464 statistics.error_actions += total;
465 let message = format!("Error making Reqwest call: {:?}", e);
466 error!(message);
467 }
468 }
469}
470
471#[cfg(test)]
472mod tests {
473 use crate::{ConfigurationBuilder, OsClient};
474 use opensearch_testcontainer::*;
475 use serde_json::json;
476 use std::env;
477 use testcontainers::runners::AsyncRunner;
478 use tracing_test::traced_test;
479 use url::Url;
480
481 async fn get_client() -> OsClient {
482 if let Some(_) = env::var("OPENSEARCH_URL").ok() {
483 let client = OsClient::from_environment().unwrap();
484 return client;
485 } else {
486 let os_image = OpenSearch::default();
487 let opensearch = os_image.clone().start().await.unwrap();
488 let host_port = opensearch.get_host_port_ipv4(9200).await.unwrap();
489
490 let client = ConfigurationBuilder::new()
491 .accept_invalid_certificates(true)
492 .base_url(&format!("https://127.0.0.1:{host_port}"))
493 .basic_auth(os_image.username(), os_image.password())
494 .build();
495 return client;
496 }
497 }
498
499 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
500 #[traced_test]
501 async fn bulker_ingester() -> Result<(), Box<dyn std::error::Error>> {
502 let client = get_client().await;
503
504 let test_size: u32 = 100000;
505 let bulker = client
506 .bulker()
507 .bulk_size(1000)
508 .max_concurrent_connections(10)
509 .build();
510 for i in 0..test_size {
511 bulker
512 .index("test", &json!({"id":i}), Some(i.to_string()))
513 .await
514 .unwrap();
515 }
516 bulker.flush().await;
517 let statitics = bulker.statistics();
518 drop(bulker);
519
520 assert_eq!(statitics.index_actions, test_size as u64);
521 assert_eq!(statitics.create_actions, 0);
522 assert_eq!(statitics.delete_actions, 0);
523 assert_eq!(statitics.update_actions, 0);
524 client.indices().refresh().call().await.unwrap();
525
526 let count = client.count().index("test").call().await.unwrap();
527 assert_eq!(count.count, test_size);
528 Ok(())
529 }
530}