-
Notifications
You must be signed in to change notification settings - Fork 548
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
Allow StripeClient to be configured per instance #968
Conversation
24f6a12
to
e012990
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.
Amazing @joeltaylor!! Left a couple comments below (Rubocop's probably going to make you change some things back), but this looks great.
lib/stripe/stripe_client.rb
Outdated
when Stripe::ConnectionManager | ||
{} | ||
when String | ||
{ api_key: config_overrides } |
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.
Is this just a convenience use? I wonder if we should just make them be explicit.
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 added this based on a recommendation from @ob-stripe (#954 (comment)). I'm happy to go either way on it.
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.
Ah oops. Admittedly, this was a bit of a bike shed opinion, so okay, happy to go either way.
current_thread_context.default_connection_manager ||= begin | ||
connection_manager = ConnectionManager.new | ||
connection_manager = ConnectionManager.new(config) |
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.
There's a potential for a bug here if a StripeClient
starts making requests with one configuration, then another is initialized on the same thread with a different configuration, but still inherits the ConnectionManager
of the first client.
I'm seeing two possibilities:
- Error if we detect that configuration is different. This has the substantial downside though of not allowing two active
StripeClients
with different configurations to share the same thread. - Change
ThreadContext#default_connection_manager
to a map, and key connect managers to a map keyed to configuration. e.g.current_thread_context.default_connection_managers[config] = ...
.
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.
Aye, that's a great catch. I completely missed ||=
there.
I'm in favor of option two. Seems like it would better satisfy the type of functionality that's requested in #966.
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.
@brandur-stripe I've implemented the second approach and added a couple of specs to capture any regressions for the bug.
At first, I used the configuration's object_id
to key the connection managers, but realized that could potentially increase the memory footprint when configuration objects are identical and aren't eligible for manual GC. I decided to opt for a deterministic key here:
stripe-ruby/lib/stripe/stripe_configuration.rb
Lines 188 to 193 in 190052b
def key | |
@key ||= begin | |
instance_variables | |
.collect { |variable| instance_variable_get(variable) } | |
.join | |
end |
An alternative would be to use Marshal.dump
, but I'm a bit skeptical about the performance (I haven't benchmarked, but I'm more than happy to.)
bd37e7e
to
2da0ffa
Compare
2da0ffa
to
df32b93
Compare
def self.retrieve(id = ARGUMENT_NOT_PROVIDED, opts = {}) | ||
id = if id.equal?(ARGUMENT_NOT_PROVIDED) | ||
nil | ||
else | ||
Util.check_string_argument!(id) | ||
end |
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.
This PR is a chunk of work pulled out of #954. For context on this change:
ARGUMENT_NOT_PROVIDED doesn't play very well with how we merge options. The args object in method_missing is an array and merging options when an ID is omitted results in [nil, {}] being sent to the resource, which isn't desirable.
However, I wasn't able to re-create any of the scenarios that warranted the addition of it in c269e9b and believe it's safe for removal.
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.
Thx for checking those. Not sure if it there was something different going on when we originally put this in, but re-reading it again, it looks safe to take it out `ARGUMENT_NOT_PROVIDED.
df32b93
to
190052b
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.
@joeltaylor This is beautiful! Thanks. Left a couple more minor comments around the connection manager pruning process, but thank you so much for modifying that to make it multi-client friendly it.
lib/stripe/stripe_client.rb
Outdated
@@ -345,6 +366,17 @@ class ThreadContext | |||
# because that's wrapped in an `ensure` block, they should never leave | |||
# garbage in `Thread.current`. | |||
attr_accessor :last_responses | |||
|
|||
# A default `ConnectionManager` for the thread. Normally shared between |
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.
Mind updating this comment to just refer to these in the plural?
lib/stripe/stripe_client.rb
Outdated
next if connection_manager.last_used > last_used_threshold | ||
|
||
connection_manager.clear | ||
thread_context.reset_connection_managers |
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.
Shouldn't we only remove the connection manager that we're currently looping over rather than all the ones on the thread?
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.
Good call! I'll update this to clear the single connection manager.
lib/stripe/stripe_client.rb
Outdated
|
||
connection_manager.clear | ||
thread_context.reset_connection_managers | ||
pruned_thread_contexts << thread_context |
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.
And similarly, I wonder if we should only prune the thread context once all connect managers in it have confirmed to not be in use. This might be easiest to do by just introducing a second loop that runs after we prune all eligible connection managers.
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.
Sounds reasonable to me. I'll get that update added 👍
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.
Excellent! Thx again.
190052b
to
588dd93
Compare
@@ -9,19 +9,35 @@ module Stripe | |||
class StripeClient | |||
# A set of all known thread contexts across all threads and a mutex to | |||
# synchronize global access to them. | |||
@thread_contexts_with_connection_managers = [] | |||
@thread_contexts_with_connection_managers = Set.new |
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.
Updated this to be a Set so prevent duplicate thread contexts from being added.
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.
Good idea.
This changes allows for each instance of StripeClient to have its own configuration object instead of relying on the global config. Each instance can be configured to override any global config values previously set.
588dd93
to
344fa7a
Compare
assert_equal({}, StripeClient.current_thread_context.default_connection_managers) | ||
end | ||
|
||
should "only garbage collect when all connection managers for a thread are expired" do |
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.
@brandur-stripe added this spec to capture the behavior of waiting for all connection managers to be expired before GCing a thread_context
.
Side note: I got bit pretty hard when writing this spec because I created the second StripeClient
with the config override {read_timeout: 100}
without realizing that some configuration options will call StripeClient.clear_all_connection_managers
. I wonder if that's something worth pointing out in the README since it could adversely affect those trying to get bleeding performance?
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.
Thanks! And very nice — the connect manager management logic looks great now.
Side note: I got bit pretty hard when writing this spec because I created the second
StripeClient
with the config override{read_timeout: 100}
without realizing that some configuration options will callStripeClient.clear_all_connection_managers
. I wonder if that's something worth pointing out in the README since it could adversely affect those trying to get bleeding performance?
Oof :/ Actually, the fact that this happens now should be considered a bug. The reason that setting those configurations clears config is that we previously only had a single configuration object, and when people set a new global configuration like Stripe.api_base =
, it was appropriate to clear out any existing clients. Now that we have specific client configurations, the preferable way of doing that instead would be just to create a new StripeClient
with a different configuration. I suppose we could also support changing configuration per-client, but in that case we should only clear out connect managers relevant to that specific config object.
I'll bring in your PR first and take a stab at fixing this separately.
Thanks again for helping to get this in Joel! |
Follows up #968. As a relic from when we had global configuration, anytime any config value is changed on any client, we still clear all connection managers everywhere on every thread, even though this is not necessary. This means that we lose all open connections, etc. Here, we make changes so that if a configuration is changed, we only clear the configuration managers pertaining to that one particular configuration, thus conserving resources globally.
Follows up #968. As a relic from when we had global configuration, anytime any config value is changed on any client, we still clear all connection managers everywhere on every thread, even though this is not necessary. This means that we lose all open connections, etc. Here, we make changes so that if a configuration is changed, we only clear the configuration managers pertaining to that one particular configuration, thus conserving resources globally.
Follows up #968. As a relic from when we had global configuration, anytime any config value is changed on any client, we still clear all connection managers everywhere on every thread, even though this is not necessary. This means that we lose all open connections, etc. Here, we make changes so that if a configuration is changed, we only clear the configuration managers pertaining to that one particular configuration, thus conserving resources globally.
…971) Follows up #968. As a relic from when we had global configuration, anytime any config value is changed on any client, we still clear all connection managers everywhere on every thread, even though this is not necessary. This means that we lose all open connections, etc. Here, we make changes so that if a configuration is changed, we only clear the configuration managers pertaining to that one particular configuration, thus conserving resources globally. Co-authored-by: Brandur <[email protected]>
This is just a cosmetic change that renames `Stripe.configuration` to just `Stripe.config`. We use the shorter "config" in most other places including `StripeClient#config`, so I feel that this is overall more consistent. This change is backwards compatible because the new accessor came in with #968, and that hasn't been given a formal release yet. I've left the class name as `StripeConfiguration` which IMO is fine. The class uses the expanded form of the name while vars and accessors use the shorter `config`. Also, `StripeConfiguration` has been around a little bit longer, so renaming it is somewhat backwards incompatible too.
This is just a cosmetic change that renames `Stripe.configuration` to just `Stripe.config`. We use the shorter "config" in most other places including `StripeClient#config`, so I feel that this is overall more consistent. This change is backwards compatible because the new accessor came in with #968, and that hasn't been given a formal release yet. I've left the class name as `StripeConfiguration` which IMO is fine. The class uses the expanded form of the name while vars and accessors use the shorter `config`. Also, `StripeConfiguration` has been around a little bit longer, so renaming it is somewhat backwards incompatible too.
Released as 5.31.0. |
This changes allows for each instance of
StripeClient
to have its ownconfiguration object instead of relying on the global config. Each
instance can be configured to override any global config values
previously set.