speed_limiter 0.1.0 → 0.2.1
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 +4 -4
- data/.github/workflows/lint.yml +1 -1
- data/.github/workflows/test.yml +23 -6
- data/Gemfile +1 -0
- data/README.md +46 -4
- data/lib/speed_limiter/config.rb +4 -0
- data/lib/speed_limiter/state.rb +4 -1
- data/lib/speed_limiter/throttle.rb +43 -18
- data/lib/speed_limiter/throttle_params.rb +11 -3
- data/lib/speed_limiter/version.rb +1 -1
- data/lib/speed_limiter.rb +12 -12
- metadata +3 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: dcefe51efe099102f75381a902006faa8655f7207c7c971e0e0b0f356dc9c5e0
|
4
|
+
data.tar.gz: c3a3647ea48bb31a95dcd1bc70ddcb72c0402e4474c459e88271666021ba878f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: bcfa6c8194738f83b7f688cc8211a79d10d8cee564a9b5099694c2730e71a1305c3d78dedad1b06654105d7da4b9070c9abb9d98aee97eca566eeaff32aed531
|
7
|
+
data.tar.gz: cb88e0be3f91a53919d019009c35cadad7e50a46e719f06405b13912f24f195030a06686e7d28241d30a26bdf516946b7f1ca37cb429473bda189815f85cc1c7
|
data/.github/workflows/lint.yml
CHANGED
data/.github/workflows/test.yml
CHANGED
@@ -20,22 +20,39 @@ jobs:
|
|
20
20
|
strategy:
|
21
21
|
max-parallel: 6
|
22
22
|
matrix:
|
23
|
-
|
24
|
-
|
23
|
+
set:
|
24
|
+
- ruby-version: '3.0'
|
25
|
+
redis-version: '7.2'
|
26
|
+
- ruby-version: '3.1'
|
27
|
+
redis-version: '7.2'
|
28
|
+
- ruby-version: '3.2'
|
29
|
+
redis-version: '5.0'
|
30
|
+
- ruby-version: '3.2'
|
31
|
+
redis-version: '6.0'
|
32
|
+
- ruby-version: '3.2'
|
33
|
+
redis-version: '6.2'
|
34
|
+
- ruby-version: '3.2'
|
35
|
+
redis-version: '7.0'
|
36
|
+
- ruby-version: '3.2'
|
37
|
+
redis-version: '7.2'
|
38
|
+
- ruby-version: '3.2'
|
39
|
+
redis-version: latest
|
40
|
+
- ruby-version: ruby-head
|
41
|
+
redis-version: '7.2'
|
25
42
|
|
26
43
|
services:
|
27
44
|
redis:
|
28
|
-
image: redis:${{ matrix.redis-version }}
|
45
|
+
image: redis:${{ matrix.set.redis-version }}
|
29
46
|
ports:
|
30
47
|
- 6379:6379
|
31
48
|
|
32
49
|
steps:
|
33
|
-
- uses: actions/checkout@
|
50
|
+
- uses: actions/checkout@v4
|
34
51
|
|
35
|
-
- name: Set up Ruby ${{ matrix.ruby-version }}
|
52
|
+
- name: Set up Ruby ${{ matrix.set.ruby-version }}
|
36
53
|
uses: ruby/setup-ruby@v1
|
37
54
|
with:
|
38
|
-
ruby-version: ${{ matrix.ruby-version }}
|
55
|
+
ruby-version: ${{ matrix.set.ruby-version }}
|
39
56
|
bundler-cache: true
|
40
57
|
|
41
58
|
- name: Rackup test web server
|
data/Gemfile
CHANGED
data/README.md
CHANGED
@@ -35,6 +35,15 @@ SpeedLimiter.throttle('server_name/method_name', limit: 10, period: 1) do |state
|
|
35
35
|
puts state #=> <SpeedLimiter::State key=server_name/method_name count=1 ttl=0>
|
36
36
|
http.get(path)
|
37
37
|
end
|
38
|
+
|
39
|
+
# or
|
40
|
+
|
41
|
+
throttle_limit_10_par_sec = SpeedLimiter.throttle('server_name/method_name', limit: 10, period: 1)
|
42
|
+
|
43
|
+
throttle_limit_10_par_sec.call do |state|
|
44
|
+
puts state #=> <SpeedLimiter::State key=server_name/method_name count=1 ttl=0>
|
45
|
+
http.get(path)
|
46
|
+
end
|
38
47
|
```
|
39
48
|
|
40
49
|
It returns the result of the block execution.
|
@@ -46,6 +55,8 @@ end
|
|
46
55
|
puts result.code #=> 200
|
47
56
|
```
|
48
57
|
|
58
|
+
### on_throttled option
|
59
|
+
|
49
60
|
Specify the process when the limit is exceeded.
|
50
61
|
|
51
62
|
```ruby
|
@@ -73,9 +84,40 @@ class CreateSlackChannelJob < ApplicationJob
|
|
73
84
|
end
|
74
85
|
```
|
75
86
|
|
76
|
-
###
|
87
|
+
### retry option
|
88
|
+
|
89
|
+
To use the `retry:` option, you need to introduce the [Retryable gem](https://github.com/nfedyashev/retryable).
|
90
|
+
By specifying the options of the Retryable gem, you can retry when an exception occurs.
|
91
|
+
|
92
|
+
```ruby
|
93
|
+
# Gemfile
|
94
|
+
gem 'retryable'
|
95
|
+
```
|
96
|
+
|
97
|
+
```ruby
|
98
|
+
SpeedLimiter.throttle('server_name/method_name', limit: 10, period: 1, retry: { tries: 3, on: OpenURI::HTTPError }) do
|
99
|
+
http.get(path)
|
100
|
+
end
|
101
|
+
|
102
|
+
# equivalent to
|
103
|
+
SpeedLimiter.throttle('server_name/method_name', limit: 10, period: 1) do
|
104
|
+
Retryable.retryable(tries: 3, on: OpenURI::HTTPError) do
|
105
|
+
http.get(path)
|
106
|
+
end
|
107
|
+
end
|
108
|
+
```
|
109
|
+
|
110
|
+
`retry: true` or `retry: {}` is default use of `Retryable.configure`.
|
111
|
+
|
112
|
+
```ruby
|
113
|
+
SpeedLimiter.throttle('server_name/method_name', limit: 10, period: 1, retry: true) do
|
114
|
+
http.get(path)
|
115
|
+
end
|
116
|
+
```
|
117
|
+
|
118
|
+
## Configuration
|
77
119
|
|
78
|
-
|
120
|
+
### Redis configuration
|
79
121
|
|
80
122
|
Redis can be specified as follows
|
81
123
|
|
@@ -99,7 +141,7 @@ SpeedLimiter.configure do |config|
|
|
99
141
|
end
|
100
142
|
```
|
101
143
|
|
102
|
-
|
144
|
+
### Other configuration defaults
|
103
145
|
|
104
146
|
```ruby
|
105
147
|
SpeedLimiter.configure do |config|
|
@@ -111,7 +153,7 @@ SpeedLimiter.configure do |config|
|
|
111
153
|
end
|
112
154
|
```
|
113
155
|
|
114
|
-
|
156
|
+
### Example
|
115
157
|
|
116
158
|
If you do not want to impose a limit in the test environment, please set it as follows.
|
117
159
|
|
data/lib/speed_limiter/config.rb
CHANGED
data/lib/speed_limiter/state.rb
CHANGED
@@ -8,6 +8,9 @@ module SpeedLimiter
|
|
8
8
|
class State
|
9
9
|
extend Forwardable
|
10
10
|
|
11
|
+
# @param params [SpeedLimiter::ThrottleParams]
|
12
|
+
# @param count [Integer] current count
|
13
|
+
# @param ttl [Float] remaining time to reset
|
11
14
|
def initialize(params:, count:, ttl:)
|
12
15
|
@params = params
|
13
16
|
@count = count
|
@@ -16,7 +19,7 @@ module SpeedLimiter
|
|
16
19
|
|
17
20
|
attr_reader :params, :count, :ttl
|
18
21
|
|
19
|
-
def_delegators(:params, :config, :key, :limit, :period, :on_throttled)
|
22
|
+
def_delegators(:params, :config, :key, :limit, :period, :on_throttled, :retry)
|
20
23
|
|
21
24
|
def inspect
|
22
25
|
"<#{self.class.name} key=#{key.inspect} count=#{count} ttl=#{ttl}>"
|
@@ -9,27 +9,58 @@ module SpeedLimiter
|
|
9
9
|
class Throttle
|
10
10
|
extend Forwardable
|
11
11
|
|
12
|
-
# @
|
12
|
+
# @param key [String, #to_s] Throttle key name
|
13
13
|
# @option params [Integer] :limit limit count per period
|
14
14
|
# @option params [Integer] :period period time (seconds)
|
15
|
-
# @option params [Proc] :on_throttled Block called when limit exceeded, with ttl(Float) and key as argument
|
16
|
-
# @
|
17
|
-
|
18
|
-
|
19
|
-
|
15
|
+
# @option params [Proc, #call] :on_throttled Block called when limit exceeded, with ttl(Float) and key as argument
|
16
|
+
# @option params [true, Hash] :retry Retry options. (see {Retryable.retryable} for details)
|
17
|
+
def initialize(key, config:, **params)
|
18
|
+
params[:key] = key.to_s
|
19
|
+
|
20
20
|
@config = config
|
21
21
|
@params = ThrottleParams.new(config: config, **params)
|
22
|
-
@block = block
|
23
22
|
end
|
24
23
|
attr_reader :config, :params, :block
|
25
24
|
|
26
|
-
|
25
|
+
delegate %i[redis_client] => :config
|
26
|
+
|
27
|
+
delegate %i[key redis_key limit period on_throttled create_state] => :params
|
28
|
+
|
29
|
+
# @yield [state]
|
30
|
+
# @yieldparam state [SpeedLimiter::State]
|
31
|
+
# @return [any] block return value
|
32
|
+
def call(&block)
|
33
|
+
if use_retryable?
|
34
|
+
Retryable.retryable(**retryable_options) { run_block(&block) }
|
35
|
+
else
|
36
|
+
run_block(&block)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
def use_retryable?
|
43
|
+
return false if params.retry == false || params.retry.nil?
|
44
|
+
|
45
|
+
unless Gem::Specification.find_by_name("retryable")
|
46
|
+
raise ArgumentError, "To use the 'retry' option, you need to install the Retryable gem."
|
47
|
+
end
|
27
48
|
|
28
|
-
|
49
|
+
require "retryable"
|
50
|
+
params.retry.is_a?(Hash) || params.retry == true
|
51
|
+
end
|
52
|
+
|
53
|
+
def retryable_options
|
54
|
+
return {} if params.retry == true
|
55
|
+
|
56
|
+
params.retry
|
57
|
+
end
|
58
|
+
|
59
|
+
def run_block(&block)
|
29
60
|
return block.call(create_state) if config.no_limit?
|
30
61
|
|
31
62
|
loop do
|
32
|
-
count, ttl =
|
63
|
+
count, ttl = redis_client.increment(redis_key, period)
|
33
64
|
|
34
65
|
break(block.call(create_state(count: count, ttl: ttl))) if count <= limit
|
35
66
|
|
@@ -37,23 +68,17 @@ module SpeedLimiter
|
|
37
68
|
end
|
38
69
|
end
|
39
70
|
|
40
|
-
private
|
41
|
-
|
42
71
|
def wait_for_interval(count)
|
43
|
-
ttl =
|
72
|
+
ttl = redis_client.ttl(redis_key)
|
44
73
|
return if ttl.negative?
|
45
74
|
|
46
75
|
config.on_throttled.call(create_state(count: count, ttl: ttl)) if config.on_throttled.respond_to?(:call)
|
47
76
|
on_throttled.call(create_state(count: count, ttl: ttl)) if on_throttled.respond_to?(:call)
|
48
77
|
|
49
|
-
ttl =
|
78
|
+
ttl = redis_client.ttl(redis_key)
|
50
79
|
return if ttl.negative?
|
51
80
|
|
52
81
|
sleep ttl
|
53
82
|
end
|
54
|
-
|
55
|
-
def redis
|
56
|
-
@redis ||= SpeedLimiter::Redis.new(config.redis || ::Redis.new(url: config.redis_url))
|
57
|
-
end
|
58
83
|
end
|
59
84
|
end
|
@@ -5,15 +5,23 @@ require "speed_limiter/state"
|
|
5
5
|
module SpeedLimiter
|
6
6
|
# Throttle params model
|
7
7
|
class ThrottleParams
|
8
|
-
def initialize(config:, key:, limit:, period:,
|
8
|
+
def initialize(config:, key:, limit:, period:, **options)
|
9
9
|
@config = config
|
10
10
|
@key = key
|
11
11
|
@limit = limit
|
12
12
|
@period = period
|
13
|
-
@
|
13
|
+
@options = options
|
14
14
|
end
|
15
15
|
|
16
|
-
attr_reader :config, :key, :limit, :period
|
16
|
+
attr_reader :config, :key, :limit, :period
|
17
|
+
|
18
|
+
def on_throttled
|
19
|
+
@options[:on_throttled]
|
20
|
+
end
|
21
|
+
|
22
|
+
def retry
|
23
|
+
@options[:retry]
|
24
|
+
end
|
17
25
|
|
18
26
|
def redis_key
|
19
27
|
"#{config.prefix}:#{key}"
|
data/lib/speed_limiter.rb
CHANGED
@@ -16,19 +16,19 @@ module SpeedLimiter
|
|
16
16
|
yield(config)
|
17
17
|
end
|
18
18
|
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
# @
|
24
|
-
# @param limit [Integer] limit count per period
|
25
|
-
# @param period [Integer] period time (seconds)
|
26
|
-
# @param on_throttled [Proc] Block called when limit exceeded, with ttl(Float) and key as argument
|
27
|
-
# @yield [count] Block called to not reach limit
|
28
|
-
# @yieldparam count [Integer] count of period
|
29
|
-
# @yieldreturn [any] block return value
|
19
|
+
# @param key (see Throttle#initialize)
|
20
|
+
# @option (see Throttle#initialize)
|
21
|
+
# @yield (see Throttle#call)
|
22
|
+
# @yieldparam (see Throttle#call)
|
23
|
+
# @return Return value of block if argument contains block, otherwise Throttle instance
|
30
24
|
def throttle(key, **params, &block)
|
31
|
-
Throttle.new(config: config,
|
25
|
+
throttle = Throttle.new(key, config: config, **params)
|
26
|
+
|
27
|
+
if block
|
28
|
+
throttle.call(&block)
|
29
|
+
else
|
30
|
+
throttle
|
31
|
+
end
|
32
32
|
end
|
33
33
|
end
|
34
34
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: speed_limiter
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1
|
4
|
+
version: 0.2.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- yuhei mukoyama
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2024-04-13 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: redis
|
@@ -75,7 +75,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
75
75
|
- !ruby/object:Gem::Version
|
76
76
|
version: '0'
|
77
77
|
requirements: []
|
78
|
-
rubygems_version: 3.
|
78
|
+
rubygems_version: 3.5.7
|
79
79
|
signing_key:
|
80
80
|
specification_version: 4
|
81
81
|
summary: Limit the frequency of execution across multiple threads and processes
|