From 5b4593ea3f406dd577572fc46d8e7fef95564c06 Mon Sep 17 00:00:00 2001 From: Brendon Muir Date: Tue, 19 Mar 2024 10:49:17 +1300 Subject: [PATCH] Introduce Advisory Lock - Remove column_default method (inline) - Shift handle_ranking call to `ranks` method --- lib/ranked-model.rb | 27 +++++----- lib/ranked-model/advisory_lock.rb | 82 +++++++++++++++++++++++++++++++ 2 files changed, 97 insertions(+), 12 deletions(-) create mode 100644 lib/ranked-model/advisory_lock.rb diff --git a/lib/ranked-model.rb b/lib/ranked-model.rb index 1eee1e6..830b38f 100644 --- a/lib/ranked-model.rb +++ b/lib/ranked-model.rb @@ -1,4 +1,5 @@ require File.dirname(__FILE__)+'/ranked-model/ranker' +require File.dirname(__FILE__)+'/ranked-model/advisory_lock' require File.dirname(__FILE__)+'/ranked-model/railtie' if defined?(Rails::Railtie) module RankedModel @@ -17,8 +18,6 @@ def self.included base extend RankedModel::ClassMethods - before_save :handle_ranking - scope :rank, lambda { |name| reorder ranker(name.to_sym).column } @@ -28,12 +27,6 @@ def self.included base private - def handle_ranking - self.class.rankers.each do |ranker| - ranker.with(self).handle_ranking - end - end - module ClassMethods def ranker name @@ -48,7 +41,7 @@ def ranks *args self.rankers ||= [] ranker = RankedModel::Ranker.new(*args) - if column_default(ranker) + if ActiveRecord::Base.connected? && table_exists? && column_defaults[ranker.name.to_s] raise NonNilColumnDefault, %Q{Your ranked model column "#{ranker.name}" must not have a default value in the database.} end @@ -66,10 +59,20 @@ def ranks *args end public "#{ranker.name}_position", "#{ranker.name}_position=" - end - def column_default ranker - column_defaults[ranker.name.to_s] if ActiveRecord::Base.connected? && table_exists? + if ActiveRecord::Base.connected? && table_exists? + advisory_lock = AdvisoryLock.new(base_class, ranker.column) + before_create advisory_lock + before_update advisory_lock + before_destroy advisory_lock + end + + before_save { ranker.with(self).handle_ranking } + + if ActiveRecord::Base.connected? && table_exists? && local_variables.include?(:advisory_lock) + after_commit advisory_lock + after_rollback advisory_lock + end end end diff --git a/lib/ranked-model/advisory_lock.rb b/lib/ranked-model/advisory_lock.rb new file mode 100644 index 0000000..e8e6d96 --- /dev/null +++ b/lib/ranked-model/advisory_lock.rb @@ -0,0 +1,82 @@ +require "fileutils" +require "openssl" + +module RankedModel + class AdvisoryLock + Adapter = Struct.new(:initialise, :aquire, :release, keyword_init: true) + + attr_reader :base_class + + def initialize(base_class, column) + @base_class = base_class + @column = column.to_s + + @adapters = { + "Mysql2" => Adapter.new( + initialise: -> {}, + aquire: -> { connection.execute "SELECT GET_LOCK(#{connection.quote(lock_name)}, -1)" }, + release: -> { connection.execute "SELECT RELEASE_LOCK(#{connection.quote(lock_name)})" } + ), + "PostgreSQL" => Adapter.new( + initialise: -> {}, + aquire: -> { connection.execute "SELECT pg_advisory_lock(#{lock_name.hex & 0x7FFFFFFFFFFFFFFF})" }, + release: -> { connection.execute "SELECT pg_advisory_unlock(#{lock_name.hex & 0x7FFFFFFFFFFFFFFF})" } + ), + "SQLite" => Adapter.new( + initialise: -> { + FileUtils.mkdir_p "#{Dir.pwd}/tmp" + filename = "#{Dir.pwd}/tmp/#{lock_name}.lock" + @file ||= File.open filename, File::RDWR | File::CREAT, 0o644 + }, + aquire: -> { + @file.flock File::LOCK_EX + }, + release: -> { + @file.flock File::LOCK_UN + } + ) + } + + @adapters.default = Adapter.new(initialise: -> {}, aquire: -> {}, release: -> {}) + + adapter.initialise.call + end + + def aquire(record) + adapter.aquire.call + end + + def release(record) + adapter.release.call + end + + alias_method :before_create, :aquire + alias_method :before_update, :aquire + alias_method :before_destroy, :aquire + alias_method :after_commit, :release + alias_method :after_rollback, :release + + private + + def connection + base_class.connection + end + + def adapter_name + connection.adapter_name + end + + def adapter + @adapters[adapter_name] + end + + def lock_name + lock_name = ["ranked-model"] + lock_name << connection.current_database if connection.respond_to?(:current_database) + lock_name << base_class.table_name + lock_name << @column + + OpenSSL::Digest::MD5.hexdigest(lock_name.join("."))[0...32] + end + end +end