redis-throttle 1.0.0 → 2.0.0

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.
@@ -1,75 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- class Redis
4
- class Throttle
5
- class RateLimit
6
- # @!attribute [r] bucket
7
- # @return [String] Throttling group name
8
- attr_reader :bucket
9
-
10
- # @!attribute [r] limit
11
- # @return [Integer] Max allowed units per {#period}
12
- attr_reader :limit
13
-
14
- # @!attribute [r] period
15
- # @return [Integer] Period in seconds
16
- attr_reader :period
17
-
18
- # @param bucket [#to_s] Throttling group name
19
- # @param limit [#to_i] Max allowed units per `period`
20
- # @param period [#to_i] Period in seconds
21
- def initialize(bucket, limit:, period:)
22
- @bucket = -bucket.to_s
23
- @limit = limit.to_i
24
- @period = period.to_i
25
- end
26
-
27
- # Returns `true` if `other` is a {RateLimit} instance with the same
28
- # {#bucket}, {#limit}, and {#period}.
29
- #
30
- # @see https://docs.ruby-lang.org/en/master/Object.html#method-i-eql-3F
31
- # @param other [Object]
32
- # @return [Boolean]
33
- def ==(other)
34
- return true if equal? other
35
- return false unless other.is_a?(self.class)
36
-
37
- @bucket == other.bucket && @limit == other.limit && @period == other.period
38
- end
39
-
40
- alias eql? ==
41
-
42
- # @api private
43
- #
44
- # Compare `self` with `other` strategy:
45
- #
46
- # - Returns `nil` if `other` is neither {Concurrency} nor {RateLimit}
47
- # - Returns `-1` if `other` is a {Concurrency}
48
- # - Returns `1` if `other` is a {RateLimit} with lower {#limit}
49
- # - Returns `0` if `other` is a {RateLimit} with the same {#limit}
50
- # - Returns `-1` if `other` is a {RateLimit} with bigger {#limit}
51
- #
52
- # @return [-1, 0, 1, nil]
53
- def <=>(other)
54
- complexity <=> other.complexity if other.respond_to? :complexity
55
- end
56
-
57
- # @api private
58
- #
59
- # Generates an Integer hash value for this object.
60
- #
61
- # @see https://docs.ruby-lang.org/en/master/Object.html#method-i-hash
62
- # @return [Integer]
63
- def hash
64
- @hash ||= [@bucket, @limit, @period].hash
65
- end
66
-
67
- # @api private
68
- #
69
- # @return [Array(Integer, Integer)] Strategy complexity pseudo-score
70
- def complexity
71
- [0, @limit]
72
- end
73
- end
74
- end
75
- end
@@ -1,53 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "digest"
4
- require "redis/errors"
5
-
6
- require_relative "./errors"
7
-
8
- class Redis
9
- class Throttle
10
- # @api private
11
- #
12
- # Lazy-compile and run acquire script by it's sha1 digest.
13
- class Script
14
- # Redis error fired when script ID is unkown.
15
- NOSCRIPT = "NOSCRIPT"
16
- private_constant :NOSCRIPT
17
-
18
- LUA_ERROR_MESSAGE = %r{
19
- ERR\s
20
- (?<message>Error\s(?:compiling|running)\sscript)
21
- \s\([^()]+\):\s
22
- (?:@[^:]+:\d+:\s)?
23
- [^:]+:(?<loc>\d+):\s
24
- (?<details>.+)
25
- }x.freeze
26
- private_constant :LUA_ERROR_MESSAGE
27
-
28
- def initialize(source)
29
- @source = -source.to_s
30
- @digest = Digest::SHA1.hexdigest(@source).freeze
31
- end
32
-
33
- def call(redis, keys: [], argv: [])
34
- __eval__(redis, keys, argv)
35
- rescue Redis::CommandError => e
36
- md = LUA_ERROR_MESSAGE.match(e.message.to_s)
37
- raise unless md
38
-
39
- raise ScriptError, "#{md[:message]} @#{md[:loc]}: #{md[:details]}"
40
- end
41
-
42
- private
43
-
44
- def __eval__(redis, keys, argv)
45
- redis.evalsha(@digest, keys, argv)
46
- rescue Redis::CommandError => e
47
- raise unless e.message.include?(NOSCRIPT)
48
-
49
- redis.eval(@source, keys, argv)
50
- end
51
- end
52
- end
53
- end
@@ -1,8 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- class Redis
4
- class Throttle
5
- # Gem version.
6
- VERSION = "1.0.0"
7
- end
8
- end
@@ -1,288 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "redis"
4
- require "set"
5
- require "securerandom"
6
-
7
- require_relative "./throttle/api"
8
- require_relative "./throttle/errors"
9
- require_relative "./throttle/class_methods"
10
- require_relative "./throttle/concurrency"
11
- require_relative "./throttle/rate_limit"
12
- require_relative "./throttle/version"
13
-
14
- # @see https://github.com/redis/redis-rb
15
- class Redis
16
- # Distributed rate limit and concurrency throttling.
17
- class Throttle
18
- extend ClassMethods
19
-
20
- # @param (see Api#initialize)
21
- def initialize(redis: nil)
22
- @api = Api.new(:redis => redis)
23
- @strategies = SortedSet.new
24
- end
25
-
26
- # @api private
27
- #
28
- # Dup internal strategies plan.
29
- #
30
- # @return [void]
31
- def initialize_dup(original)
32
- super
33
-
34
- @strategies = original.strategies.dup
35
- end
36
-
37
- # @api private
38
- #
39
- # Clone internal strategies plan.
40
- #
41
- # @return [void]
42
- def initialize_clone(original)
43
- super
44
-
45
- @strategies = original.strategies.clone
46
- end
47
-
48
- # Add *concurrency* strategy to the throttle. Use it to guarantee `limit`
49
- # amount of concurrently running code blocks.
50
- #
51
- # @example
52
- # throttle = Redis::Throttle.new
53
- #
54
- # # Allow max 2 concurrent execution units
55
- # throttle.concurrency(:xxx, :limit => 2, :ttl => 10)
56
- #
57
- # throttle.acquire(:token => "a") && :aye || :nay # => :aye
58
- # throttle.acquire(:token => "b") && :aye || :nay # => :aye
59
- # throttle.acquire(:token => "c") && :aye || :nay # => :nay
60
- #
61
- # throttle.release(:token => "a")
62
- #
63
- # throttle.acquire(:token => "c") && :aye || :nay # => :aye
64
- #
65
- #
66
- # @param (see Concurrency#initialize)
67
- # @return [Throttle] self
68
- def concurrency(bucket, limit:, ttl:)
69
- raise FrozenError, "can't modify frozen #{self.class}" if frozen?
70
-
71
- @strategies << Concurrency.new(bucket, :limit => limit, :ttl => ttl)
72
-
73
- self
74
- end
75
-
76
- # Add *rate limit* strategy to the throttle. Use it to guarantee `limit`
77
- # amount of units in `period` of time.
78
- #
79
- # @example
80
- # throttle = Redis::Throttle.new
81
- #
82
- # # Allow 2 execution units per 10 seconds
83
- # throttle.rate_limit(:xxx, :limit => 2, :period => 10)
84
- #
85
- # throttle.acquire && :aye || :nay # => :aye
86
- # sleep 5
87
- #
88
- # throttle.acquire && :aye || :nay # => :aye
89
- # throttle.acquire && :aye || :nay # => :nay
90
- #
91
- # sleep 6
92
- # throttle.acquire && :aye || :nay # => :aye
93
- # throttle.acquire && :aye || :nay # => :nay
94
- #
95
- # @param (see RateLimit#initialize)
96
- # @return [Throttle] self
97
- def rate_limit(bucket, limit:, period:)
98
- raise FrozenError, "can't modify frozen #{self.class}" if frozen?
99
-
100
- @strategies << RateLimit.new(bucket, :limit => limit, :period => period)
101
-
102
- self
103
- end
104
-
105
- # Merge in strategies of the `other` throttle.
106
- #
107
- # @example
108
- # a = Redis::Throttle.concurrency(:a, :limit => 1, :ttl => 2)
109
- # b = Redis::Throttle.rate_limit(:b, :limit => 3, :period => 4)
110
- # c = Redis::Throttle
111
- # .concurrency(:a, :limit => 1, :ttl => 2)
112
- # .rate_limit(:b, :limit => 3, :period => 4)
113
- #
114
- # a.merge!(b)
115
- #
116
- # a == c # => true
117
- #
118
- # @return [Throttle] self
119
- def merge!(other)
120
- raise FrozenError, "can't modify frozen #{self.class}" if frozen?
121
-
122
- @strategies.merge(other.strategies)
123
-
124
- self
125
- end
126
-
127
- alias << merge!
128
-
129
- # Non-destructive version of {#merge!}. Returns new {Throttle} instance with
130
- # union of `self` and `other` strategies.
131
- #
132
- # @example
133
- # a = Redis::Throttle.concurrency(:a, :limit => 1, :ttl => 2)
134
- # b = Redis::Throttle.rate_limit(:b, :limit => 3, :period => 4)
135
- # c = Redis::Throttle
136
- # .concurrency(:a, :limit => 1, :ttl => 2)
137
- # .rate_limit(:b, :limit => 3, :period => 4)
138
- #
139
- # a.merge(b) == c # => true
140
- # a == c # => false
141
- #
142
- # @return [Throttle] new throttle
143
- def merge(other)
144
- dup.merge!(other)
145
- end
146
-
147
- alias | merge
148
-
149
- # Prevents further modifications to the throttle instance.
150
- #
151
- # @see https://docs.ruby-lang.org/en/master/Object.html#method-i-freeze
152
- # @return [Throttle] self
153
- def freeze
154
- @strategies.freeze
155
-
156
- super
157
- end
158
-
159
- # Returns `true` if the `other` is an instance of {Throttle} with the same
160
- # set of strategies.
161
- #
162
- # @example
163
- # a = Redis::Throttle
164
- # .concurrency(:a, :limit => 1, :ttl => 2)
165
- # .rate_limit(:b, :limit => 3, :period => 4)
166
- #
167
- # b = Redis::Throttle
168
- # .rate_limit(:b, :limit => 3, :period => 4)
169
- # .concurrency(:a, :limit => 1, :ttl => 2)
170
- #
171
- # a == b # => true
172
- #
173
- # @return [Boolean]
174
- def ==(other)
175
- other.is_a?(self.class) && @strategies == other.strategies
176
- end
177
-
178
- alias eql? ==
179
-
180
- # Calls given block execution lock was acquired, and ensures to {#release}
181
- # it after the block.
182
- #
183
- # @example
184
- # throttle = Redis::Throttle.concurrency(:xxx, :limit => 1, :ttl => 10)
185
- #
186
- # throttle.call { :aye } # => :aye
187
- # throttle.call { :aye } # => :aye
188
- #
189
- # throttle.acquire
190
- #
191
- # throttle.call { :aye } # => nil
192
- #
193
- # @param (see #acquire)
194
- # @return [Object] last satement of the block if execution lock was acquired.
195
- # @return [nil] otherwise
196
- def call(token: SecureRandom.uuid)
197
- return unless acquire(:token => token)
198
-
199
- begin
200
- yield
201
- ensure
202
- release(:token => token)
203
- end
204
- end
205
-
206
- # Acquire execution lock.
207
- #
208
- # @example
209
- # throttle = Redis::Throttle.concurrency(:xxx, :limit => 1, :ttl => 10)
210
- #
211
- # if (token = throttle.acquire)
212
- # # ... do something
213
- # end
214
- #
215
- # throttle.release(:token => token) if token
216
- #
217
- # @see #call
218
- # @see #release
219
- # @param token [#to_s] Unit of work ID
220
- # @return [#to_s] `token` as is if lock was acquired
221
- # @return [nil] otherwise
222
- def acquire(token: SecureRandom.uuid)
223
- token if @api.acquire(:strategies => @strategies, :token => token.to_s)
224
- end
225
-
226
- # Release acquired execution lock. Notice that this affects {#concurrency}
227
- # locks only.
228
- #
229
- # @example
230
- # concurrency = Redis::Throttle.concurrency(:xxx, :limit => 1, :ttl => 60)
231
- # rate_limit = Redis::Throttle.rate_limit(:xxx, :limit => 1, :period => 60)
232
- # throttle = concurrency | rate_limit
233
- #
234
- # throttle.acquire(:token => "uno")
235
- # throttle.release(:token => "uno")
236
- #
237
- # concurrency.acquire(:token => "dos") # => "dos"
238
- # rate_limit.acquire(:token => "dos") # => nil
239
- #
240
- # @see #acquire
241
- # @see #reset
242
- # @see #call
243
- # @param token [#to_s] Unit of work ID
244
- # @return [void]
245
- def release(token:)
246
- @api.release(:strategies => @strategies, :token => token.to_s)
247
-
248
- nil
249
- end
250
-
251
- # Flush all counters.
252
- #
253
- # @example
254
- # throttle = Redis::Throttle.concurrency(:xxx, :limit => 2, :ttl => 60)
255
- #
256
- # thottle.acquire(:token => "a") # => "a"
257
- # thottle.acquire(:token => "b") # => "b"
258
- # thottle.acquire(:token => "c") # => nil
259
- #
260
- # throttle.reset
261
- #
262
- # thottle.acquire(:token => "c") # => "c"
263
- # thottle.acquire(:token => "d") # => "d"
264
- #
265
- # @return [void]
266
- def reset
267
- @api.reset(:strategies => @strategies)
268
-
269
- nil
270
- end
271
-
272
- # Return usage info for all strategies of the throttle.
273
- #
274
- # @example
275
- # throttle.info.each do |strategy, current_value|
276
- # # ...
277
- # end
278
- #
279
- # @return (see Api#info)
280
- def info
281
- @api.info(:strategies => @strategies)
282
- end
283
-
284
- protected
285
-
286
- attr_accessor :strategies
287
- end
288
- end
File without changes