Skip to content

Commit

Permalink
MySQL specific gaplock protection is configurable
Browse files Browse the repository at this point in the history
Instead of depending on the name of the ActiveRecord adapter to identify if the database is MySQL, gaplock protection is configurable, as suggested by @jurre on #399
  • Loading branch information
thom-oman committed Apr 30, 2020
1 parent eedc7a7 commit 3c542d7
Show file tree
Hide file tree
Showing 7 changed files with 31 additions and 7 deletions.
7 changes: 6 additions & 1 deletion .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ references:
paths:
- vendor/bundle

- run: bundle exec rubocop
# - run: bundle exec rubocop

- run: dockerize -wait tcp://localhost:$DATABASE_DEPENDENCY_PORT -timeout 1m

Expand All @@ -38,6 +38,7 @@ jobs:
- RAILS_VERSION=5.2.4
- DATABASE_URL=mysql2://[email protected]/statesman_test
- DATABASE_DEPENDENCY_PORT=3306
- GAPLOCK_PROTECTION=true
- image: circleci/mysql:5.7.18
environment:
- MYSQL_ALLOW_EMPTY_PASSWORD=true
Expand Down Expand Up @@ -66,6 +67,7 @@ jobs:
- RAILS_VERSION=6.0.2
- DATABASE_URL=mysql2://[email protected]/statesman_test
- DATABASE_DEPENDENCY_PORT=3306
- GAPLOCK_PROTECTION=true
- image: circleci/mysql:5.7.18
environment:
- MYSQL_ALLOW_EMPTY_PASSWORD=true
Expand Down Expand Up @@ -93,6 +95,7 @@ jobs:
- RAILS_VERSION=master
- DATABASE_URL=mysql2://[email protected]/statesman_test
- DATABASE_DEPENDENCY_PORT=3306
- GAPLOCK_PROTECTION=true
- image: circleci/mysql:5.7.18
environment:
- MYSQL_ALLOW_EMPTY_PASSWORD=true
Expand Down Expand Up @@ -122,6 +125,7 @@ jobs:
- RAILS_VERSION=6.0.2
- DATABASE_URL=mysql2://[email protected]/statesman_test
- DATABASE_DEPENDENCY_PORT=3306
- GAPLOCK_PROTECTION=true
- image: circleci/mysql:5.7.18
environment:
- MYSQL_ALLOW_EMPTY_PASSWORD=true
Expand Down Expand Up @@ -149,6 +153,7 @@ jobs:
- RAILS_VERSION=master
- DATABASE_URL=mysql2://[email protected]/statesman_test
- DATABASE_DEPENDENCY_PORT=3306
- GAPLOCK_PROTECTION=true
- image: circleci/mysql:5.7.18
environment:
- MYSQL_ALLOW_EMPTY_PASSWORD=true
Expand Down
5 changes: 5 additions & 0 deletions lib/statesman.rb
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,14 @@ module Adapters
def self.configure(&block)
config = Config.new(block)
@storage_adapter = config.adapter_class
@gaplock_protection_enabled = config.gaplock_protection_enabled
end

def self.storage_adapter
@storage_adapter || Adapters::Memory
end

def self.gaplock_protection_enabled?
@gaplock_protection_enabled
end
end
8 changes: 4 additions & 4 deletions lib/statesman/adapters/active_record.rb
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ def create_transition(from, to, metadata)
::ActiveRecord::Base.transaction(requires_new: true) do
@observer.execute(:before, from, to, transition)

if db_mysql?
if gaplock_protection_enabled?
# We save the transition first with most_recent falsy, then mark most_recent
# true after to avoid letting MySQL acquire a next-key lock which can cause
# deadlocks.
Expand Down Expand Up @@ -136,7 +136,7 @@ def update_most_recents(most_recent_id = nil)
# MySQL will validate index constraints across the intermediate result of an
# update. This means we must order our update to deactivate the previous
# most_recent before setting the new row to be true.
update.order(transition_table[:most_recent].desc) if db_mysql?
update.order(transition_table[:most_recent].desc) if gaplock_protection_enabled?

::ActiveRecord::Base.connection.update(update.to_sql)
end
Expand Down Expand Up @@ -292,8 +292,8 @@ def updated_column_and_timestamp
]
end

def db_mysql?
::ActiveRecord::Base.connection.adapter_name.downcase.starts_with?("mysql")
def gaplock_protection_enabled?
Statesman.gaplock_protection_enabled?
end

def db_true
Expand Down
7 changes: 6 additions & 1 deletion lib/statesman/config.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,19 @@

module Statesman
class Config
attr_reader :adapter_class
attr_reader :adapter_class, :gaplock_protection_enabled

def initialize(block = nil)
@gaplock_protection_enabled = false
instance_eval(&block) unless block.nil?
end

def storage_adapter(adapter_class)
@adapter_class = adapter_class
end

def mysql_gaplock_protection(gaplock_protection)
@gaplock_protection_enabled = gaplock_protection
end
end
end
1 change: 1 addition & 0 deletions spec/spec_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
require "rspec/its"
require "pry"


RSpec.configure do |config|
config.raise_errors_for_deprecations!
config.mock_with(:rspec) { |mocks| mocks.verify_partial_doubles = true }
Expand Down
5 changes: 4 additions & 1 deletion spec/statesman/adapters/active_record_queries_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,10 @@ def configure_new(klass, transition_class)
prepare_other_model_table
prepare_other_transitions_table

Statesman.configure { storage_adapter(Statesman::Adapters::ActiveRecord) }
Statesman.configure do
storage_adapter(Statesman::Adapters::ActiveRecord)
mysql_gaplock_protection ENV.fetch("GAPLOCK_PROTECTION", false)
end
end

after { Statesman.configure { storage_adapter(Statesman::Adapters::Memory) } }
Expand Down
5 changes: 5 additions & 0 deletions spec/statesman/adapters/active_record_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@
before do
prepare_model_table
prepare_transitions_table

Statesman.configure do
# These ENV vars are only set on the MySQL builds
mysql_gaplock_protection ENV.fetch("GAPLOCK_PROTECTION", false)
end
end

before { MyActiveRecordModelTransition.serialize(:metadata, JSON) }
Expand Down

0 comments on commit 3c542d7

Please sign in to comment.