diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 83561a2e..8ef788d2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,6 +5,12 @@ on: branches: [main] pull_request: +# A new push to the same ref supersedes the running build. main keeps every +# run (github.run_id) so merged pushes always populate the bazel caches. +concurrency: + group: ${{ github.ref == 'refs/heads/main' && github.run_id || github.ref }} + cancel-in-progress: true + permissions: contents: read @@ -29,7 +35,12 @@ jobs: os: windows-2022 runs-on: ${{ matrix.os }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 + - uses: bazel-contrib/setup-bazel@0.19.0 + with: + bazelisk-cache: true + repository-cache: true + disk-cache: ${{ matrix.name }} - name: bazel test env: CC: ${{ matrix.cc }} @@ -40,7 +51,12 @@ jobs: name: bazel (asan + ubsan) runs-on: ubuntu-24.04 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 + - uses: bazel-contrib/setup-bazel@0.19.0 + with: + bazelisk-cache: true + repository-cache: true + disk-cache: asan-ubsan - name: bazel test with sanitizers env: CC: clang @@ -51,7 +67,12 @@ jobs: name: fuzz (libFuzzer smoke) runs-on: ubuntu-24.04 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 + - uses: bazel-contrib/setup-bazel@0.19.0 + with: + bazelisk-cache: true + repository-cache: true + disk-cache: fuzz # Each harness runs a short bounded libFuzzer session (a "smoke" run, not # a soak) so regressions in the parsers surface on every PR. The # deterministic-driver variants also run as ordinary tests in the bazel @@ -80,7 +101,12 @@ jobs: os: [ubuntu-24.04, macos-14, windows-2022] runs-on: ${{ matrix.os }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 + - uses: bazel-contrib/setup-bazel@0.19.0 + with: + bazelisk-cache: true + repository-cache: true + disk-cache: consumer # The quick-start acceptance test (docs/quickstart.md): a standalone # out-of-tree Bazel module consumes smithy_cpp via the rules in # bazel/defs.bzl — generation runs inside the consumer's build graph, @@ -93,7 +119,7 @@ jobs: name: codegen (gradle) runs-on: ubuntu-24.04 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - uses: actions/setup-java@v4 with: distribution: temurin @@ -118,7 +144,12 @@ jobs: runs-on: ubuntu-24.04 continue-on-error: true steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 + - uses: bazel-contrib/setup-bazel@0.19.0 + with: + bazelisk-cache: true + repository-cache: true + disk-cache: benchmarks-opt - name: serde benchmarks run: bazel run -c opt //benchmarks:serde_benchmark -- --benchmark_min_time=0.2s - name: request benchmarks @@ -130,10 +161,14 @@ jobs: name: lint runs-on: ubuntu-24.04 steps: - - uses: actions/checkout@v4 - # Boost headers so clang-tidy can process the Beast transport. + - uses: actions/checkout@v7 + # Boost headers so clang-tidy can process the Beast transport. libboost-dev + # comes from the Ubuntu archive; tolerate update failures from the runner + # image's unrelated third-party repos (packages.microsoft.com flakes). - name: install boost headers - run: sudo apt-get update -q && sudo apt-get install -y -q libboost-dev + run: | + sudo apt-get update -q || true + sudo apt-get install -y -q libboost-dev # Generated code (examples/*/generated) is exempt from format checks; # its shape is locked by the golden diff check in the codegen job. - name: clang-format diff --git a/runtime/src/http/socket_transport.cc b/runtime/src/http/socket_transport.cc index 5e361dae..d4969fbc 100644 --- a/runtime/src/http/socket_transport.cc +++ b/runtime/src/http/socket_transport.cc @@ -56,12 +56,25 @@ void SetTimeouts(SocketFd fd, int timeout_ms) { setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &value, sizeof(value)); setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &value, sizeof(value)); #endif +#ifdef SO_NOSIGPIPE + // macOS/BSD have no MSG_NOSIGNAL; a peer that closes mid-send must surface + // as an EPIPE write error, never a process-killing SIGPIPE. + const int no_sigpipe = 1; + setsockopt(fd, SOL_SOCKET, SO_NOSIGPIPE, &no_sigpipe, sizeof(no_sigpipe)); +#endif } bool SendAll(SocketFd fd, std::string_view data) { +#ifdef MSG_NOSIGNAL + // Linux: writes to a peer-closed socket fail with EPIPE instead of raising + // SIGPIPE (whose default action would kill the process). + constexpr int kSendFlags = MSG_NOSIGNAL; +#else + constexpr int kSendFlags = 0; // Windows has no SIGPIPE; macOS uses SO_NOSIGPIPE. +#endif while (!data.empty()) { const int chunk = static_cast(data.size() > 65536 ? 65536 : data.size()); - const auto sent = send(fd, data.data(), chunk, 0); + const auto sent = send(fd, data.data(), chunk, kSendFlags); if (sent <= 0) return false; data.remove_prefix(static_cast(sent)); } diff --git a/runtime/tests/core/timestamp_differential_test.cc b/runtime/tests/core/timestamp_differential_test.cc index f982e71b..67927c94 100644 --- a/runtime/tests/core/timestamp_differential_test.cc +++ b/runtime/tests/core/timestamp_differential_test.cc @@ -38,7 +38,9 @@ TEST(TimestampDifferentialTest, DateTimeMatchesGmtimeOverRandomEpochs) { if (!GmTime(seconds, &reference)) { continue; // libc range limits are not what we're testing } - char expected[32]; + // Sized for gcc's -Wformat-truncation worst case (full int widths), not + // the real bounded output (the epoch range keeps years at four digits). + char expected[80]; std::snprintf(expected, sizeof(expected), "%04d-%02d-%02dT%02d:%02d:%02dZ", reference.tm_year + 1900, reference.tm_mon + 1, reference.tm_mday, reference.tm_hour, reference.tm_min, reference.tm_sec); diff --git a/runtime/tests/http/socket_transport_test.cc b/runtime/tests/http/socket_transport_test.cc index 03c1f398..19ca7b40 100644 --- a/runtime/tests/http/socket_transport_test.cc +++ b/runtime/tests/http/socket_transport_test.cc @@ -62,6 +62,28 @@ TEST(SocketTransportTest, HandlesSequentialRequestsAndLargeBodies) { server.Stop(); } +TEST(SocketTransportTest, PeerCloseMidSendIsAnErrorNotSigpipe) { + SocketHttpServer server; + ASSERT_TRUE(server.Start([](const HttpRequest&) { return HttpResponse{200, {}, "ok"}; }).ok()); + SocketHttpClient client("127.0.0.1", server.port()); + + // Over the server's 64 MiB body cap: it rejects on content-length and + // closes while the client is still writing. That must surface as a + // transport error (or an HTTP error status), never a SIGPIPE that kills + // the process. + HttpRequest request; + request.method = "POST"; + request.target = "/"; + request.body = std::string((std::size_t{64} << 20) + 1024, 'x'); + const auto response = client.Send(request); + if (response.ok()) { + EXPECT_GE(response->status, 400); + } else { + EXPECT_EQ(response.error().kind(), ErrorKind::kTransport); + } + server.Stop(); +} + TEST(SocketTransportTest, ReportsConnectionFailure) { SocketHttpServer throwaway; ASSERT_TRUE(throwaway.Start([](const HttpRequest&) { return HttpResponse{}; }).ok());