-
Notifications
You must be signed in to change notification settings - Fork 161
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
TransformStream cleanup using "Transformer.cancel" #1283
Conversation
43ad8a2
to
147faa3
Compare
147faa3
to
4d62b77
Compare
This commit adds a "cancel" hook to "Transformer". This allows users to perform resource cleanup when the readable side of the TransformStream is cancelled, or the writable side is aborted. To preserve existing behavior, when the readable side is cancelled with a reason, the writable side is always immediately aborted with that same reason. The same is true in the reverse case. This means that the status of both sides is always either "closed", "erroring", or "erroring" when the "cancel" hook is called. "flush" and "cancel" are never both called. As per existing behaviour, when the writable side is closed the "flush" hook is called. If the readable side is cancelled while a promise returned from "flush" is still pending, "cancel" is not called. In this scenario the readable side ends up in the "errored" state, while the writable side ends up in the "closed" state.
4d62b77
to
6ec5260
Compare
Since it's used for both the
Interesting. Calling it before would be better in terms of consistency with the other APIs. I personally think that changing the number of microticks is an acceptable breakage, but others may disagree. |
For reference, exactly one WPT is broken by this. |
What about |
This operation, however, is something we'd like people to implement if they need to do cleanup, so a short friendly name seems appropriate. |
|
I don't particularly like I think we need something that implies early unsuccessful termination. |
@domenic @MattiasBuelens @saschanaz Do you have any thoughts on the "Is the immediate cancellation behaviour correct?" point? I'm leaning towards the currently implemented behaviour because it minimizes chance of breakage, but I do also sympathize with @ricea's points above. |
What about I think calling it before is better. I agree that changing the number of microtasks is acceptable. |
Ok, I'll change that. This opens up a question on error handling: const ts = new TransformStream({
async cancel(reason) {
console.log("t.cancel start with", reason);
await delay(100);
console.log("t.cancel end");
throw "t.cancel error";
},
});
console.log("ts constructed");
ts.readable.cancel("rs.cancel error")
.then(() => console.log("rs.cancel fulfilled"))
.catch((err) => console.log("rs.cancel rejected with", err));
ts.writable.abort("ws.abort error")
.then(() => console.log("ws.abort fulfilled"))
.catch((err) => console.log("ws.abort rejected with", err));
console.log("started rs.cancel and ws.abort"); I think the logical ordering of logs is:
But the following are also possible:
Also consider: const ts = new TransformStream({
flush() { console.log("t.flush") },
async cancel(reason) {
console.log("t.cancel start with", reason);
await delay(100);
console.log("t.cancel end");
throw "t.cancel error";
},
});
console.log("ts constructed");
ts.readable.cancel("rs.cancel error")
.then(() => console.log("rs.cancel fulfilled"))
.catch((err) => console.log("rs.cancel rejected with", err));
ts.writable.close()
.then(() => console.log("ws.close fulfilled"))
.catch((err) => console.log("ws.close rejected with", err));
console.log("started rs.cancel and ws.close"); Here similar options are possible:
These each also have effect on whether the error is visible in Does anyone have preferences here? I think a-1/b-1 make the most sense. |
<p>(Note that there is no need to call | ||
{{TransformStreamDefaultController/terminate()|controller.terminate()}} inside | ||
{{Transformer/cancel|cancel()}}; the stream is already in the process of cancelling/aborting, and | ||
terminating it would be counterproductive.) | ||
|
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.
<p>(Note that there is no need to call | |
{{TransformStreamDefaultController/terminate()|controller.terminate()}} inside | |
{{Transformer/cancel|cancel()}}; the stream is already in the process of cancelling/aborting, and | |
terminating it would be counterproductive.) |
This part is redundant after 1c65d61.
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.
My train of thought is that you could extract the controller
in start
and assign it to a local variable.
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.
Oh, hmm, okay then. Maybe also worth having tests for that btw.
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.
Previously it was impossible because of the immediate close semantics. I just pushed the new semantics, and will add tests for this case.
I fail to see why any of the resulting promises should not reject, so I also prefer a-1/b-1 (and I also think it's okay to have extra microtask). |
What about: const ts = new TransformStream({
flush() { console.log("t.flush") },
async cancel(reason) {
console.log("t.cancel start with", reason);
await delay(100);
console.log("t.cancel end");
throw "t.cancel error";
},
});
console.log("ts constructed");
ts.writable.close()
.then(() => console.log("ws.close fulfilled"))
.catch((err) => console.log("ws.close rejected with", err));
ts.readable.cancel("rs.cancel error")
.then(() => console.log("rs.cancel fulfilled"))
.catch((err) => console.log("rs.cancel rejected with", err));
console.log("started rs.cancel and ws.close");
I'm trying to figure out if it's observable from the outside that the RS was closed through a cancellation rather than a "clean close" (ie is |
I have now implemented a v2 that runs the cancel algorithm prior to cancel / abort of the other side. I have opted for the approach where all of This required updating some test expectations (the ones making the assumption that web-platform-tests/wpt@4156716 PTAL! |
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.
Nothing looks obviously off, which means, LGTM.
For the test part, could we replace await delay(0);
with something else?
index.bs
Outdated
1. Perform ! | ||
[$ReadableStreamDefaultControllerClose$](|readable|.[=ReadableStream/[[controller]]=]). | ||
1. Perform ! [$ReadableStreamDefaultControllerClose$](|readable|.[=ReadableStream/[[controller]]=]). | ||
1. [=Resolve=] |controller|.[=TransformStreamDefaultController/[[finishPromise]]=] with undefined. |
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.
(At the first glance it looked like it was ignoring the error, but in that case it would have finishPromise and thus would return at the above check.)
I could replace them with a fixed set of |
I have one editor approval - does anyone else still want to take a look, or is this good to merge? @MattiasBuelens do we usually do a "final comment period" for stream spec PRs to help keep things moving? |
This needs implementer interest before it's ready to merge. Although I trust @MattiasBuelens , I and other editors might be holding off on further review until we're clear on whether this has a cross-browser future. |
Ok, would be great to start getting some explicit feedback on interest from Chrome / Firefox / Safari then. I would urge folks to look at this with some urgency - it's a very serious omission in @domenic is Chrome interested in shipping this? It sounded in this issue as if you may be interested? Looking it over, it seems like a relatively trivial implementation for Chromium :) @saschanaz can I note down your LGTM as "interested in shipping" for Gecko? @annevk is WebKit interested? (if you are the wrong person to ping, please forward :) ) |
LGTM spec-wise, but not actively pursuing implementation-wise. If others are interested to ship it then Mozilla will too. |
@ricea is the relevant Chromium implementer |
@ricea Is Chrome interested in shipping this? |
This addition makes sense to me as we have more and more web platform objects that benefit from being explicitly closed. |
Yes, Chrome is interested. Sorry for the delay. |
Fantastic, thank you folks! I'll open implementation bugs shortly so we can get this landed 😃 |
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.
Found a few editorial nits, but looks good! Thanks so much for working on this.
As you may or may not be aware, because of the reference implementation landing things in the streams repo is a bit annoying. First we have to land the tests PR, then we roll the submodule, then we land this PR. I'll go merge the tests PR now.
All implementation bugs and the MDN issue have been filed, and the WPT submodule has been updated. This PR is now ready to land :) |
I think all your issues have the wrong titles, using "cleanup" instead of "cancel" :). I'll merge this though. |
@domenic The cancel hook allows for cleanup of transformers backing TransformStreams, so I think it makes some sense? |
There is no "transformer.cleanup" property though. |
Oh I realize my mistake now. Will fix. Thx |
This commit adds a
cancel
hook toTransformer
. This allows users to perform resource cleanup when the readable side of theTransformStream
is cancelled, or the writable side is aborted.To preserve existing behavior, when the readable side is cancelled with a reason, the writable side is always immediately aborted with that same reason. The same is true in the reverse case. This means that the status of both sides is always either
closed
,erroring
, orerroring
when thecancel
hook is called.flush
andcancel
are never both called. As per existing behaviour, when the writable side is closed theflush
hook is called. If the readable side is cancelled while a promise returned fromflush
is still pending,cancel
is not called. In this scenario the readable side ends up in the "errored" state, while the writable side ends up in theclosed
state.I have opted for a
cancel
hook instead of afinally
hook, to mirror the API inWritableStream
- it has one hook for successful completion (close
), and one hook for errored completion (abort
).Transformer
already has aflush
hook for successful completion. The logical addition is ancancel
hook for errored completion.Closes #1212
Open questions:
cancel
the right name? MirrorsReadableStream
, but we useabort
forWritableStream
.cancel
hook before or after cancelling the underlying streams?readable.cancel()
to thewritable
actually being aborted). This is the current behaviour.cancel
hook. This is different toabort
andcancel
hooks inWritableStream
andReadableStream
respectively.Transformer.cancel
web-platform-tests/wpt#40453Preview | Diff