r4r 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.circleci/config.yml +110 -0
- data/.gitignore +12 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +39 -0
- data/LICENSE.txt +21 -0
- data/README.md +15 -0
- data/Rakefile +38 -0
- data/bin/console +14 -0
- data/bin/ghpages +19 -0
- data/bin/rake +2 -0
- data/bin/setup +9 -0
- data/ext/r4r/ring_bits_ext/extconf.rb +7 -0
- data/ext/r4r/ring_bits_ext/ring_bits_ext.c +173 -0
- data/ext/r4r/system_clock_ext/extconf.rb +8 -0
- data/ext/r4r/system_clock_ext/system_clock_ext.c +31 -0
- data/lib/r4r.rb +17 -0
- data/lib/r4r/clock.rb +40 -0
- data/lib/r4r/retry.rb +136 -0
- data/lib/r4r/retry_budget.rb +150 -0
- data/lib/r4r/retry_policy.rb +55 -0
- data/lib/r4r/ring_bits.rb +59 -0
- data/lib/r4r/token_bucket.rb +129 -0
- data/lib/r4r/version.rb +3 -0
- data/lib/r4r/windowed_adder.rb +106 -0
- data/r4r.gemspec +43 -0
- metadata +170 -0
@@ -0,0 +1,129 @@
|
|
1
|
+
module R4r
|
2
|
+
# A token bucket is used to control the relative rates of two
|
3
|
+
# processes: one fills the bucket, another empties it.
|
4
|
+
#
|
5
|
+
# A Ruby port of the finagle's TokenBucket.
|
6
|
+
#
|
7
|
+
# @abstract
|
8
|
+
# @see https://github.com/twitter/util/blob/master/util-core/src/main/scala/com/twitter/util/TokenBucket.scala
|
9
|
+
class TokenBucket
|
10
|
+
# Put `n` tokens into the bucket.
|
11
|
+
#
|
12
|
+
# @param [Fixnum] n the number of tokens to remove from the bucket.
|
13
|
+
# Must be >= 0.
|
14
|
+
def put(n)
|
15
|
+
raise NotImplementedError
|
16
|
+
end
|
17
|
+
|
18
|
+
# Try to get `n` tokens out of the bucket.
|
19
|
+
#
|
20
|
+
# @param [Fixnum] n the number of tokens to remove from the bucket.
|
21
|
+
# Must be >= 0.
|
22
|
+
# @return [Boolean] true if successful
|
23
|
+
def try_get(n)
|
24
|
+
raise NotImplementedError
|
25
|
+
end
|
26
|
+
|
27
|
+
# The number of tokens currently in the bucket.
|
28
|
+
def count
|
29
|
+
raise NotImplementedError
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
# A token bucket that doesn't exceed a given bound.
|
34
|
+
#
|
35
|
+
# This is threadsafe, and does not require further synchronization.
|
36
|
+
# The token bucket starts empty.
|
37
|
+
class BoundedTokenBucket < TokenBucket
|
38
|
+
|
39
|
+
# Creates a new {R4r::BoundedTokenBucket}.
|
40
|
+
#
|
41
|
+
# @param [Fixnum] limit the upper bound on the number of tokens in the bucket.
|
42
|
+
# @raise [ArgumentError] if limit isn't positive
|
43
|
+
def initialize(limit:)
|
44
|
+
raise ArgumentError, "limit must be positive, got #{limit}" if limit.to_i <= 0
|
45
|
+
|
46
|
+
@limit = limit.to_i
|
47
|
+
@counter = 0
|
48
|
+
end
|
49
|
+
|
50
|
+
# Put `n` tokens into the bucket.
|
51
|
+
#
|
52
|
+
# If putting in `n` tokens would overflow `limit` tokens, instead sets the
|
53
|
+
# number of tokens to be `limit`.
|
54
|
+
#
|
55
|
+
# @raise [ArgumentError] is n isn't positive
|
56
|
+
def put(n)
|
57
|
+
n = n.to_i
|
58
|
+
raise ArgumentError, "number of tokens must be positive" if n <= 0
|
59
|
+
|
60
|
+
@counter = [@counter + n, @limit].min
|
61
|
+
end
|
62
|
+
|
63
|
+
# @see R4r::TokenBucket#try_get
|
64
|
+
def try_get(n)
|
65
|
+
n = n.to_i
|
66
|
+
raise ArgumentError, "number of tokens must be positive" if n <= 0
|
67
|
+
|
68
|
+
ok = @counter >= n
|
69
|
+
|
70
|
+
if ok
|
71
|
+
@counter -= n
|
72
|
+
end
|
73
|
+
|
74
|
+
ok
|
75
|
+
end
|
76
|
+
|
77
|
+
# @see R4r::TokenBucket#count
|
78
|
+
def count
|
79
|
+
@counter
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
# A leaky bucket expires tokens after approximately `ttl` time.
|
84
|
+
# Thus, a bucket left alone will empty itself.
|
85
|
+
class LeakyTokenBucket < TokenBucket
|
86
|
+
|
87
|
+
# Creates a new [R4r::LeakyTokenBucket]
|
88
|
+
#
|
89
|
+
# @param [Fixnum] ttl_ms the (approximate) time in milliseconds after which a token will
|
90
|
+
# expire.
|
91
|
+
# @param [Fixnum] reserve the number of reserve tokens over the TTL
|
92
|
+
# period. That is, every `ttl` has `reserve` tokens in addition to
|
93
|
+
# the ones added to the bucket.
|
94
|
+
# @param [R4r::Clock] clock the current time
|
95
|
+
def initialize(ttl_ms:, reserve:, clock: nil)
|
96
|
+
@ttl_ms = ttl_ms.to_i
|
97
|
+
@reserve = reserve.to_i
|
98
|
+
@clock = (clock || R4r.clock)
|
99
|
+
@window = R4r::WindowedAdder.new(range_ms: ttl_ms, slices: 10, clock: @clock)
|
100
|
+
end
|
101
|
+
|
102
|
+
# @see R4r::TokenBucket#put
|
103
|
+
def put(n)
|
104
|
+
n = n.to_i
|
105
|
+
raise ArgumentError, "n cannot be nagative" unless n >= 0
|
106
|
+
|
107
|
+
@window.add(n)
|
108
|
+
end
|
109
|
+
|
110
|
+
# @see R4r::TokenBucket#try_get
|
111
|
+
def try_get(n)
|
112
|
+
n = n.to_i
|
113
|
+
raise ArgumentError, "n cannot be nagative" unless n >= 0
|
114
|
+
|
115
|
+
ok = count >= n
|
116
|
+
if ok
|
117
|
+
@window.add(n * -1)
|
118
|
+
end
|
119
|
+
|
120
|
+
ok
|
121
|
+
end
|
122
|
+
|
123
|
+
# @see R4r::TokenBucket#count
|
124
|
+
def count
|
125
|
+
@window.sum + @reserve
|
126
|
+
end
|
127
|
+
|
128
|
+
end
|
129
|
+
end
|
data/lib/r4r/version.rb
ADDED
@@ -0,0 +1,106 @@
|
|
1
|
+
# require 'concurrent/thread_safe/util/adder'
|
2
|
+
# require 'concurrent/atomic/atomic_fixnum'
|
3
|
+
|
4
|
+
module R4r
|
5
|
+
|
6
|
+
# A Ruby port of the finagle's WindowedAdder.
|
7
|
+
#
|
8
|
+
# @see https://github.com/twitter/util/blob/master/util-core/src/main/scala/com/twitter/util/WindowedAdder.scala
|
9
|
+
# @see https://github.com/ruby-concurrency/concurrent-ruby/blob/master/lib/concurrent/thread_safe/util/adder.rb
|
10
|
+
class WindowedAdder
|
11
|
+
|
12
|
+
# Creates a time-windowed version of a {Concurrent::ThreadSafe::Util::Adder].
|
13
|
+
#
|
14
|
+
# @param [Fixnum] range_ms the range of time in millisecods to be kept in the adder.
|
15
|
+
# @param [Fixnum] slices the number of slices that are maintained; a higher
|
16
|
+
# number of slices means finer granularity but also more memory
|
17
|
+
# consumption. Must be more than 1.
|
18
|
+
# @param [R4r::Clock] clock the current time. for testing.
|
19
|
+
#
|
20
|
+
# @raise [ArgumentError] if slices is less then 1
|
21
|
+
# @raise [ArgumentError] if range is nil
|
22
|
+
# @raise [ArgumentError] if slices is nil
|
23
|
+
def initialize(range_ms:, slices:, clock: nil)
|
24
|
+
raise ArgumentError, "range_ms cannot be nil" if range_ms.nil?
|
25
|
+
raise ArgumentError, "slices cannot be nil" if slices.nil?
|
26
|
+
raise ArgumentError, "slices must be positive" if slices.to_i <= 1
|
27
|
+
|
28
|
+
@window = range_ms.to_i / slices.to_i
|
29
|
+
@slices = slices.to_i - 1
|
30
|
+
@writer = 0 #::Concurrent::ThreadSafe::Util::Adder.new
|
31
|
+
@gen = 0
|
32
|
+
@expired_gen = 0 #::Concurrent::AtomicFixnum.new(@gen)
|
33
|
+
@buf = Array.new(@slices) { 0 }
|
34
|
+
@index = 0
|
35
|
+
@now = (clock || R4r.clock)
|
36
|
+
@old = @now.call
|
37
|
+
end
|
38
|
+
|
39
|
+
# Reset the state of the adder.
|
40
|
+
def reset
|
41
|
+
@buf.fill(0, @slices) { 0 }
|
42
|
+
@writer = 0
|
43
|
+
@old = @now.call
|
44
|
+
end
|
45
|
+
|
46
|
+
# Increment the adder by 1
|
47
|
+
def incr
|
48
|
+
add(1)
|
49
|
+
end
|
50
|
+
|
51
|
+
# Increment the adder by `x`
|
52
|
+
def add(x)
|
53
|
+
expired if (@now.call - @old) >= @window
|
54
|
+
|
55
|
+
@writer += x
|
56
|
+
end
|
57
|
+
|
58
|
+
# Retrieve the current sum of the adder
|
59
|
+
#
|
60
|
+
# @return [Fixnum]
|
61
|
+
def sum
|
62
|
+
expired if (@now.call - @old) >= @window
|
63
|
+
|
64
|
+
value = @writer
|
65
|
+
i = 0
|
66
|
+
while i < @slices
|
67
|
+
value += @buf[i]
|
68
|
+
i += 1
|
69
|
+
end
|
70
|
+
|
71
|
+
value
|
72
|
+
end
|
73
|
+
|
74
|
+
private
|
75
|
+
|
76
|
+
def expired
|
77
|
+
# return unless @expired_gen.compare_and_set(@gen, @gen + 1)
|
78
|
+
|
79
|
+
# At the time of add, we were likely up to date,
|
80
|
+
# so we credit it to the current slice.
|
81
|
+
@buf[@index] = sum_and_reset
|
82
|
+
@index = (@index + 1) % @slices
|
83
|
+
|
84
|
+
# If it turns out we've skipped a number of
|
85
|
+
# slices, we adjust for that here.
|
86
|
+
nskip = [((@now.call - @old) / @window) - 1, @slices].min
|
87
|
+
|
88
|
+
if nskip > 0
|
89
|
+
r = [nskip, @slices - @index].min
|
90
|
+
@buf.fill(@index, r) { 0 }
|
91
|
+
@buf.fill(0, nskip - r) { 0 }
|
92
|
+
@index = (@index + nskip) % @slices
|
93
|
+
end
|
94
|
+
|
95
|
+
@old = @now.call
|
96
|
+
@gen += 1
|
97
|
+
end
|
98
|
+
|
99
|
+
def sum_and_reset
|
100
|
+
sum = @writer
|
101
|
+
@writer = 0
|
102
|
+
sum
|
103
|
+
end
|
104
|
+
|
105
|
+
end
|
106
|
+
end
|
data/r4r.gemspec
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path("../lib", __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require "r4r/version"
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "r4r"
|
8
|
+
spec.version = R4r::VERSION
|
9
|
+
spec.authors = ["Dmitry Galinsky"]
|
10
|
+
spec.email = ["dima.exe@gmail.com"]
|
11
|
+
|
12
|
+
spec.summary = %q{Write a short summary, because Rubygems requires one.}
|
13
|
+
spec.description = %q{Write a longer description or delete this line.}
|
14
|
+
spec.homepage = "https://github.com"
|
15
|
+
spec.license = "MIT"
|
16
|
+
|
17
|
+
# Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host'
|
18
|
+
# to allow pushing to a single host or delete this section to allow pushing to any host.
|
19
|
+
if spec.respond_to?(:metadata)
|
20
|
+
spec.metadata["allowed_push_host"] = "https://rubygems.org"
|
21
|
+
else
|
22
|
+
raise "RubyGems 2.0 or newer is required to protect against " \
|
23
|
+
"public gem pushes."
|
24
|
+
end
|
25
|
+
|
26
|
+
spec.files = `git ls-files -z`.split("\x0").reject do |f|
|
27
|
+
f.match(%r{^(test|spec|features)/})
|
28
|
+
end
|
29
|
+
spec.bindir = "exe"
|
30
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
31
|
+
spec.require_paths = ["lib"]
|
32
|
+
spec.extensions = Dir.glob("ext/r4r/*/extconf.rb")
|
33
|
+
|
34
|
+
# spec.add_dependency "concurrent-ruby", "~> 1.0"
|
35
|
+
|
36
|
+
spec.add_development_dependency "bundler", "~> 1.15"
|
37
|
+
spec.add_development_dependency "rake", "~> 11.0"
|
38
|
+
spec.add_development_dependency "minitest", "~> 5.0"
|
39
|
+
spec.add_development_dependency "minitest-reporters", "~> 1.1"
|
40
|
+
spec.add_development_dependency "rake-compiler", "~> 1.0"
|
41
|
+
spec.add_development_dependency "minitest-ci", "~> 3.3"
|
42
|
+
spec.add_development_dependency "yard", "~> 0.9"
|
43
|
+
end
|
metadata
ADDED
@@ -0,0 +1,170 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: r4r
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Dmitry Galinsky
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2018-03-20 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bundler
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.15'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.15'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rake
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '11.0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '11.0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: minitest
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '5.0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '5.0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: minitest-reporters
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '1.1'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '1.1'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: rake-compiler
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '1.0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '1.0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: minitest-ci
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - "~>"
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '3.3'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - "~>"
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '3.3'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: yard
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - "~>"
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '0.9'
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - "~>"
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0.9'
|
111
|
+
description: Write a longer description or delete this line.
|
112
|
+
email:
|
113
|
+
- dima.exe@gmail.com
|
114
|
+
executables: []
|
115
|
+
extensions:
|
116
|
+
- ext/r4r/ring_bits_ext/extconf.rb
|
117
|
+
- ext/r4r/system_clock_ext/extconf.rb
|
118
|
+
extra_rdoc_files: []
|
119
|
+
files:
|
120
|
+
- ".circleci/config.yml"
|
121
|
+
- ".gitignore"
|
122
|
+
- Gemfile
|
123
|
+
- Gemfile.lock
|
124
|
+
- LICENSE.txt
|
125
|
+
- README.md
|
126
|
+
- Rakefile
|
127
|
+
- bin/console
|
128
|
+
- bin/ghpages
|
129
|
+
- bin/rake
|
130
|
+
- bin/setup
|
131
|
+
- ext/r4r/ring_bits_ext/extconf.rb
|
132
|
+
- ext/r4r/ring_bits_ext/ring_bits_ext.c
|
133
|
+
- ext/r4r/system_clock_ext/extconf.rb
|
134
|
+
- ext/r4r/system_clock_ext/system_clock_ext.c
|
135
|
+
- lib/r4r.rb
|
136
|
+
- lib/r4r/clock.rb
|
137
|
+
- lib/r4r/retry.rb
|
138
|
+
- lib/r4r/retry_budget.rb
|
139
|
+
- lib/r4r/retry_policy.rb
|
140
|
+
- lib/r4r/ring_bits.rb
|
141
|
+
- lib/r4r/token_bucket.rb
|
142
|
+
- lib/r4r/version.rb
|
143
|
+
- lib/r4r/windowed_adder.rb
|
144
|
+
- r4r.gemspec
|
145
|
+
homepage: https://github.com
|
146
|
+
licenses:
|
147
|
+
- MIT
|
148
|
+
metadata:
|
149
|
+
allowed_push_host: https://rubygems.org
|
150
|
+
post_install_message:
|
151
|
+
rdoc_options: []
|
152
|
+
require_paths:
|
153
|
+
- lib
|
154
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
155
|
+
requirements:
|
156
|
+
- - ">="
|
157
|
+
- !ruby/object:Gem::Version
|
158
|
+
version: '0'
|
159
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
160
|
+
requirements:
|
161
|
+
- - ">="
|
162
|
+
- !ruby/object:Gem::Version
|
163
|
+
version: '0'
|
164
|
+
requirements: []
|
165
|
+
rubyforge_project:
|
166
|
+
rubygems_version: 2.4.5.3
|
167
|
+
signing_key:
|
168
|
+
specification_version: 4
|
169
|
+
summary: Write a short summary, because Rubygems requires one.
|
170
|
+
test_files: []
|