speed_limiter 0.0.1

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