Class: RedisThrottle

Inherits:
Object
  • Object
show all
Defined in:
lib/redis_throttle.rb,
lib/redis_throttle/api.rb,
lib/redis_throttle/version.rb,
lib/redis_throttle/rate_limit.rb,
lib/redis_throttle/concurrency.rb

Defined Under Namespace

Classes: Api, Concurrency, RateLimit

Constant Summary collapse

VERSION =

Gem version.

"2.0.1"

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initializeRedisThrottle

Returns a new instance of RedisThrottle.



46
47
48
# File 'lib/redis_throttle.rb', line 46

def initialize
  @strategies = Concurrent::Set.new
end

Class Method Details

.concurrencyThrottle

Syntax sugar for ‘RedisThrottle.new.concurrency(…)`.

Parameters:

  • bucket (#to_s)

    Throttling group name

  • limit (#to_i)

    Max allowed concurrent units

  • ttl (#to_i)

    Time (in seconds) to hold the lock before releasing it (in case it wasn’t released already)

Returns:

  • (Throttle)

    self

See Also:



18
19
20
# File 'lib/redis_throttle.rb', line 18

def concurrency(...)
  new.concurrency(...)
end

.info(redis, match: "*") ⇒ Hash{Concurrency => Integer, RateLimit => Integer}

Return usage info for all known (in use) strategies.

Examples:

RedisThrottle.info(:match => "*_api").each do |strategy, current_value|
  # ...
end

Parameters:

  • match (#to_s) (defaults to: "*")
  • redis (Redis, Redis::Namespace, RedisClient, RedisClient::Decorator::Client)

Returns:



41
42
43
# File 'lib/redis_throttle.rb', line 41

def info(redis, match: "*")
  Api.new(redis).then { |api| api.info(strategies: api.strategies(match: match.to_s)) }
end

.rate_limitThrottle

Syntax sugar for ‘RedisThrottle.new.rate_limit(…)`.

