Is Memoist Thread Safe?
benlieb opened this issue · 8 comments
I'm trying to make my app thread-safe to use the Puma server.
I'm using this in my payments code and don't want to get this wrong!
app/models/concerns/user_stripe_payments_module.rb:8: memoize :payment_profile # reload and rememoizes with #payment_profile(true)
app/models/concerns/user_stripe_payments_module.rb:15: def rememoize_payment_profile
@benlieb that depends what you mean by thread-safe.
It doesn't synchronize calls to the cache.
But it depends what objects your memoizing,
and how they're used across threads.
At the end of the day memoist is mostly just the same as taking
def my_method
do_something
end
And changing it to
def my_method
@__my_method ||= do_something
end
If you really wanted to make this "safe"
you'd have to do
def initialize(*args)
@__my_method_mutex = Mutex.new
end
def my_method
@__my_method_mutex.synchronize do
@__my_method ||= do_something
end
end
but that seems crazy
Maybe you can write a minimal example of what you're trying to do,
and I can comment on whether its "safe"
Basically I want to run my app with Puma on Heroku, which requires "thread-safety". Looking through several articles on the subject, both memoizing and using ||= can be issues when aiming for thread safety. I'm not an expert, but the two articles that I read in-depth were:
https://bearmetal.eu/theden/how-do-i-know-whether-my-rails-app-is-thread-safe-or-not/
http://thoughts.codegram.com/understanding-class-instance-variables-in-ruby/
I'm not doing anything fancy, but I just want to ensure that all my gems (not picking on you guys :) are thread-safe. In my case the worse case would be one user somehow making a payment with another user's payment profile, as shown above, but here is the code:
class User < ActiveRecord::Base
extend Memoist
include UserStripePaymentsModule
...
end
# consolidates payment-realted methods, in this case for Stripe
# try to consider method names that are generic to continue to be used if
# stripe is swapped for a different payment processor in future
module UserStripePaymentsModule
extend ActiveSupport::Concern
included do
memoize :payment_profile # reload and rememoizes with #payment_profile(true)
end
def payment_profile
Stripe::Client.instance.customer_get self
end
def rememoize_payment_profile
payment_profile(true)
end
def has_cc_on_file?
!!cc_info
end
def cc_info
return nil unless has_payment_profile?
payment_profile.sources.data.first
end
def cc_last4
cc_info && cc_info.last4
end
def has_payment_profile?
stripe_customer_id.present?
end
end
Ok, so to simplify your example, cut memoist out of it.
class User < ActiveRecord::Base
def payment_profile
@payment_profile ||= Stripe::Client.instance.customer_get self
end
end
So this is memoized for an instance of user
Two threads running on Puma may do something like this:
Thread#1 user = User.find 123; user.payment_profile
Thread#2 user = User.find 123; user.payment_profile
But these two user
objects are different.
So actually this doesn't matter.
What you do need to do is ensure that your classes are loaded at boot
or you could have race conditions where loading user.rb
happened twice, and memoized it twice.
But rails loading handles this for you.
This is a rails app, so the loading issue should be taken care of, or am I wrong?
@benlieb yeah.
An example of how you could mess this up.
user = User.find 123
5.times do
Thread.new do
5.times do
user.payment_profile
user.payment_profile(true)
end
end
end
but that's only because you're using a shared object across threads,
and not synchronizing state.
+1 on having it thread-safe, as in the example provided by @matthewrudy .
It could be optional (like having memoize
and memoize_threadsafe
methods) as I guess having every call protected by a mutex could be harmful performance wise. A developer should know which of its methods should be thread-safe, and not all of them should be. Hence having the possibility to switch this option per memoized method might be good.