Skip to content

Commit

Permalink
Drop multi-resource scopes. Resolves #20
Browse files Browse the repository at this point in the history
  • Loading branch information
inexorabletash committed Dec 15, 2017
1 parent 9ecc364 commit 1c8e905
Show file tree
Hide file tree
Showing 3 changed files with 52 additions and 73 deletions.
67 changes: 26 additions & 41 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,64 +26,33 @@ A web-based document editor stores state in memory for fast access and persists

## Concepts

A _resource_ is just a name (string) chosen by the web application.

A _scope_ is a set of one or more resources.
A _name_ is just a string chosen by the web application to represent an abstract resource.

A _mode_ is either "exclusive" or "shared".

A _lock request_ is made by script for a particular _scope_ and _mode_. A scheduling algorithm looks at the state of current and previous requests, and eventually grants a lock request.
A _lock request_ is made by script for a particular _name_ and _mode_. A scheduling algorithm looks at the state of current and previous requests, and eventually grants a lock request.

A _lock_ is granted request; it has the _scope_ and _mode_ of the lock request. It is represented as an object returned to script.
A _lock_ is granted request; it has the _name_ of the resource and _mode_ of the lock request. It is represented as an object returned to script.

As long as the lock is _held_ it may prevent other lock requests from being granted (depending on the scope and mode).
As long as the lock is _held_ it may prevent other lock requests from being granted (depending on the name and mode).

A lock can be _released_ by script, at which point it may allow other lock requests to be granted.

#### Resources and Scopes
#### Resources Names

