diff --git a/app/models/solid_queue/queue_selector.rb b/app/models/solid_queue/queue_selector.rb index d0d61dd1..c2be7419 100644 --- a/app/models/solid_queue/queue_selector.rb +++ b/app/models/solid_queue/queue_selector.rb @@ -53,12 +53,23 @@ 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) + 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 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 +85,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 a5f40659..86c0f840 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")