Skip to content

Commit 7b46d44

Browse files
RC2 Breaking - Ensure migrations persist the executed key, when executed. (#16113)
* Adds new functionality to the migrations. This requires a migration to call Context.SetDone() on the migration context. This happens automatically on scoped migrations before the scope is completed. But migrations inheriting the UnScopedMigrationBase needs to call this manually, inside the scopes or when it is considered done. Thereby, we minimize the risk (and eliminate it for SqlServer) that a migration is executed but the state is not saved. If a migration is executed without the SetDone is called, the migration upgrader throws an error, so we do not start executing the next migration * Updated tests * Renamed after review suggestion * Rename in test * More renaming after review * Remove public modifier from interface * Add missing space in exception message --------- Co-authored-by: nikolajlauridsen <[email protected]>
1 parent c6898ec commit 7b46d44

File tree

12 files changed

+116
-22
lines changed

12 files changed

+116
-22
lines changed

src/Umbraco.Infrastructure/Migrations/IMigrationContext.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,4 +44,8 @@ public interface IMigrationContext
4444
[Obsolete("This will be removed in the V13, and replaced with a RebuildCache flag on the MigrationBase")]
4545
void AddPostMigration<TMigration>()
4646
where TMigration : MigrationBase;
47+
48+
bool IsCompleted { get; }
49+
50+
void Complete();
4751
}

src/Umbraco.Infrastructure/Migrations/MigrationContext.cs

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
using Microsoft.Extensions.Logging;
2+
using Umbraco.Cms.Core;
3+
using Umbraco.Cms.Core.Services;
24
using Umbraco.Cms.Infrastructure.Persistence;
35

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

