r4r 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -0,0 +1,3 @@
1
+ module R4r
2
+ VERSION = "0.1.0"
3
+ end
@@ -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
@@ -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: []