Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
90 commits
Select commit Hold shift + click to select a range
bb12268
Remove deprecated loading of statistics.rake from Rakefile
faraquet Oct 26, 2025
777eb5c
Testing against Rails 8.1
faraquet Oct 27, 2025
15d5d72
Merge pull request #668 from faraquet/aaa/8.1
rafaelfranca Oct 27, 2025
5b18203
Merge pull request #666 from faraquet/aaa/deprecated
rafaelfranca Oct 27, 2025
6eb29d9
Removing non-deterministic check from `processes_lifecycle_test.rb`
p-schlickmann Oct 23, 2025
1f45b80
Revert "Wrap Supervisor#start and stop with the app executor"
rosa Oct 28, 2025
6b36dcc
Bump solid_queue to 1.2.3
rosa Oct 28, 2025
5c6d4b0
Add wrap_in_app_executor in a few necessary places
flavorjones Oct 29, 2025
68d4ebb
Fix testing against Rails main
flavorjones Oct 29, 2025
1a2e380
print warning on startup if path to configuration file does not exist…
lxxxvi Oct 29, 2025
a6ac0c6
Ensure dispatcher is stopped after assertion
p-schlickmann Oct 28, 2025
4b6159c
Increase `retention-days` for failed CI logs
p-schlickmann Oct 28, 2025
ac912dd
Reducing flakiness of `test/integration/concurrency_controls_test.rb`
p-schlickmann Oct 28, 2025
4fcd81b
Fix error class name in README.md
iguchi1124 Oct 30, 2025
c09de47
Bump solid_queue to 1.2.4
rosa Oct 30, 2025
2a90213
Minimizing flakiness of `dispatcher_test.rb`
p-schlickmann Nov 3, 2025
692446d
docs: Improve consistency of README tone
conradbeach Nov 5, 2025
108270c
Merge pull request #680 from conradbeach/conrad/improve-readme-consis…
rafaelfranca Nov 5, 2025
c7e161c
Setting `timeout-minutes` to prevent deadlocked actions consuming GH …
p-schlickmann Nov 10, 2025
a0d00ee
Increasing timeout gaps in `processes_lifecycle_test`
p-schlickmann Nov 10, 2025
4db1f1f
Attempt to reduce flakiness of `recurring_tasks_test`
p-schlickmann Nov 17, 2025
b61bbdf
Removing comments && extra sleep
p-schlickmann Nov 17, 2025
839ba64
Fixed indentation for the adapter line in the README’s cable.yaml exa…
simaaaji Nov 25, 2025
78b8d94
Fix spacing
danielvdao Dec 2, 2025
d0bdafc
Update README.md with MariaDB `SKIP LOCKED` version information
chapmajs Nov 30, 2025
d00184a
Attempt to reduce flakiness of `scheduler_test`
p-schlickmann Nov 18, 2025
2494630
Add Ruby 4 and stop using `minitest/mock` for `stub`
rosa Dec 30, 2025
c211352
Use minitest 5.x as we still support Ruby 3.1
rosa Dec 30, 2025
a0b35ea
Use immediate transactions for Rails < 8.2
rosa Jan 1, 2026
410acf1
Adapt tests to Rails 8.2+ re-introduction of `enqueue_after_transacti…
rosa Jan 5, 2026
cfcbc3b
Skip Ruby 3.2 with `rails_main`
rosa Jan 5, 2026
9ac1dbb
Don't force SQLite immediate transaction for Rails >= 8
rosa Jan 6, 2026
d330516
Try to make concurrency-on-conflict-discard tests less flaky
rosa Jan 6, 2026
d95b8d4
Re-introduce async mode
joshleblanc Sep 28, 2025
5043870
Refactor async mode and restore tests for it
rosa Dec 30, 2025
6e785eb
DRY a bit releasing claimed executions by both supervisors
rosa Dec 30, 2025
e7c219f
Document `async` mode and allow configuring via environment variable
rosa Jan 5, 2026
cca44d2
Run non-standalone supervisor in a separate thread
rosa Jan 5, 2026
f7feb48
Ensure that non-standalone supervisor terminates gracefully
rosa Jan 5, 2026
839d627
Merge pull request #644 from joshleblanc/async-mode
rosa Jan 9, 2026
6c54dc0
Bump solid_queue to 1.3.0
rosa Jan 9, 2026
73b4dac
Fix NoMethodError when status.exitstatus is nil
rickychilcott Jan 10, 2026
695683f
Fix typo in error message
monorkin Jan 12, 2026
05b8c0b
Update README with wildcard queue explanation
bonniesimon Jan 13, 2026
567d217
Update README with wildcard queue behavior details
bonniesimon Jan 14, 2026
6a6f1c4
Remove extra formating
bonniesimon Jan 14, 2026
6a9928f
Improve flaky tests across the board
rosa Jan 14, 2026
9fec81b
Fix undefined variable when replacing a failed thread
rosa Jan 15, 2026
552f7d0
Bump solid_queue to 1.3.1
rosa Jan 15, 2026
8d90eab
Fix race condition between job enqueue and concurrency unblock
rosa Feb 13, 2026
97a3a54
Remove default value for 'mode' in CLI options
lovro-bikic Feb 3, 2026
5518a4d
Fix: Detect and warn when schedules generate multiple CRONs (#707)
himanshukale36 Feb 18, 2026
b6825d0
Clean up dead thread from process_instances when replacing it
rosa Feb 18, 2026
629ae55
Fix crash when recording a failed execution for a job that already ha…
rosa Feb 18, 2026
67a91df
Guard against nil pid in Puma plugin stop
rosa Feb 18, 2026
1017e0a
Handle nil process in heartbeat
rosa Feb 19, 2026
4031642
Skip concurrency controls for jobs whose class has been removed
rosa Feb 19, 2026
0b13202
Bump solid_queue to 1.3.2
rosa Feb 20, 2026
2c4bbd3
Add index hint for releasing blocked executions
rosa Feb 22, 2026
9347dbd
Feature: Dynamic scheduled tasks
cupatea Apr 24, 2025
3a23087
Extract polling_interval to scheduler configuration
cupatea Jun 19, 2025
3a13962
Fix abstraction for RecurringSchedule and Process
cupatea Jun 19, 2025
d1c0efc
Add create and destroy recurring task helpers
cupatea Jun 19, 2025
77ff523
Update README with Recurring tasks info
cupatea Jul 12, 2025
f1028a0
Use task key instead of id
cupatea Aug 21, 2025
7dd67c8
Fix mismatches in readme
cupatea Aug 21, 2025
eef747f
Use correct asserts in tests
cupatea Feb 12, 2026
8e1be8b
Add missing platform to Gemfile.lock
cupatea Feb 12, 2026
5ba7384
Move some Scheduler::RecurringSchedule methods to private
cupatea Feb 12, 2026
dd67d58
Wrap reload in app executor
cupatea Feb 12, 2026
b90cce5
Fix empty? method in Scheduler::RecurringSchedule
cupatea Feb 12, 2026
6b81c95
Prevent stale change by adding clear_changes in scheduler's loop
cupatea Feb 12, 2026
99ba202
Add test for enqueuing the job
cupatea Feb 12, 2026
79fca8d
Improve create_recurring_task - use RecurringTask .from_configuration
cupatea Feb 12, 2026
7ca2eb7
Fix names for public methods
cupatea Feb 12, 2026
890dabe
Prevent stale configured_tasks value
cupatea Feb 12, 2026
abbf54e
Use wrap_in_app_executor for refresh_registered_process
cupatea Feb 12, 2026
4ec36d6
Update tests and README
cupatea Feb 12, 2026
e1c1601
Fix style to match project conventions
rosa Feb 24, 2026
2f16558
Make dynamic recurring tasks opt-in and rename public API
rosa Feb 25, 2026
f7e39b5
Read static from options in from_configuration and rename to dynamic_…
rosa Feb 25, 2026
b012535
Refactor scheduler loop and add missing tests
rosa Feb 25, 2026
3d36ca4
Simplify recurring schedule with respect to dynamic task reloading
rosa Feb 25, 2026
b4f7c99
Clean a bit top-level SolidQueue methods to manage dynamic tasks
rosa Feb 25, 2026
7ded710
Avoid extra queries for dynamic tasks for process metadata and procline
rosa Feb 25, 2026
3b490ad
Merge right value of `static` correctly
rosa Feb 25, 2026
50defe2
Avoid unintended FOR UPDATE query execution.
genya0407 Mar 2, 2026
2f142ed
docs: improve grammar a little
G-Rath Mar 11, 2026
3df3de3
Merge pull request #553 from cupatea/feature/dynamic_recurring_tasks
rosa Mar 20, 2026
176721e
Bump solid_queue to 1.4.0
rosa Mar 20, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,15 @@ jobs:
with:
ruby-version: 3.4
bundler-cache: true
cache-version: 1
- name: Run rubocop
run: |
bundle exec rubocop --parallel

tests:
name: Tests
runs-on: ubuntu-latest
timeout-minutes: 15
strategy:
fail-fast: false
matrix:
Expand All @@ -28,12 +30,17 @@ jobs:
- 3.2
- 3.3
- 3.4
- 4.0
database: [ mysql, postgres, sqlite ]
gemfile: [ rails_7_1, rails_7_2, rails_8_0, rails_main ]
gemfile: [ rails_7_1, rails_7_2, rails_8_0, rails_8_1, rails_main ]
exclude:
- ruby-version: "3.1"
gemfile: rails_8_0
- ruby-version: "3.1"
gemfile: rails_8_1
- ruby-version: "3.1"
gemfile: rails_main
- ruby-version: "3.2"
gemfile: rails_main
services:
mysql:
Expand Down Expand Up @@ -61,6 +68,7 @@ jobs:
with:
ruby-version: ${{ matrix.ruby-version }}
bundler-cache: true
cache-version: 1
- name: Update to latest Rails
run: |
bundle update railties
Expand All @@ -77,4 +85,4 @@ jobs:
path: |
test/dummy/log/test.log
if-no-files-found: ignore
retention-days: 7
retention-days: 30
4 changes: 4 additions & 0 deletions Appraisals
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ appraise "rails-8-0" do
gem "railties", "~> 8.0.0"
end

appraise "rails-8-1" do
gem "railties", "~> 8.1.0"
end

appraise "rails-main" do
gem "railties", github: "rails/rails", branch: "main"
end
3 changes: 2 additions & 1 deletion Gemfile.lock
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
PATH
remote: .
specs:
solid_queue (1.2.2)
solid_queue (1.4.0)
activejob (>= 7.1)
activerecord (>= 7.1)
concurrent-ruby (>= 1.3.1)
Expand Down Expand Up @@ -201,6 +201,7 @@ DEPENDENCIES
appraisal
debug (~> 1.9)
logger
minitest (~> 5.0)
mocha
mysql2
pg
Expand Down
120 changes: 100 additions & 20 deletions README.md

Large diffs are not rendered by default.

4 changes: 3 additions & 1 deletion Rakefile
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ require "bundler/setup"
APP_RAKEFILE = File.expand_path("test/dummy/Rakefile", __dir__)
load "rails/tasks/engine.rake"

load "rails/tasks/statistics.rake"
if Rails::VERSION::MAJOR < 8
load "rails/tasks/statistics.rake"
end

require "bundler/gem_tasks"
require "rake/tasklib"
Expand Down
4 changes: 3 additions & 1 deletion app/models/solid_queue/blocked_execution.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,9 @@ def release_many(concurrency_keys)

def release_one(concurrency_key)
transaction do
if execution = ordered.where(concurrency_key: concurrency_key).limit(1).non_blocking_lock.first
if execution = ordered.where(concurrency_key: concurrency_key).limit(1)
.use_index(:index_solid_queue_blocked_executions_for_release)
.non_blocking_lock.first
execution.release
end
end
Expand Down
2 changes: 1 addition & 1 deletion app/models/solid_queue/failed_execution.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ class FailedExecution < Execution

serialize :error, coder: JSON

before_create :expand_error_details_from_exception
before_save :expand_error_details_from_exception, if: :exception

attr_accessor :exception

Expand Down
2 changes: 1 addition & 1 deletion app/models/solid_queue/job/concurrency_controls.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ def unblock_next_blocked_job
end

def concurrency_limited?
concurrency_key.present?
concurrency_key.present? && job_class.present?
end

def blocked?
Expand Down
11 changes: 10 additions & 1 deletion app/models/solid_queue/job/retryable.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,16 @@ def retry
end

def failed_with(exception)
FailedExecution.create_or_find_by!(job_id: id, exception: exception)
FailedExecution.transaction(requires_new: true) do
FailedExecution.create!(job_id: id, exception: exception)
end
rescue ActiveRecord::RecordNotUnique
if (failed_execution = FailedExecution.find_by(job_id: id))
failed_execution.exception = exception
failed_execution.save!
else
retry
end
end

def reset_execution_counters
Expand Down
3 changes: 2 additions & 1 deletion app/models/solid_queue/ready_execution.rb
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@ def select_and_lock(queue_relation, process_id, limit)
end

def select_candidates(queue_relation, limit)
queue_relation.ordered.limit(limit).non_blocking_lock.select(:id, :job_id)
# Force query execution here with #to_a to avoid unintended FOR UPDATE query executions
queue_relation.ordered.limit(limit).non_blocking_lock.select(:id, :job_id).to_a
end

def lock_candidates(executions, process_id)
Expand Down
7 changes: 7 additions & 0 deletions app/models/solid_queue/record.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,13 @@ def supports_insert_conflict_target?
connection.supports_insert_conflict_target?
end
end

# Pass index hints to the query optimizer using SQL comment hints.
# Uses MySQL 8 optimizer hint query comments, which SQLite and
# PostgreSQL ignore.
def use_index(*indexes)
optimizer_hints "INDEX(#{quoted_table_name} #{indexes.join(', ')})"
end
end
end
end
Expand Down
32 changes: 25 additions & 7 deletions app/models/solid_queue/recurring_task.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,12 @@ module SolidQueue
class RecurringTask < Record
serialize :arguments, coder: Arguments, default: []

validate :supported_schedule
validate :ensure_schedule_supported
validate :ensure_command_or_class_present
validate :existing_job_class
validate :ensure_existing_job_class

scope :static, -> { where(static: true) }
scope :dynamic, -> { where(static: false) }

has_many :recurring_executions, foreign_key: :task_key, primary_key: :key

Expand All @@ -32,7 +33,15 @@ def from_configuration(key, **options)
queue_name: options[:queue].presence,
priority: options[:priority].presence,
description: options[:description],
static: true
static: options.fetch(:static, true)
end

def create_dynamic_task(key, **options)
from_configuration(key, **options.merge(static: false)).save!
end

def delete_dynamic_task(key)
RecurringTask.dynamic.find_by!(key: key).destroy
end

def create_or_update_all(tasks)
Expand Down Expand Up @@ -102,19 +111,28 @@ def attributes_for_upsert
end

private
def supported_schedule
def ensure_schedule_supported
unless parsed_schedule.instance_of?(Fugit::Cron)
errors.add :schedule, :unsupported, message: "is not a supported recurring schedule"
end
rescue ArgumentError => error
message = if error.message.include?("multiple crons")
"generates multiple cron schedules. Please use separate recurring tasks for each schedule, " +
"or use explicit cron syntax (e.g., '40 0,15 * * *' for multiple times with the same minutes)"
else
error.message
end

errors.add :schedule, :unsupported, message: message
end

def ensure_command_or_class_present
unless command.present? || class_name.present?
errors.add :base, :command_and_class_blank, message: "either command or class_name must be present"
errors.add :base, :command_and_class_blank, message: "either command or class must be present"
end
end

def existing_job_class
def ensure_existing_job_class
if class_name.present? && job_class.nil?
errors.add :class_name, :undefined, message: "doesn't correspond to an existing class"
end
Expand Down Expand Up @@ -152,7 +170,7 @@ def arguments_with_kwargs


def parsed_schedule
@parsed_schedule ||= Fugit.parse(schedule)
@parsed_schedule ||= Fugit.parse(schedule, multi: :fail)
end

def job_class
Expand Down
2 changes: 1 addition & 1 deletion app/models/solid_queue/semaphore.rb
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ def initialize(job)
end

def wait
if semaphore = Semaphore.find_by(key: key)
if semaphore = Semaphore.lock.find_by(key: key)
semaphore.value > 0 && attempt_decrement
else
attempt_creation
Expand Down
7 changes: 7 additions & 0 deletions gemfiles/rails_8_1.gemfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# This file was generated by Appraisal

source "https://rubygems.org"

gem "railties", "~> 8.1.0"

gemspec path: "../"
92 changes: 70 additions & 22 deletions lib/puma/plugin/solid_queue.rb
Original file line number Diff line number Diff line change
@@ -1,44 +1,92 @@
require "puma/plugin"

module Puma
class DSL
def solid_queue_mode(mode = :fork)
@options[:solid_queue_mode] = mode.to_sym
end
end
end

Puma::Plugin.create do
attr_reader :puma_pid, :solid_queue_pid, :log_writer, :solid_queue_supervisor

def start(launcher)
@log_writer = launcher.log_writer
@puma_pid = $$

in_background do
monitor_solid_queue
if launcher.options[:solid_queue_mode] == :async
start_async(launcher)
else
start_forked(launcher)
end
end

if Gem::Version.new(Puma::Const::VERSION) < Gem::Version.new("7")
launcher.events.on_booted do
@solid_queue_pid = fork do
Thread.new { monitor_puma }
SolidQueue::Supervisor.start
private
def start_forked(launcher)
in_background do
monitor_solid_queue
end

if Gem::Version.new(Puma::Const::VERSION) < Gem::Version.new("7")
launcher.events.on_booted do
@solid_queue_pid = fork do
Thread.new { monitor_puma }
SolidQueue::Supervisor.start(mode: :fork)
end
end

launcher.events.on_stopped { stop_solid_queue_fork }
launcher.events.on_restart { stop_solid_queue_fork }
else
launcher.events.after_booted do
@solid_queue_pid = fork do
Thread.new { monitor_puma }
start_solid_queue(mode: :fork)
end
end

launcher.events.after_stopped { stop_solid_queue_fork }
launcher.events.before_restart { stop_solid_queue_fork }
end
end

launcher.events.on_stopped { stop_solid_queue }
launcher.events.on_restart { stop_solid_queue }
else
launcher.events.after_booted do
@solid_queue_pid = fork do
Thread.new { monitor_puma }
SolidQueue::Supervisor.start
def start_async(launcher)
if Gem::Version.new(Puma::Const::VERSION) < Gem::Version.new("7")
launcher.events.on_booted do
start_solid_queue(mode: :async, standalone: false)
end

launcher.events.on_stopped { solid_queue_supervisor&.stop }

launcher.events.on_restart do
solid_queue_supervisor&.stop
start_solid_queue(mode: :async, standalone: false)
end
else
launcher.events.after_booted do
start_solid_queue(mode: :async, standalone: false)
end

launcher.events.after_stopped { solid_queue_supervisor&.stop }

launcher.events.before_restart do
solid_queue_supervisor&.stop
start_solid_queue(mode: :async, standalone: false)
end
end
end

launcher.events.after_stopped { stop_solid_queue }
launcher.events.before_restart { stop_solid_queue }
def start_solid_queue(**options)
@solid_queue_supervisor = SolidQueue::Supervisor.start(**options)
end
end

private
def stop_solid_queue
def stop_solid_queue_fork
return unless solid_queue_pid

Process.waitpid(solid_queue_pid, Process::WNOHANG)
log "Stopping Solid Queue..."
Process.kill(:INT, solid_queue_pid) if solid_queue_pid
Process.kill(:INT, solid_queue_pid)
Process.wait(solid_queue_pid)
rescue Errno::ECHILD, Errno::ESRCH
end
Expand All @@ -48,7 +96,7 @@ def monitor_puma
end

def monitor_solid_queue
monitor(:solid_queue_dead?, "Detected Solid Queue has gone away, stopping Puma...")
monitor(:solid_queue_fork_dead?, "Detected Solid Queue has gone away, stopping Puma...")
end

def monitor(process_dead, message)
Expand All @@ -62,7 +110,7 @@ def monitor(process_dead, message)
end
end

def solid_queue_dead?
def solid_queue_fork_dead?
if solid_queue_started?
Process.waitpid(solid_queue_pid, Process::WNOHANG)
end
Expand Down
8 changes: 8 additions & 0 deletions lib/solid_queue.rb
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,14 @@ module SolidQueue

delegate :on_start, :on_stop, :on_exit, to: Supervisor

def schedule_recurring_task(key, **options)
RecurringTask.create_dynamic_task(key, **options)
end

def unschedule_recurring_task(key)
RecurringTask.delete_dynamic_task(key)
end

[ Dispatcher, Scheduler, Worker ].each do |process|
define_singleton_method(:"on_#{process.name.demodulize.downcase}_start") do |&block|
process.on_start(&block)
Expand Down
Loading
Loading