mirror of https://github.com/Squidex/squidex.git
17 changed files with 365 additions and 8 deletions
@ -0,0 +1,29 @@ |
|||||
|
// ==========================================================================
|
||||
|
// MongoMigrationEntity.cs
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex Group
|
||||
|
// All rights reserved.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using MongoDB.Bson; |
||||
|
using MongoDB.Bson.Serialization.Attributes; |
||||
|
|
||||
|
namespace Squidex.Infrastructure.Migrations |
||||
|
{ |
||||
|
public sealed class MongoMigrationEntity |
||||
|
{ |
||||
|
[BsonId] |
||||
|
[BsonElement] |
||||
|
[BsonRepresentation(BsonType.String)] |
||||
|
public string Id { get; set; } |
||||
|
|
||||
|
[BsonElement] |
||||
|
[BsonRequired] |
||||
|
public bool IsLocked { get; set; } |
||||
|
|
||||
|
[BsonElement] |
||||
|
[BsonRequired] |
||||
|
public int Version { get; set; } |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,66 @@ |
|||||
|
// ==========================================================================
|
||||
|
// MongoMigrationStatus.cs
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex Group
|
||||
|
// All rights reserved.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System.Threading.Tasks; |
||||
|
using MongoDB.Driver; |
||||
|
using Squidex.Infrastructure.MongoDb; |
||||
|
|
||||
|
namespace Squidex.Infrastructure.Migrations |
||||
|
{ |
||||
|
public sealed class MongoMigrationStatus : MongoRepositoryBase<MongoMigrationEntity>, IMigrationStatus |
||||
|
{ |
||||
|
private const string DefaultId = "Default"; |
||||
|
|
||||
|
public MongoMigrationStatus(IMongoDatabase database) |
||||
|
: base(database) |
||||
|
{ |
||||
|
} |
||||
|
|
||||
|
public override void Connect() |
||||
|
{ |
||||
|
base.Connect(); |
||||
|
} |
||||
|
|
||||
|
public async Task<int> GetVersionAsync() |
||||
|
{ |
||||
|
var entity = await Collection.Find(x => x.Id == DefaultId).FirstOrDefaultAsync(); |
||||
|
|
||||
|
if (entity == null) |
||||
|
{ |
||||
|
try |
||||
|
{ |
||||
|
await Collection.InsertOneAsync(new MongoMigrationEntity { Id = DefaultId }); |
||||
|
} |
||||
|
catch (MongoWriteException ex) |
||||
|
{ |
||||
|
if (ex.WriteError.Category != ServerErrorCategory.DuplicateKey) |
||||
|
{ |
||||
|
throw; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return entity?.Version ?? 0; |
||||
|
} |
||||
|
|
||||
|
public async Task<bool> TryLockAsync() |
||||
|
{ |
||||
|
var entity = await Collection.FindOneAndUpdateAsync(x => x.Id == DefaultId, Update.Set(x => x.IsLocked, true)); |
||||
|
|
||||
|
return entity?.IsLocked == false; |
||||
|
} |
||||
|
|
||||
|
public Task UnlockAsync(int newVersion) |
||||
|
{ |
||||
|
return Collection.UpdateOneAsync(x => x.Id == DefaultId, |
||||
|
Update |
||||
|
.Set(x => x.IsLocked, false) |
||||
|
.Set(x => x.Version, newVersion)); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,21 @@ |
|||||
|
// ==========================================================================
|
||||
|
// IMigration.cs
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex Group
|
||||
|
// All rights reserved.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System.Threading.Tasks; |
||||
|
|
||||
|
namespace Squidex.Infrastructure.Migrations |
||||
|
{ |
||||
|
public interface IMigration |
||||
|
{ |
||||
|
int FromVersion { get; } |
||||
|
|
||||
|
int ToVersion { get; } |
||||
|
|
||||
|
Task UpdateAsync(); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,21 @@ |
|||||
|
// ==========================================================================
|
||||
|
// IMigrationState.cs
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex Group
|
||||
|
// All rights reserved.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System.Threading.Tasks; |
||||
|
|
||||
|
namespace Squidex.Infrastructure.Migrations |
||||
|
{ |
||||
|
public interface IMigrationStatus |
||||
|
{ |
||||
|
Task<int> GetVersionAsync(); |
||||
|
|
||||
|
Task<bool> TryLockAsync(); |
||||
|
|
||||
|
Task UnlockAsync(int newVersion); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,100 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Migrator.cs
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex Group
|
||||
|
// All rights reserved.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Linq; |
||||
|
using System.Threading.Tasks; |
||||
|
using Squidex.Infrastructure.Log; |
||||
|
|
||||
|
namespace Squidex.Infrastructure.Migrations |
||||
|
{ |
||||
|
public sealed class Migrator |
||||
|
{ |
||||
|
private readonly IMigrationStatus migrationStatus; |
||||
|
private readonly IEnumerable<IMigration> migrations; |
||||
|
private readonly ISemanticLog log; |
||||
|
|
||||
|
public Migrator(IMigrationStatus migrationStatus, IEnumerable<IMigration> migrations, ISemanticLog log) |
||||
|
{ |
||||
|
Guard.NotNull(migrationStatus, nameof(migrationStatus)); |
||||
|
Guard.NotNull(migrations, nameof(migrations)); |
||||
|
Guard.NotNull(log, nameof(log)); |
||||
|
|
||||
|
this.migrationStatus = migrationStatus; |
||||
|
this.migrations = migrations.OrderByDescending(x => x.ToVersion).ToList(); |
||||
|
|
||||
|
this.log = log; |
||||
|
} |
||||
|
|
||||
|
public async Task MigrateAsync() |
||||
|
{ |
||||
|
var version = await migrationStatus.GetVersionAsync(); |
||||
|
|
||||
|
var lastMigrator = migrations.FirstOrDefault(); |
||||
|
|
||||
|
if (lastMigrator != null && lastMigrator.ToVersion != version) |
||||
|
{ |
||||
|
while (!await migrationStatus.TryLockAsync()) |
||||
|
{ |
||||
|
log.LogInformation(w => w |
||||
|
.WriteProperty("action", "Migrate") |
||||
|
.WriteProperty("mesage", "Waiting 5sec to acquire lock.")); |
||||
|
|
||||
|
await Task.Delay(5000); |
||||
|
} |
||||
|
|
||||
|
try |
||||
|
{ |
||||
|
var migrationPath = FindMigratorPath(version, lastMigrator.ToVersion).ToList(); |
||||
|
|
||||
|
foreach (var migrator in migrationPath) |
||||
|
{ |
||||
|
using (log.MeasureInformation(w => w |
||||
|
.WriteProperty("action", "Migration") |
||||
|
.WriteProperty("migrator", migrator.GetType().ToString()))) |
||||
|
{ |
||||
|
await migrator.UpdateAsync(); |
||||
|
|
||||
|
version = migrator.ToVersion; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
finally |
||||
|
{ |
||||
|
await migrationStatus.UnlockAsync(version); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private IEnumerable<IMigration> FindMigratorPath(int fromVersion, int toVersion) |
||||
|
{ |
||||
|
var addedMigrators = new HashSet<IMigration>(); |
||||
|
|
||||
|
while (true) |
||||
|
{ |
||||
|
var bestMigrator = migrations.Where(x => x.FromVersion < x.ToVersion).FirstOrDefault(x => x.FromVersion == fromVersion); |
||||
|
|
||||
|
if (bestMigrator != null && addedMigrators.Add(bestMigrator)) |
||||
|
{ |
||||
|
fromVersion = bestMigrator.ToVersion; |
||||
|
|
||||
|
yield return bestMigrator; |
||||
|
} |
||||
|
else if (fromVersion != toVersion) |
||||
|
{ |
||||
|
throw new InvalidOperationException($"There is no migration path from {fromVersion} to {toVersion}."); |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,111 @@ |
|||||
|
// ==========================================================================
|
||||
|
// MigratorTests.cs
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex Group
|
||||
|
// All rights reserved.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using System.Threading.Tasks; |
||||
|
using FakeItEasy; |
||||
|
using Squidex.Infrastructure.Log; |
||||
|
using Xunit; |
||||
|
|
||||
|
namespace Squidex.Infrastructure.Migrations |
||||
|
{ |
||||
|
public sealed class MigratorTests |
||||
|
{ |
||||
|
private readonly IMigrationStatus status = A.Fake<IMigrationStatus>(); |
||||
|
|
||||
|
public MigratorTests() |
||||
|
{ |
||||
|
A.CallTo(() => status.GetVersionAsync()).Returns(0); |
||||
|
A.CallTo(() => status.TryLockAsync()).Returns(true); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_migrate_step_by_step() |
||||
|
{ |
||||
|
var migrator_0_1 = BuildMigration(0, 1); |
||||
|
var migrator_1_2 = BuildMigration(1, 2); |
||||
|
var migrator_2_3 = BuildMigration(2, 3); |
||||
|
|
||||
|
var migrator = new Migrator(status, new[] { migrator_0_1, migrator_1_2, migrator_2_3 }, A.Fake<ISemanticLog>()); |
||||
|
|
||||
|
await migrator.MigrateAsync(); |
||||
|
|
||||
|
A.CallTo(() => migrator_0_1.UpdateAsync()).MustHaveHappened(); |
||||
|
A.CallTo(() => migrator_1_2.UpdateAsync()).MustHaveHappened(); |
||||
|
A.CallTo(() => migrator_2_3.UpdateAsync()).MustHaveHappened(); |
||||
|
|
||||
|
A.CallTo(() => status.UnlockAsync(3)).MustHaveHappened(); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_unlock_when_failed() |
||||
|
{ |
||||
|
var migrator_0_1 = BuildMigration(0, 1); |
||||
|
var migrator_1_2 = BuildMigration(1, 2); |
||||
|
var migrator_2_3 = BuildMigration(2, 3); |
||||
|
|
||||
|
var migrator = new Migrator(status, new[] { migrator_0_1, migrator_1_2, migrator_2_3 }, A.Fake<ISemanticLog>()); |
||||
|
|
||||
|
A.CallTo(() => migrator_1_2.UpdateAsync()).Throws(new ArgumentException()); |
||||
|
|
||||
|
await Assert.ThrowsAsync<ArgumentException>(migrator.MigrateAsync); |
||||
|
|
||||
|
A.CallTo(() => migrator_0_1.UpdateAsync()).MustHaveHappened(); |
||||
|
A.CallTo(() => migrator_1_2.UpdateAsync()).MustHaveHappened(); |
||||
|
A.CallTo(() => migrator_2_3.UpdateAsync()).MustNotHaveHappened(); |
||||
|
|
||||
|
A.CallTo(() => status.UnlockAsync(1)).MustHaveHappened(); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_migrate_with_fastest_path() |
||||
|
{ |
||||
|
var migrator_0_1 = BuildMigration(0, 1); |
||||
|
var migrator_0_2 = BuildMigration(0, 2); |
||||
|
var migrator_1_2 = BuildMigration(1, 2); |
||||
|
var migrator_2_3 = BuildMigration(2, 3); |
||||
|
|
||||
|
var migrator = new Migrator(status, new[] { migrator_0_1, migrator_0_2, migrator_1_2, migrator_2_3 }, A.Fake<ISemanticLog>()); |
||||
|
|
||||
|
await migrator.MigrateAsync(); |
||||
|
|
||||
|
A.CallTo(() => migrator_0_2.UpdateAsync()).MustHaveHappened(); |
||||
|
A.CallTo(() => migrator_0_1.UpdateAsync()).MustNotHaveHappened(); |
||||
|
A.CallTo(() => migrator_1_2.UpdateAsync()).MustNotHaveHappened(); |
||||
|
A.CallTo(() => migrator_2_3.UpdateAsync()).MustHaveHappened(); |
||||
|
|
||||
|
A.CallTo(() => status.UnlockAsync(3)).MustHaveHappened(); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_throw_if_no_path_found() |
||||
|
{ |
||||
|
var migrator_0_1 = BuildMigration(0, 1); |
||||
|
var migrator_2_3 = BuildMigration(2, 3); |
||||
|
|
||||
|
var migrator = new Migrator(status, new[] { migrator_0_1, migrator_2_3 }, A.Fake<ISemanticLog>()); |
||||
|
|
||||
|
await Assert.ThrowsAsync<InvalidOperationException>(migrator.MigrateAsync); |
||||
|
|
||||
|
A.CallTo(() => migrator_0_1.UpdateAsync()).MustNotHaveHappened(); |
||||
|
A.CallTo(() => migrator_2_3.UpdateAsync()).MustNotHaveHappened(); |
||||
|
|
||||
|
A.CallTo(() => status.UnlockAsync(0)).MustHaveHappened(); |
||||
|
} |
||||
|
|
||||
|
private IMigration BuildMigration(int fromVersion, int toVersion) |
||||
|
{ |
||||
|
var migration = A.Fake<IMigration>(); |
||||
|
|
||||
|
A.CallTo(() => migration.FromVersion).Returns(fromVersion); |
||||
|
A.CallTo(() => migration.ToVersion).Returns(toVersion); |
||||
|
|
||||
|
return migration; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue