freno-client 0.4.0 → 0.6.0

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
  SHA1:
3
- metadata.gz: fe5c746dd4f13a74f1d005ef02525980186175a2
4
- data.tar.gz: b77184aa9a5e0a98116e6444d61405cf2f0a5cc2
3
+ metadata.gz: bf297a41a22996c917599c6bc5835a8abb482d09
4
+ data.tar.gz: 1a45ae21c0e31b7fda7769b78c3fcc8d3d6b4bf5
5
5
  SHA512:
6
- metadata.gz: e969f338ff696426546ea5c1906f8665b77ba8088aa1ed7146b8e9e4d701e42b779b8834ca92060e0c4bd322e9c7ccbaf4405bea7cde6abe0a7177a91c12fd24
7
- data.tar.gz: 7a2f7982b3027e94a4bf02b340a8cf538f7f26cfd8cbd1916591de9083afec49ccd4a7752634ca85bdf29805ddb7855da01eade6d3cd254c8a21e0182eec77a8
6
+ metadata.gz: 3f6ab33f6c2008972cc3eea6e8611d1aca5e93c64d318211ee7e44d7066cc6cc73f38a97ea2d398e3738e6f35663d8c1dbc6bcd0fb6fb27dc0fe6e7502ea120c
7
+ data.tar.gz: 9bc8e0b6a8531768b77fc7b6b6b0edd99c9d9f1c0c0bc1d50264806ced07ba960de438a7f1f9f3c1c96309c79a4edf0ee95892a84ad610b0933b456d8d114cbe
@@ -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
 
@@ -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-clientsitory
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
@@ -3,5 +3,6 @@ source "https://rubygems.org"
3
3
  gemspec
4
4
 
5
5
  group :test do
6
+ gem "mocha"
6
7
  gem "rubocop", require: false
7
8
  end
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Freno Client [![Build Status](https://travis-ci.org/github/freno-client.svg)](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. Tag and push: `git tag vx.xx.xx; git push --tags`
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
- require "bundler/gem_tasks"
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"
@@ -1,5 +1,5 @@
1
1
  module Freno
2
2
  class Client
3
- VERSION = "0.4.0"
3
+ VERSION = "0.6.0"
4
4
  end
5
5
  end
@@ -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
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle exec rake release
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.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-08-29 00:00:00.000000000 Z
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