Skip to content

Commit

Permalink
Execute migrations using the ExecutionStrategy
Browse files Browse the repository at this point in the history
Use a single transaction for all migrations in the script

Fixes #17578
Fixes #22616
  • Loading branch information
AndriySvyryd committed Jul 16, 2024
1 parent 69bcb92 commit 4697720
Show file tree
Hide file tree
Showing 14 changed files with 419 additions and 222 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ public static readonly IDictionary<Type, ServiceCharacteristics> RelationalServi
{ typeof(ISqlGenerationHelper), new ServiceCharacteristics(ServiceLifetime.Singleton) },
{ typeof(IRelationalAnnotationProvider), new ServiceCharacteristics(ServiceLifetime.Singleton) },
{ typeof(IMigrationsAnnotationProvider), new ServiceCharacteristics(ServiceLifetime.Singleton) },
{ typeof(IMigrationCommandExecutor), new ServiceCharacteristics(ServiceLifetime.Singleton) },
{ typeof(IMigrationCommandExecutor), new ServiceCharacteristics(ServiceLifetime.Scoped) },
{ typeof(IRelationalTypeMappingSource), new ServiceCharacteristics(ServiceLifetime.Singleton) },
{ typeof(IUpdateSqlGenerator), new ServiceCharacteristics(ServiceLifetime.Singleton) },
{ typeof(IRelationalTransactionFactory), new ServiceCharacteristics(ServiceLifetime.Singleton) },
Expand Down
7 changes: 4 additions & 3 deletions src/EFCore.Relational/Migrations/IMigrationCommandExecutor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,10 @@ namespace Microsoft.EntityFrameworkCore.Migrations;
/// </summary>
/// <remarks>
/// <para>
/// The service lifetime is <see cref="ServiceLifetime.Singleton" />. This means a single instance
/// is used by many <see cref="DbContext" /> instances. The implementation must be thread-safe.
/// This service cannot depend on services registered as <see cref="ServiceLifetime.Scoped" />.
/// The service lifetime is <see cref="ServiceLifetime.Scoped" />. This means that each
/// <see cref="DbContext" /> instance will use its own instance of this service.
/// The implementation may depend on other services registered with any lifetime.
/// The implementation does not need to be thread-safe.
/// </para>
/// <para>
/// See <see href="https://aka.ms/efcore-docs-migrations">Database migrations</see> for more information and examples.
Expand Down
202 changes: 131 additions & 71 deletions src/EFCore.Relational/Migrations/Internal/MigrationCommandExecutor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,19 @@ namespace Microsoft.EntityFrameworkCore.Migrations.Internal;
/// </summary>
public class MigrationCommandExecutor : IMigrationCommandExecutor
{
private readonly IExecutionStrategy _executionStrategy;

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
public MigrationCommandExecutor(IExecutionStrategy executionStrategy)
{
_executionStrategy = executionStrategy;
}

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
Expand All @@ -24,53 +37,70 @@ public virtual void ExecuteNonQuery(
IRelationalConnection connection)
{
var userTransaction = connection.CurrentTransaction;
if (userTransaction is not null && migrationCommands.Any(x => x.TransactionSuppressed))
if (userTransaction is not null
&& (migrationCommands.Any(x => x.TransactionSuppressed) || _executionStrategy.RetriesOnFailure))
{
throw new NotSupportedException(RelationalStrings.TransactionSuppressedMigrationInUserTransaction);
}

using (new TransactionScope(TransactionScopeOption.Suppress, TransactionScopeAsyncFlowOption.Enabled))
{
connection.Open();
var parameters = new ExecuteParameters(migrationCommands.ToList(), connection);
if (userTransaction is null)
{
_executionStrategy.Execute(parameters, static (_, p) => Execute(p, beginTransaction: true), verifySucceeded: null);
}
else
{
Execute(parameters, beginTransaction: false);
}
}
}

try
private static bool Execute(ExecuteParameters parameters, bool beginTransaction)
{
var migrationCommands = parameters.MigrationCommands;
var connection = parameters.Connection;
IDbContextTransaction? transaction = null;
connection.Open();
try
{
for (var i = parameters.CurrentCommandIndex; i < migrationCommands.Count; i++)
{
IDbContextTransaction? transaction = null;
var command = migrationCommands[i];
if (transaction == null
&& !command.TransactionSuppressed
&& beginTransaction)
{
transaction = connection.BeginTransaction();
}

try
if (transaction != null
&& command.TransactionSuppressed)
{
foreach (var command in migrationCommands)
{
if (transaction == null
&& !command.TransactionSuppressed
&& userTransaction is null)
{
transaction = connection.BeginTransaction();
}

if (transaction != null
&& command.TransactionSuppressed)
{
transaction.Commit();
transaction.Dispose();
transaction = null;
}

command.ExecuteNonQuery(connection);
}

transaction?.Commit();
transaction.Commit();
transaction.Dispose();
transaction = null;
parameters.CurrentCommandIndex = i;
}
finally

command.ExecuteNonQuery(connection);

if (transaction == null)
{
transaction?.Dispose();
parameters.CurrentCommandIndex = i + 1;
}
}
finally
{
connection.Close();
}

transaction?.Commit();
}
finally
{
transaction?.Dispose();
connection.Close();
}

return true;
}

/// <summary>
Expand All @@ -85,65 +115,95 @@ public virtual async Task ExecuteNonQueryAsync(
CancellationToken cancellationToken = default)
{
var userTransaction = connection.CurrentTransaction;
if (userTransaction is not null && migrationCommands.Any(x => x.TransactionSuppressed))
if (userTransaction is not null
&& (migrationCommands.Any(x => x.TransactionSuppressed) || _executionStrategy.RetriesOnFailure))
{
throw new NotSupportedException(RelationalStrings.TransactionSuppressedMigrationInUserTransaction);
}

var transactionScope = new TransactionScope(TransactionScopeOption.Suppress, TransactionScopeAsyncFlowOption.Enabled);
try
{
await connection.OpenAsync(cancellationToken).ConfigureAwait(false);
var parameters = new ExecuteParameters(migrationCommands.ToList(), connection);
if (userTransaction is null)
{
await _executionStrategy.ExecuteAsync(
parameters,
static (_, p, ct) => ExecuteAsync(p, beginTransaction: true, ct),
verifySucceeded: null,
cancellationToken).ConfigureAwait(false);
}
else
{
await ExecuteAsync(parameters, beginTransaction: false, cancellationToken).ConfigureAwait(false);
}

}
finally
{
await transactionScope.DisposeAsyncIfAvailable().ConfigureAwait(false);
}
}

try
private static async Task<bool> ExecuteAsync(ExecuteParameters parameters, bool beginTransaction, CancellationToken cancellationToken)
{
var migrationCommands = parameters.MigrationCommands;
var connection = parameters.Connection;
IDbContextTransaction? transaction = null;
await connection.OpenAsync(cancellationToken).ConfigureAwait(false);
try
{
for (var i = parameters.CurrentCommandIndex; i < migrationCommands.Count; i++)
{
IDbContextTransaction? transaction = null;
var command = migrationCommands[i];
if (transaction == null
&& !command.TransactionSuppressed
&& beginTransaction)
{
transaction = await connection.BeginTransactionAsync(cancellationToken)
.ConfigureAwait(false);
}

try
if (transaction != null
&& command.TransactionSuppressed)
{
foreach (var command in migrationCommands)
{
if (transaction == null
&& !command.TransactionSuppressed
&& userTransaction is null)
{
transaction = await connection.BeginTransactionAsync(cancellationToken)
.ConfigureAwait(false);
}

if (transaction != null
&& command.TransactionSuppressed)
{
await transaction.CommitAsync(cancellationToken).ConfigureAwait(false);
await transaction.DisposeAsync().ConfigureAwait(false);
transaction = null;
}

await command.ExecuteNonQueryAsync(connection, cancellationToken: cancellationToken)
.ConfigureAwait(false);
}

if (transaction != null)
{
await transaction.CommitAsync(cancellationToken).ConfigureAwait(false);
}
await transaction.CommitAsync(cancellationToken).ConfigureAwait(false);
await transaction.DisposeAsync().ConfigureAwait(false);
transaction = null;
parameters.CurrentCommandIndex = i;
}
finally

await command.ExecuteNonQueryAsync(connection, cancellationToken: cancellationToken)
.ConfigureAwait(false);

if (transaction == null)
{
if (transaction != null)
{
await transaction.DisposeAsync().ConfigureAwait(false);
}
parameters.CurrentCommandIndex = i + 1;
}
}
finally

if (transaction != null)
{
await connection.CloseAsync().ConfigureAwait(false);
await transaction.CommitAsync(cancellationToken).ConfigureAwait(false);
}
}
finally
{
await transactionScope.DisposeAsyncIfAvailable().ConfigureAwait(false);
if (transaction != null)
{
await transaction.DisposeAsync().ConfigureAwait(false);
}

await connection.CloseAsync().ConfigureAwait(false);
}

return true;
}

private sealed class ExecuteParameters(List<MigrationCommand> migrationCommands, IRelationalConnection connection)
{
public int CurrentCommandIndex;
public List<MigrationCommand> MigrationCommands { get; } = migrationCommands;
public IRelationalConnection Connection { get; } = connection;
}
}
26 changes: 16 additions & 10 deletions src/EFCore.Relational/Migrations/Internal/Migrator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license.

