Skip to content

Commit bcb04c5

Browse files
committed
feat(ads-client): use nimbus to experiment http cache
1 parent 72f76f9 commit bcb04c5

File tree

9 files changed

+151
-10
lines changed

9 files changed

+151
-10
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
* Try to reset cache database schema on connection initialization failure.
3030
* Reset cache on context ID rotation.
3131
* Enable staging environment support for all platforms (previously feature-gated)
32+
* Added Nimbus integration to enable HTTP cache experimentation. Builder pattern now supports optional `nimbus()` setter to configure NimbusClient integration. ([#7188](https://github.com/mozilla/application-services/pull/7188))
3233

3334
### Android
3435
* Upgraded Kotlin compiler from 2.2.21 to 2.3.0 ([#7183](https://github.com/mozilla/application-services/pull/7183))

Cargo.lock

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

components/ads-client/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,12 @@ url = { version = "2", features = ["serde"] }
2828
uuid = { version = "1.3", features = ["v4"] }
2929
viaduct = { path = "../viaduct" }
3030
sql-support = { path = "../support/sql" }
31+
nimbus = { path = "../nimbus", package = "nimbus-sdk" }
3132

3233
[dev-dependencies]
3334
mockall = "0.12"
3435
mockito = { version = "0.31", default-features = false }
36+
tempfile = "3"
3537
viaduct-dev = { path = "../support/viaduct-dev" }
3638

3739
[build-dependencies]

components/ads-client/android/build.gradle

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ android {
3838
}
3939

4040
dependencies {
41+
api project(":nimbus")
42+
4143
implementation libs.mozilla.glean
4244

4345
testImplementation libs.mozilla.glean.forUnitTests

components/ads-client/src/client.rs

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ use crate::client::ad_response::{
1111
};
1212
use crate::client::config::AdsClientConfig;
1313
use crate::error::{RecordClickError, RecordImpressionError, ReportAdError, RequestAdsError};
14+
use crate::experiments::is_http_cache_enabled;
1415
use crate::http_cache::{HttpCache, RequestCachePolicy};
1516
use crate::mars::MARSClient;
1617
use crate::telemetry::Telemetry;
@@ -52,8 +53,11 @@ where
5253
let telemetry = client_config.telemetry;
5354

5455
// Configure the cache if a path is provided.
55-
// Defaults for ttl and cache size are also set if unspecified.
56+
// Nimbus experiment can disable the cache entirely.
5657
if let Some(cache_cfg) = client_config.cache_config {
58+
// Check if cache is disabled by Nimbus experiment
59+
let cache_enabled = is_http_cache_enabled(client_config.nimbus.clone());
60+
5761
let default_cache_ttl = Duration::from_secs(
5862
cache_cfg
5963
.default_cache_ttl_seconds
@@ -62,16 +66,20 @@ where
6266
let max_cache_size =
6367
ByteSize::mib(cache_cfg.max_size_mib.unwrap_or(DEFAULT_MAX_CACHE_SIZE_MIB));
6468

65-
let http_cache = match HttpCache::builder(cache_cfg.db_path)
66-
.max_size(max_cache_size)
67-
.default_ttl(default_cache_ttl)
68-
.build()
69-
{
70-
Ok(cache) => Some(cache),
71-
Err(e) => {
72-
telemetry.record(&e);
73-
None
69+
let http_cache = if cache_enabled {
70+
match HttpCache::builder(cache_cfg.db_path)
71+
.max_size(max_cache_size)
72+
.default_ttl(default_cache_ttl)
73+
.build()
74+
{
75+
Ok(cache) => Some(cache),
76+
Err(e) => {
77+
telemetry.record(&e);
78+
None
79+
}
7480
}
81+
} else {
82+
None
7583
};
7684

7785
let client = MARSClient::new(client_config.environment, http_cache, telemetry.clone());
@@ -259,6 +267,7 @@ mod tests {
259267
environment: Environment::Test,
260268
cache_config: None,
261269
telemetry: MozAdsTelemetryWrapper::noop(),
270+
nimbus: None,
262271
};
263272
let client = AdsClient::new(config);
264273
let context_id = client.get_context_id().unwrap();
@@ -271,6 +280,7 @@ mod tests {
271280
environment: Environment::Test,
272281
cache_config: None,
273282
telemetry: MozAdsTelemetryWrapper::noop(),
283+
nimbus: None,
274284
};
275285
let mut client = AdsClient::new(config);
276286
let old_id = client.get_context_id().unwrap();

components/ads-client/src/client/config.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@
33
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
44
*/
55

6+
use nimbus::NimbusClient;
67
use once_cell::sync::Lazy;
8+
use std::sync::Arc;
79
use url::Url;
810

911
use crate::telemetry::Telemetry;
@@ -21,6 +23,7 @@ where
2123
pub environment: Environment,
2224
pub cache_config: Option<AdsCacheConfig>,
2325
pub telemetry: T,
26+
pub nimbus: Option<Arc<NimbusClient>>,
2427
}
2528

2629
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
/* This Source Code Form is subject to the terms of the Mozilla Public
2+
* License, v. 2.0. If a copy of the MPL was not distributed with this
3+
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
4+
*/
5+
6+
//! Nimbus experiment integration for ads-client.
7+
8+
use nimbus::NimbusClient;
9+
use std::sync::Arc;
10+
11+
const FEATURE_ID: &str = "ads-client";
12+
13+
/// Query NimbusClient to check if HTTP cache is enabled for this user.
14+
/// Returns true (cache enabled) by default if no NimbusClient or no experiment is active.
15+
pub fn is_http_cache_enabled(nimbus: Option<Arc<NimbusClient>>) -> bool {
16+
let Some(nimbus) = nimbus else {
17+
return true; // Default: cache enabled
18+
};
19+
20+
let Ok(Some(json)) = nimbus.get_feature_config_variables(FEATURE_ID.to_string()) else {
21+
return true; // Default: cache enabled
22+
};
23+
24+
serde_json::from_str::<serde_json::Value>(&json)
25+
.ok()
26+
.and_then(|v| v.get("http-cache-enabled")?.as_bool())
27+
.unwrap_or(true)
28+
}
29+
30+
#[cfg(test)]
31+
mod tests {
32+
use super::*;
33+
use nimbus::AppContext;
34+
use serde_json::json;
35+
use uuid::Uuid;
36+
37+
struct NoopMetricsHandler;
38+
impl nimbus::metrics::MetricsHandler for NoopMetricsHandler {
39+
fn record_enrollment_statuses(&self, _: Vec<nimbus::metrics::EnrollmentStatusExtraDef>) {}
40+
fn record_feature_activation(&self, _: nimbus::metrics::FeatureExposureExtraDef) {}
41+
fn record_feature_exposure(&self, _: nimbus::metrics::FeatureExposureExtraDef) {}
42+
fn record_malformed_feature_config(
43+
&self,
44+
_: nimbus::metrics::MalformedFeatureConfigExtraDef,
45+
) {
46+
}
47+
}
48+
49+
#[test]
50+
fn test_fifty_fifty_experiment() {
51+
let experiment = json!({
52+
"data": [{
53+
"schemaVersion": "1.0.0",
54+
"slug": "ads-cache-test",
55+
"featureIds": ["ads-client"],
56+
"branches": [
57+
{ "slug": "control", "ratio": 1, "feature": { "featureId": "ads-client", "value": { "http-cache-enabled": true } } },
58+
{ "slug": "treatment", "ratio": 1, "feature": { "featureId": "ads-client", "value": { "http-cache-enabled": false } } }
59+
],
60+
"bucketConfig": { "count": 10000, "start": 0, "total": 10000, "namespace": "test", "randomizationUnit": "nimbus_id" },
61+
"appName": "test-app", "appId": "org.mozilla.test", "channel": "test",
62+
"userFacingName": "Test", "userFacingDescription": "Test",
63+
"isEnrollmentPaused": false, "proposedEnrollment": 7, "referenceBranch": "control"
64+
}]
65+
}).to_string();
66+
67+
let (mut enabled, mut disabled) = (0, 0);
68+
for i in 0..100 {
69+
let tmp_dir = tempfile::tempdir().unwrap();
70+
let nimbus = Arc::new(
71+
NimbusClient::new(
72+
AppContext {
73+
app_name: "test-app".into(),
74+
app_id: "org.mozilla.test".into(),
75+
channel: "test".into(),
76+
..Default::default()
77+
},
78+
None,
79+
vec![],
80+
tmp_dir.path(),
81+
Box::new(NoopMetricsHandler),
82+
None,
83+
None,
84+
None,
85+
)
86+
.unwrap(),
87+
);
88+
nimbus.initialize().unwrap();
89+
// Create a deterministic UUID from the loop index
90+
let mut bytes = [0u8; 16];
91+
bytes[0] = i as u8;
92+
nimbus.set_nimbus_id(&Uuid::from_bytes(bytes)).unwrap();
93+
nimbus.set_experiments_locally(experiment.clone()).unwrap();
94+
nimbus.apply_pending_experiments().unwrap();
95+
96+
if is_http_cache_enabled(Some(nimbus.clone())) {
97+
enabled += 1;
98+
} else {
99+
disabled += 1;
100+
}
101+
}
102+
103+
assert!(
104+
(30..=70).contains(&enabled),
105+
"Expected ~50% enabled, got {enabled}/100"
106+
);
107+
assert!(
108+
(30..=70).contains(&disabled),
109+
"Expected ~50% disabled, got {disabled}/100"
110+
);
111+
}
112+
}

components/ads-client/src/ffi.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ use crate::ffi::telemetry::MozAdsTelemetryWrapper;
1818
use crate::http_cache::{CacheMode, RequestCachePolicy};
1919
use crate::MozAdsClient;
2020
use error_support::{ErrorHandling, GetErrorHandling};
21+
use nimbus::NimbusClient;
2122
use parking_lot::Mutex;
2223
use url::Url;
2324

@@ -99,6 +100,7 @@ struct MozAdsClientBuilderInner {
99100
environment: Option<MozAdsEnvironment>,
100101
cache_config: Option<MozAdsCacheConfig>,
101102
telemetry: Option<Arc<dyn MozAdsTelemetry>>,
103+
nimbus: Option<Arc<NimbusClient>>,
102104
}
103105

104106
impl Default for MozAdsClientBuilder {
@@ -129,6 +131,11 @@ impl MozAdsClientBuilder {
129131
self
130132
}
131133

134+
pub fn nimbus(self: Arc<Self>, nimbus: Arc<NimbusClient>) -> Arc<Self> {
135+
self.0.lock().nimbus = Some(nimbus);
136+
self
137+
}
138+
132139
pub fn build(&self) -> MozAdsClient {
133140
let inner = self.0.lock();
134141
let client_config = AdsClientConfig {
@@ -139,6 +146,7 @@ impl MozAdsClientBuilder {
139146
.clone()
140147
.map(MozAdsTelemetryWrapper::new)
141148
.unwrap_or_else(MozAdsTelemetryWrapper::noop),
149+
nimbus: inner.nimbus.clone(),
142150
};
143151
let client = AdsClient::new(client_config);
144152
MozAdsClient {

components/ads-client/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ use http_cache::RequestCachePolicy;
1616

1717
mod client;
1818
mod error;
19+
mod experiments;
1920
mod ffi;
2021
pub mod http_cache;
2122
mod mars;

0 commit comments

Comments
 (0)