module BatchKit::Lockable

Defines lockable behaviour, which can be added to any batch process. This behavior allows a process to define a named lock that it needs exclusively during execution. When the process is about to be executed, it will first attempt to obtain the named lock. If it is successful, execution will proceed as normal, and on completion of processing (whether succesful or otherwise), the lock will be released. If the lock is already held by another process, the requesting process will block and wait for the lock to become available. The process will only wait as long as lock_wait_timeout; if the lock has not become availabe in that time period, a LockTimeout exception will be thrown, and processing will not take place.

Public Instance Methods

lock(lock_name, lock_timeout, lock_wait_timeout = nil) click to toggle source

Attempts to obtain the named lock lock_name. If the lock is already held by another process, this method blocks until one of the following occurs:

  • the lock is released by the process that currently holds it

  • the lock expires, by reaching it's timeout period

  • the lock_wait_timeout period is reached.

Lock management is managed via the event publishing system; subscribers to the 'lock?' event indicate whether a lock is available by their response to the event. A value of false indicates the lock is currently held; a response of true indicates the lock has been granted.

@param lock_name [String] The name of the lock that is needed. @param lock_timeout [Fixnum] The maximum number of seconds that this

process can hold the requested lock before it times out (allowing
any other processes waiting on the lock to proceed). This value
should be set high enough that the lock does not timeout while
processing that relies on the lock is not still running.

@param lock_wait_timeout [Fixnum] The maximum time this process is

prepared to wait for the lock to become available. If not specified,
the wait will timeout after the same amount of time as +lock_timeout+.

@raise Timeout::Error If the lock is not obtained within

+lock_wait_timeout+ seconds.
# File lib/batch-kit/lockable.rb, line 44
def lock(lock_name, lock_timeout, lock_wait_timeout = nil)
    unless lock_timeout && lock_timeout.is_a?(Fixnum) && lock_timeout > 0
        raise ArgumentError, "Invalid lock_timeout; must be > 0"
    end
    unless lock_wait_timeout.nil? || (lock_wait_timeout.is_a?(Fixnum) && lock_wait_timeout >= 0)
        raise ArgumentError, "Invalid lock_wait_timeout; must be nil or >= 0"
    end
    unless Events.has_subscribers?(self, 'lock?')
        if self.respond_to?(:log)
            log.warn "No lock manager available; proceeding without locking"
        end
        return
    end
    lock_wait_timeout ||= lock_timeout
    lock_expire_time = nil
    wait_expire_time = Time.now + lock_wait_timeout
    if lock_wait_timeout > 0
        # Loop waiting for lock if not available
        begin
            Timeout.timeout(lock_wait_timeout) do
                i = 0
                loop do
                    lock_holder = {}
                    lock_expire_time = Events.publish(self, 'lock?', lock_name,
                                                             lock_timeout, lock_holder)
                    break if lock_expire_time
                    if i == 0
                        Events.publish(self, 'lock_held', lock_name,
                                              lock_holder[:lock_holder],
                                              lock_holder[:lock_expires_at])
                        Events.publish(self, 'lock_wait', lock_name, wait_expire_time)
                    end
                    sleep 1
                    i += 1
                end
                Events.publish(self, 'locked', lock_name, lock_expire_time)
            end
        rescue Timeout::Error
            Events.publish(self, 'lock_wait_timeout', lock_name, wait_expire_time)
            raise Timeout::Error, "Timed out waiting for lock '#{lock_name}' to become available"
        end
    else
        # No waiting for lock to become free
        lock_holder = {}
        if lock_expire_time = Events.publish(self, 'lock?', lock_name, lock_timeout, lock_holder)
            Events.publish(self, 'locked', lock_name, lock_expire_time)
        else
            Events.publish(self, 'lock_held', lock_name,
                                  lock_holder[:lock_holder], lock_holder[:lock_expires_at])
            Events.publish(self, 'lock_wait_timeout', lock_name, wait_expire_time)
            raise Timeout::Error, "Lock '#{lock_name}' is already in use"
        end
    end
end
unlock(lock_name) click to toggle source

Release a lock held by this object.

@param lock_name [String] The name of the lock to be released.

# File lib/batch-kit/lockable.rb, line 103
def unlock(lock_name)
    unless Events.has_subscribers?(self, 'unlock?')
        return
    end
    if Events.publish(self, 'unlock?', lock_name)
        Events.publish(self, 'unlocked', lock_name)
    end
end
with_lock(lock_name, lock_timeout, lock_wait_timeout = nil) { || ... } click to toggle source

Obtains the requested lock_name, then yields to the supplied block. Ensures the lock is released when the block ends or raises an error.

@param lock_name [String] The name of the lock to obtain. @param lock_timeout [Fixnum] The maximum number of seconds that this

process can hold the requested lock before it times out (allowing
any other processes waiting on the lock to proceed). This value
should be set high enough that the lock does not timeout while
processing that relies on the lock is not still running.

@param lock_wait_timeout [Fixnum] The maximum time this process is

prepared to wait for the lock to become available. If not specified,
the wait will timeout after the same amount of time as +lock_timeout+.

@raise Timeout::Error If the lock is not obtained within

+lock_wait_timeout+ seconds.
# File lib/batch-kit/lockable.rb, line 127
def with_lock(lock_name, lock_timeout, lock_wait_timeout = nil)
    self.lock(lock_name, lock_timeout, lock_wait_timeout)
    begin
        yield
    ensure
        self.unlock(lock_name)
    end
end