1use super::sink::{CommitTokenDriver, PageWrite, RowPolicy, SinkDriver};
13use super::source::{PageStream, SourceDriver};
14use crate::accounting::WriteReport;
15use crate::check::{CheckContext, CheckReport, Probe};
16use crate::dlq::BatchAtomicity;
17use crate::error::FaucetError;
18use crate::io::{CallContext, CallSlot};
19use crate::pipeline::StreamPage;
20use crate::traits::RowOutcome;
21use async_trait::async_trait;
22use futures::StreamExt;
23use serde_json::Value;
24use std::collections::HashMap;
25use std::sync::Mutex;
26use std::time::Duration;
27
28#[derive(Debug, Clone, PartialEq, Eq)]
30#[non_exhaustive]
31pub struct OutboundMessage {
32 pub destination: String,
34 pub key: Option<Vec<u8>>,
36 pub payload: Vec<u8>,
38 pub headers: Vec<(String, String)>,
40}
41
42impl OutboundMessage {
43 pub fn new(destination: impl Into<String>, payload: impl Into<Vec<u8>>) -> Self {
45 Self {
46 destination: destination.into(),
47 key: None,
48 payload: payload.into(),
49 headers: Vec::new(),
50 }
51 }
52
53 pub fn with_key(mut self, key: impl Into<Vec<u8>>) -> Self {
55 self.key = Some(key.into());
56 self
57 }
58
59 pub fn with_header(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
61 self.headers.push((name.into(), value.into()));
62 self
63 }
64}
65
66#[async_trait]
69pub trait MessageDriver: Send + Sync {
70 fn connector_name(&self) -> &'static str;
72
73 fn config_schema(&self) -> Value {
75 serde_json::json!({"type": "object", "properties": {}})
76 }
77
78 fn dataset_uri(&self) -> String {
80 format!("{}://unknown", self.connector_name())
81 }
82
83 fn call_context(&self) -> CallContext {
85 CallContext::new().without_timeout()
86 }
87
88 async fn check(
90 &self,
91 calls: &CallSlot,
92 ctx: &CheckContext,
93 ) -> Result<CheckReport, FaucetError> {
94 let _ = (calls, ctx);
95 Ok(CheckReport::not_implemented())
96 }
97
98 fn batch_atomicity(&self) -> BatchAtomicity {
100 BatchAtomicity::BestEffort
101 }
102
103 fn encode(&self, record: &Value) -> Result<OutboundMessage, FaucetError>;
105
106 fn group_size(&self) -> usize {
108 0
109 }
110
111 fn group_timeout(&self) -> Option<Duration> {
115 None
116 }
117
118 async fn connect(&self, calls: &CallSlot) -> Result<(), FaucetError> {
121 let _ = calls;
122 Ok(())
123 }
124
125 async fn publish(
129 &self,
130 calls: &CallSlot,
131 group: Vec<OutboundMessage>,
132 ) -> Result<Vec<RowOutcome>, FaucetError>;
133
134 async fn flush(&self, calls: &CallSlot) -> Result<(), FaucetError> {
136 let _ = calls;
137 Ok(())
138 }
139
140 fn transactions(&self) -> Option<&dyn MessageTransactions> {
142 None
143 }
144
145 fn publish_is_one_call(&self) -> bool {
152 true
153 }
154
155 fn page_error(&self, offered: usize, failed: &[(usize, FaucetError)]) -> Option<FaucetError> {
160 let _ = (offered, failed);
161 None
162 }
163}
164
165#[async_trait]
168pub trait MessageTransactions: Send + Sync {
169 async fn begin(&self, calls: &CallSlot, scope: &str) -> Result<(), FaucetError>;
171
172 async fn commit(&self, calls: &CallSlot, scope: &str, token: &str) -> Result<(), FaucetError>;
175
176 async fn abort(&self, calls: &CallSlot, scope: &str);
178
179 async fn last_token(
181 &self,
182 calls: &CallSlot,
183 scope: &str,
184 ) -> Result<Option<String>, FaucetError>;
185}
186
187pub struct MessageSinkDriver<D> {
189 driver: D,
190}
191
192impl<D: MessageDriver> MessageSinkDriver<D> {
193 pub fn new(driver: D) -> Self {
195 Self { driver }
196 }
197
198 pub fn driver(&self) -> &D {
200 &self.driver
201 }
202
203 pub fn driver_mut(&mut self) -> &mut D {
205 &mut self.driver
206 }
207
208 fn timeout(&self, what: &str, after: Duration) -> FaucetError {
209 FaucetError::Timeout {
210 operation: format!("{} {what}", self.driver.connector_name()),
211 after,
212 }
213 }
214
215 async fn bounded<T: Send>(
219 &self,
220 calls: &CallSlot,
221 what: &'static str,
222 fut: impl std::future::Future<Output = Result<T, FaucetError>> + Send,
223 ) -> Result<Result<T, FaucetError>, FaucetError> {
224 let timeout = self.driver.group_timeout();
225 if !self.driver.publish_is_one_call() {
226 return match timeout {
227 Some(after) => tokio::time::timeout(after, fut)
228 .await
229 .map_err(|_| self.timeout(what, after)),
230 None => Ok(fut.await),
231 };
232 }
233 calls
234 .call_once(what, async {
235 match timeout {
236 Some(after) => tokio::time::timeout(after, fut)
237 .await
238 .map_err(|_| self.timeout(what, after)),
239 None => Ok(fut.await),
240 }
241 })
242 .await
243 }
244
245 async fn publish_all(
247 &self,
248 calls: &CallSlot,
249 rows: &[Value],
250 ) -> Result<Vec<RowOutcome>, FaucetError> {
251 let mut outcomes: Vec<RowOutcome> = rows.iter().map(|_| Ok(())).collect();
252 if rows.is_empty() {
253 return Ok(outcomes);
254 }
255 self.driver.connect(calls).await?;
256 let size = match self.driver.group_size() {
257 0 => rows.len(),
258 n => n,
259 };
260 for (g, chunk) in rows.chunks(size).enumerate() {
261 let base = g * size;
262 let mut positions = Vec::with_capacity(chunk.len());
263 let mut group = Vec::with_capacity(chunk.len());
264 for (i, record) in chunk.iter().enumerate() {
265 match self.driver.encode(record) {
266 Ok(m) => {
267 positions.push(base + i);
268 group.push(m);
269 }
270 Err(e) => outcomes[base + i] = Err(e),
271 }
272 }
273 if group.is_empty() {
274 continue;
275 }
276 let sent = group.len();
277 match self
278 .bounded(calls, "publish", self.driver.publish(calls, group))
279 .await
280 {
281 Ok(Ok(results)) if results.len() == sent => {
282 for (pos, r) in positions.into_iter().zip(results) {
283 outcomes[pos] = r;
284 }
285 }
286 Ok(Ok(results)) => {
287 return Err(FaucetError::Sink(format!(
288 "{}: {} outcomes for a group of {sent} messages",
289 self.driver.connector_name(),
290 results.len()
291 )));
292 }
293 Ok(Err(e)) if g == 0 => return Err(e),
294 Ok(Err(e)) => {
295 let name = self.driver.connector_name();
296 fail_open(&mut outcomes[base..], || cut(name, &e));
297 return Ok(outcomes);
298 }
299 Err(FaucetError::Timeout { after, .. }) => {
300 let after = self.driver.group_timeout().unwrap_or(after);
301 fail_open(&mut outcomes[base..], || self.timeout("publish", after));
302 return Ok(outcomes);
303 }
304 Err(cause) => {
305 let name = self.driver.connector_name();
306 fail_open(&mut outcomes[base..], || cut(name, &cause));
307 return Ok(outcomes);
308 }
309 }
310 }
311 Ok(outcomes)
312 }
313}
314
315fn cut(connector: &str, cause: &FaucetError) -> FaucetError {
318 match cause {
319 FaucetError::Cancelled { operation } => FaucetError::Cancelled {
320 operation: operation.clone(),
321 },
322 other => FaucetError::Sink(format!("{connector} publish: {other}")),
323 }
324}
325
326fn fail_open(outcomes: &mut [RowOutcome], err: impl Fn() -> FaucetError) {
328 for o in outcomes.iter_mut().filter(|o| o.is_ok()) {
329 *o = Err(err());
330 }
331}
332
333#[async_trait]
334impl<D: MessageDriver> SinkDriver for MessageSinkDriver<D> {
335 fn connector_name(&self) -> &'static str {
336 self.driver.connector_name()
337 }
338
339 fn config_schema(&self) -> Value {
340 self.driver.config_schema()
341 }
342
343 fn dataset_uri(&self) -> String {
344 self.driver.dataset_uri()
345 }
346
347 fn call_context(&self) -> CallContext {
348 self.driver.call_context()
349 }
350
351 async fn check(
352 &self,
353 calls: &CallSlot,
354 ctx: &CheckContext,
355 ) -> Result<CheckReport, FaucetError> {
356 self.driver.check(calls, ctx).await
357 }
358
359 fn batch_atomicity(&self) -> BatchAtomicity {
360 self.driver.batch_atomicity()
361 }
362
363 async fn write(
364 &self,
365 calls: &CallSlot,
366 rows: &[Value],
367 policy: RowPolicy,
368 ) -> Result<PageWrite, FaucetError> {
369 let write = PageWrite::from_outcomes(self.publish_all(calls, rows).await?);
370 if policy == RowPolicy::WholePage
371 && !write.failed.is_empty()
372 && let Some(e) = self.driver.page_error(rows.len(), &write.failed)
373 {
374 return Err(e);
375 }
376 Ok(write)
377 }
378
379 async fn flush(&self, calls: &CallSlot) -> Result<(), FaucetError> {
380 self.driver.connect(calls).await?;
381 self.bounded(calls, "flush", self.driver.flush(calls))
382 .await?
383 }
384
385 fn commit_tokens(&self) -> Option<&dyn CommitTokenDriver> {
386 self.driver
387 .transactions()
388 .map(|_| self as &dyn CommitTokenDriver)
389 }
390}
391
392#[async_trait]
393impl<D: MessageDriver> CommitTokenDriver for MessageSinkDriver<D> {
394 async fn last_token(
395 &self,
396 calls: &CallSlot,
397 scope: &str,
398 ) -> Result<Option<String>, FaucetError> {
399 match self.driver.transactions() {
400 Some(t) => t.last_token(calls, scope).await,
401 None => Ok(None),
402 }
403 }
404
405 async fn write_with_token(
406 &self,
407 calls: &CallSlot,
408 rows: &[Value],
409 scope: &str,
410 token: &str,
411 ) -> Result<WriteReport, FaucetError> {
412 let tx = self.driver.transactions().ok_or_else(|| {
413 FaucetError::Sink(format!(
414 "{}: exactly-once needs a transactional producer",
415 self.driver.connector_name()
416 ))
417 })?;
418 tx.begin(calls, scope).await?;
419 let outcomes = match self.publish_all(calls, rows).await {
420 Ok(o) => o,
421 Err(e) => {
422 tx.abort(calls, scope).await;
423 return Err(e);
424 }
425 };
426 if let Some(e) = outcomes.into_iter().find_map(Result::err) {
427 tx.abort(calls, scope).await;
428 return Err(e);
429 }
430 if let Err(e) = tx.commit(calls, scope, token).await {
431 tx.abort(calls, scope).await;
432 return Err(e);
433 }
434 Ok(WriteReport::accepted(rows.len()))
435 }
436}
437
438#[derive(Debug, Clone, PartialEq, Eq, Hash)]
440#[non_exhaustive]
441pub struct AckToken(pub String);
442
443#[derive(Debug, Clone, PartialEq, Eq)]
445#[non_exhaustive]
446pub struct InboundMessage {
447 pub source: String,
449 pub payload: Vec<u8>,
451 pub ack: AckToken,
453}
454
455impl InboundMessage {
456 pub fn new(source: impl Into<String>, payload: impl Into<Vec<u8>>, ack: AckToken) -> Self {
458 Self {
459 source: source.into(),
460 payload: payload.into(),
461 ack,
462 }
463 }
464}
465
466#[async_trait]
469pub trait MessageConsumer: Send + Sync {
470 fn connector_name(&self) -> &'static str;
472
473 fn config_schema(&self) -> Value {
475 serde_json::json!({"type": "object", "properties": {}})
476 }
477
478 fn dataset_uri(&self) -> String {
480 format!("{}://unknown", self.connector_name())
481 }
482
483 fn call_context(&self) -> CallContext {
485 CallContext::new()
486 }
487
488 async fn receive(
491 &self,
492 calls: &CallSlot,
493 max: usize,
494 ) -> Result<Vec<InboundMessage>, FaucetError>;
495
496 async fn ack(&self, calls: &CallSlot, tokens: Vec<AckToken>) -> Result<(), FaucetError>;
498
499 fn decode(&self, message: &InboundMessage) -> Result<Value, FaucetError>;
501
502 fn max_messages(&self) -> Option<usize> {
504 None
505 }
506
507 fn idle_receives_to_stop(&self) -> u32 {
509 1
510 }
511
512 async fn check(
516 &self,
517 calls: &CallSlot,
518 ctx: &CheckContext,
519 ) -> Result<Option<CheckReport>, FaucetError> {
520 let _ = (calls, ctx);
521 Ok(None)
522 }
523}
524
525pub struct MessageSourceDriver<C> {
533 consumer: C,
534 pending: Mutex<Vec<AckToken>>,
535}
536
537impl<C: MessageConsumer> MessageSourceDriver<C> {
538 pub fn new(consumer: C) -> Self {
540 Self {
541 consumer,
542 pending: Mutex::new(Vec::new()),
543 }
544 }
545
546 pub fn consumer(&self) -> &C {
548 &self.consumer
549 }
550
551 async fn ack_pending(&self, calls: &CallSlot) -> Result<(), FaucetError> {
552 let tokens = std::mem::take(&mut *self.pending.lock().unwrap_or_else(|e| e.into_inner()));
553 if tokens.is_empty() {
554 return Ok(());
555 }
556 self.consumer.ack(calls, tokens).await
557 }
558}
559
560#[async_trait]
561impl<C: MessageConsumer> SourceDriver for MessageSourceDriver<C> {
562 fn connector_name(&self) -> &'static str {
563 self.consumer.connector_name()
564 }
565
566 fn config_schema(&self) -> Value {
567 self.consumer.config_schema()
568 }
569
570 fn dataset_uri(&self) -> String {
571 self.consumer.dataset_uri()
572 }
573
574 fn call_context(&self) -> CallContext {
575 self.consumer.call_context()
576 }
577
578 fn consumes_destructively(&self) -> bool {
579 true
580 }
581
582 fn read<'a>(
583 &'a self,
584 calls: &'a CallSlot,
585 _context: &'a HashMap<String, Value>,
586 batch_size: usize,
587 ) -> PageStream<'a> {
588 self.pages(calls, batch_size, true, self.consumer.max_messages())
589 }
590
591 async fn fetch(
592 &self,
593 calls: &CallSlot,
594 _context: &HashMap<String, Value>,
595 ) -> Option<Result<(Vec<Value>, Option<Value>), FaucetError>> {
596 let cap = self
597 .consumer
598 .max_messages()
599 .unwrap_or(crate::pipeline::DEFAULT_BATCH_SIZE);
600 let mut pages = self.pages(calls, 0, false, Some(cap));
601 let mut rows = Vec::new();
602 let mut bookmark = None;
603 while let Some(page) = pages.next().await {
604 match page {
605 Ok(page) => {
606 rows.extend(page.records);
607 bookmark = page.bookmark;
608 }
609 Err(e) => return Some(Err(e)),
610 }
611 }
612 Some(Ok((rows, bookmark)))
613 }
614
615 async fn check(
616 &self,
617 calls: &CallSlot,
618 ctx: &CheckContext,
619 ) -> Result<Option<CheckReport>, FaucetError> {
620 if let Some(report) = self.consumer.check(calls, ctx).await? {
621 return Ok(Some(report));
622 }
623 Ok(Some(CheckReport::single(Probe::skip(
624 "read",
625 "reading a page would consume messages; no consumer probe is defined",
626 ))))
627 }
628}
629
630impl<C: MessageConsumer> MessageSourceDriver<C> {
631 fn pages<'a>(
637 &'a self,
638 calls: &'a CallSlot,
639 batch_size: usize,
640 ack: bool,
641 limit: Option<usize>,
642 ) -> PageStream<'a> {
643 let per_page = if batch_size == 0 {
644 crate::pipeline::DEFAULT_BATCH_SIZE
645 } else {
646 batch_size
647 };
648 Box::pin(async_stream::try_stream! {
649 let mut consumed = 0usize;
650 let mut idle = 0u32;
651 loop {
652 if ack {
653 self.ack_pending(calls).await?;
654 }
655 let room = match limit {
656 Some(max) if consumed >= max => break,
657 Some(max) => per_page.min(max - consumed),
658 None => per_page,
659 };
660 let messages = self.consumer.receive(calls, room).await?;
661 if messages.is_empty() {
662 idle += 1;
663 if idle >= self.consumer.idle_receives_to_stop() {
664 break;
665 }
666 continue;
667 }
668 idle = 0;
669 let mut records = Vec::with_capacity(messages.len());
670 let mut tokens = Vec::with_capacity(messages.len());
671 for m in &messages {
672 records.push(self.consumer.decode(m)?);
673 tokens.push(m.ack.clone());
674 }
675 consumed += records.len();
676 if ack {
677 *self.pending.lock().unwrap_or_else(|e| e.into_inner()) = tokens;
678 }
679 yield StreamPage {
680 records,
681 bookmark: Some(serde_json::json!({ "consumed": consumed })),
682 };
683 }
684 })
685 }
686}
687
688#[cfg(test)]
689mod tests {
690 use super::*;
691
692 #[test]
693 fn a_cut_short_group_keeps_a_cancel_and_words_anything_else() {
694 let cancel = FaucetError::Cancelled {
695 operation: "publish call".into(),
696 };
697 assert!(matches!(
698 cut("broker", &cancel),
699 FaucetError::Cancelled { operation } if operation == "publish call"
700 ));
701 let other = cut("broker", &FaucetError::Source("x".into()));
702 assert!(
703 matches!(&other, FaucetError::Sink(m) if m.starts_with("broker publish: ")),
704 "{other}"
705 );
706 let mut outcomes: Vec<RowOutcome> = vec![Err(FaucetError::Sink("own".into())), Ok(())];
707 fail_open(&mut outcomes, || cut("broker", &cancel));
708 assert!(matches!(&outcomes[0], Err(FaucetError::Sink(m)) if m == "own"));
709 assert!(matches!(&outcomes[1], Err(FaucetError::Cancelled { .. })));
710 }
711}