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.
@@ -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: []