Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix dispatchSource #1344

Merged
merged 19 commits into from
Jul 2, 2024
Merged

Fix dispatchSource #1344

merged 19 commits into from
Jul 2, 2024

Conversation

richherrera
Copy link
Contributor

@richherrera richherrera commented Jun 24, 2024

Summary of changes

  • Create a class called RepeatingTimer that uses DispatchSource, moving all the logic from the BTAnalyticsService class. This way, we can reuse the same logic wherever needed. This class ensures it can be properly deallocated from memory or the implementation of pause/resume functionality. Also, RepeatingTimer aims to fix an intermittent crash.

Steps to reproduce the crash:
Xcode 15.4
iOS 17.2
Simulator iPhone 14

This branch was created where the APIClient and PaymentClient are instantiated almost at the same time, based on a potential implementation by the merchants. The steps to reproduce the crash are to tokenize and then cancel (crash is very intermittent, so you'll need to run the application multiple times and follow the tokenization process repeatedly).

Screenshot 2024-06-25 at 9 09 32 a m

Update: This branch includes the changes from the branch that reproduces the crash, as well as the changes from this branch.

Checklist

  • Added a changelog entry

Authors

jaxdesmarais and others added 4 commits June 24, 2024 10:50
@richherrera richherrera self-assigned this Jun 24, 2024
@richherrera richherrera requested a review from a team as a code owner June 24, 2024 22:00
@richherrera richherrera marked this pull request as draft June 24, 2024 22:00
@richherrera richherrera marked this pull request as ready for review June 24, 2024 22:15
Copy link
Contributor

@jaxdesmarais jaxdesmarais left a comment

Choose a reason for hiding this comment

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

Are the steps we can follow to replicate this error? Will be helpful for folks testing/reviewing this PR.

Sources/BraintreeCore/RepeatingTimer.swift Outdated Show resolved Hide resolved
case resumed
}

private var state: State = .suspended
Copy link
Contributor

Choose a reason for hiding this comment

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

Curious why we need to manage this state ourselves?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

GCD timers can be somewhat sensitive. If you try and resume/suspend an already resumed/suspended timer, you will get a crash with a reason like:

BUG IN CLIENT OF LIBDISPATCH: Over-resume of an object

This tells us we have tried to resume an already resumed timer. We just need to balance the calls to suspend and resume, like the documentation states:

As a result, you must balance each call to dispatch_suspend with a matching call to dispatch_resume before event delivery resumes.

Copy link
Contributor

Choose a reason for hiding this comment

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

Gotcha, thanks for the background here. This makes sense

Copy link
Contributor

Choose a reason for hiding this comment

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

Can we include that link & those details in a comment?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sure, added: 3be7fd8


class RepeatingTimerTests: XCTestCase {

func testTimer_resumesAndSuspends() {
Copy link
Contributor

@scannillo scannillo Jun 25, 2024

Choose a reason for hiding this comment

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

Nitpick - we try to name tests like this format: test<MethodName>_{optional when<Condition>}_<ExpectedResult>

This test might be more readable as two separate tests. For ex: testResume_callsEventHandler or testSuspend_doesNotCallEventHandler

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks for let me know, updated here: d4bb6aa

Sources/BraintreeCore/RepeatingTimer.swift Outdated Show resolved Hide resolved
/// Amount of time, in seconds.
private let timeInterval: Int

init(timeInterval: Int) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Since we've separate out this logic can we remove the timeInterval property from BTAnalyticsService and set the default in this class? That way the timer logic is fully contained here and we are only instantiating it in BTAnalyticsService.

Copy link
Contributor

Choose a reason for hiding this comment

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

Yeh, we can just hard code it to 15 in the RepeatingTimer class. BTAnalyticsService doesn't need to change the interval

Copy link
Contributor

Choose a reason for hiding this comment

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

@richherrera wanted to make sure you saw this

Copy link
Contributor Author

Choose a reason for hiding this comment

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

By removing timeInterval from class BTAnalyticsService and setting a default value in this class, we're mixing the "logic" again. Imagine we want to change the time interval from 15 seconds to 30, like in Android. I would expect to make this change in BTAnalyticsService, not RepeatingTimer. What do you think? Any ideas? What I came up with still looks like a magic number:

Suggested change
init(timeInterval: Int) {
init(timeInterval: Int = 15) {

Any suggestions on how to set the value 15 without it looking like a magic number?

Copy link
Contributor

Choose a reason for hiding this comment

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

I am not sure that I agree with this. BTAnalyticsService only responsibility should be sending events, it should not care about how often we send events at all. On the other hand the RepeatingTimer should control all time related logic including the interval of the timer. I don't think BTAnalyticsService needs to care how often events are sent only that it's sending events.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sure, I completely agree with you. I was thinking more about making this class usable in multiple places if needed, considering that in some other context, 15 seconds might be too short. But I'm happy to make the change. Do you think it would be good to put it in the initializer?

Sources/BraintreeCore/RepeatingTimer.swift Outdated Show resolved Hide resolved
Sources/BraintreeCore/RepeatingTimer.swift Outdated Show resolved Hide resolved
Sources/BraintreeCore/RepeatingTimer.swift Outdated Show resolved Hide resolved
Copy link
Contributor

@jaxdesmarais jaxdesmarais left a comment

Choose a reason for hiding this comment

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

Lets make sure to add a CHANGELOG entry for this

For more information: https://developer.apple.com/library/archive/documentation/General/Conceptual/ConcurrencyProgrammingGuide/GCDWorkQueues/GCDWorkQueues.html#//apple_ref/doc/uid/TP40008091-CH103-SW8
*/

func resume() {
Copy link
Contributor

@KunJeongPark KunJeongPark Jul 1, 2024

Choose a reason for hiding this comment

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

This PR is great work Rich. I am wondering if we want to synchronize resume and suspends as well since analytics could be sent from different threads. We could use actor which would require awaits on function calls but we could also use serial dispatch queue. wdyt?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Excellent suggestion. For now, I think DispatchSource is a task that runs periodically, so we could proceed without needing actor's. Also, based on Jax's implementation, the DispatchSource is created to run on the main thread. From what I see, the task of sending analytics can be on different threads, but both only enqueue events. The task of sending them is handled by the single instance of DispatchSource.

Copy link
Contributor

Choose a reason for hiding this comment

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

Interesting. I didn't know that this line let timerSource = DispatchSource.makeTimerSource(queue: DispatchQueue.main) ensures that the handler is run on main as well.

Copy link
Contributor

@scannillo scannillo left a comment

Choose a reason for hiding this comment

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

👏 We want to try to get this merged & released by EOW as an LE merchant is integrating w/ App Switch next week

@richherrera richherrera merged commit 5b14e06 into main Jul 2, 2024
6 of 7 checks passed
@richherrera richherrera deleted the fix-crash-dispatchSource branch July 2, 2024 14:46
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