Commands queued on an ITransaction or IBatch are not sent when you call them - they are buffered, and
go to the server only when you call Execute/ExecuteAsync. The Task you get back therefore cannot complete
until then, so awaiting it at the point of queueing waits for something that can only happen after the line
that is waiting. Your code stops there, permanently.
// flagged - this hangs, for good
var tran = db.CreateTransaction();
var value = await tran.StringGetAsync(key); // SER305: nothing has been sent yet
await tran.ExecuteAsync(); // never reached
// suggested - capture the task, await it after Execute
var tran = db.CreateTransaction();
var pending = tran.StringGetAsync(key);
await tran.ExecuteAsync();
var value = await pending;
// or, if you do not want the result at all
_ = tran.StringSetAsync(key, value);
await tran.ExecuteAsync();
This is the one rule here reported as an error, because unlike the rest of the family it does not describe
code that could be better - it describes code that cannot work. There is no arrangement of the surrounding
lines that rescues it: the wait sits at the queueing site, so even a previous Execute does not help, because
this command was queued after it and will never be sent.
It applies equally to a batch (db.CreateBatch()), which buffers in exactly the same way, and to every way of
waiting - await, .Result, .Wait(), .GetAwaiter().GetResult().
Why this happens so easily
It is the one place where the advice everyone has absorbed - await your tasks, and await them promptly - is
exactly wrong. Every other IDatabaseAsync method returns a task you should await straight away, and
ITransaction offers the same method names on what looks like the same interface. Nothing about the call site
suggests that this one is a buffer rather than a dispatch.
Fixes
Two are offered in the IDE:
- Discard the queued result -
_ = tran.StringSetAsync(...). Right when you only wanted the write. - Capture it and await after Execute - introduces a local for the task and moves the
awaitbelow theExecute[Async]call.
The second is only offered where it is unambiguously safe: the await form, with an Execute[Async] on the
same transaction later in the same block, and - if the wait declares a variable - nothing between the two
already using that variable, since the declaration is what moves.
Cases that are deliberately not flagged
- A task captured to a local, then awaited.
var pending = tran.StringGetAsync(key); await pending;is broken if it is above theExecuteand correct if it is below - it is the recommended fix. Telling those apart means reasoning about statement order, and this rule is an error: it must not guess. The same goes forawait Task.WhenAll(...)over queued tasks. - Fire-and-forget.
CommandFlags.FireAndForgetreturns an already-completed task, so awaiting it does not hang. It is still not giving you an answer, which is SER306 - a warning, not an error. - Flags this rule cannot read.
tran.StringGetAsync(key, flags), whereflagsis a variable, might carryFireAndForgetat runtime, so nothing is reported at all. - A transaction reached through
IDatabaseAsync. A helper takingIDatabaseAsynccannot tell a transaction from a plain database, and neither can this rule.
Suppressing
Reported as an error, so it fails the build. If you are suppressing it, please check first that the code really does complete - this rule fires only on shapes that cannot.
<NoWarn>$(NoWarn);SER305</NoWarn>
or locally:
#pragma warning disable SER305
If you believe it has flagged something that works, that is a bug in the rule and one that reaches every consumer of the package - please report it with the code as written.
See also Transactions and SER306.