faucet_core/driver/
http.rs1use super::sink::{PageWrite, RowPolicy, SinkDriver};
11use crate::check::{CheckContext, CheckReport};
12use crate::dlq::BatchAtomicity;
13use crate::error::FaucetError;
14use crate::io::{CallContext, CallSlot};
15use crate::traits::RowOutcome;
16use async_trait::async_trait;
17use futures::stream::{FuturesUnordered, StreamExt};
18use serde_json::Value;
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
22#[non_exhaustive]
23pub enum HttpBatching {
24 PerRecord {
26 concurrency: usize,
28 },
29 Array {
32 batch_size: usize,
34 },
35}
36
37#[async_trait]
40pub trait HttpDriver: Send + Sync {
41 type Session: Send + Sync;
43
44 fn connector_name(&self) -> &'static str;
46
47 fn config_schema(&self) -> Value {
49 serde_json::json!({"type": "object", "properties": {}})
50 }
51
52 fn dataset_uri(&self) -> String {
54 format!("{}://unknown", self.connector_name())
55 }
56
57 fn call_context(&self) -> CallContext {
59 CallContext::new()
60 }
61
62 async fn check(
64 &self,
65 calls: &CallSlot,
66 ctx: &CheckContext,
67 ) -> Result<CheckReport, FaucetError> {
68 let _ = (calls, ctx);
69 Ok(CheckReport::not_implemented())
70 }
71
72 fn batch_atomicity(&self) -> BatchAtomicity {
74 BatchAtomicity::BestEffort
75 }
76
77 fn batching(&self) -> HttpBatching;
79
80 async fn session(&self, calls: &CallSlot) -> Result<Self::Session, FaucetError>;
82
83 async fn send(
85 &self,
86 calls: &CallSlot,
87 session: &Self::Session,
88 body: &Value,
89 ) -> Result<(), FaucetError>;
90}
91
92pub struct HttpSinkDriver<D> {
101 driver: D,
102}
103
104impl<D: HttpDriver> HttpSinkDriver<D> {
105 pub fn new(driver: D) -> Self {
107 Self { driver }
108 }
109
110 pub fn driver(&self) -> &D {
112 &self.driver
113 }
114
115 pub fn driver_mut(&mut self) -> &mut D {
117 &mut self.driver
118 }
119
120 async fn per_record(
121 &self,
122 calls: &CallSlot,
123 session: &D::Session,
124 rows: &[Value],
125 concurrency: usize,
126 policy: RowPolicy,
127 ) -> Result<PageWrite, FaucetError> {
128 let concurrency = concurrency.max(1);
129 if policy == RowPolicy::WholePage {
130 let mut in_flight = FuturesUnordered::new();
131 let mut iter = rows.iter();
132 for record in iter.by_ref().take(concurrency) {
133 in_flight.push(self.driver.send(calls, session, record));
134 }
135 while let Some(result) = in_flight.next().await {
136 result?;
137 if let Some(record) = iter.next() {
138 in_flight.push(self.driver.send(calls, session, record));
139 }
140 }
141 return Ok(PageWrite::accepted(rows.len()));
142 }
143 let pending: Vec<_> = rows
144 .iter()
145 .enumerate()
146 .map(|(i, record)| async move { (i, self.driver.send(calls, session, record).await) })
147 .collect();
148 let mut indexed: Vec<(usize, RowOutcome)> = futures::stream::iter(pending)
149 .buffer_unordered(concurrency)
150 .collect()
151 .await;
152 indexed.sort_by_key(|(i, _)| *i);
153 Ok(PageWrite::from_outcomes(
154 indexed.into_iter().map(|(_, o)| o).collect(),
155 ))
156 }
157
158 async fn arrays(
159 &self,
160 calls: &CallSlot,
161 session: &D::Session,
162 rows: &[Value],
163 batch_size: usize,
164 policy: RowPolicy,
165 ) -> Result<PageWrite, FaucetError> {
166 let mut delivered = 0usize;
167 let mut failed = Vec::new();
168 let mut cause: Option<String> = None;
169 for chunk in crate::batching::chunks(rows, batch_size) {
170 let start = delivered + failed.len();
171 if let Some(msg) = &cause {
172 failed.extend((start..start + chunk.len()).map(|i| {
173 (
174 i,
175 FaucetError::Sink(format!(
176 "array-mode chunk not sent after earlier failure: {msg}"
177 )),
178 )
179 }));
180 continue;
181 }
182 match self
183 .driver
184 .send(calls, session, &Value::Array(chunk.to_vec()))
185 .await
186 {
187 Ok(()) => delivered += chunk.len(),
188 Err(e) if policy == RowPolicy::WholePage || delivered == 0 => return Err(e),
189 Err(e) => {
190 failed.extend((start..start + chunk.len()).map(|i| {
191 (
192 i,
193 FaucetError::Sink(
194 "array-mode chunk POST failed; rows not delivered".into(),
195 ),
196 )
197 }));
198 cause = Some(e.to_string());
199 }
200 }
201 }
202 Ok(PageWrite::accepted(delivered).with_failures(failed))
203 }
204}
205
206#[async_trait]
207impl<D: HttpDriver> SinkDriver for HttpSinkDriver<D> {
208 fn connector_name(&self) -> &'static str {
209 self.driver.connector_name()
210 }
211
212 fn config_schema(&self) -> Value {
213 self.driver.config_schema()
214 }
215
216 fn dataset_uri(&self) -> String {
217 self.driver.dataset_uri()
218 }
219
220 fn call_context(&self) -> CallContext {
221 self.driver.call_context()
222 }
223
224 async fn check(
225 &self,
226 calls: &CallSlot,
227 ctx: &CheckContext,
228 ) -> Result<CheckReport, FaucetError> {
229 self.driver.check(calls, ctx).await
230 }
231
232 fn batch_atomicity(&self) -> BatchAtomicity {
233 self.driver.batch_atomicity()
234 }
235
236 async fn write(
237 &self,
238 calls: &CallSlot,
239 rows: &[Value],
240 policy: RowPolicy,
241 ) -> Result<PageWrite, FaucetError> {
242 if rows.is_empty() {
243 return Ok(PageWrite::accepted(0));
244 }
245 let session = self.driver.session(calls).await?;
246 match self.driver.batching() {
247 HttpBatching::PerRecord { concurrency } => {
248 self.per_record(calls, &session, rows, concurrency, policy)
249 .await
250 }
251 HttpBatching::Array { batch_size } => {
252 self.arrays(calls, &session, rows, batch_size, policy).await
253 }
254 }
255 }
256}