network_resiliency 0.5.0 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4eb56ca7bf0ce8610999ebfa3d51ddb42c0d1781fdbb944f305e362c03bb0f9a
4
- data.tar.gz: 9e40b02dfd30f3d60d0983586fe09842fab43a1decc1c419bf6b3b4f640c5613
3
+ metadata.gz: 9cd120266db1dfb8d2e5b37155534f3d5dc37a560d329352fa1a061ffd19ec97
4
+ data.tar.gz: c09fb2b6dc81e74a838e7da9ae9bc7e4d177c9ba7104a3b8e6ecb6b8668ebf97
5
5
  SHA512:
6
- metadata.gz: f872730f5fd3e25bb405b24b5cc0c0e3a807c9c582ca70d8bbc3ee172cba6126433f2a3f7d116a9df059fe08a5f4215856a7e3318d50bc160ebecfd463387aa7
7
- data.tar.gz: 8db779e5dd4fee001e9298806510351d6d0d0748ba0760d37acc9e205b6a425ea22a1f0757d26512a63936020baf764ec266620eb9cd6cbe23745abbb4f8acba
6
+ metadata.gz: 9caeeab2d22c4feea69b03438f8c66588179fe130ef1620935c475654a4661bbba36d82c79b4650fb3bcdead86cf0b5f3e3746513c40d16c8208b6f09ca32e5a
7
+ data.tar.gz: 05ad9ab8d5ec490cf6064cc83c54e9132b2a10f4bf6ab8ca0ab83f94d84db706622c951a49ad0de8e0ac7551ce32963fb86ca7aad5d6f47bb0c33162d3ba0e6d
data/CHANGELOG.md CHANGED
@@ -1,3 +1,11 @@
1
+ ### v0.6.0 (2023-11-21)
2
+ - http resiliency
3
+ - redis resilience
4
+ - spec cleanup
5
+
6
+ ### v0.5.1 (2023-11-17)
7
+ - support nil timeouts
8
+
1
9
  ### v0.5.0 (2023-11-16)
2
10
  - postgres timeout stats
3
11
  - mysql timeout stats
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- network_resiliency (0.5.0)
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 * 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
@@ -36,7 +36,7 @@ module NetworkResiliency
36
36
  destination: host,
37
37
  error: e&.class,
38
38
  duration: ts,
39
- timeout: query_options[:connect_timeout] * 1_000,
39
+ timeout: query_options[:connect_timeout].to_f * 1_000,
40
40
  )
41
41
  end
42
42
  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] * 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.0"
2
+ VERSION = "0.6.0"
3
3
  end
@@ -135,7 +135,7 @@ module NetworkResiliency
135
135
  adapter: adapter,
136
136
  destination: destination,
137
137
  },
138
- )
138
+ ) if timeout && timeout > 0
139
139
 
140
140
  if error
141
141
  NetworkResiliency.statsd&.distribution(
@@ -145,7 +145,7 @@ module NetworkResiliency
145
145
  adapter: adapter,
146
146
  destination: destination,
147
147
  },
148
- ) if timeout
148
+ ) if timeout && timeout > 0
149
149
  else
150
150
  # track successful retries
151
151
  NetworkResiliency.statsd&.increment(
@@ -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.0
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