1

Hello i have the following problem:

I have a hash that contains strings.This hash will get queried by multiple users.

When a user comes with a Key to first check if it exists in this hash and if it does not, add it.

How can I make the operations "check if hash exists", "add if not exists" atomic? Reading the redis documentation it seems Watch is what I need. Basically start a transaction and end it if the variable changes.

I have tried using Condition.HashNotExists to no avail:

class Program {

        public static async Task<bool> LockFileForEditAsync(int fileId) {
            var database = ConnectionMultiplexer.Connect(CON).GetDatabase();
            var exists = await database.HashExistsAsync("files", fileId); //this line is for  shorting the transaction if hash exists 

            if (exists) {
                return false;
            }

            var tran = database.CreateTransaction();
            tran.AddCondition(Condition.HashNotExists("files", fileId));
            var setKey = tran.HashSetAsync("files", new HashEntry[] { new HashEntry(fileId, 1) });
            var existsTsc = tran.HashExistsAsync("files", fileId);

            if (!await tran.ExecuteAsync()) {
                return false;
            }

            var rezult = await existsTsc;
            return rezult;
        }

        public const string CON = "127.0.0.1:6379,ssl=False,allowAdmin=True,abortConnect=False,defaultDatabase=0";

        static async Task Main(string[] args) {
            int fid = 1;
            var locked = await LockFileForEditAsync(fid);
        }
    }

If I connect via redis-cli and issue in the cli : hset files {fileId} 1 , right BEFORE I issue the ExecuteAsync (in the debugger) I am expecting this transaction to fail since I placed the Condition. However it does not happen so.

How can I basically use redis commands place something like a lock on the two operations:

  1. Check if hashentry exists
  2. Add hashentry
LeoMurillo
  • 6,048
  • 1
  • 19
  • 34
Bercovici Adrian
  • 8,794
  • 17
  • 73
  • 152

1 Answers1

1

Bad news, good news, and for everything else...

Bad news

This doesn't work because SE.Redis pipelines the transaction. This means all the transaction commands are sent to the server simultaneously when ExecuteAsync() is called. However, conditions are evaluated first.

tran.AddCondition(Condition.HashNotExists("files", fileId)); translates to:

WATCH "files"
HEXISTS "files" "1"

And these two commands are sent first when ExecuteAsync() is called to evaluate the condition. If the condition holds true (HEXISTS "files" "1" = 0) then the rest of the transaction commands are sent.

This effectively ensures no false positives, because if the files key is modified in between (while SE.Redis is evaluating the condition), the WATCH will make the transaction fail.

The problem is the false negatives. For example, the transaction will also fail if another field of the hash was set while SE.Redis is evaluating the condition. The WATCH "files" makes it so.

I tested this by having a redis-benchmark -c 5 HSET files 2 1 running when ExecuteAsync() was called. The condition passed, but the transaction failed although the field "1" did not exist, because the field "2" was set in between.

I verified using the MONITOR command in a separate redis-cli window. This is handy to troubleshoot expectations not met or simply to see what is really going to the server and when.

A WATCH is not helpful when you care about a field of a hash, as it would create false negatives when other fields are touched.

A solution to this would be to use a regular key instead (files:1).

Good news

There is a command to do exactly what you want: HSETNX.

This simplifies your LockFileForEditAsync() completely:

    public static async Task<bool> LockFileForEditAsync(int fileId)
    {
        var database = ConnectionMultiplexer.Connect(CON).GetDatabase();
        var setKey = await database.HashSetAsync("files", fileId, 1, When.NotExists);
        return setKey;
    }

Notice the When.NotExists parameter. It results in HSETNX "files" "1" "1" command being sent.

For everything else...

You may use Lua scripts in situations like this, where you want to have some conditional action done atomically, like in how to use spop command with count if set have that much (count) element in set.

It seems like you are trying to do a distributed lock. See Distributed locks with Redis for other aspects you may want to consider. See What is distributed Atomic lock in caches drivers? for a nice story on this one.

LeoMurillo
  • 6,048
  • 1
  • 19
  • 34
  • Apparently it works with my variant .I do not want just to add if not exists.I want to add and return true (if it did not exist and was created successful) and false if it did exist , or , it tried to create it but someone created in the meantime.(transaction) – Bercovici Adrian Jan 31 '20 at 08:19
  • That is exactly what HSETNX does: "Sets field in the hash stored at key to value, only if field does not yet exist. If key does not exist, a new key holding a hash is created. **If field already exists, this operation has no effect.**". Your last condition " it tried to create it but someone created in the meantime.(transaction)" doesn't apply if you use one command, Redis is single threaded, so one command is automatically atomic. – LeoMurillo Jan 31 '20 at 08:26
  • See the return values for HSETNX: 1 if field is a new field in the hash and value was set. 0 if field already exists in the hash and no operation was performed. I am failing to see how this doesn't exactly match what you want :-) What variant are you referring to? – LeoMurillo Jan 31 '20 at 08:29
  • You are indeed right.I did not see the overload of `HashSetAsync` with the `When`.It really replaces all my logic (that did work though :D - transaction +condition ) – Bercovici Adrian Jan 31 '20 at 08:30
  • I did reproduce your problem copy/pasting your code, and could see the WATCH command sent until Execute, so not sure how you got the transaction to work. But yeah, HSETNX will do the trick much simpler :-) – LeoMurillo Jan 31 '20 at 08:35
  • 1
    Well in my code you can see each user first tries to see if the hash exists and then if not adds it.In the case where user 1 checks the hash and suceeds , right before writing the hash user 2 checks the hash and suceeds too (user 1 hasnt wrote yet) , then user 2 transaction has condition which will fail since user 1 has written at this point. – Bercovici Adrian Jan 31 '20 at 09:28
  • 1
    Updated the answer, indeed it works if another client sets the field 1, but fails (false negative) if another client sets another field. Thanks for sharing, forced me to dig deeper and I learned more – LeoMurillo Jan 31 '20 at 10:37
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/206961/discussion-between-bercovici-adrian-and-leomurillo). – Bercovici Adrian Jan 31 '20 at 11:04