class SynCache::Cache

Constants

LOCK_SLEEP

a float number of seconds to sleep when a race condition is detected (actual delay is randomized to avoid live lock situation)

Attributes

debug[RW]

set to true to report every single cache operation

Public Class Methods

new(ttl = 60*60, max_size = 5000, flush_delay = nil) click to toggle source

ttl (time to live) is time in seconds from the last access until cache entry is expired (set to nil to disable time limit)

max_size is max number of objects in cache

flush_delay is used to rate-limit flush operations: if less than that number of seconds has passed since last flush, next flush will be delayed; default is no rate limit

# File lib/syncache/syncache.rb, line 80
def initialize(ttl = 60*60, max_size = 5000, flush_delay = nil)
  @ttl = ttl
  @max_size = max_size
  @debug = false

  if @flush_delay = flush_delay
    @last_flush = Time.now
  end

  @sync = Mutex.new
  @cache = {}
end

Public Instance Methods

[](key) click to toggle source

retrieve value from cache if it's still fresh

see also Cache#fetch_or_add

# File lib/syncache/syncache.rb, line 152
def [](key)
  debug { '[] ' << key.to_s }

  entry = get_locked_entry(key, false)
  unless entry.nil?
    begin
      return entry.value
    ensure
      entry.sync.unlock
    end
  end
end
[]=(key, value) click to toggle source

store new value in cache

see also Cache#fetch_or_add

# File lib/syncache/syncache.rb, line 137
def []=(key, value)
  debug { '[]= ' << key.to_s }

  entry = get_locked_entry(key)
  begin
    return entry.value = value
  ensure
    entry.sync.unlock
  end
end
delete(key) click to toggle source

remove single value from cache

# File lib/syncache/syncache.rb, line 125
def delete(key)
  debug { 'delete ' << key.to_s }

  @sync.synchronize do
    @cache.delete(key)
  end
end
fetch_or_add(key) { || ... } click to toggle source

initialize missing cache entry from supplied block

this is the preferred method of adding values to the cache as it locks the key for the duration of computation of the supplied block to prevent parallel execution of resource-intensive actions

# File lib/syncache/syncache.rb, line 171
def fetch_or_add(key)
  debug { 'fetch_or_add ' << key.to_s }

  entry = get_locked_entry(key)
  begin
    if entry.value.nil?
      entry.value = yield
    end
    return entry.value
  ensure
    entry.sync.unlock
  end
end
flush(base = nil) click to toggle source

remove all values from cache

if base is given, only values with keys matching the base (using === operator) are removed

# File lib/syncache/syncache.rb, line 102
def flush(base = nil)
  debug { 'flush ' << base.to_s }

  @sync.synchronize do

    if @flush_delay
      next_flush = @last_flush + @flush_delay

      if next_flush > Time.now
        flush_at(next_flush, base)
      else
        flush_now(base)
        @last_flush = Time.now
      end

    else
      flush_now(base)
    end
  end
end

Private Instance Methods

add_blank_entry(key) click to toggle source
# File lib/syncache/syncache.rb, line 210
def add_blank_entry(key)
  @sync.locked? or raise CacheError,
    'add_entry called while @sync is not locked'

  had_same_key = @cache.has_key?(key)
  entry = @cache[key] = CacheEntry.new(@ttl)
  check_size unless had_same_key
  entry
end
check_size() click to toggle source

remove oldest item from cache if size limit reached

# File lib/syncache/syncache.rb, line 249
def check_size
  debug { 'check_size' }

  return unless @max_size.kind_of? Numeric

  if @sync.locked?
    check_size_internal
  else
    @sync.synchronize { check_size_internal }
  end
end
check_size_internal() click to toggle source
# File lib/syncache/syncache.rb, line 261
def check_size_internal
  while @cache.size > @max_size do
    # optimize: supplement hash with queue
    oldest = @cache.keys.min {|a, b| @cache[a].replacement_index <=> @cache[b].replacement_index }
    @cache.delete(oldest)
  end
end
flush_at(next_flush, base = nil) click to toggle source

delayed flush (ensure all entries matching base expire no later than next_flush)

must be run from inside global lock, see flush

# File lib/syncache/syncache.rb, line 203
def flush_at(next_flush, base = nil)
  @cache.each do |key, entry|
    next if base and not base === key
    entry.expire_at(next_flush)
  end
end
flush_now(base = nil) click to toggle source

immediate flush (delete all entries matching base)

must be run from inside global lock, see flush

# File lib/syncache/syncache.rb, line 191
def flush_now(base = nil)
  if base
    @cache.delete_if {|key, entry| base === key }
  else
    @cache = {}
  end
end
get_locked_entry(key, add_if_missing=true) click to toggle source
# File lib/syncache/syncache.rb, line 220
def get_locked_entry(key, add_if_missing=true)
  debug { "get_locked_entry #{key}, #{add_if_missing}" }

  entry = nil   # scope fix
  entry_locked = false
  until entry_locked do
    @sync.synchronize do
      entry = @cache[key]

      if entry.nil? or entry.stale?
        if add_if_missing
          entry = add_blank_entry(key)
        else
          @cache.delete(key) unless entry.nil?
          return nil
        end
      end

      entry_locked = entry.sync.try_lock
    end
    sleep(rand * LOCK_SLEEP) unless entry_locked
  end

  entry.record_access
  entry
end