Skip to content

ProductsFetcherSK2: removed now redundant caching logic#1908

Merged
NachoSoto merged 1 commit into
mainfrom
sk2-products-request-caching-remove-cache
Sep 28, 2022
Merged

ProductsFetcherSK2: removed now redundant caching logic#1908
NachoSoto merged 1 commit into
mainfrom
sk2-products-request-caching-remove-cache

Conversation

@NachoSoto

Copy link
Copy Markdown
Contributor

Follow up to #1907.

This logic is now implemented and shared in CachingProductsManager.

I've decided to keep the logic in ProductsFetcherSK1 because it's intertwined with the retry mechanism, and it wasn't trivial to remove caching while leaving that logic.

@NachoSoto NachoSoto requested a review from a team September 15, 2022 16:51
@NachoSoto NachoSoto force-pushed the sk2-products-request-caching-remove-cache branch from abbc573 to dd5caae Compare September 15, 2022 17:00
@NachoSoto NachoSoto force-pushed the sk2-product-request-caching branch from 682dfee to fde5fc4 Compare September 15, 2022 17:12
@NachoSoto NachoSoto force-pushed the sk2-products-request-caching-remove-cache branch 2 times, most recently from b491ce5 to 6b164ce Compare September 15, 2022 17:44

@tonidero tonidero 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.

Looks good!

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 guess we want to remove the caching from the ProductsFetcherSK1 in the future, since we are now caching it in the CachingProductsManager. Did we create a Jira task for it?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yeah that's why I wrote this:

I've decided to keep the logic in ProductsFetcherSK1 because it's intertwined with the retry mechanism, and it wasn't trivial to remove caching while leaving that logic.

We could file a Jira to do it in the future, but I wonder if it's worth spending the time on that if SK1 is going away eventually /cc @aboedo thoughts?

