network_resiliency 0.5.1 → 0.6.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6bf447ec73846d8911df202dc4a2ba576879a2768cd2a2e817ae9119a0e40506
4
- data.tar.gz: d38481d92461272cbcefd34ea81f2c82d38381e135a06daed527210cdce6a178
3
+ metadata.gz: 9cd120266db1dfb8d2e5b37155534f3d5dc37a560d329352fa1a061ffd19ec97
4
+ data.tar.gz: c09fb2b6dc81e74a838e7da9ae9bc7e4d177c9ba7104a3b8e6ecb6b8668ebf97
5
5
  SHA512:
6
- metadata.gz: 73b6e3fa940f4f8b91e26cc039b2bdfb37f7879a2a4c60ac1cc29e0572e79fd427c4984dfd8a139695e1225a370e295330833d81e250c6b3f922f467fb5f0b00
7
- data.tar.gz: 00a8fe95736393d1e9133ca4d2d0c9873d859a93c53ef487503ad00762eadcaba18bad6c99138910a9bbf4916cc7eae5a464d8324657321b68eaaca076a5a10f
6
+ metadata.gz: 9caeeab2d22c4feea69b03438f8c66588179fe130ef1620935c475654a4661bbba36d82c79b4650fb3bcdead86cf0b5f3e3746513c40d16c8208b6f09ca32e5a
7
+ data.tar.gz: 05ad9ab8d5ec490cf6064cc83c54e9132b2a10f4bf6ab8ca0ab83f94d84db706622c951a49ad0de8e0ac7551ce32963fb86ca7aad5d6f47bb0c33162d3ba0e6d
data/CHANGELOG.md CHANGED
@@ -1,3 +1,8 @@
1
+ ### v0.6.0 (2023-11-21)
2
+ - http resiliency
3
+ - redis resilience
4
+ - spec cleanup
5
+
1
6
  ### v0.5.1 (2023-11-17)
2
7
  - support nil timeouts
3
8
 
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- network_resiliency (0.5.1)
4
+ network_resiliency (0.6.0)
5
5
 
6
6
  GEM
7
7
  remote: https://rubygems.org/
@@ -15,19 +15,41 @@ module NetworkResiliency
15
15
  (instance&.singleton_class || Net::HTTP).ancestors.include?(Instrumentation)
16
16
  end
17
17
 
18
- module Instrumentation
19
- def connect
20
- return super unless NetworkResiliency.enabled?(:http)
21
- original_timeout = self.open_timeout
18
+ ID_REGEX = %r{/[0-9]+(?=/|$)}.freeze
19
+ UUID_REGEX = %r`/\h{8}-\h{4}-(\h{4})-\h{4}-\h{12}(?=/|$)`.freeze
20
+ refine Net::HTTP do
21
+ def normalize_path(path)
22
+ path.gsub(
23
+ Regexp.union(
24
+ NetworkResiliency::Adapter::HTTP::ID_REGEX,
25
+ NetworkResiliency::Adapter::HTTP::UUID_REGEX,
26
+ ),
27
+ '/x',
28
+ )
29
+ end
30
+
31
+ def with_resilience(action, destination, idempotent, &block)
32
+ if action == :connect
33
+ original_timeout = self.open_timeout
34
+ set_timeout = ->(timeout) { self.open_timeout = timeout }
35
+ else
36
+ original_timeout = self.read_timeout
37
+ set_timeout = ->(timeout) { self.read_timeout = timeout }
38
+ end
22
39
 
23
40
  timeouts = NetworkResiliency.timeouts_for(
24
41
  adapter: "http",
25
- action: "connect",
26
- destination: address,
42
+ action: action.to_s,
43
+ destination: destination,
27
44
  max: original_timeout,
28
45
  units: :seconds,
29
46
  )
30
47
 
48
+ unless idempotent
49
+ # only try once, with most lenient timeout
50
+ timeouts = timeouts.last(1)
51
+ end
52
+
31
53
  attempts = 0
32
54
  ts = -NetworkResiliency.timestamp
33
55
 
@@ -35,10 +57,13 @@ module NetworkResiliency
35
57
  attempts += 1
36
58
  error = nil
37
59
 
38
- self.open_timeout = timeouts.shift
60
+ set_timeout.call(timeouts.shift)
61
+
62
+ yield
63
+ rescue ::Timeout::Error,
64
+ defined?(OpenSSL::SSL) ? OpenSSL::OpenSSLError : IOError,
65
+ SystemCallError => e
39
66
 
40
- super
41
- rescue Net::OpenTimeout => e
42
67
  # capture error
43
68
  error = e.class
44
69
 
@@ -47,20 +72,49 @@ module NetworkResiliency
47
72
  raise
48
73
  ensure
49
74
  ts += NetworkResiliency.timestamp
50
- self.open_timeout = original_timeout
75
+ set_timeout.call(original_timeout)
51
76
 
52
77
  NetworkResiliency.record(
53
78
  adapter: "http",
54
- action: "connect",
55
- destination: address,
56
- error: error,
79
+ action: action.to_s,
80
+ destination: destination,
57
81
  duration: ts,
58
- timeout: self.open_timeout.to_f * 1_000,
82
+ error: error,
83
+ timeout: original_timeout.to_f * 1_000,
59
84
  attempts: attempts,
60
85
  )
61
86
  end
62
87
  end
63
88
  end
