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