1316
/// <summary>
1417
/// Initializes a new instance of the <see cref="MigrationContext" /> class.
1518
/// </summary>
16-
public MigrationContext(MigrationPlan plan, IUmbracoDatabase? database, ILogger<MigrationContext> logger)
19+
public MigrationContext(MigrationPlan plan, IUmbracoDatabase? database, ILogger<MigrationContext> logger, Action? onCompleteAction = null)
1720
{
21+
_onCompleteAction = onCompleteAction;
1822
Plan = plan;
1923
Database = database ?? throw new ArgumentNullException(nameof(database));
2024
Logger = logger ?? throw new ArgumentNullException(nameof(logger));
@@ -41,6 +45,19 @@ public MigrationContext(MigrationPlan plan, IUmbracoDatabase? database, ILogger<
4145
/// <inheritdoc />
4246
public bool BuildingExpression { get; set; }
4347

48+
public bool IsCompleted { get; private set; } = false;
49+
50+
public void Complete()
51+
{
52+
if (IsCompleted)
53+
{
54+
return;
55+
}
56+
57+
_onCompleteAction?.Invoke();
58+
59+
IsCompleted = true;
60+
}
4461

4562
/// <inheritdoc />
4663
[Obsolete("This will be removed in the V13, and replaced with a RebuildCache flag on the MigrationBase, and a UmbracoPlanExecutedNotification.")]

src/Umbraco.Infrastructure/Migrations/MigrationPlanExecutor.cs

Lines changed: 56 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
using Microsoft.Extensions.DependencyInjection;
22
using Microsoft.Extensions.Logging;
3+
using Umbraco.Cms.Core;
34
using Umbraco.Cms.Core.Cache;
45
using Umbraco.Cms.Core.DependencyInjection;
56
using Umbraco.Cms.Core.Migrations;
67
using Umbraco.Cms.Core.PublishedCache;
78
using Umbraco.Cms.Core.Scoping;
9+
using Umbraco.Cms.Core.Services;
810
using Umbraco.Cms.Infrastructure.Persistence;
911
using Umbraco.Cms.Infrastructure.Scoping;
1012
using Umbraco.Extensions;
@@ -39,6 +41,7 @@ public class MigrationPlanExecutor : IMigrationPlanExecutor
3941
private readonly IMigrationBuilder _migrationBuilder;
4042
private readonly IUmbracoDatabaseFactory _databaseFactory;
4143
private readonly IPublishedSnapshotService _publishedSnapshotService;
44+
private readonly IKeyValueService _keyValueService;
4245
private readonly DistributedCache _distributedCache;
4346
private readonly IScopeAccessor _scopeAccessor;
4447
private readonly ICoreScopeProvider _scopeProvider;
@@ -51,19 +54,42 @@ public MigrationPlanExecutor(
5154
IMigrationBuilder migrationBuilder,
5255
IUmbracoDatabaseFactory databaseFactory,
5356
IPublishedSnapshotService publishedSnapshotService,
54-
DistributedCache distributedCache)
57+
DistributedCache distributedCache,
58+
IKeyValueService keyValueService)
5559
{
5660
_scopeProvider = scopeProvider;
5761
_scopeAccessor = scopeAccessor;
5862
_loggerFactory = loggerFactory;
5963
_migrationBuilder = migrationBuilder;
6064
_databaseFactory = databaseFactory;
6165
_publishedSnapshotService = publishedSnapshotService;
66+
_keyValueService = keyValueService;
6267
_distributedCache = distributedCache;
6368
_logger = _loggerFactory.CreateLogger<MigrationPlanExecutor>();
6469
}
6570

66-
[Obsolete("Use constructor with 7 parameters")]
71+
[Obsolete("Use non-obsolete constructor. This will be removed in Umbraco 15.")]
72+
public MigrationPlanExecutor(
73+
ICoreScopeProvider scopeProvider,
74+
IScopeAccessor scopeAccessor,
75+
ILoggerFactory loggerFactory,
76+
IMigrationBuilder migrationBuilder,
77+
IUmbracoDatabaseFactory databaseFactory,
78+
IPublishedSnapshotService publishedSnapshotService,
79+
DistributedCache distributedCache)
80+
: this(
81+
scopeProvider,
82+
scopeAccessor,
83+
loggerFactory,
84+
migrationBuilder,
85+
StaticServiceProvider.Instance.GetRequiredService<IUmbracoDatabaseFactory>(),
86+
StaticServiceProvider.Instance.GetRequiredService<IPublishedSnapshotService>(),
87+
StaticServiceProvider.Instance.GetRequiredService<DistributedCache>(),
88+
StaticServiceProvider.Instance.GetRequiredService<IKeyValueService>())
89+
{
90+
}
91+
92+
[Obsolete("Use non-obsolete constructor. This will be removed in Umbraco 15.")]
6793
public MigrationPlanExecutor(
6894
ICoreScopeProvider scopeProvider,
6995
IScopeAccessor scopeAccessor,
@@ -76,7 +102,9 @@ public MigrationPlanExecutor(
76102
migrationBuilder,
77103
StaticServiceProvider.Instance.GetRequiredService<IUmbracoDatabaseFactory>(),
78104
StaticServiceProvider.Instance.GetRequiredService<IPublishedSnapshotService>(),
79-
StaticServiceProvider.Instance.GetRequiredService<DistributedCache>())
105+
StaticServiceProvider.Instance.GetRequiredService<DistributedCache>(),
106+
StaticServiceProvider.Instance.GetRequiredService<IKeyValueService>()
107+
)
80108
{
81109
}
82110

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

@@ -160,11 +188,11 @@ private ExecutedMigrationPlan RunMigrationPlan(MigrationPlan plan, string fromSt
160188
{
161189
if (transition.MigrationType.IsAssignableTo(typeof(UnscopedMigrationBase)))
162190
{
163-
executedMigrationContexts.Add(RunUnscopedMigration(transition.MigrationType, plan));
191+
executedMigrationContexts.Add(RunUnscopedMigration(transition, plan));
164192
}
165193
else
166194
{
167-
executedMigrationContexts.Add(RunScopedMigration(transition.MigrationType, plan));
195+
executedMigrationContexts.Add(RunScopedMigration(transition, plan));
168196
}
169197
}
170198
catch (Exception exception)
@@ -184,6 +212,13 @@ private ExecutedMigrationPlan RunMigrationPlan(MigrationPlan plan, string fromSt
184212
};
185213
}
186214

215+
216+
IEnumerable<IMigrationContext> nonCompletedMigrationsContexts = executedMigrationContexts.Where(x => x.IsCompleted is false);
217+
if (nonCompletedMigrationsContexts.Any())
218+
{
219+
throw new InvalidOperationException($"Migration ({transition.MigrationType.FullName}) has been executed without indicated it was completed correctly.");
220+
}
221+
187222
// The plan migration (transition), completed, so we'll add this to our list so we can return this at some point.
188223
completedTransitions.Add(transition);
189224
nextState = transition.TargetState;
@@ -233,17 +268,22 @@ private ExecutedMigrationPlan RunMigrationPlan(MigrationPlan plan, string fromSt
233268
};
234269
}
235270

