async_storage 0.0.2 → 0.0.3

Sign up to get free protection for your applications and to get access to all the features.
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