Closed hartbit closed 10 years ago
I'm curious about that [NSRecursiveLock lock]
call from the KVO implementation on thread 3. It looks like KVO takes a lock there, but that's a different lock than the blocked Thread 1 queue.
On Thread 1, what comes next in the stack after level 10? Is instanceWithPrimaryKey:
being invoked from another KVO call, maybe, and the internal KVO lock is deadlocking with the recursive calls?
In the absence of more information, I'm going to close this for now — please comment or reopen if necessary. Thanks.
I had not thought about a KVO-centric lock. And indeed, the stack trace on Thread 1 contains a `addObserver' method call, which ends up calling the property, which ends up doing the DB call.
What do you think about this problem? Is it an issue that should just be well documented, or do you think that FCModel can do something about it so it does not happen?
Well, glad we figured it out. I'm not really sure what to do about it, though. (I'm open to suggestions.)
The weird thing is that, looking at that KVO source code (assuming that's still how it's implemented), it looks like every NSObject gets its own lock (iLock
there). So the only way this should be a problem is if addObserver:
is recursively called on the same object, but how is that possible with FCModel?
To narrow it down a bit, could you clarify what object the enclosing addObserver
call was observing, which FCModel was invoked for instanceWithPrimaryKey:
, and anything else that may be relevant about its context?
But looking at the stack trace, the DB thread shows the use of [NSRecursiveLock lock]
which is different from the NSLock* iLock
. I'll get more info for you once I get to work tomorow.
If you wanted to fix this, you'd need your KVO call to happen after the instanceWithPrimaryKey
call has returned. Perhaps if you setup the call with an async block on the same queue?
iLock
is set as an NSRecursiveLock
in initWithInstance
.
If I dispatch my KVO calls to happen after that, I believe immediately trying to update a just-loaded model would fail:
Person *p = [Person instanceWithPrimaryKey:@(1)]; // already exists in DB
p.name = @"New Name";
[p save];
Attaching the internal KVO listeners would be delayed until the next run loop, so p
would have no idea that .name
had changed, save
would do nothing and return FCModelSaveNoChanges
, and the database row would not be updated.
Another option would be to rewrite FCModel's change-tracking not to use KVO at all, probably by keeping a dictionary of the originally loaded values on each model and checking them for equality with the current values on save
. This isn't necessarily worse than the KVO method, just different: it would use a bit more memory for each property, saves would incur a slight performance penalty, and didChangeValueForFieldName
would need to be removed, but changing the model properties would be faster since the KVO logic wouldn't be there.
The more I think about rewriting the change-tracking not to use KVO, the better I think that option is. What do you think?
Check out commit 3ea84d7 on the new remove-internal-kvo branch. (And feel free to try it at work where this deadlock was happening. Shouldn't happen anymore.)
Works like a charm!
I also think that the solution you went for is superior to KVO!
I agree. I think I'll finish it up today and merge it into master.
Hi there! I'm using HEAD, and I'm experiencing a deadlock. Care to have a look?
Stack trace for Thread 1
Stack trace for Thread 3