Skip to content

Deadlock when saving objects with circular references #916

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

Closed
czgarrett opened this issue May 8, 2016 · 14 comments
Closed

Deadlock when saving objects with circular references #916

czgarrett opened this issue May 8, 2016 · 14 comments
Labels
type:bug Impaired feature or lacking behavior that is likely assumed
Milestone

Comments

@czgarrett
Copy link

I have two PFObject subclasses A and B. A has an array of pointers to B, and B has a pointer to A.

Sometimes my app will fire off two save operations for A in rapid succession, and this will result in a deadlock. Here's where the deadlock occurs when saving an instance of A that references two B instances:

On thread 1:

  • A iterates through the remaining items to save in PFObject#_deepSaveAsyncChildrenOfObject:withCurrentUser:sessionToken:. Those remaining items are a collection in the order B,A,B. It encounters A in the enumeration and calls canBeSerializedAfterSaving:withCurrentUser:error
  • A synchronizes on its lock in PFObject#canBeSerializedAfterSaving:withCurrentUser:error
  • A enumerates through its keys, and eventually comes to its reference to B.
  • In PFObject#canBeSerializedAsValue:afterSaving:error:, the object ID for B is accessed.
  • B synchronizes with its lock when it access its state.
  • So now we have synchronized locks on this thread in the order A,B

Meanwhile, on thread 2:

  • A iterates through the remaining items to save in PFObject#_deepSaveAsyncChildrenOfObject:withCurrentUser:sessionToken:. Those remaining items are a collection in the order B,A,B. It encounters B in the enumeration and calls canBeSerializedAfterSaving:withCurrentUser:error
  • B synchronizes on its lock in PFObject#canBeSerializedAfterSaving:withCurrentUser:error
  • B enumerates through its keys, and eventually comes to its reference to A.
  • In PFObject#canBeSerializedAsValue:afterSaving:error:, the object ID for A is accessed.
  • A synchronizes with its lock when it access its state.
  • So now we have synchronized locks on this thread in the order B,A

BAM! Classic deadlock
One thread has lock in the order A,B, and the other has locked in the order B,A

Two possible ideas for fixes:

  1. Sort the collection of remaining objects in _deepSave...
  2. Limit the synchronization scope in canBeSerializedAsValue:afterSaving:error:, perhaps by making a copy of the dictionary and then proceeding. It seems risky to be synchronizing that entire block of code there because it could be doing all kinds of recursive accesses that could result in this deadlock.
@czgarrett
Copy link
Author

The following change fixed this issue for me:

OLD:


// Returns YES if this object can be serialized for saving.
// @param saved A set of objects that we can assume will have been saved.
// @param error The reason why it can't be serialized.
- (BOOL)canBeSerializedAfterSaving:(NSMutableArray *)saved withCurrentUser:(PFUser *)user error:(NSError **)error {
    @synchronized (lock) {
        // This method is only used for batching sets of objects for saveAll
        // and when saving children automatically. Since it's only used to
        // determine whether or not save should be called on them, it only
        // needs to examine their current values, so we use estimatedData.
        if (![[self class] canBeSerializedAsValue:_estimatedData.dictionaryRepresentation
                                      afterSaving:saved
                                            error:error]) {
            return NO;
        }

        if ([self isDataAvailableForKey:@"ACL"] &&
            [[self ACLWithoutCopying] hasUnresolvedUser] &&
            ![saved containsObject:user]) {
            if (error) {
                *error = [PFErrorUtilities errorWithCode:kPFErrorInvalidACL
                                                 message:@"User associated with ACL must be signed up."];
            }
            return NO;
        }

        return YES;
    }
}

NEW:

// Returns YES if this object can be serialized for saving.
// @param saved A set of objects that we can assume will have been saved.
// @param error The reason why it can't be serialized.
- (BOOL)canBeSerializedAfterSaving:(NSMutableArray *)saved withCurrentUser:(PFUser *)user error:(NSError **)error {
    NSDictionary *dictionaryRepresentationCopy;
    @synchronized (lock) {
        dictionaryRepresentationCopy = _estimatedData.dictionaryRepresentation; // This copies the dictionary representation
    }
    // This method is only used for batching sets of objects for saveAll
    // and when saving children automatically. Since it's only used to
    // determine whether or not save should be called on them, it only
    // needs to examine their current values, so we use estimatedData.
    if (![[self class] canBeSerializedAsValue: dictionaryRepresentationCopy
                                  afterSaving:saved
                                        error:error]) {
        return NO;
    }

    if ([self isDataAvailableForKey:@"ACL"] &&
        [[self ACLWithoutCopying] hasUnresolvedUser] &&
        ![saved containsObject:user]) {
        if (error) {
            *error = [PFErrorUtilities errorWithCode:kPFErrorInvalidACL
                                             message:@"User associated with ACL must be signed up."];
        }
        return NO;
    }

    return YES;
}

@czgarrett
Copy link
Author

Also: I tried to follow the instructions for contributing, but when I forked the repo the master branch won't even compile. I was hoping to write a unit test and create a pull request but I'm not sure how to make that happen. I'm sure the master branch actually does compile, but the contributor instructions need more details for project newcomers to get started.

@richardjrossiii
Copy link
Contributor

@czgarrett Did you do a submodule fetch? git submodule update --init --recursive. It's stated in the README under steps for 'compiling the SDK yourself'.

