diff --git a/.github/workflows/android.yaml b/.github/workflows/android.yaml index 4d15372be..961a10951 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,20 @@ 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 + 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: | diff --git a/AGENTS.md b/AGENTS.md index 01cb8567b..9f69688e3 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 | |----------|----------| @@ -219,6 +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 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 2c44baaa4..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,33 +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; + 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); + } - // 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); - // 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; - 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); + 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); } @@ -416,4 +464,3 @@ TEST(TestConnectionAPI, Listen) srt_cleanup(); } -