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

tls: simplify write mechanism #17883

Closed
wants to merge 2 commits into from

Conversation

addaleax
Copy link
Member

  • tls: refactor write queues away

    The TLS implementation previously had two separate queues for
    WriteWrap instances, one for currently active and one for
    finishing writes (i.e. where the encrypted output is being written
    to the underlying socket).

    However, the streams implementation in Node doesn’t allow for
    more than one write to be active at a time; effectively,
    the only possible states were that:

    • No write was active.
    • The first write queue had one item, the second one was empty.
    • Only the second write queue had one item, the first one was empty.

    To reduce overhead and implementation complexity, remove these
    queues, and instead store a single WriteWrap pointer and
    keep track of whether its write callback should be called
    on the next invocation of InvokeQueued() or not
    (which is what being in the second queue previously effected).

  • tls: remove cleartext input data queue

    The TLS implementation previously kept a separate buffer for
    incoming pieces of data, into which buffers were copied
    before they were up for writing.

    This removes this buffer, and replaces it with a simple list
    of uv_buf_ts:

    • The previous implementation copied all incoming data into
      that buffer, both allocating new storage and wasting time
      with copy operations. Node’s streams/net implementation
      already has to make sure that the allocated memory stays
      fresh until the write is finished, since that is what
      libuv streams rely on anyway.
    • The fact that a separate kind of buffer, crypto::NodeBIO
      was used, was confusing: These BIO instances are
      only used to communicate with openssl’s streams system
      otherwise, whereas this one was purely for internal
      memory management.
    • The name clear_in_ was not very helpful.
Checklist
  • make -j4 test (UNIX), or vcbuild test (Windows) passes
  • commit message follows commit guidelines
Affected core subsystem(s)

tls

@addaleax addaleax added the tls Issues and PRs related to the tls subsystem. label Dec 27, 2017
@nodejs-github-bot nodejs-github-bot added c++ Issues and PRs that require attention from people who are familiar with C++. tls Issues and PRs related to the tls subsystem. labels Dec 27, 2017
@addaleax
Copy link
Member Author

@apapirovski
Copy link
Member

Related failure here: https://ci.nodejs.org/job/node-test-commit-linux/15238/nodes=fedora24/tapResults/ 😢

@addaleax
Copy link
Member Author

@apapirovski I couldn’t reproduce locally in a couple thousand runs … I’ll wait until CI has freed up a bit, then run stress tests to verify it’s a problem with this PR (if the rest of the CI doesn’t already point that out)

@apapirovski
Copy link
Member

@addaleax It's also very possible this is related to #17863 — maybe you could just land that, rebase and then run the stress test?

@addaleax
Copy link
Member Author

addaleax commented Dec 27, 2017

@apapirovski That would make a lot of sense! I’ll try that tomorrow or so – don’t want to rush in #17863 since it’s holiday season for a lot of people … :)

Edit:
Stress test this PR: https://ci.nodejs.org/job/node-stress-single-test/1589/
Stress test master: https://ci.nodejs.org/job/node-stress-single-test/1590/
Stress test #17863: https://ci.nodejs.org/job/node-stress-single-test/1591/