236-
private MigrationContext RunUnscopedMigration(Type migrationType, MigrationPlan plan)
271+
private MigrationContext RunUnscopedMigration(MigrationPlan.Transition transition, MigrationPlan plan)
237272
{
238273
using IUmbracoDatabase database = _databaseFactory.CreateDatabase();
239-
var context = new MigrationContext(plan, database, _loggerFactory.CreateLogger<MigrationContext>());
274+
var context = new MigrationContext(plan, database, _loggerFactory.CreateLogger<MigrationContext>(), () => OnComplete(plan, transition.TargetState));
240275

241-
RunMigration(migrationType, context);
276+
RunMigration(transition.MigrationType, context);
242277

243278
return context;
244279
}
245280

246-
private MigrationContext RunScopedMigration(Type migrationType, MigrationPlan plan)
281+
private void OnComplete(MigrationPlan plan, string targetState)
282+
{
283+
_keyValueService.SetValue(Constants.Conventions.Migrations.KeyValuePrefix + plan.Name, targetState);
284+
}
285+
286+
private MigrationContext RunScopedMigration(MigrationPlan.Transition transition, MigrationPlan plan)
247287
{
248288
// We want to suppress scope (service, etc...) notifications during a migration plan
249289
// execution. This is because if a package that doesn't have their migration plan
@@ -255,9 +295,13 @@ private MigrationContext RunScopedMigration(Type migrationType, MigrationPlan pl
255295
var context = new MigrationContext(
256296
plan,
257297
_scopeAccessor.AmbientScope?.Database,
258-
_loggerFactory.CreateLogger<MigrationContext>());
298+
_loggerFactory.CreateLogger<MigrationContext>(),
299+
() => OnComplete(plan, transition.TargetState));
300+
301+
RunMigration(transition.MigrationType, context);
259302

260-
RunMigration(migrationType, context);
303+
// Ensure we mark the context as complete before the scope completes
304+
context.Complete();
261305

262306
scope.Complete();
263307

src/Umbraco.Infrastructure/Migrations/Upgrade/Upgrader.cs

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -70,10 +70,6 @@ public ExecutedMigrationPlan Execute(
7070
return result;
7171
}
7272

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

src/Umbraco.Infrastructure/Migrations/Upgrade/V_14_0_0/AddGuidsToUserGroups.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,8 @@ private void MigrateSqlServer()
5757
Database.Update(userGroup);
5858
}
5959

60+
Context.Complete();
61+
6062
scope.Complete();
6163
}
6264

@@ -87,6 +89,8 @@ private void MigrateSqlite()
8789

8890
// Now that keys are disabled and we have a transaction, we'll do our migration.
8991
MigrateColumnSqlite();
92+
93+
Context.Complete();
9094
scope.Complete();
9195
}
9296

src/Umbraco.Infrastructure/Migrations/Upgrade/V_14_0_0/AddGuidsToUsers.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,11 +33,13 @@ protected override void Migrate()
3333
if (DatabaseType != DatabaseType.SQLite)
3434
{
3535
MigrateSqlServer();
36+
Context.Complete();
3637
scope.Complete();
3738
return;
3839
}
3940

4041
MigrateSqlite();
42+
Context.Complete();
4143
scope.Complete();
4244
}
4345

src/Umbraco.Infrastructure/Migrations/Upgrade/V_14_0_0/AddListViewKeysToDocumentTypes.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,13 @@ protected override void Migrate()
3131
if (DatabaseType != DatabaseType.SQLite)
3232
{
3333
MigrateSqlServer();
34+
Context.Complete();
3435
scope.Complete();
3536
return;
3637
}
3738

3839
MigrateSqlite();
40+
Context.Complete();
3941
scope.Complete();
4042
}
4143

src/Umbraco.Infrastructure/Migrations/Upgrade/V_14_0_0/MigrateTours.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,11 +79,13 @@ protected override void Migrate()
7979
if (DatabaseType != DatabaseType.SQLite)
8080
{
8181
MigrateUserTableSqlServer();
82+
Context.Complete();
8283
scope.Complete();
8384
return;
8485
}
8586

