Skip to content

AB#292770 fix: give configuration networking a cache large enough to hold the tenant config - #147

Open
eligutovsky wants to merge 2 commits into
masterfrom
fix/292770-config-cache-revalidation
Open

AB#292770 fix: give configuration networking a cache large enough to hold the tenant config#147
eligutovsky wants to merge 2 commits into
masterfrom
fix/292770-config-cache-revalidation

Conversation

@eligutovsky

@eligutovsky eligutovsky commented Aug 5, 2026

Copy link
Copy Markdown
Member

Description of Changes

The tenant configuration is downloaded in full on every launch instead of revalidating.
Roughly 500 KB–1 MB decoded, every time.

Root cause: URLSession's automatic caching refuses to store a response larger than
roughly 5% of the cache capacity. URLCache.shared has a 10 MB disk capacity — a ~500 KB
ceiling — and the tenant configuration exceeds it. So it was never stored, no validator was
ever kept, and no conditional request was ever sent. Nothing could return 304, because
nothing ever asked.

Measured against a local server serving ETag + Cache-Control: max-age=300, driving
URLSession's own caching rather than storeCachedResponse:

Cache disk capacity Response Stored? Second fetch
10 MB (= URLCache.shared) 944 KB no hits the network
20 MB 944 KB yes served from cache, no request
10 MB 109 B yes served from cache

944 KB is 9.4% of 10 MB and 4.5% of 20 MB, consistent with the ~5% rule.

Fix: configuration networking gets its own URLCache with a 20 MB disk capacity, set on
the URLSessionConfiguration passed to NetworkClientImpl. Scoped to
ServiceLocator.networkingFactory(), which has a single call site feeding only
createRemoteConfigurationNetworking(), so no other networking changes behaviour.
Deliberately not URLCache.shared — that belongs to the host app. static, because the
factory returns a new instance per call and the cache has to outlive them.

The origin does support revalidation

Worth recording, since it was the main open question. GCS varies the ETag by representation,
and conditional requests succeed when the client sends the ETag for the encoding it accepts:

Accept-Encoding If-None-Match Result
none W/"fb65…" (weak, as served) 304
none "fb65…" (strong) 200
gzip W/"fb65…" (weak) 200
gzip "fb65…" (strong, as served) 304

URLSession sends Accept-Encoding: gzip and would cache the gzip representation together
with its strong ETag, which is the working row. So raising the capacity is sufficient — no
SDK-side ETag handling is needed.

