Skip to content

PostReceiptDataOperation: replaced receipt base64 with hash for cache key#2199

Merged
NachoSoto merged 7 commits into
mainfrom
cache-key-receipt-uuid
Jan 12, 2023
Merged

PostReceiptDataOperation: replaced receipt base64 with hash for cache key#2199
NachoSoto merged 7 commits into
mainfrom
cache-key-receipt-uuid

Conversation

@NachoSoto

@NachoSoto NachoSoto commented Jan 5, 2023

Copy link
Copy Markdown
Contributor

Should fix #2198 and SDKONCALL-200. This is a big performance optimization in time and memory, as we no longer need to convert the entire receipt to base64 and concatenate it to the string, which is what was likely causing #2198.
This now uses Data.hashString, which avoids any data copies and efficiently creates a hash with the receipt content.

Apart from the new tests for this property, BackendPostReceiptDataTests.testDoesntCacheForDifferentReceipts ensures that different receipts have different cache keys, and therefore this new implementation still avoids collisions (other than the extremely unlikely hash collisions).

@NachoSoto NachoSoto added the perf label Jan 5, 2023
@NachoSoto NachoSoto requested a review from a team January 5, 2023 19:40
@NachoSoto NachoSoto changed the title PostReceiptDataOperation: replaced receipt base64 with hash for cache key PostReceiptDataOperation: replaced receipt base64 with hash for cache key Jan 5, 2023
Comment thread Tests/UnitTests/FoundationExtensions/NSData+RCExtensionsTests.swift Outdated
@NachoSoto NachoSoto force-pushed the cache-key-receipt-uuid branch from 0e28264 to 21f7e07 Compare January 5, 2023 20:14
expect("sample string".asData.hashString) == "99ad9154f94977dd8913f3b7ea14091d00e52b8931c2bc1cfc7ea62b7c26727b"
}

