1use std::collections::BTreeMap;
2use std::error::Error;
3use std::fmt::{self, Display, Formatter};
4use std::io;
5use std::sync::Arc;
6use std::time::Duration;
7
8use http::{Uri, uri::PathAndQuery};
9use hyper_util::rt::TokioIo;
10use lodestone_wire::{
11 Digest, LODESTONE_API_SPEC_SHA256, LODESTONE_API_SUBSCRIPTION_ACCEPTED_FORMAT_VERSION,
12 LODESTONE_API_SUBSCRIPTION_EVENT_FORMAT_VERSION, LODESTONE_API_SUBSCRIPTION_GAP_FORMAT_VERSION,
13 LODESTONE_API_SUBSCRIPTION_HEARTBEAT_FORMAT_VERSION, LODESTONE_API_SUBSCRIPTION_START_FORMAT_VERSION,
14 LODESTONE_API_SUBSCRIPTION_TERMINAL_FORMAT_VERSION, Resource, SubscriptionAccepted, SubscriptionCursorEntry,
15 SubscriptionEvent, SubscriptionFrame, SubscriptionRoute, SubscriptionStart, SubscriptionStartPosition,
16};
17use rustls::pki_types::CertificateDer;
18use tokio::net::TcpStream;
19use tokio_rustls::TlsConnector;
20use tonic::Code;
21use tonic::metadata::MetadataValue;
22use tonic::transport::{Channel, Endpoint};
23use tower::service_fn;
24
25use crate::config::parse_u64;
26use crate::transport::authorization_value;
27use crate::{CallOptions, Client, ClientError};
28
29const SUBSCRIPTION_METHOD: &str = "/lodestone.api.FilterSubscription/Subscribe";
30const MAX_SUBSCRIPTION_MESSAGE_BYTES: usize = 2 * 1024 * 1024;
31
32#[derive(Clone, PartialEq, prost::Message)]
33struct GrpcSubscriptionRequest {
34 #[prost(bytes = "vec", tag = "1")]
35 json_request: Vec<u8>,
36}
37
38#[derive(Clone, PartialEq, prost::Message)]
39struct GrpcSubscriptionFrame {
40 #[prost(bytes = "vec", tag = "1")]
41 json_frame: Vec<u8>,
42}
43
44#[derive(Clone, Debug, PartialEq)]
45pub struct SubscriptionCheckpoint {
46 pub index: Resource,
47 pub routes: Vec<SubscriptionRoute>,
48 pub cut: Vec<SubscriptionCursorEntry>,
49}
50
51pub struct Subscription {
52 client: Client,
53 template: SubscriptionStart,
54 options: CallOptions,
55 stream: tonic::Streaming<GrpcSubscriptionFrame>,
56 checkpoint: SubscriptionCheckpoint,
57 pending: Option<SubscriptionFrame>,
58 reconnects: u8,
59 ended: bool,
60 authorization_revision: Option<u64>,
61 last_events: BTreeMap<CursorKey, SubscriptionEvent>,
62}
63
64impl Client {
65 pub async fn subscribe(
66 &self,
67 start: SubscriptionStart,
68 options: CallOptions,
69 ) -> Result<Subscription, SubscriptionError> {
70 if self.profile_verification().is_none() {
71 return Err(SubscriptionError::ProfileNotVerified);
72 }
73 validate_start(&start, self.connection_profile().as_wire().schema_digest.clone())?;
74 let (stream, accepted) = open_stream(self, &start, &options).await?;
75 let checkpoint = checkpoint_from_accepted(&start.index, &accepted)?;
76 Ok(Subscription {
77 client: self.clone(),
78 template: start,
79 options,
80 stream,
81 checkpoint,
82 pending: Some(SubscriptionFrame::Accepted { body: accepted }),
83 reconnects: 0,
84 ended: false,
85 authorization_revision: None,
86 last_events: BTreeMap::new(),
87 })
88 }
89}
90
91impl Subscription {
92 #[must_use]
93 pub const fn checkpoint(&self) -> &SubscriptionCheckpoint {
94 &self.checkpoint
95 }
96
97 pub async fn next_frame(&mut self) -> Result<Option<SubscriptionFrame>, SubscriptionError> {
98 if self.ended {
99 return Ok(None);
100 }
101 if let Some(frame) = self.pending.take() {
102 return Ok(Some(frame));
103 }
104 loop {
105 let deadline = self.options.local_deadline().map_err(SubscriptionError::Client)?;
106 let next = tokio::select! {
107 result = tokio::time::timeout_at(deadline, self.stream.message()) => {
108 match result {
109 Ok(result) => result.map_err(SubscriptionError::Status),
110 Err(_) => Err(SubscriptionError::FrameDeadline),
111 }
112 }
113 () = self.options.cancellation().cancelled() => return Err(SubscriptionError::Cancelled),
114 };
115 let reconnect = match next {
116 Ok(Some(message)) => {
117 let frame = decode_frame(message.json_frame)?;
118 self.validate_and_advance(&frame)?;
119 return Ok(Some(frame));
120 }
121 Err(SubscriptionError::FrameDeadline) => {
122 self.ended = true;
123 return Err(SubscriptionError::FrameDeadline);
124 }
125 Ok(None) => true,
126 Err(SubscriptionError::Status(status)) if reconnectable_status(&status) => true,
127 Err(SubscriptionError::Status(status)) => {
128 self.ended = true;
129 return Err(SubscriptionError::Status(status));
130 }
131 Err(error) => return Err(error),
132 };
133 if reconnect {
134 if self.reconnects >= self.client.pool_config().maximum_subscription_reconnects {
135 self.ended = true;
136 return Err(SubscriptionError::ReconnectExhausted);
137 }
138 self.reconnects = self.reconnects.saturating_add(1);
139 let backoff = Duration::from_millis(25_u64.saturating_mul(1_u64 << self.reconnects.min(3)));
140 tokio::select! {
141 result = tokio::time::timeout_at(deadline, tokio::time::sleep(backoff)) => {
142 if result.is_err() {
143 self.ended = true;
144 return Err(SubscriptionError::FrameDeadline);
145 }
146 }
147 () = self.options.cancellation().cancelled() => {
148 return Err(SubscriptionError::Cancelled);
149 }
150 }
151 let resume = self.resume_start()?;
152 let (stream, accepted) = open_stream(&self.client, &resume, &self.options).await?;
153 let checkpoint = checkpoint_from_accepted(&resume.index, &accepted)?;
154 if checkpoint.routes != self.checkpoint.routes || checkpoint.cut != self.checkpoint.cut {
155 self.ended = true;
156 return Err(SubscriptionError::CheckpointMismatch);
157 }
158 self.template = resume;
159 self.stream = stream;
160 }
161 }
162 }
163
164 fn resume_start(&self) -> Result<SubscriptionStart, SubscriptionError> {
165 let mut start = self.template.clone();
166 start.request_id = random_request_id()?;
167 start.reconnect_attempt = u32::from(self.reconnects);
168 start.start = SubscriptionStartPosition::Resume {
169 routes: self.checkpoint.routes.clone(),
170 cut: self.checkpoint.cut.clone(),
171 };
172 Ok(start)
173 }
174
175 fn validate_and_advance(&mut self, frame: &SubscriptionFrame) -> Result<(), SubscriptionError> {
176 match frame {
177 SubscriptionFrame::Accepted { .. } => Err(SubscriptionError::UnexpectedAccepted),
178 SubscriptionFrame::Event { body } => {
179 validate_event(body, &self.checkpoint.index)?;
180 let cursor = cursor_from_event(body);
181 let key = CursorKey::from_cursor(&cursor);
182 if let Some(previous) = cursor_for_key(&self.checkpoint.cut, &key) {
183 let prior = parse_u64(&previous.sequence.value).ok_or(SubscriptionError::InvalidFrame)?;
184 let next = parse_u64(&cursor.sequence.value).ok_or(SubscriptionError::InvalidFrame)?;
185 if next < prior {
186 return Err(SubscriptionError::CursorRegression);
187 }
188 if next == prior {
189 if self.last_events.get(&key) == Some(body) {
190 return Ok(());
191 }
192 return Err(SubscriptionError::DuplicateIdentityMismatch);
193 }
194 }
195 upsert_cursor(&mut self.checkpoint.cut, cursor)?;
196 self.last_events.insert(key, body.clone());
197 Ok(())
198 }
199 SubscriptionFrame::Heartbeat { body } => {
200 if body.format_version != u32::from(LODESTONE_API_SUBSCRIPTION_HEARTBEAT_FORMAT_VERSION) {
201 return Err(SubscriptionError::InvalidFrame);
202 }
203 let revision = parse_u64(&body.authorization_revision.value).ok_or(SubscriptionError::InvalidFrame)?;
204 if parse_u64(&body.route_revision.value).is_none()
205 || parse_u64(&body.authorization_valid_until_millis.value).is_none()
206 {
207 return Err(SubscriptionError::InvalidFrame);
208 }
209 if self.authorization_revision.is_some_and(|previous| revision < previous) {
210 return Err(SubscriptionError::AuthorizationRegression);
211 }
212 merge_cut(&mut self.checkpoint.cut, &body.confirmed_cut, &self.checkpoint.index)?;
213 self.authorization_revision = Some(revision);
214 Ok(())
215 }
216 SubscriptionFrame::Gap { body } => {
217 if body.format_version != u32::from(LODESTONE_API_SUBSCRIPTION_GAP_FORMAT_VERSION) || !body.terminal {
218 return Err(SubscriptionError::InvalidFrame);
219 }
220 for cursor in body
221 .affected
222 .iter()
223 .chain(&body.last_confirmed)
224 .chain(&body.earliest_available)
225 {
226 if cursor.index != self.checkpoint.index {
227 return Err(SubscriptionError::InvalidFrame);
228 }
229 }
230 self.ended = true;
231 Ok(())
232 }
233 SubscriptionFrame::Terminal { body } => {
234 if body.format_version != u32::from(LODESTONE_API_SUBSCRIPTION_TERMINAL_FORMAT_VERSION) {
235 return Err(SubscriptionError::InvalidFrame);
236 }
237 self.ended = true;
238 Ok(())
239 }
240 }
241 }
242}
243
244async fn open_stream(
245 client: &Client,
246 start: &SubscriptionStart,
247 options: &CallOptions,
248) -> Result<(tonic::Streaming<GrpcSubscriptionFrame>, SubscriptionAccepted), SubscriptionError> {
249 let payload = serde_json::to_vec(start).map_err(|_| SubscriptionError::RequestCodec)?;
250 if payload.is_empty() || payload.len() > MAX_SUBSCRIPTION_MESSAGE_BYTES {
251 return Err(SubscriptionError::RequestTooLarge);
252 }
253 let deadline = options.local_deadline().map_err(SubscriptionError::Client)?;
254 let credential = tokio::select! {
255 result = tokio::time::timeout_at(deadline, client.credential()) => {
256 result.map_err(|_| SubscriptionError::OpenDeadline)?
257 .map_err(SubscriptionError::Client)?
258 }
259 () = options.cancellation().cancelled() => return Err(SubscriptionError::Cancelled),
260 };
261 let authorization = authorization_value(&credential).map_err(SubscriptionError::Client)?;
262 let channel = connect_channel(client, options, deadline).await?;
263 let mut grpc = tonic::client::Grpc::new(channel)
264 .max_decoding_message_size(MAX_SUBSCRIPTION_MESSAGE_BYTES)
265 .max_encoding_message_size(MAX_SUBSCRIPTION_MESSAGE_BYTES);
266 let mut request = tonic::Request::new(GrpcSubscriptionRequest { json_request: payload });
267 let authorization =
268 std::str::from_utf8(authorization.as_slice()).map_err(|_| SubscriptionError::InvalidMetadata)?;
269 request.metadata_mut().insert(
270 "authorization",
271 MetadataValue::try_from(authorization).map_err(|_| SubscriptionError::InvalidMetadata)?,
272 );
273 request.metadata_mut().insert(
274 "x-lodestone-collection-id",
275 MetadataValue::try_from(
276 client
277 .connection_profile()
278 .as_wire()
279 .expected_collection_id
280 .value
281 .as_str(),
282 )
283 .map_err(|_| SubscriptionError::InvalidMetadata)?,
284 );
285 request.metadata_mut().insert(
286 "x-lodestone-index-name",
287 MetadataValue::try_from(start.index.name.as_str()).map_err(|_| SubscriptionError::InvalidMetadata)?,
288 );
289 tokio::select! {
292 result = tokio::time::timeout_at(deadline, grpc.ready()) => {
293 result.map_err(|_| SubscriptionError::OpenDeadline)?
294 .map_err(|_| SubscriptionError::Connect)?;
295 }
296 () = options.cancellation().cancelled() => return Err(SubscriptionError::Cancelled),
297 }
298 let response = tokio::select! {
299 result = tokio::time::timeout_at(
300 deadline,
301 grpc.server_streaming(
302 request,
303 PathAndQuery::from_static(SUBSCRIPTION_METHOD),
304 tonic_prost::ProstCodec::default(),
305 ),
306 ) => result.map_err(|_| SubscriptionError::OpenDeadline)?
307 .map_err(SubscriptionError::Status)?,
308 () = options.cancellation().cancelled() => return Err(SubscriptionError::Cancelled),
309 };
310 let mut stream = response.into_inner();
311 let first: GrpcSubscriptionFrame = tokio::select! {
312 result = tokio::time::timeout_at(deadline, stream.message()) => {
313 result.map_err(|_| SubscriptionError::OpenDeadline)?
314 .map_err(SubscriptionError::Status)?
315 }
316 () = options.cancellation().cancelled() => return Err(SubscriptionError::Cancelled),
317 }
318 .ok_or(SubscriptionError::AcceptedAbsent)?;
319 let frame = decode_frame(first.json_frame)?;
320 let SubscriptionFrame::Accepted { body } = frame else {
321 return Err(SubscriptionError::AcceptedAbsent);
322 };
323 Ok((stream, body))
324}
325
326async fn connect_channel(
327 client: &Client,
328 options: &CallOptions,
329 deadline: tokio::time::Instant,
330) -> Result<Channel, SubscriptionError> {
331 let config = client.transport_config();
332 let mut roots = rustls::RootCertStore::empty();
333 for certificate in config.trust.certificates_der() {
334 roots
335 .add(CertificateDer::from(certificate.clone()))
336 .map_err(|_| SubscriptionError::InvalidTls)?;
337 }
338 let provider = Arc::new(rustls::crypto::aws_lc_rs::default_provider());
339 let mut tls = rustls::ClientConfig::builder_with_provider(provider)
340 .with_protocol_versions(&[&rustls::version::TLS13])
341 .map_err(|_| SubscriptionError::InvalidTls)?
342 .with_root_certificates(roots)
343 .with_no_client_auth();
344 tls.alpn_protocols = vec![b"h2".to_vec()];
345 let connector = TlsConnector::from(Arc::new(tls));
346 let host = config.stream_endpoint.host().to_owned();
347 let port = config.stream_endpoint.port();
348 let server_name = rustls::pki_types::ServerName::try_from(config.identity.tls_server_name().to_owned())
349 .map_err(|_| SubscriptionError::InvalidTls)?;
350 let connect = service_fn(move |_uri: Uri| {
351 let connector = connector.clone();
352 let host = host.clone();
353 let server_name = server_name.clone();
354 async move {
355 let tcp = TcpStream::connect((host.as_str(), port)).await?;
356 let tls = connector.connect(server_name, tcp).await.map_err(io::Error::other)?;
357 if tls.get_ref().1.alpn_protocol() != Some(b"h2".as_slice()) {
358 return Err(io::Error::other("Core subscription TLS did not negotiate h2"));
359 }
360 Ok::<_, io::Error>(TokioIo::new(tls))
361 }
362 });
363 let transport_uri = format!(
364 "http://{}:{}",
365 config.stream_endpoint.host(),
366 config.stream_endpoint.port()
367 );
368 let origin: Uri = config
369 .stream_endpoint
370 .as_str()
371 .parse()
372 .map_err(|_| SubscriptionError::InvalidEndpoint)?;
373 let endpoint = Endpoint::from_shared(transport_uri)
374 .map_err(|_| SubscriptionError::InvalidEndpoint)?
375 .connect_timeout(config.pool.connect_timeout)
376 .origin(origin);
377 tokio::select! {
378 result = tokio::time::timeout_at(deadline, endpoint.connect_with_connector(connect)) => {
379 result.map_err(|_| SubscriptionError::OpenDeadline)?
380 .map_err(|_| SubscriptionError::Connect)
381 }
382 () = options.cancellation().cancelled() => Err(SubscriptionError::Cancelled),
383 }
384}
385
386fn validate_start(start: &SubscriptionStart, schema: Digest) -> Result<(), SubscriptionError> {
387 if start.format_version != u32::from(LODESTONE_API_SUBSCRIPTION_START_FORMAT_VERSION)
388 || start.request_id.is_empty()
389 || start.request_id.len() > 128
390 || start.request_id.bytes().any(|byte| byte.is_ascii_control())
391 || start.index.kind != "index"
392 || start.index.name.is_empty()
393 || start.index.incarnation.validate().is_err()
394 || start.schema_digest != schema
395 || start.schema_digest.value != LODESTONE_API_SPEC_SHA256
396 || parse_u64(&start.heartbeat_millis.value).is_none_or(|value| value == 0)
397 {
398 return Err(SubscriptionError::InvalidStart);
399 }
400 Ok(())
401}
402
403fn checkpoint_from_accepted(
404 index: &Resource,
405 accepted: &SubscriptionAccepted,
406) -> Result<SubscriptionCheckpoint, SubscriptionError> {
407 if accepted.format_version != u32::from(LODESTONE_API_SUBSCRIPTION_ACCEPTED_FORMAT_VERSION)
408 || accepted.schema_digest.value != LODESTONE_API_SPEC_SHA256
409 || accepted.routes.is_empty()
410 {
411 return Err(SubscriptionError::InvalidFrame);
412 }
413 for route in &accepted.routes {
414 if &route.index != index || route.shard_ids.is_empty() {
415 return Err(SubscriptionError::InvalidFrame);
416 }
417 }
418 let mut cut = Vec::new();
419 merge_cut(&mut cut, &accepted.accepted_cut, index)?;
420 Ok(SubscriptionCheckpoint {
421 index: index.clone(),
422 routes: accepted.routes.clone(),
423 cut,
424 })
425}
426
427fn decode_frame(bytes: Vec<u8>) -> Result<SubscriptionFrame, SubscriptionError> {
428 if bytes.is_empty() || bytes.len() > MAX_SUBSCRIPTION_MESSAGE_BYTES {
429 return Err(SubscriptionError::InvalidFrame);
430 }
431 let frame: SubscriptionFrame = serde_json::from_slice(&bytes).map_err(|_| SubscriptionError::FrameCodec)?;
432 if serde_json::to_vec(&frame).map_err(|_| SubscriptionError::FrameCodec)? != bytes {
433 return Err(SubscriptionError::NonCanonicalFrame);
434 }
435 Ok(frame)
436}
437
438fn validate_event(event: &SubscriptionEvent, index: &Resource) -> Result<(), SubscriptionError> {
439 if event.format_version != u32::from(LODESTONE_API_SUBSCRIPTION_EVENT_FORMAT_VERSION)
440 || &event.index != index
441 || event.event_id.validate().is_err()
442 || event.recovery_era.validate().is_err()
443 || event.writer_fence.recovery_era != event.recovery_era
444 || parse_u64(&event.routing_revision.value).is_none()
445 || parse_u64(&event.shard_id.value).is_none()
446 || parse_u64(&event.sequence.value).is_none()
447 {
448 return Err(SubscriptionError::InvalidFrame);
449 }
450 Ok(())
451}
452
453fn cursor_from_event(event: &SubscriptionEvent) -> SubscriptionCursorEntry {
454 SubscriptionCursorEntry {
455 index: event.index.clone(),
456 routing_revision: event.routing_revision.clone(),
457 recovery_era: event.recovery_era.clone(),
458 shard_id: event.shard_id.clone(),
459 sequence: event.sequence.clone(),
460 }
461}
462
463fn merge_cut(
464 current: &mut Vec<SubscriptionCursorEntry>,
465 observed: &[SubscriptionCursorEntry],
466 index: &Resource,
467) -> Result<(), SubscriptionError> {
468 for cursor in observed {
469 if &cursor.index != index {
470 return Err(SubscriptionError::InvalidFrame);
471 }
472 upsert_cursor(current, cursor.clone())?;
473 }
474 Ok(())
475}
476
477fn upsert_cursor(
478 current: &mut Vec<SubscriptionCursorEntry>,
479 observed: SubscriptionCursorEntry,
480) -> Result<(), SubscriptionError> {
481 let key = CursorKey::from_cursor(&observed);
482 let next = parse_u64(&observed.sequence.value).ok_or(SubscriptionError::InvalidFrame)?;
483 if let Some(existing) = current.iter_mut().find(|cursor| CursorKey::from_cursor(cursor) == key) {
484 let prior = parse_u64(&existing.sequence.value).ok_or(SubscriptionError::InvalidFrame)?;
485 if next < prior {
486 return Err(SubscriptionError::CursorRegression);
487 }
488 *existing = observed;
489 } else {
490 current.push(observed);
491 }
492 current.sort_by_key(CursorKey::from_cursor);
493 Ok(())
494}
495
496fn cursor_for_key<'a>(cut: &'a [SubscriptionCursorEntry], key: &CursorKey) -> Option<&'a SubscriptionCursorEntry> {
497 cut.iter().find(|cursor| &CursorKey::from_cursor(cursor) == key)
498}
499
500#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
501struct CursorKey {
502 routing_revision: String,
503 recovery_era: String,
504 shard_id: String,
505}
506
507impl CursorKey {
508 fn from_cursor(cursor: &SubscriptionCursorEntry) -> Self {
509 Self {
510 routing_revision: cursor.routing_revision.value.clone(),
511 recovery_era: cursor.recovery_era.value.clone(),
512 shard_id: cursor.shard_id.value.clone(),
513 }
514 }
515}
516
517fn random_request_id() -> Result<String, SubscriptionError> {
518 let mut bytes = [0_u8; 16];
519 getrandom::fill(&mut bytes).map_err(|_| SubscriptionError::RandomUnavailable)?;
520 if bytes == [0; 16] {
521 return Err(SubscriptionError::RandomUnavailable);
522 }
523 const HEX: &[u8; 16] = b"0123456789abcdef";
524 let mut output = String::with_capacity(34);
525 output.push_str("s-");
526 for byte in bytes {
527 output.push(char::from(HEX[usize::from(byte >> 4)]));
528 output.push(char::from(HEX[usize::from(byte & 0x0f)]));
529 }
530 Ok(output)
531}
532
533fn reconnectable_status(status: &tonic::Status) -> bool {
534 matches!(
535 status.code(),
536 Code::Cancelled | Code::Unknown | Code::DeadlineExceeded | Code::Aborted | Code::Internal | Code::Unavailable
537 )
538}
539
540#[derive(Debug)]
541pub enum SubscriptionError {
542 Client(ClientError),
543 ProfileNotVerified,
544 InvalidStart,
545 RequestCodec,
546 RequestTooLarge,
547 InvalidEndpoint,
548 InvalidTls,
549 InvalidMetadata,
550 Connect,
551 OpenDeadline,
552 FrameDeadline,
553 Cancelled,
554 Status(tonic::Status),
555 AcceptedAbsent,
556 UnexpectedAccepted,
557 FrameCodec,
558 NonCanonicalFrame,
559 InvalidFrame,
560 CursorRegression,
561 AuthorizationRegression,
562 DuplicateIdentityMismatch,
563 CheckpointMismatch,
564 ReconnectExhausted,
565 RandomUnavailable,
566}
567
568impl Display for SubscriptionError {
569 fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
570 formatter.write_str(match self {
571 Self::Client(_) => "Core subscription client dependency failed",
572 Self::ProfileNotVerified => "Core subscription is refused until discovery and binding are verified",
573 Self::InvalidStart => "Core subscription start is invalid",
574 Self::RequestCodec => "Core subscription start could not be encoded",
575 Self::RequestTooLarge => "Core subscription start exceeds its byte bound",
576 Self::InvalidEndpoint => "Core subscription endpoint is invalid",
577 Self::InvalidTls => "Core subscription TLS configuration is invalid",
578 Self::InvalidMetadata => "Core subscription admission metadata is invalid",
579 Self::Connect => "Core subscription connection failed",
580 Self::OpenDeadline => "Core subscription open deadline elapsed",
581 Self::FrameDeadline => "Core subscription frame deadline elapsed",
582 Self::Cancelled => "Core subscription was cancelled",
583 Self::Status(_) => "Core subscription transport returned a gRPC error",
584 Self::AcceptedAbsent => "Core subscription did not begin with an accepted checkpoint",
585 Self::UnexpectedAccepted => "Core subscription repeated its accepted checkpoint",
586 Self::FrameCodec => "Core subscription frame is malformed",
587 Self::NonCanonicalFrame => "Core subscription frame encoding is noncanonical",
588 Self::InvalidFrame => "Core subscription frame violates its current semantic contract",
589 Self::CursorRegression => "Core subscription cursor regressed",
590 Self::AuthorizationRegression => "Core subscription authorization revision regressed",
591 Self::DuplicateIdentityMismatch => "Core subscription reused a cursor with different event identity",
592 Self::CheckpointMismatch => "Core subscription resume accepted a different checkpoint",
593 Self::ReconnectExhausted => "Core subscription reconnect budget is exhausted",
594 Self::RandomUnavailable => "secure subscription request identity generation is unavailable",
595 })
596 }
597}
598
599impl Error for SubscriptionError {}
600
601#[cfg(test)]
602mod tests {
603 use super::*;
604
605 #[test]
606 fn authorization_and_policy_statuses_are_terminal() {
607 for code in [Code::Unauthenticated, Code::PermissionDenied, Code::FailedPrecondition] {
608 assert!(!reconnectable_status(&tonic::Status::new(code, "terminal")));
609 }
610 for code in [Code::Unavailable, Code::DeadlineExceeded, Code::Internal] {
611 assert!(reconnectable_status(&tonic::Status::new(code, "transient")));
612 }
613 }
614}