lib/mongoid/locker.rb in mongoid-locker-0.2.1 vs lib/mongoid/locker.rb in mongoid-locker-0.3.0

- old
+ new

@@ -1,9 +1,12 @@ require File.expand_path(File.join(File.dirname(__FILE__), 'locker', 'wrapper')) module Mongoid module Locker + # Error thrown if document could not be successfully locked. + class LockError < Exception; end + module ClassMethods # A scope to retrieve all locked documents in the collection. # # @return [Mongoid::Criteria] def locked @@ -12,18 +15,18 @@ # A scope to retrieve all unlocked documents in the collection. # # @return [Mongoid::Criteria] def unlocked - any_of({:locked_until => nil}, {:locked_until.lte => Time.now}) + any_of({ locked_until: nil }, { :locked_until.lte => Time.now }) end # Set the default lock timeout for this class. Note this only applies to new locks. Defaults to five seconds. # # @param [Fixnum] new_time the default number of seconds until a lock is considered "expired", in seconds # @return [void] - def timeout_lock_after new_time + def timeout_lock_after(new_time) @lock_timeout = new_time end # Retrieve the lock timeout default for this class. # @@ -33,122 +36,132 @@ @lock_timeout || 5 end end # @api private - def self.included mod + def self.included(mod) mod.extend ClassMethods - mod.field :locked_at, :type => Time - mod.field :locked_until, :type => Time + mod.field :locked_at, type: Time + mod.field :locked_until, type: Time end - # Returns whether the document is currently locked or not. # # @return [Boolean] true if locked, false otherwise def locked? - !!(self.locked_until && self.locked_until > Time.now) + !!(locked_until && locked_until > Time.now) end # Returns whether the current instance has the lock or not. # # @return [Boolean] true if locked, false otherwise def has_lock? - @has_lock && self.locked? + !!(@has_lock && self.locked?) end # Primary method of plugin: execute the provided code once the document has been successfully locked. # # @param [Hash] opts for the locking mechanism # @option opts [Fixnum] :timeout The number of seconds until the lock is considered "expired" - defaults to the {ClassMethods#lock_timeout} - # @option opts [Boolean] :wait If the document is currently locked, wait until the lock expires and try again + # @option opts [Fixnum] :retries If the document is currently locked, the number of times to retry. Defaults to 0 (note: setting this to 1 is the equivalent of using :wait => true) + # @option opts [Float] :retry_sleep How long to sleep between attempts to acquire lock - defaults to time left until lock is available + # @option opts [Boolean] :wait If the document is currently locked, wait until the lock expires and try again - defaults to false. If set, :retries will be ignored + # @option opts [Boolean] :reload After acquiring the lock, reload the document - defaults to true # @return [void] - def with_lock opts={}, &block - # don't try to re-lock/unlock on recursive calls - had_lock = self.has_lock? - self.lock(opts) unless had_lock + def with_lock(opts = {}) + have_lock = self.has_lock? + unless have_lock + opts[:retries] = 1 if opts[:wait] + lock(opts) + end + begin yield ensure - self.unlock unless had_lock + unlock unless have_lock end end - protected - def lock opts={} + def acquire_lock(opts = {}) time = Time.now timeout = opts[:timeout] || self.class.lock_timeout expiration = time + timeout # lock the document atomically in the DB without persisting entire doc locked = Mongoid::Locker::Wrapper.update( self.class, { - :_id => self.id, + :_id => id, '$or' => [ # not locked - {:locked_until => nil}, + { locked_until: nil }, # expired - {:locked_until => {'$lte' => time}} + { locked_until: { '$lte' => time } } ] }, - { - '$set' => { - :locked_at => time, - :locked_until => expiration - } + + '$set' => { + locked_at: time, + locked_until: expiration } + ) if locked # document successfully updated, meaning it was locked self.locked_at = time self.locked_until = expiration + reload unless opts[:reload] == false @has_lock = true else - # couldn't grab lock + @has_lock = false + end + end - if opts[:wait] && locked_until = Mongoid::Locker::Wrapper.locked_until(self) - # doc is locked - wait until it expires - wait_time = locked_until - Time.now - sleep wait_time if wait_time > 0 + def lock(opts = {}) + opts = { retries: 0 }.merge(opts) - # only wait once - opts.dup - opts.delete :wait + attempts_left = opts[:retries] + 1 + retry_sleep = opts[:retry_sleep] - # reload to update with any new values - self.reload - # retry lock grab - self.lock opts + loop do + return if acquire_lock(opts) + + attempts_left -= 1 + + if attempts_left > 0 + # if not passed a retry_sleep value, we sleep for the remaining life of the lock + unless opts[:retry_sleep] + locked_until = Mongoid::Locker::Wrapper.locked_until(self) + retry_sleep = locked_until - Time.now + end + + sleep retry_sleep if retry_sleep > 0 else - raise LockError.new("could not get lock") + fail LockError.new('could not get lock') end end end def unlock # unlock the document in the DB without persisting entire doc Mongoid::Locker::Wrapper.update( self.class, - {:_id => self.id}, - { - '$set' => { - :locked_at => nil, - :locked_until => nil, - } + { _id: id }, + + '$set' => { + locked_at: nil, + locked_until: nil } + ) self.locked_at = nil self.locked_until = nil @has_lock = false end end - - # Error thrown if document could not be successfully locked. - class LockError < Exception; end end