-
Couldn't load subscription status.
- Fork 1.4k
Semaphores revisited #302
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
Semaphores revisited #302
Conversation
|
You will probably need the release action in Promise.bracket so you can remove the promise from the Semaphore state. Because if someone calls acquire but needs to wait, then it gets interrupted, that needs to be cleaned up. |
|
|
||
| val release: (Boolean, Promise[Nothing, Unit]) => IO[Nothing, Unit] = { | ||
| case (_, p) => | ||
| p.poll.void <> state.update { |
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.
p.poll.void can have no effect, why use 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.
I was going to say the promise can only be completed if it is taken out of the queue, but interruption can occur between the complete call and the moment the queue is updated in the ref.
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 still don't understand. I think if you delete p.poll.void <>, the meaning of this code won't change.
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.
In the happy path, the promise given to release is already completed (that is part of Promise.bracket, specifically the p.get), which can only happen if it is not in the queue (that is imposed by the definition of acquireN/releaseN).
Actually I take back my previous comment, because Promise.bracket guarantees atomicity, if the promise is completed we can deduce that it is not in the queue anymore! So, with p.poll.void we avoid the extra state update if the promise has already been removed from the queue.
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 does p.poll do? AFAIK, it doesn't do anything, it just peeks in the promise and returns the value as a result, which is then thrown away with void. Am I missing something?
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.
In this case, I'm only interested to know whether the promise is complete or not: and poll fails in the latter case, which means I can provide an alternative action (update the state) with the <> combinator.
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.
Ah, OK, I see now that poll fails with Unit if the result is not available. Unfortunately there's a race condition—it's possible the promise will be completed between p.poll.void and state.update. Because other threads may be concurrently modifying state (releasing, which could trigger promise completion). If there's a race condition then we should always update the state. Or am I missing something else?
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.
The release action is inside a finaliser (via io.ensuring(...)), which means it is not interruptible and so this should be fine. Amirite?
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.
It's not interruptible, however, the state of the promise can still change during the execution of the action. Promise.bracket provides no guarantees on concurrent access, only a guarantee if acquire succeed, that release will succeed, regardless of interruption.
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.
That is correct 😞 Thanks for the explanation!
| val release: (Boolean, Promise[Nothing, Unit]) => IO[Nothing, Unit] = { | ||
| case (_, p) => | ||
| p.poll.void <> state.update { | ||
| case Left(q) => Left(q.filterNot(_._1 == p)) |
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.
Yes, this will help ensure the promise is removed. I'd use eq instead of ==.
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.
We'd have to make Promise a subtype of AnyRef, which sounds reasonable anyway?
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.
Nah, let's just leave it.
| _ <- s.acquire | ||
| } yield () must_=== (())) | ||
| } | ||
|
|
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.
We may want to add one crazy test in here that does stuff from a lot of fibers. The above one is certainly helpful.
|
@regiskuckaertz Huge improvement! Way less code and easier to understand. I'm must more confident in its correctness. Would be good to add a test for the case of an interrupted acquire. |
Closes #293
I feel this version is more in line with the style we use elsewhere and also much easier to read. And I think now I understand
Promise.bracket, even though I don't really need its full power (i.e. thereleaseaction).