using Microsoft.EntityFrameworkCore.Diagnostics.Internal;
using Microsoft.EntityFrameworkCore.Storage;

namespace Microsoft.EntityFrameworkCore.Migrations.Internal;

Expand Down Expand Up @@ -383,6 +384,7 @@ public virtual string GenerateScript(
var migrationsToApply = migratorData.AppliedMigrations;
var migrationsToRevert = migratorData.RevertedMigrations;
var actualTargetMigration = migratorData.TargetMigration;
var transactionStarted = false;
for (var i = 0; i < migrationsToRevert.Count; i++)
{
var migration = migrationsToRevert[i];
Expand All @@ -396,7 +398,9 @@ public virtual string GenerateScript(
? _historyRepository.GetBeginIfExistsScript(migration.GetId())
: null;

GenerateSqlScript(GenerateDownSql(migration, previousMigration, options), builder, _sqlGenerationHelper, noTransactions, idempotencyCondition, idempotencyEnd);
GenerateSqlScript(
GenerateDownSql(migration, previousMigration, options),
builder, _sqlGenerationHelper, ref transactionStarted, noTransactions, idempotencyCondition, idempotencyEnd);
}

foreach (var migration in migrationsToApply)
Expand All @@ -407,7 +411,16 @@ public virtual string GenerateScript(
? _historyRepository.GetBeginIfNotExistsScript(migration.GetId())
: null;

GenerateSqlScript(GenerateUpSql(migration, options), builder, _sqlGenerationHelper, noTransactions, idempotencyCondition, idempotencyEnd);
GenerateSqlScript(
GenerateUpSql(migration, options),
builder, _sqlGenerationHelper, ref transactionStarted, noTransactions, idempotencyCondition, idempotencyEnd);
}

if (!noTransactions && transactionStarted)
{
builder
.AppendLine(_sqlGenerationHelper.CommitTransactionStatement)
.Append(_sqlGenerationHelper.BatchTerminator);
}

return builder.ToString();
Expand All @@ -417,11 +430,11 @@ private static void GenerateSqlScript(
IEnumerable<MigrationCommand> commands,
IndentedStringBuilder builder,
ISqlGenerationHelper sqlGenerationHelper,
ref bool transactionStarted,
bool noTransactions = false,
string? idempotencyCondition = null,
string? idempotencyEnd = null)
{
var transactionStarted = false;
foreach (var command in commands)
{
if (!noTransactions)
Expand Down Expand Up @@ -461,13 +474,6 @@ private static void GenerateSqlScript(

builder.Append(sqlGenerationHelper.BatchTerminator);
}

if (!noTransactions && transactionStarted)
{
builder
.AppendLine(sqlGenerationHelper.CommitTransactionStatement)
.Append(sqlGenerationHelper.BatchTerminator);
}
}

/// <summary>
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion src/EFCore.Relational/Properties/RelationalStrings.resx
Original file line number Diff line number Diff line change
Expand Up @@ -1209,7 +1209,7 @@
<value>The specified transaction is not associated with the current connection. Only transactions associated with the current connection may be used.</value>
</data>
<data name="TransactionSuppressedMigrationInUserTransaction" xml:space="preserve">
<value>User transaction is not supported with a TransactionSuppressed migrations.</value>
<value>User transaction is not supported with a TransactionSuppressed migrations or a retrying execution strategy.</value>
</data>
<data name="TriggerWithMismatchedTable" xml:space="preserve">
<value>Trigger '{trigger}' for table '{triggerTable}' is defined on entity type '{entityType}', which is mapped to table '{entityTable}'. See https://aka.ms/efcore-docs-triggers for more information on triggers.</value>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,9 +117,8 @@ public static IServiceCollection AddEntityFrameworkSqlite(this IServiceCollectio
.TryAdd<ISqlExpressionFactory, SqliteSqlExpressionFactory>()
.TryAdd<IRelationalParameterBasedSqlProcessorFactory, SqliteParameterBasedSqlProcessorFactory>()
.TryAddProviderSpecificServices(
b => b.TryAddScoped<ISqliteRelationalConnection, SqliteRelationalConnection>());

builder.TryAddCoreServices();
b => b.TryAddScoped<ISqliteRelationalConnection, SqliteRelationalConnection>())
.TryAddCoreServices();

return serviceCollection;
}
Expand Down
Loading

0 comments on commit 4697720

Please sign in to comment.