Is there a way to get Rails to lock sessions in a multi-process FastCGI environment, so as to handle concurrent requests from the same user without the requests clobbering each other''s session data? I would like to avoid rolling my own session module to handle this, but I don''t know enough about Rails internals to understand how to reuse the current functionality. I have tested in a fastcgi environment that a race condition does happen, which is illustrated by this controller action: def clobber @session[''counter''] ||= 0 @session[''counter''] += 1 sleep(5) render_text "counter: #{@session[''counter'']}" end Try accessing this controller action from two separate Internet Explorer windows within a second or two of each other. (Note: FireFox is smart enough not to send two concurrent requests to the same URL, so to test it in multiple FireFox tabs, put an arbitrary parameter in the query string of each request). If you''re wondering why I''m interested in this, it''s because I want to implement a smart controller action that can handle multiple concurrent submissions of the same form, e.g. when the user clicks the submit button multiple times before receiving a response. (This stemmed from a recent list thread about handling double clicks.) Thanks for any advice. -Josh W.
David Teare
2005-Jul-16 20:00 UTC
Re: Avoiding session race condition in concurrent requests
I had a similar worry re: Rails. In java I simply synchronized on the session object and all was well (assuming a non-clustered environment). But each "thread" in Rails is separate process, and therefore there is nothing to synchronize on (AFAIK). Rails is built on the "share nothing" principle to allow brain-dead easy scalability. To work with this as opposed to against it, I place very little in my session object and instead push everything to the DB; you can then have a version number to detect concurrency issues. HTH --Dave. On 16-Jul-05, at 3:44 PM, Josh Whiting wrote:> Is there a way to get Rails to lock sessions in a multi-process > FastCGI environment, so as to handle concurrent requests from the same > user without the requests clobbering each other''s session data? I > would like to avoid rolling my own session module to handle this, but > I don''t know enough about Rails internals to understand how to reuse > the current functionality. > > I have tested in a fastcgi environment that a race condition does > happen, which is illustrated by this controller action: > > def clobber > @session[''counter''] ||= 0 > @session[''counter''] += 1 > sleep(5) > render_text "counter: #{@session[''counter'']}" > end > > Try accessing this controller action from two separate Internet > Explorer windows within a second or two of each other. (Note: FireFox > is smart enough not to send two concurrent requests to the same URL, > so to test it in multiple FireFox tabs, put an arbitrary parameter in > the query string of each request). > > If you''re wondering why I''m interested in this, it''s because I want to > implement a smart controller action that can handle multiple > concurrent submissions of the same form, e.g. when the user clicks the > submit button multiple times before receiving a response. (This > stemmed from a recent list thread about handling double clicks.) > > Thanks for any advice. > -Josh W. > _______________________________________________ > Rails mailing list > Rails-1W37MKcQCpIf0INCOvqR/iCwEArCW2h5@public.gmane.org > http://lists.rubyonrails.org/mailman/listinfo/rails >
Would using DRb to store the session help solve this problem? David Teare wrote:> I had a similar worry re: Rails. In java I simply synchronized on the > session object and all was well (assuming a non-clustered > environment). But each "thread" in Rails is separate process, and > therefore there is nothing to synchronize on (AFAIK). > > Rails is built on the "share nothing" principle to allow brain-dead > easy scalability. To work with this as opposed to against it, I place > very little in my session object and instead push everything to the DB; > you can then have a version number to detect concurrency issues. > > HTH > --Dave. > > On 16-Jul-05, at 3:44 PM, Josh Whiting wrote: > >> Is there a way to get Rails to lock sessions in a multi-process >> FastCGI environment, so as to handle concurrent requests from the same >> user without the requests clobbering each other''s session data? I >> would like to avoid rolling my own session module to handle this, but >> I don''t know enough about Rails internals to understand how to reuse >> the current functionality. >> >> I have tested in a fastcgi environment that a race condition does >> happen, which is illustrated by this controller action: >> >> def clobber >> @session[''counter''] ||= 0 >> @session[''counter''] += 1 >> sleep(5) >> render_text "counter: #{@session[''counter'']}" >> end >> >> Try accessing this controller action from two separate Internet >> Explorer windows within a second or two of each other. (Note: FireFox >> is smart enough not to send two concurrent requests to the same URL, >> so to test it in multiple FireFox tabs, put an arbitrary parameter in >> the query string of each request). >> >> If you''re wondering why I''m interested in this, it''s because I want to >> implement a smart controller action that can handle multiple >> concurrent submissions of the same form, e.g. when the user clicks the >> submit button multiple times before receiving a response. (This >> stemmed from a recent list thread about handling double clicks.) >> >> Thanks for any advice. >> -Josh W.
Jeremy Weathers
2005-Jul-16 20:57 UTC
Handling multiple button clicks (was: Avoiding session race condition in concurrent requests)
> If you''re wondering why I''m interested in this, it''s because I want to > implement a smart controller action that can handle multiple > concurrent submissions of the same form, e.g. when the user clicks the > submit button multiple times before receiving a response. (This > stemmed from a recent list thread about handling double clicks.)My first line of defense against multiple submit button clicks is to disable the submit button and change the button text to notify the user that the form submit is currently processing. This does not solve the issue you raised of session concurrency - I look forward to seeing what developments are made in that area. Simultaneous form submits in separate browsers (or even multiple clicks of a link) can still cause problems. This method simply prevents most of the unintentional user actions. I find that this procedure fits the vast majority of my forms. When I occasionally have forms that go to (relatively) slow search results or slow-building reports I change the button text but do not disable the button. In these cases, the user can stop the browser request and re-submit the form with different criteria if they so desire without having to reload the page to re-enable the buttons. Here are my JavaScript functions: ======== var originalButtonId = new Array; var originalButtonText = new Array; function disableFormButton(theButtonId, altText, noDisable) { if(document.getElementById(theButtonId)) { originalButtonId.push(theButtonId); originalButtonText.push(document.getElementById(theButtonId).value); document.getElementById(theButtonId).value = (altText) ? altText : "Processing..."; if(!noDisable) { document.getElementById(theButtonId).disabled = true; } } } function restoreFormButton() { for(i = 0; i < originalButtonId.length; i++) { if(document.getElementById(originalButtonId[i])) { document.getElementById(originalButtonId[i]).value originalButtonText[i]; document.getElementById(originalButtonId[i]).disabled false; } } originalButtonId = new Array; originalButtonText = new Array; } ======== Here is an example form page with simple JavaScript validation. If the simple JS validation fails, the buttons are restored and an error displayed. If client-side validation doesn''t make sense for a form, I simply disable the buttons on submit. ======== <script type="text/javascript"> function sendForm() { disableFormButton("formSubmit"); disableFormButton("formCancel", "..."); if("" == theForm.sample_field.value) { alert("ERROR\nPlease enter a ''Sample''."); restoreFormButton(); return false; } return true; } function cancelForm() { disableFormButton("formCancel"); disableFormButton("formSubmit"); document.location = "/"; } </script> <form name="theForm" method="post" onsubmit="return sendForm();"> Sample: <input type="text" name="sample_field"><br> <input type="button" value="Cancel" onclick="cancelForm"> <input type="submit" value="Send Sample" id="formSubmit"> </form> ======== -- Jeremy Weathers Some mornings it just doesn''t seem worth it to gnaw through the leather straps. - Emo Phillips
Jeremy Kemper
2005-Jul-17 00:18 UTC
Re: Avoiding session race condition in concurrent requests
On Jul 16, 2005, at 1:00 PM, David Teare wrote:> I had a similar worry re: Rails. In java I simply synchronized on > the session object and all was well (assuming a non-clustered > environment). But each "thread" in Rails is separate process, and > therefore there is nothing to synchronize on (AFAIK). > > Rails is built on the "share nothing" principle to allow brain-dead > easy scalability. To work with this as opposed to against it, I > place very little in my session object and instead push everything > to the DB; you can then have a version number to detect concurrency > issues.Indeed, if you use ActiveRecordStore, add a lock_version column to your sessions table to get optimistic locking for free. This adds a bit of complexity to your code, however, since you need to manually save the session in order to rescue potential stale object errors when the lock fails. def clobber session[:counter] ||= 0 session[:counter] += 1 # This is a CGI::Session instance (part of the Ruby standard library) # so we have access to all its methods. The update method saves the # session data. session.update sleep 5 render :text => "counter: #{session[:counter]}" rescue ActiveRecord::StaleObjectError # Lock failed due to version number mismatch. # In order to retry we''d need a way to cleanly # reload the session. Settle for a redirect. redirect_to :action => :clobber end Handling failed locks is always a difficult problem, particular for the user. Take a look at Michael Schuerig''s BoilerPlate library; he''s done some nice work catching, displaying, and resolving conflicts due to concurrent access. Best, jeremy
Josh Whiting
2005-Jul-18 21:13 UTC
Re: Avoiding session race condition in concurrent requests
On 7/16/05, Jeremy Kemper <jeremy-w7CzD/W5Ocjk1uMJSBkQmQ@public.gmane.org> wrote:> Indeed, if you use ActiveRecordStore, add a lock_version column to > your sessions table to get optimistic locking for free. This adds[..]> Handling failed locks is always a difficult problem, particular for > the user. > Take a look at Michael Schuerig''s BoilerPlate library; he''s done some > nice work > catching, displaying, and resolving conflicts due to concurrent access.There are a number of problems with the optimistic locking approach, not the least of which is that you don''t know there''s a failure until the end when you try to save the data, instead of at the beginning. By that time you might have already done some side effect stuff that you shouldn''t have otherwise done. Another problem is the need to show the user some kind of error, when really all they need to see is the (successful) results of the first/earlier request. I was hoping for a way to actually implement a lock on the session level, e.g. to force a concurrent request to wait until previous request(s) had released their locks before even reading the session data. (Like a "select ... for update" in an sql transaction.) This would allow concurrent requests to know upon startup that they''re a duplicate of a previous one and simply redirect to the (known) results of the earlier request. I''m probably going to be building a custom database session layer and would like to smoothly replace ActionPack''s session support with it. Any initial pointers or tips to help me ford this river? Thanks for your help so far! -josh
Jeremy Kemper
2005-Jul-19 05:08 UTC
Re: Avoiding session race condition in concurrent requests
On Jul 18, 2005, at 2:13 PM, Josh Whiting wrote:> I was hoping for a way to actually implement a lock on the session > level, e.g. to force a concurrent request to wait until previous > request(s) had released their locks before even reading the session > data. (Like a "select ... for update" in an sql transaction.) This > would allow concurrent requests to know upon startup that they''re a > duplicate of a previous one and simply redirect to the (known) results > of the earlier request. > > I''m probably going to be building a custom database session layer and > would like to smoothly replace ActionPack''s session support with it. > Any initial pointers or tips to help me ford this river? Thanks for > your help so far!The ActiveRecordStore backend for CGI::Session allows you to plug in custom session classes. See session/active_record_store.rb in Action Pack. The default session class is a normal Active Record class named Session. An alternative, SqlBypass, is provided which duck-types with the AR class but uses straight SQL over the db connection. You can use it as a starting point for pessimistic locking. class MyPessimisticSession < CGI::Session::ActiveRecordStore::SqlBypass # Pick a database connection for straight SQL access. self.connection = ActiveRecord::Base.connection # Override the finder class method to do a SELECT ... FOR UPDATE. def self.find_by_session_id(session_id) @@connection.select_one "..." end end # Use our custom session class. CGI::Session::ActiveRecordStore.session_class = MyPessimisticSession # Use the ActiveRecordStore for CGI sessions. ActionController::CgiRequest::DEFAULT_SESSION_OPTIONS.update( :database_manager => CGI::Session::ActiveRecordStore ) As a footnote, I believe the default session backend (PStore) serializes access since it uses file locking. Perhaps it''s worth a second look? Best, jeremy
Josh Whiting
2005-Jul-19 23:30 UTC
Re: Avoiding session race condition in concurrent requests
On 7/19/05, Jeremy Kemper <jeremy-w7CzD/W5Ocjk1uMJSBkQmQ@public.gmane.org> wrote:> The ActiveRecordStore backend for CGI::Session allows you to plug in > custom session classes. See session/active_record_store.rb in > Action Pack. The default session class is a normal Active Record class > named Session. An alternative, SqlBypass, is provided which duck-types > with the AR class but uses straight SQL over the db connection. You > can use it as a starting point for pessimistic locking.Excellent - thank you.> As a footnote, I believe the default session backend (PStore) serializes > access since it uses file locking. Perhaps it''s worth a second look?That''s what I would have thought myself. I haven''t looked at the source code for PStore, but my testing w/ multi-pricess rails+fastcgi+lighttpd shows that the requests happen concurrently and do indeed overwrite each other''s session store (see my original parent post). However, I might have done something wrong. How might I verify the behavior? If I don''t have to write a custom handler, I don''t want to... -Josh Whiting