The _resource_ strings have no external meaning beyond the scheduling algorithm, but are global
The resource _name_ strings have no external meaning beyond the scheduling algorithm, but are global
across browsing contexts within an origin. Web applications are free to use any resource naming
scheme. For example, to mimic [IndexedDB](https://w3c.github.io/IndexedDB/#transaction-construct)'s transaction locking over named stores within a named
database, an origin might use `db_name + '/' + store_name` (with appropriate restrictions on
allowed names).

The _scope_ concept originates with databases, and is present in the web platform in [IndexedDB](https://w3c.github.io/IndexedDB/#transaction-scope). It allows atomic acquisition of multiple
resources without multiple asynchronous requests and the risk of deadlock from fragile algorithms.

#### Modes and Scheduling

The _mode_ property and can be used to model the common [readers-writer lock](http://en.wikipedia.org/wiki/Readers%E2%80%93writer_lock) pattern. If a held "exclusive" lock has a resource in its scope, no other locks with that resource in scope can be granted. If a held "shared" lock has a resource in its scope, other "shared" locks with that resource in scope can be granted - but not any "exclusive" locks. The default mode in the API is "exclusive".
The _mode_ property and can be used to model the common [readers-writer lock](http://en.wikipedia.org/wiki/Readers%E2%80%93writer_lock) pattern. If an "exclusive" lock is held, no other locks with that name can be granted. If "shared" lock is held, other "shared" locks with that name can be granted - but not any "exclusive" locks. The default mode in the API is "exclusive".

Additional properties may influence scheduling, such as timeouts, fairness, and so on.

The model for granting lock requests is based on the transaction model for
[IndexedDB](https://w3c.github.io/IndexedDB/), with IDB's "readwrite" transactions
the equivalent of "shared" locks and "readonly" transactions the equivalent of "exclusive" locks.
One way to conceptualize this is to consider an ordered list of held locks and requested locks
within an origin — here identified with numbers — with their respective scopes and
modes:

* Held:
* #1: scope: ['a'], mode: "exclusive"
* #2: scope: ['b'], mode: "shared"
* Requested:
* #3: scope: ['b'], mode: "shared"
* #4: scope: ['a', 'b'], mode: "exclusive"
* #5: scope: ['b'], mode: "shared"
* #6: scope: ['c'], mode: "exclusive"

At this point, two locks (#1, #2) are held. Request #3 can be granted immediately since it is
for a "shared" lock on 'b' and #2 is also a "shared" lock on 'b'. Request #4 is for an exclusive
lock on both 'a' and 'b'; it must wait for all of locks #1, #2 and #3 to be released before it
can be granted. Request #5 arrived after request #4 and has overlapping scope, so it is blocked
until #4 is granted then released. (See Q&A below.) The scope of request #6 doesn't overlap with
any other held or requested lock, so it can be granted immediately.

New requests get appended to the end of the list. Any lock release, request, or aborted request
causes the state to be evaluated, with requests considered in order to determine if they can be
granted.


## API Proposal

Expand All @@ -106,7 +75,7 @@ This "scoped release" API model requires callers to pass in an async callback wh

> See [alternate API proposals](alternate-api-proposals.md) for slightly different API styles which were considered.
The _scope_ (required first argument) can be a string or array of strings, e.g. `'thing'` or `['thing1', 'thing2']`.
The _name_ (required first argument) is a string, e.g. `'thing'.

The method returns a promise that resolves/rejects with the result of the callback, or rejects if the request is aborted.

Expand Down Expand Up @@ -177,6 +146,22 @@ navigator.locks.acquire('resource', {ifAvailable: true}, async lock => {
It's much clearer in (b) that the request will not wait if the lock is not available. In (a) you need to read all the way through the lock handling code (artificially short/simple here) before noting the very different behavior of the two requests.


*Can you lock over multiple resources at the same time?*

This is present in the [Indexed DB API](https://w3c.github.io/IndexedDB/#transaction-scope) as a _scope_.
This can be implemented in user-space with the following helper:

```js
async function acquireMultiple(resources) {
const sortedResources = Array.from(resources);
sortedResources.sort();
for (const resource of sortedResources)
await aquireSingle(resource);
}
```

See [issue 20](https://github.com/inexorabletash/web-locks/issues/20) for further discussion.

*What happens if a tab is throttled/suspended?*

If a tab holds a lock and stops running code it can inhibit work done by other tabs. If this is because tabs are not appropriately breaking up work it's an application problem. But browsers could throttle or even suspend tabs (e.g.
Expand All @@ -194,7 +179,7 @@ around formalizing these states and notifications.
* To wrap a lock around a transaction:

```js
navigator.locks.acquire(scope, options, lock => {
navigator.locks.acquire(name, options, lock => {
return new Promise((resolve, reject) => {
const tx = db.transaction(...);
tx.oncomplete = resolve;
Expand All @@ -208,7 +193,7 @@ around formalizing these states and notifications.

```js
const tx = db.transaction(...);
tx.waitUntil(locks.acquire(scope, options, async lock => {
tx.waitUntil(locks.acquire(name, options, async lock => {
// use lock and tx
});
```
Expand Down
16 changes: 7 additions & 9 deletions interface.webidl
Original file line number Diff line number Diff line change
@@ -1,26 +1,24 @@
[SecureContext, Exposed=Window]
[SecureContext, Exposed=Window]
partial interface Navigator {
readonly attribute LockManager locks;
};
[SecureContext, Exposed=Worker]
[SecureContext, Exposed=Worker]
partial interface WorkerNavigator {
readonly attribute LockManager locks;
};

[SecureContext]
interface LockManager {
Promise<any> acquire(LockScope scope,
Promise<any> acquire(DOMString name,
LockRequestCallback callback);
Promise<any> acquire(LockScope scope,
Promise<any> acquire(DOMString name,
optional LockOptions options,
LockRequestCallback callback);

Promise<LockState> queryState();
void forceRelease((DOMString or sequence<DOMString>) scope);
void forceRelease(DOMString name);
};

typedef (DOMString or sequence<DOMString>) LockScope;

callback LockRequestCallback = Promise<any> (Lock lock);

dictionary LockOptions {
Expand All @@ -33,7 +31,7 @@ enum LockMode { "shared", "exclusive" };

[SecureContext, Exposed=(Window,Worker)]
interface Lock {
readonly attribute FrozenArray<DOMString> scope;
readonly attribute DOMString name;
readonly attribute LockMode mode;
};

Expand All @@ -43,6 +41,6 @@ dictionary LockState {
};

dictionary LockRequest {
sequence<DOMString> scope;
DOMString name;
LockMode mode;
};
42 changes: 19 additions & 23 deletions proto-spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ Web IDL is defined in [interface.webidl](interface.webidl)

### Lock

A **lock** has an associated **scope** which is a set of DOMStrings.
A **lock** has an associated **name** which is a DOMStrings.

A **lock** has an associated **mode** which is one of "`exclusive`" or "`shared`".

Expand All @@ -25,7 +25,7 @@ When **lock**'s **waiting promise** settles (fulfills or rejects):

### Lock Requests

A **lock request** is a tuple of (*scope*, *mode*).
A **lock request** is a tuple of (*name*, *mode*).

Each origin has an associated **lock request queue**, which is a [queue](https://infra.spec.whatwg.org/#queue) of **lock requests**.

Expand All @@ -34,13 +34,13 @@ A **lock request** _request_ is said to be **grantable** if the following steps
1. Let _queue_ be the origin's **lock request queue**
1. Let _held_ be the origin's **held lock set**
1. Let _mode_ be _request_'s associated **mode**
1. Let _scope_ be _request_'s associated **scope**
1. Let _name_ be _request_'s associated **name**
1. If _mode_ is "`exclusive`", return true if all of the following conditions are true, and false otherwise:
* No **lock** in _held_ has a **scope** that intersects _scope_
* No entry in _queue_ earlier than _request_ has a **scope** that intersects _scope_.
* No **lock** in _held_ has a **name** that equals _name_
* No entry in _queue_ earlier than _request_ has a **name** that equals _name_.
1. Otherwise, mode is "`shared`"; return true if all of the following conditions are true, and false otherwise:
* No **lock** in _held_ has **mode** "`exclusive`" and has a **scope** that intersects _scope_.
* No entry in _queue_ earlier than _request_ has a **mode** "`exclusive`" and **scope** that intersects _scope_.
* No **lock** in _held_ has **mode** "`exclusive`" and has a **name** that equals _name_.
* No entry in _queue_ earlier than _request_ has a **mode** "`exclusive`" and **name** that equals _name_.


## API
Expand All @@ -49,32 +49,30 @@ A **lock request** _request_ is said to be **grantable** if the following steps

A `Lock` object has an associated **lock**.

#### `Lock.prototype.scope`
#### `Lock.prototype.name`

Returns a frozen array containing the DOMStrings from the associated **scope** of the **lock**, in sorted in lexicographic order.
Returns a DOMString with the associated **name** of the **lock**.

#### `Lock.prototype.mode`

Returns a DOMString containing the associated **mode** of the **lock**.

#### `LockManager.prototype.acquire(scope, callback)`
#### `LockManager.prototype.acquire(scope, options, callback)`
#### `LockManager.prototype.acquire(name, callback)`
#### `LockManager.prototype.acquire(name, options, callback)`

1. If _options_ was not passed, let _options_ be a new _LockOptions_ dictionary with default members.
1. Let _origin_ be context object’s relevant settings object’s origin.
1. If _origin_ is an opaque origin, return a Promise rejected with a "`SecurityError`" DOMException and abort these steps.
1. Let _scope_ be the set of unique DOMStrings in `scope` if a sequence was passed, otherwise a set containing just the string passed as `scope`
1. If _scope_ is empty, return a new Promise rejected with `TypeError`
1. Return the result of running the **request a lock** algorithm, passing _origin_, _callback_, _scope_, _options_'s _mode_, _options_'s _ifAvailable_, and _options_'s _signal_ (if present).
1. Return the result of running the **request a lock** algorithm, passing _origin_, _callback_, _name_, _options_'s _mode_, _options_'s _ifAvailable_, and _options_'s _signal_ (if present).

#### Algorithm: request a lock

To *request a lock* with _origin_, _callback_, _scope_, _mode_, _ifAvailable_, and optional _signal_:
To *request a lock* with _origin_, _callback_, _name_, _mode_, _ifAvailable_, and optional _signal_:

1. Let _p_ be a new Promise.
1. Let _queue_ be _origin_'s **lock request queue**.
1. Let _held_ be _origin_'s **held lock set**.
1. Let _request_ be a new **lock request** (_scope_, _mode_).
1. Let _request_ be a new **lock request** (_name_, _mode_).
1. If _ifAvailable_ is true and _request_ is not **grantable**, then run these steps:
1. Let _r_ be the result of invoking _callback_ with `null` as the only argument. (Note that _r_ may be a regular completion, an abrupt completion, or an unresolved Promise.)
1. Resolve _p_ with _r_.
Expand All @@ -89,7 +87,7 @@ To *request a lock* with _origin_, _callback_, _scope_, _mode_, _ifAvailable_, a
1. Wait until _request_ is **grantable**
1. Abort any other steps running in parallel.
1. Let _waiting_ be a new Promise.
1. Let _lock_ be a **lock** with **mode** _mode_, **scope** _scope_, and **waiting promise** _waiting_.
1. Let _lock_ be a **lock** with **mode** _mode_, **name** _name_, and **waiting promise** _waiting_.
1. [Remove](https://infra.spec.whatwg.org/#list-remove) _request_ from _queue_
1. [Append](https://infra.spec.whatwg.org/#set-append) _lock_ to _set_
1. Let _r_ be the result of invoking _callback_ with a new `Lock` object associated with _lock_ as the only argument. (Note that _r_ may be a regular completion, an abrupt completion, or an unresolved Promise.)
Expand Down Expand Up @@ -117,13 +115,13 @@ To *request a lock* with _origin_, _callback_, _scope_, _mode_, _ifAvailable_, a
1. Let _pending_ be a new [list](https://infra.spec.whatwg.org/#list).
1. For each _request_ in _origin_'s **lock request queue**:
1. Let _r_ be a new _LockRequest_ dictionary.
1. Set _r_'s `scope` dictionary member to _request_'s **scope**.
1. Set _r_'s `name` dictionary member to _request_'s **name**.
1. Set _r_'s `mode` dictionary member to _request_'s **mode**.
1. [Append](https://infra.spec.whatwg.org/#list-append) _r_ to _pending_.
1. Let _held_ be a new [list](https://infra.spec.whatwg.org/#list).
1. For each _lock_ in _origin_'s **held lock set**:
1. Let _r_ be a new _LockRequest_ dictionary.
1. Set _r_'s `scope` dictionary member to _lock_'s **scope**.
1. Set _r_'s `name` dictionary member to _lock_'s **name**.
1. Set _r_'s `mode` dictionary member to _lock_'s **mode**.
1. [Append](https://infra.spec.whatwg.org/#list-append) _r_ to _held_.
1. Let _state_ be a new `LockState` dictionary.
Expand All @@ -133,16 +131,14 @@ To *request a lock* with _origin_, _callback_, _scope_, _mode_, _ifAvailable_, a
1. Return _p_.


#### `LockManager.prototype.forceRelease(scope)`
#### `LockManager.prototype.forceRelease(name)`

> The intent of this method is for web applications to accomodate unexpected behavior in the applications themselves or in the user agent. A lock released by this method leaves the previous holder in a potentially untested state.
1. Let _origin_ be context object’s relevant settings object’s origin.
1. If _origin_ is an opaque origin, return a Promise rejected with a "`SecurityError`" DOMException and abort these steps.
1. Let _scope_ be the set of unique DOMStrings in `scope` if a sequence was passed, otherwise a set containing just the string passed as `scope`
1. If _scope_ is empty, return a new Promise rejected with `TypeError`
1. Run the following in parallel:
1. [Remove](https://infra.spec.whatwg.org/#list-remove) all members of _origin_'s **held lock set** whose **scope** contains any member of _scope_.
1. [Remove](https://infra.spec.whatwg.org/#list-remove) all members of _origin_'s **held lock set** whose **name** equals _name_.

> The intent is that the removal is atomic. That is, all removals occur before any of the waiting steps in _request a lock_ are allowed to proceed.
Expand Down

0 comments on commit 1c8e905

Please sign in to comment.