I might be wrong in understanding the problem, but I believe I would use transactions to solve the problems you're describing. I don't see the need to manage locks manually.
Allen Madsen http://www.allenmadsen.com On Sun, Oct 31, 2010 at 7:28 AM, Rodrigo Rosenfeld Rosas <[email protected] > wrote: > On 30-10-2010 15:55, Aaron Patterson wrote: > >> ... >> >>> When listening to Yehuda's talk about development on the client >>> side, which was really great by the way, I got really worried when >>> he commented about some Rails validations not being concurrent-safe >>> nor even thread-safe. >>> >>> While I can understand it is hard to guarantee uniqueness validation >>> among different server instances, this can be easily avoided in a >>> single server configuration with config.threadsafe! enabled. >>> >>> ... >>> >>> I've just read the documentation for validates_uniqueness_of and it >>> explains well the problem: >>> >>> >>> http://api.rubyonrails.org/classes/ActiveRecord/Validations/ClassMethods.html#method-i-validates_uniqueness_of >>> >>> But I was thinking if Rails could provide some way for avoiding >>> dealing with exceptions when using a single multi-thread server >>> environment. For instance: >>> >>> @record.validate :lock => @shared_lock do >>> # code run if @record.valid? >>> ... >>> @record.save >>> end or (render :edit; return) >>> redirect_to :list >>> ... >>> >>> The :lock parameter should be optional and an internal lock (one per >>> model, maybe) should be used when not specified in the case it is >>> not necessary to share this code with another block which could also >>> affect the record, for instance. >>> >> A shared lock like this would work if you only had one process running. >> It won't work for people that run multiple processes as they won't share >> the lock. >> > > Yes, I know, but for lots of applications, a single multi-thread server > would suffice and Ruby locks are probably much faster than database locks > and don't have the database locks shortcomings. > > For instance, usually I use PostgreSQL. There is no LOCK instruction in > ANSI SQL. PostgreSQL only support locking an entire table, and it is not > possible to name the locks (maybe an alternative could be the > pg_advisory_lock function added in 8.2). So, it is probably better to rely > on another kind of shared locks between different servers, but probably this > won't be as efficient as it can be in a single multi-thread server, even > when using a solution similar to memcached, I mean a socket in-memory lock > server. > > > An error should raise if 'config.threadsafe!' is not enabled and it >>> should be pointed out in the docs that this won't work for multiple >>> servers setup, for avoiding confusing end-developer users. Maybe a >>> warning instead of an error should suffice, for allowing this usage >>> by plugins that don't have control of the deployment decisions. >>> >>> If the user calls 'save' directly without validating first, the >>> validation and save operations shoud be atomic in this case. So, >>> 'save' should also support the :lock parameter. >>> >>> Is this reasonable or am I missing something? >>> >> Even people running multi threaded servers run multiple processes. >> You could use the database connection to create a shared lock. >> >> Though, IMHO if you want to guarantee a unique column, you should add a >> unique index on the column. The uniqueness validation should work most >> of the time, and for edge cases, the user could see an exception. >> > > Yes, the unique index should exist anyway. Maybe it would be great to > create a 'db:create_unique_indexes' Rake task for creating a migration to > add the missing unique indexes to a new migration. But for a single > multi-thread server, such a lock would avoid the exception generation in the > corner case. But my concerns aren't specific to unique validations only. For > instance, imagine a situation where a user needs credits to do something and > the system provides the services 'put_credit' and 'consume_credit'. > > # current user's credit = 5 > > # consume_credit is launched > [consume_service] (render :error; return) unless @user.credit > 0 > [consume_service] do_consume_credit > > # put_credit is launched in another thread > [put_credit_service] @user.credit += 1 # 5 + 1 = 6 > [put_credit_service] @user.save > > [consume_service] @user.credit -= 1 # 5 - 1 = 4 > [consume_service] @user.credit = 4 > > The correct expected user's credit should obviously be 5 instead of 4. For > this simple case, a manual SQL UPDATE would solve this concurrency problem > (UPDATE user SET credit = credit + 1). Also, this situation would probably > be modeled as 'user has_many credits' and this situation wouldn't happen, > but you get the idea. > > The problem could easily be avoided in the above situation with a shared > named lock between the two services. In this case, a normal Ruby lock could > be used instead of modifying Rails API to support locks. But the advantage > of improving the API is that the user can choose if he wants to enable a > shared lock between multiple servers (more expensive) or using a less > expensive Ruby lock in some configuration file that could be changed later. > Also, maybe the Rails community can come with a better implementation of > locking and it would be easier to apply it instead of having to modify all > manually put locking code. > > Maybe we could start with a new Concurrency guide on Rails Guides. > > > If you're really paranoid, you could implement it now with something >> like this (note this is mysql specific): >> >> def shared_lock(name) >> r = AR::Base.connection.execute("SELECT GET_LOCK('#{name}', 2)") >> # ... make sure to check the return value ... >> yield >> ensure >> AR::Base.connection.execute("SELECT RELEASE_LOCK('#{name}')") >> end >> >> > I'm just curious. What would happen in this case (MySQL) if the application > is killed after the lock is acquired but before the ensure block is > executed? The connection would probably be closed. Would this free the lock > too? > > > def some_function >> # this function must calculate a value that you can reproduce across >> # servers and processes >> end >> >> shared_lock(some_function) do >> my_model.save! >> end >> >> Read here for more info: >> >> >> http://dev.mysql.com/doc/refman/4.1/en/miscellaneous-functions.html#function_get-lock >> >> There may be a way to do this in a non-database specific way (a lock >> server or something). >> > > Maybe a lock server of just temporary file locking would fit better. But > then, what would happen again in the case I presented above regarding > killing the app between acquiring and releasing the lock? > > > Best regards, > > > Rodrigo. > > -- > You received this message because you are subscribed to the Google Groups > "Ruby on Rails: Core" group. > To post to this group, send email to [email protected]. > To unsubscribe from this group, send email to > [email protected]<rubyonrails-core%[email protected]> > . > For more options, visit this group at > http://groups.google.com/group/rubyonrails-core?hl=en. > > -- You received this message because you are subscribed to the Google Groups "Ruby on Rails: Core" group. To post to this group, send email to [email protected]. To unsubscribe from this group, send email to [email protected]. For more options, visit this group at http://groups.google.com/group/rubyonrails-core?hl=en.
