diff --git a/components/ads-client/src/client.rs b/components/ads-client/src/client.rs index 8b645c5c97..737ce47ece 100644 --- a/components/ads-client/src/client.rs +++ b/components/ads-client/src/client.rs @@ -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; @@ -31,7 +32,18 @@ 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, + #[cfg(feature = "stateful")] + _worker: BackgroundWorker, +} + +pub struct AdsClientInner { #[cfg(feature = "stateful")] ads_store: Arc>, client: MARSClient, @@ -39,6 +51,139 @@ pub struct AdsClient { } 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 { + 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, + flags: AdRequestFlags, + options: Option, + ohttp: bool, + blocks: Vec, + ) -> Result, RequestAdsError> { + self.inner + .request_image_ads(ad_placement_requests, flags, options, ohttp, blocks) + } + + pub fn request_spoc_ads( + &self, + ad_placement_requests: Vec, + flags: AdRequestFlags, + options: Option, + ohttp: bool, + blocks: Vec, + ) -> Result>, RequestAdsError> { + self.inner + .request_spoc_ads(ad_placement_requests, flags, options, ohttp, blocks) + } + + pub fn request_tile_ads( + &self, + ad_placement_requests: Vec, + flags: AdRequestFlags, + options: Option, + ohttp: bool, + blocks: Vec, + relevance: AdRequestRelevance, + ) -> Result, 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, + flags: AdRequestFlags, + options: Option, + ohttp: bool, + blocks: Vec, + ) -> Result>, 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( + &self, + placements: Vec, + flags: AdRequestFlags, + options: Option, + ohttp: bool, + blocks: Vec, + relevance: AdRequestRelevance, + ) -> Result, 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(), @@ -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") @@ -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), } } @@ -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(); @@ -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); @@ -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); diff --git a/components/ads-client/src/client/config.rs b/components/ads-client/src/client/config.rs index 9567e7d7a3..8afcd989ca 100644 --- a/components/ads-client/src/client/config.rs +++ b/components/ads-client/src/client/config.rs @@ -3,6 +3,9 @@ * 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 { @@ -10,6 +13,8 @@ pub struct AdsClientConfig { pub environment: Environment, #[cfg(feature = "stateful")] pub store_config: Option, + #[cfg(feature = "stateful")] + pub worker_config: Option, } #[derive(Clone, Debug)] @@ -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, +} diff --git a/components/ads-client/src/client/error.rs b/components/ads-client/src/client/error.rs index 2542939493..d8b275f781 100644 --- a/components/ads-client/src/client/error.rs +++ b/components/ads-client/src/client/error.rs @@ -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), @@ -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, +} diff --git a/components/ads-client/src/ffi.rs b/components/ads-client/src/ffi.rs index de57508ecd..7f92a32405 100644 --- a/components/ads-client/src/ffi.rs +++ b/components/ads-client/src/ffi.rs @@ -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::{ @@ -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; @@ -83,8 +84,11 @@ pub struct MozAdsClientBuilder(Mutex); struct MozAdsClientBuilderInner { cache_config: Option, environment: Option, + #[cfg(feature = "stateful")] store_config: Option, telemetry: Option>, + #[cfg(feature = "stateful")] + worker_config: Option, } impl Default for MozAdsClientBuilder { @@ -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, cache_config: MozAdsCacheConfig) -> Arc { @@ -127,13 +134,24 @@ impl MozAdsClientBuilder { self } + pub fn telemetry(self: Arc, telemetry: Box) -> Arc { + 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, store_config: MozAdsStoreConfig) -> Arc { self.0.lock().store_config = Some(store_config); self } - pub fn telemetry(self: Arc, telemetry: Box) -> Arc { - self.0.lock().telemetry = Some(Arc::from(telemetry)); + #[cfg(feature = "stateful")] + pub fn worker_config(self: Arc, worker_config: MozAdsWorkerConfig) -> Arc { + self.0.lock().worker_config = Some(worker_config); self } } @@ -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, +} + #[derive(Debug, PartialEq, uniffi::Record)] pub struct MozAdsContentCategory { pub categories: Vec, @@ -463,6 +487,15 @@ impl From for AdsStoreConfig { } } +#[cfg(feature = "stateful")] +impl From 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 { diff --git a/components/ads-client/src/lib.rs b/components/ads-client/src/lib.rs index 5399eb611f..0f16111c24 100644 --- a/components/ads-client/src/lib.rs +++ b/components/ads-client/src/lib.rs @@ -3,7 +3,7 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -use std::collections::HashMap; +use std::{collections::HashMap, sync::Arc}; use client::error::ComponentError; use error_support::handle_error; @@ -25,6 +25,8 @@ mod mars; #[cfg(feature = "stateful")] pub mod request; pub mod telemetry; +#[cfg(feature = "stateful")] +pub mod worker; pub use ffi::*; @@ -38,10 +40,9 @@ uniffi::custom_type!(AdsClientUrl, String, { try_lift: |val| Ok(AdsClientUrl::parse(&val)?), lower: |obj| obj.as_str().to_string(), }); - #[derive(uniffi::Object)] pub struct MozAdsClient { - inner: AdsClient, + inner: Arc, } #[uniffi::export] diff --git a/components/ads-client/src/request.rs b/components/ads-client/src/request.rs index c144a8a078..8e520ebf93 100644 --- a/components/ads-client/src/request.rs +++ b/components/ads-client/src/request.rs @@ -1,16 +1,26 @@ +use parking_lot::Mutex; use url::Url; use crate::{ ads::PlacementId, mars::{ad_request::AdPlacementRequest, ReportReason}, + worker::DispatchRequest, +}; +use std::{ + collections::{HashMap, HashSet, VecDeque}, + sync::Arc, }; -use std::collections::{HashMap, HashSet, VecDeque}; pub const MAXIMUM_ADS_BATCH_COUNT: usize = 100; // Structure for storing and queuing and storing `DispatchRequest`s sent to background worker. // AdPlacementRequest will be merged together, allowing fewer network requests to be sent. +#[derive(Clone)] pub struct RequestQueue { + inner: Arc>, +} + +struct RequestQueueInner { queued_ads: HashMap, // The ordered queue of non-AdPlacementRequest commands to iterate through. @@ -19,72 +29,81 @@ pub struct RequestQueue { impl RequestQueue { pub fn new() -> RequestQueue { - RequestQueue { + let inner = RequestQueueInner { queued_ads: HashMap::new(), request_queue: VecDeque::new(), + }; + RequestQueue { + inner: Arc::new(Mutex::new(inner)), } } - pub fn clear(&mut self) { - self.request_queue = VecDeque::new(); - self.queued_ads = HashMap::new(); + pub fn clear(&self) { + let mut inner = self.inner.lock(); + inner.request_queue = VecDeque::new(); + inner.queued_ads = HashMap::new(); } // Pops the next available DispatchRequest to be run by the worker. // This will prioritize any ad_requests, batched, followed by any queued QueuedRequests (eg: RecordClick, etc) - pub fn next(&mut self) -> Option { + pub fn next(&self) -> Option { + let mut inner = self.inner.lock(); + // First, if any ad requests are queued, batch the first `MAXIMUM_ADS_BATCH_COUNT` and resolve those. - let num_ads = self.queued_ads.len().min(MAXIMUM_ADS_BATCH_COUNT); - let mut ad_requests: Vec<_> = self.queued_ads.drain().collect(); + let num_ads = inner.queued_ads.len().min(MAXIMUM_ADS_BATCH_COUNT); + let mut ad_requests: Vec<_> = inner.queued_ads.drain().collect(); if ad_requests.len() > 0 { let requeue_requests = ad_requests.split_off(num_ads); - self.queued_ads = requeue_requests.into_iter().collect(); + inner.queued_ads = requeue_requests.into_iter().collect(); return Some(DispatchRequest::RequestAds { ad_requests: ad_requests.into_iter().map(|(_, v)| v).collect(), }); } // Otherwise, pop the next queue-able command. - self.request_queue.pop_front().map(|c| c.into()) + inner.request_queue.pop_front().map(|c| c.into()) } - pub fn push_ad_request(&mut self, ad_request: AdPlacementRequest) { - self.queued_ads - .insert(ad_request.placement.clone().into(), ad_request); + // Add a `DispatchRequest` to the `RequestQueue` (primary entry point for queuing requests). + pub fn push_request(&self, request: DispatchRequest) { + // Split DispatchRequest into either a queue-able request, or a batchable ad request, and queue it. + match request { + DispatchRequest::RequestAds { ad_requests } => self.push_ad_requests(ad_requests), + DispatchRequest::RecordImpression { url } => { + self.push_queued_request(QueuedRequest::RecordImpression { url }) + } + DispatchRequest::RecordClick { url } => { + self.push_queued_request(QueuedRequest::RecordClick { url }) + } + DispatchRequest::ReportAd { url, reason } => { + self.push_queued_request(QueuedRequest::ReportAd { url, reason }) + } + } } - pub fn push_queued_request(&mut self, queued_command: QueuedRequest) { - self.request_queue.push_back(queued_command); + fn push_ad_requests(&self, ad_requests: HashSet) { + let mut inner = self.inner.lock(); + for ad_request in ad_requests { + inner + .queued_ads + .insert(ad_request.placement.clone().into(), ad_request); + } + } + + fn push_queued_request(&self, queued_command: QueuedRequest) { + let mut inner = self.inner.lock(); + inner.request_queue.push_back(queued_command); } } // Queue-able command (ReportAd, etc.) #[derive(Clone, Debug, PartialEq)] -pub enum QueuedRequest { +enum QueuedRequest { RecordClick { url: Url }, RecordImpression { url: Url }, ReportAd { reason: ReportReason, url: Url }, } -// Request dispatch enum for passing different instructions to the background worker thread. -// `RequestAds` are prefetch mechanisms that query and load data into the local cache. -#[derive(Clone, Debug, PartialEq)] -pub enum DispatchRequest { - RecordClick { - url: Url, - }, - RecordImpression { - url: Url, - }, - ReportAd { - reason: ReportReason, - url: Url, - }, - RequestAds { - ad_requests: HashSet, - }, -} - impl From for DispatchRequest { fn from(value: QueuedRequest) -> Self { match value { @@ -116,14 +135,6 @@ mod tests { } } - fn example_request_ads(identifier: usize) -> AdPlacementRequest { - AdPlacementRequest { - content: None, - count: 4, - placement: format!("test_placement_{identifier}"), - } - } - fn example_request_ads_dispatch(identifiers: &[usize]) -> DispatchRequest { let mut ad_requests = HashSet::new(); for id in identifiers { @@ -138,43 +149,48 @@ mod tests { #[test] fn identical_command_comes_out() { - let request = example_request_ads(0); + let request = example_request_ads_dispatch(&[0]); let command = example_request_ads_dispatch(&[0]); - let mut queue = RequestQueue::new(); + let queue = RequestQueue::new(); - queue.push_ad_request(request.clone()); + queue.push_request(request.clone()); let retrieved_dispatch = queue.next().expect("Request should exist in queue"); assert_eq!(command, retrieved_dispatch); } #[test] fn batch_similar_requests() { - let request_0 = example_request_ads(0); - let request_1 = example_request_ads(1); + let request_0 = example_request_ads_dispatch(&[0]); + let request_1 = example_request_ads_dispatch(&[1]); let dispatch = example_request_ads_dispatch(&[0, 1]); - let mut queue = RequestQueue::new(); + let queue = RequestQueue::new(); // Having two "placement_0" and one "placement_1" should result in returning a single DispatchRequest with one "placement_0" and one "placement_1" - queue.push_ad_request(request_0.clone()); - queue.push_ad_request(request_0.clone()); - queue.push_ad_request(request_1.clone()); + queue.push_request(request_0.clone()); + queue.push_request(request_0.clone()); + queue.push_request(request_1.clone()); let retrieved_dispatch = queue.next().expect("Request should exist in queue"); + assert!(queue.next().is_none()); - assert!(queue.queued_ads.is_empty()); - assert!(queue.request_queue.is_empty()); + + { + let inner = queue.inner.lock(); + assert!(inner.queued_ads.is_empty()); + assert!(inner.request_queue.is_empty()); + } assert_eq!(dispatch, retrieved_dispatch); } #[test] fn split_off_too_many_ads() { - let mut queue = RequestQueue::new(); + let queue = RequestQueue::new(); // Queue enough of this command to go one-over the limit. for i in 0..(MAXIMUM_ADS_BATCH_COUNT + 1) { - let request = example_request_ads(i); - queue.push_ad_request(request.clone()); + let request = example_request_ads_dispatch(&[i]); + queue.push_request(request.clone()); } let mut retrieved_dispatch_many = @@ -183,8 +199,11 @@ mod tests { queue.next().expect("Second command should exist in queue"); assert!(queue.next().is_none()); - assert!(queue.queued_ads.is_empty()); - assert!(queue.request_queue.is_empty()); + { + let inner = queue.inner.lock(); + assert!(inner.queued_ads.is_empty()); + assert!(inner.request_queue.is_empty()); + } // We do not check for a precise match with sample data here, because HashMap<..>s are unsorted, so which placement gets pushed to the next one is random. // It is also irrelevant to the MARS request. diff --git a/components/ads-client/src/worker.rs b/components/ads-client/src/worker.rs new file mode 100644 index 0000000000..f8dc2ec123 --- /dev/null +++ b/components/ads-client/src/worker.rs @@ -0,0 +1,118 @@ +use url::Url; + +use crate::{ + client::{ + error::{BackgroundWorkerError, ComponentError}, + AdsClientInner, + }, + mars::{ad_request::AdPlacementRequest, ReportReason}, + request::RequestQueue, +}; +use std::{collections::HashSet, sync::Arc, thread::JoinHandle, time::Duration}; + +pub const ADS_CLIENT_WORKER_THREAD_NAME: &str = "ads-client.worker"; + +pub struct BackgroundWorker { + // Worker thread is not needed to be accessed, but the handle needs to be held by the structure. + // If None, no worker thread is present, and attempts to write to the request queue will be rejected + // (because no consumer of the request queue will exist) + thread_handle: Option>, + request_queue: RequestQueue, +} + +impl BackgroundWorker { + pub fn new( + inner_client: Arc, + worker_sleep_duration: Duration, + ) -> BackgroundWorker { + let request_queue = RequestQueue::new(); + let worker_request_queue = request_queue.clone(); + + // Attempt to spawn a background worker thread to handle requests. + // If it fails, all fields of `BackgroundWorker` will be populated with None, and an error will be logged. + let Some(worker_thread) = std::thread::Builder::new() + .name(ADS_CLIENT_WORKER_THREAD_NAME.to_string()) + .spawn(move || crate::worker::worker(inner_client, worker_request_queue.clone(), worker_sleep_duration)).inspect_err(|err| { + // TODO: Replace this log with telemetry. + error_support::error!("Failed to create ads-client worker thread `{ADS_CLIENT_WORKER_THREAD_NAME}` with: {err}") + }).ok() + else { + return BackgroundWorker { thread_handle: None, request_queue } + }; + + BackgroundWorker { + thread_handle: Some(worker_thread), + request_queue, + } + } + + pub fn new_empty() -> BackgroundWorker { + BackgroundWorker { + thread_handle: None, + request_queue: RequestQueue::new(), + } + } + + // Add to the background worker's queue a request. It may be queued or batched, depending on the request. + pub fn dispatch(&self, request: DispatchRequest) -> Result<(), ComponentError> { + if self.thread_handle.is_some() { + self.request_queue.push_request(request); + Ok(()) + } else { + Err(BackgroundWorkerError::Closed.into()) + } + } +} + +// Endless worker for background thread that synchronously run tasks in the order provided by the RequestQueue. +fn worker( + inner_client: Arc, + request_queue: RequestQueue, + sleep_duration: Duration, +) { + loop { + // Get lock and extract next request. Lock is not held over the duration of the request but immediately dropped. + let next_request = request_queue.next(); + + if let Some(request) = next_request { + // TODO: Request should be logged via telemetry here. + let _ = request.handle_request(inner_client.clone()); + } else { + // If the queue is empty, we have a very short pause with no locks to allow it to be written to. + std::thread::sleep(sleep_duration); + } + } +} + +// Request dispatch enum for passing different instructions to the background worker thread. +// `RequestAds` are prefetch mechanisms that query and load data into the local cache. +#[derive(Clone, Debug, PartialEq)] +pub enum DispatchRequest { + RecordClick { + url: Url, + }, + RecordImpression { + url: Url, + }, + ReportAd { + url: Url, + reason: ReportReason, + }, + RequestAds { + ad_requests: HashSet, + }, +} + +impl DispatchRequest { + // Runs a dispatched command synchronously in it's thread. + // The dispatched command calls the corresponding `AdsClient` synchronous method, meaning that behavior between the two is shared. + pub fn handle_request( + self, + _ads_client_inner: Arc, + ) -> Result<(), ComponentError> { + // TODO: Add 'running a request' logic to here. + error_support::error!("Running a request is currently not set up yet: {self:?}"); + + Ok(()) + } +}