1use crate::generated::gapic_dataplane::client::Spanner as GapicSpanner;
16use crate::model::{
17 BeginTransactionRequest, CommitRequest, CommitResponse, CreateSessionRequest,
18 ExecuteBatchDmlRequest, ExecuteBatchDmlResponse, ExecuteSqlRequest, PartitionQueryRequest,
19 PartitionReadRequest, PartitionResponse, RollbackRequest, Session, Transaction,
20};
21use crate::omni::{InstanceType, is_plaintext_endpoint};
22use crate::server_streaming::builder;
23use gaxi::options::{ClientConfig, Credentials};
24use google_cloud_auth::credentials::anonymous;
25use google_cloud_gax::client_builder::ClientBuilder as GaxClientBuilder;
26use google_cloud_gax::client_builder::internal::new_builder;
27use google_cloud_gax::options::{
28 RequestOptions as GaxRequestOptions, internal::RequestOptionsExt as _,
29};
30use google_cloud_spanner_admin_database_v1::builder::database_admin::ClientBuilder as DatabaseAdminBuilder;
31use google_cloud_spanner_admin_instance_v1::builder::instance_admin::ClientBuilder as InstanceAdminBuilder;
32use http::{
33 HeaderMap,
34 header::{HeaderName, HeaderValue},
35};
36use std::sync::{
37 LazyLock,
38 atomic::{AtomicUsize, Ordering},
39};
40
41pub use crate::database_client::DatabaseClient;
42pub use google_cloud_spanner_admin_database_v1::client::DatabaseAdmin;
43pub use google_cloud_spanner_admin_instance_v1::client::InstanceAdmin;
44
45#[derive(Clone, Debug)]
51pub struct Spanner {
52 pub(crate) channels: Vec<Channel>,
53 pub(crate) counter: std::sync::Arc<AtomicUsize>,
54 pub(crate) config: ClientConfig,
55 pub(crate) is_emulator: bool,
56 pub(crate) instance_type: InstanceType,
57}
58
59pub struct Factory;
61
62impl google_cloud_gax::client_builder::internal::ClientFactory for Factory {
63 type Client = Spanner;
64 type Credentials = Credentials;
65
66 async fn build(self, mut config: ClientConfig) -> crate::ClientBuilderResult<Self::Client> {
67 let mut is_emulator = false;
68 if let Some(endpoint) = std::env::var("SPANNER_EMULATOR_HOST")
69 .ok()
70 .filter(|s| !s.is_empty())
71 {
72 is_emulator = true;
73 if config.endpoint.is_none() {
74 config.endpoint = Some(parse_emulator_endpoint(&endpoint));
75 }
76 if config.cred.is_none() {
77 config.cred = Some(anonymous::Builder::new().build());
78 }
79 }
80
81 if config
82 .endpoint
83 .as_ref()
84 .is_some_and(|ep| is_plaintext_endpoint(ep))
85 && config.cred.is_none()
86 {
87 config.cred = Some(anonymous::Builder::new().build());
88 }
89
90 let num_channels = std::env::var("SPANNER_NUM_CHANNELS")
91 .ok()
92 .and_then(|s| s.parse::<usize>().ok())
93 .unwrap_or(4);
94
95 let mut channels = Vec::with_capacity(num_channels);
96 for _ in 0..num_channels {
97 channels.push(Channel::create(&config).await?);
98 }
99
100 let instance_type = config
101 .extensions
102 .get::<InstanceType>()
103 .copied()
104 .unwrap_or_default();
105
106 Ok(Spanner {
107 channels,
108 counter: std::sync::Arc::new(AtomicUsize::new(0)),
109 config,
110 is_emulator,
111 instance_type,
112 })
113 }
114}
115
116pub type ClientBuilder = google_cloud_gax::client_builder::ClientBuilder<Factory, Credentials>;
118
119pub trait SpannerBuilderExt {
121 fn with_instance_type(self, instance_type: InstanceType) -> Self;
135}
136
137impl SpannerBuilderExt for ClientBuilder {
138 fn with_instance_type(self, instance_type: InstanceType) -> Self {
139 self.with_extension(instance_type)
140 }
141}
142
143fn parse_emulator_endpoint(endpoint: &str) -> String {
144 match url::Url::parse(endpoint) {
145 Ok(url) if url.has_host() => endpoint.to_string(),
146 _ => format!("http://{}", endpoint),
147 }
148}
149
150macro_rules! define_idempotent_rpc {
151 ($method:ident, $request_type:ty, $response_type:ty, $canonical_name:expr) => {
152 pub(crate) async fn $method(
153 &self,
154 request: $request_type,
155 options: crate::RequestOptions,
156 channel_hint: usize,
157 o11y: &crate::observability::Observability,
158 ) -> crate::Result<$response_type> {
159 o11y.trace_operation($canonical_name, || async move {
160 self.get_channel(channel_hint)
161 .inner
162 .$method()
163 .with_request(request)
164 .with_options(apply_request_defaults(options))
165 .send()
166 .await
167 })
168 .await
169 }
170 };
171}
172
173fn apply_request_defaults(mut options: crate::RequestOptions) -> crate::RequestOptions {
174 if options.idempotent().is_none() {
175 options.set_idempotency(true);
176 }
177 if options.retry_policy().is_none() {
178 options.set_retry_policy(crate::retry_policy::SpannerRetryPolicy::new());
179 }
180 options
181}
182
183pub(crate) static LAR_HEADER_MAP: LazyLock<HeaderMap> = LazyLock::new(|| {
184 let mut map = HeaderMap::new();
185 map.insert(
186 HeaderName::from_static("x-goog-spanner-route-to-leader"),
187 HeaderValue::from_static("true"),
188 );
189 map
190});
191
192pub(crate) fn amend_request_options_for_lar(
193 leader_aware_routing_enabled: bool,
194 mut options: GaxRequestOptions,
195) -> GaxRequestOptions {
196 if leader_aware_routing_enabled {
197 let mut headers = options
198 .get_extension::<HeaderMap>()
199 .cloned()
200 .unwrap_or_default();
201 headers.extend((*LAR_HEADER_MAP).clone());
202 options = options.insert_extension(headers);
203 }
204 options
205}
206
207fn map_emulator_admin_endpoint(endpoint: &str, is_emulator: bool) -> String {
208 let mut ep = endpoint.trim_end_matches('/').to_string();
209 if is_emulator && ep.ends_with(":9010") {
210 ep = ep.replace(":9010", ":9020");
211 }
212 ep
213}
214
215impl Spanner {
216 pub fn builder() -> ClientBuilder {
245 new_builder(Factory)
246 }
247
248 pub fn database_admin_builder(&self) -> DatabaseAdminBuilder {
255 self.configure_admin_builder(DatabaseAdmin::builder())
256 }
257
258 pub fn instance_admin_builder(&self) -> InstanceAdminBuilder {
265 self.configure_admin_builder(InstanceAdmin::builder())
266 }
267
268 fn configure_admin_builder<F, C>(
269 &self,
270 mut builder: GaxClientBuilder<F, C>,
271 ) -> GaxClientBuilder<F, C>
272 where
273 C: Clone + From<Credentials>,
274 {
275 if let Some(ref endpoint) = self.config.endpoint {
276 let ep = map_emulator_admin_endpoint(endpoint, self.is_emulator);
277 builder = builder.with_endpoint(ep);
278 }
279 if let Some(ref cred) = self.config.cred {
280 builder = builder.with_credentials(cred.clone());
281 }
282 if let Some(ref ud) = self.config.universe_domain {
283 builder = builder.with_universe_domain(ud.clone());
284 }
285 builder
286 }
287
288 pub fn database_client(
307 &self,
308 database: impl Into<String>,
309 ) -> crate::builder::DatabaseClientBuilder {
310 crate::builder::DatabaseClientBuilder::new(self.clone(), database.into())
311 }
312
313 pub fn from_stub<T>(stub: T) -> Self
318 where
319 T: crate::generated::gapic_dataplane::stub::Spanner + 'static,
320 {
321 Self {
324 channels: vec![Channel {
325 inner: GapicSpanner::from_stub(stub),
326 grpc_client: None,
327 }],
328 counter: std::sync::Arc::new(AtomicUsize::new(0)),
329 config: ClientConfig::default(),
330 is_emulator: false,
331 instance_type: InstanceType::Cloud,
332 }
333 }
334
335 pub(crate) fn is_emulator(&self) -> bool {
336 self.is_emulator
337 }
338
339 pub(crate) fn instance_type(&self) -> InstanceType {
340 self.instance_type
341 }
342
343 pub(crate) fn get_channel(&self, hint: usize) -> &Channel {
344 let idx = hint % self.channels.len();
345 &self.channels[idx]
346 }
347
348 pub(crate) fn next_channel_hint(&self) -> usize {
349 self.counter.fetch_add(1, Ordering::Relaxed)
350 }
351
352 define_idempotent_rpc!(
353 create_session,
354 CreateSessionRequest,
355 Session,
356 "google.spanner.v1.Spanner/CreateSession"
357 );
358 define_idempotent_rpc!(
359 execute_sql,
360 ExecuteSqlRequest,
361 crate::model::ResultSet,
362 "google.spanner.v1.Spanner/ExecuteSql"
363 );
364 define_idempotent_rpc!(
365 execute_batch_dml,
366 ExecuteBatchDmlRequest,
367 ExecuteBatchDmlResponse,
368 "google.spanner.v1.Spanner/ExecuteBatchDml"
369 );
370 define_idempotent_rpc!(
371 begin_transaction,
372 BeginTransactionRequest,
373 Transaction,
374 "google.spanner.v1.Spanner/BeginTransaction"
375 );
376 define_idempotent_rpc!(
377 commit,
378 CommitRequest,
379 CommitResponse,
380 "google.spanner.v1.Spanner/Commit"
381 );
382 define_idempotent_rpc!(
383 rollback,
384 RollbackRequest,
385 (),
386 "google.spanner.v1.Spanner/Rollback"
387 );
388 define_idempotent_rpc!(
389 partition_query,
390 PartitionQueryRequest,
391 PartitionResponse,
392 "google.spanner.v1.Spanner/PartitionQuery"
393 );
394 define_idempotent_rpc!(
395 partition_read,
396 PartitionReadRequest,
397 PartitionResponse,
398 "google.spanner.v1.Spanner/PartitionRead"
399 );
400
401 pub(crate) fn execute_streaming_sql(
406 &self,
407 request: crate::model::ExecuteSqlRequest,
408 options: crate::RequestOptions,
409 channel_hint: usize,
410 ) -> builder::ExecuteStreamingSql {
411 let channel = self.get_channel(channel_hint);
412 let grpc = channel
413 .grpc_client
414 .as_ref()
415 .expect("Streaming RPCs are not supported when using a stub client");
416 builder::ExecuteStreamingSql::new(grpc.clone())
417 .with_request(request)
418 .with_options(options)
419 }
420
421 pub(crate) fn streaming_read(
426 &self,
427 request: crate::model::ReadRequest,
428 options: crate::RequestOptions,
429 channel_hint: usize,
430 ) -> builder::StreamingRead {
431 let channel = self.get_channel(channel_hint);
432 let grpc = channel
433 .grpc_client
434 .as_ref()
435 .expect("Streaming RPCs are not supported when using a stub client");
436 builder::StreamingRead::new(grpc.clone())
437 .with_request(request)
438 .with_options(options)
439 }
440
441 pub(crate) fn batch_write(
442 &self,
443 request: crate::model::BatchWriteRequest,
444 options: crate::RequestOptions,
445 channel_hint: usize,
446 ) -> builder::BatchWrite {
447 let channel = self.get_channel(channel_hint);
448 let grpc = channel
449 .grpc_client
450 .as_ref()
451 .expect("Streaming RPCs are not supported when using a stub client");
452 builder::BatchWrite::new(grpc.clone())
453 .with_request(request)
454 .with_options(options)
455 }
456}
457
458#[derive(Clone, Debug)]
459pub(crate) struct Channel {
460 pub(crate) inner: GapicSpanner,
461 pub(crate) grpc_client: Option<gaxi::grpc::Client>,
462}
463
464impl Channel {
465 pub(crate) async fn create(config: &ClientConfig) -> crate::ClientBuilderResult<Self> {
466 let transport =
467 crate::generated::gapic_dataplane::transport::Spanner::new(config.clone()).await?;
468 let grpc_client = transport.inner.clone();
469
470 let inner = if gaxi::options::tracing_enabled(config) {
471 GapicSpanner::from_stub(crate::generated::gapic_dataplane::tracing::Spanner::new(
472 transport,
473 ))
474 } else {
475 GapicSpanner::from_stub(transport)
476 };
477 Ok(Self {
478 inner,
479 grpc_client: Some(grpc_client),
480 })
481 }
482}
483
484#[cfg(test)]
485mod tests {
486 use super::*;
487 use crate::model::CreateSessionRequest;
488 use crate::read::ReadRequest;
489 use crate::result_set::tests::adapt;
490 use crate::statement::Statement;
491 use gaxi::grpc::tonic::MetadataMap;
492 use gaxi::grpc::tonic::{Code as GrpcCode, Response, Status};
493 use google_cloud_auth::credentials::anonymous::Builder as Anonymous;
494 use google_cloud_gax::backoff_policy::BackoffPolicy;
495 use google_cloud_gax::error::rpc::Code;
496 use google_cloud_gax::retry_state::RetryState;
497 use google_cloud_test_macros::tokio_test_no_panics;
498 use spanner_grpc_mock::google::rpc as mock_rpc;
499 use spanner_grpc_mock::google::spanner::v1 as mock_v1;
500 use spanner_grpc_mock::google::spanner::v1::CommitResponse;
501 use spanner_grpc_mock::google::spanner::v1::ResultSet;
502 use spanner_grpc_mock::google::spanner::v1::ResultSetStats;
503 use spanner_grpc_mock::google::spanner::v1::Session;
504 use spanner_grpc_mock::google::spanner::v1::result_set_stats::RowCount;
505 use spanner_grpc_mock::{MockSpanner, start};
506 use static_assertions::{assert_impl_all, assert_not_impl_any};
507 use std::sync::Arc;
508 use std::sync::atomic::{AtomicU64, Ordering};
509 use std::time::Duration;
510
511 mockall::mock! {
512 #[derive(Debug)]
513 BackoffPolicy {}
514 impl BackoffPolicy for BackoffPolicy {
515 fn on_failure(&self, state: &RetryState) -> Duration;
516 }
517 }
518
519 #[test]
520 fn auto_traits() {
521 assert_impl_all!(Spanner: std::fmt::Debug, Clone, Send, Sync);
522 assert_not_impl_any!(Spanner: std::panic::RefUnwindSafe, std::panic::UnwindSafe);
523 }
524
525 #[tokio_test_no_panics]
526 async fn channel_pool_default_size() {
527 let mock = MockSpanner::new();
528 let (address, _server) = start("0.0.0.0:0", mock)
529 .await
530 .expect("Failed to start mock server");
531
532 let client = Spanner::builder()
533 .with_endpoint(address)
534 .with_credentials(Anonymous::new().build())
535 .build()
536 .await
537 .expect("Failed to build client");
538
539 assert_eq!(client.channels.len(), 4);
540 }
541
542 #[test]
543 fn test_map_emulator_admin_endpoint() {
544 assert_eq!(
546 map_emulator_admin_endpoint("https://spanner.googleapis.com", false),
547 "https://spanner.googleapis.com"
548 );
549
550 assert_eq!(
552 map_emulator_admin_endpoint("http://localhost:9010", true),
553 "http://localhost:9020"
554 );
555
556 assert_eq!(
558 map_emulator_admin_endpoint("http://127.0.0.1:9010/", true),
559 "http://127.0.0.1:9020"
560 );
561
562 assert_eq!(
564 map_emulator_admin_endpoint("http://localhost:9010", false),
565 "http://localhost:9010"
566 );
567 }
568
569 #[tokio_test_no_panics]
570 async fn channel_selection() {
571 let mock = MockSpanner::new();
572 let (address, _server) = start("0.0.0.0:0", mock)
573 .await
574 .expect("Failed to start mock server");
575
576 let client = Spanner::builder()
577 .with_endpoint(address)
578 .with_credentials(Anonymous::new().build())
579 .build()
580 .await
581 .expect("Failed to build client");
582
583 let hint0 = client.next_channel_hint();
584 let hint1 = client.next_channel_hint();
585 let hint2 = client.next_channel_hint();
586 let hint3 = client.next_channel_hint();
587 let hint4 = client.next_channel_hint();
588
589 assert_eq!(hint0 % 4, 0);
590 assert_eq!(hint1 % 4, 1);
591 assert_eq!(hint2 % 4, 2);
592 assert_eq!(hint3 % 4, 3);
593 assert_eq!(hint4 % 4, 0);
594 }
595
596 #[tokio_test_no_panics]
597 async fn test_create_session() {
598 let mut mock = MockSpanner::new();
600 mock.expect_create_session().once().returning(|_| {
601 Ok(gaxi::grpc::tonic::Response::new(mock_v1::Session {
602 name:
603 "projects/test-project/instances/test-instance/databases/test-db/sessions/123"
604 .to_string(),
605 ..Default::default()
606 }))
607 });
608
609 let (address, _server) = start("0.0.0.0:0", mock)
611 .await
612 .expect("Failed to start mock server");
613
614 let client = Spanner::builder()
616 .with_endpoint(address)
617 .with_credentials(Anonymous::new().build())
618 .build()
619 .await
620 .expect("Failed to build client");
621
622 let mut req = CreateSessionRequest::new();
624 req.database =
625 "projects/test-project/instances/test-instance/databases/test-db".to_string();
626
627 let session = client
628 .create_session(
629 req,
630 crate::RequestOptions::default(),
631 client.next_channel_hint(),
632 &crate::observability::Observability::disabled(),
633 )
634 .await
635 .expect("Failed to call create_session");
636
637 assert_eq!(
639 session.name,
640 "projects/test-project/instances/test-instance/databases/test-db/sessions/123"
641 );
642 }
643
644 #[tokio_test_no_panics]
645 async fn test_create_session_retry() {
646 use google_cloud_gax::options::RequestOptionsBuilder;
647 use google_cloud_gax::retry_policy::{Aip194Strict, RetryPolicyExt};
648
649 let mut mock = MockSpanner::new();
651 let mut seq = mockall::Sequence::new();
652 mock.expect_create_session()
653 .once()
654 .in_sequence(&mut seq)
655 .returning(|_| {
656 Err(gaxi::grpc::tonic::Status::unavailable(
657 "server is unavailable",
658 ))
659 });
660 mock.expect_create_session().once().in_sequence(&mut seq).returning(|_| {
661 Ok(gaxi::grpc::tonic::Response::new(mock_v1::Session {
662 name: "projects/test-project/instances/test-instance/databases/test-db/sessions/456".to_string(),
663 ..Default::default()
664 }))
665 });
666
667 let (address, _server) = start("0.0.0.0:0", mock)
669 .await
670 .expect("Failed to start mock server");
671
672 let client = Spanner::builder()
675 .with_endpoint(address)
676 .with_credentials(Anonymous::new().build())
677 .build()
678 .await
679 .expect("Failed to build client");
680
681 let mut req = CreateSessionRequest::new();
683 req.database =
684 "projects/test-project/instances/test-instance/databases/test-db".to_string();
685
686 let session = client
687 .get_channel(client.next_channel_hint())
688 .inner
689 .create_session()
690 .with_request(req)
691 .with_idempotency(true)
692 .with_retry_policy(Aip194Strict.with_attempt_limit(3))
693 .send()
694 .await
695 .expect("Failed to call create_session");
696
697 assert_eq!(
699 session.name,
700 "projects/test-project/instances/test-instance/databases/test-db/sessions/456"
701 );
702 }
703
704 #[tokio_test_no_panics]
705 async fn test_create_session_transport_retry() {
706 let mut mock = MockSpanner::new();
708 let mut seq = mockall::Sequence::new();
709 mock.expect_create_session()
710 .once()
711 .in_sequence(&mut seq)
712 .returning(|_| {
713 let mut status = Status::unavailable("connection reset");
714 let mut headers = std::mem::take(status.metadata_mut()).into_headers();
715 headers.insert("content-type", http::HeaderValue::from_static("text/html"));
716 *status.metadata_mut() = MetadataMap::from_headers(headers);
717 Err(status)
718 });
719 mock.expect_create_session()
720 .once()
721 .in_sequence(&mut seq)
722 .returning(|_| {
723 Ok(gaxi::grpc::tonic::Response::new(mock_v1::Session {
724 name: "projects/test-project/instances/test-instance/databases/test-db/sessions/789".to_string(),
725 ..Default::default()
726 }))
727 });
728
729 let (address, _server) = start("0.0.0.0:0", mock)
731 .await
732 .expect("Failed to start mock server");
733
734 let client = Spanner::builder()
736 .with_endpoint(address)
737 .with_credentials(Anonymous::new().build())
738 .build()
739 .await
740 .expect("Failed to build client");
741
742 let mut req = CreateSessionRequest::new();
744 req.database =
745 "projects/test-project/instances/test-instance/databases/test-db".to_string();
746
747 let session = client
748 .create_session(
749 req,
750 crate::RequestOptions::default(),
751 client.next_channel_hint(),
752 &crate::observability::Observability::disabled(),
753 )
754 .await
755 .expect("Failed to call create_session after transport error retry");
756
757 assert_eq!(
759 session.name,
760 "projects/test-project/instances/test-instance/databases/test-db/sessions/789",
761 "Expected session name to match the second successful response after transport retry"
762 );
763 }
764
765 #[tokio_test_no_panics]
766 async fn test_execute_sql() {
767 use crate::model::ExecuteSqlRequest;
768
769 let mut mock = MockSpanner::new();
770 mock.expect_execute_sql().once().returning(|_| {
771 Ok(gaxi::grpc::tonic::Response::new(mock_v1::ResultSet {
772 metadata: Some(mock_v1::ResultSetMetadata {
773 row_type: Some(mock_v1::StructType { fields: vec![] }),
774 transaction: None,
775 undeclared_parameters: None,
776 }),
777 rows: vec![],
778 stats: None,
779 precommit_token: None,
780 cache_update: None,
781 }))
782 });
783
784 let (address, _server) = start("0.0.0.0:0", mock)
785 .await
786 .expect("Failed to start mock server");
787 let client = Spanner::builder()
788 .with_endpoint(address)
789 .with_credentials(Anonymous::new().build())
790 .build()
791 .await
792 .expect("Failed to build client");
793
794 let mut req = ExecuteSqlRequest::new();
795 req.sql = "SELECT 1".to_string();
796
797 let result_set = client
798 .execute_sql(
799 req,
800 crate::RequestOptions::default(),
801 client.next_channel_hint(),
802 &crate::observability::Observability::disabled(),
803 )
804 .await
805 .expect("Failed to call execute_sql");
806 assert!(result_set.metadata.is_some());
807 }
808
809 #[tokio_test_no_panics]
810 async fn test_execute_batch_dml() {
811 use crate::model::ExecuteBatchDmlRequest;
812
813 let mut mock = MockSpanner::new();
814 mock.expect_execute_batch_dml().once().returning(|_| {
815 Ok(gaxi::grpc::tonic::Response::new(
816 mock_v1::ExecuteBatchDmlResponse {
817 result_sets: vec![],
818 status: Some(mock_rpc::Status {
819 code: 0,
820 message: "OK".to_string(),
821 details: vec![],
822 }),
823 precommit_token: None,
824 },
825 ))
826 });
827
828 let (address, _server) = start("0.0.0.0:0", mock)
829 .await
830 .expect("Failed to start mock server");
831 let client = Spanner::builder()
832 .with_endpoint(address)
833 .with_credentials(Anonymous::new().build())
834 .build()
835 .await
836 .expect("Failed to build client");
837
838 let mut req = ExecuteBatchDmlRequest::new();
839 req.session = "test_session".to_string();
840
841 let response = client
842 .execute_batch_dml(
843 req,
844 crate::RequestOptions::default(),
845 client.next_channel_hint(),
846 &crate::observability::Observability::disabled(),
847 )
848 .await
849 .expect("Failed to call execute_batch_dml");
850 assert!(response.status.is_some());
851 }
852
853 #[tokio_test_no_panics]
854 async fn test_begin_transaction() {
855 use crate::model::BeginTransactionRequest;
856
857 let mut mock = MockSpanner::new();
858 mock.expect_begin_transaction().once().returning(|_| {
859 Ok(gaxi::grpc::tonic::Response::new(mock_v1::Transaction {
860 id: vec![1, 2, 3],
861 read_timestamp: None,
862 precommit_token: None,
863 ..Default::default()
864 }))
865 });
866
867 let (address, _server) = start("0.0.0.0:0", mock)
868 .await
869 .expect("Failed to start mock server");
870 let client = Spanner::builder()
871 .with_endpoint(address)
872 .with_credentials(Anonymous::new().build())
873 .build()
874 .await
875 .expect("Failed to build client");
876
877 let mut req = BeginTransactionRequest::new();
878 req.session = "test_session".to_string();
879
880 let tx = client
881 .begin_transaction(
882 req,
883 crate::RequestOptions::default(),
884 client.next_channel_hint(),
885 &crate::observability::Observability::disabled(),
886 )
887 .await
888 .expect("Failed to call begin_transaction");
889 assert_eq!(tx.id, vec![1, 2, 3]);
890 }
891
892 #[tokio_test_no_panics]
893 async fn test_commit() {
894 use crate::model::CommitRequest;
895
896 let mut mock = MockSpanner::new();
897 mock.expect_commit().once().returning(|_| {
898 Ok(gaxi::grpc::tonic::Response::new(mock_v1::CommitResponse {
899 commit_timestamp: Some(prost_types::Timestamp {
900 seconds: 12345,
901 nanos: 0,
902 }),
903 commit_stats: None,
904 multiplexed_session_retry: None,
905 snapshot_timestamp: None,
906 ..Default::default()
907 }))
908 });
909
910 let (address, _server) = start("0.0.0.0:0", mock)
911 .await
912 .expect("Failed to start mock server");
913 let client = Spanner::builder()
914 .with_endpoint(address)
915 .with_credentials(Anonymous::new().build())
916 .build()
917 .await
918 .expect("Failed to build client");
919
920 let mut req = CommitRequest::new();
921 req.session = "test_session".to_string();
922
923 let response = client
924 .commit(
925 req,
926 crate::RequestOptions::default(),
927 client.next_channel_hint(),
928 &crate::observability::Observability::disabled(),
929 )
930 .await
931 .expect("Failed to call commit");
932 assert!(response.commit_timestamp.is_some());
933 }
934
935 #[tokio_test_no_panics]
936 async fn test_rollback() {
937 use crate::model::RollbackRequest;
938
939 let mut mock = MockSpanner::new();
940 mock.expect_rollback()
941 .once()
942 .returning(|_| Ok(gaxi::grpc::tonic::Response::new(())));
943
944 let (address, _server) = start("0.0.0.0:0", mock)
945 .await
946 .expect("Failed to start mock server");
947 let client = Spanner::builder()
948 .with_endpoint(address)
949 .with_credentials(Anonymous::new().build())
950 .build()
951 .await
952 .expect("Failed to build client");
953
954 let mut req = RollbackRequest::new();
955 req.session = "test_session".to_string();
956
957 client
958 .rollback(
959 req,
960 crate::RequestOptions::default(),
961 client.next_channel_hint(),
962 &crate::observability::Observability::disabled(),
963 )
964 .await
965 .expect("Failed to call rollback");
966 }
967
968 #[tokio_test_no_panics]
969 async fn test_execute_streaming_sql() {
970 use crate::model::ExecuteSqlRequest;
971
972 let mut mock = MockSpanner::new();
973 mock.expect_execute_streaming_sql().once().returning(|_| {
974 let result_set = mock_v1::PartialResultSet {
975 metadata: Some(mock_v1::ResultSetMetadata {
976 row_type: Some(mock_v1::StructType { fields: vec![] }),
977 transaction: None,
978 undeclared_parameters: None,
979 }),
980 values: vec![],
981 chunked_value: false,
982 resume_token: vec![],
983 stats: None,
984 precommit_token: None,
985 cache_update: None,
986 last: false,
987 };
988 Ok(gaxi::grpc::tonic::Response::new(adapt([Ok(result_set)])))
989 });
990
991 let (address, _server) = start("0.0.0.0:0", mock)
992 .await
993 .expect("Failed to start mock server");
994 let client = Spanner::builder()
995 .with_endpoint(address)
996 .with_credentials(Anonymous::new().build())
997 .build()
998 .await
999 .expect("Failed to build client");
1000
1001 let mut req = ExecuteSqlRequest::new();
1002 req.sql = "SELECT 1".to_string();
1003
1004 let mut stream = client
1005 .execute_streaming_sql(
1006 req,
1007 crate::RequestOptions::default(),
1008 client.next_channel_hint(),
1009 )
1010 .send()
1011 .await
1012 .expect("Failed to call execute_streaming_sql");
1013
1014 let result = stream.next_message().await;
1015 assert!(result.is_some());
1016 assert!(result.unwrap().is_ok());
1017 }
1018
1019 #[tokio_test_no_panics]
1020 async fn test_streaming_read() {
1021 use crate::model::ReadRequest;
1022
1023 let mut mock = MockSpanner::new();
1024 mock.expect_streaming_read().once().returning(|_| {
1025 let result_set = mock_v1::PartialResultSet {
1026 metadata: Some(mock_v1::ResultSetMetadata {
1027 row_type: Some(mock_v1::StructType { fields: vec![] }),
1028 transaction: None,
1029 undeclared_parameters: None,
1030 }),
1031 values: vec![],
1032 chunked_value: false,
1033 resume_token: vec![],
1034 stats: None,
1035 precommit_token: None,
1036 cache_update: None,
1037 last: false,
1038 };
1039 Ok(gaxi::grpc::tonic::Response::from(adapt([Ok(result_set)])))
1040 });
1041
1042 let (address, _server) = start("0.0.0.0:0", mock)
1043 .await
1044 .expect("Failed to start mock server");
1045 let client = Spanner::builder()
1046 .with_endpoint(address)
1047 .with_credentials(Anonymous::new().build())
1048 .build()
1049 .await
1050 .expect("Failed to build client");
1051
1052 let mut req = ReadRequest::new();
1053 req.table = "test_table".to_string();
1054 req.columns = vec!["col1".to_string()];
1055
1056 let mut stream = client
1057 .streaming_read(
1058 req,
1059 crate::RequestOptions::default(),
1060 client.next_channel_hint(),
1061 )
1062 .send()
1063 .await
1064 .expect("Failed to call streaming_read");
1065
1066 let result = stream.next_message().await;
1067 assert!(result.is_some());
1068 assert!(result.unwrap().is_ok());
1069 }
1070
1071 #[tokio_test_no_panics]
1072 async fn test_batch_write() {
1073 use crate::model::BatchWriteRequest;
1074
1075 let mut mock = MockSpanner::new();
1076 mock.expect_batch_write().once().returning(|_| {
1077 let response = mock_v1::BatchWriteResponse {
1078 indexes: vec![],
1079 status: None,
1080 commit_timestamp: None,
1081 };
1082 Ok(gaxi::grpc::tonic::Response::from(adapt([Ok(response)])))
1083 });
1084
1085 let (address, _server) = start("0.0.0.0:0", mock)
1086 .await
1087 .expect("Failed to start mock server");
1088 let client = Spanner::builder()
1089 .with_endpoint(address)
1090 .with_credentials(Anonymous::new().build())
1091 .build()
1092 .await
1093 .expect("Failed to build client");
1094
1095 let mut req = BatchWriteRequest::new();
1096 req.session = "test_session".to_string();
1097
1098 let mut stream = client
1099 .batch_write(
1100 req,
1101 crate::RequestOptions::default(),
1102 client.next_channel_hint(),
1103 )
1104 .send()
1105 .await
1106 .expect("Failed to call batch_write");
1107
1108 let result = stream.next_message().await;
1109 assert!(result.is_some());
1110 assert!(result.unwrap().is_ok());
1111 }
1112
1113 #[tokio_test_no_panics]
1114 async fn test_execute_streaming_sql_error() {
1115 use crate::model::ExecuteSqlRequest;
1116
1117 let mut mock = MockSpanner::new();
1118 mock.expect_execute_streaming_sql().once().returning(|_| {
1119 let stream = adapt([Err(gaxi::grpc::tonic::Status::internal(
1120 "unexpected internal error",
1121 ))]);
1122 Ok(gaxi::grpc::tonic::Response::from(stream))
1123 });
1124
1125 let (address, _server) = start("0.0.0.0:0", mock)
1126 .await
1127 .expect("Failed to start mock server");
1128 let client = Spanner::builder()
1129 .with_endpoint(address)
1130 .with_credentials(Anonymous::new().build())
1131 .build()
1132 .await
1133 .expect("Failed to build client");
1134
1135 let mut req = ExecuteSqlRequest::new();
1136 req.sql = "SELECT 1".to_string();
1137
1138 let mut stream = client
1139 .execute_streaming_sql(
1140 req,
1141 crate::RequestOptions::default(),
1142 client.next_channel_hint(),
1143 )
1144 .send()
1145 .await
1146 .expect("Failed to call execute_streaming_sql");
1147
1148 let result = stream.next_message().await;
1149 assert!(result.is_some());
1150 let err = result.unwrap().expect_err("expected error");
1151 assert_eq!(
1152 err.status().unwrap().code,
1153 google_cloud_gax::error::rpc::Code::Internal
1154 );
1155 }
1156
1157 #[tokio_test_no_panics]
1158 async fn default_retry_respected() -> anyhow::Result<()> {
1159 use crate::model::CreateSessionRequest;
1160
1161 let mut mock = MockSpanner::new();
1163 let mut seq = mockall::Sequence::new();
1164 mock.expect_create_session()
1165 .once()
1166 .in_sequence(&mut seq)
1167 .returning(|_| Err(Status::unavailable("server is unavailable")));
1168 mock.expect_create_session().once().in_sequence(&mut seq).returning(|_| {
1169 Ok(Response::new(Session {
1170 name: "projects/test-project/instances/test-instance/databases/test-db/sessions/456".to_string(),
1171 ..Default::default()
1172 }))
1173 });
1174
1175 let (address, _server) = start("0.0.0.0:0", mock).await?;
1177
1178 let client = Spanner::builder()
1180 .with_endpoint(address)
1181 .with_credentials(Anonymous::new().build())
1182 .build()
1183 .await?;
1184
1185 let mut req = CreateSessionRequest::new();
1187 req.database =
1188 "projects/test-project/instances/test-instance/databases/test-db".to_string();
1189
1190 let session = client
1191 .create_session(
1192 req,
1193 crate::RequestOptions::default(),
1194 client.next_channel_hint(),
1195 &crate::observability::Observability::disabled(),
1196 )
1197 .await
1198 .expect("Failed to call create_session");
1199
1200 assert_eq!(
1202 session.name,
1203 "projects/test-project/instances/test-instance/databases/test-db/sessions/456"
1204 );
1205
1206 Ok(())
1207 }
1208
1209 #[tokio_test_no_panics]
1210 async fn override_idempotency_to_false() -> anyhow::Result<()> {
1211 use crate::model::CreateSessionRequest;
1212
1213 let mut mock = MockSpanner::new();
1215 mock.expect_create_session()
1216 .once()
1217 .returning(|_| Err(Status::unavailable("server is unavailable")));
1218
1219 let (address, _server) = start("0.0.0.0:0", mock).await?;
1221
1222 let client = Spanner::builder()
1224 .with_endpoint(address)
1225 .with_credentials(Anonymous::new().build())
1226 .build()
1227 .await?;
1228
1229 let mut req = CreateSessionRequest::new();
1231 req.database =
1232 "projects/test-project/instances/test-instance/databases/test-db".to_string();
1233
1234 let mut options = crate::RequestOptions::default();
1235 options.set_idempotency(false);
1236
1237 let result = client
1238 .create_session(
1239 req,
1240 options,
1241 client.next_channel_hint(),
1242 &crate::observability::Observability::disabled(),
1243 )
1244 .await;
1245
1246 assert!(result.is_err(), "Expected error, got {:?}", result);
1248 let err = result.unwrap_err();
1249 assert_eq!(err.status().map(|s| s.code), Some(Code::Unavailable));
1250
1251 Ok(())
1252 }
1253
1254 #[tokio_test_no_panics]
1255 async fn timeout_respected() -> anyhow::Result<()> {
1256 use crate::batch_dml::BatchDml;
1257 use std::time::Duration;
1258
1259 let mut mock = MockSpanner::new();
1261
1262 mock.expect_create_session().returning(|_| {
1263 Ok(Response::new(Session {
1264 name: "projects/p/instances/i/databases/d/sessions/123".to_string(),
1265 ..Default::default()
1266 }))
1267 });
1268
1269 mock.expect_begin_transaction().returning(|_| {
1270 Ok(Response::new(mock_v1::Transaction {
1271 id: vec![42],
1272 ..Default::default()
1273 }))
1274 });
1275
1276 mock.expect_execute_streaming_sql().once().returning(|req| {
1277 let metadata = req.metadata();
1278 let timeout = metadata.get("grpc-timeout");
1279 assert!(
1280 timeout.is_some(),
1281 "grpc-timeout header should be present for query"
1282 );
1283
1284 let (tx, rx) = tokio::sync::mpsc::channel(1);
1285 let metadata = mock_v1::ResultSetMetadata {
1286 transaction: Some(mock_v1::Transaction {
1287 id: vec![42],
1288 ..Default::default()
1289 }),
1290 ..Default::default()
1291 };
1292 let prs = mock_v1::PartialResultSet {
1293 metadata: Some(metadata),
1294 ..Default::default()
1295 };
1296 tx.try_send(Ok(prs)).unwrap();
1297 Ok(Response::new(rx))
1298 });
1299
1300 mock.expect_streaming_read().once().returning(|req| {
1301 let metadata = req.metadata();
1302 let timeout = metadata.get("grpc-timeout");
1303 assert!(
1304 timeout.is_some(),
1305 "grpc-timeout header should be present for read"
1306 );
1307
1308 let (tx, rx) = tokio::sync::mpsc::channel(1);
1309 let metadata = mock_v1::ResultSetMetadata {
1310 transaction: None,
1311 ..Default::default()
1312 };
1313 let prs = mock_v1::PartialResultSet {
1314 metadata: Some(metadata),
1315 ..Default::default()
1316 };
1317 tx.try_send(Ok(prs)).unwrap();
1318 Ok(Response::new(rx))
1319 });
1320
1321 mock.expect_execute_sql().once().returning(|req| {
1322 let metadata = req.metadata();
1323 let timeout = metadata.get("grpc-timeout");
1324 assert!(
1325 timeout.is_some(),
1326 "grpc-timeout header should be present for single DML"
1327 );
1328
1329 Ok(Response::new(mock_v1::ResultSet {
1330 metadata: Some(mock_v1::ResultSetMetadata {
1331 transaction: Some(mock_v1::Transaction {
1332 id: vec![42],
1333 ..Default::default()
1334 }),
1335 ..Default::default()
1336 }),
1337 stats: Some(mock_v1::ResultSetStats {
1338 row_count: Some(mock_v1::result_set_stats::RowCount::RowCountExact(1)),
1339 ..Default::default()
1340 }),
1341 ..Default::default()
1342 }))
1343 });
1344
1345 mock.expect_execute_batch_dml().once().returning(|req| {
1346 let metadata = req.metadata();
1347 let timeout = metadata.get("grpc-timeout");
1348 assert!(
1349 timeout.is_some(),
1350 "grpc-timeout header should be present for batch dml"
1351 );
1352
1353 Ok(Response::new(mock_v1::ExecuteBatchDmlResponse {
1354 result_sets: vec![mock_v1::ResultSet {
1355 stats: Some(mock_v1::ResultSetStats {
1356 row_count: Some(mock_v1::result_set_stats::RowCount::RowCountExact(1)),
1357 ..Default::default()
1358 }),
1359 ..Default::default()
1360 }],
1361 ..Default::default()
1362 }))
1363 });
1364
1365 mock.expect_commit().returning(|_| {
1366 Ok(Response::new(mock_v1::CommitResponse {
1367 commit_timestamp: Some(prost_types::Timestamp {
1368 seconds: 1234,
1369 nanos: 0,
1370 }),
1371 ..Default::default()
1372 }))
1373 });
1374
1375 let (address, _server) = start("0.0.0.0:0", mock).await?;
1377
1378 let client = Spanner::builder()
1380 .with_endpoint(address)
1381 .with_credentials(Anonymous::new().build())
1382 .build()
1383 .await?;
1384
1385 let db = client
1386 .database_client("projects/p/instances/i/databases/d")
1387 .build()
1388 .await?;
1389 let runner = db.read_write_transaction().build().await?;
1390
1391 runner
1393 .run(async |tx| {
1394 let stmt = Statement::builder("SELECT 1")
1396 .with_attempt_timeout(Duration::from_secs(10))
1397 .build();
1398 let _rs = tx.execute_query(stmt).await?;
1400
1401 let req = ReadRequest::builder("Table", vec!["Col"])
1403 .with_keys(crate::key::KeySet::all())
1404 .with_attempt_timeout(Duration::from_secs(5))
1405 .build();
1406 let _ = tx.execute_read(req).await?;
1407
1408 let dml = Statement::builder("UPDATE t SET c = 1")
1410 .with_attempt_timeout(Duration::from_secs(7))
1411 .build();
1412 let _ = tx.execute_update(dml).await?;
1413
1414 let batch = BatchDml::builder()
1416 .add_statement("UPDATE t SET c = 2")
1417 .with_attempt_timeout(Duration::from_secs(8))
1418 .build();
1419 let _ = tx.execute_batch_update(batch).await?;
1420
1421 Ok(())
1422 })
1423 .await?;
1424
1425 Ok(())
1426 }
1427
1428 #[tokio_test_no_panics]
1429 async fn retry_policy_respected() -> anyhow::Result<()> {
1430 use google_cloud_gax::retry_policy::{Aip194Strict, RetryPolicyExt};
1431
1432 let retry_policy = Aip194Strict.continue_on_too_many_requests();
1434
1435 let mut mock = MockSpanner::new();
1437
1438 mock.expect_create_session().returning(|_| {
1439 Ok(Response::new(Session {
1440 name: "projects/p/instances/i/databases/d/sessions/123".to_string(),
1441 ..Default::default()
1442 }))
1443 });
1444
1445 mock.expect_begin_transaction().returning(|_| {
1446 Ok(Response::new(mock_v1::Transaction {
1447 id: vec![42],
1448 ..Default::default()
1449 }))
1450 });
1451
1452 let mut seq = mockall::Sequence::new();
1454
1455 mock.expect_execute_sql()
1456 .once()
1457 .in_sequence(&mut seq)
1458 .returning(|_| Err(Status::new(GrpcCode::ResourceExhausted, "quota exceeded")));
1459
1460 mock.expect_execute_sql()
1461 .once()
1462 .in_sequence(&mut seq)
1463 .returning(|_| {
1464 Ok(Response::new(mock_v1::ResultSet {
1465 metadata: Some(mock_v1::ResultSetMetadata {
1466 transaction: Some(mock_v1::Transaction {
1467 id: vec![42],
1468 ..Default::default()
1469 }),
1470 ..Default::default()
1471 }),
1472 stats: Some(mock_v1::ResultSetStats {
1473 row_count: Some(mock_v1::result_set_stats::RowCount::RowCountExact(1)),
1474 ..Default::default()
1475 }),
1476 ..Default::default()
1477 }))
1478 });
1479
1480 mock.expect_commit().returning(|_| {
1481 Ok(Response::new(mock_v1::CommitResponse {
1482 commit_timestamp: Some(prost_types::Timestamp {
1483 seconds: 1234,
1484 nanos: 0,
1485 }),
1486 ..Default::default()
1487 }))
1488 });
1489
1490 let (address, _server) = start("0.0.0.0:0", mock).await?;
1492
1493 let client = Spanner::builder()
1495 .with_endpoint(address)
1496 .with_credentials(Anonymous::new().build())
1497 .build()
1498 .await?;
1499
1500 let db = client
1501 .database_client("projects/p/instances/i/databases/d")
1502 .build()
1503 .await?;
1504 let runner = db.read_write_transaction().build().await?;
1505
1506 let mut mock_backoff = MockBackoffPolicy::new();
1508 mock_backoff
1509 .expect_on_failure()
1510 .once()
1511 .returning(|_| Duration::from_nanos(1));
1512
1513 let stmt = Statement::builder("UPDATE t SET c = 1")
1514 .with_retry_policy(retry_policy)
1515 .with_backoff_policy(mock_backoff)
1516 .build();
1517
1518 let result = runner
1519 .run(async |tx| {
1520 let count = tx.execute_update(stmt.clone()).await?;
1521 Ok(count)
1522 })
1523 .await?;
1524
1525 assert_eq!(result.result, 1);
1527
1528 Ok(())
1529 }
1530
1531 fn parse_timeout(metadata: &MetadataMap) -> u64 {
1532 let timeout = metadata
1533 .get("grpc-timeout")
1534 .expect("grpc-timeout header should be present");
1535 let timeout_str = timeout
1536 .to_str()
1537 .expect("grpc-timeout should be a valid string");
1538 if timeout_str.ends_with('u') {
1539 timeout_str
1540 .trim_end_matches('u')
1541 .parse()
1542 .expect("valid u64")
1543 } else if timeout_str.ends_with('m') {
1544 timeout_str
1545 .trim_end_matches('m')
1546 .parse::<u64>()
1547 .expect("valid u64")
1548 * 1000
1549 } else if timeout_str.ends_with('n') {
1550 timeout_str
1551 .trim_end_matches('n')
1552 .parse::<u64>()
1553 .expect("valid u64")
1554 / 1000
1555 } else {
1556 panic!("Unknown timeout unit in {}", timeout_str);
1557 }
1558 }
1559
1560 #[tokio_test_no_panics]
1561 async fn transaction_timeout_respected() -> anyhow::Result<()> {
1562 use google_cloud_gax::retry_policy::{Aip194Strict, RetryPolicyExt};
1563 use spanner_grpc_mock::google::spanner::v1::Transaction;
1564
1565 let mut mock = MockSpanner::new();
1567
1568 mock.expect_create_session().returning(|_| {
1569 Ok(Response::new(Session {
1570 name: "projects/p/instances/i/databases/d/sessions/123".to_string(),
1571 ..Default::default()
1572 }))
1573 });
1574
1575 mock.expect_begin_transaction().returning(|_| {
1576 Ok(Response::new(Transaction {
1577 id: vec![1, 2, 3],
1578 ..Default::default()
1579 }))
1580 });
1581
1582 mock.expect_commit().once().returning(|_| {
1583 Ok(Response::new(CommitResponse {
1584 commit_timestamp: Some(prost_types::Timestamp {
1585 seconds: 12345,
1586 nanos: 0,
1587 }),
1588 ..Default::default()
1589 }))
1590 });
1591
1592 let mut seq = mockall::Sequence::new();
1594
1595 mock.expect_execute_sql()
1596 .once()
1597 .in_sequence(&mut seq)
1598 .returning(|req| {
1599 let timeout_val = parse_timeout(req.metadata());
1600 assert!(
1601 timeout_val <= 100000,
1602 "Expected timeout to be <= 100ms, got {}",
1603 timeout_val
1604 );
1605 Err(Status::new(GrpcCode::ResourceExhausted, "quota exceeded"))
1606 });
1607
1608 mock.expect_execute_sql()
1609 .once()
1610 .in_sequence(&mut seq)
1611 .returning(|req| {
1612 let timeout_val = parse_timeout(req.metadata());
1613 assert!(
1614 timeout_val <= 100000,
1615 "Expected timeout to be <= 100ms, got {}",
1616 timeout_val
1617 );
1618
1619 let res = ResultSet {
1620 metadata: Some(spanner_grpc_mock::google::spanner::v1::ResultSetMetadata {
1621 transaction: Some(Transaction {
1622 id: vec![1, 2, 3],
1623 ..Default::default()
1624 }),
1625 ..Default::default()
1626 }),
1627 stats: Some(ResultSetStats {
1628 row_count: Some(RowCount::RowCountExact(1)),
1629 ..Default::default()
1630 }),
1631 ..Default::default()
1632 };
1633 Ok(Response::new(res))
1634 });
1635
1636 let (address, _server) = start("127.0.0.1:0", mock).await?;
1638 let client = Spanner::builder()
1639 .with_endpoint(address)
1640 .with_credentials(Anonymous::new().build())
1641 .build()
1642 .await?;
1643 let db = client
1644 .database_client("projects/p/instances/i/databases/d")
1645 .build()
1646 .await?;
1647
1648 let runner = db
1650 .read_write_transaction()
1651 .with_transaction_timeout(Duration::from_millis(100))
1652 .build()
1653 .await?;
1654
1655 let result = runner
1657 .run(async |tx| {
1658 let mut mock_backoff = MockBackoffPolicy::new();
1659 mock_backoff
1660 .expect_on_failure()
1661 .times(1)
1662 .returning(|_| Duration::from_nanos(1));
1663
1664 let retry_policy = Aip194Strict.continue_on_too_many_requests();
1665
1666 let stmt = Statement::builder("SELECT 1")
1667 .with_retry_policy(retry_policy)
1668 .with_backoff_policy(mock_backoff)
1669 .build();
1670 tx.execute_update(stmt).await?;
1671 Ok(())
1672 })
1673 .await;
1674
1675 result.expect("Transaction should have succeeded");
1676
1677 Ok(())
1678 }
1679
1680 #[tokio_test_no_panics]
1681 async fn transaction_timeout_ticks_down() -> anyhow::Result<()> {
1682 use spanner_grpc_mock::google::spanner::v1::Transaction;
1683
1684 let mut mock = MockSpanner::new();
1685
1686 mock.expect_create_session().returning(|_| {
1687 Ok(Response::new(Session {
1688 name: "projects/p/instances/i/databases/d/sessions/123".to_string(),
1689 ..Default::default()
1690 }))
1691 });
1692
1693 let mut seq = mockall::Sequence::new();
1694
1695 let previous_timeout = Arc::new(AtomicU64::new(0));
1696 let prev_clone1 = previous_timeout.clone();
1697 mock.expect_execute_sql()
1698 .once()
1699 .in_sequence(&mut seq)
1700 .returning(move |req| {
1701 let timeout_val = parse_timeout(req.metadata());
1702 assert!(
1703 timeout_val <= 500000,
1704 "Expected timeout to be <= 500ms, got {}",
1705 timeout_val
1706 );
1707 prev_clone1.store(timeout_val, Ordering::SeqCst);
1708 Err(Status::new(GrpcCode::Aborted, "Aborted"))
1709 });
1710
1711 let prev_clone2 = previous_timeout.clone();
1714 mock.expect_execute_sql()
1715 .once()
1716 .in_sequence(&mut seq)
1717 .returning(move |req| {
1718 let timeout_val = parse_timeout(req.metadata());
1719 let prev = prev_clone2.load(Ordering::SeqCst);
1720 assert!(
1721 timeout_val <= prev,
1722 "Timeout should tick down between attempts or be equal, got {} and {}",
1723 timeout_val,
1724 prev
1725 );
1726 prev_clone2.store(timeout_val, Ordering::SeqCst); let res = ResultSet {
1729 metadata: Some(spanner_grpc_mock::google::spanner::v1::ResultSetMetadata {
1730 transaction: Some(Transaction {
1731 id: vec![2],
1732 ..Default::default()
1733 }),
1734 ..Default::default()
1735 }),
1736 stats: Some(ResultSetStats {
1737 row_count: Some(RowCount::RowCountExact(1)),
1738 ..Default::default()
1739 }),
1740 ..Default::default()
1741 };
1742 Ok(Response::new(res))
1743 });
1744
1745 let prev_clone3 = previous_timeout.clone();
1746 mock.expect_commit().once().returning(move |req| {
1747 let timeout_val = parse_timeout(req.metadata());
1748 let prev = prev_clone3.load(Ordering::SeqCst);
1749 assert!(
1750 timeout_val < prev,
1751 "Timeout should be smaller for commit, got {} and {}",
1752 timeout_val,
1753 prev
1754 );
1755
1756 Ok(Response::new(CommitResponse {
1757 commit_timestamp: Some(prost_types::Timestamp {
1758 seconds: 12345,
1759 nanos: 0,
1760 }),
1761 ..Default::default()
1762 }))
1763 });
1764
1765 let (address, _server) = start("127.0.0.1:0", mock).await?;
1766 let client = Spanner::builder()
1767 .with_endpoint(address)
1768 .with_credentials(Anonymous::new().build())
1769 .build()
1770 .await?;
1771 let db = client
1772 .database_client("projects/p/instances/i/databases/d")
1773 .build()
1774 .await?;
1775
1776 let runner = db
1777 .read_write_transaction()
1778 .with_transaction_timeout(Duration::from_millis(500))
1779 .build()
1780 .await?;
1781
1782 let result = runner
1783 .run(async |tx| {
1784 let stmt = Statement::builder("SELECT 1").build();
1785 tx.execute_update(stmt).await?;
1786 Ok(())
1787 })
1788 .await;
1789
1790 result.expect("Transaction should have succeeded");
1791
1792 Ok(())
1793 }
1794
1795 #[test]
1796 fn test_parse_emulator_endpoint() {
1797 assert_eq!(
1798 super::parse_emulator_endpoint("localhost:9010"),
1799 "http://localhost:9010"
1800 );
1801 assert_eq!(
1802 super::parse_emulator_endpoint("spanner-emulator:9010"),
1803 "http://spanner-emulator:9010"
1804 );
1805 assert_eq!(
1806 super::parse_emulator_endpoint("http://localhost:9010"),
1807 "http://localhost:9010"
1808 );
1809 assert_eq!(
1810 super::parse_emulator_endpoint("https://localhost:9010"),
1811 "https://localhost:9010"
1812 );
1813 assert_eq!(
1814 super::parse_emulator_endpoint("grpc://localhost:9010"),
1815 "grpc://localhost:9010"
1816 );
1817 assert_eq!(
1818 super::parse_emulator_endpoint("http_localhost:9010"),
1819 "http://http_localhost:9010"
1820 );
1821 }
1822}