class LockAndCache::Action

@private

Constants

ERROR_MAGIC_KEY
NIL

Attributes

blk[R]
key[R]
options[R]

Public Class Methods

new(key, options, blk) click to toggle source
# File lib/lock_and_cache/action.rb, line 10
def initialize(key, options, blk)
  raise "need a block" unless blk
  @key = key
  @options = options.stringify_keys
  @blk = blk
end

Public Instance Methods

cache_storage() click to toggle source
# File lib/lock_and_cache/action.rb, line 39
def cache_storage
  @cache_storage ||= LockAndCache.cache_storage or raise("must set LockAndCache.cache_storage=[Redis]")
end
digest() click to toggle source
# File lib/lock_and_cache/action.rb, line 27
def digest
  @digest ||= key.digest
end
expires() click to toggle source
# File lib/lock_and_cache/action.rb, line 17
def expires
  return @expires if defined?(@expires)
  @expires = options.has_key?('expires') ? options['expires'].to_f.round : nil
end
load_existing(existing) click to toggle source
# File lib/lock_and_cache/action.rb, line 43
def load_existing(existing)
  v = ::Marshal.load(existing)
  if v.is_a?(::Hash) and (founderr = v[ERROR_MAGIC_KEY])
    raise "Another LockAndCache process raised #{founderr}"
  else
    v
  end
end
lock_digest() click to toggle source
# File lib/lock_and_cache/action.rb, line 31
def lock_digest
  @lock_digest ||= key.lock_digest
end
lock_storage() click to toggle source
# File lib/lock_and_cache/action.rb, line 35
def lock_storage
  @lock_storage ||= LockAndCache.lock_storage or raise("must set LockAndCache.lock_storage=[Redis]")
end
nil_expires() click to toggle source
# File lib/lock_and_cache/action.rb, line 22
def nil_expires
  return @nil_expires if defined?(@nil_expires)
  @nil_expires = options.has_key?('nil_expires') ? options['nil_expires'].to_f.round : nil
end
perform() click to toggle source
# File lib/lock_and_cache/action.rb, line 52
def perform
  max_lock_wait = options.fetch 'max_lock_wait', LockAndCache.max_lock_wait
  heartbeat_expires = options.fetch('heartbeat_expires', LockAndCache.heartbeat_expires).to_f.ceil
  raise "heartbeat_expires must be >= 2 seconds" unless heartbeat_expires >= 2
  heartbeat_frequency = (heartbeat_expires / 2).ceil
  LockAndCache.logger.debug { "[lock_and_cache] A1 #{key.debug} #{Base64.encode64(digest).strip} #{Digest::SHA1.hexdigest digest}" }
  if cache_storage.exists?(digest) and (existing = cache_storage.get(digest)).is_a?(String)
    return load_existing(existing)
  end
  LockAndCache.logger.debug { "[lock_and_cache] B1 #{key.debug} #{Base64.encode64(digest).strip} #{Digest::SHA1.hexdigest digest}" }
  retval = nil
  lock_secret = SecureRandom.hex 16
  acquired = false
  begin
    Timeout.timeout(max_lock_wait, TimeoutWaitingForLock) do
      until lock_storage.set(lock_digest, lock_secret, nx: true, ex: heartbeat_expires)
        LockAndCache.logger.debug { "[lock_and_cache] C1 #{key.debug} #{Base64.encode64(digest).strip} #{Digest::SHA1.hexdigest digest}" }
        sleep rand
      end
      acquired = true
    end
    LockAndCache.logger.debug { "[lock_and_cache] D1 #{key.debug} #{Base64.encode64(digest).strip} #{Digest::SHA1.hexdigest digest}" }
    if cache_storage.exists?(digest) and (existing = cache_storage.get(digest)).is_a?(String)
      LockAndCache.logger.debug { "[lock_and_cache] E1 #{key.debug} #{Base64.encode64(digest).strip} #{Digest::SHA1.hexdigest digest}" }
      retval = load_existing existing
    end
    unless retval
      LockAndCache.logger.debug { "[lock_and_cache] F1 #{key.debug} #{Base64.encode64(digest).strip} #{Digest::SHA1.hexdigest digest}" }
      done = false
      begin
        lock_extender = Thread.new do
          loop do
            LockAndCache.logger.debug { "[lock_and_cache] heartbeat1 #{key.debug} #{Base64.encode64(digest).strip} #{Digest::SHA1.hexdigest digest}" }
            break if done
            sleep heartbeat_frequency
            break if done
            LockAndCache.logger.debug { "[lock_and_cache] heartbeat2 #{key.debug} #{Base64.encode64(digest).strip} #{Digest::SHA1.hexdigest digest}" }
            # FIXME use lua to check the value
            raise "unexpectedly lost lock for #{key.debug}" unless lock_storage.get(lock_digest) == lock_secret
            lock_storage.set lock_digest, lock_secret, xx: true, ex: heartbeat_expires
          end
        end
        begin
          retval = blk.call
          retval.nil? ? set_nil : set_non_nil(retval)
        rescue
          set_error $!
          raise
        end
      ensure
        done = true
        lock_extender.join if lock_extender.status.nil?
      end
    end
  ensure
    lock_storage.del lock_digest if acquired
  end
  retval
end
set_error(exception) click to toggle source
# File lib/lock_and_cache/action.rb, line 112
def set_error(exception)
  cache_storage.set digest, ::Marshal.dump(ERROR_MAGIC_KEY => exception.message), ex: 1
end
set_nil() click to toggle source
# File lib/lock_and_cache/action.rb, line 117
def set_nil
  if nil_expires
    cache_storage.set digest, NIL, ex: nil_expires
  elsif expires
    cache_storage.set digest, NIL, ex: expires
  else
    cache_storage.set digest, NIL
  end
end
set_non_nil(retval) click to toggle source
# File lib/lock_and_cache/action.rb, line 127
def set_non_nil(retval)
  raise "expected not null #{retval.inspect}" if retval.nil?
  if expires
    cache_storage.set digest, ::Marshal.dump(retval), ex: expires
  else
    cache_storage.set digest, ::Marshal.dump(retval)
  end
end