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