Skip to content

AC-220: Complete request implementation for stateful client - #7758

Open
thesuzerain wants to merge 4 commits into
mainfrom
Ads-client-AC-220-completes-request-background-worker
Open

thesuzerain wants to merge 4 commits into
mainfrom
Ads-client-AC-220-completes-request-background-worker

Conversation

@thesuzerain

@thesuzerain thesuzerain commented Nov 10, 2026 •

Copy link
Copy Markdown
Collaborator

Allows background worker to successfully run + insert into the AdsStore.

This was tested with a 'full' FFI implementation, but I extracted these components to reduce PR load.

Not included/TODO:

  • Comments about infallibility of memory database: to wait until we have more information about this.
  • FFI layer: next PR after this.
  • Telemetry: also a subsequent PR (to do telemetry for adsstore)
  • Comments about MARS options: this PR is alongside this one. Whichever one merges second will handle where they intersect.

No new tests, but integration tests in FFI layer have been run and will be included in FFI layer PR.

Pull Request checklist

  • Breaking changes: This PR follows our breaking change policy
    • This PR follows the breaking change policy:
      • This PR has no breaking API changes, or
      • There are corresponding PRs for our consumer applications that resolve the breaking changes and have been approved
  • Quality: This PR builds and tests run cleanly
    • Note:
      • For changes that need extra cross-platform testing, consider adding [ci full] to the PR title.
      • If this pull request includes a breaking change, consider cutting a new release after merging.
  • Tests: This PR includes thorough tests or an explanation of why it does not
  • Changelog: This PR includes a changelog entry in CHANGELOG.md or an explanation of why it does not need one
    • Any breaking changes to Swift or Kotlin binding APIs are noted explicitly
  • Dependencies: This PR follows our dependency management guidelines
    • Any new dependencies are accompanied by a summary of the due diligence applied in selecting them.

@thesuzerain thesuzerain changed the title fix: Extracted AC-220 from FFI full PR AC-220: Complete request implementation for stateful client Nov 10, 2026
error_support::error!("Running a request is currently not set up yet: {self:?}");