Parameters:

  • bucket (#to_s)

    Throttling group name

  • limit (#to_i)

    Max allowed units per ‘period`

  • period (#to_i)

    Period in seconds

Returns:

  • (Throttle)

    self

See Also:



27
28
29
# File 'lib/redis_throttle.rb', line 27

def rate_limit(...)
  new.rate_limit(...)
end

Instance Method Details

#==(other) ⇒ Boolean Also known as: eql?

Returns ‘true` if the `other` is an instance of RedisThrottle with the same set of strategies.

Examples:

a = RedisThrottle
  .concurrency(:a, limit: 1, ttl: 2)
  .rate_limit(:b, limit: 3, period: 4)

b = RedisThrottle
  .rate_limit(:b, limit: 3, period: 4)
  .concurrency(:a, limit: 1, ttl: 2)

a == b # => true

Returns:

  • (Boolean)


196
197
198
# File 'lib/redis_throttle.rb', line 196

def ==(other)
  other.is_a?(self.class) && @strategies == other.strategies
end

#acquire(redis, token: SecureRandom.uuid) ⇒ #to_s?

Acquire execution lock.

Examples:

throttle = RedisThrottle.concurrency(:xxx, limit: 1, ttl: 10)

if (token = throttle.acquire(redis))
  # ... do something
end

throttle.release(redis, token: token) if token

Parameters:

  • token (#to_s) (defaults to: SecureRandom.uuid)

    Unit of work ID

  • redis (Redis, Redis::Namespace, RedisClient, RedisClient::Decorator::Client)

Returns:

  • (#to_s)

    ‘token` as is if lock was acquired

  • (nil)

    otherwise

See Also:



246
247
248
# File 'lib/redis_throttle.rb', line 246

def acquire(redis, token: SecureRandom.uuid)
  token if Api.new(redis).acquire(strategies: @strategies, token: token.to_s)
end

#call(redis, token: SecureRandom.uuid) ⇒ Object?

Calls given block execution lock was acquired, and ensures to #release it after the block.

Examples:

throttle = RedisThrottle.concurrency(:xxx, limit: 1, ttl: 10)

throttle.call(redis) { :aye } # => :aye
throttle.call(redis) { :aye } # => :aye

throttle.acquire(redis)

throttle.call(redis) { :aye } # => nil

Parameters:

  • redis (Redis, Redis::Namespace, RedisClient, RedisClient::Decorator::Client)
  • token (#to_s) (defaults to: SecureRandom.uuid)

    Unit of work ID

Returns:

  • (Object)

    last satement of the block if execution lock was acquired.

  • (nil)

    otherwise



219
220
221
222
223
224
225
226
227
# File 'lib/redis_throttle.rb', line 219

def call(redis, token: SecureRandom.uuid)
  return unless acquire(redis, token: token)

  begin
    yield
  ensure
    release(redis, token: token)
  end
end

#concurrency(bucket, limit:, ttl:) ⇒ Throttle

Add concurrency strategy to the throttle. Use it to guarantee ‘limit` amount of concurrently running code blocks.

Examples:

throttle = RedisThrottle.new

# Allow max 2 concurrent execution units
throttle.concurrency(:xxx, limit: 2, ttl: 10)

throttle.acquire(redis, token: "a") && :aye || :nay # => :aye
throttle.acquire(redis, token: "b") && :aye || :nay # => :aye
throttle.acquire(redis, token: "c") && :aye || :nay # => :nay

throttle.release(redis, token: "a")

throttle.acquire(redis, token: "c") && :aye || :nay # => :aye

Parameters:

  • bucket (#to_s)

    Throttling group name

  • limit (#to_i)

    Max allowed concurrent units

  • ttl (#to_i)

    Time (in seconds) to hold the lock before releasing it (in case it wasn’t released already)

Returns:

  • (Throttle)

    self

Raises:

  • (FrozenError)


92
93
94
95
96
97
98
# File 'lib/redis_throttle.rb', line 92

def concurrency(bucket, limit:, ttl:)
  raise FrozenError, "can't modify frozen #{self.class}" if frozen?

  @strategies << Concurrency.new(bucket, limit: limit, ttl: ttl)

  self
end

#freezeThrottle

Prevents further modifications to the throttle instance.



175
176
177
178
179
# File 'lib/redis_throttle.rb', line 175

def freeze
  @strategies.freeze

  super
end

#info(redis) ⇒ Hash{Concurrency => Integer, RateLimit => Integer}

Return usage info for all strategies of the throttle.

Examples:

throttle.info(redis).each do |strategy, current_value|
  # ...
end

Parameters:

  • redis (Redis, Redis::Namespace, RedisClient, RedisClient::Decorator::Client)

Returns:



307
308
309
# File 'lib/redis_throttle.rb', line 307

def info(redis)
  Api.new(redis).info(strategies: @strategies)
end

#initialize_clone(original) ⇒ void

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

This method returns an undefined value.

Clone internal strategies plan.



66
67
68
69
70
# File 'lib/redis_throttle.rb', line 66

def initialize_clone(original)
  super

  @strategies = original.strategies.clone
end

#initialize_dup(original) ⇒ void

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

This method returns an undefined value.

Dup internal strategies plan.



55
56
57
58
59
# File 'lib/redis_throttle.rb', line 55

def initialize_dup(original)
  super

  @strategies = original.strategies.dup
end

#merge(other) ⇒ Throttle Also known as: +

Non-destructive version of #merge!. Returns new RedisThrottle instance with union of ‘self` and `other` strategies.

Examples:

a = RedisThrottle.concurrency(:a, limit: 1, ttl: 2)
b = RedisThrottle.rate_limit(:b, limit: 3, period: 4)
c = RedisThrottle
  .concurrency(:a, limit: 1, ttl: 2)
  .rate_limit(:b, limit: 3, period: 4)

a.merge(b) == c # => true
a == c          # => false

Returns:

  • (Throttle)

    new throttle



165
166
167
# File 'lib/redis_throttle.rb', line 165

def merge(other)
  dup.merge!(other)
end

#merge!(other) ⇒ Throttle

Merge in strategies of the ‘other` throttle.

Examples:

a = RedisThrottle.concurrency(:a, limit: 1, ttl: 2)
b = RedisThrottle.rate_limit(:b, limit: 3, period: 4)
c = RedisThrottle
  .concurrency(:a, limit: 1, ttl: 2)
  .rate_limit(:b, limit: 3, period: 4)

a.merge!(b)

a == c # => true

Returns:

  • (Throttle)

    self

Raises:

  • (FrozenError)


143
144
145
146
147
148
149
# File 'lib/redis_throttle.rb', line 143

def merge!(other)
  raise FrozenError, "can't modify frozen #{self.class}" if frozen?

  @strategies.merge(other.strategies)

  self
end

#rate_limit(bucket, limit:, period:) ⇒ Throttle

Add *rate limit* strategy to the throttle. Use it to guarantee ‘limit` amount of units in `period` of time.

Examples:

throttle = RedisThrottle.new

# Allow 2 execution units per 10 seconds
throttle.rate_limit(:xxx, limit: 2, period: 10)

throttle.acquire(redis) && :aye || :nay # => :aye
sleep 5

throttle.acquire(redis) && :aye || :nay # => :aye
throttle.acquire(redis) && :aye || :nay # => :nay

sleep 6
throttle.acquire(redis) && :aye || :nay # => :aye
throttle.acquire(redis) && :aye || :nay # => :nay

Parameters:

  • bucket (#to_s)

    Throttling group name

  • limit (#to_i)

    Max allowed units per ‘period`

  • period (#to_i)

    Period in seconds

Returns:

  • (Throttle)

    self

Raises:

  • (FrozenError)


121
122
123
124
125
126
127
# File 'lib/redis_throttle.rb', line 121

def rate_limit(bucket, limit:, period:)
  raise FrozenError, "can't modify frozen #{self.class}" if frozen?

  @strategies << RateLimit.new(bucket, limit: limit, period: period)

  self
end

#release(redis, token:) ⇒ void

This method returns an undefined value.

Release acquired execution lock. Notice that this affects #concurrency locks only.

Examples:

concurrency = RedisThrottle.concurrency(:xxx, limit: 1, ttl: 60)
rate_limit  = RedisThrottle.rate_limit(:xxx, limit: 1, period: 60)
throttle    = concurrency + rate_limit

throttle.acquire(redis, token: "uno")
throttle.release(redis, token: "uno")

concurrency.acquire(redis, token: "dos") # => "dos"
rate_limit.acquire(redis, token: "dos")  # => nil

Parameters:

  • token (#to_s)

    Unit of work ID

  • redis (Redis, Redis::Namespace, RedisClient, RedisClient::Decorator::Client)

See Also:



270
271
272
273
274
# File 'lib/redis_throttle.rb', line 270

def release(redis, token:)
  Api.new(redis).release(strategies: @strategies, token: token.to_s)

  nil
end

#reset(redis) ⇒ void

This method returns an undefined value.

Flush all counters.

Examples:

throttle = RedisThrottle.concurrency(:xxx, limit: 2, ttl: 60)

thottle.acquire(redis, token: "a") # => "a"
thottle.acquire(redis, token: "b") # => "b"
thottle.acquire(redis, token: "c") # => nil

throttle.reset(redis)

thottle.acquire(redis, token: "c") # => "c"
thottle.acquire(redis, token: "d") # => "d"

Parameters:

  • redis (Redis, Redis::Namespace, RedisClient, RedisClient::Decorator::Client)


292
293
294
295
296
# File 'lib/redis_throttle.rb', line 292

def reset(redis)
  Api.new(redis).reset(strategies: @strategies)

  nil
end