From ecfafdc702fc31ce1cf4636ff887a72ee2f2436d Mon Sep 17 00:00:00 2001 From: Sebastian Date: Sun, 1 Jul 2018 18:48:14 +0200 Subject: [PATCH] Snapshot history. --- .../Apps/AppGrain.cs | 7 ++ .../Contents/ContentGrain.cs | 6 ++ .../Contents/IContentGrain.cs | 4 ++ .../Rules/RuleGrain.cs | 7 ++ .../Schemas/SchemaGrain.cs | 7 ++ .../Commands/DomainObjectGrain.cs | 60 ++++++++++++++--- .../Commands/DomainObjectGrainTests.cs | 67 +++++++++++++++++++ 7 files changed, 147 insertions(+), 11 deletions(-) diff --git a/src/Squidex.Domain.Apps.Entities/Apps/AppGrain.cs b/src/Squidex.Domain.Apps.Entities/Apps/AppGrain.cs index 975b1cd36..43bf53061 100644 --- a/src/Squidex.Domain.Apps.Entities/Apps/AppGrain.cs +++ b/src/Squidex.Domain.Apps.Entities/Apps/AppGrain.cs @@ -57,6 +57,13 @@ namespace Squidex.Domain.Apps.Entities.Apps this.initialPatterns = initialPatterns; } + public override Task OnActivateAsync() + { + CleanupOldSnapshots(); + + return base.OnActivateAsync(); + } + protected override Task ExecuteAsync(IAggregateCommand command) { VerifyNotArchived(); diff --git a/src/Squidex.Domain.Apps.Entities/Contents/ContentGrain.cs b/src/Squidex.Domain.Apps.Entities/Contents/ContentGrain.cs index 755e30a1d..161f433ae 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/ContentGrain.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/ContentGrain.cs @@ -20,6 +20,7 @@ using Squidex.Infrastructure; using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.Log; +using Squidex.Infrastructure.Orleans; using Squidex.Infrastructure.Reflection; using Squidex.Infrastructure.States; @@ -289,5 +290,10 @@ namespace Squidex.Domain.Apps.Entities.Contents return operationContext; } + + public Task> GetStateAsync(long version = -2) + { + return Task.FromResult(J.Of(GetSnapshot(version))); + } } } diff --git a/src/Squidex.Domain.Apps.Entities/Contents/IContentGrain.cs b/src/Squidex.Domain.Apps.Entities/Contents/IContentGrain.cs index 0b2d547c1..429a27746 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/IContentGrain.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/IContentGrain.cs @@ -5,11 +5,15 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using System.Threading.Tasks; +using Squidex.Infrastructure; using Squidex.Infrastructure.Commands; +using Squidex.Infrastructure.Orleans; namespace Squidex.Domain.Apps.Entities.Contents { public interface IContentGrain : IDomainObjectGrain { + Task> GetStateAsync(long version = EtagVersion.Any); } } diff --git a/src/Squidex.Domain.Apps.Entities/Rules/RuleGrain.cs b/src/Squidex.Domain.Apps.Entities/Rules/RuleGrain.cs index 81e475e4d..0ea48b9f2 100644 --- a/src/Squidex.Domain.Apps.Entities/Rules/RuleGrain.cs +++ b/src/Squidex.Domain.Apps.Entities/Rules/RuleGrain.cs @@ -34,6 +34,13 @@ namespace Squidex.Domain.Apps.Entities.Rules this.appProvider = appProvider; } + public override Task OnActivateAsync() + { + CleanupOldSnapshots(); + + return base.OnActivateAsync(); + } + protected override Task ExecuteAsync(IAggregateCommand command) { VerifyNotDeleted(); diff --git a/src/Squidex.Domain.Apps.Entities/Schemas/SchemaGrain.cs b/src/Squidex.Domain.Apps.Entities/Schemas/SchemaGrain.cs index e7eb3a28d..3356a4bbe 100644 --- a/src/Squidex.Domain.Apps.Entities/Schemas/SchemaGrain.cs +++ b/src/Squidex.Domain.Apps.Entities/Schemas/SchemaGrain.cs @@ -40,6 +40,13 @@ namespace Squidex.Domain.Apps.Entities.Schemas this.registry = registry; } + public override Task OnActivateAsync() + { + CleanupOldSnapshots(); + + return base.OnActivateAsync(); + } + protected override Task ExecuteAsync(IAggregateCommand command) { VerifyNotDeleted(); diff --git a/src/Squidex.Infrastructure/Commands/DomainObjectGrain.cs b/src/Squidex.Infrastructure/Commands/DomainObjectGrain.cs index 91beac110..fcea5d4cc 100644 --- a/src/Squidex.Infrastructure/Commands/DomainObjectGrain.cs +++ b/src/Squidex.Infrastructure/Commands/DomainObjectGrain.cs @@ -21,8 +21,9 @@ namespace Squidex.Infrastructure.Commands private readonly List> uncomittedEvents = new List>(); private readonly IStore store; private readonly ISemanticLog log; + private readonly List snapshots = new List { new T { Version = EtagVersion.Empty } }; + private bool cleanup; private Guid id; - private T snapshot = new T { Version = EtagVersion.Empty }; private IPersistence persistence; public Guid Id @@ -32,17 +33,17 @@ namespace Squidex.Infrastructure.Commands public long Version { - get { return snapshot.Version; } + get { return snapshots.Count - 2; } } public long NewVersion { - get { return snapshot.Version + uncomittedEvents.Count; } + get { return Version; } } public T Snapshot { - get { return snapshot; } + get { return snapshots[snapshots.Count - 1]; } } protected DomainObjectGrain(IStore store, ISemanticLog log) @@ -55,6 +56,31 @@ namespace Squidex.Infrastructure.Commands this.log = log; } + public void CleanupOldSnapshots() + { + cleanup = true; + } + + public T GetSnapshot(long version) + { + if (version == EtagVersion.Any) + { + return Snapshot; + } + + if (version == EtagVersion.Empty) + { + return snapshots[0]; + } + + if (version >= 0 && version < snapshots.Count - 1) + { + return snapshots[(int)version + 1]; + } + + return default(T); + } + public override async Task OnActivateAsync(Guid key) { using (log.MeasureInformation(w => w @@ -96,9 +122,11 @@ namespace Squidex.Infrastructure.Commands uncomittedEvents.Clear(); } - public virtual void ApplySnapshot(T newSnapshot) + public virtual void ApplySnapshot(T snapshot) { - snapshot = newSnapshot; + snapshot.Version = snapshots.Count - 1; + + snapshots.Add(snapshot); } public virtual void ApplyEvent(Envelope @event) @@ -172,7 +200,8 @@ namespace Squidex.Infrastructure.Commands throw new DomainException("Object has already been created."); } - var previousSnapshot = snapshot; + var size = snapshots.Count; + try { var result = await handler(command); @@ -181,10 +210,8 @@ namespace Squidex.Infrastructure.Commands if (events.Length > 0) { - snapshot.Version = NewVersion; - await persistence.WriteEventsAsync(events); - await persistence.WriteSnapshotAsync(snapshot); + await persistence.WriteSnapshotAsync(Snapshot); } if (result == null) @@ -203,12 +230,23 @@ namespace Squidex.Infrastructure.Commands } catch { - snapshot = previousSnapshot; + while (snapshots.Count > size) + { + snapshots.RemoveAt(snapshots.Count - 1); + } throw; } finally { + if (cleanup) + { + for (var i = 0; i < snapshots.Count - 1; i++) + { + snapshots[i] = default(T); + } + } + uncomittedEvents.Clear(); } } diff --git a/tests/Squidex.Infrastructure.Tests/Commands/DomainObjectGrainTests.cs b/tests/Squidex.Infrastructure.Tests/Commands/DomainObjectGrainTests.cs index 95d2976ab..5c296deec 100644 --- a/tests/Squidex.Infrastructure.Tests/Commands/DomainObjectGrainTests.cs +++ b/tests/Squidex.Infrastructure.Tests/Commands/DomainObjectGrainTests.cs @@ -10,6 +10,7 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using FakeItEasy; +using FluentAssertions; using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.Log; using Squidex.Infrastructure.Orleans; @@ -124,6 +125,72 @@ namespace Squidex.Infrastructure.Commands Assert.Equal(EtagVersion.Empty, sut.Version); } + [Fact] + public async Task Should_get_latestet_version_when_requesting_state_with_any() + { + await SetupEmptyAsync(); + + await sut.ExecuteAsync(C(new CreateAuto { Value = 5 })); + await sut.ExecuteAsync(C(new UpdateAuto { Value = 10 })); + + var result = sut.GetSnapshot(EtagVersion.Any); + + result.Should().BeEquivalentTo(new MyDomainState { Value = 10, Version = 1 }); + } + + [Fact] + public async Task Should_get_empty_version_when_requesting_state_with_empty_version() + { + await SetupEmptyAsync(); + + await sut.ExecuteAsync(C(new CreateAuto { Value = 5 })); + await sut.ExecuteAsync(C(new UpdateAuto { Value = 10 })); + + var result = sut.GetSnapshot(EtagVersion.Empty); + + result.Should().BeEquivalentTo(new MyDomainState { Value = 0, Version = -1 }); + } + + [Fact] + public async Task Should_get_specific_version_when_requesting_state_with_specific_version() + { + await SetupEmptyAsync(); + + await sut.ExecuteAsync(C(new CreateAuto { Value = 5 })); + await sut.ExecuteAsync(C(new UpdateAuto { Value = 10 })); + + sut.GetSnapshot(0).Should().BeEquivalentTo(new MyDomainState { Value = 5, Version = 0 }); + sut.GetSnapshot(1).Should().BeEquivalentTo(new MyDomainState { Value = 10, Version = 1 }); + } + + [Fact] + public async Task Should_automatically_cleanup_old_snapshots_when_enabled() + { + await SetupEmptyAsync(); + + sut.CleanupOldSnapshots(); + + await sut.ExecuteAsync(C(new CreateAuto { Value = 5 })); + await sut.ExecuteAsync(C(new UpdateAuto { Value = 10 })); + await sut.ExecuteAsync(C(new UpdateAuto { Value = 15 })); + + Assert.Null(sut.GetSnapshot(0)); + Assert.Null(sut.GetSnapshot(1)); + Assert.NotNull(sut.GetSnapshot(2)); + } + + [Fact] + public async Task Should_get_null_state_when_requesting_state_with_invalid_version() + { + await SetupEmptyAsync(); + + await sut.ExecuteAsync(C(new CreateAuto { Value = 5 })); + await sut.ExecuteAsync(C(new UpdateAuto { Value = 10 })); + + Assert.Null(sut.GetSnapshot(-3)); + Assert.Null(sut.GetSnapshot(2)); + } + [Fact] public async Task Should_write_state_and_events_when_created() {