-
Notifications
You must be signed in to change notification settings - Fork 126
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 deadlock in CachedValues by using more narrow locking #273
Conversation
Hi @jtbergman, thanks for bringing this up and for investigating changes to the library! Stephen and I will discuss it soon, but one thing that would help a lot is if you could cook up a test case that deadlocks without your changes. Would it be possible to push that to this PR? |
@mbrandonw Thanks for the quick response. I'll see what I can do and try to push one later today 🙏 |
@jtbergman Thanks! Also, I went ahead and ran tests on this PR and it does seem like the change has caused one failure:
This test fires up 10 tasks in a group to access an I will say that the less you do in a dependency initializer, the better overall. Many people have run into issues trying to force a dependency to be created on the correct thread, or initialized in a specific manner, and it's always ended poorly. It's just the cost of having a very simple syntax like |
f74aaa0
to
bf5d6d3
Compare
Hi @mbrandonw, Thanks for the response! I initially misread the code, and thought that accessing the dependency before it's in the cache would cause a runtime warning (which I thought might be preferable to deadlock), but now I see that it causes the dependency value to be initialized multiple times. I did, however, push some changes that show an example of the deadlock. If you run That being said, I agree that the failing test has the correct behavior, so the fix in I tried that approach and pushed it here. It fixes the currently broken test and actually works quite well, but it seems to cause issues in the tests named
In theory, this could probably be fixed by implementing some sort of spin lock mechanism to replace the condition variable. But given the issues that could introduce, plus the difficulty of even creating a deadlocking example for this issue, I think it's probably better to just leave the code in its current state. Happy to close this, and we'll just fix in our codebase, but I appreciate the feedback 😄 |
Problem
In our project, we are hitting an interesting deadlock scenario with Swift Dependencies. It is quite convoluted, but it roughly works like this.
CachedValues.lock
and runs itsliveValue
initializerlock
and exhaust a thread poolObject A
cannot complete until the subscriptions are ready, so the entire project deadlocksThis is an avoidable issue without any framework changes. We could:
@Dependency
inside the thread poolliveValue
initializersProposed Solution
However, we'd be a bit worried about this deadlock pattern (or other priority inversion) manifesting in other ways. The issue is fixed if we make the
lock
forCachedValues.value
use a more narrow scoping as shown in this PR.Let me know if you have any questions!
Additional Note on
CachedValues.cache
Given that
CachedValues.cache
should be accessed via alock
I'm not sure it should be exposed publicly since thelock
is private. The deprecatedreset
method that accesses it could be moved toCachedValues
itself, and the test that sets_current.cachedValues.cached = [:]
could use the reset method instead. This would require using@testable import Dependencies
though and the usage seems deprecated anyway. Let me know if you'd like these changes made though.