braintree / braintree_ios

Braintree SDK for iOS
https://developer.paypal.com/braintree/docs/start/hello-client/ios/v5
MIT License
557 stars 291 forks source link

Prevent duplicate `v1/configuration` GET requests #1363

Closed scannillo closed 1 month ago

scannillo commented 1 month ago

Summary

See Victoria's original PR https://github.com/braintree/braintree_ios/pull/1308 for full description and background.

Short summary: our SDK would trigger multiple of the same outbound API request to GET v1/configuration if a merchant instantiated their BTAPIClient and also called tokenize() all upon button click. This is a common pattern for merchants, since nowhere do our docs recommend otherwise.

Goal: Prevent duplicate network requests to GET v1/configuration if one is already pending over the network.

Changes

Manual Testing

Checklist

Authors

@scannillo

KunJeongPark commented 1 month ago

Great job Sammy!

KunJeongPark commented 1 month ago

One last thing for future improvement is to make array handling thread safe by using locks or DispatchQueues. Also managing cache read and writes for possible race conditions. That was original reason for me using dispatch queue with barrier.

I've also thought about adding syncing in ConfigurationCache functions as well since it is possible to have multiple instances of ConfigurationLoader and BTAPIClient. I have no idea if this is so unlikely as to be negligible.

scannillo commented 1 month ago

One last thing for future improvement is to make array handling thread safe by using locks or DispatchQueues.

Yeh, this is another good call. I'm thinking of using an Actor like we did here for our analytics events. This will be easier if everything is in async-await syntax. I might make that change in another PR, and Draft this one for now.

scannillo commented 1 month ago

Ok, I trialed several ways of making the array of completions thread-safe.

1st I tried actors, similar to BTAnalyticsEventsStorage. The problem with this is needing to call all actor methods with await. This means all callsites must either be in a Task block, or in an async function. Wrapping all callsites in Task blocks borks our test suite since there is no way for the test to know when a Task block completed. There are ways around this, but it got messy and it didn't feel worth it.

Converting our ConfigurationLoader.fetchConfig method to only use async-await style syntax was another thing I tried, but we can't store references to the async Task to call at a later point, like we can with completions. There might be a way to do this I'm not aware of.

Lastly, I ended up going with the tried and true DispatchQueue for ensuring all array operations are serial.