1use std::collections::BTreeMap;
4use std::fmt;
5use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
6use std::sync::{Arc, Mutex};
7
8use futures_util::stream;
9use riffdb_proto::{decode_application_error, decode_public_error, v1};
10use tokio::sync::{mpsc, oneshot, watch};
11use tonic::{Code, Status};
12
13use crate::status::{checked_application_status, checked_status};
14use crate::{
15 CallMetadata, ClientError, DetailsFreeStatus, OutcomeUnknown, ProtocolFailure,
16 ProtocolFailureKind, RiffDbClient, generate_request_id,
17};
18
19pub const APPLICATION_SESSION_PROTOCOL_V1: u32 = 1;
21pub const MAX_APPLICATION_SESSION_IN_FLIGHT: u16 = 128;
23const OUTBOUND_SESSION_ITEMS: usize = 257;
24
25#[derive(Clone, Copy, Debug, Eq, PartialEq)]
27pub struct ApplicationSessionConfigurationError;
28
29impl fmt::Display for ApplicationSessionConfigurationError {
30 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
31 formatter.write_str("application session identity is invalid")
32 }
33}
34
35impl std::error::Error for ApplicationSessionConfigurationError {}
36
37#[derive(Clone, Debug, Eq, PartialEq)]
39pub struct ApplicationSessionIdentity {
40 contract_lineage: String,
41 contract_version: u64,
42 contract_bundle_hash: [u8; 32],
43 query_module_hashes: Vec<[u8; 32]>,
44 application_lock_hash: [u8; 32],
45 maximum_in_flight: u16,
46}
47
48impl ApplicationSessionIdentity {
49 pub fn new(
53 contract_lineage: String,
54 contract_version: u64,
55 contract_bundle_hash: [u8; 32],
56 query_module_hashes: Vec<[u8; 32]>,
57 application_lock_hash: [u8; 32],
58 maximum_in_flight: u16,
59 ) -> Result<Self, ApplicationSessionConfigurationError> {
60 if contract_lineage.is_empty()
61 || contract_lineage.len() > 256
62 || contract_version == 0
63 || maximum_in_flight == 0
64 || maximum_in_flight > MAX_APPLICATION_SESSION_IN_FLIGHT
65 || query_module_hashes.len() > 32
66 || query_module_hashes
67 .windows(2)
68 .any(|pair| pair[0] >= pair[1])
69 {
70 return Err(ApplicationSessionConfigurationError);
71 }
72 Ok(Self {
73 contract_lineage,
74 contract_version,
75 contract_bundle_hash,
76 query_module_hashes,
77 application_lock_hash,
78 maximum_in_flight,
79 })
80 }
81
82 fn open_request(&self, request_id: Vec<u8>) -> v1::ApplicationSessionOpen {
83 v1::ApplicationSessionOpen {
84 protocol_version: APPLICATION_SESSION_PROTOCOL_V1,
85 contract: Some(riffdb_proto::app::v1::ContractSelector {
86 lineage: self.contract_lineage.clone(),
87 version: self.contract_version,
88 bundle_hash: self.contract_bundle_hash.to_vec(),
89 }),
90 query_module_hashes: self
91 .query_module_hashes
92 .iter()
93 .map(|hash| hash.to_vec())
94 .collect(),
95 application_lock_hash: self.application_lock_hash.to_vec(),
96 requested_max_in_flight: u32::from(self.maximum_in_flight),
97 request_id,
98 }
99 }
100
101 fn matches_opened(&self, opened: &v1::ApplicationSessionOpened) -> bool {
102 let Some(contract) = opened.contract.as_ref() else {
103 return false;
104 };
105 opened.protocol_version == APPLICATION_SESSION_PROTOCOL_V1
106 && contract.lineage == self.contract_lineage
107 && contract.version == self.contract_version
108 && contract.bundle_hash.as_slice() == self.contract_bundle_hash
109 && opened.application_lock_hash.as_slice() == self.application_lock_hash
110 && opened.maximum_in_flight == u32::from(self.maximum_in_flight)
111 && opened.query_module_hashes.len() == self.query_module_hashes.len()
112 && opened
113 .query_module_hashes
114 .iter()
115 .zip(&self.query_module_hashes)
116 .all(|(actual, expected)| actual.as_slice() == expected)
117 }
118}
119
120enum PendingResponse {
121 Awaiting(oneshot::Sender<v1::ApplicationSessionResponse>),
122 Ignore,
126}
127
128#[derive(Clone)]
131pub(crate) struct BoundedApplicationSession {
132 outbound: mpsc::Sender<v1::ApplicationSessionRequest>,
133 pending: Arc<Mutex<BTreeMap<u64, PendingResponse>>>,
134 closed: Arc<AtomicBool>,
135 shutdown: watch::Sender<bool>,
136 next_correlation: Arc<AtomicU64>,
137 metadata: CallMetadata,
138 maximum_in_flight: usize,
139}
140
141impl BoundedApplicationSession {
142 pub(crate) async fn open(
143 client: &mut RiffDbClient,
144 identity: ApplicationSessionIdentity,
145 metadata: CallMetadata,
146 ) -> Result<Self, ClientError> {
147 let (outbound, receiver) = mpsc::channel(OUTBOUND_SESSION_ITEMS);
148 let (shutdown, shutdown_receiver) = watch::channel(false);
149 let opening_correlation = 1_u64;
150 outbound
151 .send(v1::ApplicationSessionRequest {
152 correlation_id: opening_correlation,
153 request: Some(v1::application_session_request::Request::Open(
154 identity.open_request(
155 generate_request_id()
156 .map_err(ClientError::IdentifierGeneration)?
157 .into_bytes()
158 .to_vec(),
159 ),
160 )),
161 })
162 .await
163 .map_err(|_| ClientError::ConnectionFailure)?;
164 let outbound_stream = session_outbound_stream(receiver, shutdown_receiver);
165 let mut inbound = client
166 .open_application_session(outbound_stream, &metadata)
167 .await?;
168 let opened = inbound
169 .message()
170 .await
171 .map_err(checked_status)?
172 .ok_or(ClientError::ConnectionFailure)?;
173 let Some(v1::application_session_response::Response::Opened(opened_identity)) =
174 opened.response.as_ref()
175 else {
176 return Err(invalid_inbound());
177 };
178 if opened.correlation_id != opening_correlation || !identity.matches_opened(opened_identity)
179 {
180 return Err(invalid_inbound());
181 }
182
183 let pending = Arc::new(Mutex::new(BTreeMap::<u64, PendingResponse>::new()));
184 let response_pending = Arc::clone(&pending);
185 let closed = Arc::new(AtomicBool::new(false));
186 let response_closed = Arc::clone(&closed);
187 tokio::spawn(async move {
188 while let Ok(Some(response)) = inbound.message().await {
189 if !route_session_response(&response_pending, response) {
190 break;
191 }
192 }
193 response_closed.store(true, Ordering::Release);
194 close_session_pending(&response_pending);
195 });
196
197 Ok(Self {
198 outbound,
199 pending,
200 closed,
201 shutdown,
202 next_correlation: Arc::new(AtomicU64::new(opening_correlation + 1)),
203 metadata,
204 maximum_in_flight: usize::from(identity.maximum_in_flight),
205 })
206 }
207
208 pub(crate) fn matches_metadata(&self, metadata: &CallMetadata) -> bool {
209 self.metadata.has_same_session_scope(metadata)
210 }
211
212 pub(crate) fn close(&self) {
216 self.closed.store(true, Ordering::Release);
217 let _ = self.shutdown.send(true);
218 close_session_pending(&self.pending);
219 }
220
221 async fn call(
222 &self,
223 request: v1::application_session_request::Request,
224 ) -> Result<v1::ApplicationSessionResponse, ClientError> {
225 if self.closed.load(Ordering::Acquire) {
226 return Err(ClientError::ConnectionFailure);
227 }
228 let (sender, receiver) = oneshot::channel();
229 let correlation_id = {
230 let mut pending = self.pending.lock().map_err(|_| invalid_inbound())?;
231 if self.closed.load(Ordering::Acquire) {
232 return Err(ClientError::ConnectionFailure);
233 }
234 if pending
235 .values()
236 .filter(|pending| matches!(pending, PendingResponse::Awaiting(_)))
237 .count()
238 >= self.maximum_in_flight
239 || pending.len() >= self.maximum_in_flight.saturating_mul(2)
240 {
241 return Err(local_capacity());
242 }
243 let correlation_id = self.next_correlation.fetch_add(1, Ordering::Relaxed);
244 if correlation_id == 0 || correlation_id == u64::MAX {
245 self.closed.store(true, Ordering::Release);
246 return Err(invalid_inbound());
247 }
248 if pending
249 .insert(correlation_id, PendingResponse::Awaiting(sender))
250 .is_some()
251 {
252 self.closed.store(true, Ordering::Release);
253 return Err(invalid_inbound());
254 }
255 if self
256 .outbound
257 .try_send(v1::ApplicationSessionRequest {
258 correlation_id,
259 request: Some(request),
260 })
261 .is_err()
262 {
263 pending.remove(&correlation_id);
264 self.closed.store(true, Ordering::Release);
265 return Err(ClientError::ConnectionFailure);
266 }
267 correlation_id
268 };
269 let mut guard = PendingCallGuard {
270 correlation_id,
271 next_correlation: Arc::clone(&self.next_correlation),
272 outbound: self.outbound.clone(),
273 pending: Arc::clone(&self.pending),
274 closed: Arc::clone(&self.closed),
275 maximum_in_flight: self.maximum_in_flight,
276 armed: true,
277 };
278 let response = receiver.await;
279 guard.armed = false;
280 response.map_err(|_| ClientError::ConnectionFailure)
281 }
282
283 pub(crate) async fn execute_command(
284 &self,
285 request: v1::ExecuteCommandRequest,
286 ) -> Result<v1::ExecuteCommandResponse, ClientError> {
287 let response = self
288 .call(v1::application_session_request::Request::Command(request))
289 .await?;
290 match response.response {
291 Some(v1::application_session_response::Response::Command(response)) => Ok(response),
292 Some(v1::application_session_response::Response::Failure(failure)) => {
293 Err(session_failure(failure, SessionExpected::Command))
294 }
295 _ => Err(invalid_inbound()),
296 }
297 }
298
299 pub(crate) async fn execute_query(
300 &self,
301 request: riffdb_proto::app::v1::ExecuteQueryRequest,
302 ) -> Result<riffdb_proto::app::v1::ExecuteQueryResponse, ClientError> {
303 let response = self
304 .call(v1::application_session_request::Request::Query(request))
305 .await?;
306 match response.response {
307 Some(v1::application_session_response::Response::Query(response)) => Ok(response),
308 Some(v1::application_session_response::Response::Failure(failure)) => {
309 Err(session_failure(failure, SessionExpected::Query))
310 }
311 _ => Err(invalid_inbound()),
312 }
313 }
314}
315
316fn session_outbound_stream(
317 receiver: mpsc::Receiver<v1::ApplicationSessionRequest>,
318 shutdown: watch::Receiver<bool>,
319) -> impl futures_util::Stream<Item = v1::ApplicationSessionRequest> + Send + 'static {
320 stream::unfold(
321 (receiver, shutdown),
322 |(mut receiver, mut shutdown)| async move {
323 if *shutdown.borrow() {
324 return None;
325 }
326 tokio::select! {
327 biased;
328 changed = shutdown.changed() => {
329 let _ = changed;
330 None
331 }
332 item = receiver.recv() => item.map(|item| (item, (receiver, shutdown))),
333 }
334 },
335 )
336}
337
338fn route_session_response(
339 pending: &Mutex<BTreeMap<u64, PendingResponse>>,
340 response: v1::ApplicationSessionResponse,
341) -> bool {
342 let pending = pending
343 .lock()
344 .ok()
345 .and_then(|mut pending| pending.remove(&response.correlation_id));
346 match pending {
347 Some(PendingResponse::Awaiting(sender)) => {
348 let _ = sender.send(response);
349 true
350 }
351 Some(PendingResponse::Ignore) => true,
352 None => false,
353 }
354}
355
356fn close_session_pending(pending: &Mutex<BTreeMap<u64, PendingResponse>>) {
357 if let Ok(mut pending) = pending.lock() {
358 pending.clear();
359 }
360}
361
362struct PendingCallGuard {
363 correlation_id: u64,
364 next_correlation: Arc<AtomicU64>,
365 outbound: mpsc::Sender<v1::ApplicationSessionRequest>,
366 pending: Arc<Mutex<BTreeMap<u64, PendingResponse>>>,
367 closed: Arc<AtomicBool>,
368 maximum_in_flight: usize,
369 armed: bool,
370}
371
372impl Drop for PendingCallGuard {
373 fn drop(&mut self) {
374 if !self.armed {
375 return;
376 }
377 let retained = self.pending.lock().ok().is_some_and(|mut pending| {
378 if !pending.contains_key(&self.correlation_id) {
381 return true;
382 }
383 let cancel_correlation = self.next_correlation.fetch_add(1, Ordering::Relaxed);
384 if cancel_correlation == 0 || cancel_correlation == u64::MAX {
385 pending.remove(&self.correlation_id);
386 return false;
387 }
388 if !matches!(
389 pending.insert(self.correlation_id, PendingResponse::Ignore),
390 Some(PendingResponse::Awaiting(_))
391 ) || pending.len() >= self.maximum_in_flight.saturating_mul(2)
392 {
393 return false;
394 }
395 pending.insert(cancel_correlation, PendingResponse::Ignore);
396 if self
397 .outbound
398 .try_send(v1::ApplicationSessionRequest {
399 correlation_id: cancel_correlation,
400 request: Some(v1::application_session_request::Request::Cancel(
401 v1::ApplicationSessionCancel {
402 target_correlation_id: self.correlation_id,
403 },
404 )),
405 })
406 .is_err()
407 {
408 pending.remove(&self.correlation_id);
409 pending.remove(&cancel_correlation);
410 return false;
411 }
412 true
413 });
414 if !retained {
415 self.closed.store(true, Ordering::Release);
416 }
417 }
418}
419
420#[derive(Clone, Copy)]
421enum SessionExpected {
422 Command,
423 Query,
424}
425
426fn session_failure(
427 failure: v1::ApplicationSessionFailure,
428 expected: SessionExpected,
429) -> ClientError {
430 let expected_kind = match expected {
431 SessionExpected::Command => v1::ApplicationSessionOperationKind::Command,
432 SessionExpected::Query => v1::ApplicationSessionOperationKind::Query,
433 };
434 if v1::ApplicationSessionOperationKind::try_from(failure.operation_kind) != Ok(expected_kind) {
435 return invalid_inbound();
436 }
437 let code = Code::from_i32(failure.grpc_code);
438 if failure.details.is_empty() {
439 return match (expected, code) {
440 (SessionExpected::Command, Code::Unknown) => {
441 ClientError::OutcomeUnknown(OutcomeUnknown)
442 }
443 (_, Code::Unauthenticated) => {
444 ClientError::DetailsFree(DetailsFreeStatus::Unauthenticated)
445 }
446 (_, Code::Cancelled) => ClientError::DetailsFree(DetailsFreeStatus::Cancelled),
447 (_, Code::DeadlineExceeded) => {
448 ClientError::DetailsFree(DetailsFreeStatus::DeadlineExceeded)
449 }
450 (_, Code::Unavailable) => {
451 ClientError::DetailsFree(DetailsFreeStatus::TransportUnavailable)
452 }
453 (_, Code::Internal) => ClientError::DetailsFree(DetailsFreeStatus::EmergencyInternal),
454 _ => ClientError::ConnectionFailure,
455 };
456 }
457 match expected {
458 SessionExpected::Command => match decode_public_error(&failure.details) {
459 Ok(error) => checked_status(Status::with_details(
460 code,
461 error.safe_message(),
462 failure.details.into(),
463 )),
464 Err(_) => invalid_inbound(),
465 },
466 SessionExpected::Query => match decode_application_error(&failure.details) {
467 Ok(error) => checked_application_status(Status::with_details(
468 code,
469 error.safe_message(),
470 failure.details.into(),
471 )),
472 Err(_) => invalid_inbound(),
473 },
474 }
475}
476
477fn invalid_inbound() -> ClientError {
478 ClientError::Protocol(ProtocolFailure::new(
479 ProtocolFailureKind::InvalidInboundMessage,
480 ))
481}
482
483fn local_capacity() -> ClientError {
484 ClientError::Public(riffdb_errors::PublicError::overloaded())
485}
486
487#[cfg(test)]
488mod tests {
489 use super::*;
490
491 fn test_session(
492 maximum_in_flight: usize,
493 ) -> (
494 BoundedApplicationSession,
495 mpsc::Receiver<v1::ApplicationSessionRequest>,
496 ) {
497 let (outbound, receiver) = mpsc::channel(OUTBOUND_SESSION_ITEMS);
498 let (shutdown, _shutdown_receiver) = watch::channel(false);
499 (
500 BoundedApplicationSession {
501 outbound,
502 pending: Arc::new(Mutex::new(BTreeMap::new())),
503 closed: Arc::new(AtomicBool::new(false)),
504 shutdown,
505 next_correlation: Arc::new(AtomicU64::new(2)),
506 metadata: CallMetadata::default(),
507 maximum_in_flight,
508 },
509 receiver,
510 )
511 }
512
513 #[tokio::test]
514 async fn explicit_close_releases_pending_and_ends_the_request_stream() {
515 use futures_util::StreamExt;
516
517 let (outbound, receiver) = mpsc::channel(OUTBOUND_SESSION_ITEMS);
518 let (shutdown, shutdown_receiver) = watch::channel(false);
519 let session = BoundedApplicationSession {
520 outbound,
521 pending: Arc::new(Mutex::new(BTreeMap::new())),
522 closed: Arc::new(AtomicBool::new(false)),
523 shutdown,
524 next_correlation: Arc::new(AtomicU64::new(2)),
525 metadata: CallMetadata::default(),
526 maximum_in_flight: 1,
527 };
528 let stream = session_outbound_stream(receiver, shutdown_receiver);
529 tokio::pin!(stream);
530 session.close();
531 assert!(stream.next().await.is_none());
532 assert!(session.closed.load(Ordering::Acquire));
533 assert!(session.pending.lock().expect("pending").is_empty());
534 }
535
536 #[test]
537 fn identity_rejects_unsorted_modules_and_unbounded_inflight() {
538 let high = [9; 32];
539 let low = [8; 32];
540 assert!(
541 ApplicationSessionIdentity::new(
542 "TicketDesk".to_owned(),
543 1,
544 [1; 32],
545 vec![high, low],
546 [2; 32],
547 16,
548 )
549 .is_err()
550 );
551 assert!(
552 ApplicationSessionIdentity::new(
553 "TicketDesk".to_owned(),
554 1,
555 [1; 32],
556 vec![low, high],
557 [2; 32],
558 129,
559 )
560 .is_err()
561 );
562 }
563
564 #[tokio::test]
565 async fn responses_complete_by_correlation_not_submission_order() {
566 let (session, mut outbound) = test_session(2);
567 let first_session = session.clone();
568 let first = tokio::spawn(async move {
569 first_session
570 .execute_query(riffdb_proto::app::v1::ExecuteQueryRequest::default())
571 .await
572 });
573 let second_session = session.clone();
574 let second = tokio::spawn(async move {
575 second_session
576 .execute_query(riffdb_proto::app::v1::ExecuteQueryRequest::default())
577 .await
578 });
579 let first_request = outbound.recv().await.expect("first request");
580 let second_request = outbound.recv().await.expect("second request");
581 assert!(first_request.correlation_id < second_request.correlation_id);
582
583 for correlation_id in [second_request.correlation_id, first_request.correlation_id] {
584 assert!(route_session_response(
585 &session.pending,
586 v1::ApplicationSessionResponse {
587 correlation_id,
588 response: Some(v1::application_session_response::Response::Query(
589 riffdb_proto::app::v1::ExecuteQueryResponse::default(),
590 )),
591 },
592 ));
593 }
594 assert!(first.await.expect("first task").is_ok());
595 assert!(second.await.expect("second task").is_ok());
596 }
597
598 #[tokio::test]
599 async fn in_flight_capacity_is_bounded_and_cancellation_is_explicit() {
600 let (session, mut outbound) = test_session(1);
601 let first_session = session.clone();
602 let first = tokio::spawn(async move {
603 first_session
604 .execute_query(riffdb_proto::app::v1::ExecuteQueryRequest::default())
605 .await
606 });
607 let submitted = outbound.recv().await.expect("submitted request");
608 assert!(matches!(
609 session
610 .execute_query(riffdb_proto::app::v1::ExecuteQueryRequest::default())
611 .await,
612 Err(ClientError::Public(error))
613 if error.kind() == riffdb_errors::PublicErrorKind::Overloaded
614 ));
615
616 first.abort();
617 let _ = first.await;
618 let cancellation = outbound.recv().await.expect("cancellation request");
619 let cancellation_correlation = cancellation.correlation_id;
620 let Some(v1::application_session_request::Request::Cancel(cancellation)) =
621 cancellation.request
622 else {
623 panic!("dropped operation must send cancellation")
624 };
625 assert_eq!(cancellation.target_correlation_id, submitted.correlation_id);
626 assert!(route_session_response(
627 &session.pending,
628 v1::ApplicationSessionResponse {
629 correlation_id: submitted.correlation_id,
630 response: Some(v1::application_session_response::Response::Failure(
631 v1::ApplicationSessionFailure {
632 operation_kind: v1::ApplicationSessionOperationKind::Query as i32,
633 grpc_code: Code::Cancelled as i32,
634 details: Vec::new(),
635 },
636 )),
637 },
638 ));
639 assert!(route_session_response(
640 &session.pending,
641 v1::ApplicationSessionResponse {
642 correlation_id: cancellation_correlation,
643 response: Some(v1::application_session_response::Response::Cancellation(
644 v1::ApplicationSessionCancellation {
645 target_correlation_id: submitted.correlation_id,
646 disposition: v1::ApplicationSessionCancellationDisposition::QueryCancelled
647 as i32,
648 },
649 ),),
650 },
651 ));
652 assert!(session.pending.lock().expect("pending").is_empty());
653 }
654
655 #[tokio::test]
656 async fn cancelling_after_response_routing_preserves_the_session() {
657 let (session, mut outbound) = test_session(1);
658 let mut call =
659 Box::pin(session.execute_query(riffdb_proto::app::v1::ExecuteQueryRequest::default()));
660 assert!(futures_util::poll!(call.as_mut()).is_pending());
661 let request = outbound.recv().await.unwrap();
662 assert!(route_session_response(
663 &session.pending,
664 v1::ApplicationSessionResponse {
665 correlation_id: request.correlation_id,
666 response: None,
667 }
668 ));
669 drop(call);
671 assert!(!session.closed.load(Ordering::Acquire));
672 assert!(session.pending.lock().unwrap().is_empty());
673 assert!(matches!(
674 outbound.try_recv(),
675 Err(mpsc::error::TryRecvError::Empty)
676 ));
677 let mut next =
678 Box::pin(session.execute_query(riffdb_proto::app::v1::ExecuteQueryRequest::default()));
679 assert!(futures_util::poll!(next.as_mut()).is_pending());
680 assert!(outbound.recv().await.unwrap().correlation_id > request.correlation_id);
681 }
682
683 #[tokio::test]
684 async fn stream_loss_releases_all_pending_calls() {
685 let (session, mut outbound) = test_session(2);
686 let call_session = session.clone();
687 let call = tokio::spawn(async move {
688 call_session
689 .execute_query(riffdb_proto::app::v1::ExecuteQueryRequest::default())
690 .await
691 });
692 let _submitted = outbound.recv().await.expect("submitted request");
693 close_session_pending(&session.pending);
694 session.closed.store(true, Ordering::Release);
695 assert!(matches!(
696 call.await.expect("call task"),
697 Err(ClientError::ConnectionFailure)
698 ));
699 assert!(matches!(
700 session
701 .execute_query(riffdb_proto::app::v1::ExecuteQueryRequest::default())
702 .await,
703 Err(ClientError::ConnectionFailure)
704 ));
705 }
706
707 #[tokio::test]
708 async fn unknown_or_duplicate_response_identity_fails_closed() {
709 let (session, mut outbound) = test_session(1);
710 let call_session = session.clone();
711 let call = tokio::spawn(async move {
712 call_session
713 .execute_query(riffdb_proto::app::v1::ExecuteQueryRequest::default())
714 .await
715 });
716 let submitted = outbound.recv().await.expect("submitted request");
717 assert!(!route_session_response(
718 &session.pending,
719 v1::ApplicationSessionResponse {
720 correlation_id: submitted.correlation_id + 1,
721 response: Some(v1::application_session_response::Response::Query(
722 riffdb_proto::app::v1::ExecuteQueryResponse::default(),
723 )),
724 },
725 ));
726 close_session_pending(&session.pending);
727 session.closed.store(true, Ordering::Release);
728 assert!(matches!(
729 call.await.expect("call task"),
730 Err(ClientError::ConnectionFailure)
731 ));
732 assert!(matches!(
733 session
734 .execute_query(riffdb_proto::app::v1::ExecuteQueryRequest::default())
735 .await,
736 Err(ClientError::ConnectionFailure)
737 ));
738 }
739}