A: 

Don't you just have to test if @run.full??

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

   # ...
end

Edit

What if you add a validation like:

class Attendance < ActiveRecord::Base
   validate :validates_scheduled_run

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

It won't save the @attendance if the associated scheduled_run is full.

I haven't tested this code... but I believe it's ok.

j.
That won't work. The problem is that the record @run represents may already have been updated by another request, leaving @run inconsistent with whats represented in the database. To my knowledge, optimistic locking is the way to solve this problem. However, how do you go about applying this to associations?
Cathal
Right... I've edited my answer :]
j.
+1  A: 

Hi,

You need to use optimistic locking. This screencast will show you how to do it: link text

rtacconi
A: 

Optimistic locking is the way to go, but as you might have noticed already, your code will never raise ActiveRecord::StaleObjectError, since child object creation in has_many association skips the locking mechanism. Take a look at the following SQL:

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

When you update attributes in the parent object, you usually see the following SQL instead:

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

The above statement shows how optimistic locking is implemented: Notice the lock_version = 1 in WHERE clause. When race condition happens, concurrent processes try to run this exact query, but only the first one succeeds, because the first one atomically updates the lock_version to 2, and subsequent processes will fail to find the record and raise ActiveRecord::StaleObjectError, since the same record doesn't have lock_version = 1 any longer.

So, in your case, a possible workaround is to touch the parent right before you create/destroy a child object, like so:

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

It's not meant to strictly avoid race conditions, but practically it should work in most cases.

kenn
Thanks Kenn. I didn't realise that child object creation skipped the locking mechanism. I wrapped the whole thing in a transaction too, just so the parent object isn't unnecessarily updated if the child object creation fails.
Cathal