-
Notifications
You must be signed in to change notification settings - Fork 72
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
RFC 90: [testdriver] Primitives for cross-context messaging #90
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,122 @@ | ||
# RFC 90: `testdriver` Add a cross-context messaging primitive | ||
|
||
## Summary | ||
|
||
Add `send`/`poll`/`recv` methods to testdriver that can be used to | ||
send messages to other browsing contexts, and receive messages for | ||
the current browsing context. | ||
|
||
## Details | ||
|
||
In tests that involve multiple origins, or multiple browsing context | ||
groups, there's often a need to make asserts about the behaviour of | ||
browsing contexts other than the test context. This problem has been | ||
solved by the COOP tests with | ||
[dispatcher.js](https://github.com/web-platform-tests/wpt/blob/7b0ebaccc62b566a1965396e5be7bb2bc06f841f/html/cross-origin-opener-policy/resources/dispatcher.js). This | ||
provides functions `send(uuid, message)` and `receive(uuid, | ||
maybe_timeout)` which are used to send and recieve messages between | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: "receive" (×2) |
||
contexts given a shared key `uuid`. Typically the way this works is a | ||
specific resource load is given a `uuid` via a query parameter in the | ||
url. That resource will then `recieve()` messages using that `uuid` | ||
whereas other contexts, such as that containing the main test, can | ||
send messages using `uuid`. | ||
|
||
The backend transport layer is the wptserve stash, which stores a list | ||
of messages per `uuid`. A Python script `pop()`s the message from the | ||
from of the queue when it handles a `GET` request with `uuid` as a | ||
query parameter, and pushes the request body to the stash when it | ||
handles a `POST` request, also with a `uuid` parameter. | ||
|
||
This functionality is useful enough that it was adopted for Chrome's | ||
bfcache tests, which suggests it's likely to be useful as a primitive | ||
in many cases where tests involve multiple contexts or navigations. | ||
|
||
It is proposed that we lift this functionality into testdriver itself, | ||
integrating it with the context system used to target. The underlying | ||
communication is poll-based, so as a generalisation we expose three | ||
functions on `test_driver`, each of which returns a promise that is | ||
resolved when the relevant action is complete: | ||
|
||
``` | ||
test_driver.send(message, context); | ||
test_driver.poll(); | ||
test_driver.recv(timeout=null) | ||
Comment on lines
+42
to
+43
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Shouldn't those specify the uuid of the queue your are receiving messages? It is highly important to be able to listen to different queues. You might want to control multiple context or run multiple subtest in parallel. Having multiple queues is very important. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The way it's set up at the moment, you can only listen for messages sent to your queue. But each message has a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. How can this be used to run several subtest in parallel? Having a single queue per context is a strong limitation. As is, I don't believe this solution could replace code from: There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Presumably you could have some code like:
And then a test would do something like
So I don't think it's impossible, but I agree there's some additional overhead from the fact that the message queues are per context. But it feels like the per-context model is closer to the model for other testdriver commands. Do you think that adding some additional built-in API surface for this kind of message dispatch would be enough to make the solution work for you, or do you have another suggested approach? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why does it has to belong to testdriver? I am not sure to understand the benefits. Maybe if I knew, they could potentially offset the drawback mentioned above? I liked a lot hiroshige@ patches. Keep the message queue API very simple. Every context can pop/push to any arbitrary queue. Messages are simple string. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I forgot... Most COEP:credentials are built this way:
So there are multiple message queues to communicate in each pair (driver, helper) contexts. With the current solution, it is limited to one queue per receiver context. As you said, we can simulate more by dispatching per sender context. So at most we have one queue per (receiver, sender) pair, but this is not enough. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It doesn't have to of course. But the reason for proposing this is to end up with a unified way of working with remote contexts, rather than having the concept of a remote context with a bunch of functionality like invoking user gestures, or running script, and a semi-decoupled model of messaging where the message queue ids are kind of coupled to context ids (because we give contexts an id through the URL), but anyone with the id has full read/write access. The model here seems closer to other things like I suppose another solution to the multi-test problem above would be introducing the ability to create non-context-bound message queues like the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I am thinking, what about:
|
||
``` | ||
|
||
`test_driver.send(mesage, context)` Sends `message` to the context | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: "message" There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Optional: I like specifying the target context/uuid before the messages.: test_driver.send(context, `
Hello from a different context
`); There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. All the other testdriver commands take the context as the final argument, which is why I went with that order, even though I agree that this is more natural. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ACK. That's unfortunate, but not a big deal. |
||
`context`. `message` must be JSON-encodable. Unlike the existing | ||
implementation we don't send `message` directly, but instead wrap it | ||
in a js object like: | ||
|
||
``` | ||
{ | ||
src: <uuid of sending context>, | ||
data: message, | ||
} | ||
``` | ||
Including `src` in the wrapper means we're always able to reply to a | ||
message without needing to explictly communicate the uuid for the | ||
source context. | ||
|
||
`test_driver.poll()` polls the server for a message with the `uuid` | ||
for the current context. The return value is `null` if there is no | ||
message, or an object representing the wrapped message, if there is. | ||
|
||
`test_driver.recv(timeout=null)` waits at most `timeout` ms to receive | ||
a message, and throws an error if no message is received. If the | ||
timeout is null, the function polls indefinitely. | ||
|
||
As with the existing implementation, the backend is based on the | ||
server stash. Since this is standard wpt infrastructure not tied to | ||
wptrunner, it's possible to provide an implementation directly in | ||
`testdriver.js` as part of `test_driver_internal`, meaning that it can | ||
be overridden with a different transport if required, but there is no | ||
downstream work required for implementors as part of this feature. | ||
|
||
The use of the stash does present some promises; to avoid races if | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Promises? Or challenges? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It promises some challenges :p (but yes, good catch). |
||
multiple processes try to access the same stash item we need to take a | ||
lock. This implies a performance cost to using the feature since all | ||
requests are serialised, even if there's multiprocessing. This could | ||
be reduced by using a per-key lock rather than a single global. The | ||
existing code also has a limit to the number of concurrent requests on | ||
the js side to avoid overwhelming the Python server. | ||
|
||
## Example | ||
|
||
``` | ||
let uuid = "{{uuid()}}"; | ||
window.open(`child.html?uuid=${uuid}`); | ||
await test_driver.send("start_test", uuid); | ||
let result = await test_driver.recv(uuid); | ||
assert_equals(result, "PASS"); | ||
``` | ||
|
||
## Risks | ||
|
||
The model in which we send messages to other contexts to get them to | ||
do work leaves authors with a lot of challenges for e.g. error | ||
handling. If the remote end throws an unexpected exception and fails | ||
to reply as expected it's likely we end up with a hard-to-debug | ||
timeout. It would be beneficial to supply some higher-level | ||
functionality for running parts of tests outside the main test window | ||
and communicating the result back to the text context. | ||
|
||
The promise-based polling model means that the default style of | ||
communication is command/response, where we `send()` a message to a | ||
remote context and `await recv()` a result. To implement something like | ||
events from the remote it would be necessary to build a custom event | ||
loop that continually polls and updates local state as | ||
necessary. Although this is possible, it would require repeated | ||
polling of the server, which is inefficient. This could be addressed | ||
by using Server-Sent-Events or websockets as the transport | ||
mechanism. These also expose incoming messages as DOM events rather | ||
than as promises. This probably makes simple cases more complex, but | ||
is easier in cases where there are an unknown number of events. | ||
|
||
## References | ||
|
||
[RFC 86](https://github.com/web-platform-tests/rfcs/pull/86) contains | ||
some discussion of the existing implementation and its reuse for bfcache. | ||
|
||
[PR 29803](https://github.com/web-platform-tests/wpt/pull/29803) | ||
contains a prototype implementation of this. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Where does the UUID come from?
You said we can't generate them at will, but there will be instead one per context predefined.
Do we have an UUID for every type of context? (document, sharedworker, dedicatedworker, serviceworker).
How do you convey the uuid from one context to the other?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess they come from the uuid params of the URL?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See #88