Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
34415fc
feat: Adds the background worker
thesuzerain Sep 10, 2026
791f0a6
fix: removes the FFI layer
thesuzerain Sep 10, 2026
ef7a00a
feat: fixes, updates
thesuzerain Sep 10, 2026
b187cd4
fix: removed ping
thesuzerain Sep 10, 2026
0e4c7d5
fix: pong removal
thesuzerain Sep 10, 2026
59fa831
fix: adds shutdown
thesuzerain Sep 10, 2026
93f7d7d
fix: buffer
thesuzerain Sep 11, 2026
231d4cd
fix: changelog
thesuzerain Sep 11, 2026
2e932a1
Merge branch 'main' of https://github.com/mozilla/application-service…
thesuzerain Sep 11, 2026
df0fcac
fix: Revisions
thesuzerain Sep 12, 2026
517f7c8
Merge branch 'main' of https://github.com/mozilla/application-service…
thesuzerain Sep 15, 2026
141f1d5
fix: updated with cfg
thesuzerain Sep 15, 2026
46b0abe
fix: fmt clippy
thesuzerain Sep 15, 2026
a179fc0
Merge branch 'main' into AC-154-ads-client-background-worker
thesuzerain Sep 15, 2026
d02d8a1
fix: reorganizes background worker part
thesuzerain Sep 17, 2026
fc78c89
Merge branch 'main' of https://github.com/mozilla/application-service…
thesuzerain Sep 24, 2026
1084aa5
fix: update to shrink PR size and only use RequestQueue
thesuzerain Sep 25, 2026
8ca8550
fix: Some cleanup
thesuzerain Sep 25, 2026
e4cecc8
fix: some comments, removed some unnecessary things
thesuzerain Sep 25, 2026
bd02225
Merge branch 'main' of https://github.com/mozilla/application-service…
thesuzerain Sep 25, 2026
bdccc8b
fix: revisions
thesuzerain Oct 1, 2026
e8c8d91
fix: fmt
thesuzerain Oct 1, 2026
dd04663
Merge branch 'main' of https://github.com/mozilla/application-service…
thesuzerain Oct 10, 2026
05a8beb
fix: Merge conflict resolution
thesuzerain Oct 10, 2026
ad9fb2c
fix: Background worker ping interval is 100ms by default, customizable
thesuzerain Oct 13, 2026
d2863fc
feat: Encapsulates locks in RequestQueue, moves 'up' defaulting logic
thesuzerain Oct 14, 2026
f36cee6
Merge branch 'main' of https://github.com/mozilla/application-service…
thesuzerain Oct 16, 2026
0272b6b
feat: refactor
thesuzerain Oct 16, 2026
6c3c609
fix: some renaming
thesuzerain Oct 16, 2026
b60c9f2
fix: goes back to dispatchrequest
thesuzerain Oct 16, 2026
edd23cd
Merge branch 'main' of https://github.com/mozilla/application-service…
thesuzerain Oct 16, 2026
8245510
fix: merg conflict, fixes
thesuzerain Oct 16, 2026
18e7bdc
fix: fmt
thesuzerain Oct 16, 2026
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
160 changes: 158 additions & 2 deletions components/ads-client/src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,13 @@ use crate::mars::ad_response::{AdResponse, AdResponseValue};
use crate::mars::error::{RecordClickError, RecordImpressionError, ReportAdError};
use crate::mars::{MARSClient, ReportReason};
use crate::telemetry::{self, ClientOperation};
#[cfg(feature = "stateful")]
use crate::worker::{self, BackgroundWorker};
use config::AdsClientConfig;
use context_id::{ContextIDComponent, DefaultContextIdCallback};
use error::RequestAdsError;
use sql_support::open_database;
use std::collections::HashMap;
#[cfg(feature = "stateful")]
use std::sync::Arc;
use std::time::Duration;
use url::Url;
Expand All @@ -31,14 +32,158 @@ const DEFAULT_TTL_SECONDS: u64 = 300;
const DEFAULT_MAX_CACHE_SIZE_MIB: u64 = 10;
const DEFAULT_ROTATION_DAYS: u8 = 3;

