active_utils 1.0.3 → 1.0.4

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/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