Skip to content

Move to the 2026 dependency baseline - #316

Merged
ChristopherDavenport merged 4 commits into
mainfrom
baseline-2026
Sep 26, 2026
Merged

ChristopherDavenport merged 4 commits into
mainfrom
baseline-2026

Conversation

@ChristopherDavenport

Copy link
Copy Markdown
Collaborator

Moves this repo to the org's 2026 dependency baseline.

Scala 2.13.18, 3.3.8 LTS — 2.12 dropped
Scala.js / Native 1.22.0 / 0.5.12
cats / cats-effect / fs2 2.13.0 / 3.7.1 / 3.14.0
http4s 0.23.37
log4cats 2.8.0
test stack munit 1.3.1, munit-cats-effect 2.2.1, scalacheck 1.20.0, discipline-munit 2.0.0

Why now. fs2 and http4s are the drivers: every repo in the org sat below the fs2 3.12.2 advisory floor and thirteen below the 3.2.11 one, with fourteen below http4s 0.23.17. The Typelevel stack moves together, so clearing those meant moving the rest.

Why a minor and not a patch. Dropping Scala 2.12 and moving Scala Native 0.4 → 0.5 are both breaking for consumers.

Validated locally across the org by cross-building and publishLocal-ing each dependency layer in turn. No source incompatibility with cats-effect 3.7, Scala 3.3.8 or Native 0.5 was found in any repo; where a source change appears it is the removal of epollcat, whose Native IOApp cats-effect 3.6 absorbed.

🤖 Generated with Claude Code

Scala 2.13.18 and 3.3.8 LTS, dropping 2.12. cats 2.13.0, cats-effect 3.7.1,
fs2 3.14.0, http4s 0.23.37, log4cats 2.8.0, and the munit/scalacheck/scodec
test stack brought current. Scala.js 1.22.0 and Scala Native 0.5.12.

fs2 and http4s are the reason this is not optional: every repo sat below the
fs2 3.12.2 floor, thirteen below the 3.2.11 one, and fourteen below http4s
0.23.17. The rest follows because the Typelevel stack moves together.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The baseline drops Scala 2.12 and moves Scala Native from 0.4 to 0.5, both of
which are breaking for consumers, so this is not a patch release.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@mergify

mergify Bot commented Sep 26, 2026 •

Copy link
Copy Markdown
Contributor

This pull request does not currently match the merge queue conditions, so it cannot be queued from here. The box comes back if it matches again.

Dropping Scala 2.12 changes the generated CI matrix, so the committed
workflow no longer matched the build and githubWorkflowCheck failed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@ChristopherDavenport

Copy link
Copy Markdown
Collaborator Author

Parked: CircuitBreakerTests 'Handles cancel correctly when half-open' times out on Scala Native / Scala 3 under CI (Linux x86-64, temurin@8), reproducibly across three runs. Passes on macOS arm64 and Linux arm64 with JDK 17, so it is unreproduced outside CI. Branch kept.

CircuitBreakerTests."Handles cancel correctly when half-open" timed out after
30s on Native in CI, reproducibly, while passing everywhere locally.

The hang is not in the cancel. tryReset rejected when

  open.expiresAt >= now

so at now == expiresAt the breaker stayed Open and protect raised
RejectedExecution. The test's effect therefore never ran, fiberStarted was
never completed, and fiberStarted.get blocked until munit's 30s timeout - a
timeout rather than an assertion failure, which is what the symptom was.

now == expiresAt is not a rare race, it is the floor of the distribution.
expiresAt is startedAt + resetTimeout in truncated milliseconds, and sleeping
resetTimeout guarantees only that at least that much elapsed, so

  now = floor(t0 + 200) >= floor(t0) + 200 = expiresAt

with equality whenever the timer is precise. A precise timer hits the boundary
every time, which is why CI failed 3/3 rather than intermittently. Measuring
the margin on macOS gave 1-74ms of overshoot, so it never lands there.

Semantically the strict comparison is also the correct one: at exactly
expiresAt the reset timeout has fully elapsed, so the attempt is due. The old
comparison held the breaker open for resetTimeout + 1ms.

The new test pins the boundary with TestControl rather than a real clock, so it
fails on the old comparison on every platform instead of only where the timer
happens to be precise. Verified: it fails with the original >= and passes with
>. cats-effect-testkit is added test-only for TestControl.

Local runs were never going to catch this. macOS overshoots, Docker on macOS
inherits that, and emulated x86-64 is slower still - all three pass on the
unfixed code, including the arch that was assumed to be the trigger.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@ChristopherDavenport

Copy link
Copy Markdown
Collaborator Author

Reopening with the Native timeout fixed.

What was actually failing

CircuitBreakerTests."Handles cancel correctly when half-open" timed out after 30s on Native in CI, 3/3, while passing everywhere locally. The hang was not in the cancel.

tryReset rejected when open.expiresAt >= now, so at now == expiresAt the breaker stayed Open and protect raised RejectedExecution. The test's effect therefore never ran, fiberStarted was never completed, and fiberStarted.get blocked until munit's 30s timeout — a timeout rather than an assertion failure, which matches the symptom.

Why it was deterministic, not flaky

now == expiresAt is the floor of the distribution, not a rare race. expiresAt is startedAt + resetTimeout in truncated milliseconds, and sleeping resetTimeout guarantees only that at least that much elapsed:

now = floor(t0 + 200) >= floor(t0) + 200 = expiresAt

with equality whenever the timer is precise. A precise timer hits the boundary every time — hence 3/3. Measuring the margin on macOS gave 1–74 ms of overshoot, so it never lands there.

The strict comparison is also the semantically correct one: at exactly expiresAt the reset timeout has fully elapsed, so the attempt is due. The old comparison held the breaker open for resetTimeout + 1ms.

On the reproduction attempts

The earlier notes had two things off. The failing job was Scala 2.13 Native, not Scala 3 — the Scala 3 job shows cancelled, killed by fail-fast after 2.13 failed. And x86-64 was not the trigger: I ran it under emulation and it passes on the unfixed code.

macOS overshoots, Docker-on-macOS inherits that, and QEMU is slower still, so all three pass unfixed. The discriminator is timer precision, not architecture or Scala version.

The test

Rather than chase the environment, the new test pins the boundary with TestControl, so it fails on the old comparison on every platform instead of only where the timer happens to be precise. Verified both ways: fails with >=, passes with >. cats-effect-testkit is added test-only.

Green locally on 2.13 and 3 across JVM, JS and Native — 16 tests each.

🤖 Generated with Claude Code

@ChristopherDavenport
ChristopherDavenport merged commit cce1c3f into main Sep 26, 2026
20 of 28 checks passed
@ChristopherDavenport
ChristopherDavenport deleted the baseline-2026 branch September 26, 2026 17:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant