Forum: Ruby on Rails validates_uniqueness_of and create atomicity

Announcement (2017-05-07): www.ruby-forum.com is now read-only since I unfortunately do not have the time to support and maintain the forum any more. Please see rubyonrails.org/community and ruby-lang.org/en/community for other Rails- und Ruby-related community platforms.
Bosko M. (Guest)
on 2006-05-10 19:52
(Received via mailing list)
Hi folks,

I have a question regarding validates_uniqueness_of, and similar
before-filter-like events attempting to guarantee some DB state prior
to modification.

>From my reading of ActiveRecord, validate_uniqueness_of appears to
cause a SELECT on the underlying table attempting to ensure that a
record with ID(s) specified in the validates_uniqueness_of statement
is not already present, prior to the INSERT (or UPDATE).  However, in
between the SELECT and INSERT, an offending record might appear.

example race condition scenario:

A) dispatcherInstance1 is to perform a create on entity Foo, which has
a validates_uniqueness_of

B) dispatcherInstance2 is to perform a create on entity Foo with the
same parameters as in A

C) dispatcherInstance1 enters the validation code and performs its
SELECT, it decides everything is OK

D) dispatcherInstance2 enters the validation code and performs its
SELECT, it decides everything is OK

E) dispatcherInstance1 succeeds with its INSERT

F) dispatcherInstance2 either raises exception because DB implements
uniqueness property as well or inserts duplicate record


So I guess the question is what is the point of
validates_uniqueness_of if it doesn't guarantee uniqueness?  I had
imagined it would do something "clever" like attempt the insert, trap
an exception if it occurs, and attempt to subsequently verify if the
exception is the result of a violation of the uniqueness condition
specified.  But clearly this is not what is happening.

Regards,
--
Bosko M. <removed_email_address@domain.invalid>
Bosko M. (Guest)
on 2006-05-10 21:52
(Received via mailing list)
To further follow up on this: what I find myself doing now in order to
completely guarantee atomicity for the check-and-insert, is simply
bypass the check (i.e., not enforce validates_uniqueness_of) and catch
exceptions raised due to the insert failing (realizing that such
exceptions are probably the result of a duplicate key violation on the
unique index in my [PostgreSQL] DB).  This completely guarantees that
my application doesn't barf up (with a wierd internal server error)
but fails "politely" should the admittedly rare race condition
scenario below actually occur.

--
Bosko M. <removed_email_address@domain.invalid>
Jeremy K. (Guest)
on 2006-05-10 22:39
(Received via mailing list)
On May 10, 2006, at 8:46 AM, Bosko M. wrote:
> between the SELECT and INSERT, an offending record might appear.
Yes, it should obtain a write lock on the entire table to ensure its
uniqueness check is valid. Clearly this is a poor idea.


> So I guess the question is what is the point of
> validates_uniqueness_of if it doesn't guarantee uniqueness?  I had
> imagined it would do something "clever" like attempt the insert, trap
> an exception if it occurs, and attempt to subsequently verify if the
> exception is the result of a violation of the uniqueness condition
> specified.  But clearly this is not what is happening.

That sounds good. Would you care to pursue an implementation?

jeremy
Bosko M. (Guest)
on 2006-05-10 23:13
(Received via mailing list)
On 5/10/06, Jeremy K. <removed_email_address@domain.invalid> wrote:
[...]
> > So I guess the question is what is the point of
> > validates_uniqueness_of if it doesn't guarantee uniqueness?  I had
> > imagined it would do something "clever" like attempt the insert, trap
> > an exception if it occurs, and attempt to subsequently verify if the
> > exception is the result of a violation of the uniqueness condition
> > specified.  But clearly this is not what is happening.
>
> That sounds good. Would you care to pursue an implementation?
>
> jeremy

Yes, I've thought about a possible implementation.  I'll whip up a
diff and propose it.  I decided to not touch the current
validates_uniqueness_of but implement it in the form of a
guarantee_uniqueness_of.  It's a little tricky because it is difficult
to determine that the exception raised by the create_or_update in
ActiveRecord::Base's save method is due to a unique index violation --
what I do is attempt a SELECT following the exception to verify that
the first one was indeed due to a unique record violation.  It works
but needs a little bit of cleaning up.

--
Bosko M. | removed_email_address@domain.invalid | 
removed_email_address@domain.invalid
Bosko M. (Guest)
on 2006-07-30 03:28
(Received via mailing list)
On 5/10/06, Bosko M. <removed_email_address@domain.invalid> wrote:
> >
> > jeremy

Hrm, it's been a while since we last discussed this.

I did not forget.  In fact, I have a local implementation in the form
of an unpublished plugin I named
"validates_uniqueness_of_unique_fields", but I'm not perfectly happy
with it because it is too dependent on PostgreSQL being used as the db
connection adapter (unfortunately there is no way to detect in a
standard fashion whether the StatementInvalid exception raised by the
save is actually due to a duplicate key violation).  It works, though,
and I use it in conjunction with validates_uniqueness_of since I am
terribly paranoid (i've inline-included a code snippet below if you
are curious).

I continue to use this in conjunction with UNIQUE db constraints in
Postgres because the fact is that checking for uniqueness is not a
validation in the classic sense (it is only partly a validation) --
unlike other validations, a uniqueness guarantee must be checked
against not only client-provided data but the DB backing store, which
makes it significantly more involved than, say,
validates_confirmation_of, or any other validations for that matter.

<-- snippet -->
  module ValidatesUniquenessOfUniqueFields

    def self.included(base)
      base.extend ValidatesUniquenessOfUniqueFieldsMacro
    end

    module ValidatesUniquenessOfUniqueFieldsMacro
      def validates_uniqueness_of_unique_fields(msg = nil)
        unless
method_defined?(:__old_save_validates_uniqueness_of_unique_fields)
          alias_method
:__old_save_validates_uniqueness_of_unique_fields, :save
          define_method :save do
            begin
              __old_save_validates_uniqueness_of_unique_fields
            rescue ActiveRecord::StatementInvalid
              error_msg = $!.to_s
              if error_msg.match(/duplicate/) and
error_msg.match(/unique/)
                msg = "Record clashes with one or more unique fields"
if msg.nil?
                errors.add_to_base(msg)
                false
              else
                raise $!
              end
            end
          end
        end
      end
    end

  end
<-- snippet -->
This topic is locked and can not be replied to.