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