Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions source/common/grpc/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -213,5 +213,15 @@ envoy_cc_library(
deps = [
":typed_async_client_lib",
"//source/common/protobuf:utility_lib",
"@com_google_absl//absl/container:btree",
],
)

envoy_cc_library(
name = "buffered_message_ttl_manager_lib",
hdrs = ["buffered_message_ttl_manager.h"],
deps = [
":buffered_async_client_lib",
"//envoy/event:dispatcher_interface",
],
)
33 changes: 29 additions & 4 deletions source/common/grpc/buffered_async_client.h
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,33 @@ namespace Grpc {

enum class BufferState { Buffered, PendingFlush };

class BufferedAsyncClientCallbacks {
public:
virtual ~BufferedAsyncClientCallbacks() = default;

/**
* Called if a message related with id has been proceeded successfully.
*
* @param id the ID related with buffered messages. This ID must be already published by client.
* If not, this callback will do nothing.
*/
virtual void onSuccess(uint64_t id) PURE;

/**
* Called if a message related with id has not been proceeded.
*
* @param id the ID related with buffered messages. This ID must be already published by client.
* If not, this callback will do nothing.
*/
virtual void onError(uint64_t id) PURE;
};

// This class wraps bidirectional gRPC and provides message arrival guarantee.
// It stores messages to be sent or in the process of being sent in a buffer,
// and can track the status of the message based on the ID assigned to each message.
// If a message fails to be sent, it can be re-buffered to guarantee its arrival.
template <class RequestType, class ResponseType> class BufferedAsyncClient {
template <class RequestType, class ResponseType>
class BufferedAsyncClient : public BufferedAsyncClientCallbacks {
public:
BufferedAsyncClient(uint32_t max_buffer_bytes, const Protobuf::MethodDescriptor& service_method,
Grpc::AsyncStreamCallbacks<ResponseType>& callbacks,
Expand Down Expand Up @@ -73,10 +95,13 @@ template <class RequestType, class ResponseType> class BufferedAsyncClient {
return inflight_message_ids;
}

void onSuccess(uint64_t message_id) { erasePendingMessage(message_id); }
void onSuccess(uint64_t message_id) override { erasePendingMessage(message_id); }

void onError(uint64_t message_id) {
if (message_buffer_.find(message_id) == message_buffer_.end()) {
void onError(uint64_t message_id) override {
const auto& message_it = message_buffer_.find(message_id);

if (message_it == message_buffer_.end() ||
message_it->second.first != Grpc::BufferState::PendingFlush) {
return;
}

Expand Down
66 changes: 66 additions & 0 deletions source/common/grpc/buffered_message_ttl_manager.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
#pragma once

#include <queue>

#include "envoy/event/dispatcher.h"

#include "source/common/grpc/buffered_async_client.h"

namespace Envoy {
namespace Grpc {

class BufferedMessageTtlManager {
Comment thread
Shikugawa marked this conversation as resolved.
public:
BufferedMessageTtlManager(Event::Dispatcher& dispatcher, BufferedAsyncClientCallbacks& callbacks,
std::chrono::milliseconds message_ack_timeout)
: dispatcher_(dispatcher), message_ack_timeout_(message_ack_timeout), callbacks_(callbacks),
timer_(dispatcher_.createTimer([this] { checkMessages(); })) {}

~BufferedMessageTtlManager() { timer_->disableTimer(); }

void setDeadline(absl::flat_hash_set<uint64_t>&& ids) {
Comment thread
Shikugawa marked this conversation as resolved.
Outdated
const auto expires_at = dispatcher_.timeSource().monotonicTime() + message_ack_timeout_;
deadline_.emplace(expires_at, std::move(ids));

if (!timer_->enabled()) {
timer_->enableTimer(message_ack_timeout_);
}
}

const std::queue<std::pair<MonotonicTime, absl::flat_hash_set<uint64_t>>>& deadline() {
Comment thread
Shikugawa marked this conversation as resolved.
Outdated
return deadline_;
}

private:
void checkMessages() {
Comment thread
Shikugawa marked this conversation as resolved.
Outdated
const auto now = dispatcher_.timeSource().monotonicTime();

while (!deadline_.empty()) {
auto& it = deadline_.front();
if (it.first > now) {
break;
}
for (auto&& id : it.second) {
// If the retrieved message is a PendingFlush, it means that the message
// has timed out. A timeout is treated as an error, and the callback will
// re-buffer the message.
callbacks_.onError(id);
Comment thread
Shikugawa marked this conversation as resolved.
Outdated
Comment thread
Shikugawa marked this conversation as resolved.
Outdated
}
deadline_.pop();
}

if (!deadline_.empty()) {
const auto earliest_timepoint = deadline_.front().first;
timer_->enableTimer(
std::chrono::duration_cast<std::chrono::milliseconds>(earliest_timepoint - now));
}
}

Event::Dispatcher& dispatcher_;
std::chrono::milliseconds message_ack_timeout_;
BufferedAsyncClientCallbacks& callbacks_;
Event::TimerPtr timer_;
std::queue<std::pair<MonotonicTime, absl::flat_hash_set<uint64_t>>> deadline_;
};
} // namespace Grpc
} // namespace Envoy
10 changes: 10 additions & 0 deletions test/common/grpc/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -202,3 +202,13 @@ envoy_cc_test(
"@envoy_api//envoy/config/core/v3:pkg_cc_proto",
],
)

envoy_cc_test(
name = "buffered_message_ttl_manager_test",
srcs = ["buffered_message_ttl_manager_test.cc"],
deps = [
"//source/common/event:dispatcher_lib",
"//source/common/grpc:buffered_message_ttl_manager_lib",
"//test/test_common:utility_lib",
],
)
67 changes: 67 additions & 0 deletions test/common/grpc/buffered_message_ttl_manager_test.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
#include <chrono>
#include <memory>

#include "source/common/event/dispatcher_impl.h"
#include "source/common/grpc/buffered_message_ttl_manager.h"

#include "test/test_common/utility.h"

#include "gmock/gmock.h"
#include "gtest/gtest.h"

namespace Envoy {
namespace Grpc {
namespace {

class MockBufferedAsyncClientCallbacks : public BufferedAsyncClientCallbacks {
public:
MOCK_METHOD(void, onSuccess, (uint64_t), ());
MOCK_METHOD(void, onError, (uint64_t), ());
};

using testing::Invoke;
using testing::NiceMock;

class BufferedMessageTtlManagerTest : public testing::Test {
public:
BufferedMessageTtlManagerTest()
: api_(Api::createApiForTest()), dispatcher_(api_->allocateDispatcher("test_thread")) {}

void SetUp() override {
ttl_manager_ = std::make_shared<BufferedMessageTtlManager>(*dispatcher_, callbacks_, msec_);
}

Api::ApiPtr api_;
Event::DispatcherPtr dispatcher_;
std::shared_ptr<BufferedMessageTtlManager> ttl_manager_;
NiceMock<MockBufferedAsyncClientCallbacks> callbacks_;
std::chrono::milliseconds msec_{1000};
};

TEST_F(BufferedMessageTtlManagerTest, Basic) {
Comment thread
Shikugawa marked this conversation as resolved.
Outdated
absl::flat_hash_set<uint64_t> ids{0};
EXPECT_CALL(callbacks_, onError(_)).WillOnce(Invoke([this] {
EXPECT_EQ(ttl_manager_->deadline().size(), 1);
absl::flat_hash_set<uint64_t> ids{1, 2};
EXPECT_CALL(callbacks_, onError(_)).Times(2);
ttl_manager_->setDeadline(std::move(ids));
}));
ttl_manager_->setDeadline(std::move(ids));

dispatcher_->run(Event::Dispatcher::RunType::Block);
EXPECT_EQ(ttl_manager_->deadline().size(), 0);

// Test if deadline queue is empty after queue cleared once.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Should this comment be before L54?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

fixed

absl::flat_hash_set<uint64_t> ids2{3};
EXPECT_CALL(callbacks_, onError(_)).WillOnce(Invoke([this] {
EXPECT_EQ(ttl_manager_->deadline().size(), 1);
}));
ttl_manager_->setDeadline(std::move(ids2));

dispatcher_->run(Event::Dispatcher::RunType::Block);
EXPECT_EQ(ttl_manager_->deadline().size(), 0);
}

} // namespace
} // namespace Grpc
} // namespace Envoy