Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

RC2 Breaking - Ensure migrations persist the executed key, when executed. #16113

4 changes: 4 additions & 0 deletions src/Umbraco.Infrastructure/Migrations/IMigrationContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,4 +44,8 @@ public interface IMigrationContext
[Obsolete("This will be removed in the V13, and replaced with a RebuildCache flag on the MigrationBase")]
void AddPostMigration<TMigration>()
where TMigration : MigrationBase;

bool IsCompleted { get; }

void Complete();
}
19 changes: 18 additions & 1 deletion src/Umbraco.Infrastructure/Migrations/MigrationContext.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
using Microsoft.Extensions.Logging;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Infrastructure.Persistence;

namespace Umbraco.Cms.Infrastructure.Migrations;
Expand All @@ -8,13 +10,15 @@ namespace Umbraco.Cms.Infrastructure.Migrations;
/// </summary>
internal class MigrationContext : IMigrationContext
{
private readonly Action? _onCompleteAction;
private readonly List<Type> _postMigrations = new();

/// <summary>
/// Initializes a new instance of the <see cref="MigrationContext" /> class.
/// </summary>
public MigrationContext(MigrationPlan plan, IUmbracoDatabase? database, ILogger<MigrationContext> logger)
public MigrationContext(MigrationPlan plan, IUmbracoDatabase? database, ILogger<MigrationContext> logger, Action? onCompleteAction = null)
{
_onCompleteAction = onCompleteAction;
Plan = plan;
Database = database ?? throw new ArgumentNullException(nameof(database));
Logger = logger ?? throw new ArgumentNullException(nameof(logger));
Expand All @@ -41,6 +45,19 @@ public MigrationContext(MigrationPlan plan, IUmbracoDatabase? database, ILogger<
/// <inheritdoc />
public bool BuildingExpression { get; set; }

public bool IsCompleted { get; private set; } = false;

public void Complete()
{
if (IsCompleted)
{
return;
}

_onCompleteAction?.Invoke();

IsCompleted = true;
}

/// <inheritdoc />
[Obsolete("This will be removed in the V13, and replaced with a RebuildCache flag on the MigrationBase, and a UmbracoPlanExecutedNotification.")]
Expand Down
68 changes: 56 additions & 12 deletions src/Umbraco.Infrastructure/Migrations/MigrationPlanExecutor.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Cache;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Migrations;
using Umbraco.Cms.Core.PublishedCache;
using Umbraco.Cms.Core.Scoping;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Infrastructure.Persistence;
using Umbraco.Cms.Infrastructure.Scoping;
using Umbraco.Extensions;
Expand Down Expand Up @@ -39,6 +41,7 @@
private readonly IMigrationBuilder _migrationBuilder;
private readonly IUmbracoDatabaseFactory _databaseFactory;
private readonly IPublishedSnapshotService _publishedSnapshotService;
private readonly IKeyValueService _keyValueService;
private readonly DistributedCache _distributedCache;
private readonly IScopeAccessor _scopeAccessor;
private readonly ICoreScopeProvider _scopeProvider;
Expand All @@ -51,19 +54,42 @@
IMigrationBuilder migrationBuilder,
IUmbracoDatabaseFactory databaseFactory,
IPublishedSnapshotService publishedSnapshotService,
DistributedCache distributedCache)
DistributedCache distributedCache,
IKeyValueService keyValueService)
{
_scopeProvider = scopeProvider;
_scopeAccessor = scopeAccessor;
_loggerFactory = loggerFactory;
_migrationBuilder = migrationBuilder;
_databaseFactory = databaseFactory;
_publishedSnapshotService = publishedSnapshotService;
_keyValueService = keyValueService;

Check notice on line 66 in src/Umbraco.Infrastructure/Migrations/MigrationPlanExecutor.cs

View check run for this annotation

CodeScene Delta Analysis / CodeScene Cloud Delta Analysis (v14/dev)

ℹ Getting worse: Constructor Over-Injection

MigrationPlanExecutor increases from 7 to 8 arguments, threshold = 5. This constructor has too many arguments, indicating an object with low cohesion or missing function argument abstraction. Avoid adding more arguments.
_distributedCache = distributedCache;
_logger = _loggerFactory.CreateLogger<MigrationPlanExecutor>();
}

[Obsolete("Use constructor with 7 parameters")]
[Obsolete("Use non-obsolete constructor. This will be removed in Umbraco 15.")]
public MigrationPlanExecutor(
ICoreScopeProvider scopeProvider,
IScopeAccessor scopeAccessor,
ILoggerFactory loggerFactory,
IMigrationBuilder migrationBuilder,
IUmbracoDatabaseFactory databaseFactory,
IPublishedSnapshotService publishedSnapshotService,
DistributedCache distributedCache)
: this(
scopeProvider,
scopeAccessor,
loggerFactory,
migrationBuilder,
StaticServiceProvider.Instance.GetRequiredService<IUmbracoDatabaseFactory>(),
StaticServiceProvider.Instance.GetRequiredService<IPublishedSnapshotService>(),
StaticServiceProvider.Instance.GetRequiredService<DistributedCache>(),
StaticServiceProvider.Instance.GetRequiredService<IKeyValueService>())
{
}

Check notice on line 90 in src/Umbraco.Infrastructure/Migrations/MigrationPlanExecutor.cs

View check run for this annotation

CodeScene Delta Analysis / CodeScene Cloud Delta Analysis (v14/dev)

ℹ New issue: Constructor Over-Injection

MigrationPlanExecutor has 7 arguments, threshold = 5. This constructor has too many arguments, indicating an object with low cohesion or missing function argument abstraction. Avoid adding more arguments.

[Obsolete("Use non-obsolete constructor. This will be removed in Umbraco 15.")]
public MigrationPlanExecutor(
ICoreScopeProvider scopeProvider,
IScopeAccessor scopeAccessor,
Expand All @@ -76,7 +102,9 @@
migrationBuilder,
StaticServiceProvider.Instance.GetRequiredService<IUmbracoDatabaseFactory>(),
StaticServiceProvider.Instance.GetRequiredService<IPublishedSnapshotService>(),
StaticServiceProvider.Instance.GetRequiredService<DistributedCache>())
StaticServiceProvider.Instance.GetRequiredService<DistributedCache>(),
StaticServiceProvider.Instance.GetRequiredService<IKeyValueService>()
)
{
}

