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

reader.read() inside strategy.size() doesn't work #794

Open
ricea opened this issue Sep 8, 2017 · 6 comments
Open

reader.read() inside strategy.size() doesn't work #794

ricea opened this issue Sep 8, 2017 · 6 comments
Assignees

Comments

@ricea
Copy link
Collaborator

ricea commented Sep 8, 2017

If I understand correctly, the following test should pass:

promise_test(() => {

  let controller;
  let readPromise;
  let reader;
  const rs = new ReadableStream({
    start(c) {
      controller = c;
    }
  }, {
    size() {
      readPromise = reader.read();
      return 1;
    }
  });
  reader = rs.getReader();
  controller.enqueue();
  return readPromise;

}, 'Readable stream: read() inside strategy size() should resolve');

In practice it times out.

I think the reason is that at step 3 of ReadableStreamDefaultControllerEnqueue it decides that since there isn't a read in process, the chunk should be queued. Then when read() is called inside size() the chunk isn't queued yet, so the read is queued in ReadableStreamAddReadRequest and remains pending.

Probably ReadableStreamDefaultControllerEnqueue should check ReadableStreamGetNumReadRequests a second time.

Or maybe this is too weird to fix.

@ricea
Copy link
Collaborator Author

ricea commented Sep 13, 2017

A related issue that makes me even more uncomfortable is that if you call controller.close() from inside size() with no chunks queued then the stream will be closed but the chunk will be added to the queue, and be stuck there. This violates expectations about the consistency of the internal state. I haven't found a way to make it throw an assertion, mostly because a closed ReadableStream won't do anything at all.

@domenic
Copy link
Member

domenic commented Sep 15, 2017

Huh. I share your intuition that this should work. And also that it's code that should never be written.

The full fix would be to do any necessary state checks immediately after calling size(). So I guess after step 4.b in ReadableStreamDefaultControllerEnqueue. Those checks would be... basically steps 2 and 3.

(Also interesting: what if someone releases the reader lock? Seems to work OK.)

I guess at a high level I see three choices:

  1. Try to ensure size() is well-behaved. E.g. at one extreme we could have a boolean, [[sizeBeingCalled]], and make everything throw/fail if this flag is true.
  2. Allow size() to do whatever it wants, and recover gracefully. So, insert the above checks.
  3. Say that if size() is not well-behaved, the behavior will be unexpected (but well-specified). It will essentially be whatever is simplest to spec and implement, i.e. no extra checks.

I think we are currently going for (3). Although you could make an argument that handling exceptions thrown by size() at all, like we do now, is partially leaning toward (2).

I think I am OK with that. It'd be improved a bit if I got around to fixing #427.

We could also probably transition to (1) or (2) later if we really wanted, breaking only pathological code.

@ricea
Copy link
Collaborator Author

ricea commented Sep 15, 2017

We have the semantic that if a read() is in progress then size() isn't called at all. I was initially surprised by this, but it makes sense. However, it means that unlike WritableStream we can't just fix the reentrancy issue by hoisting the call to size() to before any other work is done. Meaning that there is unavoidable cost in overhead for fixing this.

So I'm leaning towards (3), but I do worry that maybe the reason that no assert()s are tripping is because we don't have enough of them.

There is a risk that implementations will track extra internal state that causes them violate their own invariants and crash here. I think I will add tests for the current behaviour so that hopefully such problems can be caught early.

@ricea ricea self-assigned this Sep 15, 2017
ricea added a commit to ricea/web-platform-tests that referenced this issue Nov 13, 2017
The 'pipeTo() inside size() should work' test didn't pass on Chromium's
implementation as it performs the read() synchronously inside the call
to pipeTo(). This encounters the race condition described in
whatwg/streams#794.

Although different from the reference implementation, What Chromium's
implementation does here is valid and other implementations may do the
same thing.

As a workaround, queue a second chunk inside the test to trigger the
first one to be read.
@ricea
Copy link
Collaborator Author

ricea commented Nov 13, 2017

Chromium's implementation of pipeTo() is also affected, as it performs the first read synchronously. See web-platform-tests/wpt#8163. Although the reference implementation and Chromium are observably different here, I don't think either is wrong.

ricea added a commit to ricea/web-platform-tests that referenced this issue Nov 13, 2017
Verify the results when calling ReadableStream and related class methods inside
the size() function of the strategy. This results in user code being executed
inside ReadableStreamDefaultControllerEnqueue, which can have surprising
results. No real user code should be doing this, but it is important that
implementations be robust in these cases.

These tests are based closely on the ones in
streams/transform-streams/reentrant-strategies.js, and are somewhat redundant
with them. Since less code is under test here, these tests are easier to
understand and debug. Additional tests cover getReader() and tee().

See whatwg/streams#794 for background of some of the
stranger expectations here.
ricea added a commit to ricea/web-platform-tests that referenced this issue Nov 13, 2017
Verify the results when calling ReadableStream and related class methods inside
the size() function of the strategy. This results in user code being executed
inside ReadableStreamDefaultControllerEnqueue, which can have surprising
results. No real user code should be doing this, but it is important that
implementations be robust in these cases.

These tests are based closely on the ones in
streams/transform-streams/reentrant-strategies.js, and are somewhat redundant
with them. Since less code is under test here, these tests are easier to
understand and debug. Additional tests cover getReader() and tee().

See whatwg/streams#794 for background of some of the
stranger expectations here.
@ricea
Copy link
Collaborator Author

ricea commented Nov 13, 2017

I've written some ReadableStream tests at web-platform-tests/wpt#8166. I'm not sure I've covered every possible case where things can go wrong. As far as I can tell there is no way to call strategySize while already inside a call to strategySize, but I might have missed something.

domenic pushed a commit to web-platform-tests/wpt that referenced this issue Nov 13, 2017
The 'pipeTo() inside size() should work' test didn't pass on Chromium's
implementation as it performs the read() synchronously inside the call
to pipeTo(). This encounters the race condition described in
whatwg/streams#794.

Although different from the reference implementation, What Chromium's
implementation does here is valid and other implementations may do the
same thing.

As a workaround, queue a second chunk inside the test to trigger the
first one to be read.
domenic pushed a commit to web-platform-tests/wpt that referenced this issue Nov 13, 2017
Verify the results when calling ReadableStream and related class methods inside
the size() function of the strategy. This results in user code being executed
inside ReadableStreamDefaultControllerEnqueue, which can have surprising
results. No real user code should be doing this, but it is important that
implementations be robust in these cases.

These tests are based closely on the ones in
streams/transform-streams/reentrant-strategies.js, and are somewhat redundant
with them. Since less code is under test here, these tests are easier to
understand and debug. Additional tests cover getReader() and tee().

See whatwg/streams#794 for background of some of the
stranger expectations here.
@ricea
Copy link
Collaborator Author

ricea commented May 18, 2018

I think we've decided to live with this rather than change it.

Having said that, I will leave the issue open in case any implementers have problems with it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

No branches or pull requests

2 participants