Skip to content

Commit

Permalink
Lock the database while executing Migrate()
Browse files Browse the repository at this point in the history
Fixes #33731
  • Loading branch information
AndriySvyryd committed Jun 28, 2024
1 parent 5ac6eca commit 6923bc0
Show file tree
Hide file tree
Showing 21 changed files with 697 additions and 190 deletions.
76 changes: 53 additions & 23 deletions src/EFCore.Relational/Migrations/HistoryRepository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,6 @@ namespace Microsoft.EntityFrameworkCore.Migrations;
/// See <see href="https://aka.ms/efcore-docs-migrations">Database migrations</see> for more information and examples.
/// </para>
/// </remarks>
// TODO: Leverage query pipeline for GetAppliedMigrations
// TODO: Leverage update pipeline for GetInsertScript & GetDeleteScript
public abstract class HistoryRepository : IHistoryRepository
{
/// <summary>
Expand Down Expand Up @@ -122,15 +120,14 @@ protected virtual string ProductVersionColumnName
/// </summary>
/// <returns><see langword="true" /> if the table already exists, <see langword="false" /> otherwise.</returns>
public virtual bool Exists()
=> Dependencies.DatabaseCreator.Exists()
&& InterpretExistsResult(
Dependencies.RawSqlCommandBuilder.Build(ExistsSql).ExecuteScalar(
new RelationalCommandParameterObject(
Dependencies.Connection,
null,
null,
Dependencies.CurrentContext.Context,
Dependencies.CommandLogger, CommandSource.Migrations)));
=> InterpretExistsResult(
Dependencies.RawSqlCommandBuilder.Build(ExistsSql).ExecuteScalar(
new RelationalCommandParameterObject(
Dependencies.Connection,
null,
null,
Dependencies.CurrentContext.Context,
Dependencies.CommandLogger, CommandSource.Migrations)));

/// <summary>
/// Checks whether or not the history table exists.
Expand All @@ -142,16 +139,15 @@ public virtual bool Exists()
/// </returns>
/// <exception cref="OperationCanceledException">If the <see cref="CancellationToken" /> is canceled.</exception>
public virtual async Task<bool> ExistsAsync(CancellationToken cancellationToken = default)
=> await Dependencies.DatabaseCreator.ExistsAsync(cancellationToken).ConfigureAwait(false)
&& InterpretExistsResult(
await Dependencies.RawSqlCommandBuilder.Build(ExistsSql).ExecuteScalarAsync(
new RelationalCommandParameterObject(
Dependencies.Connection,
null,
null,
Dependencies.CurrentContext.Context,
Dependencies.CommandLogger, CommandSource.Migrations),
cancellationToken).ConfigureAwait(false));
=> InterpretExistsResult(
await Dependencies.RawSqlCommandBuilder.Build(ExistsSql).ExecuteScalarAsync(
new RelationalCommandParameterObject(
Dependencies.Connection,
null,
null,
Dependencies.CurrentContext.Context,
Dependencies.CommandLogger, CommandSource.Migrations),
cancellationToken).ConfigureAwait(false));

/// <summary>
/// Interprets the result of executing <see cref="ExistsSql" />.
Expand All @@ -171,15 +167,49 @@ await Dependencies.RawSqlCommandBuilder.Build(ExistsSql).ExecuteScalarAsync(
/// </summary>
/// <returns>The SQL script.</returns>
public virtual string GetCreateScript()
=> string.Concat(GetCreateCommands().Select(c => c.CommandText));

/// <summary>
/// Creates the history table.
/// </summary>
public virtual void Create()
=> Dependencies.MigrationCommandExecutor.ExecuteNonQuery(GetCreateCommands(), Dependencies.Connection);

/// <summary>
/// Creates the history table.
/// </summary>
public virtual Task CreateAsync(CancellationToken cancellationToken = default)
=> Dependencies.MigrationCommandExecutor.ExecuteNonQueryAsync(GetCreateCommands(), Dependencies.Connection, cancellationToken);

/// <summary>
/// Returns the migration commands that will create the history table.
/// </summary>
/// <returns>The migration commands that will create the history table.</returns>
protected virtual IReadOnlyList<MigrationCommand> GetCreateCommands()
{
var model = EnsureModel();

var operations = Dependencies.ModelDiffer.GetDifferences(null, model.GetRelationalModel());
var commandList = Dependencies.MigrationsSqlGenerator.Generate(operations, model);

return string.Concat(commandList.Select(c => c.CommandText));
return commandList;
}

/// <summary>
/// Gets an exclusive lock on the database.
/// </summary>
/// <param name="timeout">The time to wait for the lock before an exception is thrown.</param>
/// <returns>An object that can be disposed to release the lock.</returns>
public abstract IMigrationDatabaseLock GetDatabaseLock(TimeSpan timeout);

/// <summary>
/// Gets an exclusive lock on the database.
/// </summary>
/// <param name="timeout">The time to wait for the lock before an exception is thrown.</param>
/// <param name="cancellationToken">A <see cref="CancellationToken" /> to observe while waiting for the task to complete.</param>
/// <returns>An object that can be disposed to release the lock.</returns>
/// <exception cref="OperationCanceledException">If the <see cref="CancellationToken" /> is canceled.</exception>
public abstract Task<IMigrationDatabaseLock> GetDatabaseLockAsync(TimeSpan timeout, CancellationToken cancellationToken = default);

/// <summary>
/// Configures the entity type mapped to the history table.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ public HistoryRepositoryDependencies(
IDbContextOptions options,
IMigrationsModelDiffer modelDiffer,
IMigrationsSqlGenerator migrationsSqlGenerator,
IMigrationCommandExecutor migrationCommandExecutor,
ISqlGenerationHelper sqlGenerationHelper,
IConventionSetBuilder conventionSetBuilder,
ModelDependencies modelDependencies,
Expand All @@ -66,6 +67,7 @@ public HistoryRepositoryDependencies(
Options = options;
ModelDiffer = modelDiffer;
MigrationsSqlGenerator = migrationsSqlGenerator;
MigrationCommandExecutor = migrationCommandExecutor;
SqlGenerationHelper = sqlGenerationHelper;
ConventionSetBuilder = conventionSetBuilder;
ModelDependencies = modelDependencies;
Expand Down Expand Up @@ -110,6 +112,11 @@ public HistoryRepositoryDependencies(
/// </summary>
public ISqlGenerationHelper SqlGenerationHelper { get; init; }

/// <summary>
/// The service for executing Migrations operations.
/// </summary>
public IMigrationCommandExecutor MigrationCommandExecutor { get; init; }

/// <summary>
/// The core convention set to use when creating the model.
/// </summary>
Expand Down
36 changes: 34 additions & 2 deletions src/EFCore.Relational/Migrations/IHistoryRepository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,13 @@ namespace Microsoft.EntityFrameworkCore.Migrations;
public interface IHistoryRepository
{
/// <summary>
/// Checks whether or not the history table exists.
/// Checks whether the history table exists.
/// </summary>
/// <returns><see langword="true" /> if the table already exists, <see langword="false" /> otherwise.</returns>
bool Exists();

/// <summary>
/// Checks whether or not the history table exists.
/// Checks whether the history table exists.
/// </summary>
/// <param name="cancellationToken">A <see cref="CancellationToken" /> to observe while waiting for the task to complete.</param>
/// <returns>
Expand All @@ -40,6 +40,22 @@ public interface IHistoryRepository
/// <exception cref="OperationCanceledException">If the <see cref="CancellationToken" /> is canceled.</exception>
Task<bool> ExistsAsync(CancellationToken cancellationToken = default);

/// <summary>
/// Creates the history table.
/// </summary>
void Create();

/// <summary>
/// Creates the history table.
/// </summary>
/// <param name="cancellationToken">A <see cref="CancellationToken" /> to observe while waiting for the task to complete.</param>
/// <returns>
/// A task that represents the asynchronous operation. The task result contains
/// <see langword="true" /> if the table already exists, <see langword="false" /> otherwise.
/// </returns>
/// <exception cref="OperationCanceledException">If the <see cref="CancellationToken" /> is canceled.</exception>
Task CreateAsync(CancellationToken cancellationToken = default);

/// <summary>
/// Queries the history table for all migrations that have been applied.
/// </summary>
Expand All @@ -58,6 +74,22 @@ public interface IHistoryRepository
Task<IReadOnlyList<HistoryRow>> GetAppliedMigrationsAsync(
CancellationToken cancellationToken = default);

/// <summary>
/// Gets an exclusive lock on the database.
/// </summary>
/// <param name="timeout">The time to wait for the lock before an exception is thrown.</param>
/// <returns>An object that can be disposed to release the lock.</returns>
IMigrationDatabaseLock GetDatabaseLock(TimeSpan timeout);

/// <summary>
/// Gets an exclusive lock on the database.
/// </summary>
/// <param name="timeout">The time to wait for the lock before an exception is thrown.</param>
/// <param name="cancellationToken">A <see cref="CancellationToken" /> to observe while waiting for the task to complete.</param>
/// <returns>An object that can be disposed to release the lock.</returns>
/// <exception cref="OperationCanceledException">If the <see cref="CancellationToken" /> is canceled.</exception>
Task<IMigrationDatabaseLock> GetDatabaseLockAsync(TimeSpan timeout, CancellationToken cancellationToken = default);

/// <summary>
/// Generates a SQL script that will create the history table.
/// </summary>
Expand Down
11 changes: 11 additions & 0 deletions src/EFCore.Relational/Migrations/IMigrationDatabaseLock.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

namespace Microsoft.EntityFrameworkCore.Migrations;

/// <summary>
/// Represents an exclusive lock on the database that is used to ensure that only one migration application can be run at a time.
/// </summary>
public interface IMigrationDatabaseLock : IDisposable, IAsyncDisposable
{
}
Loading

0 comments on commit 6923bc0

Please sign in to comment.