Expand All @@ -92,7 +120,6 @@
/// <para>Each migration in the plan, may or may not run in a scope depending on the type of plan.</para>
/// <para>A plan can complete partially, the changes of each completed migration will be saved.</para>
/// </remarks>
[Obsolete("This will return an ExecutedMigrationPlan in V13")]
public ExecutedMigrationPlan ExecutePlan(MigrationPlan plan, string fromState)
{
plan.Validate();
Expand All @@ -104,6 +131,7 @@
// If any completed migration requires us to rebuild cache we'll do that.
if (_rebuildCache)
{
_logger.LogInformation("Starts rebuilding the cache. This can be a long running operation");
RebuildCache();
}

Expand Down Expand Up @@ -160,30 +188,37 @@
{
if (transition.MigrationType.IsAssignableTo(typeof(UnscopedMigrationBase)))
{
executedMigrationContexts.Add(RunUnscopedMigration(transition.MigrationType, plan));
executedMigrationContexts.Add(RunUnscopedMigration(transition, plan));
}
else
{
executedMigrationContexts.Add(RunScopedMigration(transition.MigrationType, plan));
executedMigrationContexts.Add(RunScopedMigration(transition, plan));
}
}
catch (Exception exception)
{
_logger.LogError(exception, "Plan {PlanName} failed at step {TargetState}", plan.Name, transition.TargetState);

// We have to always return something, so whatever running this has a chance to save the state we got to.
return new ExecutedMigrationPlan
{
Successful = false,
Exception = exception,
InitialState = fromState,
FinalState = transition.SourceState,
CompletedTransitions = completedTransitions,
Plan = plan,
ExecutedMigrationContexts = executedMigrationContexts
};
}


IEnumerable<IMigrationContext> nonCompletedMigrationsContexts = executedMigrationContexts.Where(x => x.IsCompleted is false);
if (nonCompletedMigrationsContexts.Any())
{
throw new InvalidOperationException($"Migration ({transition.MigrationType.FullName}) has been executed without indicated it was completed correctly.");
}

Check warning on line 221 in src/Umbraco.Infrastructure/Migrations/MigrationPlanExecutor.cs

View check run for this annotation

CodeScene Delta Analysis / CodeScene Cloud Delta Analysis (v14/dev)

❌ Getting worse: Complex Method

RunMigrationPlan increases in cyclomatic complexity from 9 to 10, threshold = 9. This function has many conditional statements (e.g. if, for, while), leading to lower code health. Avoid adding more conditionals and code to it without refactoring.
// The plan migration (transition), completed, so we'll add this to our list so we can return this at some point.
completedTransitions.Add(transition);
nextState = transition.TargetState;
Expand Down Expand Up @@ -233,17 +268,22 @@
};
}

