Skip to content

Commit

Permalink
feat: add metadata headers for bigtable (#586)
Browse files Browse the repository at this point in the history
  • Loading branch information
jrconlin and pjenvey authored Feb 15, 2024
1 parent 80c961a commit e043fb0
Show file tree
Hide file tree
Showing 10 changed files with 231 additions and 31 deletions.
4 changes: 2 additions & 2 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ jobs:
audit:
docker:
# NOTE: update version for all # RUST_VER
- image: rust:1.74
- image: rust:1.76
auth:
username: $DOCKER_USER
password: $DOCKER_PASS
Expand Down Expand Up @@ -133,7 +133,7 @@ jobs:
apt install build-essential curl libstdc++6 libstdc++-10-dev libssl-dev pkg-config -y
apt install cmake -y
# RUST_VER
curl https://sh.rustup.rs -sSf | sh -s -- --default-toolchain 1.73 -y
curl https://sh.rustup.rs -sSf | sh -s -- --default-toolchain 1.76 -y
export PATH=$PATH:$HOME/.cargo/bin
echo 'export PATH=$PATH:$HOME/.cargo/bin' >> $BASH_ENV
rustc --version
Expand Down
33 changes: 17 additions & 16 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# NOTE: Ensure builder's Rust version matches CI's in .circleci/config.yml
FROM rust:1.74-buster as builder
# RUST_VER
FROM rust:1.76-buster as builder
ARG CRATE

ADD . /app
Expand Down
2 changes: 1 addition & 1 deletion autoconnect/autoconnect-web/src/dockerflow.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ pub fn config(config: &mut web::ServiceConfig) {

/// Handle the `/health` and `/__heartbeat__` routes
// note, this changes to `blocks_in_conditions` for 1.76+
#[allow(clippy::blocks_in_if_conditions)]
#[allow(clippy::blocks_in_conditions)]
pub async fn health_route(state: Data<AppState>) -> Json<serde_json::Value> {
let status = if state
.db
Expand Down
8 changes: 7 additions & 1 deletion autopush-common/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ google-cloud-rust-raw = { version = "0.16", default-features = false, features =
], optional = true }
grpcio = { version = "=0.13.0", features = ["openssl"], optional = true }
protobuf = { version = "=2.28.0", optional = true } # grpcio does not support protobuf 3+
form_urlencoded = { version = "1.2", optional = true }

[dev-dependencies]
mockito = "0.31"
Expand All @@ -78,7 +79,12 @@ actix-rt = "2.8"
[features]
# NOTE: Do not set a `default` here, rather specify them in the calling library.
# This is to reduce complexity around feature specification.
bigtable = ["dep:google-cloud-rust-raw", "dep:grpcio", "dep:protobuf"]
bigtable = [
"dep:google-cloud-rust-raw",
"dep:grpcio",
"dep:protobuf",
"dep:form_urlencoded",
]
dynamodb = ["dep:rusoto_core", "dep:rusoto_credential", "dep:rusoto_dynamodb"]
dual = ["dynamodb", "bigtable"]
aws = []
Expand Down
5 changes: 5 additions & 0 deletions autopush-common/src/db/bigtable/bigtable_client/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,9 @@ pub enum BigTableError {
#[error("Bigtable write error: {0}")]
Write(#[source] grpcio::Error),

#[error("GRPC Error: {0}")]
GRPC(#[source] grpcio::Error),

/// Return a GRPC status code and any message.
/// See https://grpc.github.io/grpc/core/md_doc_statuscodes.html
#[error("Bigtable status response: {0:?}")]
Expand Down Expand Up @@ -140,6 +143,7 @@ impl ReportableError for BigTableError {
BigTableError::Admin(_, _) => "storage.bigtable.error.admin",
BigTableError::Recycle => "storage.bigtable.error.recycle",
BigTableError::Pool(_) => "storage.bigtable.error.pool",
BigTableError::GRPC(_) => "storage.bigtable.error.grpc",
};
Some(err)
}
Expand All @@ -148,6 +152,7 @@ impl ReportableError for BigTableError {
match &self {
BigTableError::InvalidRowResponse(s) => vec![("error", s.to_string())],
BigTableError::InvalidChunk(s) => vec![("error", s.to_string())],
BigTableError::GRPC(s) => vec![("error", s.to_string())],
BigTableError::Read(s) => vec![("error", s.to_string())],
BigTableError::Write(s) => vec![("error", s.to_string())],
BigTableError::Status(code, s) => {
Expand Down
138 changes: 138 additions & 0 deletions autopush-common/src/db/bigtable/bigtable_client/metadata.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
/// gRPC metadata Resource prefix header
///
/// Generic across Google APIs. This "improves routing by the backend" as
/// described by other clients
const PREFIX_KEY: &str = "google-cloud-resource-prefix";

/// gRPC metadata Client information header
///
/// A `User-Agent` like header, likely its main use is for GCP's metrics
const METRICS_KEY: &str = "x-goog-api-client";

/// gRPC metadata Dynamic Routing header:
/// https://google.aip.dev/client-libraries/4222
///
/// See the googleapis protobuf for which routing header params are used for
/// each Spanner operation (under the `google.api.http` option).
///
/// https://github.com/googleapis/googleapis/blob/master/google/spanner/v1/spanner.proto
const ROUTING_KEY: &str = "x-goog-request-params";

/// gRPC metadata Leader Aware Routing header
///
/// Not well documented. Added to clients in early 2023 defaulting to disabled.
/// Clients have began defaulting it to enabled in late 2023.
///
/// "Enabling leader aware routing would route all requests in RW/PDML
/// transactions to the leader region." as described by other Spanner clients
const LEADER_AWARE_KEY: &str = "x-goog-spanner-route-to-leader";

/// The USER_AGENT string is a static value specified by Google.
/// Its meaning is not to be known to the uninitiated.
const USER_AGENT: &str = "gl-external/1.0 gccl/1.0";

/// Builds the [grpcio::Metadata] for all db operations
#[derive(Default)]
pub struct MetadataBuilder<'a> {
prefix: &'a str,
routing_params: Vec<(&'a str, &'a str)>,
route_to_leader: bool,
}

impl<'a> MetadataBuilder<'a> {
/// Initialize a new builder with a [PREFIX_KEY] header for the given
/// resource
pub fn with_prefix(prefix: &'a str) -> Self {
Self {
prefix,
..Default::default()
}
}

/// Add a [ROUTING_KEY] header
/// This normally specifies the session name, but unlike spanner, bigtable does not appear to have one of those?
pub fn routing_param(mut self, key: &'a str, value: &'a str) -> Self {
self.routing_params.push((key, value));
self
}

/// Toggle the [LEADER_AWARE_KEY] header
pub fn route_to_leader(mut self, route_to_leader: bool) -> Self {
self.route_to_leader = route_to_leader;
self
}

/// Build the [grpcio::Metadata]
pub fn build(self) -> Result<grpcio::Metadata, grpcio::Error> {
let mut meta = grpcio::MetadataBuilder::new();

meta.add_str(PREFIX_KEY, self.prefix)?;
meta.add_str(METRICS_KEY, USER_AGENT)?;
if self.route_to_leader {
meta.add_str(LEADER_AWARE_KEY, "true")?;
}
if !self.routing_params.is_empty() {
meta.add_str(ROUTING_KEY, &self.routing_header())?;
}
Ok(meta.build())
}

fn routing_header(self) -> String {
let mut ser = form_urlencoded::Serializer::new(String::new());
for (key, val) in self.routing_params {
ser.append_pair(key, val);
}
// python-spanner (python-api-core) doesn't encode '/':
// https://github.com/googleapis/python-api-core/blob/6251eab/google/api_core/gapic_v1/routing_header.py#L85
ser.finish().replace("%2F", "/")
}
}

#[cfg(test)]
mod tests {
use std::{collections::HashMap, str};

use super::{
MetadataBuilder, LEADER_AWARE_KEY, METRICS_KEY, PREFIX_KEY, ROUTING_KEY, USER_AGENT,
};

// Resource paths should not start with a "/"
pub const DB: &str = "projects/foo/instances/bar/databases/gorp";
pub const SESSION: &str = "projects/foo/instances/bar/databases/gorp/sessions/f00B4r_quuX";

#[test]
fn metadata_basic() {
let meta = MetadataBuilder::with_prefix(DB)
.routing_param("session", SESSION)
.routing_param("foo", "bar baz")
.build()
.unwrap();
let meta: HashMap<_, _> = meta.into_iter().collect();

assert_eq!(meta.len(), 3);
assert_eq!(str::from_utf8(meta.get(PREFIX_KEY).unwrap()).unwrap(), DB);
assert_eq!(
str::from_utf8(meta.get(METRICS_KEY).unwrap()).unwrap(),
USER_AGENT
);
assert_eq!(
str::from_utf8(meta.get(ROUTING_KEY).unwrap()).unwrap(),
format!("session={SESSION}&foo=bar+baz")
);
}

#[test]
fn leader_aware() {
let meta = MetadataBuilder::with_prefix(DB)
.route_to_leader(true)
.build()
.unwrap();
let meta: HashMap<_, _> = meta.into_iter().collect();

assert_eq!(meta.len(), 3);
assert_eq!(
str::from_utf8(meta.get(LEADER_AWARE_KEY).unwrap()).unwrap(),
"true"
);
}
}
Loading

0 comments on commit e043fb0

Please sign in to comment.