// Thread sleep delay to wait for new requests to queue before resolving them.
// This must be a very short amount of time to ensure ads are requested near-immediately on startup.
#[cfg(feature = "stateful")]
pub const ADS_CLIENT_WORKER_SLEEP_LENGTH_DEFAULT: Duration = Duration::from_millis(100);

pub struct AdsClient {
inner: Arc<AdsClientInner>,
#[cfg(feature = "stateful")]
_worker: BackgroundWorker,

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.

I'll remove the _ on this once its used.

}

pub struct AdsClientInner {
#[cfg(feature = "stateful")]
ads_store: Arc<Option<AdsStore>>,
client: MARSClient,
context_id_component: ContextIDComponent,
}

impl AdsClient {
pub fn new(client_config: AdsClientConfig) -> Self {
#[cfg(feature = "stateful")]
let store_exists = client_config.store_config.is_some();
#[cfg(feature = "stateful")]
let worker_sleep_duration = client_config
.worker_config
.as_ref()
.and_then(|x| x.batch_interval_duration)
.unwrap_or(ADS_CLIENT_WORKER_SLEEP_LENGTH_DEFAULT);
let inner = Arc::new(AdsClientInner::new(client_config));

#[cfg(feature = "stateful")]
let worker = if store_exists {
worker::BackgroundWorker::new(inner.clone(), worker_sleep_duration)
} else {
worker::BackgroundWorker::new_empty()
};

AdsClient {
inner,
#[cfg(feature = "stateful")]
_worker: worker,
}
}

pub fn clear_cache(&self) -> Result<(), open_database::Error> {
self.inner.clear_cache()
}

#[cfg(test)]
pub fn get_context_id(&self) -> context_id::ApiResult<String> {
self.inner.get_context_id()
}

pub fn record_click(&self, click_url: Url, ohttp: bool) -> Result<(), RecordClickError> {
self.inner.record_click(click_url, ohttp)
}

pub fn record_impression(
&self,
impression_url: Url,
ohttp: bool,
) -> Result<(), RecordImpressionError> {
self.inner.record_impression(impression_url, ohttp)
}

pub fn report_ad(
&self,
report_url: Url,
reason: ReportReason,
ohttp: bool,
) -> Result<(), ReportAdError> {
self.inner.report_ad(report_url, reason, ohttp)
}

pub fn request_image_ads(
&self,
ad_placement_requests: Vec<AdPlacementRequest>,
flags: AdRequestFlags,
options: Option<CachePolicy>,
ohttp: bool,
blocks: Vec<String>,
) -> Result<HashMap<String, AdImage>, RequestAdsError> {
self.inner
.request_image_ads(ad_placement_requests, flags, options, ohttp, blocks)
}

pub fn request_spoc_ads(
&self,
ad_placement_requests: Vec<AdPlacementRequest>,
flags: AdRequestFlags,
options: Option<CachePolicy>,
ohttp: bool,
blocks: Vec<String>,
) -> Result<HashMap<String, Vec<AdSpoc>>, RequestAdsError> {
self.inner
.request_spoc_ads(ad_placement_requests, flags, options, ohttp, blocks)
}

pub fn request_tile_ads(
&self,
ad_placement_requests: Vec<AdPlacementRequest>,
flags: AdRequestFlags,
options: Option<CachePolicy>,
ohttp: bool,
blocks: Vec<String>,
relevance: AdRequestRelevance,
) -> Result<HashMap<String, AdTile>, RequestAdsError> {
self.inner.request_tile_ads(
ad_placement_requests,
flags,
options,
ohttp,
blocks,
relevance,
)
}

pub fn request_spoc_or_image_ads(
&self,
ad_placement_requests: Vec<AdPlacementRequest>,
flags: AdRequestFlags,
options: Option<CachePolicy>,
ohttp: bool,
blocks: Vec<String>,
) -> Result<HashMap<String, Vec<AdImageOrSpoc>>, RequestAdsError> {
self.inner
.request_spoc_or_image_ads(ad_placement_requests, flags, options, ohttp, blocks)
}

pub fn shutdown(&self) {
self.inner.shutdown();
}

#[cfg(test)]
fn request_ads<A>(
&self,
placements: Vec<AdPlacementRequest>,
flags: AdRequestFlags,
options: Option<CachePolicy>,
ohttp: bool,
blocks: Vec<String>,
relevance: AdRequestRelevance,
) -> Result<AdResponse<A>, RequestAdsError>
where
A: AdResponseValue,
{
self.inner
.request_ads(placements, flags, options, ohttp, blocks, relevance)
}
}

