Skip to content

Commit 02df846

Browse files
committed
feat(kernel): process callbacks while EndRequest is awaiting promise
1 parent 381ae70 commit 02df846

File tree

17 files changed

+668
-170
lines changed

17 files changed

+668
-170
lines changed

CONTRIBUTING.md

Lines changed: 13 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,12 @@
11
# Contributing to jsii
2-
32
Thanks for your interest in contributing to AWS JSII! :heart:
43

54
This document describes how to set up a development environment and submit your
65
contributions. Please read it carefully and let us know if it's not up-to date
76
(or even better, submit a pull request with your corrections! :wink:).
87

98
## Pre-requisites
10-
119
### Setup Docker image
12-
1310
Due to the polyglot nature of `jsii`, the toolchain requirements are somewhat
1411
more complicated than for most projects. In order to locally develop `jsii`, you
1512
will need a number of tools.
@@ -22,38 +19,37 @@ our own CI/CD: the ["superchain" image][superchain] from.
2219
The image can be built for local usage, too:
2320

2421
```console
25-
IMAGE=superchain
26-
docker build -t ${IMAGE} ./superchain
22+
$ IMAGE=superchain
23+
$ docker build -t ${IMAGE} ./superchain
2724
```
2825

2926
In order to get an interactive shell within a Docker container using the
3027
*superchain* image you just built:
3128

3229
```console
33-
cd jsii # go to the root of the jsii repo
34-
docker run --rm --net=host -it -v $PWD:$PWD -w $PWD ${IMAGE}
30+
$ cd jsii # go to the root of the jsii repo
31+
$ docker run --rm --net=host -it -v $PWD:$PWD -w $PWD ${IMAGE}
3532
```
3633

3734
In the shell that pops up, the `npm run` commands in the following sections must
3835
be executed.
3936

4037
### Alternative: Manually install the toolchain
41-
4238
The following tools need to be installed to develop on JSII locally. We recommend
4339
using the docker image from the above section, but if you wish to, you can install
4440
in your development environment.
4541

