Search code examples
databaseredisbooksleeve

Check-and-Set (CAS) operations with Booksleeve and Redis


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.


Solution

  • 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.