2010-06-14 12 views
20

Ho un'applicazione Rails davvero semplice che consente agli utenti di registrare la loro presenza su una serie di corsi. I modelli ActiveRecord sono i seguenti:Come evitare una condizione di competizione nella mia app Rails?

class Course < ActiveRecord::Base 
    has_many :scheduled_runs 
    ... 
end 

class ScheduledRun < ActiveRecord::Base 
    belongs_to :course 
    has_many :attendances 
    has_many :attendees, :through => :attendances 
    ... 
end 

class Attendance < ActiveRecord::Base 
    belongs_to :user 
    belongs_to :scheduled_run, :counter_cache => true 
    ... 
end 

class User < ActiveRecord::Base 
    has_many :attendances 
    has_many :registered_courses, :through => :attendances, :source => :scheduled_run 
end 

esempio Uno ScheduledRun ha un numero finito di posti disponibili, e una volta raggiunto il limite, non più presenze può essere accolto.

def full? 
    attendances_count == capacity 
end 

attendances_count è una colonna di cache contatore che tiene il numero di associazioni di presenza creati per un record ScheduledRun.

Il mio problema è che non conosco appieno il modo corretto per garantire che una condizione di competizione non si verifichi quando 1 o più persone tentano di registrarsi per l'ultimo posto disponibile su un campo allo stesso tempo.

mio regolatore di frequenza simile a questa:

class AttendancesController < ApplicationController 
    before_filter :load_scheduled_run 
    before_filter :load_user, :only => :create 

    def new 
    @user = User.new 
    end 

    def create 
    unless @user.valid? 
     render :action => 'new' 
    end 

    @attendance = @user.attendances.build(:scheduled_run_id => params[:scheduled_run_id]) 

    if @attendance.save 
     flash[:notice] = "Successfully created attendance." 
     redirect_to root_url 
    else 
     render :action => 'new' 
    end 

    end 

    protected 
    def load_scheduled_run 
    @run = ScheduledRun.find(params[:scheduled_run_id]) 
    end 

    def load_user 
    @user = User.create_new_or_load_existing(params[:user]) 
    end 

end 

Come si può vedere, non tiene conto in cui l'istanza ScheduledRun ha già raggiunto la capacità.

Qualsiasi aiuto su questo sarebbe molto apprezzato.

Aggiornamento

Io non sono certo se questo è il modo giusto per eseguire il blocco ottimistico in questo caso, ma qui è quello che ho fatto:

ho aggiunto due colonne alla tabella ScheduledRuns -

t.integer :attendances_count, :default => 0 
t.integer :lock_version, :default => 0 

anche ho aggiunto un metodo per il modello ScheduledRun:

def attend(user) 
    attendance = self.attendances.build(:user_id => user.id) 
    attendance.save 
    rescue ActiveRecord::StaleObjectError 
    self.reload! 
    retry unless full? 
    end 

Quando il modello di presenze viene salvato, ActiveRecord procede e aggiorna la colonna della cache del contatore sul modello ScheduledRun. Ecco la proiezione di uscita registro in cui questo accade -

ScheduledRun Load (0.2ms) SELECT * FROM `scheduled_runs` WHERE (`scheduled_runs`.`id` = 113338481) ORDER BY date DESC 

Attendance Create (0.2ms) INSERT INTO `attendances` (`created_at`, `scheduled_run_id`, `updated_at`, `user_id`) VALUES('2010-06-15 10:16:43', 113338481, '2010-06-15 10:16:43', 350162832) 

ScheduledRun Update (0.2ms) UPDATE `scheduled_runs` SET `lock_version` = COALESCE(`lock_version`, 0) + 1, `attendances_count` = COALESCE(`attendances_count`, 0) + 1 WHERE (`id` = 113338481) 

Se un aggiornamento successivo si verifica al modello ScheduledRun prima che il nuovo modello di partecipazione è salvato, questo dovrebbe far scattare l'eccezione StaleObjectError. A quel punto, l'intera operazione viene ritentata di nuovo, se la capacità non è già stata raggiunta.

