traffic_jam 1.0.1 → 1.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/traffic_jam/errors.rb +3 -0
- data/lib/traffic_jam/lifetime_limit.rb +53 -0
- data/lib/traffic_jam/limit.rb +11 -9
- data/lib/traffic_jam/rolling_limit.rb +82 -0
- data/lib/traffic_jam/scripts.rb +8 -0
- data/lib/traffic_jam/simple_limit.rb +94 -0
- data/lib/traffic_jam.rb +3 -1
- data/scripts/incrby.lua +18 -0
- data/scripts/increment_rolling.lua +44 -0
- data/scripts/increment_simple.lua +17 -0
- data/scripts/sum_rolling.lua +32 -0
- metadata +10 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: dc24485cfcc470e68b80dbb53f6b135ab9879bf0
|
4
|
+
data.tar.gz: ab8ea5c0a5e1075af9606fae0e4a0cc9bb1fc071
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f428099b9b945e9196a4e54146a79af29d8e8e8c430320f1e9237be2e96df3b9f39e29cad2b78c53694f9c76e85ee698f38af91b23ab715339ab9f578eaa4434
|
7
|
+
data.tar.gz: 7c9afc9302a854fa68c042c9329e73a6224858bec71e25fb0ff5ac2a72fdcd6ab7f5aeedba4af936242a20e5fb96bc4fc87dcacdf9a77b3291f582adb5ddd540
|
data/lib/traffic_jam/errors.rb
CHANGED
@@ -0,0 +1,53 @@
|
|
1
|
+
module TrafficJam
|
2
|
+
# This class represents a lifetime limit on an action, value pair. For example, if
|
3
|
+
# limiting the amount of money a user can transfer, the action could be
|
4
|
+
# +:transfers+ and the value would be the user ID. The class exposes atomic
|
5
|
+
# increment operations and allows querying of the current amount used and
|
6
|
+
# amount remaining.
|
7
|
+
class LifetimeLimit < Limit
|
8
|
+
# Constructor takes an action name as a symbol, a maximum cap, and the
|
9
|
+
# period of limit. +max+ and +period+ are required keyword arguments.
|
10
|
+
#
|
11
|
+
# @param action [Symbol] action name
|
12
|
+
# @param value [String] limit target value
|
13
|
+
# @param max [Integer] required limit maximum
|
14
|
+
# @raise [ArgumentError] if max is nil
|
15
|
+
def initialize(action, value, max: nil)
|
16
|
+
super(action, value, max: max, period: -1)
|
17
|
+
end
|
18
|
+
|
19
|
+
# Increment the amount used by the given number. Does not perform increment
|
20
|
+
# if the operation would exceed the limit. Returns whether the operation was
|
21
|
+
# successful.
|
22
|
+
#
|
23
|
+
# @param amount [Integer] amount to increment by
|
24
|
+
# @return [Boolean] true if increment succeded and false if incrementing
|
25
|
+
# would exceed the limit
|
26
|
+
def increment(amount = 1, time: Time.now)
|
27
|
+
raise ArgumentError, 'Amount must be an integer' if amount != amount.to_i
|
28
|
+
return amount <= 0 if max.zero?
|
29
|
+
|
30
|
+
!!run_script([amount.to_i, max])
|
31
|
+
end
|
32
|
+
|
33
|
+
# Return amount of limit used
|
34
|
+
#
|
35
|
+
# @return [Integer] amount used
|
36
|
+
def used
|
37
|
+
return 0 if max.zero?
|
38
|
+
amount = redis.get(key) || 0
|
39
|
+
[amount.to_i, max].min
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
def run_script(argv)
|
45
|
+
redis.evalsha(
|
46
|
+
Scripts::INCRBY_HASH, keys: [key], argv: argv
|
47
|
+
)
|
48
|
+
rescue Redis::CommandError => error
|
49
|
+
raise error if /ERR Error running script/ =~ error.message
|
50
|
+
redis.eval(Scripts::INCRBY, keys: [key], argv: argv)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
data/lib/traffic_jam/limit.rb
CHANGED
@@ -33,8 +33,8 @@ module TrafficJam
|
|
33
33
|
# @param period [Integer] required limit period in seconds
|
34
34
|
# @raise [ArgumentError] if max or period is nil
|
35
35
|
def initialize(action, value, max: nil, period: nil)
|
36
|
-
raise ArgumentError('Max is required') if max.nil?
|
37
|
-
raise ArgumentError('Period is required') if period.nil?
|
36
|
+
raise ArgumentError.new('Max is required') if max.nil?
|
37
|
+
raise ArgumentError.new('Period is required') if period.nil?
|
38
38
|
@action, @value, @max, @period = action, value, max, period
|
39
39
|
end
|
40
40
|
|
@@ -71,14 +71,14 @@ module TrafficJam
|
|
71
71
|
raise ArgumentError.new("Amount must be an integer")
|
72
72
|
end
|
73
73
|
|
74
|
-
timestamp = (time.to_f * 1000).
|
74
|
+
timestamp = (time.to_f * 1000).to_i
|
75
75
|
argv = [timestamp, amount.to_i, max, period * 1000]
|
76
76
|
|
77
77
|
result =
|
78
78
|
begin
|
79
79
|
redis.evalsha(
|
80
80
|
Scripts::INCREMENT_SCRIPT_HASH, keys: [key], argv: argv)
|
81
|
-
rescue Redis::CommandError
|
81
|
+
rescue Redis::CommandError
|
82
82
|
redis.eval(Scripts::INCREMENT_SCRIPT, keys: [key], argv: argv)
|
83
83
|
end
|
84
84
|
|
@@ -127,9 +127,7 @@ module TrafficJam
|
|
127
127
|
def used
|
128
128
|
return 0 if max.zero?
|
129
129
|
|
130
|
-
|
131
|
-
timestamp = obj['timestamp']
|
132
|
-
amount = obj['amount']
|
130
|
+
timestamp, amount = redis.hmget(key, 'timestamp', 'amount')
|
133
131
|
if timestamp && amount
|
134
132
|
time_passed = Time.now.to_f - timestamp.to_i / 1000.0
|
135
133
|
drift = max * time_passed / period
|
@@ -161,7 +159,7 @@ module TrafficJam
|
|
161
159
|
end
|
162
160
|
|
163
161
|
def key
|
164
|
-
if @key.nil?
|
162
|
+
if !defined?(@key) || @key.nil?
|
165
163
|
converted_value =
|
166
164
|
begin
|
167
165
|
value.to_rate_limit_value
|
@@ -170,9 +168,13 @@ module TrafficJam
|
|
170
168
|
end
|
171
169
|
hash = Digest::MD5.base64digest(converted_value.to_s)
|
172
170
|
hash = hash[0...config.hash_length]
|
173
|
-
@key = "#{
|
171
|
+
@key = "#{key_prefix}:#{action}:#{hash}"
|
174
172
|
end
|
175
173
|
@key
|
176
174
|
end
|
175
|
+
|
176
|
+
def key_prefix
|
177
|
+
config.key_prefix
|
178
|
+
end
|
177
179
|
end
|
178
180
|
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
require_relative 'scripts'
|
2
|
+
|
3
|
+
module TrafficJam
|
4
|
+
# This class represents a rolling limit on an action, value pair. For example,
|
5
|
+
# if limiting the amount of money a user can transfer in a week, the action
|
6
|
+
# could be +:transfers+ and the value would be the user ID. The class exposes
|
7
|
+
# atomic increment operations and allows querying of the current amount used
|
8
|
+
# and amount remaining.
|
9
|
+
#
|
10
|
+
# This class also handles 0 for period, where 0 is no period (each
|
11
|
+
# request is compared to the max).
|
12
|
+
#
|
13
|
+
# This class departs from the design of Limit by tracking a sum of the actions
|
14
|
+
# in a second, in a hash keyed by the timestamp. Therefore, this limit can put
|
15
|
+
# a lot of data size pressure on the Redis storage, so use it wisely.
|
16
|
+
class RollingLimit < Limit
|
17
|
+
# Constructor takes an action name as a symbol, a maximum cap, and the
|
18
|
+
# period of limit. +max+ and +period+ are required keyword arguments.
|
19
|
+
#
|
20
|
+
# @param action [Symbol] action name
|
21
|
+
# @param value [String] limit target value
|
22
|
+
# @param max [Integer] required limit maximum
|
23
|
+
# @param period [Integer] required limit period in seconds
|
24
|
+
# @raise [ArgumentError] if max or period is nil
|
25
|
+
def initialize(action, value, max: nil, period: nil)
|
26
|
+
super(action, value, max: max, period: period)
|
27
|
+
end
|
28
|
+
|
29
|
+
# Increment the amount used by the given number. Rolls back the increment
|
30
|
+
# if the operation exceeds the limit. Returns whether the operation was
|
31
|
+
# successful. Time of increment can be specified optionally with a keyword
|
32
|
+
# argument, which is not really useful since it be undone by used.
|
33
|
+
#
|
34
|
+
# @param amount [Integer] amount to increment by
|
35
|
+
# @param time [Time] time when increment occurs (ignored)
|
36
|
+
# @return [Boolean] true if increment succeded and false if incrementing
|
37
|
+
# would exceed the limit
|
38
|
+
def increment(amount = 1, time: Time.now)
|
39
|
+
raise ArgumentError, 'Amount must be an integer' if amount != amount.to_i
|
40
|
+
return amount <= 0 if max.zero?
|
41
|
+
return amount <= max if period.zero?
|
42
|
+
return true if amount.zero?
|
43
|
+
return false if amount > max
|
44
|
+
|
45
|
+
!run_incr([time.to_i, amount.to_i, max, period]).nil?
|
46
|
+
end
|
47
|
+
|
48
|
+
# Return amount of limit used
|
49
|
+
#
|
50
|
+
# @return [Integer] amount used
|
51
|
+
def used
|
52
|
+
return 0 if max.zero? || period.zero?
|
53
|
+
[sum, max].min
|
54
|
+
end
|
55
|
+
|
56
|
+
private
|
57
|
+
|
58
|
+
def sum
|
59
|
+
run_sum([Time.now.to_i, period])
|
60
|
+
end
|
61
|
+
|
62
|
+
def clear_before
|
63
|
+
Time.now.to_i - period
|
64
|
+
end
|
65
|
+
|
66
|
+
def run_sum(argv)
|
67
|
+
redis.evalsha(Scripts::SUM_ROLLING_HASH, keys: [key], argv: argv)
|
68
|
+
rescue Redis::CommandError => error
|
69
|
+
raise error if /ERR Error running script/ =~ error.message
|
70
|
+
redis.eval(Scripts::SUM_ROLLING, keys: [key], argv: argv)
|
71
|
+
end
|
72
|
+
|
73
|
+
def run_incr(argv)
|
74
|
+
redis.evalsha(
|
75
|
+
Scripts::INCREMENT_ROLLING_HASH, keys: [key], argv: argv
|
76
|
+
)
|
77
|
+
rescue Redis::CommandError => error
|
78
|
+
raise error if /ERR Error running script/ =~ error.message
|
79
|
+
redis.eval(Scripts::INCREMENT_ROLLING, keys: [key], argv: argv)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
data/lib/traffic_jam/scripts.rb
CHANGED
@@ -10,5 +10,13 @@ module TrafficJam
|
|
10
10
|
|
11
11
|
INCREMENT_SCRIPT = load('increment')
|
12
12
|
INCREMENT_SCRIPT_HASH = Digest::SHA1.hexdigest(INCREMENT_SCRIPT)
|
13
|
+
INCREMENT_SIMPLE = load('increment_simple')
|
14
|
+
INCREMENT_SIMPLE_HASH = Digest::SHA1.hexdigest(INCREMENT_SIMPLE)
|
15
|
+
INCREMENT_ROLLING = load('increment_rolling')
|
16
|
+
INCREMENT_ROLLING_HASH = Digest::SHA1.hexdigest(INCREMENT_ROLLING)
|
17
|
+
INCRBY = load('incrby')
|
18
|
+
INCRBY_HASH = Digest::SHA1.hexdigest(INCRBY)
|
19
|
+
SUM_ROLLING = load('sum_rolling')
|
20
|
+
SUM_ROLLING_HASH = Digest::SHA1.hexdigest(SUM_ROLLING)
|
13
21
|
end
|
14
22
|
end
|
@@ -0,0 +1,94 @@
|
|
1
|
+
require_relative 'limit'
|
2
|
+
require_relative 'scripts'
|
3
|
+
|
4
|
+
module TrafficJam
|
5
|
+
# A SimpleLimit is a limit type that is more efficient for increments but does
|
6
|
+
# not support decrements or changing the max value without a complete reset.
|
7
|
+
# This means that if the period or max value for an action, value key changes,
|
8
|
+
# the used and remaining values cannot be preserved.
|
9
|
+
#
|
10
|
+
# This works by storing a key in Redis with a millisecond-precision expiry
|
11
|
+
# representing the time that the limit will be completely reset. Each
|
12
|
+
# increment operation converts the increment amount into the number of
|
13
|
+
# milliseconds to be added to the expiry.
|
14
|
+
#
|
15
|
+
# Example: Limit is 5 per 10 seconds.
|
16
|
+
# An increment by 1 first sets the key to expire in 2s.
|
17
|
+
# Another immediate increment by 4 sets the expiry to 10s.
|
18
|
+
# Subsequent increments fail until clock time catches up to expiry
|
19
|
+
class SimpleLimit < Limit
|
20
|
+
# Increment the amount used by the given number. Does not perform increment
|
21
|
+
# if the operation would exceed the limit. Returns whether the operation was
|
22
|
+
# successful.
|
23
|
+
#
|
24
|
+
# @param amount [Integer] amount to increment by
|
25
|
+
# @param time [Time] time is ignored
|
26
|
+
# @return [Boolean] true if increment succeded and false if incrementing
|
27
|
+
# would exceed the limit
|
28
|
+
def increment(amount = 1, time: Time.now)
|
29
|
+
return true if amount == 0
|
30
|
+
return false if max == 0
|
31
|
+
raise ArgumentError.new("Amount must be positive") if amount < 0
|
32
|
+
|
33
|
+
if amount != amount.to_i
|
34
|
+
raise ArgumentError.new("Amount must be an integer")
|
35
|
+
end
|
36
|
+
|
37
|
+
return false if amount > max
|
38
|
+
|
39
|
+
incrby = (period * 1000 * amount / max).to_i
|
40
|
+
argv = [incrby, period * 1000]
|
41
|
+
|
42
|
+
result =
|
43
|
+
begin
|
44
|
+
redis.evalsha(
|
45
|
+
Scripts::INCREMENT_SIMPLE_HASH, keys: [key], argv: argv)
|
46
|
+
rescue Redis::CommandError
|
47
|
+
redis.eval(Scripts::INCREMENT_SIMPLE, keys: [key], argv: argv)
|
48
|
+
end
|
49
|
+
|
50
|
+
case result
|
51
|
+
when 0
|
52
|
+
return true
|
53
|
+
when -1
|
54
|
+
raise Errors::InvalidKeyError, "Redis key #{key} has no expire time set"
|
55
|
+
when -2
|
56
|
+
return false
|
57
|
+
else
|
58
|
+
raise Errors::UnknownReturnValue,
|
59
|
+
"Received unexpected return value #{result} from " \
|
60
|
+
"increment_simple eval"
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
# Decrement the amount used by the given number.
|
65
|
+
#
|
66
|
+
# @param amount [Integer] amount to decrement by
|
67
|
+
# @param time [Time] time is ignored
|
68
|
+
# @raise [NotImplementedError] decrement is not defined for SimpleLimit
|
69
|
+
def decrement(_amount = 1, time: Time.now)
|
70
|
+
raise NotImplementedError, "decrement is not defined for SimpleLimit"
|
71
|
+
end
|
72
|
+
|
73
|
+
# Return amount of limit used, taking time drift into account.
|
74
|
+
#
|
75
|
+
# @return [Integer] amount used
|
76
|
+
def used
|
77
|
+
return 0 if max.zero?
|
78
|
+
|
79
|
+
expiry = redis.pttl(key)
|
80
|
+
case expiry
|
81
|
+
when -1 # key exists but has no associated expire
|
82
|
+
raise Errors::InvalidKeyError, "Redis key #{key} has no expire time set"
|
83
|
+
when -2 # key does not exist
|
84
|
+
return 0
|
85
|
+
end
|
86
|
+
|
87
|
+
(max * expiry / (period * 1000.0)).ceil
|
88
|
+
end
|
89
|
+
|
90
|
+
def key_prefix
|
91
|
+
"#{config.key_prefix}:s"
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
data/lib/traffic_jam.rb
CHANGED
@@ -4,7 +4,9 @@ require_relative 'traffic_jam/errors'
|
|
4
4
|
require_relative 'traffic_jam/configuration'
|
5
5
|
require_relative 'traffic_jam/limit'
|
6
6
|
require_relative 'traffic_jam/limit_group'
|
7
|
-
|
7
|
+
require_relative 'traffic_jam/simple_limit'
|
8
|
+
require_relative 'traffic_jam/rolling_limit'
|
9
|
+
require_relative 'traffic_jam/lifetime_limit'
|
8
10
|
|
9
11
|
module TrafficJam
|
10
12
|
include Errors
|
data/scripts/incrby.lua
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
local arg_amount = tonumber(ARGV[1])
|
2
|
+
local arg_max = tonumber(ARGV[2])
|
3
|
+
|
4
|
+
local old_amount = tonumber(redis.call("GET", KEYS[1]))
|
5
|
+
local new_amount
|
6
|
+
|
7
|
+
if not old_amount then
|
8
|
+
new_amount = arg_amount
|
9
|
+
else
|
10
|
+
new_amount = old_amount + arg_amount
|
11
|
+
|
12
|
+
if new_amount > arg_max then
|
13
|
+
return false
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
redis.call("INCRBY", KEYS[1], arg_amount)
|
18
|
+
return true
|
@@ -0,0 +1,44 @@
|
|
1
|
+
-- gets all fields from a hash as a dictionary
|
2
|
+
local hgetall = function (key)
|
3
|
+
local bulk = redis.call('HGETALL', key)
|
4
|
+
local result = {}
|
5
|
+
local nextkey
|
6
|
+
for i, v in ipairs(bulk) do
|
7
|
+
if i % 2 == 1 then
|
8
|
+
nextkey = v
|
9
|
+
else
|
10
|
+
result[nextkey] = v
|
11
|
+
end
|
12
|
+
end
|
13
|
+
return result
|
14
|
+
end
|
15
|
+
|
16
|
+
local arg_timestamp = tonumber(ARGV[1])
|
17
|
+
local arg_amount = tonumber(ARGV[2])
|
18
|
+
local arg_max = tonumber(ARGV[3])
|
19
|
+
local arg_period = tonumber(ARGV[4])
|
20
|
+
|
21
|
+
local sum = arg_amount
|
22
|
+
|
23
|
+
local clear_before = arg_timestamp - arg_period
|
24
|
+
local mytable = hgetall(KEYS[1])
|
25
|
+
if mytable ~= false then
|
26
|
+
-- print key -> value for mytable
|
27
|
+
print('mytable:')
|
28
|
+
for key, val in pairs(mytable) do
|
29
|
+
print(' ' .. key .. ' -> ' .. val)
|
30
|
+
if tonumber(key) < clear_before then
|
31
|
+
redis.call('HDEL', KEYS[1], key)
|
32
|
+
else
|
33
|
+
sum = sum + tonumber(val)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
if sum > arg_max then
|
39
|
+
return false
|
40
|
+
end
|
41
|
+
|
42
|
+
redis.call("HINCRBY", KEYS[1], arg_timestamp, arg_amount)
|
43
|
+
redis.call("EXPIRE", KEYS[1], arg_period)
|
44
|
+
return true
|
@@ -0,0 +1,17 @@
|
|
1
|
+
local arg_incrby = tonumber(ARGV[1])
|
2
|
+
local arg_max = tonumber(ARGV[2])
|
3
|
+
|
4
|
+
local old_value = redis.call("PTTL", KEYS[1])
|
5
|
+
if old_value == -1 then -- key exists but has no associated expire
|
6
|
+
return -1 -- -1 signals key exists but has no associated expire
|
7
|
+
elseif old_value == -2 then -- key does not exist
|
8
|
+
redis.call("SET", KEYS[1], "", "PX", arg_incrby)
|
9
|
+
else
|
10
|
+
local new_value = old_value + arg_incrby
|
11
|
+
if new_value > arg_max then
|
12
|
+
return -2 -- -2 signals increment exceeds max
|
13
|
+
end
|
14
|
+
redis.call("PEXPIRE", KEYS[1], new_value)
|
15
|
+
end
|
16
|
+
|
17
|
+
return 0 -- 0 signals success
|
@@ -0,0 +1,32 @@
|
|
1
|
+
-- gets all fields from a hash as a dictionary
|
2
|
+
local hgetall = function (key)
|
3
|
+
local bulk = redis.call('HGETALL', key)
|
4
|
+
local result = {}
|
5
|
+
local nextkey
|
6
|
+
for i, v in ipairs(bulk) do
|
7
|
+
if i % 2 == 1 then
|
8
|
+
nextkey = v
|
9
|
+
else
|
10
|
+
result[nextkey] = v
|
11
|
+
end
|
12
|
+
end
|
13
|
+
return result
|
14
|
+
end
|
15
|
+
|
16
|
+
local arg_timestamp = tonumber(ARGV[1])
|
17
|
+
local arg_period = tonumber(ARGV[2])
|
18
|
+
local sum = 0
|
19
|
+
|
20
|
+
local clear_before = arg_timestamp - arg_period
|
21
|
+
local mytable = hgetall(KEYS[1])
|
22
|
+
if mytable ~= false then
|
23
|
+
for key, val in pairs(mytable) do
|
24
|
+
if tonumber(key) < clear_before then
|
25
|
+
redis.call('HDEL', KEYS[1], key)
|
26
|
+
else
|
27
|
+
sum = sum + tonumber(val)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
return sum
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: traffic_jam
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.0
|
4
|
+
version: 1.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Jim Posen
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2018-01-09 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: redis
|
@@ -47,10 +47,17 @@ files:
|
|
47
47
|
- lib/traffic_jam.rb
|
48
48
|
- lib/traffic_jam/configuration.rb
|
49
49
|
- lib/traffic_jam/errors.rb
|
50
|
+
- lib/traffic_jam/lifetime_limit.rb
|
50
51
|
- lib/traffic_jam/limit.rb
|
51
52
|
- lib/traffic_jam/limit_group.rb
|
53
|
+
- lib/traffic_jam/rolling_limit.rb
|
52
54
|
- lib/traffic_jam/scripts.rb
|
55
|
+
- lib/traffic_jam/simple_limit.rb
|
56
|
+
- scripts/incrby.lua
|
53
57
|
- scripts/increment.lua
|
58
|
+
- scripts/increment_rolling.lua
|
59
|
+
- scripts/increment_simple.lua
|
60
|
+
- scripts/sum_rolling.lua
|
54
61
|
homepage: https://github.com/coinbase/traffic_jam
|
55
62
|
licenses:
|
56
63
|
- MIT
|
@@ -71,9 +78,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
71
78
|
version: '0'
|
72
79
|
requirements: []
|
73
80
|
rubyforge_project:
|
74
|
-
rubygems_version: 2.
|
81
|
+
rubygems_version: 2.6.13
|
75
82
|
signing_key:
|
76
83
|
specification_version: 4
|
77
84
|
summary: Library for time-based rate limiting
|
78
85
|
test_files: []
|
79
|
-
has_rdoc:
|