Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,20 @@
## X.X.X
- ! Minor breaking change ! Added SDK internal limits enforcement (max key length, value size, segmentation values, breadcrumb count, stack-trace lines per thread, stack-trace line length) across events, views, crashes, and user properties. Limits use config defaults overridable by server SDK Behavior Settings, and can be set via `setMaxKeyLength`, `setMaxValueSize`, `setMaxSegmentationValues`, `setMaxBreadcrumbCount`, `setMaxStackTraceLinesPerThread`, `setMaxStackTraceLineLength` during init.

- Added multi-instance support: several `Countly` instances can now run in one process, each with its own app key, queues, storage, and threads. Instances can be owned by the integrator, or created and looked up by name with `createInstance`, `getInstance(name)`, `findInstance`, `hasInstance`, `destroyInstance` and `destroyAllInstances`.
- ! Minor breaking change ! When built with SQLite, each instance requires its own database path. A second instance claiming a path already in use logs an error and does not initialize.
- Fixed the libcurl global lifecycle: `curl_global_init` now runs once per process, and cleanup no longer runs when an instance is destroyed, which could tear down networking underneath another live instance. Added `shutdownNetworking()` for hosts that load and unload the SDK without exiting.
- Fixed non-unique event and view IDs: the random component of generated IDs was constant for the lifetime of the process, and on platforms with a coarse `system_clock` (Windows, ~15ms) the timestamp component did not change either, so IDs generated within one tick were identical.
- Fixed a remote config fetch outliving the objects it used: the fetch no longer holds any reference to the SDK instance, only to the modules and the value store it needs, each kept alive for as long as the fetch runs. Neither the calling thread nor destruction ever waits for it. Only one fetch runs at a time per instance: a call made while a fetch is in flight is logged and ignored rather than queued, so `updateRemoteConfig`, `updateRemoteConfigFor` and `updateRemoteConfigExcept` never block the calling thread. `shutdownNetworking()` now also refuses while a fetch is in flight.
- Calling the SDK from the log callback no longer hangs or recurses without bound. Log messages raised while a thread is inside the callback are dropped, `checkRQSize()` no longer takes the instance mutex (the storage modules serialize their own access), and the event queue size getters report -1 instead of deadlocking when called from the callback.
- Fixed dropped events and requests on SQLite builds: no busy timeout was set on any database connection, so a write that overlapped a read from another thread failed immediately with `SQLITE_BUSY` and the event or request was discarded with only an error log. Every connection now waits for the lock.
- Fixed `endSession` checking whether a session was active without holding the instance mutex, so two concurrent calls could both send an `end_session` request.
- Fixed the event queue flush when the queue is emptied by another thread mid-flush: it queued a request with an empty event list, and on SQLite builds it also ran a malformed `DELETE ... WHERE evtid IN )` statement that failed with a SQLite syntax error.
- Fixed `createEventTableSchema` and `debugReturnStateOfEQ` falling off the end of a non-void function when a `std::system_error` was caught; in the first case the indeterminate value decided whether the SDK considered itself initialized.
- Fixed a data race in the views module: the open-view map and the first-view flag were kept without a lock, so opening or closing views from more than one thread could lose view events, report `start` on more than one view, or corrupt the map. View recording still happens outside that lock, so a log callback that calls back into the SDK cannot deadlock.
- Fixed a data race in the `dow`, `hour` and `tz` fields of events and requests: they were derived with `std::localtime` and `std::gmtime`, which share one process-wide buffer, so concurrent recording (each instance runs its own update loop) could report another thread's or the other function's values. Both now use the reentrant variants.
- Fixed a lost wakeup when stopping the periodic SDK Behavior Settings timer: the stop flag was set without holding the mutex the timer thread waits on, so the notification could be missed and the joining thread could block for up to the full four-hour update interval.

