Class: SupportTableCache::MemoryCache

Inherits:
Object
  • Object
show all
Defined in:
lib/support_table_cache/memory_cache.rb

Overview

An optimized cache implementation that can be used when all records can easily fit in memory and are never changed. It is intended for use with small, static support tables only.

This cache will not store nil values. This is to prevent the cache from filling up with cache misses because there is no purging mechanism.

Instance Method Summary collapse

Constructor Details

#initializeSupportTableCache::MemoryCache

Create a new memory cache.



14
15
16
17
18
19
20
21
# File 'lib/support_table_cache/memory_cache.rb', line 14

def initialize
  @cache = {}
  @mutex = Mutex.new
  # Maps a cache key to the tokens of the fetches currently generating a value for it.
  # Invalidating a key drops its tokens so that those fetches will not store the stale
  # value they generated. Only keys with a fetch in flight are tracked.
  @pending = {}
end

Instance Method Details

#clearvoid

This method returns an undefined value.

Clear all values from the cache.



124
125
126
127
128
129
130
# File 'lib/support_table_cache/memory_cache.rb', line 124

def clear
  @mutex.synchronize do
    @pending.clear
    @cache.clear
  end
  nil
end

#delete(key) ⇒ void

This method returns an undefined value.

Delete a value from the cache.

Parameters:

  • key (Object)

    The cache key.



113
114
115
116
117
118
119
# File 'lib/support_table_cache/memory_cache.rb', line 113

def delete(key)
  @mutex.synchronize do
    @pending.delete(key)
    @cache.delete(key)
  end
  nil
end

#fetch(key, expires_in: nil) { ... } ⇒ Object?

Fetch a value from the cache. If the key is not found or has expired, yields to get a new value.

Parameters:

  • key (Object)

    The cache key.

  • expires_in (Integer, nil) (defaults to: nil)

    Time in seconds until the cached value expires.

Yields:

  • Block to execute to get a new value if the key is not cached.

Returns:

  • (Object, nil)

    The cached value or the result of the block, or nil if no value is found.



29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
# File 'lib/support_table_cache/memory_cache.rb', line 29

def fetch(key, expires_in: nil)
  serialized_value = nil
  token = nil
  @mutex.synchronize do
    cached_value, cached_expire_at = @cache[key]
    if cached_expire_at && cached_expire_at < Process.clock_gettime(Process::CLOCK_MONOTONIC)
      @cache.delete(key)
    else
      serialized_value = cached_value
    end

    if serialized_value.nil? && block_given?
      token = Object.new
      (@pending[key] ||= []) << token
    end
  end

  if serialized_value.nil?
    begin
      value = yield if block_given?
      return nil if value.nil?

      serialized_value = Marshal.dump(value)
      # The expiration is always recalculated from the expires_in argument so that replacing
      # an expired entry without an expiration does not carry over the old expiration time.
      expire_at = (Process.clock_gettime(Process::CLOCK_MONOTONIC) + expires_in if expires_in)

      @mutex.synchronize do
        # Only store the value if this key was not invalidated while the value was being
        # generated. Otherwise a record deleted or overwritten by a concurrent update could
        # be resurrected in the cache with stale data.
        @cache[key] = [serialized_value, expire_at] if @pending[key]&.include?(token)
      end
    ensure
      @mutex.synchronize do
        tokens = @pending[key]
        if tokens
          tokens.delete(token)
          @pending.delete(key) if tokens.empty?
        end
      end
    end
  end

  Marshal.load(serialized_value)
end

#read(key) ⇒ Object?

Read a value from the cache.

Parameters:

  • key (Object)

    The cache key.

Returns:

  • (Object, nil)

    The cached value or nil if not found.



80
81
82
# File 'lib/support_table_cache/memory_cache.rb', line 80

def read(key)
  fetch(key)
end

#write(key, value, expires_in: nil) ⇒ void

This method returns an undefined value.

Write a value to the cache.

Parameters:

  • key (Object)

    The cache key.

  • value (Object)

    The value to cache. Nil values are not cached.

  • expires_in (Integer, nil) (defaults to: nil)

    Time in seconds until the cached value expires.



90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
# File 'lib/support_table_cache/memory_cache.rb', line 90

def write(key, value, expires_in: nil)
  return if value.nil?

  if expires_in
    expire_at = Process.clock_gettime(Process::CLOCK_MONOTONIC) + expires_in
  end

  serialized_value = Marshal.dump(value)

  @mutex.synchronize do
    # Discard any fetch generating a value for this key so that it cannot overwrite the
    # newer value being written here.
    @pending.delete(key)
    @cache[key] = [serialized_value, expire_at]
  end

  nil
end