Class: SafeMemoize::Adapters::ConcurrentRuby

Inherits:
Stores::Base show all
Defined in:
lib/safe_memoize/adapters/concurrent_ruby.rb

Overview

Optional store adapter backed by +concurrent-ruby+.

Replaces the default +Mutex+-guarded +Hash+ with +Concurrent::Map+ and +Concurrent::ReentrantReadWriteLock+. Multiple readers proceed in parallel; writers still get exclusive access. For hot paths with many concurrent readers this can meaningfully reduce lock contention compared to Stores::Memory.

Opt in per class:

class MyService prepend SafeMemoize self.safe_memoize_store = SafeMemoize::Adapters::ConcurrentRuby.new end

Or globally via SafeMemoize.configure:

SafeMemoize.configure do |c| c.default_store = SafeMemoize::Adapters::ConcurrentRuby.new end

Requires the +concurrent-ruby+ gem, which is not a runtime dependency of +safe_memoize+. Add it to your own Gemfile or gemspec:

gem "concurrent-ruby"

A LoadError with an actionable message is raised at instantiation time if the gem is not available.

Constant Summary

Constants inherited from Stores::Base

Stores::Base::MISS

Instance Method Summary collapse

Methods inherited from Stores::Base

#exist?

Constructor Details

#initializeConcurrentRuby

Returns a new instance of ConcurrentRuby.



33
34
35
36
37
38
39
40
41
42
# File 'lib/safe_memoize/adapters/concurrent_ruby.rb', line 33

def initialize
  require "concurrent/map"
  require "concurrent/atomic/reentrant_read_write_lock"
  @data = Concurrent::Map.new
  @lock = Concurrent::ReentrantReadWriteLock.new
rescue LoadError
  raise LoadError,
    "SafeMemoize::Adapters::ConcurrentRuby requires the concurrent-ruby gem. " \
    "Add `gem 'concurrent-ruby'` to your Gemfile."
end

Instance Method Details

#clearvoid

This method returns an undefined value.



75
76
77
# File 'lib/safe_memoize/adapters/concurrent_ruby.rb', line 75

def clear
  @lock.with_write_lock { @data.clear }
end

#delete(key) ⇒ void

This method returns an undefined value.

Parameters:

  • key (Object)


70
71
72
# File 'lib/safe_memoize/adapters/concurrent_ruby.rb', line 70

def delete(key)
  @lock.with_write_lock { @data.delete(key) }
end

#keysArray<Object>

Returns all live (non-expired) keys.

Returns:

  • (Array<Object>)


81
82
83
84
85
86
87
88
# File 'lib/safe_memoize/adapters/concurrent_ruby.rb', line 81

def keys
  now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
  @lock.with_read_lock do
    result = []
    @data.each_pair { |k, entry| result << k unless entry[:expires_at] && entry[:expires_at] <= now }
    result
  end
end

#read(key) ⇒ Object

Returns the stored value, or Stores::Base::MISS if absent or expired.

Parameters:

  • key (Object)

Returns:



46
47
48
49
50
51
52
53
54
# File 'lib/safe_memoize/adapters/concurrent_ruby.rb', line 46

def read(key)
  @lock.with_read_lock do
    entry = @data[key]
    return MISS unless entry
    return MISS if expired?(entry)

    entry[:value]
  end
end

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

This method returns an undefined value.

Parameters:

  • key (Object)
  • value (Object)
  • expires_in (Numeric, nil) (defaults to: nil)

    seconds until expiry; +nil+ means no expiry



60
61
62
63
64
65
66
# File 'lib/safe_memoize/adapters/concurrent_ruby.rb', line 60

def write(key, value, expires_in: nil)
  now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
  expires_at = expires_in ? now + expires_in.to_f : nil
  @lock.with_write_lock do
    @data[key] = {value: value, expires_at: expires_at, cached_at: now}
  end
end