exponential-backoff 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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: