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

src: refactor WriteWrap and ShutdownWrap #18676

Closed
wants to merge 8 commits into from

Conversation

addaleax
Copy link
Member

@addaleax addaleax commented Feb 9, 2018

Encapsulate stream requests more:

  • WriteWrap and ShutdownWrap classes are now tailored to the
    streams on which they are used. In particular, for most streams
    these are now plain AsyncWraps and do not carry the overhead
    of unused libuv request data.
  • Provide generic Write() and Shutdown() methods that wrap
    around the actual implementations, and make usage of streams
    easier, rather than implementing; for example, wrap objects
    don’t need to be provided by callers anymore.
  • Use EmitAfterWrite() and EmitAfterShutdown() handlers to
    call the corresponding JS handlers, rather than always trying
    to call them. This makes usage of streams by other C++ code
    easier and leaner.

Also fix up some tests that were previously not actually testing
asynchronicity when the comments indicated that they would.

Checklist
  • make -j4 test (UNIX), or vcbuild test (Windows) passes
  • tests and/or benchmarks are included
  • commit message follows commit guidelines
Affected core subsystem(s)

src

Maybe @apapirovski could take a look some time? :)

@nodejs-github-bot nodejs-github-bot added the lib / src Issues and PRs related to general changes in the lib or src directory. label Feb 9, 2018
@addaleax addaleax added the c++ Issues and PRs that require attention from people who are familiar with C++. label Feb 9, 2018
// There are similar assumptions in other places in the code base.
// A better idea would be having all BaseObject's internal pointers
// refer to the BaseObject* itself; this would require refactoring
// throughout the code base but makes Node rely much less on C++ quirks.
Copy link
Member Author

Choose a reason for hiding this comment

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

@TimothyGu Since you were asking for ideas, this might be a decent medium-sized project?

@apapirovski
Copy link
Member

I've started reviewing but will probably take a good few passes. Looks great so far. 🙏 🎉

if (stream_->DoWrite(req, writebufs, count, nullptr) != 0) {
req->Dispose();
StreamWriteResult res =
static_cast<StreamBase*>(stream_)->Write(*bufs, count);
Copy link
Member

Choose a reason for hiding this comment

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

I'm probably missing something but why the static_cast?

Copy link
Member Author

Choose a reason for hiding this comment

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

It’s because *stream_ is a StreamResource instance, but we know that we only ever use a StreamBase for HTTP2.

One could make a case for merging the two classes, given that it’s unclear whether there will ever be StreamResources that are not tied to an AsyncWrap. I took this path in #18334, because it presents a nice separation of concerns: The actual implementation of a data stream vs all the JS stuff surrounding it.

Copy link
Member

Choose a reason for hiding this comment

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

StreamResource is-a StreamBase so Write() is available, isn't it? The static_cast looks superfluous to me.

Copy link
Member Author

Choose a reason for hiding this comment

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

StreamResource is-a StreamBase

Unfortunately, it’s the reverse situation. Maybe StreamBase is not the best name for this… :/

Anyway, I’ve added a underlying_stream() helper to Http2Session and TLSWrap that makes the cast transparent, so that shouldn’t be an issue for niow.


inline void StreamReq::Dispose() {
object()->SetAlignedPointerInInternalField(
kStreamReqField, nullptr);
Copy link
Member

Choose a reason for hiding this comment

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

Fits on one line.


// Remove the reference to the .handle property
v8::Local<v8::Object> req_wrap_obj = req_wrap->object();
req_wrap_obj->Delete(env->context(), env->handle_string()).FromJust();
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 necessary?

Copy link
Member Author

Choose a reason for hiding this comment

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

@bnoordhuis I don’t think so. It was carried over from the original code.

I’ve removed it for now, the tests seem to pass and we’ll see if it causes any trouble.

auto& persistent = static_cast<AsyncWrap*>(this)->persistent();
CHECK_EQ(persistent.IsEmpty(), false);
persistent.Reset();
}
Copy link
Member

Choose a reason for hiding this comment

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

This won't work when #18656 lands.

I don't quite understand why auto-reset is disabled for LibuvStreamWrap. Does it need to?

I see the logic in the TLS code. That's essentially about moving over the JS object from one instance to another?

Copy link
Member Author

Choose a reason for hiding this comment

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

This won't work when #18656 lands.

I think we could remove the entire conditional, and the template parameter once that lands (yay!). I’ll rebase & remove it once that happens to be sure.

I don't quite understand why auto-reset is disabled for LibuvStreamWrap. Does it need to?

Yes, because the libuv streams use inherit from ReqWrap, which currently also checks that the persistent is not empty inside its destructor and resets it afterwards. (The CHECK there would crash).

That's essentially about moving over the JS object from one instance to another?

Yes, exactly.

wrap->get_async_id());
req_wrap = WriteWrap::New(env, req_wrap_obj, this, storage_size);
}
char* storage = nullptr;
Copy link
Member

Choose a reason for hiding this comment

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

Suggestion: use a std::unique_ptr<char> with a custom deleter and .release() it in the call to SetAllocatedStorage().

(Or perhaps even better: pass it to WriteWrap so you don't have to free the memory manually in its destructor.)

Copy link
Member Author

Choose a reason for hiding this comment

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

Sounds good, done!

wrap->get_async_id());
req_wrap = WriteWrap::New(env, req_wrap_obj, this);
}
buf.base = const_cast<char*>(Buffer::Data(args[1]));
Copy link
Member

Choose a reason for hiding this comment

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

Unnecessary const_cast, Buffer::Data() already returns char*.

(Looks like it was existing code but since you're here.)

}

data = req_wrap->Extra();
char* data;
Copy link
Member

Choose a reason for hiding this comment

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

Could be a std::unique_ptr<char>.

// if negative, a libuv error code.
virtual void OnStreamAfterWrite(WriteWrap* w, int status) {}
// By the fault, this is simply passed on to the previous listener
Copy link
Member

Choose a reason for hiding this comment

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

'By default'? :-)


// This is called once a shutdown has finished. `status` may be 0 or,
// if negative, a libuv error code.
// By the fault, this is simply passed on to the previous listener
Copy link
Member

Choose a reason for hiding this comment

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

Ditto.

@addaleax
Copy link
Member Author

@bnoordhuis Thanks for the review! I think I got everything so far.

Copy link
Member

@bnoordhuis bnoordhuis left a comment

Choose a reason for hiding this comment

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

LGTM with some comments/questions. Nice work, Anna.

src/js_stream.cc Outdated
CHECK(args[0]->IsObject());
ASSIGN_OR_RETURN_UNWRAP(&w, args[0].As<Object>());
Wrap* w = static_cast<Wrap*>(Wrap::FromObject(args[0].As<Object>()));
Copy link
Member

Choose a reason for hiding this comment

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

StreamReq::FromObject()? static_cast<Wrap*>(Wrap::FromObject(...)) probably looks redundant and mildly confusing to the casual reader.

if (stream_->DoWrite(req, writebufs, count, nullptr) != 0) {
req->Dispose();
StreamWriteResult res =
static_cast<StreamBase*>(stream_)->Write(*bufs, count);
Copy link
Member

Choose a reason for hiding this comment

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

StreamResource is-a StreamBase so Write() is available, isn't it? The static_cast looks superfluous to me.

wrap->get_async_id());
req_wrap = WriteWrap::New(env, req_wrap_obj, this, storage_size);
}
std::unique_ptr<char, Free> storage;
Copy link
Member

Choose a reason for hiding this comment

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

char[] - pointer to array, not pointer to single char. Likewise around line 250.

// `OtherBase` must have a constructor that matches the `AsyncWrap`
// constructors’s (Environment*, Local<Object>, AsyncWrap::Provider) signature
// and be a subclass of `AsyncWrap`.
template<typename OtherBase, bool kResetPersistentOnDestroy = true>
Copy link
Member

Choose a reason for hiding this comment

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

Space before <. Likewise around line 336.

src/tls_wrap.cc Outdated
@@ -665,6 +679,11 @@ void TLSWrap::OnStreamRead(ssize_t nread, const uv_buf_t& buf) {
}


ShutdownWrap* TLSWrap::CreateShutdownWrap(Local<Object> req_wrap_object) {
return static_cast<StreamBase*>(stream_)->CreateShutdownWrap(req_wrap_object);
Copy link
Member

Choose a reason for hiding this comment

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

Is the static_cast necessary here?

Use `DoTryWrite()` to write data to the underlying socket.
This does probably not make any difference in performance
because the callback is still deferred (for now), but
brings TLSWrap in line with other things that write to
streams.
Otherwise `this[kCurrentWriteRequest]` is set to a value even
if one of the `write` calls throws.

This is needed in order not to break tests in a later commit.
Encapsulate stream requests more:

- `WriteWrap` and `ShutdownWrap` classes are now tailored to the
  streams on which they are used. In particular, for most streams
  these are now plain `AsyncWrap`s and do not carry the overhead
  of unused libuv request data.
- Provide generic `Write()` and `Shutdown()` methods that wrap
  around the actual implementations, and make *usage* of streams
  easier, rather than implementing; for example, wrap objects
  don’t need to be provided by callers anymore.
- Use `EmitAfterWrite()` and `EmitAfterShutdown()` handlers to
  call the corresponding JS handlers, rather than always trying
  to call them. This makes usage of streams by other C++ code
  easier and leaner.

Also fix up some tests that were previously not actually testing
asynchronicity when the comments indicated that they would.
Copy link
Member

@apapirovski apapirovski left a comment

Choose a reason for hiding this comment

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

LGTM from a flow / logic perspective. Didn't review the intricacies of C++ in detail, there are certainly more qualified folks around for that.

Also appreciate the extremely detailed comments. Made the review a lot easier. 👍

(One unrelated comment below, more as a general discussion point.)

stream[kState].writeQueueSize -= bytes;

if (session !== undefined)
session[kState].writeQueueSize -= bytes;
if (typeof req.callback === 'function')
req.callback(null);
req.handle = undefined;
Copy link
Member

Choose a reason for hiding this comment

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

This makes me wonder, is handle on WriteWrap used at all? I had a brief look at the C++ side and saw nothing. All I'm seeing is it being set/deleted all around. The only place I see it being used at all is process_wrap.

Admittedly I could be missing something more intricate here...

Copy link
Member Author

Choose a reason for hiding this comment

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

@apapirovski I think it’s only for diagnostic purposes and preventing the handle from being garbage collected; I think at least in the case of JSStreams that could happen because that’s a weak handle and otherwise there might not be any backreference to it?

Copy link
Member

Choose a reason for hiding this comment

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

Trying to think about this... Since the socket & http2 implementations reference the handle on _handle and kHandle, this seems like it would mainly come into play if the stream is socket/session is destroyed? Would the handle still be needed in that case? I don't recall if this would still trigger oncomplete or not.

Copy link
Member Author

Choose a reason for hiding this comment

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

@apapirovski I think it would call oncomplete, but not synchronously (in the case of libuv streams)?

But generally, it’s not a requirement that streams are only destroyed when they are explicitly closed… http2 objects + JSStream contain no strong Persistents, so they can be garbage collected at any time once there no longer is a reference to them, but that shouldn’t happen during a write, should it?

Copy link
Member

Choose a reason for hiding this comment

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

I guess it's (very) possible that in certain situations the handle is the only thing referencing the stream and the WriteWrap is the only thing referencing the handle. I didn't really think about that originally... Was thinking too literally about its usage.

Copy link
Member

Choose a reason for hiding this comment

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

I figured no harm in putting this to practice... So far, at least as far as http2 is concerned, it seems like removing .handle on WriteWrap & ShutdownWrap is fine, even when running stress tests. I might play around with explicit global.gc() calls and study the code in more detail to understand how useful these references are.

Copy link
Member Author

Choose a reason for hiding this comment

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

@apapirovski I can’t think of any way in which this would break HTTP/2, yes…

I’m a bit worried removing it might break async_hooks users… but then again, this really isn’t supposed to be public API. :/

@addaleax
Copy link
Member Author

@addaleax addaleax added the author ready PRs that have at least one approval, no pending requests for changes, and a CI started. label Feb 13, 2018
Copy link
Member

@jasnell jasnell left a comment

Choose a reason for hiding this comment

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

Thank you for this :-)

@addaleax
Copy link
Member Author

Landed in b2e20b0...0e7b612

Thanks for the reviews!

@addaleax addaleax closed this Feb 14, 2018
@addaleax addaleax deleted the writewrap-red branch February 14, 2018 10:03
@addaleax addaleax removed the author ready PRs that have at least one approval, no pending requests for changes, and a CI started. label Feb 14, 2018
addaleax added a commit that referenced this pull request Feb 14, 2018
Use `DoTryWrite()` to write data to the underlying socket.
This does probably not make any difference in performance
because the callback is still deferred (for now), but
brings TLSWrap in line with other things that write to
streams.

PR-URL: #18676
Reviewed-By: Ben Noordhuis <[email protected]>
Reviewed-By: Anatoli Papirovski <[email protected]>
Reviewed-By: James M Snell <[email protected]>
addaleax added a commit that referenced this pull request Feb 14, 2018
Otherwise `this[kCurrentWriteRequest]` is set to a value even
if one of the `write` calls throws.

