1use std::future::Future;
4use std::sync::Arc;
5
6use tokio::sync::{Mutex, Semaphore};
7use tokio::task::JoinSet;
8
9use crate::error::{Error, Result};
10use crate::schema::Statement;
11
12pub const DEFAULT_CHUNK_SIZE: usize = 100;
14pub const DEFAULT_MAX_WORKERS: usize = 8;
16
17pub(crate) fn chunk_statements(stmts: &[Statement], chunk_size: usize) -> Vec<Vec<Statement>> {
20 let chunk_size = if chunk_size == 0 {
21 DEFAULT_CHUNK_SIZE
22 } else {
23 chunk_size
24 };
25 stmts.chunks(chunk_size).map(|c| c.to_vec()).collect()
26}
27
28pub(crate) async fn pipeline_drive<F, Fut>(
32 stmts: &[Statement],
33 chunk_size: usize,
34 max_workers: usize,
35 exec: F,
36) -> Result<()>
37where
38 F: Fn(Vec<Statement>) -> Fut + Send + Sync + 'static,
39 Fut: Future<Output = Result<()>> + Send + 'static,
40{
41 if stmts.is_empty() {
42 return Ok(());
43 }
44 let max_workers = if max_workers == 0 {
45 DEFAULT_MAX_WORKERS
46 } else {
47 max_workers
48 };
49 let chunks = chunk_statements(stmts, chunk_size);
50
51 let sem = Arc::new(Semaphore::new(max_workers));
52 let first_err: Arc<Mutex<Option<Error>>> = Arc::new(Mutex::new(None));
53 let exec = Arc::new(exec);
54 let mut set: JoinSet<()> = JoinSet::new();
55
56 for chunk in chunks {
57 if first_err.lock().await.is_some() {
59 break;
60 }
61 let permit = Arc::clone(&sem)
62 .acquire_owned()
63 .await
64 .map_err(|_| Error::pool("pipeline semaphore closed"))?;
65 let exec = Arc::clone(&exec);
66 let first_err = Arc::clone(&first_err);
67 set.spawn(async move {
68 let _permit = permit; if let Err(e) = exec(chunk).await {
70 let mut slot = first_err.lock().await;
71 if slot.is_none() {
72 *slot = Some(e);
73 }
74 }
75 });
76 }
77
78 while set.join_next().await.is_some() {}
80
81 match Arc::try_unwrap(first_err) {
82 Ok(m) => match m.into_inner() {
83 Some(e) => Err(e),
84 None => Ok(()),
85 },
86 Err(arc) => {
87 let mut guard = arc.lock().await;
88 match guard.take() {
89 Some(e) => Err(e),
90 None => Ok(()),
91 }
92 }
93 }
94}
95
96#[cfg(test)]
97mod tests {
98 use super::*;
99 use crate::error::Error;
100 use crate::schema::Statement;
101 use std::sync::Arc;
102 use std::sync::atomic::{AtomicUsize, Ordering};
103 use std::time::Duration;
104
105 struct ConcurrencyGauge {
107 current: AtomicUsize,
108 max: AtomicUsize,
109 }
110
111 impl ConcurrencyGauge {
112 fn new() -> Self {
113 Self {
114 current: AtomicUsize::new(0),
115 max: AtomicUsize::new(0),
116 }
117 }
118 fn enter(&self) {
119 let now = self.current.fetch_add(1, Ordering::SeqCst) + 1;
120 self.max.fetch_max(now, Ordering::SeqCst);
121 }
122 fn leave(&self) {
123 self.current.fetch_sub(1, Ordering::SeqCst);
124 }
125 }
126
127 fn stmts(n: usize) -> Vec<Statement> {
128 (0..n)
129 .map(|i| Statement::new(format!("CREATE (n {{i: {}}})", i)))
130 .collect()
131 }
132
133 #[test]
134 fn test_chunk_statements_exact() {
135 let c = chunk_statements(&stmts(10), 3);
136 assert_eq!(c.len(), 4); assert_eq!(c[0].len(), 3);
138 assert_eq!(c[3].len(), 1);
139 }
140
141 #[test]
142 fn test_chunk_statements_single_chunk() {
143 let c = chunk_statements(&stmts(50), 100);
144 assert_eq!(c.len(), 1);
145 assert_eq!(c[0].len(), 50);
146 }
147
148 #[test]
149 fn test_chunk_statements_chunk_size_one() {
150 let c = chunk_statements(&stmts(5), 1);
151 assert_eq!(c.len(), 5);
152 }
153
154 #[test]
155 fn test_chunk_statements_zero_defaults() {
156 let c = chunk_statements(&stmts(250), 0);
157 assert_eq!(c.len(), 3); }
159
160 #[test]
161 fn test_chunk_statements_empty() {
162 let c = chunk_statements(&[], 10);
163 assert!(c.is_empty());
164 }
165
166 #[tokio::test]
167 async fn test_pipeline_drive_all_succeed() {
168 let s = stmts(25);
169 let res = super::pipeline_drive(&s, 10, 4, |_chunk| async { Ok(()) }).await;
170 assert!(res.is_ok());
171 }
172
173 #[tokio::test]
174 async fn test_pipeline_drive_first_error_returned() {
175 let s = stmts(30);
176 let res = super::pipeline_drive(&s, 10, 8, |chunk| async move {
178 if chunk[0].query.contains("i: 0") {
179 Err(Error::Query {
180 code: "42000".into(),
181 message: "boom".into(),
182 })
183 } else {
184 Ok(())
185 }
186 })
187 .await;
188 let err = res.unwrap_err();
189 assert!(err.to_string().contains("boom"));
190 }
191
192 #[tokio::test]
193 async fn test_pipeline_drive_respects_worker_bound() {
194 let s = stmts(40); let gauge = Arc::new(ConcurrencyGauge::new());
196 let g = gauge.clone();
197 let res = super::pipeline_drive(&s, 10, 2, move |_chunk| {
198 let g = g.clone();
199 async move {
200 g.enter();
201 tokio::time::sleep(Duration::from_millis(20)).await;
202 g.leave();
203 Ok(())
204 }
205 })
206 .await;
207 assert!(res.is_ok());
208 assert!(
209 gauge.max.load(Ordering::SeqCst) <= 2,
210 "exceeded worker bound"
211 );
212 }
213
214 #[tokio::test]
215 async fn test_pipeline_drive_empty_ok() {
216 let res = super::pipeline_drive(&[], 10, 4, |_c| async { Ok(()) }).await;
217 assert!(res.is_ok());
218 }
219}