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 May 8, 2020
1 parent eedc7a7 commit 55be707
Show file tree
Hide file tree
Showing 5 changed files with 41 additions and 6 deletions.
5 changes: 5 additions & 0 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
@mysql_gaplock_protection = config.mysql_gaplock_protection
end

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

def self.mysql_gaplock_protection?
@mysql_gaplock_protection
end
end
12 changes: 8 additions & 4 deletions lib/statesman/adapters/active_record.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ def self.database_supports_partial_indexes?
end
end

def self.adapter_name
::ActiveRecord::Base.connection.adapter_name.downcase
end

def initialize(transition_class, parent_model, observer, options = {})
serialized = serialized?(transition_class)
column_type = transition_class.columns_hash["metadata"].sql_type
Expand Down Expand Up @@ -74,7 +78,7 @@ def create_transition(from, to, metadata)
::ActiveRecord::Base.transaction(requires_new: true) do
@observer.execute(:before, from, to, transition)

if db_mysql?
if mysql_gaplock_protection?
# 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 +140,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 mysql_gaplock_protection?

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

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

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

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

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

def storage_adapter(adapter_class)
@adapter_class = adapter_class

# If our adapter class suggests we're using mysql, enable gaplock protection by
# default.
enable_mysql_gaplock_protection if mysql_adapter?(adapter_class)
end

def enable_mysql_gaplock_protection
@mysql_gaplock_protection = true
end

private

def mysql_adapter?(adapter_class)
adapter_name = adapter_name(adapter_class)
adapter_name&.starts_with("mysql")
end

def adapter_name(adapter_class)
adapter_class.respond_to?(:adapter_name) && adapter_class&.adapter_name
end
end
end
4 changes: 3 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,9 @@ 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)
end
end

after { Statesman.configure { storage_adapter(Statesman::Adapters::Memory) } }
Expand Down

0 comments on commit 55be707

Please sign in to comment.