From 033c4496f7010474215cc599e80125ddf2efdfe8 Mon Sep 17 00:00:00 2001 From: marquessv Date: Fri, 28 Jul 2023 10:21:32 -0700 Subject: [PATCH 01/13] feat: Gateway accessors are cached between requests --- Cargo.lock | 58 +++++++++++++++++++++++++++--- crates/lib/Cargo.toml | 6 ++-- crates/lib/src/qpu/api.rs | 75 ++++++++++++++++++++++----------------- 3 files changed, 101 insertions(+), 38 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 67b7555a3..c1c9ed59f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -210,6 +210,42 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be" +[[package]] +name = "cached" +version = "0.44.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b195e4fbc4b6862bbd065b991a34750399c119797efff72492f28a5864de8700" +dependencies = [ + "async-trait", + "cached_proc_macro", + "cached_proc_macro_types", + "futures", + "hashbrown 0.13.2", + "instant", + "once_cell", + "thiserror", + "tokio", +] + +[[package]] +name = "cached_proc_macro" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b48814962d2fd604c50d2b9433c2a41a0ab567779ee2c02f7fba6eca1221f082" +dependencies = [ + "cached_proc_macro_types", + "darling", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "cached_proc_macro_types" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a4f925191b4367301851c6d99b09890311d74b0d43f274c0b34c86d308a3663" + [[package]] name = "cargo-lock" version = "9.0.0" @@ -428,7 +464,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "907076dfda823b0b36d2a1bb5f90c96660a5bbcd7729e10727f07858f22c4edc" dependencies = [ "cfg-if", - "hashbrown", + "hashbrown 0.12.3", "lock_api", "once_cell", "parking_lot_core", @@ -527,7 +563,7 @@ checksum = "96beaf9d35dbc4686bc86a4ecb851fd6a406f0bf32d9f646b1225a5c5cf5b5d7" dependencies = [ "env_logger", "fxhash", - "hashbrown", + "hashbrown 0.12.3", "indexmap", "instant", "log", @@ -814,6 +850,12 @@ dependencies = [ "ahash", ] +[[package]] +name = "hashbrown" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e" + [[package]] name = "headers" version = "0.3.8" @@ -1040,7 +1082,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" dependencies = [ "autocfg", - "hashbrown", + "hashbrown 0.12.3", ] [[package]] @@ -1515,6 +1557,12 @@ dependencies = [ "pyo3", ] +[[package]] +name = "once" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60bfe75a40f755f162b794140436c57845cb106fd1467598631c76c6fff08e28" + [[package]] name = "once_cell" version = "1.18.0" @@ -1899,6 +1947,7 @@ name = "qcs" version = "0.15.3" dependencies = [ "built", + "cached", "derive_builder", "dirs", "enum-as-inner", @@ -1913,6 +1962,7 @@ dependencies = [ "maplit", "ndarray", "num", + "once", "qcs-api", "qcs-api-client-common", "qcs-api-client-grpc", @@ -2706,7 +2756,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32bf088d1d7df2b2b6711b06da3471bc86677383c57b27251e18c56df8deac14" dependencies = [ "ahash", - "hashbrown", + "hashbrown 0.12.3", ] [[package]] diff --git a/crates/lib/Cargo.toml b/crates/lib/Cargo.toml index 8200c808b..fa734fe98 100644 --- a/crates/lib/Cargo.toml +++ b/crates/lib/Cargo.toml @@ -16,6 +16,7 @@ tracing-config = ["tracing", "qcs-api-client-common/tracing-config", "qcs-api-cl otel-tracing = ["tracing-config", "qcs-api-client-grpc/otel-tracing", "qcs-api-client-openapi/otel-tracing"] [dependencies] +cached = "0.44.0" dirs = "5.0.0" enum-as-inner = "0.5.1" futures = "0.3.24" @@ -43,9 +44,8 @@ uuid = { version = "1.2.1", features = ["v4"] } tonic = { version = "0.9.2", features = ["tls", "tls-roots"] } zmq = { version = "0.10.0" } itertools = "0.11.0" -rstest = "0.17.0" -insta = "1.29.0" derive_builder = "0.12.0" +once = "0.3.4" [dev-dependencies] erased-serde = "0.3.23" @@ -60,6 +60,8 @@ warp = { version = "0.3.3", default-features = false } regex = "1.7.0" test-case = "3.1.0" tracing-subscriber = "0.3.17" +rstest = "0.17.0" +insta = "1.29.0" [build-dependencies] built = "0.6.1" diff --git a/crates/lib/src/qpu/api.rs b/crates/lib/src/qpu/api.rs index 223f5f88b..3e43ac9d8 100644 --- a/crates/lib/src/qpu/api.rs +++ b/crates/lib/src/qpu/api.rs @@ -3,6 +3,7 @@ use std::{fmt, time::Duration}; +use cached::proc_macro::cached; use derive_builder::Builder; use qcs_api_client_common::{configuration::RefreshError, ClientConfiguration}; pub use qcs_api_client_grpc::channel::Error as GrpcError; @@ -293,38 +294,7 @@ impl ExecutionOptions { quantum_processor_id: &str, client: &Qcs, ) -> Result { - let mut min = None; - let mut next_page_token = None; - loop { - let accessors = list_quantum_processor_accessors( - &client.get_openapi_client(), - quantum_processor_id, - Some(100), - next_page_token.as_deref(), - ) - .await?; - - let accessor = accessors - .accessors - .into_iter() - .filter(|acc| { - acc.live - // `as_deref` needed to work around the `Option>` type. - && acc.access_type.as_deref() == Some(&QuantumProcessorAccessorType::GatewayV1) - }) - .min_by_key(|acc| acc.rank.unwrap_or(i64::MAX)); - - min = std::cmp::min_by_key(min, accessor, |acc| { - acc.as_ref().and_then(|acc| acc.rank).unwrap_or(i64::MAX) - }); - - next_page_token = accessors.next_page_token.clone(); - if next_page_token.is_none() { - break; - } - } - min.map(|accessor| accessor.url) - .ok_or_else(|| QpuApiError::GatewayNotFound(quantum_processor_id.to_string())) + get_accessor(quantum_processor_id, client).await } async fn get_default_endpoint_address( @@ -342,6 +312,47 @@ impl ExecutionOptions { } } +#[cached( + result = true, + time = 60, + key = "String", + convert = r"{ String::from(quantum_processor_id)}" +)] +async fn get_accessor(quantum_processor_id: &str, client: &Qcs) -> Result { + let mut min = None; + let mut next_page_token = None; + loop { + let accessors = list_quantum_processor_accessors( + &client.get_openapi_client(), + quantum_processor_id, + Some(100), + next_page_token.as_deref(), + ) + .await?; + + let accessor = accessors + .accessors + .into_iter() + .filter(|acc| { + acc.live + // `as_deref` needed to work around the `Option>` type. + && acc.access_type.as_deref() == Some(&QuantumProcessorAccessorType::GatewayV1) + }) + .min_by_key(|acc| acc.rank.unwrap_or(i64::MAX)); + + min = std::cmp::min_by_key(min, accessor, |acc| { + acc.as_ref().and_then(|acc| acc.rank).unwrap_or(i64::MAX) + }); + + next_page_token = accessors.next_page_token.clone(); + if next_page_token.is_none() { + break; + } + } + min.map(|accessor| accessor.url) + .ok_or_else(|| QpuApiError::GatewayNotFound(quantum_processor_id.to_string())) +} + /// Errors that can occur while attempting to establish a connection to the QPU. #[derive(Debug, thiserror::Error)] pub enum QpuApiError { From 01526fe7462f8ba57faad8374c0ec3b660fc5ba3 Mon Sep 17 00:00:00 2001 From: marquessv Date: Mon, 31 Jul 2023 10:39:22 -0700 Subject: [PATCH 02/13] break up function into cached and non-cached version --- crates/lib/src/qpu/api.rs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/crates/lib/src/qpu/api.rs b/crates/lib/src/qpu/api.rs index 3e43ac9d8..07288b9b5 100644 --- a/crates/lib/src/qpu/api.rs +++ b/crates/lib/src/qpu/api.rs @@ -294,7 +294,7 @@ impl ExecutionOptions { quantum_processor_id: &str, client: &Qcs, ) -> Result { - get_accessor(quantum_processor_id, client).await + get_accessor_with_cache(quantum_processor_id, client).await } async fn get_default_endpoint_address( @@ -318,6 +318,14 @@ impl ExecutionOptions { key = "String", convert = r"{ String::from(quantum_processor_id)}" )] +async fn get_accessor_with_cache( + quantum_processor_id: &str, + client: &Qcs, +) -> Result { + println!("hydrating cache!"); + get_accessor(quantum_processor_id, client).await +} + async fn get_accessor(quantum_processor_id: &str, client: &Qcs) -> Result { let mut min = None; let mut next_page_token = None; From dd01494f447a20684608f12728a5660f87b8af44 Mon Sep 17 00:00:00 2001 From: Kyle Strand Date: Mon, 31 Jul 2023 13:20:05 -0700 Subject: [PATCH 03/13] remove unused dependency --- Cargo.lock | 7 ------- crates/lib/Cargo.toml | 1 - 2 files changed, 8 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c1c9ed59f..d035d8cac 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1557,12 +1557,6 @@ dependencies = [ "pyo3", ] -[[package]] -name = "once" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60bfe75a40f755f162b794140436c57845cb106fd1467598631c76c6fff08e28" - [[package]] name = "once_cell" version = "1.18.0" @@ -1962,7 +1956,6 @@ dependencies = [ "maplit", "ndarray", "num", - "once", "qcs-api", "qcs-api-client-common", "qcs-api-client-grpc", diff --git a/crates/lib/Cargo.toml b/crates/lib/Cargo.toml index fa734fe98..7d1940b7e 100644 --- a/crates/lib/Cargo.toml +++ b/crates/lib/Cargo.toml @@ -45,7 +45,6 @@ tonic = { version = "0.9.2", features = ["tls", "tls-roots"] } zmq = { version = "0.10.0" } itertools = "0.11.0" derive_builder = "0.12.0" -once = "0.3.4" [dev-dependencies] erased-serde = "0.3.23" From 7f1d2000d17ef61c3a74175a9d198a5a1f91afc3 Mon Sep 17 00:00:00 2001 From: Kyle Strand Date: Mon, 31 Jul 2023 18:23:37 -0700 Subject: [PATCH 04/13] passing test! --- crates/lib/src/qpu/api.rs | 5 +-- crates/lib/tests/mocked_qpu.rs | 63 +++++++++++++++++++++++++++++++--- 2 files changed, 61 insertions(+), 7 deletions(-) diff --git a/crates/lib/src/qpu/api.rs b/crates/lib/src/qpu/api.rs index ac8d130e7..214959210 100644 --- a/crates/lib/src/qpu/api.rs +++ b/crates/lib/src/qpu/api.rs @@ -328,7 +328,7 @@ impl ExecutionOptions { #[cached( result = true, - time = 60, + time = 3600, key = "String", convert = r"{ String::from(quantum_processor_id)}" )] @@ -336,7 +336,8 @@ async fn get_accessor_with_cache( quantum_processor_id: &str, client: &Qcs, ) -> Result { - println!("hydrating cache!"); + #[cfg(feature = "tracing")] + tracing::info!(quantum_processor_id=%quantum_processor_id, "accessor cache miss"); get_accessor(quantum_processor_id, client).await } diff --git a/crates/lib/tests/mocked_qpu.rs b/crates/lib/tests/mocked_qpu.rs index edbf581ae..32a443085 100644 --- a/crates/lib/tests/mocked_qpu.rs +++ b/crates/lib/tests/mocked_qpu.rs @@ -24,8 +24,21 @@ MEASURE 1 ro[1] const QPU_ID: &str = "Aspen-M-3"; #[tokio::test] -async fn successful_bell_state() { +async fn test_qcs_against_mocks() { + // Shared setup + // TODO: even with `cached`, this can be called multiple times if present in + // multiple tests, which is why the different test cases are lumped into a + // single function. + // How can we clean this up? + // a) manually guarantee that `setup()` is only called once + // b) use a crate to enable shared setup & teardown: + // * https://crates.io/crates/tokio-shared-rt + // * https://docs.rs/test-with-tokio/latest/test_with_tokio/ + // c) split into multiple test modules -- would that be sufficient? + // d) ...something else? setup().await; + + // Test a basic case let result = Executable::from_quil(BELL_STATE) .with_shots(std::num::NonZeroU16::new(2).expect("value is non-zero")) .execute_on_qpu( @@ -50,6 +63,24 @@ async fn successful_bell_state() { arr2(&[[0, 1], [0, 1],]), ); assert_eq!(result.duration, Some(Duration::from_micros(8675))); + + // Check that accessors are cached + let execution_options = ExecutionOptionsBuilder::default() + .connection_strategy(ConnectionStrategy::Gateway) + .build() + .expect("should be valid execution options"); + let executable = Executable::from_quil(BELL_STATE); + for _ in 0..10 { + let _ = executable + .clone() + .execute_on_qpu(QPU_ID, None, &execution_options) + .await + .expect("Failed to run program that should be successful"); + } + assert_eq!( + 1, + mock_qcs::ACCESSORS_CALL_COUNT.load(std::sync::atomic::Ordering::SeqCst) + ); } async fn setup() { @@ -101,10 +132,13 @@ mod mock_qcs { use warp::Filter; use qcs_api_client_openapi::models::{ - InstructionSetArchitecture, TranslateNativeQuilToEncryptedBinaryRequest, - TranslateNativeQuilToEncryptedBinaryResponse, + InstructionSetArchitecture, QuantumProcessorAccessor, QuantumProcessorAccessorType, + TranslateNativeQuilToEncryptedBinaryRequest, TranslateNativeQuilToEncryptedBinaryResponse, }; + pub(crate) static ACCESSORS_CALL_COUNT: std::sync::atomic::AtomicUsize = + std::sync::atomic::AtomicUsize::new(0); + use super::QPU_ID; #[derive(Debug, Deserialize)] @@ -167,8 +201,27 @@ mod mock_qcs { warp::reply::json(&endpoint) }); - let quantum_processors = - warp::path("quantumProcessors").and(isa.or(translate).or(default_endpoint)); + let accessors = warp::path(QPU_ID) + .and(warp::path("accessors")) + .and(warp::get()) + .map(|| { + ACCESSORS_CALL_COUNT.fetch_add(1, std::sync::atomic::Ordering::SeqCst); + let rsp = qcs_api_client_openapi::models::ListQuantumProcessorAccessorsResponse { + accessors: vec![QuantumProcessorAccessor { + access_type: Some(Box::new(QuantumProcessorAccessorType::GatewayV1)), + live: true, + rank: Some(0), + id: Some(QPU_ID.to_string()), + // TODO: does the content of this string matter? + url: "http://127.0.0.1:8002".into(), + }], + next_page_token: None, + }; + warp::reply::json(&rsp) + }); + + let quantum_processors = warp::path("quantumProcessors") + .and(isa.or(translate).or(default_endpoint).or(accessors)); warp::serve(warp::path("v1").and(quantum_processors)) .run(([127, 0, 0, 1], 8000)) From dff43703663211e3561856d9e66837fa6eb4a6fc Mon Sep 17 00:00:00 2001 From: Kyle Strand Date: Mon, 31 Jul 2023 18:26:51 -0700 Subject: [PATCH 05/13] reduce number of runs --- crates/lib/tests/mocked_qpu.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/lib/tests/mocked_qpu.rs b/crates/lib/tests/mocked_qpu.rs index 32a443085..7b8bb1eee 100644 --- a/crates/lib/tests/mocked_qpu.rs +++ b/crates/lib/tests/mocked_qpu.rs @@ -70,7 +70,7 @@ async fn test_qcs_against_mocks() { .build() .expect("should be valid execution options"); let executable = Executable::from_quil(BELL_STATE); - for _ in 0..10 { + for _ in 0..3 { let _ = executable .clone() .execute_on_qpu(QPU_ID, None, &execution_options) From 61500676323412df549fad4075b41655394cabc4 Mon Sep 17 00:00:00 2001 From: Kyle Strand Date: Mon, 31 Jul 2023 18:32:16 -0700 Subject: [PATCH 06/13] can't just split some code into another file --- crates/lib/tests/mocked_qpu.rs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/crates/lib/tests/mocked_qpu.rs b/crates/lib/tests/mocked_qpu.rs index 7b8bb1eee..0c9e73fb7 100644 --- a/crates/lib/tests/mocked_qpu.rs +++ b/crates/lib/tests/mocked_qpu.rs @@ -26,16 +26,17 @@ const QPU_ID: &str = "Aspen-M-3"; #[tokio::test] async fn test_qcs_against_mocks() { // Shared setup - // TODO: even with `cached`, this can be called multiple times if present in - // multiple tests, which is why the different test cases are lumped into a - // single function. + // TODO: even with `cached`, this `setup` function can be called multiple + // times if present in multiple tests, which is why the different test cases + // are lumped into a single function. // How can we clean this up? // a) manually guarantee that `setup()` is only called once // b) use a crate to enable shared setup & teardown: // * https://crates.io/crates/tokio-shared-rt // * https://docs.rs/test-with-tokio/latest/test_with_tokio/ - // c) split into multiple test modules -- would that be sufficient? - // d) ...something else? + // c) copy-and-paste all this code into a separate file (...don't love this) + // d) move the mock server to a utility crate + // e) ...something else? setup().await; // Test a basic case From 769368e8dc43791e52ba05cd3bc388ba33b84e79 Mon Sep 17 00:00:00 2001 From: Kyle Strand Date: Tue, 1 Aug 2023 10:39:03 -0700 Subject: [PATCH 07/13] also cache default-endpoint --- crates/lib/src/qpu/api.rs | 40 ++++++++++++++++++++++++++++++--------- 1 file changed, 31 insertions(+), 9 deletions(-) diff --git a/crates/lib/src/qpu/api.rs b/crates/lib/src/qpu/api.rs index 214959210..811d2cc81 100644 --- a/crates/lib/src/qpu/api.rs +++ b/crates/lib/src/qpu/api.rs @@ -23,7 +23,7 @@ use qcs_api_client_grpc::{ pub use qcs_api_client_openapi::apis::Error as OpenApiError; use qcs_api_client_openapi::apis::{ endpoints_api::{ - get_default_endpoint, get_endpoint, GetDefaultEndpointError, GetEndpointError, + get_default_endpoint as api_get_default_endpoint, get_endpoint, GetDefaultEndpointError, GetEndpointError, }, quantum_processors_api::{ list_quantum_processor_accessors, ListQuantumProcessorAccessorsError, @@ -316,13 +316,7 @@ impl ExecutionOptions { quantum_processor_id: &str, client: &Qcs, ) -> Result { - let default_endpoint = - get_default_endpoint(&client.get_openapi_client(), quantum_processor_id).await?; - let addresses = default_endpoint.addresses.as_ref(); - let grpc_address = addresses.grpc.as_ref(); - grpc_address - .ok_or_else(|| QpuApiError::QpuEndpointNotFound(quantum_processor_id.into())) - .cloned() + get_default_endpoint_with_cache(quantum_processor_id, client).await } } @@ -337,7 +331,7 @@ async fn get_accessor_with_cache( client: &Qcs, ) -> Result { #[cfg(feature = "tracing")] - tracing::info!(quantum_processor_id=%quantum_processor_id, "accessor cache miss"); + tracing::info!(quantum_processor_id=%quantum_processor_id, "get_accessor cache miss"); get_accessor(quantum_processor_id, client).await } @@ -376,6 +370,34 @@ async fn get_accessor(quantum_processor_id: &str, client: &Qcs) -> Result Result { + #[cfg(feature = "tracing")] + tracing::info!(quantum_processor_id=%quantum_processor_id, "get_default_endpoint cache miss"); + get_default_endpoint(quantum_processor_id, client).await +} + +async fn get_default_endpoint( + quantum_processor_id: &str, + client: &Qcs, +) -> Result { + let default_endpoint = + api_get_default_endpoint(&client.get_openapi_client(), quantum_processor_id).await?; + let addresses = default_endpoint.addresses.as_ref(); + let grpc_address = addresses.grpc.as_ref(); + grpc_address + .ok_or_else(|| QpuApiError::QpuEndpointNotFound(quantum_processor_id.into())) + .cloned() +} + /// Errors that can occur while attempting to establish a connection to the QPU. #[derive(Debug, thiserror::Error)] pub enum QpuApiError { From 3e10bc3e0973fe688d7a17143891d719daf9c810 Mon Sep 17 00:00:00 2001 From: Kyle Strand Date: Tue, 1 Aug 2023 10:44:07 -0700 Subject: [PATCH 08/13] make direct-access and accessors tests more similar --- crates/lib/tests/mocked_qpu.rs | 88 ++++++++++++++++++---------------- 1 file changed, 47 insertions(+), 41 deletions(-) diff --git a/crates/lib/tests/mocked_qpu.rs b/crates/lib/tests/mocked_qpu.rs index 0c9e73fb7..17ed37a9d 100644 --- a/crates/lib/tests/mocked_qpu.rs +++ b/crates/lib/tests/mocked_qpu.rs @@ -7,7 +7,7 @@ use ndarray::arr2; use qcs::{ qpu::api::{ConnectionStrategy, ExecutionOptionsBuilder}, - Executable, + Executable, ExecutionData, }; use qcs_api_client_common::configuration::{SECRETS_PATH_VAR, SETTINGS_PATH_VAR}; @@ -26,57 +26,58 @@ const QPU_ID: &str = "Aspen-M-3"; #[tokio::test] async fn test_qcs_against_mocks() { // Shared setup - // TODO: even with `cached`, this `setup` function can be called multiple - // times if present in multiple tests, which is why the different test cases - // are lumped into a single function. - // How can we clean this up? - // a) manually guarantee that `setup()` is only called once - // b) use a crate to enable shared setup & teardown: - // * https://crates.io/crates/tokio-shared-rt - // * https://docs.rs/test-with-tokio/latest/test_with_tokio/ - // c) copy-and-paste all this code into a separate file (...don't love this) - // d) move the mock server to a utility crate - // e) ...something else? setup().await; - // Test a basic case - let result = Executable::from_quil(BELL_STATE) - .with_shots(std::num::NonZeroU16::new(2).expect("value is non-zero")) - .execute_on_qpu( - QPU_ID, - None, - &ExecutionOptionsBuilder::default() + let mut executable_two_shots = Executable::from_quil(BELL_STATE) + .with_shots(std::num::NonZeroU16::new(2).expect("value is non-zero")); + let check_result = |result: ExecutionData| { + assert_eq!( + result + .result_data + .to_register_map() + .expect("should convert to RegisterMap") + .get_register_matrix("ro") + .expect("should have values for `ro`") + .as_integer() + .expect("`ro` should have integer values"), + arr2(&[[0, 1], [0, 1],]), + ); + assert_eq!(result.duration, Some(Duration::from_micros(8675))); + }; + + // Test direct access + let execution_options_direct_access = ExecutionOptionsBuilder::default() .connection_strategy(ConnectionStrategy::DirectAccess) .build() - .expect("should be valid execution options"), - ) - .await - .expect("Failed to run program that should be successful"); + .expect("should be valid execution options"); + for _ in 0..3 { + let result = executable_two_shots + .execute_on_qpu( + QPU_ID, + None, + &execution_options_direct_access, + ) + .await + .expect("Failed to run program that should be successful"); + check_result(result); + } assert_eq!( - result - .result_data - .to_register_map() - .expect("should convert to RegisterMap") - .get_register_matrix("ro") - .expect("should have values for `ro`") - .as_integer() - .expect("`ro` should have integer values"), - arr2(&[[0, 1], [0, 1],]), + 1, + mock_qcs::DEFAULT_ENDPOINT_CALL_COUNT.load(std::sync::atomic::Ordering::SeqCst) ); - assert_eq!(result.duration, Some(Duration::from_micros(8675))); - // Check that accessors are cached - let execution_options = ExecutionOptionsBuilder::default() + // Check gateway access + let execution_options_gateway = ExecutionOptionsBuilder::default() .connection_strategy(ConnectionStrategy::Gateway) .build() .expect("should be valid execution options"); - let executable = Executable::from_quil(BELL_STATE); for _ in 0..3 { - let _ = executable + let result = executable_two_shots .clone() - .execute_on_qpu(QPU_ID, None, &execution_options) + .execute_on_qpu(QPU_ID, None, &execution_options_gateway) .await .expect("Failed to run program that should be successful"); + check_result(result); } assert_eq!( 1, @@ -135,8 +136,11 @@ mod mock_qcs { use qcs_api_client_openapi::models::{ InstructionSetArchitecture, QuantumProcessorAccessor, QuantumProcessorAccessorType, TranslateNativeQuilToEncryptedBinaryRequest, TranslateNativeQuilToEncryptedBinaryResponse, + ListQuantumProcessorAccessorsResponse, }; + pub(crate) static DEFAULT_ENDPOINT_CALL_COUNT: std::sync::atomic::AtomicUsize = + std::sync::atomic::AtomicUsize::new(0); pub(crate) static ACCESSORS_CALL_COUNT: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0); @@ -183,11 +187,13 @@ mod mock_qcs { settings_timestamp: None, }) }); - + + use std::sync::atomic::Ordering::SeqCst; let default_endpoint = warp::path(QPU_ID) .and(warp::path("endpoints:getDefault")) .and(warp::get()) .map(|| { + DEFAULT_ENDPOINT_CALL_COUNT.fetch_add(1, SeqCst); let endpoint = json!({ "address": "", "addresses": { @@ -206,8 +212,8 @@ mod mock_qcs { .and(warp::path("accessors")) .and(warp::get()) .map(|| { - ACCESSORS_CALL_COUNT.fetch_add(1, std::sync::atomic::Ordering::SeqCst); - let rsp = qcs_api_client_openapi::models::ListQuantumProcessorAccessorsResponse { + ACCESSORS_CALL_COUNT.fetch_add(1, SeqCst); + let rsp = ListQuantumProcessorAccessorsResponse { accessors: vec![QuantumProcessorAccessor { access_type: Some(Box::new(QuantumProcessorAccessorType::GatewayV1)), live: true, From 754b97eee70afc87ad8bfcacca507850042fbf85 Mon Sep 17 00:00:00 2001 From: Kyle Strand Date: Tue, 1 Aug 2023 13:34:29 -0700 Subject: [PATCH 09/13] resolve TODO --- crates/lib/tests/mocked_qpu.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/crates/lib/tests/mocked_qpu.rs b/crates/lib/tests/mocked_qpu.rs index 17ed37a9d..8cfe7caf4 100644 --- a/crates/lib/tests/mocked_qpu.rs +++ b/crates/lib/tests/mocked_qpu.rs @@ -139,6 +139,7 @@ mod mock_qcs { ListQuantumProcessorAccessorsResponse, }; + const MOCK_QPU_ADDRESS: &str = "http://127.0.0.1:8002"; pub(crate) static DEFAULT_ENDPOINT_CALL_COUNT: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0); pub(crate) static ACCESSORS_CALL_COUNT: std::sync::atomic::AtomicUsize = @@ -197,7 +198,7 @@ mod mock_qcs { let endpoint = json!({ "address": "", "addresses": { - "grpc": "http://127.0.0.1:8002", + "grpc": MOCK_QPU_ADDRESS, }, "datacenter": "west-1", "healthy": true, @@ -219,8 +220,7 @@ mod mock_qcs { live: true, rank: Some(0), id: Some(QPU_ID.to_string()), - // TODO: does the content of this string matter? - url: "http://127.0.0.1:8002".into(), + url: MOCK_QPU_ADDRESS.into(), }], next_page_token: None, }; @@ -377,6 +377,7 @@ mod qpu { let service = ControllerService::default(); Server::builder() .add_service(ControllerServer::new(service)) + // port must match MOCK_QPU_ADDRESS .serve("127.0.0.1:8002".parse().expect("address can be parsed")) .await .expect("service can be awaited"); From 96c04105f9854881fb0739303f5f92400781bdd5 Mon Sep 17 00:00:00 2001 From: Kyle Strand Date: Tue, 1 Aug 2023 16:15:41 -0700 Subject: [PATCH 10/13] make cache threadsafe; slightly improve test performance by refactoring --- crates/lib/src/qpu/api.rs | 5 +- crates/lib/tests/mocked_qpu.rs | 88 +++++++++++++++------------------- 2 files changed, 43 insertions(+), 50 deletions(-) diff --git a/crates/lib/src/qpu/api.rs b/crates/lib/src/qpu/api.rs index 811d2cc81..be20093d9 100644 --- a/crates/lib/src/qpu/api.rs +++ b/crates/lib/src/qpu/api.rs @@ -23,7 +23,8 @@ use qcs_api_client_grpc::{ pub use qcs_api_client_openapi::apis::Error as OpenApiError; use qcs_api_client_openapi::apis::{ endpoints_api::{ - get_default_endpoint as api_get_default_endpoint, get_endpoint, GetDefaultEndpointError, GetEndpointError, + get_default_endpoint as api_get_default_endpoint, get_endpoint, GetDefaultEndpointError, + GetEndpointError, }, quantum_processors_api::{ list_quantum_processor_accessors, ListQuantumProcessorAccessorsError, @@ -323,6 +324,7 @@ impl ExecutionOptions { #[cached( result = true, time = 3600, + sync_writes = true, key = "String", convert = r"{ String::from(quantum_processor_id)}" )] @@ -373,6 +375,7 @@ async fn get_accessor(quantum_processor_id: &str, client: &Qcs) -> Result Date: Tue, 1 Aug 2023 16:31:58 -0700 Subject: [PATCH 11/13] with more concurrency, threads make a bigger difference --- crates/lib/tests/mocked_qpu.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/lib/tests/mocked_qpu.rs b/crates/lib/tests/mocked_qpu.rs index ed230d6c3..568d2e49b 100644 --- a/crates/lib/tests/mocked_qpu.rs +++ b/crates/lib/tests/mocked_qpu.rs @@ -24,7 +24,7 @@ MEASURE 1 ro[1] const QPU_ID: &str = "Aspen-M-3"; -#[tokio::test] +#[tokio::test(flavor = "multi_thread", worker_threads = 4)] async fn test_qcs_against_mocks() { // Shared setup setup().await; From 4009baf44adc6c24de4c4429da746e7dabd31523 Mon Sep 17 00:00:00 2001 From: Kyle Strand Date: Wed, 2 Aug 2023 10:19:23 -0700 Subject: [PATCH 12/13] refresh TTL on cache-hit --- crates/lib/src/qpu/api.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/crates/lib/src/qpu/api.rs b/crates/lib/src/qpu/api.rs index be20093d9..910162426 100644 --- a/crates/lib/src/qpu/api.rs +++ b/crates/lib/src/qpu/api.rs @@ -324,6 +324,7 @@ impl ExecutionOptions { #[cached( result = true, time = 3600, + time_refresh = true, sync_writes = true, key = "String", convert = r"{ String::from(quantum_processor_id)}" @@ -375,6 +376,7 @@ async fn get_accessor(quantum_processor_id: &str, client: &Qcs) -> Result Date: Wed, 2 Aug 2023 11:39:01 -0700 Subject: [PATCH 13/13] 1-minute ttl --- crates/lib/src/qpu/api.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/lib/src/qpu/api.rs b/crates/lib/src/qpu/api.rs index 910162426..e9cb42aab 100644 --- a/crates/lib/src/qpu/api.rs +++ b/crates/lib/src/qpu/api.rs @@ -323,7 +323,7 @@ impl ExecutionOptions { #[cached( result = true, - time = 3600, + time = 60, time_refresh = true, sync_writes = true, key = "String", @@ -375,7 +375,7 @@ async fn get_accessor(quantum_processor_id: &str, client: &Qcs) -> Result