Class: Zache

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

Overview

It is a very simple thread-safe in-memory cache with an ability to expire keys automatically, when their lifetime is over. Use it like this:

require 'zache'
zache = Zache.new
# Expires in 5 minutes
v = zache.get(:count, lifetime: 5 * 60) { expensive() }

For more information read README file.

Author

Yegor Bugayenko ([email protected])

Copyright

Copyright © 2018-2025 Yegor Bugayenko

License

MIT

Defined Under Namespace

Classes: Fake

Instance Method Summary collapse

Constructor Details

#initialize(sync: true, dirty: false) ⇒ Zache

Makes a new object of the cache.

“sync” is whether the hash is thread-safe (‘true`) or not (`false`); it is recommended to leave this parameter untouched, unless you really know what you are doing.

If the dirty argument is set to true, a previously calculated result will be returned if it exists, even if it is already expired.

Parameters:

  • sync (Boolean) (defaults to: true)

    Whether the hash is thread-safe

  • dirty (Boolean) (defaults to: false)

    Whether to return expired values



81
82
83
84
85
86
87
# File 'lib/zache.rb', line 81

def initialize(sync: true, dirty: false)
  @hash = {}
  @sync = sync
  @dirty = dirty
  @mutex = Mutex.new
  @locks = {}
end

Instance Method Details

#cleanInteger

Remove keys that are expired. This cleans up the cache by removing all keys where the lifetime has been exceeded.

Returns:

  • (Integer)

    Number of keys removed



250
251
252
253
254
255
256
# File 'lib/zache.rb', line 250

def clean
  synchronize_all do
    size_before = @hash.size
    @hash.delete_if { |key, _value| expired_unsafe?(key) }
    size_before - @hash.size
  end
end

#empty?Boolean

Returns TRUE if the cache is empty, FALSE otherwise.

Returns:

  • (Boolean)

    True if the cache is empty



261
262
263
# File 'lib/zache.rb', line 261

def empty?
  synchronize_all { @hash.empty? }
end

#exists?(key, dirty: false) ⇒ Boolean

Checks whether the value exists in the cache by the provided key. Returns TRUE if the value is here. If the key is already expired in the cache, it will be removed by this method and the result will be FALSE.

Parameters:

  • key (Object)

    The key to check in the cache

  • dirty (Boolean) (defaults to: false)

    Whether to consider expired values as existing

Returns:

  • (Boolean)

    True if the key exists and is not expired (unless dirty is true)



155
156
157
158
159
160
161
162
163
164
# File 'lib/zache.rb', line 155

def exists?(key, dirty: false)
  synchronize_all do
    rec = @hash[key]
    if expired_unsafe?(key) && !dirty && !@dirty
      @hash.delete(key)
      rec = nil
    end
    !rec.nil?
  end
end

#expired?(key) ⇒ Boolean

Checks whether the key exists in the cache and is expired. If the key is absent FALSE is returned.

Parameters:

  • key (Object)

    The key to check in the cache

Returns:

  • (Boolean)

    True if the key exists and is expired



171
172
173
# File 'lib/zache.rb', line 171

def expired?(key)
  synchronize_all { expired_unsafe?(key) }
end

#get(key, lifetime: 2**32, dirty: false, placeholder: nil, eager: false) { ... } ⇒ Object

Gets the value from the cache by the provided key.

If the value is not found in the cache, it will be calculated via the provided block. If the block is not given and the key doesn’t exist or is expired, an exception will be raised (unless dirty is set to true). The lifetime must be in seconds. The default lifetime is huge, which means that the key will never be expired.

If the dirty argument is set to true, a previously calculated result will be returned if it exists, even if it is already expired.

Parameters:

  • key (Object)

    The key to retrieve from the cache

  • lifetime (Integer) (defaults to: 2**32)

    Time in seconds until the key expires

  • dirty (Boolean) (defaults to: false)

    Whether to return expired values

  • eager (Boolean) (defaults to: false)

    Whether to return placeholder while working?

  • placeholder (Object) (defaults to: nil)

    The placeholder to return in eager mode

Yields:

  • Block to calculate the value if not in cache

Yield Returns:

  • (Object)

    The value to cache

Returns:

  • (Object)

    The cached value



116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
# File 'lib/zache.rb', line 116

def get(key, lifetime: 2**32, dirty: false, placeholder: nil, eager: false, &block)
  if block_given?
    if (dirty || @dirty) && locked?(key)
      synchronize_all do
        return @hash[key][:value] if expired_unsafe?(key) && @hash.key?(key)
      end
    end
    if eager
      has_key = synchronize_all { @hash.key?(key) }
      return synchronize_all { @hash[key][:value] } if has_key
      put(key, placeholder, lifetime: 0)
      Thread.new do
        synchronize_one(key) { calc(key, lifetime, &block) }
      end
      placeholder
    else
      synchronize_one(key) { calc(key, lifetime, &block) }
    end
  else
    synchronize_all do
      rec = @hash[key]
      if expired_unsafe?(key)
        return rec[:value] if dirty || @dirty
        @hash.delete(key)
        rec = nil
      end
      raise 'The key is absent in the cache' if rec.nil?
      rec[:value]
    end
  end
end

#locked?(key) ⇒ Boolean

Is key currently locked doing something?

Parameters:

  • key (Object)

    The key to check

Returns:

  • (Boolean)

    True if the cache is locked



191
192
193
# File 'lib/zache.rb', line 191

def locked?(key)
  synchronize_all { @locks[key]&.locked? }
end

#mtime(key) ⇒ Time

Returns the modification time of the key, if it exists. If not, current time is returned.

Parameters:

  • key (Object)

    The key to get the modification time for

Returns:

  • (Time)

    The modification time of the key or current time if key doesn’t exist



180
181
182
183
184
185
# File 'lib/zache.rb', line 180

def mtime(key)
  synchronize_all do
    rec = @hash[key]
    rec.nil? ? Time.now : rec[:start]
  end
end

#put(key, value, lifetime: 2**32) ⇒ Object

Put a value into the cache.

Parameters:

  • key (Object)

    The key to store the value under

  • value (Object)

    The value to store in the cache

  • lifetime (Integer) (defaults to: 2**32)

    Time in seconds until the key expires (default: never expires)

Returns:

  • (Object)

    The value stored



201
202
203
204
205
206
207
208
209
# File 'lib/zache.rb', line 201

def put(key, value, lifetime: 2**32)
  synchronize_one(key) do
    @hash[key] = {
      value: value,
      start: Time.now,
      lifetime: lifetime
    }
  end
end

#remove(key) { ... } ⇒ Object

Removes the value from the cache, by the provided key. If the key is absent and the block is provided, the block will be called.

Parameters:

  • key (Object)

    The key to remove from the cache

Yields:

  • Block to call if the key is not found

Returns:

  • (Object)

    The removed value or the result of the block



217
218
219
# File 'lib/zache.rb', line 217

def remove(key)
  synchronize_one(key) { @hash.delete(key) { yield if block_given? } }
end

#remove_allHash

Remove all keys from the cache.

Returns:

  • (Hash)

    Empty hash



224
225
226
# File 'lib/zache.rb', line 224

def remove_all
  synchronize_all { @hash = {} }
end

#remove_by {|key| ... } ⇒ Integer

Remove all keys that match the block.

Yields:

  • (key)

    Block that should return true for keys to be removed

Yield Parameters:

  • key (Object)

    The cache key to evaluate

Returns:

  • (Integer)

    Number of keys removed



233
234
235
236
237
238
239
240
241
242
243
244
# File 'lib/zache.rb', line 233

def remove_by
  synchronize_all do
    count = 0
    @hash.each_key do |k|
      if yield(k)
        @hash.delete(k)
        count += 1
      end
    end
    count
  end
end

#sizeInteger

Total number of keys currently in cache.

Returns:

  • (Integer)

    Number of keys in the cache



92
93
94
# File 'lib/zache.rb', line 92

def size
  synchronize_all { @hash.size }
end