canic_host/icp/query/
mod.rs1#[cfg(test)]
7mod tests;
8
9use crate::icp::{IcpCli, IcpManagementCallError};
10use candid::{CandidType, Principal};
11use ic_agent::{Agent, AgentError, agent::agent_error::TransportError};
12use serde::de::DeserializeOwned;
13use std::{future::Future, sync::Arc, time::Duration};
14use thiserror::Error;
15
16const RESPONSE_BYTES: usize = 8 * 1024 * 1024;
18const ATTEMPTS: usize = 3;
19const ATTEMPT_TIMEOUT: Duration = Duration::from_secs(10);
20const TOTAL_TIMEOUT: Duration = Duration::from_secs(30);
21
22#[derive(Debug, Error)]
29pub enum IcpQueryError {
30 #[error("authenticated query failed: {0}")]
31 Agent(#[source] Box<AgentError>),
32
33 #[error("query identity/network resolution failed: {0}")]
34 Authority(#[source] Box<IcpManagementCallError>),
35
36 #[error("authenticated query exceeded its bounded read deadline")]
37 Deadline,
38
39 #[error("query response decoding failed: {0}")]
40 Decode(#[source] candid::Error),
41
42 #[error("query argument encoding failed: {0}")]
43 Encode(#[source] candid::Error),
44
45 #[error("query runtime failed: {0}")]
46 Runtime(#[source] std::io::Error),
47
48 #[error("query HTTP client setup failed: {0}")]
49 Client(#[source] reqwest::Error),
50}
51
52#[derive(Debug)]
55pub(super) struct QueryTransport {
56 client: reqwest::Client,
57 runtime: Option<tokio::runtime::Runtime>,
58}
59
60impl QueryTransport {
61 fn new() -> Result<Self, IcpQueryError> {
62 let client = reqwest::Client::builder()
64 .use_rustls_tls()
65 .timeout(Duration::from_secs(360))
66 .build()
67 .map_err(IcpQueryError::Client)?;
68 let runtime = tokio::runtime::Builder::new_multi_thread()
69 .worker_threads(1)
70 .thread_name("canic-query")
71 .enable_all()
72 .build()
73 .map_err(IcpQueryError::Runtime)?;
74 Ok(Self {
75 client,
76 runtime: Some(runtime),
77 })
78 }
79
80 fn block_on<F: Future>(&self, future: F) -> F::Output {
81 self.runtime
82 .as_ref()
83 .expect("live query transport")
84 .block_on(future)
85 }
86}
87
88impl Drop for QueryTransport {
89 fn drop(&mut self) {
90 if let Some(runtime) = self.runtime.take() {
93 runtime.shutdown_background();
94 }
95 }
96}
97
98impl IcpCli {
99 fn query_transport(&self) -> Result<Arc<QueryTransport>, IcpQueryError> {
100 let mut retained = self.query_transport.lock().expect("query transport lock");
101 if let Some(transport) = retained.as_ref() {
102 return Ok(Arc::clone(transport));
103 }
104 let transport = Arc::new(QueryTransport::new()?);
105 *retained = Some(Arc::clone(&transport));
106 drop(retained);
107 Ok(transport)
108 }
109
110 pub(crate) fn query_candid_readonly<I: CandidType, O: CandidType + DeserializeOwned>(
115 &self,
116 canister: Principal,
117 method: &str,
118 input: &I,
119 ) -> Result<O, IcpQueryError> {
120 self.measure_request(
121 crate::icp::IcpRequestKind::AgentQuery,
122 Some(&canister.to_text()),
123 Some(method),
124 || {
125 let argument = candid::encode_one(input).map_err(IcpQueryError::Encode)?;
126 let transport = self.query_transport()?;
127 let agent = self
128 .build_authenticated_agent(
129 Agent::builder()
130 .with_http_client(transport.client.clone())
131 .with_max_response_body_size(RESPONSE_BYTES),
132 )
133 .map_err(|error| IcpQueryError::Authority(Box::new(error)))?;
134 let bytes = query_bytes(self, &agent, canister, method, &argument)?;
135 let mut config = candid::de::DecoderConfig::new();
136 config.set_decoding_quota(RESPONSE_BYTES * 64);
137 config.set_skipping_quota(RESPONSE_BYTES);
138 candid::utils::decode_one_with_config(&bytes, &config)
139 .map_err(IcpQueryError::Decode)
140 },
141 )
142 }
143}
144
145fn query_bytes(
146 icp: &IcpCli,
147 agent: &Agent,
148 canister: Principal,
149 method: &str,
150 argument: &[u8],
151) -> Result<Vec<u8>, IcpQueryError> {
152 icp.query_transport()?.block_on(async {
153 tokio::time::timeout(
154 TOTAL_TIMEOUT,
155 retry_query(|| async {
156 icp.record_remote_call();
157 tokio::time::timeout(
158 ATTEMPT_TIMEOUT,
159 agent.query(&canister, method).with_arg(argument).call(),
160 )
161 .await
162 .map_err(|_| IcpQueryError::Deadline)?
163 .map_err(|error| IcpQueryError::Agent(Box::new(error)))
164 }),
165 )
166 .await
167 .map_err(|_| IcpQueryError::Deadline)?
168 })
169}
170
171async fn retry_query<T, F, Fut>(mut query: F) -> Result<T, IcpQueryError>
172where
173 F: FnMut() -> Fut,
174 Fut: Future<Output = Result<T, IcpQueryError>>,
175{
176 for attempt in 0..ATTEMPTS {
177 match query().await {
178 Err(error) if attempt + 1 < ATTEMPTS && transient(&error) => {
179 tokio::time::sleep(Duration::from_millis(250 << attempt)).await;
180 }
181 result => return result,
182 }
183 }
184 unreachable!("the last attempt always returns")
185}
186
187fn transient(error: &IcpQueryError) -> bool {
188 match error {
189 IcpQueryError::Deadline => true,
190 IcpQueryError::Agent(error) => match error.as_ref() {
191 AgentError::TimeoutWaitingForResponse() => true,
192 AgentError::HttpError(error) => matches!(error.status, 408 | 429 | 502 | 503 | 504),
193 AgentError::TransportError(TransportError::Reqwest(error)) => {
194 error.is_timeout() || error.is_connect() || error.is_body()
195 }
196 _ => false,
197 },
198 _ => false,
199 }
200}