semian 0.18.0 → 0.19.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: d24779fb714aedf9f83b9ea263d7a7bd0387bc487dbc2313fed0fa265be33b92
4
- data.tar.gz: a505d6861420fe42f5f366184b73bdeb6aa8f3b4c5ee2a95ceb224f4a6770286
3
+ metadata.gz: 47d9f8254a2231b2ca3210ff09331ace5bd23f86bfe0f178a95e64187d3f1203
4
+ data.tar.gz: 644bcb5a30040499af851b17a9cb46c7a2a4fe0a3896cfd7052bc39e115fb4bc
5
5
  SHA512:
6
- metadata.gz: 0e24a1414fb44190bde478cc02cf4c2f12dde3b73673446dee2225ee0155951be5121dbf077da05ce8d1746485f8ce2914b46a191ba09f05297f70177713d15f
7
- data.tar.gz: d7e41a294ec3819216b4e5762b0ea98ca545002c528e64c291e606c0e1901b847c3d6ee31a4b927c2402b0249fed38faa5b46ecdbb5a4ae9e2ef767fa34702f7
6
+ metadata.gz: b3d5769728bd70009178ad78d1b397b5a067e2482578e0916ae76963870cc9808499a49cd5a22f6a3ffa254cbb203f5dec37584352815cb9ba32a8a281432468
7
+ data.tar.gz: 7ea88141334bed5ecf490f25bd3b8d98759de50f38824af6f0af70619f0688eeacea05c80181a079fb3563d887fc1d1dbc6840893eb4a8c78e729a1c4b498b94
data/README.md CHANGED
@@ -1,5 +1,4 @@
1
- ## Semian ![Build Status](https://github.com/Shopify/semian/actions/workflows/main.yml/badge.svg)
2
-
1
+ ## Semian ![Build Status](https://github.com/Shopify/semian/actions/workflows/test.yml/badge.svg)
3
2
 
4
3
  ![](http://i.imgur.com/7Vn2ibF.png)
5
4
 
@@ -255,10 +254,40 @@ The `semian_options` passed apply to that resource. Semian creates the `semian_i
255
254
  from the `name` to look up and store changes in the circuit breaker and bulkhead states
256
255
  and associate successes, failures, errors with the protected resource.
257
256
 
258
- We only require that:
259
- * the `semian_configuration` be **set only once** over the lifetime of the library
260
- * the output of the `proc` be the same over time, that is, the configuration produced by
261
- each pair of `host`, `port` is **the same each time** the callback is invoked.
257
+ We only require that the `semian_configuration` be **set only once** over the lifetime of
258
+ the library.
259
+
260
+ If you need to return different values for the same pair of `host`/`port` value, you **must**
261
+ include the `dynamic: true` option. Returning different values for the same `host`/`port` values
262
+ without setting the `dynamic` option can lead to undesirable behavior.
263
+
264
+ A common example for dynamic options is the use of a thread local variable, such as
265
+ `ActiveSupport::CurrentAttributes`, for requests to a service acting as a proxy.
266
+
267
+ ```ruby
268
+ SEMIAN_PARAMETERS = {
269
+ # ...
270
+ dynamic: true,
271
+ }
272
+
273
+ class CurrentSemianSubResource < ActiveSupport::Attributes
274
+ attribute :name
275
+ end
276
+
277
+ Semian::NetHTTP.semian_configuration = proc do |host, port|
278
+ name = "#{host}_#{port}"
279
+ if (sub_resource_name = CurrentSemianSubResource.name)
280
+ name << "_#{name}"
281
+ end
282
+ SEMIAN_PARAMETERS.merge(name: name)
283
+ end
284
+
285
+ # Two requests to example.com can use two different semian resources,
286
+ # as long as `CurrentSemianSubResource.name` is set accordingly:
287
+ # CurrentSemianSubResource.set(name: "sub_resource_1") { Net::HTTP.get_response(URI("http://example.com")) }
288
+ # and:
289
+ # CurrentSemianSubResource.set(name: "sub_resource_2") { Net::HTTP.get_response(URI("http://example.com")) }
290
+ ```
262
291
 
263
292
  For most purposes, `"#{host}_#{port}"` is a good default `name`. Custom `name` formats
264
293
  can be useful to grouping related subdomains as one resource, so that they all
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "semian/adapter"
4
- require "activerecord-trilogy-adapter"
4
+ require "active_record"
5
5
  require "active_record/connection_adapters/trilogy_adapter"
6
6
 
7
7
  module ActiveRecord
@@ -47,15 +47,16 @@ module Semian
47
47
  super
48
48
  end
49
49
 
50
- def execute(sql, name = nil, async: false, allow_retry: false)
50
+ def execute(sql, *)
51
51
  if query_allowlisted?(sql)
52
- super(sql, name, async: async, allow_retry: allow_retry)
52
+ super
53
53
  else
54
54
  acquire_semian_resource(adapter: :trilogy_adapter, scope: :execute) do
55
- super(sql, name, async: async, allow_retry: allow_retry)
55
+ super
56
56
  end
57
57
  end
58
58
  end
59
+ ruby2_keywords :execute
59
60
 
60
61
  def active?
61
62
  acquire_semian_resource(adapter: :trilogy_adapter, scope: :ping) do
@@ -9,19 +9,25 @@ module Semian
9
9
  end
10
10
 
11
11
  def semian_resource
12
- @semian_resource ||= case semian_options
12
+ return @semian_resource if @semian_resource
13
+
14
+ case semian_options
13
15
  when false
14
- UnprotectedResource.new(semian_identifier)
16
+ @semian_resource = UnprotectedResource.new(semian_identifier)
15
17
  when nil
16
18
  Semian.logger.info("Semian is not configured for #{self.class.name}: #{semian_identifier}")
17
- UnprotectedResource.new(semian_identifier)
19
+ @semian_resource = UnprotectedResource.new(semian_identifier)
18
20
  else
19
21
  options = semian_options.dup
20
22
  options.delete(:name)
21
23
  options[:consumer] = self
22
24
  options[:exceptions] ||= []
23
25
  options[:exceptions] += resource_exceptions
24
- ::Semian.retrieve_or_register(semian_identifier, **options)
26
+ resource = ::Semian.retrieve_or_register(semian_identifier, **options)
27
+
28
+ @semian_resource = resource unless options.fetch(:dynamic, false)
29
+
30
+ resource
25
31
  end
26
32
  end
27
33
 
@@ -53,7 +59,11 @@ module Semian
53
59
  return @semian_options if defined? @semian_options
54
60
 
55
61
  options = raw_semian_options
56
- @semian_options = options && options.map { |k, v| [k.to_sym, v] }.to_h
62
+
63
+ symbolized_options = options && options.transform_keys(&:to_sym) # rubocop:disable Style/SafeNavigation
64
+ symbolized_options.tap do
65
+ @semian_options = symbolized_options if !symbolized_options || !symbolized_options.fetch(:dynamic, false)
66
+ end
57
67
  end
58
68
 
59
69
  def raw_semian_options
@@ -91,16 +91,20 @@ module Semian
91
91
  end
92
92
 
93
93
  def connect
94
- return super if disabled?
94
+ with_cleared_dynamic_options do
95
+ return super if disabled?
95
96
 
96
- acquire_semian_resource(adapter: :http, scope: :connection) { super }
97
+ acquire_semian_resource(adapter: :http, scope: :connection) { super }
98
+ end
97
99
  end
98
100
 
99
101
  def transport_request(*)
100
- return super if disabled?
102
+ with_cleared_dynamic_options do
103
+ return super if disabled?
101
104
 
102
- acquire_semian_resource(adapter: :http, scope: :query) do
103
- handle_error_responses(super)
105
+ acquire_semian_resource(adapter: :http, scope: :query) do
106
+ handle_error_responses(super)
107
+ end
104
108
  end
105
109
  end
106
110
 
@@ -125,6 +129,24 @@ module Semian
125
129
  end
126
130
  result
127
131
  end
132
+
133
+ def with_cleared_dynamic_options
134
+ unless @resource_acquisition_in_progress
135
+ @resource_acquisition_in_progress = true
136
+ resource_acquisition_started = true
137
+ end
138
+
139
+ yield
140
+ ensure
141
+ if resource_acquisition_started
142
+ if @raw_semian_options&.fetch(:dynamic, false)
143
+ # Clear @raw_semian_options if the resource was flagged as dynamic.
144
+ @raw_semian_options = nil
145
+ end
146
+
147
+ @resource_acquisition_in_progress = false
148
+ end
149
+ end
128
150
  end
129
151
  end
130
152
 
@@ -6,6 +6,14 @@ class Redis
6
6
  BaseConnectionError.include(::Semian::AdapterError)
7
7
  OutOfMemoryError.include(::Semian::AdapterError)
8
8
 
9
+ class ReadOnlyError < Redis::BaseConnectionError
10
+ # A ReadOnlyError is a fast failure and we don't want to track these errors so that we can reconnect
11
+ # to the new primary ASAP
12
+ def marks_semian_circuits?
13
+ false
14
+ end
15
+ end
16
+
9
17
  class SemianError < BaseConnectionError
10
18
  def initialize(semian_identifier, *args)
11
19
  super(*args)
data/lib/semian/redis.rb CHANGED
@@ -32,6 +32,14 @@ class Redis
32
32
  end
33
33
  end
34
34
 
35
+ class ReadOnlyError < Redis::CommandError
36
+ # A ReadOnlyError is a fast failure and we don't want to track these errors so that we can reconnect
37
+ # to the new primary ASAP
38
+ def marks_semian_circuits?
39
+ false
40
+ end
41
+ end
42
+
35
43
  ResourceBusyError = Class.new(SemianError)
36
44
  CircuitOpenError = Class.new(SemianError)
37
45
  ResolveError = Class.new(SemianError)
@@ -14,6 +14,14 @@ class RedisClient
14
14
 
15
15
  OutOfMemoryError.include(::Semian::AdapterError)
16
16
 
17
+ class ReadOnlyError < RedisClient::ConnectionError
18
+ # A ReadOnlyError is a fast failure and we don't want to track these errors so that we can reconnect
19
+ # to the new primary ASAP
20
+ def marks_semian_circuits?
21
+ false
22
+ end
23
+ end
24
+
17
25
  class SemianError < ConnectionError
18
26
  def initialize(semian_identifier, *args)
19
27
  super(*args)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Semian
4
- VERSION = "0.18.0"
4
+ VERSION = "0.19.0"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: semian
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.18.0
4
+ version: 0.19.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Scott Francis
@@ -10,7 +10,7 @@ authors:
10
10
  autorequire:
11
11
  bindir: bin
12
12
  cert_chain: []
13
- date: 2023-02-15 00:00:00.000000000 Z
13
+ date: 2023-05-03 00:00:00.000000000 Z
14
14
  dependencies: []
15
15
  description: |2
16
16
  A Ruby C extention that is used to control access to shared resources
@@ -79,7 +79,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
79
79
  - !ruby/object:Gem::Version
80
80
  version: '0'
81
81
  requirements: []
82
- rubygems_version: 3.3.3
82
+ rubygems_version: 3.4.12
83
83
  signing_key:
84
84
  specification_version: 4
85
85
  summary: Bulkheading for Ruby with SysV semaphores