Created
November 13, 2017 15:47
-
-
Save Zooip/493839eb396fcc96d9b0fc004349753e to your computer and use it in GitHub Desktop.
Mongoid lock system
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
class Lock | |
include Mongoid::Document | |
include Mongoid::Timestamps | |
include Mongoid::EmbeddedFindable | |
include GlobalID::Identification | |
DEFAULT_INTERFACE = :default | |
embedded_in :lockable, polymorphic: true | |
def self.find(id) | |
Lockable::Children.included_in.each do |klass| | |
res=find_through(klass, 'lock', id) | |
return res if res | |
end | |
end | |
DEFAULT_TTL=30.minutes | |
belongs_to :user | |
field :interface, type: Symbol, default: DEFAULT_INTERFACE | |
field :expires_at, type: Time, default: -> { Time.now+DEFAULT_TTL } | |
# A user setted id for interfaces needing their own lock id. Default to random UUID | |
field :external_id, type: String, default: -> { SecureRandom.uuid } | |
# Hash serialisation of ActiveJobs that need to be requeued when this lock is released | |
field :after_release_serialized_jobs, type: Array, default: [] | |
after_create :schedule_expiration_job | |
def release | |
lockable.try(:perform_callbacks, :before_unlock) | |
Rails.logger.debug "Destroy lock #{self.id}" | |
self.destroy | |
lockable.try(:perform_callbacks, :after_unlock) | |
self.trigger_after_release_jobs | |
end | |
def schedule_expiration_job | |
if expires_at && expires_at>Time.now | |
Rails.logger.debug "Schedule expiration job of lock #{self.id} at #{expires_at}" | |
ExpireLockJob.set(wait_until: expires_at).perform_later(self.id.to_s) | |
else | |
Rails.logger.debug "No expiration date for lock #{self.id}" | |
end | |
end | |
def enqueue_job_after_release(job) | |
add_to_set(:after_release_serialized_jobs => job.serialize) | |
end | |
def trigger_after_release_jobs | |
begin | |
until self.after_release_serialized_jobs.to_a.empty? | |
seria_job = self.after_release_serialized_jobs.shift | |
ActiveJob::Base.deserialize(seria_job).retry_job | |
end | |
ensure | |
self.save if self.persisted? | |
end | |
true | |
end | |
def matches?(attrs) | |
conditions={} | |
if attrs[:user]||attrs[:user_id] | |
u =attrs[:user]||User.find(attrs[:user_id]) | |
conditions[:user]=(u==self.user&&self.interface==(attrs[:interface]||Lock::DEFAULT_INTERFACE)) | |
end | |
conditions[:id] =attrs[:id]==self.id if attrs[:id] | |
conditions[:external_id]=attrs[:external_id]==self.external_id if attrs[:external_id] | |
conditions.any?&&conditions.all? { |_k, v| v } | |
end | |
end |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
module Lockable | |
extend ActiveSupport::Concern | |
# keep track of what classes have included this concern: | |
module Children | |
extend self | |
@included_in ||= [] | |
def add(klass) | |
@included_in << klass | |
end | |
def included_in | |
@included_in | |
end | |
end | |
included do | |
Children.add self | |
embeds_one :lock, as: :lockable | |
index({'lock.external_id' => 1}, {unique: true}) | |
def create_lock(attrs={}) | |
return false if self.locked? | |
lock_attrs ={ | |
lockable: self, | |
interface: attrs[:interface]||self.class.get_locking_default_interface | |
} | |
lock_attrs[:user] =attrs[:user] if attrs[:user] | |
lock_attrs[:external_id]=attrs[:external_id] if attrs[:external_id] | |
if attrs[:expires_at] | |
lock_attrs[:expires_at]=attrs[:expires_at] | |
elsif attrs[:ttl] | |
lock_attrs[:expires_at]=Time.now+attrs[:ttl] | |
end | |
perform_callbacks(:before_lock) | |
self.lock=Lock.create(lock_attrs) | |
perform_callbacks(:after_lock) | |
self.lock | |
end | |
def locked? | |
lock.present? | |
end | |
def locked_by?(lock_attrs) | |
locked?&&lock.matches(lock_attrs) | |
end | |
def unlock(verify_lock: nil) | |
if locked? | |
return false if verify_lock&&!locked_by?(verify_lock) | |
self.lock.release | |
true | |
else | |
true | |
end | |
end | |
def with_lock(attrs={}, create_lock: true) | |
if lock&.matches?(attrs)||new_lock=(create_lock&&create_lock(attrs)) | |
begin | |
yield | |
ensure | |
new_lock.release if new_lock | |
end | |
else | |
false | |
end | |
end | |
def self.locking_default_interface(value) | |
@locking_default_interface=value | |
end | |
def self.get_locking_default_interface | |
@locking_default_interface||Lock::DEFAULT_INTERFACE | |
end | |
def self.locking_callbacks | |
@locking_callbacks||=Hash.new { |h, k| h[k] = [] } | |
end | |
def self.before_lock(method_name=nil, **opts, &block) | |
locking_callbacks[:before_lock]<<(block_given? ? block : method_name) | |
end | |
def self.after_lock(method_name=nil, **opts, &block) | |
locking_callbacks[:after_lock]<<(block_given? ? block : method_name) | |
end | |
def self.before_unlock(method_name=nil, **opts, &block) | |
locking_callbacks[:before_unlock]<<(block_given? ? block : method_name) | |
end | |
def self.after_unlock(method_name=nil, **opts, &block) | |
locking_callbacks[:after_unlock]<<(block_given? ? block : method_name) | |
end | |
def perform_callbacks(callback_group) | |
Rails.logger.debug "Perform locking callback group : #{callback_group}" | |
self.class.locking_callbacks[callback_group].to_a.each do |callback| | |
if callback.is_a?(Proc) | |
instance_eval(&callback) | |
else | |
self.send(callback) | |
end | |
end | |
end | |
end | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment