async_storage 0.0.2 → 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: add79afe0dfa294abeee5fb5102e05f5d5fbecfc70d700c5b25fc4f17cf9881f
4
- data.tar.gz: e0a8841f106ac58f7146938749732c43d99e0f56dacfcc4816d62c251bf34d2a
3
+ metadata.gz: a6066c81131bf563929ebf745a0025e424aade1c2c85cae099bbf0052bd43423
4
+ data.tar.gz: 750940e2dab1347fc3fb61825b5a9fbf5d0094b5ae53d30a99fdcb18676913d0
5
5
  SHA512:
6
- metadata.gz: 28800339448b6668b3da60bb5ad80d5e998c26ef42ebd3c06c881d49b09bfe7a8aed7893323578f3c0171183f705a5ca58fc9b871aa420425f161896e0e02d2a
7
- data.tar.gz: dff1ad907077f87c95977f14b56c4a8c9d49f394f781a1c164a53266c5a0ee3cae45078728b41b1020d47b3bb9c7949bf9e5286f83c21fc8e39c49735a286c36
6
+ metadata.gz: 5fd978fe08f9a6bf521480a7025899eb58d15b26b0312e2cd80c8dd7beb79db677056d6a6083fb375af1fcea48228a9f3aa005311f0adbe014d63c0529d1ffde
7
+ data.tar.gz: 6679f8603e517b2df74e5abbce7a8c6d6bc3c6bd69eab48c6b36bef7bf3c0219c6bd422ff4ef7e00f3d4ec0afd17ced001a18bd216ebf41269379fd5cf234182
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- async_storage (0.0.1)
4
+ async_storage (0.0.3)
5
5
  multi_json (> 0.0.0)
6
6
  redis (> 0.0.0)
7
7
 
data/README.md CHANGED
@@ -33,6 +33,7 @@ AsyncStorage.configuration do |config|
33
33
  end
34
34
  config.namespace = 'async_storage' # Default to 'async_storage'
35
35
  config.expires_in = 3_600 # Default to nil
36
+ config.circuit_breaker = true # Call the resolver instead of thrown redis connection error when the redis service is down. Default to true
36
37
  end
