Skip to main content

faucet_core/driver/
http.rs

1//! The HTTP family (RFC 0014): send a record, or an array of records, to an
2//! endpoint.
3//!
4//! An [`HttpDriver`] knows how to authenticate a page and send one request
5//! body; [`HttpSinkDriver`] drives batching, the in-flight limit and which
6//! rows a failed request leaves undelivered. Requests are made through the
7//! [`CallSlot`] the driver is handed, so the call context's retry policy,
8//! deadline and metering apply to each one.
9
10use 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/// How a page becomes requests.
21#[derive(Debug, Clone, Copy, PartialEq, Eq)]
22#[non_exhaustive]
23pub enum HttpBatching {
24    /// One request per record, at most `concurrency` in flight.
25    PerRecord {
26        /// Requests in flight at once (at least 1).
27        concurrency: usize,
28    },
29    /// One request per `batch_size` records, the body a JSON array (`0`: one
30    /// request for the page).
31    Array {
32        /// Records per request.
33        batch_size: usize,
34    },
35}
36
37/// An HTTP destination's primitives (RFC 0014). Wrap it in an
38/// [`HttpSinkDriver`], then a [`DriverSink`](super::DriverSink).
39#[async_trait]
40pub trait HttpDriver: Send + Sync {
41    /// What one page's requests share — resolved credentials, typically.
42    type Session: Send + Sync;
43
44    /// The connector's name.
45    fn connector_name(&self) -> &'static str;
46
47    /// The JSON Schema of the connector's config.
48    fn config_schema(&self) -> Value {
49        serde_json::json!({"type": "object", "properties": {}})
50    }
51
52    /// The endpoint, for lineage (credential-free).
53    fn dataset_uri(&self) -> String {
54        format!("{}://unknown", self.connector_name())
55    }
56
57    /// The connector's own call defaults (its retry policy).
58    fn call_context(&self) -> CallContext {
59        CallContext::new()
60    }
61
62    /// A non-mutating preflight probe.
63    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    /// What a failed write leaves behind.
73    fn batch_atomicity(&self) -> BatchAtomicity {
74        BatchAtomicity::BestEffort
75    }
76
77    /// How pages become requests.
78    fn batching(&self) -> HttpBatching;
79
80    /// Prepare what one page's requests share.
81    async fn session(&self, calls: &CallSlot) -> Result<Self::Session, FaucetError>;
82
83    /// Send one body. `Ok` once the endpoint accepted it.
84    async fn send(
85        &self,
86        calls: &CallSlot,
87        session: &Self::Session,
88        body: &Value,
89    ) -> Result<(), FaucetError>;
90}
91
92/// An [`HttpDriver`] as a [`SinkDriver`].
93///
94/// Per record: the whole-page write stops at the first failed request; the
95/// per-row write sends every record and reports each. As arrays: requests go
96/// out in order; the whole-page write stops at the first failure, and the
97/// per-row write reports the rows of every request after a delivered one as
98/// failed, so a delivered row is never reported failed — and a page none of
99/// whose requests was delivered fails as a whole.
100pub struct HttpSinkDriver<D> {
101    driver: D,
102}
103
104impl<D: HttpDriver> HttpSinkDriver<D> {
105    /// The sink driver over `driver`.
106    pub fn new(driver: D) -> Self {
107        Self { driver }
108    }
109
110    /// The HTTP driver.
111    pub fn driver(&self) -> &D {
112        &self.driver
113    }
114
115    /// The HTTP driver, mutably.
116    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}