7

I have a strange problem with my rails application. My application accept duplicate POST requests within one second

This duplicate request, containing the same data, strangely able to bypass the uniqueness validation of my model. This results in creation of two rows of data with exact same contents.

What really baffled me is that it only happened once a day, starting yesterday, I am not sure what caused this. ( The system are already live, and being used by my clients, this method call is used 200-300 times a day, and I cannot reproduce this at all )

So here's the situation with my code snippent and link to the full code, with chronological order

  1. A Users want to create a new transaction, will call this method on the controller

    def new  
      @penjualan = Penjualan.new  
      @penjualan.kode_transaksi = "J"+ DateTime.now.strftime("%d%m%Y%H%M%S")+@active_user.id.to_s  
      @customers = Customer.all(:limit => cookies[:limit], :order=>:kode_kustomer )  
      @barangs = Barang.all(:limit => cookies[:limit] )  
      respond_to do |format|  
        format.html # new.html.erb  
        format.json { render json: @penjualan }  
      end     
    end  
    

    full controller on http://pastebin.com/Lmp7hncn line 648

  2. On the 'new' view, I have disabled the button with :disable_with, so that user cannot click on submit button twice, preventing user initiated double POST request

    .row  
      .span4  
        = f.submit 'Proses', :class=>"btn btn-large btn-primary", :disable_with => "Processing..."
    

    full view on http://pastebin.com/7b9W68RY line 97

  3. The Submitted request will call the 'create' method on controller, the same controller as #1, This method is called twice on 1 second difference. Even more strange is that this request bypass the uniqueness validation that I defined on the model, where it is supposed to fail the second request for having the same kode_transaksi as the first request

  4. I have a uniqueness constraints on my model (Penjualan) attributes (kode_transaksi)

    class Penjualan < ActiveRecord::Base  
      attr_accessible :customer_id, :jatuh_tempo, :kode_transaksi, :no_sj, :tanggal_bayar, :tanggal_transaksi, :total,:total_diskon, :ongkos, :user_id, :status_pembayaran, :is_returned, :kartu_kredit, :kartu_debit  
      has_many :detil_penjualans  
      attr_accessible :cash_total, :kembali  
      belongs_to :user  
      belongs_to :customer  
    
      validates :kode_transaksi, :uniqueness =>{:message=>"Transaksi Sudah Terjadi"}  
    
      scoped_search :on => [:kode_transaksi, :tanggal_transaksi, :status_pembayaran, :tanggal_bayar, :jatuh_tempo, :total ]  
      scoped_search :in => :customer, :on => [:nama_kustomer, :kode_kustomer]  
      scoped_search :in => :user, :on => [:username]  
    end  
    
  5. My Production log with snippet of the case

    Started POST "/penjualans" for 192.168.1.104 at 2012-11-24 12:15:40 +0900   
    Processing by PenjualansController#create as HTML     
    Parameters: {.... too long, see below ....}  
    
    
    Started POST "/penjualans" for 192.168.1.104 at 2012-11-24 12:15:41 +0900   
    Processing by PenjualansController#create as HTML     
    Parameters: {..... too long, see below ....}   
    Redirected to url/penjualans/17403   
    Completed 302 Found in 378ms (ActiveRecord: 246.0ms)   
    Redirected to url/penjualans/17404   
    Completed 302 Found in 367ms (ActiveRecord: 233.8ms)
    

Snippet of the logs http://pastebin.com/3tpua9gi

  1. This situation created a duplicate entry on my database which causes problem

I am really baffled with this behaviour and I'm at my wits end. Any help will be much appreciated.

Holger Just
  • 52,918
  • 14
  • 115
  • 123
Steven St
  • 483
  • 1
  • 8
  • 14
  • In old versions of :disable_with there is a "this.form.submit" in the "onclick" in the html that Rails generate, if you use :disable_with. Don't know if it's still like that, or if it can explain your problem. If the user submit with instead of clicking the button I think the form will submit anyway (while it's not disabled). Can there be a difference if they use or click the submit-button? – 244an Nov 26 '12 at 13:50

2 Answers2

7

To quickly fix the problem I'd suggest you add a unique constraint to the database besides the model.

The rails docs suggest that uniqueness validation should be accompanied by a unique constraint in the database to prevent issues with two connections inserting the same unique value at the same time.

Other than that, is there maybe a problem with a user double-clicking the form in rapid succession? Maybe the disabling of the form does not work correctly and is therefore allowing users to click twice?

Is it every day at the same time or only at specific times?

Tigraine
  • 23,358
  • 11
  • 65
  • 110
  • Hmm, I'll add the constraints to the database now. It has only happened twice, today and yesterday at a seemingly random time. The system has been on production mode for two months, and no modifications are made, this situation really baffled me. – Steven St Nov 26 '12 at 12:26
  • Yes really weird.. the constraint should prevent the issue from appearing again, but maybe keep an eye on the logs to see if it happens again. It could also be that by some random chance one request was dispatched to two worker processes in your web server.. No idea how that can happen but who knows.. – Tigraine Nov 26 '12 at 12:32
  • 4
    Nginx when working as a reverse-proxy (e.g. in front of Thins or Unicorns) sometimes re-dispatches requests to an when certain errors occur on the backend. In violation of RFC 2616, it even does that for POST and PUT requests that are definitely not meant to be redispatched... Thus you really have to make sure your DB constraint are sound. – Holger Just Nov 26 '12 at 12:38
  • Will get back here tomorrow and post the results, Thanks for responding @Tigraine – Steven St Nov 26 '12 at 12:38
  • Adding Unique constraints solved the problem, though seeing the logs, double POST requests are still being made. Probably due to what Holger Just explained – Steven St Nov 27 '12 at 14:45
3

The issue is caused by the way in which the model-based uniqueness constraints are implemented in Rails. Basically, they work by asking the database if there are any existing rows for the given the uniqueness constraint and refusing the create the object if that is the case.

However, given the commonly used transactional isolation levels (typically repeatable-read) you can have overlapping transactions which both successfully check the constraint and then insert their objects without knowing from each other.

That is because to achieve actual uniqueness, you have to define your constraint in the database using UNIQUE indexes. This is much more important than defining the constraints in your model as only the database is able to ensure actual uniqueness by checking the constraint as a row is actually inserted/updated during multi-threaded operation.

About the only reason why you still want to additionally define the constraint in Ruby is that its error messages are much more friendly and you can thus handle the common case.

If the database constraint is hit rather than the Rails constraint, you'll just get false back when calling save without much information what went wrong except for a failed database constraint. The upside is however that you are guaranteed to still have a consistent database afterwards.

Holger Just
  • 52,918
  • 14
  • 115
  • 123
  • Hope it's ok with a question to that, what if you wrap the create in a transaction and uses some kind of db lock, will that solve the problem? Not saying it's a better solution, only want to know if it works. – 244an Nov 26 '12 at 12:43
  • @244an That's more or less that transactions do. And while you can do that by hand, it is probably easier (and more performant) to chose the right transaction isolation level for your needs (which basically is a tradeoff between consistency and performance) and defining the required data constraints. – Holger Just Nov 26 '12 at 12:47