8687
MigrateUserTableSqlite();
88+
Context.Complete();
8789
scope.Complete();
8890
}
8991

tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Migrations/AdvancedMigrationTests.cs

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,22 @@
22
// See LICENSE for more details.
33

44
using System.Linq;
5+
using Microsoft.Extensions.Logging;
56
using Moq;
67
using NUnit.Framework;
8+
using Umbraco.Cms.Core.Cache;
79
using Umbraco.Cms.Core.Configuration;
810
using Umbraco.Cms.Core.Events;
911
using Umbraco.Cms.Core.Migrations;
12+
using Umbraco.Cms.Core.PublishedCache;
13+
using Umbraco.Cms.Core.Scoping;
1014
using Umbraco.Cms.Core.Services;
1115
using Umbraco.Cms.Infrastructure.Migrations;
1216
using Umbraco.Cms.Infrastructure.Migrations.Install;
1317
using Umbraco.Cms.Infrastructure.Migrations.Upgrade;
18+
using Umbraco.Cms.Infrastructure.Persistence;
1419
using Umbraco.Cms.Infrastructure.Persistence.Dtos;
20+
using Umbraco.Cms.Infrastructure.Scoping;
1521
using Umbraco.Cms.Tests.Common.Testing;
1622
using Umbraco.Cms.Tests.Integration.Testing;
1723

@@ -23,7 +29,22 @@ public class AdvancedMigrationTests : UmbracoIntegrationTest
2329
{
2430
private IUmbracoVersion UmbracoVersion => GetRequiredService<IUmbracoVersion>();
2531
private IEventAggregator EventAggregator => GetRequiredService<IEventAggregator>();
26-
private IMigrationPlanExecutor MigrationPlanExecutor => GetRequiredService<IMigrationPlanExecutor>();
32+
private ICoreScopeProvider CoreScopeProvider => GetRequiredService<ICoreScopeProvider>();
33+
private IScopeAccessor ScopeAccessor => GetRequiredService<IScopeAccessor>();
34+
private ILoggerFactory LoggerFactory => GetRequiredService<ILoggerFactory>();
35+
private IMigrationBuilder MigrationBuilder => GetRequiredService<IMigrationBuilder>();
36+
private IUmbracoDatabaseFactory UmbracoDatabaseFactory => GetRequiredService<IUmbracoDatabaseFactory>();
37+
private IPublishedSnapshotService PublishedSnapshotService => GetRequiredService<IPublishedSnapshotService>();
38+
private DistributedCache DistributedCache => GetRequiredService<DistributedCache>();
39+
private IMigrationPlanExecutor MigrationPlanExecutor => new MigrationPlanExecutor(
40+
CoreScopeProvider,
41+
ScopeAccessor,
42+
LoggerFactory,
43+
MigrationBuilder,
44+
UmbracoDatabaseFactory,
45+
PublishedSnapshotService,
46+
DistributedCache,
47+
Mock.Of<IKeyValueService>());
2748

2849
[Test]
2950
public void CreateTableOfTDto()

tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Migrations/Expressions/Create/Expressions/CreateTableExpressionTests.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ public void Do_ForDefinitionWithPrimaryKey_PrimaryKeyConstraintIsAdded()
4040
.WithColumn("bar").AsInt32().PrimaryKey("PK_foo")
4141
.Do();
4242

43-
// (TableName, ColumnName, ConstraintName)
43+
// (TableName, ColumnName, ConstraintName)
4444
var constraint = database.SqlContext.SqlSyntax.GetConstraintsPerColumn(database).Single();
4545

4646
Assert.Multiple(() =>
@@ -92,7 +92,7 @@ public void Do_ForDefinitionWithForeignKeys_ForeignKeysAreCreated()
9292
.ForeignKey("MY_SUPER_COOL_FK", "foo", "bar")
9393
.Do();
9494

95-
// (TableName, ColumnName, ConstraintName)
95+
// (TableName, ColumnName, ConstraintName)
9696
var constraint = database.SqlContext.SqlSyntax
9797
.GetConstraintsPerColumn(database)
9898
.Single(x => x.Item3 == "MY_SUPER_COOL_FK");

0 commit comments

Comments
 (0)