1use std::collections::{BTreeMap, BTreeSet};
4use std::fmt;
5use std::sync::atomic::{AtomicUsize, Ordering};
6use std::sync::{Arc, Mutex, OnceLock};
7use std::time::Duration;
8
9use futures_util::{StreamExt, stream};
10use riffdb_errors::{
11 ApplicationError, ApplicationErrorCode, ApplicationErrorContext, ApplicationOperation,
12 ApplicationRecoveryAction, PublicErrorKind,
13};
14use riffdb_proto::{app::v1 as app_v1, application_error_from_proto, v1};
15use tonic::transport::{Channel, Endpoint};
16
17use crate::{
18 AttemptBudget, CallMetadata, ClientError, DetailsFreeStatus, GeneratedExecutionError,
19 IdempotentCommand, RiffDbClient, generate_request_id,
20 generated::{GeneratedCommand, GeneratedQuery},
21};
22use riffdb_config::TlsClientConfig;
23
24const MAX_GENERATED_TRANSPORT_BATCH_ITEMS: usize = 16;
25pub const MAX_GENERATED_BATCH_CONCURRENCY: usize = 384;
27const MAX_STANDBY_CLIENTS: usize = 4;
28const MAX_READ_REPLICA_CLIENTS: usize = 4;
29
30const APPLICATION_CATALOG_SCHEMA_V1: &str = "riffdb.application-catalog/v1";
31
32#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
34pub enum ApplicationCatalogFeature {
35 OperationalOptionalPredicates,
37 StableCursorPages,
39 NullExistencePredicates,
41 BinaryTextPrefix,
43 UnicodeFoldTextPrefixV1,
45 ExactAggregates,
47}
48
49#[derive(Clone, Copy, Debug, Eq, PartialEq)]
51pub enum ApplicationCatalogFeatureState {
52 Available,
54 Unavailable,
56}
57
58#[derive(Clone, Copy, Debug, Eq, PartialEq)]
60pub struct ApplicationCatalogFeatureView {
61 pub feature: ApplicationCatalogFeature,
63 pub state: ApplicationCatalogFeatureState,
65}
66
67#[derive(Clone, Debug, Eq, PartialEq)]
69pub struct ApplicationCatalogPreflight {
70 pub contract_lineage: String,
72 pub contract_version: u64,
74 pub contract_bundle_hash: [u8; 32],
76 pub query_module_hashes: Vec<[u8; 32]>,
78 pub features: Vec<ApplicationCatalogFeatureView>,
80}
81
82impl ApplicationCatalogPreflight {
83 #[must_use]
85 pub fn is_available(&self, feature: ApplicationCatalogFeature) -> bool {
86 self.features.iter().any(|view| {
87 view.feature == feature && view.state == ApplicationCatalogFeatureState::Available
88 })
89 }
90}
91
92#[derive(Clone, Copy, Debug, Eq, PartialEq)]
94pub enum IdempotentTransportBatchError {
95 Empty,
97 TooManyItems,
99}
100
101impl fmt::Display for IdempotentTransportBatchError {
102 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
103 formatter.write_str(match self {
104 Self::Empty => "command transport batch is empty",
105 Self::TooManyItems => "command transport batch exceeds 16 items",
106 })
107 }
108}
109
110impl std::error::Error for IdempotentTransportBatchError {}
111
112#[derive(Clone, Debug, Eq, PartialEq)]
113struct PinnedContractIdentity {
114 database: String,
115 lineage: String,
116 version: u64,
117 bundle_hash: [u8; 32],
118}
119
120#[derive(Default)]
121struct ScriptedPeer {
122 active_contract: Mutex<Option<v1::GetActiveContractResponse>>,
123 commands: Mutex<std::collections::VecDeque<Result<v1::ExecuteCommandResponse, ClientError>>>,
124 queries: Mutex<std::collections::VecDeque<Result<app_v1::ExecuteQueryResponse, ClientError>>>,
125 recorded_command_inputs: Mutex<Vec<Option<v1::Value>>>,
126 recorded_queries: Mutex<Vec<(String, Option<u64>, bool)>>,
127}
128
129#[derive(Clone)]
130struct PeerClient {
131 inner: RiffDbClient,
132 scripted: Option<Arc<ScriptedPeer>>,
133}
134
135impl PeerClient {
136 async fn verify_active_contract(
137 &mut self,
138 expected: &PinnedContractIdentity,
139 metadata: &CallMetadata,
140 ) -> Result<(), ApplicationClientError> {
141 if expected.database.is_empty() || expected.lineage.is_empty() || expected.version == 0 {
142 return Err(ApplicationClientError::InvalidInput);
143 }
144 let request_id = Vec::from(
145 generate_request_id()
146 .map_err(|_| ApplicationClientError::IdentifierUnavailable)?
147 .into_bytes(),
148 );
149 let response = if let Some(scripted) = &self.scripted {
150 scripted
151 .active_contract
152 .lock()
153 .unwrap_or_else(std::sync::PoisonError::into_inner)
154 .clone()
155 .ok_or(ClientError::ConnectionFailure)?
156 } else {
157 self.inner
158 .get_active_contract(v1::GetActiveContractRequest { request_id }, metadata)
159 .await?
160 };
161 let Some(v1::get_active_contract_response::Result::Present(contract)) = response.result
162 else {
163 return Err(ApplicationClientError::InvalidResponse);
164 };
165 if response.database_alias != expected.database
166 || contract.contract_lineage != expected.lineage
167 || contract.contract_version != expected.version
168 || contract.bundle_hash.as_slice() != expected.bundle_hash
169 {
170 return Err(ApplicationClientError::InvalidResponse);
171 }
172 Ok(())
173 }
174
175 async fn execute_with_retry(
176 &mut self,
177 command: &IdempotentCommand,
178 attempts: AttemptBudget,
179 metadata: &CallMetadata,
180 ) -> Result<v1::ExecuteCommandResponse, ClientError> {
181 if let Some(scripted) = &self.scripted {
182 let request_id = generate_request_id().map_err(ClientError::IdentifierGeneration)?;
183 let request = command.request(request_id);
184 scripted
185 .recorded_command_inputs
186 .lock()
187 .unwrap_or_else(std::sync::PoisonError::into_inner)
188 .push(request.input);
189 return scripted
190 .commands
191 .lock()
192 .unwrap_or_else(std::sync::PoisonError::into_inner)
193 .pop_front()
194 .unwrap_or(Err(ClientError::ConnectionFailure));
195 }
196 self.inner
197 .execute_with_retry(command, attempts, metadata)
198 .await
199 }
200
201 async fn execute_named_application_query_wire(
202 &mut self,
203 query: NamedQuery,
204 metadata: &CallMetadata,
205 ) -> Result<app_v1::ExecuteQueryResponse, ApplicationClientError> {
206 if let Some(scripted) = &self.scripted {
207 scripted
208 .recorded_queries
209 .lock()
210 .unwrap_or_else(std::sync::PoisonError::into_inner)
211 .push((
212 query.name.clone(),
213 query.minimum_application_head,
214 query.admission_head_consistency,
215 ));
216 let response = scripted
217 .queries
218 .lock()
219 .unwrap_or_else(std::sync::PoisonError::into_inner)
220 .pop_front()
221 .unwrap_or(Err(ClientError::ConnectionFailure))?;
222 validate_query_response_identity(
223 &response,
224 &query.contract,
225 &query.name,
226 query.module_hash,
227 query.expected_plan_hash,
228 )?;
229 return Ok(response);
230 }
231 self.inner
232 .execute_named_application_query_wire(query, metadata)
233 .await
234 }
235}
236
237struct TopologyState {
238 promoted_active: Option<PeerClient>,
239 standby: Vec<PeerClient>,
240 pinned_contract: Option<PinnedContractIdentity>,
241}
242
243struct ClientTopology {
244 state: Mutex<TopologyState>,
245 read_replicas: Vec<PeerClient>,
246 next_replica: AtomicUsize,
247}
248
249impl ClientTopology {
250 fn lock_state(&self) -> std::sync::MutexGuard<'_, TopologyState> {
251 self.state
252 .lock()
253 .unwrap_or_else(std::sync::PoisonError::into_inner)
254 }
255}
256
257#[derive(Clone)]
264pub struct StableApplicationClient {
265 pub(crate) inner: RiffDbClient,
266 scripted: Option<Arc<ScriptedPeer>>,
267 pinned_contract: Option<PinnedContractIdentity>,
268 topology: Option<Arc<ClientTopology>>,
269}
270
271impl StableApplicationClient {
272 pub async fn connect_verified_tls(config: &TlsClientConfig) -> Result<Self, ClientError> {
275 Ok(Self {
276 inner: RiffDbClient::connect_verified_tls(config).await?,
277 scripted: None,
278 pinned_contract: None,
279 topology: None,
280 })
281 }
282
283 pub async fn connect_uri(endpoint: String) -> Result<Self, ClientError> {
286 if endpoint.is_empty() || endpoint.len() > 2_048 {
287 return Err(ClientError::ConnectionFailure);
288 }
289 let endpoint = Endpoint::from_shared(endpoint)
290 .map_err(|_| ClientError::ConnectionFailure)?
291 .connect_timeout(Duration::from_secs(10))
292 .timeout(Duration::from_secs(30));
293 Self::connect(endpoint).await
294 }
295
296 pub async fn connect(endpoint: Endpoint) -> Result<Self, ClientError> {
298 Ok(Self {
299 inner: RiffDbClient::connect(endpoint).await?,
300 scripted: None,
301 pinned_contract: None,
302 topology: None,
303 })
304 }
305
306 #[must_use]
308 pub fn from_channel(channel: Channel) -> Self {
309 Self {
310 inner: RiffDbClient::from_channel(channel),
311 scripted: None,
312 pinned_contract: None,
313 topology: None,
314 }
315 }
316
317 #[doc(hidden)]
318 #[must_use]
319 pub fn scripted_for_test(active_contract: v1::GetActiveContractResponse) -> Self {
320 let scripted = Arc::new(ScriptedPeer {
321 active_contract: Mutex::new(Some(active_contract)),
322 ..ScriptedPeer::default()
323 });
324 Self {
325 inner: RiffDbClient::from_channel(
326 Endpoint::from_static("http://127.0.0.1:9").connect_lazy(),
327 ),
328 scripted: Some(scripted),
329 pinned_contract: None,
330 topology: None,
331 }
332 }
333
334 #[doc(hidden)]
335 #[must_use]
336 pub fn push_scripted_command(
337 self,
338 result: Result<v1::ExecuteCommandResponse, ClientError>,
339 ) -> Self {
340 if let Some(scripted) = &self.scripted {
341 scripted
342 .commands
343 .lock()
344 .unwrap_or_else(std::sync::PoisonError::into_inner)
345 .push_back(result);
346 }
347 self
348 }
349
350 #[doc(hidden)]
351 #[must_use]
352 pub fn push_scripted_query(
353 self,
354 result: Result<app_v1::ExecuteQueryResponse, ClientError>,
355 ) -> Self {
356 if let Some(scripted) = &self.scripted {
357 scripted
358 .queries
359 .lock()
360 .unwrap_or_else(std::sync::PoisonError::into_inner)
361 .push_back(result);
362 }
363 self
364 }
365
366 #[doc(hidden)]
367 #[must_use]
368 pub fn scripted_command_inputs(&self) -> Vec<Option<v1::Value>> {
369 self.scripted
370 .as_ref()
371 .map(|scripted| {
372 scripted
373 .recorded_command_inputs
374 .lock()
375 .unwrap_or_else(std::sync::PoisonError::into_inner)
376 .clone()
377 })
378 .unwrap_or_default()
379 }
380
381 #[doc(hidden)]
382 #[must_use]
383 pub fn scripted_queries(&self) -> Vec<(String, Option<u64>, bool)> {
384 self.scripted
385 .as_ref()
386 .map(|scripted| {
387 scripted
388 .recorded_queries
389 .lock()
390 .unwrap_or_else(std::sync::PoisonError::into_inner)
391 .clone()
392 })
393 .unwrap_or_default()
394 }
395
396 fn peer_client(&self) -> PeerClient {
397 PeerClient {
398 inner: self.inner.clone(),
399 scripted: self.scripted.clone(),
400 }
401 }
402
403 pub fn with_standby_clients(
406 mut self,
407 standby: Vec<Self>,
408 ) -> Result<Self, ApplicationClientError> {
409 if standby.len() > MAX_STANDBY_CLIENTS {
410 return Err(ApplicationClientError::InvalidInput);
411 }
412 let standby_peers: Vec<PeerClient> = standby
413 .into_iter()
414 .map(|client| client.peer_client())
415 .collect();
416 let (promoted_active, read_replicas, pinned_contract) = match self.topology.take() {
417 Some(existing) => {
418 let state = existing.lock_state();
419 (
420 state.promoted_active.clone(),
421 existing.read_replicas.clone(),
422 state
423 .pinned_contract
424 .clone()
425 .or_else(|| self.pinned_contract.clone()),
426 )
427 }
428 None => (None, Vec::new(), self.pinned_contract.clone()),
429 };
430 if standby_peers.is_empty() && read_replicas.is_empty() && promoted_active.is_none() {
431 self.topology = None;
432 } else {
433 self.topology = Some(Arc::new(ClientTopology {
434 state: Mutex::new(TopologyState {
435 promoted_active,
436 standby: standby_peers,
437 pinned_contract,
438 }),
439 read_replicas,
440 next_replica: AtomicUsize::new(0),
441 }));
442 }
443 Ok(self)
444 }
445
446 pub fn with_read_replica_clients(
449 mut self,
450 replicas: Vec<Self>,
451 ) -> Result<Self, ApplicationClientError> {
452 if replicas.len() > MAX_READ_REPLICA_CLIENTS {
453 return Err(ApplicationClientError::InvalidInput);
454 }
455 let read_replicas: Vec<PeerClient> = replicas
456 .into_iter()
457 .map(|client| client.peer_client())
458 .collect();
459 let (promoted_active, standby, pinned_contract) = match self.topology.take() {
460 Some(existing) => {
461 let state = existing.lock_state();
462 (
463 state.promoted_active.clone(),
464 state.standby.clone(),
465 state
466 .pinned_contract
467 .clone()
468 .or_else(|| self.pinned_contract.clone()),
469 )
470 }
471 None => (None, Vec::new(), self.pinned_contract.clone()),
472 };
473 if standby.is_empty() && read_replicas.is_empty() && promoted_active.is_none() {
474 self.topology = None;
475 } else {
476 self.topology = Some(Arc::new(ClientTopology {
477 state: Mutex::new(TopologyState {
478 promoted_active,
479 standby,
480 pinned_contract,
481 }),
482 read_replicas,
483 next_replica: AtomicUsize::new(0),
484 }));
485 }
486 Ok(self)
487 }
488
489 #[must_use]
491 pub fn standby_client_count(&self) -> usize {
492 self.topology
493 .as_ref()
494 .map_or(0, |topology| topology.lock_state().standby.len())
495 }
496
497 #[must_use]
499 pub fn read_replica_client_count(&self) -> usize {
500 self.topology
501 .as_ref()
502 .map_or(0, |topology| topology.read_replicas.len())
503 }
504
505 fn sync_active_from_topology(&mut self) {
506 if let Some(topology) = &self.topology {
507 let state = topology.lock_state();
508 if let Some(promoted) = &state.promoted_active {
509 self.inner = promoted.inner.clone();
510 self.scripted = promoted.scripted.clone();
511 }
512 if self.pinned_contract.is_none() {
513 self.pinned_contract = state.pinned_contract.clone();
514 }
515 }
516 }
517
518 fn standby_candidates(&self) -> (Vec<PeerClient>, Option<PinnedContractIdentity>) {
519 let Some(topology) = &self.topology else {
520 return (Vec::new(), self.pinned_contract.clone());
521 };
522 let state = topology.lock_state();
523 (
524 state.standby.clone(),
525 state
526 .pinned_contract
527 .clone()
528 .or_else(|| self.pinned_contract.clone()),
529 )
530 }
531
532 fn promote_standby_candidate(&mut self, index: usize, promoted: PeerClient) {
533 self.inner = promoted.inner.clone();
534 self.scripted = promoted.scripted.clone();
535 if let Some(topology) = &self.topology {
536 let mut state = topology.lock_state();
537 if index < state.standby.len() {
538 state.standby.remove(index);
539 }
540 state.promoted_active = Some(promoted);
541 }
542 }
543
544 pub async fn open_bounded_session(
548 &mut self,
549 identity: crate::ApplicationSessionIdentity,
550 metadata: &CallMetadata,
551 ) -> Result<(), ApplicationClientError> {
552 self.sync_active_from_topology();
553 self.inner
554 .enable_bounded_application_session(identity, metadata.clone())
555 .await?;
556 Ok(())
557 }
558
559 #[must_use]
561 pub const fn bounded_session_enabled(&self) -> bool {
562 self.inner.bounded_application_session_enabled()
563 }
564
565 pub fn close_bounded_session(&mut self) {
569 self.inner.disable_bounded_application_session();
570 }
571
572 pub async fn verify_active_contract(
575 &mut self,
576 database: &str,
577 lineage: &str,
578 version: u64,
579 bundle_hash: [u8; 32],
580 metadata: &CallMetadata,
581 ) -> Result<(), ApplicationClientError> {
582 self.sync_active_from_topology();
583 let expected = PinnedContractIdentity {
584 database: database.to_owned(),
585 lineage: lineage.to_owned(),
586 version,
587 bundle_hash,
588 };
589 self.peer_client()
590 .verify_active_contract(&expected, metadata)
591 .await?;
592 self.pinned_contract = Some(expected.clone());
593 if let Some(topology) = &self.topology {
594 topology.lock_state().pinned_contract = Some(expected);
595 }
596 Ok(())
597 }
598
599 pub async fn preflight_application_features(
605 &mut self,
606 contract: ApplicationContract,
607 metadata: &CallMetadata,
608 ) -> Result<ApplicationCatalogPreflight, ApplicationClientError> {
609 self.sync_active_from_topology();
610 let expected = contract.clone();
611 let request_id = Vec::from(
612 generate_request_id()
613 .map_err(|_| ApplicationClientError::IdentifierUnavailable)?
614 .into_bytes(),
615 );
616 let response = self
617 .inner
618 .get_application_catalog(
619 app_v1::GetApplicationCatalogRequest {
620 contract: lower_contract(contract),
621 limit: 1,
622 cursor: None,
623 request_id,
624 },
625 metadata,
626 )
627 .await?;
628 raise_application_catalog_preflight(response, &expected)
629 }
630
631 pub async fn inspect_vector_state(
634 &mut self,
635 request: VectorStateInspection,
636 metadata: &CallMetadata,
637 ) -> Result<VectorStateInspectionResult, ApplicationClientError> {
638 self.sync_active_from_topology();
639 request.validate()?;
640 let request_id = Vec::from(
641 generate_request_id()
642 .map_err(|_| ApplicationClientError::IdentifierUnavailable)?
643 .into_bytes(),
644 );
645 let response = self
646 .inner
647 .inspect_vector_state(
648 app_v1::InspectVectorStateRequest {
649 contract: lower_contract(request.contract),
650 entity: request.entity,
651 field: request.field,
652 partition: Some(lower_value(request.partition)?),
653 kind: match request.kind {
654 VectorStateInspectionKind::Stale => {
655 app_v1::VectorStateInspectionKind::StaleEntities as i32
656 }
657 VectorStateInspectionKind::OutdatedModel => {
658 app_v1::VectorStateInspectionKind::OutdatedModelEntities as i32
659 }
660 },
661 page: Some(v1::PageRequest {
662 limit: Some(request.limit),
663 cursor: request.cursor,
664 }),
665 request_id,
666 },
667 metadata,
668 )
669 .await?;
670 VectorStateInspectionResult::try_from(response)
671 }
672
673 pub async fn execute_named_query(
675 &mut self,
676 query: NamedQuery,
677 metadata: &CallMetadata,
678 ) -> Result<NamedQueryResult, ApplicationClientError> {
679 let shared = self.execute_named_query_shared(query, metadata).await?;
680 Ok(Arc::unwrap_or_clone(shared))
681 }
682
683 #[doc(hidden)]
686 pub async fn execute_named_query_shared(
687 &mut self,
688 query: NamedQuery,
689 metadata: &CallMetadata,
690 ) -> Result<Arc<NamedQueryResult>, ApplicationClientError> {
691 let response =
692 with_client_wire_carrier(self.execute_named_query_wire(query, metadata)).await?;
693 raise_query_result_shared_from_client_wire(response)
694 }
695
696 #[doc(hidden)]
699 pub async fn execute_named_query_wire(
700 &mut self,
701 query: NamedQuery,
702 metadata: &CallMetadata,
703 ) -> Result<app_v1::ExecuteQueryResponse, ApplicationClientError> {
704 self.sync_active_from_topology();
705 if self.topology.is_none() {
706 let mut active = self.peer_client();
707 return active
708 .execute_named_application_query_wire(query, metadata)
709 .await;
710 }
711 if !query.admission_head_consistency
712 && let Some(topology) = &self.topology
713 && !topology.read_replicas.is_empty()
714 {
715 let replicas = &topology.read_replicas;
716 let start = topology.next_replica.fetch_add(1, Ordering::Relaxed);
717 let pinned = topology
718 .lock_state()
719 .pinned_contract
720 .clone()
721 .or_else(|| self.pinned_contract.clone());
722 for offset in 0..replicas.len() {
723 let mut replica = replicas[(start + offset) % replicas.len()].clone();
724 match replica
725 .execute_named_application_query_wire(query.clone(), metadata)
726 .await
727 {
728 Ok(response) => {
729 if let Some(pinned) = &pinned {
730 validate_pinned_query_response(&response, pinned)?;
731 }
732 return Ok(response);
733 }
734 Err(error) if application_query_error_allows_replica_fallback(&error) => {}
735 Err(error) => return Err(error),
736 }
737 }
738 }
739 let mut active = self.peer_client();
740 match active
741 .execute_named_application_query_wire(query.clone(), metadata)
742 .await
743 {
744 Ok(response) => Ok(response),
745 Err(primary_error) => {
746 let (standby, pinned) = self.standby_candidates();
747 if !standby.is_empty()
748 && matches!(
749 &primary_error,
750 ApplicationClientError::Client(error)
751 if client_error_triggers_standby_failover(error)
752 )
753 {
754 for (index, mut candidate) in standby.into_iter().enumerate() {
755 if let Some(pinned) = &pinned
756 && candidate
757 .verify_active_contract(pinned, metadata)
758 .await
759 .is_err()
760 {
761 continue;
762 }
763 match candidate
764 .execute_named_application_query_wire(query.clone(), metadata)
765 .await
766 {
767 Ok(response) => {
768 self.promote_standby_candidate(index, candidate);
769 return Ok(response);
770 }
771 Err(ApplicationClientError::Client(ref error))
772 if client_error_triggers_standby_failover(error) => {}
773 Err(error) => {
774 self.promote_standby_candidate(index, candidate);
775 return Err(error);
776 }
777 }
778 }
779 }
780 Err(primary_error)
781 }
782 }
783 }
784
785 pub async fn execute_generated_query<Q: GeneratedQuery>(
787 &mut self,
788 query: Q,
789 options: QueryOptions,
790 metadata: &CallMetadata,
791 ) -> Result<TypedQueryResult<Q::Output>, ApplicationClientError> {
792 let query = query.named_query(options)?;
793 let response = self.execute_named_query_wire(query, metadata).await?;
794 let application_head = response.application_head;
795 let next_cursor = response.next_cursor.clone();
796 let identity = query_response_identity(&response)?;
797 let value = match app_v1::NamedResultEncoding::try_from(response.selected_result_encoding) {
798 Ok(app_v1::NamedResultEncoding::CompactV1) => {
799 if !response.fields.is_empty() {
800 return Err(ApplicationClientError::InvalidResponse);
801 }
802 Q::decode_compact_result(
803 response.outcome,
804 response
805 .compact_result
806 .ok_or(ApplicationClientError::InvalidResponse)?,
807 )?
808 }
809 Ok(app_v1::NamedResultEncoding::PackedV1) => {
810 if !response.fields.is_empty() || response.compact_result.is_some() {
811 return Err(ApplicationClientError::InvalidResponse);
812 }
813 Q::decode_packed_result(
814 response.outcome,
815 response
816 .packed_result
817 .ok_or(ApplicationClientError::InvalidResponse)?,
818 )?
819 }
820 Ok(app_v1::NamedResultEncoding::LegacyRecords)
821 | Ok(app_v1::NamedResultEncoding::Unspecified) => {
822 Q::decode_result(raise_query_result(response)?)?
823 }
824 Err(_) => return Err(ApplicationClientError::InvalidResponse),
825 };
826 Ok(TypedQueryResult {
827 value,
828 identity,
829 application_head,
830 next_cursor,
831 })
832 }
833
834 pub async fn execute_command(
836 &mut self,
837 command: ApplicationCommand,
838 attempts: AttemptBudget,
839 metadata: &CallMetadata,
840 ) -> Result<ApplicationCommandResult, ApplicationClientError> {
841 self.sync_active_from_topology();
842 let command_name = command.name.clone();
843 let idempotent = command.into_idempotent_command()?;
844 let mut active = self.peer_client();
845 match active
846 .execute_with_retry(&idempotent, attempts, metadata)
847 .await
848 {
849 Ok(response) => raise_application_command_result(response),
850 Err(primary_error) => {
851 let (standby, pinned) = self.standby_candidates();
852 if !standby.is_empty() && client_error_triggers_standby_failover(&primary_error) {
853 for (index, mut candidate) in standby.into_iter().enumerate() {
854 if let Some(pinned) = &pinned
855 && candidate
856 .verify_active_contract(pinned, metadata)
857 .await
858 .is_err()
859 {
860 continue;
861 }
862 match candidate
863 .execute_with_retry(&idempotent, attempts, metadata)
864 .await
865 {
866 Ok(response) => {
867 self.promote_standby_candidate(index, candidate);
868 return raise_application_command_result(response);
869 }
870 Err(error) if client_error_triggers_standby_failover(&error) => {}
871 Err(error) => {
872 self.promote_standby_candidate(index, candidate);
873 return Err(ApplicationClientError::Client(
874 contextualize_command_client_error(error, &command_name),
875 ));
876 }
877 }
878 }
879 }
880 Err(ApplicationClientError::Client(
881 contextualize_command_client_error(primary_error, &command_name),
882 ))
883 }
884 }
885 }
886
887 pub async fn execute_generated_command<C: GeneratedCommand>(
890 &mut self,
891 command: &C,
892 attempts: AttemptBudget,
893 metadata: &CallMetadata,
894 ) -> Result<TypedCommandResult<C::Outcome>, GeneratedExecutionError> {
895 self.sync_active_from_topology();
896 let execution = match self
897 .inner
898 .execute_generated_with_recovery(command, attempts, metadata)
899 .await
900 {
901 Ok(execution) => execution,
902 Err(primary_error) => {
903 let (standby, pinned) = self.standby_candidates();
904 let mut promoted_execution = None;
905 if !standby.is_empty()
906 && matches!(
907 &primary_error,
908 GeneratedExecutionError::Client(error)
909 if client_error_triggers_standby_failover(error)
910 )
911 {
912 for (index, mut candidate) in standby.into_iter().enumerate() {
913 if let Some(pinned) = &pinned
914 && candidate
915 .verify_active_contract(pinned, metadata)
916 .await
917 .is_err()
918 {
919 continue;
920 }
921 match candidate
922 .inner
923 .execute_generated_with_recovery(command, attempts, metadata)
924 .await
925 {
926 Ok(execution) => {
927 self.promote_standby_candidate(index, candidate);
928 promoted_execution = Some(Ok(execution));
929 break;
930 }
931 Err(GeneratedExecutionError::Client(ref error))
932 if client_error_triggers_standby_failover(error) => {}
933 Err(error) => {
934 self.promote_standby_candidate(index, candidate);
935 promoted_execution = Some(Err(error));
936 break;
937 }
938 }
939 }
940 }
941 match promoted_execution {
942 Some(result) => result?,
943 None => return Err(primary_error),
944 }
945 }
946 };
947 let (outcome, response) = execution.into_parts();
948 let workflow_revisions = command
949 .workflow_successor_revisions(&response)
950 .map_err(GeneratedExecutionError::CommandShape)?;
951 Ok(TypedCommandResult {
952 outcome,
953 commit_sequence: (response.commit_sequence != 0).then_some(response.commit_sequence),
954 contract_version: response.contract_version,
955 plan_hash: response.plan_hash.try_into().map_err(|_| {
956 GeneratedExecutionError::CommandShape(
957 crate::generated::GeneratedCommandError::InvalidOutcomeShape,
958 )
959 })?,
960 replayed: response.status
961 == v1::execute_command_response::CompletionStatus::Replayed as i32,
962 outcome_uri: response.outcome_uri,
963 workflow_revisions,
964 })
965 }
966
967 pub async fn execute_generated_command_batch<C>(
970 &self,
971 commands: Vec<C>,
972 options: GeneratedBatchOptions,
973 attempts: AttemptBudget,
974 metadata: &CallMetadata,
975 ) -> Result<GeneratedBatchResult<C::Outcome>, GeneratedBatchError>
976 where
977 C: GeneratedCommand + Clone,
978 {
979 self.execute_generated_command_batch_with_progress(
980 commands,
981 options,
982 attempts,
983 metadata,
984 |_| {},
985 )
986 .await
987 }
988
989 pub async fn execute_generated_command_batch_with_progress<C, F>(
992 &self,
993 commands: Vec<C>,
994 options: GeneratedBatchOptions,
995 attempts: AttemptBudget,
996 metadata: &CallMetadata,
997 mut report_progress: F,
998 ) -> Result<GeneratedBatchResult<C::Outcome>, GeneratedBatchError>
999 where
1000 C: GeneratedCommand + Clone,
1001 F: FnMut(GeneratedBatchProgress),
1002 {
1003 options.validate(commands.len())?;
1004 let total = commands.len();
1005 let (transport_batch_size, transport_concurrency) =
1006 generated_transport_batch_policy(options.concurrency);
1007 let mut chunks = Vec::new();
1008 let mut chunk = Vec::with_capacity(transport_batch_size);
1009 for item in commands.into_iter().enumerate().skip(options.checkpoint) {
1010 chunk.push(item);
1011 if chunk.len() == transport_batch_size {
1012 chunks.push(std::mem::replace(
1013 &mut chunk,
1014 Vec::with_capacity(transport_batch_size),
1015 ));
1016 }
1017 }
1018 if !chunk.is_empty() {
1019 chunks.push(chunk);
1020 }
1021 let mut pending = stream::iter(chunks.into_iter().map(|chunk| {
1022 let client = self.clone();
1023 let metadata = metadata.clone();
1024 async move {
1025 client
1026 .execute_generated_transport_batch(chunk, attempts, &metadata)
1027 .await
1028 }
1029 }))
1030 .buffer_unordered(transport_concurrency);
1031 let mut items = Vec::with_capacity(total - options.checkpoint);
1032 let mut progress_state = BatchCheckpointState::new(options.checkpoint);
1033 while let Some(completed_chunk) = pending.next().await {
1034 for (index, result) in completed_chunk {
1035 record_generated_batch_item_completion(
1036 index,
1037 result,
1038 total,
1039 &mut progress_state,
1040 &mut items,
1041 &mut report_progress,
1042 );
1043 }
1044 }
1045 items.sort_by_key(|item| item.index);
1046 Ok(GeneratedBatchResult {
1047 items,
1048 checkpoint: progress_state.checkpoint,
1049 })
1050 }
1051
1052 async fn execute_generated_transport_batch<C: GeneratedCommand + Clone>(
1053 &self,
1054 commands: Vec<(usize, C)>,
1055 attempts: AttemptBudget,
1056 metadata: &CallMetadata,
1057 ) -> Vec<(
1058 usize,
1059 Result<TypedCommandResult<C::Outcome>, GeneratedExecutionError>,
1060 )> {
1061 let mut requests = Vec::with_capacity(commands.len());
1062 let mut prepared = Vec::with_capacity(commands.len());
1063 let mut completed = Vec::new();
1064 for (index, command) in commands {
1065 let generic = match command.idempotent_command() {
1066 Ok(generic) => generic,
1067 Err(error) => {
1068 completed.push((index, Err(GeneratedExecutionError::CommandShape(error))));
1069 continue;
1070 }
1071 };
1072 let request_id = match generate_request_id() {
1073 Ok(request_id) => request_id,
1074 Err(error) => {
1075 completed.push((
1076 index,
1077 Err(GeneratedExecutionError::Client(
1078 ClientError::IdentifierGeneration(error),
1079 )),
1080 ));
1081 continue;
1082 }
1083 };
1084 requests.push(generic.request(request_id));
1085 prepared.push((index, command));
1086 }
1087 if prepared.is_empty() {
1088 return completed;
1089 }
1090
1091 let mut client = self.inner.clone();
1092 let response = client
1093 .execute_batch(
1094 v1::ExecuteCommandBatchRequest { commands: requests },
1095 metadata,
1096 )
1097 .await;
1098 let Ok(response) = response else {
1099 let recovered: Vec<_> = stream::iter(prepared.into_iter().map(|(index, command)| {
1102 let mut client = self.clone();
1103 let metadata = metadata.clone();
1104 async move {
1105 let result = client
1106 .execute_generated_command(&command, attempts, &metadata)
1107 .await;
1108 (index, result)
1109 }
1110 }))
1111 .buffer_unordered(MAX_GENERATED_TRANSPORT_BATCH_ITEMS)
1112 .collect()
1113 .await;
1114 completed.extend(recovered);
1115 return completed;
1116 };
1117
1118 if !response.items.is_empty() {
1121 let recovered =
1122 complete_batch_items_with_reentry(prepared, response.items, |index, command| {
1123 let mut client = self.clone();
1124 let metadata = metadata.clone();
1125 async move {
1126 let result = client
1127 .execute_generated_command(&command, attempts, &metadata)
1128 .await;
1129 (index, result)
1130 }
1131 })
1132 .await;
1133 completed.extend(recovered);
1134 return completed;
1135 }
1136
1137 completed.extend(prepared.into_iter().zip(response.responses).map(
1139 |((index, command), response)| {
1140 let outcome = command
1141 .decode_outcome(&response)
1142 .map_err(GeneratedExecutionError::CommandShape);
1143 (
1144 index,
1145 outcome.and_then(|outcome| typed_command_result(&command, outcome, response)),
1146 )
1147 },
1148 ));
1149 completed
1150 }
1151}
1152
1153impl RiffDbClient {
1154 pub async fn execute_idempotent_transport_batch_with_retry(
1163 &self,
1164 commands: Vec<IdempotentCommand>,
1165 attempts: AttemptBudget,
1166 metadata: &CallMetadata,
1167 ) -> Result<Vec<Result<v1::ExecuteCommandResponse, ClientError>>, IdempotentTransportBatchError>
1168 {
1169 validate_idempotent_transport_batch_len(commands.len())?;
1170
1171 let mut requests = Vec::with_capacity(commands.len());
1172 for command in &commands {
1173 let request_id = match generate_request_id() {
1174 Ok(request_id) => request_id,
1175 Err(_) => {
1176 return Ok(
1180 reenter_idempotent_commands(self, commands, attempts, metadata).await,
1181 );
1182 }
1183 };
1184 requests.push(command.request(request_id));
1185 }
1186
1187 let mut client = self.clone();
1188 let response = client
1189 .execute_batch(
1190 v1::ExecuteCommandBatchRequest { commands: requests },
1191 metadata,
1192 )
1193 .await;
1194 let Ok(response) = response else {
1195 return Ok(reenter_idempotent_commands(self, commands, attempts, metadata).await);
1196 };
1197
1198 if response.items.is_empty() {
1199 return Ok(response.responses.into_iter().map(Ok).collect());
1200 }
1201
1202 let mut resolved = Vec::with_capacity(commands.len());
1203 let mut reenter = Vec::new();
1204 for (index, (command, item)) in commands.into_iter().zip(response.items).enumerate() {
1205 match item.result {
1206 Some(v1::execute_command_batch_item::Result::Response(response)) => {
1207 resolved.push((index, Ok(response)));
1208 }
1209 Some(v1::execute_command_batch_item::Result::Error(error_wire)) => {
1210 match application_error_from_proto(&error_wire) {
1211 Ok(error) if application_error_requires_reentry(&error) => {
1212 reenter.push((index, command));
1213 }
1214 Ok(error) => {
1215 resolved.push((index, Err(ClientError::Application(Box::new(error)))));
1216 }
1217 Err(_) => reenter.push((index, command)),
1218 }
1219 }
1220 None => reenter.push((index, command)),
1221 }
1222 }
1223
1224 if !reenter.is_empty() {
1225 let recovered: Vec<_> = stream::iter(reenter.into_iter().map(|(index, command)| {
1226 let mut client = self.clone();
1227 let metadata = metadata.clone();
1228 async move {
1229 let result = client
1230 .execute_with_retry(&command, attempts, &metadata)
1231 .await;
1232 (index, result)
1233 }
1234 }))
1235 .buffer_unordered(MAX_GENERATED_TRANSPORT_BATCH_ITEMS)
1236 .collect()
1237 .await;
1238 resolved.extend(recovered);
1239 }
1240 resolved.sort_by_key(|(index, _)| *index);
1241 Ok(resolved.into_iter().map(|(_, result)| result).collect())
1242 }
1243}
1244
1245const fn validate_idempotent_transport_batch_len(
1246 item_count: usize,
1247) -> Result<(), IdempotentTransportBatchError> {
1248 if item_count == 0 {
1249 return Err(IdempotentTransportBatchError::Empty);
1250 }
1251 if item_count > MAX_GENERATED_TRANSPORT_BATCH_ITEMS {
1252 return Err(IdempotentTransportBatchError::TooManyItems);
1253 }
1254 Ok(())
1255}
1256
1257async fn reenter_idempotent_commands(
1258 client: &RiffDbClient,
1259 commands: Vec<IdempotentCommand>,
1260 attempts: AttemptBudget,
1261 metadata: &CallMetadata,
1262) -> Vec<Result<v1::ExecuteCommandResponse, ClientError>> {
1263 stream::iter(commands.into_iter().map(|command| {
1264 let mut client = client.clone();
1265 let metadata = metadata.clone();
1266 async move {
1267 client
1268 .execute_with_retry(&command, attempts, &metadata)
1269 .await
1270 }
1271 }))
1272 .buffered(MAX_GENERATED_TRANSPORT_BATCH_ITEMS)
1273 .collect()
1274 .await
1275}
1276
1277type ResolvedBatchItem<T> = (
1278 usize,
1279 Result<TypedCommandResult<T>, GeneratedExecutionError>,
1280);
1281
1282struct ReenterCandidate<C> {
1284 index: usize,
1285 command: C,
1286 protocol_failure: Option<crate::ProtocolFailure>,
1290}
1291
1292type BatchItemResolution<C> = (
1293 Vec<ResolvedBatchItem<<C as GeneratedCommand>::Outcome>>,
1294 Vec<ReenterCandidate<C>>,
1295);
1296
1297struct BatchCheckpointState {
1299 completed: usize,
1300 checkpoint: usize,
1301 completed_after_checkpoint: BTreeSet<usize>,
1302}
1303
1304impl BatchCheckpointState {
1305 fn new(checkpoint: usize) -> Self {
1307 Self {
1308 completed: checkpoint,
1309 checkpoint,
1310 completed_after_checkpoint: BTreeSet::new(),
1311 }
1312 }
1313}
1314
1315fn record_generated_batch_item_completion<T, F>(
1320 index: usize,
1321 result: Result<TypedCommandResult<T>, GeneratedExecutionError>,
1322 total: usize,
1323 state: &mut BatchCheckpointState,
1324 items: &mut Vec<GeneratedBatchItem<T>>,
1325 report_progress: &mut F,
1326) where
1327 F: FnMut(GeneratedBatchProgress),
1328{
1329 state.completed += 1;
1330 state.completed_after_checkpoint.insert(index);
1331 while state.completed_after_checkpoint.remove(&state.checkpoint) {
1332 state.checkpoint += 1;
1333 }
1334 items.push(GeneratedBatchItem { index, result });
1335 report_progress(GeneratedBatchProgress {
1336 completed: state.completed,
1337 total,
1338 checkpoint: state.checkpoint,
1339 });
1340}
1341
1342fn resolve_generated_batch_items<C: GeneratedCommand>(
1360 prepared: Vec<(usize, C)>,
1361 items: Vec<v1::ExecuteCommandBatchItem>,
1362) -> BatchItemResolution<C> {
1363 let mut resolved = Vec::with_capacity(prepared.len());
1364 let mut reenter = Vec::new();
1365 for ((index, command), item) in prepared.into_iter().zip(items) {
1366 match item.result {
1367 Some(v1::execute_command_batch_item::Result::Response(item_response)) => {
1368 let outcome = command
1369 .decode_outcome(&item_response)
1370 .map_err(GeneratedExecutionError::CommandShape);
1371 resolved.push((
1372 index,
1373 outcome
1374 .and_then(|outcome| typed_command_result(&command, outcome, item_response)),
1375 ));
1376 }
1377 Some(v1::execute_command_batch_item::Result::Error(error_wire)) => {
1378 match application_error_from_proto(&error_wire) {
1379 Ok(error) if application_error_requires_reentry(&error) => {
1380 reenter.push(ReenterCandidate {
1381 index,
1382 command,
1383 protocol_failure: None,
1384 });
1385 }
1386 Ok(error) => {
1387 resolved.push((
1388 index,
1389 Err(GeneratedExecutionError::Client(ClientError::Application(
1390 Box::new(error),
1391 ))),
1392 ));
1393 }
1394 Err(_) => {
1395 reenter.push(ReenterCandidate {
1396 index,
1397 command,
1398 protocol_failure: Some(crate::ProtocolFailure::new(
1399 crate::ProtocolFailureKind::InvalidApplicationErrorDetails,
1400 )),
1401 });
1402 }
1403 }
1404 }
1405 None => {
1406 reenter.push(ReenterCandidate {
1407 index,
1408 command,
1409 protocol_failure: Some(crate::ProtocolFailure::new(
1410 crate::ProtocolFailureKind::InvalidInboundMessage,
1411 )),
1412 });
1413 }
1414 }
1415 }
1416 (resolved, reenter)
1417}
1418
1419async fn complete_batch_items_with_reentry<C, F, Fut>(
1421 prepared: Vec<(usize, C)>,
1422 items: Vec<v1::ExecuteCommandBatchItem>,
1423 reenter_one: F,
1424) -> Vec<ResolvedBatchItem<C::Outcome>>
1425where
1426 C: GeneratedCommand + Clone,
1427 F: Fn(usize, C) -> Fut,
1428 Fut: std::future::Future<
1429 Output = (
1430 usize,
1431 Result<TypedCommandResult<C::Outcome>, GeneratedExecutionError>,
1432 ),
1433 >,
1434{
1435 let (mut resolved, reenter) = resolve_generated_batch_items(prepared, items);
1436 if !reenter.is_empty() {
1437 let recovered: Vec<_> = stream::iter(reenter.into_iter().map(|candidate| {
1438 let protocol_failure = candidate.protocol_failure;
1439 let reenter_one = &reenter_one;
1440 async move {
1441 let (index, result) = reenter_one(candidate.index, candidate.command).await;
1442 let result = match (result, protocol_failure) {
1443 (Err(_), Some(failure)) => Err(GeneratedExecutionError::Client(
1444 ClientError::Protocol(failure),
1445 )),
1446 (other, _) => other,
1447 };
1448 (index, result)
1449 }
1450 }))
1451 .buffer_unordered(MAX_GENERATED_TRANSPORT_BATCH_ITEMS)
1452 .collect()
1453 .await;
1454 resolved.extend(recovered);
1455 }
1456 resolved
1457}
1458
1459const fn application_error_requires_reentry(error: &ApplicationError) -> bool {
1460 matches!(
1461 error.recovery_action(),
1462 ApplicationRecoveryAction::Retry | ApplicationRecoveryAction::ResolveWithSameIdempotencyKey
1463 )
1464}
1465
1466fn generated_transport_batch_policy(item_concurrency: usize) -> (usize, usize) {
1467 let transport_concurrency = item_concurrency.div_ceil(MAX_GENERATED_TRANSPORT_BATCH_ITEMS);
1468 let transport_batch_size = item_concurrency / transport_concurrency;
1469 (transport_batch_size, transport_concurrency)
1470}
1471
1472pub(crate) fn typed_command_result<C: GeneratedCommand<Outcome = T>, T>(
1473 command: &C,
1474 outcome: T,
1475 response: v1::ExecuteCommandResponse,
1476) -> Result<TypedCommandResult<T>, GeneratedExecutionError> {
1477 let workflow_revisions = command
1478 .workflow_successor_revisions(&response)
1479 .map_err(GeneratedExecutionError::CommandShape)?;
1480 let plan_hash = response.plan_hash.try_into().map_err(|_| {
1481 GeneratedExecutionError::CommandShape(
1482 crate::generated::GeneratedCommandError::InvalidOutcomeShape,
1483 )
1484 })?;
1485 Ok(TypedCommandResult {
1486 outcome,
1487 commit_sequence: (response.commit_sequence != 0).then_some(response.commit_sequence),
1488 contract_version: response.contract_version,
1489 plan_hash,
1490 replayed: response.status
1491 == v1::execute_command_response::CompletionStatus::Replayed as i32,
1492 outcome_uri: response.outcome_uri,
1493 workflow_revisions,
1494 })
1495}
1496
1497#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1499pub struct GeneratedBatchOptions {
1500 pub concurrency: usize,
1502 pub checkpoint: usize,
1504}
1505
1506impl GeneratedBatchOptions {
1507 pub fn new(concurrency: usize) -> Result<Self, GeneratedBatchError> {
1509 let value = Self {
1510 concurrency,
1511 checkpoint: 0,
1512 };
1513 value.validate(1)?;
1514 Ok(value)
1515 }
1516
1517 #[must_use]
1519 pub const fn with_checkpoint(mut self, checkpoint: usize) -> Self {
1520 self.checkpoint = checkpoint;
1521 self
1522 }
1523
1524 fn validate(self, item_count: usize) -> Result<(), GeneratedBatchError> {
1525 if self.concurrency == 0
1526 || self.concurrency > MAX_GENERATED_BATCH_CONCURRENCY
1527 || item_count == 0
1528 || item_count > 4_096
1529 || self.checkpoint > item_count
1530 {
1531 return Err(GeneratedBatchError::InvalidBounds);
1532 }
1533 Ok(())
1534 }
1535}
1536
1537pub struct GeneratedBatchItem<T> {
1539 pub index: usize,
1541 pub result: Result<TypedCommandResult<T>, GeneratedExecutionError>,
1543}
1544
1545#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1547pub struct GeneratedBatchProgress {
1548 pub completed: usize,
1550 pub total: usize,
1552 pub checkpoint: usize,
1554}
1555
1556pub struct GeneratedBatchResult<T> {
1558 pub items: Vec<GeneratedBatchItem<T>>,
1560 pub checkpoint: usize,
1562}
1563
1564#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1566pub enum GeneratedBatchError {
1567 InvalidBounds,
1569}
1570
1571impl fmt::Display for GeneratedBatchError {
1572 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1573 formatter.write_str("generated command batch bounds are invalid")
1574 }
1575}
1576
1577impl std::error::Error for GeneratedBatchError {}
1578
1579#[derive(Debug)]
1587pub struct ApplicationUuid {
1588 bytes: [u8; 16],
1589 text: OnceLock<String>,
1590}
1591
1592impl Clone for ApplicationUuid {
1593 fn clone(&self) -> Self {
1594 let text = OnceLock::new();
1595 if let Some(existing) = self.text.get() {
1596 let _ = text.set(existing.clone());
1597 }
1598 Self {
1599 bytes: self.bytes,
1600 text,
1601 }
1602 }
1603}
1604
1605impl ApplicationUuid {
1606 #[must_use]
1608 pub fn from_bytes(bytes: [u8; 16]) -> Self {
1609 Self {
1610 bytes,
1611 text: OnceLock::new(),
1612 }
1613 }
1614
1615 pub fn from_text(text: impl Into<String>) -> Result<Self, ApplicationClientError> {
1617 let text = text.into();
1618 let bytes = parse_uuid(&text)?;
1619 let cell = OnceLock::new();
1620 let _ = cell.set(text);
1621 Ok(Self { bytes, text: cell })
1622 }
1623
1624 #[must_use]
1626 pub const fn as_bytes(&self) -> &[u8; 16] {
1627 &self.bytes
1628 }
1629
1630 #[must_use]
1632 pub fn as_str(&self) -> &str {
1633 self.text
1634 .get_or_init(|| format_uuid_text(&self.bytes))
1635 .as_str()
1636 }
1637
1638 #[must_use]
1640 pub fn into_string(self) -> String {
1641 match self.text.into_inner() {
1642 Some(text) => text,
1643 None => format_uuid_text(&self.bytes),
1644 }
1645 }
1646
1647 #[must_use]
1649 pub fn text_is_materialized(&self) -> bool {
1650 self.text.get().is_some()
1651 }
1652}
1653
1654impl PartialEq for ApplicationUuid {
1655 fn eq(&self, other: &Self) -> bool {
1656 self.bytes == other.bytes
1657 }
1658}
1659
1660impl Eq for ApplicationUuid {}
1661
1662impl From<[u8; 16]> for ApplicationUuid {
1663 fn from(bytes: [u8; 16]) -> Self {
1664 Self::from_bytes(bytes)
1665 }
1666}
1667
1668#[derive(Clone, Debug, Eq, PartialEq)]
1670pub enum ApplicationValue {
1671 Null,
1673 Bool(bool),
1675 I64(i64),
1677 U64(u64),
1679 Decimal {
1681 coefficient_twos_complement: Vec<u8>,
1683 scale: u32,
1685 precision: Option<u32>,
1687 },
1688 Money {
1690 currency: String,
1692 amount: Box<Self>,
1694 },
1695 String(String),
1697 Uuid(ApplicationUuid),
1699 Enum(String),
1701 EnumIdentity {
1703 type_id: u32,
1705 variant_id: u32,
1707 name: String,
1709 },
1710 Bytes(Vec<u8>),
1712 Date(i32),
1714 Timestamp {
1716 seconds: i64,
1718 nanos: u32,
1720 },
1721 Vector(riffdb_types::CanonicalVector),
1723 List(Vec<Self>),
1725 Record(BTreeMap<String, Self>),
1727}
1728
1729#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1731pub enum VectorStateInspectionKind {
1732 Stale,
1734 OutdatedModel,
1736}
1737
1738#[derive(Clone, Debug, Eq, PartialEq)]
1740pub struct VectorStateInspection {
1741 contract: ApplicationContract,
1742 entity: String,
1743 field: String,
1744 partition: ApplicationValue,
1745 kind: VectorStateInspectionKind,
1746 limit: u32,
1747 cursor: Option<Vec<u8>>,
1748}
1749
1750impl VectorStateInspection {
1751 #[must_use]
1753 pub fn new(
1754 contract: ApplicationContract,
1755 entity: impl Into<String>,
1756 field: impl Into<String>,
1757 partition: ApplicationValue,
1758 kind: VectorStateInspectionKind,
1759 limit: u32,
1760 ) -> Self {
1761 Self {
1762 contract,
1763 entity: entity.into(),
1764 field: field.into(),
1765 partition,
1766 kind,
1767 limit,
1768 cursor: None,
1769 }
1770 }
1771
1772 #[must_use]
1774 pub fn after(mut self, cursor: Vec<u8>) -> Self {
1775 self.cursor = Some(cursor);
1776 self
1777 }
1778
1779 fn validate(&self) -> Result<(), ApplicationClientError> {
1780 validate_contract(&self.contract)?;
1781 if self.entity.is_empty()
1782 || self.entity.len() > 256
1783 || self.field.is_empty()
1784 || self.field.len() > 256
1785 || self.limit == 0
1786 || self.limit > 500
1787 || self
1788 .cursor
1789 .as_ref()
1790 .is_some_and(|cursor| cursor.len() != 16)
1791 {
1792 return Err(ApplicationClientError::InvalidInput);
1793 }
1794 Ok(())
1795 }
1796}
1797
1798#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1800pub struct VectorStalenessSummary {
1801 pub total_entities: u64,
1803 pub stale_count: u64,
1805 pub stale_entity_count_threshold: u64,
1807 pub slo_breached: bool,
1809}
1810
1811#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1813pub struct VectorModelVersionSummary {
1814 pub current_count: u64,
1816 pub outdated_count: u64,
1818}
1819
1820#[derive(Clone, Eq, PartialEq)]
1822pub struct VectorStalenessItem {
1823 pub entity_key: Vec<u8>,
1825 pub newest_source_write: u64,
1827 pub embedding_write: Option<u64>,
1829}
1830
1831impl fmt::Debug for VectorStalenessItem {
1832 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1833 formatter
1834 .debug_struct("VectorStalenessItem")
1835 .field("entity_key", &"[REDACTED]")
1836 .field("newest_source_write", &self.newest_source_write)
1837 .field("embedding_write", &self.embedding_write)
1838 .finish()
1839 }
1840}
1841
1842#[derive(Clone, Eq, PartialEq)]
1844pub struct VectorModelVersionItem {
1845 pub entity_key: Vec<u8>,
1847 pub model: String,
1849 pub model_version: String,
1851 pub embedding_write: u64,
1853}
1854
1855impl fmt::Debug for VectorModelVersionItem {
1856 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1857 formatter
1858 .debug_struct("VectorModelVersionItem")
1859 .field("entity_key", &"[REDACTED]")
1860 .field("model", &"[REDACTED]")
1861 .field("model_version", &"[REDACTED]")
1862 .field("embedding_write", &self.embedding_write)
1863 .finish()
1864 }
1865}
1866
1867#[derive(Clone, Debug, Eq, PartialEq)]
1869pub struct VectorInspectionPage<T> {
1870 pub items: Vec<T>,
1872 pub next_cursor: Option<Vec<u8>>,
1874 pub observed_frontier: Option<u64>,
1876}
1877
1878#[derive(Clone, Debug, Eq, PartialEq)]
1881pub enum VectorStateInspectionResult {
1882 StalenessSummary(VectorStalenessSummary),
1884 StaleEntities(VectorInspectionPage<VectorStalenessItem>),
1886 ModelVersionSummary(VectorModelVersionSummary),
1888 OutdatedModelEntities(VectorInspectionPage<VectorModelVersionItem>),
1890}
1891
1892impl TryFrom<app_v1::InspectVectorStateResponse> for VectorStateInspectionResult {
1893 type Error = ApplicationClientError;
1894
1895 fn try_from(response: app_v1::InspectVectorStateResponse) -> Result<Self, Self::Error> {
1896 use app_v1::inspect_vector_state_response::Result;
1897 Ok(
1898 match response
1899 .result
1900 .ok_or(ApplicationClientError::InvalidResponse)?
1901 {
1902 Result::StalenessSummary(report) => {
1903 Self::StalenessSummary(VectorStalenessSummary {
1904 total_entities: report.total_entities,
1905 stale_count: report.stale_count,
1906 stale_entity_count_threshold: report.stale_entity_count_threshold,
1907 slo_breached: report.slo_breached,
1908 })
1909 }
1910 Result::StaleEntities(page) => Self::StaleEntities(VectorInspectionPage {
1911 items: page
1912 .items
1913 .into_iter()
1914 .map(|item| VectorStalenessItem {
1915 entity_key: item.entity_key,
1916 newest_source_write: item.newest_source_write,
1917 embedding_write: item.embedding_write,
1918 })
1919 .collect(),
1920 next_cursor: page.next_cursor,
1921 observed_frontier: page.observed_frontier,
1922 }),
1923 Result::ModelVersionSummary(report) => {
1924 Self::ModelVersionSummary(VectorModelVersionSummary {
1925 current_count: report.current_count,
1926 outdated_count: report.outdated_count,
1927 })
1928 }
1929 Result::OutdatedModelEntities(page) => {
1930 Self::OutdatedModelEntities(VectorInspectionPage {
1931 items: page
1932 .items
1933 .into_iter()
1934 .map(|item| VectorModelVersionItem {
1935 entity_key: item.entity_key,
1936 model: item.model,
1937 model_version: item.model_version,
1938 embedding_write: item.embedding_write,
1939 })
1940 .collect(),
1941 next_cursor: page.next_cursor,
1942 observed_frontier: page.observed_frontier,
1943 })
1944 }
1945 },
1946 )
1947 }
1948}
1949
1950#[derive(Clone, Debug, Eq, PartialEq)]
1952pub enum ApplicationContract {
1953 Active,
1955 Exact {
1957 lineage: String,
1959 version: u64,
1961 bundle_hash: Option<[u8; 32]>,
1963 },
1964}
1965
1966#[derive(Clone, Debug, Eq, PartialEq)]
1968pub struct NamedQuery {
1969 contract: ApplicationContract,
1970 name: String,
1971 module_hash: Option<[u8; 32]>,
1972 expected_plan_hash: Option<[u8; 32]>,
1973 parameters: BTreeMap<String, ApplicationValue>,
1974 cursor: Option<String>,
1975 minimum_application_head: Option<u64>,
1976 admission_head_consistency: bool,
1977 accept_packed_result_v1: bool,
1978}
1979
1980impl NamedQuery {
1981 pub fn new(
1983 contract: ApplicationContract,
1984 name: impl Into<String>,
1985 module_hash: Option<[u8; 32]>,
1986 parameters: BTreeMap<String, ApplicationValue>,
1987 cursor: Option<String>,
1988 ) -> Result<Self, ApplicationClientError> {
1989 let name = name.into();
1990 if name.is_empty() || name.len() > 256 || parameters.len() > 4_096 {
1991 return Err(ApplicationClientError::InvalidInput);
1992 }
1993 validate_contract(&contract)?;
1994 if parameters
1995 .iter()
1996 .any(|(name, _)| name.is_empty() || name.len() > 256)
1997 {
1998 return Err(ApplicationClientError::InvalidInput);
1999 }
2000 Ok(Self {
2001 contract,
2002 name,
2003 module_hash,
2004 expected_plan_hash: None,
2005 parameters,
2006 cursor,
2007 minimum_application_head: None,
2008 admission_head_consistency: false,
2009 accept_packed_result_v1: false,
2010 })
2011 }
2012
2013 #[must_use]
2015 pub const fn expect_plan_hash(mut self, plan_hash: [u8; 32]) -> Self {
2016 self.expected_plan_hash = Some(plan_hash);
2017 self
2018 }
2019
2020 #[doc(hidden)]
2022 #[must_use]
2023 pub const fn accept_packed_result_v1(mut self) -> Self {
2024 self.accept_packed_result_v1 = true;
2025 self
2026 }
2027
2028 pub fn with_options(mut self, options: QueryOptions) -> Result<Self, ApplicationClientError> {
2030 if options.read_after_commit == Some(0) {
2031 return Err(ApplicationClientError::InvalidInput);
2032 }
2033 self.cursor = options.cursor;
2034 self.minimum_application_head = options.read_after_commit;
2035 self.admission_head_consistency = options.admission_head;
2036 Ok(self)
2037 }
2038}
2039
2040#[derive(Clone, Debug, Default, Eq, PartialEq)]
2042pub struct QueryOptions {
2043 cursor: Option<String>,
2044 read_after_commit: Option<u64>,
2045 admission_head: bool,
2046}
2047
2048impl QueryOptions {
2049 #[must_use]
2051 pub const fn new() -> Self {
2052 Self {
2053 cursor: None,
2054 read_after_commit: None,
2055 admission_head: false,
2056 }
2057 }
2058
2059 #[must_use]
2061 pub fn after(mut self, cursor: impl Into<String>) -> Self {
2062 self.cursor = Some(cursor.into());
2063 self
2064 }
2065
2066 #[doc(hidden)]
2068 pub fn with_generated_cursor(
2069 mut self,
2070 cursor: Option<String>,
2071 ) -> Result<Self, ApplicationClientError> {
2072 if self.cursor.is_some() && cursor.is_some() {
2073 return Err(ApplicationClientError::InvalidInput);
2074 }
2075 if cursor.is_some() {
2076 self.cursor = cursor;
2077 }
2078 Ok(self)
2079 }
2080
2081 #[must_use]
2083 pub const fn read_after_commit(mut self, commit_sequence: u64) -> Self {
2084 self.read_after_commit = Some(commit_sequence);
2085 self
2086 }
2087
2088 #[must_use]
2091 pub const fn at_least_admission_head(mut self) -> Self {
2092 self.admission_head = true;
2093 self
2094 }
2095}
2096
2097#[derive(Clone, Debug, Eq, PartialEq)]
2099pub struct TypedQueryResult<T> {
2100 pub value: T,
2102 pub identity: QueryResponseIdentity,
2104 pub application_head: u64,
2106 pub next_cursor: Option<String>,
2108}
2109
2110#[derive(Clone, Debug, Eq, PartialEq)]
2112pub struct TypedCommandResult<T> {
2113 pub outcome: T,
2115 pub commit_sequence: Option<u64>,
2117 pub contract_version: u64,
2119 pub plan_hash: [u8; 32],
2121 pub replayed: bool,
2123 pub outcome_uri: Option<String>,
2125 pub workflow_revisions: Vec<crate::generated::WorkflowSuccessorRevision>,
2128}
2129
2130#[derive(Clone, Debug, Eq, PartialEq)]
2132pub struct ApplicationCommand {
2133 name: String,
2134 expected_contract_version: Option<u64>,
2135 input: BTreeMap<String, ApplicationValue>,
2136}
2137
2138impl ApplicationCommand {
2139 pub fn new(
2141 name: impl Into<String>,
2142 expected_contract_version: Option<u64>,
2143 input: BTreeMap<String, ApplicationValue>,
2144 ) -> Result<Self, ApplicationClientError> {
2145 let name = name.into();
2146 if name.is_empty()
2147 || name.len() > 256
2148 || expected_contract_version == Some(0)
2149 || input.is_empty()
2150 || input.len() > 4_096
2151 || input
2152 .keys()
2153 .any(|field| field.is_empty() || field.len() > 256)
2154 {
2155 return Err(ApplicationClientError::InvalidInput);
2156 }
2157 Ok(Self {
2158 name,
2159 expected_contract_version,
2160 input,
2161 })
2162 }
2163
2164 pub(crate) fn into_idempotent_command(
2165 self,
2166 ) -> Result<IdempotentCommand, ApplicationClientError> {
2167 let input = lower_value(ApplicationValue::Record(self.input))?;
2168 IdempotentCommand::new(self.name, self.expected_contract_version, input)
2169 .map_err(|_| ApplicationClientError::InvalidInput)
2170 }
2171}
2172
2173#[derive(Clone, Copy, Debug, Eq, PartialEq)]
2175pub enum ApplicationCardinality {
2176 One,
2178 Maybe,
2180 Many,
2182}
2183
2184#[derive(Clone, Debug, Eq, PartialEq)]
2186pub struct ApplicationRecord {
2187 pub entity: String,
2189 pub fields: BTreeMap<String, ApplicationValue>,
2191}
2192
2193#[derive(Clone, Debug, Eq, PartialEq)]
2195pub struct ApplicationResultField {
2196 pub cardinality: ApplicationCardinality,
2198 pub records: Vec<ApplicationRecord>,
2200}
2201
2202#[derive(Clone, Debug, Eq, PartialEq)]
2204pub struct NamedQueryResult {
2205 pub outcome: String,
2207 pub application_head: u64,
2209 pub fields: BTreeMap<String, ApplicationResultField>,
2211 pub next_cursor: Option<String>,
2213 pub identity: QueryResponseIdentity,
2215}
2216
2217#[derive(Clone, Debug, Eq, PartialEq)]
2219pub struct QueryResponseIdentity {
2220 pub contract_lineage: String,
2222 pub contract_version: u64,
2224 pub contract_bundle_hash: [u8; 32],
2226 pub module_hash: [u8; 32],
2228 pub query_name: String,
2230 pub plan_hash: [u8; 32],
2232}
2233
2234#[derive(Clone, Debug, Eq, PartialEq)]
2236pub struct ApplicationCommandResult {
2237 pub outcome: Option<String>,
2239 pub outcome_value: Option<ApplicationValue>,
2241 pub commit_sequence: Option<u64>,
2243 pub contract_version: u64,
2245 pub plan_hash: [u8; 32],
2247 pub replayed: bool,
2249 pub outcome_uri: Option<String>,
2251}
2252
2253pub(crate) fn raise_application_command_result(
2254 response: v1::ExecuteCommandResponse,
2255) -> Result<ApplicationCommandResult, ApplicationClientError> {
2256 Ok(ApplicationCommandResult {
2257 outcome: (!response.outcome_type.is_empty()).then_some(response.outcome_type),
2258 outcome_value: response.outcome.map(raise_value).transpose()?,
2259 commit_sequence: (response.commit_sequence != 0).then_some(response.commit_sequence),
2260 contract_version: response.contract_version,
2261 plan_hash: response
2262 .plan_hash
2263 .try_into()
2264 .map_err(|_| ApplicationClientError::InvalidResponse)?,
2265 replayed: response.status
2266 == v1::execute_command_response::CompletionStatus::Replayed as i32,
2267 outcome_uri: response.outcome_uri,
2268 })
2269}
2270
2271#[derive(Debug)]
2273pub enum ApplicationClientError {
2274 InvalidInput,
2276 InvalidResponse,
2278 IdentifierUnavailable,
2280 Client(ClientError),
2282}
2283
2284impl fmt::Display for ApplicationClientError {
2285 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
2286 match self {
2287 Self::InvalidInput => formatter.write_str("application input is invalid"),
2288 Self::InvalidResponse => formatter.write_str("application response is invalid"),
2289 Self::IdentifierUnavailable => formatter.write_str("request identity is unavailable"),
2290 Self::Client(error) => error.fmt(formatter),
2291 }
2292 }
2293}
2294
2295impl std::error::Error for ApplicationClientError {
2296 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
2297 match self {
2298 Self::Client(error) => Some(error),
2299 Self::InvalidInput | Self::InvalidResponse | Self::IdentifierUnavailable => None,
2300 }
2301 }
2302}
2303
2304impl ApplicationClientError {
2305 #[must_use]
2307 pub const fn semantic_error(&self) -> Option<&riffdb_errors::ApplicationError> {
2308 match self {
2309 Self::Client(error) => error.application_error(),
2310 Self::InvalidInput | Self::InvalidResponse | Self::IdentifierUnavailable => None,
2311 }
2312 }
2313}
2314
2315impl From<ClientError> for ApplicationClientError {
2316 fn from(error: ClientError) -> Self {
2317 Self::Client(error)
2318 }
2319}
2320
2321impl From<GeneratedExecutionError> for ApplicationClientError {
2322 fn from(error: GeneratedExecutionError) -> Self {
2323 match error {
2324 GeneratedExecutionError::Client(error) => Self::Client(error),
2325 GeneratedExecutionError::CommandShape(
2330 crate::generated::GeneratedCommandError::InvalidInputShape
2331 | crate::generated::GeneratedCommandError::InputBudget(_),
2332 ) => Self::InvalidInput,
2333 GeneratedExecutionError::CommandShape(
2334 crate::generated::GeneratedCommandError::InvalidOutcomeShape,
2335 ) => Self::InvalidResponse,
2336 }
2337 }
2338}
2339
2340impl RiffDbClient {
2341 pub async fn execute_named_application_query(
2343 &mut self,
2344 query: NamedQuery,
2345 metadata: &CallMetadata,
2346 ) -> Result<NamedQueryResult, ApplicationClientError> {
2347 let response =
2348 with_client_wire_carrier(self.execute_named_application_query_wire(query, metadata))
2349 .await?;
2350 raise_query_result_from_client_wire(response)
2351 }
2352
2353 async fn execute_named_application_query_wire(
2354 &mut self,
2355 query: NamedQuery,
2356 metadata: &CallMetadata,
2357 ) -> Result<app_v1::ExecuteQueryResponse, ApplicationClientError> {
2358 let expected_contract = query.contract.clone();
2359 let expected_name = query.name.clone();
2360 let expected_module_hash = query.module_hash;
2361 let expected_plan_hash = query.expected_plan_hash;
2362 let request_id = Vec::from(
2363 generate_request_id()
2364 .map_err(|_| ApplicationClientError::IdentifierUnavailable)?
2365 .into_bytes(),
2366 );
2367 let mut parameters = Vec::with_capacity(query.parameters.len());
2369 for (name, value) in query.parameters {
2370 parameters.push(app_v1::Parameter {
2371 name,
2372 value: Some(lower_value(value)?),
2373 });
2374 }
2375 let response = self
2376 .execute_selected_query_transport(
2377 app_v1::ExecuteQueryRequest {
2378 contract: lower_contract(query.contract),
2379 query: Some(app_v1::execute_query_request::Query::QueryName(query.name)),
2380 module_hash: query.module_hash.map(|hash| hash.to_vec()),
2381 parameters,
2382 cursor: query.cursor,
2383 minimum_application_head: query.minimum_application_head,
2384 accepted_result_encodings: if query.accept_packed_result_v1 {
2385 vec![
2386 app_v1::NamedResultEncoding::LegacyRecords as i32,
2387 app_v1::NamedResultEncoding::CompactV1 as i32,
2388 app_v1::NamedResultEncoding::PackedV1 as i32,
2389 ]
2390 } else {
2391 vec![
2392 app_v1::NamedResultEncoding::LegacyRecords as i32,
2393 app_v1::NamedResultEncoding::CompactV1 as i32,
2394 ]
2395 },
2396 consistency: if query.admission_head_consistency {
2397 app_v1::QueryConsistency::AdmissionHead as i32
2398 } else {
2399 app_v1::QueryConsistency::Unspecified as i32
2400 },
2401 request_id,
2402 },
2403 metadata,
2404 )
2405 .await?;
2406 validate_query_response_identity(
2407 &response,
2408 &expected_contract,
2409 &expected_name,
2410 expected_module_hash,
2411 expected_plan_hash,
2412 )?;
2413 Ok(response)
2414 }
2415
2416 pub async fn execute_application_command(
2418 &mut self,
2419 command: ApplicationCommand,
2420 attempts: AttemptBudget,
2421 metadata: &CallMetadata,
2422 ) -> Result<ApplicationCommandResult, ApplicationClientError> {
2423 let command_name = command.name.clone();
2424 let command = command.into_idempotent_command()?;
2425 let response = self
2426 .execute_with_retry(&command, attempts, metadata)
2427 .await
2428 .map_err(|error| contextualize_command_client_error(error, &command_name))?;
2429 raise_application_command_result(response)
2430 }
2431}
2432
2433pub(crate) fn contextualize_command_client_error(
2434 error: ClientError,
2435 command_name: &str,
2436) -> ClientError {
2437 let ClientError::Public(public) = error else {
2438 return error;
2439 };
2440 let context = ApplicationErrorContext::empty()
2441 .with_operation_symbol(command_name.to_owned())
2442 .unwrap_or_else(|_| ApplicationErrorContext::empty());
2443 ClientError::Application(Box::new(ApplicationError::from_public_error(
2444 &public,
2445 ApplicationOperation::ExecuteCommand,
2446 context,
2447 )))
2448}
2449
2450fn validate_pinned_query_response(
2451 response: &app_v1::ExecuteQueryResponse,
2452 pinned: &PinnedContractIdentity,
2453) -> Result<(), ApplicationClientError> {
2454 let identity = response
2455 .identity
2456 .as_ref()
2457 .ok_or(ApplicationClientError::InvalidResponse)?;
2458 if identity.contract_lineage != pinned.lineage
2459 || identity.contract_version != pinned.version
2460 || identity.contract_bundle_hash.as_slice() != pinned.bundle_hash
2461 {
2462 return Err(ApplicationClientError::InvalidResponse);
2463 }
2464 Ok(())
2465}
2466
2467const fn client_error_triggers_standby_failover(error: &ClientError) -> bool {
2468 match error {
2469 ClientError::DetailsFree(DetailsFreeStatus::TransportUnavailable)
2470 | ClientError::ConnectionFailure
2471 | ClientError::OutcomeUnknown(_) => true,
2472 ClientError::Public(public) => matches!(
2473 public.kind(),
2474 PublicErrorKind::PrimaryFenced
2475 | PublicErrorKind::FollowerMode
2476 | PublicErrorKind::StorageUnavailable
2477 ),
2478 ClientError::Application(application) => matches!(
2479 application.code(),
2480 ApplicationErrorCode::PrimaryFenced
2481 | ApplicationErrorCode::FollowerMode
2482 | ApplicationErrorCode::StorageUnavailable
2483 | ApplicationErrorCode::OutcomeUnknown
2484 ),
2485 ClientError::Protocol(_)
2486 | ClientError::IdentifierGeneration(_)
2487 | ClientError::Tls(_)
2488 | ClientError::DetailsFree(_) => false,
2489 }
2490}
2491
2492const fn application_query_error_allows_replica_fallback(error: &ApplicationClientError) -> bool {
2493 match error {
2494 ApplicationClientError::Client(client) => match client {
2495 ClientError::DetailsFree(DetailsFreeStatus::TransportUnavailable)
2496 | ClientError::ConnectionFailure
2497 | ClientError::OutcomeUnknown(_) => true,
2498 ClientError::Public(public) => matches!(
2499 public.kind(),
2500 PublicErrorKind::FollowerMode
2501 | PublicErrorKind::PrimaryFenced
2502 | PublicErrorKind::StorageUnavailable
2503 | PublicErrorKind::Overloaded
2504 ),
2505 ClientError::Application(application) => matches!(
2506 application.code(),
2507 ApplicationErrorCode::FreshnessUnsatisfied
2508 | ApplicationErrorCode::SnapshotRetired
2509 | ApplicationErrorCode::ProjectionDiverged
2510 | ApplicationErrorCode::FollowerMode
2511 | ApplicationErrorCode::PrimaryFenced
2512 | ApplicationErrorCode::StorageUnavailable
2513 | ApplicationErrorCode::Overloaded
2514 ),
2515 ClientError::Protocol(_)
2516 | ClientError::IdentifierGeneration(_)
2517 | ClientError::Tls(_)
2518 | ClientError::DetailsFree(_) => false,
2519 },
2520 ApplicationClientError::InvalidInput
2521 | ApplicationClientError::InvalidResponse
2522 | ApplicationClientError::IdentifierUnavailable => false,
2523 }
2524}
2525
2526fn validate_query_response_identity(
2527 response: &app_v1::ExecuteQueryResponse,
2528 contract: &ApplicationContract,
2529 query_name: &str,
2530 module_hash: Option<[u8; 32]>,
2531 plan_hash: Option<[u8; 32]>,
2532) -> Result<(), ApplicationClientError> {
2533 let identity = response
2534 .identity
2535 .as_ref()
2536 .ok_or(ApplicationClientError::InvalidResponse)?;
2537 if identity.query_name.as_deref() != Some(query_name)
2538 || module_hash
2539 .is_some_and(|expected| identity.module_hash.as_deref() != Some(expected.as_slice()))
2540 || plan_hash.is_some_and(|expected| identity.plan_hash.as_slice() != expected.as_slice())
2541 {
2542 return Err(ApplicationClientError::InvalidResponse);
2543 }
2544 if let ApplicationContract::Exact {
2545 lineage,
2546 version,
2547 bundle_hash,
2548 } = contract
2549 && (identity.contract_lineage != *lineage
2550 || identity.contract_version != *version
2551 || bundle_hash.is_some_and(|expected| {
2552 identity.contract_bundle_hash.as_slice() != expected.as_slice()
2553 }))
2554 {
2555 return Err(ApplicationClientError::InvalidResponse);
2556 }
2557 Ok(())
2558}
2559
2560fn raise_application_catalog_preflight(
2561 response: app_v1::GetApplicationCatalogResponse,
2562 contract: &ApplicationContract,
2563) -> Result<ApplicationCatalogPreflight, ApplicationClientError> {
2564 if response.schema != APPLICATION_CATALOG_SCHEMA_V1
2565 || response.contract_lineage.is_empty()
2566 || response.contract_version == 0
2567 {
2568 return Err(ApplicationClientError::InvalidResponse);
2569 }
2570 let contract_bundle_hash: [u8; 32] = response
2571 .contract_bundle_hash
2572 .try_into()
2573 .map_err(|_| ApplicationClientError::InvalidResponse)?;
2574 if let ApplicationContract::Exact {
2575 lineage,
2576 version,
2577 bundle_hash,
2578 } = contract
2579 && (response.contract_lineage != *lineage
2580 || response.contract_version != *version
2581 || bundle_hash.is_some_and(|expected| expected != contract_bundle_hash))
2582 {
2583 return Err(ApplicationClientError::InvalidResponse);
2584 }
2585
2586 let mut query_module_hashes = Vec::with_capacity(response.query_module_hashes.len());
2587 for hash in response.query_module_hashes {
2588 query_module_hashes.push(
2589 hash.try_into()
2590 .map_err(|_| ApplicationClientError::InvalidResponse)?,
2591 );
2592 }
2593 if query_module_hashes
2594 .windows(2)
2595 .any(|pair| pair[0] >= pair[1])
2596 {
2597 return Err(ApplicationClientError::InvalidResponse);
2598 }
2599
2600 let mut features = Vec::with_capacity(response.features.len());
2601 for view in response.features {
2602 let feature = match app_v1::ApplicationCatalogFeature::try_from(view.feature) {
2603 Ok(app_v1::ApplicationCatalogFeature::OperationalOptionalPredicates) => {
2604 ApplicationCatalogFeature::OperationalOptionalPredicates
2605 }
2606 Ok(app_v1::ApplicationCatalogFeature::StableCursorPages) => {
2607 ApplicationCatalogFeature::StableCursorPages
2608 }
2609 Ok(app_v1::ApplicationCatalogFeature::NullExistencePredicates) => {
2610 ApplicationCatalogFeature::NullExistencePredicates
2611 }
2612 Ok(app_v1::ApplicationCatalogFeature::BinaryTextPrefix) => {
2613 ApplicationCatalogFeature::BinaryTextPrefix
2614 }
2615 Ok(app_v1::ApplicationCatalogFeature::UnicodeFoldTextPrefixV1) => {
2616 ApplicationCatalogFeature::UnicodeFoldTextPrefixV1
2617 }
2618 Ok(app_v1::ApplicationCatalogFeature::ExactAggregates) => {
2619 ApplicationCatalogFeature::ExactAggregates
2620 }
2621 Ok(app_v1::ApplicationCatalogFeature::Unspecified) | Err(_) => {
2622 return Err(ApplicationClientError::InvalidResponse);
2623 }
2624 };
2625 let state = match app_v1::ApplicationCatalogFeatureState::try_from(view.state) {
2626 Ok(app_v1::ApplicationCatalogFeatureState::Available) => {
2627 ApplicationCatalogFeatureState::Available
2628 }
2629 Ok(app_v1::ApplicationCatalogFeatureState::Unavailable) => {
2630 ApplicationCatalogFeatureState::Unavailable
2631 }
2632 Ok(app_v1::ApplicationCatalogFeatureState::Unspecified) | Err(_) => {
2633 return Err(ApplicationClientError::InvalidResponse);
2634 }
2635 };
2636 features.push(ApplicationCatalogFeatureView { feature, state });
2637 }
2638 if features.len() != 6
2639 || features
2640 .windows(2)
2641 .any(|pair| pair[0].feature >= pair[1].feature)
2642 {
2643 return Err(ApplicationClientError::InvalidResponse);
2644 }
2645
2646 Ok(ApplicationCatalogPreflight {
2647 contract_lineage: response.contract_lineage,
2648 contract_version: response.contract_version,
2649 contract_bundle_hash,
2650 query_module_hashes,
2651 features,
2652 })
2653}
2654
2655pub(crate) fn validate_contract(
2656 contract: &ApplicationContract,
2657) -> Result<(), ApplicationClientError> {
2658 if let ApplicationContract::Exact {
2659 lineage, version, ..
2660 } = contract
2661 && (lineage.is_empty() || lineage.len() > 256 || *version == 0)
2662 {
2663 return Err(ApplicationClientError::InvalidInput);
2664 }
2665 Ok(())
2666}
2667
2668fn lower_contract(contract: ApplicationContract) -> Option<app_v1::ContractSelector> {
2669 match contract {
2670 ApplicationContract::Active => None,
2671 ApplicationContract::Exact {
2672 lineage,
2673 version,
2674 bundle_hash,
2675 } => Some(app_v1::ContractSelector {
2676 lineage,
2677 version,
2678 bundle_hash: bundle_hash.map_or_else(Vec::new, |hash| hash.to_vec()),
2679 }),
2680 }
2681}
2682
2683pub(crate) fn lower_value(value: ApplicationValue) -> Result<v1::Value, ApplicationClientError> {
2684 use v1::value::Kind;
2685 let kind = match value {
2686 ApplicationValue::Null => Kind::NullValue(v1::NullValue::NullValue as i32),
2687 ApplicationValue::Bool(value) => Kind::BoolValue(value),
2688 ApplicationValue::I64(value) => Kind::I64Value(value),
2689 ApplicationValue::U64(value) => Kind::U64Value(value),
2690 ApplicationValue::Decimal {
2691 coefficient_twos_complement,
2692 scale,
2693 precision,
2694 } => Kind::DecimalValue(v1::Decimal {
2695 coefficient_twos_complement,
2696 scale,
2697 precision,
2698 }),
2699 ApplicationValue::Money { currency, amount } => {
2700 let Kind::DecimalValue(amount) = lower_value(*amount)?
2701 .kind
2702 .ok_or(ApplicationClientError::InvalidInput)?
2703 else {
2704 return Err(ApplicationClientError::InvalidInput);
2705 };
2706 Kind::MoneyValue(v1::Money {
2707 currency,
2708 amount: Some(amount),
2709 })
2710 }
2711 ApplicationValue::String(value) => Kind::StringValue(value),
2712 ApplicationValue::Uuid(value) => Kind::UuidValue(value.as_bytes().to_vec()),
2713 ApplicationValue::Enum(name) if !name.is_empty() && name.len() <= 256 => {
2714 Kind::EnumValue(v1::EnumValue {
2715 type_id: 0,
2716 variant_id: 0,
2717 name,
2718 })
2719 }
2720 ApplicationValue::Enum(_) => return Err(ApplicationClientError::InvalidInput),
2721 ApplicationValue::EnumIdentity {
2722 type_id,
2723 variant_id,
2724 name,
2725 } if type_id != 0 && variant_id != 0 && !name.is_empty() && name.len() <= 256 => {
2726 Kind::EnumValue(v1::EnumValue {
2727 type_id,
2728 variant_id,
2729 name,
2730 })
2731 }
2732 ApplicationValue::EnumIdentity { .. } => {
2733 return Err(ApplicationClientError::InvalidInput);
2734 }
2735 ApplicationValue::Bytes(value) => Kind::BytesValue(value),
2736 ApplicationValue::Date(days_since_unix_epoch) => Kind::DateValue(v1::Date {
2737 days_since_unix_epoch,
2738 }),
2739 ApplicationValue::Timestamp { seconds, nanos } if nanos < 1_000_000_000 => {
2740 Kind::TimestampValue(v1::Timestamp { seconds, nanos })
2741 }
2742 ApplicationValue::Timestamp { .. } => {
2743 return Err(ApplicationClientError::InvalidInput);
2744 }
2745 ApplicationValue::Vector(value) => Kind::VectorValue(v1::VectorValue {
2746 components: value.into_components(),
2747 }),
2748 ApplicationValue::List(values) => Kind::ListValue(v1::ValueList {
2749 values: values
2750 .into_iter()
2751 .map(lower_value)
2752 .collect::<Result<Vec<_>, _>>()?,
2753 }),
2754 ApplicationValue::Record(fields) => Kind::RecordValue(v1::ValueRecord {
2755 fields: fields
2756 .into_iter()
2757 .map(|(name, value)| {
2758 Ok(v1::ValueField {
2759 field_id: None,
2760 name,
2761 value: Some(lower_value(value)?),
2762 })
2763 })
2764 .collect::<Result<Vec<_>, ApplicationClientError>>()?,
2765 }),
2766 };
2767 Ok(v1::Value { kind: Some(kind) })
2768}
2769
2770const LOCAL_RAISED_QUERY_CACHE_CAP: usize = 256;
2771const LOCAL_RAISED_QUERY_HASH_SLOTS: usize = 512;
2772const SHARED_RAISED_QUERY_SHARDS: usize = 16;
2773const SHARED_RAISED_QUERY_SHARD_CAP: usize = 64;
2774
2775#[derive(Clone)]
2776struct CachedRaisedQueryResult {
2777 wire_cache_id: u32,
2778 result: Arc<NamedQueryResult>,
2779}
2780
2781struct LocalRaisedQueryCache {
2782 entries: Vec<CachedRaisedQueryResult>,
2783 next_slot: usize,
2784 by_wire_id: [u16; LOCAL_RAISED_QUERY_HASH_SLOTS],
2785}
2786
2787impl Default for LocalRaisedQueryCache {
2788 fn default() -> Self {
2789 Self {
2790 entries: Vec::new(),
2791 next_slot: 0,
2792 by_wire_id: [0; LOCAL_RAISED_QUERY_HASH_SLOTS],
2793 }
2794 }
2795}
2796
2797impl LocalRaisedQueryCache {
2798 fn bucket(wire_cache_id: u32) -> usize {
2799 (wire_cache_id as usize) & (LOCAL_RAISED_QUERY_HASH_SLOTS - 1)
2800 }
2801
2802 fn lookup(&self, wire_cache_id: u32) -> Option<&Arc<NamedQueryResult>> {
2803 let bucket = Self::bucket(wire_cache_id);
2804 let idx1 = self.by_wire_id[bucket] as usize;
2805 if idx1 != 0
2806 && let Some(entry) = self.entries.get(idx1 - 1)
2807 && entry.wire_cache_id == wire_cache_id
2808 {
2809 return Some(&entry.result);
2810 }
2811 self.entries
2812 .iter()
2813 .find(|entry| entry.wire_cache_id == wire_cache_id)
2814 .map(|entry| &entry.result)
2815 }
2816
2817 fn insert(&mut self, wire_cache_id: u32, result: Arc<NamedQueryResult>) {
2818 let bucket = Self::bucket(wire_cache_id);
2819 let entry = CachedRaisedQueryResult {
2820 wire_cache_id,
2821 result,
2822 };
2823 let slot = if self.entries.len() < LOCAL_RAISED_QUERY_CACHE_CAP {
2824 let slot = self.entries.len();
2825 self.entries.push(entry);
2826 slot
2827 } else {
2828 let slot = self.next_slot;
2829 self.entries[slot] = entry;
2830 self.next_slot = (slot + 1) % LOCAL_RAISED_QUERY_CACHE_CAP;
2831 slot
2832 };
2833 self.by_wire_id[bucket] = (slot + 1) as u16;
2834 }
2835}
2836
2837#[derive(Default)]
2838struct SharedRaisedQueryShard {
2839 entries: Vec<CachedRaisedQueryResult>,
2840 next_slot: usize,
2841}
2842
2843static SHARED_RAISED_QUERY_CACHE: OnceLock<
2844 [std::sync::RwLock<SharedRaisedQueryShard>; SHARED_RAISED_QUERY_SHARDS],
2845> = OnceLock::new();
2846
2847fn shared_raised_query_cache()
2848-> &'static [std::sync::RwLock<SharedRaisedQueryShard>; SHARED_RAISED_QUERY_SHARDS] {
2849 SHARED_RAISED_QUERY_CACHE.get_or_init(|| {
2850 std::array::from_fn(|_| std::sync::RwLock::new(SharedRaisedQueryShard::default()))
2851 })
2852}
2853
2854std::thread_local! {
2855 static LOCAL_RAISED_QUERY_CACHE: std::cell::RefCell<LocalRaisedQueryCache> =
2856 std::cell::RefCell::new(LocalRaisedQueryCache::default());
2857}
2858
2859fn lookup_shared_raised_query_result(wire_cache_id: u32) -> Option<Arc<NamedQueryResult>> {
2860 let shard_idx = (wire_cache_id as usize) & (SHARED_RAISED_QUERY_SHARDS - 1);
2861 let guard = shared_raised_query_cache()[shard_idx].read().ok()?;
2862 guard
2863 .entries
2864 .iter()
2865 .find(|entry| entry.wire_cache_id == wire_cache_id)
2866 .map(|entry| Arc::clone(&entry.result))
2867}
2868
2869fn insert_shared_raised_query_result(wire_cache_id: u32, result: Arc<NamedQueryResult>) {
2870 let shard_idx = (wire_cache_id as usize) & (SHARED_RAISED_QUERY_SHARDS - 1);
2871 let Ok(mut guard) = shared_raised_query_cache()[shard_idx].write() else {
2872 return;
2873 };
2874 if guard
2875 .entries
2876 .iter()
2877 .any(|entry| entry.wire_cache_id == wire_cache_id)
2878 {
2879 return;
2880 }
2881 let entry = CachedRaisedQueryResult {
2882 wire_cache_id,
2883 result,
2884 };
2885 if guard.entries.len() < SHARED_RAISED_QUERY_SHARD_CAP {
2886 guard.entries.push(entry);
2887 } else {
2888 let slot = guard.next_slot;
2889 guard.entries[slot] = entry;
2890 guard.next_slot = (slot + 1) % SHARED_RAISED_QUERY_SHARD_CAP;
2891 }
2892}
2893
2894async fn with_client_wire_carrier<F: std::future::Future>(future: F) -> F::Output {
2895 let mut pinned = std::pin::pin!(future);
2896 std::future::poll_fn(|cx| {
2897 let _guard = riffdb_proto::enter_client_query_wire_carrier_scope();
2898 pinned.as_mut().poll(cx)
2899 })
2900 .await
2901}
2902
2903fn raise_query_result_from_client_wire(
2904 response: app_v1::ExecuteQueryResponse,
2905) -> Result<NamedQueryResult, ApplicationClientError> {
2906 raise_query_result_shared_from_client_wire(response).map(Arc::unwrap_or_clone)
2907}
2908
2909fn raise_query_result_shared_from_client_wire(
2910 response: app_v1::ExecuteQueryResponse,
2911) -> Result<Arc<NamedQueryResult>, ApplicationClientError> {
2912 if response.selected_result_encoding < 0
2913 && response.fields.is_empty()
2914 && response.compact_result.is_none()
2915 && response.packed_result.is_none()
2916 {
2917 let wire_cache_id = response.selected_result_encoding.unsigned_abs();
2918 if wire_cache_id >= riffdb_proto::CLIENT_WIRE_CARRIER_ID_BASE {
2919 if let Some(cached) = LOCAL_RAISED_QUERY_CACHE.with(|cell| {
2920 let guard = cell.try_borrow().ok()?;
2921 guard.lookup(wire_cache_id).cloned()
2922 }) {
2923 return Ok(cached);
2924 }
2925 if let Some(shared) = lookup_shared_raised_query_result(wire_cache_id) {
2926 LOCAL_RAISED_QUERY_CACHE.with(|cell| {
2927 if let Ok(mut guard) = cell.try_borrow_mut() {
2928 guard.insert(wire_cache_id, Arc::clone(&shared));
2929 }
2930 });
2931 return Ok(shared);
2932 }
2933 if let Some(full_response) =
2934 riffdb_proto::lookup_client_query_wire_response(wire_cache_id)
2935 {
2936 let raised = Arc::new(raise_query_result((*full_response).clone())?);
2937 insert_shared_raised_query_result(wire_cache_id, Arc::clone(&raised));
2938 LOCAL_RAISED_QUERY_CACHE.with(|cell| {
2939 if let Ok(mut guard) = cell.try_borrow_mut() {
2940 guard.insert(wire_cache_id, Arc::clone(&raised));
2941 }
2942 });
2943 return Ok(raised);
2944 }
2945 }
2946 }
2947 raise_query_result(response).map(Arc::new)
2948}
2949
2950#[doc(hidden)]
2951pub fn raise_query_result(
2952 response: app_v1::ExecuteQueryResponse,
2953) -> Result<NamedQueryResult, ApplicationClientError> {
2954 let identity = query_response_identity(&response)?;
2955 let selected_encoding =
2956 app_v1::NamedResultEncoding::try_from(response.selected_result_encoding)
2957 .map_err(|_| ApplicationClientError::InvalidResponse)?;
2958 let fields = match selected_encoding {
2959 app_v1::NamedResultEncoding::Unspecified | app_v1::NamedResultEncoding::LegacyRecords => {
2960 if response.compact_result.is_some() || response.packed_result.is_some() {
2961 return Err(ApplicationClientError::InvalidResponse);
2962 }
2963 raise_legacy_query_fields(response.fields)?
2964 }
2965 app_v1::NamedResultEncoding::CompactV1 => {
2966 if !response.fields.is_empty() || response.packed_result.is_some() {
2967 return Err(ApplicationClientError::InvalidResponse);
2968 }
2969 raise_compact_query_field(
2970 response
2971 .compact_result
2972 .ok_or(ApplicationClientError::InvalidResponse)?,
2973 )?
2974 }
2975 app_v1::NamedResultEncoding::PackedV1 => {
2976 return Err(ApplicationClientError::InvalidResponse);
2977 }
2978 };
2979 Ok(NamedQueryResult {
2980 outcome: response.outcome,
2981 application_head: response.application_head,
2982 fields,
2983 next_cursor: response.next_cursor,
2984 identity,
2985 })
2986}
2987
2988fn query_response_identity(
2989 response: &app_v1::ExecuteQueryResponse,
2990) -> Result<QueryResponseIdentity, ApplicationClientError> {
2991 let identity = response
2992 .identity
2993 .as_ref()
2994 .ok_or(ApplicationClientError::InvalidResponse)?;
2995 Ok(QueryResponseIdentity {
2996 contract_lineage: identity.contract_lineage.clone(),
2997 contract_version: identity.contract_version,
2998 contract_bundle_hash: identity
2999 .contract_bundle_hash
3000 .as_slice()
3001 .try_into()
3002 .map_err(|_| ApplicationClientError::InvalidResponse)?,
3003 module_hash: identity
3004 .module_hash
3005 .as_deref()
3006 .ok_or(ApplicationClientError::InvalidResponse)?
3007 .try_into()
3008 .map_err(|_| ApplicationClientError::InvalidResponse)?,
3009 query_name: identity
3010 .query_name
3011 .clone()
3012 .ok_or(ApplicationClientError::InvalidResponse)?,
3013 plan_hash: identity
3014 .plan_hash
3015 .as_slice()
3016 .try_into()
3017 .map_err(|_| ApplicationClientError::InvalidResponse)?,
3018 })
3019}
3020
3021fn raise_legacy_query_fields(
3022 wire_fields: Vec<app_v1::ResultField>,
3023) -> Result<BTreeMap<String, ApplicationResultField>, ApplicationClientError> {
3024 let mut fields = BTreeMap::new();
3025 for field in wire_fields {
3026 let cardinality = match app_v1::ResultCardinality::try_from(field.cardinality) {
3027 Ok(app_v1::ResultCardinality::One) => ApplicationCardinality::One,
3028 Ok(app_v1::ResultCardinality::Maybe) => ApplicationCardinality::Maybe,
3029 Ok(app_v1::ResultCardinality::Many) => ApplicationCardinality::Many,
3030 Ok(app_v1::ResultCardinality::Unspecified) | Err(_) => {
3031 return Err(ApplicationClientError::InvalidResponse);
3032 }
3033 };
3034 let records = field
3035 .records
3036 .into_iter()
3037 .map(|record| {
3038 let mut fields = BTreeMap::new();
3039 for field in record.fields {
3040 if fields
3041 .insert(
3042 field.name,
3043 raise_value(
3044 field.value.ok_or(ApplicationClientError::InvalidResponse)?,
3045 )?,
3046 )
3047 .is_some()
3048 {
3049 return Err(ApplicationClientError::InvalidResponse);
3050 }
3051 }
3052 Ok(ApplicationRecord {
3053 entity: record.entity,
3054 fields,
3055 })
3056 })
3057 .collect::<Result<Vec<_>, ApplicationClientError>>()?;
3058 if fields
3059 .insert(
3060 field.name,
3061 ApplicationResultField {
3062 cardinality,
3063 records,
3064 },
3065 )
3066 .is_some()
3067 {
3068 return Err(ApplicationClientError::InvalidResponse);
3069 }
3070 }
3071 Ok(fields)
3072}
3073
3074fn raise_compact_query_field(
3075 field: app_v1::CompactResultField,
3076) -> Result<BTreeMap<String, ApplicationResultField>, ApplicationClientError> {
3077 if field.name.is_empty() || field.entity.is_empty() || field.fields.is_empty() {
3078 return Err(ApplicationClientError::InvalidResponse);
3079 }
3080 let cardinality = match app_v1::ResultCardinality::try_from(field.cardinality) {
3081 Ok(app_v1::ResultCardinality::Many) => ApplicationCardinality::Many,
3082 _ => return Err(ApplicationClientError::InvalidResponse),
3083 };
3084 let mut names = BTreeSet::new();
3085 for name in &field.fields {
3086 if name.is_empty() || !names.insert(name.as_str()) {
3087 return Err(ApplicationClientError::InvalidResponse);
3088 }
3089 }
3090 let width = field.fields.len();
3091 let records = field
3092 .rows
3093 .into_iter()
3094 .map(|row| {
3095 if row.values.len() != width {
3096 return Err(ApplicationClientError::InvalidResponse);
3097 }
3098 let fields = field
3099 .fields
3100 .iter()
3101 .cloned()
3102 .zip(row.values)
3103 .map(|(name, value)| Ok((name, raise_value(value)?)))
3104 .collect::<Result<BTreeMap<_, _>, ApplicationClientError>>()?;
3105 Ok(ApplicationRecord {
3106 entity: field.entity.clone(),
3107 fields,
3108 })
3109 })
3110 .collect::<Result<Vec<_>, ApplicationClientError>>()?;
3111 Ok(BTreeMap::from([(
3112 field.name,
3113 ApplicationResultField {
3114 cardinality,
3115 records,
3116 },
3117 )]))
3118}
3119
3120#[doc(hidden)]
3121pub fn raise_value(value: v1::Value) -> Result<ApplicationValue, ApplicationClientError> {
3122 use v1::value::Kind;
3123 match value.kind.ok_or(ApplicationClientError::InvalidResponse)? {
3124 Kind::NullValue(_) => Ok(ApplicationValue::Null),
3125 Kind::BoolValue(value) => Ok(ApplicationValue::Bool(value)),
3126 Kind::I64Value(value) => Ok(ApplicationValue::I64(value)),
3127 Kind::U64Value(value) => Ok(ApplicationValue::U64(value)),
3128 Kind::DecimalValue(value) => Ok(ApplicationValue::Decimal {
3129 coefficient_twos_complement: value.coefficient_twos_complement,
3130 scale: value.scale,
3131 precision: value.precision,
3132 }),
3133 Kind::MoneyValue(value) => {
3134 let amount = value
3135 .amount
3136 .ok_or(ApplicationClientError::InvalidResponse)?;
3137 Ok(ApplicationValue::Money {
3138 currency: value.currency,
3139 amount: Box::new(ApplicationValue::Decimal {
3140 coefficient_twos_complement: amount.coefficient_twos_complement,
3141 scale: amount.scale,
3142 precision: amount.precision,
3143 }),
3144 })
3145 }
3146 Kind::StringValue(value) => Ok(ApplicationValue::String(value)),
3147 Kind::BytesValue(value) => Ok(ApplicationValue::Bytes(value)),
3148 Kind::UuidValue(value) => {
3149 let bytes: [u8; 16] = value
3150 .as_slice()
3151 .try_into()
3152 .map_err(|_| ApplicationClientError::InvalidResponse)?;
3153 Ok(ApplicationValue::Uuid(ApplicationUuid::from_bytes(bytes)))
3154 }
3155 Kind::EnumValue(value) if !value.name.is_empty() => Ok(ApplicationValue::Enum(value.name)),
3156 Kind::ListValue(values) => Ok(ApplicationValue::List(
3157 values
3158 .values
3159 .into_iter()
3160 .map(raise_value)
3161 .collect::<Result<Vec<_>, _>>()?,
3162 )),
3163 Kind::RecordValue(record) => {
3164 let mut fields = BTreeMap::new();
3165 for field in record.fields {
3166 if field.name.is_empty()
3167 || fields
3168 .insert(
3169 field.name,
3170 raise_value(
3171 field.value.ok_or(ApplicationClientError::InvalidResponse)?,
3172 )?,
3173 )
3174 .is_some()
3175 {
3176 return Err(ApplicationClientError::InvalidResponse);
3177 }
3178 }
3179 Ok(ApplicationValue::Record(fields))
3180 }
3181 Kind::DateValue(value) => Ok(ApplicationValue::Date(value.days_since_unix_epoch)),
3182 Kind::TimestampValue(value) if value.nanos < 1_000_000_000 => {
3183 Ok(ApplicationValue::Timestamp {
3184 seconds: value.seconds,
3185 nanos: value.nanos,
3186 })
3187 }
3188 Kind::VectorValue(value) => riffdb_types::CanonicalVector::new(value.components)
3189 .map(ApplicationValue::Vector)
3190 .map_err(|_| ApplicationClientError::InvalidResponse),
3191 Kind::TimestampValue(_) | Kind::EnumValue(_) => {
3192 Err(ApplicationClientError::InvalidResponse)
3193 }
3194 }
3195}
3196
3197fn format_uuid_text(bytes: &[u8; 16]) -> String {
3198 const HEX: &[u8; 16] = b"0123456789abcdef";
3199 let mut output = String::with_capacity(36);
3200 for (index, byte) in bytes.iter().copied().enumerate() {
3201 if matches!(index, 4 | 6 | 8 | 10) {
3202 output.push('-');
3203 }
3204 output.push(char::from(HEX[usize::from(byte >> 4)]));
3205 output.push(char::from(HEX[usize::from(byte & 0x0f)]));
3206 }
3207 output
3208}
3209
3210fn parse_uuid(value: &str) -> Result<[u8; 16], ApplicationClientError> {
3211 if value.len() != 36 {
3212 return Err(ApplicationClientError::InvalidInput);
3213 }
3214 let bytes = value.as_bytes();
3215 if [8, 13, 18, 23]
3216 .into_iter()
3217 .any(|index| bytes[index] != b'-')
3218 {
3219 return Err(ApplicationClientError::InvalidInput);
3220 }
3221 let mut output = [0_u8; 16];
3222 let mut encoded = bytes.iter().copied().filter(|byte| *byte != b'-');
3223 for byte in &mut output {
3224 let high = encoded
3225 .next()
3226 .and_then(hex_nibble)
3227 .ok_or(ApplicationClientError::InvalidInput)?;
3228 let low = encoded
3229 .next()
3230 .and_then(hex_nibble)
3231 .ok_or(ApplicationClientError::InvalidInput)?;
3232 *byte = (high << 4) | low;
3233 }
3234 encoded
3235 .next()
3236 .is_none()
3237 .then_some(output)
3238 .ok_or(ApplicationClientError::InvalidInput)
3239}
3240
3241const fn hex_nibble(byte: u8) -> Option<u8> {
3242 match byte {
3243 b'0'..=b'9' => Some(byte - b'0'),
3244 b'a'..=b'f' => Some(byte - b'a' + 10),
3245 _ => None,
3246 }
3247}
3248
3249#[cfg(test)]
3250mod tests {
3251 use super::*;
3252 use riffdb_errors::ApplicationErrorCode;
3253
3254 #[test]
3255 fn exact_tonic_endpoint_defaults_tcp_nodelay_on() {
3256 let endpoint = Endpoint::from_static("http://127.0.0.1:7443");
3260 assert!(endpoint.get_tcp_nodelay());
3261 }
3262
3263 #[test]
3264 fn named_query_builder_is_name_addressed_and_module_pinned() {
3265 let mut parameters = BTreeMap::new();
3266 parameters.insert(
3267 "organization_id".to_owned(),
3268 ApplicationValue::String("01900000-0000-7000-8000-000000000001".to_owned()),
3269 );
3270 let query = NamedQuery::new(
3271 ApplicationContract::Exact {
3272 lineage: "TicketDesk".to_owned(),
3273 version: 1,
3274 bundle_hash: None,
3275 },
3276 "TicketPage",
3277 Some([7; 32]),
3278 parameters,
3279 None,
3280 )
3281 .expect("query");
3282 assert_eq!(query.name, "TicketPage");
3283 assert_eq!(query.module_hash, Some([7; 32]));
3284 }
3285
3286 #[test]
3287 fn generated_query_options_preserve_cursor_and_read_fence() {
3288 let query = NamedQuery::new(
3289 ApplicationContract::Exact {
3290 lineage: "TicketDesk".to_owned(),
3291 version: 1,
3292 bundle_hash: Some([9; 32]),
3293 },
3294 "TicketPage",
3295 Some([7; 32]),
3296 BTreeMap::new(),
3297 None,
3298 )
3299 .expect("query")
3300 .with_options(
3301 QueryOptions::new()
3302 .after("opaque-cursor")
3303 .read_after_commit(41)
3304 .at_least_admission_head(),
3305 )
3306 .expect("options");
3307 assert_eq!(query.cursor.as_deref(), Some("opaque-cursor"));
3308 assert_eq!(query.minimum_application_head, Some(41));
3309 assert!(query.admission_head_consistency);
3310 assert!(
3311 NamedQuery::new(
3312 ApplicationContract::Active,
3313 "TicketPage",
3314 Some([7; 32]),
3315 BTreeMap::new(),
3316 None,
3317 )
3318 .expect("query")
3319 .with_options(QueryOptions::new().read_after_commit(0))
3320 .is_err()
3321 );
3322 }
3323
3324 #[test]
3326 fn generated_cursor_routing_preserves_low_level_options_and_rejects_collision() {
3327 let routed = QueryOptions::new()
3328 .read_after_commit(41)
3329 .at_least_admission_head()
3330 .with_generated_cursor(Some("generated-cursor".to_owned()))
3331 .expect("generated cursor");
3332 assert_eq!(routed.cursor.as_deref(), Some("generated-cursor"));
3333 assert_eq!(routed.read_after_commit, Some(41));
3334 assert!(routed.admission_head);
3335
3336 let first_page = QueryOptions::new()
3337 .read_after_commit(42)
3338 .with_generated_cursor(None)
3339 .expect("first page");
3340 assert_eq!(first_page.cursor, None);
3341 assert_eq!(first_page.read_after_commit, Some(42));
3342
3343 assert!(matches!(
3344 QueryOptions::new()
3345 .after("low-level-cursor")
3346 .with_generated_cursor(Some("generated-cursor".to_owned())),
3347 Err(ApplicationClientError::InvalidInput)
3348 ));
3349 }
3350
3351 #[test]
3352 fn query_response_raises_only_names_and_typed_values() {
3353 let response = app_v1::ExecuteQueryResponse {
3354 identity: Some(app_v1::QueryIdentity {
3355 contract_lineage: "TicketDesk".to_owned(),
3356 contract_version: 1,
3357 contract_bundle_hash: vec![1; 32],
3358 query_name: Some("TicketPage".to_owned()),
3359 plan_hash: vec![2; 32],
3360 module_hash: Some(vec![3; 32]),
3361 }),
3362 outcome: "Found".to_owned(),
3363 application_head: 4,
3364 fields: vec![app_v1::ResultField {
3365 name: "ticket".to_owned(),
3366 cardinality: app_v1::ResultCardinality::One as i32,
3367 records: vec![app_v1::ResultRecord {
3368 fields: vec![app_v1::Parameter {
3369 name: "title".to_owned(),
3370 value: Some(v1::Value {
3371 kind: Some(v1::value::Kind::StringValue("Hello".to_owned())),
3372 }),
3373 }],
3374 entity: "Ticket".to_owned(),
3375 }],
3376 }],
3377 next_cursor: None,
3378 selected_result_encoding: app_v1::NamedResultEncoding::LegacyRecords as i32,
3379 compact_result: None,
3380 packed_result: None,
3381 };
3382 let result = raise_query_result(response).expect("result");
3383 assert_eq!(result.outcome, "Found");
3384 assert_eq!(result.identity.contract_lineage, "TicketDesk");
3385 assert_eq!(result.identity.contract_version, 1);
3386 assert_eq!(result.identity.contract_bundle_hash, [1; 32]);
3387 assert_eq!(result.identity.module_hash, [3; 32]);
3388 assert_eq!(result.identity.query_name, "TicketPage");
3389 assert_eq!(result.identity.plan_hash, [2; 32]);
3390 assert_eq!(
3391 result.fields["ticket"].records[0].fields["title"],
3392 ApplicationValue::String("Hello".to_owned())
3393 );
3394 }
3395
3396 #[test]
3397 fn generated_identity_expectations_reject_contract_module_and_name_drift() {
3398 let response = app_v1::ExecuteQueryResponse {
3399 identity: Some(app_v1::QueryIdentity {
3400 contract_lineage: "TicketDesk".to_owned(),
3401 contract_version: 1,
3402 contract_bundle_hash: vec![1; 32],
3403 query_name: Some("TicketPage".to_owned()),
3404 plan_hash: vec![2; 32],
3405 module_hash: Some(vec![3; 32]),
3406 }),
3407 ..Default::default()
3408 };
3409 let contract = ApplicationContract::Exact {
3410 lineage: "TicketDesk".to_owned(),
3411 version: 1,
3412 bundle_hash: Some([1; 32]),
3413 };
3414 validate_query_response_identity(
3415 &response,
3416 &contract,
3417 "TicketPage",
3418 Some([3; 32]),
3419 Some([2; 32]),
3420 )
3421 .expect("exact identity");
3422 assert!(matches!(
3423 validate_query_response_identity(
3424 &response,
3425 &contract,
3426 "Other",
3427 Some([3; 32]),
3428 Some([2; 32])
3429 ),
3430 Err(ApplicationClientError::InvalidResponse)
3431 ));
3432 assert!(matches!(
3433 validate_query_response_identity(
3434 &response,
3435 &contract,
3436 "TicketPage",
3437 Some([4; 32]),
3438 Some([2; 32])
3439 ),
3440 Err(ApplicationClientError::InvalidResponse)
3441 ));
3442 assert!(matches!(
3443 validate_query_response_identity(
3444 &response,
3445 &contract,
3446 "TicketPage",
3447 Some([3; 32]),
3448 Some([4; 32])
3449 ),
3450 Err(ApplicationClientError::InvalidResponse)
3451 ));
3452 let changed_contract = ApplicationContract::Exact {
3453 lineage: "TicketDesk".to_owned(),
3454 version: 1,
3455 bundle_hash: Some([9; 32]),
3456 };
3457 assert!(matches!(
3458 validate_query_response_identity(
3459 &response,
3460 &changed_contract,
3461 "TicketPage",
3462 Some([3; 32]),
3463 Some([2; 32])
3464 ),
3465 Err(ApplicationClientError::InvalidResponse)
3466 ));
3467 }
3468
3469 #[test]
3470 fn application_catalog_preflight_is_exact_closed_and_name_only() {
3471 let contract = ApplicationContract::Exact {
3472 lineage: "TicketDesk".to_owned(),
3473 version: 3,
3474 bundle_hash: Some([7; 32]),
3475 };
3476 let response = app_v1::GetApplicationCatalogResponse {
3477 schema: APPLICATION_CATALOG_SCHEMA_V1.to_owned(),
3478 contract_lineage: "TicketDesk".to_owned(),
3479 contract_version: 3,
3480 contract_bundle_hash: vec![7; 32],
3481 query_module_hashes: vec![vec![3; 32], vec![5; 32]],
3482 features: [
3483 app_v1::ApplicationCatalogFeature::OperationalOptionalPredicates,
3484 app_v1::ApplicationCatalogFeature::StableCursorPages,
3485 app_v1::ApplicationCatalogFeature::NullExistencePredicates,
3486 app_v1::ApplicationCatalogFeature::BinaryTextPrefix,
3487 app_v1::ApplicationCatalogFeature::UnicodeFoldTextPrefixV1,
3488 app_v1::ApplicationCatalogFeature::ExactAggregates,
3489 ]
3490 .into_iter()
3491 .map(|feature| app_v1::ApplicationCatalogFeatureView {
3492 feature: feature as i32,
3493 state: if feature == app_v1::ApplicationCatalogFeature::UnicodeFoldTextPrefixV1 {
3494 app_v1::ApplicationCatalogFeatureState::Unavailable as i32
3495 } else {
3496 app_v1::ApplicationCatalogFeatureState::Available as i32
3497 },
3498 })
3499 .collect(),
3500 ..Default::default()
3501 };
3502
3503 let preflight =
3504 raise_application_catalog_preflight(response, &contract).expect("preflight");
3505 assert_eq!(preflight.contract_lineage, "TicketDesk");
3506 assert_eq!(preflight.contract_version, 3);
3507 assert_eq!(preflight.contract_bundle_hash, [7; 32]);
3508 assert_eq!(preflight.query_module_hashes, vec![[3; 32], [5; 32]]);
3509 assert!(preflight.is_available(ApplicationCatalogFeature::BinaryTextPrefix));
3510 assert!(!preflight.is_available(ApplicationCatalogFeature::UnicodeFoldTextPrefixV1));
3511 }
3512
3513 #[test]
3514 fn application_catalog_preflight_rejects_identity_and_registry_drift() {
3515 let contract = ApplicationContract::Exact {
3516 lineage: "TicketDesk".to_owned(),
3517 version: 3,
3518 bundle_hash: Some([7; 32]),
3519 };
3520 let response = app_v1::GetApplicationCatalogResponse {
3521 schema: APPLICATION_CATALOG_SCHEMA_V1.to_owned(),
3522 contract_lineage: "TicketDesk".to_owned(),
3523 contract_version: 4,
3524 contract_bundle_hash: vec![7; 32],
3525 ..Default::default()
3526 };
3527 assert!(matches!(
3528 raise_application_catalog_preflight(response, &contract),
3529 Err(ApplicationClientError::InvalidResponse)
3530 ));
3531
3532 let response = app_v1::GetApplicationCatalogResponse {
3533 schema: APPLICATION_CATALOG_SCHEMA_V1.to_owned(),
3534 contract_lineage: "TicketDesk".to_owned(),
3535 contract_version: 3,
3536 contract_bundle_hash: vec![7; 32],
3537 query_module_hashes: vec![vec![5; 32], vec![3; 32]],
3538 ..Default::default()
3539 };
3540 assert!(matches!(
3541 raise_application_catalog_preflight(response, &contract),
3542 Err(ApplicationClientError::InvalidResponse)
3543 ));
3544 }
3545
3546 #[test]
3547 fn generated_batch_bounds_and_resume_checkpoint_are_closed() {
3548 let options = GeneratedBatchOptions::new(MAX_GENERATED_BATCH_CONCURRENCY)
3549 .expect("maximum concurrency")
3550 .with_checkpoint(4_096);
3551 options.validate(4_096).expect("complete checkpoint");
3552 assert_eq!(options.checkpoint, 4_096);
3553
3554 assert_eq!(
3555 GeneratedBatchOptions::new(0),
3556 Err(GeneratedBatchError::InvalidBounds)
3557 );
3558 assert_eq!(
3559 GeneratedBatchOptions::new(MAX_GENERATED_BATCH_CONCURRENCY + 1),
3560 Err(GeneratedBatchError::InvalidBounds)
3561 );
3562 assert_eq!(
3563 GeneratedBatchOptions::new(1)
3564 .expect("options")
3565 .with_checkpoint(2)
3566 .validate(1),
3567 Err(GeneratedBatchError::InvalidBounds)
3568 );
3569 assert_eq!(
3570 GeneratedBatchOptions::new(1)
3571 .expect("options")
3572 .validate(4_097),
3573 Err(GeneratedBatchError::InvalidBounds)
3574 );
3575 }
3576
3577 #[test]
3578 fn generated_transport_batches_never_exceed_the_item_concurrency_bound() {
3579 for item_concurrency in 1..=MAX_GENERATED_BATCH_CONCURRENCY {
3580 let (batch_size, transport_concurrency) =
3581 generated_transport_batch_policy(item_concurrency);
3582 assert!((1..=MAX_GENERATED_TRANSPORT_BATCH_ITEMS).contains(&batch_size));
3583 assert!(transport_concurrency > 0);
3584 assert!(batch_size * transport_concurrency <= item_concurrency);
3585 }
3586 assert_eq!(generated_transport_batch_policy(64), (16, 4));
3587 assert_eq!(generated_transport_batch_policy(128), (16, 8));
3588 assert_eq!(generated_transport_batch_policy(384), (16, 24));
3589 assert_eq!(generated_transport_batch_policy(17), (8, 2));
3590 }
3591
3592 #[test]
3593 fn ordinary_transport_batch_bound_is_closed_before_submission() {
3594 assert_eq!(
3595 validate_idempotent_transport_batch_len(0),
3596 Err(IdempotentTransportBatchError::Empty)
3597 );
3598 assert_eq!(validate_idempotent_transport_batch_len(1), Ok(()));
3599 assert_eq!(
3600 validate_idempotent_transport_batch_len(MAX_GENERATED_TRANSPORT_BATCH_ITEMS),
3601 Ok(())
3602 );
3603 assert_eq!(
3604 validate_idempotent_transport_batch_len(MAX_GENERATED_TRANSPORT_BATCH_ITEMS + 1),
3605 Err(IdempotentTransportBatchError::TooManyItems)
3606 );
3607 }
3608
3609 #[derive(Clone)]
3610 struct StubBatchCommand {
3611 outcome: &'static str,
3612 key: &'static str,
3613 }
3614
3615 impl GeneratedCommand for StubBatchCommand {
3616 type Outcome = String;
3617
3618 fn idempotent_command(
3619 &self,
3620 ) -> Result<crate::IdempotentCommand, crate::generated::GeneratedCommandError> {
3621 crate::IdempotentCommand::new(
3622 "OkCommand",
3623 Some(1),
3624 v1::Value {
3625 kind: Some(v1::value::Kind::RecordValue(v1::ValueRecord {
3626 fields: vec![v1::ValueField {
3627 field_id: Some(1),
3628 name: String::new(),
3629 value: Some(v1::Value {
3630 kind: Some(v1::value::Kind::StringValue(self.key.to_owned())),
3631 }),
3632 }],
3633 })),
3634 },
3635 )
3636 .map_err(|_| crate::generated::GeneratedCommandError::InvalidInputShape)
3637 }
3638
3639 fn outcome_request(
3640 &self,
3641 request_id: riffdb_types::RequestId,
3642 ) -> Result<v1::GetOutcomeRequest, crate::generated::GeneratedCommandError> {
3643 Ok(v1::GetOutcomeRequest {
3644 request_id: request_id.into_bytes().to_vec(),
3645 contract_lineage: "lineage".to_owned(),
3646 command_name: "OkCommand".to_owned(),
3647 idempotency_key: self.key.to_owned(),
3648 outcome_uri: None,
3649 })
3650 }
3651
3652 fn decode_outcome(
3653 &self,
3654 response: &v1::ExecuteCommandResponse,
3655 ) -> Result<Self::Outcome, crate::generated::GeneratedCommandError> {
3656 if response.outcome_type == self.outcome {
3657 Ok(self.outcome.to_owned())
3658 } else {
3659 Err(crate::generated::GeneratedCommandError::InvalidOutcomeShape)
3660 }
3661 }
3662
3663 fn workflow_successor_revisions(
3664 &self,
3665 response: &v1::ExecuteCommandResponse,
3666 ) -> Result<
3667 Vec<crate::generated::WorkflowSuccessorRevision>,
3668 crate::generated::GeneratedCommandError,
3669 > {
3670 if response.outcome_type == "Workflow" {
3671 Ok(vec![
3672 crate::generated::WorkflowSuccessorRevision::generated("work", 8),
3673 ])
3674 } else {
3675 Ok(Vec::new())
3676 }
3677 }
3678 }
3679
3680 fn stub_command(outcome: &'static str, key: &'static str) -> StubBatchCommand {
3681 StubBatchCommand { outcome, key }
3682 }
3683
3684 fn stub_response(outcome: &str) -> v1::ExecuteCommandResponse {
3685 v1::ExecuteCommandResponse {
3686 status: v1::execute_command_response::CompletionStatus::ExecutedReadOnly as i32,
3687 commit_sequence: 0,
3688 contract_version: 1,
3689 plan_hash: vec![0x44; 32],
3690 outcome_type: outcome.to_owned(),
3691 outcome: Some(v1::Value {
3692 kind: Some(v1::value::Kind::NullValue(v1::NullValue::NullValue as i32)),
3693 }),
3694 provenance_uri: String::new(),
3695 durability_mode: String::new(),
3696 outcome_uri: None,
3697 history_incarnation: 1,
3698 }
3699 }
3700
3701 #[test]
3702 fn typed_command_results_preserve_generated_workflow_revision_evidence() {
3703 let command = stub_command("Workflow", "workflow-key");
3704 let response = stub_response("Workflow");
3705 let outcome = command.decode_outcome(&response).expect("outcome");
3706 let result = typed_command_result(&command, outcome, response).expect("typed result");
3707
3708 assert_eq!(result.workflow_revisions.len(), 1);
3709 assert_eq!(result.workflow_revisions[0].binding(), "work");
3710 assert_eq!(result.workflow_revisions[0].revision(), 8);
3711 }
3712
3713 fn stub_error_item(code: ApplicationErrorCode) -> v1::ExecuteCommandBatchItem {
3714 v1::ExecuteCommandBatchItem {
3715 result: Some(v1::execute_command_batch_item::Result::Error(
3716 riffdb_proto::application_error_to_proto(&ApplicationError::new(
3717 code,
3718 ApplicationOperation::BatchCommand,
3719 ApplicationErrorContext::empty(),
3720 None,
3721 )),
3722 )),
3723 }
3724 }
3725
3726 fn application_overloaded() -> ClientError {
3727 ClientError::Application(Box::new(ApplicationError::new(
3728 ApplicationErrorCode::Overloaded,
3729 ApplicationOperation::BatchCommand,
3730 ApplicationErrorContext::empty(),
3731 None,
3732 )))
3733 }
3734
3735 fn block_on_with_time<T>(future: impl std::future::Future<Output = T>) -> T {
3736 tokio::runtime::Builder::new_current_thread()
3737 .enable_time()
3738 .build()
3739 .expect("test runtime")
3740 .block_on(future)
3741 }
3742
3743 struct ScriptedExecuteAttempts {
3745 results: std::collections::VecDeque<Result<v1::ExecuteCommandResponse, ClientError>>,
3746 requests: Vec<v1::ExecuteCommandRequest>,
3747 }
3748
3749 impl ScriptedExecuteAttempts {
3750 fn new(
3751 results: impl IntoIterator<Item = Result<v1::ExecuteCommandResponse, ClientError>>,
3752 ) -> Self {
3753 Self {
3754 results: results.into_iter().collect(),
3755 requests: Vec::new(),
3756 }
3757 }
3758 }
3759
3760 impl crate::client::ExecuteRetryAttempt for ScriptedExecuteAttempts {
3761 async fn submit_execute_attempt(
3762 &mut self,
3763 request: v1::ExecuteCommandRequest,
3764 _metadata: &CallMetadata,
3765 ) -> Result<v1::ExecuteCommandResponse, ClientError> {
3766 self.requests.push(request);
3767 self.results.pop_front().expect("scripted execute result")
3768 }
3769 }
3770
3771 struct FixedRequestIds {
3772 values: std::collections::VecDeque<riffdb_types::RequestId>,
3773 }
3774
3775 impl crate::client::RetryRequestIdSource for FixedRequestIds {
3776 fn next_request_id(
3777 &mut self,
3778 ) -> Result<riffdb_types::RequestId, crate::IdentifierGenerationError> {
3779 self.values
3780 .pop_front()
3781 .ok_or(crate::IdentifierGenerationError::EntropyUnavailable)
3782 }
3783 }
3784
3785 fn request_id(ordinal: u8) -> riffdb_types::RequestId {
3786 riffdb_types::RequestId::from_unix_milliseconds_and_random(
3787 u64::from(ordinal),
3788 [ordinal; 10],
3789 )
3790 .expect("request id")
3791 }
3792
3793 #[test]
3794 fn batch_items_non_retryable_errors_surface_without_reentry_candidates() {
3795 let prepared = vec![
3796 (0, stub_command("Ok", "k0")),
3797 (1, stub_command("Ok", "k1")),
3798 (2, stub_command("Ok", "k2")),
3799 ];
3800 let items = vec![
3801 v1::ExecuteCommandBatchItem {
3802 result: Some(v1::execute_command_batch_item::Result::Response(
3803 stub_response("Ok"),
3804 )),
3805 },
3806 stub_error_item(ApplicationErrorCode::InputInvalid),
3807 stub_error_item(ApplicationErrorCode::AuthorizationDenied),
3808 ];
3809 let (resolved, reenter) = resolve_generated_batch_items(prepared, items);
3810 assert!(reenter.is_empty(), "non-retryable errors must not re-enter");
3811 assert_eq!(resolved.len(), 3);
3812 assert!(resolved[0].1.is_ok());
3813 assert!(matches!(
3814 &resolved[1].1,
3815 Err(GeneratedExecutionError::Client(ClientError::Application(error)))
3816 if error.code() == ApplicationErrorCode::InputInvalid
3817 ));
3818 assert!(matches!(
3819 &resolved[2].1,
3820 Err(GeneratedExecutionError::Client(ClientError::Application(error)))
3821 if error.code() == ApplicationErrorCode::AuthorizationDenied
3822 ));
3823 assert_eq!(
3824 resolved.iter().map(|(index, _)| *index).collect::<Vec<_>>(),
3825 vec![0, 1, 2]
3826 );
3827 }
3828
3829 #[test]
3830 fn batch_items_registry_retryable_errors_are_reentry_candidates() {
3831 let prepared = vec![
3832 (0, stub_command("A", "k0")),
3833 (1, stub_command("B", "k1")),
3834 (2, stub_command("C", "k2")),
3835 (3, stub_command("D", "k3")),
3836 ];
3837 let items = vec![
3838 v1::ExecuteCommandBatchItem {
3839 result: Some(v1::execute_command_batch_item::Result::Response(
3840 stub_response("A"),
3841 )),
3842 },
3843 stub_error_item(ApplicationErrorCode::OutcomeUnknown),
3844 stub_error_item(ApplicationErrorCode::Overloaded),
3845 stub_error_item(ApplicationErrorCode::StorageUnavailable),
3846 ];
3847 let (resolved, reenter) = resolve_generated_batch_items(prepared, items);
3848 assert_eq!(resolved.len(), 1);
3849 assert!(resolved[0].1.is_ok());
3850 assert_eq!(
3851 reenter
3852 .iter()
3853 .map(|candidate| candidate.index)
3854 .collect::<Vec<_>>(),
3855 vec![1, 2, 3],
3856 "Retry and ResolveWithSameIdempotencyKey must re-enter"
3857 );
3858 assert!(reenter.iter().all(|c| c.protocol_failure.is_none()));
3859 assert_eq!(
3860 ApplicationErrorCode::Overloaded.recovery_action(),
3861 ApplicationRecoveryAction::Retry
3862 );
3863 assert_eq!(
3864 ApplicationErrorCode::OutcomeUnknown.recovery_action(),
3865 ApplicationRecoveryAction::ResolveWithSameIdempotencyKey
3866 );
3867 }
3868
3869 #[test]
3870 fn batch_items_corrupt_or_unset_error_arms_reenter() {
3871 let prepared = vec![
3872 (0, stub_command("A", "k0")),
3873 (1, stub_command("B", "k1")),
3874 (2, stub_command("C", "k2")),
3875 ];
3876 let corrupt = v1::ExecuteCommandBatchItem {
3877 result: Some(v1::execute_command_batch_item::Result::Error(
3878 app_v1::ApplicationError {
3879 envelope_version: 0,
3880 code: 0,
3881 category: 0,
3882 recovery_action: 0,
3883 operation: 0,
3884 ..Default::default()
3885 },
3886 )),
3887 };
3888 let items = vec![
3889 v1::ExecuteCommandBatchItem {
3890 result: Some(v1::execute_command_batch_item::Result::Response(
3891 stub_response("A"),
3892 )),
3893 },
3894 corrupt,
3895 v1::ExecuteCommandBatchItem { result: None },
3896 ];
3897 let (resolved, reenter) = resolve_generated_batch_items(prepared, items);
3898 assert_eq!(resolved.len(), 1);
3899 assert_eq!(
3900 reenter
3901 .iter()
3902 .map(|candidate| candidate.index)
3903 .collect::<Vec<_>>(),
3904 vec![1, 2]
3905 );
3906 assert_eq!(
3907 reenter[0].protocol_failure.map(|f| f.kind()),
3908 Some(crate::ProtocolFailureKind::InvalidApplicationErrorDetails)
3909 );
3910 assert_eq!(
3911 reenter[1].protocol_failure.map(|f| f.kind()),
3912 Some(crate::ProtocolFailureKind::InvalidInboundMessage)
3913 );
3914 }
3915
3916 #[test]
3917 fn complete_batch_items_retries_all_overloaded_through_real_budget_and_succeeds() {
3918 use std::collections::HashMap;
3919 use std::sync::{Arc, Mutex};
3920
3921 let prepared = vec![
3922 (0, stub_command("Ok", "k0")),
3923 (1, stub_command("Ok", "k1")),
3924 (2, stub_command("Ok", "k2")),
3925 ];
3926 let items = vec![
3927 stub_error_item(ApplicationErrorCode::Overloaded),
3928 stub_error_item(ApplicationErrorCode::Overloaded),
3929 stub_error_item(ApplicationErrorCode::Overloaded),
3930 ];
3931 let attempt_counts: Arc<Mutex<HashMap<usize, usize>>> =
3932 Arc::new(Mutex::new(HashMap::new()));
3933 let recovered = block_on_with_time(complete_batch_items_with_reentry(
3934 prepared,
3935 items,
3936 |index, command| {
3937 let attempt_counts = Arc::clone(&attempt_counts);
3938 async move {
3939 let generic = command.idempotent_command().expect("stub command shape");
3940 let success = stub_response(command.outcome);
3941 let mut attempts = ScriptedExecuteAttempts::new([
3942 Err(application_overloaded()),
3943 Ok(success.clone()),
3944 ]);
3945 let mut ids = FixedRequestIds {
3946 values: [request_id(10 + index as u8), request_id(40 + index as u8)]
3947 .into_iter()
3948 .collect(),
3949 };
3950 let response = crate::client::execute_retry_attempts(
3951 &mut attempts,
3952 &generic,
3953 AttemptBudget::new(3).expect("budget"),
3954 &CallMetadata::default(),
3955 &mut ids,
3956 )
3957 .await
3958 .expect("capacity frees on second attempt");
3959 attempt_counts
3960 .lock()
3961 .expect("counts")
3962 .insert(index, attempts.requests.len());
3963 let outcome = command
3964 .decode_outcome(&response)
3965 .map_err(GeneratedExecutionError::CommandShape)
3966 .and_then(|outcome| typed_command_result(&command, outcome, response));
3967 (index, outcome)
3968 }
3969 },
3970 ));
3971 assert_eq!(recovered.len(), 3);
3972 assert!(recovered.iter().all(|(_, result)| result.is_ok()));
3973 let counts = attempt_counts.lock().expect("counts");
3974 assert_eq!(counts.len(), 3);
3975 assert!(
3976 counts.values().all(|count| *count > 1),
3977 "mock must record multi-attempt real RetryState path: {counts:?}"
3978 );
3979 }
3980
3981 #[test]
3982 fn complete_batch_items_real_budget_exhaustion_surfaces_err_while_siblings_succeed() {
3983 use std::sync::Arc;
3984 use std::sync::atomic::{AtomicUsize, Ordering};
3985
3986 let prepared = vec![
3987 (0, stub_command("Ok", "k0")),
3988 (1, stub_command("Fail", "k1")),
3989 (2, stub_command("Ok", "k2")),
3990 ];
3991 let items = vec![
3992 v1::ExecuteCommandBatchItem {
3993 result: Some(v1::execute_command_batch_item::Result::Response(
3994 stub_response("Ok"),
3995 )),
3996 },
3997 stub_error_item(ApplicationErrorCode::Overloaded),
3998 v1::ExecuteCommandBatchItem {
3999 result: Some(v1::execute_command_batch_item::Result::Response(
4000 stub_response("Ok"),
4001 )),
4002 },
4003 ];
4004 let reenter_attempts = Arc::new(AtomicUsize::new(0));
4005 let mut recovered = block_on_with_time(complete_batch_items_with_reentry(
4006 prepared,
4007 items,
4008 |index, command| {
4009 let reenter_attempts = Arc::clone(&reenter_attempts);
4010 async move {
4011 let generic = command.idempotent_command().expect("stub command shape");
4012 let mut attempts = ScriptedExecuteAttempts::new([
4015 Err(application_overloaded()),
4016 Err(application_overloaded()),
4017 ]);
4018 let mut ids = FixedRequestIds {
4019 values: [request_id(20), request_id(21)].into_iter().collect(),
4020 };
4021 let budget = AttemptBudget::new(2).expect("budget");
4022 let error = crate::client::execute_retry_attempts(
4023 &mut attempts,
4024 &generic,
4025 budget,
4026 &CallMetadata::default(),
4027 &mut ids,
4028 )
4029 .await
4030 .expect_err("budget exhausted");
4031 reenter_attempts.store(attempts.requests.len(), Ordering::SeqCst);
4032 assert_eq!(
4033 attempts.requests.len(),
4034 budget.maximum_submissions() as usize
4035 );
4036 (index, Err(GeneratedExecutionError::Client(error)))
4037 }
4038 },
4039 ));
4040 recovered.sort_by_key(|(index, _)| *index);
4041 assert_eq!(reenter_attempts.load(Ordering::SeqCst), 2);
4042 assert!(recovered[0].1.is_ok());
4043 assert!(matches!(
4044 &recovered[1].1,
4045 Err(GeneratedExecutionError::Client(ClientError::Application(error)))
4046 if error.code() == ApplicationErrorCode::Overloaded
4047 ));
4048 assert!(recovered[2].1.is_ok());
4049 }
4050
4051 #[test]
4052 fn complete_batch_items_corrupt_error_arm_reenters_and_can_succeed() {
4053 let prepared = vec![
4054 (0, stub_command("Ok", "k0")),
4055 (1, stub_command("Replay", "k1")),
4056 ];
4057 let items = vec![
4058 v1::ExecuteCommandBatchItem {
4059 result: Some(v1::execute_command_batch_item::Result::Response(
4060 stub_response("Ok"),
4061 )),
4062 },
4063 v1::ExecuteCommandBatchItem {
4064 result: Some(v1::execute_command_batch_item::Result::Error(
4065 app_v1::ApplicationError {
4066 envelope_version: 0,
4067 code: 0,
4068 category: 0,
4069 recovery_action: 0,
4070 operation: 0,
4071 ..Default::default()
4072 },
4073 )),
4074 },
4075 ];
4076 let mut recovered = block_on_with_time(complete_batch_items_with_reentry(
4077 prepared,
4078 items,
4079 |index, command| async move {
4080 (
4081 index,
4082 Ok(TypedCommandResult {
4083 outcome: command.outcome.to_owned(),
4084 commit_sequence: None,
4085 contract_version: 1,
4086 plan_hash: [0x55; 32],
4087 replayed: true,
4088 outcome_uri: None,
4089 workflow_revisions: Vec::new(),
4090 }),
4091 )
4092 },
4093 ));
4094 recovered.sort_by_key(|(index, _)| *index);
4095 assert_eq!(recovered.len(), 2);
4096 assert!(recovered[0].1.is_ok());
4097 assert_eq!(recovered[1].1.as_ref().expect("replayed").outcome, "Replay");
4098 }
4099
4100 #[test]
4101 fn complete_batch_items_corrupt_error_arm_surfaces_protocol_failure_on_reentry_exhaustion() {
4102 let prepared = vec![(0, stub_command("Replay", "k0"))];
4103 let items = vec![v1::ExecuteCommandBatchItem {
4104 result: Some(v1::execute_command_batch_item::Result::Error(
4105 app_v1::ApplicationError {
4106 envelope_version: 0,
4107 code: 0,
4108 category: 0,
4109 recovery_action: 0,
4110 operation: 0,
4111 ..Default::default()
4112 },
4113 )),
4114 }];
4115 let recovered = block_on_with_time(complete_batch_items_with_reentry(
4116 prepared,
4117 items,
4118 |index, _command| async move {
4119 (
4121 index,
4122 Err(GeneratedExecutionError::Client(ClientError::Application(
4123 Box::new(ApplicationError::new(
4124 ApplicationErrorCode::Overloaded,
4125 ApplicationOperation::BatchCommand,
4126 ApplicationErrorContext::empty(),
4127 None,
4128 )),
4129 ))),
4130 )
4131 },
4132 ));
4133 assert_eq!(recovered.len(), 1);
4134 assert!(matches!(
4135 &recovered[0].1,
4136 Err(GeneratedExecutionError::Client(ClientError::Protocol(failure)))
4137 if failure.kind() == crate::ProtocolFailureKind::InvalidApplicationErrorDetails
4138 ));
4139 }
4140
4141 #[test]
4142 fn record_generated_batch_item_completion_advances_checkpoint_in_production_order() {
4143 let mut items = Vec::new();
4147 let mut state = BatchCheckpointState::new(0);
4148 let mut progress = Vec::new();
4149 let total = 3usize;
4150
4151 let ok = |label: &str| {
4152 Ok(TypedCommandResult {
4153 outcome: label.to_owned(),
4154 commit_sequence: None,
4155 contract_version: 1,
4156 plan_hash: [0x11; 32],
4157 replayed: false,
4158 outcome_uri: None,
4159 workflow_revisions: Vec::new(),
4160 })
4161 };
4162 let err = Err(GeneratedExecutionError::Client(ClientError::Application(
4163 Box::new(ApplicationError::new(
4164 ApplicationErrorCode::InputInvalid,
4165 ApplicationOperation::BatchCommand,
4166 ApplicationErrorContext::empty(),
4167 None,
4168 )),
4169 )));
4170
4171 record_generated_batch_item_completion(
4174 2,
4175 ok("two"),
4176 total,
4177 &mut state,
4178 &mut items,
4179 &mut |p| progress.push(p.checkpoint),
4180 );
4181 assert_eq!(state.checkpoint, 0);
4182 record_generated_batch_item_completion(
4183 0,
4184 ok("zero"),
4185 total,
4186 &mut state,
4187 &mut items,
4188 &mut |p| progress.push(p.checkpoint),
4189 );
4190 assert_eq!(state.checkpoint, 1);
4191 record_generated_batch_item_completion(1, err, total, &mut state, &mut items, &mut |p| {
4192 progress.push(p.checkpoint)
4193 });
4194 assert_eq!(state.checkpoint, 3);
4195 assert_eq!(progress, vec![0, 1, 3]);
4196 assert_eq!(items.len(), 3);
4197 assert!(
4198 items
4199 .iter()
4200 .any(|item| item.index == 1 && item.result.is_err())
4201 );
4202 }
4203
4204 #[test]
4205 fn record_generated_batch_item_completion_respects_resumed_checkpoint_seed() {
4206 let mut items = Vec::new();
4207 let mut state = BatchCheckpointState::new(2);
4209 let mut progress = Vec::new();
4210 let total = 4usize;
4211 let ok = |label: &str| {
4212 Ok(TypedCommandResult {
4213 outcome: label.to_owned(),
4214 commit_sequence: None,
4215 contract_version: 1,
4216 plan_hash: [0x22; 32],
4217 replayed: false,
4218 outcome_uri: None,
4219 workflow_revisions: Vec::new(),
4220 })
4221 };
4222 record_generated_batch_item_completion(
4223 2,
4224 ok("two"),
4225 total,
4226 &mut state,
4227 &mut items,
4228 &mut |p| progress.push((p.completed, p.checkpoint)),
4229 );
4230 record_generated_batch_item_completion(
4231 3,
4232 ok("three"),
4233 total,
4234 &mut state,
4235 &mut items,
4236 &mut |p| progress.push((p.completed, p.checkpoint)),
4237 );
4238 assert_eq!(state.checkpoint, 4);
4239 assert_eq!(state.completed, 4);
4240 assert_eq!(progress, vec![(3, 3), (4, 4)]);
4241 }
4242
4243 #[test]
4246 fn raised_uuid_bytes_match_parsed_text_and_text_is_lazy() {
4247 let bytes = [
4248 0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef, 0x01, 0x23, 0x45, 0x67, 0x89, 0xab,
4249 0xcd, 0xef,
4250 ];
4251 let raised = raise_value(v1::Value {
4252 kind: Some(v1::value::Kind::UuidValue(bytes.to_vec())),
4253 })
4254 .expect("raise uuid");
4255 let ApplicationValue::Uuid(uuid) = raised else {
4256 panic!("expected uuid value");
4257 };
4258 assert!(!uuid.text_is_materialized(), "raise must not allocate text");
4259 assert_eq!(uuid.as_bytes(), &bytes);
4260 let text = uuid.as_str().to_owned();
4261 assert!(uuid.text_is_materialized());
4262 let from_text = ApplicationUuid::from_text(text.clone()).expect("parse text");
4263 assert_eq!(from_text.as_bytes(), &bytes);
4264 assert_eq!(from_text.into_string(), text);
4265 let re_lowered = lower_value(ApplicationValue::Uuid(ApplicationUuid::from_bytes(bytes)))
4267 .expect("lower bytes");
4268 let text_lowered = lower_value(ApplicationValue::Uuid(
4269 ApplicationUuid::from_text(text).expect("text"),
4270 ))
4271 .expect("lower text");
4272 assert_eq!(re_lowered, text_lowered);
4273 }
4274
4275 #[test]
4276 fn canonical_vector_values_round_trip_without_punning() {
4277 let vector =
4278 riffdb_types::CanonicalVector::new(vec![-0.0, 1.5, -2.25]).expect("canonical vector");
4279 let lowered = lower_value(ApplicationValue::Vector(vector.clone())).expect("lower");
4280 let Some(v1::value::Kind::VectorValue(wire)) = lowered.kind.as_ref() else {
4281 panic!("expected the dedicated vector wire arm");
4282 };
4283 assert_eq!(wire.components, vector.components());
4284 assert_eq!(
4285 raise_value(lowered).expect("raise"),
4286 ApplicationValue::Vector(vector)
4287 );
4288 }
4289
4290 #[test]
4291 fn non_finite_vector_response_is_rejected() {
4292 let result = raise_value(v1::Value {
4293 kind: Some(v1::value::Kind::VectorValue(v1::VectorValue {
4294 components: vec![f32::NAN],
4295 })),
4296 });
4297 assert!(matches!(
4298 result,
4299 Err(ApplicationClientError::InvalidResponse)
4300 ));
4301 }
4302
4303 #[test]
4305 fn driver_pool_and_application_client_failover_to_standby_and_route_replica_reads() {
4306 let runtime = tokio::runtime::Builder::new_current_thread()
4307 .enable_all()
4308 .build()
4309 .expect("current-thread runtime");
4310 runtime.block_on(async {
4311 let active_contract = |bundle_hash: [u8; 32]| v1::GetActiveContractResponse {
4312 result: Some(v1::get_active_contract_response::Result::Present(
4313 v1::ContractDescriptor {
4314 contract_lineage: "TicketDesk".to_owned(),
4315 contract_version: 1,
4316 bundle_hash: bundle_hash.to_vec(),
4317 ..v1::ContractDescriptor::default()
4318 },
4319 )),
4320 database_alias: "default".to_owned(),
4321 };
4322
4323 let primary = StableApplicationClient::scripted_for_test(active_contract([7; 32]))
4324 .push_scripted_command(Err(ClientError::Public(
4325 riffdb_errors::PublicError::primary_fenced(),
4326 )));
4327 let standby_mismatched =
4328 StableApplicationClient::scripted_for_test(active_contract([9; 32]))
4329 .push_scripted_command(Ok(stub_response("WrongStandby")));
4330 let query_response = |head: u64, outcome: &str| app_v1::ExecuteQueryResponse {
4331 identity: Some(app_v1::QueryIdentity {
4332 contract_lineage: "TicketDesk".to_owned(),
4333 contract_version: 1,
4334 contract_bundle_hash: vec![7; 32],
4335 query_name: Some("TicketPage".to_owned()),
4336 plan_hash: vec![2; 32],
4337 module_hash: Some(vec![3; 32]),
4338 }),
4339 outcome: outcome.to_owned(),
4340 application_head: head,
4341 fields: Vec::new(),
4342 next_cursor: None,
4343 selected_result_encoding: app_v1::NamedResultEncoding::LegacyRecords as i32,
4344 compact_result: None,
4345 packed_result: None,
4346 };
4347 let standby_promoted =
4348 StableApplicationClient::scripted_for_test(active_contract([7; 32]))
4349 .push_scripted_command(Ok(v1::ExecuteCommandResponse {
4350 status: v1::execute_command_response::CompletionStatus::Committed as i32,
4351 commit_sequence: 19,
4352 contract_version: 1,
4353 plan_hash: vec![0x44; 32],
4354 outcome_type: "Created".to_owned(),
4355 outcome: Some(v1::Value {
4356 kind: Some(v1::value::Kind::NullValue(
4357 v1::NullValue::NullValue as i32,
4358 )),
4359 }),
4360 provenance_uri: String::new(),
4361 durability_mode: String::new(),
4362 outcome_uri: Some("riffdb://outcomes/19".to_owned()),
4363 history_incarnation: 2,
4364 }))
4365 .push_scripted_query(Ok(query_response(50, "PrimaryCaughtUp")));
4366 let replica = StableApplicationClient::scripted_for_test(active_contract([7; 32]))
4367 .push_scripted_query(Ok(query_response(18, "ReplicaAvailable")))
4368 .push_scripted_query(Err(ClientError::Application(Box::new(
4369 ApplicationError::new(
4370 ApplicationErrorCode::FreshnessUnsatisfied,
4371 ApplicationOperation::ExecuteQuery,
4372 ApplicationErrorContext::empty(),
4373 None,
4374 ),
4375 ))));
4376
4377 assert!(matches!(
4378 primary.clone().with_standby_clients(vec![
4379 standby_promoted.clone(),
4380 standby_promoted.clone(),
4381 standby_promoted.clone(),
4382 standby_promoted.clone(),
4383 standby_promoted.clone(),
4384 ]),
4385 Err(ApplicationClientError::InvalidInput)
4386 ));
4387 assert!(matches!(
4388 primary.clone().with_read_replica_clients(vec![
4389 replica.clone(),
4390 replica.clone(),
4391 replica.clone(),
4392 replica.clone(),
4393 replica.clone(),
4394 ]),
4395 Err(ApplicationClientError::InvalidInput)
4396 ));
4397
4398 let mut client = primary
4399 .clone()
4400 .with_standby_clients(vec![standby_mismatched.clone(), standby_promoted.clone()])
4401 .expect("bounded standbys")
4402 .with_read_replica_clients(vec![replica.clone()])
4403 .expect("bounded replicas");
4404 assert_eq!(client.standby_client_count(), 2);
4405 assert_eq!(client.read_replica_client_count(), 1);
4406
4407 let metadata = CallMetadata::default();
4408 client
4409 .verify_active_contract("default", "TicketDesk", 1, [7; 32], &metadata)
4410 .await
4411 .expect("verify primary active contract");
4412
4413 let command = ApplicationCommand::new(
4414 "CreateTicket",
4415 Some(1),
4416 BTreeMap::from([(
4417 "title".to_owned(),
4418 ApplicationValue::String("Failover ticket".to_owned()),
4419 )]),
4420 )
4421 .expect("command");
4422 let result = client
4423 .execute_command(
4424 command,
4425 AttemptBudget::new(2).expect("attempt budget"),
4426 &metadata,
4427 )
4428 .await
4429 .expect("failover command execution");
4430 assert_eq!(result.outcome.as_deref(), Some("Created"));
4431 assert_eq!(result.commit_sequence, Some(19));
4432 assert_eq!(standby_mismatched.scripted_command_inputs().len(), 0);
4433 assert_eq!(primary.scripted_command_inputs().len(), 1);
4434 assert_eq!(
4435 primary.scripted_command_inputs(),
4436 standby_promoted.scripted_command_inputs(),
4437 "standby retry must preserve the exact command input and idempotency key from the fenced primary attempt"
4438 );
4439
4440 let base_query = NamedQuery::new(
4441 ApplicationContract::Exact {
4442 lineage: "TicketDesk".to_owned(),
4443 version: 1,
4444 bundle_hash: Some([7; 32]),
4445 },
4446 "TicketPage",
4447 Some([3; 32]),
4448 BTreeMap::new(),
4449 None,
4450 )
4451 .expect("query")
4452 .expect_plan_hash([2; 32]);
4453
4454 let replica_result = client
4455 .execute_named_query(base_query.clone(), &metadata)
4456 .await
4457 .expect("replica query");
4458 assert_eq!(replica_result.outcome, "ReplicaAvailable");
4459 assert_eq!(replica.scripted_queries().len(), 1);
4460 assert_eq!(standby_promoted.scripted_queries().len(), 0);
4461
4462 let causal_query = base_query
4463 .with_options(QueryOptions::new().read_after_commit(50))
4464 .expect("causal query");
4465 let fallback_result = client
4466 .execute_named_query(causal_query, &metadata)
4467 .await
4468 .expect("fallback to promoted primary on replica lag");
4469 assert_eq!(fallback_result.outcome, "PrimaryCaughtUp");
4470 assert_eq!(replica.scripted_queries().len(), 2);
4471 assert_eq!(standby_promoted.scripted_queries().len(), 1);
4472 });
4473 }
4474}