rollie 0.1.1 → 0.1.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +4 -0
- data/README.md +83 -39
- data/lib/rollie/rate_limiter.rb +11 -11
- data/lib/rollie/redis_pool.rb +10 -10
- data/lib/rollie/status.rb +2 -0
- data/lib/rollie/version.rb +3 -1
- data/lib/rollie.rb +11 -12
- metadata +68 -22
- data/.github/workflows/ci.yml +0 -38
- data/.gitignore +0 -13
- data/.rspec +0 -2
- data/Gemfile +0 -8
- data/LICENSE +0 -21
- data/Rakefile +0 -28
- data/bin/check-version +0 -14
- data/bin/rspec +0 -29
- data/rollie.gemspec +0 -24
- data/spec/rollie/rate_limiter_spec.rb +0 -84
- data/spec/rollie_spec.rb +0 -36
- data/spec/spec_helper.rb +0 -106
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c4588225ac38e980c73e7d5f20b99dca196de34e217d6307b08f3a86f8ae7ef3
|
4
|
+
data.tar.gz: ee71e83156c12dea359f67f00593849c119ffb3e2cb867c4b1e07f2af22ac59e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a399df06ad904645db53bbf56a4d9de9661ac3f62b5cb3f8d90c40e7f3106191aad322e1afd2b457e5a22f9f22cd0c0469e41d1f29b4dbe532353e7ecefd807a
|
7
|
+
data.tar.gz: cd284a9a5dd513a6c0a41311ed3b282e20a9115c1bf0e1ca626460580aa27a141d6bb5600bca62a3215e8a96177bcb8c2a8f394a1b1e45b234699c937979735b
|
data/CHANGELOG.md
CHANGED
data/README.md
CHANGED
@@ -1,17 +1,26 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
[
|
9
|
-
|
10
|
-
|
11
|
-
|
1
|
+
Rollie
|
2
|
+
===========
|
3
|
+
|
4
|
+
[![Gem Version](https://badge.fury.io/rb/rollie.svg)](https://badge.fury.io/rb/rollie)
|
5
|
+
[![CI](https://github.com/ParentSquare/rollie/workflows/CI/badge.svg)](https://github.com/ParentSquare/rollie/actions?query=workflow%3ACI+branch%3Amaster)
|
6
|
+
[![Code Quality](https://app.codacy.com/project/badge/Grade/20f8a080aca5444cbdaebff3a4e7e702)](https://www.codacy.com/gh/ParentSquare/rollie/dashboard?utm_source=github.com&utm_medium=referral&utm_content=ParentSquare/rollie&utm_campaign=Badge_Grade)
|
7
|
+
[![Coverage Status](https://codecov.io/gh/ParentSquare/rollie/branch/master/graph/badge.svg?token=0I92PXGZCM)](https://codecov.io/gh/ParentSquare/rollie)
|
8
|
+
[![Online docs](https://img.shields.io/badge/docs-✓-green.svg)](https://www.rubydoc.info/github/ParentSquare/rollie)
|
9
|
+
|
10
|
+
Rollie is a multi-purpose, fast, Redis backed rate limiter that can be used to
|
11
|
+
limit requests to external APIs, in Rack middleware, etc. Rollie uses a
|
12
|
+
dedicated Redis connection pool implemented using `connection_pool` for more
|
13
|
+
efficient Redis connection management. The Redis algorithm was inspired by the
|
14
|
+
[rolling-rate-limiter](https://www.npmjs.com/package/rolling-rate-limiter) node
|
15
|
+
package.
|
16
|
+
|
17
|
+
The key implementation detail is that Rollie utilizes a rolling window to bucket
|
18
|
+
invocations in. Meaning, if you set a limit of 100 per 30 seconds, Rollie will
|
19
|
+
start the clock in instant it is first executed with a given key.
|
12
20
|
|
13
21
|
For example, first execution:
|
14
|
-
|
22
|
+
|
23
|
+
```ruby
|
15
24
|
rollie = Rollie::RateLimiter.new("api", limit: 10, interval: 30000)
|
16
25
|
rollie.within_limit do
|
17
26
|
puts Time.now
|
@@ -19,25 +28,38 @@ end
|
|
19
28
|
# => 2016-12-03 08:31:23.873
|
20
29
|
```
|
21
30
|
|
22
|
-
This doesn't mean the count is reset back to 0 at `2016-12-03 08:31:53.873`. Its
|
23
|
-
is checked with every invocation over the
|
24
|
-
|
25
|
-
|
31
|
+
This doesn't mean the count is reset back to 0 at `2016-12-03 08:31:53.873`. Its
|
32
|
+
a continuous rolling count, the count is checked with every invocation over the
|
33
|
+
last 30 seconds.
|
34
|
+
|
35
|
+
If you invoke this rate 9 times at `2016-12-03 08:31:53.500`, you will only be
|
36
|
+
able to make one more call until `2016-12-03 08:32:23.500`.
|
26
37
|
|
27
38
|
## Install
|
28
39
|
|
40
|
+
Add it to your `Gemfile`:
|
41
|
+
|
42
|
+
```ruby
|
43
|
+
gem 'rollie'
|
29
44
|
```
|
45
|
+
|
46
|
+
Or install it manually:
|
47
|
+
|
48
|
+
```sh
|
30
49
|
gem install rollie
|
31
50
|
```
|
32
51
|
|
33
|
-
|
52
|
+
Usage
|
53
|
+
-----------
|
34
54
|
|
35
|
-
Rollie is simple to use and has only one method, `within_limit`. `within_limit`
|
36
|
-
executed only if you are within the
|
55
|
+
Rollie is simple to use and has only one method, `within_limit`. `within_limit`
|
56
|
+
expects a block and that block will be executed only if you are within the
|
57
|
+
limit.
|
37
58
|
|
38
|
-
Initialize Rollie with a key used to uniquely identify what you are limiting.
|
39
|
-
interval in milliseconds.
|
40
|
-
|
59
|
+
Initialize Rollie with a key used to uniquely identify what you are limiting.
|
60
|
+
Use the options to set the limit and interval in milliseconds.
|
61
|
+
|
62
|
+
```ruby
|
41
63
|
# limit 30 requests per second.
|
42
64
|
twitter_rate = Rollie::RateLimiter.new("twitter_requests", limit: 30, interval: 1000)
|
43
65
|
status = twitter_rate.within_limit do
|
@@ -45,8 +67,10 @@ status = twitter_rate.within_limit do
|
|
45
67
|
end
|
46
68
|
```
|
47
69
|
|
48
|
-
The status will tell you the current state. You can also see the current count
|
49
|
-
|
70
|
+
The status will tell you the current state. You can also see the current count
|
71
|
+
and how long until the bucket resets.
|
72
|
+
|
73
|
+
```ruby
|
50
74
|
status.exceeded?
|
51
75
|
# => false
|
52
76
|
status.count
|
@@ -56,34 +80,53 @@ status.time_remaining
|
|
56
80
|
```
|
57
81
|
|
58
82
|
Once exceeded:
|
59
|
-
|
83
|
+
|
84
|
+
```ruby
|
60
85
|
status.exceeded?
|
61
86
|
# => true
|
62
87
|
status.count
|
63
88
|
# => 30
|
64
89
|
status.time_remaining
|
65
90
|
# => 461 # milliseconds
|
66
|
-
```
|
67
|
-
|
68
|
-
You can also use a namespace if you want to track multiple entities, for example users.
|
69
91
|
```
|
70
|
-
|
92
|
+
|
93
|
+
You can also use a namespace if you want to track multiple entities, for example
|
94
|
+
users.
|
95
|
+
|
96
|
+
```ruby
|
97
|
+
Rollie::RateLimiter.new(
|
98
|
+
user_id,
|
99
|
+
namespace: "user_messages",
|
100
|
+
limit: 100,
|
101
|
+
interval: 30000
|
102
|
+
)
|
71
103
|
```
|
72
104
|
|
73
105
|
### Counting blocked actions
|
74
106
|
|
75
|
-
By default, blocked actions are not counted against the callee. This allows for
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
107
|
+
By default, blocked actions are not counted against the callee. This allows for
|
108
|
+
the block to be executed within the rate even when there is a continuous flood
|
109
|
+
of action. If you wish to change this behaviour, for example to require the
|
110
|
+
callee to back off before being allowed to execute again, set this option to
|
111
|
+
true.
|
112
|
+
|
113
|
+
```ruby
|
114
|
+
request_rate = Rollie::RateLimiter.new(
|
115
|
+
ip,
|
116
|
+
namespace: "ip",
|
117
|
+
limit: 30,
|
118
|
+
interval: 1000,
|
119
|
+
count_blocked: true
|
120
|
+
)
|
80
121
|
```
|
81
122
|
|
82
|
-
|
123
|
+
Configuration
|
124
|
+
-------------------
|
83
125
|
|
84
|
-
By default Rollie will try to connect to
|
85
|
-
set an alternate
|
86
|
-
|
126
|
+
By default Rollie will try to connect to Redis using `ENV["REDIS_URL"]` if set
|
127
|
+
or fallback to localhost:6379. You can set an alternate Redis configuration:
|
128
|
+
|
129
|
+
```ruby
|
87
130
|
Rollie.redis = {
|
88
131
|
url: CONFIG[:redis_url],
|
89
132
|
pool_size: 5,
|
@@ -92,4 +135,5 @@ Rollie.redis = {
|
|
92
135
|
}
|
93
136
|
```
|
94
137
|
|
95
|
-
If using rails, create an initializer `config/initializers/rollie.rb` with these
|
138
|
+
If using rails, create an initializer `config/initializers/rollie.rb` with these
|
139
|
+
settings.
|
data/lib/rollie/rate_limiter.rb
CHANGED
@@ -1,14 +1,15 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
2
|
|
3
|
+
module Rollie
|
3
4
|
class RateLimiter
|
4
|
-
|
5
5
|
# Create a new RateLimiter instance.
|
6
6
|
#
|
7
7
|
# @param [String] key A unique name to track this rate limit against.
|
8
8
|
# @option options [Integer] :limit The limit
|
9
9
|
# @option options [Integer] :interval The interval in milliseconds for this rate limit
|
10
10
|
# @option options [String] :namespace Optional namespace for this rate limit
|
11
|
-
# @option options [Boolean] :count_blocked if true, all calls to
|
11
|
+
# @option options [Boolean] :count_blocked if true, all calls to
|
12
|
+
# within_limit will count towards total execution count, even if blocked.
|
12
13
|
#
|
13
14
|
# @return [RateLimiter] RateLimiter instance
|
14
15
|
def initialize(key, options = {})
|
@@ -22,7 +23,7 @@ module Rollie
|
|
22
23
|
#
|
23
24
|
# @return [Status] The current status for this RateLimiter.
|
24
25
|
def within_limit
|
25
|
-
raise ArgumentError,
|
26
|
+
raise ArgumentError, 'requires a block' unless block_given?
|
26
27
|
|
27
28
|
Rollie.redis do |conn|
|
28
29
|
status = inc(conn)
|
@@ -44,13 +45,13 @@ module Rollie
|
|
44
45
|
private
|
45
46
|
|
46
47
|
def inc(conn)
|
47
|
-
time = (Time.now.to_r *
|
48
|
+
time = (Time.now.to_r * 1_000_000).round
|
48
49
|
old = time - @interval
|
49
|
-
range = conn.multi do
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
50
|
+
range = conn.multi do |multi|
|
51
|
+
multi.zremrangebyscore(@key, 0, old)
|
52
|
+
multi.zadd(@key, time, time)
|
53
|
+
multi.zrange(@key, 0, -1)
|
54
|
+
multi.expire(@key, (@interval / 1_000_000.0).ceil)
|
54
55
|
end[2]
|
55
56
|
|
56
57
|
exceeded = range.length > @limit
|
@@ -64,6 +65,5 @@ module Rollie
|
|
64
65
|
|
65
66
|
Rollie::Status.new((time_remaining / 1000).floor, current_count, exceeded)
|
66
67
|
end
|
67
|
-
|
68
68
|
end
|
69
69
|
end
|
data/lib/rollie/redis_pool.rb
CHANGED
@@ -1,16 +1,17 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
require
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'connection_pool'
|
4
|
+
require 'redis'
|
5
|
+
require 'redis-namespace'
|
4
6
|
|
5
7
|
module Rollie
|
6
8
|
class RedisPool
|
7
9
|
class << self
|
8
|
-
|
9
|
-
def create(options={})
|
10
|
+
def create(options = {})
|
10
11
|
pool_size = options[:pool_size] || 5
|
11
12
|
pool_timeout = options[:pool_timeout] || 1
|
12
13
|
|
13
|
-
ConnectionPool.new(:
|
14
|
+
ConnectionPool.new(timeout: pool_timeout, size: pool_size) do
|
14
15
|
build_client(options)
|
15
16
|
end
|
16
17
|
end
|
@@ -18,19 +19,18 @@ module Rollie
|
|
18
19
|
private
|
19
20
|
|
20
21
|
def build_client(options)
|
21
|
-
namespace = options[:namespace] ||
|
22
|
+
namespace = options[:namespace] || 'Rollie'
|
22
23
|
client = Redis.new redis_options(options)
|
23
24
|
Redis::Namespace.new(namespace, redis: client)
|
24
25
|
end
|
25
26
|
|
26
27
|
def redis_options(options)
|
27
28
|
redis = options.dup
|
28
|
-
redis[:url] ||= ENV
|
29
|
-
redis[:driver] ||=
|
29
|
+
redis[:url] ||= ENV.fetch('REDIS_URL', nil)
|
30
|
+
redis[:driver] ||= 'ruby'
|
30
31
|
redis.delete(:namespace)
|
31
32
|
redis
|
32
33
|
end
|
33
|
-
|
34
34
|
end
|
35
35
|
end
|
36
36
|
end
|
data/lib/rollie/status.rb
CHANGED
data/lib/rollie/version.rb
CHANGED
data/lib/rollie.rb
CHANGED
@@ -1,16 +1,16 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
require
|
4
|
-
require
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'rollie/rate_limiter'
|
4
|
+
require 'rollie/redis_pool'
|
5
|
+
require 'rollie/status'
|
6
|
+
require 'rollie/version'
|
5
7
|
|
6
8
|
module Rollie
|
7
9
|
class << self
|
10
|
+
def redis(&block)
|
11
|
+
raise ArgumentError, 'requires a block' unless block
|
8
12
|
|
9
|
-
|
10
|
-
raise ArgumentError, "requires a block" unless block_given?
|
11
|
-
redis_pool.with do |conn|
|
12
|
-
yield(conn)
|
13
|
-
end
|
13
|
+
redis_pool.with(&block)
|
14
14
|
end
|
15
15
|
|
16
16
|
# Configures the redis connection pool. Options can be a hash of redis connection pool options or a pre-configured
|
@@ -24,14 +24,13 @@ module Rollie
|
|
24
24
|
def redis=(options)
|
25
25
|
@redis_pool = if options.is_a?(ConnectionPool)
|
26
26
|
options
|
27
|
-
|
27
|
+
else
|
28
28
|
Rollie::RedisPool.create(options)
|
29
|
-
|
29
|
+
end
|
30
30
|
end
|
31
31
|
|
32
32
|
def redis_pool
|
33
33
|
@redis_pool ||= Rollie::RedisPool.create
|
34
34
|
end
|
35
|
-
|
36
35
|
end
|
37
36
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: rollie
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Zach Davis
|
@@ -9,8 +9,22 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date:
|
12
|
+
date: 2023-04-04 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: connection_pool
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
requirements:
|
18
|
+
- - ">="
|
19
|
+
- !ruby/object:Gem::Version
|
20
|
+
version: 2.2.0
|
21
|
+
type: :runtime
|
22
|
+
prerelease: false
|
23
|
+
version_requirements: !ruby/object:Gem::Requirement
|
24
|
+
requirements:
|
25
|
+
- - ">="
|
26
|
+
- !ruby/object:Gem::Version
|
27
|
+
version: 2.2.0
|
14
28
|
- !ruby/object:Gem::Dependency
|
15
29
|
name: redis
|
16
30
|
requirement: !ruby/object:Gem::Requirement
|
@@ -40,19 +54,61 @@ dependencies:
|
|
40
54
|
- !ruby/object:Gem::Version
|
41
55
|
version: 1.5.2
|
42
56
|
- !ruby/object:Gem::Dependency
|
43
|
-
name:
|
57
|
+
name: rspec
|
58
|
+
requirement: !ruby/object:Gem::Requirement
|
59
|
+
requirements:
|
60
|
+
- - "~>"
|
61
|
+
- !ruby/object:Gem::Version
|
62
|
+
version: '3.8'
|
63
|
+
type: :development
|
64
|
+
prerelease: false
|
65
|
+
version_requirements: !ruby/object:Gem::Requirement
|
66
|
+
requirements:
|
67
|
+
- - "~>"
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: '3.8'
|
70
|
+
- !ruby/object:Gem::Dependency
|
71
|
+
name: rubocop
|
72
|
+
requirement: !ruby/object:Gem::Requirement
|
73
|
+
requirements:
|
74
|
+
- - "~>"
|
75
|
+
- !ruby/object:Gem::Version
|
76
|
+
version: '1.7'
|
77
|
+
type: :development
|
78
|
+
prerelease: false
|
79
|
+
version_requirements: !ruby/object:Gem::Requirement
|
80
|
+
requirements:
|
81
|
+
- - "~>"
|
82
|
+
- !ruby/object:Gem::Version
|
83
|
+
version: '1.7'
|
84
|
+
- !ruby/object:Gem::Dependency
|
85
|
+
name: rubocop-rspec
|
86
|
+
requirement: !ruby/object:Gem::Requirement
|
87
|
+
requirements:
|
88
|
+
- - "~>"
|
89
|
+
- !ruby/object:Gem::Version
|
90
|
+
version: '2.1'
|
91
|
+
type: :development
|
92
|
+
prerelease: false
|
93
|
+
version_requirements: !ruby/object:Gem::Requirement
|
94
|
+
requirements:
|
95
|
+
- - "~>"
|
96
|
+
- !ruby/object:Gem::Version
|
97
|
+
version: '2.1'
|
98
|
+
- !ruby/object:Gem::Dependency
|
99
|
+
name: timecop
|
44
100
|
requirement: !ruby/object:Gem::Requirement
|
45
101
|
requirements:
|
46
102
|
- - ">="
|
47
103
|
- !ruby/object:Gem::Version
|
48
|
-
version:
|
49
|
-
type: :
|
104
|
+
version: '0.9'
|
105
|
+
type: :development
|
50
106
|
prerelease: false
|
51
107
|
version_requirements: !ruby/object:Gem::Requirement
|
52
108
|
requirements:
|
53
109
|
- - ">="
|
54
110
|
- !ruby/object:Gem::Version
|
55
|
-
version:
|
111
|
+
version: '0.9'
|
56
112
|
description: Generic rate limiter backed by Redis for efficient limiting using sliding
|
57
113
|
windows.
|
58
114
|
email: justin.howard@parentsquare.com
|
@@ -60,29 +116,20 @@ executables: []
|
|
60
116
|
extensions: []
|
61
117
|
extra_rdoc_files: []
|
62
118
|
files:
|
63
|
-
- ".github/workflows/ci.yml"
|
64
|
-
- ".gitignore"
|
65
|
-
- ".rspec"
|
66
119
|
- CHANGELOG.md
|
67
|
-
- Gemfile
|
68
|
-
- LICENSE
|
69
120
|
- README.md
|
70
|
-
- Rakefile
|
71
|
-
- bin/check-version
|
72
|
-
- bin/rspec
|
73
121
|
- lib/rollie.rb
|
74
122
|
- lib/rollie/rate_limiter.rb
|
75
123
|
- lib/rollie/redis_pool.rb
|
76
124
|
- lib/rollie/status.rb
|
77
125
|
- lib/rollie/version.rb
|
78
|
-
- rollie.gemspec
|
79
|
-
- spec/rollie/rate_limiter_spec.rb
|
80
|
-
- spec/rollie_spec.rb
|
81
|
-
- spec/spec_helper.rb
|
82
126
|
homepage: https://github.com/ParentSquare/rollie
|
83
127
|
licenses:
|
84
128
|
- MIT
|
85
|
-
metadata:
|
129
|
+
metadata:
|
130
|
+
rubygems_mfa_required: 'true'
|
131
|
+
changelog_uri: https://github.com/ParentSquare/rollie/blob/master/CHANGELOG.md
|
132
|
+
documentation_uri: https://www.rubydoc.info/gems/rollie/0.1.2
|
86
133
|
post_install_message:
|
87
134
|
rdoc_options: []
|
88
135
|
require_paths:
|
@@ -91,15 +138,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
91
138
|
requirements:
|
92
139
|
- - ">="
|
93
140
|
- !ruby/object:Gem::Version
|
94
|
-
version: '
|
141
|
+
version: '2.4'
|
95
142
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
96
143
|
requirements:
|
97
144
|
- - ">="
|
98
145
|
- !ruby/object:Gem::Version
|
99
146
|
version: '0'
|
100
147
|
requirements: []
|
101
|
-
|
102
|
-
rubygems_version: 2.7.6
|
148
|
+
rubygems_version: 3.3.5
|
103
149
|
signing_key:
|
104
150
|
specification_version: 4
|
105
151
|
summary: Generic rate limiter backed by Redis for efficient limiting using sliding
|
data/.github/workflows/ci.yml
DELETED
@@ -1,38 +0,0 @@
|
|
1
|
-
---
|
2
|
-
name: CI
|
3
|
-
on:
|
4
|
-
push:
|
5
|
-
tags: ['v*']
|
6
|
-
branches: [master]
|
7
|
-
pull_request:
|
8
|
-
branches: ['**']
|
9
|
-
jobs:
|
10
|
-
test:
|
11
|
-
runs-on: ubuntu-latest
|
12
|
-
strategy:
|
13
|
-
fail-fast: false
|
14
|
-
matrix:
|
15
|
-
ruby: [2.3, 2.4, 2.5, 2.6, 2.7, 3.0]
|
16
|
-
services:
|
17
|
-
redis:
|
18
|
-
image: redis
|
19
|
-
ports:
|
20
|
-
- 6379:6379
|
21
|
-
steps:
|
22
|
-
- uses: actions/checkout@v2
|
23
|
-
- uses: ruby/setup-ruby@v1
|
24
|
-
with:
|
25
|
-
ruby-version: ${{ matrix.ruby }}
|
26
|
-
bundler-cache: true
|
27
|
-
- run: bundle exec rspec --format doc
|
28
|
-
- run: bin/check-version
|
29
|
-
|
30
|
-
release:
|
31
|
-
needs: test
|
32
|
-
if: startsWith(github.ref, 'refs/tags/v')
|
33
|
-
runs-on: ubuntu-latest
|
34
|
-
steps:
|
35
|
-
- uses: actions/checkout@v2
|
36
|
-
- uses: dawidd6/action-publish-gem@v1
|
37
|
-
with:
|
38
|
-
api_key: ${{secrets.RUBYGEMS_API_KEY}}
|
data/.gitignore
DELETED
data/.rspec
DELETED
data/Gemfile
DELETED
data/LICENSE
DELETED
@@ -1,21 +0,0 @@
|
|
1
|
-
MIT License
|
2
|
-
|
3
|
-
Copyright (c) 2016 Zach Davis
|
4
|
-
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
7
|
-
in the Software without restriction, including without limitation the rights
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
10
|
-
furnished to do so, subject to the following conditions:
|
11
|
-
|
12
|
-
The above copyright notice and this permission notice shall be included in all
|
13
|
-
copies or substantial portions of the Software.
|
14
|
-
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
-
SOFTWARE.
|
data/Rakefile
DELETED
@@ -1,28 +0,0 @@
|
|
1
|
-
require "rake"
|
2
|
-
require "rdoc"
|
3
|
-
|
4
|
-
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), *%w[lib]))
|
5
|
-
require "rollie/version"
|
6
|
-
|
7
|
-
def name
|
8
|
-
"rollie"
|
9
|
-
end
|
10
|
-
|
11
|
-
def version
|
12
|
-
Rollie::VERSION
|
13
|
-
end
|
14
|
-
|
15
|
-
begin
|
16
|
-
require "rspec/core/rake_task"
|
17
|
-
RSpec::Core::RakeTask.new(:spec)
|
18
|
-
rescue LoadError; end
|
19
|
-
|
20
|
-
require "rdoc/task"
|
21
|
-
Rake::RDocTask.new do |rdoc|
|
22
|
-
rdoc.rdoc_dir = "rdoc"
|
23
|
-
rdoc.title = "#{name} #{version}"
|
24
|
-
rdoc.rdoc_files.include("README*")
|
25
|
-
rdoc.rdoc_files.include("lib/**/*.rb")
|
26
|
-
end
|
27
|
-
|
28
|
-
task :default => :spec
|
data/bin/check-version
DELETED
@@ -1,14 +0,0 @@
|
|
1
|
-
#!/usr/bin/env sh
|
2
|
-
|
3
|
-
set -e
|
4
|
-
|
5
|
-
tag="$(git describe --abbrev=0 2>/dev/null || echo)"
|
6
|
-
echo "Tag: ${tag}"
|
7
|
-
tag="${tag#v}"
|
8
|
-
echo "Git Version: ${tag}"
|
9
|
-
[ "$tag" = '' ] && exit 0
|
10
|
-
gem_version="$(ruby -r ./lib/rollie/version -e "puts Rollie::VERSION" | tail -n1)"
|
11
|
-
echo "Gem Version: ${gem_version}"
|
12
|
-
|
13
|
-
tag_gt_version="$(ruby -r ./lib/rollie/version -e "puts Gem::Version.new(Rollie::VERSION) >= Gem::Version.new('${tag}')" | tail -n1)"
|
14
|
-
test "$tag_gt_version" = true
|
data/bin/rspec
DELETED
@@ -1,29 +0,0 @@
|
|
1
|
-
#!/usr/bin/env ruby
|
2
|
-
# frozen_string_literal: true
|
3
|
-
|
4
|
-
#
|
5
|
-
# This file was generated by Bundler.
|
6
|
-
#
|
7
|
-
# The application 'rspec' is installed as part of a gem, and
|
8
|
-
# this file is here to facilitate running it.
|
9
|
-
#
|
10
|
-
|
11
|
-
require "pathname"
|
12
|
-
ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
|
13
|
-
Pathname.new(__FILE__).realpath)
|
14
|
-
|
15
|
-
bundle_binstub = File.expand_path("../bundle", __FILE__)
|
16
|
-
|
17
|
-
if File.file?(bundle_binstub)
|
18
|
-
if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/
|
19
|
-
load(bundle_binstub)
|
20
|
-
else
|
21
|
-
abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
|
22
|
-
Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
|
23
|
-
end
|
24
|
-
end
|
25
|
-
|
26
|
-
require "rubygems"
|
27
|
-
require "bundler/setup"
|
28
|
-
|
29
|
-
load Gem.bin_path("rspec-core", "rspec")
|
data/rollie.gemspec
DELETED
@@ -1,24 +0,0 @@
|
|
1
|
-
$LOAD_PATH.push File.expand_path("../lib", __FILE__)
|
2
|
-
require "rollie/version"
|
3
|
-
|
4
|
-
Gem::Specification.new do |s|
|
5
|
-
s.name = "rollie"
|
6
|
-
s.version = Rollie::VERSION
|
7
|
-
s.license = "MIT"
|
8
|
-
|
9
|
-
s.summary = "Generic rate limiter backed by Redis for efficient limiting using sliding windows."
|
10
|
-
s.description = s.summary
|
11
|
-
|
12
|
-
s.authors = ["Zach Davis", "Justin Howard"]
|
13
|
-
s.email = "justin.howard@parentsquare.com"
|
14
|
-
s.homepage = "https://github.com/ParentSquare/rollie"
|
15
|
-
|
16
|
-
s.files = `git ls-files -z`.split("\x0")
|
17
|
-
s.test_files = `git ls-files -- test/*`.split("\n")
|
18
|
-
|
19
|
-
s.require_paths = ["lib"]
|
20
|
-
|
21
|
-
s.add_dependency "redis", ">= 3.2.1"
|
22
|
-
s.add_dependency "redis-namespace", ">= 1.5.2"
|
23
|
-
s.add_dependency "connection_pool", ">= 2.2.0"
|
24
|
-
end
|
@@ -1,84 +0,0 @@
|
|
1
|
-
require "spec_helper"
|
2
|
-
require "securerandom"
|
3
|
-
|
4
|
-
module Rollie
|
5
|
-
describe RateLimiter do
|
6
|
-
|
7
|
-
before do
|
8
|
-
@r = RateLimiter.new(SecureRandom.hex(8), count_blocked: true)
|
9
|
-
end
|
10
|
-
|
11
|
-
describe :within_limit do
|
12
|
-
|
13
|
-
it "should require a block" do
|
14
|
-
expect{ @r.within_limit }.to raise_error(ArgumentError)
|
15
|
-
end
|
16
|
-
|
17
|
-
it "should return status" do
|
18
|
-
status = @r.within_limit do; end
|
19
|
-
expect(status.count).to eq(1)
|
20
|
-
expect(status.exceeded?).to be(false)
|
21
|
-
expect(status.time_remaining).to eq(1000)
|
22
|
-
end
|
23
|
-
|
24
|
-
it "should execute block only while within limit" do
|
25
|
-
count = 0
|
26
|
-
status = nil
|
27
|
-
30.times do
|
28
|
-
status = @r.within_limit do
|
29
|
-
count += 1
|
30
|
-
end
|
31
|
-
end
|
32
|
-
expect(count).to eq(25)
|
33
|
-
expect(status.count).to eq(30)
|
34
|
-
expect(status.exceeded?).to be(true)
|
35
|
-
end
|
36
|
-
|
37
|
-
it "should block all actions within the window" do
|
38
|
-
@r = RateLimiter.new(SecureRandom.hex(8), limit: 10, interval: 100, count_blocked: true)
|
39
|
-
count = 0
|
40
|
-
30.times do
|
41
|
-
@r.within_limit do
|
42
|
-
count += 1
|
43
|
-
end
|
44
|
-
sleep 0.004
|
45
|
-
end
|
46
|
-
expect(count).to eq(10)
|
47
|
-
end
|
48
|
-
|
49
|
-
it "should allow blocked actions not to be counted" do
|
50
|
-
@r = RateLimiter.new(SecureRandom.hex(8), limit: 10, interval: 100, count_blocked: false)
|
51
|
-
count = 0
|
52
|
-
30.times do
|
53
|
-
@r.within_limit do
|
54
|
-
count += 1
|
55
|
-
end
|
56
|
-
sleep 0.0045
|
57
|
-
end
|
58
|
-
expect(count).to eq(20)
|
59
|
-
end
|
60
|
-
|
61
|
-
end
|
62
|
-
|
63
|
-
describe :count do
|
64
|
-
|
65
|
-
it "should return the current count" do
|
66
|
-
30.times do
|
67
|
-
@r.within_limit do; sleep 0.001; end
|
68
|
-
end
|
69
|
-
|
70
|
-
expect(@r.count).to eq(30)
|
71
|
-
|
72
|
-
@r = RateLimiter.new(SecureRandom.hex(8), limit: 10, count_blocked: false)
|
73
|
-
|
74
|
-
30.times do
|
75
|
-
@r.within_limit do; sleep 0.001; end
|
76
|
-
end
|
77
|
-
|
78
|
-
expect(@r.count).to eq(10)
|
79
|
-
end
|
80
|
-
|
81
|
-
end
|
82
|
-
|
83
|
-
end
|
84
|
-
end
|
data/spec/rollie_spec.rb
DELETED
@@ -1,36 +0,0 @@
|
|
1
|
-
require "spec_helper"
|
2
|
-
|
3
|
-
describe Rollie do
|
4
|
-
|
5
|
-
describe :redis do
|
6
|
-
|
7
|
-
it "should require a block" do
|
8
|
-
expect{ Rollie.redis }.to raise_error(ArgumentError)
|
9
|
-
end
|
10
|
-
|
11
|
-
it "should return a Redis instance from the pool" do
|
12
|
-
Rollie.redis do |conn|
|
13
|
-
expect(conn.class).to eq(Redis::Namespace)
|
14
|
-
end
|
15
|
-
end
|
16
|
-
|
17
|
-
end
|
18
|
-
|
19
|
-
describe :redis= do
|
20
|
-
|
21
|
-
it "should allow hash options to initialize connection pool" do
|
22
|
-
options = {url: "redis://foo"}
|
23
|
-
pool = ConnectionPool.new do; end
|
24
|
-
expect(Rollie::RedisPool).to receive(:create).with(options).and_return(pool)
|
25
|
-
Rollie.redis = options
|
26
|
-
end
|
27
|
-
|
28
|
-
it "should allow a connection pool" do
|
29
|
-
pool = ConnectionPool.new do; end
|
30
|
-
Rollie.redis = pool
|
31
|
-
expect(Rollie.redis_pool).to eq(pool)
|
32
|
-
end
|
33
|
-
|
34
|
-
end
|
35
|
-
|
36
|
-
end
|
data/spec/spec_helper.rb
DELETED
@@ -1,106 +0,0 @@
|
|
1
|
-
# This file was generated by the `rspec --init` command. Conventionally, all
|
2
|
-
# specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
|
3
|
-
# The generated `.rspec` file contains `--require spec_helper` which will cause
|
4
|
-
# this file to always be loaded, without a need to explicitly require it in any
|
5
|
-
# files.
|
6
|
-
#
|
7
|
-
# Given that it is always loaded, you are encouraged to keep this file as
|
8
|
-
# light-weight as possible. Requiring heavyweight dependencies from this file
|
9
|
-
# will add to the boot time of your test suite on EVERY test run, even for an
|
10
|
-
# individual file that may not need all of that loaded. Instead, consider making
|
11
|
-
# a separate helper file that requires the additional dependencies and performs
|
12
|
-
# the additional setup, and require it from the spec files that actually need
|
13
|
-
# it.
|
14
|
-
#
|
15
|
-
# The `.rspec` file also contains a few flags that are not defaults but that
|
16
|
-
# users commonly want.
|
17
|
-
#
|
18
|
-
# See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
|
19
|
-
|
20
|
-
require "rollie"
|
21
|
-
|
22
|
-
RSpec.configure do |config|
|
23
|
-
# rspec-expectations config goes here. You can use an alternate
|
24
|
-
# assertion/expectation library such as wrong or the stdlib/minitest
|
25
|
-
# assertions if you prefer.
|
26
|
-
config.expect_with :rspec do |expectations|
|
27
|
-
# This option will default to `true` in RSpec 4. It makes the `description`
|
28
|
-
# and `failure_message` of custom matchers include text for helper methods
|
29
|
-
# defined using `chain`, e.g.:
|
30
|
-
# be_bigger_than(2).and_smaller_than(4).description
|
31
|
-
# # => "be bigger than 2 and smaller than 4"
|
32
|
-
# ...rather than:
|
33
|
-
# # => "be bigger than 2"
|
34
|
-
expectations.include_chain_clauses_in_custom_matcher_descriptions = true
|
35
|
-
end
|
36
|
-
|
37
|
-
# rspec-mocks config goes here. You can use an alternate test double
|
38
|
-
# library (such as bogus or mocha) by changing the `mock_with` option here.
|
39
|
-
config.mock_with :rspec do |mocks|
|
40
|
-
# Prevents you from mocking or stubbing a method that does not exist on
|
41
|
-
# a real object. This is generally recommended, and will default to
|
42
|
-
# `true` in RSpec 4.
|
43
|
-
mocks.verify_partial_doubles = true
|
44
|
-
end
|
45
|
-
|
46
|
-
# This option will default to `:apply_to_host_groups` in RSpec 4 (and will
|
47
|
-
# have no way to turn it off -- the option exists only for backwards
|
48
|
-
# compatibility in RSpec 3). It causes shared context metadata to be
|
49
|
-
# inherited by the metadata hash of host groups and examples, rather than
|
50
|
-
# triggering implicit auto-inclusion in groups with matching metadata.
|
51
|
-
config.shared_context_metadata_behavior = :apply_to_host_groups
|
52
|
-
|
53
|
-
# The settings below are suggested to provide a good initial experience
|
54
|
-
# with RSpec, but feel free to customize to your heart's content.
|
55
|
-
=begin
|
56
|
-
# This allows you to limit a spec run to individual examples or groups
|
57
|
-
# you care about by tagging them with `:focus` metadata. When nothing
|
58
|
-
# is tagged with `:focus`, all examples get run. RSpec also provides
|
59
|
-
# aliases for `it`, `describe`, and `context` that include `:focus`
|
60
|
-
# metadata: `fit`, `fdescribe` and `fcontext`, respectively.
|
61
|
-
config.filter_run_when_matching :focus
|
62
|
-
|
63
|
-
# Allows RSpec to persist some state between runs in order to support
|
64
|
-
# the `--only-failures` and `--next-failure` CLI options. We recommend
|
65
|
-
# you configure your source control system to ignore this file.
|
66
|
-
config.example_status_persistence_file_path = "spec/examples.txt"
|
67
|
-
|
68
|
-
# Limits the available syntax to the non-monkey patched syntax that is
|
69
|
-
# recommended. For more details, see:
|
70
|
-
# - http://rspec.info/blog/2012/06/rspecs-new-expectation-syntax/
|
71
|
-
# - http://www.teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/
|
72
|
-
# - http://rspec.info/blog/2014/05/notable-changes-in-rspec-3/#zero-monkey-patching-mode
|
73
|
-
config.disable_monkey_patching!
|
74
|
-
|
75
|
-
# This setting enables warnings. It's recommended, but in some cases may
|
76
|
-
# be too noisy due to issues in dependencies.
|
77
|
-
config.warnings = true
|
78
|
-
|
79
|
-
# Many RSpec users commonly either run the entire suite or an individual
|
80
|
-
# file, and it's useful to allow more verbose output when running an
|
81
|
-
# individual spec file.
|
82
|
-
if config.files_to_run.one?
|
83
|
-
# Use the documentation formatter for detailed output,
|
84
|
-
# unless a formatter has already been configured
|
85
|
-
# (e.g. via a command-line flag).
|
86
|
-
config.default_formatter = 'doc'
|
87
|
-
end
|
88
|
-
|
89
|
-
# Print the 10 slowest examples and example groups at the
|
90
|
-
# end of the spec run, to help surface which specs are running
|
91
|
-
# particularly slow.
|
92
|
-
config.profile_examples = 10
|
93
|
-
|
94
|
-
# Run specs in random order to surface order dependencies. If you find an
|
95
|
-
# order dependency and want to debug it, you can fix the order by providing
|
96
|
-
# the seed, which is printed after each run.
|
97
|
-
# --seed 1234
|
98
|
-
config.order = :random
|
99
|
-
|
100
|
-
# Seed global randomization in this process using the `--seed` CLI option.
|
101
|
-
# Setting this allows you to use `--seed` to deterministically reproduce
|
102
|
-
# test failures related to randomization by passing the same `--seed` value
|
103
|
-
# as the one that triggered the failure.
|
104
|
-
Kernel.srand config.seed
|
105
|
-
=end
|
106
|
-
end
|