-
Notifications
You must be signed in to change notification settings - Fork 5
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
Implement semaphores #5
Conversation
Nice, thanks for the PR @ninevra! Curious to hear your thoughts on the shared worker API model.
Why is this? Could it not act as a default but be irrelevant otherwise if a different test worker ran first with a different value?
Are you referring to the
Per https://en.wikipedia.org/wiki/Semaphore_(programming)#Operation_names we could use
👍
👍
Could you elaborate on this? The intended execution model for shared workers is to clean up automatically on teardown.
That's fine. This package is purposefully outside of AVA so we can make breaking changes more easily.
Should this be an error? |
I think this is primarily a usability question. Test files could be run in any combination and any order, so an individual test file doesn't know whether it's actually creating the semaphore or just accessing a previously-created one. Therefore each test file must supply an initial value. If two test files supply different initial values, I figure that likely reflects a bug in the tests. Failing on mismatch is a way of reporting that bug. Treating the initial value as only a default would hide that bug, in a manner that would be execution-order dependent and potentially very hard to debug. If we were to treat it as a default, the onus would then be on each test file to check the initial value of the semaphore it receives. That would require a somewhat different interface (probably an There are probably cases where it's difficult to guarantee consistent initial values - like if the initial value represents the size of a possibly fluctuating resource pool - but ignoring the mismatch probably isn't correct in these cases either. They'd be more likely to want a
I'm referring to the Alternatively, |
So, if you're using a semaphore to model acquiring resources from a fixed pool (for example, CPU threads) then acquire/release works fine. A test worker would
However, semaphores can be used to model lots of other situations, many of which break these assumptions. One example: a provider/consumer relationship with a fixed-size queue/buffer (or other analogy for backpressure). One semaphore, Another example: rendezvous semaphores. Suppose Another example: a growable resource pool. The point is: at this low level there's no way to know what teardown actions are semantically correct, if any, without making restrictive assumptions about how the semaphore's being used. Teardown becomes the responsibility of whatever tool (perhaps itself a shared worker) is built on top of I prefer That said, it could be helpful to expose helpers for the acquire/release model, since it's probably the most common use case. This could be done in a lightweight fashion, just moving the class Semaphore {
// ...
async task<T>(job: () => T, amount = 1): Promise<Awaited<T>> {
await this.down(amount);
try {
return await job();
} finally {
await this.up(amount);
}
}
} or in a heavyweight fashion, with fourth and fifth operations |
Maybe? I'm not sure whether there's ever a good reason to use |
Hey @ninevra — sorry I haven't had the time to review your comments all week. Regarding initialization:
I see. I imagine it likely that the semaphore is actually "created" in a helper file, rather than in each test file, so then it would be unlikely for any hardcoded value to differ, making this behavior manageable. Even better if you could initialize the semaphore through
It's nice to be able to set stuff up synchronously in helpers. If I may bike-shed… rather than
Cool, let's start with It would be nice though if test('upside down', async t => {
const up = await semaphore.down()
t.teardown(up)
t.pass()
})
Wouldn't hurt to restrict it for now. Any thoughts on this?
|
I tried to elaborate on this in #5 (comment) (which was kind of a long ramble, sorry). Relevant part:
If we were to always clean up "acquisitions" (i.e. imbalanced usage of I think it makes more sense to support all possible use cases here, since this is explicitly a low-level library? Alternatively we could have both an |
Agreed. What do you think of making the clean-up opt-out? Simpler use cases will work fine out of the box, and more advanced use cases come with some extra lifecycle management responsibilities. Could change |
Making the clean-up opt-out makes sense to me. I'll try to implement that in the next few days. |
Sorry it took me a while to get back to this. I've implemented cleaning up after semaphore acquisitions; it was a minor nightmare trying to organize the code for that. Currently that's exposed via extra methods |
Hey @ninevra I'm so sorry I wanted to land avajs/ava#2741, and didn't even succeed at that, so I haven't been able to look at this yet. |
@novemberborn there's nothing to apologize for! I still need to do a bit more work & cleanup on this anyway; I'll ping you when I think it's ready again. #2741 looks like a huge amount of work! I'm super happy to see projects adopting esm. |
0 seemed like a confusing initial value, especially given that a 0-value AcquiringSemaphore does nothing.
Allows the test to at least sometimes detect the bug fixed in 24d6f23.
I think I've made a logic error in trying to support non-integer semaphore values. We've been tracking the semaphore value with a As long as user uses safe integers in a reasonable range, everything'll be fine, but non-integers or very large semaphore values will produce small errors that might add up over time. We could let that be, or switch to |
@novemberborn I think this is ready for review! There's no hurry at all, though; it can wait until you're got time for it. |
@ninevra this is still on my to-do list, sorry. |
This is great @ninevra!
Agreed. Let's make sure they're safe integers. I've pushed a commit to that effect. I've also tweaked naming, since both semaphores are counting semaphores. Now using managed and unmanaged. What do you think? I found it strange to have separate ID spaces for both types of semaphores, so I've changed it to share a single space but enforce that the created semaphores are in the same mode. |
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.
Looks good to me! I tweaked the readme to state that amounts and initialValues must be integers.
test/semaphore.ts
Outdated
t.notThrows(() => release(0)); | ||
}); | ||
|
||
test.serial('managed and unmanaged semaphores share their ID space', async t => { |
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.
Why's this test .serial()
? It seems to pass just fine without 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.
Ah left-over from a debugging attempt, good catch.
source/index.ts
Outdated
} | ||
} | ||
|
||
/* c8 ignore next 2 */ |
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.
Should these ignore comments perhaps be removed? I couldn't find a good way to satisfy both c8 and eslint:
- With the
/* c8 ignore next 2 */
comments are currently used, c8 just warns about the blank line before and the}
line after not being covered. - Removing the line above and increasing the range to `/* c8 ignore next 3 */ makes c8 happy, but eslint says "Expected blank line before this statement."
- Moving the comment up to line 237 and increasing the range to `/* c8 ignore next 4 */ works, but doesn't exactly look great.
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.
Ugh yea, getting rid.
Nice one. |
Thanks for your patience and hard work on this @ninevra! I'll try and push out a release tomorrow. |
This PR implements weighted semaphores, using an API and backend patterned after
Lock
.Design choices:
Like
Lock
,Semaphore
is constructed synchronously (viaSharedContext.prototype.createSemaphore(id, initialValue)
) and used asynchronously (viaSemaphore.prototype.down()
,downNow()
, andup()
).Unlike
Lock
,createSemaphore()
can fail, if the initial value doesn't match another initialization of the same semaphore. Since the creation is synchronous and doesn't touch the backend, failure is reported on use. This means that allSemaphore
operations can reject.down()
waiters are woken in strict FIFO order.downNow()
ignores the queue, meaningdownNow()
callers can potentially starve other threads.The semaphore is weighted:
down(2)
acquires 2 units from the semaphore. I think this feature is worth including because a) it's useful for modeling resource pools and b) trying to implement weights on top of unweighted semaphores can accidentally lead to deadlock or busy-waiting. (I'm not sure locks, reservations, and unweighted semaphores are enough to build a weighted semaphore, without arbitrary mutable shared state or condition variables.)down()
"acquisitions" are not released on test worker teardown, because many use cases for semaphores don't fit the acquire/release model.I made
amount
an optional positional argument because it's the only currently implemented option. Might be a little awkward if/when other options, e.g.timeout
, are added.Open questions:
down(0)
anddownNow(0)
do?down(0)
blocks for any waiters, anddownNow(0)
always succeeds.