redis-throttle 1.0.0 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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