25

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();
boostivan
  • 279
  • 4
  • 9

3 Answers3

30

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.

Marc Gravell
  • 1,026,079
  • 266
  • 2,566
  • 2,900
  • @boostivan it does take some thinking to get used to; note - another way to do this is to use `Script*` and send a Lua script that does the ops all at the server. Much simpler. – Marc Gravell Sep 23 '14 at 12:50
  • @MarcGravell What is the best practice if I do _not_ need the result(s) of the command(s)? Capture the tasks and `await` them after the transaction anyway, or fire and forget? (Guessing the former, but just want to be sure.) – Todd Menier Feb 15 '18 at 16:36
  • @MarcGravell How would you use the result of one operation in another operation in the same transaction? Could you answer this [question](https://stackoverflow.com/questions/63945465/redis-c-sharp-using-incr-value-in-a-transaction) – Jins Peter Sep 18 '20 at 19:54
7

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.

Todd Menier
  • 37,557
  • 17
  • 150
  • 173
5

I and our team were bitten by this issue several times, so I created a simple Roslyn analyzer to spot such problems.

https://github.com/olsh/stack-exchange-redis-analyzer

olsh
  • 464
  • 1
  • 6
  • 14