redis-throttle 0.0.1 → 1.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/LICENSE.txt +1 -1
 - data/README.adoc +178 -0
 - data/lib/redis/throttle.rb +17 -7
 - data/lib/redis-throttle.rb +4 -0
 - data/lib/redis_throttle/api.lua +195 -0
 - data/lib/redis_throttle/api.rb +118 -0
 - data/lib/redis_throttle/class_methods.rb +43 -0
 - data/lib/redis_throttle/concurrency.rb +75 -0
 - data/lib/redis_throttle/rate_limit.rb +73 -0
 - data/lib/redis_throttle/version.rb +6 -0
 - data/lib/redis_throttle.rb +286 -0
 - metadata +33 -32
 - data/CHANGES.md +0 -7
 - data/README.md +0 -123
 - data/lib/redis/throttle/concurrency.lua +0 -13
 - data/lib/redis/throttle/concurrency.rb +0 -43
 - data/lib/redis/throttle/errors.rb +0 -8
 - data/lib/redis/throttle/script.rb +0 -50
 - data/lib/redis/throttle/threshold.lua +0 -12
 - data/lib/redis/throttle/threshold.rb +0 -38
 - data/lib/redis/throttle/version.rb +0 -8
 
    
        checksums.yaml
    CHANGED
    
    | 
         @@ -1,7 +1,7 @@ 
     | 
|
| 
       1 
1 
     | 
    
         
             
            ---
         
     | 
| 
       2 
2 
     | 
    
         
             
            SHA256:
         
     | 
| 
       3 
     | 
    
         
            -
              metadata.gz:  
     | 
| 
       4 
     | 
    
         
            -
              data.tar.gz:  
     | 
| 
      
 3 
     | 
    
         
            +
              metadata.gz: df74619037a1515419ebbf8381326577c540376855146fece1b9624f9ee443f6
         
     | 
| 
      
 4 
     | 
    
         
            +
              data.tar.gz: 4b465e4412d3fa8408b48470b867576457737524cded5b41ea8bb04aef53a39b
         
     | 
| 
       5 
5 
     | 
    
         
             
            SHA512:
         
     | 
| 
       6 
     | 
    
         
            -
              metadata.gz:  
     | 
| 
       7 
     | 
    
         
            -
              data.tar.gz:  
     | 
| 
      
 6 
     | 
    
         
            +
              metadata.gz: cfcdaef05593dd858baef2d65ae0a1240cea10cbb3adda0989f5bba724672233c4ec8551040b746421a8d40eb5240a0338de5f027bafa3eabe6a3c6a28071c0d
         
     | 
| 
      
 7 
     | 
    
         
            +
              data.tar.gz: e2a761f5962106a211ed285f527a73e24d040c414053a707c26fa11dad1b2477f73ecd38248b4ba639c32c47a773719af788dd02918af8259af1c283f100e979
         
     | 
    
        data/LICENSE.txt
    CHANGED
    
    
    
        data/README.adoc
    ADDED
    
    | 
         @@ -0,0 +1,178 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            = RedisThrottle
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            Redis based rate limit and concurrency throttling.
         
     | 
| 
      
 4 
     | 
    
         
            +
             
     | 
| 
      
 5 
     | 
    
         
            +
             
     | 
| 
      
 6 
     | 
    
         
            +
            == Installation
         
     | 
| 
      
 7 
     | 
    
         
            +
             
     | 
| 
      
 8 
     | 
    
         
            +
            Add this line to your application's Gemfile:
         
     | 
| 
      
 9 
     | 
    
         
            +
             
     | 
| 
      
 10 
     | 
    
         
            +
                $ bundle add redis-throttle
         
     | 
| 
      
 11 
     | 
    
         
            +
             
     | 
| 
      
 12 
     | 
    
         
            +
            Or install it yourself as:
         
     | 
| 
      
 13 
     | 
    
         
            +
             
     | 
| 
      
 14 
     | 
    
         
            +
                $ gem install redis-throttle
         
     | 
| 
      
 15 
     | 
    
         
            +
             
     | 
| 
      
 16 
     | 
    
         
            +
             
     | 
| 
      
 17 
     | 
    
         
            +
            == Usage
         
     | 
| 
      
 18 
     | 
    
         
            +
             
     | 
| 
      
 19 
     | 
    
         
            +
            === Concurrency Limit
         
     | 
| 
      
 20 
     | 
    
         
            +
             
     | 
| 
      
 21 
     | 
    
         
            +
            [source,ruby]
         
     | 
| 
      
 22 
     | 
    
         
            +
            ----
         
     | 
| 
      
 23 
     | 
    
         
            +
            # Allow 1 concurrent calls. If call takes more than 10 seconds, consider it
         
     | 
| 
      
 24 
     | 
    
         
            +
            # gone (as if process died, or by any other reason did not called `#release`):
         
     | 
| 
      
 25 
     | 
    
         
            +
            concurrency = RedisThrottle.concurrency(:bucket_name,
         
     | 
| 
      
 26 
     | 
    
         
            +
              :limit => 1,
         
     | 
| 
      
 27 
     | 
    
         
            +
              :ttl   => 10
         
     | 
| 
      
 28 
     | 
    
         
            +
            )
         
     | 
| 
      
 29 
     | 
    
         
            +
             
     | 
| 
      
 30 
     | 
    
         
            +
            concurrency.acquire(:token => "abc") # => "abc"
         
     | 
| 
      
 31 
     | 
    
         
            +
            concurrency.acquire(:token => "xyz") # => nil
         
     | 
| 
      
 32 
     | 
    
         
            +
             
     | 
| 
      
 33 
     | 
    
         
            +
            concurrency.release(:token => "abc")
         
     | 
| 
      
 34 
     | 
    
         
            +
             
     | 
| 
      
 35 
     | 
    
         
            +
            concurrency.acquire(:token => "xyz") # => "xyz"
         
     | 
| 
      
 36 
     | 
    
         
            +
            ----
         
     | 
| 
      
 37 
     | 
    
         
            +
             
     | 
| 
      
 38 
     | 
    
         
            +
            === Rate Limit
         
     | 
| 
      
 39 
     | 
    
         
            +
             
     | 
| 
      
 40 
     | 
    
         
            +
            [source,ruby]
         
     | 
| 
      
 41 
     | 
    
         
            +
            ----
         
     | 
| 
      
 42 
     | 
    
         
            +
            # Allow 1 calls per 10 seconds:
         
     | 
| 
      
 43 
     | 
    
         
            +
            rate_limit = RedisThrottle.rate_limit(:bucket_name,
         
     | 
| 
      
 44 
     | 
    
         
            +
              :limit  => 1,
         
     | 
| 
      
 45 
     | 
    
         
            +
              :period => 10
         
     | 
| 
      
 46 
     | 
    
         
            +
            )
         
     | 
| 
      
 47 
     | 
    
         
            +
             
     | 
| 
      
 48 
     | 
    
         
            +
            rate_limit.acquire # => "6a6c6546-268d-4216-bcf3-3139b8e11609"
         
     | 
| 
      
 49 
     | 
    
         
            +
            rate_limit.acquire # => nil
         
     | 
| 
      
 50 
     | 
    
         
            +
             
     | 
| 
      
 51 
     | 
    
         
            +
            sleep 10
         
     | 
| 
      
 52 
     | 
    
         
            +
             
     | 
| 
      
 53 
     | 
    
         
            +
            rate_limit.acquire # => "e2926a90-2cf4-4bff-9401-65f3a70d32bd"
         
     | 
| 
      
 54 
     | 
    
         
            +
            ----
         
     | 
| 
      
 55 
     | 
    
         
            +
             
     | 