37
38
  ```
38
39
 
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'async_storage/naming'
4
+ require 'async_storage/circuit_breaker'
4
5
 
5
6
  module AsyncStorage
6
7
  class Allocator
@@ -31,19 +32,21 @@ module AsyncStorage
31
32
  #
32
33
  # @return [Object, NilClass] Return both stale or fresh object. If does not exist async call the retriever and return nil
33
34
  def get
34
- connection do |redis|
35
- raw_head = redis.get(naming.head)
36
- case raw_head
37
- when CTRL[:executed], CTRL[:enqueued]
38
- read_body(redis) # Try to deliver stale content
39
- when CTRL[:missing]
40
- return update!(redis) unless async?
41
-
42
- perform_async(redis) # Enqueue background job to resolve content
43
- redis.set(naming.head, CTRL[:enqueued])
44
- read_body(redis) # Try to deliver stale content
45
- else
46
- raise AsyncStorage::Error, format('the key %<k>s have an invalid value. Only "1" or "0" values are expected. And we got %<v>p', v: raw_head, k: naming.head)
35
+ breaker.run(fallback: -> { fetch! }) do
36
+ connection do |redis|
37
+ raw_head = redis.get(naming.head)
38
+ case raw_head
39
+ when CTRL[:executed], CTRL[:enqueued]
40
+ read_body(redis) # Try to deliver stale content
41
+ when CTRL[:missing]
42
+ return update!(redis) unless async?
43
+
44
+ perform_async(redis) # Enqueue background job to resolve content
45
+ redis.set(naming.head, CTRL[:enqueued])
46
+ read_body(redis) # Try to deliver stale content
47
+ else
48
+ raise AsyncStorage::Error, format('the key %<k>s have an invalid value. Only "1" or "0" values are expected. And we got %<v>p', v: raw_head, k: naming.head)
49
+ end
47
50
  end
48
51
  end
49
52
  end
@@ -52,17 +55,19 @@ module AsyncStorage
52
55
  #
53
56
  # @return [Object] Return the result from resolver
54
57
  def get!
55
- connection do |redis|
56
- raw_head = redis.get(naming.head)
57
- case raw_head
58
- when CTRL[:executed]
59
- read_body(redis) || begin
60
- update!(redis) unless redis.exists?(naming.body)
58
+ breaker.run(fallback: -> { fetch! }) do
59
+ connection do |redis|
60
+ raw_head = redis.get(naming.head)
61
+ case raw_head
62
+ when CTRL[:executed]
63
+ read_body(redis) || begin
64
+ update!(redis) unless redis.exists?(naming.body)
65
+ end
66
+ when CTRL[:missing], CTRL[:enqueued]
67
+ update!(redis)
68
+ else
69
+ raise AsyncStorage::Error, format('the key %<k>s have an invalid value. Only "1" or "0" values are expected. And we got %<v>p', v: raw_head, k: naming.head)
61
70
  end
62
- when CTRL[:missing], CTRL[:enqueued]
63
- update!(redis)
64
- else
65
- raise AsyncStorage::Error, format('the key %<k>s have an invalid value. Only "1" or "0" values are expected. And we got %<v>p', v: raw_head, k: naming.head)
66
71
  end
67
72
  end
68
73
  end
@@ -71,8 +76,10 @@ module AsyncStorage
71
76
  #
72
77
  # @return [Boolean] True or False according to the object existence
73
78
  def invalidate
74
- connection do |redis|
75
- redis.del(naming.head) == 1
79
+ breaker.run(fallback: -> { false }) do
80
+ connection do |redis|
81
+ redis.del(naming.head) == 1
82
+ end
76
83
  end
77
84
  end
78
85
 
@@ -80,11 +87,13 @@ module AsyncStorage
80
87
  #
81
88
  # @return [Boolean] True or False according to the object existence
82
89
  def invalidate!
83
- connection do |redis|
84
- redis.multi do |cli|
85
- cli.del(naming.body)
86
- cli.del(naming.head)
87
- end.include?(1)
90
+ breaker.run(fallback: -> { false }) do
91
+ connection do |redis|
92
+ redis.multi do |cli|
93
+ cli.del(naming.body)
94
+ cli.del(naming.head)
95
+ end.include?(1)
96
+ end
88
97
  end
89
98
  end
90
99
 
@@ -92,37 +101,45 @@ module AsyncStorage
92
101
  #
93
102
  # @return [Object, NilClass] Stale object or nil when it does not exist
94
103
  def refresh
95
- value = get(*@args)
96
- invalidate(*@args)
97
- value
104
+ breaker.run(fallback: -> { fetch! }) do
105
+ get.tap { invalidate }
106
+ end
98
107
  end
99
108
 
100
109
  # Fetch data from resolver and store it into redis
101
110
  #
102
111
  # @return [Object] Return the result from resolver
103
112
  def refresh!
104
- connection { |redis| update!(redis) }
113
+ breaker.run(fallback: -> { fetch! }) do
114
+ connection { |redis| update!(redis) }
115
+ end
105
116
  end
106
117
 
107
118
  # Check if a fresh value exist.
108
119
  #
109
120
  # @return [Boolean] True or False according the object existence
110
121
  def exist?
111
- connection { |redis| redis.exists?(naming.head) && redis.exists?(naming.body) }
122
+ breaker.run(fallback: -> { false }) do
123
+ connection { |redis| redis.exists?(naming.head) && redis.exists?(naming.body) }
124
+ end
112
125
  end
113
126
 
114
127
  # Check if object with a given key is stale
115
128
  #
116
129
  # @return [NilClass, Boolean] Return nil if the object does not exist or true/false according to the object freshness state
117
130
  def stale?
118
- connection { |redis| redis.exists?(naming.body) && redis.ttl(naming.head) < 0 }
131
+ breaker.run(fallback: -> { false }) do
132
+ connection { |redis| redis.exists?(naming.body) && redis.ttl(naming.head) < 0 }
133
+ end
119
134
  end
120
135
 
121
136
  # Check if a fresh object exists into the storage
122
137
  #
123
138
  # @return [Boolean] true/false according to the object existence and freshness
124
139
  def fresh?
125
- connection { |redis| redis.exists?(naming.body) && redis.ttl(naming.head) > 0 }
140
+ breaker.run(fallback: -> { false }) do
141
+ connection { |redis| redis.exists?(naming.body) && redis.ttl(naming.head) > 0 }
142
+ end
126
143
  end
127
144
 
128
145
  private
@@ -138,7 +155,7 @@ module AsyncStorage
138
155
  end
139
156
 
140
157
  def update!(redis)
141
- payload = resolver_class.new.(*@args)
158
+ payload = fetch!
142
159
 
143
160
  json = AsyncStorage::JSON.dump(payload, mode: :compat)
144
161
  redis.multi do |cli|
@@ -149,6 +166,10 @@ module AsyncStorage
149
166
  AsyncStorage::JSON.load(json)
150
167
  end
151
168
 
169
+ def fetch!
170
+ resolver_class.new.(*@args)
171
+ end
172
+
152
173
  def read_body(redis)
153
174
  raw = redis.get(naming.body)
154
175
  return unless raw
@@ -161,5 +182,9 @@ module AsyncStorage
161
182
 
162
183
  AsyncStorage.redis_pool.with { |redis| yield(redis) }
163
184
  end
185
+
186
+ def breaker
187
+ CircuitBreaker.new(self, exceptions: [Redis::BaseConnectionError])
188
+ end
164
189
  end
165
190
  end
@@ -0,0 +1,32 @@
1
+ # frizen_string_literal: true
2
+
3
+ module AsyncStorage
4
+ # This is not a real circuit breaker. We can improve it later.
5
+ # Basicaly only call the fallback function when some known exception is thrown
6
+ #
7
+ # @see https://martinfowler.com/bliki/CircuitBreaker.html
8
+ class CircuitBreaker
9
+ def initialize(context, exceptions: [])
10
+ @context = context
11
+ @exceptions = exceptions || []
12
+ end
13
+
14
+ def run(fallback: nil)
15
+ func = fallback.is_a?(Proc) ? fallback : Proc.new { fallback }
16
+ yield
17
+ rescue => err
18
+ if exception?(err)
19
+ func.arity == 0 ? @context.instance_exec(&func) : func.call(@context)
20
+ else
21
+ raise(err)
22
+ end
23
+ end
24
+
25
+ private
26
+
27
+ def exception?(error)
28
+ AsyncStorage.config.circuit_breaker? && \
29
+ (@exceptions.empty? || @exceptions.any? { |known_error| error.is_a?(known_error) })
30
+ end
31
+ end
32
+ end
@@ -43,6 +43,10 @@ module AsyncStorage
43
43
  # The global TTL for the redis storage. Keep nil if you don't want to expire objects.
44
44
  attribute_accessor :expires_in, default: nil
45
45
 
46
+ # When enabled it automatically calls the resolver when there is an issue with RedisConnection
47
+ attribute_accessor :circuit_breaker, default: true, normalizer: :normalizer_boolean, validator: :validate_boolean
48
+ alias circuit_breaker? circuit_breaker
49
+
46
50
  def config_path=(value)
47
51
  @config_from_yaml = nil
48
52
  @config_path = value
@@ -50,6 +54,23 @@ module AsyncStorage
50
54
 
51
55
  private
52
56
 
57
+ def normalizer_boolean(_attr, value)
58
+ return true if [1, '1', true, 'true'].include?(value)
59
+ return false if [nil, 0, '0', false, 'false'].include?(value)
60
+
61
+ value
62
+ end
63
+
64
+ def validate_boolean(attribute, value)
65
+ return if [true, false].include?(value)
66
+
67
+ raise InvalidConfig, format(
68
+ "The value %<value>p for %<attr>s is not valid. It must be a boolean",
69
+ value: value,
70
+ attr: attribute,
71
+ )
72
+ end
73
+
53
74
  def normalize_namespace(_attribute, value)
54
75
  return value.to_s if value.is_a?(Symbol)
55
76
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module AsyncStorage
4
- VERSION = '0.0.2'
4
+ VERSION = '0.0.3'
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: async_storage
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.2
4
+ version: 0.0.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Marcos G. Zimmermann
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2020-11-09 00:00:00.000000000 Z
11
+ date: 2020-11-10 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: redis
@@ -59,6 +59,7 @@ files:
59
59
  - lib/async_storage.rb
60
60
  - lib/async_storage/allocator.rb
61
61
  - lib/async_storage/bath_actions.rb
62
+ - lib/async_storage/circuit_breaker.rb
62
63
  - lib/async_storage/config.rb
63
64
  - lib/async_storage/json.rb
64
65
  - lib/async_storage/naming.rb