composite_cache_store 0.0.1 → 0.0.3

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6b6a9480fa18ff9ed0c17318f3ea093c6f8b586b4595427ff9b1056e7c0e9a5f
4
- data.tar.gz: 5e9af188c3726dbebec690aabcb838f426e9d26f9c96e48d291b3bb8083a28bc
3
+ metadata.gz: 689876c5fc7b2bd00343817eac73b522a0687482aaaeb1058a471c7fcac275e7
4
+ data.tar.gz: dba46b9f25d1302377dd0fb64c9765a8c6016545f78fdb57444282e6b4d94cd8
5
5
  SHA512:
6
- metadata.gz: 636f9164938ce4dc6d5fbfc54d24e0e2e0870b8822c770b13bfdb293f02ac5fe4787bc0d9f883a398563f0e8210dcdfd7e89824298cc2dff20c03d4a9a98efea
7
- data.tar.gz: c4b79fefc6e70e02243ab8cfdf2c662de139e1e2b3062cd2f171d078cefb2869dae0c8a035d1269be5238015a020d378b77b9288cc42e630858eb91866641706
6
+ metadata.gz: a04ad705343eea18cdd44d399cbd2264b11fa7eb91a6be8c0d98960b8937510abd77a780a95c816217bf2752cf7fde885dbf423b04aaee11024280876e19f994
7
+ data.tar.gz: 72bcee27ef6647b444582c481dfa011fedbb587d540537d193a3f1042965bf16e7cb740ed059f6483b7926afdd99dd2b84fe80715665ea8908dcf2c3efc1ca0f
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # CompositeCacheStore
2
2
 
3
- ### A composite cache store comprised of 2 ActiveSupport::Cache::Store instances
3
+ ### A composite cache store comprised of layered ActiveSupport::Cache::Store instances
4
4
 
5
5
  <!-- Tocer[start]: Auto-generated, don't remove. -->
6
6
 
@@ -27,9 +27,12 @@ While these services are robust and performant, they can also be a source of lat
27
27
  __A composite (or layered) cache can mitigate these risks__
28
28
  by reducing traffic and backpressure on the persistence service.
29
29
 
30
- Consider a composite cache that wraps a remote Redis-backed store with an local in-memory store.
31
- When both caches are warm, a read hit on the local in-memory store will return instantly, avoiding the overhead
32
- of inter-process communication (IPC) and/or network traffic _(with its attendant data marshaling and socket/wire noise)._
30
+ Consider a composite cache that wraps a remote Redis-backed "layer 2 cache" with a local in-memory "layer 1 cache".
31
+ When both caches are warm, a read hit on the local in-memory "layer 1 cache" returns instantly and avoids the overhead of
32
+ inter-process communication (IPC) and/or network traffic _(with its attendant data marshaling and socket/wire noise)_
33
+ associated with accessing the remote Redis-backed "layer 2 cache".
34
+
35
+ To summarize: __Reads prioritize the inner cache and fall back to the outer cache.__
33
36
 
34
37
  ## Sponsors
35
38
 
@@ -68,12 +71,17 @@ end
68
71
  ```ruby
69
72
  # config/initializers/composite_cache_store.rb
70
73
  def Rails.composite_cache
71
- @store ||= CompositeCacheStore.new(
72
- inner_cache_store: Rails.cache, # use whatever makes sense for your app as the remote inner-cache
73
- outer_cache_store: ActiveSupport::Cache::MemoryStore.new( # employs an LRU eviction policy
74
- expires_in: 15.minutes, # constrain entry lifetime so the local outer-cache doesn't drift out of sync
75
- size: 32.megabytes # constrain max memory used by the local outer-cache
76
- )
74
+ @composite_cache ||= CompositeCacheStore.new(
75
+ # Layer 1 cache (inner) - employs an LRU eviction policy
76
+ ActiveSupport::Cache::MemoryStore.new(
77
+ expires_in: 15.minutes, # constrain entry lifetime so the local cache doesn't drift out of sync
78
+ size: 32.megabytes # constrain max memory used by the local cache
79
+ ),
80
+
81
+ # Layer 2 cache (outer)
82
+ Rails.cache, # use whatever makes sense for your app
83
+
84
+ # additional layers are optional
77
85
  )
78
86
  end
79
87
  ```