| 
      
 56 
     | 
    
         
            +
             
     | 
| 
      
 57 
     | 
    
         
            +
            === Multi-strategy
         
     | 
| 
      
 58 
     | 
    
         
            +
             
     | 
| 
      
 59 
     | 
    
         
            +
            [source,ruby]
         
     | 
| 
      
 60 
     | 
    
         
            +
            ----
         
     | 
| 
      
 61 
     | 
    
         
            +
            throttle = RedisThrottle
         
     | 
| 
      
 62 
     | 
    
         
            +
              .concurrency(:db, :limit => 3, :ttl => 900)
         
     | 
| 
      
 63 
     | 
    
         
            +
              .rate_limit(:api_minutely, :limit => 1, :period => 60)
         
     | 
| 
      
 64 
     | 
    
         
            +
              .rate_limit(:api_hourly, :limit => 10, :period => 3600)
         
     | 
| 
      
 65 
     | 
    
         
            +
             
     | 
| 
      
 66 
     | 
    
         
            +
            throttle.call(:token => "abc") do
         
     | 
| 
      
 67 
     | 
    
         
            +
              # do something if all strategies are resolved
         
     | 
| 
      
 68 
     | 
    
         
            +
            end
         
     | 
| 
      
 69 
     | 
    
         
            +
            ----
         
     | 
| 
      
 70 
     | 
    
         
            +
             
     | 
| 
      
 71 
     | 
    
         
            +
            You can also compose multiple throttlers together:
         
     | 
| 
      
 72 
     | 
    
         
            +
             
     | 
| 
      
 73 
     | 
    
         
            +
            [source,ruby]
         
     | 
| 
      
 74 
     | 
    
         
            +
            ----
         
     | 
| 
      
 75 
     | 
    
         
            +
            db_limiter  = RedisThrottle.concurrency(:db, :limit => 3, :ttl => 900)
         
     | 
| 
      
 76 
     | 
    
         
            +
            api_limiter = RedisThrottle
         
     | 
| 
      
 77 
     | 
    
         
            +
              .rate_limit(:api_minutely, :limit => 1, :period => 60)
         
     | 
| 
      
 78 
     | 
    
         
            +
              .rate_limit(:api_hourly, :limit => 10, :period => 3600)
         
     | 
| 
      
 79 
     | 
    
         
            +
             
     | 
| 
      
 80 
     | 
    
         
            +
            (db_limiter + api_limiter).call do
         
     | 
| 
      
 81 
     | 
    
         
            +
              # ...
         
     | 
| 
      
 82 
     | 
    
         
            +
            end
         
     | 
| 
      
 83 
     | 
    
         
            +
            ----
         
     | 
| 
      
 84 
     | 
    
         
            +
             
     | 
| 
      
 85 
     | 
    
         
            +
             
     | 
| 
      
 86 
     | 
    
         
            +
            === With ConnectionPool
         
     | 
| 
      
 87 
     | 
    
         
            +
             
     | 
| 
      
 88 
     | 
    
         
            +
            If you're using [connection_pool](https://github.com/mperham/connection_pool),
         
     | 
| 
      
 89 
     | 
    
         
            +
            you can pass its `#with` method as connection builder:
         
     | 
| 
      
 90 
     | 
    
         
            +
             
     | 
| 
      
 91 
     | 
    
         
            +
            [source,ruby]
         
     | 
| 
      
 92 
     | 
    
         
            +
            ----
         
     | 
| 
      
 93 
     | 
    
         
            +
            pool     = ConnectionPool.new { Redis.new }
         
     | 
| 
      
 94 
     | 
    
         
            +
            throttle = RedisThrottle.new(:redis => pool.method(:with))
         
     | 
| 
      
 95 
     | 
    
         
            +
            ----
         
     | 
| 
      
 96 
     | 
    
         
            +
             
     | 
| 
      
 97 
     | 
    
         
            +
            === With Sidekiq
         
     | 
| 
      
 98 
     | 
    
         
            +
             
     | 
| 
      
 99 
     | 
    
         
            +
            [Sidekiq](https://github.com/mperham/sidekiq): uses ConnectionPool, so you can
         
     | 
| 
      
 100 
     | 
    
         
            +
            use the same approach:
         
     | 
| 
      
 101 
     | 
    
         
            +
             
     | 
| 
      
 102 
     | 
    
         
            +
            [source,ruby]
         
     | 
| 
      
 103 
     | 
    
         
            +
            ----
         
     | 
| 
      
 104 
     | 
    
         
            +
            throttle = RedisThrottle.new(:redis => Sidekiq.redis_pool.method(:with))
         
     | 
| 
      
 105 
     | 
    
         
            +
            ----
         
     | 
| 
      
 106 
     | 
    
         
            +
             
     | 
| 
      
 107 
     | 
    
         
            +
            Or, you can use its `.redis` method directly:
         
     | 
| 
      
 108 
     | 
    
         
            +
             
     | 
| 
      
 109 
     | 
    
         
            +
            [source,ruby]
         
     | 
| 
      
 110 
     | 
    
         
            +
            ----
         
     | 
| 
      
 111 
     | 
    
         
            +
            throttle = RedisThrottle.new(:redis => Sidekiq.method(:redis))
         
     | 
| 
      
 112 
     | 
    
         
            +
            ----
         
     | 
| 
      
 113 
     | 
    
         
            +
             
     | 
| 
      
 114 
     | 
    
         
            +
             
     | 
| 
      
 115 
     | 
    
         
            +
            == Compatibility
         
     | 
| 
      
 116 
     | 
    
         
            +
             
     | 
| 
      
 117 
     | 
    
         
            +
            This library aims to support and is tested against:
         
     | 
| 
      
 118 
     | 
    
         
            +
             
     | 
| 
      
 119 
     | 
    
         
            +
            * https://www.ruby-lang.org[Ruby]
         
     | 
| 
      
 120 
     | 
    
         
            +
            ** MRI 2.7.x
         
     | 
| 
      
 121 
     | 
    
         
            +
            ** MRI 3.0.x
         
     | 
| 
      
 122 
     | 
    
         
            +
            ** MRI 3.1.x
         
     | 
| 
      
 123 
     | 
    
         
            +
            ** MRI 3.2.x
         
     | 
| 
      
 124 
     | 
    
         
            +
            * https://redis.io[Redis Server]
         
     | 
| 
      
 125 
     | 
    
         
            +
            ** 6.0.x
         
     | 
| 
      
 126 
     | 
    
         
            +
            ** 6.2.x
         
     | 
| 
      
 127 
     | 
    
         
            +
            ** 7.0.x
         
     | 
| 
      
 128 
     | 
    
         
            +
            * https://github.com/redis/redis-rb[redis-rb]
         
     | 
| 
      
 129 
     | 
    
         
            +
            ** 4.1.x
         
     | 
| 
      
 130 
     | 
    
         
            +
            ** 4.2.x
         
     | 
| 
      
 131 
     | 
    
         
            +
            ** 4.3.x
         
     | 
| 
      
 132 
     | 
    
         
            +
            ** 4.4.x
         
     | 
| 
      
 133 
     | 
    
         
            +
            ** 4.5.x
         
     | 
| 
      
 134 
     | 
    
         
            +
            ** 4.6.x
         
     | 
| 
      
 135 
     | 
    
         
            +
            * https://github.com/resque/redis-namespace[redis-namespace]
         
     | 
| 
      
 136 
     | 
    
         
            +
            ** 1.10.x
         
     | 
| 
      
 137 
     | 
    
         
            +
             
     | 
| 
      
 138 
     | 
    
         
            +
            If something doesn't work on one of these versions, it's a bug.
         
     | 
| 
      
 139 
     | 
    
         
            +
             
     | 
| 
      
 140 
     | 
    
         
            +
            This library may inadvertently work (or seem to work) on other Ruby versions,
         
     | 
| 
      
 141 
     | 
    
         
            +
            however support will only be provided for the versions listed above.
         
     | 
| 
      
 142 
     | 
    
         
            +
             
     | 
| 
      
 143 
     | 
    
         
            +
            If you would like this library to support another Ruby version or
         
     | 
| 
      
 144 
     | 
    
         
            +
            implementation, you may volunteer to be a maintainer. Being a maintainer
         
     | 
| 
      
 145 
     | 
    
         
            +
            entails making sure all tests run and pass on that implementation. When
         
     | 
| 
      
 146 
     | 
    
         
            +
            something breaks on your implementation, you will be responsible for providing
         
     | 
| 
      
 147 
     | 
    
         
            +
            patches in a timely fashion. If critical issues for a particular implementation
         
     | 
| 
      
 148 
     | 
    
         
            +
            exist at the time of a major release, support for that Ruby version may be
         
     | 
| 
      
 149 
     | 
    
         
            +
            dropped.
         
     | 
| 
      
 150 
     | 
    
         
            +
             
     | 
| 
      
 151 
     | 
    
         
            +
            The same applies to *Redis Server*, *redis-rb*, and *redis-namespace* support.
         
     | 
| 
      
 152 
     | 
    
         
            +
             
     | 
| 
      
 153 
     | 
    
         
            +
             
     | 
| 
      
 154 
     | 
    
         
            +
            == Development
         
     | 
| 
      
 155 
     | 
    
         
            +
             
     | 
| 
      
 156 
     | 
    
         
            +
              scripts/update-gemfiles
         
     | 
| 
      
 157 
     | 
    
         
            +
              scripts/run-rspec
         
     | 
| 
      
 158 
     | 
    
         
            +
              bundle exec rubocop
         
     | 
| 
      
 159 
     | 
    
         
            +
             
     | 
| 
      
 160 
     | 
    
         
            +
             
     | 
| 
      
 161 
     | 
    
         
            +
            == Contributing
         
     | 
| 
      
 162 
     | 
    
         
            +
             
     | 
| 
      
 163 
     | 
    
         
            +
            * Fork redis-throttle
         
     | 
| 
      
 164 
     | 
    
         
            +
            * Make your changes
         
     | 
| 
      
 165 
     | 
    
         
            +
            * Ensure all tests pass (`bundle exec rake`)
         
     | 
| 
      
 166 
     | 
    
         
            +
            * Send a merge request
         
     | 
| 
      
 167 
     | 
    
         
            +
            * If we like them we'll merge them
         
     | 
| 
      
 168 
     | 
    
         
            +
            * If we've accepted a patch, feel free to ask for commit access!
         
     | 
| 
      
 169 
     | 
    
         
            +
             
     | 
| 
      
 170 
     | 
    
         
            +
             
     | 
| 
      
 171 
     | 
    
         
            +
            == Appreciations
         
     | 
| 
      
 172 
     | 
    
         
            +
             
     | 
| 
      
 173 
     | 
    
         
            +
            Thanks to all how providede suggestions and criticism, especially to those who
         
     | 
| 
      
 174 
     | 
    
         
            +
            helped me shape some of the initial ideas:
         
     | 
| 
      
 175 
     | 
    
         
            +
             
     | 
| 
      
 176 
     | 
    
         
            +
            * https://gitlab.com/freemanoid[@freemanoid]
         
     | 
| 
      
 177 
     | 
    
         
            +
            * https://gitlab.com/petethepig[@petethepig]
         
     | 
| 
      
 178 
     | 
    
         
            +
            * https://gitlab.com/dervus[@dervus]
         
     | 
    
        data/lib/redis/throttle.rb
    CHANGED
    
    | 
         @@ -1,12 +1,22 @@ 
     | 
|
| 
       1 
1 
     | 
    
         
             
            # frozen_string_literal: true
         
     | 
| 
       2 
2 
     | 
    
         | 
| 
       3 
     | 
    
         
            -
            require_relative " 
     | 
| 
       4 
     | 
    
         
            -
            require_relative "throttle/version"
         
     | 
| 
       5 
     | 
    
         
            -
            require_relative "throttle/concurrency"
         
     | 
| 
       6 
     | 
    
         
            -
            require_relative "throttle/threshold"
         
     | 
| 
      
 3 
     | 
    
         
            +
            require_relative "../redis_throttle"
         
     | 
| 
       7 
4 
     | 
    
         | 
| 
       8 
     | 
    
         
            -
            # @see https://github.com/redis/redis-rb
         
     | 
| 
       9 
5 
     | 
    
         
             
            class Redis
         
     | 
| 
       10 
     | 
    
         
            -
              #  
     | 
| 
       11 
     | 
    
         
            -
               
     | 
| 
      
 6 
     | 
    
         
            +
              # @deprecated Use ::RedisThrottle
         
     | 
| 
      
 7 
     | 
    
         
            +
              class Throttle < RedisThrottle
         
     | 
| 
      
 8 
     | 
    
         
            +
                class << self
         
     | 
| 
      
 9 
     | 
    
         
            +
                  attr_accessor :silence_deprecation_warning
         
     | 
| 
      
 10 
     | 
    
         
            +
                end
         
     | 
| 
      
 11 
     | 
    
         
            +
             
     | 
| 
      
 12 
     | 
    
         
            +
                self.silence_deprecation_warning = false
         
     | 
| 
      
 13 
     | 
    
         
            +
             
     | 
| 
      
 14 
     | 
    
         
            +
                def initialize(*args, **kwargs, &block)
         
     | 
| 
      
 15 
     | 
    
         
            +
                  super(*args, **kwargs, &block)
         
     | 
| 
      
 16 
     | 
    
         
            +
             
     | 
| 
      
 17 
     | 
    
         
            +
                  return if self.class.silence_deprecation_warning
         
     | 
| 
      
 18 
     | 
    
         
            +
             
     | 
| 
      
 19 
     | 
    
         
            +
                  warn "#{self.class} usage was deprecated, please use RedisThrottle instead"
         
     | 
| 
      
 20 
     | 
    
         
            +
                end
         
     | 
| 
      
 21 
     | 
    
         
            +
              end
         
     | 
| 
       12 
22 
     | 
    
         
             
            end
         
     | 
| 
         @@ -0,0 +1,195 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            if 1 ~= #KEYS or 0 == #ARGV then
         
     | 
| 
      
 2 
     | 
    
         
            +
              return redis.error_reply("syntax error")
         
     | 
| 
      
 3 
     | 
    
         
            +
            end
         
     | 
| 
      
 4 
     | 
    
         
            +
             
     | 
| 
      
 5 
     | 
    
         
            +
            local commands = {}
         
     | 
| 
      
 6 
     | 
    
         
            +
             
     | 
| 
      
 7 
     | 
    
         
            +
            commands.ACQUIRE = {
         
     | 
| 
      
 8 
     | 
    
         
            +
              params = { "strategies", "token", "timestamp" },
         
     | 
| 
      
 9 
     | 
    
         
            +
              handler = function (params)
         
     | 
| 
      
 10 
     | 
    
         
            +
                local now   = params.timestamp
         
     | 
| 
      
 11 
     | 
    
         
            +
                local token = params.token
         
     | 
| 
      
 12 
     | 
    
         
            +
                local locks = {}
         
     | 
| 
      
 13 
     | 
    
         
            +
             
     | 
| 
      
 14 
     | 
    
         
            +
                local acquire = {
         
     | 
| 
      
 15 
     | 
    
         
            +
                  rate_limit = function (strategy)
         
     | 
| 
      
 16 
     | 
    
         
            +
                    local key, limit, period = strategy.key, strategy.limit, strategy.period
         
     | 
| 
      
 17 
     | 
    
         
            +
             
     | 
| 
      
 18 
     | 
    
         
            +
                    if redis.call("LLEN", key) < limit or tonumber(redis.call("LINDEX", key, -1)) < now then
         
     | 
| 
      
 19 
     | 
    
         
            +
                      return function ()
         
     | 
| 
      
 20 
     | 
    
         
            +
                        redis.call("LPUSH", key, now + period)
         
     | 
| 
      
 21 
     | 
    
         
            +
                        redis.call("LTRIM", key, 0, limit - 1)
         
     | 
| 
      
 22 
     | 
    
         
            +
                        redis.call("EXPIRE", key, period)
         
     | 
| 
      
 23 
     | 
    
         
            +
                      end
         
     | 
| 
      
 24 
     | 
    
         
            +
                    end
         
     | 
| 
      
 25 
     | 
    
         
            +
                  end,
         
     | 
| 
      
 26 
     | 
    
         
            +
             
     | 
| 
      
 27 
     | 
    
         
            +
                  concurrency = function (strategy)
         
     | 
| 
      
 28 
     | 
    
         
            +
                    local key, limit, ttl = strategy.key, strategy.limit, strategy.ttl
         
     | 
| 
      
 29 
     | 
    
         
            +
             
     | 
| 
      
 30 
     | 
    
         
            +
                    redis.call("ZREMRANGEBYSCORE", key, "-inf", "(" .. now)
         
     | 
| 
      
 31 
     | 
    
         
            +
             
     | 
| 
      
 32 
     | 
    
         
            +
                    if redis.call("ZCARD", key) < limit or redis.call("ZSCORE", key, token) then
         
     | 
| 
      
 33 
     | 
    
         
            +
                      return function ()
         
     | 
| 
      
 34 
     | 
    
         
            +
                        redis.call("ZADD", key, now + ttl, token)
         
     | 
| 
      
 35 
     | 
    
         
            +
                        redis.call("EXPIRE", key, ttl)
         
     | 
| 
      
 36 
     | 
    
         
            +
                      end
         
     | 
| 
      
 37 
     | 
    
         
            +
                    end
         
     | 
| 
      
 38 
     | 
    
         
            +
                  end
         
     | 
| 
      
 39 
     | 
    
         
            +
                }
         
     | 
| 
      
 40 
     | 
    
         
            +
             
     | 
| 
      
 41 
     | 
    
         
            +
                for _, strategy in ipairs(params.strategies) do
         
     | 
| 
      
 42 
     | 
    
         
            +
                  local lock = acquire[strategy.name](strategy)
         
     | 
| 
      
 43 
     | 
    
         
            +
             
     | 
| 
      
 44 
     | 
    
         
            +
                  if lock then
         
     | 
| 
      
 45 
     | 
    
         
            +
                    table.insert(locks, lock)
         
     | 
| 
      
 46 
     | 
    
         
            +
                  else
         
     | 
| 
      
 47 
     | 
    
         
            +
                    return 1
         
     | 
| 
      
 48 
     | 
    
         
            +
                  end
         
     | 
| 
      
 49 
     | 
    
         
            +
                end
         
     | 
| 
      
 50 
     | 
    
         
            +
             
     | 
| 
      
 51 
     | 
    
         
            +
                for _, lock in ipairs(locks) do
         
     | 
| 
      
 52 
     | 
    
         
            +
                  lock()
         
     | 
| 
      
 53 
     | 
    
         
            +
                end
         
     | 
| 
      
 54 
     | 
    
         
            +
             
     | 
| 
      
 55 
     | 
    
         
            +
                return 0
         
     | 
| 
      
 56 
     | 
    
         
            +
              end
         
     | 
| 
      
 57 
     | 
    
         
            +
            }
         
     | 
| 
      
 58 
     | 
    
         
            +
             
     | 
| 
      
 59 
     | 
    
         
            +
            commands.RELEASE = {
         
     | 
| 
      
 60 
     | 
    
         
            +
              params = { "strategies", "token" },
         
     | 
| 
      
 61 
     | 
    
         
            +
              handler = function (params)
         
     | 
| 
      
 62 
     | 
    
         
            +
                for _, strategy in ipairs(params.strategies) do
         
     | 
| 
      
 63 
     | 
    
         
            +
                  if "concurrency" == strategy.name then
         
     | 
| 
      
 64 
     | 
    
         
            +
                    redis.call("ZREM", strategy.key, params.token)
         
     | 
| 
      
 65 
     | 
    
         
            +
                  end
         
     | 
| 
      
 66 
     | 
    
         
            +
                end
         
     | 
| 
      
 67 
     | 
    
         
            +
             
     | 
| 
      
 68 
     | 
    
         
            +
                return redis.status_reply("ok")
         
     | 
| 
      
 69 
     | 
    
         
            +
              end
         
     | 
| 
      
 70 
     | 
    
         
            +
            }
         
     | 
| 
      
 71 
     | 
    
         
            +
             
     | 
| 
      
 72 
     | 
    
         
            +
            commands.RESET = {
         
     | 
| 
      
 73 
     | 
    
         
            +
              params = { "strategies" },
         
     | 
| 
      
 74 
     | 
    
         
            +
              handler = function (params)
         
     | 
| 
      
 75 
     | 
    
         
            +
                for _, strategy in ipairs(params.strategies) do
         
     | 
| 
      
 76 
     | 
    
         
            +
                  redis.call("DEL", strategy.key)
         
     | 
| 
      
 77 
     | 
    
         
            +
                end
         
     | 
| 
      
 78 
     | 
    
         
            +
             
     | 
| 
      
 79 
     | 
    
         
            +
                return redis.status_reply("ok")
         
     | 
| 
      
 80 
     | 
    
         
            +
              end
         
     | 
| 
      
 81 
     | 
    
         
            +
            }
         
     | 
| 
      
 82 
     | 
    
         
            +
             
     | 
| 
      
 83 
     | 
    
         
            +
            commands.INFO = {
         
     | 
| 
      
 84 
     | 
    
         
            +
              params = { "strategies", "timestamp" },
         
     | 
| 
      
 85 
     | 
    
         
            +
              handler = function (params)
         
     | 
| 
      
 86 
     | 
    
         
            +
                local usage, now = {}, params.timestamp
         
     | 
| 
      
 87 
     | 
    
         
            +
             
     | 
| 
      
 88 
     | 
    
         
            +
                for _, strategy in ipairs(params.strategies) do
         
     | 
| 
      
 89 
     | 
    
         
            +
                  local key = strategy.key
         
     | 
| 
      
 90 
     | 
    
         
            +
             
     | 
| 
      
 91 
     | 
    
         
            +
                  if "concurrency" == strategy.name then
         
     | 
| 
      
 92 
     | 
    
         
            +
                    redis.call("ZREMRANGEBYSCORE", key, "-inf", "(" .. now)
         
     | 
| 
      
 93 
     | 
    
         
            +
                    table.insert(usage, redis.call("ZCARD", key))
         
     | 
| 
      
 94 
     | 
    
         
            +
                  elseif "rate_limit" == strategy.name then
         
     | 
| 
      
 95 
     | 
    
         
            +
                    local last = tonumber(redis.call("LINDEX", key, -1) or now)
         
     | 
| 
      
 96 
     | 
    
         
            +
             
     | 
| 
      
 97 
     | 
    
         
            +
                    while last < now do
         
     | 
| 
      
 98 
     | 
    
         
            +
                      redis.call("RPOP", key)
         
     | 
| 
      
 99 
     | 
    
         
            +
                      last = tonumber(redis.call("LINDEX", key, -1) or now)
         
     | 
| 
      
 100 
     | 
    
         
            +
                    end
         
     | 
| 
      
 101 
     | 
    
         
            +
             
     | 
| 
      
 102 
     | 
    
         
            +
                    table.insert(usage, redis.call("LLEN", key))
         
     | 
| 
      
 103 
     | 
    
         
            +
                  end
         
     | 
| 
      
 104 
     | 
    
         
            +
                end
         
     | 
| 
      
 105 
     | 
    
         
            +
             
     | 
| 
      
 106 
     | 
    
         
            +
                return usage
         
     | 
| 
      
 107 
     | 
    
         
            +
              end
         
     | 
| 
      
 108 
     | 
    
         
            +
            }
         
     | 
| 
      
 109 
     | 
    
         
            +
             
     | 
| 
      
 110 
     | 
    
         
            +
            local function parse_params (parts)
         
     | 
| 
      
 111 
     | 
    
         
            +
              local parse = {}
         
     | 
| 
      
 112 
     | 
    
         
            +
             
     | 
| 
      
 113 
     | 
    
         
            +
              function parse.strategies (pos)
         
     | 
| 
      
 114 
     | 
    
         
            +
                local strategies = {}
         
     | 
| 
      
 115 
     | 
    
         
            +
             
     | 
| 
      
 116 
     | 
    
         
            +
                while pos + 3 <= #ARGV do
         
     | 
| 
      
 117 
     | 
    
         
            +
                  local name, strategy = string.lower(ARGV[pos]), nil
         
     | 
| 
      
 118 
     | 
    
         
            +
                  local bucket, limit, ttl_or_period = ARGV[pos + 1], tonumber(ARGV[pos + 2]), tonumber(ARGV[pos + 3])
         
     | 
| 
      
 119 
     | 
    
         
            +
             
     | 
| 
      
 120 
     | 
    
         
            +
                  if "concurrency" == name then
         
     | 
| 
      
 121 
     | 
    
         
            +
                    strategy = { name = name, bucket = bucket, limit = limit, ttl = ttl_or_period }
         
     | 
| 
      
 122 
     | 
    
         
            +
                  elseif "rate_limit" == name then
         
     | 
| 
      
 123 
     | 
    
         
            +
                    strategy = { name = name, bucket = bucket, limit = limit, period = ttl_or_period }
         
     | 
| 
      
 124 
     | 
    
         
            +
                  else
         
     | 
| 
      
 125 
     | 
    
         
            +
                    break
         
     | 
| 
      
 126 
     | 
    
         
            +
                  end
         
     | 
| 
      
 127 
     | 
    
         
            +
             
     | 
| 
      
 128 
     | 
    
         
            +
                  if bucket and 0 < limit and 0 < ttl_or_period then
         
     | 
| 
      
 129 
     | 
    
         
            +
                    strategy.key = table.concat({ KEYS[1], name, bucket, limit, ttl_or_period }, ":")
         
     | 
| 
      
 130 
     | 
    
         
            +
                    table.insert(strategies, strategy)
         
     | 
| 
      
 131 
     | 
    
         
            +
             
     | 
| 
      
 132 
     | 
    
         
            +
                    pos = pos + 4
         
     | 
| 
      
 133 
     | 
    
         
            +
                  else
         
     | 
| 
      
 134 
     | 
    
         
            +
                    return { err = "invalid " .. name .. " options" }
         
     | 
| 
      
 135 
     | 
    
         
            +
                  end
         
     | 
| 
      
 136 
     | 
    
         
            +
                end
         
     | 
| 
      
 137 
     | 
    
         
            +
             
     | 
| 
      
 138 
     | 
    
         
            +
                if 0 == #strategies then
         
     | 
| 
      
 139 
     | 
    
         
            +
                  return { err = "missing strategies" }
         
     | 
| 
      
 140 
     | 
    
         
            +
                end
         
     | 
| 
      
 141 
     | 
    
         
            +
             
     | 
| 
      
 142 
     | 
    
         
            +
                return { val = strategies, pos = pos }
         
     | 
| 
      
 143 
     | 
    
         
            +
              end
         
     | 
| 
      
 144 
     | 
    
         
            +
             
     | 
| 
      
 145 
     | 
    
         
            +
              function parse.token (pos)
         
     | 
| 
      
 146 
     | 
    
         
            +
                if ARGV[pos] and ARGV[pos + 1] and "TOKEN" == string.upper(ARGV[pos]) then
         
     | 
| 
      
 147 
     | 
    
         
            +
                  return { val = ARGV[pos + 1], pos = pos + 2 }
         
     | 
| 
      
 148 
     | 
    
         
            +
                end
         
     | 
| 
      
 149 
     | 
    
         
            +
             
     | 
| 
      
 150 
     | 
    
         
            +
                return { err = "missing or invalid token" }
         
     | 
| 
      
 151 
     | 
    
         
            +
              end
         
     | 
| 
      
 152 
     | 
    
         
            +
             
     | 
| 
      
 153 
     | 
    
         
            +
              function parse.timestamp (pos)
         
     | 
| 
      
 154 
     | 
    
         
            +
                if ARGV[pos] and ARGV[pos + 1] and "TS" == string.upper(ARGV[pos]) then
         
     | 
| 
      
 155 
     | 
    
         
            +
                  local timestamp = tonumber(ARGV[pos + 1])
         
     | 
| 
      
 156 
     | 
    
         
            +
             
     | 
| 
      
 157 
     | 
    
         
            +
                  if 0 < timestamp then
         
     | 
| 
      
 158 
     | 
    
         
            +
                    return { val = timestamp, pos = pos + 2 }
         
     | 
| 
      
 159 
     | 
    
         
            +
                  end
         
     | 
| 
      
 160 
     | 
    
         
            +
                end
         
     | 
| 
      
 161 
     | 
    
         
            +
             
     | 
| 
      
 162 
     | 
    
         
            +
                return { err = "missing or invalid timestamp" }
         
     | 
| 
      
 163 
     | 
    
         
            +
              end
         
     | 
| 
      
 164 
     | 
    
         
            +
             
     | 
| 
      
 165 
     | 
    
         
            +
              local params, pos = {}, 2
         
     | 
| 
      
 166 
     | 
    
         
            +
             
     | 
| 
      
 167 
     | 
    
         
            +
              for _, part in ipairs(parts) do
         
     | 
| 
      
 168 
     | 
    
         
            +
                local out = parse[part](pos)
         
     | 
| 
      
 169 
     | 
    
         
            +
             
     | 
| 
      
 170 
     | 
    
         
            +
                if out.err then
         
     | 
| 
      
 171 
     | 
    
         
            +
                  return out
         
     | 
| 
      
 172 
     | 
    
         
            +
                end
         
     | 
| 
      
 173 
     | 
    
         
            +
             
     | 
| 
      
 174 
     | 
    
         
            +
                params[part] = out.val
         
     | 
| 
      
 175 
     | 
    
         
            +
                pos = out.pos
         
     | 
| 
      
 176 
     | 
    
         
            +
              end
         
     | 
| 
      
 177 
     | 
    
         
            +
             
     | 
| 
      
 178 
     | 
    
         
            +
              if pos < #ARGV then
         
     | 
| 
      
 179 
     | 
    
         
            +
                return { err = "wrong number of arguments" }
         
     | 
| 
      
 180 
     | 
    
         
            +
              end
         
     | 
| 
      
 181 
     | 
    
         
            +
             
     | 
| 
      
 182 
     | 
    
         
            +
              return { val = params }
         
     | 
| 
      
 183 
     | 
    
         
            +
            end
         
     | 
| 
      
 184 
     | 
    
         
            +
             
     | 
| 
      
 185 
     | 
    
         
            +
            local command = commands[string.upper(ARGV[1])]
         
     | 
| 
      
 186 
     | 
    
         
            +
            if not command then
         
     | 
| 
      
 187 
     | 
    
         
            +
              return redis.error_reply("invalid command")
         
     | 
| 
      
 188 
     | 
    
         
            +
            end
         
     | 
| 
      
 189 
     | 
    
         
            +
             
     | 
| 
      
 190 
     | 
    
         
            +
            local params = parse_params(command.params)
         
     | 
| 
      
 191 
     | 
    
         
            +
            if params.err then
         
     | 
| 
      
 192 
     | 
    
         
            +
              return redis.error_reply(params.err)
         
     | 
| 
      
 193 
     | 
    
         
            +
            end
         
     | 
| 
      
 194 
     | 
    
         
            +
             
     | 
| 
      
 195 
     | 
    
         
            +
            return command.handler(params.val)
         
     | 
| 
         @@ -0,0 +1,118 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # frozen_string_literal: true
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            require "redis"
         
     | 
| 
      
 4 
     | 
    
         
            +
            require "redis-prescription"
         
     | 
| 
      
 5 
     | 
    
         
            +
             
     | 
| 
      
 6 
     | 
    
         
            +
            require_relative "./concurrency"
         
     | 
| 
      
 7 
     | 
    
         
            +
            require_relative "./rate_limit"
         
     | 
| 
      
 8 
     | 
    
         
            +
             
     | 
| 
      
 9 
     | 
    
         
            +
            class RedisThrottle
         
     | 
| 
      
 10 
     | 
    
         
            +
              # @api private
         
     | 
| 
      
 11 
     | 
    
         
            +
              class Api
         
     | 
| 
      
 12 
     | 
    
         
            +
                NAMESPACE = "throttle"
         
     | 
| 
      
 13 
     | 
    
         
            +
                private_constant :NAMESPACE
         
     | 
| 
      
 14 
     | 
    
         
            +
             
     | 
| 
      
 15 
     | 
    
         
            +
                KEYS_PATTERN = %r{
         
     | 
| 
      
 16 
     | 
    
         
            +
                  \A
         
     | 
| 
      
 17 
     | 
    
         
            +
                  #{NAMESPACE}:
         
     | 
| 
      
 18 
     | 
    
         
            +
                  (?<strategy>concurrency|rate_limit):
         
     | 
| 
      
 19 
     | 
    
         
            +
                  (?<bucket>.+):
         
     | 
| 
      
 20 
     | 
    
         
            +
                  (?<limit>\d+):
         
     | 
| 
      
 21 
     | 
    
         
            +
                  (?<ttl_or_period>\d+)
         
     | 
| 
      
 22 
     | 
    
         
            +
                \z
         
     | 
| 
      
 23 
     | 
    
         
            +
                }x.freeze
         
     | 
| 
      
 24 
     | 
    
         
            +
                private_constant :KEYS_PATTERN
         
     | 
| 
      
 25 
     | 
    
         
            +
             
     | 
| 
      
 26 
     | 
    
         
            +
                SCRIPT = RedisPrescription.new(File.read("#{__dir__}/api.lua"))
         
     | 
| 
      
 27 
     | 
    
         
            +
                private_constant :SCRIPT
         
     | 
| 
      
 28 
     | 
    
         
            +
             
     | 
| 
      
 29 
     | 
    
         
            +
                # @param redis [Redis, Redis::Namespace, #to_proc]
         
     | 
| 
      
 30 
     | 
    
         
            +
                def initialize(redis: nil)
         
     | 
| 
      
 31 
     | 
    
         
            +
                  @redis =
         
     | 
| 
      
 32 
     | 
    
         
            +
                    if redis.respond_to?(:to_proc)
         
     | 
| 
      
 33 
     | 
    
         
            +
                      redis.to_proc
         
     | 
| 
      
 34 
     | 
    
         
            +
                    else
         
     | 
| 
      
 35 
     | 
    
         
            +
                      ->(&b) { b.call(redis || Redis.current) }
         
     | 
| 
      
 36 
     | 
    
         
            +
                    end
         
     | 
| 
      
 37 
     | 
    
         
            +
                end
         
     | 
| 
      
 38 
     | 
    
         
            +
             
     | 
| 
      
 39 
     | 
    
         
            +
                # @param strategies [Enumerable<Concurrency, RateLimit>]
         
     | 
| 
      
 40 
     | 
    
         
            +
                # @param token [String]
         
     | 
| 
      
 41 
     | 
    
         
            +
                # @return [Boolean]
         
     | 
| 
      
 42 
     | 
    
         
            +
                def acquire(strategies:, token:)
         
     | 
| 
      
 43 
     | 
    
         
            +
                  execute(:ACQUIRE, to_params(strategies.sort_by(&:itself)) << :TOKEN << token << :TS << Time.now.to_i).zero?
         
     | 
| 
      
 44 
     | 
    
         
            +
                end
         
     | 
| 
      
 45 
     | 
    
         
            +
             
     | 
| 
      
 46 
     | 
    
         
            +
                # @param strategies [Enumerable<Concurrency, RateLimit>]
         
     | 
| 
      
 47 
     | 
    
         
            +
                # @param token [String]
         
     | 
| 
      
 48 
     | 
    
         
            +
                # @return [void]
         
     | 
| 
      
 49 
     | 
    
         
            +
                def release(strategies:, token:)
         
     | 
| 
      
 50 
     | 
    
         
            +
                  execute(:RELEASE, to_params(strategies.grep(Concurrency)) << :TOKEN << token)
         
     | 
| 
      
 51 
     | 
    
         
            +
                end
         
     | 
| 
      
 52 
     | 
    
         
            +
             
     | 
| 
      
 53 
     | 
    
         
            +
                # @param strategies [Enumerable<Concurrency, RateLimit>]
         
     | 
| 
      
 54 
     | 
    
         
            +
                # @return [void]
         
     | 
| 
      
 55 
     | 
    
         
            +
                def reset(strategies:)
         
     | 
| 
      
 56 
     | 
    
         
            +
                  execute(:RESET, to_params(strategies))
         
     | 
| 
      
 57 
     | 
    
         
            +
                end
         
     | 
| 
      
 58 
     | 
    
         
            +
             
     | 
| 
      
 59 
     | 
    
         
            +
                # @param match [String]
         
     | 
| 
      
 60 
     | 
    
         
            +
                # @return [Array<Concurrency, RateLimit>]
         
     | 
| 
      
 61 
     | 
    
         
            +
                def strategies(match:)
         
     | 
| 
      
 62 
     | 
    
         
            +
                  results = []
         
     | 
| 
      
 63 
     | 
    
         
            +
             
     | 
| 
      
 64 
     | 
    
         
            +
                  @redis.call do |redis|
         
     | 
| 
      
 65 
     | 
    
         
            +
                    redis.scan_each(match: "#{NAMESPACE}:*:#{match}:*:*") do |key|
         
     | 
| 
      
 66 
     | 
    
         
            +
                      strategy = from_key(key)
         
     | 
| 
      
 67 
     | 
    
         
            +
                      results << strategy if strategy
         
     | 
| 
      
 68 
     | 
    
         
            +
                    end
         
     | 
| 
      
 69 
     | 
    
         
            +
                  end
         
     | 
| 
      
 70 
     | 
    
         
            +
             
     | 
| 
      
 71 
     | 
    
         
            +
                  results
         
     | 
| 
      
 72 
     | 
    
         
            +
                end
         
     | 
| 
      
 73 
     | 
    
         
            +
             
     | 
| 
      
 74 
     | 
    
         
            +
                # @param strategies [Enumerable<Concurrency, RateLimit>]
         
     | 
| 
      
 75 
     | 
    
         
            +
                # @return [Hash{Concurrency => Integer, RateLimit => Integer}]
         
     | 
| 
      
 76 
     | 
    
         
            +
                def info(strategies:)
         
     | 
| 
      
 77 
     | 
    
         
            +
                  strategies.zip(execute(:INFO, to_params(strategies) << :TS << Time.now.to_i)).to_h
         
     | 
| 
      
 78 
     | 
    
         
            +
                end
         
     | 
| 
      
 79 
     | 
    
         
            +
             
     | 
| 
      
 80 
     | 
    
         
            +
                # @note Used for specs only.
         
     | 
| 
      
 81 
     | 
    
         
            +
                # @return [void]
         
     | 
| 
      
 82 
     | 
    
         
            +
                def ping
         
     | 
| 
      
 83 
     | 
    
         
            +
                  @redis.call(&:ping)
         
     | 
| 
      
 84 
     | 
    
         
            +
                end
         
     | 
| 
      
 85 
     | 
    
         
            +
             
     | 
| 
      
 86 
     | 
    
         
            +
                private
         
     | 
| 
      
 87 
     | 
    
         
            +
             
     | 
| 
      
 88 
     | 
    
         
            +
                def execute(command, argv)
         
     | 
| 
      
 89 
     | 
    
         
            +
                  @redis.call { |redis| SCRIPT.call(redis, keys: [NAMESPACE], argv: [command, *argv]) }
         
     | 
| 
      
 90 
     | 
    
         
            +
                end
         
     | 
| 
      
 91 
     | 
    
         
            +
             
     | 
| 
      
 92 
     | 
    
         
            +
                def from_key(key)
         
     | 
| 
      
 93 
     | 
    
         
            +
                  md = KEYS_PATTERN.match(key)
         
     | 
| 
      
 94 
     | 
    
         
            +
             
     | 
| 
      
 95 
     | 
    
         
            +
                  case md && md[:strategy]
         
     | 
| 
      
 96 
     | 
    
         
            +
                  when "concurrency"
         
     | 
| 
      
 97 
     | 
    
         
            +
                    Concurrency.new(md[:bucket], limit: md[:limit], ttl: md[:ttl_or_period])
         
     | 
| 
      
 98 
     | 
    
         
            +
                  when "rate_limit"
         
     | 
| 
      
 99 
     | 
    
         
            +
                    RateLimit.new(md[:bucket], limit: md[:limit], period: md[:ttl_or_period])
         
     | 
| 
      
 100 
     | 
    
         
            +
                  end
         
     | 
| 
      
 101 
     | 
    
         
            +
                end
         
     | 
| 
      
 102 
     | 
    
         
            +
             
     | 
| 
      
 103 
     | 
    
         
            +
                def to_params(strategies)
         
     | 
| 
      
 104 
     | 
    
         
            +
                  result = []
         
     | 
| 
      
 105 
     | 
    
         
            +
             
     | 
| 
      
 106 
     | 
    
         
            +
                  strategies.each do |strategy|
         
     | 
| 
      
 107 
     | 
    
         
            +
                    case strategy
         
     | 
| 
      
 108 
     | 
    
         
            +
                    when Concurrency
         
     | 
| 
      
 109 
     | 
    
         
            +
                      result << "concurrency" << strategy.bucket << strategy.limit << strategy.ttl
         
     | 
| 
      
 110 
     | 
    
         
            +
                    when RateLimit
         
     | 
| 
      
 111 
     | 
    
         
            +
                      result << "rate_limit" << strategy.bucket << strategy.limit << strategy.period
         
     | 
| 
      
 112 
     | 
    
         
            +
                    end
         
     | 
| 
      
 113 
     | 
    
         
            +
                  end
         
     | 
| 
      
 114 
     | 
    
         
            +
             
     | 
| 
      
 115 
     | 
    
         
            +
                  result
         
     | 
| 
      
 116 
     | 
    
         
            +
                end
         
     | 
| 
      
 117 
     | 
    
         
            +
              end
         
     | 
| 
      
 118 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,43 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # frozen_string_literal: true
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            require_relative "./api"
         
     | 
| 
      
 4 
     | 
    
         
            +
             
     | 
| 
      
 5 
     | 
    
         
            +
            class RedisThrottle
         
     | 
| 
      
 6 
     | 
    
         
            +
              module ClassMethods
         
     | 
| 
      
 7 
     | 
    
         
            +
                # Syntax sugar for {Throttle#concurrency}.
         
     | 
| 
      
 8 
     | 
    
         
            +
                #
         
     | 
| 
      
 9 
     | 
    
         
            +
                # @see #concurrency
         
     | 
| 
      
 10 
     | 
    
         
            +
                # @param (see Throttle#initialize)
         
     | 
| 
      
 11 
     | 
    
         
            +
                # @param (see Throttle#concurrency)
         
     | 
| 
      
 12 
     | 
    
         
            +
                # @return (see Throttle#concurrency)
         
     | 
| 
      
 13 
     | 
    
         
            +
                def concurrency(bucket, limit:, ttl:, redis: nil)
         
     | 
| 
      
 14 
     | 
    
         
            +
                  new(redis: redis).concurrency(bucket, limit: limit, ttl: ttl)
         
     | 
| 
      
 15 
     | 
    
         
            +
                end
         
     | 
| 
      
 16 
     | 
    
         
            +
             
     | 
| 
      
 17 
     | 
    
         
            +
                # Syntax sugar for {Throttle#rate_limit}.
         
     | 
| 
      
 18 
     | 
    
         
            +
                #
         
     | 
| 
      
 19 
     | 
    
         
            +
                # @see #concurrency
         
     | 
| 
      
 20 
     | 
    
         
            +
                # @param (see Throttle#initialize)
         
     | 
| 
      
 21 
     | 
    
         
            +
                # @param (see Throttle#rate_limit)
         
     | 
| 
      
 22 
     | 
    
         
            +
                # @return (see Throttle#rate_limit)
         
     | 
| 
      
 23 
     | 
    
         
            +
                def rate_limit(bucket, limit:, period:, redis: nil)
         
     | 
| 
      
 24 
     | 
    
         
            +
                  new(redis: redis).rate_limit(bucket, limit: limit, period: period)
         
     | 
| 
      
 25 
     | 
    
         
            +
                end
         
     | 
| 
      
 26 
     | 
    
         
            +
             
     | 
| 
      
 27 
     | 
    
         
            +
                # Return usage info for all known (in use) strategies.
         
     | 
| 
      
 28 
     | 
    
         
            +
                #
         
     | 
| 
      
 29 
     | 
    
         
            +
                # @example
         
     | 
| 
      
 30 
     | 
    
         
            +
                #   Redis::Throttle.info(:match => "*_api").each do |strategy, current_value|
         
     | 
| 
      
 31 
     | 
    
         
            +
                #     # ...
         
     | 
| 
      
 32 
     | 
    
         
            +
                #   end
         
     | 
| 
      
 33 
     | 
    
         
            +
                #
         
     | 
| 
      
 34 
     | 
    
         
            +
                # @param match [#to_s]
         
     | 
| 
      
 35 
     | 
    
         
            +
                # @return (see Api#info)
         
     | 
| 
      
 36 
     | 
    
         
            +
                def info(match: "*", redis: nil)
         
     | 
| 
      
 37 
     | 
    
         
            +
                  api        = Api.new(redis: redis)
         
     | 
| 
      
 38 
     | 
    
         
            +
                  strategies = api.strategies(match: match.to_s)
         
     | 
| 
      
 39 
     | 
    
         
            +
             
     | 
| 
      
 40 
     | 
    
         
            +
                  api.info(strategies: strategies)
         
     | 
| 
      
 41 
     | 
    
         
            +
                end
         
     | 
| 
      
 42 
     | 
    
         
            +
              end
         
     | 
| 
      
 43 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,75 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # frozen_string_literal: true
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            class RedisThrottle
         
     | 
| 
      
 4 
     | 
    
         
            +
              class Concurrency
         
     | 
| 
      
 5 
     | 
    
         
            +
                # @!attribute [r] bucket
         
     | 
| 
      
 6 
     | 
    
         
            +
                #   @return [String] Throttling group name
         
     | 
| 
      
 7 
     | 
    
         
            +
                attr_reader :bucket
         
     | 
| 
      
 8 
     | 
    
         
            +
             
     | 
| 
      
 9 
     | 
    
         
            +
                # @!attribute [r] limit
         
     | 
| 
      
 10 
     | 
    
         
            +
                #   @return [Integer] Max allowed concurrent units
         
     | 
| 
      
 11 
     | 
    
         
            +
                attr_reader :limit
         
     | 
| 
      
 12 
     | 
    
         
            +
             
     | 
| 
      
 13 
     | 
    
         
            +
                # @!attribute [r] ttl
         
     | 
| 
      
 14 
     | 
    
         
            +
                #   @return [Integer] Time (in seconds) to hold the lock before
         
     | 
| 
      
 15 
     | 
    
         
            +
                #     releasing it (in case it wasn't released already)
         
     | 
| 
      
 16 
     | 
    
         
            +
                attr_reader :ttl
         
     | 
| 
      
 17 
     | 
    
         
            +
             
     | 
| 
      
 18 
     | 
    
         
            +
                # @param bucket [#to_s] Throttling group name
         
     | 
| 
      
 19 
     | 
    
         
            +
                # @param limit [#to_i] Max allowed concurrent units
         
     | 
| 
      
 20 
     | 
    
         
            +
                # @param ttl [#to_i] Time (in seconds) to hold the lock before
         
     | 
| 
      
 21 
     | 
    
         
            +
                #   releasing it (in case it wasn't released already)
         
     | 
| 
      
 22 
     | 
    
         
            +
                def initialize(bucket, limit:, ttl:)
         
     | 
| 
      
 23 
     | 
    
         
            +
                  @bucket = -bucket.to_s
         
     | 
| 
      
 24 
     | 
    
         
            +
                  @limit  = limit.to_i
         
     | 
| 
      
 25 
     | 
    
         
            +
                  @ttl    = ttl.to_i
         
     | 
| 
      
 26 
     | 
    
         
            +
                end
         
     | 
| 
      
 27 
     | 
    
         
            +
             
     | 
| 
      
 28 
     | 
    
         
            +
                # Returns `true` if `other` is a {Concurrency} instance with the same
         
     | 
| 
      
 29 
     | 
    
         
            +
                # {#bucket}, {#limit}, and {#ttl}.
         
     | 
| 
      
 30 
     | 
    
         
            +
                #
         
     | 
| 
      
 31 
     | 
    
         
            +
                # @see https://docs.ruby-lang.org/en/master/Object.html#method-i-eql-3F
         
     | 
| 
      
 32 
     | 
    
         
            +
                # @param other [Object]
         
     | 
| 
      
 33 
     | 
    
         
            +
                # @return [Boolean]
         
     | 
| 
      
 34 
     | 
    
         
            +
                def ==(other)
         
     | 
| 
      
 35 
     | 
    
         
            +
                  return true  if equal? other
         
     | 
| 
      
 36 
     | 
    
         
            +
                  return false unless other.is_a?(self.class)
         
     | 
| 
      
 37 
     | 
    
         
            +
             
     | 
| 
      
 38 
     | 
    
         
            +
                  @bucket == other.bucket && @limit == other.limit && @ttl == other.ttl
         
     | 
| 
      
 39 
     | 
    
         
            +
                end
         
     | 
| 
      
 40 
     | 
    
         
            +
             
     | 
| 
      
 41 
     | 
    
         
            +
                alias eql? ==
         
     | 
| 
      
 42 
     | 
    
         
            +
             
     | 
| 
      
 43 
     | 
    
         
            +
                # @api private
         
     | 
| 
      
 44 
     | 
    
         
            +
                #
         
     | 
| 
      
 45 
     | 
    
         
            +
                # Compare `self` with `other` strategy:
         
     | 
| 
      
 46 
     | 
    
         
            +
                #
         
     | 
| 
      
 47 
     | 
    
         
            +
                # - Returns `nil` if `other` is neither {Concurrency} nor {RateLimit}
         
     | 
| 
      
 48 
     | 
    
         
            +
                # - Returns `1` if `other` is a {RateLimit}
         
     | 
| 
      
 49 
     | 
    
         
            +
                # - Returns `1` if `other` is a {Concurrency} with lower {#limit}
         
     | 
| 
      
 50 
     | 
    
         
            +
                # - Returns `0` if `other` is a {Concurrency} with the same {#limit}
         
     | 
| 
      
 51 
     | 
    
         
            +
                # - Returns `-1` if `other` is a {Concurrency} with bigger {#limit}
         
     | 
| 
      
 52 
     | 
    
         
            +
                #
         
     | 
| 
      
 53 
     | 
    
         
            +
                # @return [-1, 0, 1, nil]
         
     | 
| 
      
 54 
     | 
    
         
            +
                def <=>(other)
         
     | 
| 
      
 55 
     | 
    
         
            +
                  complexity <=> other.complexity if other.respond_to? :complexity
         
     | 
| 
      
 56 
     | 
    
         
            +
                end
         
     | 
| 
      
 57 
     | 
    
         
            +
             
     | 
| 
      
 58 
     | 
    
         
            +
                # @api private
         
     | 
| 
      
 59 
     | 
    
         
            +
                #
         
     | 
| 
      
 60 
     | 
    
         
            +
                # Generates an Integer hash value for this object.
         
     | 
| 
      
 61 
     | 
    
         
            +
                #
         
     | 
| 
      
 62 
     | 
    
         
            +
                # @see https://docs.ruby-lang.org/en/master/Object.html#method-i-hash
         
     | 
| 
      
 63 
     | 
    
         
            +
                # @return [Integer]
         
     | 
| 
      
 64 
     | 
    
         
            +
                def hash
         
     | 
| 
      
 65 
     | 
    
         
            +
                  @hash ||= [@bucket, @limit, @ttl].hash
         
     | 
| 
      
 66 
     | 
    
         
            +
                end
         
     | 
| 
      
 67 
     | 
    
         
            +
             
     | 
| 
      
 68 
     | 
    
         
            +
                # @api private
         
     | 
| 
      
 69 
     | 
    
         
            +
                #
         
     | 
| 
      
 70 
     | 
    
         
            +
                # @return [Array(Integer, Integer)] Strategy complexity pseudo-score
         
     | 
| 
      
 71 
     | 
    
         
            +
                def complexity
         
     | 
| 
      
 72 
     | 
    
         
            +
                  [1, @limit]
         
     | 
| 
      
 73 
     | 
    
         
            +
                end
         
     | 
| 
      
 74 
     | 
    
         
            +
              end
         
     | 
| 
      
 75 
     | 
    
         
            +
            end
         
     |