1use std::{
2 collections::VecDeque,
3 future::Future,
4 num::NonZeroU32,
5 pin::Pin,
6 sync::{Arc, OnceLock},
7 task::{Context, Poll},
8 time::Duration,
9};
10
11use futures_util::StreamExt;
12use tokio::{
13 sync::{OwnedSemaphorePermit, Semaphore, mpsc, oneshot},
14 time::Instant,
15};
16use tokio_muxt::{CoalesceMode, MuxTimer};
17use tokio_stream::wrappers::ReceiverStream;
18use tokio_util::task::AbortOnDropHandle;
19use tracing::debug;
20
21use crate::{
22 api::{ApiError, BasinClient, Streaming, retry_builder},
23 error::{AppendError, RequestError},
24 frame_signal::FrameSignal,
25 reconnect::{AdvisedReconnects, ReconnectAdvice},
26 retry::{AppendRetryError, RetryBackoffBuilder},
27 session::StreamHeaders,
28 types::{
29 AccessTokenMode, AppendAck, AppendInput, AppendRetryPolicy, MeteredBytes, ONE_MIB,
30 StreamConfig, StreamName, StreamPosition, ValidationError,
31 },
32};
33
34#[derive(Debug, Clone, thiserror::Error)]
36#[non_exhaustive]
37pub enum AppendSessionError {
38 #[error(transparent)]
40 Append(#[from] AppendError),
41 #[error("append acknowledgement timed out")]
43 AckTimeout,
44 #[error("server disconnected")]
46 ServerDisconnected,
47 #[error("response stream closed early while appends in flight")]
49 StreamClosedEarly,
50 #[error("session already closed")]
52 SessionClosed,
53 #[error("session is closing")]
55 SessionClosing,
56 #[error("session dropped without calling close")]
58 SessionDropped,
59 #[error("invalid append acknowledgement: {0}")]
61 InvalidAck(String),
62 #[error(
65 "append may have taken effect in an earlier attempt; final attempt failed: {final_attempt_error}"
66 )]
67 IndefiniteFailure {
68 #[source]
70 final_attempt_error: Box<Self>,
71 },
72}
73
74impl AppendSessionError {
75 pub fn is_retryable(&self) -> bool {
77 match self {
78 Self::Append(error) => error.is_retryable(),
79 Self::IndefiniteFailure {
80 final_attempt_error,
81 } => final_attempt_error.is_retryable(),
82 Self::AckTimeout | Self::ServerDisconnected => true,
83 Self::StreamClosedEarly
84 | Self::SessionClosed
85 | Self::SessionClosing
86 | Self::SessionDropped
87 | Self::InvalidAck(_) => false,
88 }
89 }
90
91 pub fn has_no_side_effects(&self) -> bool {
93 match self {
94 Self::Append(error) => error.has_no_side_effects(),
95 Self::IndefiniteFailure { .. } => false,
96 Self::SessionClosed | Self::SessionClosing => true,
97 Self::AckTimeout
98 | Self::ServerDisconnected
99 | Self::StreamClosedEarly
100 | Self::SessionDropped
101 | Self::InvalidAck(_) => false,
102 }
103 }
104
105 pub fn request_error(&self) -> Option<&RequestError> {
107 match self {
108 Self::Append(error) => error.request_error(),
109 Self::IndefiniteFailure {
110 final_attempt_error,
111 } => final_attempt_error.request_error(),
112 Self::AckTimeout
113 | Self::ServerDisconnected
114 | Self::StreamClosedEarly
115 | Self::SessionClosed
116 | Self::SessionClosing
117 | Self::SessionDropped
118 | Self::InvalidAck(_) => None,
119 }
120 }
121
122 fn is_authentication_error(&self) -> bool {
123 matches!(
124 self,
125 Self::Append(AppendError::Request(error)) if error.is_authentication_error()
126 )
127 }
128
129 fn is_server_draining(&self) -> bool {
130 matches!(
131 self,
132 Self::Append(AppendError::Request(error)) if error.is_server_draining()
133 )
134 }
135}
136
137impl AppendRetryError for AppendSessionError {
138 fn has_no_side_effects(&self) -> bool {
139 Self::has_no_side_effects(self)
140 }
141
142 fn into_indefinite_failure(self) -> Self {
143 Self::IndefiniteFailure {
144 final_attempt_error: Box::new(self),
145 }
146 }
147}
148
149impl From<ApiError> for AppendSessionError {
150 fn from(error: ApiError) -> Self {
151 Self::Append(error.into())
152 }
153}
154
155pub struct BatchSubmitTicket {
157 rx: oneshot::Receiver<Result<AppendAck, AppendSessionError>>,
158 terminal_err: Arc<OnceLock<AppendSessionError>>,
159}
160
161impl Future for BatchSubmitTicket {
162 type Output = Result<AppendAck, AppendSessionError>;
163
164 fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
165 match Pin::new(&mut self.rx).poll(cx) {
166 Poll::Ready(Ok(res)) => Poll::Ready(res),
167 Poll::Ready(Err(_)) => Poll::Ready(Err(self
168 .terminal_err
169 .get()
170 .cloned()
171 .unwrap_or(AppendSessionError::SessionDropped))),
172 Poll::Pending => Poll::Pending,
173 }
174 }
175}
176
177#[derive(Debug, Clone)]
178pub struct AppendSessionConfig {
180 max_unacked_bytes: u32,
181 max_unacked_batches: Option<u32>,
182 stream_config: Option<StreamConfig>,
183}
184
185impl Default for AppendSessionConfig {
186 fn default() -> Self {
187 Self {
188 max_unacked_bytes: 5 * ONE_MIB,
189 max_unacked_batches: None,
190 stream_config: None,
191 }
192 }
193}
194
195impl AppendSessionConfig {
196 pub fn new() -> Self {
198 Self::default()
199 }
200
201 pub fn with_max_unacked_bytes(self, max_unacked_bytes: u32) -> Result<Self, ValidationError> {
207 if max_unacked_bytes < ONE_MIB {
208 return Err(format!("max_unacked_bytes must be at least {ONE_MIB}").into());
209 }
210 Ok(Self {
211 max_unacked_bytes,
212 ..self
213 })
214 }
215
216 pub fn with_max_unacked_batches(self, max_unacked_batches: NonZeroU32) -> Self {
220 Self {
221 max_unacked_batches: Some(max_unacked_batches.get()),
222 ..self
223 }
224 }
225
226 pub fn with_stream_config(self, stream_config: StreamConfig) -> Self {
233 Self {
234 stream_config: Some(stream_config),
235 ..self
236 }
237 }
238
239 pub(crate) fn stream_config(&self) -> Option<&StreamConfig> {
240 self.stream_config.as_ref()
241 }
242}
243
244struct SessionState {
245 cmd_rx: mpsc::Receiver<Command>,
246 inflight_appends: VecDeque<InflightAppend>,
247 inflight_bytes: usize,
248 close_tx: Option<oneshot::Sender<Result<(), AppendSessionError>>>,
249 total_records: usize,
250 total_acked_records: usize,
251 prev_ack_end: Option<StreamPosition>,
252 stashed_submission: Option<StashedSubmission>,
253}
254
255impl SessionState {
256 fn is_close_complete(&self) -> bool {
257 self.close_tx.is_some()
258 && self.inflight_appends.is_empty()
259 && self.stashed_submission.is_none()
260 }
261}
262
263pub struct AppendSession {
268 cmd_tx: mpsc::Sender<Command>,
269 permits: AppendPermits,
270 terminal_err: Arc<OnceLock<AppendSessionError>>,
271 _handle: AbortOnDropHandle<()>,
272}
273
274impl AppendSession {
275 pub(crate) fn new(
276 client: BasinClient,
277 stream: StreamName,
278 headers: StreamHeaders,
279 config: AppendSessionConfig,
280 ) -> Self {
281 let buffer_size = config
282 .max_unacked_batches
283 .map(|mib| mib as usize)
284 .unwrap_or(DEFAULT_CHANNEL_BUFFER_SIZE);
285 let (cmd_tx, cmd_rx) = mpsc::channel(buffer_size);
286 let permits = AppendPermits::new(config.max_unacked_batches, config.max_unacked_bytes);
287 let retry_builder = retry_builder(&client.config.retry);
288 let terminal_err = Arc::new(OnceLock::new());
289 let handle = AbortOnDropHandle::new(tokio::spawn(run_session_with_retry(
290 client,
291 stream,
292 headers,
293 cmd_rx,
294 retry_builder,
295 buffer_size,
296 terminal_err.clone(),
297 )));
298 Self {
299 cmd_tx,
300 permits,
301 terminal_err,
302 _handle: handle,
303 }
304 }
305
306 pub async fn submit(
316 &self,
317 input: AppendInput,
318 ) -> Result<BatchSubmitTicket, AppendSessionError> {
319 let permit = self.reserve(input.records.metered_bytes() as u32).await?;
320 Ok(permit.submit(input))
321 }
322
323 pub async fn reserve(&self, bytes: u32) -> Result<BatchSubmitPermit, AppendSessionError> {
340 let append_permit = self.permits.acquire(bytes).await;
341 let cmd_tx_permit = self
342 .cmd_tx
343 .clone()
344 .reserve_owned()
345 .await
346 .map_err(|_| self.terminal_err())?;
347 Ok(BatchSubmitPermit {
348 append_permit,
349 cmd_tx_permit,
350 terminal_err: self.terminal_err.clone(),
351 })
352 }
353
354 pub async fn close(self) -> Result<(), AppendSessionError> {
356 let (done_tx, done_rx) = oneshot::channel();
357 self.cmd_tx
358 .send(Command::Close { done_tx })
359 .await
360 .map_err(|_| self.terminal_err())?;
361 done_rx.await.map_err(|_| self.terminal_err())??;
362 Ok(())
363 }
364
365 fn terminal_err(&self) -> AppendSessionError {
366 self.terminal_err
367 .get()
368 .cloned()
369 .unwrap_or(AppendSessionError::SessionClosed)
370 }
371}
372
373pub struct BatchSubmitPermit {
375 append_permit: AppendPermit,
376 cmd_tx_permit: mpsc::OwnedPermit<Command>,
377 terminal_err: Arc<OnceLock<AppendSessionError>>,
378}
379
380impl BatchSubmitPermit {
381 pub fn submit(self, input: AppendInput) -> BatchSubmitTicket {
383 let (ack_tx, ack_rx) = oneshot::channel();
384 self.cmd_tx_permit.send(Command::Submit {
385 input,
386 ack_tx,
387 permit: Some(self.append_permit),
388 });
389 BatchSubmitTicket {
390 rx: ack_rx,
391 terminal_err: self.terminal_err,
392 }
393 }
394}
395
396pub(crate) struct AppendSessionInternal {
397 cmd_tx: mpsc::Sender<Command>,
398 terminal_err: Arc<OnceLock<AppendSessionError>>,
399 _handle: AbortOnDropHandle<()>,
400}
401
402impl AppendSessionInternal {
403 pub(crate) fn new(client: BasinClient, stream: StreamName, headers: StreamHeaders) -> Self {
404 let buffer_size = DEFAULT_CHANNEL_BUFFER_SIZE;
405 let (cmd_tx, cmd_rx) = mpsc::channel(buffer_size);
406 let retry_builder = retry_builder(&client.config.retry);
407 let terminal_err = Arc::new(OnceLock::new());
408 let handle = AbortOnDropHandle::new(tokio::spawn(run_session_with_retry(
409 client,
410 stream,
411 headers,
412 cmd_rx,
413 retry_builder,
414 buffer_size,
415 terminal_err.clone(),
416 )));
417 Self {
418 cmd_tx,
419 terminal_err,
420 _handle: handle,
421 }
422 }
423
424 pub(crate) fn submit(
425 &self,
426 input: AppendInput,
427 ) -> impl Future<Output = Result<BatchSubmitTicket, AppendSessionError>> + Send + 'static {
428 let cmd_tx = self.cmd_tx.clone();
429 let terminal_err = self.terminal_err.clone();
430 async move {
431 let (ack_tx, ack_rx) = oneshot::channel();
432 cmd_tx
433 .send(Command::Submit {
434 input,
435 ack_tx,
436 permit: None,
437 })
438 .await
439 .map_err(|_| {
440 terminal_err
441 .get()
442 .cloned()
443 .unwrap_or(AppendSessionError::SessionClosed)
444 })?;
445 Ok(BatchSubmitTicket {
446 rx: ack_rx,
447 terminal_err,
448 })
449 }
450 }
451
452 pub(crate) async fn close(self) -> Result<(), AppendSessionError> {
453 let (done_tx, done_rx) = oneshot::channel();
454 self.cmd_tx
455 .send(Command::Close { done_tx })
456 .await
457 .map_err(|_| self.terminal_err())?;
458 done_rx.await.map_err(|_| self.terminal_err())??;
459 Ok(())
460 }
461
462 fn terminal_err(&self) -> AppendSessionError {
463 self.terminal_err
464 .get()
465 .cloned()
466 .unwrap_or(AppendSessionError::SessionClosed)
467 }
468}
469
470#[derive(Debug)]
471pub(crate) struct AppendPermit {
472 _count: Option<OwnedSemaphorePermit>,
473 _bytes: OwnedSemaphorePermit,
474}
475
476#[derive(Clone)]
477pub(crate) struct AppendPermits {
478 count: Option<Arc<Semaphore>>,
479 bytes: Arc<Semaphore>,
480}
481
482impl AppendPermits {
483 pub(crate) fn new(count_permits: Option<u32>, bytes_permits: u32) -> Self {
484 Self {
485 count: count_permits.map(|permits| Arc::new(Semaphore::new(permits as usize))),
486 bytes: Arc::new(Semaphore::new(bytes_permits as usize)),
487 }
488 }
489
490 pub(crate) async fn acquire(&self, bytes: u32) -> AppendPermit {
491 AppendPermit {
492 _count: if let Some(count) = self.count.as_ref() {
493 Some(
494 count
495 .clone()
496 .acquire_many_owned(1)
497 .await
498 .expect("semaphore should not be closed"),
499 )
500 } else {
501 None
502 },
503 _bytes: self
504 .bytes
505 .clone()
506 .acquire_many_owned(bytes)
507 .await
508 .expect("semaphore should not be closed"),
509 }
510 }
511}
512
513async fn run_session_with_retry(
514 client: BasinClient,
515 stream: StreamName,
516 headers: StreamHeaders,
517 cmd_rx: mpsc::Receiver<Command>,
518 retry_builder: RetryBackoffBuilder,
519 buffer_size: usize,
520 terminal_err: Arc<OnceLock<AppendSessionError>>,
521) {
522 let access_token_mode = client.config.access_token.mode();
523 let frame_signal = match client.config.retry.append_retry_policy {
524 AppendRetryPolicy::NoSideEffects => Some(FrameSignal::new()),
525 AppendRetryPolicy::All => None,
526 };
527
528 let mut state = SessionState {
529 cmd_rx,
530 inflight_appends: VecDeque::new(),
531 inflight_bytes: 0,
532 close_tx: None,
533 total_records: 0,
534 total_acked_records: 0,
535 prev_ack_end: None,
536 stashed_submission: None,
537 };
538 let mut prev_total_acked_records = 0;
539 let mut retry_backoff = retry_builder.build();
540 let mut advised_reconnects = AdvisedReconnects::default();
541
542 loop {
543 let result = run_session(
544 &client,
545 &stream,
546 &headers,
547 &mut state,
548 buffer_size,
549 &frame_signal,
550 advised_reconnects,
551 )
552 .await;
553
554 match result {
555 Ok(SessionOutcome::Closed) => {
556 break;
557 }
558 Ok(SessionOutcome::ReconnectAdvised) => {
559 advised_reconnects.record();
562 debug!(
563 inflight_appends_len = state.inflight_appends.len(),
564 advised_reconnects = advised_reconnects.count(),
565 "reconnecting append session on server advice"
566 );
567 }
568 Err(err) if err.is_server_draining() && state.is_close_complete() => break,
569 Err(err) if err.is_server_draining() => {
570 advised_reconnects.record();
571 debug!(
572 inflight_appends_len = state.inflight_appends.len(),
573 advised_reconnects = advised_reconnects.count(),
574 "reconnecting append session while server drains"
575 );
576 }
577 Err(err) => {
578 if prev_total_acked_records < state.total_acked_records {
579 prev_total_acked_records = state.total_acked_records;
580 retry_backoff.reset();
581 }
582
583 if is_safe_to_retry(
584 &err,
585 client.config.retry.append_retry_policy,
586 !state.inflight_appends.is_empty(),
587 frame_signal.as_ref(),
588 access_token_mode,
589 ) && let Some(backoff) = retry_backoff.next()
590 {
591 if err.attempt_may_have_side_effects(frame_signal.as_ref()) {
592 for append in &mut state.inflight_appends {
593 append.prior_uncertainty = true;
594 }
595 }
596 debug!(
597 %err,
598 ?backoff,
599 num_retries_remaining = retry_backoff.remaining(),
600 "retrying append session"
601 );
602 tokio::time::sleep(backoff).await;
603 } else {
604 debug!(
605 %err,
606 retries_exhausted = retry_backoff.is_exhausted(),
607 "not retrying append session"
608 );
609
610 let session_err = err.clone().with_prior_uncertainty(
611 state.inflight_appends.iter().any(|a| a.prior_uncertainty),
612 );
613 let _ = terminal_err.set(session_err.clone());
614
615 for inflight_append in state.inflight_appends.drain(..) {
616 let error = err
617 .clone()
618 .with_prior_uncertainty(inflight_append.prior_uncertainty);
619 let _ = inflight_append.ack_tx.send(Err(error));
620 }
621
622 if let Some(stashed) = state.stashed_submission.take() {
623 let _ = stashed.ack_tx.send(Err(err.clone()));
624 }
625
626 if let Some(done_tx) = state.close_tx.take() {
627 let _ = done_tx.send(Err(session_err.clone()));
628 }
629
630 state.cmd_rx.close();
631 while let Some(cmd) = state.cmd_rx.recv().await {
632 let error = match &cmd {
633 Command::Submit { .. } => &err,
634 Command::Close { .. } => &session_err,
635 };
636 cmd.reject(error.clone());
637 }
638 break;
639 }
640 }
641 }
642 }
643
644 if let Some(done_tx) = state.close_tx.take() {
645 let _ = done_tx.send(Ok(()));
646 }
647}
648
649enum SessionOutcome {
651 Closed,
653 ReconnectAdvised,
655}
656
657async fn run_session(
658 client: &BasinClient,
659 stream: &StreamName,
660 headers: &StreamHeaders,
661 state: &mut SessionState,
662 buffer_size: usize,
663 frame_signal: &Option<FrameSignal>,
664 advised_reconnects: AdvisedReconnects,
665) -> Result<SessionOutcome, AppendSessionError> {
666 if let Some(s) = frame_signal {
667 s.reset();
668 }
669
670 let reconnect = ReconnectAdvice::default();
671 let (input_tx, mut acks) = connect(
672 client,
673 stream,
674 headers,
675 buffer_size,
676 frame_signal.clone(),
677 reconnect.clone(),
678 )
679 .await?;
680 let ack_timeout = client.config.request_timeout;
681
682 if !state.inflight_appends.is_empty() {
683 resend(state, &input_tx, &mut acks, ack_timeout).await?;
684
685 if let Some(s) = frame_signal {
686 s.reset();
687 }
688
689 assert!(state.inflight_appends.is_empty());
690 assert_eq!(state.inflight_bytes, 0);
691 }
692
693 if state.is_close_complete() {
694 return Ok(SessionOutcome::Closed);
695 }
696
697 let timer = MuxTimer::<N_TIMER_VARIANTS>::default();
698 tokio::pin!(timer);
699
700 let mut declined_advice = false;
701
702 loop {
703 if reconnect.is_advised() && state.close_tx.is_none() && !declined_advice {
704 if advised_reconnects.should_reconnect() {
705 drain_for_reconnect(input_tx, acks, state, timer.as_mut(), ack_timeout).await?;
706 return Ok(SessionOutcome::ReconnectAdvised);
707 }
708 declined_advice = true;
709 }
710
711 tokio::select! {
712 (event_ord, _deadline) = &mut timer, if timer.is_armed() => {
713 match TimerEvent::from(event_ord) {
714 TimerEvent::AckDeadline => {
715 return Err(AppendSessionError::AckTimeout);
716 }
717 }
718 }
719
720 input_tx_permit = input_tx.reserve(), if state.stashed_submission.is_some() => {
721 let input_tx_permit = input_tx_permit
722 .map_err(|_| AppendSessionError::ServerDisconnected)?;
723 let submission = state.stashed_submission
724 .take()
725 .expect("stashed_submission should not be None");
726
727 let ack_deadline = Instant::now() + ack_timeout;
728 input_tx_permit.send(submission.input.clone());
729
730 state.total_records += submission.input.records.len();
731 state.inflight_bytes += submission.input_metered_bytes;
732
733 timer.as_mut().fire_at(
734 TimerEvent::AckDeadline,
735 ack_deadline,
736 CoalesceMode::Earliest,
737 );
738 state.inflight_appends.push_back(InflightAppend {
739 input: submission.input,
740 input_metered_bytes: submission.input_metered_bytes,
741 ack_tx: submission.ack_tx,
742 ack_deadline,
743 _permit: submission.permit,
744 prior_uncertainty: false,
745 });
746 }
747
748 cmd = state.cmd_rx.recv(), if state.stashed_submission.is_none() => {
749 match cmd {
750 Some(Command::Submit { input, ack_tx, permit }) => {
751 if state.close_tx.is_some() {
752 let _ = ack_tx.send(
753 Err(AppendSessionError::SessionClosing)
754 );
755 } else {
756 let input_metered_bytes = input.records.metered_bytes();
757 state.stashed_submission = Some(StashedSubmission {
758 input,
759 input_metered_bytes,
760 ack_tx,
761 permit,
762 });
763 }
764 }
765 Some(Command::Close { done_tx }) => {
766 state.close_tx = Some(done_tx);
767 }
768 None => {
769 return Err(AppendSessionError::SessionDropped);
770 }
771 }
772 }
773
774 ack = acks.next() => {
775 match ack {
776 Some(Ok(ack)) => {
777 process_ack(
778 ack,
779 state,
780 timer.as_mut(),
781 )?;
782 }
783 Some(Err(err)) => {
784 return Err(err.into());
785 }
786 None => {
787 if !state.inflight_appends.is_empty() || state.stashed_submission.is_some() {
788 return Err(AppendSessionError::StreamClosedEarly);
789 }
790 break;
791 }
792 }
793 }
794 }
795
796 if state.is_close_complete() {
797 break;
798 }
799 }
800
801 assert!(state.inflight_appends.is_empty());
802 assert_eq!(state.inflight_bytes, 0);
803 assert!(state.stashed_submission.is_none());
804
805 Ok(SessionOutcome::Closed)
806}
807
808async fn resend(
809 state: &mut SessionState,
810 input_tx: &mpsc::Sender<AppendInput>,
811 acks: &mut Streaming<AppendAck>,
812 ack_timeout: Duration,
813) -> Result<(), AppendSessionError> {
814 debug!(
815 inflight_appends_len = state.inflight_appends.len(),
816 inflight_bytes = state.inflight_bytes,
817 "resending inflight appends"
818 );
819
820 let mut resend_index = 0;
821 let mut resend_finished = false;
822
823 let timer = MuxTimer::<N_TIMER_VARIANTS>::default();
824 tokio::pin!(timer);
825
826 while !state.inflight_appends.is_empty() {
827 tokio::select! {
828 (event_ord, _deadline) = &mut timer, if timer.is_armed() => {
829 match TimerEvent::from(event_ord) {
830 TimerEvent::AckDeadline => {
831 return Err(AppendSessionError::AckTimeout);
832 }
833 }
834 }
835
836 input_tx_permit = input_tx.reserve(), if !resend_finished => {
837 let input_tx_permit = input_tx_permit
838 .map_err(|_| AppendSessionError::ServerDisconnected)?;
839
840 if let Some(inflight_append) = state.inflight_appends.get_mut(resend_index) {
841 inflight_append.ack_deadline = Instant::now() + ack_timeout;
842 timer.as_mut().fire_at(
843 TimerEvent::AckDeadline,
844 inflight_append.ack_deadline,
845 CoalesceMode::Latest,
846 );
847 input_tx_permit.send(inflight_append.input.clone());
848 resend_index += 1;
849 } else {
850 resend_finished = true;
851 }
852 }
853
854 ack = acks.next() => {
855 match ack {
856 Some(Ok(ack)) => {
857 process_ack(
858 ack,
859 state,
860 timer.as_mut(),
861 )?;
862 resend_index = resend_index.checked_sub(1).ok_or_else(|| {
863 AppendSessionError::InvalidAck(
864 "received ack without a corresponding resent append in flight".to_string(),
865 )
866 })?;
867 }
868 Some(Err(err)) => {
869 return Err(err.into());
870 }
871 None => {
872 return Err(AppendSessionError::StreamClosedEarly);
873 }
874 }
875 }
876 }
877 }
878
879 assert_eq!(
880 resend_index, 0,
881 "resend_index should be 0 after resend completes"
882 );
883 debug!("finished resending inflight appends");
884 Ok(())
885}
886
887async fn drain_for_reconnect(
892 input_tx: mpsc::Sender<AppendInput>,
893 mut acks: Streaming<AppendAck>,
894 state: &mut SessionState,
895 mut timer: Pin<&mut MuxTimer<N_TIMER_VARIANTS>>,
896 ack_timeout: Duration,
897) -> Result<(), AppendSessionError> {
898 drop(input_tx);
899 loop {
900 if !timer.is_armed() {
903 timer.as_mut().fire_at(
904 TimerEvent::AckDeadline,
905 Instant::now() + ack_timeout,
906 CoalesceMode::Earliest,
907 );
908 }
909
910 tokio::select! {
911 (event_ord, _deadline) = &mut timer, if timer.is_armed() => {
912 match TimerEvent::from(event_ord) {
913 TimerEvent::AckDeadline => {
914 return Err(AppendSessionError::AckTimeout);
915 }
916 }
917 }
918
919 ack = acks.next() => {
920 match ack {
921 Some(Ok(ack)) => {
922 process_ack(ack, state, timer.as_mut())?;
923 }
924 Some(Err(err)) if err.is_server_draining() => {
925 return Ok(());
926 }
927 Some(Err(err)) => {
928 return Err(err.into());
929 }
930 None => {
931 if !state.inflight_appends.is_empty() {
932 return Err(AppendSessionError::StreamClosedEarly);
933 }
934 return Ok(());
935 }
936 }
937 }
938 }
939 }
940}
941
942async fn connect(
943 client: &BasinClient,
944 stream: &StreamName,
945 headers: &StreamHeaders,
946 buffer_size: usize,
947 frame_signal: Option<FrameSignal>,
948 reconnect: ReconnectAdvice,
949) -> Result<(mpsc::Sender<AppendInput>, Streaming<AppendAck>), AppendSessionError> {
950 let (input_tx, input_rx) = mpsc::channel::<AppendInput>(buffer_size);
951 let ack_stream = Box::pin(
952 client
953 .append_session(
954 stream,
955 ReceiverStream::new(input_rx).map(|i| i.into()),
956 headers.encryption.as_ref(),
957 headers.stream_config.as_ref(),
958 frame_signal,
959 reconnect,
960 )
961 .await?
962 .map(|ack| match ack {
963 Ok(ack) => Ok(ack.into()),
964 Err(err) => Err(err),
965 }),
966 );
967 Ok((input_tx, ack_stream))
968}
969
970fn process_ack(
971 ack: AppendAck,
972 state: &mut SessionState,
973 timer: Pin<&mut MuxTimer<N_TIMER_VARIANTS>>,
974) -> Result<(), AppendSessionError> {
975 let corresponding_append = state.inflight_appends.pop_front().ok_or_else(|| {
976 AppendSessionError::InvalidAck(
977 "received ack without a corresponding append in flight".to_string(),
978 )
979 })?;
980
981 if ack.end.seq_num < ack.start.seq_num {
982 return Err(AppendSessionError::InvalidAck(
983 "ack end seq_num should be greater than or equal to start seq_num".to_string(),
984 ));
985 }
986
987 if state
988 .prev_ack_end
989 .is_some_and(|end| ack.end.seq_num <= end.seq_num)
990 {
991 return Err(AppendSessionError::InvalidAck(
992 "ack end seq_num should be greater than previous ack end".to_string(),
993 ));
994 }
995
996 let num_acked_records = (ack.end.seq_num - ack.start.seq_num) as usize;
997 let expected_records = corresponding_append.input.records.len();
998 if num_acked_records != expected_records {
999 return Err(AppendSessionError::InvalidAck(format!(
1000 "acked record count {num_acked_records} does not match submitted batch size {expected_records}"
1001 )));
1002 }
1003
1004 state.total_acked_records += num_acked_records;
1005 state.inflight_bytes -= corresponding_append.input_metered_bytes;
1006 state.prev_ack_end = Some(ack.end);
1007
1008 let _ = corresponding_append.ack_tx.send(Ok(ack));
1009
1010 if let Some(oldest_append) = state.inflight_appends.front() {
1011 timer.fire_at(
1012 TimerEvent::AckDeadline,
1013 oldest_append.ack_deadline,
1014 CoalesceMode::Latest,
1015 );
1016 } else {
1017 timer.cancel(TimerEvent::AckDeadline);
1018 assert_eq!(
1019 state.total_records, state.total_acked_records,
1020 "all records should be acked when inflight is empty"
1021 );
1022 }
1023
1024 Ok(())
1025}
1026
1027struct StashedSubmission {
1028 input: AppendInput,
1029 input_metered_bytes: usize,
1030 ack_tx: oneshot::Sender<Result<AppendAck, AppendSessionError>>,
1031 permit: Option<AppendPermit>,
1032}
1033
1034struct InflightAppend {
1035 input: AppendInput,
1036 input_metered_bytes: usize,
1037 ack_tx: oneshot::Sender<Result<AppendAck, AppendSessionError>>,
1038 ack_deadline: Instant,
1039 _permit: Option<AppendPermit>,
1040 prior_uncertainty: bool,
1041}
1042
1043enum Command {
1044 Submit {
1045 input: AppendInput,
1046 ack_tx: oneshot::Sender<Result<AppendAck, AppendSessionError>>,
1047 permit: Option<AppendPermit>,
1048 },
1049 Close {
1050 done_tx: oneshot::Sender<Result<(), AppendSessionError>>,
1051 },
1052}
1053
1054impl Command {
1055 fn reject(self, err: AppendSessionError) {
1056 match self {
1057 Command::Submit { ack_tx, .. } => {
1058 let _ = ack_tx.send(Err(err));
1059 }
1060 Command::Close { done_tx } => {
1061 let _ = done_tx.send(Err(err));
1062 }
1063 }
1064 }
1065}
1066
1067fn is_safe_to_retry(
1068 err: &AppendSessionError,
1069 policy: AppendRetryPolicy,
1070 has_inflight: bool,
1071 frame_signal: Option<&FrameSignal>,
1072 access_token_mode: AccessTokenMode,
1073) -> bool {
1074 let policy_compliant = match policy {
1075 AppendRetryPolicy::All => true,
1076 AppendRetryPolicy::NoSideEffects => {
1077 !has_inflight || !err.attempt_may_have_side_effects(frame_signal)
1078 }
1079 };
1080 policy_compliant
1081 && (err.is_retryable()
1082 || (access_token_mode.is_refreshable() && err.is_authentication_error()))
1083}
1084
1085const DEFAULT_CHANNEL_BUFFER_SIZE: usize = 100;
1086
1087#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1088enum TimerEvent {
1089 AckDeadline,
1090}
1091
1092const N_TIMER_VARIANTS: usize = 1;
1093
1094impl From<TimerEvent> for usize {
1095 fn from(event: TimerEvent) -> Self {
1096 match event {
1097 TimerEvent::AckDeadline => 0,
1098 }
1099 }
1100}
1101
1102impl From<usize> for TimerEvent {
1103 fn from(value: usize) -> Self {
1104 match value {
1105 0 => TimerEvent::AckDeadline,
1106 _ => panic!("invalid ordinal"),
1107 }
1108 }
1109}
1110
1111#[cfg(test)]
1112mod tests {
1113 use std::error::Error;
1114
1115 use http::StatusCode;
1116
1117 use super::{AppendSessionError, is_safe_to_retry};
1118 use crate::{
1119 api::{ApiError, ServerErrorBody},
1120 error::{AppendError, ProducerError, RequestError},
1121 frame_signal::FrameSignal,
1122 retry::AppendRetryError,
1123 types::{AccessTokenMode, AppendRetryPolicy},
1124 };
1125
1126 fn server_error(status: StatusCode, code: &str) -> AppendSessionError {
1127 AppendSessionError::Append(AppendError::Request(RequestError::from(ApiError::Server(
1128 status,
1129 ServerErrorBody {
1130 code: code.to_owned(),
1131 message: "test".to_owned(),
1132 },
1133 ))))
1134 }
1135
1136 #[rstest::rstest]
1137 #[case(StatusCode::FORBIDDEN, "permission_denied", false, false)]
1138 #[case(StatusCode::FORBIDDEN, "permission_denied", true, true)]
1139 #[case(StatusCode::SERVICE_UNAVAILABLE, "unavailable", false, false)]
1140 #[case(StatusCode::SERVICE_UNAVAILABLE, "unavailable", true, false)]
1141 #[case(StatusCode::TOO_MANY_REQUESTS, "rate_limited", true, true)]
1142 #[test]
1143 fn session_failure_preserves_uncertainty_and_latest_error(
1144 #[case] status: StatusCode,
1145 #[case] code: &str,
1146 #[case] prior_uncertainty: bool,
1147 #[case] wrapped: bool,
1148 ) {
1149 let latest = server_error(status, code);
1150 let error = latest.clone().with_prior_uncertainty(prior_uncertainty);
1151 assert_eq!(
1152 matches!(error, AppendSessionError::IndefiniteFailure { .. }),
1153 wrapped
1154 );
1155 assert_eq!(error.is_retryable(), latest.is_retryable());
1156 assert_eq!(
1157 error.has_no_side_effects(),
1158 !wrapped && latest.has_no_side_effects()
1159 );
1160 assert_eq!(
1161 error.request_error().unwrap().server_error().unwrap().code,
1162 code
1163 );
1164 if wrapped {
1165 let source = error
1166 .source()
1167 .unwrap()
1168 .downcast_ref::<Box<AppendSessionError>>()
1169 .unwrap();
1170 assert_eq!(source.to_string(), latest.to_string());
1171 assert!(source.has_no_side_effects());
1172 }
1173
1174 let producer_error = ProducerError::from(error.clone());
1175 assert_eq!(producer_error.is_retryable(), error.is_retryable());
1176 assert_eq!(
1177 producer_error.has_no_side_effects(),
1178 error.has_no_side_effects()
1179 );
1180 assert_eq!(
1181 producer_error
1182 .request_error()
1183 .unwrap()
1184 .server_error()
1185 .unwrap()
1186 .code,
1187 code
1188 );
1189 }
1190
1191 #[test]
1192 fn safe_to_retry_session_all_policy() {
1193 let retryable = server_error(StatusCode::INTERNAL_SERVER_ERROR, "internal");
1194 let non_retryable = server_error(StatusCode::BAD_REQUEST, "bad_request");
1195 let policy = AppendRetryPolicy::All;
1196 let static_mode = AccessTokenMode::Static;
1197
1198 assert!(is_safe_to_retry(
1200 &retryable,
1201 policy,
1202 true,
1203 None,
1204 static_mode
1205 ));
1206 assert!(!is_safe_to_retry(
1207 &non_retryable,
1208 policy,
1209 true,
1210 None,
1211 static_mode,
1212 ));
1213
1214 let unauthorized = server_error(StatusCode::UNAUTHORIZED, "authn");
1215 #[cfg(feature = "_hidden")]
1216 assert!(is_safe_to_retry(
1217 &unauthorized,
1218 policy,
1219 true,
1220 None,
1221 AccessTokenMode::Refreshable,
1222 ));
1223 assert!(!is_safe_to_retry(
1224 &unauthorized,
1225 policy,
1226 true,
1227 None,
1228 static_mode,
1229 ));
1230
1231 #[cfg(feature = "_hidden")]
1232 let unrelated_unauthorized = server_error(StatusCode::UNAUTHORIZED, "other");
1233 #[cfg(feature = "_hidden")]
1234 assert!(!is_safe_to_retry(
1235 &unrelated_unauthorized,
1236 policy,
1237 true,
1238 None,
1239 AccessTokenMode::Refreshable,
1240 ));
1241 }
1242
1243 #[test]
1244 fn safe_to_retry_session_no_side_effects_policy() {
1245 let retryable = server_error(StatusCode::INTERNAL_SERVER_ERROR, "internal");
1246 let no_side_effect = server_error(StatusCode::TOO_MANY_REQUESTS, "rate_limited");
1247 let policy = AppendRetryPolicy::NoSideEffects;
1248 let signal = FrameSignal::new();
1249 let mode = AccessTokenMode::Static;
1250
1251 signal.signal();
1253 assert!(is_safe_to_retry(
1254 &retryable,
1255 policy,
1256 false,
1257 Some(&signal),
1258 mode,
1259 ));
1260
1261 signal.reset();
1263 assert!(is_safe_to_retry(
1264 &retryable,
1265 policy,
1266 true,
1267 Some(&signal),
1268 mode,
1269 ));
1270
1271 signal.signal();
1273 assert!(!is_safe_to_retry(
1274 &retryable,
1275 policy,
1276 true,
1277 Some(&signal),
1278 mode,
1279 ));
1280
1281 assert!(is_safe_to_retry(
1283 &no_side_effect,
1284 policy,
1285 true,
1286 Some(&signal),
1287 mode,
1288 ));
1289
1290 assert!(!is_safe_to_retry(
1292 &AppendSessionError::AckTimeout,
1293 policy,
1294 true,
1295 Some(&signal),
1296 mode,
1297 ));
1298 }
1299}