This is an automated email from the ASF dual-hosted git repository.
wwbmmm pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/brpc.git
The following commit(s) were added to refs/heads/master by this push:
new 9aa41f79 Support flow-controlled gRPC client requests (#3430)
9aa41f79 is described below
commit 9aa41f79f2bc39ad081e8e2ab80fe3b4dead328b
Author: Xiaofeng Wang <[email protected]>
AuthorDate: Sun Aug 16 15:22:43 2026 +0800
Support flow-controlled gRPC client requests (#3430)
* Support flow-controlled gRPC client requests
- Split client request DATA frames according to the peer's connection and
stream flow-control windows.
- Buffer unsent DATA and resume transmission when WINDOW_UPDATE restores
capacity.
- Track pending request bytes per H2 connection and apply
socket_max_unwritten_bytes as an upper bound.
- Reject or reroute new requests once the pending DATA limit is reached.
- Release buffered DATA when an RPC fails, times out, or its stream is
removed.
- Add tests for fragmented transmission, deferred DATA flushing,
pending-byte accounting, and buffer cleanup.
* Fix pending HTTP/2 data limit race
- Check pending DATA capacity atomically with client stream insertion.
- Leave stream and window state unchanged when the limit is exceeded.
- Use an ephemeral port in the gRPC flow-control test.
- Avoid accessing an empty payload buffer in H2 frame tests.
---
src/brpc/policy/http2_rpc_protocol.cpp | 395 +++++++++++++++++++++----------
src/brpc/policy/http2_rpc_protocol.h | 26 +-
test/brpc_grpc_protocol_unittest.cpp | 50 ++++
test/brpc_h2_unsent_message_unittest.cpp | 212 +++++++++++++++++
test/brpc_http_rpc_protocol_unittest.cpp | 22 +-
5 files changed, 566 insertions(+), 139 deletions(-)
diff --git a/src/brpc/policy/http2_rpc_protocol.cpp
b/src/brpc/policy/http2_rpc_protocol.cpp
index d527a055..eb0e3531 100644
--- a/src/brpc/policy/http2_rpc_protocol.cpp
+++ b/src/brpc/policy/http2_rpc_protocol.cpp
@@ -28,6 +28,7 @@ DECLARE_bool(http_verbose);
DECLARE_int32(http_verbose_max_body_length);
DECLARE_int32(health_check_interval);
DECLARE_bool(usercode_in_pthread);
+DECLARE_int64(socket_max_unwritten_bytes);
namespace policy {
@@ -147,6 +148,12 @@ static int WriteAck(Socket* s, const void* data, size_t n)
{
return s->Write(&sendbuf, &wopt);
}
+static int WriteAck(Socket* s, butil::IOBuf* data) {
+ Socket::WriteOptions wopt;
+ wopt.ignore_eovercrowded = true;
+ return s->Write(data, &wopt);
+}
+
// [ https://tools.ietf.org/html/rfc7540#section-6.5.1 ]
enum H2SettingsIdentifier {
@@ -269,18 +276,13 @@ inline bool AddWindowSize(butil::atomic<int64_t>*
window_size, int64_t diff) {
// If a sender receives a WINDOW_UPDATE that causes a flow-control window
// to exceed this maximum, it MUST terminate either the stream or the
connection,
// as appropriate.
- int64_t before_add = window_size->fetch_add(diff,
butil::memory_order_relaxed);
- if ((((before_add | diff) >> 31) & 1) == 0) {
- // two positive int64_t, check positive overflow
- if ((before_add + diff) & (1 << 31)) {
- return false;
- }
- }
- if ((((before_add & diff) >> 31) & 1) == 1) {
- // two negative int64_t, check negaitive overflow
- if (((before_add + diff) & (1 << 31)) == 0) {
- return false;
- }
+ const int64_t before_add =
+ window_size->fetch_add(diff, butil::memory_order_relaxed);
+ const int64_t after_add = before_add + diff;
+ if (after_add > std::numeric_limits<int32_t>::max() ||
+ after_add < std::numeric_limits<int32_t>::min()) {
+ window_size->fetch_sub(diff, butil::memory_order_relaxed);
+ return false;
}
// window_size being negative is OK
return true;
@@ -323,20 +325,19 @@ inline H2Context::FrameHandler
FindFrameHandler(H2FrameType type) {
H2Context::H2Context(Socket* socket, const Server* server)
: _socket(socket)
- // Maximize the window size to make sending big request possible before
- // receving the remote settings.
- , _remote_window_left(H2Settings::MAX_WINDOW_SIZE)
+ , _remote_window_left(H2Settings::DEFAULT_INITIAL_WINDOW_SIZE)
, _conn_state(H2_CONNECTION_UNINITIALIZED)
, _last_received_stream_id(-1)
, _last_sent_stream_id(1)
, _goaway_stream_id(-1)
, _remote_settings_received(false)
+ , _pending_data_size(0)
, _deferred_window_update(0) {
// Stop printing the field which is useless for remote settings.
_remote_settings.connection_window_size = 0;
- // Maximize the window size to make sending big request possible before
- // receving the remote settings.
- _remote_settings.stream_window_size = H2Settings::MAX_WINDOW_SIZE;
+ // SETTINGS_INITIAL_WINDOW_SIZE defaults to 65535 until the peer sends a
+ // different value. Larger requests are resumed by WINDOW_UPDATE.
+ _remote_settings.stream_window_size =
H2Settings::DEFAULT_INITIAL_WINDOW_SIZE;
if (server) {
_unack_local_settings = server->options().h2_settings;
} else {
@@ -370,6 +371,16 @@ int H2Context::Init() {
return 0;
}
+H2Settings H2Context::remote_settings() const {
+ std::unique_lock<butil::Mutex> mu(_stream_mutex);
+ return _remote_settings;
+}
+
+size_t H2Context::VolatilePendingStreamSize() const {
+ std::unique_lock<butil::Mutex> mu(_stream_mutex);
+ return _pending_streams.size();
+}
+
H2StreamContext* H2Context::RemoveStreamAndDeferWU(int stream_id) {
H2StreamContext* sctx = NULL;
{
@@ -377,6 +388,8 @@ H2StreamContext* H2Context::RemoveStreamAndDeferWU(int
stream_id) {
if (!_pending_streams.erase(stream_id, &sctx)) {
return NULL;
}
+ CHECK_GE(_pending_data_size, sctx->_pending_data.size());
+ _pending_data_size -= sctx->_pending_data.size();
}
// The remote stream will not send any more data, sending back the
// stream-level WINDOW_UPDATE is pointless, just move the value into
@@ -394,6 +407,7 @@ void H2Context::RemoveGoAwayStreams(
std::unique_lock<butil::Mutex> mu(_stream_mutex);
_goaway_stream_id = goaway_stream_id;
_pending_streams.swap(tmp);
+ _pending_data_size = 0;
}
for (StreamMap::const_iterator it = tmp.begin(); it != tmp.end();
++it) {
out_streams->push_back(it->second);
@@ -408,6 +422,9 @@ void H2Context::RemoveGoAwayStreams(
}
}
for (size_t i = 0; i < out_streams->size(); ++i) {
+ CHECK_GE(_pending_data_size,
+ (*out_streams)[i]->_pending_data.size());
+ _pending_data_size -= (*out_streams)[i]->_pending_data.size();
_pending_streams.erase((*out_streams)[i]->stream_id());
}
}
@@ -429,6 +446,9 @@ int H2Context::TryToInsertStream(int stream_id,
H2StreamContext* ctx) {
}
H2StreamContext*& sctx = _pending_streams[stream_id];
if (sctx == NULL) {
+ // Synchronize creation with SETTINGS_INITIAL_WINDOW_SIZE updates.
+ ctx->_remote_window_left.store(_remote_settings.stream_window_size,
+ butil::memory_order_relaxed);
sctx = ctx;
return 0;
}
@@ -872,43 +892,27 @@ H2ParseResult H2Context::OnSettings(
_local_settings = _unack_local_settings;
return MakeH2Message(NULL);
}
- const int64_t old_stream_window_size = _remote_settings.stream_window_size;
- if (!_remote_settings_received) {
- // To solve the problem that sender can't send large request before
receving
- // remote setting, the initial window size of stream/connection is set
to
- // MAX_WINDOW_SIZE(see constructor of H2Context).
- // As a result, in the view of remote side, window size is 65535 by
default so
- // it may not send its stream size to sender, making stream size still
be
- // MAX_WINDOW_SIZE. In this case we need to revert this value to
default.
- H2Settings tmp_settings;
- if (!ParseH2Settings(&tmp_settings, it, frame_head.payload_size)) {
- LOG(ERROR) << "Fail to parse from SETTINGS";
- return MakeH2Error(H2_PROTOCOL_ERROR);
- }
- _remote_settings = tmp_settings;
- _remote_window_left.fetch_sub(
- H2Settings::MAX_WINDOW_SIZE -
H2Settings::DEFAULT_INITIAL_WINDOW_SIZE,
- butil::memory_order_relaxed);
- _remote_settings_received = true;
- } else {
+ int64_t window_diff = 0;
+ {
+ std::unique_lock<butil::Mutex> mu(_stream_mutex);
+ const int64_t old_stream_window_size =
+ _remote_settings.stream_window_size;
if (!ParseH2Settings(&_remote_settings, it, frame_head.payload_size)) {
LOG(ERROR) << "Fail to parse from SETTINGS";
return MakeH2Error(H2_PROTOCOL_ERROR);
}
- }
- const int64_t window_diff =
- static_cast<int64_t>(_remote_settings.stream_window_size)
- - old_stream_window_size;
- if (window_diff) {
- // Do not update the connection flow-control window here, which can
only
- // be changed using WINDOW_UPDATE frames.
- // https://tools.ietf.org/html/rfc7540#section-6.9.2
- // TODO(gejun): Has race conditions with AppendAndDestroySelf
- std::unique_lock<butil::Mutex> mu(_stream_mutex);
- for (StreamMap::const_iterator it = _pending_streams.begin();
- it != _pending_streams.end(); ++it) {
- if (!AddWindowSize(&it->second->_remote_window_left, window_diff))
{
- return MakeH2Error(H2_FLOW_CONTROL_ERROR);
+ _remote_settings_received = true;
+ window_diff = static_cast<int64_t>(_remote_settings.stream_window_size)
+ - old_stream_window_size;
+ if (window_diff) {
+ // SETTINGS_INITIAL_WINDOW_SIZE changes all existing stream
windows,
+ // but never the connection-level flow-control window.
+ for (StreamMap::const_iterator it = _pending_streams.begin();
+ it != _pending_streams.end(); ++it) {
+ if (!AddWindowSize(&it->second->_remote_window_left,
+ window_diff)) {
+ return MakeH2Error(H2_FLOW_CONTROL_ERROR);
+ }
}
}
}
@@ -919,6 +923,9 @@ H2ParseResult H2Context::OnSettings(
LOG(WARNING) << "Fail to respond settings with ack to " << *_socket;
return MakeH2Error(H2_PROTOCOL_ERROR);
}
+ if (window_diff > 0 && !FlushPendingData(0)) {
+ return MakeH2Error(H2_PROTOCOL_ERROR);
+ }
return MakeH2Message(NULL);
}
@@ -1026,27 +1033,51 @@ H2ParseResult H2Context::OnWindowUpdate(
return MakeH2Error(H2_PROTOCOL_ERROR);
}
if (frame_head.stream_id == 0) {
- if (!AddWindowSize(&_remote_window_left, inc)) {
- LOG(ERROR) << "Invalid connection-level window_size_increment=" <<
inc;
- return MakeH2Error(H2_FLOW_CONTROL_ERROR);
+ {
+ std::unique_lock<butil::Mutex> mu(_stream_mutex);
+ if (!AddWindowSize(&_remote_window_left, inc)) {
+ LOG(ERROR) << "Invalid connection-level
window_size_increment=" << inc;
+ return MakeH2Error(H2_FLOW_CONTROL_ERROR);
+ }
+ }
+ if (!FlushPendingData(0)) {
+ return MakeH2Error(H2_PROTOCOL_ERROR);
}
return MakeH2Message(NULL);
} else {
- H2StreamContext* sctx = FindStream(frame_head.stream_id);
- if (sctx == NULL) {
- RPC_VLOG << "Fail to find stream_id=" << frame_head.stream_id;
- return MakeH2Message(NULL);
+ {
+ std::unique_lock<butil::Mutex> mu(_stream_mutex);
+ H2StreamContext** psctx =
_pending_streams.seek(frame_head.stream_id);
+ if (psctx == nullptr) {
+ RPC_VLOG << "Fail to find stream_id=" << frame_head.stream_id;
+ return MakeH2Message(nullptr);
+ }
+ if (!AddWindowSize(&(*psctx)->_remote_window_left, inc)) {
+ LOG(ERROR) << "Invalid stream-level window_size_increment=" <<
inc
+ << " to remote_window_left="
+ <<
(*psctx)->_remote_window_left.load(butil::memory_order_relaxed);
+ return MakeH2Error(H2_FLOW_CONTROL_ERROR);
+ }
}
- if (!AddWindowSize(&sctx->_remote_window_left, inc)) {
- LOG(ERROR) << "Invalid stream-level window_size_increment=" << inc
- << " to remote_window_left=" <<
sctx->_remote_window_left.load(butil::memory_order_relaxed);
- return MakeH2Error(H2_FLOW_CONTROL_ERROR);
+ if (!FlushPendingData(frame_head.stream_id)) {
+ return MakeH2Error(H2_PROTOCOL_ERROR);
}
return MakeH2Message(NULL);
}
}
void H2Context::Describe(std::ostream& os, const DescribeOptions& opt) const {
+ H2Settings remote_settings;
+ bool remote_settings_received = false;
+ size_t pending_stream_size = 0;
+ size_t pending_data_size = 0;
+ {
+ std::unique_lock<butil::Mutex> mu(_stream_mutex);
+ remote_settings = _remote_settings;
+ remote_settings_received = _remote_settings_received;
+ pending_stream_size = _pending_streams.size();
+ pending_data_size = _pending_data_size;
+ }
if (opt.verbose) {
os << '\n';
}
@@ -1058,8 +1089,8 @@ void H2Context::Describe(std::ostream& os, const
DescribeOptions& opt) const {
<< _deferred_window_update.load(butil::memory_order_relaxed)
<< sep << "remote_conn_window_left="
<< _remote_window_left.load(butil::memory_order_relaxed)
- << sep << "remote_settings=" << _remote_settings
- << sep << "remote_settings_received=" << _remote_settings_received
+ << sep << "remote_settings=" << remote_settings
+ << sep << "remote_settings_received=" << remote_settings_received
<< sep << "local_settings=" << _local_settings
<< sep << "hpacker={";
IndentingOStream os2(os, 2);
@@ -1071,7 +1102,8 @@ void H2Context::Describe(std::ostream& os, const
DescribeOptions& opt) const {
abandoned_size = _abandoned_streams.size();
}
os << sep << "abandoned_streams=" << abandoned_size
- << sep << "pending_streams=" << VolatilePendingStreamSize();
+ << sep << "pending_streams=" << pending_stream_size
+ << sep << "pending_data_size=" << pending_data_size;
if (opt.verbose) {
os << '\n';
}
@@ -1205,28 +1237,6 @@ void H2StreamContext::SetState(H2StreamState state) {
}
#endif
-bool H2StreamContext::ConsumeWindowSize(int64_t size) {
- // This method is guaranteed to be called in AppendAndDestroySelf() which
- // is run sequentially. As a result, _remote_window_left of this stream
- // context will not be decremented (may be incremented) because following
- // AppendAndDestroySelf() are not run yet.
- // This fact is important to make window_size changes to stream and
- // connection contexts transactionally.
- if (_remote_window_left.load(butil::memory_order_relaxed) < size) {
- return false;
- }
- if (!MinusWindowSize(&_conn_ctx->_remote_window_left, size)) {
- return false;
- }
- int64_t after_sub = _remote_window_left.fetch_sub(size,
butil::memory_order_relaxed) - size;
- if (after_sub < 0) {
- LOG(FATAL) << "Impossible, the http2 impl is buggy";
- _remote_window_left.fetch_add(size, butil::memory_order_relaxed);
- return false;
- }
- return true;
-}
-
int H2StreamContext::ConsumeHeaders(butil::IOBufBytesIterator& it) {
HPacker& hpacker = _conn_ctx->hpacker();
HttpHeader& h = header();
@@ -1316,43 +1326,53 @@ int
H2StreamContext::ConsumeHeaders(butil::IOBufBytesIterator& it) {
const CommonStrings* get_common_strings();
-static void PackH2Message(butil::IOBuf* out,
+static void PackH2Headers(butil::IOBuf* out,
butil::IOBuf& headers,
- butil::IOBuf& trailer_headers,
- const butil::IOBuf& data,
int stream_id,
- H2Context* conn_ctx) {
- const H2Settings& remote_settings = conn_ctx->remote_settings();
+ uint32_t max_frame_size,
+ bool end_stream) {
char headbuf[FRAME_HEAD_SIZE];
H2FrameHead headers_head = {
(uint32_t)headers.size(), H2_FRAME_HEADERS, 0, stream_id};
- if (data.empty() && trailer_headers.empty()) {
+ if (end_stream) {
headers_head.flags |= H2_FLAGS_END_STREAM;
}
- if (headers_head.payload_size <= remote_settings.max_frame_size) {
+ if (headers_head.payload_size <= max_frame_size) {
headers_head.flags |= H2_FLAGS_END_HEADERS;
SerializeFrameHead(headbuf, headers_head);
out->append(headbuf, sizeof(headbuf));
out->append(butil::IOBuf::Movable(headers));
} else {
- headers_head.payload_size = remote_settings.max_frame_size;
+ headers_head.payload_size = max_frame_size;
SerializeFrameHead(headbuf, headers_head);
out->append(headbuf, sizeof(headbuf));
headers.cutn(out, headers_head.payload_size);
H2FrameHead cont_head = {0, H2_FRAME_CONTINUATION, 0, stream_id};
while (!headers.empty()) {
- if (headers.size() <= remote_settings.max_frame_size) {
+ if (headers.size() <= max_frame_size) {
cont_head.flags |= H2_FLAGS_END_HEADERS;
cont_head.payload_size = headers.size();
} else {
- cont_head.payload_size = remote_settings.max_frame_size;
+ cont_head.payload_size = max_frame_size;
}
SerializeFrameHead(headbuf, cont_head);
out->append(headbuf, FRAME_HEAD_SIZE);
headers.cutn(out, cont_head.payload_size);
}
}
+}
+
+static void PackH2Message(butil::IOBuf* out,
+ butil::IOBuf& headers,
+ butil::IOBuf& trailer_headers,
+ const butil::IOBuf& data,
+ int stream_id,
+ H2Context* conn_ctx) {
+ const H2Settings& remote_settings = conn_ctx->remote_settings();
+ char headbuf[FRAME_HEAD_SIZE];
+ PackH2Headers(out, headers, stream_id, remote_settings.max_frame_size,
+ data.empty() && trailer_headers.empty());
if (!data.empty()) {
H2FrameHead data_head = {0, H2_FRAME_DATA, 0, stream_id};
butil::IOBufBytesIterator it(data);
@@ -1388,6 +1408,135 @@ static void PackH2Message(butil::IOBuf* out,
}
}
+void H2Context::AppendPendingDataLocked(H2StreamContext* sctx,
+ butil::IOBuf* out) {
+ CHECK(sctx != nullptr);
+ const uint32_t max_frame_size = _remote_settings.max_frame_size;
+ char headbuf[FRAME_HEAD_SIZE];
+ while (!sctx->_pending_data.empty()) {
+ const int64_t conn_window =
+ _remote_window_left.load(butil::memory_order_relaxed);
+ const int64_t stream_window =
+ sctx->_remote_window_left.load(butil::memory_order_relaxed);
+ if (conn_window <= 0 || stream_window <= 0) {
+ break;
+ }
+ const size_t payload_size = std::min(
+ sctx->_pending_data.size(),
+ std::min(static_cast<size_t>(max_frame_size),
+ static_cast<size_t>(std::min(conn_window,
stream_window))));
+ CHECK_GT(payload_size, 0u);
+ _remote_window_left.fetch_sub(payload_size,
butil::memory_order_relaxed);
+ sctx->_remote_window_left.fetch_sub(payload_size,
+ butil::memory_order_relaxed);
+
+ H2FrameHead data_head = {
+ static_cast<uint32_t>(payload_size), H2_FRAME_DATA, 0,
+ sctx->stream_id()};
+ if (payload_size == sctx->_pending_data.size()) {
+ data_head.flags |= H2_FLAGS_END_STREAM;
+ }
+ SerializeFrameHead(headbuf, data_head);
+ out->append(headbuf, sizeof(headbuf));
+ sctx->_pending_data.cutn(out, payload_size);
+ CHECK_GE(_pending_data_size, payload_size);
+ _pending_data_size -= payload_size;
+ }
+}
+
+butil::Status H2Context::TryToInsertClientStream(
+ int stream_id, H2StreamContext* sctx, const butil::IOBuf& data,
+ butil::IOBuf* out) {
+ std::unique_lock<butil::Mutex> mu(_stream_mutex);
+ if (_goaway_stream_id >= 0 && stream_id > _goaway_stream_id) {
+ return butil::Status(ELOGOFF, "the connection just issued GOAWAY");
+ }
+ if (_pending_streams.seek(stream_id) != nullptr) {
+ return butil::Status(EINTERNAL,
+ "Fail to insert existing stream_id");
+ }
+ if (_pending_streams.size() >= _remote_settings.max_concurrent_streams) {
+ return butil::Status(
+ ELIMIT, "Pending Stream count exceeds max concurrent stream");
+ }
+
+ sctx->_remote_window_left.store(_remote_settings.stream_window_size,
+ butil::memory_order_relaxed);
+ const int64_t conn_window =
+ _remote_window_left.load(butil::memory_order_relaxed);
+ const int64_t stream_window =
+ sctx->_remote_window_left.load(butil::memory_order_relaxed);
+ size_t sendable_size = 0;
+ if (conn_window > 0 && stream_window > 0) {
+ sendable_size = std::min(
+ data.size(),
+ static_cast<size_t>(std::min(conn_window, stream_window)));
+ }
+ const size_t pending_size = data.size() - sendable_size;
+ if (FLAGS_socket_max_unwritten_bytes > 0) {
+ const auto limit =
+ static_cast<size_t>(FLAGS_socket_max_unwritten_bytes);
+ // Check and reserve pending bytes under the same lock. Otherwise,
+ // concurrent requests may all observe available capacity before any
+ // of them adds its unsent DATA.
+ if (_pending_data_size > limit ||
+ pending_size > limit - _pending_data_size) {
+ return butil::Status(EOVERCROWDED,
+ "Too much pending HTTP/2 request data");
+ }
+ }
+
+ // Mutate stream and window state only after all failure checks above.
+ _pending_streams[stream_id] = sctx;
+ if (!data.empty()) {
+ CHECK(sctx->_pending_data.empty());
+ sctx->_pending_data = data;
+ _pending_data_size += data.size();
+ AppendPendingDataLocked(sctx, out);
+ }
+ return butil::Status::OK();
+}
+
+void H2Context::ClearPendingData(int stream_id) {
+ std::unique_lock<butil::Mutex> mu(_stream_mutex);
+ H2StreamContext** psctx = _pending_streams.seek(stream_id);
+ if (psctx == nullptr) {
+ return;
+ }
+ CHECK_GE(_pending_data_size, (*psctx)->_pending_data.size());
+ _pending_data_size -= (*psctx)->_pending_data.size();
+ (*psctx)->_pending_data.clear();
+}
+
+bool H2Context::PendingDataOvercrowded() const {
+ std::unique_lock<butil::Mutex> mu(_stream_mutex);
+ return FLAGS_socket_max_unwritten_bytes > 0 &&
+ _pending_data_size >=
+ static_cast<size_t>(FLAGS_socket_max_unwritten_bytes);
+}
+
+bool H2Context::FlushPendingData(int stream_id) {
+ butil::IOBuf out;
+ {
+ std::unique_lock<butil::Mutex> mu(_stream_mutex);
+ if (stream_id != 0) {
+ H2StreamContext** psctx = _pending_streams.seek(stream_id);
+ if (psctx != nullptr) {
+ AppendPendingDataLocked(*psctx, &out);
+ }
+ } else {
+ for (StreamMap::const_iterator it = _pending_streams.begin();
+ it != _pending_streams.end(); ++it) {
+ if (_remote_window_left.load(butil::memory_order_relaxed) <=
0) {
+ break;
+ }
+ AppendPendingDataLocked(it->second, &out);
+ }
+ }
+ }
+ return out.empty() || WriteAck(_socket, &out) == 0;
+}
+
H2UnsentRequest* H2UnsentRequest::New(Controller* c) {
const HttpHeader& h = c->http_request();
const CommonStrings* const common = get_common_strings();
@@ -1485,12 +1634,13 @@ void
H2UnsentRequest::DestroyStreamUserData(SocketUniquePtr& sending_sock,
int error_code,
bool /*end_of_rpc*/) {
RemoveRefOnQuit deref_self(this);
- if (sending_sock != NULL && error_code != 0) {
+ if (sending_sock != nullptr && error_code != 0) {
CHECK_EQ(cntl, _cntl);
std::unique_lock<butil::Mutex> mu(_mutex);
_cntl = NULL;
if (_stream_id != 0) {
H2Context* ctx =
static_cast<H2Context*>(sending_sock->parsing_context());
+ ctx->ClearPendingData(_stream_id);
ctx->AddAbandonedStream(_stream_id);
}
}
@@ -1534,11 +1684,6 @@ H2UnsentRequest::AppendAndDestroySelf(butil::IOBuf* out,
Socket* socket) {
out->append(settingsbuf, nb);
}
- // TODO(zhujiashun): also check this in server push
- if (ctx->VolatilePendingStreamSize() >
ctx->remote_settings().max_concurrent_streams) {
- return butil::Status(ELIMIT, "Pending Stream count exceeds max
concurrent stream");
- }
-
// Although the critical section looks huge, it should rarely be contended
// since timeout of RPC is much larger than the delay of sending.
std::unique_lock<butil::Mutex> mu(_mutex);
@@ -1557,30 +1702,15 @@ H2UnsentRequest::AppendAndDestroySelf(butil::IOBuf*
out, Socket* socket) {
}
_sctx->Init(ctx, id);
- // check flow control restriction
- if (!_cntl->request_attachment().empty()) {
- const int64_t data_size = _cntl->request_attachment().size();
- if (!_sctx->ConsumeWindowSize(data_size)) {
- return butil::Status(ELIMIT, "remote_window_left is not enough,
data_size=%" PRId64, data_size);
- }
- }
-
- const int rc = ctx->TryToInsertStream(id, _sctx.get());
- if (rc < 0) {
- return butil::Status(EINTERNAL, "Fail to insert existing stream_id");
- } else if (rc > 0) {
- return butil::Status(ELOGOFF, "the connection just issued GOAWAY");
- }
- _stream_id = _sctx->stream_id();
- // After calling TryToInsertStream, the ownership of _sctx is transferred
to ctx
- _sctx.release();
+ H2StreamContext* const sctx = _sctx.get();
HPacker& hpacker = ctx->hpacker();
butil::IOBufAppender appender;
HPackOptions options;
options.encode_name = FLAGS_h2_hpack_encode_name;
options.encode_value = FLAGS_h2_hpack_encode_value;
- if (ctx->remote_settings().header_table_size == 0) {
+ const H2Settings remote_settings = ctx->remote_settings();
+ if (remote_settings.header_table_size == 0) {
options.index_policy = HPACK_NEVER_INDEX_HEADER;
}
@@ -1597,8 +1727,24 @@ H2UnsentRequest::AppendAndDestroySelf(butil::IOBuf* out,
Socket* socket) {
}
butil::IOBuf frag;
appender.move_to(frag);
- butil::IOBuf dummy_buf;
- PackH2Message(out, frag, dummy_buf, _cntl->request_attachment(),
_stream_id, ctx);
+ const butil::IOBuf& request_data = _cntl->request_attachment();
+ PackH2Headers(out, frag, id, remote_settings.max_frame_size,
+ request_data.empty());
+ const butil::Status insert_status =
+ ctx->TryToInsertClientStream(id, sctx, request_data, out);
+ if (!insert_status.ok()) {
+ return insert_status;
+ }
+ _stream_id = id;
+ // TryToInsertClientStream transfers ownership of _sctx to ctx on success.
+ _sctx.release();
+ const int64_t conn_wu = ctx->ReleaseDeferredWindowUpdate();
+ if (conn_wu > 0) {
+ char winbuf[FRAME_HEAD_SIZE + 4];
+ SerializeFrameHead(winbuf, 4, H2_FRAME_WINDOW_UPDATE, 0, 0);
+ SaveUint32(winbuf + FRAME_HEAD_SIZE, conn_wu);
+ out->append(winbuf, sizeof(winbuf));
+ }
return butil::Status::OK();
}
@@ -1821,7 +1967,8 @@ void PackH2Request(butil::IOBuf*,
static bool IsH2SocketValid(Socket* s) {
H2Context* c = static_cast<H2Context*>(s->parsing_context());
- return (c == NULL || !c->RunOutStreams());
+ return c == nullptr ||
+ (!c->RunOutStreams() && !c->PendingDataOvercrowded());
}
StreamUserData* H2GlobalStreamCreator::OnCreatingStream(
diff --git a/src/brpc/policy/http2_rpc_protocol.h
b/src/brpc/policy/http2_rpc_protocol.h
index b4422ee0..27055ae9 100644
--- a/src/brpc/policy/http2_rpc_protocol.h
+++ b/src/brpc/policy/http2_rpc_protocol.h
@@ -258,8 +258,6 @@ public:
return _deferred_window_update.exchange(0,
butil::memory_order_relaxed);
}
- bool ConsumeWindowSize(int64_t size);
-
#if defined(BRPC_H2_STREAM_STATE)
H2StreamState state() const { return _state; }
void SetState(H2StreamState state);
@@ -276,6 +274,9 @@ friend class H2Context;
butil::atomic<int64_t> _deferred_window_update;
uint64_t _correlation_id;
butil::IOBuf _remaining_header_fragment;
+ // Request body which cannot be sent yet due to remote flow control.
+ // Accessed under H2Context::_stream_mutex.
+ butil::IOBuf _pending_data;
};
StreamCreator* get_h2_global_stream_creator();
@@ -319,7 +320,7 @@ public:
// main_socket: the socket owns this object as parsing_context
// server: NULL means client-side
H2Context(Socket* main_socket, const Server* server);
- ~H2Context();
+ ~H2Context() override;
// Must be called before usage.
int Init();
@@ -337,10 +338,13 @@ public:
// Try to map stream_id to ctx if stream_id does not exist before
// Returns 0 on success, -1 on exist, 1 on goaway.
int TryToInsertStream(int stream_id, H2StreamContext* ctx);
- size_t VolatilePendingStreamSize() const { return _pending_streams.size();
}
+ size_t VolatilePendingStreamSize() const;
+ bool PendingDataOvercrowded() const;
HPacker& hpacker() { return _hpacker; }
- const H2Settings& remote_settings() const { return _remote_settings; }
+ // Return a consistent snapshot because SETTINGS may be processed by the
+ // socket reader while a request is being packed by a writer.
+ H2Settings remote_settings() const;
const H2Settings& local_settings() const { return _local_settings; }
bool is_client_side() const { return _socket->CreatedByConnect(); }
@@ -374,6 +378,15 @@ friend void InitFrameHandlers();
void RemoveGoAwayStreams(int goaway_stream_id,
std::vector<H2StreamContext*>* out_streams);
H2StreamContext* FindStream(int stream_id);
+ // Atomically checks stream and pending-DATA limits, inserts the client
+ // stream, and appends DATA allowed by the current remote windows. On
+ // success, ownership of sctx is transferred to this context. On failure,
+ // no stream, window, or pending-DATA state is changed.
+ butil::Status TryToInsertClientStream(
+ int stream_id, H2StreamContext*, const butil::IOBuf&, butil::IOBuf*);
+ void AppendPendingDataLocked(H2StreamContext*, butil::IOBuf*);
+ void ClearPendingData(int stream_id);
+ bool FlushPendingData(int stream_id);
// True if the connection is established by client, otherwise it's
// accepted by server.
@@ -393,6 +406,9 @@ friend void InitFrameHandlers();
typedef butil::FlatMap<int, H2StreamContext*> StreamMap;
mutable butil::Mutex _stream_mutex;
StreamMap _pending_streams;
+ // Total bytes retained in H2StreamContext::_pending_data on this
+ // connection. Accessed under _stream_mutex.
+ size_t _pending_data_size;
butil::atomic<int64_t> _deferred_window_update;
};
diff --git a/test/brpc_grpc_protocol_unittest.cpp
b/test/brpc_grpc_protocol_unittest.cpp
index f170639d..5a9752ea 100644
--- a/test/brpc_grpc_protocol_unittest.cpp
+++ b/test/brpc_grpc_protocol_unittest.cpp
@@ -88,6 +88,17 @@ public:
}
};
+class WindowGrpcService : public ::test::GrpcService {
+public:
+ void Method(::google::protobuf::RpcController*,
+ const ::test::GrpcRequest* req,
+ ::test::GrpcResponse* res,
+ ::google::protobuf::Closure* done) override {
+ brpc::ClosureGuard done_guard(done);
+ res->set_message(req->message());
+ }
+};
+
class GrpcTest : public ::testing::Test {
protected:
GrpcTest() {
@@ -272,4 +283,43 @@ TEST_F(GrpcTest, GrpcTimeOut) {
}
}
+TEST(GrpcProtocol, client_sends_large_request_with_small_remote_window) {
+ WindowGrpcService service;
+ brpc::Server server;
+ ASSERT_EQ(0, server.AddService(&service, brpc::SERVER_DOESNT_OWN_SERVICE));
+ brpc::ServerOptions server_options;
+ server_options.h2_settings.stream_window_size = 32;
+ ASSERT_EQ(0, server.Start("127.0.0.1:0", &server_options));
+
+ brpc::Channel channel;
+ brpc::ChannelOptions channel_options;
+ channel_options.protocol = g_protocol;
+ channel_options.timeout_ms = 10000;
+ ASSERT_EQ(0, channel.Init(server.listen_address(), &channel_options));
+ test::GrpcService_Stub stub(&channel);
+
+ // Establish the H2 connection and receive the server SETTINGS first.
+ {
+ test::GrpcRequest request;
+ test::GrpcResponse response;
+ brpc::Controller cntl;
+ request.set_message("warmup");
+ request.set_gzip(false);
+ request.set_return_error(false);
+ stub.Method(&cntl, &request, &response, nullptr);
+ ASSERT_FALSE(cntl.Failed()) << cntl.ErrorText();
+ ASSERT_EQ(request.message(), response.message());
+ }
+
+ test::GrpcRequest request;
+ test::GrpcResponse response;
+ brpc::Controller cntl;
+ request.set_message(std::string(128 * 1024, 'x'));
+ request.set_gzip(false);
+ request.set_return_error(false);
+ stub.Method(&cntl, &request, &response, nullptr);
+ EXPECT_FALSE(cntl.Failed()) << cntl.ErrorText();
+ EXPECT_EQ(request.message(), response.message());
+}
+
} // namespace
diff --git a/test/brpc_h2_unsent_message_unittest.cpp
b/test/brpc_h2_unsent_message_unittest.cpp
index 5e3b266d..1c7b985a 100644
--- a/test/brpc_h2_unsent_message_unittest.cpp
+++ b/test/brpc_h2_unsent_message_unittest.cpp
@@ -27,11 +27,223 @@
#include "brpc/policy/http2_rpc_protocol.h"
#include "gperftools_helper.h"
+namespace brpc {
+DECLARE_int64(socket_max_unwritten_bytes);
+}
+
int main(int argc, char* argv[]) {
testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}
+namespace {
+
+brpc::policy::H2FrameHead PopFrame(butil::IOBuf* buf, std::string* payload) {
+ char head[brpc::policy::FRAME_HEAD_SIZE];
+ CHECK_EQ(sizeof(head), buf->cutn(head, sizeof(head)));
+ brpc::policy::H2FrameHead frame;
+ frame.payload_size =
+ (static_cast<uint8_t>(head[0]) << 16) |
+ (static_cast<uint8_t>(head[1]) << 8) |
+ static_cast<uint8_t>(head[2]);
+ frame.type = static_cast<brpc::policy::H2FrameType>(head[3]);
+ frame.flags = head[4];
+ frame.stream_id =
+ (static_cast<uint8_t>(head[5]) << 24) |
+ (static_cast<uint8_t>(head[6]) << 16) |
+ (static_cast<uint8_t>(head[7]) << 8) |
+ static_cast<uint8_t>(head[8]);
+ payload->resize(frame.payload_size);
+ if (frame.payload_size != 0) {
+ CHECK_EQ(frame.payload_size,
+ buf->cutn(&(*payload)[0], frame.payload_size));
+ }
+ return frame;
+}
+
+} // namespace
+
+TEST(H2UnsentMessage, split_request_data_by_remote_window) {
+ brpc::SocketId id;
+ brpc::SocketUniquePtr sock;
+ brpc::SocketOptions options;
+ options.user = brpc::get_client_side_messenger();
+ ASSERT_EQ(0, brpc::Socket::Create(options, &id));
+ ASSERT_EQ(0, brpc::Socket::Address(id, &sock));
+
+ brpc::policy::H2Context* ctx =
+ new brpc::policy::H2Context(sock.get(), nullptr);
+ ASSERT_EQ(0, ctx->Init());
+ sock->initialize_parsing_context(&ctx);
+ ctx->_remote_settings.max_frame_size = 4;
+ ctx->_remote_settings.stream_window_size = 5;
+ ctx->_remote_window_left = 6;
+
+ brpc::policy::H2StreamContext* sctx =
+ new brpc::policy::H2StreamContext(false);
+ sctx->Init(ctx, 1);
+
+ butil::IOBuf body;
+ body.append("abcdefghij");
+ butil::IOBuf out;
+ ASSERT_TRUE(ctx->TryToInsertClientStream(1, sctx, body, &out).ok());
+
+ std::string payload;
+ brpc::policy::H2FrameHead frame = PopFrame(&out, &payload);
+ EXPECT_EQ(4u, frame.payload_size);
+ EXPECT_EQ(brpc::policy::H2_FRAME_DATA, frame.type);
+ EXPECT_EQ(0, frame.flags & 0x1);
+ EXPECT_EQ("abcd", payload);
+ frame = PopFrame(&out, &payload);
+ EXPECT_EQ(1u, frame.payload_size);
+ EXPECT_EQ(0, frame.flags & 0x1);
+ EXPECT_EQ("e", payload);
+ EXPECT_TRUE(out.empty());
+ EXPECT_EQ(5u, sctx->_pending_data.size());
+ EXPECT_EQ(5u, ctx->_pending_data_size);
+ EXPECT_EQ(1, ctx->_remote_window_left);
+ EXPECT_EQ(0, sctx->_remote_window_left);
+
+ ctx->_remote_window_left.fetch_add(3, butil::memory_order_relaxed);
+ sctx->_remote_window_left.fetch_add(3, butil::memory_order_relaxed);
+ {
+ std::unique_lock<butil::Mutex> mu(ctx->_stream_mutex);
+ ctx->AppendPendingDataLocked(sctx, &out);
+ }
+ frame = PopFrame(&out, &payload);
+ EXPECT_EQ(3u, frame.payload_size);
+ EXPECT_EQ(0, frame.flags & 0x1);
+ EXPECT_EQ("fgh", payload);
+ EXPECT_TRUE(out.empty());
+ EXPECT_EQ(2u, ctx->_pending_data_size);
+
+ ctx->_remote_window_left.fetch_add(2, butil::memory_order_relaxed);
+ sctx->_remote_window_left.fetch_add(2, butil::memory_order_relaxed);
+ {
+ std::unique_lock<butil::Mutex> mu(ctx->_stream_mutex);
+ ctx->AppendPendingDataLocked(sctx, &out);
+ }
+ frame = PopFrame(&out, &payload);
+ EXPECT_EQ(2u, frame.payload_size);
+ EXPECT_NE(0, frame.flags & 0x1);
+ EXPECT_EQ("ij", payload);
+ EXPECT_TRUE(out.empty());
+ EXPECT_TRUE(sctx->_pending_data.empty());
+ EXPECT_EQ(0u, ctx->_pending_data_size);
+}
+
+TEST(H2UnsentMessage, request_does_not_fail_when_body_exceeds_window) {
+ brpc::SocketId id;
+ brpc::SocketUniquePtr sock;
+ brpc::SocketOptions options;
+ options.user = brpc::get_client_side_messenger();
+ ASSERT_EQ(0, brpc::Socket::Create(options, &id));
+ ASSERT_EQ(0, brpc::Socket::Address(id, &sock));
+
+ brpc::policy::H2Context* ctx =
+ new brpc::policy::H2Context(sock.get(), nullptr);
+ ASSERT_EQ(0, ctx->Init());
+ sock->initialize_parsing_context(&ctx);
+ ctx->_last_sent_stream_id = 1;
+ ctx->_remote_settings.max_frame_size = 4;
+ ctx->_remote_settings.stream_window_size = 3;
+ ctx->_remote_window_left = 3;
+
+ brpc::Controller cntl;
+ cntl.http_request().uri() = "http://example.com/echo";
+ cntl.request_attachment().append("abcdefghij");
+ brpc::policy::H2UnsentRequest* request =
+ brpc::policy::H2UnsentRequest::New(&cntl);
+ ASSERT_TRUE(request != nullptr);
+
+ butil::IOBuf out;
+ const butil::Status status =
+ request->AppendAndDestroySelf(&out, sock.get());
+ EXPECT_TRUE(status.ok()) << status;
+ brpc::policy::H2StreamContext* sctx = ctx->FindStream(1);
+ ASSERT_TRUE(sctx != nullptr);
+ EXPECT_EQ(7u, sctx->_pending_data.size());
+ EXPECT_EQ(7u, ctx->_pending_data_size);
+ EXPECT_EQ(0, ctx->_remote_window_left);
+ EXPECT_EQ(0, sctx->_remote_window_left);
+}
+
+TEST(H2UnsentMessage, invalid_window_update_does_not_change_window) {
+ brpc::SocketId id;
+ brpc::SocketUniquePtr sock;
+ brpc::SocketOptions options;
+ options.user = brpc::get_client_side_messenger();
+ ASSERT_EQ(0, brpc::Socket::Create(options, &id));
+ ASSERT_EQ(0, brpc::Socket::Address(id, &sock));
+
+ brpc::policy::H2Context* ctx =
+ new brpc::policy::H2Context(sock.get(), nullptr);
+ ASSERT_EQ(0, ctx->Init());
+ sock->initialize_parsing_context(&ctx);
+ const int64_t max_window_size = std::numeric_limits<int32_t>::max();
+ ctx->_remote_window_left = max_window_size;
+
+ const char increment[] = {0, 0, 0, 1};
+ butil::IOBuf payload;
+ payload.append(increment, sizeof(increment));
+ butil::IOBufBytesIterator it(payload);
+ const brpc::policy::H2FrameHead frame = {
+ 4, brpc::policy::H2_FRAME_WINDOW_UPDATE, 0, 0};
+ const brpc::policy::H2ParseResult result = ctx->OnWindowUpdate(it, frame);
+
+ EXPECT_EQ(brpc::H2_FLOW_CONTROL_ERROR, result.error());
+ EXPECT_EQ(max_window_size,
+ ctx->_remote_window_left.load(butil::memory_order_relaxed));
+}
+
+TEST(H2UnsentMessage, clear_pending_data_releases_overcrowded_buffer) {
+ brpc::SocketId id;
+ brpc::SocketUniquePtr sock;
+ brpc::SocketOptions options;
+ options.user = brpc::get_client_side_messenger();
+ ASSERT_EQ(0, brpc::Socket::Create(options, &id));
+ ASSERT_EQ(0, brpc::Socket::Address(id, &sock));
+
+ brpc::policy::H2Context* ctx =
+ new brpc::policy::H2Context(sock.get(), nullptr);
+ ASSERT_EQ(0, ctx->Init());
+ sock->initialize_parsing_context(&ctx);
+ ctx->_remote_window_left = 0;
+
+ butil::IOBuf body;
+ body.append("pending");
+ butil::IOBuf out;
+ GFLAGS_NAMESPACE::FlagSaver flag_saver;
+ brpc::FLAGS_socket_max_unwritten_bytes = body.size();
+
+ std::unique_ptr<brpc::policy::H2StreamContext> sctx(
+ new brpc::policy::H2StreamContext(false));
+ sctx->Init(ctx, 1);
+ ASSERT_TRUE(
+ ctx->TryToInsertClientStream(1, sctx.get(), body, &out).ok());
+ brpc::policy::H2StreamContext* inserted_sctx = sctx.release();
+ ASSERT_TRUE(out.empty());
+ ASSERT_EQ(body.size(), ctx->_pending_data_size);
+ EXPECT_TRUE(ctx->PendingDataOvercrowded());
+
+ std::unique_ptr<brpc::policy::H2StreamContext> rejected_sctx(
+ new brpc::policy::H2StreamContext(false));
+ rejected_sctx->Init(ctx, 3);
+ butil::IOBuf rejected_body;
+ rejected_body.append("x");
+ const butil::Status rejected = ctx->TryToInsertClientStream(
+ 3, rejected_sctx.get(), rejected_body, &out);
+ EXPECT_EQ(brpc::EOVERCROWDED, rejected.error_code());
+ EXPECT_EQ(nullptr, ctx->FindStream(3));
+ EXPECT_EQ(body.size(), ctx->_pending_data_size);
+
+ ctx->ClearPendingData(1);
+ EXPECT_FALSE(ctx->PendingDataOvercrowded());
+
+ EXPECT_TRUE(inserted_sctx->_pending_data.empty());
+ EXPECT_EQ(0u, ctx->_pending_data_size);
+}
+
TEST(H2UnsentMessage, request_throughput) {
brpc::Controller cntl;
butil::IOBuf request_buf;
diff --git a/test/brpc_http_rpc_protocol_unittest.cpp
b/test/brpc_http_rpc_protocol_unittest.cpp
index 6735a171..70691314 100644
--- a/test/brpc_http_rpc_protocol_unittest.cpp
+++ b/test/brpc_http_rpc_protocol_unittest.cpp
@@ -1643,12 +1643,12 @@ TEST_F(HttpTest, http2_sanity) {
options.protocol = "h2";
ASSERT_EQ(0, channel.Init(butil::EndPoint(butil::my_ip(), port),
&options));
- // Check that the first request with size larger than the default window
can
- // be sent out, when remote settings are not received.
+ // Check that the first request larger than the default window completes
+ // after SETTINGS and WINDOW_UPDATE make more capacity available.
brpc::Controller cntl;
test::EchoRequest big_req;
test::EchoResponse res;
- std::string message(2 * 1024 * 1024 /* 2M */, 'x');
+ std::string message(128 * 1024, 'x');
big_req.set_message(message);
cntl.http_request().set_method(brpc::HTTP_METHOD_POST);
cntl.http_request().uri() = "/EchoService/Echo";
@@ -1762,7 +1762,7 @@ TEST_F(HttpTest, http2_rst_after_header_and_data) {
ASSERT_TRUE(cntl.http_response().status_code() == brpc::HTTP_STATUS_OK);
}
-TEST_F(HttpTest, http2_window_used_up) {
+TEST_F(HttpTest, http2_window_used_up_buffers_request) {
brpc::Controller cntl;
butil::IOBuf request_buf;
test::EchoRequest req;
@@ -1780,6 +1780,8 @@ TEST_F(HttpTest, http2_window_used_up) {
buf.append(settingsbuf, brpc::policy::FRAME_HEAD_SIZE + nb);
brpc::policy::ParseH2Message(&buf, _h2_client_sock.get(), false, NULL);
+ brpc::policy::H2Context* ctx = static_cast<brpc::policy::H2Context*>(
+ _h2_client_sock->parsing_context());
int nsuc = brpc::H2Settings::DEFAULT_INITIAL_WINDOW_SIZE /
cntl.request_attachment().size();
for (int i = 0; i <= nsuc; i++) {
brpc::policy::H2UnsentRequest* h2_req =
brpc::policy::H2UnsentRequest::New(&cntl);
@@ -1789,15 +1791,15 @@ TEST_F(HttpTest, http2_window_used_up) {
NULL, &cntl, request_buf, NULL);
butil::IOBuf dummy;
butil::Status st = socket_message->AppendAndDestroySelf(&dummy,
_h2_client_sock.get());
+ ASSERT_TRUE(st.ok());
if (i == nsuc) {
- // the last message should fail according to flow control policy.
- ASSERT_FALSE(st.ok());
- ASSERT_TRUE(st.error_code() == brpc::ELIMIT);
-
ASSERT_TRUE(butil::StringPiece(st.error_str()).starts_with("remote_window_left
is not enough"));
+ ASSERT_GT(ctx->_pending_data_size, 0u);
+ h2_req->DestroyStreamUserData(
+ _h2_client_sock, &cntl, ECANCELED, false);
+ ASSERT_EQ(0u, ctx->_pending_data_size);
} else {
- ASSERT_TRUE(st.ok());
+ h2_req->DestroyStreamUserData(_h2_client_sock, &cntl, 0, false);
}
- h2_req->DestroyStreamUserData(_h2_client_sock, &cntl, 0, false);
}
}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]