This is needed in order not to break tests in a later commit.

PR-URL: #18676
Reviewed-By: Ben Noordhuis <[email protected]>
Reviewed-By: Anatoli Papirovski <[email protected]>
Reviewed-By: James M Snell <[email protected]>
addaleax added a commit that referenced this pull request Feb 14, 2018
PR-URL: #18676
Reviewed-By: Ben Noordhuis <[email protected]>
Reviewed-By: Anatoli Papirovski <[email protected]>
Reviewed-By: James M Snell <[email protected]>
addaleax added a commit that referenced this pull request Feb 14, 2018
Encapsulate stream requests more:

- `WriteWrap` and `ShutdownWrap` classes are now tailored to the
  streams on which they are used. In particular, for most streams
  these are now plain `AsyncWrap`s and do not carry the overhead
  of unused libuv request data.
- Provide generic `Write()` and `Shutdown()` methods that wrap
  around the actual implementations, and make *usage* of streams
  easier, rather than implementing; for example, wrap objects
  don’t need to be provided by callers anymore.
- Use `EmitAfterWrite()` and `EmitAfterShutdown()` handlers to
  call the corresponding JS handlers, rather than always trying
  to call them. This makes usage of streams by other C++ code
  easier and leaner.

Also fix up some tests that were previously not actually testing
asynchronicity when the comments indicated that they would.

PR-URL: #18676
Reviewed-By: Ben Noordhuis <[email protected]>
Reviewed-By: Anatoli Papirovski <[email protected]>
Reviewed-By: James M Snell <[email protected]>
@kfarnung kfarnung mentioned this pull request Feb 19, 2018
2 tasks
@MylesBorins
Copy link
Contributor

Should this be backported to v9.x-staging? If yes please follow the guide and raise a backport PR, if not let me know or add the dont-land-on label.

kfarnung added a commit to kfarnung/node that referenced this pull request Feb 24, 2018
The root cause is that `req_wrap` is created in `StreamBase::Write`
and passed to `TLSWrap::DoWrite`. In the TLS case the object gets
disposed and replaced with a new instance, but the caller's pointer is
never updated. When the `StreamBase::Write` method returns, it returns
a pointer to the freed object to the caller. In some cases when the
object memory has already been reused an assert is hit in
`WriteWrap::SetAllocatedStorage` because the pointer is non-null.

PR-URL: nodejs#18860
Refs: nodejs#18676
Reviewed-By: Anna Henningsen <[email protected]>
addaleax added a commit to addaleax/node that referenced this pull request Feb 27, 2018
Use `DoTryWrite()` to write data to the underlying socket.
This does probably not make any difference in performance
because the callback is still deferred (for now), but
brings TLSWrap in line with other things that write to
streams.

PR-URL: nodejs#18676
Reviewed-By: Ben Noordhuis <[email protected]>
Reviewed-By: Anatoli Papirovski <[email protected]>
Reviewed-By: James M Snell <[email protected]>
addaleax added a commit to addaleax/node that referenced this pull request Feb 27, 2018
Otherwise `this[kCurrentWriteRequest]` is set to a value even
if one of the `write` calls throws.

This is needed in order not to break tests in a later commit.

PR-URL: nodejs#18676
Reviewed-By: Ben Noordhuis <[email protected]>
Reviewed-By: Anatoli Papirovski <[email protected]>
Reviewed-By: James M Snell <[email protected]>
addaleax added a commit to addaleax/node that referenced this pull request Feb 27, 2018
PR-URL: nodejs#18676
Reviewed-By: Ben Noordhuis <[email protected]>
Reviewed-By: Anatoli Papirovski <[email protected]>
Reviewed-By: James M Snell <[email protected]>
addaleax added a commit to addaleax/node that referenced this pull request Feb 27, 2018
Encapsulate stream requests more:

- `WriteWrap` and `ShutdownWrap` classes are now tailored to the
  streams on which they are used. In particular, for most streams
  these are now plain `AsyncWrap`s and do not carry the overhead
  of unused libuv request data.
- Provide generic `Write()` and `Shutdown()` methods that wrap
  around the actual implementations, and make *usage* of streams
  easier, rather than implementing; for example, wrap objects
  don’t need to be provided by callers anymore.
