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();
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'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 await
ing those commands at the time I call them.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With