From 0786efbf3045cb3da1527eb64e2e23d75ef19c72 Mon Sep 17 00:00:00 2001 From: Andrii Furmanets Date: Sat, 3 Oct 2026 08:39:53 +0300 Subject: [PATCH 1/2] Escape LIKE metacharacters in queue prefixes A worker prefix such as mail_* became LIKE 'mail_%', so _ and % matched any character. When that false match was the only row, the selector returned it without the literal prefix check and the worker claimed a queue it was not configured to run. --- app/models/solid_queue/queue_selector.rb | 10 +++---- .../solid_queue/ready_execution_test.rb | 28 +++++++++++++++++++ 2 files changed, 33 insertions(+), 5 deletions(-) diff --git a/app/models/solid_queue/queue_selector.rb b/app/models/solid_queue/queue_selector.rb index d0d61dd14..110a78dc3 100644 --- a/app/models/solid_queue/queue_selector.rb +++ b/app/models/solid_queue/queue_selector.rb @@ -53,12 +53,12 @@ def exact_names def prefixed_names if prefixes.empty? then [] else - relation.where(([ "queue_name LIKE ?" ] * prefixes.count).join(" OR "), *prefixes).distinct_values_of(:queue_name) + relation.where(([ "queue_name LIKE ? ESCAPE '\\'" ] * prefixes.count).join(" OR "), *prefixes).distinct_values_of(:queue_name) end end def prefixes - @prefixes ||= raw_queues.select { |queue| prefixed_name?(queue) }.map { |queue| queue.tr("*", "%") } + @prefixes ||= raw_queues.select { |queue| prefixed_name?(queue) }.map { |queue| relation.sanitize_sql_like(queue).tr("*", "%") } end def exact_name?(queue) @@ -74,9 +74,9 @@ def paused_queues end def in_raw_order(queues) - # Only need to sort if we have prefixes and more than one queue name. - # Exact names are selected in the same order as they're found - if queues.one? || prefixes.empty? + # Exact names are already in configuration order. Prefix matches always + # go through the literal filter so a single false LIKE hit is not returned. + if prefixes.empty? queues else queues = queues.dup diff --git a/test/models/solid_queue/ready_execution_test.rb b/test/models/solid_queue/ready_execution_test.rb index a5f40659a..86c0f8409 100644 --- a/test/models/solid_queue/ready_execution_test.rb +++ b/test/models/solid_queue/ready_execution_test.rb @@ -67,6 +67,34 @@ class SolidQueue::ReadyExecutionTest < ActiveSupport::TestCase assert @jobs.none?(&:claimed?) end + test "prefixes escape LIKE wildcards so only literal queue names match" do + AddToBufferJob.set(queue: "mailbox").perform_later(1) + AddToBufferJob.set(queue: "mail-ops").perform_later(1) + + assert_claimed_jobs(0) do + SolidQueue::ReadyExecution.claim("mail_*", SolidQueue::Job.count + 1, 42) + end + + AddToBufferJob.set(queue: "mail_ops").perform_later(1) + AddToBufferJob.set(queue: "100x_done").perform_later(1) + + assert_claimed_jobs(1) do + SolidQueue::ReadyExecution.claim("mail_*", SolidQueue::Job.count + 1, 42) + end + assert_equal "mail_ops", SolidQueue::ClaimedExecution.last.job.queue_name + + assert_claimed_jobs(0) do + SolidQueue::ReadyExecution.claim("100%_*", SolidQueue::Job.count + 1, 42) + end + + AddToBufferJob.set(queue: "100%_done").perform_later(1) + + assert_claimed_jobs(1) do + SolidQueue::ReadyExecution.claim("100%_*", 1, 42) + end + assert_equal "100%_done", SolidQueue::ClaimedExecution.last.job.queue_name + end + test "claim jobs using a wildcard and having paused queues" do AddToBufferJob.perform_later("hey") From f89eb4bc82678d13aade714b11cd05a4869c19b2 Mon Sep 17 00:00:00 2001 From: Andrii Furmanets Date: Sat, 3 Oct 2026 08:55:26 +0300 Subject: [PATCH 2/2] Quote the LIKE escape so MySQL accepts queue prefixes ESCAPE '\' is a valid string on SQLite and PostgreSQL, but MySQL treats the backslash as an escape and rejects the clause. MySQL needs the same one-character escape written as '\\'. --- app/models/solid_queue/queue_selector.rb | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/app/models/solid_queue/queue_selector.rb b/app/models/solid_queue/queue_selector.rb index 110a78dc3..c2be7419b 100644 --- a/app/models/solid_queue/queue_selector.rb +++ b/app/models/solid_queue/queue_selector.rb @@ -53,7 +53,18 @@ def exact_names def prefixed_names if prefixes.empty? then [] else - relation.where(([ "queue_name LIKE ? ESCAPE '\\'" ] * prefixes.count).join(" OR "), *prefixes).distinct_values_of(:queue_name) + pattern = "queue_name LIKE ? #{like_escape_clause}" + relation.where(([ pattern ] * prefixes.count).join(" OR "), *prefixes).distinct_values_of(:queue_name) + end + end + + # MySQL string literals treat a backslash as an escape, so the single-character + # ESCAPE value is written with a doubled backslash. SQLite and PostgreSQL take one. + def like_escape_clause + if relation.connection.adapter_name.match?(/mysql|trilogy/i) + "ESCAPE '\\\\'" + else + "ESCAPE '\\'" end end