Skip to content

Commit cd78f39

Browse files
[C++] Wait until event loop terminates when closing the Client (#15316)
* [C++] Wait until event loops terminates when closing the Client Fixes #13267 ### Motivation Unlike Java client, the `Client` of C++ client has a `shutdown` method that is responsible to execute the following steps: 1. Call `shutdown` on all internal producers and consumers 2. Close all connections in the pool 3. Close all executors of the executor providers. When an executor is closed, it call `io_service::stop()`, which makes the event loop (`io_service::run()`) in another thread return as soon as possible. However, there is no wait operation. If a client failed to create a producer or consumer, the `close` method will call `shutdown` and close all executors immediately and exits the application. In this case, the detached event loop thread might not exit ASAP, then valgrind will detect the memory leak. This memory leak can be avoided by sleeping for a while after `Client::close` returns or there are still other things to do after that. However, we should still adopt the semantics that after `Client::shutdown` returns, all event loop threads should be terminated. ### Modifications - Add a timeout parameter to the `close` method of `ExecutorService` and `ExecutorServiceProvider` as the max blocking timeout if it's non-negative. - Add a `TimeoutProcessor` helper class to update the left timeout after calling all methods that accept the timeout parameter. - Call `close` on all `ExecutorServiceProvider`s in `ClientImpl::shutdown` with 500ms timeout, which could be long enough. In addition, in `handleClose` method, call `shutdown` in another thread to avoid the deadlock. ### Verifying this change After applying this patch, the reproduce code in #13627 will pass the valgrind check. ``` ==3013== LEAK SUMMARY: ==3013== definitely lost: 0 bytes in 0 blocks ==3013== indirectly lost: 0 bytes in 0 blocks ==3013== possibly lost: 0 bytes in 0 blocks ```
1 parent 7b3b4d7 commit cd78f39

5 files changed

Lines changed: 130 additions & 25 deletions

File tree

pulsar-client-cpp/lib/ClientImpl.cc

Lines changed: 30 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
#include "PartitionedConsumerImpl.h"
2727
#include "MultiTopicsConsumerImpl.h"
2828
#include "PatternMultiTopicsConsumerImpl.h"
29+
#include "TimeUtils.h"
2930
#include <pulsar/ConsoleLoggerFactory.h>
3031
#include <boost/algorithm/string/predicate.hpp>
3132
#include <sstream>
@@ -35,6 +36,7 @@
3536
#include <algorithm>
3637
#include <random>
3738
#include <mutex>
39+
#include <thread>
3840
#ifdef USE_LOG4CXX
3941
#include "Log4CxxLogger.h"
4042
#endif
@@ -538,13 +540,20 @@ void ClientImpl::handleClose(Result result, SharedInt numberOfOpenHandlers, Resu
538540
lock.unlock();
539541

540542
LOG_DEBUG("Shutting down producers and consumers for client");
541-
shutdown();
542-
if (callback) {
543-
if (closingError != ResultOk) {
544-
LOG_DEBUG("Problem in closing client, could not close one or more consumers or producers");
543+
// handleClose() is called in ExecutorService's event loop, while shutdown() tried to wait the event
544+
// loop exits. So here we use another thread to call shutdown().
545+
auto self = shared_from_this();
546+
std::thread shutdownTask{[this, self, callback] {
547+
shutdown();
548+
if (callback) {
549+
if (closingError != ResultOk) {
550+
LOG_DEBUG(
551+
"Problem in closing client, could not close one or more consumers or producers");
552+
}
553+
callback(closingError);
545554
}
546-
callback(closingError);
547-
}
555+
}};
556+
shutdownTask.detach();
548557
}
549558
}
550559

@@ -580,11 +589,25 @@ void ClientImpl::shutdown() {
580589
return;
581590
}
582591
LOG_DEBUG("ConnectionPool is closed");
583-
ioExecutorProvider_->close();
592+
593+
// 500ms as the timeout is long enough because ExecutorService::close calls io_service::stop() internally
594+
// and waits until io_service::run() in another thread returns, which should be as soon as possible after
595+
// stop() is called.
596+
TimeoutProcessor<std::chrono::milliseconds> timeoutProcessor{500};
597+
598+
timeoutProcessor.tik();
599+
ioExecutorProvider_->close(timeoutProcessor.getLeftTimeout());
600+
timeoutProcessor.tok();
584601
LOG_DEBUG("ioExecutorProvider_ is closed");
602+
603+
timeoutProcessor.tik();
585604
listenerExecutorProvider_->close();
605+
timeoutProcessor.tok();
586606
LOG_DEBUG("listenerExecutorProvider_ is closed");
607+
608+
timeoutProcessor.tik();
587609
partitionListenerExecutorProvider_->close();
610+
timeoutProcessor.tok();
588611
LOG_DEBUG("partitionListenerExecutorProvider_ is closed");
589612
}
590613

