Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
### Ads-Client

- Added `blocks: Vec<String>` to `ffi::MozAdsRequestOptions`, `AdsClient::request*_ads`, `MARSClient::fetch_ads`, `mars::AdRequest`, and `mars::AdRequest::try_new`. This is serialized and passed to MARS so that it can remove blocks server-side.
- `shutdown` no longer requires a full `AdsClient` lock (at the cost of no longer shutting down the sqlite db), and telemetry is no longer cloned in the `MozAdsClientBuilder` functions.

# v156.0 (_2026-08-27_)

Expand Down
83 changes: 1 addition & 82 deletions components/ads-client/src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -98,18 +98,6 @@ where
self.client.clear_cache()
}

// Shutdown the db connection and drop references to telemetry callbacks.
// Should be used only when dropping the ads client, this may be extended to drop more things.
pub fn shutdown_client(&mut self) -> Result<(), rusqlite::Error> {
// Drop telemetry (within the telemetry wrapper)
self.telemetry.shutdown();

// Shutdown DB
self.client.shutdown_db()?;

Ok(())
}

pub fn get_context_id(&self) -> context_id::ApiResult<String> {
self.context_id_provider.context_id()
}
Expand Down Expand Up @@ -286,7 +274,7 @@ pub enum ClientOperationEvent {

#[cfg(test)]
mod tests {
use std::{assert_eq, assert_ne, sync::Arc};
use std::assert_eq;

use crate::{
ffi::telemetry::MozAdsTelemetryWrapper,
Expand Down Expand Up @@ -535,73 +523,4 @@ mod tests {
m1.assert();
m2.assert();
}

#[test]
fn test_shutdown_telemetry() {
viaduct_dev::init_backend_dev();

// test with client created from config
let noop_telemetry = MozAdsTelemetryWrapper::noop();
let weak_reference = Arc::downgrade(
&noop_telemetry
.clone_inner_arc()
.expect("Inner telemetry should be Some before dropping"),
);
let config = AdsClientConfig {
cache_config: None,
context_id_provider: None,
environment: Environment::Test,
telemetry: noop_telemetry,
};
let mut client = AdsClient::new(config);

// weak ref will show 0 strong references when the Arc<dyn MozAdsTelemetry> is gone.
assert_ne!(weak_reference.strong_count(), 0);
client.shutdown_client().unwrap();
assert_eq!(weak_reference.strong_count(), 0);

// test also with internal function from_mars
let noop_telemetry = MozAdsTelemetryWrapper::noop();
let weak_reference = Arc::downgrade(
&noop_telemetry
.clone_inner_arc()
.expect("Inner telemetry should be Some before dropping"),
);
let cache = HttpCache::builder("test_shutdown_telemetry")
.build()
.unwrap();
let mars_client = MARSClient::new(Environment::Test, Some(cache), noop_telemetry);
let mut client = new_with_mars_client(mars_client);

// weak ref will show 0 strong references when the Arc<dyn MozAdsTelemetry> is gone.
assert_ne!(weak_reference.strong_count(), 0);
client.shutdown_client().unwrap();
assert_eq!(weak_reference.strong_count(), 0);
}

#[test]
fn test_shutdown_is_idempotent() {
viaduct_dev::init_backend_dev();

let noop_telemetry = MozAdsTelemetryWrapper::noop();
let weak_reference = Arc::downgrade(
&noop_telemetry
.clone_inner_arc()
.expect("Inner telemetry should be Some before dropping"),
);
// A real cache so the second shutdown exercises the db close path.
let cache = HttpCache::builder("test_shutdown_is_idempotent")
.build()
.unwrap();
let mars_client = MARSClient::new(Environment::Test, Some(cache), noop_telemetry);
let mut client = new_with_mars_client(mars_client);

client.shutdown_client().unwrap();
assert_eq!(weak_reference.strong_count(), 0);

// Repeated shutdowns must not error or re-close an already closed connection.
client.shutdown_client().unwrap();
client.shutdown_client().unwrap();
assert_eq!(weak_reference.strong_count(), 0);
}
}
56 changes: 49 additions & 7 deletions components/ads-client/src/ffi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ pub mod error;
pub mod telemetry;

use std::sync::Arc;
#[cfg(test)]
use std::sync::Weak;

use crate::client::config::{AdsCacheConfig, AdsClientConfig};
use crate::client::{AdsClient, ContextIdProvider};
Expand All @@ -20,8 +22,8 @@ use crate::mars::ad_response::{
};
use crate::mars::Environment;
use crate::mars::ReportReason;
use crate::AdsClientUrl;
use crate::MozAdsClient;
use crate::{AdsClientUrl, ShutdownReferences};
use parking_lot::Mutex;
use std::collections::HashMap;

Expand Down Expand Up @@ -125,7 +127,12 @@ impl MozAdsClientBuilder {
}

pub fn build(&self) -> MozAdsClient {
let inner = self.0.lock();
let mut inner = self.0.lock();
let telemetry = inner
.telemetry
.take()
.map(MozAdsTelemetryWrapper::new)
.unwrap_or_else(MozAdsTelemetryWrapper::noop);
let client_config = AdsClientConfig {
cache_config: inner.cache_config.clone().map(Into::into),
context_id_provider: inner
Expand All @@ -134,15 +141,12 @@ impl MozAdsClientBuilder {
.map(MozAdsContextIdProviderWrapper::new)
.map(Into::into),
environment: inner.environment.unwrap_or_default().into(),
telemetry: inner
.telemetry
.clone()
.map(MozAdsTelemetryWrapper::new)
.unwrap_or_else(MozAdsTelemetryWrapper::noop),
telemetry: telemetry.clone(),
};
let client = AdsClient::new(client_config);
MozAdsClient {
inner: Mutex::new(client),
shutdown_references: ShutdownReferences::new(telemetry),
}
}

Expand Down Expand Up @@ -170,6 +174,13 @@ impl MozAdsClientBuilder {
}
}

impl MozAdsClientBuilder {
#[cfg(test)]
pub fn fetch_telemetry(&self) -> Option<Weak<dyn MozAdsTelemetry>> {
self.0.lock().telemetry.as_ref().map(Arc::downgrade)
}
}

#[derive(Clone, Copy, Debug, Default, uniffi::Enum, Eq, PartialEq)]
pub enum MozAdsEnvironment {
#[default]
Expand Down Expand Up @@ -473,3 +484,34 @@ impl From<&MozAdsPlacementRequestWithCount> for AdPlacementRequest {
}
}
}

#[cfg(test)]
mod tests {
use crate::{ffi::telemetry::NoopMozAdsTelemetry, MozAdsClientBuilder};
use std::sync::Arc;

#[test]
fn test_telemetry_not_held_by_builder() {
// Related to Bug 2064543
// The builder can hold a reference to the passed telemetry, meaning that if the builder still exists, `shutdown` doesn't drop all references.

// Make a builder and pass in telemetry.
let builder = Arc::new(MozAdsClientBuilder::new());
assert!(builder.fetch_telemetry().is_none());
let builder = MozAdsClientBuilder::telemetry(builder, Box::new(NoopMozAdsTelemetry));
let weak_telemetry = builder
.fetch_telemetry()
.expect("Telemetry should be set in builder after being passed");
assert_eq!(weak_telemetry.strong_count(), 1);

// Building the MozAdsClient should pass the telemetry, not clone it.
let built_client = builder.build();
assert_eq!(weak_telemetry.strong_count(), 1);

// Shutting down, even though the builder still exists, should successfully shutdown all telemetry references.
built_client.shutdown().unwrap();
assert_eq!(weak_telemetry.strong_count(), 0);
builder.build();
assert_eq!(weak_telemetry.strong_count(), 0);
}
}
12 changes: 5 additions & 7 deletions components/ads-client/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,18 +12,18 @@ use parking_lot::Mutex;
use url::Url as AdsClientUrl;

use client::AdsClient;
use error_support::error;
use http_cache::CachePolicy;
use mars::ad_request::{AdPlacementRequest, AdRequestFlags};
mod client;
mod ffi;
pub mod http_cache;
mod mars;
pub mod shutdown;
pub mod telemetry;

pub use ffi::*;

use crate::ffi::telemetry::MozAdsTelemetryWrapper;
use crate::{ffi::telemetry::MozAdsTelemetryWrapper, shutdown::ShutdownReferences};

#[cfg(test)]
mod test_utils;
Expand All @@ -39,6 +39,7 @@ uniffi::custom_type!(AdsClientUrl, String, {
#[derive(uniffi::Object)]
pub struct MozAdsClient {
inner: Mutex<AdsClient<MozAdsTelemetryWrapper>>,
shutdown_references: ShutdownReferences,
}

#[uniffi::export]
Expand All @@ -54,13 +55,10 @@ impl MozAdsClient {

// Allows the ads-client to unload some references and prepare for a safe shutdown.
// Other methods should not be called after this one.
// Currently it is not possible to return an error, but it may yet be possible to do so, so we keep the Result.
#[uniffi::method()]
pub fn shutdown(&self) -> AdsClientApiResult<()> {
let mut inner = self.inner.lock();
if let Err(err) = inner.shutdown_client() {
// Log the error, but continue with shutdown.
error!("Failed to shutdown the ads client: {:?}", err);
}
self.shutdown_references.shutdown();

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note the significant difference here: we are no longer shutting down the sqlite database 'safely', as it doesn't seem to be needed to do to fix this crash. We will revisit this in another refactor.

Ok(())
}

Expand Down
1 change: 1 addition & 0 deletions components/ads-client/src/mars.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ where
self.transport.clear_cache()
}

#[allow(dead_code)]
pub fn shutdown_db(&mut self) -> Result<(), rusqlite::Error> {
self.transport.shutdown_db()
}
Expand Down
127 changes: 127 additions & 0 deletions components/ads-client/src/shutdown.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
use crate::{ffi::telemetry::MozAdsTelemetryWrapper, telemetry::Telemetry};

pub struct ShutdownReferences {
telemetry: MozAdsTelemetryWrapper,
}

impl ShutdownReferences {
pub fn new(telemetry: MozAdsTelemetryWrapper) -> ShutdownReferences {
ShutdownReferences { telemetry }
}

// Shutdown anything that needs to be shut down safely and drop references to telemetry callbacks.
// Should be called only when dropping the ads client. This may be extended to drop more things.
pub fn shutdown(&self) {
// Drop telemetry (within the telemetry wrapper)
self.telemetry.shutdown();

// TODO: It may be prudent to call the MARSClient `shutdown_db` function here as well.
// However, this requires a mutable lock to be held over the MARSClient (and/or AdsClient),
// which might get held elsewhere over a network request. We can consider re-adding this after
// a refactor or for the new stateful sqlite database.
}
}

#[cfg(test)]
mod tests {
use crate::{ffi::telemetry::NoopMozAdsTelemetry, MozAdsCacheConfig, MozAdsClientBuilder};
use std::{
sync::{mpsc, Arc},
thread,
time::Duration,
};

fn test_timeout<F>(timeout: Duration, func: F)
where
F: FnOnce() + Send + 'static,
{
let (tx, rx) = mpsc::channel();
let handle = thread::spawn(move || {
func();
tx.send(())
.expect("Internal test error: Could not send completion signal");
});

match rx.recv_timeout(timeout) {
Ok(_) => handle.join().unwrap(),
Err(_) => panic!("Test exceeded timeout duration"),
}
}

// Shutdown procedure must not require a lock to be held on the inner AdsClient.
// This is because sync functions like `request_tile_ads` require (at worst) to wait on a hanging non-cancellable network request to resolve,
// and they hold the lock for the entirety of that time. Shutdown should only require the minimal amount of waiting/locking possible.
#[test]
fn shutdown_does_not_require_ads_client_lock() {
test_timeout(Duration::from_secs(5), || {
let builder = MozAdsClientBuilder::new().build();
let lock = builder.inner.lock();

// Holding a inner lock, we try to run shutdown.
builder.shutdown().unwrap();

// We explicitly drop the lock at the end.
drop(lock);
});
}

#[test]
fn test_shutdown_telemetry_basic() {

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These tests are not identical to the ones before, notice the use of MozAdsClientBuilder instead of an internal function to create an AdsClient. This is because I've moved some of the shutdown logic outside of AdsClient to not have to get a lock on the entire AdsClient. I think the logic of these tests should be functionally identical, but worth noting.

viaduct_dev::init_backend_dev();

// test with client created from config with no cache
let builder = Arc::new(MozAdsClientBuilder::new()).telemetry(Box::new(NoopMozAdsTelemetry));
let weak_reference = builder
.fetch_telemetry()
.expect("Inner telemetry should be Some in builder");
let client = builder.build();

// weak ref will show 0 strong references when the Arc<dyn MozAdsTelemetry> is gone.
assert_ne!(weak_reference.strong_count(), 0);
client.shutdown().unwrap();
assert_eq!(weak_reference.strong_count(), 0);

// test also with http cache
let builder = Arc::new(MozAdsClientBuilder::new())
.telemetry(Box::new(NoopMozAdsTelemetry))
.cache_config(MozAdsCacheConfig {
db_path: "test_shutdown_is_idempotent".to_string(),
default_cache_ttl_seconds: None,
max_size_mib: None,
});
let weak_reference = builder
.fetch_telemetry()
.expect("Inner telemetry should be Some in builder");
let client = builder.build();

// weak ref will show 0 strong references when the Arc<dyn MozAdsTelemetry> is gone.
assert_ne!(weak_reference.strong_count(), 0);
client.shutdown().unwrap();
assert_eq!(weak_reference.strong_count(), 0);
}

#[test]
fn test_shutdown_is_idempotent() {
viaduct_dev::init_backend_dev();

let builder = Arc::new(MozAdsClientBuilder::new())
.telemetry(Box::new(NoopMozAdsTelemetry))
.cache_config(MozAdsCacheConfig {
db_path: "test_shutdown_is_idempotent".to_string(),
default_cache_ttl_seconds: None,
max_size_mib: None,
});
let weak_reference = builder
.fetch_telemetry()
.expect("Inner telemetry should be Some in builder");
let client = builder.build();

client.shutdown().unwrap();
assert_eq!(weak_reference.strong_count(), 0);

// Repeated shutdowns must not error or re-close an already closed connection.
client.shutdown().unwrap();
client.shutdown().unwrap();
assert_eq!(weak_reference.strong_count(), 0);
}
}
Loading