impl AdsClientInner {
pub fn new(client_config: AdsClientConfig) -> Self {
let context_id_component = ContextIDComponent::new(
&Uuid::new_v4().to_string(),
Expand Down Expand Up @@ -349,7 +494,7 @@ mod tests {
use super::*;

fn new_with_mars_client(client: MARSClient) -> AdsClient {
AdsClient {
let inner = Arc::new(AdsClientInner {
#[cfg(feature = "stateful")]
ads_store: Arc::new(Some(
AdsStoreBuilder::new("test_store.db")
Expand All @@ -363,6 +508,11 @@ mod tests {
false,
Box::new(DefaultContextIdCallback),
),
});
AdsClient {
inner: inner.clone(),
#[cfg(feature = "stateful")]
_worker: BackgroundWorker::new(inner.clone(), ADS_CLIENT_WORKER_SLEEP_LENGTH_DEFAULT),
}
}

Expand All @@ -373,6 +523,8 @@ mod tests {
environment: Environment::Test,
#[cfg(feature = "stateful")]
store_config: None,
#[cfg(feature = "stateful")]
worker_config: None,
};
let client = AdsClient::new(config);
let context_id = client.get_context_id().unwrap();
Expand Down Expand Up @@ -464,6 +616,8 @@ mod tests {
environment: Environment::Test,
#[cfg(feature = "stateful")]
store_config: None,
#[cfg(feature = "stateful")]
worker_config: None,
};
let client = AdsClient::new(config);

Expand Down Expand Up @@ -502,6 +656,8 @@ mod tests {
environment: Environment::Test,
#[cfg(feature = "stateful")]
store_config: None,
#[cfg(feature = "stateful")]
worker_config: None,
};
let client = AdsClient::new(config);

Expand Down
11 changes: 11 additions & 0 deletions components/ads-client/src/client/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,18 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/

#[cfg(feature = "stateful")]
use std::time::Duration;

use crate::mars::Environment;

pub struct AdsClientConfig {
pub cache_config: Option<AdsCacheConfig>,
pub environment: Environment,
#[cfg(feature = "stateful")]
pub store_config: Option<AdsStoreConfig>,
#[cfg(feature = "stateful")]
pub worker_config: Option<AdsWorkerConfig>,
}

#[derive(Clone, Debug)]
Expand All @@ -24,3 +29,9 @@ pub struct AdsCacheConfig {
pub struct AdsStoreConfig {
pub db_path: String,
}

#[cfg(feature = "stateful")]
#[derive(Clone, Debug)]
pub struct AdsWorkerConfig {
pub batch_interval_duration: Option<Duration>,
}
11 changes: 11 additions & 0 deletions components/ads-client/src/client/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ use crate::mars::error::{FetchAdsError, RecordClickError, RecordImpressionError,

#[derive(Debug, thiserror::Error)]
pub enum ComponentError {
#[cfg(feature = "stateful")]
#[error("Error requesting ads from worker: {0}")]
BackgroundWorker(#[from] BackgroundWorkerError),

#[error("Error recording a click for a placement: {0}")]
RecordClick(#[from] RecordClickError),

Expand All @@ -28,3 +32,10 @@ pub enum RequestAdsError {
#[error("Error requesting ads from MARS: {0}")]
FetchAds(#[from] FetchAdsError),
}

#[cfg(feature = "stateful")]
#[derive(Debug, thiserror::Error)]
pub enum BackgroundWorkerError {
#[error("Background worker is closed")]
Closed,
}
47 changes: 40 additions & 7 deletions components/ads-client/src/ffi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@ pub mod telemetry;
use crate::ads::{
AdCallbacks, AdImage, AdImageOrSpoc, AdSpoc, AdTile, SpocFrequencyCaps, SpocRanking,
};
#[cfg(feature = "stateful")]
use crate::client::config::AdsStoreConfig;
use crate::client::config::{AdsCacheConfig, AdsClientConfig};
#[cfg(feature = "stateful")]
use crate::client::config::{AdsStoreConfig, AdsWorkerConfig};
use crate::client::AdsClient;
use crate::http_cache::CachePolicy;
use crate::mars::ad_request::{
Expand All @@ -25,6 +25,7 @@ use std::collections::HashMap;
use std::sync::Arc;
#[cfg(test)]
use std::sync::Weak;
use std::time::Duration;

pub use error::{AdsClientApiResult, MozAdsClientApiError};
pub use telemetry::MozAdsTelemetry;
Expand Down Expand Up @@ -83,8 +84,11 @@ pub struct MozAdsClientBuilder(Mutex<MozAdsClientBuilderInner>);
struct MozAdsClientBuilderInner {
cache_config: Option<MozAdsCacheConfig>,
environment: Option<MozAdsEnvironment>,
#[cfg(feature = "stateful")]
store_config: Option<MozAdsStoreConfig>,
telemetry: Option<Arc<dyn MozAdsTelemetry>>,
#[cfg(feature = "stateful")]
worker_config: Option<MozAdsWorkerConfig>,
}

impl Default for MozAdsClientBuilder {
Expand All @@ -111,10 +115,13 @@ impl MozAdsClientBuilder {
environment: inner.environment.clone().unwrap_or_default().into(),
#[cfg(feature = "stateful")]
store_config: inner.store_config.clone().map(Into::into),
#[cfg(feature = "stateful")]
worker_config: inner.worker_config.clone().map(Into::into),
};
MozAdsClient {
inner: AdsClient::new(client_config),
}

let client = AdsClient::new(client_config);
let inner = Arc::new(client);
MozAdsClient { inner }
}

pub fn cache_config(self: Arc<Self>, cache_config: MozAdsCacheConfig) -> Arc<Self> {
Expand All @@ -127,13 +134,24 @@ impl MozAdsClientBuilder {
self
}

pub fn telemetry(self: Arc<Self>, telemetry: Box<dyn MozAdsTelemetry>) -> Arc<Self> {
self.0.lock().telemetry = Some(Arc::from(telemetry));
self
}
}

#[cfg(feature = "stateful")]
#[uniffi::export]
impl MozAdsClientBuilder {
#[cfg(feature = "stateful")]
pub fn store_config(self: Arc<Self>, store_config: MozAdsStoreConfig) -> Arc<Self> {
self.0.lock().store_config = Some(store_config);
self
}

pub fn telemetry(self: Arc<Self>, telemetry: Box<dyn MozAdsTelemetry>) -> Arc<Self> {
self.0.lock().telemetry = Some(Arc::from(telemetry));
#[cfg(feature = "stateful")]
pub fn worker_config(self: Arc<Self>, worker_config: MozAdsWorkerConfig) -> Arc<Self> {
self.0.lock().worker_config = Some(worker_config);
self
}
}
Expand Down Expand Up @@ -171,6 +189,12 @@ pub struct MozAdsStoreConfig {
pub db_path: String,
}

#[derive(Clone, uniffi::Record)]
pub struct MozAdsWorkerConfig {
/// Interval rate for batching commands used by the background worker.
pub batch_interval_duration: Option<Duration>,
}

#[derive(Debug, PartialEq, uniffi::Record)]
pub struct MozAdsContentCategory {
pub categories: Vec<String>,
Expand Down Expand Up @@ -463,6 +487,15 @@ impl From<MozAdsStoreConfig> for AdsStoreConfig {
}
}

#[cfg(feature = "stateful")]
impl From<MozAdsWorkerConfig> for AdsWorkerConfig {
fn from(config: MozAdsWorkerConfig) -> Self {
Self {
batch_interval_duration: config.batch_interval_duration,
}
}
}

impl From<&MozAdsPlacementRequest> for AdPlacementRequest {
fn from(request: &MozAdsPlacementRequest) -> Self {
Self {
Expand Down
Loading
Loading