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 +1 -0
- data/lib/active_utils/common/connection.rb +3 -30
- data/lib/active_utils/common/network_connection_retries.rb +58 -0
- data/lib/active_utils/common/posts_data.rb +2 -0
- data/lib/active_utils/common/utils.rb +2 -9
- data/lib/active_utils/version.rb +1 -1
- data/lib/active_utils.rb +1 -0
- data/test/unit/network_connection_retries_test.rb +127 -0
- data/test/unit/posts_data_test.rb +35 -0
- metadata +15 -10
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 '
|
1
|
+
require 'securerandom'
|
2
2
|
|
3
3
|
module ActiveMerchant #:nodoc:
|
4
4
|
module Utils #:nodoc:
|
5
5
|
def generate_unique_id
|
6
|
-
|
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
|
data/lib/active_utils/version.rb
CHANGED
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.
|
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-
|
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: &
|
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: *
|
24
|
+
version_requirements: *70122750912260
|
25
25
|
- !ruby/object:Gem::Dependency
|
26
26
|
name: i18n
|
27
|
-
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: *
|
35
|
+
version_requirements: *70122750911460
|
36
36
|
- !ruby/object:Gem::Dependency
|
37
37
|
name: rake
|
38
|
-
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: *
|
46
|
+
version_requirements: *70122750910440
|
47
47
|
- !ruby/object:Gem::Dependency
|
48
48
|
name: mocha
|
49
|
-
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: *
|
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
|