1

I encountered a problem wherein asynchronous saving in one of our models with uniqueness validation declared in Application level is not blocked by the said validation. Adding the validation in the Database level is not an option for me due to a toggleable option on my app to enable/disable the uniqueness validation.

While searching I saw that Redis lock is what's best for this kind of case, but since I am new to the term of "Redis lock" I need a bit of advice on which plugin/implementation is better to achieve my needed behaviour.

I saw one of the responses that could be similar to my problem here Ruby - Redis based mutex with expiration implementation, and it says that a plugin is not needed since it could be achieved with Redis alone like this

def lock(key, timeout)
  if @redis.set(key, Time.now, nx: true, px: timeout)
    begin
      yield
    ensure
      release key
    end
  end
end

And I also saw that some recommend this redlock plugin found here https://github.com/leandromoreira/redlock-rb but could be an overkill.

Thanks in advance

Eyeslandic
  • 14,553
  • 13
  • 41
  • 54
Kok A.
  • 167
  • 1
  • 15
  • 1
    Personally I use RedLock but I would be curious if someonne has an opinion on this – max pleaner Jul 12 '19 at 05:17
  • it seems you are solving the problem at the wrong level. Can you explain a bit more why async saving would not run a uniqueness validation in a Rails model? Or are you referring to the fact, that uniqueness validation are not transactional? – Pascal Jul 12 '19 at 06:08
  • @pascalbetz, for example, I have 2 concurrent requests to save a record on my model, and they came in with just milliseconds of gap. The uniqueness validation for both will become true since both records are not yet persisted in the database, so I end up having duplicate records upon saving. – Kok A. Jul 12 '19 at 07:07
  • Added an answer. Does not yet answer all your questions but hard to discuss in the comments. – Pascal Jul 12 '19 at 13:58

1 Answers1

2

You have several possibilities:

Serialize the save operation

Assuming you use some sort of background job: drop those jobs in a queue where there is just one worker.

Lock the save operation

Don't enter the save operation while the lock is held. Could be done via Redis and Redlocker (don't do it on your own, see the comment and details about Redlocker here: https://redis.io/commands/set)

But both don't solve this problem: what will you do when there are two records with and one of them can nit be saved? Is this handled or can it be ignored?

Note that the uniqueness validation of Rails is NOT transactional and you can not rely on it to enforce uniqueness (basically it is a select before the insert/update). See https://phraseapp.com/blog/posts/pitfalls-uniqueness-validation-for-uniqueness-using-rails-activerecord/

The only place to properly enforce uniqueness is in the DB. I consider validations good for reporting errors to a user but when it comes to integrity of your data you should have this secured in the DB.

The "toggleable option" does not seem to make much sense to me. Once the uniqueness check is disabled, duplicates will end up in the DB. Enabling the uniqueness check will not change this for existing data.

Perhaps you can add a partial index? (https://www.postgresql.org/docs/current/indexes-partial.html)

CREATE UNIQUE INDEX unique ON orders (some_column) WHERE (some_othercolumn is null);

(assuming Postgres)

Pascal
  • 8,464
  • 1
  • 20
  • 31
  • Thanks for the thorough comment! Yes, I handled the code in a background worker and if there are concurrent request, the first one will be executed while the other one will be enqueued again after x number of minutes. About the "don't do it on your own", well, I used the code that I pasted on my question. That's considered as Redis implementation of 'set' right? I just want to clarify that it's not considered as own implementation that you are pertaining to – Kok A. Jul 12 '19 at 14:51