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 +4 -4
- data/Gemfile.lock +1 -1
- data/README.md +1 -0
- data/lib/async_storage/allocator.rb +63 -38
- data/lib/async_storage/circuit_breaker.rb +32 -0
- data/lib/async_storage/config.rb +21 -0
- data/lib/async_storage/version.rb +1 -1
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a6066c81131bf563929ebf745a0025e424aade1c2c85cae099bbf0052bd43423
|
4
|
+
data.tar.gz: 750940e2dab1347fc3fb61825b5a9fbf5d0094b5ae53d30a99fdcb18676913d0
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 5fd978fe08f9a6bf521480a7025899eb58d15b26b0312e2cd80c8dd7beb79db677056d6a6083fb375af1fcea48228a9f3aa005311f0adbe014d63c0529d1ffde
|
7
|
+
data.tar.gz: 6679f8603e517b2df74e5abbce7a8c6d6bc3c6bd69eab48c6b36bef7bf3c0219c6bd422ff4ef7e00f3d4ec0afd17ced001a18bd216ebf41269379fd5cf234182
|
data/Gemfile.lock
CHANGED
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
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
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
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
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
|
-
|
75
|
-
|
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
|
-
|
84
|
-
|
85
|
-
cli
|
86
|
-
|
87
|
-
|
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
|
-
|
96
|
-
|
97
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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 =
|
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
|
data/lib/async_storage/config.rb
CHANGED
@@ -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
|
|
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.
|
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-
|
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
|