From f9d020bd84cdcd76a1b28ea8ab704abc3a65eb9b Mon Sep 17 00:00:00 2001 From: Bhagirath Mehta Date: Mon, 3 Aug 2026 12:34:13 -0500 Subject: [PATCH 1/4] Preserve sub-millisecond event timestamp precision Use precise wall-clock time where available and retain nanosecond-derived 100 ns ticks on POSIX so record.time no longer truncates every event to milliseconds. Add regression coverage for POSIX timestamp precision.\n\nCo-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>\nCopilot-Session: 05d1030e-75b0-447f-9856-65091d59a97f --- lib/pal/PAL.cpp | 25 ++++++++++++++++++++----- tests/unittests/PalTests.cpp | 16 ++++++++++++++++ 2 files changed, 36 insertions(+), 5 deletions(-) diff --git a/lib/pal/PAL.cpp b/lib/pal/PAL.cpp index 3e667653f..a5291b0e3 100644 --- a/lib/pal/PAL.cpp +++ b/lib/pal/PAL.cpp @@ -430,7 +430,23 @@ namespace PAL_NS_BEGIN { { #ifdef _WIN32 FILETIME tocks; - ::GetSystemTimeAsFileTime(&tocks); + // Resolve the precise API dynamically so the SDK retains its Windows 7 + // runtime compatibility and falls back when the API is unavailable. + using GetSystemTimePreciseAsFileTimeProc = VOID (WINAPI*)(LPFILETIME); + HMODULE kernel32 = ::GetModuleHandleW(L"kernel32.dll"); + auto getSystemTimePreciseAsFileTime = + kernel32 + ? reinterpret_cast( + ::GetProcAddress(kernel32, "GetSystemTimePreciseAsFileTime")) + : nullptr; + if (getSystemTimePreciseAsFileTime) + { + getSystemTimePreciseAsFileTime(&tocks); + } + else + { + ::GetSystemTimeAsFileTime(&tocks); + } ULONGLONG ticks = (ULONGLONG(tocks.dwHighDateTime) << 32) | tocks.dwLowDateTime; // number of days from beginning to 1601 multiplied by ticks per day return ticks + 0x701ce1722770000ULL; @@ -440,10 +456,9 @@ namespace PAL_NS_BEGIN { // This UTC epoch contract has been signed in blood since C++20 std::chrono::time_point now = std::chrono::system_clock::now(); auto duration = now.time_since_epoch(); - auto millis = std::chrono::duration_cast(duration).count(); - uint64_t ticks = millis; - ticks *= 10000; // convert millis to ticks (1 tick = 100ns) - ticks += 0x89F7FF5F7B58000ULL; // UTC time 0 in .NET ticks + auto nanos = std::chrono::duration_cast(duration).count(); + int64_t ticks = nanos / 100; // convert nanoseconds to .NET ticks (1 tick = 100ns) + ticks += static_cast(0x89F7FF5F7B58000ULL); // UTC time 0 in .NET ticks return ticks; #endif } diff --git a/tests/unittests/PalTests.cpp b/tests/unittests/PalTests.cpp index ddf1f6dd2..c931ff376 100644 --- a/tests/unittests/PalTests.cpp +++ b/tests/unittests/PalTests.cpp @@ -122,6 +122,22 @@ TEST_F(PalTests, SystemTime) EXPECT_THAT(t1, Lt(t0 + 1000)); } +#if !defined(_WIN32) && !defined(_WIN64) +TEST_F(PalTests, SystemTimeInTicksPreservesSubMillisecondPrecision) +{ + constexpr int64_t TicksPerMillisecond = 10000; + bool observedSubMillisecondTick = false; + + for (int i = 0; i < 1000 && !observedSubMillisecondTick; ++i) + { + observedSubMillisecondTick = + PAL::getUtcSystemTimeinTicks() % TicksPerMillisecond != 0; + } + + EXPECT_TRUE(observedSubMillisecondTick); +} +#endif + TEST_F(PalTests, FormatUtcTimestampMsAsISO8601) { EXPECT_THAT(PAL::formatUtcTimestampMsAsISO8601(0ll), Eq("1970-01-01T00:00:00.000Z")); From 6a2e9ffe4417ea1c248f7ecef32e8f56c29cd1cc Mon Sep 17 00:00:00 2001 From: bmehta001 Date: Mon, 3 Aug 2026 14:05:40 -0500 Subject: [PATCH 2/4] Stabilize timing-sensitive tests (#1513) * Stabilize timing-sensitive tests Use a monotonic injectable clock for kill-switch deadlines, replace the sleep-heavy expiration functional test with deterministic unit coverage, and simulate expired SQLite leases directly. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 77649dd9-3b60-4ca4-861d-8c5ac165d53c * Restore temporary kill-switch integration coverage Rewrite killIsTemporary to observe active drops and eventual server delivery instead of sleeping for a fixed expiration window. Keep every wait bounded without adding test-only access to production internals. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 77649dd9-3b60-4ca4-861d-8c5ac165d53c * Fix Windows CI and harden injected clocks Rename the temporary kill-switch logger so MSVC /WX no longer promotes C4458 into C2220 in both Windows pipelines. Files changed: - lib/offline/KillSwitchManager.hpp: fall back from an empty Clock and invoke injected callbacks outside the mutex. - tests/unittests/KillSwitchManagerTests.cpp: cover the empty-clock fallback. - tests/functests/BasicFuncTests.cpp: avoid shadowing the fixture logger member. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 7fe5faca-d77c-45c4-85d3-0d4a00d68a94 * Harden temporary kill-switch polling Decode only newly arrived requests after releasing the HTTP callback mutex, avoiding repeated parsing and preventing the polling helper from delaying incoming requests. Treat kill-switch activation as a fatal prerequisite while preserving teardown on failure. Files changed: - tests/functests/BasicFuncTests.cpp: snapshot new requests outside the decode path and fail fast when activation is not observed. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 7fe5faca-d77c-45c4-85d3-0d4a00d68a94 * Make bad-network teardown test deterministic Replace external endpoints with an injected HTTP client that holds requests until teardown cancellation, then reports NetworkFailure through the required exactly-once callback. This preserves the real cancellation and callback-drain path without simulator or network timing. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 77649dd9-3b60-4ca4-861d-8c5ac165d53c * tests: use SentCount() in WaitForRequest instead of m_sent.load() directly WaitForRequest polled m_sent.load() directly while SentCount() was already the named accessor for the same value. Using SentCount() keeps the implementation consistent with the class's own public API and means any future change to the accessor (e.g. different memory order) is automatically picked up. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 7fe5faca-d77c-45c4-85d3-0d4a00d68a94 * tests: clean up kill-switch test and reduce lease TTL in offline storage test BasicFuncTests/killIsTemporary: flatten acceptedAfterKillExpires polling loop. - Remove redundant pre-loop waitForEvent (nothing sent yet at that point, so it always returned false). - Remove redundant post-loop grace-period block; absorb the 100 ms into expiryDeadline so the single loop covers both the poll and the grace. OfflineStorageTests_SQLite/ReservedRecordsAreReleasedAfterTimeout: - Reduce lease TTL from 60000 ms to 5000 ms. The value is the storage reservation duration, not a wall-clock wait (the test fast-forwards expiry via SQL). 5 s is clearer to readers and equally correct. KillSwitchManager::expiryFromNow: add precondition comment documenting that seconds > 0 is required and why all callers must guard it. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 7fe5faca-d77c-45c4-85d3-0d4a00d68a94 --------- Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 77649dd9-3b60-4ca4-861d-8c5ac165d53c Copilot-Session: 7fe5faca-d77c-45c4-85d3-0d4a00d68a94 --- lib/offline/KillSwitchManager.hpp | 53 +++++-- tests/functests/APITest.cpp | 149 ++++++++++++++---- tests/functests/BasicFuncTests.cpp | 147 ++++++++++------- tests/unittests/KillSwitchManagerTests.cpp | 47 ++++++ .../unittests/OfflineStorageTests_SQLite.cpp | 21 ++- 5 files changed, 309 insertions(+), 108 deletions(-) diff --git a/lib/offline/KillSwitchManager.hpp b/lib/offline/KillSwitchManager.hpp index a70569877..d5f5a1211 100644 --- a/lib/offline/KillSwitchManager.hpp +++ b/lib/offline/KillSwitchManager.hpp @@ -7,11 +7,14 @@ #include "pal/PAL.hpp" +#include +#include #include #include #include #include #include +#include #include #include @@ -21,13 +24,24 @@ namespace MAT_NS_BEGIN { class KillSwitchManager { public: + using Clock = std::function; bool isActive() { return !m_tokenTime.empty(); } - KillSwitchManager() : m_isRetryAfterActive(false), m_retryAfterExpiryTime(0) + KillSwitchManager() + : KillSwitchManager([]() { return static_cast(PAL::getMonotonicTimeMs()); }) + { + } + + explicit KillSwitchManager(Clock clock) + : m_clock(clock + ? std::move(clock) + : Clock([]() { return static_cast(PAL::getMonotonicTimeMs()); })), + m_isRetryAfterActive(false), + m_retryAfterExpiryTime(0) { } @@ -45,8 +59,9 @@ namespace MAT_NS_BEGIN { int64_t timeinSecs = 0; if (tryParseSeconds(timeStr, timeinSecs) && timeinSecs > 0) { + const int64_t expiryTime = expiryFromNow(timeinSecs); std::lock_guard guard(m_lock); - m_retryAfterExpiryTime = PAL::getUtcSystemTime() + timeinSecs; + m_retryAfterExpiryTime = expiryTime; m_isRetryAfterActive = true; } } @@ -101,20 +116,22 @@ namespace MAT_NS_BEGIN { void addToken(const std::string& tokenId, int64_t timeInSeconds) { - std::lock_guard guard(m_lock); if (timeInSeconds > 0) { - m_tokenTime[tokenId] = PAL::getUtcSystemTime() + timeInSeconds; //convert milisec to sec + const int64_t expiryTime = expiryFromNow(timeInSeconds); + std::lock_guard guard(m_lock); + m_tokenTime[tokenId] = expiryTime; } } bool isTokenBlocked(const std::string& tokenId) { + const int64_t now = m_clock(); std::lock_guard guard(m_lock); if (m_isRetryAfterActive) { - if (m_retryAfterExpiryTime > PAL::getUtcSystemTime()) + if (m_retryAfterExpiryTime > now) { return true;//always return true for all tokens } @@ -129,7 +146,7 @@ namespace MAT_NS_BEGIN { {//found, check the time stamp int64_t timeStamp = m_tokenTime[tokenId]; - if (timeStamp > PAL::getUtcSystemTime()) //convert milisec to sec + if (timeStamp > now) { return true; } @@ -169,6 +186,24 @@ namespace MAT_NS_BEGIN { } private: + // Precondition: seconds > 0. All call sites enforce this (handleResponse + // and addToken both guard with `timeinSecs > 0` / `timeInSeconds > 0`). + // Passing a non-positive value is UB: a negative durationMs makes the + // overflow check `now > maxTime - durationMs` wrap (signed overflow), so + // the result is unpredictable — do not relax the call-site guards. + int64_t expiryFromNow(int64_t seconds) const + { + constexpr int64_t millisecondsPerSecond = 1000; + constexpr int64_t maxTime = std::numeric_limits::max(); + const int64_t now = m_clock(); + if (seconds > maxTime / millisecondsPerSecond) + { + return maxTime; + } + const int64_t durationMs = seconds * millisecondsPerSecond; + return now > maxTime - durationMs ? maxTime : now + durationMs; + } + // Parse a count of seconds from a response-header value (Retry-After / // kill-duration). Returns false when the value is malformed or out of // range instead of letting std::stoll throw: the worker thread that drives @@ -225,8 +260,8 @@ namespace MAT_NS_BEGIN { // Either way the std::exception catch below ignores the value rather // than crashing. const long long parsed = std::stoll(value.substr(begin, end - begin)); - // Clamp to a value that cannot overflow when later added to a current - // UTC timestamp (seconds) to compute an expiry time. No legitimate + // Clamp to a value that cannot overflow when later converted to + // milliseconds to compute an expiry time. No legitimate // Retry-After / kill-duration approaches this; an absurd value is // capped instead of wrapping the expiry into the past. const int64_t kMaxSeconds = 100LL * 365 * 24 * 60 * 60; // ~100 years @@ -272,6 +307,7 @@ namespace MAT_NS_BEGIN { return true; } + Clock m_clock; std::map m_tokenTime; std::mutex m_lock; bool m_isRetryAfterActive; @@ -280,4 +316,3 @@ namespace MAT_NS_BEGIN { } MAT_NS_END #endif - diff --git a/tests/functests/APITest.cpp b/tests/functests/APITest.cpp index 0347807f6..baea0112e 100644 --- a/tests/functests/APITest.cpp +++ b/tests/functests/APITest.cpp @@ -210,6 +210,98 @@ class TestDebugEventListener : public DebugEventListener { } }; +// Keep requests in flight until teardown cancels them, then simulate a connection +// reset while honoring IHttpClient's exactly-once callback contract. +class NetworkFailureHttpClient final : public IHttpClient +{ +public: + IHttpRequest* CreateRequest() override + { + return new SimpleHttpRequest("bad-network-" + std::to_string(m_nextRequestId.fetch_add(1))); + } + + void SendRequestAsync(IHttpRequest* request, IHttpResponseCallback* callback) override + { + std::lock_guard lock(m_mutex); + m_pending[request->GetId()] = callback; + m_sent.fetch_add(1); + } + + void CancelRequestAsync(const std::string& id) override + { + IHttpResponseCallback* callback = nullptr; + { + std::lock_guard lock(m_mutex); + auto it = m_pending.find(id); + if (it != m_pending.end()) + { + callback = it->second; + m_pending.erase(it); + } + } + if (callback != nullptr) + { + m_cancelled.fetch_add(1); + CompleteWithNetworkFailure(id, callback); + } + } + + void CancelAllRequests() override + { + std::map pending; + { + std::lock_guard lock(m_mutex); + pending.swap(m_pending); + } + m_cancelled.fetch_add(static_cast(pending.size())); + for (const auto& request : pending) + { + CompleteWithNetworkFailure(request.first, request.second); + } + } + + bool WaitForRequest(unsigned timeoutMs) const + { + const auto deadline = PAL::getMonotonicTimeMs() + timeoutMs; + while (SentCount() == 0 && PAL::getMonotonicTimeMs() < deadline) + { + PAL::sleep(10); + } + return SentCount() > 0; + } + + unsigned SentCount() const + { + return m_sent.load(); + } + + unsigned CancelledCount() const + { + return m_cancelled.load(); + } + + unsigned CompletedCount() const + { + return m_completed.load(); + } + +private: + void CompleteWithNetworkFailure(const std::string& id, IHttpResponseCallback* callback) + { + auto response = new SimpleHttpResponse("failure-" + id); + response->m_result = HttpResult_NetworkFailure; + callback->OnHttpResponse(response); + m_completed.fetch_add(1); + } + + mutable std::mutex m_mutex; + std::map m_pending; + std::atomic m_nextRequestId{0}; + std::atomic m_sent{0}; + std::atomic m_cancelled{0}; + std::atomic m_completed{0}; +}; + /// /// Add all event listeners /// @@ -1204,41 +1296,43 @@ TEST(APITest, LogConfiguration_MsRoot_Check) TEST(APITest, LogManager_BadNetwork_Test) { auto& config = LogManager::GetLogConfiguration(); - - // Clean temp file first const char *cacheFilePath = "bad-network.db"; std::string fileName = MAT::GetTempDirectory(); fileName += cacheFilePath; - printf("remove %s\n", fileName.c_str()); std::remove(fileName.c_str()); std::remove((fileName + "-wal").c_str()); std::remove((fileName + "-shm").c_str()); std::remove((fileName + "-journal").c_str()); - for (auto url : { -#if 0 /* [MG}: Temporary change to avoid GitHub Actions crash #92 */ - "https://0.0.0.0/", - "https://127.0.0.1/", -#endif - "https://mobile.events-sandbox.data.microsoft.com/OneCollector/1.0/", - "https://invalid.host.name.microsoft.com/" - }) - { - printf("--- trying %s", url); - config[CFG_STR_CACHE_FILE_PATH] = cacheFilePath; - config[CFG_INT_TRACE_LEVEL_MASK] = 0; - config[CFG_INT_TRACE_LEVEL_MIN] = ACTTraceLevel_Warn; - config[CFG_INT_SDK_MODE] = SdkModeTypes::SdkModeTypes_CS; - config[CFG_INT_MAX_TEARDOWN_TIME] = 0; - config[CFG_STR_COLLECTOR_URL] = url; - size_t numIterations = 5; - while (numIterations--) - { - printf("."); - EXPECT_GE(StressSingleThreaded(config), MAX_ITERATIONS); - } - printf("\n"); - } + auto httpClient = std::make_shared(); + config.AddModule(CFG_MODULE_HTTP_CLIENT, httpClient); + config[CFG_STR_CACHE_FILE_PATH] = cacheFilePath; + config[CFG_INT_TRACE_LEVEL_MASK] = 0; + config[CFG_INT_TRACE_LEVEL_MIN] = ACTTraceLevel_Warn; + config[CFG_INT_SDK_MODE] = SdkModeTypes::SdkModeTypes_CS; + config[CFG_INT_MAX_TEARDOWN_TIME] = 0; + config[CFG_STR_COLLECTOR_URL] = "https://unused.invalid/"; + + TestDebugEventListener debugListener; + addAllListeners(debugListener); + LogManager::AddEventListener(DebugEventType::EVT_HTTP_FAILURE, debugListener); + auto logger = LogManager::Initialize(TEST_TOKEN, config); + LogManager::SetTransmitProfile(TransmitProfile_RealTime); + logger->LogEvent("badNetworkEvent"); + LogManager::UploadNow(); + + const bool requestStarted = httpClient->WaitForRequest(10000); + LogManager::FlushAndTeardown(); + LogManager::RemoveEventListener(DebugEventType::EVT_HTTP_FAILURE, debugListener); + removeAllListeners(debugListener); + config.AddModule(CFG_MODULE_HTTP_CLIENT, nullptr); + + EXPECT_TRUE(requestStarted); + EXPECT_GE(debugListener.numLogged.load(), 1u); + EXPECT_GE(debugListener.numHttpError.load(), 1u); + EXPECT_GE(httpClient->SentCount(), 1u); + EXPECT_EQ(httpClient->SentCount(), httpClient->CancelledCount()); + EXPECT_EQ(httpClient->CancelledCount(), httpClient->CompletedCount()); } TEST(APITest, LogManager_GetLoggerSameLoggerMultithreaded) @@ -1485,4 +1579,3 @@ TEST(APITest, Custom_Decorator) #endif // HAVE_MAT_DEFAULT_HTTP_CLIENT // TEST_PULL_ME_IN(APITest) - diff --git a/tests/functests/BasicFuncTests.cpp b/tests/functests/BasicFuncTests.cpp index 438411425..bc879d3e6 100644 --- a/tests/functests/BasicFuncTests.cpp +++ b/tests/functests/BasicFuncTests.cpp @@ -541,6 +541,35 @@ class BasicFuncTests : public ::testing::Test, } return result; } + + bool waitForEvent(const std::string& name, unsigned timeoutMs, size_t& nextRequestIndex) + { + const auto deadline = PAL::getMonotonicTimeMs() + timeoutMs; + while (PAL::getMonotonicTimeMs() < deadline) + { + std::vector newRequests; + { + LOCKGUARD(mtx_requests); + while (nextRequestIndex < receivedRequests.size()) + { + newRequests.push_back(receivedRequests[nextRequestIndex]); + ++nextRequestIndex; + } + } + for (const auto& request : newRequests) + { + for (const auto& record : decodeRequest(request, false)) + { + if (record.name == name) + { + return true; + } + } + } + PAL::sleep(10); + } + return false; + } }; @@ -1110,6 +1139,17 @@ public : break; }; } + + bool waitForAtLeast(const std::atomic& counter, unsigned expected, unsigned timeoutMs) + { + const auto deadline = PAL::getMonotonicTimeMs() + timeoutMs; + while (counter.load() < expected && PAL::getMonotonicTimeMs() < deadline) + { + PAL::sleep(10); + } + return counter.load() >= expected; + } + void printStats(){ std::cerr << "[ ] numLogged = " << numLogged << std::endl; std::cerr << "[ ] numSent = " << numSent << std::endl; @@ -1231,84 +1271,71 @@ TEST_F(BasicFuncTests, killSwitchWorks) TEST_F(BasicFuncTests, killIsTemporary) { CleanStorage(); - // Create the configuration to send to fake server auto configuration = LogManager::GetLogConfiguration(); configuration[CFG_INT_TRACE_LEVEL_MASK] = 0xFFFFFFFF; configuration[CFG_INT_TRACE_LEVEL_MIN] = ACTTraceLevel_Warn; configuration[CFG_INT_SDK_MODE] = SdkModeTypes::SdkModeTypes_CS; - configuration[CFG_INT_RAM_QUEUE_SIZE] = 4096 * 20; configuration[CFG_STR_CACHE_FILE_PATH] = TEST_STORAGE_FILENAME; - configuration[CFG_INT_MAX_TEARDOWN_TIME] = 2; // 2 seconds wait on shutdown + configuration[CFG_INT_MAX_TEARDOWN_TIME] = 2; configuration[CFG_STR_COLLECTOR_URL] = serverAddress.c_str(); - configuration[CFG_MAP_HTTP][CFG_BOOL_HTTP_COMPRESSION] = false; // disable compression for now - configuration[CFG_MAP_METASTATS_CONFIG]["interval"] = 30 * 60; // 30 mins - configuration[CFG_MAP_METASTATS_CONFIG]["enabled"] = true; // opt in to stats (disabled by default since #1420) - + configuration[CFG_MAP_HTTP][CFG_BOOL_HTTP_COMPRESSION] = false; + configuration[CFG_MAP_METASTATS_CONFIG]["interval"] = 30 * 60; + configuration[CFG_MAP_METASTATS_CONFIG]["enabled"] = true; configuration["name"] = __FILE__; configuration["version"] = "1.0.0"; - configuration["config"] = { { "host", __FILE__ } }; // Host instance + configuration["config"] = { { "host", __FILE__ } }; - // set the killed token on the server - server.setKilledToken(KILLED_TOKEN, 10); + constexpr unsigned killDurationSec = 5; + server.setKilledToken(KILLED_TOKEN, killDurationSec); KillSwitchListener listener; addListeners(listener); - // Log 100 events from valid and invalid 4 times - int repetitions = 4; - for (int i = 0; i < repetitions; i++) { - // Initialize the logger for the valid token and log 100 events - LogManager::Initialize(TEST_TOKEN, configuration); - LogManager::ResumeTransmission(); - auto myLogger = LogManager::GetLogger(TEST_TOKEN, "killed"); - int numIterations = 100; - while (numIterations--) { - EventProperties event1("fooEvent"); - event1.SetProperty("property", "value"); - myLogger->LogEvent(event1); - } - // Initialize the logger for the killed token and log 100 events - LogManager::Initialize(KILLED_TOKEN, configuration); - LogManager::ResumeTransmission(); - myLogger = LogManager::GetLogger(KILLED_TOKEN, "killed"); - numIterations = 100; - while (numIterations--) { - EventProperties event2("failEvent"); - event2.SetProperty("property", "value"); - myLogger->LogEvent(event2); - } - } - // Try and wait to upload - LogManager::UploadNow(); - PAL::sleep(2000); - // Sleep for 11 seconds so the killed time has expired, clear the killed tokens on server - PAL::sleep(11000); - server.clearKilledTokens(); - // Log 100 events with valid logger - LogManager::Initialize(TEST_TOKEN, configuration); - LogManager::ResumeTransmission(); - auto myLogger = LogManager::GetLogger(TEST_TOKEN, "killed"); - int numIterations = 100; - while (numIterations--) { - EventProperties event1("fooEvent"); - event1.SetProperty("property", "value"); - myLogger->LogEvent(event1); - } LogManager::Initialize(KILLED_TOKEN, configuration); + LogManager::SetTransmitProfile(TransmitProfile_RealTime); LogManager::ResumeTransmission(); - myLogger = LogManager::GetLogger(KILLED_TOKEN, "killed"); - numIterations = 100; - while (numIterations--) { - EventProperties event2("failEvent"); - event2.SetProperty("property", "value"); - myLogger->LogEvent(event2); + + auto killedLogger = LogManager::GetLogger(KILLED_TOKEN, "killed"); + killedLogger->LogEvent("activateKillSwitch"); + LogManager::UploadNow(); + + const bool killSwitchActivated = listener.waitForAtLeast(listener.numHttpOK, 1, 10000); + if (!killSwitchActivated) + { + LogManager::FlushAndTeardown(); + removeListeners(listener); + server.clearKilledTokens(); } - // Expect to 0 events to be dropped - EXPECT_EQ(uint32_t { 0 }, listener.numDropped); - LogManager::FlushAndTeardown(); + ASSERT_TRUE(killSwitchActivated) << "Kill-switch response was not observed before timeout"; + server.clearKilledTokens(); - listener.printStats(); + const unsigned droppedBeforeKill = listener.numDropped.load(); + const auto activeDeadline = PAL::getMonotonicTimeMs() + 2000; + unsigned probe = 0; + while (listener.numDropped.load() == droppedBeforeKill + && PAL::getMonotonicTimeMs() < activeDeadline) + { + killedLogger->LogEvent("blockedWhileKillIsActive" + std::to_string(probe++)); + PAL::sleep(20); + } + EXPECT_GT(listener.numDropped.load(), droppedBeforeKill); + + // Poll until the kill-switch TTL expires and the SDK resumes sending. + // Budget: kill duration + 5 s headroom; the extra 100 ms absorbs any + // request that was dispatched just before the deadline fires. + const auto expiryDeadline = PAL::getMonotonicTimeMs() + (killDurationSec + 5) * 1000 + 100; + size_t nextRequestIndex = 0; + bool acceptedAfterKillExpires = false; + while (!acceptedAfterKillExpires && PAL::getMonotonicTimeMs() < expiryDeadline) + { + killedLogger->LogEvent("acceptedAfterKillExpires"); + LogManager::UploadNow(); + acceptedAfterKillExpires = waitForEvent("acceptedAfterKillExpires", 100, nextRequestIndex); + } + EXPECT_TRUE(acceptedAfterKillExpires); + + LogManager::FlushAndTeardown(); removeListeners(listener); server.clearKilledTokens(); } diff --git a/tests/unittests/KillSwitchManagerTests.cpp b/tests/unittests/KillSwitchManagerTests.cpp index 15aaaee18..ceec1f450 100644 --- a/tests/unittests/KillSwitchManagerTests.cpp +++ b/tests/unittests/KillSwitchManagerTests.cpp @@ -16,6 +16,34 @@ TEST(KillSwitchManagerTests, handleResponse_ValidRetryAfter_ActivatesRetryAfter) ASSERT_TRUE(manager.isRetryAfterActive()); } +TEST(KillSwitchManagerTests, constructor_EmptyClockUsesMonotonicClock) +{ + KillSwitchManager manager(KillSwitchManager::Clock{}); + HttpHeaders headers; + headers.add("Retry-After", "120"); + + ASSERT_NO_THROW(manager.handleResponse(headers)); + EXPECT_TRUE(manager.isTokenBlocked("any-token")); +} + +TEST(KillSwitchManagerTests, handleResponse_RetryAfterExpiresAtDeadline) +{ + int64_t nowMs = 1000; + KillSwitchManager manager([&nowMs]() { return nowMs; }); + HttpHeaders headers; + headers.add("Retry-After", "120"); + + manager.handleResponse(headers); + ASSERT_TRUE(manager.isTokenBlocked("any-token")); + + nowMs += 119999; + EXPECT_TRUE(manager.isTokenBlocked("any-token")); + + nowMs += 1; + EXPECT_FALSE(manager.isTokenBlocked("any-token")); + EXPECT_FALSE(manager.isRetryAfterActive()); +} + TEST(KillSwitchManagerTests, handleResponse_NonNumericRetryAfter_DoesNotThrowAndIsIgnored) { KillSwitchManager manager; @@ -120,6 +148,25 @@ TEST(KillSwitchManagerTests, handleResponse_ValidKillTokenAndDuration_BlocksToke ASSERT_TRUE(manager.isTokenBlocked("tenant-token-1")); } +TEST(KillSwitchManagerTests, handleResponse_KillDurationExpiresAtDeadline) +{ + int64_t nowMs = 1000; + KillSwitchManager manager([&nowMs]() { return nowMs; }); + HttpHeaders headers; + headers.add("kill-tokens", "tenant-token-1"); + headers.add("kill-duration", "10"); + + ASSERT_TRUE(manager.handleResponse(headers)); + ASSERT_TRUE(manager.isTokenBlocked("tenant-token-1")); + + nowMs += 9999; + EXPECT_TRUE(manager.isTokenBlocked("tenant-token-1")); + + nowMs += 1; + EXPECT_FALSE(manager.isTokenBlocked("tenant-token-1")); + EXPECT_FALSE(manager.isActive()); +} + TEST(KillSwitchManagerTests, handleResponse_NonNumericKillDuration_DoesNotThrowAndDoesNotBlock) { KillSwitchManager manager; diff --git a/tests/unittests/OfflineStorageTests_SQLite.cpp b/tests/unittests/OfflineStorageTests_SQLite.cpp index d5aa6808a..015e197d7 100644 --- a/tests/unittests/OfflineStorageTests_SQLite.cpp +++ b/tests/unittests/OfflineStorageTests_SQLite.cpp @@ -312,32 +312,31 @@ TEST_F(OfflineStorageTests_SQLite, ReservedRecordsAreReleasedAfterTimeout) ASSERT_THAT(offlineStorage->StoreRecord({"guid1", "token", EventLatency_Normal, EventPersistence_Normal, 1, {}}), true); ASSERT_THAT(offlineStorage->StoreRecord({"guid2", "token", EventLatency_Normal, EventPersistence_Normal, 1, {}}), true); TestRecordConsumer consumer; - // Reserve first for 2 secs - EXPECT_THAT(offlineStorage->GetAndReserveRecords(consumer, 2000, EventLatency_Unspecified, 1), true); + EXPECT_THAT(offlineStorage->GetAndReserveRecords(consumer, 5000, EventLatency_Unspecified, 1), true); ASSERT_THAT(consumer.records.size(), 1); consumer.records.clear(); - PAL::sleep(500); - - // Reserve second for 1 sec, first still unavailable - EXPECT_THAT(offlineStorage->GetAndReserveRecords(consumer, 1000, EventLatency_Unspecified, 1), true); + // The first record remains reserved, so the second call returns the other record. + EXPECT_THAT(offlineStorage->GetAndReserveRecords(consumer, 5000, EventLatency_Unspecified, 1), true); ASSERT_THAT(consumer.records.size(), 1); consumer.records.clear(); auto records = offlineStorage->GetRecords(true, EventLatency_Unspecified, 0); ASSERT_THAT(records.size(), 2); - int64_t waitUntilMs = 0; for (auto const& record : records) { - waitUntilMs = std::max(waitUntilMs, record.reservedUntil); + EXPECT_GT(record.reservedUntil, 1); } - while (PAL::getUtcSystemTimeMs() <= waitUntilMs + 250) + // Simulate lease expiry without depending on wall-clock sleeps or CI scheduling. + offlineStorage->Execute("UPDATE events SET reserved_until=1"); + records = offlineStorage->GetRecords(true, EventLatency_Unspecified, 0); + ASSERT_THAT(records.size(), 2); + for (auto const& record : records) { - PAL::sleep(50); + EXPECT_EQ(record.reservedUntil, 1); } - // Both records are timed out EXPECT_THAT(offlineStorage->GetAndReserveRecords(consumer, 1000), true); ASSERT_THAT(consumer.records.size(), 2); EXPECT_THAT(consumer.records[0].retryCount, 1); From babeb981262cb88bdf74b036c0da95489c477450 Mon Sep 17 00:00:00 2001 From: Bhagirath Mehta Date: Mon, 3 Aug 2026 15:31:27 -0500 Subject: [PATCH 3/4] Cache precise Windows clock lookup Resolve GetSystemTimePreciseAsFileTime once instead of repeating module and symbol lookups for every event timestamp.\n\nCo-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>\nCopilot-Session: 05d1030e-75b0-447f-9856-65091d59a97f --- lib/pal/PAL.cpp | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/lib/pal/PAL.cpp b/lib/pal/PAL.cpp index a5291b0e3..0fc28abfb 100644 --- a/lib/pal/PAL.cpp +++ b/lib/pal/PAL.cpp @@ -433,12 +433,15 @@ namespace PAL_NS_BEGIN { // Resolve the precise API dynamically so the SDK retains its Windows 7 // runtime compatibility and falls back when the API is unavailable. using GetSystemTimePreciseAsFileTimeProc = VOID (WINAPI*)(LPFILETIME); - HMODULE kernel32 = ::GetModuleHandleW(L"kernel32.dll"); - auto getSystemTimePreciseAsFileTime = - kernel32 - ? reinterpret_cast( - ::GetProcAddress(kernel32, "GetSystemTimePreciseAsFileTime")) - : nullptr; + static const GetSystemTimePreciseAsFileTimeProc getSystemTimePreciseAsFileTime = + []() -> GetSystemTimePreciseAsFileTimeProc + { + HMODULE kernel32 = ::GetModuleHandleW(L"kernel32.dll"); + return kernel32 + ? reinterpret_cast( + ::GetProcAddress(kernel32, "GetSystemTimePreciseAsFileTime")) + : nullptr; + }(); if (getSystemTimePreciseAsFileTime) { getSystemTimePreciseAsFileTime(&tocks); From 3a3a83b4927b6e71839e82f5539e5528b0daf44d Mon Sep 17 00:00:00 2001 From: Microsoft Open Source Security Bot Date: Tue, 4 Aug 2026 13:02:39 -0700 Subject: [PATCH 4/4] Pin GitHub Actions to full-length commit SHAs (#1517) --- .github/dependabot.yml | 11 +++++++++++ .github/workflows/build-android.yml | 8 ++++---- .github/workflows/build-ios-mac.yml | 2 +- .github/workflows/build-posix-latest.yml | 4 ++-- .github/workflows/build-ubuntu-2204.yml | 2 +- .github/workflows/build-windows-vs2022.yaml | 2 +- .github/workflows/codeql-analysis.yml | 16 ++++++++-------- .github/workflows/deploy-docs-pages.yml | 10 +++++----- .github/workflows/spellcheck.yml | 2 +- .github/workflows/test-vcpkg.yml | 10 +++++----- .github/workflows/test-win-latest.yml | 6 +++--- 11 files changed, 42 insertions(+), 31 deletions(-) create mode 100644 .github/dependabot.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 000000000..2c48305b7 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,11 @@ +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + groups: + github-actions: + patterns: ["*"] + schedule: + interval: "weekly" + cooldown: + default-days: 7 diff --git a/.github/workflows/build-android.yml b/.github/workflows/build-android.yml index 1235e8dc7..1ce8a3aa9 100644 --- a/.github/workflows/build-android.yml +++ b/.github/workflows/build-android.yml @@ -35,7 +35,7 @@ jobs: name: Build for Android steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: submodules: false - name: Update submodules @@ -44,7 +44,7 @@ jobs: git config --global submodule.lib/modules.update none git -c protocol.version=2 submodule update --init --force --depth=1 - name: Setup Java - uses: actions/setup-java@v5 + uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0 with: distribution: 'adopt' java-version: '17' @@ -52,7 +52,7 @@ jobs: # Workaround for: 'Unable to decrypt local Maven settings credentials' run: rm $Env:USERPROFILE\.m2\settings.xml - name: Setup Android SDK - uses: android-actions/setup-android@v3 + uses: android-actions/setup-android@9fc6c4e9069bf8d3d10b2204b1fb8f6ef7065407 # v3.2.2 - name: Install NDK run: | java -version @@ -83,7 +83,7 @@ jobs: working-directory: lib\android_build - name: Upload Reports if: failure() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: reports path: lib\android_build\maesdk\build\reports diff --git a/.github/workflows/build-ios-mac.yml b/.github/workflows/build-ios-mac.yml index 7ca85012b..29b3dfc34 100644 --- a/.github/workflows/build-ios-mac.yml +++ b/.github/workflows/build-ios-mac.yml @@ -54,7 +54,7 @@ jobs: - name: Grant write permissions to /usr/local run: | sudo chown -R $USER:staff /usr/local - - uses: actions/checkout@v4 + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: submodules: 'true' continue-on-error: true diff --git a/.github/workflows/build-posix-latest.yml b/.github/workflows/build-posix-latest.yml index 8f9320e57..7a35c5a54 100644 --- a/.github/workflows/build-posix-latest.yml +++ b/.github/workflows/build-posix-latest.yml @@ -43,7 +43,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 continue-on-error: true - name: Test ${{ matrix.os }} ${{ matrix.config }} run: ./build-tests.sh ${{ matrix.config }} @@ -53,7 +53,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Install clang run: sudo apt-get update && sudo apt-get install -y clang - name: Compile each public header standalone under strict flags diff --git a/.github/workflows/build-ubuntu-2204.yml b/.github/workflows/build-ubuntu-2204.yml index 1fbcc6404..6c779c8b5 100644 --- a/.github/workflows/build-ubuntu-2204.yml +++ b/.github/workflows/build-ubuntu-2204.yml @@ -43,7 +43,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 continue-on-error: true - name: Test ${{ matrix.os }} ${{ matrix.config }} run: ./build-tests.sh ${{ matrix.config }} \ No newline at end of file diff --git a/.github/workflows/build-windows-vs2022.yaml b/.github/workflows/build-windows-vs2022.yaml index 222e32e67..e109575e8 100644 --- a/.github/workflows/build-windows-vs2022.yaml +++ b/.github/workflows/build-windows-vs2022.yaml @@ -28,7 +28,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Build env: diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index db7b4870a..a1f7a9c7f 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -39,12 +39,12 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 continue-on-error: true # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@v4 + uses: github/codeql-action/init@5595ccaf912efad79be6eef63a5619ff05969be3 # v4.37.6 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -75,7 +75,7 @@ jobs: # make release - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v4 + uses: github/codeql-action/analyze@5595ccaf912efad79be6eef63a5619ff05969be3 # v4.37.6 analyze-java: name: Analyze Java @@ -90,7 +90,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 continue-on-error: true - name: Update submodules @@ -100,19 +100,19 @@ jobs: git -c protocol.version=2 submodule update --init --force --depth=1 - name: Initialize CodeQL - uses: github/codeql-action/init@v4 + uses: github/codeql-action/init@5595ccaf912efad79be6eef63a5619ff05969be3 # v4.37.6 with: languages: java - name: Setup Java - uses: actions/setup-java@v5 + uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0 with: distribution: 'adopt' java-version: '17' - name: Remove default github maven configuration run: rm $Env:USERPROFILE\.m2\settings.xml - name: Setup Android SDK - uses: android-actions/setup-android@v3 + uses: android-actions/setup-android@9fc6c4e9069bf8d3d10b2204b1fb8f6ef7065407 # v3.2.2 - name: Install NDK run: | java -version @@ -139,4 +139,4 @@ jobs: working-directory: lib\android_build - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v4 + uses: github/codeql-action/analyze@5595ccaf912efad79be6eef63a5619ff05969be3 # v4.37.6 diff --git a/.github/workflows/deploy-docs-pages.yml b/.github/workflows/deploy-docs-pages.yml index 09ecd2d35..a3f13366f 100644 --- a/.github/workflows/deploy-docs-pages.yml +++ b/.github/workflows/deploy-docs-pages.yml @@ -33,10 +33,10 @@ jobs: steps: - name: Check out repository - uses: actions/checkout@v4 + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python - uses: actions/setup-python@v5 + uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.12" @@ -55,7 +55,7 @@ jobs: - name: Upload Pages artifact if: github.event_name != 'pull_request' - uses: actions/upload-pages-artifact@v3 + uses: actions/upload-pages-artifact@56afc609e74202658d3ffba0e8f6dda462b719fa # v3.0.1 with: path: docs/public/_build/html @@ -71,8 +71,8 @@ jobs: steps: - name: Configure GitHub Pages - uses: actions/configure-pages@v5 + uses: actions/configure-pages@983d7736d9b0ae728b81ab479565c72886d7745b # v5.0.0 - name: Deploy to GitHub Pages id: deployment - uses: actions/deploy-pages@v4 + uses: actions/deploy-pages@d6db90164ac5ed86f2b6aed7e0febac5b3c0c03e # v4.0.5 diff --git a/.github/workflows/spellcheck.yml b/.github/workflows/spellcheck.yml index eeedb9c62..261ff567f 100644 --- a/.github/workflows/spellcheck.yml +++ b/.github/workflows/spellcheck.yml @@ -24,7 +24,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 continue-on-error: true - name: install misspell diff --git a/.github/workflows/test-vcpkg.yml b/.github/workflows/test-vcpkg.yml index bdf37bd2e..59961ce53 100644 --- a/.github/workflows/test-vcpkg.yml +++ b/.github/workflows/test-vcpkg.yml @@ -26,7 +26,7 @@ jobs: runs-on: windows-latest name: Windows (x64-windows-static) steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Bootstrap vcpkg run: | @@ -42,7 +42,7 @@ jobs: runs-on: ubuntu-latest name: Linux (x64-linux) steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Bootstrap vcpkg run: | @@ -60,7 +60,7 @@ jobs: runs-on: macos-latest name: macOS (native) steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Bootstrap vcpkg run: | @@ -78,7 +78,7 @@ jobs: runs-on: macos-latest name: iOS (arm64-ios cross-compile) steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Bootstrap vcpkg run: | @@ -96,7 +96,7 @@ jobs: runs-on: ubuntu-latest name: Android (arm64-v8a API 23 cross-compile) steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Bootstrap vcpkg run: | diff --git a/.github/workflows/test-win-latest.yml b/.github/workflows/test-win-latest.yml index 4928fc71f..2a77d5e2a 100644 --- a/.github/workflows/test-win-latest.yml +++ b/.github/workflows/test-win-latest.yml @@ -43,11 +43,11 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 continue-on-error: true - name: setup-msbuild - uses: microsoft/setup-msbuild@v2 + uses: microsoft/setup-msbuild@6fb02220983dee41ce7ae257b6f4d8f9bf5ed4ce # v2.0.0 with: vs-version: '[17,)' @@ -60,7 +60,7 @@ jobs: runs-on: windows-2022 steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Compile each public header standalone under /W4 /WX shell: cmd run: tests\headers\check_public_headers.cmd