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 +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
|