Class: Readthis::Cache

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

Overview

Readthis is a Redis backed cache client. It is a drop in replacement for any ‘ActiveSupport` compliant cache Above all Readthis emphasizes performance, simplicity, and explicitness.

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(options = {}) ⇒ Cache

Creates a new Readthis::Cache object with the given options.

Examples:

Create a new cache instance


Readthis::Cache.new(namespace: 'cache',
                    redis: { url: 'redis://localhost:6379/0' })

Create a compressed cache instance


Readthis::Cache.new(compress: true, compression_threshold: 2048)

Parameters:

  • options (Hash) (defaults to: {})

    a customizable set of options

Options Hash (options):

  • :redis (Hash)

    Options that will be passed to the redis connection

  • :compress (Boolean) — default: false

    Enable or disable automatic compression

  • :compression_threshold (Number) — default: 8k

    Minimum string size for compression

  • :expires_in (Number)

    The number of seconds until an entry expires

  • :refresh (Boolean) — default: false

    Automatically refresh key expiration

  • :retain_nils (Boolean) — default: false

    Whether nil values should be included in read_multi output

  • :marshal (Module) — default: Marshal

    Module that responds to ‘dump` and `load`

  • :namespace (String)

    Prefix used to namespace entries

  • :pool_size (Number) — default: 5

    The number of threads in the pool

  • :pool_timeout (Number) — default: 5

    How long before a thread times out



55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
# File 'lib/readthis/cache.rb', line 55

def initialize(options = {})
  @options = options

  @entity = Readthis::Entity.new(
    marshal: options.fetch(:marshal, Marshal),
    compress: options.fetch(:compress, false),
    threshold: options.fetch(:compression_threshold, 1024)
  )

  @pool = ConnectionPool.new(pool_options(options)) do
    Redis.new(options.fetch(:redis, {}))
  end

  @scripts = Readthis::Scripts.new
end

Instance Attribute Details

#entityObject (readonly)

Returns the value of attribute entity.



15
16
17
# File 'lib/readthis/cache.rb', line 15

def entity
  @entity
end

#notificationsObject (readonly)

Returns the value of attribute notifications.



15
16
17
# File 'lib/readthis/cache.rb', line 15

def notifications
  @notifications
end

#optionsObject (readonly)

Returns the value of attribute options.



15
16
17
# File 'lib/readthis/cache.rb', line 15

def options
  @options
end

#poolObject (readonly)

Returns the value of attribute pool.



15
16
17
# File 'lib/readthis/cache.rb', line 15

def pool
  @pool
end

#scriptsObject (readonly)

Returns the value of attribute scripts.



15
16
17
# File 'lib/readthis/cache.rb', line 15

def scripts
  @scripts
end

Class Method Details

.notificationsObject

Provide a class level lookup of the proper notifications module. Instrumention is expected to occur within applications that have ActiveSupport::Notifications available, but needs to work even when it isn’t.



21
22
23
# File 'lib/readthis/cache.rb', line 21

def self.notifications
  ActiveSupport::Notifications if defined?(ActiveSupport::Notifications)
end

Instance Method Details

#clear(options = {}) ⇒ Object

Clear the entire cache by flushing the current database.

This flushes everything in the current database, with no globbing applied. Data in other numbered databases will be preserved.

Examples:


cache.clear #=> 'OK'
cache.clear(async: true) #=> 'OK'

Parameters:

  • options (Hash) (defaults to: {})

    a customizable set of options

Options Hash (options):

  • :async (Hash)

    Flush the database asynchronously, only supported in Redis 4.0+



397
398
399
400
401
402
403
404
405
# File 'lib/readthis/cache.rb', line 397

def clear(options = {})
  invoke(:clear, '*') do |store|
    if options[:async]
      store.flushdb(async: true)
    else
      store.flushdb
    end
  end
end

#decrement(key, amount = 1, options = {}) ⇒ Object

Decrement a key in the store.

If the key doesn’t exist it will be initialized at 0. If the key exists but it isn’t a Fixnum it will be coerced to 0. Like ‘increment`, this does not make use of the native `decr` or `decrby` commands.

Examples:


cache.write('counter', 20) # => 20
cache.decrement('counter') # => 19
cache.decrement('counter', 2) # => 17

Parameters:

  • key (String)

    Key for lookup

  • amount (Fixnum) (defaults to: 1)

    Value to decrement by

  • options (Hash) (defaults to: {})

    Optional overrides



270
271
272
273
274
# File 'lib/readthis/cache.rb', line 270

def decrement(key, amount = 1, options = {})
  invoke(:decrement, key) do |store|
    alter(store, key, -amount, options)
  end
end

#delete(key, options = {}) ⇒ Object

Delete the value stored at the specified key. Returns ‘true` if anything was deleted, `false` otherwise.

