Mutex for Activerecord Model

Mutex for ActiveRecord Model

I found a gem Remote lock when searching for a solution for my problem. It is a mutex solution that uses Redis in the backend.

It:

  • is accessible for all processes
  • does not lock the database
  • is in memory -> fast and no IO

The method looks like this now

def nasty
$lock = RemoteLock.new(RemoteLock::Adapters::Redis.new(REDIS))
$lock.synchronize("capi_lock_#{user_id}") do
http_request_1
http_request_2
update_user
end
end

Mutex for Rails Processes

If you just need to prevent multiple writers from working with a file simultaneously, you can use the File#flock method to request an exclusive write lock from each process:

fh = File.new("/some/file/path")
begin
fh.flock(File::LOCK_EX)
# ... write to the file here, or perform some other critical operation
ensure
fh.flock(File::LOCK_UN)
end

Note: putting the unlock call in an ensure block is important to prevent deadlock if an uncaught exception is thrown after you've locked the file.

Mutex in rails API among several client request

A standard ActiveRecord transaction will prevent deadlock, or other contention. You might have to use a pessimistic locking strategy. Databases are designed to prevent exactly this kind of thing.

Rails / ActiveRecord: avoid two threads updating model at the same time with locks

Thanks Robert for the Optimistic Locking info, I could definitely see me going that route, but Optimistic locking works by raising an exception on the moment of writing to the database (SQL UPDATE), and I have a lot of complex business logic that I wouldn't even want to run with the stale data in the first place.

This is how I solved it, and it was simpler than what I imagined.

First of all, I learned that pessimistic locking DOES NOT preventing any other threads from reading that database row.

But I also learned that with_lock also initiates the lock immediately, regardless of you trying to make a write or not.

So if you start 2 rails consoles (simulating two different threads), you can test that:

  • If you type ModelName.last.with_lock { sleep 30 } on Console 1 and ModelName.last on Console 2, Console 2 can read that record immediately.

  • However, if you type ModelName.last.with_lock { sleep 30 } on Console 1 and ModelName.last.with_lock { p 'I'm waiting' } on Console 2, Console 2 will wait for the lock hold by console 1, even though it's not issuing any write whatsoever.

So that's a way of 'locking the read': if you have a piece of code that you want to be sure that it won't be run simultaneously (not even for reads!), begin that method opening a with_lock block and issue your model reads inside it that they'll wait for any other locks to be released first. If you issue your reads outside it, your reads will be performed even tough some other piece of code in another thread has a lock on that table row.

Some other nice things I learned:

  • As per rails documentation, with_lock will not only start a transaction with a lock, but it will also reload your model for you, so you can be sure that inside the block ModelName.last is on it's most up-to-date state, since it issues a .reload on that instance.

  • That are some gems designed specifically to block the same piece of code running at the same time in multiple threads (which I believe the majority of every Rails app is while in production environment), regardless of the database lock. Take a look at redis-mutex, redis-semaphore and redis-lock.

  • That are many articles on the web (I could find at least 3) that state that Rails with_lock will prevent a READ on the database row, while we can easily see with the tests above that's not the case. Take care and always confirm information testing it yourself! I tried to comment on them warning about this.

Does this code require a Mutex to access the @clients variable thread safely?

Any data that may be modified by one thread and read by a different one must be protected by a Mutex or a similar synchronization construct. Inasmuch as multiple threads may safely read the same data at the same time, a synchronization construct a bit more sophisticated than a single Mutex might yield better performance.

In your code, it looks like not only does @clients need to be properly synchronized, but so also do all its elements because writing to a socket is a modification.



Related Topics



Leave a reply



Submit