Skip to content

Commit

Permalink
Run solid_queue with SQLite
Browse files Browse the repository at this point in the history
Update the tests to run solid_queue on SQLite.

They can be triggered by:
```
TARGET_DB=sqlite rails test
```

The adapter is prone to raising `SQLite3::BusyException`s when
concurrent transactions occur. Preventing this requires a couple of
patches to the adapter.

1. Implement retry with backoff - add a `retries` config setting
to the adapter and sleep progressively longer for each retry. This
setting is currently in the Rails main branch, but that implementation
has no backoff. That doesn't work well in our case.
1. Always create immediate transactions - SQLite by default creates
deferred transactions, which don't take a write lock. Then later if
there is a write it tries to upgrade the lock. This won't work if the
transaction has a stale read so retrying the write by itself is not
possible. Starting with an immediate transaction moves the write lock to
that point and ensures that we only get blocked on a retryable
transaction.
  • Loading branch information
djmb committed Oct 16, 2023
1 parent 6ca84fc commit 30ad653
Show file tree
Hide file tree
Showing 7 changed files with 62 additions and 3 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ jobs:
fail-fast: false
matrix:
ruby-version: [3.2.2]
database: [mysql]
database: [mysql, sqlite]
services:
mysql:
image: mysql:8.0.31
Expand Down
1 change: 1 addition & 0 deletions lib/solid_queue/supervisor.rb
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ def start_runner(runner)

pid = fork do
runner.start
exit!
end

forks[pid] = runner
Expand Down
18 changes: 18 additions & 0 deletions test/dummy/config/database.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,22 @@
# gem "mysql2"
#

<% if ENV["TARGET_DB"] == "sqlite" %>
default: &default
adapter: sqlite3
pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 50 } %>
retries: 100

development:
<<: *default
database: db/development.sqlite3

test:
<<: *default
pool: 20
database: db/test.sqlite3

<% else %>
default: &default
adapter: mysql2
username: root
Expand All @@ -20,3 +36,5 @@ test:
<<: *default
pool: 20
database: solid_queue_test
<% end %>

30 changes: 30 additions & 0 deletions test/dummy/config/initializers/sqlite3.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
module SqliteImmediateTransactions
def begin_db_transaction
log("begin immediate transaction", "TRANSACTION") do
with_raw_connection(allow_retry: true, materialize_transactions: false) do |conn|
conn.transaction(:immediate)
end
end
end
end

module SQLite3Configuration
private
def configure_connection
super

if @config[:retries]
retries = self.class.type_cast_config_to_integer(@config[:retries])
raw_connection.busy_handler do |count|
(count <= retries).tap { |result| sleep count * 0.001 if result }
end
end
end
end

ActiveSupport.on_load :active_record do
if defined?(ActiveRecord::ConnectionAdapters::SQLite3Adapter)
ActiveRecord::ConnectionAdapters::SQLite3Adapter.prepend SqliteImmediateTransactions
ActiveRecord::ConnectionAdapters::SQLite3Adapter.prepend SQLite3Configuration
end
end
2 changes: 1 addition & 1 deletion test/dummy/config/solid_queue.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ default: &default
default:
pool_size: 5
scheduler:
polling_interval: 300
polling_interval: 1
batch_size: 500

development:
Expand Down
2 changes: 1 addition & 1 deletion test/integration/jobs_lifecycle_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ class JobsLifecycleTest < ActiveSupport::TestCase

travel_to 5.days.from_now

wait_for_jobs_to_finish_for(0.5.seconds)
wait_for_jobs_to_finish_for(5.seconds)

assert_equal 2, JobBuffer.size
assert_equal "I'm scheduled later", JobBuffer.last_value
Expand Down
10 changes: 10 additions & 0 deletions test/test_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,16 @@
ActiveSupport::TestCase.fixtures :all
end

module BlockLogDeviceTimeoutExceptions
def write(...)
# Prevents Timeout exceptions from occurring during log writing, where they will be swallowed
# See https://bugs.ruby-lang.org/issues/9115
Thread.handle_interrupt(Timeout::Error => :never, Timeout::ExitException => :never) { super }
end
end

Logger::LogDevice.prepend(BlockLogDeviceTimeoutExceptions)

class ActiveSupport::TestCase
setup do
SolidQueue.logger = ActiveSupport::Logger.new(nil)
Expand Down

0 comments on commit 30ad653

Please sign in to comment.