private MigrationContext RunUnscopedMigration(Type migrationType, MigrationPlan plan)
private MigrationContext RunUnscopedMigration(MigrationPlan.Transition transition, MigrationPlan plan)
{
using IUmbracoDatabase database = _databaseFactory.CreateDatabase();
var context = new MigrationContext(plan, database, _loggerFactory.CreateLogger<MigrationContext>());
var context = new MigrationContext(plan, database, _loggerFactory.CreateLogger<MigrationContext>(), () => OnComplete(plan, transition.TargetState));

RunMigration(migrationType, context);
RunMigration(transition.MigrationType, context);

return context;
}

private MigrationContext RunScopedMigration(Type migrationType, MigrationPlan plan)
private void OnComplete(MigrationPlan plan, string targetState)
{
_keyValueService.SetValue(Constants.Conventions.Migrations.KeyValuePrefix + plan.Name, targetState);
}

private MigrationContext RunScopedMigration(MigrationPlan.Transition transition, MigrationPlan plan)
{
// We want to suppress scope (service, etc...) notifications during a migration plan
// execution. This is because if a package that doesn't have their migration plan
Expand All @@ -255,9 +295,13 @@
var context = new MigrationContext(
plan,
_scopeAccessor.AmbientScope?.Database,
_loggerFactory.CreateLogger<MigrationContext>());
_loggerFactory.CreateLogger<MigrationContext>(),
() => OnComplete(plan, transition.TargetState));

RunMigration(transition.MigrationType, context);

RunMigration(migrationType, context);
// Ensure we mark the context as complete before the scope completes
context.Complete();

scope.Complete();

Expand Down
4 changes: 0 additions & 4 deletions src/Umbraco.Infrastructure/Migrations/Upgrade/Upgrader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -70,10 +70,6 @@ public ExecutedMigrationPlan Execute(
return result;
}

// We always save the final state of the migration plan, this is because a partial success is possible
// So we still want to save the place we got to in the database-
SetState(result.FinalState, scopeProvider, keyValueService);

return result;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ private void MigrateSqlServer()
Database.Update(userGroup);
}

Context.Complete();

scope.Complete();
}

Expand Down Expand Up @@ -87,6 +89,8 @@ private void MigrateSqlite()

// Now that keys are disabled and we have a transaction, we'll do our migration.
MigrateColumnSqlite();

Context.Complete();
scope.Complete();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,13 @@ protected override void Migrate()
if (DatabaseType != DatabaseType.SQLite)
{
MigrateSqlServer();
Context.Complete();
scope.Complete();
return;
}

MigrateSqlite();
Context.Complete();
scope.Complete();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,13 @@ protected override void Migrate()
if (DatabaseType != DatabaseType.SQLite)
{
MigrateSqlServer();
Context.Complete();
scope.Complete();
return;
}

MigrateSqlite();
Context.Complete();
scope.Complete();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,11 +79,13 @@ protected override void Migrate()
if (DatabaseType != DatabaseType.SQLite)
{
MigrateUserTableSqlServer();
Context.Complete();
scope.Complete();
return;
}

MigrateUserTableSqlite();
Context.Complete();
scope.Complete();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,22 @@
// See LICENSE for more details.

