redis-throttle 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: d6578fcf4ed5b682873d8f6c31a629bf8ab04fa3f747a6e85e67d552fa22ad40
4
+ data.tar.gz: c3d2ea3307187912af4d4f93ecee4f8a2fc2e386922511b63e3a9ad93e547c49
5
+ SHA512:
6
+ metadata.gz: 861efde66ca39700db8a6922add9e7539ecd13b9ecea1a79fe14575df1ca75772ac49608a298bbdb92fce7ec7681cb40eeaf3768089e7481395c57eb6ad23418
7
+ data.tar.gz: a1299fb9d99b21c5cca90823812e77feda7fccc7ed4e69778afcd2bb87a0c74aa94f48746047f3fd8b02ccf2076d4344c4047ca499b69090bfffd823cdc6ae87
@@ -0,0 +1,7 @@
1
+ # v0.0.1 (2020-08-17)
2
+
3
+ * Initial release. Effectively this version represents extraction of concurrency
4
+ and threshold strategies from [sidekiq-throttled][].
5
+
6
+
7
+ [sidekiq-throttled]: https://github.com/sensortower/sidekiq-throttled
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2020 Alexey Zapparov
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
@@ -0,0 +1,123 @@
1
+ # Redis::Throttle
2
+
3
+ [Redis](https://redis.io/) based threshold and concurrency throttling.
4
+
5
+
6
+ ## Installation
7
+
8
+ Add this line to your application's Gemfile:
9
+
10
+ ```ruby
11
+ gem "redis-throttle"
12
+ ```
13
+
14
+ And then execute:
15
+
16
+ $ bundle install
17
+
18
+ Or install it yourself as:
19
+
20
+ $ gem install redis-throttle
21
+
22
+
23
+ ## Usage
24
+
25
+ ### Limit concurrency
26
+
27
+ ``` ruby
28
+ # Allow 1 concurrent calls. If call takes more than 10 seconds, consider it
29
+ # gone (as if process died, or by any other reason did not called `#release`):
30
+ concurrency = Redis::Throttle::Concurrency.new(:bucket_name,
31
+ :limit => 1,
32
+ :ttl => 10
33
+ )
34
+
35
+ concurrency.acquire(Redis.current, :token => "abc") # => true
36
+ concurrency.acquire(Redis.current, :token => "xyz") # => false
37
+
38
+ concurrency.release(Redis.current, :token => "abc")
39
+
40
+ concurrency.acquire(Redis.current, :token => "xyz") # => true
41
+ ```
42
+
43
+ ### Limit threshold
44
+
45
+ ``` ruby
46
+ # Allow 1 calls per 10 seconds:
47
+ threshold = Redis::Throttle::Threshold.new(:bucket_name,
48
+ :limit => 1,
49
+ :period => 10
50
+ )
51
+
52
+ threshold.acquire(Redis.current) # => true
53
+ threshold.acquire(Redis.current) # => false
54
+
55
+ sleep 10
56
+
57
+ threshold.acquire(Redis.current) # => true
58
+ ```
59
+
60
+
61
+ ## Supported Ruby Versions
62
+
63
+ This library aims to support and is tested against:
64
+
65
+ * Ruby
66
+ * MRI 2.4.x
67
+ * MRI 2.5.x
68
+ * MRI 2.6.x
69
+ * MRI 2.7.x
70
+ * JRuby 9.2.x
71
+
72
+ * [redis-rb](https://github.com/redis/redis-rb)
73
+ * 4.0.x
74
+ * 4.1.x
75
+ * 4.2.x
76
+
77
+ * [redis-namespace](https://github.com/resque/redis-namespace)
78
+ * 1.6.x
79
+ * 1.7.x
80
+
81
+
82
+ If something doesn't work on one of these versions, it's a bug.
83
+
84
+ This library may inadvertently work (or seem to work) on other Ruby versions,
85
+ however support will only be provided for the versions listed above.
86
+
87
+ If you would like this library to support another Ruby version or
88
+ implementation, you may volunteer to be a maintainer. Being a maintainer
89
+ entails making sure all tests run and pass on that implementation. When
90
+ something breaks on your implementation, you will be responsible for providing
91
+ patches in a timely fashion. If critical issues for a particular implementation
92
+ exist at the time of a major release, support for that Ruby version may be
93
+ dropped.
94
+
95
+
96
+ ## Development
97
+
98
+ After checking out the repo, run `bundle install` to install dependencies.
99
+ Then, run `bundle exec rake spec` to run the tests with ruby-rb client.
100
+
101
+ To install this gem onto your local machine, run `bundle exec rake install`.
102
+ To release a new version, update the version number in `version.rb`, and then
103
+ run `bundle exec rake release`, which will create a git tag for the version,
104
+ push git commits and tags, and push the `.gem` file to [rubygems.org][].
105
+
106
+
107
+ ## Contributing
108
+
109
+ * Fork redis-throttle
110
+ * Make your changes
111
+ * Ensure all tests pass (`bundle exec rake`)
112
+ * Send a merge request
113
+ * If we like them we'll merge them
114
+ * If we've accepted a patch, feel free to ask for commit access!
115
+
116
+
117
+ ## Copyright
118
+
119
+ Copyright (c) 2020 Alexey Zapparov<br>
120
+ See [LICENSE.txt][] for further details.
121
+
122
+
123
+ [LICENSE.txt]: https://gitlab.com/ixti/redis-throttle/blob/master/LICENSE.txt
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "throttle/errors"
4
+ require_relative "throttle/version"
5
+ require_relative "throttle/concurrency"
6
+ require_relative "throttle/threshold"
7
+
8
+ # @see https://github.com/redis/redis-rb
9
+ class Redis
10
+ # Distributed threshold and concurrency throttling.
11
+ module Throttle; end
12
+ end
@@ -0,0 +1,13 @@
1
+ local bucket, token, limit, ttl, now =
2
+ KEYS[1], ARGV[1], tonumber(ARGV[2]), tonumber(ARGV[3]), tonumber(ARGV[4])
3
+
4
+ redis.call("ZREMRANGEBYSCORE", bucket, "-inf", "(" .. now)
5
+
6
+ if limit <= redis.call("ZCARD", bucket) and not redis.call("ZSCORE", bucket, token) then
7
+ return 1
8
+ end
9
+
10
+ redis.call("ZADD", bucket, now + ttl, token)
11
+ redis.call("EXPIRE", bucket, ttl)
12
+
13
+ return 0
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "./script"
4
+
5
+ class Redis
6
+ module Throttle
7
+ class Concurrency
8
+ SCRIPT = Script.new(File.read("#{__dir__}/concurrency.lua"))
9
+ private_constant :SCRIPT
10
+
11
+ # @param bucket [#to_s]
12
+ # @param limit [#to_i]
13
+ # @param ttl [#to_i]
14
+ def initialize(bucket, limit:, ttl:)
15
+ @bucket = bucket.to_s
16
+ @limit = limit.to_i
17
+ @ttl = ttl.to_i
18
+ end
19
+
20
+ # @param redis [Redis, Redis::Namespace]
21
+ # @param token [#to_s]
22
+ # @return [Boolean]
23
+ def acquire(redis, token:)
24
+ SCRIPT
25
+ .call(redis, :keys => [@bucket], :argv => [token.to_s, @limit, @ttl, Time.now.to_i])
26
+ .zero?
27
+ end
28
+
29
+ # @param redis [Redis, Redis::Namespace]
30
+ # @param token [#to_s]
31
+ # @return [void]
32
+ def release(redis, token:)
33
+ redis.zrem(@bucket, token.to_s)
34
+ end
35
+
36
+ # @param redis [Redis, Redis::Namespace]
37
+ # @return [void]
38
+ def reset(redis)
39
+ redis.del(@bucket)
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Redis
4
+ module Throttle
5
+ class Error < StandardError; end
6
+ class LuaError < Error; end
7
+ end
8
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "digest"
4
+ require "redis/errors"
5
+
6
+ require_relative "./errors"
7
+
8
+ class Redis
9
+ module Throttle
10
+ # Simple helper to run script by it's sha1 digest with fallbak to script
11
+ # load if it was not loaded yet.
12
+ class Script
13
+ # Redis error fired when script ID is unkown.
14
+ NOSCRIPT = "NOSCRIPT"
15
+ private_constant :NOSCRIPT
16
+
17
+ LUA_ERROR_MESSAGE = %r{
18
+ ERR\s
19
+ (?<message>Error\s(?:compiling|running)\sscript\s\(.*?\)):\s
20
+ (?:[^:]+:\d+:\s)+
21
+ (?<details>.+)
22
+ }x.freeze
23
+ private_constant :LUA_ERROR_MESSAGE
24
+
25
+ def initialize(source)
26
+ @source = -source.to_s
27
+ @digest = Digest::SHA1.hexdigest(@source)
28
+ end
29
+
30
+ def call(redis, keys: [], argv: [])
31
+ __call__(redis, :keys => keys, :argv => argv)
32
+ rescue Redis::CommandError => e
33
+ md = LUA_ERROR_MESSAGE.match(e.message.to_s)
34
+ raise unless md
35
+
36
+ raise LuaError, [md[:message], md[:details]].compact.join(": ")
37
+ end
38
+
39
+ private
40
+
41
+ def __call__(redis, keys:, argv:)
42
+ redis.evalsha(@digest, keys, argv)
43
+ rescue Redis::CommandError => e
44
+ raise unless e.message.include?(NOSCRIPT)
45
+
46
+ redis.eval(@source, keys, argv)
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,12 @@
1
+ local bucket, limit, period, now =
2
+ KEYS[1], tonumber(ARGV[1]), tonumber(ARGV[2]), tonumber(ARGV[3])
3
+
4
+ if limit <= redis.call("LLEN", bucket) and now - redis.call("LINDEX", bucket, -1) < period then
5
+ return 1
6
+ end
7
+
8
+ redis.call("LPUSH", bucket, now)
9
+ redis.call("LTRIM", bucket, 0, limit - 1)
10
+ redis.call("EXPIRE", bucket, period)
11
+
12
+ return 0
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "./script"
4
+
5
+ class Redis
6
+ module Throttle
7
+ class Threshold
8
+ SCRIPT = Script.new(File.read("#{__dir__}/threshold.lua"))
9
+ private_constant :SCRIPT
10
+
11
+ NOOP = ->(_) { nil }
12
+ private_constant :NOOP
13
+
14
+ # @param bucket [#to_s]
15
+ # @param limit [#to_i]
16
+ # @param period [#to_i]
17
+ def initialize(bucket, limit:, period:)
18
+ @bucket = bucket.to_s
19
+ @limit = limit.to_i
20
+ @period = period.to_i
21
+ end
22
+
23
+ # @param redis [Redis, Redis::Namespace]
24
+ # @return [Boolean]
25
+ def acquire(redis)
26
+ SCRIPT
27
+ .call(redis, :keys => [@bucket], :argv => [@limit, @period, Time.now.to_i])
28
+ .zero?
29
+ end
30
+
31
+ # @param redis [Redis, Redis::Namespace]
32
+ # @return [void]
33
+ def reset(redis)
34
+ redis.del(@bucket)
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Redis
4
+ module Throttle
5
+ # Gem version.
6
+ VERSION = "0.0.1"
7
+ end
8
+ end
metadata ADDED
@@ -0,0 +1,86 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: redis-throttle
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Alexey Zapparov
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2020-08-18 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: redis
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '4.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '4.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: bundler
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '2.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '2.0'
41
+ description:
42
+ email:
43
+ - alexey@zapparov.com
44
+ executables: []
45
+ extensions: []
46
+ extra_rdoc_files: []
47
+ files:
48
+ - CHANGES.md
49
+ - LICENSE.txt
50
+ - README.md
51
+ - lib/redis/throttle.rb
52
+ - lib/redis/throttle/concurrency.lua
53
+ - lib/redis/throttle/concurrency.rb
54
+ - lib/redis/throttle/errors.rb
55
+ - lib/redis/throttle/script.rb
56
+ - lib/redis/throttle/threshold.lua
57
+ - lib/redis/throttle/threshold.rb
58
+ - lib/redis/throttle/version.rb
59
+ homepage: https://gitlab.com/ixti/redis-throttle
60
+ licenses:
61
+ - MIT
62
+ metadata:
63
+ homepage_uri: https://gitlab.com/ixti/redis-throttle
64
+ source_code_uri: https://gitlab.com/ixti/redis-throttle/tree/v0.0.1
65
+ bug_tracker_uri: https://gitlab.com/ixti/redis-throttle/issues
66
+ changelog_uri: https://gitlab.com/ixti/redis-throttle/blob/v0.0.1/CHANGES.md
67
+ post_install_message:
68
+ rdoc_options: []
69
+ require_paths:
70
+ - lib
71
+ required_ruby_version: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '2.5'
76
+ required_rubygems_version: !ruby/object:Gem::Requirement
77
+ requirements:
78
+ - - ">="
79
+ - !ruby/object:Gem::Version
80
+ version: '0'
81
+ requirements: []
82
+ rubygems_version: 3.1.2
83
+ signing_key:
84
+ specification_version: 4
85
+ summary: Redis based threshold and concurrency throttling.
86
+ test_files: []