pause 0.2.1 → 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/.github/workflows/rspec.yml +31 -0
- data/.github/workflows/rubocop.yml +31 -0
- data/.gitignore +2 -1
- data/.rubocop.yml +28 -0
- data/.rubocop_todo.yml +192 -0
- data/Gemfile +16 -9
- data/Gemfile.lock +145 -0
- data/Guardfile +7 -6
- data/LICENSE.txt +1 -1
- data/README.md +152 -27
- data/Rakefile +44 -2
- data/bin/spec +14 -0
- data/lib/pause/action.rb +116 -94
- data/lib/pause/analyzer.rb +7 -3
- data/lib/pause/configuration.rb +4 -2
- 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 +41 -19
- data/lib/pause/redis/sharded_adapter.rb +12 -16
- data/lib/pause/version.rb +3 -1
- data/lib/pause.rb +16 -13
- data/pause.gemspec +18 -12
- data/spec/pause/action_spec.rb +168 -98
- data/spec/pause/analyzer_spec.rb +9 -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/redis/adapter_spec.rb +56 -31
- data/spec/pause/redis/sharded_adapter_spec.rb +19 -5
- data/spec/spec_helper.rb +24 -11
- metadata +39 -27
- data/.travis.yml +0 -10
- data/spec/support/fakeredis.rb +0 -2
data/README.md
CHANGED
@@ -1,17 +1,125 @@
|
|
1
|
-
Pause
|
2
|
-
======
|
3
1
|
|
4
|
-
[](https://github.com/kigster/pause/actions/workflows/rspec.yml)
|
3
|
+
[](https://github.com/kigster/pause/actions/workflows/rubocop.yml)
|
6
4
|
|
7
|
-
|
5
|
+
[](https://badge.fury.io/rb/pause)
|
6
|
+
|
7
|
+
[](https://rubygems.org/gems/pause)
|
8
|
+
[](https://opensource.org/licenses/MIT)
|
9
|
+
|
10
|
+
# Pause
|
11
|
+
|
12
|
+
## In a Nutshell
|
13
|
+
|
14
|
+
**Pause** is a fast and very flexible Redis-backed rate-limiter. You can use it to track events, with
|
8
15
|
rules around how often they are allowed to occur within configured time checks.
|
9
16
|
|
10
|
-
|
11
|
-
|
17
|
+
Sample applications include:
|
18
|
+
|
19
|
+
* throttling notifications sent to a user as to not overwhelm them with too much frequency,
|
20
|
+
* IP-based blocking based on HTTP request volume (see the related gem [spanx](https://github.com/wanelo/spanx)) that uses Pause,
|
21
|
+
* ensuring you do not exceed API rate limits when calling external web APIs.
|
22
|
+
* etc.
|
23
|
+
|
24
|
+
Pause currently does not offer a CLI client, and can only be used from within a Ruby application.
|
25
|
+
|
26
|
+
Additionally:
|
27
|
+
|
28
|
+
* Pause is pure-ruby gem and does not depend on Rails or Rack
|
29
|
+
* Pause can be used across multiple ruby processes, since it uses a distributed Redis backend
|
30
|
+
* Pause is currently in use by a web application receiving 6K-10K web requests per second
|
31
|
+
* Pause will work with a horizontally sharded multi-Redis-backend by using Twitter's [Twemproxy](https://github.com/twitter/twemproxy). This way, millions of concurrent users can be handled with ease.
|
32
|
+
|
33
|
+
### Quick Start
|
34
|
+
|
35
|
+
This section is meant to give you a rapid introduction, so that you can start using Pause immediately.
|
36
|
+
|
37
|
+
Our use case: we want to rate limit notifications sent to users, identified by their `user_id`, to:
|
38
|
+
|
39
|
+
* no more than 1 in any 2-hour period
|
40
|
+
* no more than 3 per day
|
41
|
+
* no more than 7 per week
|
42
|
+
|
43
|
+
Here is how we could set this up using Pause:
|
44
|
+
|
45
|
+
#### Configuration
|
46
|
+
|
47
|
+
We need to setup Pause with a Redis instance. Here is how we do it:
|
48
|
+
|
49
|
+
```ruby
|
50
|
+
require 'pause'
|
51
|
+
|
52
|
+
# First, lets point Pause to a Redis instance
|
53
|
+
Pause.configure do |config|
|
54
|
+
# Redis connection parameters
|
55
|
+
config.redis_host = '127.0.0.1'
|
56
|
+
config.redis_port = 6379
|
57
|
+
config.redis_db = 1
|
58
|
+
config.resolution = 600
|
59
|
+
config.history = 7 * 86400 # discard events older than 7 days
|
60
|
+
end
|
61
|
+
```
|
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
|
+
|
73
|
+
#### Define Rate Limited "Action"
|
74
|
+
|
75
|
+
Next we must define the rate limited action based on the specification above. This is how easy it is:
|
76
|
+
|
77
|
+
```ruby
|
78
|
+
module MyApp
|
79
|
+
class UserNotificationLimiter < ::Pause::Action
|
80
|
+
# this is a redis key namespace added to all data in this action
|
81
|
+
scope 'un'
|
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
|
92
|
+
end
|
93
|
+
end
|
94
|
+
```
|
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
|
+
|
98
|
+
#### Perform operation, but only if the user is not rate-limited
|
99
|
+
|
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:
|
101
|
+
|
102
|
+
```ruby
|
103
|
+
class NotificationsWorker
|
104
|
+
def perform(user_id)
|
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
|
117
|
+
end
|
118
|
+
end
|
119
|
+
```
|
120
|
+
|
121
|
+
That's it! Using these two methods you can pretty much ensure that your rate limits are always in check.
|
12
122
|
|
13
|
-
Sample applications include IP-based blocking based on HTTP request volume (see related gem "spanx"),
|
14
|
-
throttling push notifications as to not overwhelm the user with too much frequency, etc.
|
15
123
|
|
16
124
|
## Installation
|
17
125
|
|
@@ -86,47 +194,44 @@ In other words, if your shortest check is 1 minute, you could set resolution to
|
|
86
194
|
require 'pause'
|
87
195
|
|
88
196
|
class FollowAction < Pause::Action
|
89
|
-
scope
|
197
|
+
scope 'fa' # keep those short
|
90
198
|
check period_seconds: 60, max_allowed: 100, block_ttl: 3600
|
91
199
|
check period_seconds: 1800, max_allowed: 2000, block_ttl: 3600
|
92
200
|
end
|
93
201
|
```
|
94
202
|
|
95
|
-
When an event occurs, you increment an instance of your action, optionally with a timestamp and count. This saves
|
96
|
-
|
203
|
+
When an event occurs, you increment an instance of your action, optionally with a timestamp and count. This saves data into a redis store, so it can be checked later by other processes. Timestamps should be in unix epoch format.
|
204
|
+
|
205
|
+
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:
|
97
206
|
|
98
207
|
```ruby
|
99
208
|
class FollowsController < ApplicationController
|
100
209
|
def create
|
101
210
|
action = FollowAction.new(user.id)
|
102
211
|
if action.ok?
|
103
|
-
|
104
|
-
# and track
|
212
|
+
user.follow!
|
213
|
+
# and don't forget to track the "success"
|
105
214
|
action.increment!
|
106
|
-
else
|
107
|
-
# action is rate limited, either skip
|
108
|
-
# or show error, depending on the context.
|
109
215
|
end
|
110
216
|
end
|
111
217
|
end
|
112
218
|
|
113
219
|
class OtherController < ApplicationController
|
114
220
|
def index
|
115
|
-
action = OtherAction.new(params[:thing])
|
221
|
+
action = OtherAction.new(params[:thing])d
|
116
222
|
unless action.rate_limited?
|
117
223
|
# perform business logic
|
118
|
-
|
119
|
-
# track it
|
224
|
+
# but in this
|
120
225
|
action.increment!(params[:count].to_i, Time.now.to_i)
|
121
226
|
end
|
122
227
|
end
|
123
228
|
end
|
124
229
|
```
|
125
230
|
|
126
|
-
If more data is needed about why the action is blocked, the `analyze` can be called
|
231
|
+
If more data is needed about why the action is blocked, the `analyze` can be called:
|
127
232
|
|
128
233
|
```ruby
|
129
|
-
action = NotifyViaEmailAction.new(
|
234
|
+
action = NotifyViaEmailAction.new(:thing)
|
130
235
|
|
131
236
|
while true
|
132
237
|
action.increment!
|
@@ -216,12 +321,30 @@ Pause.configure do |config|
|
|
216
321
|
end
|
217
322
|
```
|
218
323
|
|
219
|
-
With this configuration, any Pause operation that we know is not supported by Twemproxy will raise
|
220
|
-
`Pause::Redis::OperationNotSupported`. For instance, when sharding we are unable to get a list of all
|
221
|
-
tracked identifiers.
|
324
|
+
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.
|
222
325
|
|
223
326
|
The action block list is implemented as a sorted set, so it should still be usable when sharding.
|
224
327
|
|
328
|
+
## Testing
|
329
|
+
|
330
|
+
By default, `fakeredis` gem is used to emulate Redis in development. However, the same test-suite should be able to run against a real redis — however, be aware that it will flush the current db during spec run. In order to run specs against real redis, make sure you have Redis running locally on the default port, and that you are able to connect to it using `redis-cli`.
|
331
|
+
|
332
|
+
Please note that Travis suite, as well as the default rake task, run both.
|
333
|
+
|
334
|
+
### Unit Testing with Fakeredis
|
335
|
+
|
336
|
+
Fakeredis is the default, and is also run whenever `bundle exec rspec` is executed, or `rake spec` task invoked.
|
337
|
+
|
338
|
+
```bash
|
339
|
+
bundle exec rake spec:unit
|
340
|
+
```
|
341
|
+
|
342
|
+
### Integration Testing with Redis
|
343
|
+
|
344
|
+
```bash
|
345
|
+
bundle exec rake spec:integration
|
346
|
+
```
|
347
|
+
|
225
348
|
## Contributing
|
226
349
|
|
227
350
|
Want to make it better? Cool. Here's how:
|
@@ -234,8 +357,10 @@ Want to make it better? Cool. Here's how:
|
|
234
357
|
|
235
358
|
## Authors
|
236
359
|
|
237
|
-
This gem was written by Eric Saxby, Atasay Gokkaya and Konstantin Gredeskoul at Wanelo, Inc.
|
360
|
+
* This gem was written by Eric Saxby, Atasay Gokkaya and Konstantin Gredeskoul at Wanelo, Inc.
|
361
|
+
* It's been updated and refreshed by Konstantin Gredeskoul.
|
362
|
+
|
238
363
|
|
239
|
-
Please see the LICENSE.txt file for further details.
|
364
|
+
Please see the [LICENSE.txt](LICENSE.txt) file for further details.
|
240
365
|
|
241
366
|
|
data/Rakefile
CHANGED
@@ -1,6 +1,48 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'bundler/gem_tasks'
|
2
4
|
require 'rspec/core/rake_task'
|
5
|
+
require 'yard'
|
3
6
|
|
4
7
|
RSpec::Core::RakeTask.new(:spec)
|
5
8
|
|
6
|
-
task :
|
9
|
+
task default: %w[spec:unit spec:integration]
|
10
|
+
|
11
|
+
namespace :spec do
|
12
|
+
desc 'Run specs using fakeredis'
|
13
|
+
task :unit do
|
14
|
+
ENV['PAUSE_REAL_REDIS'] = nil
|
15
|
+
Rake::Task['spec'].execute
|
16
|
+
end
|
17
|
+
desc 'Run specs against a local Redis server'
|
18
|
+
task :integration do
|
19
|
+
ENV['PAUSE_REAL_REDIS'] = 'true'
|
20
|
+
Rake::Task['spec'].execute
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def shell(*args)
|
25
|
+
puts "running: #{args.join(' ')}"
|
26
|
+
system(args.join(' '))
|
27
|
+
end
|
28
|
+
|
29
|
+
task :clean do
|
30
|
+
shell('rm -rf pkg/ tmp/ coverage/ doc/ ')
|
31
|
+
end
|
32
|
+
|
33
|
+
task gem: [:build] do
|
34
|
+
shell('gem install pkg/*')
|
35
|
+
end
|
36
|
+
|
37
|
+
task permissions: [:clean] do
|
38
|
+
shell('chmod -v o+r,g+r * */* */*/* */*/*/* */*/*/*/* */*/*/*/*/*')
|
39
|
+
shell('find . -type d -exec chmod o+x,g+x {} \\;')
|
40
|
+
end
|
41
|
+
|
42
|
+
task build: :permissions
|
43
|
+
|
44
|
+
YARD::Rake::YardocTask.new(:doc) do |t|
|
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') }
|
48
|
+
end
|
data/bin/spec
ADDED
data/lib/pause/action.rb
CHANGED
@@ -1,62 +1,131 @@
|
|
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)
|
7
|
+
def initialize(identifier, &block)
|
6
8
|
@identifier = identifier
|
7
|
-
self.class.checks
|
9
|
+
self.class.checks ||= []
|
10
|
+
instance_exec(&block) if block
|
8
11
|
end
|
9
12
|
|
10
|
-
# Action subclasses should define their scope as follows
|
11
|
-
#
|
12
|
-
# class MyAction < Pause::Action
|
13
|
-
# scope "my:scope"
|
14
|
-
# end
|
15
|
-
#
|
16
13
|
def scope
|
17
|
-
|
18
|
-
end
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
period_seconds, max_allowed, block_ttl =
|
40
|
-
if args.first.is_a?(Hash)
|
41
|
-
[args.first[:period_seconds], args.first[:max_allowed], args.first[:block_ttl]]
|
42
|
-
else
|
43
|
-
args
|
14
|
+
self.class.scope
|
15
|
+
end
|
16
|
+
|
17
|
+
class << self
|
18
|
+
attr_accessor :checks
|
19
|
+
|
20
|
+
def inherited(klass)
|
21
|
+
klass.instance_eval do
|
22
|
+
# Action subclasses should define their scope as follows
|
23
|
+
#
|
24
|
+
# class MyAction < Pause::Action
|
25
|
+
# scope "my:scope"
|
26
|
+
# end
|
27
|
+
#
|
28
|
+
@scope = klass.name.downcase.gsub('::', '.')
|
29
|
+
class << self
|
30
|
+
# @param [String] args
|
31
|
+
def scope(*args)
|
32
|
+
@scope = args.first if args && args.size == 1
|
33
|
+
@scope
|
34
|
+
end
|
35
|
+
end
|
44
36
|
end
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
37
|
+
end
|
38
|
+
|
39
|
+
# Actions can be globally disabled or re-enabled in a persistent
|
40
|
+
# way.
|
41
|
+
#
|
42
|
+
# MyAction.disable
|
43
|
+
# MyAction.enabled? => false
|
44
|
+
# MyAction.disabled? => true
|
45
|
+
#
|
46
|
+
# MyAction.enable
|
47
|
+
# MyAction.enabled? => true
|
48
|
+
# MyAction.disabled? => false
|
49
|
+
#
|
50
|
+
def enable
|
51
|
+
adapter.enable(scope)
|
52
|
+
end
|
53
|
+
|
54
|
+
def disable
|
55
|
+
adapter.disable(scope)
|
56
|
+
end
|
57
|
+
|
58
|
+
def enabled?
|
59
|
+
adapter.enabled?(scope)
|
60
|
+
end
|
61
|
+
|
62
|
+
def disabled?
|
63
|
+
!enabled?
|
64
|
+
end
|
65
|
+
|
66
|
+
# Action subclasses should define their checks as follows
|
67
|
+
#
|
68
|
+
# period_seconds - compare all activity by an identifier within the time period
|
69
|
+
# max_allowed - if the number of actions by an identifier exceeds max_allowed for the time period marked
|
70
|
+
# by period_seconds, it is no longer ok.
|
71
|
+
# block_ttl - time to mark identifier as not ok
|
72
|
+
#
|
73
|
+
# class MyAction < Pause::Action
|
74
|
+
# check period_seconds: 60, max_allowed: 100, block_ttl: 3600
|
75
|
+
# check period_seconds: 1800, max_allowed: 2000, block_ttl: 3600
|
76
|
+
# end
|
77
|
+
#
|
78
|
+
def check(*args, **opts)
|
79
|
+
self.checks ||= []
|
80
|
+
|
81
|
+
params =
|
82
|
+
if args.empty?
|
83
|
+
# if block_ttl is not provided, just default to the period
|
84
|
+
opts[:block_ttl] ||= opts[:period_seconds]
|
85
|
+
[opts[:period_seconds], opts[:max_allowed], opts[:block_ttl]]
|
86
|
+
else
|
87
|
+
args
|
88
|
+
end
|
89
|
+
|
90
|
+
self.checks << Pause::PeriodCheck.new(*params)
|
91
|
+
end
|
92
|
+
|
93
|
+
def tracked_identifiers
|
94
|
+
adapter.all_keys(scope)
|
95
|
+
end
|
96
|
+
|
97
|
+
def rate_limited_identifiers
|
98
|
+
adapter.rate_limited_keys(scope)
|
99
|
+
end
|
100
|
+
|
101
|
+
def unblock_all
|
102
|
+
adapter.delete_rate_limited_keys(scope)
|
103
|
+
end
|
104
|
+
|
105
|
+
def adapter
|
106
|
+
Pause.adapter
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
def unless_rate_limited(count: 1, timestamp: Time.now.to_i, &_block)
|
111
|
+
check_result = analyze
|
112
|
+
if check_result.nil?
|
113
|
+
yield
|
114
|
+
increment!(count, timestamp)
|
115
|
+
else
|
116
|
+
check_result
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
def if_rate_limited(&_block)
|
121
|
+
check_result = analyze(recalculate: true)
|
122
|
+
yield(check_result) unless check_result.nil?
|
50
123
|
end
|
51
124
|
|
52
125
|
def checks
|
53
126
|
self.class.checks
|
54
127
|
end
|
55
128
|
|
56
|
-
def self.checks=(period_checks)
|
57
|
-
@checks = period_checks
|
58
|
-
end
|
59
|
-
|
60
129
|
def block_for(ttl)
|
61
130
|
adapter.rate_limit!(scope, identifier, ttl)
|
62
131
|
end
|
@@ -66,7 +135,7 @@ module Pause
|
|
66
135
|
end
|
67
136
|
|
68
137
|
def rate_limited?
|
69
|
-
!
|
138
|
+
!ok?
|
70
139
|
end
|
71
140
|
|
72
141
|
def ok?
|
@@ -76,65 +145,18 @@ module Pause
|
|
76
145
|
false
|
77
146
|
end
|
78
147
|
|
79
|
-
def analyze
|
80
|
-
Pause.analyzer.check(self)
|
81
|
-
end
|
82
|
-
|
83
|
-
def self.tracked_identifiers
|
84
|
-
adapter.all_keys(self.class_scope)
|
85
|
-
end
|
86
|
-
|
87
|
-
def self.rate_limited_identifiers
|
88
|
-
adapter.rate_limited_keys(self.class_scope)
|
89
|
-
end
|
90
|
-
|
91
|
-
def self.unblock_all
|
92
|
-
adapter.delete_rate_limited_keys(self.class_scope)
|
148
|
+
def analyze(recalculate: false)
|
149
|
+
Pause.analyzer.check(self, recalculate: recalculate)
|
93
150
|
end
|
94
151
|
|
95
152
|
def unblock
|
96
153
|
adapter.delete_rate_limited_key(scope, identifier)
|
97
154
|
end
|
98
155
|
|
99
|
-
# Actions can be globally disabled or re-enabled in a persistent
|
100
|
-
# way.
|
101
|
-
#
|
102
|
-
# MyAction.disable
|
103
|
-
# MyAction.enabled? => false
|
104
|
-
# MyAction.disabled? => true
|
105
|
-
#
|
106
|
-
# MyAction.enable
|
107
|
-
# MyAction.enabled? => true
|
108
|
-
# MyAction.disabled? => false
|
109
|
-
#
|
110
|
-
def self.enable
|
111
|
-
adapter.enable(class_scope)
|
112
|
-
end
|
113
|
-
|
114
|
-
def self.disable
|
115
|
-
adapter.disable(class_scope)
|
116
|
-
end
|
117
|
-
|
118
|
-
def self.enabled?
|
119
|
-
adapter.enabled?(class_scope)
|
120
|
-
end
|
121
|
-
|
122
|
-
def self.disabled?
|
123
|
-
! enabled?
|
124
|
-
end
|
125
|
-
|
126
156
|
private
|
127
157
|
|
128
|
-
def self.adapter
|
129
|
-
Pause.adapter
|
130
|
-
end
|
131
|
-
|
132
158
|
def adapter
|
133
159
|
self.class.adapter
|
134
160
|
end
|
135
|
-
|
136
|
-
def self.class_scope
|
137
|
-
class_variable_get:@@class_scope if class_variable_defined?(:@@class_scope)
|
138
|
-
end
|
139
161
|
end
|
140
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
|
@@ -10,14 +12,16 @@ module Pause
|
|
10
12
|
# @return [nil] everything is fine
|
11
13
|
# @return [false] this action is already blocked
|
12
14
|
# @return [Pause::RateLimitedEvent] the action was blocked as a result of this check
|
13
|
-
def check(action)
|
14
|
-
return false if adapter.rate_limited?(action.scope, action.identifier)
|
15
|
+
def check(action, recalculate: false)
|
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
|
-
set
|
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,9 +1,11 @@
|
|
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
|
4
6
|
|
5
7
|
def configure
|
6
|
-
yield self
|
8
|
+
yield self if block_given?
|
7
9
|
self
|
8
10
|
end
|
9
11
|
|
@@ -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
|