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