2012-06-26 14 views
9

Mi API permite a los usuarios comprar ciertos artículos únicos, donde cada artículo solo se puede vender a un usuario. Entonces, cuando varios usuarios intentan comprar el mismo artículo, un usuario debe obtener la respuesta: ok y el otro usuario debe obtener la respuesta too_late.¿Solicitudes de carpincho concurrentes de subprocesos múltiples?

Ahora, parece haber errores en mi código. Una condición de carrera. Si dos usuarios intentan comprar el mismo artículo al mismo tiempo, ambos obtienen la respuesta ok. El problema es claramente reproducible en producción. Ahora he escrito una prueba sencilla que intenta reproducirla a través de rspec:

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 

Parece que no ejecuta las consultas al mismo tiempo. Para probar esto, puse el código siguiente en mi acción del controlador:

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

espera que la producción sería, si las solicitudes son concurrentes:

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

Sin embargo, me sale el siguiente resultado:

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

Lo que me dice que las solicitudes de capibara no se ejecutan al mismo tiempo, sino de a una por vez. ¿Cómo hago que mis solicitudes de capabara sean simultáneas?

+0

El ejemplo de código anterior no se parece al actual DSL de Capybara. Se parece más a una prueba de controlador simple usando Rack :: Test. ¿Eso es lo que es? –

Respuesta

13

El multihilo y el capibara no funcionan, porque Capabara usa un hilo de servidor separado que maneja la conexión secuencialmente. Pero si te bifurcas, funciona.

Estoy usando códigos de salida como un mecanismo de comunicación entre procesos. Si haces cosas más complejas, quizás quieras usar sockets.

Esta es mi rápido y sucio truco:

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 

utilizo códigos de salida más exóticos como y , porque los procesos de salida con el código 0 si no se alcanza el bloque caso y 1 si una excepción ocurre. Ambos no deberían suceder. Entonces, al usar códigos más altos, noto cuando las cosas van mal.

+0

¡Buena solución! Solo como referencia, ¿puede publicar el controlador relevante/código de modelo que manifestó la condición de carrera? –

+0

No puedo creer que esta pregunta y respuesta aún no se hayan votado. ¡Salvó mi día! –

+0

¡Gran solución! Mni thx para compartir! – ctp

5

No puede realizar solicitudes concurrentes de capybara. Sin embargo, puede crear varias sesiones de capibara y usarlas en la misma prueba para simular usuarios concurrentes.

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

Conocía las solicitudes secuenciales. Finalmente resolví el problema yo mismo. Ver mi respuesta ** Puedo ** hacer solicitudes concurrentes. – iblue

Cuestiones relacionadas