Skip to content

Bugfix FXIOS-15847 [Homepage] Serve last known sponsored tiles while revalidating in the background#34695

Open
devzahirul wants to merge 2 commits into
mozilla-mobile:mainfrom
devzahirul:fxios-15847-sponsored-tiles-stale-while-revalidate
Open

Bugfix FXIOS-15847 [Homepage] Serve last known sponsored tiles while revalidating in the background#34695
devzahirul wants to merge 2 commits into
mozilla-mobile:mainfrom
devzahirul:fxios-15847-sponsored-tiles-stale-while-revalidate

Conversation

@devzahirul

Copy link
Copy Markdown
Contributor

📜 Tickets

Jira ticket
Github issue

💡 Description

This implements the sponsored-shortcuts part of #33899 — "Use stale sponsored shortcuts … while refreshing the cache in the background" — scoped entirely to UnifiedAdsProvider, so it does not change how or when the homepage renders.

Why the shortcuts section stalls on slow networks

  • MozAdsClient.requestTileAds is a blocking FFI call, and the ads client's HTTP cache hard-expires entries (default 30 minutes): delete_expired_entries() runs before every lookup, so once the TTL passes the client must go to the network — the previous response is unreadable at that point.
  • TopSitesMiddleware.getTopSitesDataAndUpdateState dispatches a single retrievedUpdatedSites action that awaits both getOtherSites() (local, milliseconds) and fetchSponsoredSites(). With an expired ad cache on a slow network, the entire shortcuts section (pinned + history + Google tile — all local data) waits on the sponsored request until the 3-second timeout added in FXIOS-16077.
  • When that timeout fires, two more things go wrong: the late result is discarded, so that homepage build shows no sponsored tiles at all; and the blocking call keeps occupying a Swift Concurrency cooperative-pool thread until the request actually finishes, since fetchTiles ran it synchronously on the calling task's thread (the same class of problem as FXIOS-16156 / FXIOS-16307).
  • The legacy provider could serve its cached POST response via URLCache within maxCacheAge (30 min) without touching the network. That ability was lost in the Rust ads client migration (FXIOS-14580; legacy path removed with the flag in FXIOS-15992) — the timestamp parameter on fetchTiles has been vestigial since then.

What this PR changes (all inside UnifiedAdsProvider)

  1. The blocking requestTileAds call now runs on a dedicated dispatch queue instead of the caller's thread, mirroring DefaultUnifiedAdsCallbackTelemetry (FXIOS-15787) and the merino WC fetcher fix (FXIOS-16156).
  2. The provider keeps the last successfully fetched tiles in memory and, while they are at most maxTileStaleness (1 hour) old, completes immediately with them and revalidates in the background. After the first successful fetch of a session, a homepage build never waits on the ad network again. Concurrent revalidations are coalesced.
  3. Results arriving after the caller stopped waiting (timeout) are cached, so the next homepage build gets tiles instantly instead of fetching again.

Scope / behavior notes

Testing

UnifiedAdsProviderTests covers every branch of the new behavior deterministically (no sleeps, timestamps injected): serving within / at / beyond the staleness bound, revalidation updating the cache, failed revalidation keeping the previous tiles, empty responses being cached, revalidation coalescing while one is in flight, and a late-arriving result being served on the next fetch. Existing tests were updated for the queue-based completion with explicit expectations. SwiftLint passes on the changed files; unit tests run in CI.

🎥 Demos

No visual changes — the shortcuts section simply appears without waiting on the ads request when tiles were fetched within the last hour.

📝 Checklist

  • I filled in the ticket numbers and a description of my work
  • I updated the PR name to follow our PR naming guidelines
  • I ensured unit tests pass and wrote tests for new code
  • If working on UI, I checked and implemented accessibility (Dynamic Text and VoiceOver)
  • If adding telemetry, I read the data stewardship requirements and will request a data review
  • If adding or modifying strings, I read the guidelines and will request a string review from l10n
  • If needed, I updated documentation and added comments to complex code

…revalidating in the background

When the ads client cache expires (every 30 minutes), requestTileAds
performs a blocking network request. The homepage top sites section
waits on that request (up to the 3s timeout) before presenting any
shortcuts, and a timed-out request drops sponsored tiles from that
homepage build entirely while still tying up a Swift Concurrency
cooperative thread until the request finishes.

UnifiedAdsProvider now keeps the last successfully fetched tiles in
memory and serves them immediately for up to an hour while a refresh
runs on a dedicated dispatch queue. Results that arrive after the
caller stopped waiting are cached for the next homepage build, and
concurrent revalidations are coalesced.

@lmarceau lmarceau left a comment

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.

Throwing in some thoughts in there 👀 Let me know what you think Roux & Matt!

}

final class UnifiedAdsProvider: UnifiedAdsProviderInterface, Sendable {
final class UnifiedAdsProvider: UnifiedAdsProviderInterface, @unchecked Sendable {

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.

UnifiedAdsProvider is currently using completion handlers since it was introduced at the time as a "let's throw in something quickly to support ads", but it might be worth rethinking it as a whole so we can avoid introducing locking mechanism and support modern concurrency instead.

@@ -43,10 +61,15 @@ final class UnifiedAdsProvider: UnifiedAdsProviderInterface, Sendable {

init(
adsClientFactory: MozAdsClientFactory = DefaultMozAdsClientFactory(),

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.

TBH this whole situation looks like we should have this living under the MozAdsClient instead of the Client level UnifiedAdsProvider, what do you think Matt/Roux?

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.

Yeah, I'm looking into this right now. This whole thing might need a better refactor as it was meant to be temporary.

…ait in UnifiedAdsProvider

Replaces the completion-handler + NSLock design flagged in review with a
modern-concurrency one: UnifiedAdsProvider is now an actor with an async
fetchTiles, so the in-memory tile cache is protected by actor isolation
instead of a lock and @unchecked Sendable is narrowed to a small box around
the thread-safe Rust ads client. The blocking requestTileAds FFI call still
runs on a dedicated queue so it never occupies a cooperative-pool thread.

Adapts the single caller (TopSitesManager) to await the async API while
keeping the return-early-on-timeout, cache-late-results behavior, and
updates MockUnifiedAdsProvider and the provider tests to the async API.
@devzahirul

Copy link
Copy Markdown
Contributor Author

@lmarceau @adudenamedruby Thanks both 🙏 (replying here since I couldn't reply in-thread)

I've reworked UnifiedAdsProvider to drop the NSLock + completion-handler in favour of modern concurrency: it's now an actor with an async fetchTiles, so the in-memory tile cache is protected by actor isolation instead of a lock, and @unchecked Sendable is narrowed to a tiny box around the thread-safe Rust ads client rather than the whole type. The blocking requestTileAds call still runs on the dedicated queue so it never ties up a Swift Concurrency cooperative thread.

I kept it deliberately self-contained and easy to delete or fold into a MozAdsClient-level refactor — so if moving this down to the ads client is the direction you'd rather take, @adudenamedruby, I'm very happy to park this or rework it to fit whatever you land on. The change is pushed.

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.

3 participants