@@ -85,8 +93,8 @@ A composite cache is ideal for mitigating hot spot latency in frequently invoked
85
93
  ```ruby
86
94
  # method that's invoked frequently by multiple processes
87
95
  def hotspot
88
- # NOTE: the expires_in option is only applied to the remote inner-cache
89
- # the local outer-cache uses its globally configured expiration policy
96
+ # NOTE: expiration options are only applied to the outermost cache
97
+ # inner caches use their globally configured expiration policy
90
98
  Rails.composite_cache.fetch("example/slow/operation", expires_in: 12.hours) do
91
99
  # a slow operation
92
100
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class CompositeCacheStore
4
- VERSION = "0.0.1"
4
+ VERSION = "0.0.3"
5
5
  end
@@ -1,123 +1,149 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "active_support/cache"
3
+ require "active_support/all"
4
4
  require_relative "composite_cache_store/version"
5
5
 
6
6
  class CompositeCacheStore
7
- DEFAULT_OUTER_OPTIONS = {
7
+ DEFAULT_LAYER_1_OPTIONS = {
8
8
  expires_in: 5.minutes,
9
9
  size: 16.megabytes
10
10
  }
11
11
 
12
- DEFAULT_INNER_OPTIONS = {
12
+ DEFAULT_LAYER_2_OPTIONS = {
13
13
  expires_in: 1.day,
14
14
  size: 32.megabytes
15
15
  }
16
16
 
17
- attr_reader :outer_cache_store, :inner_cache_store
18
-
19
- alias_method :outer, :outer_cache_store
20
- alias_method :inner, :inner_cache_store
17
+ attr_reader :layers
21
18
 
22
19
  # Returns a new CompositeCacheStore instance
23
- # - inner_cache_store: An ActiveSupport::Cache::Store instance to use for the inner cache store (typically remote)
24
- # - outer_cache_store: An ActiveSupport::Cache::Store instance to use for the outer cache store (typically local)
25
- def initialize(options = {})
26
- options ||= {}
20
+ def initialize(*layers)
21
+ if layers.blank?
22
+ layers << ActiveSupport::Cache::MemoryStore.new(DEFAULT_LAYER_1_OPTIONS)
23
+ layers << ActiveSupport::Cache::MemoryStore.new(DEFAULT_LAYER_2_OPTIONS)
24
+ end
27
25
 
28
- @inner_cache_store = options[:inner_cache_store]
29
- @inner_cache_store = ActiveSupport::Cache::MemoryStore.new(DEFAULT_INNER_OPTIONS) unless inner.is_a?(ActiveSupport::Cache::Store)
26
+ message = "All layers must be instances of ActiveSupport::Cache::Store"
27
+ layers.each do |layer|
28
+ raise ArgumentError.new(message) unless layer.is_a?(ActiveSupport::Cache::Store)
29
+ end
30
30
 
31
- @outer_cache_store = options[:outer_cache_store]
32
- @outer_cache_store = ActiveSupport::Cache::MemoryStore.new(DEFAULT_OUTER_OPTIONS) unless outer.is_a?(ActiveSupport::Cache::Store)
31
+ layers.freeze
32
+ @layers = layers
33
33
  end
34
34
 
35
35
  def cleanup(...)
36
- outer.cleanup(...)
37
- inner.cleanup(...)
36
+ layers.each { |store| store.cleanup(...) }
38
37
  end
39
38
 
40
39
  def clear(...)
41
- outer.clear(...)
42
- inner.clear(...)
40
+ layers.each { |store| store.clear(...) }
43
41
  end
44
42
 
45
43
  def decrement(...)
46
- outer.decrement(...)
47
- inner.decrement(...)
44
+ layers.each { |store| store.decrement(...) }
48
45
  end
49
46
 
50
47
  def delete(...)
51
- outer.delete(...)
52
- inner.delete(...)
48
+ layers.each { |store| store.delete(...) }
53
49
  end
54
50
 
55
51
  def delete_matched(...)
56
- outer.delete_matched(...)
57
- inner.delete_matched(...)
52
+ layers.each { |store| store.delete_matched(...) }
58
53
  end
59
54
 
60
55
  def delete_multi(...)
61
- outer.delete_multi(...)
62
- inner.delete_multi(...)
56
+ layers.each { |store| store.delete_multi(...) }
63
57
  end
64
58
 
65
59
  def exist?(...)
66
- outer.exist?(...) || inner.exist?(...)
60
+ layers.each do |store|
61
+ return true if store.exist?(...)
62
+ end
63
+ false
67
64
  end
68
65
 
69
66
  def fetch(*args, &block)
70
- outer.fetch(*args) do
71
- inner.fetch(*args, &block)
67
+ f = ->(store) do
68
+ return store.fetch(*args, &block) if store == layers.last
69
+ store.fetch(*args) { f.call(layers[layers.index(store) + 1]) }
72
70
  end
71
+ f.call(layers.first)
73
72
  end
74
73
 
75
74
  def fetch_multi(*args, &block)
76
- outer.fetch_multi(*args) do
77
- inner.fetch_multi(*args, &block)
75
+ fm = ->(store) do
76
+ return store.fetch_multi(*args, &block) if store == layers.last
77
+ store.fetch_multi(*args) { fm.call(layers[layers.index(store) + 1]) }
78
78
  end
79
+ fm.call(layers.first)
79
80
  end
80
81
 
81
- # write
82
82
  def increment(...)
83
- outer.increment(...)
84
- inner.increment(...)
83
+ layers.each { |store| store.increment(...) }
85
84
  end
86
85
 
87
86
  def mute
88
- outer.mute do
89
- inner.mute do
90
- yield
91
- end
87
+ m = ->(store) do
88
+ return store.mute { yield } if store == layers.last
89
+ store.mute { m.call(layers[layers.index(store) + 1]) }
92
90
  end
91
+ m.call(layers.first)
93
92
  end
94
93
 
95
94
  def read(*args)
96
- outer.fetch(*args) do
97
- inner.read(*args)
95
+ r = ->(store) do
96
+ return store.read(*args) if store == layers.last
97
+ store.fetch(*args) { r.call(layers[layers.index(store) + 1]) }
98
98
  end
99
+ r.call(layers.first)
99
100
  end
100
101
 
101
102
  def read_multi(...)
102
- result = outer.read_multi(...)
103
- result = inner.read_multi(...) if result.blank?
104
- result
103
+ missed_layers = []
104
+ layers.each do |store|
105
+ hash = store.read_multi(...)
106
+ if hash.present?
107
+ missed_layers.each { |s| s.write_multi(hash) }
108
+ return hash
109
+ end
110
+ missed_layers << store
111
+ end
112
+ {}
105
113
  end
106
114
 
107
115
  def silence!
108
- outer.silence!
109
- inner.silence!
116
+ layers.each { |store| store.silence! }
110
117
  end
111
118
 
112
119
  def write(name, value, options = nil)
113
- options ||= {}
114
- outer.write(name, value, options.except(:expires_in)) # ? accept expires_in if less than outer.config[:expires_in] ?
115
- inner.write(name, value, options)
120
+ layers.each do |store|
121
+ store.write name, value, permitted_options(store, options)
122
+ end
116
123
  end
117
124
 
118
125
  def write_multi(hash, options = nil)
119
- options ||= {}
120
- outer.write_multi(hash, options.except(:expires_in)) # ? accept expires_in if less than outer.config[:expires_in] ?
121
- inner.write_multi(hash, options)
126
+ layers.each do |store|
127
+ store.write_multi hash, permitted_options(store, options)
128
+ end
129
+ end
130
+
131
+ private
132
+
133
+ def permitted_options(store, options = {})
134
+ return options if options.blank?
135
+ return options if keep_expiration?(store, options)
136
+ options.except(:expires_in, :expires_at)
137
+ end
138
+
139
+ def keep_expiration?(store, options = {})
140
+ return true if store == layers.last
141
+ return true unless store.options[:expires_in]
142
+
143
+ expires_in = options[:expires_in]
144
+ expires_in ||= Time.current - options[:expires_at] if options[:expires_at]
145
+ return false unless expires_in
146
+
147
+ expires_in < store.options[:expires_in]
122
148
  end
123
149
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: composite_cache_store
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.0.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Nate Hopkins (hopsoft)
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-03-11 00:00:00.000000000 Z
11
+ date: 2023-03-14 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -82,7 +82,7 @@ dependencies:
82
82
  version: '0'
83
83
  description: |
84
84
  Enhanced application performance with faster reads, data redundancy,
85
- and reduced backpressure on the inner cache store.
85
+ and reduced backpressure on the outer cache store.
86
86
  email:
87
87
  - natehop@gmail.com
88
88
  executables: []
@@ -118,5 +118,6 @@ requirements: []
118
118
  rubygems_version: 3.4.6
119
119
  signing_key:
120
120
  specification_version: 4
121
- summary: A composite cache store comprised of 2 ActiveSupport::Cache::Store instances
121
+ summary: A composite cache store comprised of layered ActiveSupport::Cache::Store
122
+ instances
122
123
  test_files: []