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