Stabilize timing-sensitive tests - #1513
Merged
bmehta001 merged 9 commits intoAug 3, 2026
Merged
Conversation
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
Contributor
There was a problem hiding this comment.
Pull request overview
This PR reduces flakiness in timing-sensitive kill-switch and offline-storage tests by removing real sleeps and making expiry behavior deterministic via an injectable clock and direct lease manipulation.
Changes:
- Introduce an injectable monotonic clock in
KillSwitchManagerand compute expiry times in milliseconds with overflow protection. - Replace the sleep-based SQLite reservation timeout test with a deterministic “force lease expiry” approach via SQL update.
- Remove the long, sleep-heavy functional test for temporary kill-switch behavior and replace it with deterministic unit tests.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| tests/unittests/OfflineStorageTests_SQLite.cpp | Makes the reservation-timeout test deterministic by simulating expired leases directly in SQLite. |
| tests/unittests/KillSwitchManagerTests.cpp | Adds unit tests that verify kill-switch expiry exactly at the deadline using an injected clock. |
| tests/functests/BasicFuncTests.cpp | Removes a timing/sleep-heavy functional test that was sensitive to CI scheduling and wall-clock delays. |
| lib/offline/KillSwitchManager.hpp | Adds injectable monotonic clock support and overflow-safe expiry computation for kill-switch deadlines. |
Comments suppressed due to low confidence (2)
lib/offline/KillSwitchManager.hpp:124
KillSwitchManagernow accepts an injectableClock, butisTokenBlocked()callsm_clock()while holdingm_lock. Since the injected clock can be arbitrary user code, this can deadlock (e.g., if the clock calls back into the manager) or unnecessarily extend the critical section. Evaluate the clock outside the mutex, then lock only for shared state access.
bool isTokenBlocked(const std::string& tokenId)
{
std::lock_guard<std::mutex> guard(m_lock);
const int64_t now = m_clock();
lib/offline/KillSwitchManager.hpp:115
addToken()callsexpiryFromNow()while holdingm_lock, which in turn calls the injectablem_clock(). To avoid executing arbitrary code under the mutex (deadlock / contention risk), compute the expiry before taking the lock and only lock around the map mutation.
void addToken(const std::string& tokenId, int64_t timeInSeconds)
{
std::lock_guard<std::mutex> guard(m_lock);
if (timeInSeconds > 0)
{
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
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
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
Contributor
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 4 out of 4 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (2)
tests/functests/BasicFuncTests.cpp:566
- waitForEvent holds mtx_requests while decoding/parsing every received request. That blocks the HTTP server callback from appending new requests, and it also re-decodes the same requests on every poll, which can make this bounded wait slower/flakier under load. Consider snapshotting only the new requests under the mutex, then decoding outside the lock.
bool waitForEvent(const std::string& name, unsigned timeoutMs)
{
const auto deadline = PAL::getMonotonicTimeMs() + timeoutMs;
while (PAL::getMonotonicTimeMs() < deadline)
{
tests/functests/BasicFuncTests.cpp:1297
- listener.waitForAtLeast(numHttpOK, 1, ...) is a prerequisite for the rest of the test (it ensures the kill-switch response was actually observed). Using EXPECT_TRUE allows the test to continue and fail later with less actionable errors; this should be an ASSERT_TRUE precondition.
EXPECT_TRUE(listener.waitForAtLeast(listener.numHttpOK, 1, 10000));
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
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
…ectly 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
…age 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
baijumeswani
approved these changes
Aug 3, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Retry-Afterdeadlines/WXshadowing failures, and harden injected clocks by falling back from an emptyClockand invoking it outside the kill-switch mutexNetworkFailurecallbacks deterministicallyMotivation
The affected tests depended on wall-clock time and fixed sleeps. Under slower or
uneven macOS CI scheduling, they could observe state too early or wait longer than
necessary. These changes preserve the production and integration coverage while
making expiry behavior deterministic and every asynchronous wait bounded.
They also remove external network timing from the bad-network teardown coverage and
harden the injected-clock path for empty-clock callers and Windows CI.