## 26.1.1
- Updated CMake minimum required version to use the range format with upper the end of `3.31`.
- Hardened mutex handling against exceptions.
Expand Down
3 changes: 2 additions & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,8 @@ if(COUNTLY_BUILD_TESTS)
${CMAKE_CURRENT_SOURCE_DIR}/tests/mutex_exception_safety.cpp
${CMAKE_CURRENT_SOURCE_DIR}/tests/sbs.cpp
${CMAKE_CURRENT_SOURCE_DIR}/tests/internal_limits.cpp
${CMAKE_CURRENT_SOURCE_DIR}/tests/internal_limits_integration.cpp)
${CMAKE_CURRENT_SOURCE_DIR}/tests/internal_limits_integration.cpp
${CMAKE_CURRENT_SOURCE_DIR}/tests/multi_instance.cpp)

target_compile_options(countly-tests PRIVATE -g)
target_compile_definitions(countly-tests PRIVATE COUNTLY_BUILD_TESTS)
Expand Down
225 changes: 208 additions & 17 deletions examples/example_integration.cpp
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
#include "countly.hpp"
#include <atomic>
#include <chrono>
#include <iostream>
#include <random>
#include <thread>
#include <vector>

using namespace std;
using namespace cly;
Expand Down Expand Up @@ -139,37 +141,163 @@ void printLog(LogLevel level, const string &msg) {
// return response;
// }

int main() {
cout << "Sample App" << endl;
Countly &ct = Countly::getInstance();
// ---------------------------------------------------------------------------
// Multi instance support
//
// The SDK keeps a process wide registry of named instances:
// * the unnamed instance is the default one, returned by Countly::getInstance()
// * any other instance is created with Countly::createInstance(name) and later
// looked up with Countly::getInstance(name) or Countly::findInstance(name)
//
// Each instance is fully independent: its own app key, device id, session,
// event queue, request queue, background thread and modules. Two rules matter
// when you run more than one:
// 1. Give every instance its own app key.
// 2. On SQLite builds give every instance its own database file. start()
// refuses a path that another live instance has already claimed, and the
// claim is only released when that instance is destroyed (not on stop()).
// ---------------------------------------------------------------------------
static const string SERVER_URL = "https://your.server.ly";
static const int SERVER_PORT = 443;

static const string PRIMARY_APP_KEY = "YOUR_APP_KEY";
static const string PRIMARY_DEVICE_ID = "test-device-id";
static const string PRIMARY_DB_PATH = "databaseFileName.db";

// The name is just a registry key, it is never sent to the server.
static const string SECONDARY_INSTANCE_NAME = "secondary";
static const string SECONDARY_APP_KEY = "YOUR_SECOND_APP_KEY";
static const string SECONDARY_DEVICE_ID = "test-device-id-2";
static const string SECONDARY_DB_PATH = "databaseFileName2.db";

// Applies the same set of configurations to any instance and starts it.
// Note that every setter is called on the instance it belongs to: there is no
// "current" instance in the SDK, so a call on the default instance never
// configures a named one.
static void configureAndStart(Countly &instance, const string &appKey, const string &deviceId, const string &dbPath) {
// All configurations below are put here as an example
// Your configuration in your app may be different
// Please refer to the documentation for more information:
// https://support.count.ly/hc/en-us/articles/4416163384857-C-

// Custom HTTP client
// HTTPClientFunction clientPtr = customClient;
// ct.setHTTPClient(clientPtr);
// ct.alwaysUsePost(true);
ct.setLogger(printLog);
ct.SetPath("databaseFileName.db"); // this will be only built into account if the correct configurations are set
ct.setDeviceID("test-device-id");
// ct.setSalt("test-salt");
// instance.setHTTPClient(clientPtr);
// instance.alwaysUsePost(true);
instance.setLogger(printLog);
instance.SetPath(dbPath); // this will be only built into account if the correct configurations are set
instance.setDeviceID(deviceId);
// instance.setSalt("test-salt");
// OS, OS_version, device, resolution, carrier, app_version);
ct.SetMetrics("Windows 10", "10.22", "Mac", "800x600", "Carrier", "1.0");
instance.SetMetrics("Windows 10", "10.22", "Mac", "800x600", "Carrier", "1.0");

instance.setAutomaticSessionUpdateInterval(5); // The value is set so low just for internal validation. Has to be set before start.
instance.setMaxRQProcessingBatchSize(2); // in most cases not needed to be set. The value is set so low just for internal validation

// start the SDK (initialize the SDK)
string _appKey = "YOUR_APP_KEY";
string _serverUrl = "https://your.server.ly";
instance.start(appKey, SERVER_URL, SERVER_PORT, true);
}

// Creates and starts the secondary instance. createInstance returns the existing
// instance (and logs a warning) if the name is already taken, so this is safe to
// call twice.
static Countly &startSecondaryInstance() {
Countly &second = Countly::createInstance(SECONDARY_INSTANCE_NAME);
configureAndStart(second, SECONDARY_APP_KEY, SECONDARY_DEVICE_ID, SECONDARY_DB_PATH);
return second;
}

// Always resolve a named instance through findInstance instead of caching the
// reference: destroyInstance() frees the object, and any reference or pointer
// kept across that call dangles.
static Countly *secondaryInstance() {
Countly *second = Countly::findInstance(SECONDARY_INSTANCE_NAME);
if (second == nullptr) {
printLog(LogLevel::WARNING, "[ExampleIntegration] The secondary instance does not exist, create it first");
}
return second;
}

// Hammers both instances from several threads at once. Every call below is
// internally synchronised, and each instance has its own lock, so the two do not
// contend with each other either.
static void stressBothInstances(int threadsPerInstance, int iterations) {
Countly *second = secondaryInstance();
if (second == nullptr) {
return;
}

Countly *targets[2] = {&Countly::getInstance(), second};
const char *labels[2] = {"primary", "secondary"};

std::vector<std::thread> workers;
std::atomic<int> recorded(0);

for (int t = 0; t < 2; t++) {
Countly *target = targets[t];
const string label = labels[t];

for (int w = 0; w < threadsPerInstance; w++) {
workers.emplace_back([target, label, w, iterations, &recorded]() {
for (int i = 0; i < iterations; i++) {
target->RecordEvent("stress_basic_" + label, 1);

std::map<std::string, std::string> segmentation = {
{"instance", label},
{"worker", std::to_string(w)},
{"iteration", std::to_string(i)},
};
target->RecordEvent("stress_segmented_" + label, segmentation, 1, 2.5, 0.5);

target->crash().addBreadcrumb(label + "-" + std::to_string(w) + "-" + std::to_string(i));

const std::string viewId = target->views().openView("stress view " + label);
if (!viewId.empty()) {
target->views().closeViewWithID(viewId);
}

if (i % 5 == 0) {
target->updateSession();
}

recorded.fetch_add(2);
}
});
}
}

for (std::thread &worker : workers) {
worker.join();
}

printLog(LogLevel::INFO, "[ExampleIntegration] Stress finished, events recorded = " + std::to_string(recorded.load()) + ", primary EQ = " + std::to_string(Countly::getInstance().checkEQSize()) + ", secondary EQ = " + std::to_string(second->checkEQSize()));
}

static void printInstanceStatus() {
Countly *second = Countly::findInstance(SECONDARY_INSTANCE_NAME);
cout << "Default instance : initialized, RQ size = " << Countly::getInstance().checkRQSize() << ", EQ size = " << Countly::getInstance().checkEQSize() << endl;
cout << "Instance '" << SECONDARY_INSTANCE_NAME << "': " << (second == nullptr ? "not created" : "present") << endl;
if (second != nullptr) {
cout << " RQ size = " << second->checkRQSize() << ", EQ size = " << second->checkEQSize() << endl;
}
cout << "hasInstance(\"" << SECONDARY_INSTANCE_NAME << "\") = " << (Countly::hasInstance(SECONDARY_INSTANCE_NAME) ? "true" : "false") << endl;
// getInstance("") is the default instance, so this is always true.
cout << "getInstance(\"\") == getInstance() : " << (&Countly::getInstance("") == &Countly::getInstance() ? "true" : "false") << endl;
}

int main() {
cout << "Sample App" << endl;

if(_appKey.compare("YOUR_APP_KEY") == 0 || _serverUrl.compare("https://your.server.ly") == 0) {
if (PRIMARY_APP_KEY.compare("YOUR_APP_KEY") == 0 || SERVER_URL.compare("https://your.server.ly") == 0) {
printLog(LogLevel::WARNING, "[ExampleIntegration] Please do not use default set of app key and server url");
}

ct.start(_appKey, _serverUrl, 443, true);
// The default (unnamed) instance.
Countly &ct = Countly::getInstance();
configureAndStart(ct, PRIMARY_APP_KEY, PRIMARY_DEVICE_ID, PRIMARY_DB_PATH);

ct.setAutomaticSessionUpdateInterval(5);// The value is set so low just for internal validation.
ct.setMaxRQProcessingBatchSize(2); // in most cases not needed to be set. The value is set so low just for internal validation
// A second, independent instance reporting to a second app.
startSecondaryInstance();

ct.crash().addBreadcrumb("start");

Expand All @@ -189,6 +317,14 @@ int main() {
cout << "11) Record a view" << endl;
cout << "12) Leave breadcrumb" << endl;
cout << "13) Record a crash with bread crumbs and segmentation" << endl;
cout << "-- multi instance --" << endl;
cout << "14) Basic event on the secondary instance" << endl;
cout << "15) Same event on both instances" << endl;
cout << "16) Record a view on both instances" << endl;
cout << "17) Concurrency stress on both instances" << endl;
cout << "18) Instance registry status" << endl;
cout << "19) Destroy the secondary instance" << endl;
cout << "20) Create and start the secondary instance again" << endl;
cout << "0) Exit" << endl;
int a;
cin >> a;
Expand Down Expand Up @@ -222,7 +358,9 @@ int main() {
{"name", "Full name"}, {"username", "username123"}, {"email", "useremail@email.com"}, {"phone", "222-222-222"}, {"phone", "222-222-222"}, {"picture", "http://webresizer.com/images2/bird1_after.jpg"}, {"gender", "M"}, {"byear", "1991"}, {"organization", "Organization"},
};

ct.getInstance().setUserDetails(userdetail);
// Call the setter on the instance you mean. 'ct.getInstance()' would work
// here only because 'ct' happens to be the default instance.
ct.setUserDetails(userdetail);
} break;
case 8:
ct.setDeviceID("new-device-id", true);
Expand Down Expand Up @@ -272,6 +410,51 @@ int main() {

ct.crash().recordException("Divided by zero", "stack trace", true, crashMetrics, segmentation);
} break;
case 14: {
if (Countly *second = secondaryInstance()) {
second->RecordEvent("Event on the secondary instance", 1);
}
} break;
case 15: {
// The same key on both instances. Each event stays in the queue of the
// instance it was recorded on and is sent with that instance's app key.
std::map<std::string, std::string> segmentation = {{"source", "menu 15"}};
ct.RecordEvent("Event on both instances", segmentation, 1);
if (Countly *second = secondaryInstance()) {
second->RecordEvent("Event on both instances", segmentation, 1);
}
} break;
case 16: {
Countly *second = secondaryInstance();
// View ids are unique per view, so the two instances report different ids
// for the same view name.
const std::string primaryViewId = ct.views().openView("Shared view name");
const std::string secondaryViewId = second != nullptr ? second->views().openView("Shared view name") : "";
cout << "primary view id = " << primaryViewId << endl;
cout << "secondary view id = " << secondaryViewId << endl;

std::this_thread::sleep_for(2s);

ct.views().closeViewWithID(primaryViewId);
if (second != nullptr && !secondaryViewId.empty()) {
second->views().closeViewWithID(secondaryViewId);
}
} break;
case 17:
stressBothInstances(4, 25);
break;
case 18:
printInstanceStatus();
break;
case 19:
// Ends the instance's session, joins its threads and frees its database
// path claim. Any reference to it dangles afterwards.
Countly::destroyInstance(SECONDARY_INSTANCE_NAME);
printLog(LogLevel::INFO, "[ExampleIntegration] Secondary instance destroyed");
break;
case 20:
startSecondaryInstance();
break;
case 0:
flag = false;
break;
Expand All @@ -281,7 +464,15 @@ int main() {
}
}

// Stop every instance, then let the registry destroy them. stop() ends the
// session and joins the update thread; destroyAllInstances() also frees the
// database path claims. 'ct' must not be touched after that call, it refers to
// a destroyed object.
if (Countly *second = Countly::findInstance(SECONDARY_INSTANCE_NAME)) {
second->stop();
}
ct.stop();
Countly::destroyAllInstances();

return 0;
}
Loading
Loading