- Use `EmitAfterWrite()` and `EmitAfterShutdown()` handlers to
  call the corresponding JS handlers, rather than always trying
  to call them. This makes usage of streams by other C++ code
  easier and leaner.

Also fix up some tests that were previously not actually testing
asynchronicity when the comments indicated that they would.

PR-URL: nodejs#18676
Reviewed-By: Ben Noordhuis <[email protected]>
Reviewed-By: Anatoli Papirovski <[email protected]>
Reviewed-By: James M Snell <[email protected]>
addaleax pushed a commit to addaleax/node that referenced this pull request Feb 27, 2018
The root cause is that `req_wrap` is created in `StreamBase::Write`
and passed to `TLSWrap::DoWrite`. In the TLS case the object gets
disposed and replaced with a new instance, but the caller's pointer is
never updated. When the `StreamBase::Write` method returns, it returns
a pointer to the freed object to the caller. In some cases when the
object memory has already been reused an assert is hit in
`WriteWrap::SetAllocatedStorage` because the pointer is non-null.

PR-URL: nodejs#18860
Refs: nodejs#18676
Reviewed-By: Anna Henningsen <[email protected]>
@addaleax addaleax mentioned this pull request Feb 27, 2018
@addaleax
Copy link
Member Author

Marking this as dont-land on LTS because it causes #19562, but if it becomes desirable we could probably backport this either together with #19551 or a smaller custom fix

MayaLekova pushed a commit to MayaLekova/node that referenced this pull request May 8, 2018
Use `DoTryWrite()` to write data to the underlying socket.
This does probably not make any difference in performance
because the callback is still deferred (for now), but
brings TLSWrap in line with other things that write to
streams.

PR-URL: nodejs#18676
Reviewed-By: Ben Noordhuis <[email protected]>
Reviewed-By: Anatoli Papirovski <[email protected]>
Reviewed-By: James M Snell <[email protected]>
MayaLekova pushed a commit to MayaLekova/node that referenced this pull request May 8, 2018
Otherwise `this[kCurrentWriteRequest]` is set to a value even
if one of the `write` calls throws.

This is needed in order not to break tests in a later commit.

PR-URL: nodejs#18676
Reviewed-By: Ben Noordhuis <[email protected]>
Reviewed-By: Anatoli Papirovski <[email protected]>
Reviewed-By: James M Snell <[email protected]>
MayaLekova pushed a commit to MayaLekova/node that referenced this pull request May 8, 2018
PR-URL: nodejs#18676
Reviewed-By: Ben Noordhuis <[email protected]>
Reviewed-By: Anatoli Papirovski <[email protected]>
Reviewed-By: James M Snell <[email protected]>
MayaLekova pushed a commit to MayaLekova/node that referenced this pull request May 8, 2018
Encapsulate stream requests more:

- `WriteWrap` and `ShutdownWrap` classes are now tailored to the
  streams on which they are used. In particular, for most streams
  these are now plain `AsyncWrap`s and do not carry the overhead
  of unused libuv request data.
- Provide generic `Write()` and `Shutdown()` methods that wrap
  around the actual implementations, and make *usage* of streams
  easier, rather than implementing; for example, wrap objects
  don’t need to be provided by callers anymore.
- Use `EmitAfterWrite()` and `EmitAfterShutdown()` handlers to
  call the corresponding JS handlers, rather than always trying
  to call them. This makes usage of streams by other C++ code
  easier and leaner.

Also fix up some tests that were previously not actually testing
asynchronicity when the comments indicated that they would.

PR-URL: nodejs#18676
Reviewed-By: Ben Noordhuis <[email protected]>
Reviewed-By: Anatoli Papirovski <[email protected]>
Reviewed-By: James M Snell <[email protected]>
MayaLekova pushed a commit to MayaLekova/node that referenced this pull request May 8, 2018
The root cause is that `req_wrap` is created in `StreamBase::Write`
and passed to `TLSWrap::DoWrite`. In the TLS case the object gets
disposed and replaced with a new instance, but the caller's pointer is
never updated. When the `StreamBase::Write` method returns, it returns
a pointer to the freed object to the caller. In some cases when the
object memory has already been reused an assert is hit in
`WriteWrap::SetAllocatedStorage` because the pointer is non-null.

PR-URL: nodejs#18860
Refs: nodejs#18676
Reviewed-By: Anna Henningsen <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
c++ Issues and PRs that require attention from people who are familiar with C++. lib / src Issues and PRs related to general changes in the lib or src directory.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants