-
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
Unified error handling for WritableStream #721
Conversation
Needs a proper commit message writing, but you can see the behaviour changes from the tests. PTAL. The standard text changes are not written yet. |
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.
Looking pretty great... will review tests next...
|
||
assert(state === 'writable', 'state must be writable'); | ||
|
||
const error = new TypeError('Requested to abort'); | ||
if (stream._pendingAbortRequest !== undefined) { | ||
if (stream._pendingAbortRequest === true) { |
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.
Is it a boolean or a promise?
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.
It's a promise. Well, actually a record containing a promise and a reason. Great, now I need to work out whether this is a test gap or a no-op.
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.
npm run coverage
may help!
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.
It was a no-op. There was a correct check above. I have removed the redundant check.
Coverage says the real check was being tested, but I can't find the test that does it. Maybe it is a piping one? Anyway, I added an explicit test for double aborting just to make it clear.
@@ -405,7 +383,7 @@ class WritableStreamDefaultWriter { | |||
const state = stream._state; | |||
|
|||
if (state === 'writable') { | |||
if (stream._pendingAbortRequest !== undefined) { | |||
if (stream._pendingError === true) { | |||
const error = new TypeError('Requested to abort'); |
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.
Error message needs tweaking. Or should we use _storedError? I think that would be consistent with e.g. write().
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.
It's also consistent with the 'errored' state, so I've gone with that.
@@ -567,7 +542,7 @@ function WritableStreamDefaultWriterClose(writer) { | |||
stream._closeRequest = closeRequest; | |||
}); | |||
|
|||
if (stream._backpressure === true) { | |||
if (stream._backpressure === true && !stream._pendingError) { |
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.
Nit: === false to match spec text
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.
_pendingError is gone in favour of state 'erroring'.
} | ||
writer._closedPromise.catch(() => {}); | ||
// The state transitions to "errored" before the sink abort() method runs, but the writer.closed promise is not | ||
// rejected until afterwards. This means that simply testing state will not work |
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.
Nit: period at end of sentence.
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.
Done
@@ -711,8 +691,12 @@ class WritableStreamDefaultController { | |||
throw new TypeError( | |||
'WritableStreamDefaultController.prototype.error can only be used on a WritableStreamDefaultController'); | |||
} | |||
const stream = this._controlledWritableStream; | |||
if (stream._pendingError) { |
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.
Nit: === true
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.
Gone.
} else { | ||
WritableStreamHandleAbortRequestIfPending(stream); | ||
} | ||
|
||
// This is a no-op if the stream was errored above. |
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.
Is this comment still accurate?
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.
It's no longer meaningful. I have removed it.
assert(stream._state === 'writable' || stream._state === 'errored'); | ||
WritableStreamDefaultControllerErrorIfNeeded(this, r); | ||
WritableStreamRejectAbortRequestIfPending(stream); | ||
assert(stream._state === 'writable'); |
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.
Maybe this assert should be in both branches now?
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.
Done.
@@ -12,6 +12,9 @@ const ErrorSteps = Symbol('[[ErrorSteps]]'); | |||
class WritableStream { | |||
constructor(underlyingSink = {}, { size, highWaterMark = 1 } = {}) { | |||
this._state = 'writable'; | |||
|
|||
// The error that will be reported by new method calls once the state becomes errored. Only set when [[state]] is | |||
// 'errored' or [[pendingError]] is true. May be set to an undefined value. |
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.
Mentions "pendingError"
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.
Fixed.
@@ -42,6 +45,9 @@ class WritableStream { | |||
// The backpressure signal set by the controller. | |||
this._backpressure = false; | |||
|
|||
// True if an error is pending on the stream. | |||
this._pendingError = false; |
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.
No longer necessary
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.
Oops. Removed.
function WritableStreamError(stream, error) { | ||
stream._state = 'errored'; | ||
stream._storedError = error; | ||
function WritableStreamSetPendingError(stream, reason) { |
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.
Maybe this should be renamed WritableStreamBecomeErroring or similar? Might be fine as-is.
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.
Looking at existing abstract operations, it seemed like "WritableStreamErroring" was the most consistent option, so I went with that.
code changes lgtm, although @tyoshino always catches good bugs, so I'd really like his review. |
Oh, I guess changing controller.error() to no-op if erroring/errored would be a good idea actually (per my comment at web-platform-tests/wpt@f196be0#commitcomment-21575312 ) |
The standard changes are ready for review now. |
Can you rebase/resolve the test conflict so that a branch snapshot gets generated? |
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 some nits :)
index.bs
Outdated
! WritableStreamFinishError(_stream_). | ||
</emu-alg> | ||
|
||
<h4 id="writable-stream-finish-error" aoid="WritableStreamFinishError" nothrow>WritableStreamFinishError |
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.
Maybe the abstract ops should be named "WritableStreamStartErroring" and "WritableStreamFinishErroring"?
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.
Makes sense. One that that bugs me is that StartErroring conditionally calls FinishErroring directly. This avoids duplicating the condition in 2 of the callers, and the condition is always true in WritableStreamDealWithRejection so it seems like the right thing, but I still find it confusing.
index.bs
Outdated
1. Assert: ! WritableStreamHasOperationMarkedInFlight(_stream_) is *false*. | ||
1. Set _stream_.[[state]] to `"errored"`. | ||
1. Perform ! _stream_.[[writableStreamController]].[[ErrorSteps]](). | ||
1. Let _storedError_ by _stream_.[[storedError]]; |
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.
Semicolon should be period. "by" should be "be"
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.
Done.
index.bs
Outdated
1. if _stream_.[[pendingAbortRequest]] is *undefined*, | ||
1. Perform ! WritableStreamRejectCloseAndClosedPromiseIfNeeded(_stream_). | ||
1. Return. | ||
1. Let _abortRequest_ by _stream_.[[pendingAbortRequest]]. |
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.
"by" should be "be". Might be worth running a regex for "Let ... by".
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 found one of these myself which means I had it wrong in three places. 'y' and 'e' aren't even close to each other on the keyboard. Very odd.
index.bs
Outdated
1. Return. | ||
1. Let _abortRequest_ by _stream_.[[pendingAbortRequest]]. | ||
1. Set _stream_.[[pendingAbortRequest]] to *undefined*. | ||
1. Let _promise_ be stream.[[writableStreamController]].[[AbortSteps]](_abortRequest_.[[reason]]). |
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.
Missing ! (although I admit it's unclear whether these internal methods return completion values).
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.
It makes sense to follow the ECMASCRIPT convention here. As long as the reader understands the "?" and "!" notation, they should be able to make sense of it.
index.bs
Outdated
1. Let _abortRequest_ by _stream_.[[pendingAbortRequest]]. | ||
1. Set _stream_.[[pendingAbortRequest]] to *undefined*. | ||
1. Let _promise_ be stream.[[writableStreamController]].[[AbortSteps]](_abortRequest_.[[reason]]). | ||
1. <a>Upon fulfillment</a> of _promise_, <a>resolve</a> _abortRequest_.[[promise]] 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.
We're a bit inconsistent but we seem to lean toward not indenting "Upon fulfillment" and "Upon rejection" steps.
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.
Fixed here and in [[StartSteps]].
1. Return ! WritableStreamDefaultWriterClose(_writer_). | ||
</emu-alg> | ||
|
||
<h4 id="writable-stream-default-writer-ensure-closed-promise-rejected" | ||
aoid="WritableStreamDefaultWriterEnsureClosedPromiseRejected" | ||
nothrow>WritableStreamDefaultWriterEnsureClosedPromiseRejected( <var>writer</var>, <var>error</var> )</h4> |
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.
It occurs to me we could abstract this out into a utility abstract op like EnsureStatePromiseRejected(promise, error). ("State" included to justify marking it as handled.)
Not sure if worth it.
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.
It would be ugly in the reference implementation because of the way the state promises are manipulated by a set of functions there.
index.bs
Outdated
@@ -3582,10 +3568,13 @@ aoid="WritableStreamDefaultControllerAdvanceQueueIfNeeded" nothrow>WritableStrea | |||
|
|||
<emu-alg> | |||
1. Let _stream_ be _controller_.[[controlledWritableStream]]. | |||
1. if _controller_.[[started]] is *false*, return. |
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.
Capitalize
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.
Done.
44cb3d4
to
ce6d155
Compare
I am now confused about the relationship between stream.[[closeRequest]] and writer.[[closedPromise]]. In particular it seems like close() and closed are not returning the same promise anymore---close() returns closeRequest, whereas closed returns closedPromise? |
WritableStreamFinishAbort does not appear to be under the right heading based on the text
EDIT: actually it appears to be dead code. |
I haven't checked the blame history, but I assume that closeRequest and closedPromise parted ways when getWriter() was added. I simply failed to delete WritableStreamFinishAbort from the standard text. Now I need to go back and see what else I failed to delete. |
I remember that we discussed long time ago a principle whether methods should return a new promise. But I'm not sure if this is related. I no longer remember the motivation. I guess making close() return |
} | ||
stream._writeRequests = []; | ||
|
||
if (stream._pendingAbortRequest === 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.
Could you please update 'controller.error(), writer.abort() while there is an in-flight write, and then finish the write'
test to check that sink.abort()
is not called for this (error() then abort()) sequence?
controller.error() calls WritableStreamErroring() to change the state to erroring. WritableStreamAbort() skips WritableStreamErroring() when the state is erroring, but it does set _pendingAbortRequest. It should be rejected (right?) when controller.error() happened before writer.abort(), but it looks it would get processed by the code below.
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.
Good catch. It was indeed broken. I have fixed it. PTAL.
Remaining jobs for me to do:
|
Okay, ready for final reviews now. |
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.
Finished reviewing the reference impl.
looks good.
} | ||
|
||
function WritableStreamUpdateBackpressure(stream, backpressure) { | ||
assert(stream._state === 'writable'); | ||
assert(stream._state === 'writable' || stream._state === 'erroring'); |
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.
When does this get called with state set to erroring?
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.
Never. Fixed the assert.
|
||
assert(state === 'writable'); | ||
assert(state === 'writable' || state === 'erroring'); |
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.
Hmm, what was the rationale to have close() succeed while write() fails when the state is erroring?
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.
writer.close() is useful to wait for the sink to become quiescent. You can also do this with writer.closed, but that would mean writing code like
return writer.close().catch(() => writer.closed);
Also I want this to just work without the developer having to think about it. If there is a developer error like calling close() twice on the same stream then close() will reject immediately, but otherwise it consistently waits for no underlying sink methods to be running.
write() never provides any such guarantee, so rejecting immediately when we know there is no hope of the write() working is the best behaviour.
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.
OK. Makes sense.
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.
lgtm. I'll take a quick look at the spec side too now, but please go ahead merge it.
|
||
assert(state === 'writable'); | ||
assert(state === 'writable' || state === 'erroring'); |
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.
OK. Makes sense.
Commit message nit: "un" should be "an". Also let's please remember to delete the "(#721)" from the commit message title before merging. Gotta look good on the twitters :). LGTM! |
I find it convenient having the pull request linked from the commit title. Why does this look bad on the twitters? |
The pull request is always linked from the commit page, FWIW. The problem is it references a mysterious number with no context for anyone consuming the commit messages outside the GitHub UI. |
Previously, controller.error() and strategy size() evaluation could cause the stream state to change to 'errored' while un underlying sink operation was in flight. Only change state to 'errored' while no underlying sink operations are in flight. All errors now behave similarly to abort(). The first error to happen sets the [[storedError]] internal slot, regardless of type. A new 'erroring' state is used when an error is pending but the stream has not yet had a change to switch to the 'errored' state. controller.error() is a no-op in the 'erroring' state, making it easier for sinks to use without having to worry about it throwing because abort() has been called.
a1272ab
to
b741cbf
Compare
Rebased, squashed and updated web-platform-tests submodule to the merged commit. |
Port the standard changes in whatwg/streams#721. There are a number of behavioural changes related to error handling, which are listed at the above URL. This implementation has no known deviations from the standard. The brings this implementation up to parity with whatwg/streams@e7bf929. This CL also removes failing test expectations. Issue 626703 and 711529 cover lines that were removed from TestExpectations. BUG=711254,626703,711529,684543 Review-Url: https://codereview.chromium.org/2823563002 Cr-Commit-Position: refs/heads/master@{#465498}
Port the standard changes in whatwg/streams#721. There are a number of behavioural changes related to error handling, which are listed at the above URL. This implementation has no known deviations from the standard. The brings this implementation up to parity with whatwg/streams@e7bf929. The version of this CL merged to M59 branch 3071 adds failing and time-out test expectations since the tests on hte branch are now out-of-date with respect to the implementation. See http://crbug.com/713664. BUG=711254,684543 Review-Url: https://codereview.chromium.org/2823563002 Cr-Commit-Position: refs/heads/master@{#465498} (cherry picked from commit 6703f4d) Review-Url: https://codereview.chromium.org/2831763003 . Cr-Commit-Position: refs/branch-heads/3071@{#82} Cr-Branched-From: a106f0a-refs/heads/master@{#464641}
An "erroring" state was added to WritableStream in e7bf929 (whatwg#721) but was not included in the description of the [[state]] slot. Add it. Also remove a stray '.' from the description of the [[writeRequests]] slot.
Previously, controller.error() and errors from strategy size() could transition
the stream state to change to 'errored' while an underlying sink operation
was in flight.
Only change to 'errored' while no underlying sink operations are in flight,
consistent with abort(). The first error to happen sets the [[storedError]]
internal slot, regardless of type.
Other behavioural changes:
abort() method has completed.
don't fulfill or reject until no underlying sink methods are executing or will
be executed. Exceptions:
status of the underlying sink operation, provided it executes.
has been called.
longer useful and has been removed.
closed are no a no-op.
Major implementation changes:
from "writable" to "erroring" when an error condition is triggered,
then changes to "errored" once the underlying sink operation
has finished.
boolean field [[wasAlreadyErrored]]. When it is true the underlying
sink abort() method will not be called.