2012-06-26 2 views
9

Mit meiner API können Benutzer bestimmte Unikate kaufen, wobei jeder Artikel nur an einen Benutzer verkauft werden kann. Wenn also mehrere Benutzer versuchen, den gleichen Artikel zu kaufen, sollte ein Benutzer die Antwort erhalten: ok und der andere Benutzer sollte die Antwort erhalten too_late.Multi-Threaded gleichzeitige Capybara Anfragen?

Jetzt scheint es einen Fehler in meinem Code zu geben. Eine Wettlaufbedingung. Wenn zwei Benutzer versuchen, den gleichen Artikel zur gleichen Zeit zu kaufen, erhalten beide die Antwort ok. Das Problem ist in der Produktion eindeutig reproduzierbar. Jetzt habe ich einen einfachen Test geschrieben, dass es über rspec zu reproduzieren versucht:

context "when I try to provoke a race condition" do 
    # ... 

    before do 
    @concurrent_requests = 2.times.map do 
     Thread.new do 
     Thread.current[:answer] = post "/api/v1/item/buy.json", :id => item.id 
     end 
    end 

    @answers = @concurrent_requests.map do |th| 
     th.join 
     th[:answer].body 
    end 
    end 

    it "should only sell the item to one user" do 
    @answers.sort.should == ["ok", "too_late"].sort 
    end 
end 

Es scheint, wie nicht ausgeführt, die Abfragen zur gleichen Zeit. Um dies zu testen, habe ich den folgenden Code in meine Controller-Aktion:

puts "Is it concurrent?" 
sleep 0.2 
puts "Oh Noez." 

Erwartete Ausgabe wäre, wenn die Anforderungen sind gleichzeitig:

Is it concurrent? 
Is it concurrent? 
Oh Noez. 
Oh Noez. 

Allerdings bekomme ich folgende Ausgabe:

Is it concurrent? 
Oh Noez. 
Is it concurrent? 
Oh Noez. 

Was mir sagt, dass Capybara Anfragen nicht zur gleichen Zeit, aber nacheinander ausgeführt werden. Wie stelle ich meine CapaBara-Anfragen gleichzeitig?

+0

Ihr Codebeispiel oben sieht für mich nicht wie das aktuelle Capybara DSL aus. Es sieht eher wie ein einfacher Reglertest mit Rack :: Test aus. Ist es das, was es ist? –

Antwort

13

Multithreading und Capybara funktioniert nicht, weil Capabara einen separaten Server-Thread verwendet, der die Verbindung sequentiell behandelt. Aber wenn Sie Gabel, funktioniert es.

Ich verwende Exit-Codes als Interprozess-Kommunikationsmechanismus. Wenn Sie komplexere Dinge tun, möchten Sie vielleicht Sockets verwenden.

Das ist meine quick and dirty Hack:

before do 
    @concurrent_requests = 2.times.map do 
    fork do 
     # ActiveRecord explodes when you do not re-establish the sockets 
     ActiveRecord::Base.connection.reconnect! 

     answer = post "/api/v1/item/buy.json", :id => item.id 

     # Calling exit! instead of exit so we do not invoke any rspec's `at_exit` 
     # handlers, which cleans up, measures code coverage and make things explode. 
     case JSON.parse(answer.body)["status"] 
     when "accepted" 
      exit! 128 
     when "too_late" 
      exit! 129 
     end 
    end 
    end 

    # Wait for the two requests to finish and get the exit codes. 
    @exitcodes = @concurrent_requests.map do |pid| 
    Process.waitpid(pid) 
    $?.exitstatus 
    end 

    # Also reconnect in the main process, just in case things go wrong... 
    ActiveRecord::Base.connection.reconnect! 

    # And reload the item that has been modified by the seperate processs, 
    # for use in later `it` blocks. 
    item.reload 
end 

it "should only accept one of two concurrent requests" do 
    @exitcodes.sort.should == [128, 129] 
end 

ich eher exotische Exit-Codes wie und , weil Prozesse Ausgang mit dem Code 0, wenn der Fall Block nicht erreicht ist, und 1, wenn Eine Ausnahme tritt auf. Beides sollte nicht passieren. Wenn ich höhere Codes verwende, merke ich, wenn etwas schief läuft.

+0

Gute Problemumgehung! Nur als Referenz, können Sie den relevanten Controller/Modell-Code veröffentlichen, der die Race Condition manifestiert hat? –

+0

Ich kann nicht glauben, dass diese Frage und Antwort noch nicht abgestimmt wurde. Habe meinen Tag gerettet! –

+0

Große Problemumgehung! Mni thx zum Teilen! – ctp

5

Sie können keine Capybara-Anfragen gleichzeitig stellen. Sie können jedoch mehrere Capybara-Sitzungen erstellen und sie im selben Test verwenden, um gleichzeitige Benutzer zu simulieren.

user_1 = Capybara::Session.new(:webkit) # or whatever driver 
user_2 = Capybara::Session.new(:webkit) 

user_1.visit 'some/page' 
user_2.visit 'some/page' 

# ... more tests ... 

user_1.click_on 'Buy' 
user_2.click_on 'Buy' 
+1

Ich wusste über sequentielle Anfragen. Ich habe das Problem endlich selbst gelöst. Siehe meine Antwort. Ich ** kann ** gleichzeitige Anfragen machen. – iblue