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..4e1a95a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,14 @@ ## 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. +- 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. - 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 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)