Skip to main content

autopush_common/db/bigtable/bigtable_client/
mod.rs

1use std::borrow::Cow;
2use std::collections::{BTreeMap, HashMap, HashSet};
3use std::error::Error as StdError;
4use std::fmt;
5use std::fmt::Display;
6use std::future::Future;
7use std::str::FromStr;
8use std::sync::atomic::{AtomicBool, AtomicU32, AtomicU64, Ordering};
9use std::sync::{Arc, Mutex};
10use std::time::{Duration, SystemTime};
11
12use again::RetryPolicy;
13use async_trait::async_trait;
14use cadence::StatsdClient;
15#[cfg(feature = "reliable_report")]
16use chrono::TimeDelta;
17use gcp_auth::TokenProvider;
18use googleapis_tonic_google_bigtable_v2::google::bigtable::v2 as bigtable;
19use googleapis_tonic_google_bigtable_v2::google::bigtable::v2::bigtable_client::BigtableClient;
20use serde_json::{from_str, json};
21use tonic::metadata::{AsciiMetadataValue, MetadataMap};
22use tonic::transport::Channel;
23use tonic::{Code, Request, Status};
24use uuid::Uuid;
25
26use crate::MAX_ROUTER_TTL_SECS;
27use crate::db::{
28    DbSettings, Notification, USER_RECORD_VERSION, User,
29    client::{DbClient, FetchMessageResponse},
30    error::{DbError, DbResult},
31    models::RangeKey,
32};
33use crate::metric_name::MetricName;
34use crate::metrics::StatsdClientExt;
35
36pub use self::metadata::MetadataBuilder;
37use self::row::{Row, RowCells};
38use super::BigTableDbSettings;
39use super::pool::BigTablePool;
40
41pub mod cell;
42pub mod error;
43pub(crate) mod merge;
44pub mod metadata;
45pub mod row;
46
47// these are normally Vec<u8>
48pub type RowKey = String;
49
50// These are more for code clarity than functional types.
51// Rust will happily swap between the two in any case.
52// See [super::row::Row] for discussion about how these
53// are overloaded in order to simplify fetching data.
54pub type Qualifier = String;
55pub type FamilyId = String;
56
57const ROUTER_FAMILY: &str = "router";
58const MESSAGE_FAMILY: &str = "message"; // The default family for messages
59const MESSAGE_TOPIC_FAMILY: &str = "message_topic";
60#[cfg(feature = "reliable_report")]
61const RELIABLE_LOG_FAMILY: &str = "reliability";
62#[cfg(feature = "reliable_report")]
63/// The maximum TTL for reliability logging (60 days).
64/// /// In most use cases, converted to seconds through .num_seconds().
65pub const RELIABLE_LOG_TTL: TimeDelta = TimeDelta::days(60);
66
67/// Default number of retries after the initial Bigtable RPC attempt.
68///
69/// So a connectivity failure cannot fan out into a
70/// retry storm, we try to keep this number small.
71/// Can be overridden through `db_settings.retry_count`; health checks use the
72/// same configured value as other point reads.
73pub(crate) const RETRY_COUNT: usize = 2;
74
75/// Maximum gRPC message size (256MB), matching the prior grpcio configuration.
76const MAX_MESSAGE_LEN: usize = 1 << 28;
77
78/// OAuth2 scopes requested for the Bigtable data API.
79const BIGTABLE_DATA_SCOPES: &[&str] = &["https://www.googleapis.com/auth/bigtable.data"];
80
81/// Simple circuit breaker to prevent retry storms during BigTable outages.
82///
83/// After `failure_threshold` consecutive failures, the circuit opens and
84/// requests fail fast for `cooldown_secs` seconds before allowing a retry.
85#[derive(Debug)]
86pub struct CircuitBreaker {
87    consecutive_failures: AtomicU32,
88    opened_at_epoch_secs: AtomicU64,
89    failure_threshold: u32,
90    cooldown_secs: u64,
91}
92
93impl CircuitBreaker {
94    pub fn new(failure_threshold: u32, cooldown_secs: u64) -> Self {
95        Self {
96            consecutive_failures: AtomicU32::new(0),
97            opened_at_epoch_secs: AtomicU64::new(0),
98            failure_threshold,
99            cooldown_secs,
100        }
101    }
102
103    /// Check if the circuit is allowing requests through.
104    /// Returns true if the request should proceed, false if it should fail fast.
105    pub fn allow_request(&self) -> bool {
106        let failures = self.consecutive_failures.load(Ordering::Relaxed);
107        if failures < self.failure_threshold {
108            return true;
109        }
110        // Circuit is open — check if cooldown has elapsed
111        let opened_at = self.opened_at_epoch_secs.load(Ordering::Relaxed);
112        let now = SystemTime::now()
113            .duration_since(SystemTime::UNIX_EPOCH)
114            .unwrap_or_default()
115            .as_secs();
116        if now.saturating_sub(opened_at) >= self.cooldown_secs {
117            // Allow a single probe request (half-open state)
118            true
119        } else {
120            false
121        }
122    }
123
124    /// Record a successful operation, resetting the circuit breaker.
125    pub fn record_success(&self) {
126        self.consecutive_failures.store(0, Ordering::Relaxed);
127    }
128
129    /// Record a failed operation.
130    pub fn record_failure(&self) {
131        let prev = self.consecutive_failures.fetch_add(1, Ordering::Relaxed);
132        if prev + 1 >= self.failure_threshold {
133            let now = SystemTime::now()
134                .duration_since(SystemTime::UNIX_EPOCH)
135                .unwrap_or_default()
136                .as_secs();
137            self.opened_at_epoch_secs.store(now, Ordering::Relaxed);
138        }
139    }
140}
141
142impl Default for CircuitBreaker {
143    fn default() -> Self {
144        // Open after 5 consecutive failures, cooldown for 30 seconds
145        Self::new(5, 30)
146    }
147}
148
149/// Semi convenience wrapper to ensure that the UAID is formatted and displayed consistently.
150// TODO:Should we create something similar for ChannelID?
151struct Uaid(Uuid);
152
153impl Display for Uaid {
154    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
155        write!(f, "{}", self.0.as_simple())
156    }
157}
158
159impl From<Uaid> for String {
160    fn from(uaid: Uaid) -> String {
161        uaid.0.as_simple().to_string()
162    }
163}
164
165#[derive(Clone)]
166/// Bigtable-backed implementation of the application's database interface.
167pub struct BigTableClientImpl {
168    pub(crate) settings: BigTableDbSettings,
169    /// Metrics client
170    metrics: Arc<StatsdClient>,
171    /// Logical-operation pool and shared tonic channel set.
172    pool: BigTablePool,
173    metadata: MetadataMap,
174    /// Circuit breaker to prevent retry storms during BigTable outages
175    circuit_breaker: Arc<CircuitBreaker>,
176}
177
178/// Return a RowFilter matching the `maxversions=1` portion of a family's GC
179/// policy (keep only the most recent cell per column). Shared by the router and
180/// message family filters below.
181fn max_versions_filter() -> bigtable::RowFilter {
182    bigtable::RowFilter {
183        filter: Some(bigtable::row_filter::Filter::CellsPerColumnLimitFilter(1)),
184    }
185}
186
187/// Return a RowFilter that excludes already-expired cells.
188///
189/// Bigtable's server-side garbage collection can lag the expiry time by days,
190/// so this filters out any cells that have expired in the past but haven't yet
191/// been garbage collected.
192fn expiry_filter() -> Result<bigtable::RowFilter, error::BigTableError> {
193    let bt_now: i64 = SystemTime::now()
194        .duration_since(SystemTime::UNIX_EPOCH)
195        .map_err(error::BigTableError::WriteTime)?
196        .as_millis() as i64;
197    Ok(bigtable::RowFilter {
198        filter: Some(bigtable::row_filter::Filter::TimestampRangeFilter(
199            bigtable::TimestampRange {
200                start_timestamp_micros: bt_now * 1000,
201                end_timestamp_micros: 0,
202            },
203        )),
204    })
205}
206
207/// Return a chain of RowFilters matching the GC policy of the router Column
208/// Family: the `maxversions=1` limit plus `max_age`-based expiry (see
209/// [expiry_filter]). Mirrors [message_gc_policy_filter] for the router family.
210fn router_gc_policy_filter() -> Result<Vec<bigtable::RowFilter>, error::BigTableError> {
211    Ok(vec![max_versions_filter(), expiry_filter()?])
212}
213
214/// Return a chain of RowFilters matching the GC policy of the message Column
215/// Families
216fn message_gc_policy_filter() -> Result<Vec<bigtable::RowFilter>, error::BigTableError> {
217    Ok(vec![max_versions_filter(), expiry_filter()?])
218}
219
220/// Return a Column family regex RowFilter
221fn family_filter(regex: String) -> bigtable::RowFilter {
222    bigtable::RowFilter {
223        filter: Some(bigtable::row_filter::Filter::FamilyNameRegexFilter(regex)),
224    }
225}
226
227/// Escape bytes for RE values
228///
229/// Based off google-re2/perl's quotemeta function
230fn escape_bytes(bytes: &[u8]) -> Vec<u8> {
231    let mut vec = Vec::with_capacity(bytes.len() * 2);
232    for &b in bytes {
233        if !b.is_ascii_alphanumeric() && b != b'_' && (b & 128) == 0 {
234            if b == b'\0' {
235                // Special handling for null: Note that this special handling
236                // is not strictly required for RE2, but this quoting is
237                // required for other regexp libraries such as PCRE.
238                // Can't use "\\0" since the next character might be a digit.
239                vec.extend("\\x00".as_bytes());
240                continue;
241            }
242            vec.push(b'\\');
243        }
244        vec.push(b);
245    }
246    vec
247}
248
249/// Return a chain of RowFilters limiting to a match of the specified
250/// `version`'s column value
251fn version_filter(version: &Uuid) -> Vec<bigtable::RowFilter> {
252    let cq_filter = bigtable::RowFilter {
253        filter: Some(bigtable::row_filter::Filter::ColumnQualifierRegexFilter(
254            "^version$".as_bytes().to_vec(),
255        )),
256    };
257    let value_filter = bigtable::RowFilter {
258        filter: Some(bigtable::row_filter::Filter::ValueRegexFilter(
259            escape_bytes(version.as_bytes()),
260        )),
261    };
262
263    vec![
264        family_filter(format!("^{ROUTER_FAMILY}$")),
265        cq_filter,
266        value_filter,
267    ]
268}
269
270/// Return a newly generated `version` column `Cell`
271fn new_version_cell(timestamp: SystemTime) -> cell::Cell {
272    cell::Cell {
273        qualifier: "version".to_owned(),
274        value: Uuid::new_v4().into(),
275        timestamp,
276        ..Default::default()
277    }
278}
279
280/// Return a RowFilter chain from multiple RowFilters
281fn filter_chain(filters: Vec<bigtable::RowFilter>) -> bigtable::RowFilter {
282    bigtable::RowFilter {
283        filter: Some(bigtable::row_filter::Filter::Chain(
284            bigtable::row_filter::Chain { filters },
285        )),
286    }
287}
288
289/// Return a ReadRowsRequest against table for a given row key
290fn read_row_request(
291    table_name: &str,
292    app_profile_id: &str,
293    row_key: &str,
294) -> bigtable::ReadRowsRequest {
295    bigtable::ReadRowsRequest {
296        table_name: table_name.to_owned(),
297        app_profile_id: app_profile_id.to_owned(),
298        rows: Some(bigtable::RowSet {
299            row_keys: vec![row_key.as_bytes().to_vec()],
300            row_ranges: Vec::new(),
301        }),
302        ..Default::default()
303    }
304}
305
306fn to_u64(value: Vec<u8>, name: &str) -> Result<u64, DbError> {
307    let v: [u8; 8] = value
308        .try_into()
309        .map_err(|_| DbError::DeserializeU64(name.to_owned()))?;
310    Ok(u64::from_be_bytes(v))
311}
312
313fn to_string(value: Vec<u8>, name: &str) -> Result<String, DbError> {
314    String::from_utf8(value).map_err(|e| {
315        debug!("🉑 cannot read string {}: {:?}", name, e);
316        DbError::DeserializeString(name.to_owned())
317    })
318}
319
320/// Parse the "set" (see [DbClient::add_channels]) of channel ids in a bigtable Row.
321///
322/// Cells should solely contain the set of channels otherwise an Error is returned.
323fn channels_from_cells(cells: &RowCells) -> DbResult<HashSet<Uuid>> {
324    let mut result = HashSet::new();
325    for cells in cells.values() {
326        let Some(cell) = cells.last() else {
327            continue;
328        };
329        let Some((_, chid)) = cell.qualifier.split_once("chid:") else {
330            return Err(DbError::Integrity(
331                "get_channels expected: chid:<chid>".to_owned(),
332                None,
333            ));
334        };
335        result.insert(Uuid::from_str(chid).map_err(|e| DbError::General(e.to_string()))?);
336    }
337    Ok(result)
338}
339
340/// Convert the [HashSet] of channel ids to cell entries for a bigtable Row
341fn channels_to_cells(channels: Cow<HashSet<Uuid>>, expiry: SystemTime) -> Vec<cell::Cell> {
342    let channels = channels.into_owned();
343    let mut cells = Vec::with_capacity(channels.len().min(100_000));
344    for (i, channel_id) in channels.into_iter().enumerate() {
345        // There is a limit of 100,000 mutations per batch for bigtable.
346        // https://cloud.google.com/bigtable/quotas
347        // If you have 100,000 channels, you have too many.
348        if i >= 100_000 {
349            break;
350        }
351        cells.push(cell::Cell {
352            qualifier: format!("chid:{}", channel_id.as_hyphenated()),
353            timestamp: expiry,
354            ..Default::default()
355        });
356    }
357    cells
358}
359
360pub fn retry_policy(max: usize) -> RetryPolicy {
361    RetryPolicy::default()
362        .with_max_retries(max)
363        .with_jitter(true)
364}
365
366#[derive(Clone, Copy, Debug, Eq, PartialEq)]
367enum RetryKind {
368    Read,
369    IdempotentWrite,
370    ConditionalWrite,
371}
372
373#[derive(Clone, Copy, Debug)]
374enum RpcClass {
375    Point,
376    Scan,
377}
378
379#[derive(Clone, Copy, Debug, Eq, PartialEq)]
380enum BreakerPolicy {
381    Track,
382    Ignore,
383}
384
385#[derive(Clone, Copy, Debug)]
386struct RpcPolicy {
387    attempt_timeout: Duration,
388    total_timeout: Duration,
389    retry_kind: RetryKind,
390    breaker: BreakerPolicy,
391}
392
393async fn rpc_attempt_until<T>(
394    deadline: tokio::time::Instant,
395    future: impl Future<Output = Result<T, error::BigTableError>>,
396) -> Result<T, error::BigTableError> {
397    tokio::time::timeout_at(deadline, future)
398        .await
399        .map_err(|_| error::BigTableError::AttemptTimeout)?
400}
401
402async fn operation_budget_until<T>(
403    deadline: tokio::time::Instant,
404    future: impl Future<Output = Result<T, error::BigTableError>>,
405) -> Result<T, error::BigTableError> {
406    tokio::time::timeout_at(deadline, future)
407        .await
408        .map_err(|_| error::BigTableError::OperationTimeout)?
409}
410
411fn retryable_pre_send_err(status: &Status) -> bool {
412    (status.code() == Code::Unknown
413        && status
414            .message()
415            .to_ascii_lowercase()
416            .starts_with("service was not ready:"))
417        || has_connect_source(status)
418}
419
420fn retryable_transient_err(status: &Status) -> bool {
421    if retryable_pre_send_err(status) {
422        return true;
423    }
424    match status.code() {
425        Code::Internal => {
426            let message = status.message().to_ascii_lowercase();
427            [
428                "rst_stream",
429                "rst stream",
430                "received unexpected eos on data frame from server",
431            ]
432            .iter()
433            .any(|fragment| message.contains(fragment))
434        }
435        Code::Unavailable | Code::DeadlineExceeded => true,
436        _ => false,
437    }
438}
439
440/// Return whether a failed read is safe to retry.
441///
442/// In addition to the statuses that are retryable for every operation, tonic
443/// can surface HTTP/2 connection retirement as either:
444///
445/// - `Internal("h2 protocol error: http2 error")` for a remote GOAWAY, or
446/// - `Cancelled("operation was canceled")` when hyper closes the connection, or
447/// - `Unknown("transport error")` when a connection-level failure (e.g. a
448///   GFE-reaped idle connection surfacing as a broken pipe) is written to.
449///
450/// Reads are idempotent, so replaying these bounded attempts is safe. Autopush
451/// `MutateRow` requests are also replay-safe: set-cell retries reuse the same
452/// explicit timestamp, delete mutations are idempotent, and every attempt
453/// clones the same request. Conditional `CheckAndMutateRow` operations use the
454/// narrower pre-send-only policy because their returned predicate result can
455/// change after an applied-but-lost attempt.
456fn retryable_read_err(status: &Status) -> bool {
457    if retryable_transient_err(status) {
458        return true;
459    }
460    // Tonic 0.14.6 uses both Internal (for some header/GOAWAY paths) and
461    // Unknown (for a connection lost while streaming a body) for this wrapper
462    // message. The exact Hyper/H2 suffix is not a stable API.
463    if matches!(status.code(), Code::Internal | Code::Unknown)
464        && status
465            .message()
466            .to_ascii_lowercase()
467            .starts_with("h2 protocol error:")
468    {
469        return true;
470    }
471    match status.code() {
472        Code::Cancelled => status
473            .message()
474            .eq_ignore_ascii_case("operation was canceled"),
475        // Tonic retains the transport and OS errors in the source chain. Use
476        // their types rather than matching an unstable display string.
477        Code::Unknown => has_transport_source(status),
478        _ => false,
479    }
480}
481
482fn has_transport_source(status: &Status) -> bool {
483    let mut source = StdError::source(status);
484    while let Some(error) = source {
485        if error.downcast_ref::<tonic::transport::Error>().is_some()
486            || error.downcast_ref::<std::io::Error>().is_some()
487        {
488            return true;
489        }
490        source = error.source();
491    }
492    false
493}
494
495fn has_connect_source(status: &Status) -> bool {
496    let mut source = StdError::source(status);
497    while let Some(error) = source {
498        if error.downcast_ref::<tonic::ConnectError>().is_some() {
499            return true;
500        }
501        source = error.source();
502    }
503    false
504}
505
506fn counts_toward_breaker(error: &error::BigTableError, operation_timed_out_in_rpc: bool) -> bool {
507    matches!(
508        error,
509        error::BigTableError::InvalidRowResponse(_)
510            | error::BigTableError::InvalidChunk(_)
511            | error::BigTableError::Read(_)
512            | error::BigTableError::Write(_)
513            | error::BigTableError::AttemptTimeout
514            | error::BigTableError::Status(_, _)
515    ) || matches!(error, error::BigTableError::OperationTimeout) && operation_timed_out_in_rpc
516}
517
518#[cfg(test)]
519mod retry_tests {
520    use std::convert::Infallible;
521    use std::sync::Mutex;
522    use std::sync::atomic::AtomicUsize;
523
524    use futures::StreamExt;
525    use http_body_util::StreamBody;
526    use hyper::body::{Bytes, Frame, Incoming};
527    use hyper::server::conn::http2;
528    use hyper::service::service_fn;
529    use hyper::{Request as HyperRequest, Response as HyperResponse};
530    use hyper_util::rt::{TokioExecutor, TokioIo};
531    use tokio::net::TcpListener;
532    use tokio::sync::oneshot;
533
534    use super::*;
535    #[derive(Debug, thiserror::Error)]
536    #[error("transport error")]
537    struct TestTransportError {
538        #[source]
539        source: std::io::Error,
540    }
541
542    fn broken_pipe_status() -> Status {
543        Status::from_error(Box::new(TestTransportError {
544            source: std::io::Error::from(std::io::ErrorKind::BrokenPipe),
545        }))
546    }
547
548    #[test]
549    fn retries_tonic_pre_send_readiness_errors() {
550        let status = Status::unknown("Service was not ready: transport error");
551        assert!(retryable_pre_send_err(&status));
552    }
553
554    #[test]
555    fn retries_source_backed_connect_errors_before_conditional_writes() {
556        let connect = tonic::ConnectError(Box::new(std::io::Error::from(
557            std::io::ErrorKind::ConnectionRefused,
558        )));
559        let status = Status::from_error(Box::new(connect));
560
561        assert_eq!(status.code(), Code::Unavailable);
562        assert!(retryable_pre_send_err(&status));
563    }
564
565    #[test]
566    fn does_not_retry_arbitrary_unknown_errors() {
567        let status = Status::unknown("operation outcome is unknown");
568        assert!(!retryable_pre_send_err(&status));
569    }
570
571    #[test]
572    fn retries_transient_bigtable_statuses() {
573        assert!(retryable_transient_err(&Status::unavailable(
574            "No zones were available"
575        )));
576        assert!(retryable_transient_err(&Status::deadline_exceeded(
577            "deadline exceeded"
578        )));
579        assert!(retryable_transient_err(&Status::internal(
580            "stream terminated by RST_STREAM before headers"
581        )));
582    }
583
584    #[test]
585    fn retries_observed_tonic_transport_failures_for_reads() {
586        let goaway = Status::internal("h2 protocol error: http2 error");
587        assert!(retryable_read_err(&goaway));
588        assert!(classify_retry(RetryKind::Read, &error::BigTableError::Read(goaway)).is_some());
589
590        let connection_closed = Status::cancelled("operation was canceled");
591        assert!(retryable_read_err(&connection_closed));
592        assert!(
593            classify_retry(
594                RetryKind::Read,
595                &error::BigTableError::Read(connection_closed)
596            )
597            .is_some()
598        );
599
600        // A GFE-reaped idle connection written to surfaces as
601        // `Unknown("transport error")` (the "broken pipe" io error is in the
602        // source, not the status message).
603        let transport_error = broken_pipe_status();
604        assert_eq!(transport_error.code(), Code::Unknown);
605        assert_eq!(transport_error.message(), "transport error");
606        assert!(retryable_read_err(&transport_error));
607        assert!(
608            classify_retry(
609                RetryKind::Read,
610                &error::BigTableError::Read(transport_error)
611            )
612            .is_some()
613        );
614    }
615
616    #[test]
617    fn does_not_retry_non_transport_unknown_reads() {
618        // `Unknown` is a catch-all that also covers server-returned application
619        // errors; require a transport or IO error in the source chain.
620        assert!(!retryable_read_err(&Status::unknown(
621            "some application error"
622        )));
623    }
624
625    #[test]
626    fn retries_idempotent_but_not_conditional_writes() {
627        let goaway =
628            error::BigTableError::Write(Status::internal("h2 protocol error: http2 error"));
629        assert!(classify_retry(RetryKind::IdempotentWrite, &goaway).is_some());
630        let conditional_goaway =
631            error::BigTableError::Write(Status::internal("h2 protocol error: http2 error"));
632        assert!(classify_retry(RetryKind::ConditionalWrite, &conditional_goaway).is_none());
633
634        let connection_closed =
635            error::BigTableError::Write(Status::cancelled("operation was canceled"));
636        assert!(classify_retry(RetryKind::IdempotentWrite, &connection_closed).is_some());
637
638        let transport_error = error::BigTableError::Write(broken_pipe_status());
639        assert!(classify_retry(RetryKind::IdempotentWrite, &transport_error).is_some());
640
641        let unavailable = error::BigTableError::Write(Status::unavailable("try again"));
642        assert!(classify_retry(RetryKind::IdempotentWrite, &unavailable).is_some());
643        assert!(classify_retry(RetryKind::ConditionalWrite, &unavailable).is_none());
644
645        let attempt_timeout = error::BigTableError::AttemptTimeout;
646        assert!(classify_retry(RetryKind::Read, &attempt_timeout).is_some());
647        assert!(classify_retry(RetryKind::ConditionalWrite, &attempt_timeout).is_none());
648    }
649
650    #[actix_rt::test]
651    async fn attempt_and_operation_timeouts_are_enforced() {
652        let attempt = rpc_attempt_until::<()>(
653            tokio::time::Instant::now() + Duration::from_millis(1),
654            async { futures::future::pending().await },
655        )
656        .await;
657        assert!(matches!(attempt, Err(error::BigTableError::AttemptTimeout)));
658
659        let operation = operation_budget_until::<()>(
660            tokio::time::Instant::now() + Duration::from_millis(1),
661            async { futures::future::pending().await },
662        )
663        .await;
664        assert!(matches!(
665            operation,
666            Err(error::BigTableError::OperationTimeout)
667        ));
668    }
669
670    #[actix_rt::test]
671    async fn retries_a_midstream_drop_on_the_next_channel() {
672        let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
673        let address = listener.local_addr().unwrap();
674        let (drop_connection, wait_for_drop) = oneshot::channel::<()>();
675        let (finish_server, wait_for_finish) = oneshot::channel::<()>();
676
677        let server = tokio::spawn(async move {
678            // The first channel begins a valid streaming response and then
679            // loses its transport. This is the failure mode observed when a
680            // load balancer reaps an idle HTTP/2 connection.
681            let (socket, _) = listener.accept().await.unwrap();
682            let service = service_fn(|_request: HyperRequest<Incoming>| async move {
683                // One valid, empty ReadRowsResponse followed by a body that
684                // never completes. The client signals only after tonic has
685                // decoded this message, guaranteeing a mid-stream drop.
686                let first_message = Frame::data(Bytes::from_static(&[0, 0, 0, 0, 0]));
687                let body = StreamBody::new(
688                    futures::stream::once(
689                        async move { Ok::<Frame<Bytes>, Infallible>(first_message) },
690                    )
691                    .chain(futures::stream::pending::<Result<Frame<Bytes>, Infallible>>()),
692                );
693                Ok::<_, Infallible>(
694                    HyperResponse::builder()
695                        .status(200)
696                        .header("content-type", "application/grpc")
697                        .body(body)
698                        .unwrap(),
699                )
700            });
701            {
702                let connection = http2::Builder::new(TokioExecutor::new())
703                    .serve_connection(TokioIo::new(socket), service);
704                tokio::pin!(connection);
705                tokio::select! {
706                    result = &mut connection => {
707                        panic!("test HTTP/2 connection ended before it was dropped: {result:?}");
708                    }
709                    _ = wait_for_drop => {}
710                }
711            }
712
713            // A retry must select the second channel. Return a complete,
714            // successful gRPC stream there so success proves both retry
715            // classification and fresh-channel selection end to end.
716            let (socket, _) = listener.accept().await.unwrap();
717            let service = service_fn(|_request: HyperRequest<Incoming>| async move {
718                // A trailers-only success is the canonical empty gRPC
719                // streaming response and ends the stream immediately.
720                let body =
721                    StreamBody::new(futures::stream::empty::<Result<Frame<Bytes>, Infallible>>());
722                Ok::<_, Infallible>(
723                    HyperResponse::builder()
724                        .status(200)
725                        .header("content-type", "application/grpc")
726                        .header("grpc-status", "0")
727                        .body(body)
728                        .unwrap(),
729                )
730            });
731            let connection = http2::Builder::new(TokioExecutor::new())
732                .serve_connection(TokioIo::new(socket), service);
733            tokio::pin!(connection);
734            tokio::select! {
735                result = &mut connection => {
736                    panic!("successful HTTP/2 connection ended unexpectedly: {result:?}");
737                }
738                _ = wait_for_finish => {}
739            }
740        });
741
742        let metrics = Arc::new(StatsdClient::builder("", cadence::NopMetricSink).build());
743        let settings = DbSettings {
744            dsn: Some(format!("grpc://localhost:{}", address.port())),
745            db_settings: json!({
746                "table_name": "projects/test/instances/test/tables/test",
747                "grpc_channel_count": 2,
748                "retry_count": 1,
749                "grpc_connect_timeout": 2,
750                "grpc_point_attempt_timeout": 5,
751                "grpc_point_total_timeout": 10
752            })
753            .to_string(),
754        };
755        let client = BigTableClientImpl::new(metrics, &settings).unwrap();
756        let drop_connection = Arc::new(Mutex::new(Some(drop_connection)));
757        let policy = client.rpc_policy(RpcClass::Point, RetryKind::Read, BreakerPolicy::Track);
758        let started = tokio::time::Instant::now();
759        tokio::time::timeout(
760            Duration::from_secs(8),
761            client.execute_rpc(
762                bigtable::ReadRowsRequest::default(),
763                policy,
764                move |channel, request| {
765                    let drop_connection = drop_connection.clone();
766                    async move {
767                        let mut bigtable = BigtableDb::client(channel);
768                        let mut stream = bigtable
769                            .read_rows(request)
770                            .await
771                            .map_err(error::BigTableError::Read)?
772                            .into_inner();
773
774                        let signal = drop_connection
775                            .lock()
776                            .unwrap_or_else(|poisoned| poisoned.into_inner())
777                            .take();
778                        if let Some(signal) = signal {
779                            assert!(stream.message().await.unwrap().is_some());
780                            signal.send(()).unwrap();
781                        }
782                        while stream
783                            .message()
784                            .await
785                            .map_err(error::BigTableError::Read)?
786                            .is_some()
787                        {}
788                        Ok(())
789                    }
790                },
791            ),
792        )
793        .await
794        .expect("Bigtable retry exceeded the test budget")
795        .expect("the second channel should complete the read");
796        assert!(
797            started.elapsed() < Duration::from_secs(5),
798            "the retry waited for the attempt deadline instead of classifying the transport drop"
799        );
800
801        finish_server.send(()).unwrap();
802        server.await.unwrap();
803    }
804
805    #[test]
806    fn default_retry_budget_is_bounded() {
807        assert_eq!(RETRY_COUNT, 2);
808    }
809
810    #[actix_rt::test]
811    async fn request_preserves_metadata_and_grpc_timeout() {
812        let db = BigtableDb::new(None);
813        let mut metadata = MetadataMap::new();
814        metadata.insert("x-goog-request-params", "table_name=test".parse().unwrap());
815
816        let request = db
817            .request(
818                bigtable::ReadRowsRequest::default(),
819                &metadata,
820                tokio::time::Instant::now() + Duration::from_secs(5),
821            )
822            .await
823            .unwrap();
824
825        assert_eq!(
826            request.metadata().get("x-goog-request-params").unwrap(),
827            "table_name=test"
828        );
829        assert!(request.metadata().get("grpc-timeout").is_some());
830    }
831
832    #[actix_rt::test]
833    async fn retry_metric_is_consumed_only_by_a_followup_attempt() {
834        let pending = Arc::new(Mutex::new(None));
835        let attempt_pending = pending.clone();
836        let condition_pending = pending.clone();
837        let attempts = Arc::new(AtomicUsize::new(0));
838        let attempt_count = attempts.clone();
839        let emitted = Arc::new(AtomicUsize::new(0));
840        let emitted_count = emitted.clone();
841
842        let result = RetryPolicy::fixed(Duration::ZERO)
843            .with_max_retries(1)
844            .retry_if(
845                || {
846                    attempt_count.fetch_add(1, Ordering::Relaxed);
847                    if take_pending_retry_metric(&attempt_pending).is_some() {
848                        emitted_count.fetch_add(1, Ordering::Relaxed);
849                    }
850                    async { Err::<(), _>(error::BigTableError::AttemptTimeout) }
851                },
852                move |error: &error::BigTableError| {
853                    if let Some(retry_metric) = classify_retry(RetryKind::Read, error) {
854                        set_pending_retry_metric(&condition_pending, retry_metric);
855                        true
856                    } else {
857                        false
858                    }
859                },
860            )
861            .await;
862
863        assert!(matches!(result, Err(error::BigTableError::AttemptTimeout)));
864        assert_eq!(attempts.load(Ordering::Relaxed), 2);
865        assert_eq!(emitted.load(Ordering::Relaxed), 1);
866    }
867
868    #[test]
869    fn local_failures_do_not_count_toward_the_backend_breaker() {
870        assert!(!counts_toward_breaker(
871            &error::BigTableError::PreSendTimeout,
872            false,
873        ));
874        assert!(!counts_toward_breaker(
875            &error::BigTableError::CircuitBreakerOpen,
876            false,
877        ));
878        assert!(!counts_toward_breaker(
879            &error::BigTableError::OperationTimeout,
880            false,
881        ));
882        assert!(counts_toward_breaker(
883            &error::BigTableError::OperationTimeout,
884            true,
885        ));
886        assert!(counts_toward_breaker(
887            &error::BigTableError::Read(Status::unavailable("backend unavailable")),
888            false,
889        ));
890    }
891}
892
893pub fn metric(metrics: &Arc<StatsdClient>, err_type: &str, code: Option<&str>) {
894    let mut metric = metrics
895        .incr_with_tags(MetricName::DatabaseRetry)
896        .with_tag("error", err_type)
897        .with_tag("type", "bigtable");
898    if let Some(code) = code {
899        metric = metric.with_tag("code", code);
900    }
901    metric.send();
902}
903
904#[derive(Clone, Copy, Debug)]
905struct RetryMetric {
906    error: &'static str,
907    code: Option<Code>,
908}
909
910impl RetryMetric {
911    fn send(self, metrics: &Arc<StatsdClient>) {
912        let code = self.code.map(|code| format!("{code:?}"));
913        metric(metrics, self.error, code.as_deref());
914    }
915}
916
917fn take_pending_retry_metric(pending: &Mutex<Option<RetryMetric>>) -> Option<RetryMetric> {
918    pending
919        .lock()
920        .unwrap_or_else(|poisoned| poisoned.into_inner())
921        .take()
922}
923
924fn set_pending_retry_metric(pending: &Mutex<Option<RetryMetric>>, metric: RetryMetric) {
925    *pending
926        .lock()
927        .unwrap_or_else(|poisoned| poisoned.into_inner()) = Some(metric);
928}
929
930/// Classify a safe retry and describe the metric to emit if its next attempt
931/// actually starts.
932fn classify_retry(kind: RetryKind, err: &error::BigTableError) -> Option<RetryMetric> {
933    debug!("🉑 Checking BigTableError...{err}");
934    match err {
935        error::BigTableError::InvalidRowResponse(status)
936        | error::BigTableError::Read(status)
937        | error::BigTableError::Write(status) => {
938            let retry = match kind {
939                RetryKind::Read | RetryKind::IdempotentWrite => retryable_read_err(status),
940                RetryKind::ConditionalWrite => retryable_pre_send_err(status),
941            };
942            info!("GRPC Failure: {:?}", status);
943            retry.then_some(RetryMetric {
944                error: "RpcFailure",
945                code: Some(status.code()),
946            })
947        }
948        error::BigTableError::AttemptTimeout if kind != RetryKind::ConditionalWrite => {
949            Some(RetryMetric {
950                error: "AttemptTimeout",
951                code: None,
952            })
953        }
954        // Authentication and request construction happen before the RPC is
955        // dispatched, so their deadline is safe to retry for every method,
956        // including conditional mutations.
957        error::BigTableError::PreSendTimeout => Some(RetryMetric {
958            error: "PreSendTimeout",
959            code: None,
960        }),
961        // Failures to fetch an OAuth token (e.g. a transient metadata server
962        // hiccup) are retryable, matching grpcio's prior behavior.
963        error::BigTableError::Auth(_) => Some(RetryMetric {
964            error: "Auth",
965            code: None,
966        }),
967        _ => None,
968    }
969}
970
971/// Determine if a router record is "incomplete" (doesn't include [User]
972/// columns):
973///
974/// They can be incomplete for a couple reasons:
975///
976/// 1) A migration code bug caused a few incomplete migrations where
977///    `add_channels` and `increment_storage` calls occurred when the migration's
978///    initial `add_user` was never completed:
979///    https://github.com/mozilla-services/autopush-rs/pull/640
980///
981/// 2) When router TTLs are eventually enabled: `add_channel` and
982///    `increment_storage` can write cells with later expiry times than the other
983///    router cells
984fn is_incomplete_router_record(cells: &RowCells) -> bool {
985    cells
986        .keys()
987        .all(|k| ["current_timestamp", "version"].contains(&k.as_str()) || k.starts_with("chid:"))
988}
989
990/// Connect to a BigTable storage model.
991///
992/// BigTable is available via the Google Console, and is a schema less storage system.
993///
994/// The `db_dsn` string should be in the form of
995/// `grpc://{BigTableEndpoint}`
996///
997/// The settings contains the `table_name` which is the GRPC path to the data.
998/// (e.g. `projects/{project_id}/instances/{instance_id}/tables/{table_id}`)
999///
1000/// where:
1001/// _BigTableEndpoint_ is the endpoint domain to use (the default is `bigtable.googleapis.com`) See
1002/// [BigTable Endpoints](https://cloud.google.com/bigtable/docs/regional-endpoints) for more details.
1003/// _project-id_ is the Google project identifier (see the Google developer console (e.g. 'autopush-dev'))
1004/// _instance-id_ is the Google project instance, (see the Google developer console (e.g. 'development-1'))
1005/// _table_id_ is the Table Name (e.g. 'autopush')
1006///
1007/// This will automatically bring in the default credentials specified by the `GOOGLE_APPLICATION_CREDENTIALS`
1008/// environment variable.
1009///
1010/// NOTE: Some configurations may look for the default credential file (pointed to by
1011/// `GOOGLE_APPLICATION_CREDENTIALS`) to be stored in
1012/// `$HOME/.config/gcloud/application_default_credentials.json`)
1013///
1014impl BigTableClientImpl {
1015    pub fn new(metrics: Arc<StatsdClient>, settings: &DbSettings) -> DbResult<Self> {
1016        debug!("🏊 BT Pool new");
1017        let db_settings = BigTableDbSettings::try_from(settings.db_settings.as_ref())?;
1018        info!("🉑 {:#?}", db_settings);
1019        let pool = BigTablePool::new(settings, &metrics)?;
1020
1021        // create the metadata header blocks required by Google for accessing GRPC resources.
1022        let metadata = db_settings.metadata()?;
1023        Ok(Self {
1024            settings: db_settings,
1025            metrics,
1026            metadata,
1027            pool,
1028            circuit_breaker: Arc::new(CircuitBreaker::default()),
1029        })
1030    }
1031
1032    fn router_ttl(&self) -> Duration {
1033        self.settings
1034            .max_router_ttl
1035            .unwrap_or(Duration::from_secs(MAX_ROUTER_TTL_SECS))
1036    }
1037
1038    /// Return a ReadRowsRequest for a given row key
1039    fn read_row_request(&self, row_key: &str) -> bigtable::ReadRowsRequest {
1040        read_row_request(
1041            &self.settings.table_name,
1042            &self.settings.app_profile_id,
1043            row_key,
1044        )
1045    }
1046
1047    /// Return a MutateRowRequest for a given row key
1048    fn mutate_row_request(&self, row_key: &str) -> bigtable::MutateRowRequest {
1049        bigtable::MutateRowRequest {
1050            table_name: self.settings.table_name.clone(),
1051            app_profile_id: self.settings.app_profile_id.clone(),
1052            row_key: row_key.as_bytes().to_vec(),
1053            ..Default::default()
1054        }
1055    }
1056
1057    /// Return a CheckAndMutateRowRequest for a given row key
1058    fn check_and_mutate_row_request(&self, row_key: &str) -> bigtable::CheckAndMutateRowRequest {
1059        bigtable::CheckAndMutateRowRequest {
1060            table_name: self.settings.table_name.clone(),
1061            app_profile_id: self.settings.app_profile_id.clone(),
1062            row_key: row_key.as_bytes().to_vec(),
1063            ..Default::default()
1064        }
1065    }
1066
1067    fn rpc_policy(
1068        &self,
1069        class: RpcClass,
1070        retry_kind: RetryKind,
1071        breaker: BreakerPolicy,
1072    ) -> RpcPolicy {
1073        let (attempt_timeout, total_timeout) = match class {
1074            RpcClass::Point => (
1075                self.settings.grpc_point_attempt_timeout,
1076                self.settings.grpc_point_total_timeout,
1077            ),
1078            RpcClass::Scan => (
1079                self.settings.grpc_scan_attempt_timeout,
1080                self.settings.grpc_scan_total_timeout,
1081            ),
1082        };
1083        RpcPolicy {
1084            attempt_timeout,
1085            total_timeout,
1086            retry_kind,
1087            breaker,
1088        }
1089    }
1090
1091    /// Execute one logical Bigtable operation within a total budget.
1092    ///
1093    /// A deadpool checkout is a local concurrency permit. It is included in the
1094    /// caller's total budget, but checkout failures do not affect the Bigtable
1095    /// circuit breaker. Once checked out, every retry selects a fresh channel
1096    /// slot and rebuilds request metadata and credentials.
1097    async fn execute_rpc<M, T, Call, CallFuture>(
1098        &self,
1099        message: M,
1100        policy: RpcPolicy,
1101        call: Call,
1102    ) -> Result<T, error::BigTableError>
1103    where
1104        M: Clone,
1105        Call: Fn(Channel, Request<M>) -> CallFuture + Clone,
1106        CallFuture: Future<Output = Result<T, error::BigTableError>>,
1107    {
1108        if policy.breaker == BreakerPolicy::Track && !self.circuit_breaker.allow_request() {
1109            return Err(error::BigTableError::CircuitBreakerOpen);
1110        }
1111
1112        let operation_deadline = tokio::time::Instant::now() + policy.total_timeout;
1113        let pooled = match tokio::time::timeout_at(operation_deadline, self.pool.get()).await {
1114            Ok(result) => result?,
1115            Err(_) => return Err(error::BigTableError::OperationTimeout),
1116        };
1117        let bigtable = (*pooled).clone();
1118        let metadata = self.metadata.clone();
1119        let retry_policy = retry_policy(self.settings.retry_count);
1120        let pending_retry_metric = Arc::new(Mutex::new(None));
1121        let attempt_retry_metric = pending_retry_metric.clone();
1122        let condition_retry_metric = pending_retry_metric;
1123        let metrics = self.metrics.clone();
1124        // If the total budget cancels the retry future, this remains true only
1125        // when cancellation happened inside a dispatched RPC. Request
1126        // preparation and retry backoff leave it false.
1127        let rpc_in_flight = Arc::new(AtomicBool::new(false));
1128        let attempt_rpc_in_flight = rpc_in_flight.clone();
1129        let result = operation_budget_until(
1130            operation_deadline,
1131            retry_policy.retry_if(
1132                || {
1133                    // The retry predicate runs even for a terminal failure.
1134                    // Emit only when the next attempt actually starts.
1135                    if let Some(retry_metric) = take_pending_retry_metric(&attempt_retry_metric) {
1136                        retry_metric.send(&metrics);
1137                    }
1138                    // The final attempt may start near the end of the total
1139                    // retry budget. Clamp its grpc-timeout to that remaining
1140                    // budget so the server does not keep abandoned work alive
1141                    // after this logical operation has already returned.
1142                    let attempt_deadline = std::cmp::min(
1143                        tokio::time::Instant::now() + policy.attempt_timeout,
1144                        operation_deadline,
1145                    );
1146                    let bigtable = bigtable.clone();
1147                    let metadata = metadata.clone();
1148                    let message = message.clone();
1149                    let channel = self.pool.next_channel();
1150                    let call = call.clone();
1151                    let rpc_in_flight = attempt_rpc_in_flight.clone();
1152                    async move {
1153                        let request = bigtable
1154                            .request(message, &metadata, attempt_deadline)
1155                            .await?;
1156                        rpc_in_flight.store(true, Ordering::Relaxed);
1157                        let result =
1158                            rpc_attempt_until(attempt_deadline, call(channel, request)).await;
1159                        rpc_in_flight.store(false, Ordering::Relaxed);
1160                        result
1161                    }
1162                },
1163                move |error: &error::BigTableError| {
1164                    if let Some(retry_metric) = classify_retry(policy.retry_kind, error) {
1165                        set_pending_retry_metric(&condition_retry_metric, retry_metric);
1166                        true
1167                    } else {
1168                        false
1169                    }
1170                },
1171            ),
1172        )
1173        .await;
1174
1175        if policy.breaker == BreakerPolicy::Track {
1176            let operation_timed_out_in_rpc = rpc_in_flight.load(Ordering::Relaxed);
1177            match &result {
1178                Ok(_) => self.circuit_breaker.record_success(),
1179                Err(error) if counts_toward_breaker(error, operation_timed_out_in_rpc) => {
1180                    self.circuit_breaker.record_failure();
1181                }
1182                Err(_) => {}
1183            }
1184        }
1185        result
1186    }
1187
1188    /// Apply an idempotent mutation to one row.
1189    async fn mutate_row(
1190        &self,
1191        req: bigtable::MutateRowRequest,
1192    ) -> Result<(), error::BigTableError> {
1193        let policy = self.rpc_policy(
1194            RpcClass::Point,
1195            RetryKind::IdempotentWrite,
1196            BreakerPolicy::Track,
1197        );
1198        self.execute_rpc(req, policy, |channel, request| async move {
1199            let mut client = BigtableDb::client(channel);
1200            client
1201                .mutate_row(request)
1202                .await
1203                .map_err(error::BigTableError::Write)?;
1204            Ok(())
1205        })
1206        .await
1207    }
1208
1209    /// Read one row for the [ReadRowsRequest] (assuming only a single row was requested).
1210    async fn read_row(
1211        &self,
1212        req: bigtable::ReadRowsRequest,
1213    ) -> Result<Option<row::Row>, error::BigTableError> {
1214        let mut rows = self.read_rows_with_class(req, RpcClass::Point).await?;
1215        Ok(rows.pop_first().map(|(_, v)| v))
1216    }
1217
1218    /// Take a big table ReadRowsRequest (containing the keys and filters) and return a set of row data indexed by row key.
1219    ///
1220    ///
1221    async fn read_rows(
1222        &self,
1223        req: bigtable::ReadRowsRequest,
1224    ) -> Result<BTreeMap<RowKey, row::Row>, error::BigTableError> {
1225        self.read_rows_with_class(req, RpcClass::Scan).await
1226    }
1227
1228    async fn read_rows_with_class(
1229        &self,
1230        req: bigtable::ReadRowsRequest,
1231        class: RpcClass,
1232    ) -> Result<BTreeMap<RowKey, row::Row>, error::BigTableError> {
1233        self.read_rows_with_policy(req, class, BreakerPolicy::Track)
1234            .await
1235    }
1236
1237    async fn read_rows_with_policy(
1238        &self,
1239        req: bigtable::ReadRowsRequest,
1240        class: RpcClass,
1241        breaker: BreakerPolicy,
1242    ) -> Result<BTreeMap<RowKey, row::Row>, error::BigTableError> {
1243        let policy = self.rpc_policy(class, RetryKind::Read, breaker);
1244        self.execute_rpc(req, policy, |channel, request| async move {
1245            let mut client = BigtableDb::client(channel);
1246            let response = client
1247                .read_rows(request)
1248                .await
1249                .map_err(error::BigTableError::Read)?
1250                .into_inner();
1251            merge::RowMerger::process_chunks(response).await
1252        })
1253        .await
1254    }
1255
1256    /// write a given row.
1257    ///
1258    /// there's also `.mutate_rows` which I presume allows multiple.
1259    async fn write_row(&self, row: row::Row) -> Result<(), error::BigTableError> {
1260        let mut req = self.mutate_row_request(&row.row_key);
1261        // compile the mutations.
1262        // It's possible to do a lot here, including altering in process
1263        // mutations, clearing them, etc. It's all up for grabs until we commit
1264        // below. For now, let's just presume a write and be done.
1265        req.mutations = self.get_mutations(row.cells)?;
1266        self.mutate_row(req).await?;
1267        Ok(())
1268    }
1269
1270    /// Compile the list of mutations for this row.
1271    fn get_mutations(
1272        &self,
1273        cells: HashMap<FamilyId, Vec<crate::db::bigtable::bigtable_client::cell::Cell>>,
1274    ) -> Result<Vec<bigtable::Mutation>, error::BigTableError> {
1275        let mut mutations = Vec::new();
1276        for (family_id, cells) in cells {
1277            for cell in cells {
1278                let timestamp = cell
1279                    .timestamp
1280                    .duration_since(SystemTime::UNIX_EPOCH)
1281                    .map_err(error::BigTableError::WriteTime)?;
1282                debug!("🉑 expiring in {:?}", timestamp.as_millis());
1283                mutations.push(bigtable::Mutation {
1284                    mutation: Some(bigtable::mutation::Mutation::SetCell(
1285                        bigtable::mutation::SetCell {
1286                            family_name: family_id.clone(),
1287                            column_qualifier: cell.qualifier.clone().into_bytes(),
1288                            // Bigtable tables use millisecond cell-timestamp granularity;
1289                            // timestamp_micros must be a multiple of 1,000 or the server
1290                            // rejects the mutation with a granularity mismatch.
1291                            timestamp_micros: (timestamp.as_millis() * 1000) as i64,
1292                            value: cell.value,
1293                        },
1294                    )),
1295                });
1296            }
1297        }
1298        Ok(mutations)
1299    }
1300
1301    /// Write mutations if the row meets a condition specified by the filter.
1302    ///
1303    /// Mutations can be applied either when the filter matches (state `true`)
1304    /// or doesn't match (state `false`).
1305    ///
1306    /// Returns whether the filter matched records (which indicates whether the
1307    /// mutations were applied, depending on the state)
1308    async fn check_and_mutate_row(
1309        &self,
1310        row: row::Row,
1311        filter: bigtable::RowFilter,
1312        state: bool,
1313    ) -> Result<bool, error::BigTableError> {
1314        let mut req = self.check_and_mutate_row_request(&row.row_key);
1315        let mutations = self.get_mutations(row.cells)?;
1316        req.predicate_filter = Some(filter);
1317        if state {
1318            req.true_mutations = mutations;
1319        } else {
1320            req.false_mutations = mutations;
1321        }
1322        self.check_and_mutate(req).await
1323    }
1324
1325    async fn check_and_mutate(
1326        &self,
1327        req: bigtable::CheckAndMutateRowRequest,
1328    ) -> Result<bool, error::BigTableError> {
1329        let policy = self.rpc_policy(
1330            RpcClass::Point,
1331            RetryKind::ConditionalWrite,
1332            BreakerPolicy::Track,
1333        );
1334        let predicate_matched = self
1335            .execute_rpc(req, policy, |channel, request| async move {
1336                let mut client = BigtableDb::client(channel);
1337                let response = client
1338                    .check_and_mutate_row(request)
1339                    .await
1340                    .map_err(error::BigTableError::Write)?;
1341                Ok(response.into_inner().predicate_matched)
1342            })
1343            .await?;
1344        debug!("🉑 Predicate Matched: {predicate_matched}");
1345        Ok(predicate_matched)
1346    }
1347
1348    fn get_delete_mutations(
1349        &self,
1350        family: &str,
1351        column_names: &[&str],
1352        time_range: Option<&bigtable::TimestampRange>,
1353    ) -> Result<Vec<bigtable::Mutation>, error::BigTableError> {
1354        let mut mutations = Vec::new();
1355        for column in column_names {
1356            // DeleteFromRow -- Delete all cells for a given row.
1357            // DeleteFromFamily -- Delete all cells from a family for a given row.
1358            // DeleteFromColumn -- Delete all cells from a column name for a given row, restricted by timestamp range.
1359            mutations.push(bigtable::Mutation {
1360                mutation: Some(bigtable::mutation::Mutation::DeleteFromColumn(
1361                    bigtable::mutation::DeleteFromColumn {
1362                        family_name: family.to_owned(),
1363                        column_qualifier: column.as_bytes().to_vec(),
1364                        time_range: time_range.cloned(),
1365                    },
1366                )),
1367            });
1368        }
1369        Ok(mutations)
1370    }
1371
1372    /// Delete all the cells for the given row. NOTE: This will drop the row.
1373    async fn delete_row(&self, row_key: &str) -> Result<(), error::BigTableError> {
1374        let mut req = self.mutate_row_request(row_key);
1375        req.mutations = vec![bigtable::Mutation {
1376            mutation: Some(bigtable::mutation::Mutation::DeleteFromRow(
1377                bigtable::mutation::DeleteFromRow {},
1378            )),
1379        }];
1380        self.mutate_row(req).await
1381    }
1382
1383    fn rows_to_notifications(
1384        &self,
1385        rows: BTreeMap<String, Row>,
1386    ) -> Result<Vec<Notification>, DbError> {
1387        rows.into_iter()
1388            .map(|(row_key, row)| self.row_to_notification(&row_key, row))
1389            .collect()
1390    }
1391
1392    fn row_to_notification(&self, row_key: &str, mut row: Row) -> Result<Notification, DbError> {
1393        let Some((_, chidmessageid)) = row_key.split_once('#') else {
1394            return Err(DbError::Integrity(
1395                "rows_to_notification expected row_key: uaid:chidmessageid ".to_owned(),
1396                None,
1397            ));
1398        };
1399        let range_key = RangeKey::parse_chidmessageid(chidmessageid).map_err(|e| {
1400            DbError::Integrity(
1401                format!("rows_to_notification expected chidmessageid: {e}"),
1402                None,
1403            )
1404        })?;
1405
1406        // Create from the known, required fields.
1407        let mut notif = Notification {
1408            channel_id: range_key.channel_id,
1409            topic: range_key.topic,
1410            sortkey_timestamp: range_key.sortkey_timestamp,
1411            version: to_string(row.take_required_cell("version")?.value, "version")?,
1412            ttl: to_u64(row.take_required_cell("ttl")?.value, "ttl")?,
1413            timestamp: to_u64(row.take_required_cell("timestamp")?.value, "timestamp")?,
1414            ..Default::default()
1415        };
1416
1417        // Backfill the Optional fields
1418        if let Some(cell) = row.take_cell("data") {
1419            notif.data = Some(to_string(cell.value, "data")?);
1420        }
1421        #[cfg(feature = "reliable_report")]
1422        {
1423            if let Some(cell) = row.take_cell("reliability_id") {
1424                notif.reliability_id = Some(to_string(cell.value, "reliability_id")?);
1425            }
1426            if let Some(cell) = row.take_cell("reliable_state") {
1427                notif.reliable_state = Some(
1428                    crate::reliability::ReliabilityState::from_str(&to_string(
1429                        cell.value,
1430                        "reliable_state",
1431                    )?)
1432                    .map_err(|e| {
1433                        DbError::DeserializeString(format!("Could not parse reliable_state {e:?}"))
1434                    })?,
1435                );
1436            }
1437        }
1438        if let Some(cell) = row.take_cell("headers") {
1439            notif.headers = Some(
1440                serde_json::from_str::<HashMap<String, String>>(&to_string(cell.value, "headers")?)
1441                    .map_err(|e| DbError::Serialization(e.to_string()))?,
1442            );
1443        }
1444        #[cfg(feature = "reliable_report")]
1445        if let Some(cell) = row.take_cell("reliability_id") {
1446            trace!("🚣  Is reliable");
1447            notif.reliability_id = Some(to_string(cell.value, "reliability_id")?);
1448        }
1449
1450        trace!("🚣  Deserialized message row: {:?}", &notif);
1451        Ok(notif)
1452    }
1453
1454    /// Return a Row for writing from a [User] and a `version`
1455    ///
1456    /// `version` is specified as an argument (ignoring [User::version]) so
1457    /// that [update_user] may specify a new version to write before modifying
1458    /// the [User] struct
1459    fn user_to_row(&self, user: &User, version: &Uuid) -> Row {
1460        let row_key = user.uaid.simple().to_string();
1461        let mut row = Row::new(row_key);
1462        let expiry = std::time::SystemTime::now() + self.router_ttl();
1463
1464        let mut cells: Vec<cell::Cell> = vec![
1465            cell::Cell {
1466                qualifier: "connected_at".to_owned(),
1467                value: user.connected_at.to_be_bytes().to_vec(),
1468                timestamp: expiry,
1469                ..Default::default()
1470            },
1471            cell::Cell {
1472                qualifier: "router_type".to_owned(),
1473                value: user.router_type.clone().into_bytes(),
1474                timestamp: expiry,
1475                ..Default::default()
1476            },
1477            cell::Cell {
1478                qualifier: "record_version".to_owned(),
1479                value: user
1480                    .record_version
1481                    .unwrap_or(USER_RECORD_VERSION)
1482                    .to_be_bytes()
1483                    .to_vec(),
1484                timestamp: expiry,
1485                ..Default::default()
1486            },
1487            cell::Cell {
1488                qualifier: "version".to_owned(),
1489                value: (*version).into(),
1490                timestamp: expiry,
1491                ..Default::default()
1492            },
1493        ];
1494
1495        if let Some(router_data) = &user.router_data {
1496            cells.push(cell::Cell {
1497                qualifier: "router_data".to_owned(),
1498                value: json!(router_data).to_string().as_bytes().to_vec(),
1499                timestamp: expiry,
1500                ..Default::default()
1501            });
1502        };
1503        if let Some(current_timestamp) = user.current_timestamp {
1504            cells.push(cell::Cell {
1505                qualifier: "current_timestamp".to_owned(),
1506                value: current_timestamp.to_be_bytes().to_vec(),
1507                timestamp: expiry,
1508                ..Default::default()
1509            });
1510        };
1511        if let Some(node_id) = &user.node_id {
1512            cells.push(cell::Cell {
1513                qualifier: "node_id".to_owned(),
1514                value: node_id.as_bytes().to_vec(),
1515                timestamp: expiry,
1516                ..Default::default()
1517            });
1518        };
1519
1520        cells.extend(channels_to_cells(
1521            Cow::Borrowed(&user.priv_channels),
1522            expiry,
1523        ));
1524
1525        row.add_cells(ROUTER_FAMILY, cells);
1526        row
1527    }
1528}
1529
1530#[derive(Clone)]
1531pub struct BigtableDb {
1532    /// Application Default Credentials token provider, shared across the
1533    /// pool. `None` when running against the emulator (which needs no
1534    /// credentials).
1535    auth_provider: Option<Arc<dyn TokenProvider>>,
1536}
1537
1538impl BigtableDb {
1539    pub fn new(auth_provider: Option<Arc<dyn TokenProvider>>) -> Self {
1540        Self { auth_provider }
1541    }
1542
1543    pub(super) fn client(channel: Channel) -> BigtableClient<Channel> {
1544        BigtableClient::new(channel)
1545            .max_decoding_message_size(MAX_MESSAGE_LEN)
1546            .max_encoding_message_size(MAX_MESSAGE_LEN)
1547    }
1548
1549    /// Build a [tonic::Request] for the given message, attaching the standard
1550    /// Google routing metadata and (outside of the emulator) an OAuth2
1551    /// `authorization` token from the Application Default Credentials.
1552    ///
1553    /// The token provider caches tokens internally, so this is cheap to call
1554    /// per-request (and per retry attempt, where it transparently picks up a
1555    /// fresh token if the prior one expired). Token preparation is bounded by
1556    /// `deadline`; the remaining time is also sent to Bigtable as the
1557    /// `grpc-timeout` metadata value. A local deadline expiry is reported as
1558    /// [error::BigTableError::PreSendTimeout].
1559    pub(super) async fn request<T>(
1560        &self,
1561        msg: T,
1562        metadata: &MetadataMap,
1563        deadline: tokio::time::Instant,
1564    ) -> Result<Request<T>, error::BigTableError> {
1565        let authorization = if let Some(provider) = &self.auth_provider {
1566            let token = tokio::time::timeout_at(deadline, provider.token(BIGTABLE_DATA_SCOPES))
1567                .await
1568                .map_err(|_| error::BigTableError::PreSendTimeout)?
1569                .map_err(error::BigTableError::Auth)?;
1570            Some(
1571                format!("Bearer {}", token.as_str())
1572                    .parse::<AsciiMetadataValue>()
1573                    .map_err(|e| {
1574                        error::BigTableError::Config(format!("Invalid auth token: {e}"))
1575                    })?,
1576            )
1577        } else {
1578            None
1579        };
1580
1581        let remaining = deadline.saturating_duration_since(tokio::time::Instant::now());
1582        if remaining.is_zero() {
1583            return Err(error::BigTableError::PreSendTimeout);
1584        }
1585
1586        let mut request = Request::new(msg);
1587        *request.metadata_mut() = metadata.clone();
1588        // Set this after copying metadata: set_timeout is implemented by
1589        // inserting the grpc-timeout entry into the request metadata map.
1590        request.set_timeout(remaining);
1591        if let Some(value) = authorization {
1592            request.metadata_mut().insert("authorization", value);
1593        }
1594        Ok(request)
1595    }
1596}
1597
1598#[async_trait]
1599impl DbClient for BigTableClientImpl {
1600    /// add user to the database
1601    async fn add_user(&self, user: &User) -> DbResult<()> {
1602        trace!("🉑 Adding user");
1603        let Some(ref version) = user.version else {
1604            return Err(DbError::General(
1605                "add_user expected a user version field".to_owned(),
1606            ));
1607        };
1608        let row = self.user_to_row(user, version);
1609
1610        // Only add when the user doesn't already exist
1611        let row_key_filter = bigtable::RowFilter {
1612            filter: Some(bigtable::row_filter::Filter::RowKeyRegexFilter(
1613                format!("^{}$", row.row_key).into_bytes(),
1614            )),
1615        };
1616        let mut filters = router_gc_policy_filter()?;
1617        filters.push(row_key_filter);
1618        let filter = filter_chain(filters);
1619
1620        if self.check_and_mutate_row(row, filter, false).await? {
1621            return Err(DbError::Conditional);
1622        }
1623        Ok(())
1624    }
1625
1626    /// BigTable doesn't really have the concept of an "update". You simply write the data and
1627    /// the individual cells create a new version. Depending on the garbage collection rules for
1628    /// the family, these can either persist or be automatically deleted.
1629    ///
1630    /// NOTE: This function updates the key ROUTER records for a given UAID. It does this by
1631    /// calling [BigTableClientImpl::user_to_row] which creates a new row with new `cell.timestamp` values set
1632    /// to now + `MAX_ROUTER_TTL`. This function is called by mobile during the daily
1633    /// [autoendpoint::routes::update_token_route] handling, and by desktop
1634    /// [autoconnect-ws-sm::get_or_create_user]` which is called
1635    /// during the `HELLO` handler. This should be enough to ensure that the ROUTER records
1636    /// are properly refreshed for "lively" clients.
1637    ///
1638    /// NOTE: There is some, very small, potential risk that a desktop client that can
1639    /// somehow remain connected the duration of MAX_ROUTER_TTL, may be dropped as not being
1640    /// "lively".
1641    async fn update_user(&self, user: &mut User) -> DbResult<bool> {
1642        let Some(ref version) = user.version else {
1643            return Err(DbError::General(
1644                "update_user expected a user version field".to_owned(),
1645            ));
1646        };
1647
1648        let mut filters = router_gc_policy_filter()?;
1649        filters.extend(version_filter(version));
1650        let filter = filter_chain(filters);
1651
1652        let new_version = Uuid::new_v4();
1653        // Always write a newly generated version
1654        let row = self.user_to_row(user, &new_version);
1655
1656        let predicate_matched = self.check_and_mutate_row(row, filter, true).await?;
1657        user.version = Some(new_version);
1658        Ok(predicate_matched)
1659    }
1660
1661    async fn get_user(&self, uaid: &Uuid) -> DbResult<Option<User>> {
1662        let row_key = uaid.as_simple().to_string();
1663        let mut req = self.read_row_request(&row_key);
1664        // `router_gc_policy_filter` hides cells whose TTL has passed. The
1665        // `connected_at` cell is always the earliest-expiring cell in the router
1666        // row, so an expired record is treated as "no user" (via
1667        // `is_incomplete_router_record`) even with recent notifications.
1668        let mut filters = router_gc_policy_filter()?;
1669        filters.push(family_filter(format!("^{ROUTER_FAMILY}$")));
1670        req.filter = Some(filter_chain(filters));
1671        let Some(mut row) = self.read_row(req).await? else {
1672            return Ok(None);
1673        };
1674
1675        trace!("🉑 Found a record for {}", row_key);
1676
1677        let connected_at_cell = match row.take_required_cell("connected_at") {
1678            Ok(cell) => cell,
1679            Err(_) => {
1680                if !is_incomplete_router_record(&row.cells) {
1681                    return Err(DbError::Integrity(
1682                        "Expected column: connected_at".to_owned(),
1683                        Some(format!("{row:#?}")),
1684                    ));
1685                }
1686                // Special case incomplete records: they're equivalent to no
1687                // user exists. Incompletes caused by the migration bug in #640
1688                // will have their migration re-triggered by returning None:
1689                // https://github.com/mozilla-services/autopush-rs/pull/640
1690                trace!("🉑 Dropping an incomplete user record for {}", row_key);
1691                self.metrics
1692                    .incr_with_tags(MetricName::DatabaseDropUser)
1693                    .with_tag("reason", "incomplete_record")
1694                    .send();
1695                self.remove_user(uaid).await?;
1696                return Ok(None);
1697            }
1698        };
1699
1700        let mut result = User {
1701            uaid: *uaid,
1702            connected_at: to_u64(connected_at_cell.value, "connected_at")?,
1703            router_type: to_string(row.take_required_cell("router_type")?.value, "router_type")?,
1704            record_version: Some(to_u64(
1705                row.take_required_cell("record_version")?.value,
1706                "record_version",
1707            )?),
1708            version: Some(
1709                row.take_required_cell("version")?
1710                    .value
1711                    .try_into()
1712                    .map_err(|e| {
1713                        DbError::Serialization(format!("Could not deserialize version: {e:?}"))
1714                    })?,
1715            ),
1716            ..Default::default()
1717        };
1718
1719        if let Some(cell) = row.take_cell("router_data") {
1720            result.router_data = from_str(&to_string(cell.value, "router_type")?).map_err(|e| {
1721                DbError::Serialization(format!("Could not deserialize router_type: {e:?}"))
1722            })?;
1723        }
1724
1725        if let Some(cell) = row.take_cell("node_id") {
1726            result.node_id = Some(to_string(cell.value, "node_id")?);
1727        }
1728
1729        if let Some(cell) = row.take_cell("current_timestamp") {
1730            result.current_timestamp = Some(to_u64(cell.value, "current_timestamp")?)
1731        }
1732
1733        // Read the channels last, after removal of all non channel cells
1734        result.priv_channels = channels_from_cells(&row.cells)?;
1735
1736        Ok(Some(result))
1737    }
1738
1739    async fn remove_user(&self, uaid: &Uuid) -> DbResult<()> {
1740        let row_key = uaid.simple().to_string();
1741        self.delete_row(&row_key).await?;
1742        Ok(())
1743    }
1744
1745    async fn add_channel(&self, uaid: &Uuid, channel_id: &Uuid) -> DbResult<()> {
1746        let channels = HashSet::from_iter([channel_id.to_owned()]);
1747        self.add_channels(uaid, channels).await
1748    }
1749
1750    /// Add channels in bulk (used mostly during migration)
1751    ///
1752    async fn add_channels(&self, uaid: &Uuid, channels: HashSet<Uuid>) -> DbResult<()> {
1753        // channel_ids are stored as a set within one Bigtable row
1754        //
1755        // Bigtable allows "millions of columns in a table, as long as no row
1756        // exceeds the maximum limit of 256 MB per row" enabling the use of
1757        // column qualifiers as data.
1758        //
1759        // The "set" of channel_ids consists of column qualifiers named
1760        // "chid:<chid value>" as set member entries (with their cell values
1761        // being a single 0 byte).
1762        //
1763        // Storing the full set in a single row makes batch updates
1764        // (particularly to reset the GC expiry timestamps) potentially more
1765        // easy/efficient
1766        let row_key = uaid.simple().to_string();
1767        let mut row = Row::new(row_key);
1768        let expiry = std::time::SystemTime::now() + self.router_ttl();
1769
1770        // Note: updating the version column isn't necessary here because this
1771        // write only adds a new (or updates an existing) column with a 0 byte
1772        // value
1773        row.add_cells(
1774            ROUTER_FAMILY,
1775            channels_to_cells(Cow::Owned(channels), expiry),
1776        );
1777
1778        self.write_row(row).await?;
1779        Ok(())
1780    }
1781
1782    async fn get_channels(&self, uaid: &Uuid) -> DbResult<HashSet<Uuid>> {
1783        let row_key = uaid.simple().to_string();
1784        let mut req = self.read_row_request(&row_key);
1785
1786        let cq_filter = bigtable::RowFilter {
1787            filter: Some(bigtable::row_filter::Filter::ColumnQualifierRegexFilter(
1788                "^chid:.*$".as_bytes().to_vec(),
1789            )),
1790        };
1791        let mut filters = router_gc_policy_filter()?;
1792        filters.push(family_filter(format!("^{ROUTER_FAMILY}$")));
1793        filters.push(cq_filter);
1794        req.filter = Some(filter_chain(filters));
1795
1796        let Some(row) = self.read_row(req).await? else {
1797            return Ok(Default::default());
1798        };
1799        channels_from_cells(&row.cells)
1800    }
1801
1802    /// Delete the channel. Does not delete its associated pending messages.
1803    async fn remove_channel(&self, uaid: &Uuid, channel_id: &Uuid) -> DbResult<bool> {
1804        let row_key = uaid.simple().to_string();
1805        let mut req = self.check_and_mutate_row_request(&row_key);
1806
1807        // Delete the column representing the channel_id
1808        let column = format!("chid:{}", channel_id.as_hyphenated());
1809        let mut mutations = self.get_delete_mutations(ROUTER_FAMILY, &[column.as_ref()], None)?;
1810
1811        // and write a new version cell
1812        let mut row = Row::new(row_key);
1813        let expiry = std::time::SystemTime::now() + self.router_ttl();
1814        row.cells
1815            .insert(ROUTER_FAMILY.to_owned(), vec![new_version_cell(expiry)]);
1816        mutations.extend(self.get_mutations(row.cells)?);
1817
1818        // check if the channel existed/was actually removed
1819        let cq_filter = bigtable::RowFilter {
1820            filter: Some(bigtable::row_filter::Filter::ColumnQualifierRegexFilter(
1821                format!("^{column}$").into_bytes(),
1822            )),
1823        };
1824        let mut filters = router_gc_policy_filter()?;
1825        filters.push(cq_filter);
1826        req.predicate_filter = Some(filter_chain(filters));
1827        req.true_mutations = mutations;
1828
1829        Ok(self.check_and_mutate(req).await?)
1830    }
1831
1832    /// Remove the node_id
1833    async fn remove_node_id(
1834        &self,
1835        uaid: &Uuid,
1836        _node_id: &str,
1837        _connected_at: u64,
1838        version: &Option<Uuid>,
1839    ) -> DbResult<bool> {
1840        let row_key = uaid.simple().to_string();
1841        trace!("🉑 Removing node_id for: {row_key} (version: {version:?}) ",);
1842        let Some(version) = version else {
1843            return Err(DbError::General("Expected a user version field".to_owned()));
1844        };
1845
1846        let mut req = self.check_and_mutate_row_request(&row_key);
1847
1848        let mut filters = router_gc_policy_filter()?;
1849        filters.extend(version_filter(version));
1850        req.predicate_filter = Some(filter_chain(filters));
1851        req.true_mutations = self.get_delete_mutations(ROUTER_FAMILY, &["node_id"], None)?;
1852
1853        Ok(self.check_and_mutate(req).await?)
1854    }
1855
1856    /// Write the notification to storage.
1857    async fn save_message(&self, uaid: &Uuid, message: Notification) -> DbResult<()> {
1858        let row_key = format!("{}#{}", uaid.simple(), message.chidmessageid());
1859        debug!("🗄️ Saving message {} :: {:?}", &row_key, &message);
1860        trace!(
1861            "🉑 timestamp: {:?}",
1862            &message.timestamp.to_be_bytes().to_vec()
1863        );
1864        let mut row = Row::new(row_key);
1865
1866        // Remember, `timestamp` is effectively the time to kill the message, not the
1867        // current time.
1868        // TODO: use message.expiry()
1869        let expiry = SystemTime::now() + Duration::from_secs(message.ttl);
1870        trace!(
1871            "🉑 Message Expiry {}",
1872            expiry
1873                .duration_since(SystemTime::UNIX_EPOCH)
1874                .unwrap_or_default()
1875                .as_millis()
1876        );
1877
1878        let mut cells: Vec<cell::Cell> = Vec::new();
1879
1880        let is_topic = message.topic.is_some();
1881        let family = if is_topic {
1882            MESSAGE_TOPIC_FAMILY
1883        } else {
1884            MESSAGE_FAMILY
1885        };
1886        cells.extend(vec![
1887            cell::Cell {
1888                qualifier: "ttl".to_owned(),
1889                value: message.ttl.to_be_bytes().to_vec(),
1890                timestamp: expiry,
1891                ..Default::default()
1892            },
1893            cell::Cell {
1894                qualifier: "timestamp".to_owned(),
1895                value: message.timestamp.to_be_bytes().to_vec(),
1896                timestamp: expiry,
1897                ..Default::default()
1898            },
1899            cell::Cell {
1900                qualifier: "version".to_owned(),
1901                value: message.version.into_bytes(),
1902                timestamp: expiry,
1903                ..Default::default()
1904            },
1905        ]);
1906        if let Some(headers) = message.headers
1907            && !headers.is_empty()
1908        {
1909            cells.push(cell::Cell {
1910                qualifier: "headers".to_owned(),
1911                value: json!(headers).to_string().into_bytes(),
1912                timestamp: expiry,
1913                ..Default::default()
1914            });
1915        }
1916        #[cfg(feature = "reliable_report")]
1917        {
1918            if let Some(reliability_id) = message.reliability_id {
1919                trace!("🔍 FOUND RELIABILITY ID: {}", reliability_id);
1920                cells.push(cell::Cell {
1921                    qualifier: "reliability_id".to_owned(),
1922                    value: reliability_id.into_bytes(),
1923                    timestamp: expiry,
1924                    ..Default::default()
1925                });
1926            }
1927            if let Some(reliable_state) = message.reliable_state {
1928                cells.push(cell::Cell {
1929                    qualifier: "reliable_state".to_owned(),
1930                    value: reliable_state.to_string().into_bytes(),
1931                    timestamp: expiry,
1932                    ..Default::default()
1933                });
1934            }
1935        }
1936        if let Some(data) = message.data {
1937            cells.push(cell::Cell {
1938                qualifier: "data".to_owned(),
1939                value: data.into_bytes(),
1940                timestamp: expiry,
1941                ..Default::default()
1942            });
1943        }
1944
1945        row.add_cells(family, cells);
1946        trace!("🉑 Adding row");
1947        self.write_row(row).await?;
1948
1949        self.metrics
1950            .incr_with_tags(MetricName::NotificationMessageStored)
1951            .with_tag("topic", &is_topic.to_string())
1952            .with_tag("database", &self.name())
1953            .send();
1954        Ok(())
1955    }
1956
1957    /// Save a batch of messages to the database.
1958    ///
1959    /// Currently just iterating through the list and saving one at a time. There's a bulk way
1960    /// to save messages, but there are other considerations (e.g. mutation limits)
1961    async fn save_messages(&self, uaid: &Uuid, messages: Vec<Notification>) -> DbResult<()> {
1962        // plate simple way of solving this:
1963        for message in messages {
1964            self.save_message(uaid, message).await?;
1965        }
1966        Ok(())
1967    }
1968
1969    /// Set the `current_timestamp` in the meta record for this user agent.
1970    ///
1971    /// This is a bit different for BigTable. Field expiration (technically cell
1972    /// expiration) is determined by the lifetime assigned to the cell once it hits
1973    /// a given date. That means you can't really extend a lifetime by adjusting a
1974    /// single field. You'd have to adjust all the cells that are in the family.
1975    /// So, we're not going to do expiration that way.
1976    ///
1977    /// That leaves the meta "current_timestamp" field. We do not purge ACK'd records,
1978    /// instead we presume that the TTL will kill them off eventually. On reads, we use
1979    /// the `current_timestamp` to determine what records to return, since we return
1980    /// records with timestamps later than `current_timestamp`.
1981    ///
1982    async fn increment_storage(&self, uaid: &Uuid, timestamp: u64) -> DbResult<()> {
1983        let row_key = uaid.simple().to_string();
1984        debug!(
1985            "🉑 Updating {} current_timestamp:  {:?}",
1986            &row_key,
1987            timestamp.to_be_bytes().to_vec()
1988        );
1989        let expiry = std::time::SystemTime::now() + self.router_ttl();
1990        let mut row = Row::new(row_key.clone());
1991
1992        row.cells.insert(
1993            ROUTER_FAMILY.to_owned(),
1994            vec![
1995                cell::Cell {
1996                    qualifier: "current_timestamp".to_owned(),
1997                    value: timestamp.to_be_bytes().to_vec(),
1998                    timestamp: expiry,
1999                    ..Default::default()
2000                },
2001                new_version_cell(expiry),
2002            ],
2003        );
2004
2005        self.write_row(row).await?;
2006
2007        Ok(())
2008    }
2009
2010    /// Delete the notification from storage.
2011    async fn remove_message(&self, uaid: &Uuid, chidmessageid: &str) -> DbResult<()> {
2012        trace!(
2013            "🉑 attemping to delete {:?} :: {:?}",
2014            uaid.to_string(),
2015            chidmessageid
2016        );
2017        let row_key = format!("{}#{}", uaid.simple(), chidmessageid);
2018        debug!("🉑🔥 Deleting message {}", &row_key);
2019        self.delete_row(&row_key).await?;
2020        self.metrics
2021            .incr_with_tags(MetricName::NotificationMessageDeleted)
2022            .with_tag("database", &self.name())
2023            .send();
2024        Ok(())
2025    }
2026
2027    /// Return `limit` pending messages from storage. `limit=0` for all messages.
2028    async fn fetch_topic_messages(
2029        &self,
2030        uaid: &Uuid,
2031        limit: usize,
2032    ) -> DbResult<FetchMessageResponse> {
2033        let start_key = format!("{}#01:", uaid.simple());
2034        let end_key = format!("{}#02:", uaid.simple());
2035        let mut req = bigtable::ReadRowsRequest {
2036            table_name: self.settings.table_name.clone(),
2037            app_profile_id: self.settings.app_profile_id.clone(),
2038            rows: Some(bigtable::RowSet {
2039                row_keys: Vec::new(),
2040                row_ranges: vec![bigtable::RowRange {
2041                    start_key: Some(bigtable::row_range::StartKey::StartKeyOpen(
2042                        start_key.into_bytes(),
2043                    )),
2044                    end_key: Some(bigtable::row_range::EndKey::EndKeyOpen(
2045                        end_key.into_bytes(),
2046                    )),
2047                }],
2048            }),
2049            ..Default::default()
2050        };
2051
2052        let mut filters = message_gc_policy_filter()?;
2053        filters.push(family_filter(format!("^{MESSAGE_TOPIC_FAMILY}$")));
2054
2055        req.filter = Some(filter_chain(filters));
2056        if limit > 0 {
2057            trace!("🉑 Setting limit to {limit}");
2058            req.rows_limit = limit as i64;
2059        }
2060        let rows = self.read_rows(req).await?;
2061        debug!(
2062            "🉑 Fetch Topic Messages. Found {} row(s) of {}",
2063            rows.len(),
2064            limit
2065        );
2066
2067        let messages = self.rows_to_notifications(rows)?;
2068
2069        // Note: Bigtable always returns a timestamp of None.
2070        // Under Bigtable `current_timestamp` is instead initially read
2071        // from [get_user].
2072        Ok(FetchMessageResponse {
2073            messages,
2074            timestamp: None,
2075        })
2076    }
2077
2078    /// Return `limit` messages pending for a UAID that have a sortkey_timestamp after
2079    /// what's specified. `limit=0` for all messages.
2080    async fn fetch_timestamp_messages(
2081        &self,
2082        uaid: &Uuid,
2083        timestamp: Option<u64>,
2084        limit: usize,
2085    ) -> DbResult<FetchMessageResponse> {
2086        let start_key = if let Some(ts) = timestamp {
2087            // Fetch everything after the last message with timestamp: the "z"
2088            // moves past the last message's channel_id's 1st hex digit
2089            format!("{}#02:{}z", uaid.simple(), ts)
2090        } else {
2091            format!("{}#02:", uaid.simple())
2092        };
2093        let end_key = format!("{}#03:", uaid.simple());
2094        let mut req = bigtable::ReadRowsRequest {
2095            table_name: self.settings.table_name.clone(),
2096            app_profile_id: self.settings.app_profile_id.clone(),
2097            rows: Some(bigtable::RowSet {
2098                row_keys: Vec::new(),
2099                row_ranges: vec![bigtable::RowRange {
2100                    start_key: Some(bigtable::row_range::StartKey::StartKeyOpen(
2101                        start_key.into_bytes(),
2102                    )),
2103                    end_key: Some(bigtable::row_range::EndKey::EndKeyOpen(
2104                        end_key.into_bytes(),
2105                    )),
2106                }],
2107            }),
2108            ..Default::default()
2109        };
2110
2111        // We can fetch data and do [some remote filtering](https://cloud.google.com/bigtable/docs/filters),
2112        // unfortunately I don't think the filtering we need will be super helpful.
2113        //
2114        //
2115        /*
2116        //NOTE: if you filter on a given field, BigTable will only
2117        // return that specific field. Adding filters for the rest of
2118        // the known elements may NOT return those elements or may
2119        // cause the message to not be returned because any of
2120        // those elements are not present. It may be preferable to
2121        // therefore run two filters, one to fetch the candidate IDs
2122        // and another to fetch the content of the messages.
2123         */
2124        let mut filters = message_gc_policy_filter()?;
2125        filters.push(family_filter(format!("^{MESSAGE_FAMILY}$")));
2126
2127        req.filter = Some(filter_chain(filters));
2128        if limit > 0 {
2129            req.rows_limit = limit as i64;
2130        }
2131        let rows = self.read_rows(req).await?;
2132        debug!(
2133            "🉑 Fetch Timestamp Messages ({:?}) Found {} row(s) of {}",
2134            timestamp,
2135            rows.len(),
2136            limit,
2137        );
2138
2139        let messages = self.rows_to_notifications(rows)?;
2140        // The timestamp of the last message read
2141        let timestamp = messages.last().and_then(|m| m.sortkey_timestamp);
2142        Ok(FetchMessageResponse {
2143            messages,
2144            timestamp,
2145        })
2146    }
2147
2148    async fn health_check(&self) -> DbResult<bool> {
2149        // Use a random key so health checks do not create a hot tablet. The
2150        // BlockAll filter verifies the data path without returning row data.
2151        let random_uaid = Uuid::new_v4().simple().to_string();
2152        let mut req = read_row_request(
2153            &self.settings.table_name,
2154            &self.settings.app_profile_id,
2155            &random_uaid,
2156        );
2157        req.filter = Some(bigtable::RowFilter {
2158            filter: Some(bigtable::row_filter::Filter::BlockAllFilter(true)),
2159        });
2160        // Health is an independent, bounded backend probe. It must neither be
2161        // blocked by nor mutate the application traffic circuit breaker.
2162        self.read_rows_with_policy(req, RpcClass::Point, BreakerPolicy::Ignore)
2163            .await?;
2164        Ok(true)
2165    }
2166
2167    /// Returns true, because there's only one table in BigTable. We divide things up
2168    /// by `family`.
2169    async fn router_table_exists(&self) -> DbResult<bool> {
2170        Ok(true)
2171    }
2172
2173    /// Returns true, because there's only one table in BigTable. We divide things up
2174    /// by `family`.
2175    async fn message_table_exists(&self) -> DbResult<bool> {
2176        Ok(true)
2177    }
2178
2179    #[cfg(feature = "reliable_report")]
2180    async fn log_report(
2181        &self,
2182        reliability_id: &str,
2183        new_state: crate::reliability::ReliabilityState,
2184    ) -> DbResult<()> {
2185        let row_key = reliability_id.to_owned();
2186
2187        let mut row = Row::new(row_key);
2188        let expiry = SystemTime::now() + Duration::from_secs(RELIABLE_LOG_TTL.num_seconds() as u64);
2189
2190        // Log the latest transition time for this id.
2191        let cells: Vec<cell::Cell> = vec![cell::Cell {
2192            qualifier: new_state.to_string(),
2193            value: crate::util::ms_since_epoch().to_be_bytes().to_vec(),
2194            timestamp: expiry,
2195            ..Default::default()
2196        }];
2197
2198        row.add_cells(RELIABLE_LOG_FAMILY, cells);
2199
2200        self.write_row(row).await?;
2201
2202        Ok(())
2203    }
2204
2205    fn box_clone(&self) -> Box<dyn DbClient> {
2206        Box::new(self.clone())
2207    }
2208
2209    fn name(&self) -> String {
2210        "Bigtable".to_owned()
2211    }
2212
2213    fn pool_status(&self) -> Option<deadpool::Status> {
2214        Some(self.pool.pool.status())
2215    }
2216
2217    fn configured_channel_count(&self) -> Option<usize> {
2218        Some(self.pool.configured_channel_count())
2219    }
2220}
2221
2222#[cfg(all(test, feature = "emulator"))]
2223mod tests {
2224
2225    //! Currently, these test rely on having a BigTable emulator running on the current machine.
2226    //! The tests presume to be able to connect to localhost:8086. See docs/bigtable.md for
2227    //! details and how to set up and initialize an emulator.
2228    //!
2229    use std::sync::Arc;
2230    use std::time::SystemTime;
2231
2232    use cadence::StatsdClient;
2233    use uuid;
2234
2235    use super::*;
2236    use crate::{db::DbSettings, test_support::gen_test_uaid, util::ms_since_epoch};
2237
2238    const TEST_USER: &str = "DEADBEEF-0000-0000-0000-0123456789AB";
2239    const TEST_CHID: &str = "DECAFBAD-0000-0000-0000-0123456789AB";
2240    const TOPIC_CHID: &str = "DECAFBAD-1111-0000-0000-0123456789AB";
2241
2242    fn now() -> u64 {
2243        SystemTime::now()
2244            .duration_since(SystemTime::UNIX_EPOCH)
2245            .unwrap()
2246            .as_secs()
2247    }
2248
2249    fn new_client() -> DbResult<BigTableClientImpl> {
2250        let env_dsn = format!(
2251            "grpc://{}",
2252            std::env::var("BIGTABLE_EMULATOR_HOST").unwrap_or("localhost:8080".to_owned())
2253        );
2254        let settings = DbSettings {
2255            // this presumes the table was created with
2256            // ```
2257            // scripts/setup_bt.sh
2258            // ```
2259            // with `message`, `router`, and `message_topic` families
2260            dsn: Some(env_dsn),
2261            db_settings: json!({"table_name": "projects/test/instances/test/tables/autopush"})
2262                .to_string(),
2263        };
2264
2265        let metrics = Arc::new(StatsdClient::builder("", cadence::NopMetricSink).build());
2266
2267        BigTableClientImpl::new(metrics, &settings)
2268    }
2269
2270    #[test]
2271    fn escape_bytes_for_regex() {
2272        let b = b"hi";
2273        assert_eq!(escape_bytes(b), b.to_vec());
2274        assert_eq!(escape_bytes(b"h.*i!"), b"h\\.\\*i\\!".to_vec());
2275        let b = b"\xe2\x80\xb3";
2276        assert_eq!(escape_bytes(b), b.to_vec());
2277        // clippy::octal-escapes rightly discourages this ("\022") in a byte literal
2278        let b = [b'f', b'o', b'\0', b'2', b'2', b'o'];
2279        assert_eq!(escape_bytes(&b), b"fo\\x0022o".to_vec());
2280        let b = b"\xc0";
2281        assert_eq!(escape_bytes(b), b.to_vec());
2282        assert_eq!(escape_bytes(b"\x03"), b"\\\x03".to_vec());
2283    }
2284
2285    #[actix_rt::test]
2286    async fn health_check() {
2287        let client = new_client().unwrap();
2288
2289        let result = client.health_check().await;
2290        assert!(result.is_ok());
2291        assert!(result.unwrap());
2292    }
2293
2294    /// Bigtable rejects SetCell timestamps that are not aligned to the table's
2295    /// millisecond granularity (timestamp_micros must be a multiple of 1,000).
2296    #[actix_rt::test]
2297    async fn timestamp_granularity_rejects_sub_millisecond_micros() {
2298        let client = new_client().unwrap();
2299        let uaid = gen_test_uaid();
2300        let row_key = uaid.simple().to_string();
2301        let _ = client.remove_user(&uaid).await;
2302
2303        let mut req = client.mutate_row_request(&row_key);
2304        req.mutations = vec![bigtable::Mutation {
2305            mutation: Some(bigtable::mutation::Mutation::SetCell(
2306                bigtable::mutation::SetCell {
2307                    family_name: ROUTER_FAMILY.to_owned(),
2308                    column_qualifier: b"granularity_probe".to_vec(),
2309                    timestamp_micros: 1,
2310                    value: b"x".to_vec(),
2311                },
2312            )),
2313        }];
2314
2315        assert!(
2316            client.mutate_row(req).await.is_err(),
2317            "expected granularity mismatch for timestamp_micros=1"
2318        );
2319
2320        let _ = client.remove_user(&uaid).await;
2321    }
2322
2323    /// run a gauntlet of testing. These are a bit linear because they need
2324    /// to run in sequence.
2325    #[actix_rt::test]
2326    async fn run_gauntlet() -> DbResult<()> {
2327        let client = new_client()?;
2328
2329        let connected_at = ms_since_epoch();
2330
2331        let uaid = Uuid::parse_str(TEST_USER).unwrap();
2332        let chid = Uuid::parse_str(TEST_CHID).unwrap();
2333        let topic_chid = Uuid::parse_str(TOPIC_CHID).unwrap();
2334
2335        let node_id = "test_node".to_owned();
2336
2337        // purge the user record if it exists.
2338        let _ = client.remove_user(&uaid).await;
2339
2340        let test_user = User {
2341            uaid,
2342            router_type: "webpush".to_owned(),
2343            connected_at,
2344            router_data: None,
2345            node_id: Some(node_id.clone()),
2346            ..Default::default()
2347        };
2348
2349        // purge the old user (if present)
2350        // in case a prior test failed for whatever reason.
2351        let _ = client.remove_user(&uaid).await;
2352
2353        // can we add the user?
2354        client.add_user(&test_user).await?;
2355        let fetched = client.get_user(&uaid).await?;
2356        assert!(fetched.is_some());
2357        let fetched = fetched.unwrap();
2358        assert_eq!(fetched.router_type, "webpush".to_owned());
2359
2360        // Simulate a connected_at occuring before the following writes
2361        let connected_at = ms_since_epoch();
2362
2363        // can we add channels?
2364        client.add_channel(&uaid, &chid).await?;
2365        let channels = client.get_channels(&uaid).await?;
2366        assert!(channels.contains(&chid));
2367
2368        // can we add lots of channels?
2369        let mut new_channels: HashSet<Uuid> = HashSet::new();
2370        new_channels.insert(chid);
2371        for _ in 1..10 {
2372            new_channels.insert(uuid::Uuid::new_v4());
2373        }
2374        let chid_to_remove = uuid::Uuid::new_v4();
2375        new_channels.insert(chid_to_remove);
2376        client.add_channels(&uaid, new_channels.clone()).await?;
2377        let channels = client.get_channels(&uaid).await?;
2378        assert_eq!(channels, new_channels);
2379
2380        // can we remove a channel?
2381        assert!(client.remove_channel(&uaid, &chid_to_remove).await?);
2382        assert!(!client.remove_channel(&uaid, &chid_to_remove).await?);
2383        new_channels.remove(&chid_to_remove);
2384        let channels = client.get_channels(&uaid).await?;
2385        assert_eq!(channels, new_channels);
2386
2387        // now ensure that we can update a user that's after the time we set
2388        // prior. first ensure that we can't update a user that's before the
2389        // time we set prior to the last write
2390        let mut updated = User {
2391            connected_at,
2392            ..test_user.clone()
2393        };
2394        let result = client.update_user(&mut updated).await;
2395        assert!(result.is_ok());
2396        assert!(!result.unwrap());
2397
2398        // Make sure that the `connected_at` wasn't modified
2399        let fetched2 = client.get_user(&fetched.uaid).await?.unwrap();
2400        assert_eq!(fetched.connected_at, fetched2.connected_at);
2401
2402        // and make sure we can update a record with a later connected_at time.
2403        let mut updated = User {
2404            connected_at: fetched.connected_at + 300,
2405            ..fetched2
2406        };
2407        let result = client.update_user(&mut updated).await;
2408        assert!(result.is_ok());
2409        assert!(result.unwrap());
2410        assert_ne!(
2411            fetched2.connected_at,
2412            client.get_user(&uaid).await?.unwrap().connected_at
2413        );
2414
2415        // can we increment the storage for the user?
2416        client
2417            .increment_storage(
2418                &fetched.uaid,
2419                SystemTime::now()
2420                    .duration_since(SystemTime::UNIX_EPOCH)
2421                    .unwrap()
2422                    .as_secs(),
2423            )
2424            .await?;
2425
2426        let test_data = "An_encrypted_pile_of_crap".to_owned();
2427        let timestamp = now();
2428        let sort_key = now();
2429        // Can we store a message?
2430        let test_notification = crate::db::Notification {
2431            channel_id: chid,
2432            version: "test".to_owned(),
2433            ttl: 300,
2434            timestamp,
2435            data: Some(test_data.clone()),
2436            sortkey_timestamp: Some(sort_key),
2437            ..Default::default()
2438        };
2439        let res = client.save_message(&uaid, test_notification.clone()).await;
2440        assert!(res.is_ok());
2441
2442        let mut fetched = client.fetch_timestamp_messages(&uaid, None, 999).await?;
2443        assert_ne!(fetched.messages.len(), 0);
2444        let fm = fetched.messages.pop().unwrap();
2445        assert_eq!(fm.channel_id, test_notification.channel_id);
2446        assert_eq!(fm.data, Some(test_data));
2447
2448        // Grab all 1 of the messages that were submmited within the past 10 seconds.
2449        let fetched = client
2450            .fetch_timestamp_messages(&uaid, Some(timestamp - 10), 999)
2451            .await?;
2452        assert_ne!(fetched.messages.len(), 0);
2453
2454        // Try grabbing a message for 10 seconds from now.
2455        let fetched = client
2456            .fetch_timestamp_messages(&uaid, Some(timestamp + 10), 999)
2457            .await?;
2458        assert_eq!(fetched.messages.len(), 0);
2459
2460        // can we clean up our toys?
2461        assert!(
2462            client
2463                .remove_message(&uaid, &test_notification.chidmessageid())
2464                .await
2465                .is_ok()
2466        );
2467
2468        assert!(client.remove_channel(&uaid, &chid).await.is_ok());
2469
2470        // Now, can we do all that with topic messages
2471        client.add_channel(&uaid, &topic_chid).await?;
2472        let test_data = "An_encrypted_pile_of_crap_with_a_topic".to_owned();
2473        let timestamp = now();
2474        let sort_key = now();
2475        // Can we store a message?
2476        let test_notification = crate::db::Notification {
2477            channel_id: topic_chid,
2478            version: "test".to_owned(),
2479            ttl: 300,
2480            topic: Some("topic".to_owned()),
2481            timestamp,
2482            data: Some(test_data.clone()),
2483            sortkey_timestamp: Some(sort_key),
2484            ..Default::default()
2485        };
2486        assert!(
2487            client
2488                .save_message(&uaid, test_notification.clone())
2489                .await
2490                .is_ok()
2491        );
2492
2493        let mut fetched = client.fetch_topic_messages(&uaid, 999).await?;
2494        assert_ne!(fetched.messages.len(), 0);
2495        let fm = fetched.messages.pop().unwrap();
2496        assert_eq!(fm.channel_id, test_notification.channel_id);
2497        assert_eq!(fm.data, Some(test_data));
2498
2499        // Grab the message that was submmited.
2500        let fetched = client.fetch_topic_messages(&uaid, 999).await?;
2501        assert_ne!(fetched.messages.len(), 0);
2502
2503        // can we clean up our toys?
2504        assert!(
2505            client
2506                .remove_message(&uaid, &test_notification.chidmessageid())
2507                .await
2508                .is_ok()
2509        );
2510
2511        assert!(client.remove_channel(&uaid, &topic_chid).await.is_ok());
2512
2513        let msgs = client
2514            .fetch_timestamp_messages(&uaid, None, 999)
2515            .await?
2516            .messages;
2517        assert!(msgs.is_empty());
2518
2519        let fetched = client.get_user(&uaid).await?.unwrap();
2520        assert!(
2521            client
2522                .remove_node_id(&uaid, &node_id, connected_at, &fetched.version)
2523                .await
2524                .is_ok()
2525        );
2526        // did we remove it?
2527        let fetched = client.get_user(&uaid).await?.unwrap();
2528        assert_eq!(fetched.node_id, None);
2529
2530        assert!(client.remove_user(&uaid).await.is_ok());
2531
2532        assert!(client.get_user(&uaid).await?.is_none());
2533
2534        Ok(())
2535    }
2536
2537    #[actix_rt::test]
2538    async fn read_cells_family_id() -> DbResult<()> {
2539        let client = new_client().unwrap();
2540        let uaid = gen_test_uaid();
2541        client.remove_user(&uaid).await.unwrap();
2542
2543        let qualifier = "foo".to_owned();
2544
2545        let row_key = uaid.simple().to_string();
2546        let mut row = Row::new(row_key.clone());
2547        row.cells.insert(
2548            ROUTER_FAMILY.to_owned(),
2549            vec![cell::Cell {
2550                qualifier: qualifier.to_owned(),
2551                value: "bar".as_bytes().to_vec(),
2552                ..Default::default()
2553            }],
2554        );
2555        client.write_row(row).await.unwrap();
2556        let req = client.read_row_request(&row_key);
2557        let Some(row) = client.read_row(req).await.unwrap() else {
2558            panic!("Expected row");
2559        };
2560        assert_eq!(row.cells.len(), 1);
2561        assert_eq!(row.cells.keys().next().unwrap(), qualifier.as_str());
2562        client.remove_user(&uaid).await
2563    }
2564
2565    #[actix_rt::test]
2566    async fn add_user_existing() {
2567        let client = new_client().unwrap();
2568        let uaid = gen_test_uaid();
2569        let user = User {
2570            uaid,
2571            ..Default::default()
2572        };
2573        client.remove_user(&uaid).await.unwrap();
2574
2575        client.add_user(&user).await.unwrap();
2576        let err = client.add_user(&user).await.unwrap_err();
2577        assert!(matches!(err, DbError::Conditional));
2578    }
2579
2580    #[actix_rt::test]
2581    async fn version_check() {
2582        let client = new_client().unwrap();
2583        let uaid = gen_test_uaid();
2584        let user = User {
2585            uaid,
2586            ..Default::default()
2587        };
2588        client.remove_user(&uaid).await.unwrap();
2589
2590        client.add_user(&user).await.unwrap();
2591        let mut user = client.get_user(&uaid).await.unwrap().unwrap();
2592        assert!(client.update_user(&mut user.clone()).await.unwrap());
2593
2594        let fetched = client.get_user(&uaid).await.unwrap().unwrap();
2595        assert_ne!(user.version, fetched.version);
2596        // should now fail w/ a stale version
2597        assert!(!client.update_user(&mut user).await.unwrap());
2598
2599        client.remove_user(&uaid).await.unwrap();
2600    }
2601
2602    #[actix_rt::test]
2603    async fn lingering_chid_record() {
2604        let client = new_client().unwrap();
2605        let uaid = gen_test_uaid();
2606        let chid = Uuid::parse_str(TEST_CHID).unwrap();
2607        let user = User {
2608            uaid,
2609            ..Default::default()
2610        };
2611        client.remove_user(&uaid).await.unwrap();
2612
2613        // add_channel doesn't check for the existence of a user
2614        client.add_channel(&uaid, &chid).await.unwrap();
2615
2616        // w/ chid records in the router row, get_user should treat
2617        // this as the user not existing
2618        assert!(client.get_user(&uaid).await.unwrap().is_none());
2619
2620        client.add_user(&user).await.unwrap();
2621        // get_user should have also cleaned up the chids
2622        assert!(client.get_channels(&uaid).await.unwrap().is_empty());
2623
2624        client.remove_user(&uaid).await.unwrap();
2625    }
2626
2627    #[actix_rt::test]
2628    async fn lingering_current_timestamp() {
2629        let client = new_client().unwrap();
2630        let uaid = gen_test_uaid();
2631        client.remove_user(&uaid).await.unwrap();
2632
2633        client
2634            .increment_storage(&uaid, crate::util::sec_since_epoch())
2635            .await
2636            .unwrap();
2637        assert!(client.get_user(&uaid).await.unwrap().is_none());
2638
2639        client.remove_user(&uaid).await.unwrap();
2640    }
2641
2642    #[actix_rt::test]
2643    async fn lingering_chid_w_version_record() {
2644        let client = new_client().unwrap();
2645        let uaid = gen_test_uaid();
2646        let chid = Uuid::parse_str(TEST_CHID).unwrap();
2647        client.remove_user(&uaid).await.unwrap();
2648
2649        client.add_channel(&uaid, &chid).await.unwrap();
2650        assert!(client.remove_channel(&uaid, &chid).await.unwrap());
2651        assert!(client.get_user(&uaid).await.unwrap().is_none());
2652
2653        client.remove_user(&uaid).await.unwrap();
2654    }
2655
2656    #[actix_rt::test]
2657    async fn channel_and_current_timestamp_ttl_updates() {
2658        let client = new_client().unwrap();
2659        let uaid = gen_test_uaid();
2660        let chid = Uuid::parse_str(TEST_CHID).unwrap();
2661        client.remove_user(&uaid).await.unwrap();
2662
2663        // Setup a user with some channels and a current_timestamp
2664        let user = User {
2665            uaid,
2666            ..Default::default()
2667        };
2668        client.add_user(&user).await.unwrap();
2669
2670        client.add_channel(&uaid, &chid).await.unwrap();
2671        client
2672            .add_channel(&uaid, &uuid::Uuid::new_v4())
2673            .await
2674            .unwrap();
2675
2676        client
2677            .increment_storage(
2678                &uaid,
2679                SystemTime::now()
2680                    .duration_since(SystemTime::UNIX_EPOCH)
2681                    .unwrap()
2682                    .as_secs(),
2683            )
2684            .await
2685            .unwrap();
2686
2687        let req = client.read_row_request(&uaid.as_simple().to_string());
2688        let Some(mut row) = client.read_row(req).await.unwrap() else {
2689            panic!("Expected row");
2690        };
2691
2692        // Ensure the initial cell expiry (timestamp) of all the cells
2693        // in the row has been updated
2694        let ca_expiry = row.take_required_cell("connected_at").unwrap().timestamp;
2695        for mut cells in row.cells.into_values() {
2696            let Some(cell) = cells.pop() else {
2697                continue;
2698            };
2699            assert!(
2700                cell.timestamp >= ca_expiry,
2701                "{} cell timestamp should >= connected_at's",
2702                cell.qualifier
2703            );
2704        }
2705
2706        let mut user = client.get_user(&uaid).await.unwrap().unwrap();
2707
2708        // Quick nap to make sure that the ca_expiry values are different.
2709        tokio::time::sleep(Duration::from_secs_f32(0.2)).await;
2710        client.update_user(&mut user).await.unwrap();
2711
2712        // Ensure update_user updated the expiry (timestamp) of every cell in the row
2713        let req = client.read_row_request(&uaid.as_simple().to_string());
2714        let Some(mut row) = client.read_row(req).await.unwrap() else {
2715            panic!("Expected row");
2716        };
2717
2718        let ca_expiry2 = row.take_required_cell("connected_at").unwrap().timestamp;
2719
2720        assert!(ca_expiry2 > ca_expiry);
2721
2722        for mut cells in row.cells.into_values() {
2723            let Some(cell) = cells.pop() else {
2724                continue;
2725            };
2726            assert!(
2727                cell.timestamp >= ca_expiry2,
2728                "{} cell timestamp expiry should exceed connected_at's",
2729                cell.qualifier
2730            );
2731        }
2732
2733        client.remove_user(&uaid).await.unwrap();
2734    }
2735
2736    /// A router record whose core `connected_at` cell has expired (its TTL
2737    /// timestamp is in the past) but which still has fresher subordinate cells
2738    //  must be treated as "no user": `get_user` returns `None` and purges the
2739    /// leftover cells rather than erroring or resurrecting a partial record.
2740    #[actix_rt::test]
2741    async fn expired_router_record_is_dropped() {
2742        let client = new_client().unwrap();
2743        let uaid = gen_test_uaid();
2744        let chid = Uuid::parse_str(TEST_CHID).unwrap();
2745        client.remove_user(&uaid).await.unwrap();
2746
2747        // Hand-write a partially-expired record: `connected_at` (a core user
2748        // cell) has an expiry in the past, while `version` and a `chid:` cell
2749        // still have future expiries. This happens when a e.g. a user has not
2750        // checked in recently but notifications were added for them.
2751        let row_key = uaid.simple().to_string();
2752        let past = SystemTime::now() - Duration::from_secs(10);
2753        let future = SystemTime::now() + Duration::from_secs(3600);
2754        let mut row = Row::new(row_key.clone());
2755        let mut cells = vec![
2756            cell::Cell {
2757                qualifier: "connected_at".to_owned(),
2758                value: 0u64.to_be_bytes().to_vec(),
2759                timestamp: past,
2760                ..Default::default()
2761            },
2762            cell::Cell {
2763                qualifier: "version".to_owned(),
2764                value: Uuid::new_v4().into_bytes().to_vec(),
2765                timestamp: future,
2766                ..Default::default()
2767            },
2768        ];
2769        cells.extend(channels_to_cells(Cow::Owned(HashSet::from([chid])), future));
2770        row.add_cells(ROUTER_FAMILY, cells);
2771        client.write_row(row).await.unwrap();
2772
2773        // The expired core cell is filtered out at read time, leaving only
2774        // subordinate cells, so the record reads as absent.
2775        assert!(client.get_user(&uaid).await.unwrap().is_none());
2776
2777        //  `get_user` should have purged the leftover row entirely
2778        let req = client.read_row_request(&row_key);
2779        assert!(client.read_row(req).await.unwrap().is_none());
2780    }
2781}