durable_call 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --format documentation
2
+ --color
data/.travis.yml ADDED
@@ -0,0 +1,5 @@
1
+ language: ruby
2
+ rvm:
3
+ - 1.8.7
4
+ - 1.9.3
5
+ - 2.0.0
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in durable_call.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 Alexander Pavlenko
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,52 @@
1
+ # DurableCall
2
+
3
+ Invoke methods DRY and safely with parameterized retries, timeouts and logging
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ gem 'durable_call'
10
+
11
+ And then execute:
12
+
13
+ $ bundle
14
+
15
+ Or install it yourself as:
16
+
17
+ $ gem install durable_call
18
+
19
+ ## Usage
20
+
21
+ Simple usage:
22
+
23
+ DurableCall.call(Object.new, :object_id)
24
+
25
+ Multiple arguments and options:
26
+
27
+ DurableCall.call(Something.new, [:method_name, :param, :other_param], options)
28
+
29
+ Where ```options``` may take:
30
+
31
+ {
32
+ :interval # 1. lambda, which takes retries number and returns seconds to sleep
33
+ # 2. just Float
34
+ # 3. Symbol for built-in strategies, defaults to :rand
35
+ :logger # Logger object, defaults to nil
36
+ :retries # retries number, defaults to 0
37
+ :timeout # operation timeout, defaults to nil (no time limits)
38
+ }
39
+
40
+ Also, it's possible to perform multiple calls with the same options:
41
+
42
+ caller = DurableCall::Caller.new(Something.new, options)
43
+ caller.call(:method_name, :param, :other_param)
44
+ caller.call(:faster_faster, :no_instantiation_overhead)
45
+
46
+ ## Contributing
47
+
48
+ 1. Fork it
49
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
50
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
51
+ 4. Push to the branch (`git push origin my-new-feature`)
52
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
@@ -0,0 +1,19 @@
1
+ require 'benchmark'
2
+ require File.expand_path('../../lib/durable_call.rb', __FILE__)
3
+
4
+ n = 500000
5
+ Benchmark.bm do |bm|
6
+ object = Object.new
7
+
8
+ bm.report do
9
+ n.times do
10
+ object.__send__ :object_id
11
+ end
12
+ end
13
+
14
+ bm.report do
15
+ n.times do
16
+ DurableCall.call(object, :object_id)
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,24 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'durable_call/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "durable_call"
8
+ spec.version = DurableCall::VERSION
9
+ spec.authors = ["Alexander Pavlenko"]
10
+ spec.email = ["apavlenko@mirantis.com"]
11
+ spec.description = %q{Invoke methods DRY and safely with parameterized retries, timeouts and logging}
12
+ spec.summary = %q{Durable methods invocation}
13
+ spec.homepage = ""
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files`.split($/)
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_development_dependency "bundler", "~> 1.3"
22
+ spec.add_development_dependency "rake"
23
+ spec.add_development_dependency "rspec"
24
+ end
@@ -0,0 +1,78 @@
1
+ require 'timeout'
2
+
3
+ module DurableCall
4
+
5
+ class RetriesError < RuntimeError; end
6
+ TimeoutError = Timeout::Error
7
+
8
+ class Caller
9
+
10
+ INTERVALS = {
11
+ :rand => lambda{|_| rand },
12
+ # TODO: progression with the increasing delays between retries
13
+ }.freeze
14
+
15
+ MESSAGES = {
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
+ }.freeze
20
+
21
+ attr_reader :subject
22
+
23
+ def initialize(subject, options={})
24
+ @subject = subject
25
+ { # default options
26
+ :interval => :rand,
27
+ :logger => nil,
28
+ :retries => 0,
29
+ :timeout => nil,
30
+ }.each do |key, value|
31
+ instance_variable_set :"@#{key}", options.key?(key) ? options[key] : value
32
+ end
33
+ end
34
+
35
+ def call(*args)
36
+ Timeout.timeout(@timeout) do
37
+ # we want to return as soon as result will be obtained
38
+ called = false
39
+ result = nil
40
+ (0..@retries).each do |retries_counter|
41
+ begin
42
+ @logger.info MESSAGES[:new_retry] % retries_counter if retries_counter > 0 if @logger
43
+ result = @subject.__send__ *args
44
+ called = true
45
+ rescue Timeout::Error => ex
46
+ # just reraise exception if @timeout exceeded
47
+ raise
48
+ rescue => ex
49
+ # @timeout may be exceeded here and exception will be raised
50
+ @logger.warn MESSAGES[:failed_call] % [args.inspect, @subject, ex.inspect] if @logger
51
+ if @interval && retries_counter < @retries
52
+ # interval specified and it's not a last iteration
53
+ seconds = if @interval.is_a? Symbol
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
65
+ end
66
+ else
67
+ break
68
+ end
69
+ end
70
+ if called
71
+ result
72
+ else
73
+ raise RetriesError, "Number of retries exceeded: #{@retries}"
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,3 @@
1
+ module DurableCall
2
+ VERSION = '0.0.1'
3
+ end
@@ -0,0 +1,8 @@
1
+ require 'durable_call/version'
2
+ require 'durable_call/caller'
3
+
4
+ module DurableCall
5
+ def self.call(subject, args, options={})
6
+ DurableCall::Caller.new(subject, options).call(*Array(args))
7
+ end
8
+ end
@@ -0,0 +1,120 @@
1
+ require 'spec_helper'
2
+ require 'logger'
3
+
4
+ describe DurableCall do
5
+
6
+ before :all do
7
+ @subject_class = Class.new do
8
+ def simple_method
9
+ true
10
+ end
11
+ def long_method(seconds)
12
+ sleep seconds
13
+ true
14
+ end
15
+ def failing_method(condition)
16
+ raise RuntimeError.new("it happens") if condition.call
17
+ true
18
+ end
19
+ def worse_method(seconds, condition)
20
+ sleep seconds
21
+ raise RuntimeError.new("it happens") if condition.call
22
+ true
23
+ end
24
+ end.freeze
25
+ @subject = @subject_class.new.freeze
26
+ end
27
+
28
+ before do
29
+ @log = StringIO.new
30
+ @logger = Logger.new(@log)
31
+ end
32
+
33
+ it 'should have a version number' do
34
+ DurableCall::VERSION.should_not be_nil
35
+ end
36
+
37
+ it 'has default settings' do
38
+ @wrapper = DurableCall::Caller.new(@subject)
39
+ @wrapper.instance_variable_get(:@interval).should == :rand
40
+ @wrapper.instance_variable_get(:@logger ).should == nil
41
+ @wrapper.instance_variable_get(:@retries ).should == 0
42
+ @wrapper.instance_variable_get(:@timeout ).should == nil
43
+ end
44
+
45
+ it 'has subject reader' do
46
+ @wrapper = DurableCall::Caller.new(@subject)
47
+ @wrapper.subject.should === @subject
48
+ end
49
+
50
+ it 'invokes simple methods' do
51
+ @wrapper = DurableCall::Caller.new(@subject, :logger => @logger)
52
+ @wrapper.call(:simple_method).should == true
53
+ @log.string.should == ''
54
+ end
55
+
56
+ it 'invokes long method' do
57
+ @wrapper = DurableCall::Caller.new(@subject, :timeout => 0.1, :logger => @logger)
58
+ expect{ @wrapper.call(:long_method, 1) }.to raise_error(DurableCall::TimeoutError)
59
+ @log.string.should == ''
60
+ end
61
+
62
+ it 'invokes not so long method' do
63
+ @wrapper = DurableCall::Caller.new(@subject, :timeout => 0.1, :logger => @logger)
64
+ @wrapper.call(:long_method, 0.09).should == true
65
+ @log.string.should == ''
66
+ end
67
+
68
+ it 'invokes failing method' do
69
+ @wrapper = DurableCall::Caller.new(@subject)
70
+ (condition = mock).should_receive(:call).once.and_return(true)
71
+ expect{ @wrapper.call(:failing_method, condition) }.to raise_error(DurableCall::RetriesError)
72
+ end
73
+
74
+ it 'invokes failing method with logging' do
75
+ @wrapper = DurableCall::Caller.new(@subject, :retries => 2, :interval => 0.0123, :logger => @logger)
76
+ (condition = mock).should_receive(:call).exactly(3).times.and_return(true)
77
+ expect{ @wrapper.call(:failing_method, condition) }.to raise_error(DurableCall::RetriesError)
78
+ valid_log?(@log.string, [
79
+ error = /W.*Failed to call \[\:failing_method, .*RuntimeError\: it happens/,
80
+ waiting = /I.*Waiting 0\.01 seconds before retry/,
81
+ /I.*Retry \#1/,
82
+ error,
83
+ waiting,
84
+ /I.*Retry \#2/,
85
+ error,
86
+ ]).should == true
87
+ end
88
+
89
+ it 'invokes not so failing method' do
90
+ @wrapper = DurableCall::Caller.new(@subject, :retries => 2, :interval => 0.0123, :logger => @logger)
91
+ (condition = mock).should_receive(:call).twice.and_return(true, false)
92
+ @wrapper.call(:failing_method, condition).should == true
93
+ valid_log?(@log.string, [
94
+ /W.*Failed to call \[\:failing_method, .*RuntimeError\: it happens/,
95
+ /I.*Waiting 0\.01 seconds before retry/,
96
+ /I.*Retry \#1/,
97
+ ]).should == true
98
+ end
99
+
100
+ it 'invokes worse method' do
101
+ @wrapper = DurableCall::Caller.new(@subject, :retries => 2, :interval => 0.05, :timeout => 0.1, :logger => @logger)
102
+ (condition = mock).should_receive(:call).once.and_return(true)
103
+ expect{ @wrapper.call(:worse_method, 0.05, condition) }.to raise_error(DurableCall::TimeoutError)
104
+ valid_log?(@log.string, [
105
+ /W.*Failed to call \[\:worse_method, .*RuntimeError\: it happens/,
106
+ /I.*Waiting 0\.05 seconds before retry/,
107
+ ]).should == true
108
+ end
109
+
110
+ it 'has shorthand module method' do
111
+ DurableCall.call(@subject, :simple_method)
112
+ DurableCall.call(@subject, [:long_method, 0.01], {:logger => @logger, :timeout => 0.1})
113
+ end
114
+
115
+ def valid_log?(log, regexps)
116
+ raise ArgumentError if log.lines.count != regexps.size
117
+ # puts "#{'-' * 20}\n", log
118
+ log.lines.zip(regexps).all?{|(string, regexp)| string =~ regexp }
119
+ end
120
+ end
@@ -0,0 +1,2 @@
1
+ $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__)
2
+ require 'durable_call'
metadata ADDED
@@ -0,0 +1,111 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: durable_call
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Alexander Pavlenko
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2013-04-22 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: bundler
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ~>
20
+ - !ruby/object:Gem::Version
21
+ version: '1.3'
22
+ type: :development
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ~>
28
+ - !ruby/object:Gem::Version
29
+ version: '1.3'
30
+ - !ruby/object:Gem::Dependency
31
+ name: rake
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ! '>='
36
+ - !ruby/object:Gem::Version
37
+ version: '0'
38
+ type: :development
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ! '>='
44
+ - !ruby/object:Gem::Version
45
+ version: '0'
46
+ - !ruby/object:Gem::Dependency
47
+ name: rspec
48
+ requirement: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - ! '>='
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ type: :development
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ! '>='
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ description: Invoke methods DRY and safely with parameterized retries, timeouts and
63
+ logging
64
+ email:
65
+ - apavlenko@mirantis.com
66
+ executables: []
67
+ extensions: []
68
+ extra_rdoc_files: []
69
+ files:
70
+ - .gitignore
71
+ - .rspec
72
+ - .travis.yml
73
+ - Gemfile
74
+ - LICENSE.txt
75
+ - README.md
76
+ - Rakefile
77
+ - benchmarks/simple_method.rb
78
+ - durable_call.gemspec
79
+ - lib/durable_call.rb
80
+ - lib/durable_call/caller.rb
81
+ - lib/durable_call/version.rb
82
+ - spec/durable_call_spec.rb
83
+ - spec/spec_helper.rb
84
+ homepage: ''
85
+ licenses:
86
+ - MIT
87
+ post_install_message:
88
+ rdoc_options: []
89
+ require_paths:
90
+ - lib
91
+ required_ruby_version: !ruby/object:Gem::Requirement
92
+ none: false
93
+ requirements:
94
+ - - ! '>='
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ required_rubygems_version: !ruby/object:Gem::Requirement
98
+ none: false
99
+ requirements:
100
+ - - ! '>='
101
+ - !ruby/object:Gem::Version
102
+ version: '0'
103
+ requirements: []
104
+ rubyforge_project:
105
+ rubygems_version: 1.8.25
106
+ signing_key:
107
+ specification_version: 3
108
+ summary: Durable methods invocation
109
+ test_files:
110
+ - spec/durable_call_spec.rb
111
+ - spec/spec_helper.rb