From ec62b762a33fa500c8fdee4145b8a0afb1f342f8 Mon Sep 17 00:00:00 2001 From: Jim Posen Date: Tue, 5 May 2015 13:03:03 -0700 Subject: [PATCH] Document with YARD and bump version to 1.0.0. --- Gemfile.lock | 4 +- README.md | 75 +------------------------------- lib/traffic_jam.rb | 15 +++++++ lib/traffic_jam/configuration.rb | 31 +++++++++++++ lib/traffic_jam/limit.rb | 74 +++++++++++++++++++++++++++++++ lib/traffic_jam/limit_group.rb | 49 +++++++++++++++++++++ traffic_jam.gemspec | 2 +- 7 files changed, 173 insertions(+), 77 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 2d93f45..b23ab14 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - traffic_jam (0.1.0) + traffic_jam (1.0.0) redis (~> 3.0) GEM @@ -19,7 +19,7 @@ GEM multi_json (1.10.1) netrc (0.10.3) rake (10.4.2) - redis (3.2.0) + redis (3.2.1) rest-client (1.7.3) mime-types (>= 1.16, < 3.0) netrc (~> 0.7) diff --git a/README.md b/README.md index 2c99039..edcef5b 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,7 @@ limit = TrafficJam::Limit.new( limit.increment # => true limit.increment(2) # => true limit.increment # => false +limit.increment! # => raises TrafficJam::LimitExceededError sleep 1 @@ -36,80 +37,6 @@ limit.used # => 2 limit.remaining # => 1 ``` -### TrafficJam::Limit - -**initialize(action, value, max: *cap*, period: *period in seconds*)** - -Constructor for `TrafficJam::Limit` takes an action name as a symbol, an integer maximum cap, and the period of limit in seconds. `max` and `period` are required keyword arguments. The value should be a string or convertible to a distinct string when `to_s` is called. If you would like to use objects that can be converted to a unique string, like a database-mapped object, you can implement `to_rate_limit_value` on the object, which returns a deterministic string unique to that object. - -**increment(amount = 1)** - -Increment the amount used by the given number. Returns true if increment succeded and false if incrementing would exceed the limit. - -**decrement(amount = 1)** - -Decrement the amount used by the given number. Will never decrement below 0. Always returns true. - -**increment!(amount = 1)** - -Increment the amount used by the given number. Raises `TrafficJam::LimitExceededError` if incrementing would exceed the limit. - -**exceeded?(amount = 1)** - -Return whether incrementing by the given amount would exceed limit. Does not change amount used. - -**reset** - -Sets amount used to 0. - -**used** - -Return current amount used. - -**remaining** - -Return current amount remaining. - -**TrafficJam.reset_all(action: nil)** - -Reset all limits associated with the given action. If action is omitted or nil, this will reset all limits. *Warning: Not to be used in production.* - -### TrafficJam::LimitGroup - -A limit group is a way of enforcing a cap over a set of limits with the guarantee that either all limits will be incremented or none. This is useful if you must check multiple limits before allowing an action to be taken. - -**initialize(\*limits)** - -Constructor for `TrafficJam::Limit` takes either an array or splat of limits or other limit groups. - -**increment(amount = 1)** - -Increment the limits by the given number. Returns true if all increments are successful and false if incrementing would exceed any rate limit. - -**decrement(amount = 1)** - -Decrement the limits by the given number. Will never decrement below 0. Always returns true. - -**increment!(amount = 1)** - -Increment the limits by the given number. Raises `TrafficJam::LimitExceededError` if incrementing would exceed the limit. Will not increment if error is raised. - -**exceeded?(amount = 1)** - -Return whether incrementing by the given amount would exceed any limit. Does not change amount used. - -**limit_exceeded(amount = 1)** - -Return the first `TrafficJam::Limit` that would be exceeded by this amount or `nil` if incrementing would be safe. - -**reset** - -Sets limits to 0. - -**remaining** - -Return minimum amount remaining of all limits. - ## Configuration TrafficJam configuration object can be accessed with `TrafficJam.config` or in a block like `TrafficJam.configure { |config| ... }`. Configuration options are: diff --git a/lib/traffic_jam.rb b/lib/traffic_jam.rb index 6090934..b8f9cb6 100644 --- a/lib/traffic_jam.rb +++ b/lib/traffic_jam.rb @@ -17,15 +17,29 @@ module TrafficJam class << self attr_reader :config + # Configure library in a block. + # + # @yield [TrafficJam::Configuration] def configure yield config end + # Create limit with registed max/period. + # + # @param action [Symbol] registered action name + # @param value [String] limit target value + # @return [TrafficJam::Limit] def limit(action, value) limits = config.limits(action.to_sym) TrafficJam::Limit.new(action, value, **limits) end + # Reset all limits associated with the given action. If action is omitted or + # nil, this will reset all limits. + # + # @note Not recommended for use in production. + # @param action [Symbol] action to reset limits for + # @return [nil] def reset_all(action: nil) prefix = if action.nil? @@ -36,6 +50,7 @@ def reset_all(action: nil) config.redis.keys(prefix).each do |key| config.redis.del(key) end + nil end %w( exceeded? increment increment! decrement reset used remaining ) diff --git a/lib/traffic_jam/configuration.rb b/lib/traffic_jam/configuration.rb index 24889dc..0a08b31 100644 --- a/lib/traffic_jam/configuration.rb +++ b/lib/traffic_jam/configuration.rb @@ -1,6 +1,17 @@ module TrafficJam + # Configuration for TrafficJam library. + # + # @see TrafficJam#configure class Configuration OPTIONS = %i( key_prefix hash_length redis ) + + # @!attribute redis + # @return [Redis] the connected Redis client the library uses + # @!attribute key_prefix + # @return [String] the prefix of all limit keys in Redis + # @!attribute hash_length + # @return [String] the number of characters to use from the Base64 encoded + # hashes of the limit values attr_accessor *OPTIONS def initialize(options = {}) @@ -9,19 +20,39 @@ def initialize(options = {}) end end + # Register a default cap and period with an action name. For use with + # {TrafficJam.limit}. + # + # @param action [Symbol] action name + # @param max [Integer] limit cap + # @param period [Fixnum] limit period in seconds def register(action, max, period) @limits ||= {} @limits[action.to_sym] = { max: max, period: period } end + # Get the limit cap registered to an action. + # + # @see #register + # @return [Integer] limit cap def max(action) limits(action)[:max] end + # Get the limit period registered to an action. + # + # @see #register + # @return [Integer] limit period in seconds def period(action) limits(action)[:period] end + # Get registered limit parameters for an action. + # + # @see #register + # @param action [Symbol] action name + # @return [Hash] max and period parameters in a hash + # @raise [TrafficJam::LimitNotFound] if action is not registered def limits(action) @limits ||= {} limits = @limits[action.to_sym] diff --git a/lib/traffic_jam/limit.rb b/lib/traffic_jam/limit.rb index e0d4578..bb33e25 100644 --- a/lib/traffic_jam/limit.rb +++ b/lib/traffic_jam/limit.rb @@ -1,23 +1,69 @@ require_relative 'scripts' module TrafficJam + # This class represents a rate limit on an action, value pair. For example, if + # rate limiting the number of requests per IP address, the action could be + # +:requests+ and the value would be the IP address. The class exposes atomic + # increment operations and allows querying of the current amount used and + # amount remaining. class Limit + # @!attribute [r] action + # @return [Symbol] the name of the action being rate limited. + # @!attribute [r] value + # @return [String] the target of the limit. The value should be a string + # or convertible to a distinct string when +to_s+ is called. If you + # would like to use objects that can be converted to a unique string, + # like a database-mapped object with an ID, you can implement + # +to_rate_limit_value+ on the object, which returns a deterministic + # string unique to that object. + # @!attribute [r] max + # @return [Integer] the integral cap of the limit amount. + # @!attribute [r] period + # @return [Integer] the duration of the limit in seconds. Regardless of + # the current amount used, after the period passes, the amount used will + # be 0. attr_reader :action, :max, :period, :value + # Constructor takes an action name as a symbol, a maximum cap, and the + # period of limit. +max+ and +period+ are required keyword arguments. + # + # @param action [Symbol] action name + # @param value [String] limit target value + # @param max [Integer] required limit maximum + # @param period [Integer] required limit period in seconds + # @raise [ArgumentError] if max or period is nil def initialize(action, value, max: nil, period: nil) raise ArgumentError('Max is required') if max.nil? raise ArgumentError('Period is required') if period.nil? @action, @value, @max, @period = action, value, max, period end + # Return whether incrementing by the given amount would exceed limit. Does + # not change amount used. + # + # @param amount [Integer] + # @return [Boolean] def exceeded?(amount = 1) used + amount > max end + # Return itself if incrementing by the given amount would exceed limit, + # otherwise nil. Does not change amount used. + # + # @return [TrafficJam::Limit, nil] def limit_exceeded(amount = 1) self if exceeded?(amount) end + # Increment the amount used by the given number. Does not perform increment + # if the operation would exceed the limit. Returns whether the operation was + # successful. Time of increment can be specified optionally with a keyword + # argument, which is useful for rolling back with a decrement. + # + # @param amount [Integer] amount to increment by + # @param time [Time] time when increment occurs + # @return [Boolean] true if increment succeded and false if incrementing + # would exceed the limit def increment(amount = 1, time: Time.now) return amount <= 0 if max.zero? @@ -39,20 +85,45 @@ def increment(amount = 1, time: Time.now) !!result end + # Increment the amount used by the given number. Does not perform increment + # if the operation would exceed the limit. Raises an exception if the + # operation is unsuccessful. Time of# increment can be specified optionally + # with a keyword argument, which is useful for rolling back with a + # decrement. + # + # @param amount [Integer] amount to increment by + # @param time [Time] time when increment occurs + # @return [nil] + # @raise [TrafficJam::LimitExceededError] if incrementing would exceed the + # limit def increment!(amount = 1, time: Time.now) if !increment(amount, time: time) raise TrafficJam::LimitExceededError.new(self) end end + # Decrement the amount used by the given number. Time of decrement can be + # specified optionally with a keyword argument, which is useful for rolling + # back an increment operation at a certain time. + # + # @param amount [Integer] amount to increment by + # @param time [Time] time when increment occurs + # @return [true] def decrement(amount = 1, time: Time.now) increment(-amount, time: time) end + # Reset amount used to 0. + # + # @return [nil] def reset redis.del(key) + nil end + # Return amount of limit used, taking time drift into account. + # + # @return [Integer] amount used def used return 0 if max.zero? @@ -69,6 +140,9 @@ def used end end + # Return amount of limit remaining, taking time drift into account. + # + # @return [Integer] amount remaining def remaining max - used end diff --git a/lib/traffic_jam/limit_group.rb b/lib/traffic_jam/limit_group.rb index 9527e2c..777d83b 100644 --- a/lib/traffic_jam/limit_group.rb +++ b/lib/traffic_jam/limit_group.rb @@ -1,7 +1,16 @@ module TrafficJam + # A limit group is a way of enforcing a cap over a set of limits with the + # guarantee that either all limits will be incremented or none. This is useful + # if you must check multiple limits before allowing an action to be taken. + # Limit groups can contain other limit groups. class LimitGroup attr_reader :limits + # Creates a limit group from a collection of limits or other limit groups. + # + # @param limits [Array] either an array or splat of + # limits or other limit groups + # @param ignore_nil_values [Boolean] silently drop limits with a nil value def initialize(*limits, ignore_nil_values: false) @limits = limits.flatten @ignore_nil_values = ignore_nil_values @@ -12,12 +21,21 @@ def initialize(*limits, ignore_nil_values: false) end end + # Add a limit to the group. + # + # @param limit [TrafficJam::Limit, TrafficJam::LimitGroup] def <<(limit) if !(@ignore_nil_values && limit.value.nil?) limits << limit end end + # Attempt to increment the limits by the given amount. Does not increment + # if incrementing would exceed any limit. + # + # @param amount [Integer] amount to increment by + # @param time [Time] optional time of increment + # @return [Boolean] whether increment operation was successful def increment(amount = 1, time: Time.now) exceeded_index = limits.find_index do |limit| !limit.increment(amount, time: time) @@ -30,6 +48,14 @@ def increment(amount = 1, time: Time.now) exceeded_index.nil? end + # Increment the limits by the given amount. Raises an error and does not + # increment if doing so would exceed any limit. + # + # @param amount [Integer] amount to increment by + # @param time [Time] optional time of increment + # @return [nil] + # @raise [TrafficJam::LimitExceededError] if increment would exceed any + # limits def increment!(amount = 1, time: Time.now) exception = nil exceeded_index = limits.find_index do |limit| @@ -48,14 +74,29 @@ def increment!(amount = 1, time: Time.now) end end + # Decrement the limits by the given amount. + # + # @param amount [Integer] amount to decrement by + # @param time [Time] optional time of decrement + # @return [true] def decrement(amount = 1, time: Time.now) limits.all? { |limit| limit.decrement(amount, time: time) } end + # Return whether incrementing by the given amount would exceed any limit. + # Does not change amount used. + # + # @param amount [Integer] + # @return [Boolean] whether any limit would be exceeded def exceeded?(amount = 1) limits.any? { |limit| limit.exceeded?(amount) } end + # Return the first limit to be exceeded if incrementing by the given amount, + # or nil otherwise. Does not change amount used for any limit. + # + # @param amount [Integer] + # @return [TrafficJam::Limit, nil] def limit_exceeded(amount = 1) limits.each do |limit| limit_exceeded = limit.limit_exceeded(amount) @@ -64,15 +105,23 @@ def limit_exceeded(amount = 1) nil end + # Resets all limits to 0. def reset limits.each(&:reset) nil end + # Return minimum amount remaining of any limit. + # + # @return [Integer] amount remaining in limit group def remaining limits.map(&:remaining).min end + # Return flattened list of limit. Will return list limits even if this group + # contains nested limit groups. + # + # @return [Array] list of limits def flatten limits.map(&:flatten).flatten end diff --git a/traffic_jam.gemspec b/traffic_jam.gemspec index 8f7b24e..1e39d11 100644 --- a/traffic_jam.gemspec +++ b/traffic_jam.gemspec @@ -1,6 +1,6 @@ Gem::Specification.new do |s| s.name = "traffic_jam" - s.version = "0.2.0" + s.version = "1.0.0" s.licenses = ["MIT"] s.summary = "Library for time-based rate limiting" s.description = "Library for Redis-backed time-based rate limiting"