using System.Linq;
using Microsoft.Extensions.Logging;
using Moq;
using NUnit.Framework;
using Umbraco.Cms.Core.Cache;
using Umbraco.Cms.Core.Configuration;
using Umbraco.Cms.Core.Events;
using Umbraco.Cms.Core.Migrations;
using Umbraco.Cms.Core.PublishedCache;
using Umbraco.Cms.Core.Scoping;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Infrastructure.Migrations;
using Umbraco.Cms.Infrastructure.Migrations.Install;
using Umbraco.Cms.Infrastructure.Migrations.Upgrade;
using Umbraco.Cms.Infrastructure.Persistence;
using Umbraco.Cms.Infrastructure.Persistence.Dtos;
using Umbraco.Cms.Infrastructure.Scoping;
using Umbraco.Cms.Tests.Common.Testing;
using Umbraco.Cms.Tests.Integration.Testing;

Expand All @@ -23,7 +29,22 @@ public class AdvancedMigrationTests : UmbracoIntegrationTest
{
private IUmbracoVersion UmbracoVersion => GetRequiredService<IUmbracoVersion>();
private IEventAggregator EventAggregator => GetRequiredService<IEventAggregator>();
private IMigrationPlanExecutor MigrationPlanExecutor => GetRequiredService<IMigrationPlanExecutor>();
private ICoreScopeProvider CoreScopeProvider => GetRequiredService<ICoreScopeProvider>();
private IScopeAccessor ScopeAccessor => GetRequiredService<IScopeAccessor>();
private ILoggerFactory LoggerFactory => GetRequiredService<ILoggerFactory>();
private IMigrationBuilder MigrationBuilder => GetRequiredService<IMigrationBuilder>();
private IUmbracoDatabaseFactory UmbracoDatabaseFactory => GetRequiredService<IUmbracoDatabaseFactory>();
private IPublishedSnapshotService PublishedSnapshotService => GetRequiredService<IPublishedSnapshotService>();
private DistributedCache DistributedCache => GetRequiredService<DistributedCache>();
private IMigrationPlanExecutor MigrationPlanExecutor => new MigrationPlanExecutor(
CoreScopeProvider,
ScopeAccessor,
LoggerFactory,
MigrationBuilder,
UmbracoDatabaseFactory,
PublishedSnapshotService,
DistributedCache,
Mock.Of<IKeyValueService>());

[Test]
public void CreateTableOfTDto()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ public void Do_ForDefinitionWithPrimaryKey_PrimaryKeyConstraintIsAdded()
.WithColumn("bar").AsInt32().PrimaryKey("PK_foo")
.Do();

// (TableName, ColumnName, ConstraintName)
// (TableName, ColumnName, ConstraintName)
var constraint = database.SqlContext.SqlSyntax.GetConstraintsPerColumn(database).Single();

Assert.Multiple(() =>
Expand Down Expand Up @@ -92,7 +92,7 @@ public void Do_ForDefinitionWithForeignKeys_ForeignKeysAreCreated()
.ForeignKey("MY_SUPER_COOL_FK", "foo", "bar")
.Do();

// (TableName, ColumnName, ConstraintName)
// (TableName, ColumnName, ConstraintName)
var constraint = database.SqlContext.SqlSyntax
.GetConstraintsPerColumn(database)
.Single(x => x.Item3 == "MY_SUPER_COOL_FK");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,8 @@ protected override void Migrate()

using var scope = _scopeProvider.CreateScope();
Assert.IsNull(((Scope)scope).ParentScope);

Context.Complete();
}
}

Expand Down Expand Up @@ -354,4 +356,4 @@ public SimpleMigrationStep(
: base(context) => _logger = logger;

protected override void Migrate() => _logger.LogDebug("Here be migration");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ public void CanExecute()
loggerFactory,
migrationBuilder,
databaseFactory,
Mock.Of<IPublishedSnapshotService>(), distributedCache);
Mock.Of<IPublishedSnapshotService>(), distributedCache, Mock.Of<IKeyValueService>());

var plan = new MigrationPlan("default")
.From(string.Empty)
Expand Down
Loading