4642
- [Node `14.6.0`] or later
4743
- [Yarn `1.19.1`] or later
4844
- An OpenJDK-8 distribution (e.g: [Oracle's OpenJDK8], [Amazon Corretto 8])
49-
- [`maven >= 3.0.5`](https://maven.apache.org)
45+
+ [`maven >= 3.0.5`](https://maven.apache.org)
5046
- [.NET Core `3.1`] or later
51-
- *Recommended:* [`mono >= 5`](https://www.mono-project.com)
47+
+ *Recommended:* [`mono >= 5`](https://www.mono-project.com)
5248
- [Python `3.7.3`] or later
53-
- [`pip`](https://pip.pypa.io/en/stable/installing/)
54-
- [`setuptools >= 38.6.0`](https://pypi.org/project/setuptools/)
55-
- [`wheel`](https://pypi.org/project/wheel/)
56-
- *Recommended:* [`twine`](https://pypi.org/project/twine/)
49+
+ [`pip`](https://pip.pypa.io/en/stable/installing/)
50+
+ [`setuptools >= 38.6.0`](https://pypi.org/project/setuptools/)
51+
+ [`wheel`](https://pypi.org/project/wheel/)
52+
+ *Recommended:* [`twine`](https://pypi.org/project/twine/)
5753
- [Go] `1.18` or newer
5854

5955
[Node `14.6.0`]: https://nodejs.org/download/release/v14.6.0/
@@ -65,15 +61,14 @@ in your development environment.
6561
[Go]: https://go.dev/dl/
6662

6763
## Getting Started
68-
6964
### Bootstrapping
7065

7166
The project is managed as a [monorepo] using [lerna].
7267

7368
[monorepo]: https://github.com/babel/babel/blob/main/doc/design/monorepo.md
7469
[lerna]: https://github.com/lerna/lerna
7570

76-
1. Check out this repository and change directory to its root.
71+
1. Check out this respository and change directory to its root.
7772
2. Run `yarn install && yarn build` to install lerna, bootstrap the repository
7873
and perform an initial build and test cycle.
7974

@@ -145,10 +140,10 @@ The runtime client library should be implemented as a module under
145140

146141
The jsii runtime client library usually includes the following components:
147142

148-
- Child process manager: responsible to start/stop the __@jsii/runtime__ child
143+
- Child process manager: responsible to start/stop the **@jsii/runtime** child
149144
process.
150145
- Protocol layer: implements the STDIN/STDOUT protocol that interacts with the
151-
__@jsii/runtime__.
146+
**@jsii/runtime**.
152147
- Proxy layer: includes base classes and serialization utilities to implement
153148
the generated proxy classes.
154149

@@ -168,7 +163,6 @@ The [Python](./packages/jsii-pacmak/lib/targets/python.ts) target is a good
168163
example to work from.
169164

170165
## Releasing
171-
172166
### The `jsii/superchain` Docker image
173167

174168
Upon merging new changes to the `main` branch, the `jsii/superchain:nightly`

packages/@jsii/kernel/src/api.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,8 @@ export function isPropertyOverride(value: Override): value is PropertyOverride {
7373

7474
export interface Callback {
7575
readonly cbid: string;
76+
/** Whether this callback is synchronous. */
77+
readonly sync: boolean;
7678
readonly cookie: string | undefined;
7779
readonly invoke?: InvokeRequest;
7880
readonly get?: GetRequest;

packages/@jsii/kernel/src/kernel.test.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2212,6 +2212,36 @@ defineTest('invokeBinScript() accepts arguments', (sandbox) => {
22122212
});
22132213
});
22142214

2215+
// defineTest('ImplementationFromAsyncContext compliance', async (sandbox) => {
2216+
// const producer = sandbox.create({
2217+
// fqn: 'Object',
2218+
// overrides: [{ method: 'produce', cookie: 'produce1234' }],
2219+
// interfaces: ['jsii-calc.IPromiseProducer'],
2220+
// });
2221+
2222+
// const obj = sandbox.create({
2223+
// fqn: 'jsii-calc.ImplementationFromAsyncContext',
2224+
// args: [producer],
2225+
// });
2226+
2227+
// const promise1 = sandbox.begin({
2228+
// objref: obj,
2229+
// method: 'doAsyncWork',
2230+
// });
2231+
2232+
// const callbacks1 = sandbox.callbacks();
2233+
// expect(callbacks1.callbacks.length).toBe(1);
2234+
// expect(callbacks1.callbacks[0].cookie).toBe('produce1234');
2235+
2236+
// sandbox.complete({
2237+
// cbid: callbacks1.callbacks[0].cbid,
2238+
// result: 'test-string',
2239+
// });
2240+
2241+
// const result = (await sandbox.end(promise1)).result;
2242+
// expect(result).toBe('test-string');
2243+
// });
2244+
22152245
// =================================================================================================
22162246

22172247
const testNames: { [name: string]: boolean } = {};

packages/@jsii/kernel/src/kernel.ts

Lines changed: 52 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -447,7 +447,52 @@ export class Kernel {
447447

448448
let result;
449449
try {
450-
result = await promise;
450+
let settled = false;
451+
/**
452+
* Poll for new callback requests until the promise is resolved. This is
453+
* to allow any promises necessary for the promise to be able to settle.
454+
* We use setImmediate so the next poll happens on the next run loop tick,
455+
* after other microtasks might have been paused on a pending callback.
456+
*/
457+
// eslint-disable-next-line no-inner-declarations
458+
function pollForCallbacks(kernel: Kernel) {
459+
// Promise has settled already, not going any further...
460+
if (settled) {
461+
return;
462+
}
463+
464+
for (const [cbid, cb] of kernel.cbs.entries()) {
465+
kernel.waiting.set(cbid, cb);
466+
kernel.cbs.delete(cbid);
467+
try {
468+
cb.succeed(
469+
kernel.callbackHandler({
470+
cbid,
471+
sync: false,
472+
cookie: cb.override.cookie,
473+
invoke: {
474+
objref: cb.objref,
475+
method: cb.override.method,
476+
args: cb.args,
477+
},
478+
}),
479+
);
480+
} catch (err) {
481+
cb.fail(err);
482+
} finally {
483+
kernel.waiting.delete(cbid);
484+
}
485+
}
486+
if (!settled) {
487+
setImmediate(pollForCallbacks, kernel);
488+
}
489+
}
490+
pollForCallbacks(this);
491+
492+
result = await promise.finally(() => {
493+
settled = true;
494+
});
495+
451496
this._debug('promise result:', result);
452497
} catch (e: any) {
453498
this._debug('promise error:', e);
@@ -475,14 +520,16 @@ export class Kernel {
475520
};
476521
}
477522

523+
/** @deprecated the flow should be handled directly by "end" */
478524
public callbacks(_req?: api.CallbacksRequest): api.CallbacksResponse {
479525
this._debug('callbacks');
480526
const ret = Array.from(this.cbs.entries()).map(([cbid, cb]) => {
481527
this.waiting.set(cbid, cb); // move to waiting
482528
this.cbs.delete(cbid); // remove from created
483529
const callback: api.Callback = {
484-
cbid,
485530
cookie: cb.override.cookie,
531+
cbid,
532+
sync: false,
486533
invoke: {
487534
objref: cb.objref,
488535
method: cb.override.method,
@@ -758,6 +805,7 @@ export class Kernel {
758805
const result = this.callbackHandler({
759806
cookie: override.cookie,
760807
cbid: this._makecbid(),
808+
sync: true,
761809
get: { objref, property: propertyName },
762810
});
763811
this._debug('callback returned', result);
@@ -774,6 +822,7 @@ export class Kernel {
774822
this.callbackHandler({
775823
cookie: override.cookie,
776824
cbid: this._makecbid(),
825+
sync: true,
777826
set: {
778827
objref,
779828
property: propertyName,
@@ -910,6 +959,7 @@ export class Kernel {
910959
const result = this.callbackHandler({
911960
cookie: override.cookie,
912961
cbid: this._makecbid(),
962+
sync: true,
913963
invoke: {
914964
objref,
915965
method: methodName,

packages/@jsii/python-runtime/src/jsii/_kernel/__init__.py

Lines changed: 7 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import datetime
22
import inspect
33
import itertools
4+
import time
45
from types import FunctionType, MethodType, BuiltinFunctionType, LambdaType
56

67
from typing import Callable, cast, Any, List, Optional, Sequence, Type
@@ -28,6 +29,7 @@
2829
CreateResponse,
2930
DeleteRequest,
3031
EndRequest,
32+
EndResponse,
3133
EnumRef,
3234
GetRequest,
3335
GetResponse,
@@ -466,26 +468,11 @@ def ainvoke(self, obj: Any, method: str, args: Optional[List[Any]] = None) -> An
466468
if isinstance(promise, Callback):
467469
promise = _callback_till_result(self, promise, BeginResponse)
468470

469-
callbacks = self.provider.callbacks(CallbacksRequest()).callbacks
470-
while callbacks:
471-
for callback in callbacks:
472-
try:
473-
result = _handle_callback(self, callback)
474-
except Exception as exc:
475-
# TODO: Maybe we want to print the whole traceback here?
476-
complete = self.provider.complete(
477-
CompleteRequest(cbid=callback.cbid, err=str(exc))
478-
)
479-
else:
480-
complete = self.provider.complete(
481-
CompleteRequest(cbid=callback.cbid, result=result)
482-
)
483-
484-
assert complete.cbid == callback.cbid
485-
486-
callbacks = self.provider.callbacks(CallbacksRequest()).callbacks
487-
488-
return self.provider.end(EndRequest(promiseid=promise.promiseid)).result
471+
response = self.provider.end(EndRequest(promiseid=promise.promiseid))
472+
if isinstance(response, Callback):
473+
return _callback_till_result(self, response, EndResponse).result
474+
else:
475+
return response.result
489476

490477
def stats(self):
491478
resp = self.provider.stats(StatsRequest())

packages/@jsii/python-runtime/src/jsii/_kernel/providers/process.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -291,7 +291,7 @@ def stop(self):
291291
assert self._process.stdin is not None
292292
if not self._process.stdin.closed:
293293
self._process.stdin.write(b'{"exit":0}\n')
294-
# Close the process' STDIN, singaling we are done with it
294+
# Close the process' STDIN, signaling we are done with it
295295
self._process.stdin.close()
296296

297297
try:

packages/@jsii/python-runtime/src/jsii/_kernel/types.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -264,6 +264,7 @@ class StatsResponse:
264264
LoadResponse,
265265
CreateResponse,
266266
DeleteResponse,
267+
EndResponse,
267268
GetResponse,
268269
InvokeResponse,
269270
InvokeScriptResponse,

packages/@jsii/runtime/lib/host.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ export class KernelHost {
8282
return this.processRequest(
8383
req,
8484
completeCallback.bind(this),
85-
/* sync */ true,
85+
callback.sync,
8686
);
8787
}
8888
}
@@ -126,7 +126,7 @@ export class KernelHost {
126126
// promises. see the kernel test 'async overrides: two overrides'
127127
// for an example for this use case.
128128
if (apiReq.api === 'begin' || apiReq.api === 'complete') {
129-
checkIfAsyncIsAllowed();
129+
assertAsyncIsAllowed();
130130

131131
this.debug('processing pending promises before responding');
132132

@@ -141,7 +141,7 @@ export class KernelHost {
141141
// if this is an async method, return immediately and
142142
// call next only when the promise is fulfilled.
143143
if (this.isPromise(ret)) {
144-
checkIfAsyncIsAllowed();
144+
assertAsyncIsAllowed();
145145

146146
this.debug('waiting for promise to be fulfilled');
147147

@@ -169,7 +169,7 @@ export class KernelHost {
169169
// indicate this request was processed (synchronously).
170170
return next();
171171

172-
function checkIfAsyncIsAllowed() {
172+
function assertAsyncIsAllowed() {
173173
if (sync) {
174174
throw new JsiiFault(
175175
'Cannot handle async operations while waiting for a sync callback to return',

packages/@jsii/runtime/test/__snapshots__/kernel-host.test.js.snap

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)