match self {
// TODO: All of the MARS options are defaulted, as the AdsClient options refactor is required first.

@thesuzerain thesuzerain Nov 10, 2026 •

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.

This PR will conflict with this PR.

Whichever one gets merged second will have the MARSRequestOptions properly passed through here.

#[cfg(feature = "stateful")]
#[derive(Clone, Debug, Default)]
pub struct WorkerConfig {
pub worker_batch: Option<Duration>,

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.

This refers to worker batch duration (eg: batch for 10 seconds). I'll have more clear documentation on the Moz-public struct version of this, matching the existing pattern for these.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I think it's too early for that. As a V1 I don't think we'd do batching yet?

@thesuzerain thesuzerain Nov 13, 2026 •

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 agree, but this isn't publically exposed or used yet.

Most of the underlying structure already exists + fleshed out on this (eg: see discussions about RequestQueue here or here), from before we decided to be more agile about this (develop end to end first). So this config provides the field to the BackgroundWorker::new() that already exists.

If you prefer I can create a version (eg: enum variant, or if a boolean or None is passed in) whereRequestQueue exists but does not batch? That way we can ensure no batching happens in V1 (I think I'd do that in a separate PR though).

@thesuzerain
thesuzerain marked this pull request as ready for review November 10, 2026 23:45
@thesuzerain
thesuzerain requested a review from a team as a code owner November 10, 2026 23:45
@thesuzerain
thesuzerain requested review from Almaju and jonesetc and removed request for a team November 10, 2026 23:45
DispatchRequest::RequestAds { ad_requests } => {
ads_client_inner.prefetch_ads_request(
ad_requests.into_iter().collect(),
MARSRequestOptions::default(),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

worker should not know about MARS specific things
what is MARS related should stay with MARS
if we need request options here then this means that's a domain struct so no need to prefix it with MARS

Comment on lines 28 to 35
pub fn builder<P: AsRef<Path>>(db_path: P) -> AdsStoreBuilder {
AdsStoreBuilder::new(db_path.as_ref())
AdsStoreBuilder::new(Some(db_path.as_ref()))
}

pub fn builder_memory() -> AdsStoreBuilder {
AdsStoreBuilder::new(None::<String>)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I feel that this defeat the whole point of builders, you now have two constructors for two different cases. The builder is meant for that?
AdStore::builder().database(path).build()

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.

The reasoning for my choice here to do builder_memory was that we always expect a path to be provided- it generally isn't a configurable option (I remember pushback against having path=None be available as an option, and I think also pushback against something like in_memory() be a builder function available to create a memory database). We obviously need to be able to create a in-memory database 'on purpose' if there's no config passed.

I liked this setup because, as a user, have to provide a path with builder or explicitly say builder_memory() (it's a fully different kind of database, and you would need to think about it).

I'm totally fine with doing it the way you've suggested, if we are fine that AdStore::builder().build() makes an in-memory database. I can make that change along with the one below.


pub fn store_ads(&self, ads: HashMap<PlacementId, Vec<Ad>>) -> Result<(), FetchAdsError> {
for (placement_id, ad) in ads {
self.holder.store_ad(&placement_id, &ad)?;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

we cannot do multi insert in sqlite? 🤔

@thesuzerain thesuzerain Nov 13, 2026 •

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.

Our module currently doesn't have that ability- it uses the same logic the HTTPCache implementation and has a single insertion method. It definitely can though, I'll do that separately.

AdsStore::builder_memory()
.build()
.inspect_err(telemetry::record_ads_store_error)
.ok()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

didn't we already have that logic in the AdsStore builder?
Also this is not a concern for the client: the client needs an AdsStore, it doesnn't care how it was made, that's a technical concern for the AdsStore itself.

@thesuzerain thesuzerain Nov 13, 2026 •

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.

This is different behavior. This is for if "no store config exists at all" (eg: no entry in AdsClientConfig), not if it fails. We don't want to force store config to be passed as that's a breaking change.

Similar to what is being done here with .max_size(max_cache_size) and .default_ttl(default_cache_ttl), but falling back to a specific db if we don't have a config. It looks like the other fallback functions because we don't have any builder functions like max_size on the AdsStore yet.

Also this is not a concern for the client: the client needs an AdsStore, it doesnn't care how it was made, that's a technical concern for the AdsStore itself.

I'm not sure I agree. That's true for any kind of fallback mechanism- that should be kept in AdsClient but this is how to handle no proper entry in the AdsClientConfig. "If there's no config, do this instead. "

We could refactor the HttpCacheBuilder to not be a builder, and instead take a singular AdsCacheConfig field, if we are concerned about the logic being here, but I do quite like the builder setup.

If you prefer, I can change the pattern for both to look something like this (example given with HttpCache, not tidied up):

let default_cache_ttl = client_config.store_config.

        // This requires `builder` to be able to accept `None` for a memory database.
        let path = cache_cfg.map(|x| db_path).clone();
        
        // No cache config is passed, but we make it anyway with default values.
        let cache_cfg = client_config.cache_config.unwrap_or_default();
            let default_cache_ttl = Duration::from_secs(
                cache_cfg
                    .default_cache_ttl_seconds
                    .unwrap_or(DEFAULT_TTL_SECONDS),
            );

        let max_cache_size =
        ByteSize::mib(cache_cfg.max_size_mib.unwrap_or(DEFAULT_MAX_CACHE_SIZE_MIB));

        let mut http_cache = HttpCache::builder(cache_cfg.db_path);

       // Pass database path if exists, otherwise, if no config was created, we fall back to a memory database.
         if let Some(path) = path {
             http_cache = http_cache.database(path);
         }
          http_cache = http_cache.max_size(max_cache_size)
                    .default_ttl(default_cache_ttl)
                    .build()
                    .inspect_err(telemetry::record_build_cache_error)
                    .ok();

(Or similarly, but having builder be able to accept None as a short-hand for a memory path, or unwrap_or to :memory: here)

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants