1use std::{
4 future::Future,
5 pin::Pin,
6 sync::{
7 Arc,
8 atomic::{AtomicUsize, Ordering},
9 },
10};
11
12use async_trait::async_trait;
13use bytes::Bytes;
14use futures::{StreamExt, stream};
15use thiserror::Error;
16use tonic::{
17 Code, Request, Response, Status,
18 metadata::{Ascii, MetadataValue},
19 transport::{Certificate, Channel, ClientTlsConfig, Endpoint},
20};
21
22use crate::wire_codec::{
23 condition_from_wire, condition_wire, mutation_from_wire, mutation_wire, optional_key, path,
24 required_key,
25};
26use crate::{
27 AppendOutcome, AppendReceipt, AppendRequest, Child, ChildStream, ChildrenRequest,
28 CommitConflict, CommitId, CommitOutcome, CommitRequest, CommittedAppend, CommittedDelete,
29 CommittedEnvelope, CommittedFork, CommittedMutation, CommittedTrim, DeleteReceipt, ForkReceipt,
30 ForkRequest, IdempotencyKey, IdempotencyObservation, IdempotencyOutcome, ReadRequest, Record,
31 RecordStream, StreamError, StreamPath, StreamProvider, TrimReceipt, wire,
32};
33
34const OPERATION_DEADLINE: std::time::Duration = std::time::Duration::from_secs(10);
35const OPERATION_ENDPOINT_ATTEMPT_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(1);
36const FOLLOW_ENDPOINT_ATTEMPT_TIMEOUT: std::time::Duration = std::time::Duration::from_millis(500);
37const RETRY_DELAY: std::time::Duration = std::time::Duration::from_millis(10);
38pub const MAX_ENDPOINTS: usize = 16;
40pub const MAX_ENDPOINT_URI_BYTES: usize = 2_048;
42pub const MAX_CA_CERTIFICATE_BYTES: usize = 64 * 1024;
44
45#[derive(Debug, Error)]
47pub enum ConnectError {
48 #[error("invalid or unavailable Stream endpoint: {0}")]
50 Endpoint(#[from] tonic::transport::Error),
51 #[error("Stream endpoints must use https")]
53 InsecureEndpoint,
54 #[error("invalid Stream bearer credential")]
56 InvalidCredential,
57 #[error("invalid Stream private CA certificate")]
59 InvalidCaCertificate,
60 #[error("at least one Stream endpoint is required")]
62 NoEndpoints,
63 #[error("Stream endpoint pool exceeds its fixed bound")]
65 EndpointLimit,
66}
67
68#[derive(Clone)]
70pub struct Client {
71 channels: Arc<[Channel]>,
72 authorization: MetadataValue<Ascii>,
73 preferred: Arc<AtomicUsize>,
74}
75
76pub struct Service<P> {
78 provider: Arc<P>,
79}
80
81impl<P> Service<P> {
82 #[must_use]
84 pub fn new(provider: Arc<P>) -> Self {
85 Self { provider }
86 }
87}
88
89impl<P> Clone for Service<P> {
90 fn clone(&self) -> Self {
91 Self {
92 provider: Arc::clone(&self.provider),
93 }
94 }
95}
96
97impl Client {
98 pub async fn connect(
100 endpoint: impl AsRef<str>,
101 bearer_token: impl AsRef<str>,
102 ) -> Result<Self, ConnectError> {
103 Self::connect_endpoints([endpoint], bearer_token).await
104 }
105
106 pub async fn connect_endpoints<I, S>(
108 endpoints: I,
109 bearer_token: impl AsRef<str>,
110 ) -> Result<Self, ConnectError>
111 where
112 I: IntoIterator<Item = S>,
113 S: AsRef<str>,
114 {
115 Self::connect_with_tls(endpoints, bearer_token, None)
116 }
117
118 pub async fn connect_with_ca_certificate(
120 endpoint: impl AsRef<str>,
121 bearer_token: impl AsRef<str>,
122 certificate_pem: impl AsRef<[u8]>,
123 ) -> Result<Self, ConnectError> {
124 Self::connect_endpoints_with_ca_certificate(
125 [endpoint],
126 bearer_token,
127 certificate_pem.as_ref(),
128 )
129 .await
130 }
131
132 pub async fn connect_endpoints_with_ca_certificate<I, S>(
134 endpoints: I,
135 bearer_token: impl AsRef<str>,
136 certificate_pem: impl AsRef<[u8]>,
137 ) -> Result<Self, ConnectError>
138 where
139 I: IntoIterator<Item = S>,
140 S: AsRef<str>,
141 {
142 Self::connect_with_tls(endpoints, bearer_token, Some(certificate_pem.as_ref()))
143 }
144
145 fn connect_with_tls<I, S>(
146 endpoints: I,
147 bearer_token: impl AsRef<str>,
148 certificate_pem: Option<&[u8]>,
149 ) -> Result<Self, ConnectError>
150 where
151 I: IntoIterator<Item = S>,
152 S: AsRef<str>,
153 {
154 let certificate_pem = certificate_pem.map(bounded_ca_certificate).transpose()?;
155 let channels = Self::channels_with_tls(
156 endpoints,
157 certificate_pem,
158 OPERATION_ENDPOINT_ATTEMPT_TIMEOUT,
159 )?;
160 Self::from_channels(channels, bearer_token)
161 }
162
163 fn channels_with_tls<I, S>(
164 endpoints: I,
165 certificate_pem: Option<&[u8]>,
166 connect_timeout: std::time::Duration,
167 ) -> Result<Arc<[Channel]>, ConnectError>
168 where
169 I: IntoIterator<Item = S>,
170 S: AsRef<str>,
171 {
172 let mut channels = Vec::new();
173 for endpoint in endpoints.into_iter().take(MAX_ENDPOINTS + 1) {
174 if channels.len() == MAX_ENDPOINTS {
175 return Err(ConnectError::EndpointLimit);
176 }
177 let endpoint = endpoint.as_ref();
178 if endpoint.len() > MAX_ENDPOINT_URI_BYTES {
179 return Err(ConnectError::EndpointLimit);
180 }
181 let mut endpoint = Endpoint::new(endpoint.to_owned())?.connect_timeout(connect_timeout);
184 if endpoint.uri().scheme_str() != Some("https") {
185 return Err(ConnectError::InsecureEndpoint);
186 }
187 if let Some(certificate_pem) = certificate_pem {
188 endpoint = endpoint.tls_config(
189 ClientTlsConfig::new()
190 .with_enabled_roots()
191 .ca_certificate(Certificate::from_pem(certificate_pem)),
192 )?;
193 }
194 channels.push(endpoint.connect_lazy());
195 }
196 if channels.is_empty() {
197 return Err(ConnectError::NoEndpoints);
198 }
199 Ok(channels.into())
200 }
201
202 fn from_channels(
203 channels: Arc<[Channel]>,
204 bearer_token: impl AsRef<str>,
205 ) -> Result<Self, ConnectError> {
206 let authorization = format!("Bearer {}", bearer_token.as_ref())
207 .parse::<MetadataValue<Ascii>>()
208 .map_err(|_| ConnectError::InvalidCredential)?;
209 Ok(Self {
210 channels,
211 authorization,
212 preferred: Arc::new(AtomicUsize::new(0)),
213 })
214 }
215
216 #[allow(
217 clippy::indexing_slicing,
218 reason = "callers derive index via `(start + offset) % channels.len()` against this same slice, so it is always in-bounds"
219 )]
220 fn service(
221 channels: &[Channel],
222 index: usize,
223 ) -> wire::stream_service_client::StreamServiceClient<Channel> {
224 wire::stream_service_client::StreamServiceClient::new(channels[index].clone())
225 }
226
227 fn request<T>(&self, body: T) -> Request<T> {
228 let mut request = Request::new(body);
229 request
230 .metadata_mut()
231 .insert("authorization", self.authorization.clone());
232 request
233 }
234
235 async fn unary<T, U, F>(&self, body: T, mut call: F) -> Result<U, StreamError>
236 where
237 T: Clone,
238 F: FnMut(
239 wire::stream_service_client::StreamServiceClient<Channel>,
240 Request<T>,
241 ) -> Pin<Box<dyn Future<Output = Result<Response<U>, Status>> + Send>>,
242 {
243 self.unary_on(
244 &self.channels,
245 &self.preferred,
246 body,
247 OPERATION_ENDPOINT_ATTEMPT_TIMEOUT,
248 &mut call,
249 )
250 .await
251 .map(|(response, _)| response)
252 }
253
254 async fn follow_unary<T, U, F>(&self, body: T, mut call: F) -> Result<(U, usize), StreamError>
255 where
256 T: Clone,
257 F: FnMut(
258 wire::stream_service_client::StreamServiceClient<Channel>,
259 Request<T>,
260 ) -> Pin<Box<dyn Future<Output = Result<Response<U>, Status>> + Send>>,
261 {
262 self.unary_on(
263 &self.channels,
264 &self.preferred,
265 body,
266 FOLLOW_ENDPOINT_ATTEMPT_TIMEOUT,
267 &mut call,
268 )
269 .await
270 }
271
272 async fn unary_on<T, U, F>(
273 &self,
274 channels: &[Channel],
275 preferred: &AtomicUsize,
276 body: T,
277 attempt_timeout: std::time::Duration,
278 call: &mut F,
279 ) -> Result<(U, usize), StreamError>
280 where
281 T: Clone,
282 F: FnMut(
283 wire::stream_service_client::StreamServiceClient<Channel>,
284 Request<T>,
285 ) -> Pin<Box<dyn Future<Output = Result<Response<U>, Status>> + Send>>,
286 {
287 let deadline = tokio::time::Instant::now() + OPERATION_DEADLINE;
288 let mut last = None;
289 loop {
290 let start = preferred.load(Ordering::Relaxed) % channels.len();
291 for offset in 0..channels.len() {
292 let index = (start + offset) % channels.len();
293 let attempt_deadline = deadline.min(tokio::time::Instant::now() + attempt_timeout);
294 match tokio::time::timeout_at(
295 attempt_deadline,
296 call(Self::service(channels, index), self.request(body.clone())),
297 )
298 .await
299 {
300 Ok(Ok(response)) => {
301 preferred.store(index, Ordering::Relaxed);
302 return Ok((response.into_inner(), index));
303 }
304 Ok(Err(error)) if retryable(&error) => last = Some(error),
305 Ok(Err(error)) => return Err(status(&error)),
306 Err(_) => last = Some(Status::deadline_exceeded("endpoint attempt expired")),
307 }
308 }
309 tokio::time::sleep_until((tokio::time::Instant::now() + RETRY_DELAY).min(deadline))
310 .await;
311 if tokio::time::Instant::now() >= deadline {
312 return Err(last.as_ref().map_or(StreamError::Unavailable, status));
313 }
314 }
315 }
316
317 async fn records(
318 &self,
319 path: StreamPath,
320 from: u64,
321 limit: Option<u32>,
322 ) -> Result<RecordStream, StreamError> {
323 let client = self.clone();
324 Ok(stream::unfold(
325 RecordCursor {
326 client,
327 path,
328 next: from,
329 remaining: limit,
330 active: None,
331 },
332 |mut cursor| async move {
333 loop {
334 if cursor.remaining == Some(0) {
335 return None;
336 }
337 if cursor.active.is_none() {
338 match cursor.open().await {
339 Ok(active) => cursor.active = Some(active),
340 Err(error) => return Some((Err(error), cursor)),
341 }
342 }
343 let Some(active) = cursor.active.as_mut() else {
344 return Some((Err(StreamError::Unavailable), cursor));
345 };
346 let active_endpoint = active.endpoint;
347 match active.records.next().await {
348 Some(Ok(response)) => match read_response(response) {
349 Ok(record) if record.sequence == cursor.next => {
350 cursor.next = cursor.next.saturating_add(1);
351 if let Some(remaining) = &mut cursor.remaining {
352 *remaining = remaining.saturating_sub(1);
353 }
354 return Some((Ok(record), cursor));
355 }
356 Ok(record) if record.sequence < cursor.next => continue,
357 Ok(_) => return Some((Err(StreamError::Unavailable), cursor)),
358 Err(error) => return Some((Err(error), cursor)),
359 },
360 Some(Err(error)) if retryable(&error) => {
361 cursor.advance_follow(active_endpoint);
362 tokio::time::sleep(RETRY_DELAY).await;
363 cursor.active = None;
364 }
365 Some(Err(error)) => return Some((Err(status(&error)), cursor)),
366 None if cursor.remaining.is_none() => {
367 cursor.advance_follow(active_endpoint);
368 tokio::time::sleep(RETRY_DELAY).await;
369 cursor.active = None;
370 }
371 None => return None,
372 }
373 }
374 },
375 )
376 .boxed())
377 }
378}
379
380fn bounded_ca_certificate(value: &[u8]) -> Result<&[u8], ConnectError> {
381 if value.is_empty() || value.len() > MAX_CA_CERTIFICATE_BYTES {
382 Err(ConnectError::InvalidCaCertificate)
383 } else {
384 Ok(value)
385 }
386}
387
388struct RecordCursor {
389 client: Client,
390 path: StreamPath,
391 next: u64,
392 remaining: Option<u32>,
393 active: Option<ActiveRecords>,
394}
395
396struct ActiveRecords {
397 records: tonic::Streaming<wire::ReadResponse>,
398 endpoint: usize,
399}
400
401impl RecordCursor {
402 fn advance_follow(&self, observed: usize) {
403 if self.remaining.is_some() {
404 return;
405 }
406 let next = (observed + 1) % self.client.channels.len();
407 let _ = self.client.preferred.compare_exchange(
408 observed,
409 next,
410 Ordering::Relaxed,
411 Ordering::Relaxed,
412 );
413 }
414
415 async fn open(&self) -> Result<ActiveRecords, StreamError> {
416 if let Some(limit) = self.remaining {
417 self.client
418 .unary(
419 wire::ReadRequest {
420 path: self.path.to_string(),
421 from: self.next,
422 limit,
423 },
424 |mut service, request| Box::pin(async move { service.read(request).await }),
425 )
426 .await
427 .map(|records| ActiveRecords {
428 records,
429 endpoint: 0,
430 })
431 } else {
432 self.client
433 .follow_unary(
434 wire::FollowRequest {
435 path: self.path.to_string(),
436 from: self.next,
437 },
438 |mut service, request| Box::pin(async move { service.follow(request).await }),
439 )
440 .await
441 .map(|(records, endpoint)| ActiveRecords { records, endpoint })
442 }
443 }
444}
445
446fn retryable(error: &Status) -> bool {
447 if matches!(error.code(), Code::Unavailable | Code::DeadlineExceeded) {
448 return true;
449 }
450 if error.code() == Code::Unknown && std::error::Error::source(error).is_none() {
455 return true;
456 }
457 if !matches!(error.code(), Code::Unknown | Code::Cancelled) {
458 return false;
459 }
460 let mut cause = std::error::Error::source(error);
461 while let Some(current) = cause {
462 if error.code() == Code::Cancelled
465 && current
466 .downcast_ref::<hyper::Error>()
467 .is_some_and(hyper::Error::is_canceled)
468 {
469 return true;
470 }
471 if error.code() == Code::Cancelled
472 && current.downcast_ref::<h2::Error>().is_some_and(|error| {
473 error.is_remote() && error.reason() == Some(h2::Reason::CANCEL)
474 })
475 {
476 return true;
477 }
478 let io = current.downcast_ref::<std::io::Error>().or_else(|| {
479 current
480 .downcast_ref::<h2::Error>()
481 .and_then(h2::Error::get_io)
482 });
483 if error.code() == Code::Unknown
484 && io.is_some_and(|error| {
485 matches!(
486 error.kind(),
487 std::io::ErrorKind::UnexpectedEof
488 | std::io::ErrorKind::ConnectionReset
489 | std::io::ErrorKind::ConnectionAborted
490 | std::io::ErrorKind::BrokenPipe
491 | std::io::ErrorKind::NotConnected
492 | std::io::ErrorKind::TimedOut
493 )
494 })
495 {
496 return true;
497 }
498 cause = current.source();
499 }
500 false
501}
502
503#[async_trait]
504impl StreamProvider for Client {
505 async fn inspect_idempotency(
506 &self,
507 idempotency_key: IdempotencyKey,
508 ) -> Result<Option<IdempotencyObservation>, StreamError> {
509 let response = self
510 .unary(
511 wire::InspectIdempotencyRequest {
512 idempotency_key: Bytes::copy_from_slice(idempotency_key.as_bytes()),
513 },
514 |mut service, request| {
515 Box::pin(async move { service.inspect_idempotency(request).await })
516 },
517 )
518 .await?;
519 bind_observation(&idempotency_key, response.observation)
520 }
521
522 async fn tail(&self, path: StreamPath) -> Result<u64, StreamError> {
523 self.unary(
524 wire::TailRequest {
525 path: path.to_string(),
526 },
527 |mut service, request| Box::pin(async move { service.tail(request).await }),
528 )
529 .await
530 .map(|response| response.tail)
531 }
532
533 async fn append(&self, request: AppendRequest) -> Result<AppendOutcome, StreamError> {
534 let idempotency_key = request.idempotency_key.map_or_else(
535 || Bytes::copy_from_slice(uuid::Uuid::new_v4().as_bytes()),
536 |key| Bytes::copy_from_slice(key.as_bytes()),
537 );
538 let response = self
539 .unary(
540 wire::AppendRequest {
541 path: request.path.to_string(),
542 records: request.records,
543 if_tail: request.if_tail,
544 idempotency_key: Some(idempotency_key),
545 },
546 |mut service, request| Box::pin(async move { service.append(request).await }),
547 )
548 .await?;
549 append_outcome_from_wire(response)
550 }
551
552 async fn fork(&self, request: ForkRequest) -> Result<ForkReceipt, StreamError> {
553 let idempotency_key = request.idempotency_key.map_or_else(
554 || Bytes::copy_from_slice(uuid::Uuid::new_v4().as_bytes()),
555 |key| Bytes::copy_from_slice(key.as_bytes()),
556 );
557 let receipt = self
558 .unary(
559 wire::ForkRequest {
560 source: request.source.to_string(),
561 destination: request.destination.to_string(),
562 at_tail: request.at_tail,
563 idempotency_key: Some(idempotency_key),
564 },
565 |mut service, request| Box::pin(async move { service.fork(request).await }),
566 )
567 .await?;
568 Ok(ForkReceipt {
569 source: path(receipt.source)?,
570 destination: path(receipt.destination)?,
571 forked_at: receipt.forked_at,
572 tail: receipt.tail,
573 commit_id: commit_id(&receipt.commit_id)?,
574 })
575 }
576
577 async fn trim(
578 &self,
579 path: StreamPath,
580 before: u64,
581 idempotency_key: IdempotencyKey,
582 ) -> Result<TrimReceipt, StreamError> {
583 let receipt = self
584 .unary(
585 wire::TrimRequest {
586 path: path.to_string(),
587 before,
588 idempotency_key: Some(Bytes::copy_from_slice(idempotency_key.as_bytes())),
589 },
590 |mut service, request| Box::pin(async move { service.trim(request).await }),
591 )
592 .await?;
593 Ok(TrimReceipt {
594 path: crate::grpc::path(receipt.path)?,
595 trim_point: receipt.trim_point,
596 commit_id: commit_id(&receipt.commit_id)?,
597 })
598 }
599
600 async fn delete(
601 &self,
602 path: StreamPath,
603 idempotency_key: IdempotencyKey,
604 ) -> Result<DeleteReceipt, StreamError> {
605 let receipt = self
606 .unary(
607 wire::DeleteRequest {
608 path: path.to_string(),
609 idempotency_key: Some(Bytes::copy_from_slice(idempotency_key.as_bytes())),
610 },
611 |mut service, request| Box::pin(async move { service.delete(request).await }),
612 )
613 .await?;
614 Ok(DeleteReceipt {
615 path: crate::grpc::path(receipt.path)?,
616 commit_id: commit_id(&receipt.commit_id)?,
617 })
618 }
619
620 async fn read(&self, request: ReadRequest) -> Result<RecordStream, StreamError> {
621 self.records(request.path, request.from, Some(request.limit))
622 .await
623 }
624
625 async fn follow(&self, path: StreamPath, from: u64) -> Result<RecordStream, StreamError> {
626 self.records(path, from, None).await
627 }
628
629 async fn children(&self, request: ChildrenRequest) -> Result<ChildStream, StreamError> {
630 let body = wire::ChildrenRequest {
631 parent: request.parent.map(|path| path.to_string()),
632 limit: request.limit,
633 };
634 let mut last = None;
635 for _ in 0..self.channels.len() {
636 let response = self
637 .unary(body.clone(), |mut service, request| {
638 Box::pin(async move { service.children(request).await })
639 })
640 .await?;
641 let collected = response
642 .map(|item| {
643 let child = item
644 .map_err(|error| status(&error))?
645 .child
646 .ok_or(StreamError::Unavailable)?;
647 Ok(Child {
648 path: path(child.path)?,
649 })
650 })
651 .collect::<Vec<_>>()
652 .await;
653 if collected.iter().all(Result::is_ok) {
654 return Ok(stream::iter(collected).boxed());
655 }
656 let error = collected
657 .into_iter()
658 .find_map(Result::err)
659 .unwrap_or(StreamError::Unavailable);
660 if error != StreamError::Unavailable {
661 return Err(error);
662 }
663 last = Some(error);
664 }
665 Err(last.unwrap_or(StreamError::Unavailable))
666 }
667
668 async fn commit(&self, request: CommitRequest) -> Result<CommitOutcome, StreamError> {
669 let response = self
670 .unary(
671 wire::CommitRequest {
672 conditions: request.conditions.into_iter().map(condition_wire).collect(),
673 mutations: request.mutations.into_iter().map(mutation_wire).collect(),
674 idempotency_key: Bytes::copy_from_slice(request.idempotency_key.as_bytes()),
675 deadline_unix_millis: None,
676 },
677 |mut service, request| Box::pin(async move { service.commit(request).await }),
678 )
679 .await?;
680 commit_outcome_from_wire(response)
681 }
682
683 async fn commit_before(
684 &self,
685 request: CommitRequest,
686 deadline_unix_millis: u64,
687 ) -> Result<CommitOutcome, StreamError> {
688 let response = self
689 .unary(
690 wire::CommitRequest {
691 conditions: request.conditions.into_iter().map(condition_wire).collect(),
692 mutations: request.mutations.into_iter().map(mutation_wire).collect(),
693 idempotency_key: Bytes::copy_from_slice(request.idempotency_key.as_bytes()),
694 deadline_unix_millis: Some(deadline_unix_millis),
695 },
696 |mut service, request| Box::pin(async move { service.commit(request).await }),
697 )
698 .await?;
699 commit_outcome_from_wire(response)
700 }
701
702 async fn read_commit(&self, commit_id: CommitId) -> Result<CommittedEnvelope, StreamError> {
703 let envelope = self
704 .unary(
705 wire::ReadCommitRequest {
706 commit_id: Bytes::copy_from_slice(commit_id.as_bytes()),
707 },
708 |mut service, request| Box::pin(async move { service.read_commit(request).await }),
709 )
710 .await?;
711 envelope_from_wire(envelope)
712 }
713}
714
715#[async_trait]
716impl<P: StreamProvider> wire::stream_service_server::StreamService for Service<P> {
717 type ReadStream = futures::stream::BoxStream<'static, Result<wire::ReadResponse, Status>>;
718 type FollowStream = futures::stream::BoxStream<'static, Result<wire::ReadResponse, Status>>;
719 type ChildrenStream =
720 futures::stream::BoxStream<'static, Result<wire::ChildrenResponse, Status>>;
721
722 async fn inspect_idempotency(
723 &self,
724 request: Request<wire::InspectIdempotencyRequest>,
725 ) -> Result<Response<wire::InspectIdempotencyResponse>, Status> {
726 let key = IdempotencyKey::new(request.into_inner().idempotency_key)
727 .map_err(|error| error_status(&error))?;
728 let observation = self
729 .provider
730 .inspect_idempotency(key)
731 .await
732 .map_err(|error| error_status(&error))?
733 .map(observation_wire);
734 Ok(Response::new(wire::InspectIdempotencyResponse {
735 observation,
736 }))
737 }
738
739 async fn append(
740 &self,
741 request: Request<wire::AppendRequest>,
742 ) -> Result<Response<wire::AppendResponse>, Status> {
743 let request = request.into_inner();
744 let outcome = self
745 .provider
746 .append(AppendRequest {
747 path: path(request.path).map_err(|error| error_status(&error))?,
748 records: request.records,
749 if_tail: request.if_tail,
750 idempotency_key: optional_key(request.idempotency_key)
751 .map_err(|error| error_status(&error))?,
752 })
753 .await
754 .map_err(|error| error_status(&error))?;
755 Ok(Response::new(append_outcome_wire(outcome)))
756 }
757
758 async fn tail(
759 &self,
760 request: Request<wire::TailRequest>,
761 ) -> Result<Response<wire::TailResponse>, Status> {
762 let path = path(request.into_inner().path).map_err(|error| error_status(&error))?;
763 let tail = self
764 .provider
765 .tail(path)
766 .await
767 .map_err(|error| error_status(&error))?;
768 Ok(Response::new(wire::TailResponse { tail }))
769 }
770
771 async fn fork(
772 &self,
773 request: Request<wire::ForkRequest>,
774 ) -> Result<Response<wire::ForkReceipt>, Status> {
775 let request = request.into_inner();
776 let receipt = self
777 .provider
778 .fork(ForkRequest {
779 source: path(request.source).map_err(|error| error_status(&error))?,
780 destination: path(request.destination).map_err(|error| error_status(&error))?,
781 at_tail: request.at_tail,
782 idempotency_key: optional_key(request.idempotency_key)
783 .map_err(|error| error_status(&error))?,
784 })
785 .await
786 .map_err(|error| error_status(&error))?;
787 Ok(Response::new(fork_receipt_wire(&receipt)))
788 }
789
790 async fn trim(
791 &self,
792 request: Request<wire::TrimRequest>,
793 ) -> Result<Response<wire::TrimReceipt>, Status> {
794 let request = request.into_inner();
795 let receipt = self
796 .provider
797 .trim(
798 path(request.path).map_err(|error| error_status(&error))?,
799 request.before,
800 required_key(request.idempotency_key).map_err(|error| error_status(&error))?,
801 )
802 .await
803 .map_err(|error| error_status(&error))?;
804 Ok(Response::new(trim_receipt_wire(&receipt)))
805 }
806
807 async fn delete(
808 &self,
809 request: Request<wire::DeleteRequest>,
810 ) -> Result<Response<wire::DeleteReceipt>, Status> {
811 let request = request.into_inner();
812 let receipt = self
813 .provider
814 .delete(
815 path(request.path).map_err(|error| error_status(&error))?,
816 required_key(request.idempotency_key).map_err(|error| error_status(&error))?,
817 )
818 .await
819 .map_err(|error| error_status(&error))?;
820 Ok(Response::new(delete_receipt_wire(&receipt)))
821 }
822
823 async fn read(
824 &self,
825 request: Request<wire::ReadRequest>,
826 ) -> Result<Response<Self::ReadStream>, Status> {
827 let request = request.into_inner();
828 let records = self
829 .provider
830 .read(ReadRequest {
831 path: path(request.path).map_err(|error| error_status(&error))?,
832 from: request.from,
833 limit: request.limit,
834 })
835 .await
836 .map_err(|error| error_status(&error))?;
837 Ok(Response::new(
838 records
839 .map(|record| {
840 record
841 .map(record_wire)
842 .map(|record| wire::ReadResponse {
843 record: Some(record),
844 })
845 .map_err(|error| error_status(&error))
846 })
847 .boxed(),
848 ))
849 }
850
851 async fn follow(
852 &self,
853 request: Request<wire::FollowRequest>,
854 ) -> Result<Response<Self::FollowStream>, Status> {
855 let request = request.into_inner();
856 let records = self
857 .provider
858 .follow(
859 path(request.path).map_err(|error| error_status(&error))?,
860 request.from,
861 )
862 .await
863 .map_err(|error| error_status(&error))?;
864 Ok(Response::new(
865 records
866 .map(|record| {
867 record
868 .map(record_wire)
869 .map(|record| wire::ReadResponse {
870 record: Some(record),
871 })
872 .map_err(|error| error_status(&error))
873 })
874 .boxed(),
875 ))
876 }
877
878 async fn children(
879 &self,
880 request: Request<wire::ChildrenRequest>,
881 ) -> Result<Response<Self::ChildrenStream>, Status> {
882 let request = request.into_inner();
883 let children = self
884 .provider
885 .children(ChildrenRequest {
886 parent: request
887 .parent
888 .map(path)
889 .transpose()
890 .map_err(|error| error_status(&error))?,
891 limit: request.limit,
892 })
893 .await
894 .map_err(|error| error_status(&error))?;
895 Ok(Response::new(
896 children
897 .map(|child| {
898 child
899 .map(|child| wire::ChildrenResponse {
900 child: Some(wire::Child {
901 path: child.path.to_string(),
902 }),
903 })
904 .map_err(|error| error_status(&error))
905 })
906 .boxed(),
907 ))
908 }
909
910 async fn commit(
911 &self,
912 request: Request<wire::CommitRequest>,
913 ) -> Result<Response<wire::CommitResponse>, Status> {
914 let request = request.into_inner();
915 let deadline_unix_millis = request.deadline_unix_millis;
916 let request = CommitRequest {
917 conditions: request
918 .conditions
919 .into_iter()
920 .map(condition_from_wire)
921 .collect::<Result<_, _>>()
922 .map_err(|error| error_status(&error))?,
923 mutations: request
924 .mutations
925 .into_iter()
926 .map(mutation_from_wire)
927 .collect::<Result<_, _>>()
928 .map_err(|error| error_status(&error))?,
929 idempotency_key: IdempotencyKey::new(request.idempotency_key)
930 .map_err(|error| error_status(&error))?,
931 };
932 let outcome = if let Some(deadline) = deadline_unix_millis {
933 self.provider.commit_before(request, deadline).await
934 } else {
935 self.provider.commit(request).await
936 }
937 .map_err(|error| error_status(&error))?;
938 Ok(Response::new(commit_outcome_wire(outcome)))
939 }
940
941 async fn read_commit(
942 &self,
943 request: Request<wire::ReadCommitRequest>,
944 ) -> Result<Response<wire::CommittedEnvelope>, Status> {
945 let commit_id =
946 commit_id(&request.into_inner().commit_id).map_err(|error| error_status(&error))?;
947 let envelope = self
948 .provider
949 .read_commit(commit_id)
950 .await
951 .map_err(|error| error_status(&error))?;
952 Ok(Response::new(envelope_wire(envelope)))
953 }
954}
955
956fn observation_from_wire(
957 value: wire::IdempotencyObservation,
958) -> Result<IdempotencyObservation, StreamError> {
959 let request_digest = <[u8; 32]>::try_from(value.request_digest.as_ref())
960 .map_err(|_| StreamError::Unavailable)?;
961 let outcome = match value.outcome.ok_or(StreamError::Unavailable)? {
962 wire::idempotency_observation::Outcome::Append(value) => {
963 IdempotencyOutcome::Append(append_outcome_from_wire(value)?)
964 }
965 wire::idempotency_observation::Outcome::Fork(value) => {
966 IdempotencyOutcome::Fork(ForkReceipt {
967 source: path(value.source)?,
968 destination: path(value.destination)?,
969 forked_at: value.forked_at,
970 tail: value.tail,
971 commit_id: commit_id(&value.commit_id)?,
972 })
973 }
974 wire::idempotency_observation::Outcome::Trim(value) => {
975 IdempotencyOutcome::Trim(TrimReceipt {
976 path: path(value.path)?,
977 trim_point: value.trim_point,
978 commit_id: commit_id(&value.commit_id)?,
979 })
980 }
981 wire::idempotency_observation::Outcome::Delete(value) => {
982 IdempotencyOutcome::Delete(DeleteReceipt {
983 path: path(value.path)?,
984 commit_id: commit_id(&value.commit_id)?,
985 })
986 }
987 wire::idempotency_observation::Outcome::Commit(value) => {
988 IdempotencyOutcome::Commit(commit_outcome_from_wire(value)?)
989 }
990 };
991 Ok(IdempotencyObservation {
992 idempotency_key: IdempotencyKey::new(value.idempotency_key)?,
993 request_digest,
994 outcome,
995 })
996}
997
998fn bind_observation(
999 requested: &IdempotencyKey,
1000 observation: Option<wire::IdempotencyObservation>,
1001) -> Result<Option<IdempotencyObservation>, StreamError> {
1002 let observation = observation.map(observation_from_wire).transpose()?;
1003 if observation
1004 .as_ref()
1005 .is_some_and(|observation| &observation.idempotency_key != requested)
1006 {
1007 return Err(StreamError::Unavailable);
1008 }
1009 Ok(observation)
1010}
1011
1012fn observation_wire(value: IdempotencyObservation) -> wire::IdempotencyObservation {
1013 let outcome = match value.outcome {
1014 IdempotencyOutcome::Append(value) => {
1015 wire::idempotency_observation::Outcome::Append(append_outcome_wire(value))
1016 }
1017 IdempotencyOutcome::Fork(value) => {
1018 wire::idempotency_observation::Outcome::Fork(fork_receipt_wire(&value))
1019 }
1020 IdempotencyOutcome::Trim(value) => {
1021 wire::idempotency_observation::Outcome::Trim(trim_receipt_wire(&value))
1022 }
1023 IdempotencyOutcome::Delete(value) => {
1024 wire::idempotency_observation::Outcome::Delete(delete_receipt_wire(&value))
1025 }
1026 IdempotencyOutcome::Commit(value) => {
1027 wire::idempotency_observation::Outcome::Commit(commit_outcome_wire(value))
1028 }
1029 };
1030 wire::IdempotencyObservation {
1031 idempotency_key: Bytes::copy_from_slice(value.idempotency_key.as_bytes()),
1032 request_digest: Bytes::copy_from_slice(&value.request_digest),
1033 outcome: Some(outcome),
1034 }
1035}
1036
1037fn append_outcome_from_wire(value: wire::AppendResponse) -> Result<AppendOutcome, StreamError> {
1038 match value.outcome.ok_or(StreamError::Unavailable)? {
1039 wire::append_response::Outcome::Committed(receipt) => {
1040 Ok(AppendOutcome::Committed(append_receipt(&receipt)?))
1041 }
1042 wire::append_response::Outcome::Conflict(conflict) => Ok(AppendOutcome::TailConflict {
1043 actual_tail: conflict.actual_tail,
1044 }),
1045 }
1046}
1047
1048fn append_outcome_wire(value: AppendOutcome) -> wire::AppendResponse {
1049 let outcome = match value {
1050 AppendOutcome::Committed(receipt) => {
1051 wire::append_response::Outcome::Committed(append_receipt_wire(&receipt))
1052 }
1053 AppendOutcome::TailConflict { actual_tail } => {
1054 wire::append_response::Outcome::Conflict(wire::TailConflict { actual_tail })
1055 }
1056 };
1057 wire::AppendResponse {
1058 outcome: Some(outcome),
1059 }
1060}
1061
1062fn commit_outcome_from_wire(value: wire::CommitResponse) -> Result<CommitOutcome, StreamError> {
1063 match value.outcome.ok_or(StreamError::Unavailable)? {
1064 wire::commit_response::Outcome::Committed(envelope) => {
1065 Ok(CommitOutcome::Committed(envelope_from_wire(envelope)?))
1066 }
1067 wire::commit_response::Outcome::Conflict(conflicts) => Ok(CommitOutcome::Conflict(
1068 conflicts
1069 .conflicts
1070 .into_iter()
1071 .map(conflict_from_wire)
1072 .collect::<Result<_, _>>()?,
1073 )),
1074 }
1075}
1076
1077fn commit_outcome_wire(value: CommitOutcome) -> wire::CommitResponse {
1078 let outcome = match value {
1079 CommitOutcome::Committed(envelope) => {
1080 wire::commit_response::Outcome::Committed(envelope_wire(envelope))
1081 }
1082 CommitOutcome::Conflict(conflicts) => {
1083 wire::commit_response::Outcome::Conflict(wire::CommitConflicts {
1084 conflicts: conflicts.into_iter().map(conflict_wire).collect(),
1085 })
1086 }
1087 };
1088 wire::CommitResponse {
1089 outcome: Some(outcome),
1090 }
1091}
1092
1093fn error_status(error: &StreamError) -> Status {
1094 match error {
1095 StreamError::InvalidPath => Status::invalid_argument("invalid_path"),
1096 StreamError::InvalidArgument => Status::invalid_argument("invalid_argument"),
1097 StreamError::LimitExceeded => Status::invalid_argument("limit_exceeded"),
1098 StreamError::NotFound => Status::not_found(error.to_string()),
1099 StreamError::AlreadyExists => Status::already_exists(error.to_string()),
1100 StreamError::OutOfRange => Status::out_of_range(error.to_string()),
1101 StreamError::AccessDenied => Status::permission_denied(error.to_string()),
1102 StreamError::Capacity => Status::resource_exhausted(error.to_string()),
1103 StreamError::IdempotencyMismatch => Status::failed_precondition("idempotency_mismatch"),
1104 StreamError::Retired => Status::failed_precondition("retired"),
1105 StreamError::PrefixNotRetained => Status::failed_precondition("prefix_not_retained"),
1106 StreamError::Unavailable => Status::unavailable(error.to_string()),
1107 StreamError::DeadlineElapsed => Status::failed_precondition("deadline_elapsed"),
1108 StreamError::Unsupported => Status::unimplemented("unsupported_capability"),
1109 }
1110}
1111
1112fn status(error: &tonic::Status) -> StreamError {
1113 match error.code() {
1114 Code::InvalidArgument if error.message() == "invalid_path" => StreamError::InvalidPath,
1115 Code::InvalidArgument if error.message() == "limit_exceeded" => StreamError::LimitExceeded,
1116 Code::InvalidArgument => StreamError::InvalidArgument,
1117 Code::NotFound => StreamError::NotFound,
1118 Code::AlreadyExists => StreamError::AlreadyExists,
1119 Code::OutOfRange => StreamError::OutOfRange,
1120 Code::PermissionDenied | Code::Unauthenticated => StreamError::AccessDenied,
1121 Code::ResourceExhausted => StreamError::Capacity,
1122 Code::FailedPrecondition if error.message() == "idempotency_mismatch" => {
1123 StreamError::IdempotencyMismatch
1124 }
1125 Code::FailedPrecondition if error.message() == "retired" => StreamError::Retired,
1126 Code::FailedPrecondition if error.message() == "prefix_not_retained" => {
1127 StreamError::PrefixNotRetained
1128 }
1129 Code::FailedPrecondition if error.message() == "deadline_elapsed" => {
1130 StreamError::DeadlineElapsed
1131 }
1132 Code::Unimplemented if error.message() == "unsupported_capability" => {
1133 StreamError::Unsupported
1134 }
1135 _ => StreamError::Unavailable,
1136 }
1137}
1138
1139fn commit_id(value: &[u8]) -> Result<CommitId, StreamError> {
1140 let bytes = <[u8; 32]>::try_from(value).map_err(|_| StreamError::Unavailable)?;
1141 Ok(CommitId::from_bytes(bytes))
1142}
1143
1144fn record(value: wire::Record) -> Result<Record, StreamError> {
1145 Ok(Record {
1146 sequence: value.sequence,
1147 value: value.value,
1148 commit_id: commit_id(&value.commit_id)?,
1149 })
1150}
1151
1152fn read_response(value: wire::ReadResponse) -> Result<Record, StreamError> {
1153 record(value.record.ok_or(StreamError::Unavailable)?)
1154}
1155
1156fn append_receipt(value: &wire::AppendReceipt) -> Result<AppendReceipt, StreamError> {
1157 Ok(AppendReceipt {
1158 start: value.start,
1159 end: value.end,
1160 tail: value.tail,
1161 commit_id: commit_id(&value.commit_id)?,
1162 })
1163}
1164
1165fn record_wire(value: Record) -> wire::Record {
1166 wire::Record {
1167 sequence: value.sequence,
1168 value: value.value,
1169 commit_id: Bytes::copy_from_slice(value.commit_id.as_bytes()),
1170 }
1171}
1172
1173fn append_receipt_wire(value: &AppendReceipt) -> wire::AppendReceipt {
1174 wire::AppendReceipt {
1175 start: value.start,
1176 end: value.end,
1177 tail: value.tail,
1178 commit_id: Bytes::copy_from_slice(value.commit_id.as_bytes()),
1179 }
1180}
1181
1182fn fork_receipt_wire(value: &ForkReceipt) -> wire::ForkReceipt {
1183 wire::ForkReceipt {
1184 source: value.source.to_string(),
1185 destination: value.destination.to_string(),
1186 forked_at: value.forked_at,
1187 tail: value.tail,
1188 commit_id: Bytes::copy_from_slice(value.commit_id.as_bytes()),
1189 }
1190}
1191
1192fn trim_receipt_wire(value: &TrimReceipt) -> wire::TrimReceipt {
1193 wire::TrimReceipt {
1194 path: value.path.to_string(),
1195 trim_point: value.trim_point,
1196 commit_id: Bytes::copy_from_slice(value.commit_id.as_bytes()),
1197 }
1198}
1199
1200fn delete_receipt_wire(value: &DeleteReceipt) -> wire::DeleteReceipt {
1201 wire::DeleteReceipt {
1202 path: value.path.to_string(),
1203 commit_id: Bytes::copy_from_slice(value.commit_id.as_bytes()),
1204 }
1205}
1206
1207fn envelope_from_wire(value: wire::CommittedEnvelope) -> Result<CommittedEnvelope, StreamError> {
1208 Ok(CommittedEnvelope {
1209 commit_id: commit_id(&value.commit_id)?,
1210 mutations: value
1211 .mutations
1212 .into_iter()
1213 .map(committed_mutation)
1214 .collect::<Result<_, _>>()?,
1215 })
1216}
1217
1218fn envelope_wire(value: CommittedEnvelope) -> wire::CommittedEnvelope {
1219 wire::CommittedEnvelope {
1220 commit_id: Bytes::copy_from_slice(value.commit_id.as_bytes()),
1221 mutations: value
1222 .mutations
1223 .into_iter()
1224 .map(committed_mutation_wire)
1225 .collect(),
1226 }
1227}
1228
1229fn committed_mutation(value: wire::CommittedMutation) -> Result<CommittedMutation, StreamError> {
1230 match value.mutation.ok_or(StreamError::Unavailable)? {
1231 wire::committed_mutation::Mutation::Append(value) => {
1232 Ok(CommittedMutation::Append(CommittedAppend {
1233 path: path(value.path)?,
1234 start: value.start,
1235 end: value.end,
1236 tail: value.tail,
1237 records: value
1238 .records
1239 .into_iter()
1240 .map(record)
1241 .collect::<Result<_, _>>()?,
1242 }))
1243 }
1244 wire::committed_mutation::Mutation::Fork(value) => {
1245 Ok(CommittedMutation::Fork(CommittedFork {
1246 source: path(value.source)?,
1247 destination: path(value.destination)?,
1248 forked_at: value.forked_at,
1249 tail: value.tail,
1250 }))
1251 }
1252 wire::committed_mutation::Mutation::Trim(value) => {
1253 Ok(CommittedMutation::Trim(CommittedTrim {
1254 path: path(value.path)?,
1255 trim_point: value.trim_point,
1256 }))
1257 }
1258 wire::committed_mutation::Mutation::Delete(value) => {
1259 Ok(CommittedMutation::Delete(CommittedDelete {
1260 path: path(value.path)?,
1261 }))
1262 }
1263 }
1264}
1265
1266fn committed_mutation_wire(value: CommittedMutation) -> wire::CommittedMutation {
1267 let mutation = match value {
1268 CommittedMutation::Append(value) => {
1269 wire::committed_mutation::Mutation::Append(wire::CommittedAppend {
1270 path: value.path.to_string(),
1271 start: value.start,
1272 end: value.end,
1273 tail: value.tail,
1274 records: value.records.into_iter().map(record_wire).collect(),
1275 })
1276 }
1277 CommittedMutation::Fork(value) => {
1278 wire::committed_mutation::Mutation::Fork(wire::CommittedFork {
1279 source: value.source.to_string(),
1280 destination: value.destination.to_string(),
1281 forked_at: value.forked_at,
1282 tail: value.tail,
1283 })
1284 }
1285 CommittedMutation::Trim(value) => {
1286 wire::committed_mutation::Mutation::Trim(wire::CommittedTrim {
1287 path: value.path.to_string(),
1288 trim_point: value.trim_point,
1289 })
1290 }
1291 CommittedMutation::Delete(value) => {
1292 wire::committed_mutation::Mutation::Delete(wire::CommittedDelete {
1293 path: value.path.to_string(),
1294 })
1295 }
1296 };
1297 wire::CommittedMutation {
1298 mutation: Some(mutation),
1299 }
1300}
1301
1302fn conflict_from_wire(value: wire::CommitConflict) -> Result<CommitConflict, StreamError> {
1303 match value.conflict.ok_or(StreamError::Unavailable)? {
1304 wire::commit_conflict::Conflict::Tail(value) => Ok(CommitConflict::Tail {
1305 path: path(value.path)?,
1306 expected: value.expected,
1307 actual: value.actual,
1308 }),
1309 wire::commit_conflict::Conflict::Exists(value) => Ok(CommitConflict::Exists {
1310 path: path(value.path)?,
1311 }),
1312 wire::commit_conflict::Conflict::Retired(value) => Ok(CommitConflict::Retired {
1313 path: path(value.path)?,
1314 }),
1315 }
1316}
1317
1318fn conflict_wire(value: CommitConflict) -> wire::CommitConflict {
1319 let conflict = match value {
1320 CommitConflict::Tail {
1321 path,
1322 expected,
1323 actual,
1324 } => wire::commit_conflict::Conflict::Tail(wire::TailCommitConflict {
1325 path: path.to_string(),
1326 expected,
1327 actual,
1328 }),
1329 CommitConflict::Exists { path } => {
1330 wire::commit_conflict::Conflict::Exists(wire::ExistsCommitConflict {
1331 path: path.to_string(),
1332 })
1333 }
1334 CommitConflict::Retired { path } => {
1335 wire::commit_conflict::Conflict::Retired(wire::RetiredCommitConflict {
1336 path: path.to_string(),
1337 })
1338 }
1339 };
1340 wire::CommitConflict {
1341 conflict: Some(conflict),
1342 }
1343}
1344
1345#[cfg(test)]
1346mod tests {
1347 use super::*;
1348 use crate::MemoryStream;
1349 use rcgen::generate_simple_self_signed;
1350 use tokio::net::TcpListener;
1351 use tokio_stream::wrappers::TcpListenerStream;
1352 use tonic::transport::{Identity, Server, ServerTlsConfig};
1353 use wire::stream_service_server::{StreamService, StreamServiceServer};
1354
1355 struct FiniteFollow {
1356 inner: MemoryStream,
1357 follows: AtomicUsize,
1358 tail_delay: std::time::Duration,
1359 }
1360
1361 #[async_trait]
1362 impl StreamProvider for FiniteFollow {
1363 async fn inspect_idempotency(
1364 &self,
1365 key: IdempotencyKey,
1366 ) -> Result<Option<IdempotencyObservation>, StreamError> {
1367 self.inner.inspect_idempotency(key).await
1368 }
1369
1370 async fn tail(&self, path: StreamPath) -> Result<u64, StreamError> {
1371 tokio::time::sleep(self.tail_delay).await;
1372 self.inner.tail(path).await
1373 }
1374
1375 async fn append(&self, request: AppendRequest) -> Result<AppendOutcome, StreamError> {
1376 self.inner.append(request).await
1377 }
1378
1379 async fn fork(&self, request: ForkRequest) -> Result<ForkReceipt, StreamError> {
1380 self.inner.fork(request).await
1381 }
1382
1383 async fn trim(
1384 &self,
1385 path: StreamPath,
1386 before: u64,
1387 key: IdempotencyKey,
1388 ) -> Result<TrimReceipt, StreamError> {
1389 self.inner.trim(path, before, key).await
1390 }
1391
1392 async fn delete(
1393 &self,
1394 path: StreamPath,
1395 key: IdempotencyKey,
1396 ) -> Result<DeleteReceipt, StreamError> {
1397 self.inner.delete(path, key).await
1398 }
1399
1400 async fn read(&self, request: ReadRequest) -> Result<RecordStream, StreamError> {
1401 self.inner.read(request).await
1402 }
1403
1404 async fn follow(&self, _path: StreamPath, _from: u64) -> Result<RecordStream, StreamError> {
1405 self.follows.fetch_add(1, Ordering::Relaxed);
1406 Ok(stream::empty().boxed())
1407 }
1408
1409 async fn children(&self, request: ChildrenRequest) -> Result<ChildStream, StreamError> {
1410 self.inner.children(request).await
1411 }
1412
1413 async fn commit(&self, request: CommitRequest) -> Result<CommitOutcome, StreamError> {
1414 self.inner.commit(request).await
1415 }
1416
1417 async fn read_commit(&self, commit_id: CommitId) -> Result<CommittedEnvelope, StreamError> {
1418 self.inner.read_commit(commit_id).await
1419 }
1420 }
1421
1422 fn in_memory_channel(provider: Arc<MemoryStream>) -> Channel {
1423 provider_channel(Service::new(provider))
1424 }
1425
1426 fn provider_channel<P: StreamProvider>(service: Service<P>) -> Channel {
1427 Endpoint::from_static("http://fixture.invalid").connect_with_connector_lazy(
1428 tower::service_fn(move |_| {
1429 let service = service.clone();
1430 async move {
1431 let (client, server) = tokio::io::duplex(64 * 1024);
1432 tokio::spawn(async move {
1433 let incoming = stream::once(async { Ok::<_, std::io::Error>(server) });
1434 let _ = tonic::transport::Server::builder()
1435 .add_service(StreamServiceServer::new(service))
1436 .serve_with_incoming(incoming)
1437 .await;
1438 });
1439 Ok::<_, std::io::Error>(hyper_util::rt::TokioIo::new(client))
1440 }
1441 }),
1442 )
1443 }
1444
1445 #[test]
1446 fn private_ca_bundle_is_exactly_bounded() {
1447 assert!(matches!(
1448 bounded_ca_certificate(&[]),
1449 Err(ConnectError::InvalidCaCertificate)
1450 ));
1451 assert!(matches!(
1452 bounded_ca_certificate(&vec![b'x'; MAX_CA_CERTIFICATE_BYTES]),
1453 Ok(certificate) if certificate.len() == MAX_CA_CERTIFICATE_BYTES
1454 ));
1455 assert!(matches!(
1456 bounded_ca_certificate(&vec![b'x'; MAX_CA_CERTIFICATE_BYTES + 1]),
1457 Err(ConnectError::InvalidCaCertificate)
1458 ));
1459 }
1460
1461 fn unavailable_channel() -> Channel {
1462 Endpoint::from_static("http://unavailable.invalid").connect_with_connector_lazy(
1463 tower::service_fn(|_| async {
1464 Err::<hyper_util::rt::TokioIo<tokio::io::DuplexStream>, _>(std::io::Error::new(
1465 std::io::ErrorKind::ConnectionRefused,
1466 "unavailable",
1467 ))
1468 }),
1469 )
1470 }
1471
1472 fn stalled_channel() -> Channel {
1473 Endpoint::from_static("http://stalled.invalid").connect_with_connector_lazy(
1474 tower::service_fn(|_| async {
1475 let (client, server) = tokio::io::duplex(64 * 1024);
1476 tokio::spawn(async move {
1477 let _server = server;
1478 std::future::pending::<()>().await;
1479 });
1480 Ok::<_, std::io::Error>(hyper_util::rt::TokioIo::new(client))
1481 }),
1482 )
1483 }
1484
1485 #[tokio::test]
1486 async fn endpoint_pool_is_explicit_and_bounded()
1487 -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
1488 let shared = Client::connect_endpoints(
1489 ["https://data-a.invalid", "https://data-b.invalid"],
1490 "fixture",
1491 )
1492 .await?;
1493 assert_eq!(shared.channels.len(), 2);
1494
1495 assert!(matches!(
1496 Client::connect_endpoints(std::iter::empty::<&str>(), "fixture").await,
1497 Err(ConnectError::NoEndpoints)
1498 ));
1499 assert!(matches!(
1500 Client::connect_endpoints(std::iter::repeat("https://data.invalid"), "fixture",).await,
1501 Err(ConnectError::EndpointLimit)
1502 ));
1503 let oversized = format!("https://{}.invalid", "x".repeat(MAX_ENDPOINT_URI_BYTES));
1504 assert!(matches!(
1505 Client::connect_endpoints([oversized], "fixture").await,
1506 Err(ConnectError::EndpointLimit)
1507 ));
1508 Ok(())
1509 }
1510
1511 #[tokio::test]
1512 async fn default_https_pool_installs_tls_before_first_use()
1513 -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
1514 let client = Client::connect("https://127.0.0.1:1", "fixture").await?;
1515 let mut service = Client::service(&client.channels, 0);
1516 let error = tokio::time::timeout(
1517 std::time::Duration::from_secs(2),
1518 service.tail(Request::new(wire::TailRequest {
1519 path: "accounts/events".to_owned(),
1520 })),
1521 )
1522 .await?
1523 .err()
1524 .ok_or("closed local endpoint unexpectedly answered")?;
1525 assert!(
1526 !error.to_string().contains("TLS is not enabled"),
1527 "HTTPS channel was built without TLS: {error}"
1528 );
1529 Ok(())
1530 }
1531
1532 #[tokio::test]
1533 async fn private_ca_https_connection_preserves_ambient_roots_and_exact_bearer()
1534 -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
1535 let certified = generate_simple_self_signed(["localhost".to_owned()])?;
1536 let certificate_pem = certified.cert.pem();
1537 let private_key_pem = certified.signing_key.serialize_pem();
1538 let listener = TcpListener::bind("127.0.0.1:0").await?;
1539 let address = listener.local_addr()?;
1540 let (shutdown_tx, shutdown_rx) = tokio::sync::oneshot::channel();
1541 let server_certificate_pem = certificate_pem.clone();
1542 let service = StreamServiceServer::with_interceptor(
1543 Service::new(Arc::new(MemoryStream::default())),
1544 |request: Request<()>| {
1545 if request
1546 .metadata()
1547 .get("authorization")
1548 .and_then(|value| value.to_str().ok())
1549 != Some("Bearer exact-token")
1550 {
1551 return Err(Status::unauthenticated("missing exact bearer credential"));
1552 }
1553 Ok(request)
1554 },
1555 );
1556 let server = tokio::spawn(async move {
1557 Server::builder()
1558 .tls_config(
1559 ServerTlsConfig::new()
1560 .identity(Identity::from_pem(server_certificate_pem, private_key_pem)),
1561 )?
1562 .add_service(service)
1563 .serve_with_incoming_shutdown(TcpListenerStream::new(listener), async {
1564 let _ = shutdown_rx.await;
1565 })
1566 .await
1567 });
1568
1569 let endpoint = format!("https://localhost:{}", address.port());
1570 let path = StreamPath::new("accounts/events")?;
1571 let ambient = Client::connect(&endpoint, "exact-token").await?;
1572 let mut ambient_service = Client::service(&ambient.channels, 0);
1573 assert!(
1574 ambient_service
1575 .tail(Request::new(wire::TailRequest {
1576 path: path.to_string(),
1577 }))
1578 .await
1579 .is_err()
1580 );
1581 let explicit = crate::StreamClient::connect_with_ca_certificate(
1582 endpoint,
1583 "exact-token",
1584 certificate_pem,
1585 )
1586 .await?;
1587 assert_eq!(
1588 explicit.stream(path.as_str())?.tail().await,
1589 Err(StreamError::NotFound)
1590 );
1591
1592 let _ = shutdown_tx.send(());
1593 server.await??;
1594 Ok(())
1595 }
1596
1597 #[tokio::test]
1598 async fn hung_operation_fails_over_without_truncating_a_healthy_slow_response()
1599 -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
1600 let provider = Arc::new(FiniteFollow {
1601 inner: MemoryStream::default(),
1602 follows: AtomicUsize::new(0),
1603 tail_delay: std::time::Duration::from_millis(600),
1604 });
1605 provider
1606 .inner
1607 .append(AppendRequest {
1608 path: StreamPath::new("accounts/events")?,
1609 records: vec![Bytes::from_static(b"seed")],
1610 if_tail: Some(0),
1611 idempotency_key: Some(IdempotencyKey::new(Bytes::from_static(b"slow-tail"))?),
1612 })
1613 .await?;
1614 let transport = Client::from_channels(
1615 Arc::from([stalled_channel(), provider_channel(Service::new(provider))]),
1616 "fixture",
1617 )?;
1618 let tail = tokio::time::timeout(
1619 std::time::Duration::from_secs(2),
1620 transport.tail(StreamPath::new("accounts/events")?),
1621 )
1622 .await??;
1623 assert_eq!(tail, 1);
1624 Ok(())
1625 }
1626
1627 #[tokio::test]
1628 async fn clean_follow_eof_advances_once_to_the_next_endpoint()
1629 -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
1630 let ended = Arc::new(FiniteFollow {
1631 inner: MemoryStream::default(),
1632 follows: AtomicUsize::new(0),
1633 tail_delay: std::time::Duration::ZERO,
1634 });
1635 let durable = Arc::new(MemoryStream::default());
1636 durable
1637 .append(AppendRequest {
1638 path: StreamPath::new("accounts/events")?,
1639 records: vec![Bytes::from_static(b"resumed")],
1640 if_tail: Some(0),
1641 idempotency_key: Some(IdempotencyKey::new(Bytes::from_static(b"resume"))?),
1642 })
1643 .await?;
1644 let transport = Client::from_channels(
1645 Arc::from([
1646 provider_channel(Service::new(Arc::clone(&ended))),
1647 in_memory_channel(durable),
1648 ]),
1649 "fixture",
1650 )?;
1651 let preferred = Arc::clone(&transport.preferred);
1652 let stream = crate::StreamClient::new(Arc::new(transport)).stream("accounts/events")?;
1653
1654 let record = tokio::time::timeout(std::time::Duration::from_secs(1), async {
1655 stream
1656 .follow(0)
1657 .await?
1658 .next()
1659 .await
1660 .ok_or(StreamError::Unavailable)?
1661 })
1662 .await??;
1663 assert_eq!(record.value, Bytes::from_static(b"resumed"));
1664 assert_eq!(ended.follows.load(Ordering::Relaxed), 1);
1665 assert_eq!(preferred.load(Ordering::Relaxed), 1);
1666
1667 let stale = RecordCursor {
1669 client: Client::from_channels(
1670 Arc::from([unavailable_channel(), unavailable_channel()]),
1671 "fixture",
1672 )?,
1673 path: StreamPath::new("accounts/events")?,
1674 next: 0,
1675 remaining: None,
1676 active: None,
1677 };
1678 stale.client.preferred.store(1, Ordering::Relaxed);
1679 stale.advance_follow(0);
1680 assert_eq!(stale.client.preferred.load(Ordering::Relaxed), 1);
1681 Ok(())
1682 }
1683
1684 #[tokio::test]
1685 async fn lost_connection_dispatch_retries_but_peer_cancellation_does_not()
1686 -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
1687 let (client_io, _peer) = tokio::io::duplex(4096);
1688 let (mut sender, connection) = hyper::client::conn::http2::handshake(
1689 hyper_util::rt::TokioExecutor::new(),
1690 hyper_util::rt::TokioIo::new(client_io),
1691 )
1692 .await?;
1693 drop(connection);
1696 let request = tonic::codegen::http::Request::new(tonic::body::Body::empty());
1697 let error = tokio::time::timeout(
1698 std::time::Duration::from_secs(1),
1699 sender.send_request(request),
1700 )
1701 .await?
1702 .err()
1703 .ok_or("closed connection accepted a request")?;
1704 assert!(error.is_canceled());
1705 let error = Status::from_error(Box::new(error));
1706 assert_eq!(error.code(), Code::Cancelled);
1707 assert!(retryable(&error), "{error:?}");
1708 assert_unary_retry(error, true).await?;
1709 assert_unary_retry(Status::cancelled("application cancellation"), false).await?;
1710 Ok(())
1711 }
1712
1713 #[tokio::test]
1714 async fn incomplete_response_keeps_the_same_request_retryable()
1715 -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
1716 use tonic::codec::Codec;
1717 let mut codec =
1718 tonic_prost::ProstCodec::<wire::AppendResponse, wire::AppendResponse>::default();
1719 let mut response = tonic::Streaming::new_response(
1720 codec.decoder(),
1721 tonic::body::Body::empty(),
1722 tonic::codegen::http::StatusCode::OK,
1723 None,
1724 Some(1024),
1725 );
1726 let error = tokio::time::timeout(std::time::Duration::from_secs(1), response.message())
1727 .await?
1728 .err()
1729 .ok_or("missing terminal status must not succeed")?;
1730 assert_eq!(error.code(), Code::Unknown);
1731 assert!(std::error::Error::source(&error).is_none());
1732 assert!(
1733 retryable(&error),
1734 "incomplete response must preserve the retry identity: {error}"
1735 );
1736 assert_unary_retry(error, true).await?;
1737 assert_unary_retry(Status::unknown("peer outcome unknown"), true).await?;
1738 Ok(())
1739 }
1740
1741 async fn assert_unary_retry(
1742 error: Status,
1743 retry: bool,
1744 ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
1745 let client = Client::connect("https://fixture.invalid", "fixture").await?;
1746 let body = wire::AppendRequest {
1747 path: "/fixture".to_owned(),
1748 records: vec![Bytes::from_static(b"mutation")],
1749 if_tail: Some(3),
1750 idempotency_key: Some(Bytes::from_static(b"stable-key")),
1751 };
1752 let mut attempts = Vec::new();
1753 let mut first = Some(error);
1754 let result = tokio::time::timeout(
1755 std::time::Duration::from_secs(1),
1756 client.unary(body.clone(), |_, request| {
1757 attempts.push(request.into_inner());
1758 let result = first.take().map_or_else(|| Ok(Response::new(())), Err);
1759 Box::pin(async move { result })
1760 }),
1761 )
1762 .await?;
1763 if retry {
1764 assert_eq!(result, Ok(()));
1765 assert_eq!(attempts, vec![body.clone(), body]);
1766 } else {
1767 assert!(result.is_err());
1768 assert_eq!(attempts, vec![body]);
1769 }
1770 Ok(())
1771 }
1772
1773 #[tokio::test(start_paused = true)]
1774 async fn persistent_unknown_expires_without_changing_request_identity()
1775 -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
1776 let client = Client::connect("https://fixture.invalid", "fixture").await?;
1777 let started = tokio::time::Instant::now();
1778 let mut attempts = 0;
1779 let result: Result<(), StreamError> = client
1780 .unary(Bytes::from_static(b"stable-key-and-body"), |_, request| {
1781 assert_eq!(
1782 request.into_inner(),
1783 Bytes::from_static(b"stable-key-and-body")
1784 );
1785 attempts += 1;
1786 Box::pin(async {
1787 tokio::time::sleep(std::time::Duration::from_millis(100)).await;
1788 Err(Status::unknown("outcome unavailable"))
1789 })
1790 })
1791 .await;
1792 assert_eq!(result, Err(StreamError::Unavailable));
1793 assert_eq!(tokio::time::Instant::now() - started, OPERATION_DEADLINE);
1794 assert!((2..=100).contains(&attempts));
1795 Ok(())
1796 }
1797
1798 #[tokio::test]
1799 async fn remote_request_reset_is_retryable_without_retrying_protocol_errors()
1800 -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
1801 for reason in [h2::Reason::CANCEL, h2::Reason::PROTOCOL_ERROR] {
1802 let mut tasks = tokio::task::JoinSet::new();
1803 let outcome = tokio::time::timeout(std::time::Duration::from_secs(1), async {
1804 let (client_io, server_io) = tokio::io::duplex(4096);
1805 tasks.spawn(async move {
1806 let mut connection = h2::server::handshake(server_io).await?;
1807 let (_, mut response) = connection.accept().await.ok_or("request absent")??;
1808 response.send_reset(reason);
1809 connection.graceful_shutdown();
1810 while connection.accept().await.is_some() {}
1811 Ok::<(), Box<dyn std::error::Error + Send + Sync>>(())
1812 });
1813 let (mut sender, connection) = h2::client::handshake(client_io).await?;
1814 tasks.spawn(async move {
1815 connection.await?;
1816 Ok::<(), Box<dyn std::error::Error + Send + Sync>>(())
1817 });
1818 let request = tonic::codegen::http::Request::builder()
1819 .uri("http://fixture.invalid/stream")
1820 .body(())?;
1821 let (response, _) = sender.send_request(request, true)?;
1822 response.await.err().ok_or_else(|| {
1823 Box::<dyn std::error::Error + Send + Sync>::from("reset response succeeded")
1824 })
1825 })
1826 .await;
1827 tasks.shutdown().await;
1828 let error = outcome??;
1829 assert!(error.is_remote());
1830 let error = Status::from_error(Box::new(error));
1831 assert_eq!(retryable(&error), reason == h2::Reason::CANCEL, "{error:?}");
1832 assert_unary_retry(error, reason == h2::Reason::CANCEL).await?;
1833 }
1834 Ok(())
1835 }
1836
1837 #[test]
1838 fn retries_transport_loss_but_not_peer_errors_or_corrupt_data() {
1839 for kind in [
1840 std::io::ErrorKind::UnexpectedEof,
1841 std::io::ErrorKind::ConnectionReset,
1842 std::io::ErrorKind::ConnectionAborted,
1843 std::io::ErrorKind::BrokenPipe,
1844 std::io::ErrorKind::NotConnected,
1845 std::io::ErrorKind::TimedOut,
1846 ] {
1847 let mut error = Status::unknown("response body interrupted");
1848 error.set_source(Arc::new(std::io::Error::from(kind)));
1849 assert!(retryable(&error), "{kind:?}");
1850 }
1851 for error in [
1852 Status::internal("protocol error"),
1853 Status::data_loss("corrupt record"),
1854 Status::permission_denied("denied"),
1855 Status::cancelled("caller cancelled"),
1856 Status::from_error(Box::new(std::io::Error::from(
1857 std::io::ErrorKind::InvalidData,
1858 ))),
1859 ] {
1860 assert!(!retryable(&error), "{error:?}");
1861 }
1862 }
1863
1864 #[test]
1865 fn inspection_rejects_an_observation_for_another_retry_identity() -> Result<(), StreamError> {
1866 let requested = IdempotencyKey::new(Bytes::from_static(b"requested"))?;
1867 let forged = wire::IdempotencyObservation {
1868 idempotency_key: Bytes::from_static(b"other"),
1869 request_digest: Bytes::from_static(&[7; 32]),
1870 outcome: Some(wire::idempotency_observation::Outcome::Append(
1871 append_outcome_wire(AppendOutcome::TailConflict { actual_tail: 3 }),
1872 )),
1873 };
1874 assert_eq!(
1875 bind_observation(&requested, Some(forged)),
1876 Err(StreamError::Unavailable)
1877 );
1878 Ok(())
1879 }
1880
1881 #[tokio::test]
1882 async fn service_maps_the_canonical_contract_without_an_alternate_state_machine()
1883 -> Result<(), Status> {
1884 let service = Service::new(Arc::new(MemoryStream::default()));
1885 let append = service
1886 .append(Request::new(wire::AppendRequest {
1887 path: "accounts/events".to_owned(),
1888 records: vec![Bytes::from_static(b"one")],
1889 if_tail: Some(0),
1890 idempotency_key: Some(Bytes::from_static(b"wire-append")),
1891 }))
1892 .await?
1893 .into_inner();
1894 let Some(wire::append_response::Outcome::Committed(receipt)) = append.outcome else {
1895 return Err(Status::internal("append outcome missing"));
1896 };
1897 if receipt.start != 0 || receipt.end != 1 || receipt.commit_id.len() != 32 {
1898 return Err(Status::internal("append receipt changed"));
1899 }
1900 let observation = service
1901 .inspect_idempotency(Request::new(wire::InspectIdempotencyRequest {
1902 idempotency_key: Bytes::from_static(b"wire-append"),
1903 }))
1904 .await?
1905 .into_inner()
1906 .observation
1907 .ok_or_else(|| Status::internal("idempotency observation missing"))?;
1908 if observation.request_digest.len() != 32
1909 || !matches!(
1910 observation.outcome,
1911 Some(wire::idempotency_observation::Outcome::Append(_))
1912 )
1913 {
1914 return Err(Status::internal("idempotency observation changed"));
1915 }
1916 let mut read = service
1917 .read(Request::new(wire::ReadRequest {
1918 path: "accounts/events".to_owned(),
1919 from: 0,
1920 limit: 1,
1921 }))
1922 .await?
1923 .into_inner();
1924 let record = read
1925 .next()
1926 .await
1927 .ok_or_else(|| Status::internal("read ended"))??
1928 .record
1929 .ok_or_else(|| Status::internal("record missing"))?;
1930 if record.sequence != 0 || record.value != Bytes::from_static(b"one") {
1931 return Err(Status::internal("record changed"));
1932 }
1933 Ok(())
1934 }
1935}