Examples:


cache.delete('existing-key') # => true
cache.delete('random-key')   # => false

Parameters:

  • key (String)

    The key for lookup

  • options (Hash) (defaults to: {})

    Optional overrides



126
127
128
129
130
131
132
# File 'lib/readthis/cache.rb', line 126

def delete(key, options = {})
  namespaced = namespaced_key(key, merged_options(options))

  invoke(:delete, key) do |store|
    store.del(namespaced) > 0
  end
end

#delete_matched(pattern, options = {}) ⇒ Object

Delete all values that match a given pattern. The pattern must be defined using Redis compliant globs. The following examples are borrowed from the ‘KEYS` documentation:

  • ‘h?llo` matches hello, hallo and hxllo

  • ‘h*llo` matches hllo and heeeello

  • hllo` matches hello and hallo, but not hillo

  • hllo` matches hallo, hbllo, … but not hello

  • hllo` matches hallo and hbllo

Note that ‘delete_matched` does not use the `KEYS` command, making it safe for use in production.

Examples:

Delete all ‘cat’ keys


cache.delete_matched('*cats') #=> 47
cache.delete_matched('*dogs') #=> 0

Parameters:

  • pattern (String)

    The glob pattern for matching keys

  • [String] (Hash)

    a customizable set of options

  • [Number] (Hash)

    a customizable set of options



156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
# File 'lib/readthis/cache.rb', line 156

def delete_matched(pattern, options = {})
  namespaced = namespaced_key(pattern, merged_options(options))

  invoke(:delete, pattern) do |store|
    cursor = nil
    count = options.fetch(:count, 1000)
    deleted = 0

    until cursor == '0'
      cursor, matched = store.scan(cursor || 0, match: namespaced, count: count)

      if matched.any?
        store.del(*matched)
        deleted += matched.length
      end
    end

    deleted
  end
end

#exist?(key, options = {}) ⇒ Boolean

Returns ‘true` if the cache contains an entry for the given key.

Examples:


cache.exist?('some-key') # => false
cache.exist?('some-key', namespace: 'cache') # => true

Parameters:

  • key (String)

    Key for lookup

  • options (Hash) (defaults to: {})

    Optional overrides

Returns:

  • (Boolean)


378
379
380
381
382
# File 'lib/readthis/cache.rb', line 378

def exist?(key, options = {})
  invoke(:exist?, key) do |store|
    store.exists(namespaced_key(key, merged_options(options)))
  end
end

#fetch(key, options = {}) {|String| ... } ⇒ Object

Fetches data from the cache, using the given key. If there is data in the cache with the given key, then that data is returned.

If there is no such data in the cache (a cache miss), then ‘nil` will be returned. However, if a block has been passed, that block will be passed the key and executed in the event of a cache miss. The return value of the block will be written to the cache under the given cache key, and that return value will be returned.

Examples:

Typical


cache.write('today', 'Monday')
cache.fetch('today') # => "Monday"
cache.fetch('city')  # => nil

With a block


cache.fetch('city') do
  'Duckburgh'
end

cache.fetch('city') # => "Duckburgh"

Cache Miss


cache.write('today', 'Monday')
cache.fetch('today', force: true) # => nil

Parameters:

  • key (String)

    Key for lookup

  • options (Hash) (defaults to: {})

    Optional overrides

Options Hash (options):

  • :force (Boolean)

    Force a cache miss

Yields:

  • (String)

    Gives a missing key to the block, which is used to generate the missing value



211
212
213
214
215
216
217
218
219
220
221
# File 'lib/readthis/cache.rb', line 211

def fetch(key, options = {})
  options ||= {}
  value = read(key, options) unless options[:force]

  if value.nil? && block_given?
    value = yield(key)
    write(key, value, options)
  end

  value
end

#fetch_multi(keys) ⇒ Object

Fetches multiple keys from the cache using a single call to the server and filling in any cache misses. All read and write operations are executed atomically.

Examples:


cache.fetch_multi('alpha', 'beta') do |key|
  "#{key}-was-missing"
end

cache.fetch_multi('a', 'b', expires_in: 60) do |key|
  key * 2
end

Return all values for the given keys, applying the block to the key when a value is missing.

Parameters:

  • One (String)

    or more keys to fetch



348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
# File 'lib/readthis/cache.rb', line 348

def fetch_multi(*keys)
  options = extract_options!(keys).merge(retain_nils: true)
  results = read_multi(*keys, options)
  missing = {}

  invoke(:fetch_multi, keys) do |_store|
    results.each do |key, value|
      next unless value.nil?

      value = yield(key)
      missing[key] = value
      results[key] = value
    end
  end

  write_multi(missing, options) if missing.any?

  results
end

#increment(key, amount = 1, options = {}) ⇒ Object

Increment a key in the store.

If the key doesn’t exist it will be initialized at 0. If the key exists but it isn’t a Fixnum it will be coerced to 0.

Note that this method does not use Redis’ native ‘incr` or `incrby` commands. Those commands only work with number-like strings, and are incompatible with the encoded values Readthis writes to the store. The behavior of `incrby` is preserved as much as possible, but incrementing is not an atomic action. If multiple clients are incrementing the same key there will be a “last write wins” race condition, causing incorrect counts.

