From 0531f06061cae5abf274cd123ee8127b262c2d89 Mon Sep 17 00:00:00 2001 From: Peter Solnica Date: Mon, 27 Apr 2026 12:37:45 +0000 Subject: [PATCH 1/4] feat(rails): initial support for Solid Queue --- sentry-rails/.gitignore | 2 +- sentry-rails/Gemfile | 3 + .../spec/active_job/solid_queue_spec.rb | 44 ++++++ .../spec/active_job/support/harness.rb | 33 ++++- .../test_rails_app/config/application.rb | 12 ++ .../dummy/test_rails_app/db/queue_schema.rb | 131 ++++++++++++++++++ 6 files changed, 218 insertions(+), 7 deletions(-) create mode 100644 sentry-rails/spec/active_job/solid_queue_spec.rb create mode 100644 sentry-rails/spec/dummy/test_rails_app/db/queue_schema.rb diff --git a/sentry-rails/.gitignore b/sentry-rails/.gitignore index e8acda211..27b24b079 100644 --- a/sentry-rails/.gitignore +++ b/sentry-rails/.gitignore @@ -5,7 +5,7 @@ /doc/ /pkg/ /spec/reports/ -/spec/dummy/test_rails_app/db* +/spec/dummy/test_rails_app/**/*.sqlite3* /tmp/ # rspec failure tracking diff --git a/sentry-rails/Gemfile b/sentry-rails/Gemfile index ea1d6ac51..3b658d47f 100644 --- a/sentry-rails/Gemfile +++ b/sentry-rails/Gemfile @@ -32,13 +32,16 @@ gem "rails", "~> #{rails_version}" if rails_version >= Gem::Version.new("8.1.0") gem "rspec-rails", "~> 8.0.0" + gem "solid_queue" gem "sqlite3", "~> 2.1.1", platform: :ruby elsif rails_version >= Gem::Version.new("8.0.0") gem "rspec-rails", "~> 8.0.0" + gem "solid_queue" gem "sqlite3", "~> 2.1.1", platform: :ruby elsif rails_version >= Gem::Version.new("7.1.0") gem "psych", "~> 4.0.0" gem "rspec-rails", "~> 7.0" + gem "solid_queue" gem "sqlite3", "~> 1.7.3", platform: :ruby elsif rails_version >= Gem::Version.new("6.1.0") gem "rspec-rails", "~> 6.0" diff --git a/sentry-rails/spec/active_job/solid_queue_spec.rb b/sentry-rails/spec/active_job/solid_queue_spec.rb new file mode 100644 index 000000000..ed1f3711c --- /dev/null +++ b/sentry-rails/spec/active_job/solid_queue_spec.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +require "spec_helper" + +if Gem::Version.new(Rails.version) >= Gem::Version.new("7.1") + require "solid_queue" +end + +RSpec.describe "Sentry + ActiveJob on SolidQueue", skip: Gem::Version.new(Rails.version) < Gem::Version.new("7.1") do + include ActiveSupport::Testing::TimeHelpers + include_context "active_job backend harness", adapter: :solid_queue + + def boot_adapter(_adapter) + Sentry::Rails::Test::Application.load_queue_schema + end + + def reset_adapter(_adapter) + [ + SolidQueue::ReadyExecution, + SolidQueue::ClaimedExecution, + SolidQueue::FailedExecution, + SolidQueue::BlockedExecution, + SolidQueue::ScheduledExecution, + SolidQueue::RecurringExecution, + SolidQueue::Process, + SolidQueue::Job + ].each(&:delete_all) + end + + def drain(at: nil) + process = SolidQueue::Process.register( + kind: "Worker", + pid: ::Process.pid, + name: "spec-#{SecureRandom.hex(4)}" + ) + + travel_to(at || Time.current) do + SolidQueue::ScheduledExecution.dispatch_next_batch(100) + SolidQueue::ReadyExecution.claim("*", 100, process.id).each(&:perform) + end + end + + it_behaves_like "a Sentry-instrumented ActiveJob backend" +end diff --git a/sentry-rails/spec/active_job/support/harness.rb b/sentry-rails/spec/active_job/support/harness.rb index 4e489fb20..3d34ff4c1 100644 --- a/sentry-rails/spec/active_job/support/harness.rb +++ b/sentry-rails/spec/active_job/support/harness.rb @@ -19,14 +19,27 @@ teardown_sentry_test end - def boot_adapter(_adapter) - # Per-adapter setup hook. Backends extend this when they need to load - # schemas, start supervisors, or otherwise prepare the environment. + def boot_adapter(adapter) + case adapter + when :solid_queue + Sentry::Rails::Test::Application.load_queue_schema + end end - def reset_adapter(_adapter) - # Per-adapter teardown hook. Backends extend this to truncate tables - # or otherwise clean up state between examples. + def reset_adapter(adapter) + case adapter + when :solid_queue + [ + SolidQueue::ReadyExecution, + SolidQueue::ClaimedExecution, + SolidQueue::FailedExecution, + SolidQueue::BlockedExecution, + SolidQueue::ScheduledExecution, + SolidQueue::RecurringExecution, + SolidQueue::Process, + SolidQueue::Job + ].each(&:delete_all) + end end def drain(at: nil) @@ -42,6 +55,14 @@ def drain(at: nil) kwargs = at ? { at: at } : {} perform_enqueued_jobs(**kwargs) end + when :solid_queue + process = SolidQueue::Process.register( + kind: "Worker", + pid: ::Process.pid, + name: "spec-#{SecureRandom.hex(4)}" + ) + + SolidQueue::ReadyExecution.claim("*", 100, process.id).each(&:perform) else raise NotImplementedError, "active_job backend harness has no drain strategy for adapter: #{adapter.inspect}" end diff --git a/sentry-rails/spec/dummy/test_rails_app/config/application.rb b/sentry-rails/spec/dummy/test_rails_app/config/application.rb index 6275220de..31b70bfa2 100644 --- a/sentry-rails/spec/dummy/test_rails_app/config/application.rb +++ b/sentry-rails/spec/dummy/test_rails_app/config/application.rb @@ -45,6 +45,10 @@ def self.schema_file @schema_file ||= root_path.join("db/schema.rb") end + def self.queue_schema_file + @queue_schema_file ||= root_path.join("db/queue_schema.rb") + end + def self.db_path @db_path ||= root_path.join("db", "db.sqlite3") end @@ -77,6 +81,14 @@ def self.load_test_schema end end + def self.load_queue_schema + @__queue_schema_loaded__ ||= begin + load_test_schema + require Test::Application.queue_schema_file + true + end + end + # Configure method that sets up base configuration # This can be inherited and extended by subclasses def configure diff --git a/sentry-rails/spec/dummy/test_rails_app/db/queue_schema.rb b/sentry-rails/spec/dummy/test_rails_app/db/queue_schema.rb new file mode 100644 index 000000000..0c9e37bfa --- /dev/null +++ b/sentry-rails/spec/dummy/test_rails_app/db/queue_schema.rb @@ -0,0 +1,131 @@ +# frozen_string_literal: true + +ActiveRecord::Schema[7.1].define(version: 1) do + create_table "solid_queue_blocked_executions", force: :cascade do |t| + t.bigint "job_id", null: false + t.string "queue_name", null: false + t.integer "priority", default: 0, null: false + t.string "concurrency_key", null: false + t.datetime "expires_at", null: false + t.datetime "created_at", null: false + t.index [ "concurrency_key", "priority", "job_id" ], name: "index_solid_queue_blocked_executions_for_release" + t.index [ "expires_at", "concurrency_key" ], name: "index_solid_queue_blocked_executions_for_maintenance" + t.index [ "job_id" ], name: "index_solid_queue_blocked_executions_on_job_id", unique: true + end + + create_table "solid_queue_claimed_executions", force: :cascade do |t| + t.bigint "job_id", null: false + t.bigint "process_id" + t.datetime "created_at", null: false + t.index [ "job_id" ], name: "index_solid_queue_claimed_executions_on_job_id", unique: true + t.index [ "process_id", "job_id" ], name: "index_solid_queue_claimed_executions_on_process_id_and_job_id" + end + + create_table "solid_queue_failed_executions", force: :cascade do |t| + t.bigint "job_id", null: false + t.text "error" + t.datetime "created_at", null: false + t.index [ "job_id" ], name: "index_solid_queue_failed_executions_on_job_id", unique: true + end + + create_table "solid_queue_jobs", force: :cascade do |t| + t.string "queue_name", null: false + t.string "class_name", null: false + t.text "arguments" + t.integer "priority", default: 0, null: false + t.string "active_job_id" + t.datetime "scheduled_at" + t.datetime "finished_at" + t.string "concurrency_key" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index [ "active_job_id" ], name: "index_solid_queue_jobs_on_active_job_id" + t.index [ "class_name" ], name: "index_solid_queue_jobs_on_class_name" + t.index [ "finished_at" ], name: "index_solid_queue_jobs_on_finished_at" + t.index [ "queue_name", "finished_at" ], name: "index_solid_queue_jobs_for_filtering" + t.index [ "scheduled_at", "finished_at" ], name: "index_solid_queue_jobs_for_alerting" + end + + create_table "solid_queue_pauses", force: :cascade do |t| + t.string "queue_name", null: false + t.datetime "created_at", null: false + t.index [ "queue_name" ], name: "index_solid_queue_pauses_on_queue_name", unique: true + end + + create_table "solid_queue_processes", force: :cascade do |t| + t.string "kind", null: false + t.datetime "last_heartbeat_at", null: false + t.bigint "supervisor_id" + t.integer "pid", null: false + t.string "hostname" + t.text "metadata" + t.datetime "created_at", null: false + t.string "name", null: false + t.index [ "last_heartbeat_at" ], name: "index_solid_queue_processes_on_last_heartbeat_at" + t.index [ "name", "supervisor_id" ], name: "index_solid_queue_processes_on_name_and_supervisor_id", unique: true + t.index [ "supervisor_id" ], name: "index_solid_queue_processes_on_supervisor_id" + end + + create_table "solid_queue_ready_executions", force: :cascade do |t| + t.bigint "job_id", null: false + t.string "queue_name", null: false + t.integer "priority", default: 0, null: false + t.datetime "created_at", null: false + t.index [ "job_id" ], name: "index_solid_queue_ready_executions_on_job_id", unique: true + t.index [ "priority", "job_id" ], name: "index_solid_queue_poll_all" + t.index [ "queue_name", "priority", "job_id" ], name: "index_solid_queue_poll_by_queue" + end + + create_table "solid_queue_recurring_executions", force: :cascade do |t| + t.bigint "job_id", null: false + t.string "task_key", null: false + t.datetime "run_at", null: false + t.datetime "created_at", null: false + t.index [ "job_id" ], name: "index_solid_queue_recurring_executions_on_job_id", unique: true + t.index [ "task_key", "run_at" ], name: "index_solid_queue_recurring_executions_on_task_key_and_run_at", unique: true + end + + create_table "solid_queue_recurring_tasks", force: :cascade do |t| + t.string "key", null: false + t.string "schedule", null: false + t.string "command", limit: 2048 + t.string "class_name" + t.text "arguments" + t.string "queue_name" + t.integer "priority", default: 0 + t.boolean "static", default: true, null: false + t.text "description" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index [ "key" ], name: "index_solid_queue_recurring_tasks_on_key", unique: true + t.index [ "static" ], name: "index_solid_queue_recurring_tasks_on_static" + end + + create_table "solid_queue_scheduled_executions", force: :cascade do |t| + t.bigint "job_id", null: false + t.string "queue_name", null: false + t.integer "priority", default: 0, null: false + t.datetime "scheduled_at", null: false + t.datetime "created_at", null: false + t.index [ "job_id" ], name: "index_solid_queue_scheduled_executions_on_job_id", unique: true + t.index [ "scheduled_at", "priority", "job_id" ], name: "index_solid_queue_dispatch_all" + end + + create_table "solid_queue_semaphores", force: :cascade do |t| + t.string "key", null: false + t.integer "value", default: 1, null: false + t.datetime "expires_at", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index [ "expires_at" ], name: "index_solid_queue_semaphores_on_expires_at" + t.index [ "key", "value" ], name: "index_solid_queue_semaphores_on_key_and_value" + t.index [ "key" ], name: "index_solid_queue_semaphores_on_key", unique: true + end + + add_foreign_key "solid_queue_blocked_executions", "solid_queue_jobs", column: "job_id", on_delete: :cascade + add_foreign_key "solid_queue_claimed_executions", "solid_queue_jobs", column: "job_id", on_delete: :cascade + add_foreign_key "solid_queue_failed_executions", "solid_queue_jobs", column: "job_id", on_delete: :cascade + add_foreign_key "solid_queue_ready_executions", "solid_queue_jobs", column: "job_id", on_delete: :cascade + add_foreign_key "solid_queue_recurring_executions", "solid_queue_jobs", column: "job_id", on_delete: :cascade + add_foreign_key "solid_queue_scheduled_executions", "solid_queue_jobs", column: "job_id", on_delete: :cascade +end From eddcdc2dddb7eccde1028cb0e1f341c2a3db2b4e Mon Sep 17 00:00:00 2001 From: Peter Solnica Date: Thu, 30 Apr 2026 15:39:07 +0200 Subject: [PATCH 2/4] fixup: do not crash on rails w/o SQ --- .../spec/active_job/solid_queue_spec.rb | 64 +++++++++---------- 1 file changed, 32 insertions(+), 32 deletions(-) diff --git a/sentry-rails/spec/active_job/solid_queue_spec.rb b/sentry-rails/spec/active_job/solid_queue_spec.rb index ed1f3711c..412ed2ff2 100644 --- a/sentry-rails/spec/active_job/solid_queue_spec.rb +++ b/sentry-rails/spec/active_job/solid_queue_spec.rb @@ -2,43 +2,43 @@ require "spec_helper" -if Gem::Version.new(Rails.version) >= Gem::Version.new("7.1") +if RAILS_VERSION >= 7.1 require "solid_queue" -end - -RSpec.describe "Sentry + ActiveJob on SolidQueue", skip: Gem::Version.new(Rails.version) < Gem::Version.new("7.1") do - include ActiveSupport::Testing::TimeHelpers - include_context "active_job backend harness", adapter: :solid_queue - def boot_adapter(_adapter) - Sentry::Rails::Test::Application.load_queue_schema - end + RSpec.describe "Sentry + ActiveJob on SolidQueue" do + include ActiveSupport::Testing::TimeHelpers + include_context "active_job backend harness", adapter: :solid_queue - def reset_adapter(_adapter) - [ - SolidQueue::ReadyExecution, - SolidQueue::ClaimedExecution, - SolidQueue::FailedExecution, - SolidQueue::BlockedExecution, - SolidQueue::ScheduledExecution, - SolidQueue::RecurringExecution, - SolidQueue::Process, - SolidQueue::Job - ].each(&:delete_all) - end + def boot_adapter(_adapter) + Sentry::Rails::Test::Application.load_queue_schema + end - def drain(at: nil) - process = SolidQueue::Process.register( - kind: "Worker", - pid: ::Process.pid, - name: "spec-#{SecureRandom.hex(4)}" - ) + def reset_adapter(_adapter) + [ + SolidQueue::ReadyExecution, + SolidQueue::ClaimedExecution, + SolidQueue::FailedExecution, + SolidQueue::BlockedExecution, + SolidQueue::ScheduledExecution, + SolidQueue::RecurringExecution, + SolidQueue::Process, + SolidQueue::Job + ].each(&:delete_all) + end - travel_to(at || Time.current) do - SolidQueue::ScheduledExecution.dispatch_next_batch(100) - SolidQueue::ReadyExecution.claim("*", 100, process.id).each(&:perform) + def drain(at: nil) + process = SolidQueue::Process.register( + kind: "Worker", + pid: ::Process.pid, + name: "spec-#{SecureRandom.hex(4)}" + ) + + travel_to(at || Time.current) do + SolidQueue::ScheduledExecution.dispatch_next_batch(100) + SolidQueue::ReadyExecution.claim("*", 100, process.id).each(&:perform) + end end - end - it_behaves_like "a Sentry-instrumented ActiveJob backend" + it_behaves_like "a Sentry-instrumented ActiveJob backend" + end end From 0fd03e6d641b4bc24087e77150899190a600d084 Mon Sep 17 00:00:00 2001 From: Peter Solnica Date: Thu, 30 Apr 2026 15:48:37 +0200 Subject: [PATCH 3/4] fixup: fix syntax for older rubies --- sentry-rails/spec/active_job/solid_queue_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sentry-rails/spec/active_job/solid_queue_spec.rb b/sentry-rails/spec/active_job/solid_queue_spec.rb index 412ed2ff2..ec14b272b 100644 --- a/sentry-rails/spec/active_job/solid_queue_spec.rb +++ b/sentry-rails/spec/active_job/solid_queue_spec.rb @@ -2,7 +2,7 @@ require "spec_helper" -if RAILS_VERSION >= 7.1 +if RAILS_VERSION >= 7.1 && RUBY_VERSION >= "3.1" require "solid_queue" RSpec.describe "Sentry + ActiveJob on SolidQueue" do From 396f231bc1d506a9be8095eebc58a534f836ddf0 Mon Sep 17 00:00:00 2001 From: Peter Solnica Date: Thu, 30 Apr 2026 15:55:59 +0200 Subject: [PATCH 4/4] fixup: remove Solid Queue specific logic from adapter methods --- .../spec/active_job/support/harness.rb | 25 ------------------- 1 file changed, 25 deletions(-) diff --git a/sentry-rails/spec/active_job/support/harness.rb b/sentry-rails/spec/active_job/support/harness.rb index 3d34ff4c1..c19b4e37c 100644 --- a/sentry-rails/spec/active_job/support/harness.rb +++ b/sentry-rails/spec/active_job/support/harness.rb @@ -20,26 +20,9 @@ end def boot_adapter(adapter) - case adapter - when :solid_queue - Sentry::Rails::Test::Application.load_queue_schema - end end def reset_adapter(adapter) - case adapter - when :solid_queue - [ - SolidQueue::ReadyExecution, - SolidQueue::ClaimedExecution, - SolidQueue::FailedExecution, - SolidQueue::BlockedExecution, - SolidQueue::ScheduledExecution, - SolidQueue::RecurringExecution, - SolidQueue::Process, - SolidQueue::Job - ].each(&:delete_all) - end end def drain(at: nil) @@ -55,14 +38,6 @@ def drain(at: nil) kwargs = at ? { at: at } : {} perform_enqueued_jobs(**kwargs) end - when :solid_queue - process = SolidQueue::Process.register( - kind: "Worker", - pid: ::Process.pid, - name: "spec-#{SecureRandom.hex(4)}" - ) - - SolidQueue::ReadyExecution.claim("*", 100, process.id).each(&:perform) else raise NotImplementedError, "active_job backend harness has no drain strategy for adapter: #{adapter.inspect}" end