exponential-backoff 0.0.3

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,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 3b3d8890e6844a79464767d236abf56bc79bc042b7cc326cc8b02b9ec5930c42
4
+ data.tar.gz: 36a5cbf1b5858ec4f0c8c751c6dd1cbc5f7287ba8aebb793104f824f87e73950
5
+ SHA512:
6
+ metadata.gz: 9d6580e6e8fa3af28e4432bc2f6614ecdc08547f73c30892056edec9c689157ca50425cadaf8281fa2cc546526318f3cb0c679d0a8d7718f390e76cb1f944f1b
7
+ data.tar.gz: 1dac51c82eee0325daaf8708bb867e28346c78a31c6438d5dccced7cc48e24390d6d381d57799527f97e4e4b3c04793075efe1fb4372560fd575bcfab7600d0d
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
@@ -0,0 +1,6 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.6.5
4
+ - 2.5.7
5
+ - 2.4.9
6
+ - jruby
@@ -0,0 +1,12 @@
1
+ 0.0.3
2
+
3
+ - Give bundler a chance to require this gem when listed in Gemfile
4
+
5
+ 0.0.2
6
+
7
+ - Initialize backoff with Array or Range as a convenience.
8
+ - Check if given iteration is one of intervals when running in a loop.
9
+
10
+ 0.0.1
11
+
12
+ - Initial version.
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in exponential_backoff.gemspec
4
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2012 Paweł Pacana
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,136 @@
1
+ Exponential Backoff
2
+ ===================
3
+
4
+ [![Build Status](https://secure.travis-ci.org/pawelpacana/exponential-backoff.png)](http://travis-ci.org/pawelpacana/exponential-backoff)
5
+
6
+ Too lazy to make retries to external services in a fashion that providers recommend? Never heard of [exponential backoff](http://en.wikipedia.org/wiki/Exponential_backoff) technique? Now there is no excuse not to be nice.
7
+
8
+ Installation
9
+ ------------
10
+
11
+ Add this line to your application's Gemfile:
12
+
13
+ ```ruby
14
+ gem 'exponential-backoff'
15
+ ```
16
+
17
+ And then execute:
18
+
19
+ ```
20
+ $ bundle
21
+ ```
22
+
23
+ Or install it yourself as:
24
+
25
+ ```
26
+ $ gem install exponential-backoff
27
+ ```
28
+
29
+ Usage
30
+ -----
31
+
32
+ Start with specifying minimal and maximal intervals, that is `4s` and `60s` respectively:
33
+
34
+ ```ruby
35
+ minimal_interval = 4.0
36
+ maximal_elapsed_time = 60.0
37
+
38
+ backoff = ExponentialBackoff.new(minimal_interval, maximal_elapsed_time)
39
+ ```
40
+
41
+ Arrays and ranges work for your convenience too:
42
+
43
+ ```ruby
44
+ backoff = ExponentialBackoff.new(minimal_interval..maximal_elapsed_time)
45
+ backoff = ExponentialBackoff.new([minimal_interval, maximal_elapsed_time])
46
+ ```
47
+
48
+ You can get intervals for specified range:
49
+
50
+ ```ruby
51
+ backoff.intervals_for(0..5) # [4.0, 8.0, 16.0, 32.0, 60.0, 60.0]
52
+ ```
53
+
54
+ Enumerate on them:
55
+
56
+ ```ruby
57
+ backoff.intervals.each do |interval|
58
+ sleep(interval)
59
+ end
60
+ ```
61
+
62
+ Or just get interval for requested, that is 3rd, iteration:
63
+
64
+ ```ruby
65
+ backoff.interval_at(3) # 32.0
66
+ ```
67
+
68
+ Intervals don't exceed maximal allowed time:
69
+
70
+ ```ruby
71
+ backoff.interval_at(20) # 60.0
72
+ ```
73
+
74
+ Backoff instance maintains state, you can ask for next interval...
75
+
76
+ ```ruby
77
+ backoff.next_interval # 4.0
78
+ backoff.next_interval # 8.0
79
+ ```
80
+
81
+ ...and reset it to start from beginning
82
+
83
+ ```ruby
84
+ backoff.clear
85
+ backoff.next_interval # 4.0
86
+ ```
87
+
88
+ Finally you can specify interval multiplier and randomization factor:
89
+
90
+ ```ruby
91
+ backoff = ExponentialBackoff.new(min_interval, max_elapsed)
92
+ backoff.multiplier = 1.5
93
+ backoff.randomize_factor = 0.25
94
+
95
+ backoff.intervals_for(0..2) # [3.764, 6.587, 9.76]
96
+ ```
97
+
98
+ You can peek what is the current interval:
99
+
100
+ ```ruby
101
+ backoff.current_interval # 3.764
102
+ ```
103
+
104
+ You can check if given iteration is one of intervals or maximum interval multiple:
105
+
106
+ ```ruby
107
+ backoff = ExponentialBackoff.new(1, 10)
108
+ backoff.iteration_active?(4) # true
109
+ backoff.iteration_active?(20) # true
110
+ backoff.iteration_active?(3) # false
111
+ ```
112
+
113
+ There is also sugar for executing block of code until successful with increasing intervals:
114
+
115
+ ```ruby
116
+ backoff.until_success do |interval, retry_count|
117
+ # do your thing
118
+ # when last line in block evaluates to true, elapsed time clear and loop breaks
119
+ # when false, increase interval and retry
120
+
121
+ # you can break loop earlier if you want,
122
+ # `nil` return value is considered a success
123
+ return if retry_count > 3
124
+ end
125
+ ```
126
+
127
+ Running tests
128
+ -------------
129
+
130
+ bundle exec rake test
131
+
132
+
133
+ Supported rubies
134
+ ----------------
135
+
136
+ Targets all Rubies (including Rubinius and JRuby) provided it's at least 1.9 mode.
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env rake
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rake/testtask"
5
+
6
+ Rake::TestTask.new do |t|
7
+ t.pattern = "test/*_test.rb"
8
+ end
9
+
10
+ desc "Run tests"
11
+ task :default => :test
@@ -0,0 +1,19 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require File.expand_path('../lib/exponential_backoff/version', __FILE__)
3
+
4
+ Gem::Specification.new do |gem|
5
+ gem.authors = ["Paweł Pacana"]
6
+ gem.email = ["pawel.pacana@gmail.com"]
7
+ gem.summary = %q{Exponential backoff algorithm for better reconnect intervals.}
8
+ gem.homepage = ""
9
+
10
+ gem.files = `git ls-files`.split($\)
11
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
12
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
13
+ gem.name = "exponential-backoff"
14
+ gem.require_paths = ["lib"]
15
+ gem.version = ExponentialBackoff::VERSION
16
+
17
+ gem.add_development_dependency 'rake'
18
+ gem.add_development_dependency 'test-unit'
19
+ end
@@ -0,0 +1 @@
1
+ require 'exponential_backoff'
@@ -0,0 +1,81 @@
1
+ require 'exponential_backoff/version'
2
+
3
+ class ExponentialBackoff
4
+ attr_accessor :multiplier, :randomize_factor
5
+ attr_reader :current_interval
6
+
7
+ def initialize(interval, maximum_elapsed_time = nil)
8
+ if interval.respond_to?(:first)
9
+ @minimal_interval, @maximum_elapsed_time = interval.first, interval.last
10
+ else
11
+ @minimal_interval, @maximum_elapsed_time = interval, maximum_elapsed_time
12
+ end
13
+ raise ArgumentError, "Invalid range specified" if [@minimal_interval, @maximum_elapsed_time].any? { |i| !i.is_a?(Numeric) }
14
+
15
+ @randomize_factor = 0
16
+ @multiplier = 2.0
17
+ clear
18
+ end
19
+
20
+ def clear
21
+ @enumerator = intervals
22
+ end
23
+
24
+ def next_interval
25
+ @current_interval = @enumerator.next
26
+ end
27
+
28
+ def iteration_active?(iteration)
29
+ index = 0
30
+ current_interval = interval_at(index)
31
+ while current_interval < iteration && current_interval < @maximum_elapsed_time
32
+ index += 1
33
+ current_interval = interval_at(index)
34
+ end
35
+ current_interval == iteration || iteration % @maximum_elapsed_time == 0
36
+ end
37
+
38
+ def intervals_for(range)
39
+ range.to_a.map { |iteration| interval_at(iteration) }
40
+ end
41
+
42
+ def interval_at(iteration)
43
+ randomized_interval(capped_interval(regular_interval(@minimal_interval, @multiplier, iteration)))
44
+ end
45
+
46
+ def intervals
47
+ Enumerator.new do |yielder|
48
+ iteration = 0
49
+ loop do
50
+ yielder.yield interval_at(iteration)
51
+ iteration += 1
52
+ end
53
+ end
54
+ end
55
+
56
+ def until_success(&block)
57
+ intervals.each_with_index do |interval, iteration|
58
+ retval = block.call(interval, iteration)
59
+ return if retval || retval.nil?
60
+ sleep(interval)
61
+ end
62
+ end
63
+
64
+ protected
65
+
66
+ def regular_interval(initial, multiplier, iteration)
67
+ initial * multiplier ** iteration
68
+ end
69
+
70
+ def randomized_interval(interval)
71
+ return interval if @randomize_factor == 0
72
+ min = (1 - @randomize_factor) * interval
73
+ max = (1 + @randomize_factor) * interval
74
+ rand(max - min) + min
75
+ end
76
+
77
+ def capped_interval(interval)
78
+ [@maximum_elapsed_time, interval].min
79
+ end
80
+ end
81
+
@@ -0,0 +1,3 @@
1
+ class ExponentialBackoff
2
+ VERSION = "0.0.3"
3
+ end
@@ -0,0 +1,189 @@
1
+ require 'test/unit'
2
+ require 'exponential_backoff'
3
+
4
+ class ExponentialBackoffTest < Test::Unit::TestCase
5
+ def test_range_initializer
6
+ backoff = ExponentialBackoff.new(1..5)
7
+ assert_equal [1, 2, 4, 5], backoff.intervals_for(0..3)
8
+ end
9
+
10
+ def test_array_initializer
11
+ backoff = ExponentialBackoff.new([1, 5])
12
+ assert_equal [1, 2, 4, 5], backoff.intervals_for(0..3)
13
+ end
14
+
15
+ def test_no_maximal_time
16
+ assert_raise ArgumentError do
17
+ ExponentialBackoff.new(2)
18
+ end
19
+ end
20
+
21
+ def test_multiplier_default
22
+ min, max = 1, 2
23
+ backoff = ExponentialBackoff.new(min, max)
24
+
25
+ assert_equal 2, backoff.multiplier
26
+ end
27
+
28
+ def test_randomize_factor_default
29
+ min, max = 1, 2
30
+ backoff = ExponentialBackoff.new(min, max)
31
+
32
+ assert_equal 0, backoff.randomize_factor
33
+ end
34
+
35
+ def test_next_interval
36
+ min, max = 1, 5
37
+ backoff = ExponentialBackoff.new(min, max)
38
+
39
+ assert_equal min, backoff.next_interval
40
+ assert_equal 2, backoff.next_interval
41
+ assert_equal 4, backoff.next_interval
42
+ assert_equal max, backoff.next_interval
43
+ end
44
+
45
+ def test_current_interval
46
+ min, max = 1, 5
47
+ backoff = ExponentialBackoff.new(min, max)
48
+
49
+ assert_nil backoff.current_interval
50
+ backoff.next_interval
51
+ assert_equal 1, backoff.current_interval
52
+ end
53
+
54
+ def test_clear
55
+ min, max = 1, 5
56
+ backoff = ExponentialBackoff.new(min, max)
57
+ 2.times { backoff.next_interval }
58
+ backoff.clear
59
+
60
+ assert_equal min, backoff.next_interval
61
+ end
62
+
63
+ def test_interval_at
64
+ min, max = 1, 5
65
+ backoff = ExponentialBackoff.new(min, max)
66
+
67
+ assert_equal 5, backoff.interval_at(3)
68
+ end
69
+
70
+ def test_intervals_for
71
+ min, max = 1, 5
72
+ backoff = ExponentialBackoff.new(min, max)
73
+
74
+ assert_equal [2, 4, 5, 5], backoff.intervals_for(1..4)
75
+ end
76
+
77
+ def test_intervals_is_enumerator
78
+ min, max = 1, 5
79
+ backoff = ExponentialBackoff.new(min, max)
80
+
81
+ assert_kind_of Enumerator, backoff.intervals
82
+ end
83
+
84
+ def test_intervals_enumerator_is_independent
85
+ min, max = 1, 5
86
+ backoff = ExponentialBackoff.new(min, max)
87
+ first, second = backoff.intervals, backoff.intervals
88
+ first.next
89
+
90
+ assert_not_equal first.next, second.next
91
+ end
92
+
93
+ def test_interval_is_float
94
+ min, max = 1, 5
95
+ backoff = ExponentialBackoff.new(min, max)
96
+
97
+ assert_kind_of Float, backoff.next_interval
98
+ end
99
+
100
+ def test_multiplier
101
+ min, max = 1, 512
102
+ backoff = ExponentialBackoff.new(min, max)
103
+ backoff.multiplier = 4
104
+
105
+ assert_equal [4, 16, 64, 256], backoff.intervals_for(1..4)
106
+ end
107
+
108
+ def test_randomize_factor
109
+ min, max = 1, 5
110
+ backoff = ExponentialBackoff.new(min, max)
111
+ backoff.randomize_factor = 0.25
112
+
113
+ 1_000.times do
114
+ assert_in_delta(0.75, 1.25, backoff.interval_at(0))
115
+ assert_in_delta(3.75, 6.25, backoff.interval_at(100))
116
+ end
117
+ end
118
+
119
+ def test_until_success_executor_stops_on_truthiness_return_value
120
+ min, max = 0.1, 0.5
121
+ backoff = ExponentialBackoff.new(min, max)
122
+
123
+ counter = 0
124
+ return_true = -> {
125
+ counter += 1
126
+ return true
127
+ }
128
+ backoff.until_success { return_true.call }
129
+ assert_equal 1, counter
130
+ end
131
+
132
+ def test_until_success_executor_continues_on_falsy_return_value
133
+ min, max = 0.1, 0.5
134
+ backoff = ExponentialBackoff.new(min, max)
135
+
136
+ counter = 0
137
+ return_false = -> {
138
+ counter += 1
139
+ return if counter > 1
140
+ return false
141
+ }
142
+ backoff.until_success { return_false.call }
143
+ assert_equal 2, counter
144
+ end
145
+
146
+ def test_until_success_block_params
147
+ min, max = 0.1, 0.5
148
+ backoff = ExponentialBackoff.new(min, max)
149
+
150
+ assert_block_params = proc do |interval, iteration|
151
+ assert_equal backoff.interval_at(iteration), interval
152
+ end
153
+
154
+ backoff.until_success { |interval, iteration| assert_block_params.call(interval, iteration) }
155
+ end
156
+
157
+ def test_until_success_executor_sleep_time
158
+ min, max = 0.1, 0.5
159
+ backoff = ExponentialBackoff.new(min, max)
160
+
161
+ counter = 0
162
+ return_false = -> {
163
+ counter += 1
164
+ return if counter > 2
165
+ return false
166
+ }
167
+
168
+ time = Time.now.to_f
169
+ backoff.until_success { return_false.call }
170
+ elapsed = Time.now.to_f - time
171
+ assert elapsed >= 0.3
172
+ end
173
+
174
+ def test_iteration_active_for_one_from_list
175
+ backoff = ExponentialBackoff.new(1, 10)
176
+ assert backoff.iteration_active?(4)
177
+ end
178
+
179
+ def test_iteration_active_for_maximum_interval_multiple
180
+ backoff = ExponentialBackoff.new(1, 10)
181
+ assert backoff.iteration_active?(20)
182
+ end
183
+
184
+ def test_iteration_active_for_not_from_list_and_not_multiple
185
+ backoff = ExponentialBackoff.new(1, 10)
186
+ refute backoff.iteration_active?(6)
187
+ end
188
+ end
189
+
metadata ADDED
@@ -0,0 +1,83 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: exponential-backoff
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.3
5
+ platform: ruby
6
+ authors:
7
+ - Paweł Pacana
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2019-11-08 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rake
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: test-unit
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ description:
42
+ email:
43
+ - pawel.pacana@gmail.com
44
+ executables: []
45
+ extensions: []
46
+ extra_rdoc_files: []
47
+ files:
48
+ - ".gitignore"
49
+ - ".travis.yml"
50
+ - CHANGELOG.md
51
+ - Gemfile
52
+ - LICENSE
53
+ - README.md
54
+ - Rakefile
55
+ - exponential-backoff.gemspec
56
+ - lib/exponential/backoff.rb
57
+ - lib/exponential_backoff.rb
58
+ - lib/exponential_backoff/version.rb
59
+ - test/exponential_backoff_test.rb
60
+ homepage: ''
61
+ licenses: []
62
+ metadata: {}
63
+ post_install_message:
64
+ rdoc_options: []
65
+ require_paths:
66
+ - lib
67
+ required_ruby_version: !ruby/object:Gem::Requirement
68
+ requirements:
69
+ - - ">="
70
+ - !ruby/object:Gem::Version
71
+ version: '0'
72
+ required_rubygems_version: !ruby/object:Gem::Requirement
73
+ requirements:
74
+ - - ">="
75
+ - !ruby/object:Gem::Version
76
+ version: '0'
77
+ requirements: []
78
+ rubygems_version: 3.0.4
79
+ signing_key:
80
+ specification_version: 4
81
+ summary: Exponential backoff algorithm for better reconnect intervals.
82
+ test_files:
83
+ - test/exponential_backoff_test.rb