@NachoSoto NachoSoto force-pushed the sk2-product-request-caching branch from fde5fc4 to 6eef764 Compare September 28, 2022 15:58
@NachoSoto NachoSoto force-pushed the sk2-products-request-caching-remove-cache branch from 6b164ce to 4b26689 Compare September 28, 2022 16:03
Base automatically changed from sk2-product-request-caching to main September 28, 2022 16:23
NachoSoto added a commit that referenced this pull request Sep 28, 2022
…when fetching products (#1907)

Fixes [SDKONCALL-129].

### Goal

The main feature that this change introduces is providing a _request_
cache to `ProductsFetcherSK2` (like `ProductsFetcherSK1` has).
It already implements a _product_ cache, but concurrent requests for the
same products result in fetching those multiple times. It does not cache
products until they're done being fetched.

This was observed in the logs when doing something simple like
purchasing a product. This is running `testCanPurchaseProduct`
integration test:
> DEBUG: ℹ️ No existing products cached, starting store products request
for: ["com.revenuecat.monthly_4.99.1_week_intro",
"com.revenuecat.annual_39.99.2_week_intro",
"com.revenuecat.weekly_1.99.3_day_intro"]
DEBUG: ℹ️ No existing products cached, starting store products request
for: ["com.revenuecat.monthly_4.99.1_week_intro",
"com.revenuecat.annual_39.99.2_week_intro",
"com.revenuecat.weekly_1.99.3_day_intro"]
DEBUG: 😻 Store products request request received response
DEBUG: ℹ️ Store products request finished
DEBUG: 😻 Store products request request received response
DEBUG: ℹ️ Store products request finished

With this PR, we avoid the double request due to that race condition:

> DEBUG: ℹ️ No existing products cached, starting store products request
for: ["com.revenuecat.annual_39.99.2_week_intro",
"com.revenuecat.weekly_1.99.3_day_intro",
"com.revenuecat.monthly_4.99.1_week_intro"]
DEBUG: 😻 Store products request request received response
DEBUG: ℹ️ Store products request finished

### How

Instead of implementing that logic _only_ on `ProductsFetcherSK2` or in
`ProductsManager`, this aims to keep the implementation simple in
`ProductsManager` and each of the fetchers, and instead uses the
[decorator pattern](https://en.wikipedia.org/wiki/Decorator_pattern) to
provide that new functionality in a way that's simple to test and
maintain, independent of the logic for actually fetching products.

To accomplish that, this adds a new `protocol`: `ProductsManagerType`,
with 2 main methods (more about why the need for an SK2 special method
below):
```swift
/// Fetches the ``StoreProduct``s with the given identifiers
/// The returned products will be SK1 or SK2 backed depending on the implementation and configuration.
func products(withIdentifiers identifiers: Set<String>, completion: @escaping Completion)

/// Fetches the `SK2StoreProduct`s with the given identifiers.
@available(iOS 15.0, tvOS 15.0, watchOS 8.0, macOS 12.0, *)
func sk2Products(withIdentifiers identifiers: Set<String>, completion: @escaping SK2Completion)
```

Those are only completion-block APIs. To avoid repetition, an
`extension` on the `protocol` itself provides an `async` API, wrapping
on top of these 2 methods, regardless of which `ProductsManagerType`
they're being used on.

The hierarchy is now as follows:
- `CachingProductsManager`
    - `ProductsManager`
        - `ProductsFetcherSK1`
        - `ProductsFetcherSK2`

#1908 gets rid of the now duplicate caching logic in `ProductsManager`
and the fetchers.

### Other notes

What makes the implementation a bit more convoluted is the need for a
special SK2 method.
This is necessary specifically for `TrialOrIntroPriceEligibilityChecker`
which requires fetching only SK2 products regardless of
`StoreKit2Setting`. In order to guarantee this invariant at compile time
through types, a separate cache is maintained with `SK2StoreProduct`s.
However, the main caching logic is *not* duplicated and instead shared
for both.

The main tests for `CachingProductsManager` are simple unit tests that
don't make use of `StoreKitConfigTestCase`.
Because creating SK2 products does require that, I added a separate test
class for those.

### Future changes

The caching logic is very simple, just like what we had for the
fetchers. One missing piece is that if products A and B are requested,
and only A is in the cache, this is treated as a complete cache miss.

The fetchers had the same problem, but now that this caching logic is
isolated and much easier to test, we can evolve and solve that feature
in a more maintainable way.

[SDKONCALL-129]:
https://revenuecats.atlassian.net/browse/SDKONCALL-129?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
This is now implemented and shared in `CachingProductsManager`.

I've decided to keep the logic in `ProductsFetcherSK1` because its intertwined with a retry mechanism, and it wasn't trivial to remove caching while leaving that logic.
@NachoSoto NachoSoto force-pushed the sk2-products-request-caching-remove-cache branch from 4b26689 to 5a56d7c Compare September 28, 2022 16:24
@NachoSoto NachoSoto merged commit 5666eed into main Sep 28, 2022
@NachoSoto NachoSoto deleted the sk2-products-request-caching-remove-cache branch September 28, 2022 16:44
NachoSoto pushed a commit that referenced this pull request Sep 28, 2022
**This is an automatic release.**

### Other Changes
* `ProductsFetcherSK2`: removed now redundant caching logic (#1908) via
NachoSoto (@NachoSoto)
* Created `CachingProductsManager` to provide consistent caching logic
when fetching products (#1907) via NachoSoto (@NachoSoto)
* Refactored `ReceiptFetcher.receiptData` (#1941) via NachoSoto
(@NachoSoto)
* Abstracted conversion from `async` to completion-block APIs (#1943)
via NachoSoto (@NachoSoto)
* Moved `InAppPurchase` into `AppleReceipt` (#1942) via NachoSoto
(@NachoSoto)
* `Purchases+async`: combined `@available` statements into a single one
(#1944) via NachoSoto (@NachoSoto)
* `Integration Tests`: don't initialize `Purchases` until the
`SKTestSession` has been re-created (#1946) via NachoSoto (@NachoSoto)
* `PostReceiptDataOperation`: print receipt data if `debug` logs are
enabled (#1940) via NachoSoto (@NachoSoto)

Co-authored-by: RCGitBot <dev+RCGitBot@revenuecat.com>
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