atomic_cache 0.1.0.rc1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +51 -0
- data/.ruby_version +1 -0
- data/.travis.yml +26 -0
- data/CODE_OF_CONDUCT.md +46 -0
- data/Gemfile +6 -0
- data/LICENSE +201 -0
- data/README.md +49 -0
- data/Rakefile +6 -0
- data/atomic_cache.gemspec +36 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/docs/ARCH.md +34 -0
- data/docs/INTERFACES.md +45 -0
- data/docs/MODEL_SETUP.md +31 -0
- data/docs/PROJECT_SETUP.md +68 -0
- data/docs/USAGE.md +106 -0
- data/docs/img/quick_retry_graph.png +0 -0
- data/lib/atomic_cache.rb +11 -0
- data/lib/atomic_cache/atomic_cache_client.rb +197 -0
- data/lib/atomic_cache/concerns/global_lmt_cache_concern.rb +111 -0
- data/lib/atomic_cache/default_config.rb +62 -0
- data/lib/atomic_cache/key/keyspace.rb +98 -0
- data/lib/atomic_cache/key/last_mod_time_key_manager.rb +95 -0
- data/lib/atomic_cache/storage/dalli.rb +46 -0
- data/lib/atomic_cache/storage/instance_memory.rb +37 -0
- data/lib/atomic_cache/storage/memory.rb +67 -0
- data/lib/atomic_cache/storage/shared_memory.rb +40 -0
- data/lib/atomic_cache/storage/store.rb +31 -0
- data/lib/atomic_cache/version.rb +5 -0
- metadata +185 -0
@@ -0,0 +1,111 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'active_support/core_ext/object'
|
4
|
+
require 'active_support/concern'
|
5
|
+
require_relative '../atomic_cache_client'
|
6
|
+
require_relative '../default_config'
|
7
|
+
|
8
|
+
module AtomicCache
|
9
|
+
|
10
|
+
# this concern provides a single LMT for the whole model
|
11
|
+
module GlobalLMTCacheConcern
|
12
|
+
extend ActiveSupport::Concern
|
13
|
+
|
14
|
+
ATOMIC_CACHE_CONCERN_MUTEX = Mutex.new
|
15
|
+
|
16
|
+
class_methods do
|
17
|
+
|
18
|
+
def AtomicCache
|
19
|
+
init_atomic_cache
|
20
|
+
@atomic_cache
|
21
|
+
end
|
22
|
+
|
23
|
+
def cache_version(version)
|
24
|
+
ATOMIC_CACHE_CONCERN_MUTEX.synchronize do
|
25
|
+
@atomic_cache_version = version
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def cache_class(kls)
|
30
|
+
ATOMIC_CACHE_CONCERN_MUTEX.synchronize do
|
31
|
+
@atomic_cache_class = kls
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def cache_key_storage(storage)
|
36
|
+
ATOMIC_CACHE_CONCERN_MUTEX.synchronize do
|
37
|
+
@atomic_cache_key_storage = storage
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def cache_value_storage(storage)
|
42
|
+
ATOMIC_CACHE_CONCERN_MUTEX.synchronize do
|
43
|
+
@atomic_cache_value_storage = storage
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def default_cache_class
|
48
|
+
self.to_s.downcase
|
49
|
+
end
|
50
|
+
|
51
|
+
def cache_keyspace(*ks)
|
52
|
+
init_atomic_cache
|
53
|
+
@default_cache_keyspace.child(ks)
|
54
|
+
end
|
55
|
+
|
56
|
+
def expire_cache(at=Time.now)
|
57
|
+
init_atomic_cache
|
58
|
+
@timestamp_manager.last_modified_time = at
|
59
|
+
end
|
60
|
+
|
61
|
+
def last_modified_time
|
62
|
+
init_atomic_cache
|
63
|
+
@timestamp_manager.last_modified_time
|
64
|
+
end
|
65
|
+
|
66
|
+
private
|
67
|
+
|
68
|
+
def init_atomic_cache
|
69
|
+
return if @atomic_cache.present?
|
70
|
+
ATOMIC_CACHE_CONCERN_MUTEX.synchronize do
|
71
|
+
cache_class = @atomic_cache_class || default_cache_class
|
72
|
+
prefix = [cache_class]
|
73
|
+
prefix.unshift(DefaultConfig.instance.namespace) if DefaultConfig.instance.namespace.present?
|
74
|
+
prefix.push("v#{@atomic_cache_version}") if @atomic_cache_version.present?
|
75
|
+
@default_cache_keyspace = Keyspace.new(namespace: prefix, root: cache_class)
|
76
|
+
|
77
|
+
@timestamp_manager = LastModTimeKeyManager.new(
|
78
|
+
keyspace: @default_cache_keyspace,
|
79
|
+
storage: @atomic_cache_key_storage || DefaultConfig.instance.key_storage,
|
80
|
+
timestamp_formatter: DefaultConfig.instance.timestamp_formatter,
|
81
|
+
)
|
82
|
+
|
83
|
+
@atomic_cache = AtomicCacheClient.new(
|
84
|
+
default_options: DefaultConfig.instance.default_options,
|
85
|
+
logger: DefaultConfig.instance.logger,
|
86
|
+
metrics: DefaultConfig.instance.metrics,
|
87
|
+
timestamp_manager: @timestamp_manager,
|
88
|
+
storage: @atomic_cache_value_storage || DefaultConfig.instance.cache_storage
|
89
|
+
)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
def AtomicCache
|
95
|
+
self.class.AtomicCache
|
96
|
+
end
|
97
|
+
|
98
|
+
def cache_keyspace(ns)
|
99
|
+
self.class.cache_keyspace(ns)
|
100
|
+
end
|
101
|
+
|
102
|
+
def expire_cache(at=Time.now)
|
103
|
+
self.class.expire_cache(ns)
|
104
|
+
end
|
105
|
+
|
106
|
+
def last_modified_time
|
107
|
+
self.class.last_modified_time
|
108
|
+
end
|
109
|
+
|
110
|
+
end
|
111
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'singleton'
|
4
|
+
|
5
|
+
module AtomicCache
|
6
|
+
class DefaultConfig
|
7
|
+
include Singleton
|
8
|
+
|
9
|
+
CONFIG_MUTEX = Mutex.new
|
10
|
+
|
11
|
+
FLOAT_TIME_FORMATTER = Proc.new { |time| time.to_f }
|
12
|
+
TIMESTAMP_SEC_FORMATTER = Proc.new { |time| time.to_i }
|
13
|
+
TIMESTAMP_MS_FORMATTER = Proc.new { |time| (time.to_f * 1000).to_i }
|
14
|
+
ISO8601_FORMATTER = Proc.new { |time| time.iso8601 }
|
15
|
+
DEFAULT_TIME_FORMATTER = FLOAT_TIME_FORMATTER
|
16
|
+
DEFAULT_SEPARATOR = ':'
|
17
|
+
|
18
|
+
# [required] config
|
19
|
+
attr_accessor :key_storage # storage adapter instance for keys
|
20
|
+
attr_accessor :cache_storage # storage adapter instance for cached values
|
21
|
+
|
22
|
+
# [optional] config
|
23
|
+
attr_accessor :default_options # default options for all fetch requests
|
24
|
+
attr_accessor :namespace
|
25
|
+
attr_accessor :logger
|
26
|
+
attr_accessor :metrics
|
27
|
+
attr_accessor :timestamp_formatter
|
28
|
+
attr_accessor :separator
|
29
|
+
|
30
|
+
def initialize
|
31
|
+
reset
|
32
|
+
end
|
33
|
+
|
34
|
+
# Change all configured values back to default
|
35
|
+
def reset
|
36
|
+
@cache_client = nil
|
37
|
+
@default_options = {}
|
38
|
+
@namespace = nil
|
39
|
+
@logger = nil
|
40
|
+
@metrics = nil
|
41
|
+
@timestamp_formatter = DEFAULT_TIME_FORMATTER
|
42
|
+
@separator = DEFAULT_SEPARATOR
|
43
|
+
end
|
44
|
+
|
45
|
+
# Change all configured values back to default
|
46
|
+
def self.reset
|
47
|
+
self.instance.reset
|
48
|
+
end
|
49
|
+
|
50
|
+
# Quickly configure config singleton instance
|
51
|
+
# @yield mutate config
|
52
|
+
# @yieldparam [AtomicCache::DefaultConfig] config instance
|
53
|
+
def self.configure
|
54
|
+
if block_given?
|
55
|
+
manager = self.instance
|
56
|
+
CONFIG_MUTEX.synchronize do
|
57
|
+
yield(manager)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,98 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'murmurhash3'
|
4
|
+
require 'active_support/concern'
|
5
|
+
require 'active_support/core_ext/object'
|
6
|
+
require 'active_support/core_ext/hash'
|
7
|
+
require_relative '../default_config'
|
8
|
+
|
9
|
+
module AtomicCache
|
10
|
+
class Keyspace
|
11
|
+
|
12
|
+
DEFAULT_SEPARATOR = ':'
|
13
|
+
LOCK_VALUE = 1
|
14
|
+
PRIMITIVE_CLASSES = [Numeric, String, Symbol]
|
15
|
+
|
16
|
+
attr_reader :namespace, :root
|
17
|
+
|
18
|
+
# @param namespace [Object, Array<Object>] segment(s) with which to prefix the keyspace
|
19
|
+
# @param root [Object] logical 'root' or primary identifier for this keyspace
|
20
|
+
# @param separator [String] character or string to separate keyspace segments
|
21
|
+
# @param timestamp_formatter [Proc] function to turn Time -> String
|
22
|
+
def initialize(namespace:, root: nil, separator: nil, timestamp_formatter: nil)
|
23
|
+
@namespace = []
|
24
|
+
@namespace = normalize_segments(namespace) if namespace.present?
|
25
|
+
@separator = separator || DefaultConfig.instance.separator
|
26
|
+
@timestamp_formatter = timestamp_formatter || DefaultConfig.instance.timestamp_formatter
|
27
|
+
@root = root || namespace.last
|
28
|
+
end
|
29
|
+
|
30
|
+
# Create a new Keyspace, extending the namespace with the given segments and
|
31
|
+
# retaining this keyspace's root and separator
|
32
|
+
#
|
33
|
+
# @param namespace [Array<Object>] segments to concat onto this keyspace
|
34
|
+
# @return [AtomicCache::Keyspace] child keyspace
|
35
|
+
def child(namespace)
|
36
|
+
throw ArgumentError.new("Prefix must be an Array but was #{namespace.class}") unless namespace.is_a?(Array)
|
37
|
+
joined_namespacees = @namespace.clone.concat(namespace)
|
38
|
+
self.class.new(namespace: joined_namespacees)
|
39
|
+
end
|
40
|
+
|
41
|
+
# Get a string key for this keyspace, optionally suffixed by the given value
|
42
|
+
#
|
43
|
+
# @param suffix [String, Symbol, Numeric] an optional suffix
|
44
|
+
# @return [String] a key
|
45
|
+
def key(suffix=nil)
|
46
|
+
flattened_key(suffix)
|
47
|
+
end
|
48
|
+
|
49
|
+
def last_mod_time_key
|
50
|
+
@last_mod_time_key ||= flattened_key('lmt')
|
51
|
+
end
|
52
|
+
|
53
|
+
def lock_key
|
54
|
+
@lock_key ||= flattened_key('lock')
|
55
|
+
end
|
56
|
+
|
57
|
+
# the key the last_known_key is stored at, thus key key.
|
58
|
+
def last_known_key_key
|
59
|
+
@last_known_key ||= flattened_key('lkk')
|
60
|
+
end
|
61
|
+
|
62
|
+
protected
|
63
|
+
|
64
|
+
def flattened_key(suffix=nil)
|
65
|
+
segments = @namespace
|
66
|
+
segments = @namespace.clone.push(suffix) if suffix.present?
|
67
|
+
segments.join(@separator)
|
68
|
+
end
|
69
|
+
|
70
|
+
def normalize_segments(segments)
|
71
|
+
if segments.is_a? Array
|
72
|
+
segments.map { |seg| expand_segment(seg) }
|
73
|
+
elsif sgs.nil?
|
74
|
+
[]
|
75
|
+
else
|
76
|
+
[expand_segment(segments)]
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def expand_segment(segment)
|
81
|
+
case segment
|
82
|
+
when Symbol, String, Numeric
|
83
|
+
segment
|
84
|
+
when DateTime, Time
|
85
|
+
@timestamp_formatter.call(segment)
|
86
|
+
else
|
87
|
+
hexhash(segment)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def hexhash(segment)
|
92
|
+
# if the segment is sortable, sort it before hashing so that collections
|
93
|
+
# come out with the same hash
|
94
|
+
segment = segment.sort if segment.respond_to?(:sort)
|
95
|
+
MurmurHash3::V128.str_hash(Marshal.dump(segment)).pack('L*').unpack('H*').first
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
@@ -0,0 +1,95 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'active_support/core_ext/object'
|
4
|
+
require 'active_support/core_ext/hash'
|
5
|
+
|
6
|
+
module AtomicCache
|
7
|
+
class LastModTimeKeyManager
|
8
|
+
extend Forwardable
|
9
|
+
|
10
|
+
LOCK_VALUE = 1
|
11
|
+
|
12
|
+
# @param keyspace [AtomicCache::Keyspace] keyspace to store timestamp info at
|
13
|
+
# @param storage [Object] cache storage adapter
|
14
|
+
# @param timestamp_formatter [Proc] function to turn Time -> String
|
15
|
+
def initialize(keyspace: nil, storage: nil, timestamp_formatter: nil)
|
16
|
+
@timestamp_keyspace = keyspace
|
17
|
+
@storage = storage || DefaultConfig.instance.key_storage
|
18
|
+
@timestamp_formatter = timestamp_formatter || DefaultConfig.instance.timestamp_formatter
|
19
|
+
|
20
|
+
raise ArgumentError.new("`storage` required but none given") unless @storage.present?
|
21
|
+
raise ArgumentError.new("`root_keyspace` required but none given") unless @storage.present?
|
22
|
+
end
|
23
|
+
|
24
|
+
# get the key at the given keyspace, suffixed by the current timestamp
|
25
|
+
#
|
26
|
+
# @param keyspace [AtomicCache::Keyspace] keyspace to namespace this key with
|
27
|
+
# @return [String] a timestamped key
|
28
|
+
def current_key(keyspace)
|
29
|
+
keyspace.key(last_modified_time)
|
30
|
+
end
|
31
|
+
|
32
|
+
# get a key at the given keyspace, suffixed by given timestamp
|
33
|
+
#
|
34
|
+
# @param keyspace [AtomicCache::Keyspace] keyspace to namespace this key with
|
35
|
+
# @param timestamp [String, Numeric, Time] timestamp
|
36
|
+
# @return [String] a timestamped key
|
37
|
+
def next_key(keyspace, timestamp)
|
38
|
+
keyspace.key(self.format(timestamp))
|
39
|
+
end
|
40
|
+
|
41
|
+
# promote a key and timestamp after a successful re-generation of a cache keyspace
|
42
|
+
#
|
43
|
+
# @param keyspace [AtomicCache::Keyspace] keyspace to promote within
|
44
|
+
# @param last_known_key [String] a key with a known value to refer other processes to
|
45
|
+
# @param timestamp [String, Numeric, Time] the timestamp with which the last_known_key was updated at
|
46
|
+
def promote(keyspace, last_known_key:, timestamp:)
|
47
|
+
key = keyspace.last_known_key_key
|
48
|
+
@storage.set(key, last_known_key)
|
49
|
+
@storage.set(last_modified_time_key, self.format(timestamp))
|
50
|
+
end
|
51
|
+
|
52
|
+
# prevent other processes from modifying the given keyspace
|
53
|
+
#
|
54
|
+
# @param keyspace [AtomicCache::Keyspace] keyspace to lock
|
55
|
+
# @param ttl [Numeric] the duration in ms to lock (auto expires after duration is up)
|
56
|
+
# @param options [Hash] options to pass to the storage adapter
|
57
|
+
def lock(keyspace, ttl, options=nil)
|
58
|
+
@storage.add(keyspace.lock_key, LOCK_VALUE, ttl, options)
|
59
|
+
end
|
60
|
+
|
61
|
+
# remove existing lock to allow other processes to update keyspace
|
62
|
+
#
|
63
|
+
# @param keyspace [AtomicCache::Keyspace] keyspace to lock
|
64
|
+
def unlock(keyspace)
|
65
|
+
@storage.delete(keyspace.lock_key)
|
66
|
+
end
|
67
|
+
|
68
|
+
def last_known_key(keyspace)
|
69
|
+
@storage.read(keyspace.last_known_key_key)
|
70
|
+
end
|
71
|
+
|
72
|
+
def last_modified_time_key
|
73
|
+
@lmtk ||= @timestamp_keyspace.key('lmt')
|
74
|
+
end
|
75
|
+
|
76
|
+
def last_modified_time
|
77
|
+
@storage.read(last_modified_time_key)
|
78
|
+
end
|
79
|
+
|
80
|
+
def last_modified_time=(timestamp=Time.now)
|
81
|
+
@storage.set(last_modified_time_key, self.format(timestamp))
|
82
|
+
end
|
83
|
+
|
84
|
+
protected
|
85
|
+
|
86
|
+
def format(time)
|
87
|
+
if time.is_a?(Time)
|
88
|
+
@timestamp_formatter.call(time)
|
89
|
+
else
|
90
|
+
time
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
end
|
95
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'forwardable'
|
4
|
+
require_relative 'store'
|
5
|
+
|
6
|
+
module AtomicCache
|
7
|
+
module Storage
|
8
|
+
|
9
|
+
# A light wrapper over the Dalli client
|
10
|
+
class Dalli < Store
|
11
|
+
extend Forwardable
|
12
|
+
|
13
|
+
ADD_SUCCESS = 'STORED'
|
14
|
+
ADD_UNSUCCESSFUL = 'NOT_STORED'
|
15
|
+
ADD_EXISTS = 'EXISTS'
|
16
|
+
|
17
|
+
def_delegators :@dalli_client, :delete
|
18
|
+
|
19
|
+
def initialize(dalli_client)
|
20
|
+
@dalli_client = dalli_client
|
21
|
+
end
|
22
|
+
|
23
|
+
def add(key, new_value, ttl, user_options=nil)
|
24
|
+
opts = user_options&.clone || {}
|
25
|
+
opts[:raw] = true
|
26
|
+
|
27
|
+
# dalli expects time in seconds
|
28
|
+
# https://github.com/petergoldstein/dalli/blob/b8f4afe165fb3e07294c36fb1c63901b0ed9ce10/lib/dalli/client.rb#L27
|
29
|
+
# TODO: verify this unit is being treated correctly through the system
|
30
|
+
response = @dalli_client.add(key, new_value, ttl, opts)
|
31
|
+
response.start_with?(ADD_SUCCESS)
|
32
|
+
end
|
33
|
+
|
34
|
+
def read(key, user_options=nil)
|
35
|
+
user_options ||= {}
|
36
|
+
@dalli_client.read(key, user_options)
|
37
|
+
end
|
38
|
+
|
39
|
+
def set(key, value, user_options=nil)
|
40
|
+
user_options ||= {}
|
41
|
+
@dalli_client.set(key, value, user_options)
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'memory'
|
4
|
+
|
5
|
+
module AtomicCache
|
6
|
+
module Storage
|
7
|
+
|
8
|
+
# A storage adapter which keeps all values in memory, private to the instance
|
9
|
+
class InstanceMemory < Memory
|
10
|
+
|
11
|
+
def initialize(*args)
|
12
|
+
reset
|
13
|
+
super(*args)
|
14
|
+
end
|
15
|
+
|
16
|
+
def reset
|
17
|
+
@store = {}
|
18
|
+
end
|
19
|
+
|
20
|
+
def store
|
21
|
+
@store
|
22
|
+
end
|
23
|
+
|
24
|
+
def store_op(key, user_options=nil)
|
25
|
+
if !key.present?
|
26
|
+
desc = if key.nil? then 'Nil' else 'Empty' end
|
27
|
+
raise ArgumentError.new("#{desc} key given for storage operation") unless key.present?
|
28
|
+
end
|
29
|
+
|
30
|
+
normalized_key = key.to_sym
|
31
|
+
user_options ||= {}
|
32
|
+
yield(normalized_key, user_options)
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|