semian 0.18.1 → 0.19.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: 8c0cecc8c8e42f223f2cde75e3818ad4bc33897107b6a99186beb48c413c6d30
4
- data.tar.gz: 9202b9e1cf15ff54d68f8d3fe11800ace1624828d1aa840840b88180f65e5355
3
+ metadata.gz: f6bae2b0df6fceb44b91a57900ba40be6b53588866025c269627b08c2ccfa193
4
+ data.tar.gz: f11b8fb67e10cfbb5f1fe393225a45ba0836324768e5fa01366f760151b635b3
5
5
  SHA512:
6
- metadata.gz: 1bd71610d96984ada73fe625966f66ecc13cc921ac659a78c9010eec952f61e1ea56aa4c9f3e6de0572a2b2059e0cd919e1063907c4d39c9d630dd536bf4a89e
7
- data.tar.gz: 20513585134a89dc3a33bcc2895728a64fa5e35e978dbcf83ac44fccc00ae9428e0f8bd8988fe3941e4236d4d18daed380014843b0ad071f945164e68eda68af
6
+ metadata.gz: b39cc0bfcf4341bd0a008f50109abb77f27b75021c28245d4c8e4b2af3bb78ca4611deef872c3b40714d4047a8ede6bcfc88cd426a8866f2e51249f3606aefb4
7
+ data.tar.gz: 89829e6fb002b9d96c2b96ede7339e10e7d3e1c6fee8d8d023b831747e969622d08342e07d7e0cd7ce31f433239e984b881417140e15a9a736501a62c941c79b
data/README.md CHANGED
@@ -254,10 +254,40 @@ The `semian_options` passed apply to that resource. Semian creates the `semian_i
254
254
  from the `name` to look up and store changes in the circuit breaker and bulkhead states
255
255
  and associate successes, failures, errors with the protected resource.
256
256
 
257
- We only require that:
258
- * the `semian_configuration` be **set only once** over the lifetime of the library
259
- * the output of the `proc` be the same over time, that is, the configuration produced by
260
- 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
+ ```
261
291
 
262
292
  For most purposes, `"#{host}_#{port}"` is a good default `name`. Custom `name` formats
263
293
  can be useful to grouping related subdomains as one resource, so that they all
@@ -47,16 +47,16 @@ module Semian
47
47
  super
48
48
  end
49
49
 
50
- def execute(sql, *)
50
+ def raw_execute(sql, *)
51
51
  if query_allowlisted?(sql)
52
52
  super
53
53
  else
54
- acquire_semian_resource(adapter: :trilogy_adapter, scope: :execute) do
54
+ acquire_semian_resource(adapter: :trilogy_adapter, scope: :query) do
55
55
  super
56
56
  end
57
57
  end
58
58
  end
59
- ruby2_keywords :execute
59
+ ruby2_keywords :raw_execute
60
60
 
61
61
  def active?
62
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.1"
4
+ VERSION = "0.19.1"
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.1
4
+ version: 0.19.1
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-05-02 00:00:00.000000000 Z
13
+ date: 2023-05-16 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.4.12
82
+ rubygems_version: 3.4.13
83
83
  signing_key:
84
84
  specification_version: 4
85
85
  summary: Bulkheading for Ruby with SysV semaphores