network_resiliency 0.5.1 → 0.6.1

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: a94a624ababefe0045c52a57be145826d8072f8367d494afdef4892eb81af066
4
+ data.tar.gz: dc1412ead0b212c14d1707236cb76b5d19935ab6d9562cee01ef6a761c70bcb5
5
5
  SHA512:
6
- metadata.gz: 73b6e3fa940f4f8b91e26cc039b2bdfb37f7879a2a4c60ac1cc29e0572e79fd427c4984dfd8a139695e1225a370e295330833d81e250c6b3f922f467fb5f0b00
7
- data.tar.gz: 00a8fe95736393d1e9133ca4d2d0c9873d859a93c53ef487503ad00762eadcaba18bad6c99138910a9bbf4916cc7eae5a464d8324657321b68eaaca076a5a10f
6
+ metadata.gz: 2db8772835afb0358179255e94960499fbfa4bcebcd6bba532e38a0a533b658e9dfd2a94a12de5d7ef29aef372d671e14d04a226b19acddc1621d80617056d7a
7
+ data.tar.gz: b9aa489ff41eecde7aa9bee4e0b8377a8952827dff9a0911272d987bd487fb4b5bdcde02974414bc6ad25b7c6490585282baa4dd189e66a76d5b549743af088f
data/CHANGELOG.md CHANGED
@@ -1,3 +1,12 @@
1
+ ### v0.6.1 (2023-11-21)
2
+ - bugfix
3
+ - redis system errors
4
+
5
+ ### v0.6.0 (2023-11-21)
6
+ - http resiliency
7
+ - redis resilience
8
+ - spec cleanup
9
+
1
10
  ### v0.5.1 (2023-11-17)
2
11
  - support nil timeouts
3
12
 
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.1)
5
5
 
6
6
  GEM
7
7
  remote: https://rubygems.org/
@@ -15,19 +15,44 @@ 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
+
53
+ original_max_retries = self.max_retries
54
+ self.max_retries = 0 # disable
55
+
31
56
  attempts = 0
32
57
  ts = -NetworkResiliency.timestamp
33
58
 
@@ -35,10 +60,13 @@ module NetworkResiliency
35
60
  attempts += 1
36
61
  error = nil
37
62
 
38
- self.open_timeout = timeouts.shift
63
+ set_timeout.call(timeouts.shift)
64
+
65
+ yield
66
+ rescue ::Timeout::Error,
67
+ defined?(OpenSSL::SSL) ? OpenSSL::OpenSSLError : IOError,
68
+ SystemCallError => e
39
69
 
40
- super
41
- rescue Net::OpenTimeout => e
42
70
  # capture error
43
71
  error = e.class
44
72
 
@@ -47,20 +75,45 @@ module NetworkResiliency
47
75
  raise
48
76
  ensure
49
77
  ts += NetworkResiliency.timestamp
50
- self.open_timeout = original_timeout
78
+ set_timeout.call(original_timeout)
79
+ self.max_retries = original_max_retries
51
80
 
52
81
  NetworkResiliency.record(
53
82
  adapter: "http",
54
- action: "connect",
55
- destination: address,
56
- error: error,
83
+ action: action.to_s,
84
+ destination: destination,
57
85
  duration: ts,
58
- timeout: self.open_timeout.to_f * 1_000,
86
+ error: error,
87
+ timeout: original_timeout.to_f * 1_000,
59
88
  attempts: attempts,
60
89
  )
61
90
  end
62
91
  end
63
92
  end
93
+
94
+ module Instrumentation
95
+ using NetworkResiliency::Adapter::HTTP
96
+
97
+ def connect
98
+ return super unless NetworkResiliency.enabled?(:http)
99
+
100
+ with_resilience(:connect, address, true) { super }
101
+ end
102
+
103
+ def transport_request(req, &block)
104
+ return super unless NetworkResiliency.enabled?(:http)
105
+
106
+ destination = [
107
+ req.method.downcase,
108
+ address,
109
+ normalize_path(req.path),
110
+ ].join(":")
111
+
112
+ idepotent = Net::HTTP::IDEMPOTENT_METHODS_.include?(req.method)
113
+
114
+ with_resilience(:request, destination, idepotent) { super }
115
+ end
116
+ end
64
117
  end
65
118
  end
66
119
  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,33 +80,71 @@ 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, SystemCallError => e
63
87
  # capture error
64
88
 
65
89
  # grab underlying exception within Redis wrapper
66
- error = e.cause.class
90
+ error = if e.is_a?(::Redis::BaseConnectionError)
91
+ e.cause.class
92
+ else
93
+ e.class
94
+ end
67
95
 
68
96
  retry if timeouts.size > 0
69
97
 
70
98
  raise
71
99
  ensure
72
100
  ts += NetworkResiliency.timestamp
73
- @options[:connect_timeout] = original_timeout
101
+ @options[timeout_key] = original_timeout
74
102
 
75
103
  NetworkResiliency.record(
76
104
  adapter: "redis",
77
- action: "connect",
78
- destination: host,
105
+ action: action.to_s,
106
+ destination: destination,
79
107
  duration: ts,
80
108
  error: error,
81
- timeout: @options[:connect_timeout].to_f * 1_000,
109
+ timeout: @options[timeout_key].to_f * 1_000,
82
110
  attempts: attempts,
83
111
  )
84
112
  end
85
113
  end
114
+
115
+ def idempotent?(command)
116
+ NetworkResiliency::Adapter::Redis::IDEMPOTENT_COMMANDS.include?(command)
117
+ end
118
+ end
119
+
120
+ module Instrumentation
121
+ using NetworkResiliency::Refinements
122
+ using NetworkResiliency::Adapter::Redis
123
+
124
+ def establish_connection
125
+ return super unless NetworkResiliency.enabled?(:redis)
126
+
127
+ with_resilience(:connect, host, true) { super }
128
+ end
129
+
130
+ def call(command)
131
+ return super unless NetworkResiliency.enabled?(:redis)
132
+ return super unless command.is_a?(Array)
133
+
134
+ command_key = command.first.to_s
135
+
136
+ # larger commands may have larger timeouts
137
+ command_size = command.size.order_of_magnitude
138
+ destination = [
139
+ host,
140
+ command_key,
141
+ (command_size if command_size > 1),
142
+ ].compact.join(":")
143
+
144
+ idempotent = idempotent?(command_key)
145
+
146
+ with_resilience(:request, destination, idempotent) { super }
147
+ end
86
148
  end
87
149
  end
88
150
  end
@@ -1,3 +1,3 @@
1
1
  module NetworkResiliency
2
- VERSION = "0.5.1"
2
+ VERSION = "0.6.1"
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.1
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