Skip to content
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

Is Memoist Thread Safe? #45

Open
benlieb opened this issue Mar 31, 2016 · 8 comments
Open

Is Memoist Thread Safe? #45

benlieb opened this issue Mar 31, 2016 · 8 comments

Comments

@benlieb
Copy link

benlieb commented Mar 31, 2016

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

@matthewrudy
Copy link
Owner

@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"

@benlieb
Copy link
Author

benlieb commented Mar 31, 2016

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

@matthewrudy
Copy link
Owner

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.

@benlieb
Copy link
Author

benlieb commented Mar 31, 2016

This is a rails app, so the loading issue should be taken care of, or am I wrong?

@matthewrudy
Copy link
Owner

@benlieb yeah.

@matthewrudy
Copy link
Owner

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.

@Muriel-Salvan
Copy link

+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.

@pboling
Copy link
Contributor

pboling commented Jun 4, 2024

Important

Recommendation

Consider using MemoWise instead, as it is maintained, fully tested, provides thread safety guarantees, and is much, much faster.

Other Alternatives

In case you need a tool with this feature set that is currently maintained, check out:

Tip

Seriously though, read the important note above.

Warning

If you must continue - be aware that this is unmaintained software.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants