Skip to content

Instantly share code, notes, and snippets.

@Zooip
Created November 13, 2017 15:47
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Zooip/493839eb396fcc96d9b0fc004349753e to your computer and use it in GitHub Desktop.
Save Zooip/493839eb396fcc96d9b0fc004349753e to your computer and use it in GitHub Desktop.
Mongoid lock system
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
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