durable_call 0.0.1 → 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.
- data/.gitignore +1 -0
- data/Gemfile +3 -0
- data/README.md +4 -1
- data/benchmarks/simple_method.rb +7 -0
- data/durable_call.gemspec +1 -0
- data/lib/durable_call/caller.rb +39 -22
- data/lib/durable_call/version.rb +1 -1
- data/spec/durable_call_spec.rb +48 -15
- data/spec/spec_helper.rb +3 -0
- metadata +24 -2
data/.gitignore
CHANGED
data/Gemfile
CHANGED
data/README.md
CHANGED
@@ -2,6 +2,9 @@
|
|
2
2
|
|
3
3
|
Invoke methods DRY and safely with parameterized retries, timeouts and logging
|
4
4
|
|
5
|
+
[](https://travis-ci.org/AlexanderPavlenko/durable_call)
|
6
|
+
[](https://coveralls.io/r/AlexanderPavlenko/durable_call)
|
7
|
+
|
5
8
|
## Installation
|
6
9
|
|
7
10
|
Add this line to your application's Gemfile:
|
@@ -29,7 +32,7 @@ Multiple arguments and options:
|
|
29
32
|
Where ```options``` may take:
|
30
33
|
|
31
34
|
{
|
32
|
-
:interval # 1. lambda, which takes
|
35
|
+
:interval # 1. lambda, which takes retry number (min 1) and returns seconds to sleep
|
33
36
|
# 2. just Float
|
34
37
|
# 3. Symbol for built-in strategies, defaults to :rand
|
35
38
|
:logger # Logger object, defaults to nil
|
data/benchmarks/simple_method.rb
CHANGED
@@ -4,6 +4,7 @@ require File.expand_path('../../lib/durable_call.rb', __FILE__)
|
|
4
4
|
n = 500000
|
5
5
|
Benchmark.bm do |bm|
|
6
6
|
object = Object.new
|
7
|
+
caller = DurableCall::Caller.new(object)
|
7
8
|
|
8
9
|
bm.report do
|
9
10
|
n.times do
|
@@ -11,6 +12,12 @@ Benchmark.bm do |bm|
|
|
11
12
|
end
|
12
13
|
end
|
13
14
|
|
15
|
+
bm.report do
|
16
|
+
n.times do
|
17
|
+
caller.call(:object_id)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
14
21
|
bm.report do
|
15
22
|
n.times do
|
16
23
|
DurableCall.call(object, :object_id)
|
data/durable_call.gemspec
CHANGED
data/lib/durable_call/caller.rb
CHANGED
@@ -13,9 +13,11 @@ module DurableCall
|
|
13
13
|
}.freeze
|
14
14
|
|
15
15
|
MESSAGES = {
|
16
|
-
:new_retry =>
|
17
|
-
:failed_call =>
|
18
|
-
:waiting_before_retry =>
|
16
|
+
:new_retry => 'Retry #%1$i',
|
17
|
+
:failed_call => 'Failed to call %1$s on %2$s: %3$s',
|
18
|
+
:waiting_before_retry => 'Waiting %1$.2f seconds before retry',
|
19
|
+
:retries_error => 'Number of retries exceeded: %1$i',
|
20
|
+
:timeout_error => 'Timeout exceeded: %1$.2f',
|
19
21
|
}.freeze
|
20
22
|
|
21
23
|
attr_reader :subject
|
@@ -38,30 +40,19 @@ module DurableCall
|
|
38
40
|
called = false
|
39
41
|
result = nil
|
40
42
|
(0..@retries).each do |retries_counter|
|
43
|
+
# @timeout may be exceeded here and exception will be raised
|
41
44
|
begin
|
42
|
-
|
45
|
+
if retries_counter > 0
|
46
|
+
# first try isn't "retry"
|
47
|
+
log :info, :new_retry, retries_counter
|
48
|
+
end
|
43
49
|
result = @subject.__send__ *args
|
44
50
|
called = true
|
45
|
-
rescue Timeout::Error => ex
|
46
|
-
# just reraise exception if @timeout exceeded
|
47
|
-
raise
|
48
51
|
rescue => ex
|
49
|
-
|
50
|
-
@logger.warn MESSAGES[:failed_call] % [args.inspect, @subject, ex.inspect] if @logger
|
52
|
+
log :warn, :failed_call, args.inspect, @subject, ex.inspect
|
51
53
|
if @interval && retries_counter < @retries
|
52
54
|
# interval specified and it's not a last iteration
|
53
|
-
|
54
|
-
INTERVALS[@interval].call(retries_counter)
|
55
|
-
elsif @interval.respond_to?(:call)
|
56
|
-
@interval.call(retries_counter)
|
57
|
-
else
|
58
|
-
@interval
|
59
|
-
end
|
60
|
-
# sleep before next retry if needed
|
61
|
-
if seconds > 0
|
62
|
-
@logger.info MESSAGES[:waiting_before_retry] % seconds if @logger
|
63
|
-
sleep seconds
|
64
|
-
end
|
55
|
+
sleep_before_retry(retries_counter + 1)
|
65
56
|
end
|
66
57
|
else
|
67
58
|
break
|
@@ -70,9 +61,35 @@ module DurableCall
|
|
70
61
|
if called
|
71
62
|
result
|
72
63
|
else
|
73
|
-
|
64
|
+
log :error, :retries_error, @retries
|
65
|
+
raise RetriesError
|
74
66
|
end
|
75
67
|
end
|
68
|
+
rescue TimeoutError
|
69
|
+
log :error, :timeout_error, @timeout
|
70
|
+
raise
|
71
|
+
end
|
72
|
+
|
73
|
+
private
|
74
|
+
|
75
|
+
def sleep_before_retry(retries_counter)
|
76
|
+
seconds = if @interval.is_a? Symbol
|
77
|
+
INTERVALS[@interval].call(retries_counter)
|
78
|
+
elsif @interval.respond_to?(:call)
|
79
|
+
@interval.call(retries_counter)
|
80
|
+
else
|
81
|
+
@interval
|
82
|
+
end
|
83
|
+
# sleep before next retry if needed
|
84
|
+
if seconds > 0
|
85
|
+
log :info, :waiting_before_retry, seconds
|
86
|
+
sleep seconds
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
def log(level, message, *args)
|
91
|
+
return unless @logger
|
92
|
+
@logger.send level, MESSAGES[message] % args
|
76
93
|
end
|
77
94
|
end
|
78
95
|
end
|
data/lib/durable_call/version.rb
CHANGED
data/spec/durable_call_spec.rb
CHANGED
@@ -56,7 +56,9 @@ describe DurableCall do
|
|
56
56
|
it 'invokes long method' do
|
57
57
|
@wrapper = DurableCall::Caller.new(@subject, :timeout => 0.1, :logger => @logger)
|
58
58
|
expect{ @wrapper.call(:long_method, 1) }.to raise_error(DurableCall::TimeoutError)
|
59
|
-
@log.string
|
59
|
+
valid_log?(@log.string, [
|
60
|
+
/E.*Timeout exceeded: 0.10/,
|
61
|
+
]).should == true
|
60
62
|
end
|
61
63
|
|
62
64
|
it 'invokes not so long method' do
|
@@ -71,18 +73,48 @@ describe DurableCall do
|
|
71
73
|
expect{ @wrapper.call(:failing_method, condition) }.to raise_error(DurableCall::RetriesError)
|
72
74
|
end
|
73
75
|
|
74
|
-
it 'invokes failing method with logging' do
|
76
|
+
it 'invokes failing method with constant intervals and logging' do
|
75
77
|
@wrapper = DurableCall::Caller.new(@subject, :retries => 2, :interval => 0.0123, :logger => @logger)
|
76
78
|
(condition = mock).should_receive(:call).exactly(3).times.and_return(true)
|
77
79
|
expect{ @wrapper.call(:failing_method, condition) }.to raise_error(DurableCall::RetriesError)
|
78
80
|
valid_log?(@log.string, [
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
81
|
+
error = /W.*Failed to call \[\:failing_method, .*RuntimeError\: it happens/,
|
82
|
+
waiting = /I.*Waiting 0\.01 seconds before retry/,
|
83
|
+
/I.*Retry \#1/,
|
84
|
+
error,
|
85
|
+
waiting,
|
86
|
+
/I.*Retry \#2/,
|
87
|
+
error,
|
88
|
+
/E.*Number of retries exceeded: 2/,
|
89
|
+
]).should == true
|
90
|
+
end
|
91
|
+
|
92
|
+
it 'invokes failing method with :rand intervals and logging' do
|
93
|
+
@wrapper = DurableCall::Caller.new(@subject, :retries => 1, :logger => @logger)
|
94
|
+
(condition = mock).should_receive(:call).exactly(2).times.and_return(true)
|
95
|
+
expect{ @wrapper.call(:failing_method, condition) }.to raise_error(DurableCall::RetriesError)
|
96
|
+
valid_log?(@log.string, [
|
97
|
+
error = /W.*Failed to call \[\:failing_method, .*RuntimeError\: it happens/,
|
98
|
+
/I.*Waiting [01]\.\d\d seconds before retry/,
|
99
|
+
/I.*Retry \#1/,
|
100
|
+
error,
|
101
|
+
/E.*Number of retries exceeded: 1/,
|
102
|
+
]).should == true
|
103
|
+
end
|
104
|
+
|
105
|
+
it 'invokes failing method with custom intervals and logging' do
|
106
|
+
@wrapper = DurableCall::Caller.new(@subject, :retries => 2, :interval => lambda{|i| i / 100.0 }, :logger => @logger)
|
107
|
+
(condition = mock).should_receive(:call).exactly(3).times.and_return(true)
|
108
|
+
expect{ @wrapper.call(:failing_method, condition) }.to raise_error(DurableCall::RetriesError)
|
109
|
+
valid_log?(@log.string, [
|
110
|
+
error = /W.*Failed to call \[\:failing_method, .*RuntimeError\: it happens/,
|
111
|
+
/I.*Waiting 0\.01 seconds before retry/,
|
112
|
+
/I.*Retry \#1/,
|
113
|
+
error,
|
114
|
+
/I.*Waiting 0\.02 seconds before retry/,
|
115
|
+
/I.*Retry \#2/,
|
116
|
+
error,
|
117
|
+
/E.*Number of retries exceeded: 2/,
|
86
118
|
]).should == true
|
87
119
|
end
|
88
120
|
|
@@ -91,9 +123,9 @@ describe DurableCall do
|
|
91
123
|
(condition = mock).should_receive(:call).twice.and_return(true, false)
|
92
124
|
@wrapper.call(:failing_method, condition).should == true
|
93
125
|
valid_log?(@log.string, [
|
94
|
-
|
95
|
-
|
96
|
-
|
126
|
+
/W.*Failed to call \[\:failing_method, .*RuntimeError\: it happens/,
|
127
|
+
/I.*Waiting 0\.01 seconds before retry/,
|
128
|
+
/I.*Retry \#1/,
|
97
129
|
]).should == true
|
98
130
|
end
|
99
131
|
|
@@ -102,8 +134,9 @@ describe DurableCall do
|
|
102
134
|
(condition = mock).should_receive(:call).once.and_return(true)
|
103
135
|
expect{ @wrapper.call(:worse_method, 0.05, condition) }.to raise_error(DurableCall::TimeoutError)
|
104
136
|
valid_log?(@log.string, [
|
105
|
-
|
106
|
-
|
137
|
+
/W.*Failed to call \[\:worse_method, .*RuntimeError\: it happens/,
|
138
|
+
/I.*Waiting 0\.05 seconds before retry/,
|
139
|
+
/E.*Timeout exceeded: 0.10/,
|
107
140
|
]).should == true
|
108
141
|
end
|
109
142
|
|
@@ -113,8 +146,8 @@ describe DurableCall do
|
|
113
146
|
end
|
114
147
|
|
115
148
|
def valid_log?(log, regexps)
|
149
|
+
puts "#{'-' * 20}\n", log
|
116
150
|
raise ArgumentError if log.lines.count != regexps.size
|
117
|
-
# puts "#{'-' * 20}\n", log
|
118
151
|
log.lines.zip(regexps).all?{|(string, regexp)| string =~ regexp }
|
119
152
|
end
|
120
153
|
end
|
data/spec/spec_helper.rb
CHANGED
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: durable_call
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0
|
4
|
+
version: 0.1.0
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2013-04-
|
12
|
+
date: 2013-04-23 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: bundler
|
@@ -59,6 +59,22 @@ dependencies:
|
|
59
59
|
- - ! '>='
|
60
60
|
- !ruby/object:Gem::Version
|
61
61
|
version: '0'
|
62
|
+
- !ruby/object:Gem::Dependency
|
63
|
+
name: coveralls
|
64
|
+
requirement: !ruby/object:Gem::Requirement
|
65
|
+
none: false
|
66
|
+
requirements:
|
67
|
+
- - ! '>='
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: '0'
|
70
|
+
type: :development
|
71
|
+
prerelease: false
|
72
|
+
version_requirements: !ruby/object:Gem::Requirement
|
73
|
+
none: false
|
74
|
+
requirements:
|
75
|
+
- - ! '>='
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
version: '0'
|
62
78
|
description: Invoke methods DRY and safely with parameterized retries, timeouts and
|
63
79
|
logging
|
64
80
|
email:
|
@@ -94,12 +110,18 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
94
110
|
- - ! '>='
|
95
111
|
- !ruby/object:Gem::Version
|
96
112
|
version: '0'
|
113
|
+
segments:
|
114
|
+
- 0
|
115
|
+
hash: -1232904255849708953
|
97
116
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
98
117
|
none: false
|
99
118
|
requirements:
|
100
119
|
- - ! '>='
|
101
120
|
- !ruby/object:Gem::Version
|
102
121
|
version: '0'
|
122
|
+
segments:
|
123
|
+
- 0
|
124
|
+
hash: -1232904255849708953
|
103
125
|
requirements: []
|
104
126
|
rubyforge_project:
|
105
127
|
rubygems_version: 1.8.25
|