From da730a18f890d5314a73d4374765aa4d5a047de7 Mon Sep 17 00:00:00 2001 From: Andres Cera Date: Mon, 13 Jul 2026 21:07:20 -0500 Subject: [PATCH 1/4] fix(ci): stabilize Android compiler cache identity --- .github/workflows/android.yaml | 6 ++++-- AGENTS.md | 5 +++++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/.github/workflows/android.yaml b/.github/workflows/android.yaml index 4d15372be..b7781fef1 100644 --- a/.github/workflows/android.yaml +++ b/.github/workflows/android.yaml @@ -13,6 +13,7 @@ jobs: env: CMAKE_C_COMPILER_LAUNCHER: ccache CMAKE_CXX_COMPILER_LAUNCHER: ccache + CCACHE_COMPILERCHECK: content steps: - name: Setup Android NDK R23 @@ -25,7 +26,7 @@ jobs: - name: Configure ccache environment id: ccache env: - CACHE_KEY: srt-ccache-android-r23-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('CMakeLists.txt', 'cmake_object_lib_support.c', 'apps/**', 'common/**', 'configure', 'configure-data.tcl', 'haicrypt/**', 'scripts/**', 'srtcore/**') }} + CACHE_KEY: srt-ccache-android-r23-content-v1-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('CMakeLists.txt', 'cmake_object_lib_support.c', 'apps/**', 'common/**', 'configure', 'configure-data.tcl', 'haicrypt/**', 'scripts/**', 'srtcore/**') }} run: | echo "CCACHE_DIR=$RUNNER_TEMP/ccache" >> "$GITHUB_ENV" echo "key=$CACHE_KEY" >> "$GITHUB_OUTPUT" @@ -34,12 +35,13 @@ jobs: with: path: ${{ runner.temp }}/ccache key: ${{ steps.ccache.outputs.key }} - restore-keys: srt-ccache-android-r23-${{ runner.os }}-${{ runner.arch }}- + restore-keys: srt-ccache-android-r23-content-v1-${{ runner.os }}-${{ runner.arch }}- - name: Set up ccache run: | sudo apt-get update sudo apt-get install -y --no-install-recommends ccache ccache --max-size=200M + ccache --show-config ccache --zero-stats - name: build run: | diff --git a/AGENTS.md b/AGENTS.md index 01cb8567b..7dd4bed4e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -168,6 +168,11 @@ When a build can generate files beneath a hashed source glob, compute the key once from the clean post-checkout tree and reuse that immutable value for both restore and save. The Android workflow does this because `build-android` creates dependency and ABI output beneath its hashed `scripts/**` tree. +Android also uses content-based compiler identity because `setup-ndk` +materializes NDK r23 on each runner; ccache's default mtime identity would turn +identical compiler bytes with fresh mtimes into cross-run misses. Its versioned +cache namespace is part of that policy so an older mtime-keyed archive cannot +block the first content-keyed save. | Workflow | Coverage | |----------|----------| From 2cd0af889f86bada9ace522e1253f8c15561dc84 Mon Sep 17 00:00:00 2001 From: Andres Cera Date: Mon, 13 Jul 2026 21:09:57 -0500 Subject: [PATCH 2/4] ci: expose Android compiler cache identity --- .github/workflows/android.yaml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.github/workflows/android.yaml b/.github/workflows/android.yaml index b7781fef1..961a10951 100644 --- a/.github/workflows/android.yaml +++ b/.github/workflows/android.yaml @@ -37,11 +37,18 @@ jobs: key: ${{ steps.ccache.outputs.key }} restore-keys: srt-ccache-android-r23-content-v1-${{ runner.os }}-${{ runner.arch }}- - name: Set up ccache + env: + NDK_PATH: ${{ steps.setup-ndk.outputs.ndk-path }} run: | sudo apt-get update sudo apt-get install -y --no-install-recommends ccache ccache --max-size=200M ccache --show-config + compiler=$(readlink -f "$NDK_PATH/toolchains/llvm/prebuilt/linux-x86_64/bin/clang++") + printf 'NDK compiler path: %s\n' "$compiler" + sha256sum "$compiler" + stat --format='NDK compiler mtime: %y' "$compiler" + "$compiler" --version ccache --zero-stats - name: build run: | From 4fbda26baa52091270a1e826d878868a1a92f083 Mon Sep 17 00:00:00 2001 From: Andres Cera Date: Mon, 13 Jul 2026 21:38:41 -0500 Subject: [PATCH 3/4] test: enforce connection timeout through epoll deadline --- AGENTS.md | 3 +++ test/test_connection_timeout.cpp | 23 ++++++++++------------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 7dd4bed4e..2cd2eedae 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -224,6 +224,9 @@ pairs, register readiness before launching a fast peer, synchronize the worker, and close every owner explicitly; finite transfers consume their known byte count instead of using peer shutdown as an end marker. These lifecycle assertions are required gates and must not be relaxed or retried away. +Connection-timeout tests enforce their upper timing bound through the epoll API +deadline, not elapsed time after the waiting thread is rescheduled, and also +assert the resulting socket state and rejection reason. ## WHERE TO LOOK diff --git a/test/test_connection_timeout.cpp b/test/test_connection_timeout.cpp index 2c44baaa4..2c661f14f 100644 --- a/test/test_connection_timeout.cpp +++ b/test/test_connection_timeout.cpp @@ -129,27 +129,25 @@ TEST_F(TestConnectionTimeout, Nonblocking) { const chrono::steady_clock::time_point chrono_ts_start = chrono::steady_clock::now(); - // Here we check the connection timeout. - // Epoll timeout is set 100 ms greater than socket's TTL - EXPECT_EQ(srt_epoll_wait(pollid, read, &rlen, - write, &wlen, - connection_timeout_ms + 100, // +100 ms - 0, 0, 0, 0) - /* Expected return value is 2. We have only 1 socket, but - * sockets with exceptions are returned to both read and write sets. - */ - , 2); + const int epoll_timeout_ms = connection_timeout_ms + 80; + ASSERT_EQ(srt_epoll_wait(pollid, read, &rlen, write, &wlen, epoll_timeout_ms, 0, 0, 0, 0) + /* Expected return value is 2. We have only 1 socket, but + * sockets with exceptions are returned to both read and write sets. + */ + , + 2); // Check the actual timeout const chrono::steady_clock::time_point chrono_ts_end = chrono::steady_clock::now(); const auto delta_ms = chrono::duration_cast(chrono_ts_end - chrono_ts_start).count(); - // Confidence interval border : +/-80 ms - EXPECT_LE(delta_ms, connection_timeout_ms + 80) << "Timeout was: " << delta_ms; + // The API deadline excludes scheduler delay after the waiting thread is notified. EXPECT_GE(delta_ms, connection_timeout_ms - 80) << "Timeout was: " << delta_ms; EXPECT_EQ(rlen, 1); EXPECT_EQ(read[0], client_sock); EXPECT_EQ(wlen, 1); EXPECT_EQ(write[0], client_sock); + EXPECT_EQ(srt_getsockstate(client_sock), SRTS_BROKEN); + EXPECT_EQ(srt_getrejectreason(client_sock), SRT_REJ_TIMEOUT); } EXPECT_EQ(srt_epoll_remove_usock(pollid, client_sock), SRT_SUCCESS); @@ -416,4 +414,3 @@ TEST(TestConnectionAPI, Listen) srt_cleanup(); } - From 8aab52687911cc8e8b0c0e4f6372398d20eb2419 Mon Sep 17 00:00:00 2001 From: Andres Cera Date: Mon, 13 Jul 2026 21:38:41 -0500 Subject: [PATCH 4/4] test: observe connection timeout before deadline --- AGENTS.md | 7 +-- test/test_connection_timeout.cpp | 90 +++++++++++++++++++++++++------- 2 files changed, 74 insertions(+), 23 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 2cd2eedae..9f69688e3 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -224,9 +224,10 @@ pairs, register readiness before launching a fast peer, synchronize the worker, and close every owner explicitly; finite transfers consume their known byte count instead of using peer shutdown as an end marker. These lifecycle assertions are required gates and must not be relaxed or retried away. -Connection-timeout tests enforce their upper timing bound through the epoll API -deadline, not elapsed time after the waiting thread is rescheduled, and also -assert the resulting socket state and rejection reason. +Connection-timeout tests enforce their upper timing bound against the async +connect-failure callback timestamp, not elapsed time after the waiting thread is +rescheduled, and also assert the epoll result, callback error, socket state, and +rejection reason. ## WHERE TO LOOK diff --git a/test/test_connection_timeout.cpp b/test/test_connection_timeout.cpp index 2c661f14f..df9fa8205 100644 --- a/test/test_connection_timeout.cpp +++ b/test/test_connection_timeout.cpp @@ -1,4 +1,8 @@ #include +#include +#include +#include +#include #include #include #include "test_env.h" @@ -19,6 +23,43 @@ typedef int SOCKET; using namespace std; using namespace srt_logging; +struct ConnectResult +{ + int error_code; + chrono::steady_clock::time_point observed_at; +}; + +typedef shared_ptr > ConnectResultPromise; + +static mutex connect_result_mutex; +static map connect_result_promises; + +static void ObserveConnectResult(SRTSOCKET socket, const ConnectResultPromise& result_promise) +{ + lock_guard lock(connect_result_mutex); + connect_result_promises[socket] = result_promise; +} + +static void ForgetConnectResult(SRTSOCKET socket) +{ + lock_guard lock(connect_result_mutex); + connect_result_promises.erase(socket); +} + +static void RecordConnectResult(void*, SRTSOCKET socket, int error_code, const sockaddr*, int) +{ + ConnectResultPromise result_promise; + { + lock_guard lock(connect_result_mutex); + map::iterator result = connect_result_promises.find(socket); + if (result == connect_result_promises.end()) + return; + result_promise = result->second; + connect_result_promises.erase(result); + } + result_promise->set_value(ConnectResult{error_code, chrono::steady_clock::now()}); +} + class TestConnectionTimeout : public ::srt::Test { @@ -117,6 +158,11 @@ TEST_F(TestConnectionTimeout, Nonblocking) { ASSERT_NE(srt_epoll_add_usock(pollid, client_sock, &epoll_out), SRT_ERROR); const sockaddr* psa = reinterpret_cast(&m_sa); + ConnectResultPromise connect_result_promise(new promise); + future connect_result_future = connect_result_promise->get_future(); + ASSERT_EQ(srt_connect_callback(client_sock, &RecordConnectResult, NULL), SRT_SUCCESS); + ObserveConnectResult(client_sock, connect_result_promise); + const chrono::steady_clock::time_point connect_start = chrono::steady_clock::now(); ASSERT_NE(srt_connect(client_sock, psa, sizeof m_sa), SRT_ERROR); // Socket readiness for connection is checked by polling on WRITE allowed sockets. @@ -127,31 +173,35 @@ TEST_F(TestConnectionTimeout, Nonblocking) { int wlen = 2; SRTSOCKET write[2]; - const chrono::steady_clock::time_point chrono_ts_start = chrono::steady_clock::now(); - const int epoll_timeout_ms = connection_timeout_ms + 80; - ASSERT_EQ(srt_epoll_wait(pollid, read, &rlen, write, &wlen, epoll_timeout_ms, 0, 0, 0, 0) - /* Expected return value is 2. We have only 1 socket, but - * sockets with exceptions are returned to both read and write sets. - */ - , - 2); - // Check the actual timeout - const chrono::steady_clock::time_point chrono_ts_end = chrono::steady_clock::now(); - const auto delta_ms = chrono::duration_cast(chrono_ts_end - chrono_ts_start).count(); - // The API deadline excludes scheduler delay after the waiting thread is notified. - EXPECT_GE(delta_ms, connection_timeout_ms - 80) << "Timeout was: " << delta_ms; - - EXPECT_EQ(rlen, 1); - EXPECT_EQ(read[0], client_sock); - EXPECT_EQ(wlen, 1); - EXPECT_EQ(write[0], client_sock); - EXPECT_EQ(srt_getsockstate(client_sock), SRTS_BROKEN); - EXPECT_EQ(srt_getrejectreason(client_sock), SRT_REJ_TIMEOUT); + const int epoll_result = srt_epoll_wait(pollid, read, &rlen, write, &wlen, epoll_timeout_ms, 0, 0, 0, 0); + EXPECT_EQ(epoll_result, 2); + if (epoll_result == 2) + { + EXPECT_EQ(rlen, 1); + EXPECT_EQ(read[0], client_sock); + EXPECT_EQ(wlen, 1); + EXPECT_EQ(write[0], client_sock); + EXPECT_EQ(srt_getsockstate(client_sock), SRTS_BROKEN); + EXPECT_EQ(srt_getrejectreason(client_sock), SRT_REJ_TIMEOUT); + } + + const future_status callback_status = connect_result_future.wait_for(chrono::milliseconds(epoll_timeout_ms)); + EXPECT_EQ(callback_status, future_status::ready); + if (callback_status == future_status::ready) + { + const ConnectResult connect_result = connect_result_future.get(); + EXPECT_EQ(connect_result.error_code, SRT_ENOSERVER); + const int64_t delta_ms = + chrono::duration_cast(connect_result.observed_at - connect_start).count(); + EXPECT_LE(delta_ms, connection_timeout_ms + 80) << "Timeout callback was: " << delta_ms; + EXPECT_GE(delta_ms, connection_timeout_ms - 80) << "Timeout callback was: " << delta_ms; + } } EXPECT_EQ(srt_epoll_remove_usock(pollid, client_sock), SRT_SUCCESS); EXPECT_EQ(srt_close(client_sock), SRT_SUCCESS); + ForgetConnectResult(client_sock); (void)srt_epoll_release(pollid); }