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

Fixes #13 Fix threading issue in local inject #18

Merged
merged 6 commits into from
Oct 17, 2024

Conversation

ncipollo
Copy link
Contributor

  • Add thread safety around local service dictionary in container.
  • Adds mechanism to provide options / flags to WhoopDI so this functionality can be safely rolled out.

@ncipollo ncipollo force-pushed the ncipollo/13/local-inject-threading-issue branch from 3562cd0 to 4b48d2d Compare October 16, 2024 18:52
}

@Test(.bug("https://github.com/WhoopInc/WhoopDI/issues/13"))
func inject_localDefinition_concurrency() async {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

If you disable the thread safety option then run this 1000 times you will hit the race condition / crash. When you enable it and do the same you should no loner see the crash.

// We need to maintain a reference to the local service dictionary because transient dependencies may also
// need to reference dependencies from it.
// ----
// This is a little dangerous since we are mutating a static variable but it should be fine as long as you
// This is a little dangerous since we are mutating a variable but it should be fine as long as you
Copy link
Contributor

Choose a reason for hiding this comment

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

yeah this makes me think we should lock this too

@@ -0,0 +1,9 @@
struct DefaultOptionProvider: WhoopDIOptionProvider {
func isOptionEnabled(_ option: WhoopDIOption) -> Bool {
false

Choose a reason for hiding this comment

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

I think we should still add the switch here and then return false

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Why? The intent is to default everything to false.


final class ThreadSafeDependencyGraph: Sendable {
private let lock = NSRecursiveLock()
nonisolated(unsafe) private let serviceDict: ServiceDictionary<DependencyDefinition> = .init()
Copy link
Contributor

Choose a reason for hiding this comment

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

let's not make this nonisolated(unsafe), but instead make the class @unchecked Sendable since that is more accurate to our usage here

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Could you elaborate on why unchecked Sendable is preferable here? My rationale is that I wanted to scope this check down to just this variable since that is the only thing which is unsafe. If we were to make that a Sendable in the future we could remove this all together.

Copy link
Contributor

Choose a reason for hiding this comment

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

In the way I see it, we are providing the guarantee that the ThreadSafeDependencyGraph is sendable, since we are providing locking around the non-sendable type. This to me means that this type is @unchecked Sendable because we are providing this guarantee to tell the compiler. From my understanding, nonisolated(unsafe) is more of an escape hatch where we know something won't race, but the compiler can't reason about it (but we don't really need to do anything, where here we are doing something to make it Sendable)

Comment on lines 51 to 57
Task.detached {
let _: Dependency = self.container.inject("C_Factory") { module in
module.factory(name: "C_Factory") { DependencyA() as Dependency }
}
}

Task.detached {
Copy link
Contributor

Choose a reason for hiding this comment

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

I think we should store both of these and await their values so we are certain that we can be certain these run. We also could do a for loop in each one of those so that we can get it to crash every time?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That is what I had in here originally 🙃

@ncipollo ncipollo merged commit 09105e3 into main Oct 17, 2024
1 check passed
@ncipollo ncipollo deleted the ncipollo/13/local-inject-threading-issue branch October 17, 2024 14:55
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.

3 participants