Skip to content

Commit aa25e64

Browse files
authored
Add 'Component Instance Lifetime' section to Concurrency.md (#643)
* Add 'Component Instance Lifetime' section to Concurrency.md * Tweak wording about future plans to not presume the exact solution
1 parent 6b01cc4 commit aa25e64

1 file changed

Lines changed: 107 additions & 0 deletions

File tree

design/mvp/Concurrency.md

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ emojis. For an even higher-level introduction, see [these][wasmio-2024]
2929
* [Async Import ABI](#async-import-abi)
3030
* [Async Export ABI](#async-export-abi)
3131
* [Examples](#examples)
32+
* [Component Instance Lifetime](#component-instance-lifetime)
3233
* [TODO](#todo)
3334

3435

@@ -1261,6 +1262,105 @@ return values from `waitable-set.wait` in the previous example. The precise mean
12611262
these values is defined by the Canonical ABI.
12621263

12631264

1265+
## Component Instance Lifetime
1266+
1267+
In settings like [service workers] and [serverless computing], a single
1268+
component instance may handle multiple independent host events by having its
1269+
exported functions called repeatedly (and, if they're `async`, concurrently). In
1270+
settings like these, the number and lifetime of component instances and the
1271+
degree of reuse is determined by host policies based on factors like
1272+
utilization, available parallelism and security. However, when component
1273+
instance lifetimes are flexible in this manner and don't have an obvious end
1274+
(as opposed to a traditional CLI setting, where a component instance's lifetime
1275+
conventionally ends right after `main()` returns), the host still needs to
1276+
understand the expectations of component authors to enable portability.
1277+
1278+
Before the addition of native concurrency support in Preview 3, a natural
1279+
expectation is that, in the absence of atypical scenarios like timeouts or quota
1280+
exhaustion, a component author can expect that their component instance will not
1281+
be abruptly terminated during the execution of contained Core WebAssembly code.
1282+
But other than that, component authors must conservatively assume that their
1283+
component instance will be torn down at any time.
1284+
1285+
With the addition of native concurrency support, these expectations must be
1286+
nuanced to account for asynchronous Core WebAssembly execution. In particular,
1287+
when using the stackless `async callback` ABI, an active task may have no active
1288+
Core WebAssembly function invocation while it is `WAIT`ing on a Waitable Set.
1289+
Analogously, when using the stackful `async` ABI, a Core WebAssembly function
1290+
invocation will be suspended (as-if by the [stack-switching] proposal's
1291+
`suspend` instruction) when it calls `waitable-set.wait` so that there is also
1292+
no Core WebAssembly code actively executing (only a continuation stored in the
1293+
component instance's table of threads).
1294+
1295+
Now, before a task has [returned](#returning) its value, there is still a
1296+
natural expectation that, even if there is *currently* no active Core
1297+
WebAssembly execution, the task is still *logically* executing and thus the
1298+
component instance will not be terminated (under normal circumstances; timeout
1299+
or quota exhaustion could still abruptly terminate the instance). Furthermore,
1300+
even if a component instance has no active tasks that haven't returned a value,
1301+
if a component instance is holding the readable or writable end of a stream or
1302+
future that the host holds the other end of, there is also a natural expectation
1303+
that the host will keep the component instance alive until all the futures and
1304+
streams have reached a closed state.
1305+
1306+
As an example, even after `wasi:http/handler`'s `handle` function *returns* a
1307+
`response` resource, the `handle` function's component instance is expected to
1308+
be kept alive as long as it's holding the un-closed writable end of the
1309+
`stream<u8>` contained by the returned `response`. If, on the other hand, the
1310+
`stream<u8>` was forwarded from the return value of a host import, and so the
1311+
component instance is not holding any writable end, this expectation doesn't
1312+
apply and so all other conditions being met, the component instance can be
1313+
eagerly torn down.
1314+
1315+
The interesting question is what happens once all tasks have returned their
1316+
values *and* all incoming and outgoing streams and futures have been closed if
1317+
the component instance still contains live [threads](#threads-and-tasks) that
1318+
are currently suspended (and so not actively executing Core WebAssembly code)
1319+
but may potentially be resumed in the future to do important post-return work
1320+
(like performing logging, billing or metrics operations that have been taken off
1321+
the pre-return critical path for peformance reasons).
1322+
1323+
If the component instance is conservatively kept alive (until a hard timeout),
1324+
this may end up wasting resources for periodic background activities that run in
1325+
an infinite (waiting) loop. In particular, cooperative threads used to implement
1326+
pthreads are expected to sometimes be used in this manner. On the other hand,
1327+
immediately tearing down a component instance as soon as the last byte of an
1328+
outgoing stream is written and active Core WebAssembly execution returns or
1329+
suspends will break the abovementioned post-return use cases if they involve
1330+
waiting on `async` operations to complete.
1331+
1332+
To resolve this tension, threads are implicitly distinguished by a "keep-alive"
1333+
flag that determines whether the expectation is that the existence of the thread
1334+
is intended to keep the containing component instance alive. In the initial
1335+
release of Preview 3, this "keep-alive" flag is default *set* for the [implicit
1336+
thread](#summary) created for a task and default *cleared* for the explicit
1337+
threads created by `thread.new-indirect`. In particular, this means that an
1338+
`async callback`-lifted function will keep its containing component instance
1339+
alive until it returns the `EXIT` code (`0`).
1340+
1341+
As an example, in JavaScript, the Service Worker API's [`waitUntil`] method
1342+
would delay returning the `EXIT` code. In the initial 0.3.0 release without
1343+
cooperative threads (🧵), [`setInterval`] would also unfortunately delay
1344+
returning the `EXIT` code and thus, without guest code intervention, would keep
1345+
component instances alive until timeout limits were hit. The release of
1346+
cooperative threads would offer a solution to this problem, but an awkward one.
1347+
Instead, the [intention](#TODO) is to add new built-in functions that would
1348+
provide guest code more direct, dynamic control over its own keep-alive
1349+
flags, thereby allowing the JS event loop to clear its keep-alive flag once all
1350+
`waitUntil` promises resolved, thereby allowing `setInterval` callbacks to keep
1351+
running (while the host wants to keep the instance warm), but still indicating
1352+
to the host that destruction is welcome at any time.
1353+
1354+
Lastly, the above discussion refers to component *instances*, however the host
1355+
cannot tear down independent component instances when they are linked together
1356+
(as this would leave dangling function imports). In general, components must be
1357+
instantiated and destroyed as *trees*, where the host can only choose when to
1358+
instantiate or destroy the *root* component of the tree, and all other child
1359+
instances are instantiated/destroyed along with the root. Thus, when the above
1360+
rules set an expectation that any component instance in a tree be kept alive,
1361+
the whole tree would be kept alive.
1362+
1363+
12641364
## TODO
12651365

12661366
Native async support is being proposed incrementally. The following features
@@ -1272,6 +1372,8 @@ comes after:
12721372
* zero-copy forwarding/splicing
12731373
* allow the `stream<char>` type to validate; make it use `string-encoding`
12741374
and not split code points
1375+
* add built-ins providing guest code more control over its containing
1376+
[component instance's lifetime](#component-instance-lifetime)
12751377
* some way to say "no more elements are coming for a while"
12761378
* add an `async` effect on `component` type definitions allowing a component
12771379
type to block during instantiation
@@ -1312,11 +1414,16 @@ comes after:
13121414
[Overlapped I/O]: https://en.wikipedia.org/wiki/Overlapped_I/O
13131415
[`io_uring`]: https://en.wikipedia.org/wiki/Io_uring
13141416
[`epoll`]: https://en.wikipedia.org/wiki/Epoll
1417+
[Serverless Computing]: https://en.wikipedia.org/wiki/Serverless_computing
13151418

13161419
[`select`]: https://pubs.opengroup.org/onlinepubs/007908799/xsh/select.html
13171420
[`O_NONBLOCK`]: https://pubs.opengroup.org/onlinepubs/7908799/xsh/open.html
13181421
[`pthread_create`]: https://pubs.opengroup.org/onlinepubs/7908799/xsh/pthread_create.html
13191422

1423+
[Service Workers]: https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API
1424+
[`waitUntil`]: https://developer.mozilla.org/en-US/docs/Web/API/ExtendableEvent/waitUntil
1425+
[`setInterval`]: https://developer.mozilla.org/en-US/docs/Web/API/Window/setInterval
1426+
13201427
[AST Explainer]: Explainer.md
13211428
[Canonical Built-in]: Explainer.md#canonical-built-ins
13221429
[`context.get`]: Explainer.md#-contextget

0 commit comments

Comments
 (0)