active_utils 1.0.3 → 1.0.4

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -12,3 +12,4 @@ Active Utils extracts commonly used modules and classes used by [Active Merchant
12
12
  * RequiresParameters - helper method to ensure the required parameters are passed in
13
13
  * Utils - common utils such as uid generator
14
14
  * Validateable - module used for making models validateable
15
+ * NetworkConnectionRetries - module for retrying network connections when connection errors occur
@@ -5,6 +5,8 @@ require 'benchmark'
5
5
 
6
6
  module ActiveMerchant
7
7
  class Connection
8
+ include NetworkConnectionRetries
9
+
8
10
  MAX_RETRIES = 3
9
11
  OPEN_TIMEOUT = 60
10
12
  READ_TIMEOUT = 60
@@ -16,7 +18,6 @@ module ActiveMerchant
16
18
  attr_accessor :open_timeout
17
19
  attr_accessor :read_timeout
18
20
  attr_accessor :verify_peer
19
- attr_accessor :retry_safe
20
21
  attr_accessor :pem
21
22
  attr_accessor :pem_password
22
23
  attr_accessor :wiredump_device
@@ -34,7 +35,7 @@ module ActiveMerchant
34
35
  end
35
36
 
36
37
  def request(method, body, headers = {})
37
- retry_exceptions do
38
+ retry_exceptions(:max_retries => MAX_RETRIES, :logger => logger, :tag => tag) do
38
39
  begin
39
40
  info "#{method.to_s.upcase} #{endpoint}", tag
40
41
 
@@ -65,17 +66,6 @@ module ActiveMerchant
65
66
  info "--> %d %s (%d %.4fs)" % [result.code, result.message, result.body ? result.body.length : 0, realtime], tag
66
67
  debug result.body
67
68
  result
68
- rescue EOFError => e
69
- raise ConnectionError, "The remote server dropped the connection"
70
- rescue Errno::ECONNRESET => e
71
- raise ConnectionError, "The remote server reset the connection"
72
- rescue Errno::ECONNREFUSED => e
73
- raise RetriableConnectionError, "The remote server refused the connection"
74
- rescue OpenSSL::X509::CertificateError => e
75
- error(e.message, tag)
76
- raise ClientCertificateError, "The remote server did not accept the provided SSL certificate"
77
- rescue Timeout::Error, Errno::ETIMEDOUT => e
78
- raise ConnectionError, "The connection to the remote server timed out"
79
69
  end
80
70
  end
81
71
  end
@@ -124,23 +114,6 @@ module ActiveMerchant
124
114
  end
125
115
  end
126
116
 
127
- def retry_exceptions
128
- retries = MAX_RETRIES
129
- begin
130
- yield
131
- rescue RetriableConnectionError => e
132
- retries -= 1
133
- retry unless retries.zero?
134
- error(e.message, tag)
135
- raise ConnectionError, e.message
136
- rescue ConnectionError => e
137
- retries -= 1
138
- retry if retry_safe && !retries.zero?
139
- error(e.message, tag)
140
- raise
141
- end
142
- end
143
-
144
117
  def handle_response(response)
145
118
  if @ignore_http_status then
146
119
  return response.body
@@ -0,0 +1,58 @@
1
+ module ActiveMerchant
2
+ module NetworkConnectionRetries
3
+ DEFAULT_RETRIES = 3
4
+ DEFAULT_CONNECTION_ERRORS = {
5
+ EOFError => "The remote server dropped the connection",
6
+ Errno::ECONNRESET => "The remote server reset the connection",
7
+ Timeout::Error => "The connection to the remote server timed out",
8
+ Errno::ETIMEDOUT => "The connection to the remote server timed out"
9
+ }
10
+
11
+ def self.included(base)
12
+ base.send(:attr_accessor, :retry_safe)
13
+ end
14
+
15
+ def retry_exceptions(options={})
16
+ connection_errors = DEFAULT_CONNECTION_ERRORS.merge(options[:connection_exceptions] || {})
17
+
18
+ retry_network_exceptions(options) do
19
+ begin
20
+ yield
21
+ rescue Errno::ECONNREFUSED => e
22
+ raise ActiveMerchant::RetriableConnectionError, "The remote server refused the connection"
23
+ rescue OpenSSL::X509::CertificateError => e
24
+ NetworkConnectionRetries.log(options[:logger], :error, e.message, options[:tag])
25
+ raise ActiveMerchant::ClientCertificateError, "The remote server did not accept the provided SSL certificate"
26
+ rescue *connection_errors.keys => e
27
+ raise ActiveMerchant::ConnectionError, connection_errors[e.class]
28
+ end
29
+ end
30
+ end
31
+
32
+ private
33
+
34
+ def retry_network_exceptions(options = {})
35
+ retries = options[:max] || DEFAULT_RETRIES
36
+
37
+ begin
38
+ yield
39
+ rescue ActiveMerchant::RetriableConnectionError => e
40
+ retries -= 1
41
+ retry unless retries.zero?
42
+ NetworkConnectionRetries.log(options[:logger], :error, e.message, options[:tag])
43
+ raise ActiveMerchant::ConnectionError, e.message
44
+ rescue ActiveMerchant::ConnectionError => e
45
+ retries -= 1
46
+ retry if (options[:retry_safe] || retry_safe) && !retries.zero?
47
+ NetworkConnectionRetries.log(options[:logger], :error, e.message, options[:tag])
48
+ raise
49
+ end
50
+ end
51
+
52
+ def self.log(logger, level, message, tag=nil)
53
+ tag ||= self.class.to_s
54
+ message = "[#{tag}] #{message}"
55
+ logger.send(level, message) if logger
56
+ end
57
+ end
58
+ end
@@ -31,6 +31,8 @@ module ActiveMerchant #:nodoc:
31
31
  end
32
32
 
33
33
  def raw_ssl_request(method, endpoint, data, headers = {})
34
+ logger.warn "#{self.class} using ssl_strict=false, which is insecure" if logger unless ssl_strict
35
+
34
36
  connection = new_connection(endpoint)
35
37
  connection.open_timeout = open_timeout
36
38
  connection.read_timeout = read_timeout
@@ -1,16 +1,9 @@
1
- require 'digest/md5'
1
+ require 'securerandom'
2
2
 
3
3
  module ActiveMerchant #:nodoc:
4
4
  module Utils #:nodoc:
5
5
  def generate_unique_id
6
- md5 = Digest::MD5.new
7
- now = Time.now
8
- md5 << now.to_s
9
- md5 << String(now.usec)
10
- md5 << String(rand(0))
11
- md5 << String($$)
12
- md5 << self.class.name
13
- md5.hexdigest
6
+ SecureRandom.hex(16)
14
7
  end
15
8
 
16
9
  module_function :generate_unique_id
@@ -1,3 +1,3 @@
1
1
  module ActiveUtils
2
- VERSION = "1.0.3"
2
+ VERSION = "1.0.4"
3
3
  end
data/lib/active_utils.rb CHANGED
@@ -3,6 +3,7 @@ require 'active_support/core_ext/hash/conversions'
3
3
  require 'active_support/core_ext/class/attribute'
4
4
 
5
5
  module ActiveMerchant
6
+ autoload :NetworkConnectionRetries, 'active_utils/common/network_connection_retries'
6
7
  autoload :Connection, 'active_utils/common/connection'
7
8
  autoload :Country, 'active_utils/common/country'
8
9
  autoload :CountryCode, 'active_utils/common/country'
@@ -0,0 +1,127 @@
1
+ require 'test_helper'
2
+ require 'openssl'
3
+ require 'net/http'
4
+
5
+ class NetworkConnectionRetriesTest < Test::Unit::TestCase
6
+ class MyNewError < StandardError
7
+ end
8
+
9
+ include NetworkConnectionRetries
10
+
11
+ def setup
12
+ @logger = stubs(:logger)
13
+ @requester = stubs(:requester)
14
+ @ok = stub(:code => 200, :message => 'OK', :body => 'success')
15
+ end
16
+
17
+ def test_unrecoverable_exception
18
+ assert_raises(ActiveMerchant::ConnectionError) do
19
+ retry_exceptions do
20
+ raise EOFError
21
+ end
22
+ end
23
+ end
24
+
25
+ def test_unrecoverable_exception_logged_if_logger_provided
26
+ @logger.expects(:error).once
27
+ assert_raises(ActiveMerchant::ConnectionError) do
28
+ retry_exceptions :logger => @logger do
29
+ raise EOFError
30
+ end
31
+ end
32
+ end
33
+
34
+ def test_failure_then_success_with_recoverable_exception
35
+ @requester.expects(:post).times(2).raises(Errno::ECONNREFUSED).then.returns(@ok)
36
+
37
+ assert_nothing_raised do
38
+ retry_exceptions do
39
+ @requester.post
40
+ end
41
+ end
42
+ end
43
+
44
+ def test_failure_limit_reached
45
+ @requester.expects(:post).times(ActiveMerchant::NetworkConnectionRetries::DEFAULT_RETRIES).raises(Errno::ECONNREFUSED)
46
+
47
+ assert_raises(ActiveMerchant::ConnectionError) do
48
+ retry_exceptions do
49
+ @requester.post
50
+ end
51
+ end
52
+ end
53
+
54
+ def test_failure_limit_reached_logs_final_error
55
+ @logger.expects(:error).once
56
+ @requester.expects(:post).times(ActiveMerchant::NetworkConnectionRetries::DEFAULT_RETRIES).raises(Errno::ECONNREFUSED)
57
+
58
+ assert_raises(ActiveMerchant::ConnectionError) do
59
+ retry_exceptions(:logger => @logger) do
60
+ @requester.post
61
+ end
62
+ end
63
+ end
64
+
65
+ def test_failure_then_success_with_retry_safe_enabled
66
+ @requester.expects(:post).times(2).raises(EOFError).then.returns(@ok)
67
+
68
+ assert_nothing_raised do
69
+ retry_exceptions :retry_safe => true do
70
+ @requester.post
71
+ end
72
+ end
73
+ end
74
+
75
+ def test_mixture_of_failures_with_retry_safe_enabled
76
+ @requester.expects(:post).times(3).raises(Errno::ECONNRESET).
77
+ raises(Errno::ECONNREFUSED).
78
+ raises(EOFError)
79
+
80
+ assert_raises(ActiveMerchant::ConnectionError) do
81
+ retry_exceptions :retry_safe => true do
82
+ @requester.post
83
+ end
84
+ end
85
+ end
86
+
87
+ def test_failure_with_ssl_certificate
88
+ @requester.expects(:post).raises(OpenSSL::X509::CertificateError)
89
+
90
+ assert_raises(ActiveMerchant::ClientCertificateError) do
91
+ retry_exceptions do
92
+ @requester.post
93
+ end
94
+ end
95
+ end
96
+
97
+ def test_failure_with_ssl_certificate_logs_error_if_logger_specified
98
+ @logger.expects(:error).once
99
+ @requester.expects(:post).raises(OpenSSL::X509::CertificateError)
100
+
101
+ assert_raises(ActiveMerchant::ClientCertificateError) do
102
+ retry_exceptions :logger => @logger do
103
+ @requester.post
104
+ end
105
+ end
106
+ end
107
+
108
+ def test_failure_with_additional_exceptions_specified
109
+ @requester.expects(:post).raises(MyNewError)
110
+
111
+ assert_raises(ActiveMerchant::ConnectionError) do
112
+ retry_exceptions :connection_exceptions => {MyNewError => "my message"} do
113
+ @requester.post
114
+ end
115
+ end
116
+ end
117
+
118
+ def test_failure_without_additional_exceptions_specified
119
+ @requester.expects(:post).raises(MyNewError)
120
+
121
+ assert_raises(MyNewError) do
122
+ retry_exceptions do
123
+ @requester.post
124
+ end
125
+ end
126
+ end
127
+ end
@@ -0,0 +1,35 @@
1
+ require 'test_helper'
2
+ require 'active_support/core_ext/class'
3
+
4
+ class PostsDataTest < Test::Unit::TestCase
5
+
6
+ class SSLPoster
7
+ include PostsData
8
+
9
+ attr_accessor :logger
10
+ end
11
+
12
+ def setup
13
+ @poster = SSLPoster.new
14
+ end
15
+
16
+ def test_logger_warns_if_ssl_strict_disabled
17
+ @poster.logger = stub()
18
+ @poster.logger.expects(:warn).with("PostsDataTest::SSLPoster using ssl_strict=false, which is insecure")
19
+
20
+ Connection.any_instance.stubs(:request)
21
+
22
+ SSLPoster.ssl_strict = false
23
+ @poster.raw_ssl_request(:post, "https://shopify.com", "", {})
24
+ end
25
+
26
+ def test_logger_no_warning_if_ssl_strict_enabled
27
+ @poster.logger = stub()
28
+ @poster.logger.stubs(:warn).never
29
+ Connection.any_instance.stubs(:request)
30
+
31
+ SSLPoster.ssl_strict = true
32
+ @poster.raw_ssl_request(:post, "https://shopify.com", "", {})
33
+ end
34
+
35
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: active_utils
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.3
4
+ version: 1.0.4
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,11 +9,11 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2012-02-13 00:00:00.000000000 Z
12
+ date: 2012-08-09 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: activesupport
16
- requirement: &2164858760 !ruby/object:Gem::Requirement
16
+ requirement: &70122750912260 !ruby/object:Gem::Requirement
17
17
  none: false
18
18
  requirements:
19
19
  - - ! '>='
@@ -21,10 +21,10 @@ dependencies:
21
21
  version: 2.3.11
22
22
  type: :runtime
23
23
  prerelease: false
24
- version_requirements: *2164858760
24
+ version_requirements: *70122750912260
25
25
  - !ruby/object:Gem::Dependency
26
26
  name: i18n
27
- requirement: &2164856640 !ruby/object:Gem::Requirement
27
+ requirement: &70122750911460 !ruby/object:Gem::Requirement
28
28
  none: false
29
29
  requirements:
30
30
  - - ! '>='
@@ -32,10 +32,10 @@ dependencies:
32
32
  version: '0'
33
33
  type: :runtime
34
34
  prerelease: false
35
- version_requirements: *2164856640
35
+ version_requirements: *70122750911460
36
36
  - !ruby/object:Gem::Dependency
37
37
  name: rake
38
- requirement: &2164854980 !ruby/object:Gem::Requirement
38
+ requirement: &70122750910440 !ruby/object:Gem::Requirement
39
39
  none: false
40
40
  requirements:
41
41
  - - ! '>='
@@ -43,10 +43,10 @@ dependencies:
43
43
  version: '0'
44
44
  type: :development
45
45
  prerelease: false
46
- version_requirements: *2164854980
46
+ version_requirements: *70122750910440
47
47
  - !ruby/object:Gem::Dependency
48
48
  name: mocha
49
- requirement: &2164870020 !ruby/object:Gem::Requirement
49
+ requirement: &70122750909780 !ruby/object:Gem::Requirement
50
50
  none: false
51
51
  requirements:
52
52
  - - ! '>='
@@ -54,7 +54,7 @@ dependencies:
54
54
  version: '0'
55
55
  type: :development
56
56
  prerelease: false
57
- version_requirements: *2164870020
57
+ version_requirements: *70122750909780
58
58
  description:
59
59
  email:
60
60
  - developers@jadedpixel.com
@@ -72,6 +72,7 @@ files:
72
72
  - lib/active_utils/common/connection.rb
73
73
  - lib/active_utils/common/country.rb
74
74
  - lib/active_utils/common/error.rb
75
+ - lib/active_utils/common/network_connection_retries.rb
75
76
  - lib/active_utils/common/post_data.rb
76
77
  - lib/active_utils/common/posts_data.rb
77
78
  - lib/active_utils/common/requires_parameters.rb
@@ -83,7 +84,9 @@ files:
83
84
  - test/unit/connection_test.rb
84
85
  - test/unit/country_code_test.rb
85
86
  - test/unit/country_test.rb
87
+ - test/unit/network_connection_retries_test.rb
86
88
  - test/unit/post_data_test.rb
89
+ - test/unit/posts_data_test.rb
87
90
  - test/unit/utils_test.rb
88
91
  - test/unit/validateable_test.rb
89
92
  homepage: http://github.com/shopify/active_utils
@@ -115,6 +118,8 @@ test_files:
115
118
  - test/unit/connection_test.rb
116
119
  - test/unit/country_code_test.rb
117
120
  - test/unit/country_test.rb
121
+ - test/unit/network_connection_retries_test.rb
118
122
  - test/unit/post_data_test.rb
123
+ - test/unit/posts_data_test.rb
119
124
  - test/unit/utils_test.rb
120
125
  - test/unit/validateable_test.rb