From a82905145d2cab0f5157f20ffc28c80f29763e24 Mon Sep 17 00:00:00 2001 From: Lucas Carlson Date: Sun, 9 Aug 2026 21:34:56 -0700 Subject: [PATCH 1/3] ci: verify the supported Rails and Ruby span The gemspec advertises Rails 8.0 or newer on Ruby 3.3 or newer, but CI ran exactly one combination, so the range was a claim rather than a tested guarantee. Add a matrix over Ruby 3.3 and 3.4 against Rails 8.0 and 8.1, pinned through RAILS_VERSION in the Gemfile, and make the release job depend on it. Verified locally that the suite passes against Rails 8.0, which had never been exercised. --- .github/workflows/ci.yml | 19 +++++++++++++++++++ CHANGELOG.md | 7 +++++++ Gemfile | 10 ++++++++++ docs/roadmap.md | 3 +++ 4 files changed, 39 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 228b896..4817d70 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -26,6 +26,24 @@ jobs: - run: npm ci - run: npm test + compatibility: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + ruby-version: [ "3.3", "3.4" ] + rails-version: [ "8.0", "8.1" ] + env: + RAILS_VERSION: ${{ matrix.rails-version }} + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + - uses: ruby/setup-ruby@95ef2b042f9d7a56d8268cba8559e2842e2ad01b # v1 + with: + ruby-version: ${{ matrix.ruby-version }} + bundler-cache: false + - run: bundle lock --update && bundle install --jobs 4 + - run: bundle exec rake test + postgresql: runs-on: ubuntu-latest services: @@ -97,6 +115,7 @@ jobs: - mysql - static - javascript + - compatibility runs-on: ubuntu-latest permissions: contents: write diff --git a/CHANGELOG.md b/CHANGELOG.md index d882d8d..09bce6c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,13 @@ ## Unreleased +- Run compatibility CI across the span the gemspec advertises: Ruby 3.3 and 3.4 + against Rails 8.0 and 8.1. The suite previously ran on one combination, so + `>= 8.0` was a claim rather than a tested guarantee. Set `RAILS_VERSION` to + pin a Rails line locally. + +## Unreleased + - Add `SolidObjects::WakeUpAdapters::Postgresql`, an optional cross-process wake-up using PostgreSQL notifications. In-process signalling cannot reach a worker process, so reactive delivery waited out `polling_interval`. With the diff --git a/Gemfile b/Gemfile index 80bdaa2..a2b84cc 100644 --- a/Gemfile +++ b/Gemfile @@ -2,6 +2,16 @@ source "https://rubygems.org" gemspec +# Compatibility runs pin the Rails line so CI can verify the span the gemspec +# advertises rather than only the newest release that resolves. +rails_version = ENV["RAILS_VERSION"] +if rails_version + constraint = "~> #{rails_version}.0" + %w[actioncable actionpack actionview activerecord activesupport railties].each do |library| + gem library, constraint + end +end + group :development, :test do gem "mysql2", ">= 0.5", require: false gem "pg", ">= 1.5", require: false diff --git a/docs/roadmap.md b/docs/roadmap.md index 4e6bded..7b674f2 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -36,6 +36,9 @@ a listening connection per waiting thread and release on supervisor shutdown - Inline RBS generation/validation, Steep, Standard Ruby, Solid Queue's exact RuboCop policy, and a warning-free Brakeman scan +- Compatibility CI across the supported span: Ruby 3.3 and 3.4 against Rails 8.0 + and 8.1, pinned through `RAILS_VERSION` so the advertised range is verified + rather than assumed - A JavaScript suite covering the state payload and batched refresh browser modules, run in CI with Node's test runner and jsdom, with every GitHub Actions reference pinned to a commit SHA From 8720698c2375e699214f1087d4d6bea456d87f75 Mon Sep 17 00:00:00 2001 From: Lucas Carlson Date: Sun, 9 Aug 2026 21:39:14 -0700 Subject: [PATCH 2/3] fix: never wait a negative retry interval A synchronous lock retry computed its wait from the remaining deadline, which can go negative between the expiry check and the wait, raising ArgumentError instead of the SyncEnqueueTimeout the caller expected. Skip the wait when no interval remains. The new compatibility matrix surfaced this on its first run. --- CHANGELOG.md | 4 ++++ lib/solid_objects/database_adapters/sqlite.rb | 10 ++++++---- .../lib/solid_objects/database_adapters/sqlite.rbs | 2 ++ test/integration/synchronous_invocation_test.rb | 9 +++++++++ 4 files changed, 21 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 09bce6c..3e9a225 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ against Rails 8.0 and 8.1. The suite previously ran on one combination, so `>= 8.0` was a claim rather than a tested guarantee. Set `RAILS_VERSION` to pin a Rails line locally. +- Stop a synchronous lock retry from asking for a negative wait when its + deadline expires between the check and the wait, which raised + `ArgumentError: time interval must not be negative` instead of the timeout + the caller expected. Found by the new compatibility matrix. ## Unreleased diff --git a/lib/solid_objects/database_adapters/sqlite.rb b/lib/solid_objects/database_adapters/sqlite.rb index b885252..fb84927 100644 --- a/lib/solid_objects/database_adapters/sqlite.rb +++ b/lib/solid_objects/database_adapters/sqlite.rb @@ -160,13 +160,15 @@ def wait_before_busy_retry(attempts) end end + # The deadline can expire between the check above and this wait, which + # would otherwise ask for a negative interval. # @rbs () -> void def wait_before_retry + interval = [ LOCK_RETRY_INTERVAL, SyncDeadline.remaining ].min + return unless interval.positive? + LOCK_RETRY_MUTEX.synchronize do - LOCK_RETRY_CONDITION.wait( - LOCK_RETRY_MUTEX, - [ LOCK_RETRY_INTERVAL, SyncDeadline.remaining ].min - ) + LOCK_RETRY_CONDITION.wait(LOCK_RETRY_MUTEX, interval) end end end diff --git a/sig/generated/lib/solid_objects/database_adapters/sqlite.rbs b/sig/generated/lib/solid_objects/database_adapters/sqlite.rbs index ca182a3..61e3d49 100644 --- a/sig/generated/lib/solid_objects/database_adapters/sqlite.rbs +++ b/sig/generated/lib/solid_objects/database_adapters/sqlite.rbs @@ -52,6 +52,8 @@ module SolidObjects # @rbs (Integer) -> void def wait_before_busy_retry: (Integer) -> void + # The deadline can expire between the check above and this wait, which + # would otherwise ask for a negative interval. # @rbs () -> void def wait_before_retry: () -> void end diff --git a/test/integration/synchronous_invocation_test.rb b/test/integration/synchronous_invocation_test.rb index ec10994..59259ca 100644 --- a/test/integration/synchronous_invocation_test.rb +++ b/test/integration/synchronous_invocation_test.rb @@ -552,6 +552,15 @@ def wait(timeout:) "a synchronous call should read the clock once per transaction, not once per step" end + test "a retry wait whose deadline already expired does not raise" do + skip unless SolidObjects::Record.connection.adapter_name.match?(/sqlite/i) + adapter = SolidObjects.database_adapter + + SolidObjects::SyncDeadline.with(timeout: -1) do + assert_nothing_raised { adapter.send(:wait_before_retry) } + end + end + test "sync discovers the configured SQLite busy wait it has to restore" do skip unless SolidObjects::Record.connection.adapter_name.match?(/sqlite/i) From 17b5d4f54e7591baf29d15ad1862c40076db5942 Mon Sep 17 00:00:00 2001 From: Lucas Carlson Date: Sun, 9 Aug 2026 21:41:24 -0700 Subject: [PATCH 3/3] docs: merge the unreleased changelog sections --- CHANGELOG.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3e9a225..4e1a95a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,9 +10,6 @@ deadline expires between the check and the wait, which raised `ArgumentError: time interval must not be negative` instead of the timeout the caller expected. Found by the new compatibility matrix. - -## Unreleased - - Add `SolidObjects::WakeUpAdapters::Postgresql`, an optional cross-process wake-up using PostgreSQL notifications. In-process signalling cannot reach a worker process, so reactive delivery waited out `polling_interval`. With the