-
Notifications
You must be signed in to change notification settings - Fork 78
Remove thunk mutex and use a signal channel and atomic pointer #118
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
Remove thunk mutex and use a signal channel and atomic pointer #118
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Few requests:
- rename the channel variable
- add a nil check for the
result; and - add a test using testing/synctest
|
One more thing - the fix targets #117, but we don’t gain any protection against future regressions. Please add a focused test (ideally exercising |
| result.mu.RLock() | ||
| defer result.mu.RUnlock() | ||
| <-req.doneCh | ||
| result := req.result.Load() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
result can be nil which would cause a panic. Please, add a nil check!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
resultcan benilwhich would cause a panic. Please, add a nil check!
Ok, I see, it can be nil if the user provided batch function did not set the items[i] element. Rather than check it each time the thunk is invoked, what if we always ensured that it was never nil.
In this loop:
for i, req := range reqs {
req.result.Store(items[i])
close(req.done)
}
We could check item items[i] was nil and set to a item which an error that indicates that no value was set. What would you like the error to be? Should I just make one with fmt.Errorf() or should we declare one as a var ErrNoValueProvided = errors.New("no value provided") so the user could use errors.Is against it?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For starters, I'd be happy with just fmt.Errorf() - at the very least we remove the risk of a panic. Whether you add a specific error for that is up to you - no strong opinion here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
what if we always ensured that it was never nil
How do you imagine that? Can you provide an example, please?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the initial code also has would panic if results[i] of the batch function was nil.
Lines 264 to 272 in ab5318f
| result.value = v | |
| } | |
| result.mu.Unlock() | |
| } | |
| result.mu.RLock() | |
| defer result.mu.RUnlock() | |
| var ev *PanicErrorWrapper | |
| var es *SkipCacheError | |
| if result.value.Error != nil && (errors.As(result.value.Error, &ev) || errors.As(result.value.Error, &es)){ |
result.value.Error and result.value` could be nil.
I have made a separate to address this with a known error and updated the comment. This way if the batch function doesn't return a result for a given item, we can automatically give it an error result. (Similarly to how it does that if the length of the result isn't the same.)
7e4f0d8 to
81b2c7d
Compare
I could do that, but |
|
I see no problem with bumping the Go version to v1.25. In future, I'm planning to only support the last two minor Go versions. When a new release is cut it would not affect existing users of the previous version. And given that there are no breaking changes between Go v1.18 and v1.25 I see no reason why most of the users wouldn't be able to upgrade if they wanted to. |
81b2c7d to
a59f279
Compare
|
Here is the benchmark with my most recent changes (on a windows x86_64 computer): Here is it before: So the changes here both fix the synctest but also make it faster. It also is a net reduction in lines. Regarding Go 1.25, I have a couple projects using DataLoaders and while one is on go 1.25 (and using synctest which is who I discovered this), the others are a bit earlier. I can't get upgrade to 1.25 but do what them to get this code as it's slightly faster. Would you be open to approving this PR on it's own merits without the synctest? (It's just a happy coincidence that it happens to fix that as well.) |
|
Fair enough, upgrading Go version is outside of the scope of this PR. |
This appears to fix fix #117 for me. Given I'm not that familiar with dataloaders I hope I didn't break anything with my simplification.
Rather than using a RWMutex (which needs to be locked 2 (and somethings 3 times)) in the thunk, this change makes it so that the result is stored in an
atomic.Pointer[T]and the it being reader is signaled by closing of a done channel (chan struct{}). This way, "waiting" for the result to be ready is considered "durably blocked" by the synctest definition (https://pkg.go.dev/testing/synctest#hdr-Blocking). My hope is that this code is actually simpler (and faster) than what was there before and that now it works with synctest.I ran the benchmarks. Before:
After: