Forum: Nitro de-bouncing Submit Order button

Posted by Bill Kelly (Guest)
on 2007-10-10 09:44
(Received via mailing list)
Howdy!

I'm writing an online store, and I don't want mine to be one of
the ones that has to put bold-face warning text by the Submit
button, saying: DANGER: PRESS THIS BUTTON ONLY ONCE!

:)

My current plan is to have any and all POSTs from the Submit
button to redirect to a processing_order page, which uses a
meta refresh every three seconds or so to poll for the final
order completion page.

   Multiple Submit POSTs     does order processing?   redirects to:
     1st                        yes 
processing_order page
     2nd                        no 
processing_order page
     3rd                        no 
processing_order page

Coupled with that, I need a way to indicate that the current
order is in the processing state, and this has to be totally
atomic.

I'm not sure how fancy I need to get.

For example:

  unless session[:order_in_process]
    session[:order_in_process] = true
    # bill credit card
  end

That's essentially what I want to do, but I don't like the
potential race condition there.  I'll be using Mongrel to
host my Nitro app.  My understanding is Mongrel uses ruby
green threads, and I'm not planning to run multiple Mongrel
proxys, just one.  I'm also using the MemorySessionStore.

So as far as I know, all my requests will be handled in the
context of a single ruby process.

If so, I presume I could eliminate the above race condition
with a global mutex...

  do_order_processing = false
  $order_sync_mutex.synchronize {
    unless session[:order_in_process]
      session[:order_in_process] = true
      do_order_processing = true
    end
  }

  if do_order_processing
    # bill credit card
  end

Anyway, I just wanted to ask if my approach seems reasonable,
and/or if I've missed some horrible loophole.


Thanks,

Bill
Posted by Aidan Rogers (Guest)
on 2007-10-10 10:46
(Received via mailing list)
> Howdy!
>
> I'm writing an online store, and I don't want mine to be one of
> the ones that has to put bold-face warning text by the Submit
> button, saying: DANGER: PRESS THIS BUTTON ONLY ONCE!

Why not deactivate the button on click and do the order processing
asynchronously?

Aidan
Posted by Arne Brasseur (Guest)
on 2007-10-10 12:18
(Received via mailing list)
Aidan Rogers schreef:
>> Howdy!
>>
>> I'm writing an online store, and I don't want mine to be one of
>> the ones that has to put bold-face warning text by the Submit
>> button, saying: DANGER: PRESS THIS BUTTON ONLY ONCE!
>>     
>
> Why not deactivate the button on click and do the order processing
> asynchronously?
>   
Good idea but breaks when Javascript is disabled/not available.

(ab)
Posted by Bill Kelly (Guest)
on 2007-10-10 21:29
(Received via mailing list)
From: Arne Brasseur
> From: "Aidan Rogers" <aidan@yoyo.org>
> >>
> >> I'm writing an online store, and I don't want mine to be one of
> >> the ones that has to put bold-face warning text by the Submit
> >> button, saying: DANGER: PRESS THIS BUTTON ONLY ONCE!
> > 
> > Why not deactivate the button on click and do the order processing
> > asynchronously?
>
> Good idea but breaks when Javascript is disabled/not available.

Indeed, I like the idea, thanks.  My store currently uses no Javascript
at all, though.

Also I favor debouncing on the server side in the hopes of allowing
the client to recover from situations like:

  - client clicks submit, but their wireless connection drops
    after the POST, and they get a server timed out

  - client clicks back button and clicks submit again; the server
    sees the POST a second time, but that's fine

I think my proposed approach will handle these situations safely,
but it all depends on all Nitro requests being handled by a single
Ruby process.

That means I have to make sure the http server isn't set up to
proxy requests to multiple Mongrel processes, and such.  (But we
have a low-traffic site, so I don't anticipate needing to scale
to multiple Mongrel processes anytime soon.  If I did, I guess I
could use a filesystem lock instead of a Ruby Mutex.)


Regards,

Bill
Posted by George Moschovitis (Guest)
on 2007-10-10 22:16
(Received via mailing list)
I am not sure I understand your polling idea :(

-g.
Posted by Bill Kelly (Guest)
on 2007-10-11 00:32
(Received via mailing list)
From: George Moschovitis
>
> I am not sure I understand your polling idea :(

The polling idea was a method of presenting an,
"We are processing your order, this may take a moment..."
page, while the server is completing the purchase in an
asynchronous thread.  (Also it was conceived as a way of
eliminating special cases, in that every (possible)
multiple click of the Submit button goes through the same
logic.)

The processing_order page polls "itself" with a

  <meta http-equiv="refresh" content="3" />

and as long as the server is still busy processing the
order, the page remains at the "...this may take a moment."
page.  But when it polls and the order is complete, then
its Controller redirects to a final order complete, or
purchase failed page.

The logic for the refresh page looks like this:

  # This is a meta-refresh holding page... the user can click the Submit 
button on
  # final_review as much as they like, and they'll end up here
  def processing_order
    do_process_order = false
    $order_sync_mutex.synchronize {
      if session[:order_process_state].nil?
        session[:order_process_state] = :in_process
        do_process_order = true
      end
    }

    if do_process_order
      kickoff_async_process_order_and_bill_cc
      sleep 2.0  # if it hasn't completed by this time, we'll show the 
"This may take a moment..." page
    end

    order_state = $order_sync_mutex.synchronize { 
session[:order_process_state] }
    case order_state
    when :complete then redirect :order_complete
    when :fail     then redirect :purchase_error
    else                # continue and display the processing_order meta 
refresh html
    end
  end

So, everytime processing_order is invoked, it checks the
session[:order_process_state] value.  If we haven't begun
processing the order yet, it knows to kick off the async
processing.  Regardless, it always checks the current
session[:order_process_state] at the end, to determine
whether to remain on the "This may take a moment" polling
page, or proceed to the purchase succeeded/failed pages.


Does that make sense and/or seem reasonable?


Regards,

Bill
This topic is locked and can not be replied to.