Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

StackExchange.Redis transaction methods freezes

I have this code to add object and index field in Stackexchange.Redis. All methods in transaction freeze thread. Why ?

  var transaction = Database.CreateTransaction();

  //this line freeze thread. WHY ?
  await transaction.StringSetAsync(KeyProvider.GetForID(obj.ID), PreSaveObject(obj));
  await transaction.HashSetAsync(emailKey, new[] { new HashEntry(obj.Email, Convert.ToString(obj.ID)) });

  return await transaction.ExecuteAsync();
like image 394
boostivan Avatar asked Sep 22 '14 14:09

boostivan


2 Answers

Commands executed inside a transaction do not return results until after you execute the transaction. This is simply a feature of how transactions work in Redis. At the moment you are awaiting something that hasn't even been sent yet (transactions are buffered locally until executed) - but even if it had been sent: results simply aren't available until the transaction completes.

If you want the result, you should store (not await) the task, and await it after the execute:

var fooTask = tran.SomeCommandAsync(...);
if(await tran.ExecuteAsync()) {
    var foo = await fooTask;
}

Note that this is cheaper than it looks: when the transaction executes, the nested tasks get their results at the same time - and await handles that scenario efficiently.

like image 150
Marc Gravell Avatar answered Nov 15 '22 04:11

Marc Gravell


Marc's answer works, but in my case it caused a decent amount of code bloat (and it's easy to forget to do it this way), so I came up with an abstraction that sort of enforces the pattern.

Here's how you use it:

await db.TransactAsync(commands => commands
    .Enqueue(tran => tran.SomeCommandAsync(...))
    .Enqueue(tran => tran.SomeCommandAsync(...))
    .Enqueue(tran => tran.SomeCommandAsync(...)));

Here's the implementation:

public static class RedisExtensions
{
    public static async Task TransactAsync(this IDatabase db, Action<RedisCommandQueue> addCommands) 
    {
        var tran = db.CreateTransaction();
        var q = new RedisCommandQueue(tran);

        addCommands(q);

        if (await tran.ExecuteAsync())
            await q.CompleteAsync();
    }
}

public class RedisCommandQueue
{
    private readonly ITransaction _tran;
    private readonly IList<Task> _tasks = new List<Task>();

    public RedisCommandQueue Enqueue(Func<ITransaction, Task> cmd)
    {
        _tasks.Add(cmd(_tran));
        return this;
    }

    internal RedisCommandQueue(ITransaction tran) => _tran = tran;
    internal Task CompleteAsync() => Task.WhenAll(_tasks);
}

One caveat: This doesn't provide an easy way to get at the result of any of the commands. In my case (and the OP's) that's ok - I'm always using transactions for a series of writes. I found this really helped trim down my code, and by only exposing tran inside Enqueue (which requires you to return a Task), I'm less likely to "forget" that I shouldn't be awaiting those commands at the time I call them.

like image 7
Todd Menier Avatar answered Nov 15 '22 04:11

Todd Menier