Skip to content

Commit 80d4e06

Browse files
initial commit
0 parents  commit 80d4e06

File tree

1 file changed

+138
-0
lines changed

1 file changed

+138
-0
lines changed

README.md

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
JavaScript Concurrency Control Proposal
2+
=======================================
3+
4+
Stage: 0
5+
6+
Champions: Michael Ficarra, Luca Casonato
7+
8+
Authors: Michael Ficarra, Luca Casonato, Kevin Gibbons
9+
10+
This proposal aims to provide a mechanism for describing a desired amount of concurrency and a coordination mechanism to achieve it. This could be for limiting concurrent access to a shared resource or for setting a target concurrency for an otherwise unbounded workload.
11+
12+
A major motivator for this proposal is the concurrency support in the [async iterator helpers proposal](https://github.com/tc39/proposal-async-iterator-helpers). While that proposal has gone to great lengths to allow for concurrent iteration of its produced async iterators (such as through `map` and `filter`), it does not provide any way to consume async iterators concurrently (such as through `some` or `forEach`). Additionally, there is no mechanism provided by that proposal for generically limiting concurrent iteration of async iterators. This propsal attempts to address those deferred needs.
13+
14+
## Proposal
15+
16+
This proposal consists of 3 major components: a Governor interface, the Semaphore class, and the AsyncIterator.prototype integration.
17+
18+
### Governor
19+
20+
The Governor interface is used for gaining access to a limited resource and later signalling that you are finished with that resource. It is intentionally designed in a way that permits dynamically changing limits.
21+
22+
There is only a single method required by the Governor interface: `acquire`, returning a Promise that eventually resolves with a `GovernorToken`. A `GovernorToken` has a `release` method to indicate that the resource is no longer needed. The `GovernorToken` can also be automatically disposed using `using` syntax from the [Explicit Resource Management proposal](https://github.com/tc39/proposal-explicit-resource-management).
23+
24+
A Governor is meant to control access to resources among mutually trustworthy parties. For adversarial scenarios, a [Capability](https://gist.github.com/michaelficarra/415941f94ed2249b5322d077aeaa6f96) should be used instead.
25+
26+
The Governor name is taken from [the speed-limiting device in motor vehicles](https://en.wikipedia.org/wiki/Governor_%28device%29).
27+
28+
<details>
29+
<summary>
30+
There is also a Governor constructor with helpers on its prototype.
31+
</summary>
32+
33+
The constructor unconditionally throws when it is the `new.target`. To make the helpers available, a concrete Governor can be implemented as follows:
34+
35+
```js
36+
const someGovernor = {
37+
__proto__: Governor.prototype,
38+
acquire() {
39+
// ...
40+
},
41+
};
42+
```
43+
44+
The `with(fn: () => R): Promise<R>` helper takes a function and automatically acquires/releases a GovernorToken. An approximation:
45+
46+
```js
47+
Governor.prototype.with = async (fn) => {
48+
using void = await this.acquire();
49+
return await fn();
50+
};
51+
```
52+
53+
The `wrap(fn: (...args) => R): (...args) => Promise<R>` helper takes a function and returns a function with the same behaviour but limited in its concurrency by this Governor. An approximation:
54+
55+
```js
56+
Governor.prototype.wrap = fn => {
57+
const governor = this;
58+
return async function() {
59+
using void = await governor.acquire();
60+
return await fn.apply(this, arguments);
61+
};
62+
};
63+
```
64+
65+
Similarly, `wrapIterator(it: Iterator<T> | AsyncIterator<T>): AsyncIterator<T>` takes an Iterator or AsyncIterator and returns an AsyncIterator that yields the same values but limited in concurrency by this Governor.
66+
</details>
67+
68+
#### Open Questions
69+
70+
- should the protocol be Symbol-based?
71+
- maybe a sync/throwing acquire?
72+
- `tryAcquire(): GovernorToken`
73+
- or maybe not throwing? `tryAcquire(): GovernorToken | null`
74+
- non-throwing Governor() constructor
75+
- takes an `acquire: () => Promise<GovernorToken>` function
76+
- also takes a `tryAcquire` function?
77+
- easy enough to live without it
78+
79+
### Semaphore
80+
81+
This proposal subsumes Luca's [Semaphore proposal](https://github.com/lucacasonato/proposal-semaphore).
82+
83+
Semaphore is a [counting semaphore](https://en.wikipedia.org/wiki/Semaphore_%28programming%29) that implements the Governor interface and extends Governor. It can be given a non-negative integral Number *capacity* and it is responsible for ensuring that there are no more than that number of active GovernorTokens simultaneously.
84+
85+
#### Open Questions
86+
87+
- are idle listeners useful?
88+
- triggered whenever the Semaphore hits "full" capacity (0 active GovernorTokens)
89+
- `addIdleListener(cb: () => void): void`
90+
- `removeIdleListener(cb: () => void): void`
91+
- callback interface or EventTarget?
92+
- are there concerns about sharing Semaphores across Agents?
93+
94+
### AsyncIterator.prototype integration
95+
96+
This proposal adds an optional concurrency parameter to the following async iterator helper methods:
97+
98+
- `.toArray([ governor ])`
99+
- `.forEach(fn [, governor ])`
100+
- `.some(predicate [, governor ])`
101+
- `.every(predicate [, governor ])`
102+
- `.find(predicate [, governor ])`
103+
- `.reduce(reducer [, initialValue [, governor ]])`
104+
105+
When not passed, these methods operate serially, as they do in the async iterator helpers proposal.
106+
107+
This proposal also adds a `limit(governor)` method (the dual of `governor.wrapIterator(iterator)`) that returns a concurrency-limited AsyncIterator.
108+
109+
Because Semaphore will be an extremely commonly-used Governor, anywhere a Governor is accepted in any AsyncIterator.prototype method, a non-negative integral Number may be passed instead. It will be treated as if a Semaphore with that capacity was passed. Because of this, we are able to widen the first parameter of the `buffered` helper to accept a Governor in addition to the non-negative integral Number that it accepts as part of the async iterator helpers proposal.
110+
111+
#### Open Questions
112+
113+
- `reduce` parameter order: gross?
114+
- `buffered` parameter order
115+
116+
## Proposal Summary
117+
118+
- Governors
119+
- Governor Interface
120+
- `acquire(): Promise<GovernorToken>`
121+
- Governor() constructor
122+
- throws when constructed directly
123+
- Governor.prototype
124+
- `with(fn: () => R): Promise<R>`
125+
- `wrap(fn: (...args) => R): (...args) => Promise<R>`
126+
- `wrapIterator(it: Iterator<T> | AsyncIterator<T>): AsyncIterator<T>`
127+
- GovernorToken.prototype
128+
- `release(): void` === `[Symbol.dispose](): void`
129+
- Semaphores
130+
- `Semaphore(capacity: number)` constructor
131+
- extending Governor
132+
- implementing the Governor interface
133+
- shareable across threads
134+
- AsyncIterator.prototype
135+
- `buffered(limit: Governor | integer, prepopulate = false)`
136+
- `limit(limit: Governor | integer)`
137+
- a concurrency param (`Governor | integer`) added to all consuming methods
138+

0 commit comments

Comments
 (0)