Skip to main content

autopush_common/db/bigtable/
mod.rs

1/// This uses Google Cloud Platform (GCP) Bigtable as a storage and management
2/// system for Autopush Notifications and Routing information.
3///
4/// Bigtable has a single index key, and uses "cell family" designators to
5/// perform garbage collection.
6///
7/// Keys for the data are
8/// `{uaid}` - the meta data record around a given UAID record
9/// `{uaid}#{channelid}` - the meta record for a channel associated with a
10///     UAID
11/// `{uaid}#{channelid}#{sortkey_timestamp}` - a message record for a UAID
12///     and channel
13///
14/// Bigtable will automatically sort by the primary key. This schema uses
15/// regular expression lookups in order to do things like return the channels
16/// associated with a given UAID, fetch the appropriate topic messages, and
17/// other common functions. Please refer to the Bigtable documentation
18/// for how to create these keys, since they must be inclusive. Partial
19/// key matches will not return data. (e.g `/foo/` will not match `foobar`,
20/// but `/foo.*/` will)
21///
22mod 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;
35
36fn retry_default() -> usize {
37    bigtable_client::RETRY_COUNT
38}
39
40/// The settings for accessing the BigTable contents.
41#[derive(Clone, Debug, Deserialize)]
42pub struct BigTableDbSettings {
43    /// The Table name matches the GRPC template for table paths.
44    /// e.g. `projects/{projectid}/instances/{instanceid}/tables/{tablename}`
45    /// *NOTE* There is no leading `/`
46    /// By default, this (may?) use the `*` variant which translates to
47    /// `projects/*/instances/*/tables/*` which searches all data stored in
48    /// bigtable.
49    #[serde(default)]
50    pub table_name: String,
51    /// Routing replication profile id.
52    /// Should be used everywhere we set `table_name` when creating requests
53    #[serde(default)]
54    pub app_profile_id: String,
55    #[serde(default)]
56    pub router_family: String,
57    #[serde(default)]
58    pub message_family: String,
59    #[serde(default)]
60    pub message_topic_family: String,
61    #[serde(default)]
62    pub database_pool_max_size: Option<u32>,
63    /// Max time (in seconds) to wait to create a new connection to bigtable
64    #[serde(default)]
65    #[serde(deserialize_with = "deserialize_opt_u32_to_duration")]
66    pub database_pool_create_timeout: Option<Duration>,
67    /// Max time (in seconds) to wait for a socket to become available
68    #[serde(default)]
69    #[serde(deserialize_with = "deserialize_opt_u32_to_duration")]
70    pub database_pool_wait_timeout: Option<Duration>,
71    /// Max time(in seconds) to recycle a connection
72    #[serde(default)]
73    #[serde(deserialize_with = "deserialize_opt_u32_to_duration")]
74    pub database_pool_recycle_timeout: Option<Duration>,
75    /// Max time (in seconds) a connection should live
76    #[serde(default)]
77    #[serde(deserialize_with = "deserialize_opt_u32_to_duration")]
78    pub database_pool_connection_ttl: Option<Duration>,
79    /// Max idle time(in seconds) for a connection
80    #[serde(default)]
81    #[serde(deserialize_with = "deserialize_opt_u32_to_duration")]
82    pub database_pool_max_idle: Option<Duration>,
83    /// Include route to leader header in metadata
84    #[serde(default)]
85    pub route_to_leader: bool,
86    /// Number of times to retry a GRPC function
87    #[serde(default = "retry_default")]
88    pub retry_count: usize,
89    /// Max lifetime (in seconds) for a router entry
90    #[serde(default)]
91    #[serde(deserialize_with = "deserialize_opt_u32_to_duration")]
92    pub max_router_ttl: Option<Duration>,
93}
94
95// Used by test, but we don't want available for release.
96#[allow(clippy::derivable_impls)]
97#[cfg(test)]
98impl Default for BigTableDbSettings {
99    fn default() -> Self {
100        use crate::MAX_ROUTER_TTL_SECS;
101
102        Self {
103            table_name: Default::default(),
104            router_family: Default::default(),
105            message_family: Default::default(),
106            message_topic_family: Default::default(),
107            database_pool_max_size: Default::default(),
108            database_pool_create_timeout: Default::default(),
109            database_pool_wait_timeout: Default::default(),
110            database_pool_recycle_timeout: Default::default(),
111            database_pool_connection_ttl: Default::default(),
112            database_pool_max_idle: Default::default(),
113            route_to_leader: Default::default(),
114            retry_count: Default::default(),
115            app_profile_id: Default::default(),
116            max_router_ttl: Some(Duration::from_secs(MAX_ROUTER_TTL_SECS)),
117        }
118    }
119}
120
121impl BigTableDbSettings {
122    pub fn metadata(&self) -> Result<MetadataMap, BigTableError> {
123        MetadataBuilder::with_prefix(&self.table_name)
124            .routing_param("table_name", &self.table_name)
125            .route_to_leader(self.route_to_leader)
126            .build()
127    }
128
129    // Health may require a different metadata declaration.
130    pub fn health_metadata(&self) -> Result<MetadataMap, BigTableError> {
131        self.metadata()
132    }
133
134    pub fn get_instance_name(&self) -> Result<String, BigTableError> {
135        let parts: Vec<&str> = self.table_name.split('/').collect();
136        if parts.len() < 4 || parts[0] != "projects" || parts[2] != "instances" {
137            return Err(BigTableError::Config(
138                "Invalid table name specified. Cannot parse instance".to_owned(),
139            ));
140        }
141        Ok(parts[0..4].join("/"))
142    }
143}
144
145impl TryFrom<&str> for BigTableDbSettings {
146    type Error = DbError;
147    fn try_from(setting_string: &str) -> Result<Self, Self::Error> {
148        let mut me: Self = serde_json::from_str(setting_string)
149            .map_err(|e| DbError::General(format!("Could not parse DdbSettings: {e:?}")))?;
150
151        if me.table_name.starts_with('/') {
152            return Err(DbError::ConnectionError(
153                "Table name path begins with a '/'".to_owned(),
154            ));
155        };
156
157        // specify the default string "default" if it's not specified.
158        // There's a small chance that this could be reported as "unspecified", so this
159        // removes that confusion.
160        if me.app_profile_id.is_empty() {
161            "default".clone_into(&mut me.app_profile_id);
162        }
163
164        Ok(me)
165    }
166}
167
168mod tests {
169
170    #[test]
171    fn test_settings_parse() -> Result<(), crate::db::error::DbError> {
172        let settings =
173            super::BigTableDbSettings::try_from("{\"database_pool_create_timeout\": 123}")?;
174        assert_eq!(
175            settings.database_pool_create_timeout,
176            Some(std::time::Duration::from_secs(123))
177        );
178        Ok(())
179    }
180    #[test]
181    fn test_get_instance() -> Result<(), super::BigTableError> {
182        let settings = super::BigTableDbSettings {
183            table_name: "projects/foo/instances/bar/tables/gorp".to_owned(),
184            ..Default::default()
185        };
186        let res = settings.get_instance_name()?;
187        assert_eq!(res.as_str(), "projects/foo/instances/bar");
188
189        let settings = super::BigTableDbSettings {
190            table_name: "projects/foo/".to_owned(),
191            ..Default::default()
192        };
193        assert!(settings.get_instance_name().is_err());
194
195        let settings = super::BigTableDbSettings {
196            table_name: "protect/foo/instances/bar/tables/gorp".to_owned(),
197            ..Default::default()
198        };
199        assert!(settings.get_instance_name().is_err());
200
201        let settings = super::BigTableDbSettings {
202            table_name: "project/foo/instance/bar/tables/gorp".to_owned(),
203            ..Default::default()
204        };
205        assert!(settings.get_instance_name().is_err());
206
207        Ok(())
208    }
209}