-
Notifications
You must be signed in to change notification settings - Fork 163
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
WritableStream abort logic clean up #655
Conversation
0058ac5
to
49c0d29
Compare
This patch doesn't include any change on the spec text, yet. I'll change the spec text once we agree on this change. |
I'd like to summarize the current (or new for some places where I made change) state transition rule of the WritableStream. There're some Qs I'd like to discuss.
Implementation notes
|
Fixes #656 |
To add to #655 (comment), Error priority (higher first) for .closed:
Error priority (higher first) for .ready: First invocation of these three above. You can see this difference by looking at the recently added unit tests. |
return Promise.all([ | ||
promise_rejects(t, new TypeError(), writePromise2, 'writePromise2 must be rejected'), | ||
promise_rejects(t, new TypeError(), writer.ready, 'writer.ready must be rejected after abort()'), | ||
flushAsyncEvents() |
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 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.
How? The return statement is 3 lines above 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.
Sorry, I misread it. Never mind.
writer.releaseLock(); | ||
|
||
return Promise.all([ | ||
promise_rejects(t, new TypeError(), writer.ready, 'writer.ready must be reset to be rejected with a TypeError'), |
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.
Nitpick: the assertion description implies that it verifies more than it actually does. If we want to actually check that it's been reset, we have to save the previous value and verify it is not equal.
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.
Ah, good point. Resetting is internal thing. What we should verify is that the promises are rejected with an error which indicates "already released". Fixed.
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.
Answering the Qs:
Q: Is this "one pending write at a time" restriction a part of WritableStream's semantics or WritableStreamDefaultController's semantics?
I don't have a strong opinion on this.
Q: Do you like this new behavior?
This seems correct to me.
Overall this is a big change, and it will be helpful if you could draft the commit message explaining all that it changes and what the benefits are. The OP has some of the non-public facing changes but it sounds like there are also some changes to the behavior during error conditions.
I also wonder how this will tie into #634, both the normative change there and the suggested style change to use the queue more.
|
||
const events = []; | ||
return Promise.all([ | ||
closePromise.then(() => { |
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 would redo these as
promise_rejects(t, new TypeError(), error1,
'closePromise must reject with the error returned from the sink\'s close method')
.then(() => events.push('closePromise'))
Same for all tests. In general using assert_unreached for promises seems like a code smell.
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. Did you mean that I should redo rejection expecting ones e.g. at L259 as your example? Just to make sure.
}); | ||
|
||
let writePromise; | ||
let writePromiseRejected = 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.
In general I prefer arrays of events and assert_array_equals to boolean flags, but it's not a big deal.
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.
Changed to use an array
Addressed the comments and rewrote the description of assertions. I'll update the PR description to list all the visible behavior change. |
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.
Logic changes lgtm.
@@ -109,6 +109,25 @@ function IsWritableStreamLocked(stream) { | |||
return true; | |||
} | |||
|
|||
function WritableStreamEnsureReadyPromiseRejectedWith(stream, error, isPending) { |
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 feel that this is actually a WritableStreamDefaultWriter operation. The only place it's called without a stream._writer !== undefined check is inside DefaultWriter itself.
WritableStreamDefaultWriterEnsureReadyPromiseRejectedWith is a very long name but I think I can live with 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.
Good catch. Fixed.
flushAsyncEvents() | ||
]); | ||
}).then(() => { | ||
const writePromise3 = writer.write('a'); |
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 would find it more emotionally satisfying if you checked the contents of events
again here. Adding all those strings to it created a powerful sense of anticipation. Otherwise you could have just used a counter.
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.
Yeah, added.
flushAsyncEvents() | ||
]); | ||
}).then(() => { | ||
const writePromise4 = writer.write('a'); |
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.
Again, it would make me happy if you could check events
here.
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
flushAsyncEvents() | ||
]); | ||
}).then(() => { | ||
assert_array_equals(events, [], 'writePromise, abortPromise and writer.closed must be not fulfilled/rejected 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.
s/be not/not be/. Also 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.
Done
flushAsyncEvents() | ||
]); | ||
}).then(() => { | ||
assert_array_equals(events, [], 'writePromise and writer.closed must be rejected 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.
"must not be rejected". Also 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.
Fixed. Also changed the phrase to "fulfilled/rejected". writePromise will fulfill in this test case.
}).then(() => { | ||
assert_array_equals(events, [], 'writePromise and writer.closed must be rejected yet'); | ||
|
||
abortPromise = writer.abort(error1); |
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.
Also put .catch(() => event.push(...)) here so we can check it isn't called too early?
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.
Thanks for finding this. Yes, it's important. Actually, in this test case, abort() fails immediately since it checks the state.
@@ -213,4 +219,96 @@ promise_test(() => { | |||
}); | |||
}, 'the promise returned by async abort during close should resolve'); | |||
|
|||
// Though the order of the promises is not important, we're checking it for interoperability. We can change the order |
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 suggest "arbitrary" rather than "not important". Also, maybe "order in which the promises are fulfilled or rejected"?
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.
Thanks. Updated.
Oops, sorry. Some review comments are not yet addressed correctly. Fixing them. |
Spec change: whatwg/streams#655
ca44eaa
to
a0ac58b
Compare
Rebased wpt PR: web-platform-tests/wpt#4587 Here're links for checking the diff before rebasing: |
The changes look good to me, mainly based on the increase test coverage, but I'm still looking forward to a description of the visible behavior changes :) |
I would prefer the changes to the standard text to be in the same commit because I'm nervous about them getting out-of-sync. |
Yes. I'm going to make the spec text sync. I didn't mean that I'm going to have two separate commits in the comment. I created a draft of the commit message to describe all the details in response to Domenic's request in weekend, but haven't done yet. I'm going to get it done today. Adam, are you fine with the change (reference impl)? |
Yes. writable-stream.js lgtm. |
The following commit message drafted here has been moved to the actual commit This patch factors out the logic to handle fulfillment/rejection of This patch also clarifies the interface boundary, i.e. factoring out operations that are exposed by the WritableStream to controllers. This PR is recreation of #640. Principles behind this change:
Bug fixes:
Other visible changes:
Fulfillment/rejection order changes:
Refactoring:
|
I think it would be more consistent if the rejection reason changed, but unlikely to make any practical difference to anyone. So not worth making the logic more complicated for. |
I like it better actually. Ready normally rejects ASAP to indicate that writing now is useless, whereas closed rejects later so that it can provide a guarantee that everything has reached a stable state. Here both things happen together, but it's good to be consistent. Ideally ready would reject first or second, but it seems unimportant. |
|
Almost finished. I've updated #655 (comment) by adding notes about the fixes made after the post was made with NEW mark so that ricea can comment to the new ones easily. |
Thanks. ricea. I'll keep this unaddressed. |
with a fulfillment handler that returns ! WritableStreamDefaultControllerAbort(_controller_, _reason_). | ||
1. Otherwise, return _stream_.[[pendingAbortRequest]]. | ||
1. Return ! WritableStreamDefaultControllerAbort(_controller_, _reason_). | ||
1. If _state_ is `"writable"` and ! WritableStreamDefaultControllerGetBackpressure(_stream_.[[writableStreamController]]) is *true*, let _readyPromiseIsPending_ be *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.
This construction bothers me a bit because my brain interprets it as the declaration of the variable happening inside the if statement. However, I don't have a better idea.
Feel free to disregard this comment.
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.
Yeah, as it's uncommon idiom, I wondered how it looks to you reviewers.
Given that it made you think a bit, I rewrote it by separating it into let and updating statement. I think it's ok. Thanks
1. Let _abortRequest_ be _stream_.[[pendingAbortRequest]]. | ||
1. Set _stream_.[[pendingAbortRequest]] to *undefined*. | ||
1. Let _promise_ be ! WritableStreamDefaultControllerAbort(_controller_, _abortRequest_.[[reason]]). | ||
1. Transform _promise_ with a fulfillment handler which takes the argument _result_ and <a>resolves</a> _abortRequest_.[[promise]] with _result_ and a rejection handler which takes the argument _reason_ and <a>rejects</a> _abortRequest_.[[promise]] with _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.
Linkify Transform? #665
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! Fixed
1. Set _stream_.[[state]] to `"errored"`. | ||
1. Set _stream_.[[storedError]] to _reason_. | ||
1. If _wasAborted_ is *false* and _stream_.[[writer]] is not *undefined*, | ||
1. WritableStreamDefaultWriterEnsureReadyPromiseRejectedWith(_stream_.[[writer]], _reason_, _readyPromiseIsPending_). |
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 think this can be on the same line as the if condition.
Also there should be a '!' before WritableStreamDefaultWriterEnsureReadyPromiseRejectedWith.
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.
Yeah. Moved and added "perform !"
1. Set _stream_.[[state]] to `"errored"`. | ||
1. Set _stream_.[[storedError]] to _reason_. | ||
1. If _wasAborted_ is *false* and _stream_.[[writer]] is not *undefined*, | ||
1. WritableStreamDefaultWriterEnsureReadyPromiseRejectedWith(_stream_.[[writer]], _reason_, _readyPromiseIsPending_). |
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.
Again can be on the same line and missing "Perform !"
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
@@ -3156,15 +3261,14 @@ nothrow>WritableStreamDefaultWriterRelease ( <var>writer</var> )</h4> | |||
1. Assert: _stream_.[[writer]] is _writer_. | |||
1. Let _releasedError_ be a new *TypeError*. | |||
1. Let _state_ be _stream_.[[state]]. | |||
1. If _state_ is `"writable"` or `"closing"`, or _stream_.[[pendingAbortRequest]] is not *undefined*, <a>reject</a> | |||
1. Let _controller_ be _stream_.[[writableStreamController]]. | |||
1. If _state_ is `"writable"` or `"closing"` or _controller_.[[inClose]] is *true* or _controller_.[[writing]] is *true*, or _stream_.[[pendingAbortRequest]] is not *undefined*, <a>reject</a> |
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.
The stream.[[pendingAbortRequest]] condition doesn't exist in the reference implementation (unless I'm looking at the wrong version).
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.
Great catch. Right, I've removed the condition but forgot to do so on the spec text.
Done.
1. If _state_ is `"writable"` and ! WritableStreamDefaultControllerGetBackpressure(_stream_.[[writableStreamController]]) is *true*, let _readyPromiseIsPending_ be *true*. | ||
1. Otherwise, let _readyPromiseIsPending_ be *false*. | ||
1. Let _readyPromiseIsPending_ be *false*. | ||
1. If _state_ is `"writable"` and ! WritableStreamDefaultControllerGetBackpressure(_stream_.[[writableStreamController]]) is *true*, set _readyPromiseIsPending_ to *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.
Much better! Thank you!
Reviewed everything up to d560c9b. |
Addressed the review comments and wrapped long lines where possible. |
lgtm. I will leave decision on the wrapping to @domenic. |
I pushed some style tweaks (mostly line wrapping) for the spec text. LGTM now. Remember we want to land tests in WPT first then update this branch. This could be a bit tricky given the changes in web-platform-tests/wpt#4589 so if you want me to handle it let's just make sure to sign off on web-platform-tests/wpt#4587 then I can do both merges. |
Spec change: whatwg/streams#655
- Fixes the test description of one test - Modified some tests to check promise fulfillment/rejection order - Added tests to check the order and timing of promise fulfillment/rejection when writer.abort() is called while there's a pending writer.write() or writer.close() - Added tests to check that writer.releaseLock() doesn't hit assertions when called while there's a pending writer.write() or writer.close() - Added tests to check an error from which of controller.error(), sink.write()/close() or writer.abort()) is used for rejecting writer.write()/close() Corresponding spec side change: whatwg/streams#655
- Fixes the test description of one test - Modified some tests to check promise fulfillment/rejection order - Added tests to check the order and timing of promise fulfillment/rejection when writer.abort() is called while there's a pending writer.write() or writer.close() - Added tests to check that writer.releaseLock() doesn't hit assertions when called while there's a pending writer.write() or writer.close() - Added tests to check an error from which of controller.error(), sink.write()/close() or writer.abort()) is used for rejecting writer.write()/close() Corresponding spec side change: whatwg/streams#655
Thank you domenic for the commit with great catches in bac156e. LGTM.
I've updated run-web-platform-tests.js in 59820d5 as you suggested in the OP of the PR, and updated submodule in b6a2cbf.
Thanks for merging. |
push is failing on travis at deploy.sh |
Looks bikeshed API is down. Investigating. |
Looks this is due to speced/bikeshed#843 |
https://tabatkins.github.io/bikeshed/#metadata-inline-github-issues is not yet updated. According to the error message, we can choose from 'title', 'full' or false/no/n/off. Tentatively changed it to |
This patch factors out the logic to handle fulfillment/rejection of `sink.write()` and `sink.close()` operation into separate methods to make it easier to follow the code and check correctness while fixing a few bugs. This patch also clarifies the interface boundary, i.e. factoring out operations that are exposed by the WritableStream to controllers. This PR is recreation of #640. Principles behind this change: - `writer.closed` rejection is delayed while there's any pending `writer.write()` or `writer.close()` - rejection reason picking order: - 1. the error passed to `controller.error()` made during the pending operation if made - 2. the rejection reason of the pending operation if rejected - 3. a TypeError indicating a `writer.abort()` call was made if made - the picked reason will be set to `[[storedError]]` - `writer.ready` rejects immediately in response to `controller.error()` and `writer.abort()` with the error passed to the method - `writer.write()` calls made after `writer.abort()` rejects with: - if it's made before `writer.abort()` call and is staying in `[[writeRequests]]`: `[[storedError]]` - if it's made after `writer.abort()` but before `controller.error()` if any: an error indicating a `writer.abort()` call was made - if it's made after `controller.error()` if any: the error passed to the `controller.error()` - if it's made after the pending operation finishes: `[[storedError]]` which stores the reason picked by following the steps above - `writer.abort()` fulfills if the pending `writer.close()` succeeds, and `writer.closed` rejects - Note: This patch didn't change this behavior but changed the message. Bug fixes: - Fixed `sink.close()` rejection handling logic to reject `writer.closed` even if the stream has been `writer.abort()`-ed or `controller.error()`-ed #656 - Before this fix, `WritableStreamDefaultControllerErrorIfNeeded()` was used which is no-op if `[[state]]` has already been set to `"errored"` - Fixed `writer.releaseLock()` to reject `writer.closed` when there's a pending write or close since closed rejection is delayed. Just checking `[[pendingAbortRequest]]` is not enough. Other visible changes: - Now `writer.abort()` doesn't set the `[[state]]` of the stream to `"errored"` when there's pending `sink.write()` or `sink.close()` but prevents further operations on writer by setting `[[pendingAbortRequest]]` to non-**undefined** value. - Note that `writer.ready` still rejects immediately - Now `controller.error()` succeeds even after `writer.abort()`. - So, `writer.abort()` and `writer.closed` reject with the error passed to `controller.error()` - Note that `writer.ready` is kept rejected with an error indicating abort even after `controller.error()`. - Now `sink.close()` rejection handling logic doesn't change the rejection reason of `writer.ready` if it's already rejected by `writer.abort()` or `controller.error()` - Question to be discussed: Is this ok? When a writer is re-obtained, the ready promise is set to be rejected with the latest error. - Now `sink.write()` rejection handling logic doesn't change the `[[storedError]]` to the rejection reason of `sink.write()` if it's already `controller.error()`-ed - Same about `writer.ready` Fulfillment/rejection order changes: - Rejection order on `sink.write()` completion: - Before: write(), closed, abort() - After: write(), abort(), closed - Rejection order on `sink.write()` rejection: - Before: write(), closed, abort(), ready - After: write(), abort(), ready, closed - `controller.error()` rejects: - Before: closed then ready - After: ready then closed - `writer.abort()` rejects - Before: closed, ready - After: ready, closed Refactoring: - Refactored `writer.abort()` finalization steps to clarify where a certain process is scheduled to run - Factored out `[[pendingWriteRequest]]` setting logic as a WritableStream abstract operation exposed to controllers - Removed redundant `[[queue]]` clearing from `sink.write()` rejection handling logic - Factored out: - `WritableStreamDefaultControllerUpdateBackpressureIfNeeded()` - `WritableStreamRejectClosedPromiseIfAny()` - `WritableStreamDefaultWriterEnsureReadyPromiseRejectedWith()` - Call what's needed directly in `sink.write()` rejection than using `WritableStreamDefaultControllerErrorIfNeeded()` - Don't clear `[[queue]]` on `sink.close()` rejection. It's guaranteed to be empty as we're processing closure. - Finish calculating conditions at the top of methods to avoid thinking about where a certain state has been updated or not yet (e.g. `[[error]]`) - Have redundant call to promise rejection helper method if needed to keep rejection order consistent Closes #639, #656 This patch also includes some additional work as follows: - Update worker excluding pattern in test runner - web-platform-tests/wpt#4589 has changed the naming rule of streams test files. - Update the condition in run-web-platform-tests.js to exclude worker tests to follow the new rule. - Sync wpt submodule to the current HEAD to run the updated tests - Fix bikeshed option: Inline Github Issues - bikeshed API has been updated to reject true for the Inline Github Issues option. - To pass the "push" check on Travis CI, we need to fix this to make deploy.sh run successfully - Tentatively set it to one of the new valid values, "title".
83dbf13
to
673d1f7
Compare
Squashed and updated the commit message to explain the additional works required for submission. The last commit before this was 83dbf13. |
Done. Finally. Thank you for your review. |
This patch factors out the code to handle fulfillment/rejection of sink write and sink close operation into separate methods to make it easier to follow the code and check the correctness of the logic.
This patch also clarifies the interface boundary, i.e. factoring out operations that are exposed by the WritableStream to controllers.
This PR is recreation of #640.