Aggiornamento # 2

Sulla scia @ risposta di Kenn Ecco il metodo frequentare aggiornato sull'oggetto SheduledRun:

# creates a new attendee on a course 
def attend(user) 
    ScheduledRun.transaction do 
    begin 
     attendance = self.attendances.build(:user_id => user.id) 
     self.touch # force parent object to update its lock version 
     attendance.save # as child object creation in hm association skips locking mechanism 
    rescue ActiveRecord::StaleObjectError 
     self.reload! 
     retry unless full? 
    end 
    end 
end 
+0

Risolto negli ultimi binari. –

+0

È necessario utilizzare il blocco ottimistico. Questo screencast ti mostrerà come farlo: [link text] (http://railscasts.com/episodes/59-optimistic-locking) – rtacconi

+0

Cosa intendi con dmitry? – Edward

risposta

13

Il blocco ottimistico è la strada da percorrere, ma come potresti già aver notato, il tuo codice non genererà mai ActiveRecord :: StaleObjectError, poiché la creazione dell'oggetto child in has_many association salta il meccanismo di blocco. Date un'occhiata al seguente SQL:

UPDATE `scheduled_runs` SET `lock_version` = COALESCE(`lock_version`, 0) + 1, `attendances_count` = COALESCE(`attendances_count`, 0) + 1 WHERE (`id` = 113338481) 

quando si aggiorna gli attributi del genitore oggetto , di solito si vede il seguente SQL invece:

UPDATE `scheduled_runs` SET `updated_at` = '2010-07-23 10:44:19', `lock_version` = 2 WHERE id = 113338481 AND `lock_version` = 1 

La dichiarazione di cui sopra mostra come il blocco ottimistico è implementato : Notare lock_version = 1 nella clausola WHERE. Quando si verifica una condizione di competizione, i processi concorrenti tentano di eseguire questa query esatta, ma solo il primo riesce, poiché il primo aggiorna atomicamente la versione di blocco a 2 ei successivi processi non riusciranno a trovare il record e ad aumentare ActiveRecord :: StaleObjectError, poiché lo stesso record non ha più lock_version = 1.

Quindi, nel tuo caso, una possibile soluzione è quella di toccare il genitore a destra prima di creare/distruggere un oggetto figlio, in questo modo:

def attend(user) 
    self.touch # Assuming you have updated_at column 
    attendance = self.attendances.create(:user_id => user.id) 
rescue ActiveRecord::StaleObjectError 
    #...do something... 
end 

Non è intesa a evitare rigorosamente condizioni di gara, ma praticamente dovrebbe funzionare nella maggior parte dei casi.

+0

Grazie Kenn. Non mi ero reso conto che la creazione di oggetti figlio ha saltato il meccanismo di blocco. Ho anche avvolto il tutto in una transazione, così l'oggetto genitore non viene aggiornato inutilmente se la creazione dell'oggetto figlio fallisce. – Cathal

0

Non resta che verificare se @run.full??

def create 
    unless @user.valid? || @run.full? 
     render :action => 'new' 
    end 

    # ... 
end 

Modifica

Che cosa succede se si aggiunge una convalida del tipo:

class Attendance < ActiveRecord::Base 
    validate :validates_scheduled_run 

    def scheduled_run 
     errors.add_to_base("Error message") if self.scheduled_run.full? 
    end 
end 

Non salverà la @attendance se l'associato scheduled_run è pieno.

Non ho testato questo codice ... ma credo che sia ok.

+0

Questo non funzionerà. Il problema è che il record @run rappresenta potrebbe essere già stato aggiornato da un'altra richiesta, lasciando @run incoerente con ciò che è rappresentato nel database. Per quanto ne so, il blocco ottimistico è il modo per risolvere questo problema. Tuttavia, come si applica l'applicazione alle associazioni? – Cathal

+0

Giusto ... Ho modificato la mia risposta:] –

Problemi correlati