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(AWSPinpoint): Fixing a deadlock when submitting events from multiple threads #4558

Merged
merged 2 commits into from Feb 14, 2023

Conversation

ruisebas
Copy link
Member

Issue #, if available:

Description of changes:

When submitEvents is called, part of the process is to grab the currentEndpointProfile because the PutEvents call to Pinpoint requires it as an input.

Inside AWSPinpointTargetingClient.currentEndpointProfile, there's a call to AWSPinpointNotificationManager.isNotificationEnabled, which is necessary to determine if the endpoint is currently opted out of notifications or not.

isNotificationEnabled is internally calling UIApplication.isRegisteredForRemoteNotifications, but because the latter needs to be called from the main queue, it's doing a dispatch_sync(dispatch_get_main_queue()) if the current thread is not the main one.

This in isolation is not a problem, but it causes trouble when integrated to the whole event submission process, since we do a @synchronized(self.lock) right at the beginning. This will result in a deadlock:

  • A background thread calls submitEvents and gets the lock
  • The main queue calls submitEvents and waits to get the lock
  • The background thread reaches the isNotificationEnabled code, so it attempts to dispatch_sync into the main queue
  • Because the main queue is waiting for the lock to be released, it won't run.

The background thread is now blocked because it is waiting on the main queue to unblock, which un turn is waiting for the background thread to release the lock.


In order to fix this, I wanted to be the least disruptive possible since we are dealing with very old implementations.

Since submitting events is already an async operation, I decided to just introduce a new currentEndpointProfileWithCompletion: method that won't block the main queue when it needs to grab UIApplication.isRegisteredForRemoteNotifications, and call this method instead internally.

I also decoupled AWSPinpointEndpointProfile from AWSPinpointNotificationManager, so now the isRegisteredForRemoteNotifications flag needs to be provided externally when creating the profile.

Finally, I added Warning annotations to both isNotificationEnabled and currentEndpointProfile docs indicating that calling them from a non-main thread might cause deadlocks if the main queue is blocked.

Check points:

  • Added new tests to cover change, if needed
  • All unit tests pass
  • All integration tests pass
  • Updated CHANGELOG.md
  • Documentation update for the change if required
  • PR title conforms to conventional commit style

By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.

@ruisebas ruisebas requested review from a team and awsmobilesdk as code owners February 13, 2023 15:50
@@ -99,7 +99,7 @@ NS_ASSUME_NONNULL_BEGIN
An AWSPinpointEndpointProfile object with the specified context
@returns AWSPinpointEndpointProfile
*/
- (instancetype)initWithContext:(AWSPinpointContext *) context;
- (instancetype)initWithContext:(AWSPinpointContext *) context isRegisteredForRemoteNotifications:(BOOL) isRegisteredForRemoteNotifications;
Copy link
Member Author

Choose a reason for hiding this comment

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

⚠️: Since AWSPinpointContext is an internal type, this init is not externally accessible so this is not a breaking change

Copy link
Member

Choose a reason for hiding this comment

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

This is a public header right?

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah, but AWSPinpointContext is defined in the [Internal](https://github.com/aws-amplify/aws-sdk-ios/tree/main/AWSPinpoint/Internal) folder and those things can't be consumed, right?

At least on my sample app, this init never appeared as an option, and attempting to use AWSPinpointContext was greyed out because "it was not imported".

But maybe that's a Swift-only thing? If you prefer I can keep and deprecate the old method, and create a new one with the extra parameter that we use.

Copy link
Member

Choose a reason for hiding this comment

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

ah, I see what you are saying. Even here they are marked as private - https://github.com/aws-amplify/aws-sdk-ios/blob/main/AWSPinpoint.podspec#L17, I think we are good with this change.

///
/// Requests the current endpoint.
/// - Parameter completion: a block that is called with the current endpoint
- (void) currentEndpointProfileWithCompletion:(void (^_Nonnull)(AWSPinpointEndpointProfile *profile))completion;
Copy link
Member Author

@ruisebas ruisebas Feb 13, 2023

Choose a reason for hiding this comment

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

⚠️ : I did consider making this return a AWSTask instead, to be consistent with all the other async operations that exists in this client (e.g. updateEndpointProfile).

However I decided against it because AWSTasks by definition can fail, while this method should always return a valid non-null AWSPinpointEndpointProfile instance.

So it would just add an unnecessary validation/unwrapping for any customer that needed to use this method.

@ruisebas ruisebas merged commit b387a26 into main Feb 14, 2023
@ruisebas ruisebas deleted the ruisebas/fixing_pinpoint_deadlock branch February 14, 2023 16:59
gabek pushed a commit to KeepSafe/aws-sdk-ios that referenced this pull request Aug 31, 2023
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.

None yet

3 participants