pause 0.4.0 → 0.5.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/workflows/rspec.yml +31 -0
- data/.github/workflows/rubocop.yml +31 -0
- data/.gitignore +0 -1
- data/.rubocop.yml +35 -0
- data/.rubocop_todo.yml +192 -0
- data/CHANGELOG.md +21 -0
- data/Gemfile +15 -1
- data/Gemfile.lock +96 -0
- data/LICENSE.txt +1 -1
- data/README.md +94 -48
- data/Rakefile +11 -10
- data/bin/spec +14 -0
- data/lib/pause/action.rb +7 -6
- data/lib/pause/analyzer.rb +4 -0
- data/lib/pause/configuration.rb +3 -1
- data/lib/pause/helper/timing.rb +2 -0
- data/lib/pause/logger.rb +12 -5
- data/lib/pause/rate_limited_event.rb +2 -1
- data/lib/pause/redis/adapter.rb +12 -10
- data/lib/pause/redis/sharded_adapter.rb +3 -2
- data/lib/pause/version.rb +3 -1
- data/lib/pause.rb +25 -14
- data/pause.gemspec +14 -17
- data/spec/pause/action_spec.rb +48 -42
- data/spec/pause/analyzer_spec.rb +13 -10
- data/spec/pause/configuration_spec.rb +6 -5
- data/spec/pause/logger_spec.rb +25 -0
- data/spec/pause/pause_spec.rb +6 -4
- data/spec/pause/period_check_spec.rb +53 -0
- data/spec/pause/redis/adapter_spec.rb +54 -35
- data/spec/pause/redis/sharded_adapter_spec.rb +10 -10
- data/spec/spec_helper.rb +11 -7
- metadata +21 -121
- data/.travis.yml +0 -14
- data/Guardfile +0 -15
data/README.md
CHANGED
@@ -1,11 +1,17 @@
|
|
1
|
-
# Pause
|
2
1
|
|
3
|
-
[](https://github.com/kigster/pause/actions/workflows/rspec.yml)
|
3
|
+
[](https://github.com/kigster/pause/actions/workflows/rubocop.yml)
|
5
4
|
|
6
|
-
|
5
|
+
[](https://badge.fury.io/rb/pause)
|
7
6
|
|
8
|
-
|
7
|
+
[](https://rubygems.org/gems/pause)
|
8
|
+
[](https://opensource.org/licenses/MIT)
|
9
|
+
|
10
|
+
# Pause — A Redis-backed Rate Limiter
|
11
|
+
|
12
|
+
## Overview
|
13
|
+
|
14
|
+
**Pause** is a fast and very flexible Redis-backed rate-limiter, written originally at [Wanelo.com](https://www.crunchbase.com/organization/wanelo). You can use it to track events, with
|
9
15
|
rules around how often they are allowed to occur within configured time checks.
|
10
16
|
|
11
17
|
Sample applications include:
|
@@ -49,17 +55,21 @@ Pause.configure do |config|
|
|
49
55
|
config.redis_host = '127.0.0.1'
|
50
56
|
config.redis_port = 6379
|
51
57
|
config.redis_db = 1
|
52
|
-
|
53
|
-
#
|
54
|
-
# Larger blocks require less RAM and CPU, smaller blocks are more
|
55
|
-
# computationally expensive.
|
56
|
-
config.resolution = 600
|
57
|
-
|
58
|
-
# discard all events older than 1 day
|
59
|
-
config.history = 86400
|
58
|
+
config.resolution = 600
|
59
|
+
config.history = 7 * 86400 # discard events older than 7 days
|
60
60
|
end
|
61
61
|
```
|
62
62
|
|
63
|
+
> NOTE: **resolution** is an setting that's key to understanding how Pause works. It represents the length of time during which similar events are aggregated into a Hash-like object, where the key is the identifier, and the value is the count within that period.
|
64
|
+
>
|
65
|
+
> Because of this,
|
66
|
+
>
|
67
|
+
> * _Larger resolution requires less RAM and CPU and are faster to compute_
|
68
|
+
> * _Smaller resolution is more computationally expensive, but provides higher granularity_.
|
69
|
+
>
|
70
|
+
> The resolution setting must set to the smallest rate-limit period across all of your checks. Below it is set to 10 minutes, meaning that you can use Pause to **rate limit any event to no more than N times within a period of 10 minutes or more.**
|
71
|
+
|
72
|
+
|
63
73
|
#### Define Rate Limited "Action"
|
64
74
|
|
65
75
|
Next we must define the rate limited action based on the specification above. This is how easy it is:
|
@@ -69,13 +79,22 @@ module MyApp
|
|
69
79
|
class UserNotificationLimiter < ::Pause::Action
|
70
80
|
# this is a redis key namespace added to all data in this action
|
71
81
|
scope 'un'
|
72
|
-
|
73
|
-
check period_seconds:
|
74
|
-
|
82
|
+
|
83
|
+
check period_seconds: 120,
|
84
|
+
max_allowed: 1,
|
85
|
+
block_ttl: 240
|
86
|
+
|
87
|
+
check period_seconds: 86400,
|
88
|
+
max_allowed: 3
|
89
|
+
|
90
|
+
check period_seconds: 7 * 86400,
|
91
|
+
max_allowed: 7
|
75
92
|
end
|
76
93
|
end
|
77
94
|
```
|
78
95
|
|
96
|
+
> NOTE: for each check, `block_ttl` defaults to `period_seconds`, and represents the duration of time the action will consider itself as "rate limited" after a particular check reaches the limit. Note, that all actions will automatically leave the "rate limited" state after `block_ttl` seconds have passed.
|
97
|
+
|
79
98
|
#### Perform operation, but only if the user is not rate-limited
|
80
99
|
|
81
100
|
Now we simply instantiate this limiter by passing user ID (any unique identifier works). We can then ask the limiter, `ok?` or `rate_limited?`, or we can use two convenient methods that only execute enclosed block if the described condition is satisfied:
|
@@ -83,17 +102,18 @@ Now we simply instantiate this limiter by passing user ID (any unique identifier
|
|
83
102
|
```ruby
|
84
103
|
class NotificationsWorker
|
85
104
|
def perform(user_id)
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
105
|
+
MyApp::UserNotificationLimiter.new(user_id) do
|
106
|
+
unless_rate_limited do
|
107
|
+
# this block ONLY runs if rate limit is not reached
|
108
|
+
user = User.find(user_id)
|
109
|
+
PushNotifications.new(user).send_push_notification!
|
110
|
+
end
|
111
|
+
|
112
|
+
if_rate_limited do |rate_limit_event|
|
113
|
+
# this block ONLY runs if the action has reached it's rate limit.
|
114
|
+
Rails.logger.info("user #{user.id} has exceeded rate limit: #{rate_limit_event}")
|
115
|
+
end
|
116
|
+
end
|
97
117
|
end
|
98
118
|
end
|
99
119
|
```
|
@@ -119,12 +139,17 @@ Or install it yourself as:
|
|
119
139
|
|
120
140
|
### Configuration
|
121
141
|
|
122
|
-
Configure Pause. This could be in a Rails initializer.
|
142
|
+
Configure Pause. This could be in a Rails initializer. At the moment, pause can only be used as a singleton, i.e. you can not use `pause` with multiple configurations, or multiple redis backends. This is something that can be rather easily fixed, if necessary.
|
123
143
|
|
124
|
-
|
144
|
+
Therefore, you configure the Pause singleton with the following options:
|
145
|
+
|
146
|
+
* **redis connection parameters** - The host, port and db of the Redis instance to use.
|
147
|
+
|
148
|
+
* **resolution** - the length of time (in seconds) defining the minimum period into which action counts are
|
125
149
|
aggregated. This defines the size of the persistent store. The higher the number, the less data needs
|
126
150
|
to be persisted in Redis.
|
127
|
-
|
151
|
+
|
152
|
+
* **history** - The maximum amount of time (in seconds) that data is persisted.
|
128
153
|
|
129
154
|
```ruby
|
130
155
|
Pause.configure do |config|
|
@@ -136,12 +161,10 @@ Pause.configure do |config|
|
|
136
161
|
end
|
137
162
|
```
|
138
163
|
|
164
|
+
|
139
165
|
### Actions
|
140
166
|
|
141
|
-
Define local actions for your application. Actions define a scope by
|
142
|
-
which they are identified in the persistent store (aka "namespace"), and a set of checks. Checks define various
|
143
|
-
thresholds (`max_allowed`) against periods of time (`period_seconds`). When a threshold it triggered,
|
144
|
-
the action is rate limited, and stays rate limited for the duration of `block_ttl` seconds.
|
167
|
+
Define local actions for your application. Actions define a scope by which they are identified in the persistent store (aka "namespace"), and a set of checks. Checks define various thresholds (`max_allowed`) against periods of time (`period_seconds`). When a threshold it triggered, the action is rate limited, and stays rate limited for the duration of `block_ttl` seconds.
|
145
168
|
|
146
169
|
#### Checks
|
147
170
|
|
@@ -185,6 +208,7 @@ When an event occurs, you increment an instance of your action, optionally with
|
|
185
208
|
In the example at the top of the README you saw how we used `#unless_rate_limited` and `#if_rate_limited` methods. These are the recommended API methods, but if you must get a finer-grained control over the actions, you can also use methods such as `#ok?`, `#rate_limited?`, `#increment!` to do manually what the block methods do already. Below is an example of this "manual" implementation:
|
186
209
|
|
187
210
|
```ruby
|
211
|
+
# app/controllers/follows_controller.rb
|
188
212
|
class FollowsController < ApplicationController
|
189
213
|
def create
|
190
214
|
action = FollowAction.new(user.id)
|
@@ -196,6 +220,7 @@ class FollowsController < ApplicationController
|
|
196
220
|
end
|
197
221
|
end
|
198
222
|
|
223
|
+
# app/controlers/other_controller.rb
|
199
224
|
class OtherController < ApplicationController
|
200
225
|
def index
|
201
226
|
action = OtherAction.new(params[:thing])d
|
@@ -217,11 +242,16 @@ while true
|
|
217
242
|
action.increment!
|
218
243
|
|
219
244
|
rate_limit_event = action.analyze
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
245
|
+
|
246
|
+
if rate_limit_event # or action.ok?
|
247
|
+
# which key got rate limited ("thing")
|
248
|
+
rate_limit_event.identifier
|
249
|
+
# total count that triggered a rate limit
|
250
|
+
rate_limit_event.sum
|
251
|
+
# timestamp when rate limiting occurred
|
252
|
+
rate_limit_event.timestamp
|
253
|
+
# period check object, that triggered this rate limiting event
|
254
|
+
rate_limit_event.period_check
|
225
255
|
else
|
226
256
|
# not rate-limited, same as action.ok?
|
227
257
|
end
|
@@ -301,21 +331,19 @@ Pause.configure do |config|
|
|
301
331
|
end
|
302
332
|
```
|
303
333
|
|
304
|
-
With this configuration, any Pause operation that we know is not supported by Twemproxy will raise
|
305
|
-
`Pause::Redis::OperationNotSupported`. For instance, when sharding we are unable to get a list of all
|
306
|
-
tracked identifiers.
|
334
|
+
With this configuration, any Pause operation that we know is not supported by Twemproxy will raise `Pause::Redis::OperationNotSupported`. For instance, when sharding we are unable to get a list of all tracked identifiers.
|
307
335
|
|
308
336
|
The action block list is implemented as a sorted set, so it should still be usable when sharding.
|
309
337
|
|
310
338
|
## Testing
|
311
339
|
|
312
|
-
By default, `fakeredis` gem is used to emulate Redis in development.
|
340
|
+
By default, `fakeredis` gem is used to emulate Redis in development.
|
313
341
|
|
314
|
-
|
342
|
+
However, the same test-suite runs against a real redis, just be aware that using real redis server running locally on 127.0.0.1 will result in a `flush` operation on the current redis db during the spec run. In order to run specs against real redis, please make sure you have Redis running locally on the default port (6379), and that you are able to connect to it using `redis-cli -p 6379 -h 127.0.0.1`.
|
315
343
|
|
316
|
-
### Unit Testing with
|
344
|
+
### Unit Testing with FakeRedis
|
317
345
|
|
318
|
-
|
346
|
+
The gem `fakeredis` is the default for testing, and is also run whenever `bundle exec rspec` is executed, or `rake spec` task invoked.
|
319
347
|
|
320
348
|
```bash
|
321
349
|
bundle exec rake spec:unit
|
@@ -327,6 +355,16 @@ bundle exec rake spec:unit
|
|
327
355
|
bundle exec rake spec:integration
|
328
356
|
```
|
329
357
|
|
358
|
+
OR
|
359
|
+
|
360
|
+
```bash
|
361
|
+
PAUSE_REAL_REDIS=1 bundle exec rspec
|
362
|
+
```
|
363
|
+
|
364
|
+
### Test Coverage
|
365
|
+
|
366
|
+
At the time of this writing the gem has 100% test coverage. Please keep it that way ;)
|
367
|
+
|
330
368
|
## Contributing
|
331
369
|
|
332
370
|
Want to make it better? Cool. Here's how:
|
@@ -339,8 +377,16 @@ Want to make it better? Cool. Here's how:
|
|
339
377
|
|
340
378
|
## Authors
|
341
379
|
|
342
|
-
This gem was
|
380
|
+
* This gem was donated to Open Source by [Wanelo, Inc](https://www.crunchbase.com/organization/wanelo)
|
381
|
+
|
382
|
+
* The original authors are:
|
383
|
+
* [Atasay Gökkaya](https://github.com/atasay)
|
384
|
+
* [Konstantin Gredeskoul](https://github.com/kigster)
|
385
|
+
* [Eric Saxby](https://github.com/sax)
|
386
|
+
* Paul Henry
|
387
|
+
|
388
|
+
* The gem is currently maintained and kept up to date by [Konstantin Gredeskoul](https://kig.re/)
|
343
389
|
|
344
|
-
Please see the LICENSE.txt file for further details.
|
390
|
+
Please see the [LICENSE.txt](LICENSE.txt) file for further details.
|
345
391
|
|
346
392
|
|
data/Rakefile
CHANGED
@@ -1,10 +1,12 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'bundler/gem_tasks'
|
2
4
|
require 'rspec/core/rake_task'
|
3
5
|
require 'yard'
|
4
6
|
|
5
7
|
RSpec::Core::RakeTask.new(:spec)
|
6
8
|
|
7
|
-
task :
|
9
|
+
task default: %w[spec:unit spec:integration]
|
8
10
|
|
9
11
|
namespace :spec do
|
10
12
|
desc 'Run specs using fakeredis'
|
@@ -25,23 +27,22 @@ def shell(*args)
|
|
25
27
|
end
|
26
28
|
|
27
29
|
task :clean do
|
28
|
-
shell('rm -rf pkg/ tmp/ coverage/ doc/ '
|
30
|
+
shell('rm -rf pkg/ tmp/ coverage/ doc/ ')
|
29
31
|
end
|
30
32
|
|
31
|
-
task :
|
33
|
+
task gem: [:build] do
|
32
34
|
shell('gem install pkg/*')
|
33
35
|
end
|
34
36
|
|
35
|
-
task :
|
37
|
+
task permissions: [:clean] do
|
36
38
|
shell('chmod -v o+r,g+r * */* */*/* */*/*/* */*/*/*/* */*/*/*/*/*')
|
37
|
-
shell(
|
39
|
+
shell('find . -type d -exec chmod o+x,g+x {} \\;')
|
38
40
|
end
|
39
41
|
|
40
|
-
task :
|
42
|
+
task build: :permissions
|
41
43
|
|
42
44
|
YARD::Rake::YardocTask.new(:doc) do |t|
|
43
|
-
t.files = %w
|
44
|
-
t.options.unshift('--title','"Pause - Redis-backed Rate Limiter"')
|
45
|
-
t.after = ->
|
45
|
+
t.files = %w[lib/**/*.rb exe/*.rb - README.md LICENSE.txt]
|
46
|
+
t.options.unshift('--title', '"Pause - Redis-backed Rate Limiter"')
|
47
|
+
t.after = -> { exec('open doc/index.html') }
|
46
48
|
end
|
47
|
-
|
data/bin/spec
ADDED
data/lib/pause/action.rb
CHANGED
@@ -1,10 +1,13 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Pause
|
2
4
|
class Action
|
3
5
|
attr_accessor :identifier
|
4
6
|
|
5
|
-
def initialize(identifier)
|
6
|
-
@identifier
|
7
|
+
def initialize(identifier, &block)
|
8
|
+
@identifier = identifier
|
7
9
|
self.class.checks ||= []
|
10
|
+
instance_exec(&block) if block
|
8
11
|
end
|
9
12
|
|
10
13
|
def scope
|
@@ -22,9 +25,8 @@ module Pause
|
|
22
25
|
# scope "my:scope"
|
23
26
|
# end
|
24
27
|
#
|
25
|
-
@scope = klass.name.downcase.gsub(
|
28
|
+
@scope = klass.name.downcase.gsub('::', '.')
|
26
29
|
class << self
|
27
|
-
|
28
30
|
# @param [String] args
|
29
31
|
def scope(*args)
|
30
32
|
@scope = args.first if args && args.size == 1
|
@@ -115,7 +117,7 @@ module Pause
|
|
115
117
|
end
|
116
118
|
end
|
117
119
|
|
118
|
-
def if_rate_limited(&
|
120
|
+
def if_rate_limited(&)
|
119
121
|
check_result = analyze(recalculate: true)
|
120
122
|
yield(check_result) unless check_result.nil?
|
121
123
|
end
|
@@ -156,6 +158,5 @@ module Pause
|
|
156
158
|
def adapter
|
157
159
|
self.class.adapter
|
158
160
|
end
|
159
|
-
|
160
161
|
end
|
161
162
|
end
|
data/lib/pause/analyzer.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'pause/helper/timing'
|
2
4
|
|
3
5
|
module Pause
|
@@ -12,12 +14,14 @@ module Pause
|
|
12
14
|
# @return [Pause::RateLimitedEvent] the action was blocked as a result of this check
|
13
15
|
def check(action, recalculate: false)
|
14
16
|
return false if adapter.rate_limited?(action.scope, action.identifier) && !recalculate
|
17
|
+
|
15
18
|
timestamp = period_marker(Pause.config.resolution, Time.now.to_i)
|
16
19
|
set = adapter.key_history(action.scope, action.identifier)
|
17
20
|
action.checks.each do |period_check|
|
18
21
|
start_time = timestamp - period_check.period_seconds
|
19
22
|
set.reverse.inject(0) do |sum, element|
|
20
23
|
break if element.ts < start_time
|
24
|
+
|
21
25
|
sum += element.count
|
22
26
|
if sum >= period_check.max_allowed
|
23
27
|
adapter.rate_limit!(action.scope, action.identifier, period_check.block_ttl)
|
data/lib/pause/configuration.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Pause
|
2
4
|
class Configuration
|
3
5
|
attr_writer :redis_host, :redis_port, :redis_db, :resolution, :history, :sharded
|
@@ -24,7 +26,7 @@ module Pause
|
|
24
26
|
end
|
25
27
|
|
26
28
|
def history
|
27
|
-
(@history ||
|
29
|
+
(@history || 86_400).to_i
|
28
30
|
end
|
29
31
|
|
30
32
|
def sharded
|
data/lib/pause/helper/timing.rb
CHANGED
data/lib/pause/logger.rb
CHANGED
@@ -1,11 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Pause
|
4
|
+
# @description Logger class for Pause
|
2
5
|
class Logger
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
+
class << self
|
7
|
+
def puts(message)
|
8
|
+
$stdout.puts message
|
9
|
+
end
|
6
10
|
|
7
|
-
|
8
|
-
|
11
|
+
def fatal(message)
|
12
|
+
# rubocop: disable Style/StderrPuts
|
13
|
+
$stderr.puts message.red
|
14
|
+
# rubocop: enable Style/StderrPuts
|
15
|
+
end
|
9
16
|
end
|
10
17
|
end
|
11
18
|
end
|
data/lib/pause/redis/adapter.rb
CHANGED
@@ -1,19 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'pause/helper/timing'
|
2
4
|
|
3
5
|
module Pause
|
4
6
|
module Redis
|
5
|
-
|
6
7
|
# This class encapsulates Redis operations used by Pause
|
7
8
|
class Adapter
|
8
9
|
class << self
|
9
10
|
def redis
|
10
|
-
@
|
11
|
+
@redis ||= ::Redis.new(redis_connection_opts)
|
11
12
|
end
|
12
13
|
|
13
14
|
def redis_connection_opts
|
14
15
|
{ host: Pause.config.redis_host,
|
15
16
|
port: Pause.config.redis_port,
|
16
|
-
db:
|
17
|
+
db: Pause.config.redis_db }
|
17
18
|
end
|
18
19
|
end
|
19
20
|
|
@@ -72,6 +73,7 @@ module Pause
|
|
72
73
|
# @return count [Integer] the number of items deleted
|
73
74
|
def delete_rate_limited_keys(scope)
|
74
75
|
return 0 unless rate_limited_keys(scope).any?
|
76
|
+
|
75
77
|
delete_tracking_keys(scope, rate_limited_keys(scope))
|
76
78
|
redis.zremrangebyscore(rate_limited_list(scope), '-inf', '+inf').tap do |_count|
|
77
79
|
redis.del rate_limited_list(scope)
|
@@ -84,7 +86,7 @@ module Pause
|
|
84
86
|
end
|
85
87
|
|
86
88
|
def disable(scope)
|
87
|
-
redis.set("internal:|#{scope}|:disabled",
|
89
|
+
redis.set("internal:|#{scope}|:disabled", '1')
|
88
90
|
end
|
89
91
|
|
90
92
|
def enable(scope)
|
@@ -110,11 +112,11 @@ module Pause
|
|
110
112
|
end
|
111
113
|
|
112
114
|
def truncate_set_for(k)
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
115
|
+
return unless redis.zcard(k) > time_blocks_to_keep
|
116
|
+
|
117
|
+
list = extract_set_elements(k)
|
118
|
+
to_remove = list.slice(0, (list.size - time_blocks_to_keep)).map(&:ts)
|
119
|
+
redis.zrem(k, to_remove) if k && to_remove&.size&.positive?
|
118
120
|
end
|
119
121
|
|
120
122
|
def delete_tracking_keys(scope, ids)
|
@@ -137,7 +139,7 @@ module Pause
|
|
137
139
|
|
138
140
|
def keys(key_scope)
|
139
141
|
redis.keys("#{key_scope}:*").map do |key|
|
140
|
-
key.gsub(/^#{key_scope}:/,
|
142
|
+
key.gsub(/^#{key_scope}:/, '').tr('|', '')
|
141
143
|
end
|
142
144
|
end
|
143
145
|
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Pause
|
2
4
|
module Redis
|
3
5
|
class OperationNotSupported < StandardError
|
@@ -7,7 +9,6 @@ module Pause
|
|
7
9
|
# Operations that are not possible when data is sharded
|
8
10
|
# raise an error.
|
9
11
|
class ShardedAdapter < Adapter
|
10
|
-
|
11
12
|
# Overrides real multi which is not possible when sharded.
|
12
13
|
def with_multi
|
13
14
|
yield(redis) if block_given?
|
@@ -23,7 +24,7 @@ module Pause
|
|
23
24
|
private
|
24
25
|
|
25
26
|
def keys(_key_scope)
|
26
|
-
raise OperationNotSupported
|
27
|
+
raise OperationNotSupported, 'Can not be executed when Pause is configured in sharded mode'
|
27
28
|
end
|
28
29
|
end
|
29
30
|
end
|
data/lib/pause/version.rb
CHANGED
data/lib/pause.rb
CHANGED
@@ -1,4 +1,7 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'redis'
|
4
|
+
require 'colored2'
|
2
5
|
require 'pause/version'
|
3
6
|
require 'pause/configuration'
|
4
7
|
require 'pause/action'
|
@@ -9,15 +12,23 @@ require 'pause/redis/sharded_adapter'
|
|
9
12
|
require 'pause/rate_limited_event'
|
10
13
|
|
11
14
|
module Pause
|
12
|
-
|
15
|
+
PeriodCheck = Struct.new(:period_seconds, :max_allowed, :block_ttl) do
|
16
|
+
def initialize(*args, period_seconds: nil, max_allowed: nil, block_ttl: nil)
|
17
|
+
if args.any?
|
18
|
+
super(*args)
|
19
|
+
else
|
20
|
+
super(period_seconds, max_allowed, block_ttl)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
13
24
|
def <=>(other)
|
14
|
-
|
25
|
+
period_seconds <=> other.period_seconds
|
15
26
|
end
|
16
27
|
end
|
17
28
|
|
18
|
-
|
29
|
+
SetElement = Struct.new(:ts, :count) do
|
19
30
|
def <=>(other)
|
20
|
-
|
31
|
+
ts <=> other.ts
|
21
32
|
end
|
22
33
|
end
|
23
34
|
|
@@ -27,21 +38,21 @@ module Pause
|
|
27
38
|
end
|
28
39
|
|
29
40
|
def adapter
|
30
|
-
@adapter ||= config.sharded
|
31
|
-
|
32
|
-
|
41
|
+
@adapter ||= if config.sharded
|
42
|
+
Pause::Redis::ShardedAdapter.new(config)
|
43
|
+
else
|
44
|
+
Pause::Redis::Adapter.new(config)
|
45
|
+
end
|
33
46
|
end
|
34
47
|
|
35
|
-
|
36
|
-
@adapter = adapter
|
37
|
-
end
|
48
|
+
attr_writer :adapter
|
38
49
|
|
39
|
-
def configure(&
|
40
|
-
@
|
50
|
+
def configure(&)
|
51
|
+
@configure ||= Pause::Configuration.new.configure(&)
|
41
52
|
end
|
42
53
|
|
43
|
-
def config(&
|
44
|
-
configure(&
|
54
|
+
def config(&)
|
55
|
+
configure(&)
|
45
56
|
end
|
46
57
|
end
|
47
58
|
end
|
data/pause.gemspec
CHANGED
@@ -1,30 +1,27 @@
|
|
1
|
-
#
|
2
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'English'
|
4
|
+
lib = File.expand_path('lib', __dir__)
|
3
5
|
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
6
|
require 'pause/version'
|
5
7
|
|
6
8
|
Gem::Specification.new do |gem|
|
7
9
|
gem.name = 'pause'
|
8
10
|
gem.version = Pause::VERSION
|
9
|
-
gem.authors = ['Atasay Gokkaya', 'Paul Henry', 'Eric Saxby'
|
10
|
-
gem.email = %w
|
11
|
-
gem.summary =
|
12
|
-
gem.description =
|
11
|
+
gem.authors = ['Konstantin Gredeskoul', 'Atasay Gokkaya', 'Paul Henry', 'Eric Saxby']
|
12
|
+
gem.email = %w[kigster@gmail.com atasay@wanelo.com paul@wanelo.com sax@ericsaxby.com]
|
13
|
+
gem.summary = 'Fast, scalable, and flexible real time rate limiting library for distributed Ruby environments backed by Redis.'
|
14
|
+
gem.description = 'This gem provides highly flexible and easy to use interface to define rate limit checks, register events as they come, and verify if the rate limit is reached. Multiple checks for the same metric are easily supported. This gem is used at very high scale on several popular web sites.'
|
13
15
|
gem.homepage = 'https://github.com/kigster/pause'
|
14
16
|
|
15
|
-
gem.files = `git ls-files`.split(
|
16
|
-
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
17
|
-
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
17
|
+
gem.files = `git ls-files`.split($INPUT_RECORD_SEPARATOR)
|
18
|
+
gem.executables = gem.files.grep(%r{^bin/}).map { |f| File.basename(f) }
|
18
19
|
gem.require_paths = ['lib']
|
19
20
|
|
21
|
+
gem.add_dependency 'colored2'
|
20
22
|
gem.add_dependency 'redis'
|
21
|
-
gem.add_dependency 'hiredis'
|
22
23
|
|
23
|
-
|
24
|
-
gem.
|
25
|
-
gem.
|
26
|
-
gem.add_development_dependency 'fakeredis'
|
27
|
-
gem.add_development_dependency 'guard-rspec'
|
28
|
-
gem.add_development_dependency 'timecop'
|
29
|
-
gem.add_development_dependency 'rake'
|
24
|
+
# optional
|
25
|
+
# gem.add_dependency 'hiredis'
|
26
|
+
gem.metadata['rubygems_mfa_required'] = 'true'
|
30
27
|
end
|