@richardjrossiii richardjrossiii added the type:bug Impaired feature or lacking behavior that is likely assumed label May 9, 2016
@richardjrossiii richardjrossiii self-assigned this May 9, 2016
@richardjrossiii
Copy link
Contributor

richardjrossiii commented May 9, 2016

Awesome find! There's a ton of 'bad' code revolving around how we deal with locks in our SDK, thanks for fixing one of them.

Though I would recommend doing an explicit copy of dictionaryRepresentation - just in case it was a NSMutableDictionary at some point.

If you don't want to go through the trouble of creating a PR yourself, I can create a PR and credit you with the fix if you'd like. Otherwise, I'll assume you're working on a PR, and am looking forward to reviewing it shortly!

Also, related discussion from the past: #11

@czgarrett
Copy link
Author

Sure I'll do a PR. I'd like to figure out the process because I have other ideas for enhancements as well.

Chris

On May 9, 2016, at 4:44 PM, Richard Ross [email protected] wrote:

Awesome find! There's a ton of 'bad' code revolving around how we deal with locks in our SDK, thanks for fixing one of them.

If you don't want to go through the trouble of creating a PR yourself, I can create a PR and credit you with the fix if you'd like. Otherwise, I'll assume you're working on a PR, and am looking forward to reviewing it shortly!


You are receiving this because you were mentioned.
Reply to this email directly or view it on GitHub

@nlutsenko
Copy link
Contributor

Let me or @richardjrossiii know if you need help with it.
Should be pretty straightforward and for this change not even required to write tests ;)

@mbrauwers
Copy link

Hi guys, thanks for your hard work
I'm also experiencing some deadlocks, specially on isDataAvailable.

Do you have an idea when this potential fix will be committed to the master branch?

Thanks

@czgarrett
Copy link
Author

PR created. Let me know if I did it right - this is my first time doing a PR for a forked repo.

@gateway
Copy link

gateway commented Jul 19, 2016

Any updates on this?

@ranhsd
Copy link

ranhsd commented Apr 6, 2017

Hi, any update on this issue? I am using the latest version and i still experience this issue.

@czgarrett
Copy link
Author

I submitted a PR last July and it was never approved. Due to this and other issues, and a general lack of progress on this SDK, I finally gave up using the local data store and wrote my own sync engine using core data. While it was a pretty large effort, it did pay off because I have a lot more control over how sync works in my app.

@flovilmart
Copy link
Contributor

@czgarrett what PR was it?

@czgarrett
Copy link
Author

This one (actually turns out it was May, not July): #931

flovilmart added a commit that referenced this issue Jan 21, 2018
- As per discussion over there, @nlutsenko and @richardjrossiii were A1 on the resolution
Thanks @czgarrett for the fix

Closes #931
@flovilmart flovilmart added this to the 1.17.0 milestone Jan 21, 2018
flovilmart added a commit that referenced this issue Jan 30, 2018
- As per discussion over there, @nlutsenko and @richardjrossiii were A1 on the resolution
Thanks @czgarrett for the fix

Closes #931
flovilmart added a commit that referenced this issue Jan 30, 2018
* wip

* wip

* Initial refactoring of Consistency Assertion to Consistency errors

- Make Tried to get invalid local id a soft error
- Make Tried to save an object with a new, unsaved child. a soft error
- Make Tried to save an object with a pointer to a new, unsaved object. a soft error
- Make Attempted to find non-existent uuid a soft error

* Soft errors for User consistency checks `_checkSaveParametersWithCurrentUser`

* Bumps to latest Swift

* Proper failable methods for the LocalIdStore

* Adds precondition for checking if user is not merging on itself

* Swallow consistency error when decoding

* Base error checking on return parameters as unsafe to check error / *error

* Restore behaviour for nil tranformed objects without errors

* proper ARC modifier for NSError**

* Transform assertions to errors in children collection

* Adds PFPrecondition instead of Exception when saving cycles

* Properly bail on encoding errors, add tests

* nits: TODOs

* Improve handling of failures for localId resolutiosn

* Report missing localId as soft errors

* Refactor: Rename macros in PFPreconditon, more consistency on arguments

* Better error propagation in resolving localIds

* More tests for failures of localId resolutions

* Bump version to 1.16.1-alpha.1

* Bump podspec to 1.17.0-alpha.1

* Fix issue #916

- As per discussion over there, @nlutsenko and @richardjrossiii were A1 on the resolution
Thanks @czgarrett for the fix

Closes #931

* Fixes #1184

- Prevent deadlock by copying estimatedData before traversal

* Adds r/w accessors in swift for the PFACL

fixes #1083

* Adds CHANGELOG.md

* Bumps to 1.17.0-alpha.2

* Fixes flaky test

* Provide more context when asserting OfflineStore objectId assignment issues

* Bolts doesnt infer failed tasks with NSError as a return type, always return failed tasks if needed

* Bump to 1.17.0-alpha.3

* Releases on travis

* run pod trunk push with verbose to not stall travis

* Review nits

* Do no track dimensions if they are nil

* Bump version to 1.17.0-alpha.4

* Adds precondition to prevent crash

* Fixes errors

* Bumps to 1.17.0-alpha.5
@TomWFox
Copy link
Contributor

TomWFox commented Mar 8, 2019

It seems this has been fixed, could it be closed? - @parse-community/ios-osx

@TomWFox TomWFox closed this as completed Mar 28, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
type:bug Impaired feature or lacking behavior that is likely assumed
Projects
None yet
Development

No branches or pull requests

8 participants