From cbb7947e156a32ac65f499e71d43a26f5a6a3d53 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 7 Jul 2026 20:44:44 +0000 Subject: [PATCH 1/6] CI: cache Bazel between runs (bazelisk, repository, disk caches) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every job cold-built until now — each leg re-fetched external deps and recompiled the runtime, googletest, and (benchmarks) BoringSSL + asio from scratch on every push. Add bazel-contrib/setup-bazel to each Bazel-running job with the bazelisk binary cache, the repository cache (external module downloads), and a per-config disk cache (action results), so branch builds restore from the caches main builds save. The codegen job already caches through setup-gradle; lint runs no Bazel. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_019ytv3VMrURFYP2mdWhk3un --- .github/workflows/ci.yml | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 83561a2e..a5b534dd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -30,6 +30,11 @@ jobs: runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 + - uses: bazel-contrib/setup-bazel@0.15.0 + with: + bazelisk-cache: true + repository-cache: true + disk-cache: ${{ matrix.name }} - name: bazel test env: CC: ${{ matrix.cc }} @@ -41,6 +46,11 @@ jobs: runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v4 + - uses: bazel-contrib/setup-bazel@0.15.0 + with: + bazelisk-cache: true + repository-cache: true + disk-cache: asan-ubsan - name: bazel test with sanitizers env: CC: clang @@ -52,6 +62,11 @@ jobs: runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v4 + - uses: bazel-contrib/setup-bazel@0.15.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 @@ -81,6 +96,11 @@ jobs: runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 + - uses: bazel-contrib/setup-bazel@0.15.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, @@ -119,6 +139,11 @@ jobs: continue-on-error: true steps: - uses: actions/checkout@v4 + - uses: bazel-contrib/setup-bazel@0.15.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 From b0465911ed924f0cce70e24d1c609340a2dfc1a0 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 7 Jul 2026 20:47:01 +0000 Subject: [PATCH 2/6] CI: setup-bazel 0.19.0 Match the current release (as used by muchq/MoonBase's branch builds). Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_019ytv3VMrURFYP2mdWhk3un --- .github/workflows/ci.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a5b534dd..33a05ded 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -30,7 +30,7 @@ jobs: runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 - - uses: bazel-contrib/setup-bazel@0.15.0 + - uses: bazel-contrib/setup-bazel@0.19.0 with: bazelisk-cache: true repository-cache: true @@ -46,7 +46,7 @@ jobs: runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v4 - - uses: bazel-contrib/setup-bazel@0.15.0 + - uses: bazel-contrib/setup-bazel@0.19.0 with: bazelisk-cache: true repository-cache: true @@ -62,7 +62,7 @@ jobs: runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v4 - - uses: bazel-contrib/setup-bazel@0.15.0 + - uses: bazel-contrib/setup-bazel@0.19.0 with: bazelisk-cache: true repository-cache: true @@ -96,7 +96,7 @@ jobs: runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 - - uses: bazel-contrib/setup-bazel@0.15.0 + - uses: bazel-contrib/setup-bazel@0.19.0 with: bazelisk-cache: true repository-cache: true @@ -139,7 +139,7 @@ jobs: continue-on-error: true steps: - uses: actions/checkout@v4 - - uses: bazel-contrib/setup-bazel@0.15.0 + - uses: bazel-contrib/setup-bazel@0.19.0 with: bazelisk-cache: true repository-cache: true From 995fd333a7911c782592a557029425b6f49bfb9f Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 7 Jul 2026 20:48:12 +0000 Subject: [PATCH 3/6] CI lint: tolerate third-party apt repo flakes The runner image ships packages.microsoft.com repos that intermittently fail signature checks and abort apt-get update; libboost-dev comes from the Ubuntu archive, so let update fail soft and let install be the arbiter. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_019ytv3VMrURFYP2mdWhk3un --- .github/workflows/ci.yml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 33a05ded..e7c9bfa4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -156,9 +156,13 @@ jobs: runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v4 - # Boost headers so clang-tidy can process the Beast transport. + # 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 From 2889108a33af687bef4703cf543d195b9fc1a41b Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 7 Jul 2026 20:48:47 +0000 Subject: [PATCH 4/6] CI: cancel superseded runs, checkout@v7 - concurrency: a new push to a PR cancels the in-flight run for the old head; main builds keep a per-run group so every merge still populates the bazel caches. - actions/checkout v4 -> v7, clearing the Node 20 deprecation warnings on every job. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_019ytv3VMrURFYP2mdWhk3un --- .github/workflows/ci.yml | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e7c9bfa4..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,7 @@ 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 @@ -45,7 +51,7 @@ 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 @@ -61,7 +67,7 @@ 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 @@ -95,7 +101,7 @@ 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 @@ -113,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 @@ -138,7 +144,7 @@ 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 @@ -155,7 +161,7 @@ jobs: name: lint runs-on: ubuntu-24.04 steps: - - uses: actions/checkout@v4 + - 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). From abe7dd5085516ae3b2232e04af9c86300ce79ba9 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 7 Jul 2026 20:53:45 +0000 Subject: [PATCH 5/6] Silence -Wformat-truncation in the timestamp differential test gcc sizes the snprintf against full int widths (73 bytes worst case); give the buffer headroom for that instead of the real bounded output. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_019ytv3VMrURFYP2mdWhk3un --- runtime/tests/core/timestamp_differential_test.cc | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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); From 569448bba976670d821704ab6e64d610c5e96342 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 7 Jul 2026 21:04:15 +0000 Subject: [PATCH 6/6] Socket transport: never let a peer close raise SIGPIPE The asan CI leg caught //runtime:beast_transport_test dying with "Broken pipe": in RejectsOversizedBodies the Beast server closes the connection while SocketHttpClient is still streaming the request body, and the client's next send() raised SIGPIPE, killing the process. The race only fires when the close beats the remaining writes (sanitizer slowdown made the server lose it), which is why the test usually passes. Suppress the signal at the transport layer so a peer disconnect always surfaces as a write error: MSG_NOSIGNAL on send (Linux), SO_NOSIGPIPE on the socket (macOS/BSD, which lack MSG_NOSIGNAL); Windows has no SIGPIPE. Regression test drives a 64 MiB+ body into SocketHttpServer, whose content-length rejection closes mid-send the same way. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_019ytv3VMrURFYP2mdWhk3un --- runtime/src/http/socket_transport.cc | 15 +++++++++++++- runtime/tests/http/socket_transport_test.cc | 22 +++++++++++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) 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/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());