checksums.yaml ADDED
@@ -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
data/.gitignore ADDED
@@ -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
data/.travis.yml ADDED
@@ -0,0 +1,6 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.6.5
4
+ - 2.5.7
5
+ - 2.4.9
6
+ - jruby
data/CHANGELOG.md ADDED
@@ -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.
data/README.md ADDED
@@ -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.
data/Rakefile ADDED
@@ -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
data/exponential-backoff.gemspec ADDED
@@ -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
data/lib/exponential/backoff.rb ADDED
@@ -0,0 +1 @@
1
+ require 'exponential_backoff'
data/lib/exponential_backoff.rb ADDED
@@ -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
+
data/lib/exponential_backoff/version.rb ADDED
@@ -0,0 +1,3 @@
1
+ class ExponentialBackoff
2
+ VERSION = "0.0.3"
3
+ end
data/test/exponential_backoff_test.rb ADDED
@@ -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