If you absolutely require correct counts it is better to use the Redis client directly.

Examples:


cache.increment('counter') # => 1
cache.increment('counter', 2) # => 3

Parameters:

  • key (String)

    Key for lookup

  • amount (Fixnum) (defaults to: 1)

    Value to increment by

  • options (Hash) (defaults to: {})

    Optional overrides



248
249
250
251
252
# File 'lib/readthis/cache.rb', line 248

def increment(key, amount = 1, options = {})
  invoke(:increment, key) do |store|
    alter(store, key, amount, options)
  end
end

#read(key, options = {}) ⇒ Object

Fetches data from the cache, using the given key. If there is data in the cache with the given key, then that data is returned. Otherwise, nil is returned.

Examples:


cache.read('missing') # => nil
cache.read('matched') # => 'some value'

Parameters:

  • key (String)

    Key for lookup

  • options (Hash) (defaults to: {})

    Optional overrides



83
84
85
86
87
88
89
90
91
92
93
# File 'lib/readthis/cache.rb', line 83

def read(key, options = {})
  options = merged_options(options)

  invoke(:read, key) do |store|
    key = namespaced_key(key, options)

    refresh_entity(key, store, options)

    entity.load(store.get(key))
  end
end

#read_multi(keys) ⇒ Hash

Efficiently read multiple values at once from the cache. Options can be passed in the last argument.

Examples:


cache.read_multi('a', 'b') # => { 'a' => 1 }
cache.read_multi('a', 'b', retain_nils: true) # => { 'a' => 1, 'b' => nil }

Return all values for the given keys.

Parameters:

  • One (String)

    or more keys to fetch

  • options (Hash)

    Configuration to override

Returns:

  • (Hash)

    A hash mapping keys to the values found.



291
292
293
294
295
296
297
298
299
300
301
302
303
304
# File 'lib/readthis/cache.rb', line 291

def read_multi(*keys)
  options = merged_options(extract_options!(keys))
  mapping = keys.map { |key| namespaced_key(key, options) }

  return {} if keys.empty?

  invoke(:read_multi, keys) do |store|
    values = store.mget(*mapping).map { |value| entity.load(value) }

    refresh_entity(mapping, store, options)

    zipped_results(keys, values, options)
  end
end

#write(key, value, options = {}) ⇒ Object

Writes data to the cache using the given key. Will overwrite whatever value is already stored at that key.

Examples:


cache.write('some-key', 'a bunch of text')                     # => 'OK'
cache.write('some-key', 'short lived', expires_in: 60)         # => 'OK'
cache.write('some-key', 'lives elsehwere', namespace: 'cache') # => 'OK'

Parameters:

  • key (String)

    Key for lookup

  • options (Hash) (defaults to: {})

    Optional overrides



107
108
109
110
111
112
113
# File 'lib/readthis/cache.rb', line 107

def write(key, value, options = {})
  options = merged_options(options)

  invoke(:write, key) do |store|
    write_entity(key, value, store, options)
  end
end

#write_multi(hash, options = {}) ⇒ Object

Write multiple key value pairs simultaneously. This is an atomic operation that will always succeed and will overwrite existing values.

This is a non-standard, but useful, cache method.

Examples:


cache.write_multi({ 'a' => 1, 'b' => 2 }) # => true

Parameters:

  • hash (Hash)

    Key value hash to write

  • options (Hash) (defaults to: {})

    Optional overrides



319
320
321
322
323
324
325
326
327
# File 'lib/readthis/cache.rb', line 319

def write_multi(hash, options = {})
  options = merged_options(options)

  invoke(:write_multi, hash.keys) do |store|
    store.multi do
      hash.each { |key, value| write_entity(key, value, store, options) }
    end
  end
end