Public Access
Server: start a retried write transaction from the database, not from the attempt that failed
This commit is contained in:
1 parent
36fb0817a4
commit
b2516ca7fb
8 files changed
+173
-21
No files matched your search
@@ -0,0 +1,72 @@
|
||||
using DodoSSH.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace DodoSSH.Api.Setup;
|
||||
|
||||
/// <summary>
|
||||
/// Runs a write transaction under the retrying execution strategy so that a retry starts from the database
|
||||
/// rather than from what the failed attempt left in the change tracker.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// <b>The strategy re-runs the delegate on the same context, and the context remembers the attempt that
|
||||
/// failed.</b> When the failure is the commit itself — a connection dropped after <c>SaveChanges</c> had
|
||||
/// already succeeded — every entity that attempt wrote is marked unchanged, holding values the database
|
||||
/// rolled back. The retry then assigns the same values again, change detection sees nothing to save, and
|
||||
/// the transaction commits without them. Enrolment lost <c>EnrolledAtUtc</c> that way, and a team hand-over
|
||||
/// its two role changes, each reporting success.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// So a retry clears the tracker, and reloads each entity the delegate was handed from outside it — the ones
|
||||
/// it modifies or reads but did not load itself — so they are tracked again with what the database actually
|
||||
/// holds and the delegate's own assignments register as changes. The first attempt runs exactly as it did
|
||||
/// before: nothing is cleared or reloaded until something has failed.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
internal static class FreshAttempts
|
||||
{
|
||||
/// <summary>Runs <paramref name="attempt"/> under the context's execution strategy, freshly each time.</summary>
|
||||
/// <param name="database">The request's context.</param>
|
||||
/// <param name="carried">Entities loaded before the transaction that the attempt reads or modifies.</param>
|
||||
/// <param name="attempt">The transaction, which begins and commits its own.</param>
|
||||
internal static Task<T> ExecuteFreshAsync<T>(
|
||||
this DodoDbContext database,
|
||||
IReadOnlyList<object> carried,
|
||||
Func<Task<T>> attempt)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(database);
|
||||
ArgumentNullException.ThrowIfNull(carried);
|
||||
ArgumentNullException.ThrowIfNull(attempt);
|
||||
|
||||
var strategy = database.Database.CreateExecutionStrategy();
|
||||
var attempts = 0;
|
||||
|
||||
return strategy.ExecuteAsync(async () =>
|
||||
{
|
||||
if (attempts++ > 0)
|
||||
{
|
||||
database.ChangeTracker.Clear();
|
||||
|
||||
foreach (var entity in carried)
|
||||
{
|
||||
await database.Entry(entity).ReloadAsync().ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
return await attempt().ConfigureAwait(false);
|
||||
});
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="ExecuteFreshAsync{T}" />
|
||||
internal static Task ExecuteFreshAsync(
|
||||
this DodoDbContext database,
|
||||
IReadOnlyList<object> carried,
|
||||
Func<Task> attempt) =>
|
||||
database.ExecuteFreshAsync(
|
||||
carried,
|
||||
async () =>
|
||||
{
|
||||
await attempt().ConfigureAwait(false);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
Reference in new issue
Block a user