autopush_common/db/bigtable/
mod.rs1mod bigtable_client;
23mod pool;
24
25pub use bigtable_client::BigTableClientImpl;
26pub use bigtable_client::error::BigTableError;
27
28use serde::Deserialize;
29use std::time::Duration;
30use tonic::metadata::MetadataMap;
31
32use crate::db::bigtable::bigtable_client::MetadataBuilder;
33use crate::db::error::DbError;
34use crate::util::{deserialize_opt_u32_to_duration, deserialize_u32_to_duration};
35
36const DEFAULT_GRPC_CONNECT_TIMEOUT: Duration = Duration::from_secs(5);
37const DEFAULT_GRPC_POINT_ATTEMPT_TIMEOUT: Duration = Duration::from_secs(5);
38const DEFAULT_GRPC_SCAN_ATTEMPT_TIMEOUT: Duration = Duration::from_secs(20);
39const DEFAULT_GRPC_POINT_TOTAL_TIMEOUT: Duration = Duration::from_secs(15);
40const DEFAULT_GRPC_SCAN_TOTAL_TIMEOUT: Duration = Duration::from_secs(25);
45
46fn grpc_connect_timeout_default() -> Duration {
47 DEFAULT_GRPC_CONNECT_TIMEOUT
48}
49
50fn grpc_point_attempt_timeout_default() -> Duration {
51 DEFAULT_GRPC_POINT_ATTEMPT_TIMEOUT
52}
53
54fn grpc_scan_attempt_timeout_default() -> Duration {
55 DEFAULT_GRPC_SCAN_ATTEMPT_TIMEOUT
56}
57
58fn grpc_point_total_timeout_default() -> Duration {
59 DEFAULT_GRPC_POINT_TOTAL_TIMEOUT
60}
61
62fn grpc_scan_total_timeout_default() -> Duration {
63 DEFAULT_GRPC_SCAN_TOTAL_TIMEOUT
64}
65
66fn retry_default() -> usize {
67 bigtable_client::RETRY_COUNT
68}
69
70#[derive(Clone, Debug, Deserialize)]
72pub struct BigTableDbSettings {
73 #[serde(default)]
80 pub table_name: String,
81 #[serde(default)]
84 pub app_profile_id: String,
85 #[serde(default)]
86 pub router_family: String,
87 #[serde(default)]
88 pub message_family: String,
89 #[serde(default)]
90 pub message_topic_family: String,
91 #[serde(default)]
92 pub database_pool_max_size: Option<u32>,
93 #[serde(default)]
98 pub grpc_channel_count: Option<u32>,
99 #[serde(default)]
101 #[serde(deserialize_with = "deserialize_opt_u32_to_duration")]
102 pub database_pool_create_timeout: Option<Duration>,
103 #[serde(default)]
105 #[serde(deserialize_with = "deserialize_opt_u32_to_duration")]
106 pub database_pool_wait_timeout: Option<Duration>,
107 #[serde(
109 default = "grpc_connect_timeout_default",
110 deserialize_with = "deserialize_u32_to_duration"
111 )]
112 pub grpc_connect_timeout: Duration,
113 #[serde(
115 default = "grpc_point_attempt_timeout_default",
116 deserialize_with = "deserialize_u32_to_duration"
117 )]
118 pub grpc_point_attempt_timeout: Duration,
119 #[serde(
121 default = "grpc_scan_attempt_timeout_default",
122 deserialize_with = "deserialize_u32_to_duration"
123 )]
124 pub grpc_scan_attempt_timeout: Duration,
125 #[serde(
127 default = "grpc_point_total_timeout_default",
128 deserialize_with = "deserialize_u32_to_duration"
129 )]
130 pub grpc_point_total_timeout: Duration,
131 #[serde(
133 default = "grpc_scan_total_timeout_default",
134 deserialize_with = "deserialize_u32_to_duration"
135 )]
136 pub grpc_scan_total_timeout: Duration,
137 #[serde(default)]
139 pub route_to_leader: bool,
140 #[serde(default = "retry_default")]
143 pub retry_count: usize,
144 #[serde(default)]
146 #[serde(deserialize_with = "deserialize_opt_u32_to_duration")]
147 pub max_router_ttl: Option<Duration>,
148}
149
150#[allow(clippy::derivable_impls)]
152#[cfg(test)]
153impl Default for BigTableDbSettings {
154 fn default() -> Self {
155 use crate::MAX_ROUTER_TTL_SECS;
156
157 Self {
158 table_name: Default::default(),
159 router_family: Default::default(),
160 message_family: Default::default(),
161 message_topic_family: Default::default(),
162 database_pool_max_size: Default::default(),
163 grpc_channel_count: Default::default(),
164 database_pool_create_timeout: Default::default(),
165 database_pool_wait_timeout: Default::default(),
166 grpc_connect_timeout: grpc_connect_timeout_default(),
167 grpc_point_attempt_timeout: grpc_point_attempt_timeout_default(),
168 grpc_scan_attempt_timeout: grpc_scan_attempt_timeout_default(),
169 grpc_point_total_timeout: grpc_point_total_timeout_default(),
170 grpc_scan_total_timeout: grpc_scan_total_timeout_default(),
171 route_to_leader: Default::default(),
172 retry_count: Default::default(),
173 app_profile_id: Default::default(),
174 max_router_ttl: Some(Duration::from_secs(MAX_ROUTER_TTL_SECS)),
175 }
176 }
177}
178
179impl BigTableDbSettings {
180 pub fn metadata(&self) -> Result<MetadataMap, BigTableError> {
181 MetadataBuilder::with_prefix(&self.table_name)
182 .routing_param("table_name", &self.table_name)
183 .route_to_leader(self.route_to_leader)
184 .build()
185 }
186
187 pub fn get_instance_name(&self) -> Result<String, BigTableError> {
188 let parts: Vec<&str> = self.table_name.split('/').collect();
189 if parts.len() < 4 || parts[0] != "projects" || parts[2] != "instances" {
190 return Err(BigTableError::Config(
191 "Invalid table name specified. Cannot parse instance".to_owned(),
192 ));
193 }
194 Ok(parts[0..4].join("/"))
195 }
196}
197
198impl TryFrom<&str> for BigTableDbSettings {
199 type Error = DbError;
200 fn try_from(setting_string: &str) -> Result<Self, Self::Error> {
201 let mut me: Self = serde_json::from_str(setting_string)
202 .map_err(|e| DbError::General(format!("Could not parse DdbSettings: {e:?}")))?;
203
204 if me.table_name.starts_with('/') {
205 return Err(DbError::ConnectionError(
206 "Table name path begins with a '/'".to_owned(),
207 ));
208 };
209
210 if me.grpc_channel_count == Some(0) {
211 return Err(DbError::ConnectionError(
212 "grpc_channel_count must be greater than zero".to_owned(),
213 ));
214 }
215
216 let nonzero_durations = [
217 ("grpc_connect_timeout", me.grpc_connect_timeout),
218 ("grpc_point_attempt_timeout", me.grpc_point_attempt_timeout),
219 ("grpc_scan_attempt_timeout", me.grpc_scan_attempt_timeout),
220 ("grpc_point_total_timeout", me.grpc_point_total_timeout),
221 ("grpc_scan_total_timeout", me.grpc_scan_total_timeout),
222 ];
223 if let Some((name, _)) = nonzero_durations
224 .into_iter()
225 .find(|(_, duration)| duration.is_zero())
226 {
227 return Err(DbError::ConnectionError(format!(
228 "{name} must be greater than zero"
229 )));
230 }
231 if me.grpc_point_attempt_timeout > me.grpc_point_total_timeout {
232 return Err(DbError::ConnectionError(
233 "grpc_point_attempt_timeout must not exceed grpc_point_total_timeout".to_owned(),
234 ));
235 }
236 if me.grpc_scan_attempt_timeout > me.grpc_scan_total_timeout {
237 return Err(DbError::ConnectionError(
238 "grpc_scan_attempt_timeout must not exceed grpc_scan_total_timeout".to_owned(),
239 ));
240 }
241
242 if me.app_profile_id.is_empty() {
246 "default".clone_into(&mut me.app_profile_id);
247 }
248
249 Ok(me)
250 }
251}
252
253mod tests {
254
255 #[test]
256 fn test_settings_parse() -> Result<(), crate::db::error::DbError> {
257 let settings =
258 super::BigTableDbSettings::try_from("{\"database_pool_create_timeout\": 123}")?;
259 assert_eq!(
260 settings.database_pool_create_timeout,
261 Some(std::time::Duration::from_secs(123))
262 );
263 assert_eq!(settings.retry_count, 2);
264 assert_eq!(settings.grpc_channel_count, None);
265 assert_eq!(
266 settings.grpc_connect_timeout,
267 std::time::Duration::from_secs(5)
268 );
269 assert_eq!(
270 settings.grpc_point_attempt_timeout,
271 std::time::Duration::from_secs(5)
272 );
273 assert_eq!(
274 settings.grpc_scan_attempt_timeout,
275 std::time::Duration::from_secs(20)
276 );
277 assert_eq!(
278 settings.grpc_point_total_timeout,
279 std::time::Duration::from_secs(15)
280 );
281 assert_eq!(
282 settings.grpc_scan_total_timeout,
283 std::time::Duration::from_secs(25)
284 );
285 Ok(())
286 }
287
288 #[test]
289 fn test_zero_grpc_channel_count_is_rejected() {
290 let result = super::BigTableDbSettings::try_from("{\"grpc_channel_count\": 0}");
291 assert!(result.is_err());
292 }
293
294 #[test]
295 fn test_invalid_grpc_timeouts_are_rejected() {
296 assert!(
297 super::BigTableDbSettings::try_from(
298 "{\"grpc_point_attempt_timeout\": 6, \"grpc_point_total_timeout\": 5}"
299 )
300 .is_err()
301 );
302 assert!(
303 super::BigTableDbSettings::try_from(
304 "{\"grpc_scan_attempt_timeout\": 31, \"grpc_scan_total_timeout\": 30}"
305 )
306 .is_err()
307 );
308 }
309 #[test]
310 fn test_get_instance() -> Result<(), super::BigTableError> {
311 let settings = super::BigTableDbSettings {
312 table_name: "projects/foo/instances/bar/tables/gorp".to_owned(),
313 ..Default::default()
314 };
315 let res = settings.get_instance_name()?;
316 assert_eq!(res.as_str(), "projects/foo/instances/bar");
317
318 let settings = super::BigTableDbSettings {
319 table_name: "projects/foo/".to_owned(),
320 ..Default::default()
321 };
322 assert!(settings.get_instance_name().is_err());
323
324 let settings = super::BigTableDbSettings {
325 table_name: "protect/foo/instances/bar/tables/gorp".to_owned(),
326 ..Default::default()
327 };
328 assert!(settings.get_instance_name().is_err());
329
330 let settings = super::BigTableDbSettings {
331 table_name: "project/foo/instance/bar/tables/gorp".to_owned(),
332 ..Default::default()
333 };
334 assert!(settings.get_instance_name().is_err());
335
336 Ok(())
337 }
338}