Skip to content

Commit

Permalink
Migrations lock.
Browse files Browse the repository at this point in the history
  • Loading branch information
cincuranet committed Dec 10, 2024
1 parent 5c4582b commit 80f25c7
Show file tree
Hide file tree
Showing 2 changed files with 165 additions and 12 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -16,42 +16,156 @@
//$Authors = Jiri Cincura ([email protected]), Jean Ressouche, Rafael Almeida ([email protected])

using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore.Diagnostics;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage;

namespace FirebirdSql.EntityFrameworkCore.Firebird.Migrations.Internal;

public class FbHistoryRepository : HistoryRepository
public class FbHistoryRepository(HistoryRepositoryDependencies dependencies) : HistoryRepository(dependencies)
{
public FbHistoryRepository(HistoryRepositoryDependencies dependencies)
: base(dependencies)
{ }
static readonly TimeSpan RetryDelay = TimeSpan.FromSeconds(1);

protected override string ExistsSql
{
get
{
var escapedTableName = Dependencies.TypeMappingSource.GetMapping(typeof(string)).GenerateSqlLiteral(TableName);
return $@"SELECT COUNT(*) FROM rdb$relations WHERE COALESCE(rdb$system_flag, 0) = 0 AND rdb$view_blr IS NULL AND rdb$relation_name = {escapedTableName}";
return $"""
SELECT COUNT(*) FROM rdb$relations WHERE COALESCE(rdb$system_flag, 0) = 0 AND rdb$view_blr IS NULL AND rdb$relation_name = {escapedTableName}
""";
}
}

protected override bool InterpretExistsResult(object value) => Convert.ToInt64(value) != 0;
protected override bool InterpretExistsResult(object value)
=> Convert.ToInt64(value) != 0;

public override string GetCreateIfNotExistsScript() => GetCreateScript();
public override string GetCreateIfNotExistsScript()
=> GetCreateScript();

public override string GetBeginIfExistsScript(string migrationId)
protected virtual string LockTableName { get; } = "__EFMigrationsLock";

public override LockReleaseBehavior LockReleaseBehavior
=> LockReleaseBehavior.Explicit;

public override IMigrationsDatabaseLock AcquireDatabaseLock()
{
throw new NotSupportedException("Generating idempotent scripts is currently not supported.");
Dependencies.MigrationsLogger.AcquiringMigrationLock();

CreateLockTableCommand().ExecuteNonQuery(CreateRelationalCommandParameters());

var retryDelay = RetryDelay;
while (true)
{
var dbLock = CreateMigrationDatabaseLock();
var insertCount = CreateInsertLockCommand(DateTime.UtcNow)
.ExecuteScalar(CreateRelationalCommandParameters());
if ((int)insertCount == 1)
{
return dbLock;
}

Thread.Sleep(retryDelay);
if (retryDelay < TimeSpan.FromMinutes(1))
{
retryDelay = retryDelay.Add(retryDelay);
}
}
}

public override string GetBeginIfNotExistsScript(string migrationId)
public override async Task<IMigrationsDatabaseLock> AcquireDatabaseLockAsync(CancellationToken cancellationToken = default)
{
throw new NotSupportedException("Generating idempotent scripts is currently not supported.");
Dependencies.MigrationsLogger.AcquiringMigrationLock();

await CreateLockTableCommand().ExecuteNonQueryAsync(CreateRelationalCommandParameters(), cancellationToken).ConfigureAwait(false);

var retryDelay = RetryDelay;
while (true)
{
var dbLock = CreateMigrationDatabaseLock();
var insertCount = await CreateInsertLockCommand(DateTime.UtcNow)
.ExecuteScalarAsync(CreateRelationalCommandParameters(), cancellationToken)
.ConfigureAwait(false);
if ((int)insertCount == 1)
{
return dbLock;
}

await Task.Delay(RetryDelay, cancellationToken).ConfigureAwait(true);
if (retryDelay < TimeSpan.FromMinutes(1))
{
retryDelay = retryDelay.Add(retryDelay);
}
}
}

public override string GetBeginIfExistsScript(string migrationId)
=> throw new NotSupportedException("Generating idempotent scripts is currently not supported.");

public override string GetBeginIfNotExistsScript(string migrationId)
=> throw new NotSupportedException("Generating idempotent scripts is currently not supported.");

public override string GetEndIfScript()
=> throw new NotSupportedException("Generating idempotent scripts is currently not supported.");

IRelationalCommand CreateLockTableCommand()
{
throw new NotSupportedException("Generating idempotent scripts is currently not supported.");
return Dependencies.RawSqlCommandBuilder.Build($"""
EXECUTE BLOCK
AS
BEGIN
BEGIN
EXECUTE STATEMENT
'CREATE TABLE "{LockTableName}" (
"Id" INT NOT NULL CONSTRAINT "PK_{LockTableName}" PRIMARY KEY,
"Timestamp" TIMESTAMP NOT NULL
)';
WHEN SQLSTATE '42S01' DO
BEGIN
END
END
END
""");
}

IRelationalCommand CreateInsertLockCommand(DateTime timestamp)
{
var timestampLiteral = Dependencies.TypeMappingSource.GetMapping(typeof(DateTime)).GenerateSqlLiteral(timestamp);
return Dependencies.RawSqlCommandBuilder.Build($"""
EXECUTE BLOCK
RETURNS (ROWS_AFFECTED INT)
AS
BEGIN
ROWS_AFFECTED = 1;
BEGIN
INSERT INTO "{LockTableName}" ("Id", "Timestamp") VALUES (1, {timestampLiteral});
WHEN SQLSTATE '23000' DO
BEGIN
ROWS_AFFECTED = 0;
END
END
SUSPEND;
END
""");
}

IRelationalCommand CreateDeleteLockCommand()
{
return Dependencies.RawSqlCommandBuilder.Build($"""
DELETE FROM "{LockTableName}" WHERE "Id" = 1
""");
}

SqliteMigrationDatabaseLock CreateMigrationDatabaseLock()
=> new(CreateDeleteLockCommand(), CreateRelationalCommandParameters(), this);

RelationalCommandParameterObject CreateRelationalCommandParameters()
=> new(
Dependencies.Connection,
null,
null,
Dependencies.CurrentContext.Context,
Dependencies.CommandLogger, CommandSource.Migrations);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/*
* The contents of this file are subject to the Initial
* Developer's Public License Version 1.0 (the "License");
* you may not use this file except in compliance with the
* License. You may obtain a copy of the License at
* https://github.com/FirebirdSQL/NETProvider/raw/master/license.txt.
*
* Software distributed under the License is distributed on
* an "AS IS" basis, WITHOUT WARRANTY OF ANY KIND, either
* express or implied. See the License for the specific
* language governing rights and limitations under the License.
*
* All Rights Reserved.
*/

//$Authors = Jiri Cincura ([email protected])

using System.Threading;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage;

namespace FirebirdSql.EntityFrameworkCore.Firebird.Migrations.Internal;

public class SqliteMigrationDatabaseLock(
IRelationalCommand releaseLockCommand,
RelationalCommandParameterObject relationalCommandParameters,
IHistoryRepository historyRepository,
CancellationToken cancellationToken = default)
: IMigrationsDatabaseLock
{
public virtual IHistoryRepository HistoryRepository => historyRepository;

public void Dispose()
=> releaseLockCommand.ExecuteScalar(relationalCommandParameters);

public async ValueTask DisposeAsync()
=> await releaseLockCommand.ExecuteScalarAsync(relationalCommandParameters, cancellationToken).ConfigureAwait(false);
}

0 comments on commit 80f25c7

Please sign in to comment.