(Measured on the global config, sdk-cdn.optimove.net, which is publicly reachable. The
tenant config needs a token, so it could not be measured directly, but both are served by
the same bucket and the handoff's captured tenant headers match this shape.)

Verification

Full suite: 77 tests, 0 failures (75 before, plus the two added here).

The new tests were mutation-checked — reverting the factory to URLCache.shared fails both:

XCTAssertGreaterThanOrEqual failed: ("10000000") is less than ("20971520")
  - URLSession would refuse to store the tenant configuration in a cache this small
XCTAssertFalse failed
  - Configuration networking must not use the host app's shared cache

Two things deliberately not changed

No explicit 304 handling in NetworkClientImpl. A bare 304 does reach the completion
handler — confirmed with a server returning 304 unconditionally: status=304 bodyBytes=0 error=nil, which falls to the default: case and becomes a success with an empty body, so
JSONDecoder throws. But it only arises if an origin answers 304 to a non-conditional
request; when there is a cache entry, URLSession replays the cached bytes and the client sees
200. And the consequence is already benign: TenantConfigurationDownloader logs the error
and returns without calling saveTenant, so the previously stored configuration is retained
— which is what "not modified" should mean. A 304 also implies a prior successful fetch, so
there is always a stored config to fall back on. Adding handling would change the log line,
not the behaviour, so it is left out of this PR.

ServiceLocator.networking() and networkClient() untouched. They build their own
NetworkClientImpl for other purposes and keep the default shared cache.

Breaking Changes

  • None

The SDK now keeps up to 20 MB of configuration cache on disk, in its own
com.optimove.configuration-cache directory rather than in the host app's shared cache.

Release Checklist

Prepare:

  • Detail any breaking changes. Breaking changes require a new major version number — none
  • Check pod lib lint passes
  • Update any relevant sections of the repository wiki pages on a branch — n/a, no public API change

Bump versions in:

Bumped to 6.8.2. 6.8.1 is already claimed by the pending stopDispatchTimer fix
(#144), so this takes the next patch number rather than colliding with it in the changelog.
If this merges before #144, 6.8.1 is simply skipped.

  • OptimoveCore.podspec
  • OptimoveNotificationServiceExtension.podspec
  • OptimoveSDK.podspec
  • OptimoveCore/Sources/Classes/Constants/SDKVersion.swift
  • README.md — n/a, contains no version reference
  • CHANGELOG.md

Integration tests

Unit tests pass (77, 0 failures). The end-to-end check worth doing on a real tenant: capture
traffic across two launches within the 5 minute max-age and confirm the tenant config is
not refetched, then again after it expires and confirm a 304 rather than a 200.

T&T Only

  • Init SDK with only T&T credentials
  • Track events

Mobile Only

  • Init SDK with all credentials
  • Track events
  • Register for push

Release:

  • Squash and merge to master
  • Delete branch once merged

…enant config

The tenant configuration was downloaded in full on every launch instead of
revalidating.

URLSession's automatic caching refuses to store a response larger than roughly 5%
of the cache capacity. URLCache.shared has a 10 MB disk capacity, i.e. a ~500 KB
ceiling, and the tenant configuration decodes to more than that. It was therefore
never stored, no validator was ever kept, and no conditional request was ever
sent, so nothing could return 304.

Measured against a local server with ETag and Cache-Control, using URLSession's
own caching rather than storeCachedResponse:

  10 MB cache, 944 KB response -> not stored, second fetch hits the network
  20 MB cache, 944 KB response -> stored, second fetch served from cache
  10 MB cache, 109 B response  -> stored

Configuration networking now gets its own URLCache with a 20 MB disk capacity.
Scoped to that one factory, which has a single call site feeding only
createRemoteConfigurationNetworking(), and deliberately not URLCache.shared, which
belongs to the host app. It is static because the factory returns a new instance
per call.

The origin was verified to support revalidation correctly, so the fix is enough to
produce 304s. GCS varies the ETag by representation, and conditional requests
succeed when the client sends the ETag of the encoding it accepts:

  Accept-Encoding: none, If-None-Match: W/"..." -> 304
  Accept-Encoding: gzip, If-None-Match: "..."   -> 304
  mismatched pairs                              -> 200

URLSession accepts gzip and would cache the gzip representation's strong ETag, so
it lands in the working case.
Copilot AI review requested due to automatic review settings August 5, 2026 10:41

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

This PR addresses repeated full downloads of the tenant configuration by ensuring URLSession’s automatic caching can actually store the (large) configuration response, enabling proper cache reuse/revalidation across launches.

Changes:

  • Introduces a dedicated URLCache for configuration networking with increased disk capacity, and wires it into the URLSessionConfiguration used by NetworkClientImpl in ServiceLocator.networkingFactory().
  • Adds unit tests to assert the configuration cache is large enough and is not URLCache.shared.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

File Description
OptimoveSDK/Tests/Sources/Network/ConfigurationURLCacheTests.swift Adds tests validating cache sizing and isolation from the host app’s shared cache.
OptimoveSDK/Sources/Classes/Services/ServiceLocator.swift Creates and injects a dedicated configuration URLCache into the URL session used for remote configuration downloads.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +126 to +130
/// URLSession will not store a response larger than roughly 5% of its cache
/// capacity. `URLCache.shared` has a 10 MB disk capacity, i.e. a ~500 KB ceiling,
/// and the tenant configuration decodes to more than that. It was therefore never
/// cached, so no validator was ever kept, no conditional request was ever sent,
/// and the whole file was downloaded again on every launch. A dedicated cache with
6.8.1 is already claimed by the pending stopDispatchTimer fix (PR #144), so this
takes the next patch number rather than colliding with it in the changelog.
Copilot AI review requested due to automatic review settings August 5, 2026 10:51

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated no new comments.

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