4

Does Booksleeve support CAS operations (i.e. the Redis WATCH command)? For example, how would one implement something like the following?

WATCH mykey
val = GET mykey
val = val + 1
MULTI
SET mykey $val
EXEC

I would need this to avoid race conditions when multiple threads try to modify the same object with the same data.

tamaslnagy
  • 431
  • 1
  • 7
  • 16

1 Answers1

2

In nuget currently, I don't think so. For the reason that BookSleeve is usually intended to be used as a multiplexer, which makes "watch" unusable. I could add it, bit you would have to limit usage to a single caller (per BookSleeve connection) for the duration of your operation.

This has now changed; if we wanted to manually implement INCR (as per your example) we could use:

// note this could be null if the old key didn't exist
var oldVal = await connection.Strings.GetInt64(db, key);

var newVal = (oldVal ?? 0) + 1;
using (var tran = connection.CreateTransaction())
{
    // check hasn't changed (this handles the WATCH, a checked GET,
    // and an UNWATCH if necessary); note tat conditions are not sent
    // until the Execute is called
    tran.AddCondition(Condition.KeyEquals(db, key, oldVal));

    // apply changes to perform assuming the conditions succeed
    tran.Strings.Set(db, key, newVal); // the SET

    // note that Execute includes the MULTI/EXEC, assuming the conditions pass
    if (!await tran.Execute()) return null; // aborted; either a pre-condition
                                         // failed, or a WATCH-key was changed
    return newVal; // successfully incremented
}

obviously you might want to execute that in a repeated (within sane limits) loop so that if it is aborted because of the WATCH, you redo from the start.

This is slightly different to your example, as it actually does (assuming the value wasn't changed between the initial GET and the second GET):

val = GET mykey
newval = (val ?? 0) + 1
WATCH mykey
chk = GET mykey // and verifies chk == val as part of the Execute
MULTI
SET mykey $newval
EXEC

noting that the EXEC can still report cancellation if the value was changed between the WATCH and the EXEC; or (if it has changed between the two GETs):

val = GET mykey
newval = (val ?? 0) + 1
WATCH mykey
chk = GET mykey // and verifies chk == val as part of the Execute
UNWATCH

The difference is one more GET, but that is the only way it can work with a multiplexer - i.e. so that the Execute is optimized to be reliably fast, so that it doesn't impact other callers.

Marc Gravell
  • 1,026,079
  • 266
  • 2,566
  • 2,900
  • Thanks for the response. I was wondering because AppHarbor can fire up multiple instances of my background application (which has Quartz doing scheduling that should only once within a certain time frame) and I wanted to guarantee that each scheduled operation happens only once. I had planned to insert a value into a list and guarantee that it is the only one while it is there. Is there any work around? – tamaslnagy Aug 01 '12 at 22:39
  • @tamaslnagy well, the code in the question: is that anything that couldn't be done with "incr" ? Which is atomic automatically? There is also some distributed-lock code built in ow which *uses* "watch", but it does it by blocking the multiplexer for a brief moment. If the scenario fits, you could try using that code? – Marc Gravell Aug 01 '12 at 22:47
  • What is ow? If inc is atomic then that might be suitable. Is remove atomic? – tamaslnagy Aug 01 '12 at 23:09
  • 1
    @tamaslnagy "ow" is a typo of "now" ;p Yes, remove is atomic. For the locking stuff, see `.Strings.TakeLock(...)`, which does either `WATCH`, `EXISTS`, `UNWATCH` - or `WATCH`, `EXISTS`, `MULTI`, `SETNX`, `EXPIRE`, `EXEC` depending on whether it looks available – Marc Gravell Aug 02 '12 at 06:54
  • That looks like it could work. I would just TakeLock on a key until I decide to ReleaseLock or the lock expires? Oh, and fyi Booksleeve is a really awesome tool; just in case you didn't know :P – tamaslnagy Aug 05 '12 at 21:48
  • @tamaslnagy yes, the TakeLock / ReleaseLock should do the job. Try to leave plenty of time so it won't expire during normal execution. – Marc Gravell Aug 05 '12 at 21:57
  • Hmmm. I saw that ReleaseLock says this "You should not release a lock that you did not take, as this will cause problems." I have competing threads and I might release lock in a thread but the other thread would not know this and will attempt to release the lock again. Would this give me errors? – tamaslnagy Aug 05 '12 at 22:36
  • That doesn't seem to be a problem. I am curious to know why ReleaseLock() doesn't return the value stored using TakeLock() – tamaslnagy Aug 06 '12 at 01:16
  • @tamaslnagy hmmm, I guess it could. My assumption there is that you **wouldn't call it** unless it was you that took the lock, in which case you would already know the value - however, the value would be available for cross-check by *unsuccessful takers*, i.e. "this record is currently locked by tamasInagy" - but you make a fair point. I might add that. – Marc Gravell Aug 06 '12 at 05:42
  • It would be nice if it WATCH/UNWATCH was implemented but i guess i could escape key names and use a global mutex or the lock as you suggested. +1. I'm going to stick with mutex because if i leave the function its auto release/unwatch and i wont have to call ReleaseLock in redis –  Aug 31 '12 at 20:18
  • @acidzombie24 I'll probably add those in due course, but it has to be stressed that using them **breaks the multiplexer** - so: don't multiplex with that ;p In fact, I'll probably find some way of making it a mutex – Marc Gravell Aug 31 '12 at 20:21
  • What do you mean by 'break'? I don't see any performance warnings (which wont matter if i need correctness) and afaik it doesnt matter if other threads write to the connection as long as that function does watch and its gets/multi in the order it sent it out everything should be fine? –  Aug 31 '12 at 20:26
  • If your curious why i may want watch/unwatch it would be for the UVComment function http://stackoverflow.com/questions/12221882/how-would-so-post-be-written-with-redis or maybe UpdatePost –  Aug 31 '12 at 20:29