freno-client 0.4.0 → 0.6.0
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/.rubocop.yml +12 -12
- data/CONTRIBUTING.md +1 -1
- data/Gemfile +1 -0
- data/README.md +179 -4
- data/Rakefile +22 -1
- data/lib/freno/client/version.rb +1 -1
- data/lib/freno/throttler.rb +208 -0
- data/lib/freno/throttler/circuit_breaker.rb +39 -0
- data/lib/freno/throttler/errors.rb +23 -0
- data/lib/freno/throttler/instrumenter.rb +27 -0
- data/lib/freno/throttler/mapper.rb +45 -0
- data/script/release +6 -0
- metadata +8 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: bf297a41a22996c917599c6bc5835a8abb482d09
|
4
|
+
data.tar.gz: 1a45ae21c0e31b7fda7769b78c3fcc8d3d6b4bf5
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 3f6ab33f6c2008972cc3eea6e8611d1aca5e93c64d318211ee7e44d7066cc6cc73f38a97ea2d398e3738e6f35663d8c1dbc6bcd0fb6fb27dc0fe6e7502ea120c
|
7
|
+
data.tar.gz: 9bc8e0b6a8531768b77fc7b6b6b0edd99c9d9f1c0c0bc1d50264806ced07ba960de438a7f1f9f3c1c96309c79a4edf0ee95892a84ad610b0933b456d8d114cbe
|
data/.rubocop.yml
CHANGED
@@ -140,6 +140,18 @@ Metrics/ParameterLists:
|
|
140
140
|
Metrics/PerceivedComplexity:
|
141
141
|
Enabled: false
|
142
142
|
|
143
|
+
Naming/AsciiIdentifiers:
|
144
|
+
Enabled: true
|
145
|
+
|
146
|
+
Naming/ClassAndModuleCamelCase:
|
147
|
+
Enabled: true
|
148
|
+
|
149
|
+
Naming/FileName:
|
150
|
+
Enabled: true
|
151
|
+
|
152
|
+
Naming/MethodName:
|
153
|
+
Enabled: true
|
154
|
+
|
143
155
|
Performance/CaseWhenSplat:
|
144
156
|
Enabled: false
|
145
157
|
|
@@ -195,9 +207,6 @@ Security/Eval:
|
|
195
207
|
Style/ArrayJoin:
|
196
208
|
Enabled: true
|
197
209
|
|
198
|
-
Style/AsciiIdentifiers:
|
199
|
-
Enabled: true
|
200
|
-
|
201
210
|
Style/BeginBlock:
|
202
211
|
Enabled: true
|
203
212
|
|
@@ -213,9 +222,6 @@ Style/CaseEquality:
|
|
213
222
|
Style/CharacterLiteral:
|
214
223
|
Enabled: true
|
215
224
|
|
216
|
-
Style/ClassAndModuleCamelCase:
|
217
|
-
Enabled: true
|
218
|
-
|
219
225
|
Style/ClassMethods:
|
220
226
|
Enabled: true
|
221
227
|
|
@@ -231,9 +237,6 @@ Style/EndBlock:
|
|
231
237
|
Layout/EndOfLine:
|
232
238
|
Enabled: true
|
233
239
|
|
234
|
-
Style/FileName:
|
235
|
-
Enabled: true
|
236
|
-
|
237
240
|
Style/FlipFlop:
|
238
241
|
Enabled: true
|
239
242
|
|
@@ -252,9 +255,6 @@ Style/MethodCallWithoutArgsParentheses:
|
|
252
255
|
Style/MethodDefParentheses:
|
253
256
|
Enabled: true
|
254
257
|
|
255
|
-
Style/MethodName:
|
256
|
-
Enabled: true
|
257
|
-
|
258
258
|
Style/MultilineIfThen:
|
259
259
|
Enabled: true
|
260
260
|
|
data/CONTRIBUTING.md
CHANGED
@@ -11,7 +11,7 @@ Please note that this project is released with a [Contributor Code of Conduct][c
|
|
11
11
|
|
12
12
|
## Submitting a pull request
|
13
13
|
|
14
|
-
0. [Fork][fork] and clone the freno-
|
14
|
+
0. [Fork][fork] and clone the freno-client repository
|
15
15
|
0. Configure and install the dependencies: `script/bootstrap`
|
16
16
|
0. Make sure the tests pass on your machine: `rake`
|
17
17
|
0. Create a new branch: `git checkout -b my-branch-name`
|
data/Gemfile
CHANGED
data/README.md
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
# Freno Client [](https://travis-ci.org/github/freno-client)
|
2
2
|
|
3
|
-
A ruby client for [Freno](https://github.com/github/freno): the cooperative, highly available throttler service.
|
3
|
+
A ruby client and throttling library for [Freno](https://github.com/github/freno): the cooperative, highly available throttler service.
|
4
4
|
|
5
5
|
## Current status
|
6
6
|
|
@@ -175,6 +175,183 @@ freno = Freno::Client.new(faraday) do |client|
|
|
175
175
|
end
|
176
176
|
```
|
177
177
|
|
178
|
+
### Throttler objects
|
179
|
+
|
180
|
+
Apart from the operations above, freno-client comes with `Freno::Throttler`, a ruby library for throttling. You can use it in the following way:
|
181
|
+
|
182
|
+
```ruby
|
183
|
+
require "freno/throttler"
|
184
|
+
|
185
|
+
client = Freno::Client.new(faraday)
|
186
|
+
throttler = Freno::Throttler.new(client: client, app: :my_app)
|
187
|
+
context = :my_cluster
|
188
|
+
|
189
|
+
bid_data_set.each_slice(SLICE_SIZE) do |slice|
|
190
|
+
throttler.throttle(context) do
|
191
|
+
update(slice)
|
192
|
+
end
|
193
|
+
end
|
194
|
+
```
|
195
|
+
|
196
|
+
In the above example, `Freno::Throttler#throttle(context, &block)` will check freno to determine whether is OK to proceed with the given block. If so, the block will be executed immediately, otherwise the throttler will sleep and try
|
197
|
+
again.
|
198
|
+
|
199
|
+
#### Throttler configuration
|
200
|
+
|
201
|
+
```ruby
|
202
|
+
module Freno
|
203
|
+
class Throttler
|
204
|
+
|
205
|
+
DEFAULT_WAIT_SECONDS = 0.5
|
206
|
+
DEFAULT_MAX_WAIT_SECONDS = 10
|
207
|
+
|
208
|
+
def initialize(client: nil,
|
209
|
+
app: nil,
|
210
|
+
mapper: Mapper::Identity,
|
211
|
+
instrumenter: Instrumenter::Noop,
|
212
|
+
circuit_breaker: CircuitBreaker::Noop,
|
213
|
+
wait_seconds: DEFAULT_WAIT_SECONDS,
|
214
|
+
max_wait_seconds: DEFAULT_MAX_WAIT_SECONDS
|
215
|
+
|
216
|
+
|
217
|
+
@client = client
|
218
|
+
@app = app
|
219
|
+
@mapper = mapper
|
220
|
+
@instrumenter = instrumenter
|
221
|
+
@circuit_breaker = circuit_breaker
|
222
|
+
@wait_seconds = wait_seconds
|
223
|
+
@max_wait_seconds = max_wait_seconds
|
224
|
+
|
225
|
+
yield self if block_given?
|
226
|
+
|
227
|
+
validate_args
|
228
|
+
end
|
229
|
+
|
230
|
+
...
|
231
|
+
end
|
232
|
+
end
|
233
|
+
```
|
234
|
+
|
235
|
+
A Throttler instance will make calls to freno on behalf of the given `app`,
|
236
|
+
using the given `client` (an instance of `Freno::Client`).
|
237
|
+
|
238
|
+
You optionally provide the time you want the throttler to sleep in case the check to freno fails, this is `wait_seconds`.
|
239
|
+
|
240
|
+
If replication lags badly, you can control until when you want to keep sleeping
|
241
|
+
and retrying the check by setting `max_wait_seconds`. When that times out, the throttle will raise a `Freno::Throttler::WaitedTooLong` error.
|
242
|
+
|
243
|
+
#### Instrumenting the throttler
|
244
|
+
|
245
|
+
You can also configure the throttler with an `instrumenter` collaborator to subscribe to events happening during the `throttle` call.
|
246
|
+
|
247
|
+
An instrumenter is an object that responds to `instrument(event_name, payload = {})` to receive events from the throttler. One could use `ActiveSupport::Notifications` as an instrumenter and subscribe to "freno.*" events somewhere else in the application, or implement one like the following to push some metrics to a stats system.
|
248
|
+
|
249
|
+
```ruby
|
250
|
+
class StatsInstrumenter
|
251
|
+
|
252
|
+
attr_reader :stats
|
253
|
+
|
254
|
+
def initialize(stats:)
|
255
|
+
@stats = stats
|
256
|
+
end
|
257
|
+
|
258
|
+
def instrument(event_name, payload)
|
259
|
+
method = event_name.sub("throttler.", "")
|
260
|
+
send(method, payload) if respond_to?(method)
|
261
|
+
end
|
262
|
+
|
263
|
+
def called(payload)
|
264
|
+
increment("throttler.called", tags: extract_tags(payload))
|
265
|
+
end
|
266
|
+
|
267
|
+
def waited(payload)
|
268
|
+
stats.histogram("throttler.waited", payload[:waited], tags: extract_tags(payload))
|
269
|
+
end
|
270
|
+
|
271
|
+
...
|
272
|
+
|
273
|
+
def circuit_open(payload)
|
274
|
+
stats.increment("throttler.circuit_open", tags: extract_tags(payload))
|
275
|
+
end
|
276
|
+
|
277
|
+
private
|
278
|
+
|
279
|
+
def extract_tags(payload)
|
280
|
+
cluster_names = payload[:store_names] || []
|
281
|
+
cluster_tags = cluster_names.map{ |cluster_name "cluster:#{cluster_name}" }
|
282
|
+
end
|
283
|
+
end
|
284
|
+
```
|
285
|
+
|
286
|
+
#### Adding resiliency
|
287
|
+
|
288
|
+
The throttler can also receive a `circuit_breaker` object to implement resiliency.
|
289
|
+
|
290
|
+
With that information it receives, the circuit breaker determines whether or not to allow the next request. A circuit is said to be open when the next request is not allowed; and it's said to be closed when the next request is allowed
|
291
|
+
|
292
|
+
If the throttler waited too long, or an unexpected error happened; the circuit breaker will receive a `failure`. If in contrast it succeeded, the circuit breaker will receive a `success` message.
|
293
|
+
|
294
|
+
Once the circuit is open, the throttler will not try to throttle calls, an instead throw a `Freno::Throttler::CircuitOpen`
|
295
|
+
|
296
|
+
The following is a simple per-process circuit breaker implementation:
|
297
|
+
|
298
|
+
```ruby
|
299
|
+
class MemoryCircuitBreaker
|
300
|
+
|
301
|
+
DEFAULT_CIRCUIT_RETRY_INTERVAL = 10
|
302
|
+
|
303
|
+
def initialize(circuit_retry_interval: DEFAULT_CIRCUIT_RETRY_INTERVAL)
|
304
|
+
@circuit_closed = true
|
305
|
+
@last_failure = nil
|
306
|
+
@circuit_retry_interval = circuit_retry_interval
|
307
|
+
end
|
308
|
+
|
309
|
+
def allow_request?
|
310
|
+
@circuit_closed || (Time.now - @last_failure) > @circuit_retry_interval
|
311
|
+
end
|
312
|
+
|
313
|
+
def success
|
314
|
+
@circuit_closed = true
|
315
|
+
end
|
316
|
+
|
317
|
+
def failure
|
318
|
+
@last_failure = Time.now
|
319
|
+
@circuit_closed = false
|
320
|
+
end
|
321
|
+
end
|
322
|
+
```
|
323
|
+
|
324
|
+
#### Flexible throttling strategies
|
325
|
+
|
326
|
+
The throttler uses a `mapper` to determine, based on the context provided to `#throttle`, the clusters which replication delay needs to be checked.
|
327
|
+
|
328
|
+
By default the throttler uses `Mapper::Identity`, which expect the context to be the store name(s) to check:
|
329
|
+
|
330
|
+
```ruby
|
331
|
+
# will check my_cluster's health
|
332
|
+
throttler.throttle(:my_cluster) { ... }
|
333
|
+
# will check the health of cluster_a and cluster_b and throttle if any of them is not OK.
|
334
|
+
throttler.throttle([:cluster_a, :cluster_b]) { ... }
|
335
|
+
```
|
336
|
+
|
337
|
+
You can create your own mapper, which is just an callable object (like a Proc, or any other object that responds to `call(context)`). The following is a mapper that knows how to throttle access to certain tables and shards.
|
338
|
+
|
339
|
+
|
340
|
+
```ruby
|
341
|
+
class ShardMapper
|
342
|
+
def call(context = {})
|
343
|
+
context.map do |table, shards|
|
344
|
+
DatabaseStructure.cluster_for(table, shards)
|
345
|
+
end
|
346
|
+
end
|
347
|
+
end
|
348
|
+
|
349
|
+
throttler = Freno::Throttler.new(client: freno, app: :my_app, mapper: ShardMapper.new)
|
350
|
+
|
351
|
+
throttler.throttle(:users => [1,2,3], :repositories => 5) do
|
352
|
+
perform_writes
|
353
|
+
end
|
354
|
+
```
|
178
355
|
|
179
356
|
## Development
|
180
357
|
|
@@ -193,9 +370,7 @@ If you are the current maintainer of this gem:
|
|
193
370
|
1. Ensure that tests are green: `bundle exec rake test`
|
194
371
|
1. Bump gem version in `lib/freno/client/version.rb`
|
195
372
|
1. Merge a PR to github/freno-client containing the changes in the version file
|
196
|
-
1.
|
197
|
-
1. Build the gem: `gem build freno-client`
|
198
|
-
1. Push to rubygems.org: `gem push freno-client-x.y.z.gem`
|
373
|
+
1. Run `script/release`
|
199
374
|
|
200
375
|
## License
|
201
376
|
|
data/Rakefile
CHANGED
@@ -1,5 +1,26 @@
|
|
1
|
-
|
1
|
+
$LOAD_PATH.push File.expand_path("../lib", __FILE__)
|
2
2
|
require "rake/testtask"
|
3
|
+
require "freno/client/version"
|
4
|
+
|
5
|
+
# gem install pkg/*.gem
|
6
|
+
# gem uninstall freno-client freno-throttler
|
7
|
+
desc "Build gem into the pkg directory"
|
8
|
+
task :build do
|
9
|
+
FileUtils.rm_rf("pkg")
|
10
|
+
Dir["*.gemspec"].each do |gemspec|
|
11
|
+
system "gem build #{gemspec}"
|
12
|
+
end
|
13
|
+
FileUtils.mkdir_p("pkg")
|
14
|
+
FileUtils.mv(Dir["*.gem"], "pkg")
|
15
|
+
end
|
16
|
+
|
17
|
+
desc "Tags version, pushes to remote, and pushes gem"
|
18
|
+
task release: :build do
|
19
|
+
sh "git", "tag", "v#{Freno::Client::VERSION}"
|
20
|
+
sh "git push origin master"
|
21
|
+
sh "git push origin v#{Freno::Client::VERSION}"
|
22
|
+
sh "ls pkg/*.gem | xargs -n 1 gem push"
|
23
|
+
end
|
3
24
|
|
4
25
|
Rake::TestTask.new(:test) do |t|
|
5
26
|
t.libs << "test"
|
data/lib/freno/client/version.rb
CHANGED
@@ -0,0 +1,208 @@
|
|
1
|
+
require "freno/client"
|
2
|
+
require "freno/throttler/errors"
|
3
|
+
require "freno/throttler/mapper"
|
4
|
+
require "freno/throttler/instrumenter"
|
5
|
+
require "freno/throttler/circuit_breaker"
|
6
|
+
|
7
|
+
module Freno
|
8
|
+
|
9
|
+
# Freno::Throttler is the class responsible for throttling writes to a cluster
|
10
|
+
# or a set of clusters. Throttling means to slow down the pace at which write
|
11
|
+
# operations occur by checking with freno whether all the clusters affected by
|
12
|
+
# the operation are in good health before allowing it. If any of the clusters
|
13
|
+
# is not in good health, the throttler will wait some time and repeat the
|
14
|
+
# process.
|
15
|
+
#
|
16
|
+
# Examples:
|
17
|
+
#
|
18
|
+
# Let's use the following throttler, which uses Mapper::Identity implicitly.
|
19
|
+
# (see #initialze docs)
|
20
|
+
#
|
21
|
+
# ```
|
22
|
+
# throttler = Throttler.new(client: freno_client, app: :my_app)
|
23
|
+
# data.find_in_batches do |batch|
|
24
|
+
# throttler.throttle([:mysqla, :mysqlb]) do
|
25
|
+
# update(batch)
|
26
|
+
# end
|
27
|
+
# end
|
28
|
+
# ```
|
29
|
+
#
|
30
|
+
# Before each call to `update(batch)` the throttler will call freno to
|
31
|
+
# check the health of the `mysqla` and `mysqlb` stores on behalf of :my_app;
|
32
|
+
# and sleep if any of the stores is not ok.
|
33
|
+
#
|
34
|
+
class Throttler
|
35
|
+
|
36
|
+
DEFAULT_WAIT_SECONDS = 0.5
|
37
|
+
DEFAULT_MAX_WAIT_SECONDS = 10
|
38
|
+
|
39
|
+
attr_accessor :client,
|
40
|
+
:app,
|
41
|
+
:mapper,
|
42
|
+
:instrumenter,
|
43
|
+
:circuit_breaker,
|
44
|
+
:wait_seconds,
|
45
|
+
:max_wait_seconds
|
46
|
+
|
47
|
+
# Initializes a new instance of the throttler
|
48
|
+
#
|
49
|
+
# In order to initialize a Throttler you need the following arguments:
|
50
|
+
#
|
51
|
+
# - a `client`: a instance of Freno::Client
|
52
|
+
#
|
53
|
+
# - an `app`: a symbol indicating the app-name for which Freno will respond
|
54
|
+
# checks.
|
55
|
+
#
|
56
|
+
# Also, you can optionally provide the following named arguments:
|
57
|
+
#
|
58
|
+
# - `:mapper`: An object that responds to `call(context)` and returns a
|
59
|
+
# `Enumerable` of the store names for which we need to wait for
|
60
|
+
# replication delay. By default this is the `IdentityMapper`, which will
|
61
|
+
# check the stores given as context.
|
62
|
+
#
|
63
|
+
# For example, if the `throttler` object used the default mapper:
|
64
|
+
#
|
65
|
+
# ```
|
66
|
+
# throttler.throttle(:mysqlc) do
|
67
|
+
# update(batch)
|
68
|
+
# end
|
69
|
+
# ```
|
70
|
+
#
|
71
|
+
# - `:instrumenter`: An object that responds to
|
72
|
+
# `instrument(event_name, context = {}, &block)` that can be used to
|
73
|
+
# add cross-cutting concerns like logging or stats to the throttler.
|
74
|
+
#
|
75
|
+
# By default, the instrumenter is `Instrumenter::Noop`, which does
|
76
|
+
# nothing but yielding the block it receives.
|
77
|
+
#
|
78
|
+
# - `:circuit_breaker`: An object responding to `allow_request?`,
|
79
|
+
# `success`, and `failure?`, compatible with `Resilient::CircuitBreaker`
|
80
|
+
# (see https://github.com/jnunemaker/resilient).
|
81
|
+
#
|
82
|
+
# By default, the circuit breaker is `CircuitBreaker::Noop`, which
|
83
|
+
# always allows requests, and does not provide resiliency guarantees.
|
84
|
+
#
|
85
|
+
# - `:wait_seconds`: A positive float indicating the number of seconds the
|
86
|
+
# throttler will wait before checking again, in case some of the stores
|
87
|
+
# didn't catch-up the last time they were check.
|
88
|
+
#
|
89
|
+
# - `:max_wait_seconds`: A positive float indicating the maxium number of
|
90
|
+
# seconds the throttler will wait in total for replicas to catch-up
|
91
|
+
# before raising a `WaitedTooLong` error.
|
92
|
+
#
|
93
|
+
def initialize(client: nil,
|
94
|
+
app: nil,
|
95
|
+
mapper: Mapper::Identity,
|
96
|
+
instrumenter: Instrumenter::Noop,
|
97
|
+
circuit_breaker: CircuitBreaker::Noop,
|
98
|
+
wait_seconds: DEFAULT_WAIT_SECONDS,
|
99
|
+
max_wait_seconds: DEFAULT_MAX_WAIT_SECONDS)
|
100
|
+
|
101
|
+
@client = client
|
102
|
+
@app = app
|
103
|
+
@mapper = mapper
|
104
|
+
@instrumenter = instrumenter
|
105
|
+
@circuit_breaker = circuit_breaker
|
106
|
+
@wait_seconds = wait_seconds
|
107
|
+
@max_wait_seconds = max_wait_seconds
|
108
|
+
|
109
|
+
yield self if block_given?
|
110
|
+
|
111
|
+
validate_args
|
112
|
+
end
|
113
|
+
|
114
|
+
# This method receives a context to infer the set of stores that it needs to
|
115
|
+
# throttle writes to.
|
116
|
+
#
|
117
|
+
# With that information it asks freno whether all the stores are ok.
|
118
|
+
# In case they are, it executes the given block.
|
119
|
+
# Otherwise, it waits `wait_seconds` before trying again.
|
120
|
+
#
|
121
|
+
# In case the throttler has waited more than `max_wait_seconds`, it raises
|
122
|
+
# a `WaitedTooLong` error.
|
123
|
+
#
|
124
|
+
# In case there's an underlying Freno error, it raises a `ClientError`
|
125
|
+
# error.
|
126
|
+
#
|
127
|
+
# In case the circuit breaker is open, it raises a `CircuitOpen` error.
|
128
|
+
#
|
129
|
+
# this method is instrumented, the instrumenter will receive the following
|
130
|
+
# events:
|
131
|
+
#
|
132
|
+
# - "throttler.called" each time this method is called
|
133
|
+
# - "throttler.succeeded" when the stores were ok, before yielding the block
|
134
|
+
# - "throttler.waited" when the stores were not ok, after waiting
|
135
|
+
# `wait_seconds`
|
136
|
+
# - "throttler.waited_too_long" when the stores were not ok, but the
|
137
|
+
# thottler already waited at least `max_wait_seconds`, right before
|
138
|
+
# raising `WaitedTooLong`
|
139
|
+
# - "throttler.freno_errored" when there was an error with freno, before
|
140
|
+
# raising `ClientError`.
|
141
|
+
# - "throttler.circuit_open" when the circuit breaker does not allow the
|
142
|
+
# next request, before raising `CircuitOpen`
|
143
|
+
#
|
144
|
+
def throttle(context = nil)
|
145
|
+
store_names = mapper.call(context)
|
146
|
+
instrument(:called, store_names: store_names)
|
147
|
+
waited = 0
|
148
|
+
|
149
|
+
while true do # rubocop:disable Lint/LiteralInCondition
|
150
|
+
unless circuit_breaker.allow_request?
|
151
|
+
instrument(:circuit_open, store_names: store_names, waited: waited)
|
152
|
+
raise CircuitOpen
|
153
|
+
end
|
154
|
+
|
155
|
+
if all_stores_ok?(store_names)
|
156
|
+
instrument(:succeeded, store_names: store_names, waited: waited)
|
157
|
+
circuit_breaker.success
|
158
|
+
return yield
|
159
|
+
end
|
160
|
+
|
161
|
+
wait
|
162
|
+
waited += wait_seconds
|
163
|
+
instrument(:waited, store_names: store_names, waited: waited, max: max_wait_seconds)
|
164
|
+
|
165
|
+
if waited > max_wait_seconds
|
166
|
+
instrument(:waited_too_long, store_names: store_names, waited: waited, max: max_wait_seconds)
|
167
|
+
circuit_breaker.failure
|
168
|
+
raise WaitedTooLong.new(waited_seconds: waited, max_wait_seconds: max_wait_seconds)
|
169
|
+
end
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
private
|
174
|
+
|
175
|
+
def validate_args
|
176
|
+
errors = []
|
177
|
+
|
178
|
+
%i(client app mapper instrumenter circuit_breaker
|
179
|
+
wait_seconds max_wait_seconds).each do |argument|
|
180
|
+
errors << "#{argument} must be provided" unless send(argument)
|
181
|
+
end
|
182
|
+
|
183
|
+
unless max_wait_seconds > wait_seconds
|
184
|
+
errors << "max_wait_seconds (#{max_wait_seconds}) has to be greather than wait_seconds (#{wait_seconds})"
|
185
|
+
end
|
186
|
+
|
187
|
+
raise ArgumentError.new(errors.join("\n")) if errors.any?
|
188
|
+
end
|
189
|
+
|
190
|
+
def all_stores_ok?(store_names)
|
191
|
+
store_names.all? do |store_name|
|
192
|
+
client.check?(app: app, store_name: store_name)
|
193
|
+
end
|
194
|
+
rescue Freno::Error => e
|
195
|
+
instrument(:freno_errored, store_names: store_names, error: e)
|
196
|
+
circuit_breaker.failure
|
197
|
+
raise ClientError.new(e)
|
198
|
+
end
|
199
|
+
|
200
|
+
def wait
|
201
|
+
sleep wait_seconds
|
202
|
+
end
|
203
|
+
|
204
|
+
def instrument(event_name, payload = {}, &block)
|
205
|
+
instrumenter.instrument("throttler.#{event_name}", payload, &block)
|
206
|
+
end
|
207
|
+
end
|
208
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module Freno
|
2
|
+
class Throttler
|
3
|
+
|
4
|
+
# A CircuitBreaker is the entry point of the pattern with same name.
|
5
|
+
# (see https://martinfowler.com/bliki/CircuitBreaker.html)
|
6
|
+
#
|
7
|
+
# Clients that use circuit breakers to add resiliency to their processes
|
8
|
+
# send `failure` or `sucess` messages to the CircuitBreaker depending on the
|
9
|
+
# results of the last requests made.
|
10
|
+
#
|
11
|
+
# With that information, the circuit breaker determines whether or not to
|
12
|
+
# allow the next request (`allow_request?`). A circuit is said to be open
|
13
|
+
# when the next request is not allowed; and it's said to be closed when the
|
14
|
+
# next request is allowed.
|
15
|
+
#
|
16
|
+
module CircuitBreaker
|
17
|
+
|
18
|
+
# The Noop circuit breaker is the `:circuit_breaker` used by default in
|
19
|
+
# the Throttler
|
20
|
+
#
|
21
|
+
# It always allows requests, and does nothing when given `success` or
|
22
|
+
# `failure` messages. For that reason it doesn't provide any resiliency
|
23
|
+
# guarantee.
|
24
|
+
#
|
25
|
+
# See https://github.com/jnunemaker/resilient for a real ruby implementation
|
26
|
+
# of the CircuitBreaker pattern.
|
27
|
+
#
|
28
|
+
class Noop
|
29
|
+
def self.allow_request?
|
30
|
+
true
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.success; end
|
34
|
+
|
35
|
+
def self.failure; end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
require "freno/client"
|
2
|
+
|
3
|
+
module Freno
|
4
|
+
class Throttler
|
5
|
+
|
6
|
+
# Any throttler-related error.
|
7
|
+
class Error < Freno::Error; end
|
8
|
+
|
9
|
+
# Raised if the throttler has waited too long for replication delay
|
10
|
+
# to catch up.
|
11
|
+
class WaitedTooLong < Error
|
12
|
+
def initialize(waited_seconds: DEFAULT_WAIT_SECONDS, max_wait_seconds: DEFAULT_MAX_WAIT_SECONDS)
|
13
|
+
super("Waited #{waited_seconds} seconds. Max allowed was #{max_wait_seconds} seconds")
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
# Raised if the circuit breaker is open and didn't allow latest request
|
18
|
+
class CircuitOpen < Error; end
|
19
|
+
|
20
|
+
# Raised if the freno client errored.
|
21
|
+
class ClientError < Error; end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module Freno
|
2
|
+
class Throttler
|
3
|
+
|
4
|
+
# An Instrumenter is an object that responds to
|
5
|
+
# `instrument(event_name, payload = {})` to receive events from the
|
6
|
+
# throttler.
|
7
|
+
#
|
8
|
+
# As an example, in a rails app one could use ActiveSupport::Notifications
|
9
|
+
# as an instrumenter and subscribe to the "freno.*" events somewhere else in
|
10
|
+
# the application.
|
11
|
+
#
|
12
|
+
module Instrumenter
|
13
|
+
|
14
|
+
# The Noop instrumenter is the `:instrumenter` used by default in the
|
15
|
+
# Throttler
|
16
|
+
#
|
17
|
+
# It does nothing but yielding the control to the block given if it is
|
18
|
+
# provided.
|
19
|
+
#
|
20
|
+
class Noop
|
21
|
+
def self.instrument(event_name, payload = {})
|
22
|
+
yield payload if block_given?
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
module Freno
|
2
|
+
class Throttler
|
3
|
+
|
4
|
+
# A Mapper is any object that responds to `call`, by receiving a context
|
5
|
+
# object and returning a list of strings, each of which corresponds to the
|
6
|
+
# store_name that will be checked in freno.
|
7
|
+
#
|
8
|
+
# See https://github.com/github/freno/blob/master/doc/http.md#client-requests
|
9
|
+
# for more context.
|
10
|
+
#
|
11
|
+
# As an example we could use a mapper that will receive as a context a set
|
12
|
+
# of [table, shard_id] tuples, and could return the list of all the stores
|
13
|
+
# where that shards exist.
|
14
|
+
#
|
15
|
+
module Mapper
|
16
|
+
|
17
|
+
# The Identity mapper is the one used by default in the Throttler.
|
18
|
+
#
|
19
|
+
# It works by informing the throttler to check exact same stores that it
|
20
|
+
# receives as context, without any translation.
|
21
|
+
#
|
22
|
+
# Let's use the following throttler, which uses Mapper::Identity
|
23
|
+
# implicitly.
|
24
|
+
#
|
25
|
+
# ```ruby
|
26
|
+
# throttler = Throttler.new(client: freno_client, app: :my_app)
|
27
|
+
# data.find_in_batches do |batch|
|
28
|
+
# throttler.throttle([:mysqla, :mysqlb]) do
|
29
|
+
# update(batch)
|
30
|
+
# end
|
31
|
+
# end
|
32
|
+
# ```
|
33
|
+
#
|
34
|
+
# Before each call to `update(batch)` the throttler will call freno to
|
35
|
+
# check the health of `mysqla` and `mysqlb`. And sleep if any of them is
|
36
|
+
# not ok.
|
37
|
+
#
|
38
|
+
class Identity
|
39
|
+
def self.call(context)
|
40
|
+
Array(context)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
data/script/release
ADDED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: freno-client
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.6.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Miguel Fernández
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2017-
|
11
|
+
date: 2017-10-09 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -97,10 +97,16 @@ files:
|
|
97
97
|
- lib/freno/client/requests/replication_delay.rb
|
98
98
|
- lib/freno/client/result.rb
|
99
99
|
- lib/freno/client/version.rb
|
100
|
+
- lib/freno/throttler.rb
|
101
|
+
- lib/freno/throttler/circuit_breaker.rb
|
102
|
+
- lib/freno/throttler/errors.rb
|
103
|
+
- lib/freno/throttler/instrumenter.rb
|
104
|
+
- lib/freno/throttler/mapper.rb
|
100
105
|
- script/bootstrap
|
101
106
|
- script/cibuild
|
102
107
|
- script/cibuild-lint
|
103
108
|
- script/console
|
109
|
+
- script/release
|
104
110
|
homepage: https://github.com/github/freno-client
|
105
111
|
licenses:
|
106
112
|
- MIT
|