Skip to content

NetworkOperations are asynchronous#1261

Merged
NachoSoto merged 2 commits into
mainfrom
network-operation-async
Feb 8, 2022
Merged

NetworkOperations are asynchronous#1261
NachoSoto merged 2 commits into
mainfrom
network-operation-async

Conversation

@NachoSoto

Copy link
Copy Markdown
Contributor

Fixes https://app.shortcut.com/revenuecat/story/12888/api-calls-are-not-always-executed-in-order

Initial step towards fixing https://app.shortcut.com/revenuecat/story/12802/api-cache-too-aggressive
See also https://app.shortcut.com/revenuecat/story/9521/rewrite-httpclient-for-purchases-ios

Other changes

  • Overridden isAsynchronous to tell OperationQueue that operations run in the background
  • Manually calling KVO methods as required by Operation
  • Manually completing NetworkOperations when all work is completed

@NachoSoto NachoSoto requested review from a team and taquitos February 6, 2022 23:44
@NachoSoto NachoSoto force-pushed the network-operation-async branch 2 times, most recently from 7fcb496 to fc45d7e Compare February 6, 2022 23:57
Comment thread Purchases/Networking/Operations/NetworkOperation.swift Outdated

@joshdholtz joshdholtz left a comment

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 looks great! Just one thought/question about moving only calling self.finish in a defer as the first thing in each method 🤷‍♂️

Comment thread Purchases/Networking/Operations/NetworkOperation.swift Outdated
Comment on lines 51 to 54

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.

Can this defer move above the guard so self.finish() doesn't need to be explicitly called twice?

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.

It seems like most of the changes have two calls like this in it. I believe everything should work if we just have

defer {
  self.finish()
}

as the right thing in each method?

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.

Notice that this is inside the httpClient callback. We can't finish the operation until the HTTP request has finished, that's why we need 2 in this case (and in others). The first finish is to complete the operation in the case of an early return. Does that make sense?

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.

@NachoSoto I did a little sample with how defer works with callbacks in Playgrounds before commenting this and it seemed like the defer would still execute even after callback. But maybe different is happening here so its after to not rely on the defer before a callback 🤷‍♂️

Screen Shot 2022-02-07 at 8 02 04 PM

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.

That callback is executing synchronously and immediately, so of course it does 😅 but our API requests are asynchronous.
Try adding a DispatchQueue.async to the completion in api.

Fixes https://app.shortcut.com/revenuecat/story/12888/api-calls-are-not-always-executed-in-order.
Initial step towards fixing https://app.shortcut.com/revenuecat/story/12802/api-cache-too-aggressive.
See also https://app.shortcut.com/revenuecat/story/9521/rewrite-httpclient-for-purchases-ios.

- Overridden `isAsynchronous` to tell `OperationQueue` that operations run in the background
- Manually calling KVO methods as required by `Operation`
- Manually completing `NetworkOperation`s when all work is completed
@NachoSoto NachoSoto force-pushed the network-operation-async branch from fc45d7e to d7b4600 Compare February 7, 2022 18:56

@aboedo aboedo left a comment

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.

Looks awesome! I presume that #1249 now passes when rebased off of this PR, right?

@NachoSoto

Copy link
Copy Markdown
Contributor Author

Nope, not yet (@taquitos). This fixes a race condition in the test. Without this, the test semantics didn't really make sense cause all operations got started at once. But the caching issue is still present unfortunately, because the second GET operation gets invoked when the first one finishes (because they share the same cache key), before the POST even starts.

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

Excellent.
🚢 it!

@NachoSoto NachoSoto merged commit 0e52e22 into main Feb 8, 2022
@NachoSoto NachoSoto deleted the network-operation-async branch February 8, 2022 02:20
NachoSoto added a commit that referenced this pull request Feb 15, 2022
Fixes [sc-12802].

## Background

Before this change, the sequence illustrated by the new test `testGetsUpdatedSubscriberInfoAfterPost` (started in #1249, with a few modifications here) would fail:
- GET /subscribers/
- POST /receipts
- GET /subscribers/

#1261 fixed the race condition in the test, making it so that operations are actually run serially.
However, when the first GET request finished, the second one shared the cache key, so it received the (soon to be) outdated `CustomerInfo`.

## The fix

Unfortunately this fix is isolated for this specific issue, and not a generalized one. A more comprehensive rewrite of `HTTPClient` is needed ([sc-9521]).
I came up with several other alternatives, but what I ended up going with here involves the least amount of moving pieces.

If a `GetCustomerInfoOperation` request is enqueued with `PostReceiptDataOperation` in the queue, then a new unique cache key is created to avoid past `GetCustomerInfoOperation` to share outdated data.

If we simply added a random number to the `GetCustomerInfoOperation` cache key, the new test would have also passed. No other test verified the caching behavior, which is what the new `testGetSubscriberInfoDoesNotMakeTwoRequests` checks.
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.

4 participants