Skip to content

Commit 644fcf7

Browse files
Merge pull request #149 from Countly/instant_stop
Use condition_variable for interruptible thread sleep in updateLoop
2 parents c3118ef + a33a7a4 commit 644fcf7

File tree

8 files changed

+310
-33
lines changed

8 files changed

+310
-33
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
## 26.1.0
22
- ! Minor breaking change ! SDK Behavior Settings is now enabled by default. Changes made on SDK Manager > SDK Behavior Settings on your server will affect SDK behavior directly.
33

4+
- Added `enableImmediateRequestOnStop` configuration option. When enabled, the update loop uses a condition variable instead of polling, allowing `stop()` and `setUpdateInterval()` to take effect immediately rather than waiting for the current sleep interval to expire.
45
- Added init config method "disableSDKBehaviorSettingsUpdates" to disable periodic SBS updates from the server.
56
- Added init config method "setSDKBehaviorSettings" to provide server configuration in JSON format during initialization.
67

CMakeLists.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@ if(COUNTLY_BUILD_TESTS)
118118
${CMAKE_CURRENT_SOURCE_DIR}/tests/crash.cpp
119119
${CMAKE_CURRENT_SOURCE_DIR}/tests/request.cpp
120120
${CMAKE_CURRENT_SOURCE_DIR}/tests/config.cpp
121+
${CMAKE_CURRENT_SOURCE_DIR}/tests/immediate_stop.cpp
121122
${CMAKE_CURRENT_SOURCE_DIR}/tests/sbs.cpp)
122123

123124
target_compile_options(countly-tests PRIVATE -g)
@@ -159,4 +160,4 @@ endif()
159160
CXX_EXTENSIONS NO)
160161
endif()
161162

162-
install(TARGETS countly ARCHIVE DESTINATION lib PUBLIC_HEADER DESTINATION include/countly)
163+
install(TARGETS countly ARCHIVE DESTINATION lib PUBLIC_HEADER DESTINATION include/countly)

include/countly.hpp

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@
44
#include "countly/constants.hpp"
55
#include "countly/countly_configuration.hpp"
66

