speed_limiter 0.0.1

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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: ee6151563233234c9c453843d4d3d98e8de465905e38e99437caaa6a0e258d6b
4
+ data.tar.gz: 9efaa7b0784d79a9a23c77015013519380d20b94645e39e725d390030ad48569
5
+ SHA512:
6
+ metadata.gz: a9672f7b225e6325589c9bb8fae4cc76ad860024b9fbbcdb6f10edc5d4137ac593b4d6ac5370e6490b1ccc1186b0032bf20d572644ec8177afa783d5603cfe2d
7
+ data.tar.gz: 74020c4965b083e2cea5af94a05299455d2ee64060fc008e8fa9b7b5afacae0a587d432925ece9f7efb860388fa15886742d7c5d722751cdb877d6bf31d30110
@@ -0,0 +1,24 @@
1
+ name: lint
2
+
3
+ on: [push, pull_request]
4
+
5
+ permissions: # added using https://github.com/step-security/secure-workflows
6
+ contents: read
7
+
8
+ jobs:
9
+ lint:
10
+ runs-on: ubuntu-latest
11
+ continue-on-error: true
12
+ timeout-minutes: 10
13
+
14
+ steps:
15
+ - uses: actions/checkout@v3
16
+
17
+ - name: Set up Ruby
18
+ uses: ruby/setup-ruby@v1
19
+ with:
20
+ ruby-version: '3.1'
21
+ bundler-cache: true
22
+
23
+ - name: Run rubocop
24
+ run: bundle exec rubocop
@@ -0,0 +1,44 @@
1
+ name: test
2
+
3
+ on:
4
+ workflow_dispatch:
5
+ schedule:
6
+ - cron: '40 8 * * MON'
7
+ push:
8
+ paths-ignore:
9
+ - '**/*.md'
10
+ - 'LICENSE'
11
+
12
+ permissions: # added using https://github.com/step-security/secure-workflows
13
+ contents: read
14
+
15
+ jobs:
16
+ test:
17
+ runs-on: ubuntu-latest
18
+ timeout-minutes: 10
19
+
20
+ strategy:
21
+ matrix:
22
+ ruby-version: ['3.0', '3.1', '3.2', ruby-head]
23
+ redis-version: ['5.0', '6.0', '6.2', '7.0', '7.2', latest]
24
+
25
+ services:
26
+ redis:
27
+ image: redis:${{ matrix.redis-version }}
28
+ ports:
29
+ - 6379:6379
30
+
31
+ steps:
32
+ - uses: actions/checkout@v3
33
+
34
+ - name: Set up Ruby ${{ matrix.ruby-version }}
35
+ uses: ruby/setup-ruby@v1
36
+ with:
37
+ ruby-version: ${{ matrix.ruby-version }}
38
+ bundler-cache: true
39
+
40
+ - name: Rackup test throttle server
41
+ run: bundle exec pumad -C throttle_server/puma.rb throttle_server/config.ru
42
+
43
+ - name: Run tests
44
+ run: bundle exec rspec -fd
data/.gitignore ADDED
@@ -0,0 +1,16 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
9
+ /vendor
10
+ /.ruby-version
11
+ /polyrange-*.gem
12
+ .rspec_status
13
+ Gemfile.lock
14
+ /throttle_server/tmp/*
15
+
16
+ !.keep
data/.rubocop.yml ADDED
@@ -0,0 +1,20 @@
1
+ require:
2
+ - rubocop-rspec
3
+ - rubocop-rake
4
+
5
+ AllCops:
6
+ TargetRubyVersion: 3.0
7
+ NewCops: enable
8
+
9
+ Style/StringLiterals:
10
+ EnforcedStyle: double_quotes
11
+
12
+ Metrics/AbcSize:
13
+ Max: 30
14
+
15
+ Rspec/MultipleExpectations:
16
+ Enabled: false
17
+
18
+ Rspec/ExampleLength:
19
+ Enabled: false
20
+
data/Gemfile ADDED
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ # Specify your gem's dependencies in SpeedLimiter.gemspec
6
+ gemspec
7
+
8
+ group :development do
9
+ gem "parallel", "~> 1.23"
10
+ gem "puma", "~> 6.4"
11
+ gem "puma-daemon", "~> 0.3", require: false
12
+ gem "rack", "~> 3.0"
13
+ gem "rack-attack", "~> 6.7"
14
+ gem "rackup", "~> 2.1"
15
+ gem "rake"
16
+ gem "rspec"
17
+ gem "rubocop"
18
+ gem "rubocop-rake"
19
+ gem "rubocop-rspec"
20
+ end
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2023 Seibii, Inc
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 all
13
+ 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 THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,108 @@
1
+ [![lint](https://github.com/seibii/speed_limiter/actions/workflows/lint.yml/badge.svg?branch=main)](https://github.com/seibii/speed_limiter/actions/workflows/lint.yml) [![test](https://github.com/seibii/speed_limiter/actions/workflows/test.yml/badge.svg)](https://github.com/seibii/speed_limiter/actions/workflows/test.yml) [![Gem Version](https://badge.fury.io/rb/speed_limiter.svg)](https://badge.fury.io/rb/speed_limiter) [![MIT License](http://img.shields.io/badge/license-MIT-blue.svg?style=flat)](LICENSE)
2
+
3
+ # SpeedLimiter
4
+
5
+ <img src="README_image.jpg" width="400px" />
6
+
7
+ This is a Gem for execution limits in multi-process and multi-threaded environments. By using Redis, you can limit execution across multiple processes and threads.
8
+
9
+ It was mainly created to avoid hitting access limits to the API server.
10
+
11
+ ## Installation
12
+
13
+ Add this line to your application's Gemfile:
14
+
15
+ ```ruby
16
+ gem 'speed_limiter'
17
+ ```
18
+
19
+ And then execute:
20
+
21
+ $ bundle install
22
+
23
+ Or install it yourself as:
24
+
25
+ $ gem install speed_limiter
26
+
27
+ ## Usage
28
+
29
+ ```ruby
30
+ # Limit the number of executions to 10 times per second
31
+ SpeedLimiter.throttle('server_name/method_name', limit: 10, period: 1) do
32
+ # Do something
33
+ end
34
+ ```
35
+
36
+ ### Configuration
37
+
38
+ ```ruby
39
+ # config/initializers/speed_limiter.rb
40
+ SpeedLimiter.configure do |config|
41
+ config.redis_url = ENV['SPEED_LIMITER_REDIS_URL'] || 'redis://localhost:6379/2'
42
+ end
43
+ ```
44
+
45
+ or Use Redis instance
46
+
47
+ ```ruby
48
+ # config/initializers/speed_limiter.rb
49
+ SpeedLimiter.configure do |config|
50
+ config.redis = Redis.new(host: 'localhost', port: 6379, db: 2)
51
+ end
52
+ ```
53
+
54
+ If you do not want to impose a limit in the test environment, please set it as follows.
55
+
56
+ ```ruby
57
+ # spec/support/speed_limiter.rb
58
+ RSpec.configure do |config|
59
+ config.before(:suite) do
60
+ SpeedLimiter.configure do |config|
61
+ config.no_limit = true
62
+ end
63
+ end
64
+ end
65
+ ```
66
+
67
+ ## Compatibility
68
+
69
+ SpeedLimiter officially supports the following Ruby implementations and Redis :
70
+
71
+ - Ruby MRI 3.0, 3.1, 3.2
72
+ - Redis 5.0, 6.0, 6.2, 7.0, 7.2
73
+
74
+
75
+ ## Development
76
+
77
+ After checking out the repo, run `bin/setup` to install dependencies.
78
+ Please run rspec referring to the following.
79
+
80
+ Before committing, run bundle exec rubocop to perform a style check.
81
+ You can also run bin/console for an interactive prompt that will allow you to experiment.
82
+
83
+ ### rspec
84
+
85
+ Start a web server and Redis for testing with the following command.
86
+
87
+ ```
88
+ $ rake test:throttle_server
89
+ $ docker compose up
90
+ ```
91
+
92
+ After that, please run the test with the following command.
93
+
94
+ ```
95
+ $ bundle exec rspec -fd
96
+ ```
97
+
98
+ ## Contribution
99
+
100
+ 1. Fork it ( https://github.com/seibii/speed_limiter/fork )
101
+ 2. Create your feature branch (git checkout -b my-new-feature)
102
+ 3. Commit your changes (git commit -am 'Add some feature')
103
+ 4. Push to the branch (git push origin my-new-feature)
104
+ 5. Create new Pull Request
105
+
106
+ ## License
107
+
108
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/README_image.jpg ADDED
Binary file
data/Rakefile ADDED
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rubygems"
4
+ require "bundler/setup"
5
+
6
+ require "bundler/gem_tasks"
7
+ require "rspec/core/rake_task"
8
+ require "rubocop/rake_task"
9
+
10
+ RSpec::Core::RakeTask.new(:spec)
11
+ RuboCop::RakeTask.new
12
+
13
+ task default: :spec
14
+
15
+ namespace :test do
16
+ desc "Run rackup for throttle server daemon"
17
+ task :throttle_server do
18
+ system "puma -C throttle_server/puma.rb throttle_server/config.ru"
19
+ end
20
+ end
data/bin/console ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "bundler/setup"
5
+ require "speed_limiter"
6
+
7
+ require "irb"
8
+ IRB.start(__FILE__)
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
data/compose.yaml ADDED
@@ -0,0 +1,5 @@
1
+ services:
2
+ redis:
3
+ image: redis:latest
4
+ ports:
5
+ - "6379:6379"
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SpeedLimiter
4
+ # config model
5
+ class Config
6
+ attr_accessor :redis_url, :redis, :no_limit, :prefix
7
+
8
+ def initialize
9
+ @redis_url = "redis://localhost:6379/0"
10
+ @redis = nil
11
+ @no_limit = false
12
+ @prefix = "speed_limiter"
13
+ end
14
+
15
+ alias no_limit? no_limit
16
+ end
17
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SpeedLimiter
4
+ VERSION = "0.0.1"
5
+ end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "speed_limiter/version"
4
+ require "speed_limiter/config"
5
+ require "redis"
6
+
7
+ # Call speed limiter
8
+ module SpeedLimiter
9
+ class << self
10
+ def config
11
+ @config ||= Config.new
12
+ end
13
+
14
+ def configure
15
+ yield(config)
16
+ end
17
+
18
+ def redis
19
+ @redis ||= config.redis || Redis.new(url: config.redis_url)
20
+ end
21
+
22
+ def throttle(key, limit:, period:, &block)
23
+ return block&.call if config.no_limit?
24
+
25
+ key_name = "#{config.prefix}:#{key}"
26
+ loop do
27
+ count = increment(key_name, period)
28
+
29
+ break(block&.call) if count <= limit
30
+
31
+ wait_for_interval(key_name)
32
+ end
33
+ end
34
+
35
+ private
36
+
37
+ def wait_for_interval(key)
38
+ pttl = redis.pttl(key)
39
+ ttl = pttl / 1000.0
40
+
41
+ return if ttl.negative?
42
+
43
+ sleep ttl
44
+ end
45
+
46
+ def increment(key, period) # rubocop:disable Metrics/MethodLength
47
+ if supports_expire_nx?
48
+ count, = redis.pipelined do |pipeline|
49
+ pipeline.incrby(key, 1)
50
+ pipeline.call(:expire, key, period.to_i, "NX")
51
+ end
52
+ else
53
+ count, ttl = redis.pipelined do |pipeline|
54
+ pipeline.incrby(key, 1)
55
+ pipeline.ttl(key)
56
+ end
57
+ redis.expire(key, period.to_i) if ttl.negative?
58
+ end
59
+
60
+ count
61
+ end
62
+
63
+ def supports_expire_nx?
64
+ return @supports_expire_nx if defined?(@supports_expire_nx)
65
+
66
+ redis_versions = redis.info("server")["redis_version"]
67
+ @supports_expire_nx = Gem::Version.new(redis_versions) >= Gem::Version.new("7.0.0")
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ lib = File.expand_path("lib", __dir__)
4
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
+ require "speed_limiter/version"
6
+
7
+ Gem::Specification.new do |spec|
8
+ spec.required_ruby_version = ">= 3.0.0"
9
+
10
+ spec.name = "speed_limiter"
11
+ spec.version = SpeedLimiter::VERSION
12
+ spec.authors = ["yuhei mukoyama"]
13
+ spec.email = ["yuhei.mukoyama@seibii.com"]
14
+
15
+ spec.summary = "Limit the frequency of execution across multiple threads and processes"
16
+ spec.homepage = "https://github.com/seibii/speed_limiter"
17
+ spec.license = "MIT"
18
+
19
+ # Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host'
20
+ # to allow pushing to a single host or delete this section to allow pushing to any host.
21
+ if spec.respond_to?(:metadata)
22
+ spec.metadata["rubygems_mfa_required"] = "true"
23
+ spec.metadata["allowed_push_host"] = "https://rubygems.org"
24
+ else
25
+ raise "RubyGems 2.0 or newer is required to protect against " \
26
+ "public gem pushes."
27
+ end
28
+
29
+ spec.files = `git ls-files -z`.split("\x0").reject do |f|
30
+ f.match(%r{^(test|spec|features)/})
31
+ end
32
+ spec.require_paths = ["lib"]
33
+
34
+ spec.add_dependency "redis"
35
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rack/attack"
4
+ use Rack::Attack
5
+
6
+ # Throttling test server
7
+ module ThrottlingServer
8
+ THROTTLE_MATCHER = %r{/(\d+)/(\d+)s(\?.+=.*)?}
9
+
10
+ def self.call(env)
11
+ case env["REQUEST_URI"]
12
+ when "/"
13
+ [200, { "content-type" => "text/plain" }, ["Hi!\nPlease access to /#\{limit}/#\{period}s\n\n", throttle_info]]
14
+ when THROTTLE_MATCHER
15
+ (limit, period) = Regexp.last_match.captures
16
+
17
+ puts "[OK] #{env['REQUEST_METHOD']} #{env['REQUEST_URI']}\n #{throttle_info.gsub("\n", "\n ")}"
18
+ [200, { "content-type" => "text/plain" }, ["OK\n#{limit} / #{period} seconds\n\n", throttle_info]]
19
+ else
20
+ [404, { "content-type" => "text/plain" }, ["Not Found"]]
21
+ end
22
+ end
23
+
24
+ def self.throttle_info
25
+ list = REDIS.scan_each.map { |key| "#{key} count: #{REDIS.get(key)}, ttl: #{REDIS.pttl(key)}ms" }.sort.join("\n")
26
+ "Throttle Info:\n#{list}"
27
+ end
28
+
29
+ def self.limit(req)
30
+ case req.path
31
+ when THROTTLE_MATCHER
32
+ Regexp.last_match.captures[0].to_i
33
+ else
34
+ 9999
35
+ end
36
+ end
37
+
38
+ def self.period(req)
39
+ case req.path
40
+ when THROTTLE_MATCHER
41
+ Regexp.last_match.captures[1].to_i
42
+ else
43
+ 9999
44
+ end
45
+ end
46
+ end
47
+
48
+ Rack::Attack.throttle(
49
+ "requests by path", limit: ThrottlingServer.method(:limit),
50
+ period: ThrottlingServer.method(:period)
51
+ ) do |request|
52
+ request.url if request.url.match(ThrottlingServer::THROTTLE_MATCHER)
53
+ end
54
+
55
+ Rack::Attack.throttled_responder = lambda do |env|
56
+ puts "[Retry later] #{env['REQUEST_METHOD']} #{env['REQUEST_URI']}\n " \
57
+ "#{ThrottlingServer.throttle_info.gsub("\n", "\n ")}"
58
+ [429, {}, ["Retry later\n\n", ThrottlingServer.throttle_info]]
59
+ end
60
+
61
+ require "redis"
62
+ REDIS = Rack::Attack.cache.store = Redis.new(url: ENV.fetch("THROTTLE_SERVER_REDIS_URL", "redis://localhost:6379/15"))
63
+
64
+ run ThrottlingServer
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ workers 5
4
+
5
+ stdout_redirect "throttle_server/tmp/puma_stdout.log", "throttle_server/tmp/puma_stderr.log", true
6
+ pidfile "throttle_server/tmp/puma.pid"
7
+
8
+ preload_app!
File without changes
metadata ADDED
@@ -0,0 +1,78 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: speed_limiter
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - yuhei mukoyama
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2023-11-30 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: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ description:
28
+ email:
29
+ - yuhei.mukoyama@seibii.com
30
+ executables: []
31
+ extensions: []
32
+ extra_rdoc_files: []
33
+ files:
34
+ - ".github/workflows/lint.yml"
35
+ - ".github/workflows/test.yml"
36
+ - ".gitignore"
37
+ - ".rubocop.yml"
38
+ - Gemfile
39
+ - LICENSE
40
+ - README.md
41
+ - README_image.jpg
42
+ - Rakefile
43
+ - bin/console
44
+ - bin/setup
45
+ - compose.yaml
46
+ - lib/speed_limiter.rb
47
+ - lib/speed_limiter/config.rb
48
+ - lib/speed_limiter/version.rb
49
+ - speed_limitter.gemspec
50
+ - throttle_server/config.ru
51
+ - throttle_server/puma.rb
52
+ - throttle_server/tmp/.keep
53
+ homepage: https://github.com/seibii/speed_limiter
54
+ licenses:
55
+ - MIT
56
+ metadata:
57
+ rubygems_mfa_required: 'true'
58
+ allowed_push_host: https://rubygems.org
59
+ post_install_message:
60
+ rdoc_options: []
61
+ require_paths:
62
+ - lib
63
+ required_ruby_version: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ version: 3.0.0
68
+ required_rubygems_version: !ruby/object:Gem::Requirement
69
+ requirements:
70
+ - - ">="
71
+ - !ruby/object:Gem::Version
72
+ version: '0'
73
+ requirements: []
74
+ rubygems_version: 3.2.22
75
+ signing_key:
76
+ specification_version: 4
77
+ summary: Limit the frequency of execution across multiple threads and processes
78
+ test_files: []