-
Notifications
You must be signed in to change notification settings - Fork 16
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
forceTransfer/steal lock request option instead of forceRelease() ? #23
Comments
I was assuming (...) that forceRelease() would abort queued requests as well, but I'm not advocating any particular API shape for this. Presumably, if two |
Sure, last one wins should be OK. This relies on the developers writing their code to be resilient to lock steals anyways. |
Huh... I wonder if we could make seem like less of a wart by making this a non-negative safe integer But maybe that would just encourage the use in normal application logic rather than just recovery scenarios. @pwnall - thoughts? |
I originally proposed a priority option, but I split it into two: lockPriority & stealPriority, to make stealing explicit. Regular devs would probably expect that trying to acquire a lock with a higher priority should not automatically steal the lock, but rather just put the request ahead of lower priorities on the wait queue. Alternatively, lock priority can probably be achieved by storing some additional information in the database:
Obviously, this is not ideal, because technically the lock holder can change between checking the database and issuing the lock request. |
It seems to me that (without additional support in other APIs) adding lock stealing to this API would undermine any sort of guarantees one could expect from such a system. As far as I know, lock stealing works effectively when paired with operations that error out when the lock is broken (stolen). For example, in a distributed filesystem, reads and writes error out when the associated lock is broken. For stealing to be meaningful in this API, I think it'd have to be complemented with support in:
A (bold, IMO) alternative would be to restrict locks that can be stolen to worker contexts. When a lock stealing request is issued, the current lock owner's event loop is shut down as soon as possible (possibly like Worker.terminate?). When the stealing request is granted, it is guaranteed that the old worker has been terminated. I think this is simpler to reason about than the proposal above, and works for all coordination scenarios above. I'm labeling this proposal as bold because I don't know enough about worker lifecycle to know if it can be implemented in all browsers, though Worker.terminate's broad support makes me fairly optimistic. |
I took an initial stab at this in the proto-spec. |
I just looked at the spec change above. First off, it's very surprising to me that Second, I would like to see concrete use cases for lock stealing discussed. It would help guide my intuition. The case I've been thinking of is using locks for master election. For simplicity, assume a system where every tab spawns a worker and sends it a request whenever it needs to do something complex. Each worker first runs a master election algorithm. Workers that don't win forward their requests to the master, and forward the response to their tab. The master election algorithm runs the following steps in an infinite loop:
Resilience to masters getting stuck (i.e. in a deadlock) can be added with the following measures:
@ralphch0: Sorry if this is completely disconnected from what you're considering. I need a use case to reason about. Now that I've laid out a use case, we can start thinking about how the stealing API would work out there. Specifically, I'm concerned with how a master would detect that a lock was stolen from it. The code below is a sketch for a master's request-handling loop. let lock_stolen = false;
try {
await navigator.locks.acquire('master-lock', async () => {
while (!lock_stolen) {
// process request from queue
}
});
} catch(e) {
if (IsLockStolenAbortError(e)) {
lock_stolen = true;
}
} First, this looks a bit un-intuitive. Second, I'd have to pepper my code with lock_stolen checks (ideally, there'd be a check before every network / database request). Third, there's still potential for races. Based on what I know so far, I'd still like to see lock-stealing terminate the worker(s) that held the lock, if any. |
steal being true by default was a typo; and is fixed - thanks for catching that! |
Just wondering why we're also dropping other lock requests? If the intent is only to steal the lock, then this might cause an unintended side-effect. I can also maybe see this feature used intelligently by some sites to transfer the ownership of a lock from one tab to another, without affecting any other pages waiting for the lock. |
Keeping existing requests is doable. I don't have a great intuition here. Dropping everything seemed straightforward, but dropping only held locks seems simpler. Aside: what would |
And should |
I think in both cases it should succeed and steal as usual. It's not clear how these combinations would be useful, but none of these options are mutually conflicting. |
Thanks. I'll rework the spec and my implementation (just finished a first pass) ASAP next week. |
Proto-spec updated to leave the requests intact. The "steal" request jumps to the top of the request queue and is granted immediately. The The |
Ah, I can see now how ifAvailable & steal are kind of confusing together. And since ifAvailable is completely ignored, maybe it's better to disallow this combination and force devs fix their options? For steal and shared, I also see the gotcha there. We could either clarify this in the spec, or disallow if we don't think this is likely to be useful. |
(general note: I still think stealing is a footgun without something like worker termination) I like the new approach of throwing if both I find the behavior for stealing shared locks confusing. It seems to me that "steal" means "I want this lock right now without waiting, and I'm willing to step on anyone who gets in my way". In the current context of S/X locks, I would have guessed that stealing an X lock should break the lock for all current holders, while stealing an S lock would only break the lock if it's held in X mode. In a more general model, stealing a lock would break all current holders whose mode conflicts with the stealing request's mode. Am I confusing "steal" with "preempt"? It seems to me that the current behavior of booting all S lock holders when stealing an S lock is closer to "purge" than to "steal". |
I generally don't have strong opinions about shared locks, simply because we currently don't have a scenario where we would use them. So if we decide to restrict this combination, I would not object. There is also a way to simulate this behavior: While on the subject, "ifAvailable" and "signal" (abort controller) also seem to fall into the same category as "ifAvailable" & "steal", so maybe that should error out as well. |
Not if there's already a queued request for an exclusive lock. That would be run ahead of the request for the shared lock.
Good catch. BTW, I pinged the folks working on the abort stuff; sounds like that code might be in to Blink next week. |
Hrm... and
I'll file another bug about signal edge cases. |
#34 for signal racing. |
Good point. |
Based on offline conversations w/ @pwnall, making steal + shared an error. Easier to forbid now and allow later than the converse. |
Since we seem to have settled on a syntax and semantics, resolving this. I'm still sympathetic to the argument that breaking locks is crazy, but let's open new bugs for that. :) |
Current API does not allow a page to ensure that it can take over a lock. Inspecting the locks and calling forceRelease() allows that page to release the lock from the current holder, but it does not guarantee that the lock will be free for it to acquire, since other pages may have queued lock requests.
I'm suggesting instead to introduce a "steal" or "forceTransfer" option to LockOptions that allows the current page to not only to force release the lock, but also to take it over ahead of all queued requests. forceRelease() can still be achieved by acquiring the lock with steal=true, and releasing it.
The text was updated successfully, but these errors were encountered: