speed_limiter 0.0.1 → 0.1.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.
- checksums.yaml +4 -4
 - data/.github/workflows/test.yml +3 -2
 - data/.rubocop.yml +3 -0
 - data/README.md +91 -11
 - data/Rakefile +27 -3
 - data/lib/speed_limiter/config.rb +3 -2
 - data/lib/speed_limiter/redis.rb +41 -0
 - data/lib/speed_limiter/state.rb +26 -0
 - data/lib/speed_limiter/throttle.rb +59 -0
 - data/lib/speed_limiter/throttle_params.rb +26 -0
 - data/lib/speed_limiter/version.rb +1 -1
 - data/lib/speed_limiter.rb +10 -46
 - metadata +7 -3
 - /data/{speed_limitter.gemspec → speed_limiter.gemspec} +0 -0
 
    
        checksums.yaml
    CHANGED
    
    | 
         @@ -1,7 +1,7 @@ 
     | 
|
| 
       1 
1 
     | 
    
         
             
            ---
         
     | 
| 
       2 
2 
     | 
    
         
             
            SHA256:
         
     | 
| 
       3 
     | 
    
         
            -
              metadata.gz:  
     | 
| 
       4 
     | 
    
         
            -
              data.tar.gz:  
     | 
| 
      
 3 
     | 
    
         
            +
              metadata.gz: 49d4e898db4491c876e3cdf95a157aa2b9b832444c10a62598dae6214204e864
         
     | 
| 
      
 4 
     | 
    
         
            +
              data.tar.gz: f6c223a5b30ab88f844de353e9d4a893d40e629c7602ab521bf59d30a6e314e8
         
     | 
| 
       5 
5 
     | 
    
         
             
            SHA512:
         
     | 
| 
       6 
     | 
    
         
            -
              metadata.gz:  
     | 
| 
       7 
     | 
    
         
            -
              data.tar.gz:  
     | 
| 
      
 6 
     | 
    
         
            +
              metadata.gz: eb26517138fca5d75e5a43a45765ccc48b54586e9bc38b7b644663a8954f7102a513d36e6a8e94575561ad575256c52aa2c1931f481c1f65c64a6a83c663be08
         
     | 
| 
      
 7 
     | 
    
         
            +
              data.tar.gz: 54edbdd81701def3bdc0c1ac738c835198245ca187cfe53393e3887c6a59691332d957d7c0d8cd7362feb3ba18e440524d075bbb493a7b61fc21c929de90a86f
         
     | 
    
        data/.github/workflows/test.yml
    CHANGED
    
    | 
         @@ -18,6 +18,7 @@ jobs: 
     | 
|
| 
       18 
18 
     | 
    
         
             
                timeout-minutes: 10
         
     | 
| 
       19 
19 
     | 
    
         | 
| 
       20 
20 
     | 
    
         
             
                strategy:
         
     | 
| 
      
 21 
     | 
    
         
            +
                  max-parallel: 6
         
     | 
| 
       21 
22 
     | 
    
         
             
                  matrix:
         
     | 
| 
       22 
23 
     | 
    
         
             
                    ruby-version: ['3.0', '3.1', '3.2', ruby-head]
         
     | 
| 
       23 
24 
     | 
    
         
             
                    redis-version: ['5.0', '6.0', '6.2', '7.0', '7.2', latest]
         
     | 
| 
         @@ -37,8 +38,8 @@ jobs: 
     | 
|
| 
       37 
38 
     | 
    
         
             
                      ruby-version: ${{ matrix.ruby-version }}
         
     | 
| 
       38 
39 
     | 
    
         
             
                      bundler-cache: true
         
     | 
| 
       39 
40 
     | 
    
         | 
| 
       40 
     | 
    
         
            -
                  - name: Rackup test  
     | 
| 
       41 
     | 
    
         
            -
                    run: bundle exec  
     | 
| 
      
 41 
     | 
    
         
            +
                  - name: Rackup test web server
         
     | 
| 
      
 42 
     | 
    
         
            +
                    run: bundle exec rake throttle_server:start_daemon
         
     | 
| 
       42 
43 
     | 
    
         | 
| 
       43 
44 
     | 
    
         
             
                  - name: Run tests
         
     | 
| 
       44 
45 
     | 
    
         
             
                    run: bundle exec rspec -fd
         
     | 
    
        data/.rubocop.yml
    CHANGED
    
    
    
        data/README.md
    CHANGED
    
    | 
         @@ -1,12 +1,14 @@ 
     | 
|
| 
       1 
     | 
    
         
            -
            [](https://github.com/seibii/speed_limiter/actions/workflows/lint.yml) [](https://github.com/seibii/speed_limiter/actions/workflows/test.yml) [](https://badge.fury.io/rb/speed_limiter) [](https://github.com/seibii/speed_limiter/actions/workflows/lint.yml) [](https://github.com/seibii/speed_limiter/actions/workflows/test.yml) [](https://badge.fury.io/rb/speed_limiter) [](LICENSE)
         
     | 
| 
       2 
2 
     | 
    
         | 
| 
       3 
3 
     | 
    
         
             
            # SpeedLimiter
         
     | 
| 
       4 
4 
     | 
    
         | 
| 
       5 
5 
     | 
    
         
             
            <img src="README_image.jpg" width="400px" />
         
     | 
| 
       6 
6 
     | 
    
         | 
| 
       7 
     | 
    
         
            -
             
     | 
| 
      
 7 
     | 
    
         
            +
            SpeedLimiter is a gem that limits the number of executions per unit of time.
         
     | 
| 
      
 8 
     | 
    
         
            +
            By default, it achieves throttling through sleep.
         
     | 
| 
      
 9 
     | 
    
         
            +
             
     | 
| 
      
 10 
     | 
    
         
            +
            You can also use the `on_throttled` event to raise an exception instead of sleeping, or to re-enqueue the task.
         
     | 
| 
       8 
11 
     | 
    
         | 
| 
       9 
     | 
    
         
            -
            It was mainly created to avoid hitting access limits to the API server.
         
     | 
| 
       10 
12 
     | 
    
         | 
| 
       11 
13 
     | 
    
         
             
            ## Installation
         
     | 
| 
       12 
14 
     | 
    
         | 
| 
         @@ -26,19 +28,65 @@ Or install it yourself as: 
     | 
|
| 
       26 
28 
     | 
    
         | 
| 
       27 
29 
     | 
    
         
             
            ## Usage
         
     | 
| 
       28 
30 
     | 
    
         | 
| 
      
 31 
     | 
    
         
            +
            Limit the number of executions to 10 times per second.
         
     | 
| 
      
 32 
     | 
    
         
            +
             
     | 
| 
       29 
33 
     | 
    
         
             
            ```ruby
         
     | 
| 
       30 
     | 
    
         
            -
             
     | 
| 
       31 
     | 
    
         
            -
            SpeedLimiter 
     | 
| 
       32 
     | 
    
         
            -
               
     | 
| 
      
 34 
     | 
    
         
            +
            SpeedLimiter.throttle('server_name/method_name', limit: 10, period: 1) do |state|
         
     | 
| 
      
 35 
     | 
    
         
            +
              puts state #=> <SpeedLimiter::State key=server_name/method_name count=1 ttl=0>
         
     | 
| 
      
 36 
     | 
    
         
            +
              http.get(path)
         
     | 
| 
      
 37 
     | 
    
         
            +
            end
         
     | 
| 
      
 38 
     | 
    
         
            +
            ```
         
     | 
| 
      
 39 
     | 
    
         
            +
             
     | 
| 
      
 40 
     | 
    
         
            +
            It returns the result of the block execution.
         
     | 
| 
      
 41 
     | 
    
         
            +
             
     | 
| 
      
 42 
     | 
    
         
            +
            ```ruby
         
     | 
| 
      
 43 
     | 
    
         
            +
            result = SpeedLimiter.throttle('server_name/method_name', limit: 10, period: 1) do
         
     | 
| 
      
 44 
     | 
    
         
            +
              http.get(path)
         
     | 
| 
      
 45 
     | 
    
         
            +
            end
         
     | 
| 
      
 46 
     | 
    
         
            +
            puts result.code #=> 200
         
     | 
| 
      
 47 
     | 
    
         
            +
            ```
         
     | 
| 
      
 48 
     | 
    
         
            +
             
     | 
| 
      
 49 
     | 
    
         
            +
            Specify the process when the limit is exceeded.
         
     | 
| 
      
 50 
     | 
    
         
            +
             
     | 
| 
      
 51 
     | 
    
         
            +
            ```ruby
         
     | 
| 
      
 52 
     | 
    
         
            +
            on_throttled = proc { |state| logger.info("limit exceeded #{state.key} #{state.ttl}") }
         
     | 
| 
      
 53 
     | 
    
         
            +
            SpeedLimiter.throttle('server_name/method_name', limit: 10, period: 1, on_throttled: on_throttled) do
         
     | 
| 
      
 54 
     | 
    
         
            +
              http.get(path)
         
     | 
| 
      
 55 
     | 
    
         
            +
            end
         
     | 
| 
      
 56 
     | 
    
         
            +
            ```
         
     | 
| 
      
 57 
     | 
    
         
            +
             
     | 
| 
      
 58 
     | 
    
         
            +
            Reinitialize the queue instead of sleeping when the limit is reached in ActiveJob.
         
     | 
| 
      
 59 
     | 
    
         
            +
             
     | 
| 
      
 60 
     | 
    
         
            +
            ```ruby
         
     | 
| 
      
 61 
     | 
    
         
            +
            class CreateSlackChannelJob < ApplicationJob
         
     | 
| 
      
 62 
     | 
    
         
            +
              def perform(*args)
         
     | 
| 
      
 63 
     | 
    
         
            +
                on_throttled = proc do |state|
         
     | 
| 
      
 64 
     | 
    
         
            +
                  raise Slack::LimitExceeded, state.ttl if state.ttl > 5
         
     | 
| 
      
 65 
     | 
    
         
            +
                end
         
     | 
| 
      
 66 
     | 
    
         
            +
             
     | 
| 
      
 67 
     | 
    
         
            +
                SpeedLimiter.throttle("slack", limit: 20, period: 1.minute, on_throttled: on_throttled) do
         
     | 
| 
      
 68 
     | 
    
         
            +
                  create_slack_channel(*args)
         
     | 
| 
      
 69 
     | 
    
         
            +
                end
         
     | 
| 
      
 70 
     | 
    
         
            +
              rescue Slack::LimitExceeded => e
         
     | 
| 
      
 71 
     | 
    
         
            +
                self.class.set(wait: e.ttl).perform_later(*args)
         
     | 
| 
      
 72 
     | 
    
         
            +
              end
         
     | 
| 
       33 
73 
     | 
    
         
             
            end
         
     | 
| 
       34 
74 
     | 
    
         
             
            ```
         
     | 
| 
       35 
75 
     | 
    
         | 
| 
       36 
76 
     | 
    
         
             
            ### Configuration
         
     | 
| 
       37 
77 
     | 
    
         | 
| 
      
 78 
     | 
    
         
            +
            #### Redis configuration
         
     | 
| 
      
 79 
     | 
    
         
            +
             
     | 
| 
      
 80 
     | 
    
         
            +
            Redis can be specified as follows
         
     | 
| 
      
 81 
     | 
    
         
            +
             
     | 
| 
      
 82 
     | 
    
         
            +
            1. default url `redis://localhost:6379/0`
         
     | 
| 
      
 83 
     | 
    
         
            +
            2. `SPEED_LIMITER_REDIS_URL` environment variable
         
     | 
| 
      
 84 
     | 
    
         
            +
            3. Configure `SpeedLimiter.configure`
         
     | 
| 
      
 85 
     | 
    
         
            +
             
     | 
| 
       38 
86 
     | 
    
         
             
            ```ruby
         
     | 
| 
       39 
87 
     | 
    
         
             
            # config/initializers/speed_limiter.rb
         
     | 
| 
       40 
88 
     | 
    
         
             
            SpeedLimiter.configure do |config|
         
     | 
| 
       41 
     | 
    
         
            -
              config.redis_url = ENV 
     | 
| 
      
 89 
     | 
    
         
            +
              config.redis_url = ENV.fetch('SPEED_LIMITER_REDIS_URL', 'redis://localhost:6379/2')
         
     | 
| 
       42 
90 
     | 
    
         
             
            end
         
     | 
| 
       43 
91 
     | 
    
         
             
            ```
         
     | 
| 
       44 
92 
     | 
    
         | 
| 
         @@ -51,6 +99,20 @@ SpeedLimiter.configure do |config| 
     | 
|
| 
       51 
99 
     | 
    
         
             
            end
         
     | 
| 
       52 
100 
     | 
    
         
             
            ```
         
     | 
| 
       53 
101 
     | 
    
         | 
| 
      
 102 
     | 
    
         
            +
            #### Other configuration defaults
         
     | 
| 
      
 103 
     | 
    
         
            +
             
     | 
| 
      
 104 
     | 
    
         
            +
            ```ruby
         
     | 
| 
      
 105 
     | 
    
         
            +
            SpeedLimiter.configure do |config|
         
     | 
| 
      
 106 
     | 
    
         
            +
               config.redis_url = ENV.fetch("SPEED_LIMITER_REDIS_URL", "redis://localhost:6379/0")
         
     | 
| 
      
 107 
     | 
    
         
            +
               config.redis = nil
         
     | 
| 
      
 108 
     | 
    
         
            +
               config.no_limit = false # If true, it will not be throttled
         
     | 
| 
      
 109 
     | 
    
         
            +
               config.prefix = "speed_limiter" # Redis key prefix
         
     | 
| 
      
 110 
     | 
    
         
            +
               config.on_throttled = nil # Proc to be executed when the limit is exceeded
         
     | 
| 
      
 111 
     | 
    
         
            +
            end
         
     | 
| 
      
 112 
     | 
    
         
            +
            ```
         
     | 
| 
      
 113 
     | 
    
         
            +
             
     | 
| 
      
 114 
     | 
    
         
            +
            #### Example
         
     | 
| 
      
 115 
     | 
    
         
            +
             
     | 
| 
       54 
116 
     | 
    
         
             
            If you do not want to impose a limit in the test environment, please set it as follows.
         
     | 
| 
       55 
117 
     | 
    
         | 
| 
       56 
118 
     | 
    
         
             
            ```ruby
         
     | 
| 
         @@ -64,6 +126,24 @@ RSpec.configure do |config| 
     | 
|
| 
       64 
126 
     | 
    
         
             
            end
         
     | 
| 
       65 
127 
     | 
    
         
             
            ```
         
     | 
| 
       66 
128 
     | 
    
         | 
| 
      
 129 
     | 
    
         
            +
            If you want to detect the limit in the test environment, please set it as follows.
         
     | 
| 
      
 130 
     | 
    
         
            +
             
     | 
| 
      
 131 
     | 
    
         
            +
            ```ruby
         
     | 
| 
      
 132 
     | 
    
         
            +
            Rspec.describe do
         
     | 
| 
      
 133 
     | 
    
         
            +
              around do |example|
         
     | 
| 
      
 134 
     | 
    
         
            +
                SpeedLimiter.config.on_throttled = proc { |state| raise "limit exceeded #{state.key} #{state.ttl}" }
         
     | 
| 
      
 135 
     | 
    
         
            +
             
     | 
| 
      
 136 
     | 
    
         
            +
                example.run
         
     | 
| 
      
 137 
     | 
    
         
            +
             
     | 
| 
      
 138 
     | 
    
         
            +
                SpeedLimiter.config.on_throttled = nil
         
     | 
| 
      
 139 
     | 
    
         
            +
              end
         
     | 
| 
      
 140 
     | 
    
         
            +
             
     | 
| 
      
 141 
     | 
    
         
            +
              it do
         
     | 
| 
      
 142 
     | 
    
         
            +
                expect { over_limit_method }.to raise_error('limit exceeded key_name [\d.]+')
         
     | 
| 
      
 143 
     | 
    
         
            +
              end
         
     | 
| 
      
 144 
     | 
    
         
            +
            end
         
     | 
| 
      
 145 
     | 
    
         
            +
            ```
         
     | 
| 
      
 146 
     | 
    
         
            +
             
     | 
| 
       67 
147 
     | 
    
         
             
            ## Compatibility
         
     | 
| 
       68 
148 
     | 
    
         | 
| 
       69 
149 
     | 
    
         
             
            SpeedLimiter officially supports the following Ruby implementations and Redis :
         
     | 
| 
         @@ -82,11 +162,11 @@ You can also run bin/console for an interactive prompt that will allow you to ex 
     | 
|
| 
       82 
162 
     | 
    
         | 
| 
       83 
163 
     | 
    
         
             
            ### rspec
         
     | 
| 
       84 
164 
     | 
    
         | 
| 
       85 
     | 
    
         
            -
            Start a web server and Redis for testing with the following command.
         
     | 
| 
      
 165 
     | 
    
         
            +
            Start a test web server and Redis for testing with the following command.
         
     | 
| 
       86 
166 
     | 
    
         | 
| 
       87 
     | 
    
         
            -
            ```
         
     | 
| 
       88 
     | 
    
         
            -
            $ rake  
     | 
| 
       89 
     | 
    
         
            -
            $ docker compose up
         
     | 
| 
      
 167 
     | 
    
         
            +
            ```console
         
     | 
| 
      
 168 
     | 
    
         
            +
            $ rake throttle_server:start_daemon # or rake throttle_server:start
         
     | 
| 
      
 169 
     | 
    
         
            +
            $ docker compose up -d
         
     | 
| 
       90 
170 
     | 
    
         
             
            ```
         
     | 
| 
       91 
171 
     | 
    
         | 
| 
       92 
172 
     | 
    
         
             
            After that, please run the test with the following command.
         
     | 
    
        data/Rakefile
    CHANGED
    
    | 
         @@ -12,9 +12,33 @@ RuboCop::RakeTask.new 
     | 
|
| 
       12 
12 
     | 
    
         | 
| 
       13 
13 
     | 
    
         
             
            task default: :spec
         
     | 
| 
       14 
14 
     | 
    
         | 
| 
       15 
     | 
    
         
            -
            namespace : 
     | 
| 
       16 
     | 
    
         
            -
              desc "Run rackup for  
     | 
| 
       17 
     | 
    
         
            -
              task : 
     | 
| 
      
 15 
     | 
    
         
            +
            namespace :throttle_server do
         
     | 
| 
      
 16 
     | 
    
         
            +
              desc "Run rackup for Throttle server"
         
     | 
| 
      
 17 
     | 
    
         
            +
              task :start do
         
     | 
| 
       18 
18 
     | 
    
         
             
                system "puma -C throttle_server/puma.rb throttle_server/config.ru"
         
     | 
| 
       19 
19 
     | 
    
         
             
              end
         
     | 
| 
      
 20 
     | 
    
         
            +
             
     | 
| 
      
 21 
     | 
    
         
            +
              desc "Run rackup for Throttle server daemon"
         
     | 
| 
      
 22 
     | 
    
         
            +
              task :start_daemon do
         
     | 
| 
      
 23 
     | 
    
         
            +
                system "pumad -C throttle_server/puma.rb throttle_server/config.ru"
         
     | 
| 
      
 24 
     | 
    
         
            +
              end
         
     | 
| 
      
 25 
     | 
    
         
            +
             
     | 
| 
      
 26 
     | 
    
         
            +
              desc "Stop the Throttle server daemon"
         
     | 
| 
      
 27 
     | 
    
         
            +
              task :stop do
         
     | 
| 
      
 28 
     | 
    
         
            +
                pid_file = "throttle_server/tmp/puma.pid"
         
     | 
| 
      
 29 
     | 
    
         
            +
             
     | 
| 
      
 30 
     | 
    
         
            +
                if File.exist?(pid_file)
         
     | 
| 
      
 31 
     | 
    
         
            +
                  pid = File.read(pid_file).to_i
         
     | 
| 
      
 32 
     | 
    
         
            +
                  begin
         
     | 
| 
      
 33 
     | 
    
         
            +
                    Process.kill("TERM", pid)
         
     | 
| 
      
 34 
     | 
    
         
            +
                    puts "Throttle server (PID: #{pid}) has been stopped."
         
     | 
| 
      
 35 
     | 
    
         
            +
                  rescue Errno::ESRCH
         
     | 
| 
      
 36 
     | 
    
         
            +
                    puts "Throttle server (PID: #{pid}) not found. It might have already stopped."
         
     | 
| 
      
 37 
     | 
    
         
            +
                  rescue StandardError => e
         
     | 
| 
      
 38 
     | 
    
         
            +
                    puts "Failed to stop Throttle server (PID: #{pid}): #{e.message}"
         
     | 
| 
      
 39 
     | 
    
         
            +
                  end
         
     | 
| 
      
 40 
     | 
    
         
            +
                else
         
     | 
| 
      
 41 
     | 
    
         
            +
                  puts "PID file not found. Is the Throttle server running?"
         
     | 
| 
      
 42 
     | 
    
         
            +
                end
         
     | 
| 
      
 43 
     | 
    
         
            +
              end
         
     | 
| 
       20 
44 
     | 
    
         
             
            end
         
     | 
    
        data/lib/speed_limiter/config.rb
    CHANGED
    
    | 
         @@ -3,13 +3,14 @@ 
     | 
|
| 
       3 
3 
     | 
    
         
             
            module SpeedLimiter
         
     | 
| 
       4 
4 
     | 
    
         
             
              # config model
         
     | 
| 
       5 
5 
     | 
    
         
             
              class Config
         
     | 
| 
       6 
     | 
    
         
            -
                attr_accessor :redis_url, :redis, :no_limit, :prefix
         
     | 
| 
      
 6 
     | 
    
         
            +
                attr_accessor :redis_url, :redis, :no_limit, :prefix, :on_throttled
         
     | 
| 
       7 
7 
     | 
    
         | 
| 
       8 
8 
     | 
    
         
             
                def initialize
         
     | 
| 
       9 
     | 
    
         
            -
                  @redis_url = "redis://localhost:6379/0"
         
     | 
| 
      
 9 
     | 
    
         
            +
                  @redis_url = ENV.fetch("SPEED_LIMITER_REDIS_URL", "redis://localhost:6379/0")
         
     | 
| 
       10 
10 
     | 
    
         
             
                  @redis = nil
         
     | 
| 
       11 
11 
     | 
    
         
             
                  @no_limit = false
         
     | 
| 
       12 
12 
     | 
    
         
             
                  @prefix = "speed_limiter"
         
     | 
| 
      
 13 
     | 
    
         
            +
                  @on_throttled = nil
         
     | 
| 
       13 
14 
     | 
    
         
             
                end
         
     | 
| 
       14 
15 
     | 
    
         | 
| 
       15 
16 
     | 
    
         
             
                alias no_limit? no_limit
         
     | 
| 
         @@ -0,0 +1,41 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # frozen_string_literal: true
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            module SpeedLimiter
         
     | 
| 
      
 4 
     | 
    
         
            +
              # Redis wrapper
         
     | 
| 
      
 5 
     | 
    
         
            +
              class Redis
         
     | 
| 
      
 6 
     | 
    
         
            +
                def initialize(redis)
         
     | 
| 
      
 7 
     | 
    
         
            +
                  @redis = redis
         
     | 
| 
      
 8 
     | 
    
         
            +
                end
         
     | 
| 
      
 9 
     | 
    
         
            +
                attr_reader :redis
         
     | 
| 
      
 10 
     | 
    
         
            +
             
     | 
| 
      
 11 
     | 
    
         
            +
                def ttl(key)
         
     | 
| 
      
 12 
     | 
    
         
            +
                  redis.pttl(key) / 1000.0
         
     | 
| 
      
 13 
     | 
    
         
            +
                end
         
     | 
| 
      
 14 
     | 
    
         
            +
             
     | 
| 
      
 15 
     | 
    
         
            +
                def increment(key, period) # rubocop:disable Metrics/MethodLength
         
     | 
| 
      
 16 
     | 
    
         
            +
                  if supports_expire_nx?
         
     | 
| 
      
 17 
     | 
    
         
            +
                    count, ttl = redis.pipelined do |pipeline|
         
     | 
| 
      
 18 
     | 
    
         
            +
                      pipeline.incrby(key, 1)
         
     | 
| 
      
 19 
     | 
    
         
            +
                      pipeline.call(:expire, key, period.to_i, "NX")
         
     | 
| 
      
 20 
     | 
    
         
            +
                    end
         
     | 
| 
      
 21 
     | 
    
         
            +
                  else
         
     | 
| 
      
 22 
     | 
    
         
            +
                    count, ttl = redis.pipelined do |pipeline|
         
     | 
| 
      
 23 
     | 
    
         
            +
                      pipeline.incrby(key, 1)
         
     | 
| 
      
 24 
     | 
    
         
            +
                      pipeline.ttl(key)
         
     | 
| 
      
 25 
     | 
    
         
            +
                    end
         
     | 
| 
      
 26 
     | 
    
         
            +
                    redis.expire(key, period.to_i) if ttl.negative?
         
     | 
| 
      
 27 
     | 
    
         
            +
                  end
         
     | 
| 
      
 28 
     | 
    
         
            +
             
     | 
| 
      
 29 
     | 
    
         
            +
                  [count, ttl]
         
     | 
| 
      
 30 
     | 
    
         
            +
                end
         
     | 
| 
      
 31 
     | 
    
         
            +
             
     | 
| 
      
 32 
     | 
    
         
            +
                private
         
     | 
| 
      
 33 
     | 
    
         
            +
             
     | 
| 
      
 34 
     | 
    
         
            +
                def supports_expire_nx?
         
     | 
| 
      
 35 
     | 
    
         
            +
                  return @supports_expire_nx if defined?(@supports_expire_nx)
         
     | 
| 
      
 36 
     | 
    
         
            +
             
     | 
| 
      
 37 
     | 
    
         
            +
                  redis_versions = redis.info("server")["redis_version"]
         
     | 
| 
      
 38 
     | 
    
         
            +
                  @supports_expire_nx = Gem::Version.new(redis_versions) >= Gem::Version.new("7.0.0")
         
     | 
| 
      
 39 
     | 
    
         
            +
                end
         
     | 
| 
      
 40 
     | 
    
         
            +
              end
         
     | 
| 
      
 41 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,26 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # frozen_string_literal: true
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            require "speed_limiter/state"
         
     | 
| 
      
 4 
     | 
    
         
            +
            require "forwardable"
         
     | 
| 
      
 5 
     | 
    
         
            +
             
     | 
| 
      
 6 
     | 
    
         
            +
            module SpeedLimiter
         
     | 
| 
      
 7 
     | 
    
         
            +
              # Execution status model
         
     | 
| 
      
 8 
     | 
    
         
            +
              class State
         
     | 
| 
      
 9 
     | 
    
         
            +
                extend Forwardable
         
     | 
| 
      
 10 
     | 
    
         
            +
             
     | 
| 
      
 11 
     | 
    
         
            +
                def initialize(params:, count:, ttl:)
         
     | 
| 
      
 12 
     | 
    
         
            +
                  @params = params
         
     | 
| 
      
 13 
     | 
    
         
            +
                  @count = count
         
     | 
| 
      
 14 
     | 
    
         
            +
                  @ttl = ttl
         
     | 
| 
      
 15 
     | 
    
         
            +
                end
         
     | 
| 
      
 16 
     | 
    
         
            +
             
     | 
| 
      
 17 
     | 
    
         
            +
                attr_reader :params, :count, :ttl
         
     | 
| 
      
 18 
     | 
    
         
            +
             
     | 
| 
      
 19 
     | 
    
         
            +
                def_delegators(:params, :config, :key, :limit, :period, :on_throttled)
         
     | 
| 
      
 20 
     | 
    
         
            +
             
     | 
| 
      
 21 
     | 
    
         
            +
                def inspect
         
     | 
| 
      
 22 
     | 
    
         
            +
                  "<#{self.class.name} key=#{key.inspect} count=#{count} ttl=#{ttl}>"
         
     | 
| 
      
 23 
     | 
    
         
            +
                end
         
     | 
| 
      
 24 
     | 
    
         
            +
                alias to_s inspect
         
     | 
| 
      
 25 
     | 
    
         
            +
              end
         
     | 
| 
      
 26 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,59 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # frozen_string_literal: true
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            require "forwardable"
         
     | 
| 
      
 4 
     | 
    
         
            +
            require "speed_limiter/redis"
         
     | 
| 
      
 5 
     | 
    
         
            +
            require "speed_limiter/throttle_params"
         
     | 
| 
      
 6 
     | 
    
         
            +
             
     | 
| 
      
 7 
     | 
    
         
            +
            module SpeedLimiter
         
     | 
| 
      
 8 
     | 
    
         
            +
              # with actual throttle limits
         
     | 
| 
      
 9 
     | 
    
         
            +
              class Throttle
         
     | 
| 
      
 10 
     | 
    
         
            +
                extend Forwardable
         
     | 
| 
      
 11 
     | 
    
         
            +
             
     | 
| 
      
 12 
     | 
    
         
            +
                # @option params [String] :key key name
         
     | 
| 
      
 13 
     | 
    
         
            +
                # @option params [Integer] :limit limit count per period
         
     | 
| 
      
 14 
     | 
    
         
            +
                # @option params [Integer] :period period time (seconds)
         
     | 
| 
      
 15 
     | 
    
         
            +
                # @option params [Proc] :on_throttled Block called when limit exceeded, with ttl(Float) and key as argument
         
     | 
| 
      
 16 
     | 
    
         
            +
                # @yield [count] Block called to not reach limit
         
     | 
| 
      
 17 
     | 
    
         
            +
                # @yieldparam count [Integer] count of period
         
     | 
| 
      
 18 
     | 
    
         
            +
                # @yieldreturn [any] block return value
         
     | 
| 
      
 19 
     | 
    
         
            +
                def initialize(config:, **params, &block)
         
     | 
| 
      
 20 
     | 
    
         
            +
                  @config = config
         
     | 
| 
      
 21 
     | 
    
         
            +
                  @params = ThrottleParams.new(config: config, **params)
         
     | 
| 
      
 22 
     | 
    
         
            +
                  @block = block
         
     | 
| 
      
 23 
     | 
    
         
            +
                end
         
     | 
| 
      
 24 
     | 
    
         
            +
                attr_reader :config, :params, :block
         
     | 
| 
      
 25 
     | 
    
         
            +
             
     | 
| 
      
 26 
     | 
    
         
            +
                def_delegators(:params, :key, :redis_key, :limit, :period, :on_throttled, :create_state)
         
     | 
| 
      
 27 
     | 
    
         
            +
             
     | 
| 
      
 28 
     | 
    
         
            +
                def throttle
         
     | 
| 
      
 29 
     | 
    
         
            +
                  return block.call(create_state) if config.no_limit?
         
     | 
| 
      
 30 
     | 
    
         
            +
             
     | 
| 
      
 31 
     | 
    
         
            +
                  loop do
         
     | 
| 
      
 32 
     | 
    
         
            +
                    count, ttl = redis.increment(redis_key, period)
         
     | 
| 
      
 33 
     | 
    
         
            +
             
     | 
| 
      
 34 
     | 
    
         
            +
                    break(block.call(create_state(count: count, ttl: ttl))) if count <= limit
         
     | 
| 
      
 35 
     | 
    
         
            +
             
     | 
| 
      
 36 
     | 
    
         
            +
                    wait_for_interval(count)
         
     | 
| 
      
 37 
     | 
    
         
            +
                  end
         
     | 
| 
      
 38 
     | 
    
         
            +
                end
         
     | 
| 
      
 39 
     | 
    
         
            +
             
     | 
| 
      
 40 
     | 
    
         
            +
                private
         
     | 
| 
      
 41 
     | 
    
         
            +
             
     | 
| 
      
 42 
     | 
    
         
            +
                def wait_for_interval(count)
         
     | 
| 
      
 43 
     | 
    
         
            +
                  ttl = redis.ttl(redis_key)
         
     | 
| 
      
 44 
     | 
    
         
            +
                  return if ttl.negative?
         
     | 
| 
      
 45 
     | 
    
         
            +
             
     | 
| 
      
 46 
     | 
    
         
            +
                  config.on_throttled.call(create_state(count: count, ttl: ttl)) if config.on_throttled.respond_to?(:call)
         
     | 
| 
      
 47 
     | 
    
         
            +
                  on_throttled.call(create_state(count: count, ttl: ttl)) if on_throttled.respond_to?(:call)
         
     | 
| 
      
 48 
     | 
    
         
            +
             
     | 
| 
      
 49 
     | 
    
         
            +
                  ttl = redis.ttl(redis_key)
         
     | 
| 
      
 50 
     | 
    
         
            +
                  return if ttl.negative?
         
     | 
| 
      
 51 
     | 
    
         
            +
             
     | 
| 
      
 52 
     | 
    
         
            +
                  sleep ttl
         
     | 
| 
      
 53 
     | 
    
         
            +
                end
         
     | 
| 
      
 54 
     | 
    
         
            +
             
     | 
| 
      
 55 
     | 
    
         
            +
                def redis
         
     | 
| 
      
 56 
     | 
    
         
            +
                  @redis ||= SpeedLimiter::Redis.new(config.redis || ::Redis.new(url: config.redis_url))
         
     | 
| 
      
 57 
     | 
    
         
            +
                end
         
     | 
| 
      
 58 
     | 
    
         
            +
              end
         
     | 
| 
      
 59 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,26 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # frozen_string_literal: true
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            require "speed_limiter/state"
         
     | 
| 
      
 4 
     | 
    
         
            +
             
     | 
| 
      
 5 
     | 
    
         
            +
            module SpeedLimiter
         
     | 
| 
      
 6 
     | 
    
         
            +
              # Throttle params model
         
     | 
| 
      
 7 
     | 
    
         
            +
              class ThrottleParams
         
     | 
| 
      
 8 
     | 
    
         
            +
                def initialize(config:, key:, limit:, period:, on_throttled: nil)
         
     | 
| 
      
 9 
     | 
    
         
            +
                  @config = config
         
     | 
| 
      
 10 
     | 
    
         
            +
                  @key = key
         
     | 
| 
      
 11 
     | 
    
         
            +
                  @limit = limit
         
     | 
| 
      
 12 
     | 
    
         
            +
                  @period = period
         
     | 
| 
      
 13 
     | 
    
         
            +
                  @on_throttled = on_throttled
         
     | 
| 
      
 14 
     | 
    
         
            +
                end
         
     | 
| 
      
 15 
     | 
    
         
            +
             
     | 
| 
      
 16 
     | 
    
         
            +
                attr_reader :config, :key, :limit, :period, :on_throttled
         
     | 
| 
      
 17 
     | 
    
         
            +
             
     | 
| 
      
 18 
     | 
    
         
            +
                def redis_key
         
     | 
| 
      
 19 
     | 
    
         
            +
                  "#{config.prefix}:#{key}"
         
     | 
| 
      
 20 
     | 
    
         
            +
                end
         
     | 
| 
      
 21 
     | 
    
         
            +
             
     | 
| 
      
 22 
     | 
    
         
            +
                def create_state(count: nil, ttl: nil)
         
     | 
| 
      
 23 
     | 
    
         
            +
                  State.new(params: self, count: count, ttl: ttl)
         
     | 
| 
      
 24 
     | 
    
         
            +
                end
         
     | 
| 
      
 25 
     | 
    
         
            +
              end
         
     | 
| 
      
 26 
     | 
    
         
            +
            end
         
     | 
    
        data/lib/speed_limiter.rb
    CHANGED
    
    | 
         @@ -2,6 +2,7 @@ 
     | 
|
| 
       2 
2 
     | 
    
         | 
| 
       3 
3 
     | 
    
         
             
            require "speed_limiter/version"
         
     | 
| 
       4 
4 
     | 
    
         
             
            require "speed_limiter/config"
         
     | 
| 
      
 5 
     | 
    
         
            +
            require "speed_limiter/throttle"
         
     | 
| 
       5 
6 
     | 
    
         
             
            require "redis"
         
     | 
| 
       6 
7 
     | 
    
         | 
| 
       7 
8 
     | 
    
         
             
            # Call speed limiter
         
     | 
| 
         @@ -19,52 +20,15 @@ module SpeedLimiter 
     | 
|
| 
       19 
20 
     | 
    
         
             
                  @redis ||= config.redis || Redis.new(url: config.redis_url)
         
     | 
| 
       20 
21 
     | 
    
         
             
                end
         
     | 
| 
       21 
22 
     | 
    
         | 
| 
       22 
     | 
    
         
            -
                 
     | 
| 
       23 
     | 
    
         
            -
             
     | 
| 
       24 
     | 
    
         
            -
             
     | 
| 
       25 
     | 
    
         
            -
             
     | 
| 
       26 
     | 
    
         
            -
             
     | 
| 
       27 
     | 
    
         
            -
             
     | 
| 
       28 
     | 
    
         
            -
             
     | 
| 
       29 
     | 
    
         
            -
             
     | 
| 
       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")
         
     | 
| 
      
 23 
     | 
    
         
            +
                # @param key [String] key name
         
     | 
| 
      
 24 
     | 
    
         
            +
                # @param limit [Integer] limit count per period
         
     | 
| 
      
 25 
     | 
    
         
            +
                # @param period [Integer] period time (seconds)
         
     | 
| 
      
 26 
     | 
    
         
            +
                # @param on_throttled [Proc] Block called when limit exceeded, with ttl(Float) and key as argument
         
     | 
| 
      
 27 
     | 
    
         
            +
                # @yield [count] Block called to not reach limit
         
     | 
| 
      
 28 
     | 
    
         
            +
                # @yieldparam count [Integer] count of period
         
     | 
| 
      
 29 
     | 
    
         
            +
                # @yieldreturn [any] block return value
         
     | 
| 
      
 30 
     | 
    
         
            +
                def throttle(key, **params, &block)
         
     | 
| 
      
 31 
     | 
    
         
            +
                  Throttle.new(config: config, key: key, **params, &block).throttle
         
     | 
| 
       68 
32 
     | 
    
         
             
                end
         
     | 
| 
       69 
33 
     | 
    
         
             
              end
         
     | 
| 
       70 
34 
     | 
    
         
             
            end
         
     | 
    
        metadata
    CHANGED
    
    | 
         @@ -1,14 +1,14 @@ 
     | 
|
| 
       1 
1 
     | 
    
         
             
            --- !ruby/object:Gem::Specification
         
     | 
| 
       2 
2 
     | 
    
         
             
            name: speed_limiter
         
     | 
| 
       3 
3 
     | 
    
         
             
            version: !ruby/object:Gem::Version
         
     | 
| 
       4 
     | 
    
         
            -
              version: 0.0 
     | 
| 
      
 4 
     | 
    
         
            +
              version: 0.1.0
         
     | 
| 
       5 
5 
     | 
    
         
             
            platform: ruby
         
     | 
| 
       6 
6 
     | 
    
         
             
            authors:
         
     | 
| 
       7 
7 
     | 
    
         
             
            - yuhei mukoyama
         
     | 
| 
       8 
8 
     | 
    
         
             
            autorequire:
         
     | 
| 
       9 
9 
     | 
    
         
             
            bindir: bin
         
     | 
| 
       10 
10 
     | 
    
         
             
            cert_chain: []
         
     | 
| 
       11 
     | 
    
         
            -
            date: 2023- 
     | 
| 
      
 11 
     | 
    
         
            +
            date: 2023-12-16 00:00:00.000000000 Z
         
     | 
| 
       12 
12 
     | 
    
         
             
            dependencies:
         
     | 
| 
       13 
13 
     | 
    
         
             
            - !ruby/object:Gem::Dependency
         
     | 
| 
       14 
14 
     | 
    
         
             
              name: redis
         
     | 
| 
         @@ -45,8 +45,12 @@ files: 
     | 
|
| 
       45 
45 
     | 
    
         
             
            - compose.yaml
         
     | 
| 
       46 
46 
     | 
    
         
             
            - lib/speed_limiter.rb
         
     | 
| 
       47 
47 
     | 
    
         
             
            - lib/speed_limiter/config.rb
         
     | 
| 
      
 48 
     | 
    
         
            +
            - lib/speed_limiter/redis.rb
         
     | 
| 
      
 49 
     | 
    
         
            +
            - lib/speed_limiter/state.rb
         
     | 
| 
      
 50 
     | 
    
         
            +
            - lib/speed_limiter/throttle.rb
         
     | 
| 
      
 51 
     | 
    
         
            +
            - lib/speed_limiter/throttle_params.rb
         
     | 
| 
       48 
52 
     | 
    
         
             
            - lib/speed_limiter/version.rb
         
     | 
| 
       49 
     | 
    
         
            -
            -  
     | 
| 
      
 53 
     | 
    
         
            +
            - speed_limiter.gemspec
         
     | 
| 
       50 
54 
     | 
    
         
             
            - throttle_server/config.ru
         
     | 
| 
       51 
55 
     | 
    
         
             
            - throttle_server/puma.rb
         
     | 
| 
       52 
56 
     | 
    
         
             
            - throttle_server/tmp/.keep
         
     | 
| 
         
            File without changes
         
     |