-
Notifications
You must be signed in to change notification settings - Fork 80
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Support for non-deterministic resource configuration #485
Conversation
8d54d89
to
5db11d7
Compare
lib/semian/net_http.rb
Outdated
def with_cleared_non_deterministic_options | ||
unless @resource_acquisition_in_progress | ||
@resource_acquisition_in_progress = true | ||
resource_acquisition_started = true | ||
end | ||
|
||
yield | ||
ensure | ||
if resource_acquisition_started | ||
unless @raw_semian_options&.fetch(:deterministic, true) | ||
# Clear @raw_semian_options if the resource was flagged as non-deterministic. | ||
@raw_semian_options = nil | ||
end | ||
|
||
acquire_semian_resource(adapter: :http, scope: :query) do | ||
handle_error_responses(super) | ||
@resource_acquisition_in_progress = false |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I honestly was surprised that I couldn't find a simpler implementation. The goal here was to make sure that @raw_semian_options
is cleared at the end of an acquire flow, i.e. after establishing a connection, or after issuing a request
Things started getting a bit messy when I noticed that calls could sometimes be nested, IIRC when executing a request with response = http.request(request)
where http
is the result of Net::HTTP.start
and request
is an instance of Net::HTTP::Get
then transport_request
was called, and then connect
to establish the connection when the keep alive expired
This is why I ended up with this convoluted implementation. resource_acquisition_started
is used as the outermost flag, in that case, the one set from transport_request
, and not the inner call to connect
, so that we only clear @raw_semian_options
once.
I'm also realizing I should probably add tests for this
EDIT: If anyone has a suggestion for a better implementation, because I really don't love this
lib/semian/adapter.rb
Outdated
|
||
symbolized_options = options && options.map { |k, v| [k.to_sym, v] }.to_h | ||
symbolized_options.tap do | ||
@semian_options = symbolized_options if (symbolized_options || {}).fetch(:deterministic, true) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It looks a bit complex line for me.
I think symbolized_options
could be nil
, only when options
is nil
.
In other cases symbolized_options
is a Hash.
Also I have doubts of using tap
in this section of changes.
Can you help me to understand, why tap
is required?
Do you know if it is possible to avoid this block
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed, it does look overly complex.
The reasoning behind tap
was to get an easy way to always return symbolized_options
(regardless of the value for :deterministic
). The alternative version I started with (and ended up refactoring to the tap
version) looked like this:
symbolized_options = options && options.transform_keys(&:to_sym)
@semian_options = symbolized_options if (symbolized_options || {}).fetch(:deterministic, true)
symbolized_options
The symbolized_options || {}
piece was for cases where raw_semian_options
returns false
or nil
.
With all that said, would you prefer a tap
-less, and without the || {}
thingy, like this:
symbolized_options = options && options.transform_keys(&:to_sym)
@semian_options = symbolized_options if !symbolized_options || symbolized_options.fetch(:deterministic, true)
symbolized_options
lib/semian/adapter.rb
Outdated
@@ -53,7 +59,11 @@ def semian_options | |||
return @semian_options if defined? @semian_options | |||
|
|||
options = raw_semian_options | |||
@semian_options = options && options.map { |k, v| [k.to_sym, v] }.to_h | |||
|
|||
symbolized_options = options && options.map { |k, v| [k.to_sym, v] }.to_h |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would it better to symbolize Hash keys on initialization part for all adapters or HTTP one?
My goal here to have less operations.
Probably it is a good task for a future separate PR.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed. Also for a quick symbolize keys:
symbolized_options = options && options.map { |k, v| [k.to_sym, v] }.to_h | |
symbolized_options = options && options.transform_keys(&:to_sym) |
There are no blockers from my side. |
Actually I think there is missing documentation part of the new attribute and Changelog. @pjambet FYI: I keep some small examples how to use semian for net/http adapter - https://github.com/Shopify/semian/tree/c8a1ee0199ef4e983d85da447be5ec3eee86c7fb/examples/net_http . It is possible to update them with new configuration option as part of documentation. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM.
Not sure I'm a bit fine of the deterministic: false
name though. Maybe dynamic: true
or something? No strong opinion here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agree with @casperisfine. Not sure about the deterministic
name, but it's also not a blocker on my end.
@casperisfine I do like @miry I will update the changelog and the examples as you suggested |
483c639
to
bc42b7b
Compare
@@ -53,7 +59,11 @@ def semian_options | |||
return @semian_options if defined? @semian_options | |||
|
|||
options = raw_semian_options | |||
@semian_options = options && options.map { |k, v| [k.to_sym, v] }.to_h | |||
|
|||
symbolized_options = options && options.transform_keys(&:to_sym) # rubocop:disable Style/SafeNavigation |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Rubocop was incorrectly flagging this line with:
Style/SafeNavigation: Use safe navigation (&.) instead of checking if an object exists before calling the method.
But that wouldn't work when options
is false
.
922707f
to
f9154f0
Compare
f9154f0
to
e91ac68
Compare
e91ac68
to
90c3d30
Compare
Just rebased because of the conflicts in |
ed2748a
to
9e9dce8
Compare
This behavior allows the HTTP adapter to call the configuration block on every request. Used in combination with external variables, such as thread local variable, it enables the configuration of a single host/port combination as multiple resources. ```ruby SEMIAN_PARAMETERS = { # ... dynamic: true, } class CurrentSemianSubResource < ActiveSupport::Attributes attribute :name end Semian::NetHTTP.semian_configuration = proc do |host, port| name = "#{host}_#{port}" if (sub_resource_name = CurrentSemianSubResource.name) name << "_#{name}" end SEMIAN_PARAMETERS.merge(name: name) end # Two requests to example.com can use two different semian resources, # as long as `CurrentSemianSubResource.name` is set accordingly: # CurrentSemianSubResource.set(name: "sub_resource_1") { Net::HTTP.get_response(URI("http://example.com")) } # and: # CurrentSemianSubResource.set(name: "sub_resource_2") { Net::HTTP.get_response(URI("http://example.com")) } ```
9e9dce8
to
c3f20ce
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍🏼
Supersedes #479 as a solution for #480
This PR introduces handling for a new resource option,
deterministic
, which defaults totrue
.When
true
, the behavior is identical to the current behavior on master. The logic inAdapter
will memoize the result ofraw_semian_options
, as well as the result ofsemian_resource
, so that a single instance of the host class (the thing that includesSemian::Adapter
, so an instance ofNet::HTTP
, a Redis connection object or a MySQL connection object) will reuse previously fetched options and previously instantiatedSemian::ProtectedResource
objects.When the options includes
deterministic: false
, that memoization is turned off.In practice, it means that calling
Semian::Adapter#acquire_semian_resource
might see different options, and in turn returns a different instance ofSemian::ProtectedResource
.The only use case for this is for the
Net::HTTP
adapter, where the options hash is returned from the block given toSemian::NetHTTP.semian_configuration
. The use case described in #480 can now be achieved with a configuration such as:Because opting out of the memoization behavior would lead to multiple calls to
raw_semian_options
(and the execution of the config block) during a single "acquisition flow", an optimization was made in the net http adapter as to limit the number of executions of the config block to one per flow, with thewith_cleared_non_deterministic_options
method. That way, even non-deterministic options are cached during the execution of a request (or a connection).Benchmark notes
I wasn't sure what the best approach was to benchmark this, but here is a summary of what I did. Note that my goal with this feature was to have no impact on deterministic flows.
I ran the following benchmark script on a host application that uses Semian for Redis, MySQL & Net::HTTP. The Redis & MySQL config have nothing special, the values are configured in the YML files as documented in Semian's README, and the Net::HTTP block was similar to the one above, a default hash merging
#{host}_#{port}
as the value for thename
key.On a 2021 14" Macbook Pro, I got the following results:
With the changes from this branch, I got the following:
This matches my expectations, MySQL & Redis are not impacted by the changes in this branch, but Net::HTTP is. As much as the difference looks significant, ~48k i/s vs 75.2k i/s, I think this is acceptable for this use case. This amounts to ~0.014ms vs 0.021ms, which in practice is likely to be an acceptable performance hit in the context of HTTP requests that take between 10ms and 5,000+ms