7+
#include <atomic>
78
#include <chrono>
9+
#include <condition_variable>
810
#include <functional>
911
#include <iterator>
1012
#include <map>
@@ -62,6 +64,8 @@ class Countly : public cly::CountlyDelegates {
6264

6365
void disableAutoEventsOnUserProperties();
6466

67+
void enableImmediateRequestOnStop();
68+
6569
void setHTTPClient(HTTPClientFunction fun);
6670

6771
void setMetrics(const std::string &os, const std::string &os_version, const std::string &device, const std::string &resolution, const std::string &carrier, const std::string &app_version);
@@ -363,8 +367,8 @@ class Countly : public cly::CountlyDelegates {
363367
void updateLoop();
364368
void packEvents();
365369
bool began_session = false;
366-
bool is_being_disposed = false;
367-
bool is_sdk_initialized = false;
370+
std::atomic<bool> is_being_disposed{false};
371+
std::atomic<bool> is_sdk_initialized{false};
368372

369373
std::chrono::system_clock::time_point last_sent_session_request;
370374
nlohmann::json session_params;
@@ -382,9 +386,10 @@ class Countly : public cly::CountlyDelegates {
382386
std::shared_ptr<std::mutex> mutex = std::make_shared<std::mutex>();
383387

384388
bool is_queue_being_processed = false;
385-
bool enable_automatic_session = false;
386-
bool stop_thread = false;
387-
bool running = false;
389+
std::atomic<bool> enable_automatic_session{false};
390+
std::atomic<bool> stop_thread{false};
391+
std::atomic<bool> running{false};
392+
std::condition_variable stop_cv; // Wakes updateLoop immediately on stop
388393
size_t wait_milliseconds = COUNTLY_KEEPALIVE_INTERVAL;
389394

390395
size_t max_events = COUNTLY_MAX_EVENTS_DEFAULT;

include/countly/countly_configuration.hpp

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,13 @@ struct CountlyConfiguration {
7272

7373
bool autoEventsOnUserProperties = true;
7474

75+
/**
76+
* Enable immediate stop notification using a condition variable.
77+
* When enabled, the update loop wakes immediately on stop instead of
78+
* waiting for the current sleep interval to expire.
79+
*/
80+
bool immediateRequestOnStop = false;
81+
7582
HTTPClientFunction http_client_function = nullptr;
7683

7784
nlohmann::json metrics;

src/countly.cpp

Lines changed: 86 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,11 @@ Countly &Countly::getInstance() {
4545
}
4646

4747
#ifdef COUNTLY_BUILD_TESTS
48-
void Countly::halt() { _sharedInstance.reset(new Countly()); }
48+
void Countly::halt() {
49+
if (_sharedInstance) {
50+
_sharedInstance->stop(); // joins threads, releases mutex normally
51+
}
52+
_sharedInstance.reset(new Countly()); }
4953
#endif
5054

5155
/**
@@ -157,6 +161,16 @@ void Countly::disableAutoEventsOnUserProperties() {
157161
mutex->unlock();
158162
}
159163

164+
void Countly::enableImmediateRequestOnStop() {
165+
if (is_sdk_initialized) {
166+
log(LogLevel::WARNING, "[Countly][enableImmediateRequestOnStop] You can not enable immediate request on stop after SDK initialization.");
167+
return;
168+
}
169+
170+
std::lock_guard<std::mutex> lk(*mutex);
171+
configuration->immediateRequestOnStop = true;
172+
}
173+
160174
void Countly::setMetrics(const std::string &os, const std::string &os_version, const std::string &device, const std::string &resolution, const std::string &carrier, const std::string &app_version) {
161175
if (is_sdk_initialized) {
162176
log(LogLevel::WARNING, "[Countly] setMetrics, This method can't be called after SDK initialization. Returning.");
@@ -566,9 +580,11 @@ void Countly::stop() {
566580
}
567581

568582
void Countly::_deleteThread() {
569-
mutex->lock();
570-
stop_thread = true;
571-
mutex->unlock();
583+
{
584+
std::lock_guard<std::mutex> lk(*mutex);
585+
stop_thread = true;
586+
}
587+
stop_cv.notify_one();
572588
if (thread && thread->joinable()) {
573589
try {
574590
thread->join();
@@ -580,9 +596,13 @@ void Countly::_deleteThread() {
580596
}
581597

582598
void Countly::setUpdateInterval(size_t milliseconds) {
583-
mutex->lock();
584-
wait_milliseconds = milliseconds;
585-
mutex->unlock();
599+
{
600+
std::lock_guard<std::mutex> lk(*mutex);
601+
wait_milliseconds = milliseconds;
602+
}
603+
if (configuration->immediateRequestOnStop) {
604+
stop_cv.notify_one();
605+
}
586606
}
587607

588608
void Countly::addEvent(const cly::Event &event) {
@@ -1380,30 +1400,69 @@ std::chrono::system_clock::duration Countly::getSessionDuration(std::chrono::sys
13801400
std::chrono::system_clock::duration Countly::getSessionDuration() { return Countly::getSessionDuration(Countly::getTimestamp()); }
13811401

13821402
void Countly::updateLoop() {
1383-
log(LogLevel::DEBUG, "[Countly] updateLoop, Start");
1384-
mutex->lock();
1385-
running = true;
1386-
mutex->unlock();
1387-
while (true) {
1388-
mutex->lock();
1389-
if (stop_thread) {
1390-
stop_thread = false;
1403+
log(LogLevel::DEBUG, "[Countly][updateLoop]");
1404+
{
1405+
std::lock_guard<std::mutex> lk(*mutex);
1406+
running = true;
1407+
}
1408+
try {
1409+
if (configuration->immediateRequestOnStop) {
1410+
while (true) {
1411+
{
1412+
std::unique_lock<std::mutex> lk(*mutex);
1413+
stop_cv.wait_for(lk, std::chrono::milliseconds(wait_milliseconds), [this] {
1414+
return stop_thread.load();
1415+
});
1416+
if (stop_thread) {
1417+
stop_thread = false;
1418+
running = false;
1419+
return;
1420+
}
1421+
}
1422+
if (enable_automatic_session == true && configuration->manualSessionControl == false) {
1423+
updateSession();
1424+
} else if (configuration->manualSessionControl == true) {
1425+
packEvents();
1426+
}
1427+
requestModule->processQueue(mutex);
1428+
}
1429+
} else {
1430+
while (true) {
1431+
size_t last_wait_milliseconds;
1432+
{
1433+
std::lock_guard<std::mutex> lk(*mutex);
1434+
if (stop_thread) {
1435+
stop_thread = false;
1436+
break;
1437+
}
1438+
last_wait_milliseconds = wait_milliseconds;
1439+
}
1440+
std::this_thread::sleep_for(std::chrono::milliseconds(last_wait_milliseconds));
1441+
if (enable_automatic_session == true && configuration->manualSessionControl == false) {
1442+
updateSession();
1443+
} else if (configuration->manualSessionControl == true) {
1444+
packEvents();
1445+
}
1446+
requestModule->processQueue(mutex);
1447+
}
1448+
std::lock_guard<std::mutex> lk(*mutex);
1449+
running = false;
1450+
}
1451+
} catch (const std::exception &e) {
1452+
bool acquired = mutex->try_lock();
1453+
running = false;
1454+
log(LogLevel::ERROR, std::string("[Countly][updateLoop] exception in update loop: ") + e.what());
1455+
if (acquired) {
13911456
mutex->unlock();
1392-
break;
13931457
}
1394-
size_t last_wait_milliseconds = wait_milliseconds;
1395-
mutex->unlock();
1396-
std::this_thread::sleep_for(std::chrono::milliseconds(last_wait_milliseconds));
1397-
if (enable_automatic_session == true && configuration->manualSessionControl == false) {
1398-
updateSession();
1399-
} else if (configuration->manualSessionControl == true) {
1400-
packEvents();
1458+
} catch (...) {
1459+
bool acquired = mutex->try_lock();
1460+
running = false;
1461+
log(LogLevel::FATAL, "[Countly][updateLoop] unknown non-std::exception caught, stopping update loop");
1462+
if (acquired) {
1463+
mutex->unlock();
14011464
}
1402-
requestModule->processQueue(mutex);
14031465
}
1404-
mutex->lock();
1405-
running = false;
1406-
mutex->unlock();
14071466
}
14081467

14091468
void Countly::enableRemoteConfig() {

tests/config.cpp

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ TEST_CASE("Validate setting configuration values") {
5252
CHECK(config.forcePost == false);
5353
CHECK(config.port == 443);
5454
CHECK(config.manualSessionControl == false);
55+
CHECK(config.immediateRequestOnStop == false);
5556
CHECK(config.sha256_function == nullptr);
5657
CHECK(config.http_client_function == nullptr);
5758
CHECK(config.metrics.empty());
@@ -78,6 +79,7 @@ TEST_CASE("Validate setting configuration values") {
7879
ct.SetPath(TEST_DATABASE_NAME);
7980
ct.setMaxRQProcessingBatchSize(10);
8081
ct.enableManualSessionControl();
82+
ct.enableImmediateRequestOnStop();
8183
ct.start("YOUR_APP_KEY", "https://try.count.ly", -1, false);
8284

8385
// Get configuration values using Countly getters
@@ -97,6 +99,7 @@ TEST_CASE("Validate setting configuration values") {
9799
CHECK(config.forcePost == true);
98100
CHECK(config.port == 443);
99101
CHECK(config.manualSessionControl == true);
102+
CHECK(config.immediateRequestOnStop == true);
100103
CHECK(config.sha256_function("custom SHA256") == customSha_1_returnValue);
101104

102105
HTTPResponse response = config.http_client_function(true, "", "");
@@ -182,6 +185,7 @@ TEST_CASE("Validate setting configuration values") {
182185
ct.setSalt("new-salt");
183186
ct.setMaxRequestQueueSize(100);
184187
ct.SetPath("new_database.db");
188+
ct.enableImmediateRequestOnStop();
185189

186190
// get SDK configuration again and make sure that they haven't changed
187191
config = ct.getConfiguration();
@@ -199,6 +203,7 @@ TEST_CASE("Validate setting configuration values") {
199203
CHECK(config.breadcrumbsThreshold == 100);
200204
CHECK(config.forcePost == true);
201205
CHECK(config.port == 443);
206+
CHECK(config.immediateRequestOnStop == false); // was never enabled before init, should stay false
202207
CHECK(config.sha256_function("custom SHA256") == customSha_1_returnValue);
203208

204209
response = config.http_client_function(true, "", "");

0 commit comments

Comments
 (0)