89
+
90
+ module Instrumentation
91
+ using NetworkResiliency::Adapter::HTTP
92
+
93
+ def connect
94
+ return super unless NetworkResiliency.enabled?(:http)
95
+
96
+ with_resilience(:connect, address, true) { super }
97
+ end
98
+
99
+ def transport_request(req, &block)
100
+ return super unless NetworkResiliency.enabled?(:http)
101
+
102
+ destination = [
103
+ req.method.downcase,
104
+ address,
105
+ normalize_path(req.path),
106
+ ].join(":")
107
+
108
+ idepotent = Net::HTTP::IDEMPOTENT_METHODS_.include?(req.method)
109
+
110
+ retries = self.max_retries
111
+ self.max_retries = 0 # disable
112
+
113
+ with_resilience(:request, destination, idepotent) { super }
114
+ ensure
115
+ self.max_retries = retries
116
+ end
117
+ end
64
118
  end
65
119
  end
66
120
  end
@@ -35,20 +35,44 @@ module NetworkResiliency
35
35
  end
36
36
  end
37
37
 
38
- module Instrumentation
39
- def establish_connection
40
- return super unless NetworkResiliency.enabled?(:redis)
41
-
42
- original_timeout = @options[:connect_timeout]
38
+ IDEMPOTENT_COMMANDS = [
39
+ "exists",
40
+ "expire",
41
+ "get",
42
+ "getex",
43
+ "getrange",
44
+ "mget",
45
+ "mset",
46
+ "ping",
47
+ "scard",
48
+ "sdiff",
49
+ "sdiffstore",
50
+ "set",
51
+ "sismember",
52
+ "smembers",
53
+ "smismember",
54
+ ].freeze
55
+
56
+ refine ::Redis::Client do
57
+ private
58
+
59
+ def with_resilience(action, destination, idempotent, &block)
60
+ timeout_key = action == :connect ? :connect_timeout : :read_timeout
61
+ original_timeout = @options[timeout_key]
43
62
 
44
63
  timeouts = NetworkResiliency.timeouts_for(
45
64
  adapter: "redis",
46
- action: "connect",
47
- destination: host,
48
- max: original_timeout,
65
+ action: action.to_s,
66
+ destination: destination,
67
+ max: @options[timeout_key],
49
68
  units: :seconds,
50
69
  )
51
70
 
71
+ unless idempotent
72
+ # only try once, with most lenient timeout
73
+ timeouts = timeouts.last(1)
74
+ end
75
+
52
76
  attempts = 0
53
77
  ts = -NetworkResiliency.timestamp
54
78
 
@@ -56,10 +80,10 @@ module NetworkResiliency
56
80
  attempts += 1
57
81
  error = nil
58
82
 
59
- @options[:connect_timeout] = timeouts.shift
83
+ @options[timeout_key] = timeouts.shift
60
84
 
61
- super
62
- rescue ::Redis::CannotConnectError => e
85
+ yield
86
+ rescue ::Redis::BaseConnectionError => e
63
87
  # capture error
64
88
 
65
89
  # grab underlying exception within Redis wrapper
@@ -70,19 +94,53 @@ module NetworkResiliency
70
94
  raise
71
95
  ensure
72
96
  ts += NetworkResiliency.timestamp
73
- @options[:connect_timeout] = original_timeout
97
+ @options[timeout_key] = original_timeout
74
98
 
75
99
  NetworkResiliency.record(
76
100
  adapter: "redis",
77
- action: "connect",
78
- destination: host,
101
+ action: action.to_s,
102
+ destination: destination,
79
103
  duration: ts,
80
104
  error: error,
81
- timeout: @options[:connect_timeout].to_f * 1_000,
105
+ timeout: @options[timeout_key].to_f * 1_000,
82
106
  attempts: attempts,
83
107
  )
84
108
  end
85
109
  end
110
+
111
+ def idempotent?(command)
112
+ NetworkResiliency::Adapter::Redis::IDEMPOTENT_COMMANDS.include?(command)
113
+ end
114
+ end
115
+
116
+ module Instrumentation
117
+ using NetworkResiliency::Refinements
118
+ using NetworkResiliency::Adapter::Redis
119
+
120
+ def establish_connection
121
+ return super unless NetworkResiliency.enabled?(:redis)
122
+
123
+ with_resilience(:connect, host, true) { super }
124
+ end
125
+
126
+ def call(command)
127
+ return super unless NetworkResiliency.enabled?(:redis)
128
+ return super unless command.is_a?(Array)
129
+
130
+ command_key = command.first.to_s
131
+
132
+ # larger commands may have larger timeouts
133
+ command_size = command.size.order_of_magnitude
134
+ destination = [
135
+ host,
136
+ command_key,
137
+ (command_size if command_size > 1),
138
+ ].compact.join(":")
139
+
140
+ idempotent = idempotent?(command_key)
141
+
142
+ with_resilience(:request, destination, idempotent) { super }
143
+ end
86
144
  end
87
145
  end
88
146
  end
@@ -1,3 +1,3 @@
1
1
  module NetworkResiliency
2
- VERSION = "0.5.1"
2
+ VERSION = "0.6.0"
3
3
  end
@@ -299,7 +299,7 @@ module NetworkResiliency
299
299
  end
300
300
  end
301
301
 
302
- private
302
+ # private
303
303
 
304
304
  def thread_state
305
305
  Thread.current["network_resiliency"] ||= {}
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: network_resiliency
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.1
4
+ version: 0.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Daniel Pepper
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-11-17 00:00:00.000000000 Z
11
+ date: 2023-11-21 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: byebug