speed_limiter 0.2.1 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +21 -7
- data/lib/speed_limiter/errors/limit_exceeded_error.rb +35 -0
- data/lib/speed_limiter/errors/throttled_error.rb +11 -0
- data/lib/speed_limiter/state.rb +13 -1
- data/lib/speed_limiter/throttle.rb +14 -4
- data/lib/speed_limiter/throttle_params.rb +50 -0
- data/lib/speed_limiter/version.rb +1 -1
- metadata +4 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 573c75da906a932bbc66fb90a6b2dbeaf7aabe12773178c28b197c81de4be9f0
|
4
|
+
data.tar.gz: f01dd67d1f923a5d63a29d67239f6a0a348a1236396a9e14ebefcf4702e51198
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f02a8e3fd56bd84f5bf5cf3eb3b24336a9805d04cf89f01fb53dcbc745125010e555f54bb2494689e05a0527819ab094d0eb461cb0cb3f876db0c423f684b123
|
7
|
+
data.tar.gz: c61501a41484331f0041b665407c1b0ff609f84f31ffac3f4b9725162aecdc1960f10ab8207cbce6a4a54b4790abdc532de70241e22eaaf2bc1db28833b97492
|
data/README.md
CHANGED
@@ -66,20 +66,34 @@ SpeedLimiter.throttle('server_name/method_name', limit: 10, period: 1, on_thrott
|
|
66
66
|
end
|
67
67
|
```
|
68
68
|
|
69
|
+
### raise_on_throttled option
|
70
|
+
|
71
|
+
It raises an exception when the limit is exceeded.
|
72
|
+
|
73
|
+
```ruby
|
74
|
+
begin
|
75
|
+
SpeedLimiter.throttle('server_name/method_name', limit: 10, period: 1, raise_on_throttled: true) do
|
76
|
+
http.get(path)
|
77
|
+
end
|
78
|
+
rescue SpeedLimiter::ThrottledError => e
|
79
|
+
logger.info(e.message) #=> "server_name/method_name rate limit exceeded. Retry after 0.9 seconds. limit=10, count=11, period=1"
|
80
|
+
e.state #=> <SpeedLimiter::State key=server_name/method_name count=11 ttl=0.9>
|
81
|
+
end
|
82
|
+
```
|
83
|
+
|
69
84
|
Reinitialize the queue instead of sleeping when the limit is reached in ActiveJob.
|
70
85
|
|
71
86
|
```ruby
|
72
87
|
class CreateSlackChannelJob < ApplicationJob
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
88
|
+
rescue_from(SpeedLimiter::ThrottledError) do |e|
|
89
|
+
Rails.logger.warn("[#{e.class}] #{self.class} retry job. #{e.message}")
|
90
|
+
retry_job(wait: e.ttl, queue: 'low')
|
91
|
+
end
|
77
92
|
|
78
|
-
|
93
|
+
def perform(*args)
|
94
|
+
SpeedLimiter.throttle("slack", limit: 20, period: 1.minute, raise_on_throttled: true) do
|
79
95
|
create_slack_channel(*args)
|
80
96
|
end
|
81
|
-
rescue Slack::LimitExceeded => e
|
82
|
-
self.class.set(wait: e.ttl).perform_later(*args)
|
83
97
|
end
|
84
98
|
end
|
85
99
|
```
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "forwardable"
|
4
|
+
|
5
|
+
module SpeedLimiter
|
6
|
+
module Errors
|
7
|
+
# SpeedLimiter limit exceeded Base Error
|
8
|
+
class LimitExceededError < StandardError
|
9
|
+
extend Forwardable
|
10
|
+
|
11
|
+
# @param state [SpeedLimiter::State]
|
12
|
+
def initialize(state)
|
13
|
+
@state = state
|
14
|
+
super(error_message)
|
15
|
+
end
|
16
|
+
attr_reader :state
|
17
|
+
|
18
|
+
# @!method key
|
19
|
+
# @see SpeedLimiter::State#key
|
20
|
+
# @!method ttl
|
21
|
+
# @see SpeedLimiter::State#ttl
|
22
|
+
# @!method count
|
23
|
+
# @see SpeedLimiter::State#count
|
24
|
+
# @!method limit
|
25
|
+
# @see SpeedLimiter::State#limit
|
26
|
+
# @!method period
|
27
|
+
# @see SpeedLimiter::State#period
|
28
|
+
delegate %i[key ttl count limit period] => :@state
|
29
|
+
|
30
|
+
def error_message
|
31
|
+
"#{key} rate limit exceeded. Retry after #{ttl} seconds. limit=#{limit}, count=#{count}, period=#{period}"
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
data/lib/speed_limiter/state.rb
CHANGED
@@ -19,7 +19,19 @@ module SpeedLimiter
|
|
19
19
|
|
20
20
|
attr_reader :params, :count, :ttl
|
21
21
|
|
22
|
-
|
22
|
+
# @!method config
|
23
|
+
# @see SpeedLimiter::ThrottleParams#config
|
24
|
+
# @!method key
|
25
|
+
# @see SpeedLimiter::ThrottleParams#key
|
26
|
+
# @!method limit
|
27
|
+
# @see SpeedLimiter::ThrottleParams#limit
|
28
|
+
# @!method period
|
29
|
+
# @see SpeedLimiter::ThrottleParams#period
|
30
|
+
# @!method on_throttled
|
31
|
+
# @see SpeedLimiter::ThrottleParams#on_throttled
|
32
|
+
# @!method retry
|
33
|
+
# @see SpeedLimiter::ThrottleParams#retry
|
34
|
+
delegate %i[config key limit period on_throttled retry] => :@params
|
23
35
|
|
24
36
|
def inspect
|
25
37
|
"<#{self.class.name} key=#{key.inspect} count=#{count} ttl=#{ttl}>"
|
@@ -13,6 +13,10 @@ module SpeedLimiter
|
|
13
13
|
# @option params [Integer] :limit limit count per period
|
14
14
|
# @option params [Integer] :period period time (seconds)
|
15
15
|
# @option params [Proc, #call] :on_throttled Block called when limit exceeded, with ttl(Float) and key as argument
|
16
|
+
# @option params [true, Class] :raise_on_throttled
|
17
|
+
# Raise error when limit exceeded. If Class is given, it will be raised instead of SpeedLimiter::ThrottledError.
|
18
|
+
# If you want to specify a custom error class, please specify a class that inherits from
|
19
|
+
# SpeedLimiter::LimitExceededError or a class that accepts SpeedLimiter::State as an argument.
|
16
20
|
# @option params [true, Hash] :retry Retry options. (see {Retryable.retryable} for details)
|
17
21
|
def initialize(key, config:, **params)
|
18
22
|
params[:key] = key.to_s
|
@@ -22,9 +26,11 @@ module SpeedLimiter
|
|
22
26
|
end
|
23
27
|
attr_reader :config, :params, :block
|
24
28
|
|
25
|
-
delegate %i[redis_client] =>
|
29
|
+
delegate %i[redis_client] => :@config
|
26
30
|
|
27
|
-
delegate %i[
|
31
|
+
delegate %i[
|
32
|
+
key redis_key limit period on_throttled raise_on_throttled_class raise_on_throttled? create_state
|
33
|
+
] => :@params
|
28
34
|
|
29
35
|
# @yield [state]
|
30
36
|
# @yieldparam state [SpeedLimiter::State]
|
@@ -72,8 +78,12 @@ module SpeedLimiter
|
|
72
78
|
ttl = redis_client.ttl(redis_key)
|
73
79
|
return if ttl.negative?
|
74
80
|
|
75
|
-
|
76
|
-
|
81
|
+
create_state(count: count, ttl: ttl).tap do |state|
|
82
|
+
raise raise_on_throttled_class, state if raise_on_throttled?
|
83
|
+
|
84
|
+
config.on_throttled.call(state) if config.on_throttled.respond_to?(:call)
|
85
|
+
on_throttled.call(state) if on_throttled.respond_to?(:call)
|
86
|
+
end
|
77
87
|
|
78
88
|
ttl = redis_client.ttl(redis_key)
|
79
89
|
return if ttl.negative?
|
@@ -1,32 +1,82 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require "speed_limiter/state"
|
4
|
+
require "speed_limiter/errors/throttled_error"
|
4
5
|
|
5
6
|
module SpeedLimiter
|
6
7
|
# Throttle params model
|
7
8
|
class ThrottleParams
|
9
|
+
KNOWN_OPTIONS = %i[on_throttled retry raise_on_throttled].freeze
|
10
|
+
|
11
|
+
# @param config [SpeedLimiter::Config]
|
12
|
+
# @param key [String]
|
13
|
+
# @param limit [Integer] limit count per period
|
14
|
+
# @param period [Integer] period time (seconds)
|
15
|
+
# @param options [Hash] options
|
16
|
+
# @option options [Proc, #call] :on_throttled Block called when limit exceeded, with ttl(Float) and key as argument
|
17
|
+
# @option options [true, Class] :raise_on_throttled
|
18
|
+
# Raise error when limit exceeded. If Class is given, it will be raised instead of SpeedLimiter::ThrottledError.
|
19
|
+
# If you want to specify a custom error class, please specify a class that inherits from
|
20
|
+
# SpeedLimiter::LimitExceededError or a class that accepts SpeedLimiter::State as an argument.
|
21
|
+
# @option options [true, Hash] :retry Retry options. (see {Retryable.retryable} for details)
|
8
22
|
def initialize(config:, key:, limit:, period:, **options)
|
9
23
|
@config = config
|
10
24
|
@key = key
|
11
25
|
@limit = limit
|
12
26
|
@period = period
|
13
27
|
@options = options
|
28
|
+
|
29
|
+
return unless (unknown_options = options.keys - KNOWN_OPTIONS).any?
|
30
|
+
|
31
|
+
raise ArgumentError, "Unknown options: #{unknown_options.join(', ')}"
|
14
32
|
end
|
15
33
|
|
34
|
+
# @!method config
|
35
|
+
# @return [SpeedLimiter::Config]
|
36
|
+
# @!method key
|
37
|
+
# @return [String] Throttle key name
|
38
|
+
# @!method limit
|
39
|
+
# @return [Integer] limit count per period
|
40
|
+
# @!method period
|
41
|
+
# @return [Integer] period time (seconds)
|
16
42
|
attr_reader :config, :key, :limit, :period
|
17
43
|
|
18
44
|
def on_throttled
|
19
45
|
@options[:on_throttled]
|
20
46
|
end
|
21
47
|
|
48
|
+
# @return [Boolean, Class]
|
49
|
+
def raise_on_throttled
|
50
|
+
@options[:raise_on_throttled]
|
51
|
+
end
|
52
|
+
|
53
|
+
# @return [Boolean]
|
54
|
+
def raise_on_throttled?
|
55
|
+
!!raise_on_throttled
|
56
|
+
end
|
57
|
+
|
58
|
+
# @return [Class]
|
59
|
+
def raise_on_throttled_class
|
60
|
+
if raise_on_throttled.is_a?(Class)
|
61
|
+
raise_on_throttled
|
62
|
+
else
|
63
|
+
SpeedLimiter::Errors::ThrottledError
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
# @return [Boolean, Hash]
|
22
68
|
def retry
|
23
69
|
@options[:retry]
|
24
70
|
end
|
25
71
|
|
72
|
+
# @return [String]
|
26
73
|
def redis_key
|
27
74
|
"#{config.prefix}:#{key}"
|
28
75
|
end
|
29
76
|
|
77
|
+
# @param count [Integer, nil]
|
78
|
+
# @param ttl [Float, nil]
|
79
|
+
# @return [SpeedLimiter::State]
|
30
80
|
def create_state(count: nil, ttl: nil)
|
31
81
|
State.new(params: self, count: count, ttl: ttl)
|
32
82
|
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.
|
4
|
+
version: 0.3.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- yuhei mukoyama
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2024-04-
|
11
|
+
date: 2024-04-14 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: redis
|
@@ -45,6 +45,8 @@ files:
|
|
45
45
|
- compose.yaml
|
46
46
|
- lib/speed_limiter.rb
|
47
47
|
- lib/speed_limiter/config.rb
|
48
|
+
- lib/speed_limiter/errors/limit_exceeded_error.rb
|
49
|
+
- lib/speed_limiter/errors/throttled_error.rb
|
48
50
|
- lib/speed_limiter/redis.rb
|
49
51
|
- lib/speed_limiter/state.rb
|
50
52
|
- lib/speed_limiter/throttle.rb
|