func testDataAsHashStringHashesTheEntireData() {

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.

This test failed with the original UUID implementation, which only hashed the first 16 bytes (see https://opensource.apple.com/source/CF/CF-855.17/CFUUID.c.auto.html).

@NachoSoto NachoSoto force-pushed the cache-key-receipt-uuid branch from 9692281 to 0256794 Compare January 5, 2023 20:28
}

// Returns a string representing a fetch token.
/// - Returns: `UUID` from the first 16 bytes of the underlying data.

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.

I added this documentation to specify that this doesn't encode the entire data!

Comment on lines +48 to +54
return self.withUnsafeBytes {
guard let baseAddress = $0.bindMemory(to: UInt8.self).baseAddress else {
return nil
}

return NSUUID(uuidBytes: baseAddress) as UUID
}

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.

This is also a small optimization, to ensure that we don't copy self.

@NachoSoto NachoSoto force-pushed the cache-key-receipt-uuid branch from d0619cc to 0a14bf9 Compare January 5, 2023 20:49
let cacheKey =
"""
\(configuration.appUserID)-\(postData.isRestore)-\(postData.receiptData.asFetchToken)
\(configuration.appUserID)-\(postData.isRestore)-\(postData.receiptData.hashString)

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.

✅ This is the core of this change

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

this is a fantastic solution :chefskiss:
let's add a comment here explaining that we're explicitly using the hashString since the fetchToken could be huge if there are a lot of transactions in the receipt

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.

Good idea, I added this comment:

/// Cache key comprises of the following:
/// - `appUserID`
/// - `isRestore`
/// - Receipt (`hashString` instead of `fetchToken` to avoid big receipts leading to a huge cache key)
/// - `ProductRequestData.cacheKey`
/// - `presentedOfferingIdentifier`
/// - `observerMode`
/// - `subscriberAttributesByKey`

NachoSoto added a commit that referenced this pull request Jan 10, 2023
Fixes [CSDK-622].
Depends on #2199, #2200, #2202, #2203.

- Extracted `FileReader` to be able to use it in `ReceiptParser`
- Extracted `MockBundle` to be able to use it in `ReceiptParser`
- Added new `public` `ReceiptParser.fetchAndParseLocalReceipt()`, **only to `ReceiptParser`**. This is not available through `RevenueCat`.
- Added tests for the new API
- Added new API to `ReceiptParserAPITester`

Initially I thought about moving `ReceiptFetcher` to the `Receipt|Parser` target. Unfortunately that would require moving a whole bunch of other dependencies (`OperationDispatcher`, etc, etc).
We have no easy way to create a third "Core" framework in which to place these common dependencies, so that wouldn't be trivial.

This solution adds a much simpler version of `ReceiptFetcher` that simply reads from `Bundle.appStoreReceiptURL`. As it's documented there, under normal circumstances `SKReceiptRefreshRequest` is not required, so this implementation should work for 99% of use cases, and so this way we avoid increasing the.codebase complexity unnecessarily.
…he key

Should fix #2198 and [SDKONCALL-200]. This is a big performance optimization in time and memory, as we no longer need to convert the entire receipt to base 64 and concatenate it to the string, which is what was likely causing #2198.
Now, the implementation of `Data.uuid` avoids any data copies and efficiently creates a `UUID` hash with the receipt content.

Apart from the new tests for this property, `BackendPostReceiptDataTests.testDoesntCacheForDifferentReceipts` ensures that different receipts have different cache keys, and therefore this new implementation still avoids collisions (other than the extremely unlikely hash collisions).
@NachoSoto NachoSoto force-pushed the cache-key-receipt-uuid branch from 0a14bf9 to 1aa7e19 Compare January 12, 2023 18:23
NachoSoto added a commit that referenced this pull request Jan 12, 2023
Fixes [CSDK-622].
Depends on #2199, #2200, #2202, #2203.

- Extracted `FileReader` to be able to use it in `ReceiptParser`
- Extracted `MockBundle` to be able to use it in `ReceiptParser`
- Added new `public` `ReceiptParser.fetchAndParseLocalReceipt()`, **only to `ReceiptParser`**. This is not available through `RevenueCat`.
- Added tests for the new API
- Added new API to `ReceiptParserAPITester`

Initially I thought about moving `ReceiptFetcher` to the `Receipt|Parser` target. Unfortunately that would require moving a whole bunch of other dependencies (`OperationDispatcher`, etc, etc).
We have no easy way to create a third "Core" framework in which to place these common dependencies, so that wouldn't be trivial.

This solution adds a much simpler version of `ReceiptFetcher` that simply reads from `Bundle.appStoreReceiptURL`. As it's documented there, under normal circumstances `SKReceiptRefreshRequest` is not required, so this implementation should work for 99% of use cases, and so this way we avoid increasing the.codebase complexity unnecessarily.
let cacheKey =
"""
\(configuration.appUserID)-\(postData.isRestore)-\(postData.receiptData.asFetchToken)
\(configuration.appUserID)-\(postData.isRestore)-\(postData.receiptData.hashString)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

this is a fantastic solution :chefskiss:
let's add a comment here explaining that we're explicitly using the hashString since the fetchToken could be huge if there are a lot of transactions in the receipt

@NachoSoto NachoSoto force-pushed the cache-key-receipt-uuid branch from 49e7be5 to 3204749 Compare January 12, 2023 19:29
@NachoSoto NachoSoto enabled auto-merge (squash) January 12, 2023 19:29
@NachoSoto NachoSoto merged commit 9a289e5 into main Jan 12, 2023
@NachoSoto NachoSoto deleted the cache-key-receipt-uuid branch January 12, 2023 19:48
NachoSoto pushed a commit that referenced this pull request Jan 17, 2023
**This is an automatic release.**

### New Features
* Added new `ReceiptParser.fetchAndParseLocalReceipt` (#2204) via
NachoSoto (@NachoSoto)
* `PurchasesReceiptParser`: added API to parse receipts from `base64`
string (#2200) via NachoSoto (@NachoSoto)
### Bugfixes
* `CustomerInfo`: support parsing schema version 2 to restore SDK `v3.x`
compatibility (#2213) via NachoSoto (@NachoSoto)
### Other Changes
* `JSONDecoder`: added decoding type when logging
`DecodingError.keyNotFound` (#2212) via NachoSoto (@NachoSoto)
* Added `ReceiptParserTests` (#2203) via NachoSoto (@NachoSoto)
* Deploy `PurchaseTester` for `macOS` (#2011) via NachoSoto (@NachoSoto)
* `ReceiptFetcher`: refactored implementation to log error when failing
to fetch receipt (#2202) via NachoSoto (@NachoSoto)
* `PostReceiptDataOperation`: replaced receipt `base64` with `hash` for
cache key (#2199) via NachoSoto (@NachoSoto)
* `PurchaseTester`: small refactor to simplify `Date` formatting (#2210)
via NachoSoto (@NachoSoto)
* `PurchasesReceiptParser`: improved documentation to reference
`default` (#2197) via NachoSoto (@NachoSoto)
* Created `CachingTrialOrIntroPriceEligibilityChecker` (#2007) via
NachoSoto (@NachoSoto)
* Update Gemfile.lock (#2205) via Cesar de la Vega (@vegaro)
* remove stalebot in favor of SLAs in Zendesk (#2196) via Andy Boedo
(@aboedo)
* Update fastlane-plugin-revenuecat_internal to latest version (#2194)
via Cesar de la Vega (@vegaro)
@vegaro vegaro added pr:other and removed pr:perf labels Sep 17, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

App Hang in PostReceiptDataOperation

3 participants