src/tls_wrap.cc Outdated
@@ -120,20 +117,18 @@ TLSWrap::~TLSWrap() {


void TLSWrap::MakePending() {
write_item_queue_.MoveBack(&pending_write_items_);
Copy link
Member

Choose a reason for hiding this comment

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

You can remove MoveBack() from util.h and util-inl.h now, this is the only place that uses it.

src/tls_wrap.cc Outdated
// This can be skipped in the error case because no further writes
// would succeed anyway.
for (; i < buffers.size(); ++i)
pending_cleartext_input_.push_back(buffers[i]);
Copy link
Member

Choose a reason for hiding this comment

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

pending_cleartext_input_.insert(pending_cleartext_input_.end(), &buffers[i], buffers.end());?

src/tls_wrap.cc Outdated
@@ -648,7 +640,7 @@ int TLSWrap::DoWrite(WriteWrap* w,

// No errors, queue rest
for (; i < count; i++)
clear_in_->Write(bufs[i].base, bufs[i].len);
pending_cleartext_input_.push_back(bufs[i]);
Copy link
Member

Choose a reason for hiding this comment

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

Likewise.

src/tls_wrap.cc Outdated
@@ -120,20 +117,18 @@ TLSWrap::~TLSWrap() {


void TLSWrap::MakePending() {
write_item_queue_.MoveBack(&pending_write_items_);
write_callback_scheduled_ = true;
}
Copy link
Member

Choose a reason for hiding this comment

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

As well, I wouldn't object to removing MakePending() and inlining write_callback_scheduled_ = true at its two or three call sites. It arguably obscures more than it abstracts.

@@ -171,11 +157,10 @@ class TLSWrap : public AsyncWrap,
StreamBase* stream_;
BIO* enc_in_;
BIO* enc_out_;
crypto::NodeBIO* clear_in_;
std::vector<uv_buf_t> pending_cleartext_input_;
Copy link
Member

Choose a reason for hiding this comment

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

Would a std::list be more appropriate 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.

@TimothyGu Why? uv_buf_t is a pretty small structure, so what I would guess is that the overhead of possibly having a small number of them being unused is more or less negligible compared to allocating a list node for every buffer?

Copy link
Member

Choose a reason for hiding this comment

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

based on the variations I tried with http2, the performance difference is inconsequential. Using std::vector here is just fine.

@addaleax
Copy link
Member Author

addaleax commented Jan 2, 2018

@addaleax addaleax added author ready PRs that have at least one approval, no pending requests for changes, and a CI started. dont-land-on-v4.x labels Jan 2, 2018
The TLS implementation previously had two separate queues for
WriteWrap instances, one for currently active and one for
finishing writes (i.e. where the encrypted output is being written
to the underlying socket).

However, the streams implementation in Node doesn’t allow for
more than one write to be active at a time; effectively,
the only possible states were that:

- No write was active.
- The first write queue had one item, the second one was empty.
- Only the second write queue had one item, the first one was empty.

To reduce overhead and implementation complexity, remove these
queues, and instead store a single `WriteWrap` pointer and
keep track of whether its write callback should be called
on the next invocation of `InvokeQueued()` or not
(which is what being in the second queue previously effected).
The TLS implementation previously kept a separate buffer for
incoming pieces of data, into which buffers were copied
before they were up for writing.

This removes this buffer, and replaces it with a simple list
of `uv_buf_t`s:

- The previous implementation copied all incoming data into
  that buffer, both allocating new storage and wasting time
  with copy operations. Node’s streams/net implementation
  already has to make sure that the allocated memory stays
  fresh until the write is finished, since that is what
  libuv streams rely on anyway.
- The fact that a separate kind of buffer, `crypto::NodeBIO`
  was used, was confusing: These `BIO` instances are
  only used to communicate with openssl’s streams system
  otherwise, whereas this one was purely for internal
  memory management.
- The name `clear_in_` was not very helpful.
@addaleax
Copy link
Member Author

addaleax commented Jan 7, 2018

Landed in d5fa4de, 46f783d

@addaleax addaleax closed this Jan 7, 2018
@addaleax addaleax removed the author ready PRs that have at least one approval, no pending requests for changes, and a CI started. label Jan 7, 2018
@addaleax addaleax deleted the tls-simplify-writes branch January 7, 2018 21:36
addaleax added a commit that referenced this pull request Jan 7, 2018
The TLS implementation previously had two separate queues for
WriteWrap instances, one for currently active and one for
finishing writes (i.e. where the encrypted output is being written
to the underlying socket).

However, the streams implementation in Node doesn’t allow for
more than one write to be active at a time; effectively,
the only possible states were that:

- No write was active.
- The first write queue had one item, the second one was empty.
- Only the second write queue had one item, the first one was empty.

To reduce overhead and implementation complexity, remove these
queues, and instead store a single `WriteWrap` pointer and
keep track of whether its write callback should be called
on the next invocation of `InvokeQueued()` or not
(which is what being in the second queue previously effected).

PR-URL: #17883
Reviewed-By: Ben Noordhuis <info@bnoordhuis.nl>
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Tobias Nießen <tniessen@tnie.de>
addaleax added a commit that referenced this pull request Jan 7, 2018
The TLS implementation previously kept a separate buffer for
incoming pieces of data, into which buffers were copied
before they were up for writing.

This removes this buffer, and replaces it with a simple list
of `uv_buf_t`s:

- The previous implementation copied all incoming data into
  that buffer, both allocating new storage and wasting time
  with copy operations. Node’s streams/net implementation
  already has to make sure that the allocated memory stays
  fresh until the write is finished, since that is what
  libuv streams rely on anyway.
- The fact that a separate kind of buffer, `crypto::NodeBIO`
  was used, was confusing: These `BIO` instances are
  only used to communicate with openssl’s streams system
  otherwise, whereas this one was purely for internal
  memory management.
- The name `clear_in_` was not very helpful.

PR-URL: #17883
Reviewed-By: Ben Noordhuis <info@bnoordhuis.nl>
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Tobias Nießen <tniessen@tnie.de>
@MylesBorins
Copy link
Contributor

This does not land cleanly on v9.x, could it be manually backported?

@addaleax
Copy link
Member Author

This should apply cleanly after #17650 is backported

evanlucas pushed a commit that referenced this pull request Jan 30, 2018
The TLS implementation previously had two separate queues for
WriteWrap instances, one for currently active and one for
finishing writes (i.e. where the encrypted output is being written
to the underlying socket).

However, the streams implementation in Node doesn’t allow for
more than one write to be active at a time; effectively,
the only possible states were that:

- No write was active.
- The first write queue had one item, the second one was empty.
- Only the second write queue had one item, the first one was empty.

To reduce overhead and implementation complexity, remove these
queues, and instead store a single `WriteWrap` pointer and
keep track of whether its write callback should be called
on the next invocation of `InvokeQueued()` or not
(which is what being in the second queue previously effected).

PR-URL: #17883
Reviewed-By: Ben Noordhuis <info@bnoordhuis.nl>
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Tobias Nießen <tniessen@tnie.de>
MylesBorins pushed a commit that referenced this pull request Feb 20, 2018
The TLS implementation previously kept a separate buffer for
incoming pieces of data, into which buffers were copied
before they were up for writing.

This removes this buffer, and replaces it with a simple list
of `uv_buf_t`s:

- The previous implementation copied all incoming data into
  that buffer, both allocating new storage and wasting time
  with copy operations. Node’s streams/net implementation
  already has to make sure that the allocated memory stays
  fresh until the write is finished, since that is what
  libuv streams rely on anyway.
- The fact that a separate kind of buffer, `crypto::NodeBIO`
  was used, was confusing: These `BIO` instances are
  only used to communicate with openssl’s streams system
  otherwise, whereas this one was purely for internal
  memory management.
- The name `clear_in_` was not very helpful.

PR-URL: #17883
Reviewed-By: Ben Noordhuis <info@bnoordhuis.nl>
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Tobias Nießen <tniessen@tnie.de>
@MylesBorins MylesBorins mentioned this pull request Feb 21, 2018
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++. tls Issues and PRs related to the tls subsystem.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

8 participants