pulsar-client-cpp/lib/ExecutorService.cc

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
#include <boost/asio.hpp>
2222
#include <functional>
2323
#include <memory>
24+
#include "TimeUtils.h"
2425

2526
#include "LogUtils.h"
2627
DECLARE_LOG_OBJECT()
@@ -29,19 +30,24 @@ namespace pulsar {
2930

3031
ExecutorService::ExecutorService() {}
3132

32-
ExecutorService::~ExecutorService() { close(); }
33+
ExecutorService::~ExecutorService() { close(0); }
3334

3435
void ExecutorService::start() {
3536
auto self = shared_from_this();
3637
std::thread t{[self] {
3738
if (self->isClosed()) {
3839
return;
3940
}
41+
LOG_INFO("Run io_service in a single thread");
4042
boost::system::error_code ec;
4143
self->getIOService().run(ec);
4244
if (ec) {
4345
LOG_ERROR("Failed to run io_service: " << ec.message());
46+
} else {
47+
LOG_INFO("Event loop of ExecutorService exits successfully");
4448
}
49+
self->ioServiceDone_ = true;
50+
self->cond_.notify_all();
4551
}};
4652
t.detach();
4753
}
@@ -79,13 +85,23 @@ DeadlineTimerPtr ExecutorService::createDeadlineTimer() {
7985
return DeadlineTimerPtr(new boost::asio::deadline_timer(io_service_));
8086
}
8187

82-
void ExecutorService::close() {
88+
void ExecutorService::close(long timeoutMs) {
8389
bool expectedState = false;
8490
if (!closed_.compare_exchange_strong(expectedState, true)) {
8591
return;
8692
}
93+
if (timeoutMs == 0) { // non-blocking
94+
io_service_.stop();
95+
return;
96+
}
8797

98+
std::unique_lock<std::mutex> lock{mutex_};
8899
io_service_.stop();
100+
if (timeoutMs > 0) {
101+
cond_.wait_for(lock, std::chrono::milliseconds(timeoutMs), [this] { return ioServiceDone_.load(); });
102+
} else { // < 0
103+
cond_.wait(lock, [this] { return ioServiceDone_.load(); });
104+
}
89105
}
90106

91107
void ExecutorService::postWork(std::function<void(void)> task) { io_service_.post(task); }
@@ -106,14 +122,17 @@ ExecutorServicePtr ExecutorServiceProvider::get() {
106122
return executors_[idx];
107123
}
108124

109-
void ExecutorServiceProvider::close() {
125+
void ExecutorServiceProvider::close(long timeoutMs) {
110126
Lock lock(mutex_);
111127

112-
for (ExecutorList::iterator it = executors_.begin(); it != executors_.end(); ++it) {
113-
if (*it != NULL) {
114-
(*it)->close();
128+
TimeoutProcessor<std::chrono::milliseconds> timeoutProcessor{timeoutMs};
129+
for (auto &&executor : executors_) {
130+
timeoutProcessor.tik();
131+
if (executor) {
132+
executor->close(timeoutProcessor.getLeftTimeout());
115133
}
116-
it->reset();
134+
timeoutProcessor.tok();
135+
executor.reset();
117136
}
118137
}
119138
} // namespace pulsar

pulsar-client-cpp/lib/ExecutorService.h

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@
2020
#define _PULSAR_EXECUTOR_SERVICE_HEADER_
2121

2222
#include <atomic>
23+
#include <condition_variable>
24+
#include <chrono>
2325
#include <memory>
2426
#include <boost/asio.hpp>
2527
#include <boost/asio/ssl.hpp>
@@ -50,7 +52,8 @@ class PULSAR_PUBLIC ExecutorService : public std::enable_shared_from_this<Execut
5052
DeadlineTimerPtr createDeadlineTimer();
5153
void postWork(std::function<void(void)> task);
5254

53-
void close();
55+
// See TimeoutProcessor for the semantics of the parameter.
56+
void close(long timeoutMs = 3000);
5457

5558
IOService &getIOService() { return io_service_; }
5659
bool isClosed() const noexcept { return closed_; }
@@ -68,6 +71,9 @@ class PULSAR_PUBLIC ExecutorService : public std::enable_shared_from_this<Execut
6871
IOService::work work_{io_service_};
6972

7073
std::atomic_bool closed_{false};
74+
std::mutex mutex_;
75+
std::condition_variable cond_;
76+
std::atomic_bool ioServiceDone_{false};
7177

7278
ExecutorService();
7379

@@ -82,7 +88,8 @@ class PULSAR_PUBLIC ExecutorServiceProvider {
8288

8389
ExecutorServicePtr get();
8490

85-
void close();
91+
// See TimeoutProcessor for the semantics of the parameter.
92+
void close(long timeoutMs = 3000);
8693

8794
private:
8895
typedef std::vector<ExecutorServicePtr> ExecutorList;

pulsar-client-cpp/lib/TimeUtils.h

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919
#pragma once
2020

2121
#include <boost/date_time/local_time/local_time.hpp>
22+
#include <atomic>
23+
#include <chrono>
2224

2325
#include <pulsar/defines.h>
2426

@@ -33,4 +35,50 @@ class PULSAR_PUBLIC TimeUtils {
3335
static ptime now();
3436
static int64_t currentTimeMillis();
3537
};
38+
39+
// This class processes a timeout with the following semantics:
40+
// > 0: wait at most the timeout until a blocking operation completes
41+
// == 0: do not wait the blocking operation
42+
// < 0: wait infinitely until a blocking operation completes.
43+
//
44+
// Here is a simple example usage:
45+
//
46+
// ```c++
47+
// // Wait at most 300 milliseconds
48+
// TimeoutProcessor<std::chrono::milliseconds> timeoutProcessor{300};
49+
// while (!allOperationsAreDone()) {
50+
// timeoutProcessor.tik();
51+
// // This method may block for some time
52+
// performBlockingOperation(timeoutProcessor.getLeftTimeout());
53+
// timeoutProcessor.tok();
54+
// }
55+
// ```
56+
//
57+
// The template argument is the same as std::chrono::duration.
58+
template <typename Duration>
59+
class TimeoutProcessor {
60+
public:
61+
using Clock = std::chrono::high_resolution_clock;
62+
63+
TimeoutProcessor(long timeout) : leftTimeout_(timeout) {}
64+
65+
long getLeftTimeout() const noexcept { return leftTimeout_; }
66+
67+
void tik() { before_ = Clock::now(); }
68+
69+
void tok() {
70+
if (leftTimeout_ > 0) {
71+
leftTimeout_ -= std::chrono::duration_cast<Duration>(Clock::now() - before_).count();
72+
if (leftTimeout_ <= 0) {
73+
// The timeout exceeds, getLeftTimeout() will return 0 to indicate we should not wait more
74+
leftTimeout_ = 0;
75+
}
76+
}
77+
}
78+
79+
private:
80+
std::atomic_long leftTimeout_;
81+
std::chrono::time_point<Clock> before_;
82+
};
83+
3684
} // namespace pulsar

pulsar-client-cpp/tests/CustomLoggerTest.cc

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
#include <pulsar/ConsoleLoggerFactory.h>
2121
#include <LogUtils.h>
2222
#include <gtest/gtest.h>
23+
#include <atomic>
2324
#include <thread>
2425

2526
using namespace pulsar;
@@ -28,35 +29,42 @@ static std::vector<std::string> logLines;
2829

2930
class MyTestLogger : public Logger {
3031
public:
31-
MyTestLogger() = default;
32+
MyTestLogger(const std::string &fileName) : fileName_(fileName) {}
3233

3334
bool isEnabled(Level level) override { return true; }
3435

3536
void log(Level level, int line, const std::string &message) override {
3637
std::stringstream ss;
37-
ss << " " << level << ":" << line << " " << message << std::endl;
38+
ss << std::this_thread::get_id() << " " << level << " " << fileName_ << ":" << line << " " << message
39+
<< std::endl;
3840
logLines.emplace_back(ss.str());
3941
}
42+
43+
private:
44+
const std::string fileName_;
4045
};
4146

4247
class MyTestLoggerFactory : public LoggerFactory {
4348
public:
44-
Logger *getLogger(const std::string &fileName) override { return logger; }
45-
46-
private:
47-
MyTestLogger *logger = new MyTestLogger;
49+
Logger *getLogger(const std::string &fileName) override { return new MyTestLogger(fileName); }
4850
};
4951

5052
TEST(CustomLoggerTest, testCustomLogger) {
5153
// simulate new client created on a different thread (because logging factory is called once per thread)
52-
auto testThread = std::thread([] {
54+
std::atomic_int numLogLines{0};
55+
auto testThread = std::thread([&numLogLines] {
5356
ClientConfiguration clientConfig;
5457
auto customLogFactory = new MyTestLoggerFactory();
5558
clientConfig.setLogger(customLogFactory);
5659
// reset to previous log factory
5760
Client client("pulsar://localhost:6650", clientConfig);
5861
client.close();
59-
ASSERT_EQ(logLines.size(), 7);
62+
ASSERT_TRUE(logLines.size() > 0);
63+
for (auto &&line : logLines) {
64+
std::cout << line;
65+
std::cout.flush();
66+
}
67+
numLogLines = logLines.size();
6068
LogUtils::resetLoggerFactory();
6169
});
6270
testThread.join();
@@ -65,7 +73,7 @@ TEST(CustomLoggerTest, testCustomLogger) {
6573
Client client("pulsar://localhost:6650", clientConfig);
6674
client.close();
6775
// custom logger didn't get any new lines
68-
ASSERT_EQ(logLines.size(), 7);
76+
ASSERT_EQ(logLines.size(), numLogLines);
6977
}
7078

7179
TEST(CustomLoggerTest, testConsoleLoggerFactory) {

0 commit comments

Comments
 (0)