exponential-backoff 0.0.1

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,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,10 @@
1
+ language: ruby
2
+ script: ruby -Ilib -Itest test/exponential_backoff_test.rb
3
+ rvm:
4
+ - 1.9.3
5
+ - 1.9.2
6
+ - ruby-head
7
+ - jruby-19mode
8
+ - jruby-head
9
+ - rbx-19mode
10
+
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,120 @@
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
+ You can get intervals for specified range:
42
+
43
+ ```ruby
44
+ backoff.intervals_for(0..5) # [4.0, 8.0, 16.0, 32.0, 60.0, 60.0]
45
+ ```
46
+
47
+ Enumerate on them:
48
+
49
+ ```ruby
50
+ backoff.intervals.each do |interval|
51
+ sleep(interval)
52
+ end
53
+ ```
54
+
55
+ Or just get interval for requested, that is 3rd, iteration:
56
+
57
+ ```ruby
58
+ backoff.interval_at(3) # 32.0
59
+ ```
60
+
61
+ Intervals don't exceed maximal allowed time:
62
+
63
+ ```ruby
64
+ backoff.interval_at(20) # 60.0
65
+ ```
66
+
67
+ Backoff instance maintains state, you can ask for next interval...
68
+
69
+ ```ruby
70
+ backoff.next_interval # 4.0
71
+ backoff.next_interval # 8.0
72
+ ```
73
+
74
+ ...and reset it to start from beginning
75
+
76
+ ```ruby
77
+ backoff.clear
78
+ backoff.next_interval # 4.0
79
+ ```
80
+
81
+ Finally you can specify interval multiplier and randomization factor:
82
+
83
+ ```ruby
84
+ backoff = ExponentialBackoff.new(min_interval, max_elapsed)
85
+ backoff.multiplier = 1.5
86
+ backoff.randomize_factor = 0.25
87
+
88
+ backoff.intervals_for(0..2) # [3.764, 6.587, 9.76]
89
+ ```
90
+
91
+ You can peek what is the current interval:
92
+
93
+ ```ruby
94
+ backoff.current_interval # 3.764
95
+ ```
96
+
97
+ There is also sugar for executing block of code until successful with increasing intervals:
98
+
99
+ ```ruby
100
+ backoff.until_success do |interval, retry_count|
101
+ # do your thing
102
+ # when last line in block evaluates to true, elapsed time clear and loop breaks
103
+ # when false, increase interval and retry
104
+
105
+ # you can break loop earlier if you want,
106
+ # `nil` return value is considered a success
107
+ return if retry_count > 3
108
+ end
109
+ ```
110
+
111
+ Running tests
112
+ -------------
113
+
114
+ ruby -Ilib -Itest test/exponential_backoff_test.rb
115
+
116
+
117
+ Supported rubies
118
+ ----------------
119
+
120
+ Targets all Rubies (including Rubinius and JRuby) provided it's at least 1.9 mode.
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env rake
2
+ require "bundler/gem_tasks"
@@ -0,0 +1,16 @@
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
+ end
@@ -0,0 +1,66 @@
1
+ require 'exponential_backoff/version'
2
+
3
+ class ExponentialBackoff
4
+ attr_accessor :multiplier, :randomize_factor
5
+ attr_reader :current_interval
6
+
7
+ def initialize(minimal_interval, maximum_elapsed_time)
8
+ @maximum_elapsed_time = maximum_elapsed_time
9
+ @minimal_interval = minimal_interval
10
+ @randomize_factor = 0
11
+ @multiplier = 2.0
12
+ clear
13
+ end
14
+
15
+ def clear
16
+ @enumerator = intervals
17
+ end
18
+
19
+ def next_interval
20
+ @current_interval = @enumerator.next
21
+ end
22
+
23
+ def intervals_for(range)
24
+ range.to_a.map { |iteration| interval_at(iteration) }
25
+ end
26
+
27
+ def interval_at(iteration)
28
+ randomized_interval(capped_interval(regular_interval(@minimal_interval, @multiplier, iteration)))
29
+ end
30
+
31
+ def intervals
32
+ Enumerator.new do |yielder|
33
+ iteration = 0
34
+ loop do
35
+ yielder.yield interval_at(iteration)
36
+ iteration += 1
37
+ end
38
+ end
39
+ end
40
+
41
+ def until_success(&block)
42
+ intervals.each_with_index do |interval, iteration|
43
+ retval = block.call(interval, iteration)
44
+ return if retval || retval.nil?
45
+ sleep(interval)
46
+ end
47
+ end
48
+
49
+ protected
50
+
51
+ def regular_interval(initial, multiplier, iteration)
52
+ initial * multiplier ** iteration
53
+ end
54
+
55
+ def randomized_interval(interval)
56
+ return interval if @randomize_factor == 0
57
+ min = (1 - @randomize_factor) * interval
58
+ max = (1 + @randomize_factor) * interval
59
+ rand(max - min) + min
60
+ end
61
+
62
+ def capped_interval(interval)
63
+ [@maximum_elapsed_time, interval].min
64
+ end
65
+ end
66
+
@@ -0,0 +1,3 @@
1
+ class ExponentialBackoff
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,158 @@
1
+ require 'test/unit'
2
+ require 'exponential_backoff'
3
+
4
+ class ExponentialBackoffTest < Test::Unit::TestCase
5
+ def test_multiplier_default
6
+ min, max = 1, 2
7
+ backoff = ExponentialBackoff.new(min, max)
8
+
9
+ assert_equal 2, backoff.multiplier
10
+ end
11
+
12
+ def test_randomize_factor_default
13
+ min, max = 1, 2
14
+ backoff = ExponentialBackoff.new(min, max)
15
+
16
+ assert_equal 0, backoff.randomize_factor
17
+ end
18
+
19
+ def test_next_interval
20
+ min, max = 1, 5
21
+ backoff = ExponentialBackoff.new(min, max)
22
+
23
+ assert_equal min, backoff.next_interval
24
+ assert_equal 2, backoff.next_interval
25
+ assert_equal 4, backoff.next_interval
26
+ assert_equal max, backoff.next_interval
27
+ end
28
+
29
+ def test_current_interval
30
+ min, max = 1, 5
31
+ backoff = ExponentialBackoff.new(min, max)
32
+
33
+ assert_nil backoff.current_interval
34
+ backoff.next_interval
35
+ assert_equal 1, backoff.current_interval
36
+ end
37
+
38
+ def test_clear
39
+ min, max = 1, 5
40
+ backoff = ExponentialBackoff.new(min, max)
41
+ 2.times { backoff.next_interval }
42
+ backoff.clear
43
+
44
+ assert_equal min, backoff.next_interval
45
+ end
46
+
47
+ def test_interval_at
48
+ min, max = 1, 5
49
+ backoff = ExponentialBackoff.new(min, max)
50
+
51
+ assert_equal 5, backoff.interval_at(3)
52
+ end
53
+
54
+ def test_intervals_for
55
+ min, max = 1, 5
56
+ backoff = ExponentialBackoff.new(min, max)
57
+
58
+ assert_equal [2, 4, 5, 5], backoff.intervals_for(1..4)
59
+ end
60
+
61
+ def test_intervals_is_enumerator
62
+ min, max = 1, 5
63
+ backoff = ExponentialBackoff.new(min, max)
64
+
65
+ assert_kind_of Enumerator, backoff.intervals
66
+ end
67
+
68
+ def test_intervals_enumerator_is_independent
69
+ min, max = 1, 5
70
+ backoff = ExponentialBackoff.new(min, max)
71
+ first, second = backoff.intervals, backoff.intervals
72
+ first.next
73
+
74
+ assert_not_equal first.next, second.next
75
+ end
76
+
77
+ def test_interval_is_float
78
+ min, max = 1, 5
79
+ backoff = ExponentialBackoff.new(min, max)
80
+
81
+ assert_kind_of Float, backoff.next_interval
82
+ end
83
+
84
+ def test_multiplier
85
+ min, max = 1, 512
86
+ backoff = ExponentialBackoff.new(min, max)
87
+ backoff.multiplier = 4
88
+
89
+ assert_equal [4, 16, 64, 256], backoff.intervals_for(1..4)
90
+ end
91
+
92
+ def test_randomize_factor
93
+ min, max = 1, 5
94
+ backoff = ExponentialBackoff.new(min, max)
95
+ backoff.randomize_factor = 0.25
96
+
97
+ 1_000.times do
98
+ assert_in_delta(0.75, 1.25, backoff.interval_at(0))
99
+ assert_in_delta(3.75, 6.25, backoff.interval_at(100))
100
+ end
101
+ end
102
+
103
+ def test_until_success_executor_stops_on_truthiness_return_value
104
+ min, max = 0.1, 0.5
105
+ backoff = ExponentialBackoff.new(min, max)
106
+
107
+ counter = 0
108
+ return_true = -> {
109
+ counter += 1
110
+ return true
111
+ }
112
+ backoff.until_success { return_true.call }
113
+ assert_equal 1, counter
114
+ end
115
+
116
+ def test_until_success_executor_continues_on_falsy_return_value
117
+ min, max = 0.1, 0.5
118
+ backoff = ExponentialBackoff.new(min, max)
119
+
120
+ counter = 0
121
+ return_false = -> {
122
+ counter += 1
123
+ return if counter > 1
124
+ return false
125
+ }
126
+ backoff.until_success { return_false.call }
127
+ assert_equal 2, counter
128
+ end
129
+
130
+ def test_until_success_block_params
131
+ min, max = 0.1, 0.5
132
+ backoff = ExponentialBackoff.new(min, max)
133
+
134
+ assert_block_params = proc do |interval, iteration|
135
+ assert_equal backoff.interval_at(iteration), interval
136
+ end
137
+
138
+ backoff.until_success { |interval, iteration| assert_block_params.call(interval, iteration) }
139
+ end
140
+
141
+ def test_until_success_executor_sleep_time
142
+ min, max = 0.1, 0.5
143
+ backoff = ExponentialBackoff.new(min, max)
144
+
145
+ counter = 0
146
+ return_false = -> {
147
+ counter += 1
148
+ return if counter > 2
149
+ return false
150
+ }
151
+
152
+ time = Time.now.to_f
153
+ backoff.until_success { return_false.call }
154
+ elapsed = Time.now.to_f - time
155
+ assert elapsed >= 0.3
156
+ end
157
+ end
158
+
metadata ADDED
@@ -0,0 +1,57 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: exponential-backoff
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Paweł Pacana
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-09-26 00:00:00.000000000 Z
13
+ dependencies: []
14
+ description:
15
+ email:
16
+ - pawel.pacana@gmail.com
17
+ executables: []
18
+ extensions: []
19
+ extra_rdoc_files: []
20
+ files:
21
+ - .gitignore
22
+ - .travis.yml
23
+ - Gemfile
24
+ - LICENSE
25
+ - README.md
26
+ - Rakefile
27
+ - exponential-backoff.gemspec
28
+ - lib/exponential_backoff.rb
29
+ - lib/exponential_backoff/version.rb
30
+ - test/exponential_backoff_test.rb
31
+ homepage: ''
32
+ licenses: []
33
+ post_install_message:
34
+ rdoc_options: []
35
+ require_paths:
36
+ - lib
37
+ required_ruby_version: !ruby/object:Gem::Requirement
38
+ none: false
39
+ requirements:
40
+ - - ! '>='
41
+ - !ruby/object:Gem::Version
42
+ version: '0'
43
+ required_rubygems_version: !ruby/object:Gem::Requirement
44
+ none: false
45
+ requirements:
46
+ - - ! '>='
47
+ - !ruby/object:Gem::Version
48
+ version: '0'
49
+ requirements: []
50
+ rubyforge_project:
51
+ rubygems_version: 1.8.23
52
+ signing_key:
53
+ specification_version: 3
54
+ summary: Exponential backoff algorithm for better reconnect intervals.
55
+ test_files:
56
+ - test/exponential_backoff_test.rb
57
+ has_rdoc: