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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: dcefe51efe099102f75381a902006faa8655f7207c7c971e0e0b0f356dc9c5e0
4
- data.tar.gz: c3a3647ea48bb31a95dcd1bc70ddcb72c0402e4474c459e88271666021ba878f
3
+ metadata.gz: 573c75da906a932bbc66fb90a6b2dbeaf7aabe12773178c28b197c81de4be9f0
4
+ data.tar.gz: f01dd67d1f923a5d63a29d67239f6a0a348a1236396a9e14ebefcf4702e51198
5
5
  SHA512:
6
- metadata.gz: bcfa6c8194738f83b7f688cc8211a79d10d8cee564a9b5099694c2730e71a1305c3d78dedad1b06654105d7da4b9070c9abb9d98aee97eca566eeaff32aed531
7
- data.tar.gz: cb88e0be3f91a53919d019009c35cadad7e50a46e719f06405b13912f24f195030a06686e7d28241d30a26bdf516946b7f1ca37cb429473bda189815f85cc1c7
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
- def perform(*args)
74
- on_throttled = proc do |state|
75
- raise Slack::LimitExceeded, state.ttl if state.ttl > 5
76
- end
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
- SpeedLimiter.throttle("slack", limit: 20, period: 1.minute, on_throttled: on_throttled) do
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
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "speed_limiter/errors/limit_exceeded_error"
4
+
5
+ module SpeedLimiter
6
+ module Errors
7
+ # SpeedLimiter::Throttled limit exceeded Error
8
+ class ThrottledError < LimitExceededError
9
+ end
10
+ end
11
+ end
@@ -19,7 +19,19 @@ module SpeedLimiter
19
19
 
20
20
  attr_reader :params, :count, :ttl
21
21
 
22
- def_delegators(:params, :config, :key, :limit, :period, :on_throttled, :retry)
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] => :config
29
+ delegate %i[redis_client] => :@config
26
30
 
27
- delegate %i[key redis_key limit period on_throttled create_state] => :params
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
- config.on_throttled.call(create_state(count: count, ttl: ttl)) if config.on_throttled.respond_to?(:call)
76
- on_throttled.call(create_state(count: count, ttl: ttl)) if on_throttled.respond_to?(:call)
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SpeedLimiter
4
- VERSION = "0.2.1"
4
+ VERSION = "0.3.0"
5
5
  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.2.1
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-13 00:00:00.000000000 Z
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