Skip to content
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

Merged
merged 2 commits into from
Apr 13, 2017

Conversation

ricea
Copy link
Collaborator

@ricea ricea commented Mar 30, 2017

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:

  • The writer.closed promise now doesn't reject until the underlying sink
    abort() method has completed.
  • writer.close(), writer.abort() and writer.closed promises now consistently
    don't fulfill or reject until no underlying sink methods are executing or will
    be executed. Exceptions:
    • Duplicate calls to close() or abort() still reject immediately.
    • writer.closed still rejects when writer.releaseLock() is called.
  • writer.write(), writer.close() and writer.abort() consistently return the
    status of the underlying sink operation, provided it executes.
  • It is no longer possible to error the stream once underlying sink close()
    has been called.
  • The controller parameter to the underlying sink close() method is no
    longer useful and has been removed.
  • Duplicate calls to controller.error() are now a no-op.
  • Calls to controller.error() while the stream is erroring, errored or
    closed are no a no-op.

Major implementation changes:

  • There is a new "erroring" state. The state synchronously changes
    from "writable" to "erroring" when an error condition is triggered,
    then changes to "errored" once the underlying sink operation
    has finished.
  • The stream.[[pendingAbortRequest]] Record has an additional
    boolean field [[wasAlreadyErrored]]. When it is true the underlying
    sink abort() method will not be called.

@ricea
Copy link
Collaborator Author

ricea commented Mar 30, 2017

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.

Copy link
Member

@domenic domenic left a 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) {
Copy link
Member

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?

Copy link
Collaborator Author

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.

Copy link
Member

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!

Copy link
Collaborator Author

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');
Copy link
Member

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().

Copy link
Collaborator Author

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) {
Copy link
Member

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

Copy link
Collaborator Author

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
Copy link
Member

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.

Copy link
Collaborator Author

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) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: === true

Copy link
Collaborator Author

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.
Copy link
Member

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?

Copy link
Collaborator Author

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');
Copy link
Member

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?

Copy link
Collaborator Author

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.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mentions "pendingError"

Copy link
Collaborator Author

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;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No longer necessary

Copy link
Collaborator Author

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) {
Copy link
Member

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.

Copy link
Collaborator Author

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.

@domenic
Copy link
Member

domenic commented Mar 31, 2017

code changes lgtm, although @tyoshino always catches good bugs, so I'd really like his review.

@domenic
Copy link
Member

domenic commented Mar 31, 2017

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 )

@ricea
Copy link
Collaborator Author

ricea commented Apr 5, 2017

The standard changes are ready for review now.

@domenic
Copy link
Member

domenic commented Apr 5, 2017

Can you rebase/resolve the test conflict so that a branch snapshot gets generated?

Copy link
Member

@domenic domenic left a 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
Copy link
Member

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"?

Copy link
Collaborator Author

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]];
Copy link
Member

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"

Copy link
Collaborator Author

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]].
Copy link
Member

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".

Copy link
Collaborator Author

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]]).
Copy link
Member

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).

Copy link
Collaborator Author

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*.
Copy link
Member

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.

Copy link
Collaborator Author

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>
Copy link
Member

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.

Copy link
Collaborator Author

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.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Capitalize

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

@ricea ricea force-pushed the writable-stream-all-errors-queued branch from 44cb3d4 to ce6d155 Compare April 5, 2017 08:57
@domenic
Copy link
Member

domenic commented Apr 5, 2017

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?

@domenic
Copy link
Member

domenic commented Apr 5, 2017

WritableStreamFinishAbort does not appear to be under the right heading based on the text

The following abstract operations, unlike most in this specification, are meant to be generally useful by other specifications, instead of just being part of the implementation of this spec’s classes.

EDIT: actually it appears to be dead code.

@ricea
Copy link
Collaborator Author

ricea commented Apr 5, 2017

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.

@tyoshino
Copy link
Member

tyoshino commented Apr 5, 2017

I haven't checked the blame history, but I assume that closeRequest and closedPromise parted ways when getWriter() was added.

Right. It's 5a27534.

After
Before

@tyoshino
Copy link
Member

tyoshino commented Apr 5, 2017

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 [[closedPromise]] would also work.

}
stream._writeRequests = [];

if (stream._pendingAbortRequest === undefined) {
Copy link
Member

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.

Copy link
Collaborator Author

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.

@ricea
Copy link
Collaborator Author

ricea commented Apr 7, 2017

Remaining jobs for me to do:

  • Double-check the spec changes against the reference implementation changes.
  • Write a more detailed commit message describing the semantic changes.

@ricea
Copy link
Collaborator Author

ricea commented Apr 10, 2017

Okay, ready for final reviews now.

@tyoshino tyoshino self-assigned this Apr 10, 2017
Copy link
Member

@tyoshino tyoshino left a 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');
Copy link
Member

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?

Copy link
Collaborator Author

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');
Copy link
Member

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?

Copy link
Collaborator Author

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.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK. Makes sense.

Copy link
Member

@tyoshino tyoshino left a 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');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK. Makes sense.

@domenic
Copy link
Member

domenic commented Apr 13, 2017

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!

@ricea
Copy link
Collaborator Author

ricea commented Apr 13, 2017

I find it convenient having the pull request linked from the commit title. Why does this look bad on the twitters?

@domenic
Copy link
Member

domenic commented Apr 13, 2017

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.
@ricea ricea force-pushed the writable-stream-all-errors-queued branch from a1272ab to b741cbf Compare April 13, 2017 05:47
@ricea
Copy link
Collaborator Author

ricea commented Apr 13, 2017

Rebased, squashed and updated web-platform-tests submodule to the merged commit.

@ricea ricea merged commit e7bf929 into whatwg:master Apr 13, 2017
@ricea ricea deleted the writable-stream-all-errors-queued branch April 13, 2017 07:30
MXEBot pushed a commit to mirror/chromium that referenced this pull request Apr 20, 2017
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}
GnorTech pushed a commit to nwjs/chromium.src that referenced this pull request Apr 26, 2017
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}
ricea added a commit to ricea/streams that referenced this pull request Sep 22, 2017
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.
domenic pushed a commit that referenced this pull request Sep 22, 2017
An "erroring" state was added to WritableStream in
e7bf929 (#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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Development

Successfully merging this pull request may close these issues.

3 participants