http://drawohara.tumblr.com/post/18926188
it seems that *any* validation should alter the behavior of
Base#create to force the isolation level - otherwise validations end
up being nothing be a stack of race conditions. sorry if i'm late to
the party but this came as a real surprise to me. at the very least
it seems the docs should have a big fat warning about this or the
code itself should warn when using validations and the client code is
not under a strictly serializable transaction.
kind regards
a @ http://codeforpeople.com/
--
we can deny everything, except that we have the possibility of being
better. simply reflect on that.
h.h. the 14th dalai lama
I'm not sure that this applies to any validations other than
validates_uniqueness_of?
But yes, you need a unique index or serializable transactions if you
want this guaranteed. If there's a database-agnostic way to implement
the transaction stuff that sounds like a good idea, otherwise we could
at least warn in the documentation.
> kind regards
>
> a @ http://codeforpeople.com/
> --
> we can deny everything, except that we have the possibility of being
> better. simply reflect on that.
> h.h. the 14th dalai lama
>
>
>
>
> >
>
--
Cheers
Koz
> I'm not sure that this applies to any validations other than
> validates_uniqueness_of?
validates_associated
validates_each
validates_length_of # esp if a text value is being appended to
but you are right in pointing out that as the main one.
>
> But yes, you need a unique index or serializable transactions if you
> want this guaranteed. If there's a database-agnostic way to implement
> the transaction stuff that sounds like a good idea, otherwise we could
> at least warn in the documentation.
i'd vote for sticking
def transaction options = {}
end
as a start, into the base connection adapter, individual adapters can
then make appropriate use of the :isolation key which #create could
be refactored to use iff validation is present on the record's model
class being saved. overall the ar code doesn't seem to have put a
ton of concern into transactions and passing options through would at
least let the connection adapters have the option (no pun intended) ;-)
thoughts?
btw - we had this happen three times today. the culprit was submit
button that is not disabled after the first click and people were
double clicking it out of habbit - thus two requests come in quite
close together. js can solve it, but i'm not comfortable maintaining
db integrity via js ;-)
These two just aggregate other validations
> validates_length_of # esp if a text value is being appended to
save always provides the full value of every column, there's no way
you can be 'appending' and using active record's save.
> i'd vote for sticking
>
> def transaction options = {}
> end
>
> thoughts?
This definitely sounds like a long term option to consider, assuming
the terminology and api can be unified across all the databases etc.
> btw - we had this happen three times today. the culprit was submit
> button that is not disabled after the first click and people were
> double clicking it out of habbit - thus two requests come in quite
> close together. js can solve it, but i'm not comfortable maintaining
> db integrity via js ;-)
Stick a unique index on the field and you'll be fine.
>
>
> a @ http://codeforpeople.com/
> --
> we can deny everything, except that we have the possibility of being
> better. simply reflect on that.
> h.h. the 14th dalai lama
>
>
>
>
> >
>
--
Cheers
Koz
> This definitely sounds like a long term option to consider, assuming
> the terminology and api can be unified across all the databases etc.
i've already pulled out a ton of transaction code that supports
nested transactions too - i'll see if i can work this in
> Stick a unique index on the field and you'll be fine.
that's fine - but then validates_uniqness_of is not needed - right
now it's just there to provide users access to an easy a race
condition alongside find_or_create_by. although the answer is
simple, it simply raises questions, for me at least, about whether
those methods should even exist. my experience is that, even though
they should be, most web developers are not versed in acid. pun
intended.
cheers.
No, you still need the validation otherwise instead of a "name has
already been taken" validation error you get an internal server error.
validates_uniqueness_of gives a nice error message, and does an ok job
at guaranteeing uniqueness. Validates uniqueness of + a unique index
does both.
> cheers.
>
>
> a @ http://codeforpeople.com/
> --
> we can deny everything, except that we have the possibility of being
> better. simply reflect on that.
> h.h. the 14th dalai lama
>
>
>
>
> >
>
--
Cheers
Koz
>
> No, you still need the validation otherwise instead of a "name has
> already been taken" validation error you get an internal server error.
>
> validates_uniqueness_of gives a nice error message, and does an ok job
> at guaranteeing uniqueness. Validates uniqueness of + a unique index
> does both.
>
yeah i understand that - i just don't get warm and fuzzies using 'ok'
and 'guarantee' in this context since you still end up at internal
server errors some of the time. it's not required to throw and
internal error - the db conn error can be rescued (UniqueError etc)
and dealt with. the api for dealing with it can be
'validates_uniqness_of'. i've done this in some old pg specific code
and i realize this is just a bunch of noise without some code - i'll
see if i can dig it up...
cheers.
it is not enough to be compassionate. you must act.
>
> This definitely sounds like a long term option to consider, assuming
> the terminology and api can be unified across all the databases etc.
interesing comment from boulder/denver ruby group:
"I think the problem stems largely from the ActiveRecord connection
adapter
API. It provides no mechanism for acquiring or releasing table level
locks.
If you could get a table level lock, then checking the uniqueness of
a value
before performing an insert/update could be an atomic, as well as
performing
a find_or_create_by type operation"
it is not enough to be compassionate. you must act.
Table Level locking is kinda nasty, and a great way to kill your
throughput. If you're already creating so many 'foos' concurrently
that you're hitting this race condition, chances are you don't want to
continually lock and unlock the table. A unique index solves all of
the problems but leaves it up to the database to handle whatever
locking is required. I'm a little confused why we're looking for
another solution.
--
Cheers
Koz
Hey Ara, a one-two punch of validates_uniqueness_of plus a unique
index on those fields gives you nice error messages in the common case
and a rare database exception otherwise. This should definitely be in
the docs.
Now, if we could parse the db constraint violation error message to
deduce the type of constraint that failed and the affected fields,
then we could turn the dumb exception (StatementInvalid) into a smart
one (UniquenessConstraintViolation), rescue it in
Base#save_with_validations, and add the message to record.errors.
Unfortunately I tried it with postgres a while back and the db errors
left too much up to the imagination to get much further than a neat
demo.
Best,
jeremy
I discovered the same problem a year and a half ago. I asked around on
mailing lists and IRC and the answers ranged from "what's the problem"
to "your design is stupid" - nobody really had a good answer. Table
locks are expensive, and nesting table locks is hard to impossible
because there's only one unlock phase. And last time I tried
serializable transaction level on MySQL to solve this problem, MySQL
somehow detected a deadlock.
Combining validates_uniqueness_of and unique constraint is the best
solution right now. You can parse database error messages to detect
unique constraint failures but unfortunately that's database dependent.
> Table Level locking is kinda nasty, and a great way to kill your
> throughput. If you're already creating so many 'foos' concurrently
> that you're hitting this race condition, chances are you don't want to
> continually lock and unlock the table. A unique index solves all of
> the problems but leaves it up to the database to handle whatever
> locking is required. I'm a little confused why we're looking for
> another solution.
yup. that certainly seems to be the consensus, this comment followed
(which is more inline with what i was suggesting):
"
This approach would work but I have a hard time imagining wanting to
take the performance hit of a table lock on any operation that happens
often enough to make this race condition a real issue.
A workable approach for the case of find_or_create is to enforce the
uniqueness with a database constraint and do this in ruby code:
1. do a find
2a. return the model object if you found it
2b. if it does not exist try to insert it
3a. if the insert succeeds return the new object
3b. if the insert fails re-run the find and return the result of
that.
We use this pattern in a couple of the high volume models where I work
and it works quite well. With PostgreSQL, can achieve this by creating
a "savepoint" before attempting the inserts and then rolling back to
the savepoint if the insert fails.
For normal inserts and updates you could rely on database constraints
for the data integrity, but use validations for pretty error messages.
AR could then detect an insert/update failure and, in that case, it
could rerun the validations to figure out the appropriate message(s).
The validations could even be mechanically created from the database
constraints in many cases (see http://drysql.rubyforge.org/ for an
example of this).
"
the bit about re-running the validations is quite interesting.
cheers.
share your knowledge. it's a way to achieve immortality.
> Hey Ara, a one-two punch of validates_uniqueness_of plus a unique
> index on those fields gives you nice error messages in the common case
> and a rare database exception otherwise. This should definitely be in
> the docs.
that's fair enough and a great start imho. i've never contributed
docs but know marcel is the gatekeeper - what's the protocol?
>
> Now, if we could parse the db constraint violation error message to
> deduce the type of constraint that failed and the affected fields,
> then we could turn the dumb exception (StatementInvalid) into a smart
> one (UniquenessConstraintViolation), rescue it in
> Base#save_with_validations, and add the message to record.errors.
> Unfortunately I tried it with postgres a while back and the db errors
> left too much up to the imagination to get much further than a neat
> demo.
>
i played with this a bit and you are quite right. this is quite
interesting
http://drawohara.tumblr.com/post/19178992
but of little practical value. serialized transactions work too of
course - sans speed.
cheers.
That's cool. I thought FOR UPDATE wouldn't prevent inserts. Is this
also true for the other databases we support?
jeremy
It doesn't. It locks all the rows it selects, but since there's no
row yet, there's nothing to lock.
Assaf