From b7b7a804fb1b1534cb5f36c7b1dc7875ac8733f6 Mon Sep 17 00:00:00 2001 From: Maxim Shoshin Date: Tue, 9 Jun 2020 13:30:07 +0300 Subject: [PATCH 1/9] [WIP] Crash resilience ProfOfConcept implementation on MS SQL --- .../IntegrationTests/CrashResilienceTests.cs | 218 ++++++++++++++++++ Source/EventFlow.MsSql/EventFlow.MsSql.csproj | 3 + ...ptionsMsSqlReliablePublishingExtensions.cs | 38 +++ .../EventFlowPublishLogMsSql.cs | 45 ++++ .../ReliablePublish/IRecoveryDetector.cs | 32 +++ .../MsSqlPublishVerificator.cs | 217 +++++++++++++++++ .../MsSqlReliableMarkProcessor.cs | 77 +++++++ .../ReliablePublish/PublishLogItem.cs | 33 +++ .../Scripts/0001 - Create PublishLog.sql | 13 ++ .../0002 - Create PublishVerifyState.sql | 11 + ...003 - Setup initial PublishVerifyState.sql | 4 + .../TimeBasedRecoveryDetector.cs | 38 +++ ...FlowOptionsReliablePublishingExtensions.cs | 44 ++++ .../Subscribers/IPublishRecoveryProcessor.cs | 35 +++ .../Subscribers/IPublishVerificator.cs | 33 +++ .../Subscribers/IReliableMarkProcessor.cs | 34 +++ .../Subscribers/PublishVerificationResult.cs | 32 +++ .../ReliableDomainEventPublisher.cs | 60 +++++ .../RetryPublishRecoveryProcessor.cs | 53 +++++ 19 files changed, 1020 insertions(+) create mode 100644 Source/EventFlow.MsSql.Tests/IntegrationTests/CrashResilienceTests.cs create mode 100644 Source/EventFlow.MsSql/Extensions/EventFlowOptionsMsSqlReliablePublishingExtensions.cs create mode 100644 Source/EventFlow.MsSql/ReliablePublish/EventFlowPublishLogMsSql.cs create mode 100644 Source/EventFlow.MsSql/ReliablePublish/IRecoveryDetector.cs create mode 100644 Source/EventFlow.MsSql/ReliablePublish/MsSqlPublishVerificator.cs create mode 100644 Source/EventFlow.MsSql/ReliablePublish/MsSqlReliableMarkProcessor.cs create mode 100644 Source/EventFlow.MsSql/ReliablePublish/PublishLogItem.cs create mode 100644 Source/EventFlow.MsSql/ReliablePublish/Scripts/0001 - Create PublishLog.sql create mode 100644 Source/EventFlow.MsSql/ReliablePublish/Scripts/0002 - Create PublishVerifyState.sql create mode 100644 Source/EventFlow.MsSql/ReliablePublish/Scripts/0003 - Setup initial PublishVerifyState.sql create mode 100644 Source/EventFlow.MsSql/ReliablePublish/TimeBasedRecoveryDetector.cs create mode 100644 Source/EventFlow/Extensions/EventFlowOptionsReliablePublishingExtensions.cs create mode 100644 Source/EventFlow/Subscribers/IPublishRecoveryProcessor.cs create mode 100644 Source/EventFlow/Subscribers/IPublishVerificator.cs create mode 100644 Source/EventFlow/Subscribers/IReliableMarkProcessor.cs create mode 100644 Source/EventFlow/Subscribers/PublishVerificationResult.cs create mode 100644 Source/EventFlow/Subscribers/ReliableDomainEventPublisher.cs create mode 100644 Source/EventFlow/Subscribers/RetryPublishRecoveryProcessor.cs diff --git a/Source/EventFlow.MsSql.Tests/IntegrationTests/CrashResilienceTests.cs b/Source/EventFlow.MsSql.Tests/IntegrationTests/CrashResilienceTests.cs new file mode 100644 index 000000000..c3205a587 --- /dev/null +++ b/Source/EventFlow.MsSql.Tests/IntegrationTests/CrashResilienceTests.cs @@ -0,0 +1,218 @@ +// The MIT License (MIT) +// +// Copyright (c) 2015-2018 Rasmus Mikkelsen +// Copyright (c) 2015-2018 eBay Software Foundation +// https://github.com/eventflow/EventFlow +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using EventFlow.Aggregates; +using EventFlow.Configuration; +using EventFlow.Core; +using EventFlow.Extensions; +using EventFlow.MsSql.Extensions; +using EventFlow.MsSql.ReliablePublish; +using EventFlow.Subscribers; +using EventFlow.TestHelpers; +using EventFlow.TestHelpers.Aggregates; +using EventFlow.TestHelpers.Aggregates.Events; +using EventFlow.TestHelpers.MsSql; +using FluentAssertions; +using NUnit.Framework; + +namespace EventFlow.MsSql.Tests.IntegrationTests +{ + [Category(Categories.Integration)] + public sealed class CrashResilienceTests : IntegrationTest + { + private IMsSqlDatabase _testDatabase; + private TestPublisher _publisher; + private MsSqlPublishVerificator _publishVerificator; + + protected override IRootResolver CreateRootResolver(IEventFlowOptions eventFlowOptions) + { + _testDatabase = MsSqlHelpz.CreateDatabase("eventflow"); + + _publisher = null; + + var resolver = eventFlowOptions + .ConfigureMsSql(MsSqlConfiguration.New.SetConnectionString(_testDatabase.ConnectionString.Value)) + .UseMssqlReliablePublishing() + .RegisterServices(sr => sr.Decorate( + (r, dea) => + _publisher ?? (_publisher = new TestPublisher(dea)))) + .RegisterServices(sr => sr.Register()) + .CreateResolver(); + + var databaseMigrator = resolver.Resolve(); + EventFlowPublishLogMsSql.MigrateDatabase(databaseMigrator); + databaseMigrator.MigrateDatabaseUsingEmbeddedScripts(GetType().Assembly); + + _publisher = (TestPublisher)resolver.Resolve(); + _publishVerificator = (MsSqlPublishVerificator)resolver.Resolve(); + + return resolver; + } + + [TearDown] + public void TearDown() + { + _testDatabase.DisposeSafe("Failed to delete database"); + } + + [Test] + public async Task ShouldRecoverAfterFailure() + { + // Arrange + var id = ThingyId.New; + _publisher.SimulatePublishFailure = true; + await PublishPingCommandsAsync(id, 1).ConfigureAwait(false); + + // Act + _publisher.SimulatePublishFailure = false; + await _publishVerificator.VerifyOnceAsync(CancellationToken.None).ConfigureAwait(false); + + // Assert + _publisher.PublishedEvents.Should().NotBeEmpty(); + } + + [Test] + public async Task ShouldRetryVerification() + { + // Arrange + var id = ThingyId.New; + _publisher.SimulatePublishFailure = true; + await PublishPingCommandsAsync(id, 1).ConfigureAwait(false); + + // Act + var result = await _publishVerificator.VerifyOnceAsync(CancellationToken.None).ConfigureAwait(false); + + // Assert + result.Should().Be(PublishVerificationResult.RecoveredNeedVerify); + _publisher.PublishedEvents.Should().BeEmpty(); + } + + [Test] + public async Task ShouldRecoveredOnceForSimultaniousVerification() + { + // Arrange + var id = ThingyId.New; + _publisher.SimulatePublishFailure = true; + await PublishPingCommandsAsync(id, 1).ConfigureAwait(false); + + // Act + var tasks = Enumerable.Range(0, 10).Select(x => Verify()); + + _publisher.SimulatePublishFailure = false; + + await Task.WhenAll(tasks).ConfigureAwait(false); + + // Assert + _publisher.PublishedEvents.Should().ContainSingle(x => x.GetAggregateEvent() is ThingyPingEvent); + } + + [Test] + public async Task ShouldNotRecoverAfterFailureWithoutVerificator() + { + // Arrange + var id = ThingyId.New; + _publisher.SimulatePublishFailure = true; + + // Act + await PublishPingCommandsAsync(id, 1).ConfigureAwait(false); + + // Assert + _publisher.PublishedEvents.Should().BeEmpty(); + } + + private async Task Verify() + { + PublishVerificationResult result; + do + { + result = await _publishVerificator.VerifyOnceAsync(CancellationToken.None).ConfigureAwait(false); + } while (result != PublishVerificationResult.CompletedNoMoreDataToVerify); + } + + private class TestPublisher : IDomainEventPublisher + { + private readonly IDomainEventPublisher _inner; + private readonly List _publishedEvents = new List(); + private readonly List _notPublishedEvents = new List(); + + public TestPublisher(IDomainEventPublisher inner) + { + _inner = inner; + } + + public bool SimulatePublishFailure { get; set; } + + public IReadOnlyList PublishedEvents => _publishedEvents; + + public async Task PublishAsync(IReadOnlyCollection domainEvents, CancellationToken cancellationToken) + { + if (SimulatePublishFailure) + { + _notPublishedEvents.AddRange(domainEvents); + return; + } + + await _inner.PublishAsync(domainEvents, cancellationToken); + + _notPublishedEvents.RemoveAll(evnt => domainEvents.Contains(evnt, CompareEventById.Instance)); + _publishedEvents.AddRange(domainEvents); + } + + [Obsolete("Use PublishAsync (without generics and aggregate identity)")] + public Task PublishAsync(TIdentity id, IReadOnlyCollection domainEvents, + CancellationToken cancellationToken) where TAggregate : IAggregateRoot where TIdentity : IIdentity + { + return _inner.PublishAsync(id, domainEvents, cancellationToken); + } + + private sealed class CompareEventById : IEqualityComparer + { + public static CompareEventById Instance { get; } = new CompareEventById(); + + public bool Equals(IDomainEvent x, IDomainEvent y) + { + return Equals(x.GetIdentity(), y.GetIdentity()) && + x.AggregateSequenceNumber == y.AggregateSequenceNumber; + } + + public int GetHashCode(IDomainEvent obj) + { + return HashHelper.Combine(obj.AggregateSequenceNumber, obj.GetIdentity().GetHashCode()); + } + } + } + + private sealed class AlwaysRecoverDetector : IRecoveryDetector + { + public bool IsNeedRecovery(IDomainEvent evnt) + { + return true; + } + } + } +} \ No newline at end of file diff --git a/Source/EventFlow.MsSql/EventFlow.MsSql.csproj b/Source/EventFlow.MsSql/EventFlow.MsSql.csproj index fa963bb6a..530c75355 100644 --- a/Source/EventFlow.MsSql/EventFlow.MsSql.csproj +++ b/Source/EventFlow.MsSql/EventFlow.MsSql.csproj @@ -33,6 +33,9 @@ + + + \ No newline at end of file diff --git a/Source/EventFlow.MsSql/Extensions/EventFlowOptionsMsSqlReliablePublishingExtensions.cs b/Source/EventFlow.MsSql/Extensions/EventFlowOptionsMsSqlReliablePublishingExtensions.cs new file mode 100644 index 000000000..64e813ad3 --- /dev/null +++ b/Source/EventFlow.MsSql/Extensions/EventFlowOptionsMsSqlReliablePublishingExtensions.cs @@ -0,0 +1,38 @@ +// The MIT License (MIT) +// +// Copyright (c) 2015-2018 Rasmus Mikkelsen +// Copyright (c) 2015-2018 eBay Software Foundation +// https://github.com/eventflow/EventFlow +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +using EventFlow.Extensions; +using EventFlow.MsSql.ReliablePublish; + +namespace EventFlow.MsSql.Extensions +{ + public static class EventFlowOptionsMsSqlReliablePublishingExtensions + { + public static IEventFlowOptions UseMssqlReliablePublishing(this IEventFlowOptions eventFlowOptions) + { + return eventFlowOptions + .UseReliablePublishing() + .RegisterServices(r => r.Register()); + } + } +} \ No newline at end of file diff --git a/Source/EventFlow.MsSql/ReliablePublish/EventFlowPublishLogMsSql.cs b/Source/EventFlow.MsSql/ReliablePublish/EventFlowPublishLogMsSql.cs new file mode 100644 index 000000000..46dc99870 --- /dev/null +++ b/Source/EventFlow.MsSql/ReliablePublish/EventFlowPublishLogMsSql.cs @@ -0,0 +1,45 @@ +// The MIT License (MIT) +// +// Copyright (c) 2015-2018 Rasmus Mikkelsen +// Copyright (c) 2015-2018 eBay Software Foundation +// https://github.com/eventflow/EventFlow +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +using System.Collections.Generic; +using System.Reflection; +using EventFlow.Sql.Extensions; +using EventFlow.Sql.Migrations; + +namespace EventFlow.MsSql.ReliablePublish +{ + public static class EventFlowPublishLogMsSql + { + public static Assembly Assembly { get; } = typeof(EventFlowPublishLogMsSql).GetTypeInfo().Assembly; + + public static IEnumerable GetSqlScripts() + { + return Assembly.GetEmbeddedSqlScripts("EventFlow.MsSql.ReliablePublish.Scripts"); + } + + public static void MigrateDatabase(IMsSqlDatabaseMigrator msSqlDatabaseMigrator) + { + msSqlDatabaseMigrator.MigrateDatabaseUsingScripts(GetSqlScripts()); + } + } +} \ No newline at end of file diff --git a/Source/EventFlow.MsSql/ReliablePublish/IRecoveryDetector.cs b/Source/EventFlow.MsSql/ReliablePublish/IRecoveryDetector.cs new file mode 100644 index 000000000..bf57b4b15 --- /dev/null +++ b/Source/EventFlow.MsSql/ReliablePublish/IRecoveryDetector.cs @@ -0,0 +1,32 @@ +// The MIT License (MIT) +// +// Copyright (c) 2015-2018 Rasmus Mikkelsen +// Copyright (c) 2015-2018 eBay Software Foundation +// https://github.com/eventflow/EventFlow +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +using EventFlow.Aggregates; + +namespace EventFlow.MsSql.ReliablePublish +{ + public interface IRecoveryDetector + { + bool IsNeedRecovery(IDomainEvent evnt); + } +} \ No newline at end of file diff --git a/Source/EventFlow.MsSql/ReliablePublish/MsSqlPublishVerificator.cs b/Source/EventFlow.MsSql/ReliablePublish/MsSqlPublishVerificator.cs new file mode 100644 index 000000000..3d8a96484 --- /dev/null +++ b/Source/EventFlow.MsSql/ReliablePublish/MsSqlPublishVerificator.cs @@ -0,0 +1,217 @@ +// The MIT License (MIT) +// +// Copyright (c) 2015-2018 Rasmus Mikkelsen +// Copyright (c) 2015-2018 eBay Software Foundation +// https://github.com/eventflow/EventFlow +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +using System; +using System.Collections.Generic; +using System.Data; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Dapper; +using EventFlow.Aggregates; +using EventFlow.Core; +using EventFlow.EventStores; +using EventFlow.Subscribers; + +namespace EventFlow.MsSql.ReliablePublish +{ + public sealed class MsSqlPublishVerificator : IPublishVerificator + { + private const int PageSize = 200; + + private readonly IEventPersistence _eventPersistence; + private readonly IPublishRecoveryProcessor _publishRecoveryProcessor; + private readonly IMsSqlConnection _msSqlConnection; + private readonly IEventJsonSerializer _eventSerializer; + private readonly IRecoveryDetector _recoveryDetector; + + public MsSqlPublishVerificator(IEventPersistence eventPersistence, IMsSqlConnection msSqlConnection, IPublishRecoveryProcessor publishRecoveryProcessor, IEventJsonSerializer eventSerializer, IRecoveryDetector recoveryDetector) + { + _eventPersistence = eventPersistence; + _msSqlConnection = msSqlConnection; + _publishRecoveryProcessor = publishRecoveryProcessor; + _eventSerializer = eventSerializer; + _recoveryDetector = recoveryDetector; + } + + public TimeSpan ReliableThreshold { get; set; } = TimeSpan.FromMinutes(5); + + public Task VerifyOnceAsync(CancellationToken cancellationToken) + { + return _msSqlConnection.WithConnectionAsync( + Label.Named("publishlog-verify"), + VerifyAsync, + cancellationToken); + } + + private async Task VerifyAsync(IDbConnection db, CancellationToken cancellationToken) + { + var result = PublishVerificationResult.CompletedNoMoreDataToVerify; + + VerifyResult verifyResult; + AllCommittedEventsPage page; + using (var transaction = db.BeginTransaction()) + { + var state = await GetPreviousVerifyStateAndLockItAsync(db, transaction).ConfigureAwait(false); + var position = new GlobalPosition(state.LastVerifiedPosition); + + var logItemLookup = await GetLogItemsAsync(db, transaction); + + page = await _eventPersistence.LoadAllCommittedEvents(position, PageSize, cancellationToken) + .ConfigureAwait(false); + state.LastVerifiedPosition = page.NextGlobalPosition.Value; + + verifyResult = VerifyDomainEvents(page, logItemLookup); + + // Some of not published events can be in flight, so no need recovery them + // but we have to check them again on next iteration + var eventsForRecovery = GetEventsForRecovery(verifyResult.UnpublishedEvents); + + if (eventsForRecovery.Count > 0) + { + // Do it inside transaction to recover in single thread + // success recovery should put LogItem + await _publishRecoveryProcessor.RecoverEventsAsync(eventsForRecovery, cancellationToken) + .ConfigureAwait(false); + + result = PublishVerificationResult.RecoveredNeedVerify; + } + + // Remove logs and move position forward only when it is successfully recovered. + if (verifyResult.UnpublishedEvents.Count == 0) + { + await RemoveLogItemsAsync(verifyResult.PublishedLogItems, db, transaction) + .ConfigureAwait(false); + + await UpdateLastVerifyStateAsync(db, state, transaction) + .ConfigureAwait(false); + + result = page.CommittedDomainEvents.Count < PageSize + ? PublishVerificationResult.CompletedNoMoreDataToVerify + : PublishVerificationResult.HasMoreDataNeedVerify; + } + + transaction.Commit(); + } + + return result; + } + + private IReadOnlyList GetEventsForRecovery(IReadOnlyList unpublishedEvents) + { + return unpublishedEvents + .Select(evnt => _eventSerializer.Deserialize(evnt)) + .Where(evnt => _recoveryDetector.IsNeedRecovery(evnt)) + .ToList(); + } + + private async Task RemoveLogItemsAsync(IReadOnlyList publishedLogItems, IDbConnection db, IDbTransaction transaction) + { + foreach (var logItem in publishedLogItems) + { + await db.ExecuteAsync("DELETE FROM [dbo].[EventFlowPublishLog] WHERE Id = @Id", logItem, transaction); + } + } + + private VerifyResult VerifyDomainEvents(AllCommittedEventsPage page, ILookup logItemLookup) + { + var unpublishedEvents = new List(); + var publishedLogItems = new List(); + + foreach (var committedDomainEvent in page.CommittedDomainEvents) + { + var logItem = TryGetPublishedLogItem(committedDomainEvent, logItemLookup); + + if (logItem == null) + { + unpublishedEvents.Add(committedDomainEvent); + } + // Remove logItem only on the last event related with this log item + else if (committedDomainEvent.AggregateSequenceNumber == logItem.MaxAggregateSequenceNumber) + { + publishedLogItems.Add(logItem); + } + } + + return new VerifyResult(unpublishedEvents, publishedLogItems); + } + + private static async Task> GetLogItemsAsync(IDbConnection db, IDbTransaction transaction) + { + var logItems = await db.QueryAsync( + "SELECT [Id], [AggregateId],[MinAggregateSequenceNumber], [MaxAggregateSequenceNumber] FROM [dbo].[EventFlowPublishLog]", + transaction: transaction) + .ConfigureAwait(false); + + return logItems.ToLookup(x => x.AggregateId); + } + + private static Task UpdateLastVerifyStateAsync(IDbConnection db, VerifyState state, + IDbTransaction transaction) + { + return db.ExecuteAsync( + "UPDATE [dbo].[EventFlowPublishVerifyState] SET LastVerifiedPosition = @LastVerifiedPosition WHERE Id = @Id", + state, + transaction); + } + + private static async Task GetPreviousVerifyStateAndLockItAsync(IDbConnection db, IDbTransaction transaction) + { + var state = await db.QueryFirstOrDefaultAsync( + "SELECT [Id], [LastVerifiedPosition] FROM [dbo].[EventFlowPublishVerifyState] WITH (XLOCK)", transaction: transaction) + .ConfigureAwait(false); + + return state; + } + + private PublishLogItem TryGetPublishedLogItem(ICommittedDomainEvent committedDomainEvent, ILookup logItemLookup) + { + var logItems = logItemLookup[committedDomainEvent.AggregateId]; + + return logItems.FirstOrDefault( + logItem => logItem.MinAggregateSequenceNumber <= committedDomainEvent.AggregateSequenceNumber && + committedDomainEvent.AggregateSequenceNumber <= logItem.MaxAggregateSequenceNumber); + } + + public sealed class VerifyState + { + public long Id { get; set; } + public string LastVerifiedPosition { get; set; } + } + + private sealed class VerifyResult + { + public VerifyResult( + IReadOnlyList unpublishedEvents, + IReadOnlyList publishedLogItems) + { + UnpublishedEvents = unpublishedEvents; + PublishedLogItems = publishedLogItems; + } + + public IReadOnlyList UnpublishedEvents { get; } + + public IReadOnlyList PublishedLogItems { get; } + } + } +} \ No newline at end of file diff --git a/Source/EventFlow.MsSql/ReliablePublish/MsSqlReliableMarkProcessor.cs b/Source/EventFlow.MsSql/ReliablePublish/MsSqlReliableMarkProcessor.cs new file mode 100644 index 000000000..c36e93dec --- /dev/null +++ b/Source/EventFlow.MsSql/ReliablePublish/MsSqlReliableMarkProcessor.cs @@ -0,0 +1,77 @@ +// The MIT License (MIT) +// +// Copyright (c) 2015-2018 Rasmus Mikkelsen +// Copyright (c) 2015-2018 eBay Software Foundation +// https://github.com/eventflow/EventFlow +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using EventFlow.Aggregates; +using EventFlow.Core; +using EventFlow.Subscribers; + +namespace EventFlow.MsSql.ReliablePublish +{ + public sealed class MsSqlReliableMarkProcessor : IReliableMarkProcessor + { + private readonly IMsSqlConnection _msSqlConnection; + + public MsSqlReliableMarkProcessor(IMsSqlConnection msSqlConnection) + { + _msSqlConnection = msSqlConnection; + } + + public async Task MarkPublishedWithSuccess(IReadOnlyCollection domainEvents) + { + var count = domainEvents.Count; + if (count == 0) + { + return; + } + + var aggregateIdentity = domainEvents.First().GetIdentity(); + + if (domainEvents.Any(x => !Equals(x.GetIdentity(), aggregateIdentity))) + { + throw new NotSupportedException("Mark events as published successfully for several aggregates is not supported"); + } + + var item = new PublishLogItem + { + AggregateId = aggregateIdentity.Value, + MinAggregateSequenceNumber = domainEvents.Min(x => x.AggregateSequenceNumber), + MaxAggregateSequenceNumber = domainEvents.Max(x => x.AggregateSequenceNumber), + }; + + await _msSqlConnection.ExecuteAsync( + Label.Named("publishlog-commit"), + CancellationToken.None, // Unable to Cancel + @"INSERT INTO [dbo].[EventFlowPublishLog] + (AggregateId, MinAggregateSequenceNumber, MaxAggregateSequenceNumber) + VALUES + (@AggregateId, @MinAggregateSequenceNumber, @MaxAggregateSequenceNumber)", + item) + .ConfigureAwait(false); + } + } +} \ No newline at end of file diff --git a/Source/EventFlow.MsSql/ReliablePublish/PublishLogItem.cs b/Source/EventFlow.MsSql/ReliablePublish/PublishLogItem.cs new file mode 100644 index 000000000..745765b2c --- /dev/null +++ b/Source/EventFlow.MsSql/ReliablePublish/PublishLogItem.cs @@ -0,0 +1,33 @@ +// The MIT License (MIT) +// +// Copyright (c) 2015-2018 Rasmus Mikkelsen +// Copyright (c) 2015-2018 eBay Software Foundation +// https://github.com/eventflow/EventFlow +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +namespace EventFlow.MsSql.ReliablePublish +{ + public sealed class PublishLogItem + { + public long Id { get; set; } + public string AggregateId { get; set; } + public int MinAggregateSequenceNumber { get; set; } + public int MaxAggregateSequenceNumber { get; set; } + } +} \ No newline at end of file diff --git a/Source/EventFlow.MsSql/ReliablePublish/Scripts/0001 - Create PublishLog.sql b/Source/EventFlow.MsSql/ReliablePublish/Scripts/0001 - Create PublishLog.sql new file mode 100644 index 000000000..9b7c12788 --- /dev/null +++ b/Source/EventFlow.MsSql/ReliablePublish/Scripts/0001 - Create PublishLog.sql @@ -0,0 +1,13 @@ +IF NOT EXISTS (SELECT * FROM INFORMATION_SCHEMA.TABLES WHERE table_schema = N'dbo' AND table_name = N'EventFlowPublishLog') +BEGIN + CREATE TABLE [dbo].[EventFlowPublishLog]( + [Id] [bigint] IDENTITY(1,1) NOT NULL, + [AggregateId] [nvarchar](128) NOT NULL, + [MinAggregateSequenceNumber] [int] NOT NULL, + [MaxAggregateSequenceNumber] [int] NOT NULL, + CONSTRAINT [PK_EventFlowPublishLog] PRIMARY KEY CLUSTERED + ( + [Id] ASC + ) + ) +END diff --git a/Source/EventFlow.MsSql/ReliablePublish/Scripts/0002 - Create PublishVerifyState.sql b/Source/EventFlow.MsSql/ReliablePublish/Scripts/0002 - Create PublishVerifyState.sql new file mode 100644 index 000000000..c9a0e5eb3 --- /dev/null +++ b/Source/EventFlow.MsSql/ReliablePublish/Scripts/0002 - Create PublishVerifyState.sql @@ -0,0 +1,11 @@ +IF NOT EXISTS (SELECT * FROM INFORMATION_SCHEMA.TABLES WHERE table_schema = N'dbo' AND table_name = N'EventFlowPublishVerifyState') +BEGIN + CREATE TABLE [dbo].[EventFlowPublishVerifyState]( + [Id] [bigint] IDENTITY(1,1) NOT NULL, + [LastVerifiedPosition] [nvarchar](255) NOT NULL, + CONSTRAINT [PK_EventFlowPublishVerifyState] PRIMARY KEY CLUSTERED + ( + [Id] ASC + ) + ) +END diff --git a/Source/EventFlow.MsSql/ReliablePublish/Scripts/0003 - Setup initial PublishVerifyState.sql b/Source/EventFlow.MsSql/ReliablePublish/Scripts/0003 - Setup initial PublishVerifyState.sql new file mode 100644 index 000000000..4c25dccd0 --- /dev/null +++ b/Source/EventFlow.MsSql/ReliablePublish/Scripts/0003 - Setup initial PublishVerifyState.sql @@ -0,0 +1,4 @@ +IF NOT EXISTS (SELECT * FROM [dbo].[EventFlowPublishVerifyState]) +BEGIN + INSERT INTO [dbo].[EventFlowPublishVerifyState] (LastVerifiedPosition) VALUES ('') +END diff --git a/Source/EventFlow.MsSql/ReliablePublish/TimeBasedRecoveryDetector.cs b/Source/EventFlow.MsSql/ReliablePublish/TimeBasedRecoveryDetector.cs new file mode 100644 index 000000000..72c6f7ae1 --- /dev/null +++ b/Source/EventFlow.MsSql/ReliablePublish/TimeBasedRecoveryDetector.cs @@ -0,0 +1,38 @@ +// The MIT License (MIT) +// +// Copyright (c) 2015-2018 Rasmus Mikkelsen +// Copyright (c) 2015-2018 eBay Software Foundation +// https://github.com/eventflow/EventFlow +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +using System; +using EventFlow.Aggregates; + +namespace EventFlow.MsSql.ReliablePublish +{ + public sealed class TimeBasedRecoveryDetector : IRecoveryDetector + { + public TimeSpan DelayToNeedRecover { get; set; } = TimeSpan.FromMinutes(5); + + public bool IsNeedRecovery(IDomainEvent evnt) + { + return DateTimeOffset.UtcNow - evnt.Timestamp > DelayToNeedRecover; + } + } +} \ No newline at end of file diff --git a/Source/EventFlow/Extensions/EventFlowOptionsReliablePublishingExtensions.cs b/Source/EventFlow/Extensions/EventFlowOptionsReliablePublishingExtensions.cs new file mode 100644 index 000000000..99e0ed3e0 --- /dev/null +++ b/Source/EventFlow/Extensions/EventFlowOptionsReliablePublishingExtensions.cs @@ -0,0 +1,44 @@ +// The MIT License (MIT) +// +// Copyright (c) 2015-2018 Rasmus Mikkelsen +// Copyright (c) 2015-2018 eBay Software Foundation +// https://github.com/eventflow/EventFlow +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +using EventFlow.Configuration; +using EventFlow.Subscribers; + +namespace EventFlow.Extensions +{ + public static class EventFlowOptionsReliablePublishingExtensions + { + public static IEventFlowOptions UseReliablePublishing( + this IEventFlowOptions eventFlowOptions, + Lifetime lifetime = Lifetime.AlwaysUnique) + where TMarkProcessor : class, IReliableMarkProcessor + where TVerificator : class, IPublishVerificator + { + return eventFlowOptions + .RegisterServices(f => f.Register(lifetime)) + .RegisterServices(f => f.Register(lifetime)) + .RegisterServices(f => f.Register()) + .RegisterServices(f => f.Decorate((context, inner) => new ReliableDomainEventPublisher(inner, context.Resolver.Resolve()))); + } + } +} \ No newline at end of file diff --git a/Source/EventFlow/Subscribers/IPublishRecoveryProcessor.cs b/Source/EventFlow/Subscribers/IPublishRecoveryProcessor.cs new file mode 100644 index 000000000..4fe385712 --- /dev/null +++ b/Source/EventFlow/Subscribers/IPublishRecoveryProcessor.cs @@ -0,0 +1,35 @@ +// The MIT License (MIT) +// +// Copyright (c) 2015-2018 Rasmus Mikkelsen +// Copyright (c) 2015-2018 eBay Software Foundation +// https://github.com/eventflow/EventFlow +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using EventFlow.Aggregates; + +namespace EventFlow.Subscribers +{ + public interface IPublishRecoveryProcessor + { + Task RecoverEventsAsync(IReadOnlyList eventsForRecovery, CancellationToken cancellationToken); + } +} \ No newline at end of file diff --git a/Source/EventFlow/Subscribers/IPublishVerificator.cs b/Source/EventFlow/Subscribers/IPublishVerificator.cs new file mode 100644 index 000000000..cb6604e62 --- /dev/null +++ b/Source/EventFlow/Subscribers/IPublishVerificator.cs @@ -0,0 +1,33 @@ +// The MIT License (MIT) +// +// Copyright (c) 2015-2018 Rasmus Mikkelsen +// Copyright (c) 2015-2018 eBay Software Foundation +// https://github.com/eventflow/EventFlow +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +using System.Threading; +using System.Threading.Tasks; + +namespace EventFlow.Subscribers +{ + public interface IPublishVerificator + { + Task VerifyOnceAsync(CancellationToken cancellationToken); + } +} \ No newline at end of file diff --git a/Source/EventFlow/Subscribers/IReliableMarkProcessor.cs b/Source/EventFlow/Subscribers/IReliableMarkProcessor.cs new file mode 100644 index 000000000..d54c5245a --- /dev/null +++ b/Source/EventFlow/Subscribers/IReliableMarkProcessor.cs @@ -0,0 +1,34 @@ +// The MIT License (MIT) +// +// Copyright (c) 2015-2018 Rasmus Mikkelsen +// Copyright (c) 2015-2018 eBay Software Foundation +// https://github.com/eventflow/EventFlow +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +using System.Collections.Generic; +using System.Threading.Tasks; +using EventFlow.Aggregates; + +namespace EventFlow.Subscribers +{ + public interface IReliableMarkProcessor + { + Task MarkPublishedWithSuccess(IReadOnlyCollection domainEvents); + } +} \ No newline at end of file diff --git a/Source/EventFlow/Subscribers/PublishVerificationResult.cs b/Source/EventFlow/Subscribers/PublishVerificationResult.cs new file mode 100644 index 000000000..cf0a35d4f --- /dev/null +++ b/Source/EventFlow/Subscribers/PublishVerificationResult.cs @@ -0,0 +1,32 @@ +// The MIT License (MIT) +// +// Copyright (c) 2015-2018 Rasmus Mikkelsen +// Copyright (c) 2015-2018 eBay Software Foundation +// https://github.com/eventflow/EventFlow +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +namespace EventFlow.Subscribers +{ + public enum PublishVerificationResult + { + CompletedNoMoreDataToVerify, + RecoveredNeedVerify, + HasMoreDataNeedVerify, + } +} \ No newline at end of file diff --git a/Source/EventFlow/Subscribers/ReliableDomainEventPublisher.cs b/Source/EventFlow/Subscribers/ReliableDomainEventPublisher.cs new file mode 100644 index 000000000..e64bc76a5 --- /dev/null +++ b/Source/EventFlow/Subscribers/ReliableDomainEventPublisher.cs @@ -0,0 +1,60 @@ +// The MIT License (MIT) +// +// Copyright (c) 2015-2018 Rasmus Mikkelsen +// Copyright (c) 2015-2018 eBay Software Foundation +// https://github.com/eventflow/EventFlow +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using EventFlow.Aggregates; +using EventFlow.Core; + +namespace EventFlow.Subscribers +{ + public sealed class ReliableDomainEventPublisher : IDomainEventPublisher + { + private readonly IDomainEventPublisher _nonReliableDomainEventPublisher; + private readonly IReliableMarkProcessor _reliableMarkProcessor; + + public ReliableDomainEventPublisher(IDomainEventPublisher nonReliableDomainEventPublisher, IReliableMarkProcessor reliableMarkProcessor) + { + _nonReliableDomainEventPublisher = nonReliableDomainEventPublisher; + _reliableMarkProcessor = reliableMarkProcessor; + } + + public async Task PublishAsync(IReadOnlyCollection domainEvents, CancellationToken cancellationToken) + { + await _nonReliableDomainEventPublisher.PublishAsync(domainEvents, cancellationToken).ConfigureAwait(false); + + await _reliableMarkProcessor.MarkPublishedWithSuccess(domainEvents); + } + + [Obsolete("Use PublishAsync (without generics and aggregate identity)")] + public async Task PublishAsync(TIdentity id, IReadOnlyCollection domainEvents, + CancellationToken cancellationToken) where TAggregate : IAggregateRoot where TIdentity : IIdentity + { + await _nonReliableDomainEventPublisher.PublishAsync(id, domainEvents, cancellationToken); + + await _reliableMarkProcessor.MarkPublishedWithSuccess(domainEvents); + } + } +} \ No newline at end of file diff --git a/Source/EventFlow/Subscribers/RetryPublishRecoveryProcessor.cs b/Source/EventFlow/Subscribers/RetryPublishRecoveryProcessor.cs new file mode 100644 index 000000000..a84168af2 --- /dev/null +++ b/Source/EventFlow/Subscribers/RetryPublishRecoveryProcessor.cs @@ -0,0 +1,53 @@ +// The MIT License (MIT) +// +// Copyright (c) 2015-2018 Rasmus Mikkelsen +// Copyright (c) 2015-2018 eBay Software Foundation +// https://github.com/eventflow/EventFlow +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using EventFlow.Aggregates; + +namespace EventFlow.Subscribers +{ + public sealed class RetryPublishRecoveryProcessor : IPublishRecoveryProcessor + { + private readonly IDomainEventPublisher _domainEventPublisher; + + public RetryPublishRecoveryProcessor(IDomainEventPublisher domainEventPublisher) + { + _domainEventPublisher = domainEventPublisher; + } + + public async Task RecoverEventsAsync(IReadOnlyList eventsForRecovery, CancellationToken cancellationToken) + { + var groupByIdentities = eventsForRecovery.GroupBy(x => x.GetIdentity()); + + foreach (var groupByIdentity in groupByIdentities) + { + // Potentially it is possible to publish events simultaniously, + // but for stability results do it serially + await _domainEventPublisher.PublishAsync(groupByIdentity.ToList(), cancellationToken); + } + } + } +} \ No newline at end of file From 1c0c9efa1c6dbde9fe208b86d08eccede5bc0239 Mon Sep 17 00:00:00 2001 From: Maxim Shoshin Date: Wed, 10 Jun 2020 12:51:06 +0300 Subject: [PATCH 2/9] Select part of publish log outside of verification transaction --- .../ReliablePublish/MsSqlPublishVerificator.cs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/Source/EventFlow.MsSql/ReliablePublish/MsSqlPublishVerificator.cs b/Source/EventFlow.MsSql/ReliablePublish/MsSqlPublishVerificator.cs index 3d8a96484..69af4c66b 100644 --- a/Source/EventFlow.MsSql/ReliablePublish/MsSqlPublishVerificator.cs +++ b/Source/EventFlow.MsSql/ReliablePublish/MsSqlPublishVerificator.cs @@ -75,7 +75,7 @@ private async Task VerifyAsync(IDbConnection db, Canc var state = await GetPreviousVerifyStateAndLockItAsync(db, transaction).ConfigureAwait(false); var position = new GlobalPosition(state.LastVerifiedPosition); - var logItemLookup = await GetLogItemsAsync(db, transaction); + var logItemLookup = await GetLogItemsAsync(cancellationToken); page = await _eventPersistence.LoadAllCommittedEvents(position, PageSize, cancellationToken) .ConfigureAwait(false); @@ -156,11 +156,13 @@ private VerifyResult VerifyDomainEvents(AllCommittedEventsPage page, ILookup> GetLogItemsAsync(IDbConnection db, IDbTransaction transaction) + private async Task> GetLogItemsAsync(CancellationToken cancellationToken) { - var logItems = await db.QueryAsync( - "SELECT [Id], [AggregateId],[MinAggregateSequenceNumber], [MaxAggregateSequenceNumber] FROM [dbo].[EventFlowPublishLog]", - transaction: transaction) + var logItems = await _msSqlConnection.QueryAsync( + Label.Named("publishlog-select"), + cancellationToken, + "SELECT TOP(@Top) [Id], [AggregateId],[MinAggregateSequenceNumber], [MaxAggregateSequenceNumber] FROM [dbo].[EventFlowPublishLog] ORDER BY [Id]", + new { Top = PageSize }) .ConfigureAwait(false); return logItems.ToLookup(x => x.AggregateId); From e9fc2ed1f068f8c4f42e849769b83234a63311cf Mon Sep 17 00:00:00 2001 From: Maxim Shoshin Date: Wed, 10 Jun 2020 15:38:38 +0300 Subject: [PATCH 3/9] Extract ReliablePublishPersistence --- .../IntegrationTests/CrashResilienceTests.cs | 9 +- ...ptionsMsSqlReliablePublishingExtensions.cs | 3 +- .../MsSqlPublishVerificator.cs | 219 ------------------ .../MsSqlReliablePublishPersistence.cs | 132 +++++++++++ ...FlowOptionsReliablePublishingExtensions.cs | 17 +- .../IPublishRecoveryProcessor.cs | 2 +- .../IPublishVerificationItem.cs} | 15 +- .../IPublishVerificator.cs | 2 +- .../PublishRecovery}/IRecoveryDetector.cs | 4 +- .../IReliableMarkProcessor.cs | 4 +- .../IReliablePublishPersistence.cs | 41 ++++ .../PublishRecoveryProcessor.cs} | 7 +- .../PublishVerificationResult.cs | 2 +- .../PublishRecovery/PublishVerificator.cs | 146 ++++++++++++ .../ReliableDomainEventPublisher.cs | 7 +- .../PublishRecovery/ReliableMarkProcessor.cs} | 41 +--- .../TimeBasedRecoveryDetector.cs | 6 +- .../PublishRecovery/VerificationState.cs | 41 ++++ 18 files changed, 413 insertions(+), 285 deletions(-) delete mode 100644 Source/EventFlow.MsSql/ReliablePublish/MsSqlPublishVerificator.cs create mode 100644 Source/EventFlow.MsSql/ReliablePublish/MsSqlReliablePublishPersistence.cs rename Source/EventFlow/{Subscribers => PublishRecovery}/IPublishRecoveryProcessor.cs (97%) rename Source/{EventFlow.MsSql/ReliablePublish/PublishLogItem.cs => EventFlow/PublishRecovery/IPublishVerificationItem.cs} (81%) rename Source/EventFlow/{Subscribers => PublishRecovery}/IPublishVerificator.cs (97%) rename Source/{EventFlow.MsSql/ReliablePublish => EventFlow/PublishRecovery}/IRecoveryDetector.cs (93%) rename Source/EventFlow/{Subscribers => PublishRecovery}/IReliableMarkProcessor.cs (93%) create mode 100644 Source/EventFlow/PublishRecovery/IReliablePublishPersistence.cs rename Source/EventFlow/{Subscribers/RetryPublishRecoveryProcessor.cs => PublishRecovery/PublishRecoveryProcessor.cs} (90%) rename Source/EventFlow/{Subscribers => PublishRecovery}/PublishVerificationResult.cs (97%) create mode 100644 Source/EventFlow/PublishRecovery/PublishVerificator.cs rename Source/EventFlow/{Subscribers => PublishRecovery}/ReliableDomainEventPublisher.cs (92%) rename Source/{EventFlow.MsSql/ReliablePublish/MsSqlReliableMarkProcessor.cs => EventFlow/PublishRecovery/ReliableMarkProcessor.cs} (50%) rename Source/{EventFlow.MsSql/ReliablePublish => EventFlow/PublishRecovery}/TimeBasedRecoveryDetector.cs (88%) create mode 100644 Source/EventFlow/PublishRecovery/VerificationState.cs diff --git a/Source/EventFlow.MsSql.Tests/IntegrationTests/CrashResilienceTests.cs b/Source/EventFlow.MsSql.Tests/IntegrationTests/CrashResilienceTests.cs index c3205a587..e52270173 100644 --- a/Source/EventFlow.MsSql.Tests/IntegrationTests/CrashResilienceTests.cs +++ b/Source/EventFlow.MsSql.Tests/IntegrationTests/CrashResilienceTests.cs @@ -32,6 +32,7 @@ using EventFlow.Extensions; using EventFlow.MsSql.Extensions; using EventFlow.MsSql.ReliablePublish; +using EventFlow.PublishRecovery; using EventFlow.Subscribers; using EventFlow.TestHelpers; using EventFlow.TestHelpers.Aggregates; @@ -47,7 +48,7 @@ public sealed class CrashResilienceTests : IntegrationTest { private IMsSqlDatabase _testDatabase; private TestPublisher _publisher; - private MsSqlPublishVerificator _publishVerificator; + private PublishVerificator _publishVerificator; protected override IRootResolver CreateRootResolver(IEventFlowOptions eventFlowOptions) { @@ -69,7 +70,7 @@ protected override IRootResolver CreateRootResolver(IEventFlowOptions eventFlowO databaseMigrator.MigrateDatabaseUsingEmbeddedScripts(GetType().Assembly); _publisher = (TestPublisher)resolver.Resolve(); - _publishVerificator = (MsSqlPublishVerificator)resolver.Resolve(); + _publishVerificator = (PublishVerificator)resolver.Resolve(); return resolver; } @@ -90,7 +91,7 @@ public async Task ShouldRecoverAfterFailure() // Act _publisher.SimulatePublishFailure = false; - await _publishVerificator.VerifyOnceAsync(CancellationToken.None).ConfigureAwait(false); + await Verify().ConfigureAwait(false); // Assert _publisher.PublishedEvents.Should().NotBeEmpty(); @@ -209,7 +210,7 @@ public int GetHashCode(IDomainEvent obj) private sealed class AlwaysRecoverDetector : IRecoveryDetector { - public bool IsNeedRecovery(IDomainEvent evnt) + public bool IsNeedRecovery(IDomainEvent domainEvent) { return true; } diff --git a/Source/EventFlow.MsSql/Extensions/EventFlowOptionsMsSqlReliablePublishingExtensions.cs b/Source/EventFlow.MsSql/Extensions/EventFlowOptionsMsSqlReliablePublishingExtensions.cs index 64e813ad3..b9222fafe 100644 --- a/Source/EventFlow.MsSql/Extensions/EventFlowOptionsMsSqlReliablePublishingExtensions.cs +++ b/Source/EventFlow.MsSql/Extensions/EventFlowOptionsMsSqlReliablePublishingExtensions.cs @@ -31,8 +31,7 @@ public static class EventFlowOptionsMsSqlReliablePublishingExtensions public static IEventFlowOptions UseMssqlReliablePublishing(this IEventFlowOptions eventFlowOptions) { return eventFlowOptions - .UseReliablePublishing() - .RegisterServices(r => r.Register()); + .UseReliablePublishing(); } } } \ No newline at end of file diff --git a/Source/EventFlow.MsSql/ReliablePublish/MsSqlPublishVerificator.cs b/Source/EventFlow.MsSql/ReliablePublish/MsSqlPublishVerificator.cs deleted file mode 100644 index 69af4c66b..000000000 --- a/Source/EventFlow.MsSql/ReliablePublish/MsSqlPublishVerificator.cs +++ /dev/null @@ -1,219 +0,0 @@ -// The MIT License (MIT) -// -// Copyright (c) 2015-2018 Rasmus Mikkelsen -// Copyright (c) 2015-2018 eBay Software Foundation -// https://github.com/eventflow/EventFlow -// -// Permission is hereby granted, free of charge, to any person obtaining a copy of -// this software and associated documentation files (the "Software"), to deal in -// the Software without restriction, including without limitation the rights to -// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of -// the Software, and to permit persons to whom the Software is furnished to do so, -// subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS -// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR -// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER -// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -using System; -using System.Collections.Generic; -using System.Data; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Dapper; -using EventFlow.Aggregates; -using EventFlow.Core; -using EventFlow.EventStores; -using EventFlow.Subscribers; - -namespace EventFlow.MsSql.ReliablePublish -{ - public sealed class MsSqlPublishVerificator : IPublishVerificator - { - private const int PageSize = 200; - - private readonly IEventPersistence _eventPersistence; - private readonly IPublishRecoveryProcessor _publishRecoveryProcessor; - private readonly IMsSqlConnection _msSqlConnection; - private readonly IEventJsonSerializer _eventSerializer; - private readonly IRecoveryDetector _recoveryDetector; - - public MsSqlPublishVerificator(IEventPersistence eventPersistence, IMsSqlConnection msSqlConnection, IPublishRecoveryProcessor publishRecoveryProcessor, IEventJsonSerializer eventSerializer, IRecoveryDetector recoveryDetector) - { - _eventPersistence = eventPersistence; - _msSqlConnection = msSqlConnection; - _publishRecoveryProcessor = publishRecoveryProcessor; - _eventSerializer = eventSerializer; - _recoveryDetector = recoveryDetector; - } - - public TimeSpan ReliableThreshold { get; set; } = TimeSpan.FromMinutes(5); - - public Task VerifyOnceAsync(CancellationToken cancellationToken) - { - return _msSqlConnection.WithConnectionAsync( - Label.Named("publishlog-verify"), - VerifyAsync, - cancellationToken); - } - - private async Task VerifyAsync(IDbConnection db, CancellationToken cancellationToken) - { - var result = PublishVerificationResult.CompletedNoMoreDataToVerify; - - VerifyResult verifyResult; - AllCommittedEventsPage page; - using (var transaction = db.BeginTransaction()) - { - var state = await GetPreviousVerifyStateAndLockItAsync(db, transaction).ConfigureAwait(false); - var position = new GlobalPosition(state.LastVerifiedPosition); - - var logItemLookup = await GetLogItemsAsync(cancellationToken); - - page = await _eventPersistence.LoadAllCommittedEvents(position, PageSize, cancellationToken) - .ConfigureAwait(false); - state.LastVerifiedPosition = page.NextGlobalPosition.Value; - - verifyResult = VerifyDomainEvents(page, logItemLookup); - - // Some of not published events can be in flight, so no need recovery them - // but we have to check them again on next iteration - var eventsForRecovery = GetEventsForRecovery(verifyResult.UnpublishedEvents); - - if (eventsForRecovery.Count > 0) - { - // Do it inside transaction to recover in single thread - // success recovery should put LogItem - await _publishRecoveryProcessor.RecoverEventsAsync(eventsForRecovery, cancellationToken) - .ConfigureAwait(false); - - result = PublishVerificationResult.RecoveredNeedVerify; - } - - // Remove logs and move position forward only when it is successfully recovered. - if (verifyResult.UnpublishedEvents.Count == 0) - { - await RemoveLogItemsAsync(verifyResult.PublishedLogItems, db, transaction) - .ConfigureAwait(false); - - await UpdateLastVerifyStateAsync(db, state, transaction) - .ConfigureAwait(false); - - result = page.CommittedDomainEvents.Count < PageSize - ? PublishVerificationResult.CompletedNoMoreDataToVerify - : PublishVerificationResult.HasMoreDataNeedVerify; - } - - transaction.Commit(); - } - - return result; - } - - private IReadOnlyList GetEventsForRecovery(IReadOnlyList unpublishedEvents) - { - return unpublishedEvents - .Select(evnt => _eventSerializer.Deserialize(evnt)) - .Where(evnt => _recoveryDetector.IsNeedRecovery(evnt)) - .ToList(); - } - - private async Task RemoveLogItemsAsync(IReadOnlyList publishedLogItems, IDbConnection db, IDbTransaction transaction) - { - foreach (var logItem in publishedLogItems) - { - await db.ExecuteAsync("DELETE FROM [dbo].[EventFlowPublishLog] WHERE Id = @Id", logItem, transaction); - } - } - - private VerifyResult VerifyDomainEvents(AllCommittedEventsPage page, ILookup logItemLookup) - { - var unpublishedEvents = new List(); - var publishedLogItems = new List(); - - foreach (var committedDomainEvent in page.CommittedDomainEvents) - { - var logItem = TryGetPublishedLogItem(committedDomainEvent, logItemLookup); - - if (logItem == null) - { - unpublishedEvents.Add(committedDomainEvent); - } - // Remove logItem only on the last event related with this log item - else if (committedDomainEvent.AggregateSequenceNumber == logItem.MaxAggregateSequenceNumber) - { - publishedLogItems.Add(logItem); - } - } - - return new VerifyResult(unpublishedEvents, publishedLogItems); - } - - private async Task> GetLogItemsAsync(CancellationToken cancellationToken) - { - var logItems = await _msSqlConnection.QueryAsync( - Label.Named("publishlog-select"), - cancellationToken, - "SELECT TOP(@Top) [Id], [AggregateId],[MinAggregateSequenceNumber], [MaxAggregateSequenceNumber] FROM [dbo].[EventFlowPublishLog] ORDER BY [Id]", - new { Top = PageSize }) - .ConfigureAwait(false); - - return logItems.ToLookup(x => x.AggregateId); - } - - private static Task UpdateLastVerifyStateAsync(IDbConnection db, VerifyState state, - IDbTransaction transaction) - { - return db.ExecuteAsync( - "UPDATE [dbo].[EventFlowPublishVerifyState] SET LastVerifiedPosition = @LastVerifiedPosition WHERE Id = @Id", - state, - transaction); - } - - private static async Task GetPreviousVerifyStateAndLockItAsync(IDbConnection db, IDbTransaction transaction) - { - var state = await db.QueryFirstOrDefaultAsync( - "SELECT [Id], [LastVerifiedPosition] FROM [dbo].[EventFlowPublishVerifyState] WITH (XLOCK)", transaction: transaction) - .ConfigureAwait(false); - - return state; - } - - private PublishLogItem TryGetPublishedLogItem(ICommittedDomainEvent committedDomainEvent, ILookup logItemLookup) - { - var logItems = logItemLookup[committedDomainEvent.AggregateId]; - - return logItems.FirstOrDefault( - logItem => logItem.MinAggregateSequenceNumber <= committedDomainEvent.AggregateSequenceNumber && - committedDomainEvent.AggregateSequenceNumber <= logItem.MaxAggregateSequenceNumber); - } - - public sealed class VerifyState - { - public long Id { get; set; } - public string LastVerifiedPosition { get; set; } - } - - private sealed class VerifyResult - { - public VerifyResult( - IReadOnlyList unpublishedEvents, - IReadOnlyList publishedLogItems) - { - UnpublishedEvents = unpublishedEvents; - PublishedLogItems = publishedLogItems; - } - - public IReadOnlyList UnpublishedEvents { get; } - - public IReadOnlyList PublishedLogItems { get; } - } - } -} \ No newline at end of file diff --git a/Source/EventFlow.MsSql/ReliablePublish/MsSqlReliablePublishPersistence.cs b/Source/EventFlow.MsSql/ReliablePublish/MsSqlReliablePublishPersistence.cs new file mode 100644 index 000000000..ef6a36f27 --- /dev/null +++ b/Source/EventFlow.MsSql/ReliablePublish/MsSqlReliablePublishPersistence.cs @@ -0,0 +1,132 @@ +// The MIT License (MIT) +// +// Copyright (c) 2015-2018 Rasmus Mikkelsen +// Copyright (c) 2015-2018 eBay Software Foundation +// https://github.com/eventflow/EventFlow +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using EventFlow.Aggregates; +using EventFlow.Core; +using EventFlow.EventStores; +using EventFlow.PublishRecovery; +using EventFlow.Subscribers; + +namespace EventFlow.MsSql.ReliablePublish +{ + public sealed class MsSqlReliablePublishPersistence : IReliablePublishPersistence + { + private readonly IMsSqlConnection _msSqlConnection; + + public MsSqlReliablePublishPersistence(IMsSqlConnection msSqlConnection) + { + _msSqlConnection = msSqlConnection; + } + + public async Task MarkPublishedAsync(IIdentity aggregateIdentity, IReadOnlyCollection domainEvents) + { + var item = new PublishLogItem + { + AggregateId = aggregateIdentity.Value, + MinAggregateSequenceNumber = domainEvents.Min(x => x.AggregateSequenceNumber), + MaxAggregateSequenceNumber = domainEvents.Max(x => x.AggregateSequenceNumber), + }; + + await _msSqlConnection.ExecuteAsync( + Label.Named("publishlog-commit"), + CancellationToken.None, // Unable to Cancel + @"INSERT INTO [dbo].[EventFlowPublishLog] + (AggregateId, MinAggregateSequenceNumber, MaxAggregateSequenceNumber) + VALUES + (@AggregateId, @MinAggregateSequenceNumber, @MaxAggregateSequenceNumber)", + item) + .ConfigureAwait(false); + } + + public async Task GetUnverifiedItemsAsync(int maxCount, CancellationToken cancellationToken) + { + var logItems = await _msSqlConnection.QueryAsync( + Label.Named("publishlog-select"), + cancellationToken, + "SELECT TOP(@Top) [Id], [AggregateId],[MinAggregateSequenceNumber], [MaxAggregateSequenceNumber] FROM [dbo].[EventFlowPublishLog] ORDER BY [Id]", + new { Top = maxCount }) + .ConfigureAwait(false); + + var positions = await _msSqlConnection.QueryAsync( + Label.Named("publishlog-global-position-select"), + cancellationToken, + "SELECT TOP 1 [LastVerifiedPosition] FROM [dbo].[EventFlowPublishVerifyState]") + .ConfigureAwait(false); + + return new VerificationState( + new GlobalPosition(positions.First()), + logItems); + } + + public async Task MarkVerifiedAsync( + IReadOnlyCollection verifiedItems, + GlobalPosition newVerifiedPosition, + CancellationToken cancellationToken) + { + await _msSqlConnection.ExecuteAsync( + Label.Named("publishlog-global-position-update"), + cancellationToken, + "UPDATE [dbo].[EventFlowPublishVerifyState] SET LastVerifiedPosition = @LastVerifiedPosition", + new + { + LastVerifiedPosition = newVerifiedPosition.Value + }); + + foreach (var publishVerificationItem in verifiedItems) + { + var logItem = (PublishLogItem)publishVerificationItem; + + await _msSqlConnection.ExecuteAsync( + Label.Named("publishlog-confirm"), + cancellationToken, + "DELETE FROM [dbo].[EventFlowPublishLog] WHERE Id = @Id", + new { logItem.Id}); + } + } + + private sealed class PublishLogItem : IPublishVerificationItem + { + public long Id { get; set; } + public string AggregateId { get; set; } + public int MinAggregateSequenceNumber { get; set; } + public int MaxAggregateSequenceNumber { get; set; } + + public bool IsPublished(ICommittedDomainEvent committedDomainEvent) + { + return AggregateId == committedDomainEvent.AggregateId && + MinAggregateSequenceNumber <= committedDomainEvent.AggregateSequenceNumber && + committedDomainEvent.AggregateSequenceNumber <= MaxAggregateSequenceNumber; + } + + public bool IsFinalEvent(ICommittedDomainEvent committedDomainEvent) + { + return committedDomainEvent.AggregateId == AggregateId && + MaxAggregateSequenceNumber == committedDomainEvent.AggregateSequenceNumber; + } + } + } +} \ No newline at end of file diff --git a/Source/EventFlow/Extensions/EventFlowOptionsReliablePublishingExtensions.cs b/Source/EventFlow/Extensions/EventFlowOptionsReliablePublishingExtensions.cs index 99e0ed3e0..00c6b0af7 100644 --- a/Source/EventFlow/Extensions/EventFlowOptionsReliablePublishingExtensions.cs +++ b/Source/EventFlow/Extensions/EventFlowOptionsReliablePublishingExtensions.cs @@ -22,23 +22,26 @@ // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. using EventFlow.Configuration; +using EventFlow.PublishRecovery; using EventFlow.Subscribers; namespace EventFlow.Extensions { public static class EventFlowOptionsReliablePublishingExtensions { - public static IEventFlowOptions UseReliablePublishing( + public static IEventFlowOptions UseReliablePublishing( this IEventFlowOptions eventFlowOptions, Lifetime lifetime = Lifetime.AlwaysUnique) - where TMarkProcessor : class, IReliableMarkProcessor - where TVerificator : class, IPublishVerificator + where TReliablePublishPersistence : class, IReliablePublishPersistence { return eventFlowOptions - .RegisterServices(f => f.Register(lifetime)) - .RegisterServices(f => f.Register(lifetime)) - .RegisterServices(f => f.Register()) - .RegisterServices(f => f.Decorate((context, inner) => new ReliableDomainEventPublisher(inner, context.Resolver.Resolve()))); + .RegisterServices(f => f.Register()) + .RegisterServices(f => f.Register()) + .RegisterServices(f => f.Register()) + .RegisterServices(r => r.Register()) + .RegisterServices(f => f.Register(lifetime)) + .RegisterServices(f => f.Decorate( + (context, inner) => new ReliableDomainEventPublisher(inner, context.Resolver.Resolve()))); } } } \ No newline at end of file diff --git a/Source/EventFlow/Subscribers/IPublishRecoveryProcessor.cs b/Source/EventFlow/PublishRecovery/IPublishRecoveryProcessor.cs similarity index 97% rename from Source/EventFlow/Subscribers/IPublishRecoveryProcessor.cs rename to Source/EventFlow/PublishRecovery/IPublishRecoveryProcessor.cs index 4fe385712..0bac92de7 100644 --- a/Source/EventFlow/Subscribers/IPublishRecoveryProcessor.cs +++ b/Source/EventFlow/PublishRecovery/IPublishRecoveryProcessor.cs @@ -26,7 +26,7 @@ using System.Threading.Tasks; using EventFlow.Aggregates; -namespace EventFlow.Subscribers +namespace EventFlow.PublishRecovery { public interface IPublishRecoveryProcessor { diff --git a/Source/EventFlow.MsSql/ReliablePublish/PublishLogItem.cs b/Source/EventFlow/PublishRecovery/IPublishVerificationItem.cs similarity index 81% rename from Source/EventFlow.MsSql/ReliablePublish/PublishLogItem.cs rename to Source/EventFlow/PublishRecovery/IPublishVerificationItem.cs index 745765b2c..44630a285 100644 --- a/Source/EventFlow.MsSql/ReliablePublish/PublishLogItem.cs +++ b/Source/EventFlow/PublishRecovery/IPublishVerificationItem.cs @@ -21,13 +21,16 @@ // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -namespace EventFlow.MsSql.ReliablePublish +using EventFlow.EventStores; + +namespace EventFlow.PublishRecovery { - public sealed class PublishLogItem + public interface IPublishVerificationItem { - public long Id { get; set; } - public string AggregateId { get; set; } - public int MinAggregateSequenceNumber { get; set; } - public int MaxAggregateSequenceNumber { get; set; } + string AggregateId { get; } + + bool IsPublished(ICommittedDomainEvent committedDomainEvent); + + bool IsFinalEvent(ICommittedDomainEvent committedDomainEvent); } } \ No newline at end of file diff --git a/Source/EventFlow/Subscribers/IPublishVerificator.cs b/Source/EventFlow/PublishRecovery/IPublishVerificator.cs similarity index 97% rename from Source/EventFlow/Subscribers/IPublishVerificator.cs rename to Source/EventFlow/PublishRecovery/IPublishVerificator.cs index cb6604e62..3a5a0e519 100644 --- a/Source/EventFlow/Subscribers/IPublishVerificator.cs +++ b/Source/EventFlow/PublishRecovery/IPublishVerificator.cs @@ -24,7 +24,7 @@ using System.Threading; using System.Threading.Tasks; -namespace EventFlow.Subscribers +namespace EventFlow.PublishRecovery { public interface IPublishVerificator { diff --git a/Source/EventFlow.MsSql/ReliablePublish/IRecoveryDetector.cs b/Source/EventFlow/PublishRecovery/IRecoveryDetector.cs similarity index 93% rename from Source/EventFlow.MsSql/ReliablePublish/IRecoveryDetector.cs rename to Source/EventFlow/PublishRecovery/IRecoveryDetector.cs index bf57b4b15..9f8aeddd3 100644 --- a/Source/EventFlow.MsSql/ReliablePublish/IRecoveryDetector.cs +++ b/Source/EventFlow/PublishRecovery/IRecoveryDetector.cs @@ -23,10 +23,10 @@ using EventFlow.Aggregates; -namespace EventFlow.MsSql.ReliablePublish +namespace EventFlow.PublishRecovery { public interface IRecoveryDetector { - bool IsNeedRecovery(IDomainEvent evnt); + bool IsNeedRecovery(IDomainEvent domainEvent); } } \ No newline at end of file diff --git a/Source/EventFlow/Subscribers/IReliableMarkProcessor.cs b/Source/EventFlow/PublishRecovery/IReliableMarkProcessor.cs similarity index 93% rename from Source/EventFlow/Subscribers/IReliableMarkProcessor.cs rename to Source/EventFlow/PublishRecovery/IReliableMarkProcessor.cs index d54c5245a..100948157 100644 --- a/Source/EventFlow/Subscribers/IReliableMarkProcessor.cs +++ b/Source/EventFlow/PublishRecovery/IReliableMarkProcessor.cs @@ -25,10 +25,10 @@ using System.Threading.Tasks; using EventFlow.Aggregates; -namespace EventFlow.Subscribers +namespace EventFlow.PublishRecovery { public interface IReliableMarkProcessor { - Task MarkPublishedWithSuccess(IReadOnlyCollection domainEvents); + Task MarkEventsPublishedAsync(IReadOnlyCollection domainEvents); } } \ No newline at end of file diff --git a/Source/EventFlow/PublishRecovery/IReliablePublishPersistence.cs b/Source/EventFlow/PublishRecovery/IReliablePublishPersistence.cs new file mode 100644 index 000000000..4df097673 --- /dev/null +++ b/Source/EventFlow/PublishRecovery/IReliablePublishPersistence.cs @@ -0,0 +1,41 @@ +// The MIT License (MIT) +// +// Copyright (c) 2015-2018 Rasmus Mikkelsen +// Copyright (c) 2015-2018 eBay Software Foundation +// https://github.com/eventflow/EventFlow +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using EventFlow.Aggregates; +using EventFlow.Core; +using EventFlow.EventStores; + +namespace EventFlow.PublishRecovery +{ + public interface IReliablePublishPersistence + { + Task MarkPublishedAsync(IIdentity aggregateIdentity, IReadOnlyCollection domainEvents); + + Task GetUnverifiedItemsAsync(int maxCount, CancellationToken cancellationToken); + + Task MarkVerifiedAsync(IReadOnlyCollection verifiedItems, GlobalPosition newVerifiedPosition, CancellationToken cancellationToken); + } +} \ No newline at end of file diff --git a/Source/EventFlow/Subscribers/RetryPublishRecoveryProcessor.cs b/Source/EventFlow/PublishRecovery/PublishRecoveryProcessor.cs similarity index 90% rename from Source/EventFlow/Subscribers/RetryPublishRecoveryProcessor.cs rename to Source/EventFlow/PublishRecovery/PublishRecoveryProcessor.cs index a84168af2..b4990f3b0 100644 --- a/Source/EventFlow/Subscribers/RetryPublishRecoveryProcessor.cs +++ b/Source/EventFlow/PublishRecovery/PublishRecoveryProcessor.cs @@ -26,14 +26,15 @@ using System.Threading; using System.Threading.Tasks; using EventFlow.Aggregates; +using EventFlow.Subscribers; -namespace EventFlow.Subscribers +namespace EventFlow.PublishRecovery { - public sealed class RetryPublishRecoveryProcessor : IPublishRecoveryProcessor + public sealed class PublishRecoveryProcessor : IPublishRecoveryProcessor { private readonly IDomainEventPublisher _domainEventPublisher; - public RetryPublishRecoveryProcessor(IDomainEventPublisher domainEventPublisher) + public PublishRecoveryProcessor(IDomainEventPublisher domainEventPublisher) { _domainEventPublisher = domainEventPublisher; } diff --git a/Source/EventFlow/Subscribers/PublishVerificationResult.cs b/Source/EventFlow/PublishRecovery/PublishVerificationResult.cs similarity index 97% rename from Source/EventFlow/Subscribers/PublishVerificationResult.cs rename to Source/EventFlow/PublishRecovery/PublishVerificationResult.cs index cf0a35d4f..b1ca9fb11 100644 --- a/Source/EventFlow/Subscribers/PublishVerificationResult.cs +++ b/Source/EventFlow/PublishRecovery/PublishVerificationResult.cs @@ -21,7 +21,7 @@ // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -namespace EventFlow.Subscribers +namespace EventFlow.PublishRecovery { public enum PublishVerificationResult { diff --git a/Source/EventFlow/PublishRecovery/PublishVerificator.cs b/Source/EventFlow/PublishRecovery/PublishVerificator.cs new file mode 100644 index 000000000..c890bc9a0 --- /dev/null +++ b/Source/EventFlow/PublishRecovery/PublishVerificator.cs @@ -0,0 +1,146 @@ +// The MIT License (MIT) +// +// Copyright (c) 2015-2018 Rasmus Mikkelsen +// Copyright (c) 2015-2018 eBay Software Foundation +// https://github.com/eventflow/EventFlow +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using EventFlow.Aggregates; +using EventFlow.EventStores; + +namespace EventFlow.PublishRecovery +{ + public sealed class PublishVerificator : IPublishVerificator + { + private const int PageSize = 200; + + private readonly IEventPersistence _eventPersistence; + private readonly IPublishRecoveryProcessor _publishRecoveryProcessor; + private readonly IEventJsonSerializer _eventSerializer; + private readonly IRecoveryDetector _recoveryDetector; + private readonly IReliablePublishPersistence _reliablePublishPersistence; + + public PublishVerificator(IEventPersistence eventPersistence, IPublishRecoveryProcessor publishRecoveryProcessor, IEventJsonSerializer eventSerializer, IRecoveryDetector recoveryDetector, IReliablePublishPersistence reliablePublishPersistence) + { + _eventPersistence = eventPersistence; + _publishRecoveryProcessor = publishRecoveryProcessor; + _eventSerializer = eventSerializer; + _recoveryDetector = recoveryDetector; + _reliablePublishPersistence = reliablePublishPersistence; + } + + public async Task VerifyOnceAsync(CancellationToken cancellationToken) + { + var state = await _reliablePublishPersistence.GetUnverifiedItemsAsync(PageSize, cancellationToken) + .ConfigureAwait(false); + + var logItemLookup = state.Items.ToLookup(x => x.AggregateId); + + var page = await _eventPersistence.LoadAllCommittedEvents(state.Position, PageSize, cancellationToken) + .ConfigureAwait(false); + + var verifyResult = VerifyDomainEvents(page, logItemLookup); + + // Some of not published events can be in flight, so no need recovery them + // but we have to check them again on next iteration + var eventsForRecovery = GetEventsForRecovery(verifyResult.UnpublishedEvents); + + if (eventsForRecovery.Count > 0) + { + // Do it inside transaction to recover in single thread + // success recovery should put LogItem + await _publishRecoveryProcessor.RecoverEventsAsync(eventsForRecovery, cancellationToken) + .ConfigureAwait(false); + + return PublishVerificationResult.RecoveredNeedVerify; + } + + // Remove logs and move position forward only when it is successfully recovered. + if (verifyResult.UnpublishedEvents.Count == 0) + { + await _reliablePublishPersistence + .MarkVerifiedAsync(verifyResult.PublishedLogItems, page.NextGlobalPosition, cancellationToken) + .ConfigureAwait(false); + + return page.CommittedDomainEvents.Count < PageSize + ? PublishVerificationResult.CompletedNoMoreDataToVerify + : PublishVerificationResult.HasMoreDataNeedVerify; + } + + return PublishVerificationResult.CompletedNoMoreDataToVerify; + } + + private IReadOnlyList GetEventsForRecovery(IReadOnlyList unpublishedEvents) + { + return unpublishedEvents + .Select(evnt => _eventSerializer.Deserialize(evnt)) + .Where(evnt => _recoveryDetector.IsNeedRecovery(evnt)) + .ToList(); + } + + private VerifyResult VerifyDomainEvents(AllCommittedEventsPage page, ILookup logItemLookup) + { + var unpublishedEvents = new List(); + var publishedLogItems = new List(); + + foreach (var committedDomainEvent in page.CommittedDomainEvents) + { + var logItem = TryGetPublishedLogItem(committedDomainEvent, logItemLookup); + + if (logItem == null) + { + unpublishedEvents.Add(committedDomainEvent); + } + // Remove logItem only on the last event related with this log item + else if (logItem.IsFinalEvent(committedDomainEvent)) + { + publishedLogItems.Add(logItem); + } + } + + return new VerifyResult(unpublishedEvents, publishedLogItems); + } + + private IPublishVerificationItem TryGetPublishedLogItem(ICommittedDomainEvent committedDomainEvent, ILookup logItemLookup) + { + var logItems = logItemLookup[committedDomainEvent.AggregateId]; + + return logItems.FirstOrDefault(logItem => logItem.IsPublished(committedDomainEvent)); + } + + private sealed class VerifyResult + { + public VerifyResult( + IReadOnlyList unpublishedEvents, + IReadOnlyList publishedLogItems) + { + UnpublishedEvents = unpublishedEvents; + PublishedLogItems = publishedLogItems; + } + + public IReadOnlyList UnpublishedEvents { get; } + + public IReadOnlyList PublishedLogItems { get; } + } + } +} \ No newline at end of file diff --git a/Source/EventFlow/Subscribers/ReliableDomainEventPublisher.cs b/Source/EventFlow/PublishRecovery/ReliableDomainEventPublisher.cs similarity index 92% rename from Source/EventFlow/Subscribers/ReliableDomainEventPublisher.cs rename to Source/EventFlow/PublishRecovery/ReliableDomainEventPublisher.cs index e64bc76a5..22fe554cf 100644 --- a/Source/EventFlow/Subscribers/ReliableDomainEventPublisher.cs +++ b/Source/EventFlow/PublishRecovery/ReliableDomainEventPublisher.cs @@ -27,8 +27,9 @@ using System.Threading.Tasks; using EventFlow.Aggregates; using EventFlow.Core; +using EventFlow.Subscribers; -namespace EventFlow.Subscribers +namespace EventFlow.PublishRecovery { public sealed class ReliableDomainEventPublisher : IDomainEventPublisher { @@ -45,7 +46,7 @@ public async Task PublishAsync(IReadOnlyCollection domainEvents, C { await _nonReliableDomainEventPublisher.PublishAsync(domainEvents, cancellationToken).ConfigureAwait(false); - await _reliableMarkProcessor.MarkPublishedWithSuccess(domainEvents); + await _reliableMarkProcessor.MarkEventsPublishedAsync(domainEvents); } [Obsolete("Use PublishAsync (without generics and aggregate identity)")] @@ -54,7 +55,7 @@ public async Task PublishAsync(TIdentity id, IReadOnlyCol { await _nonReliableDomainEventPublisher.PublishAsync(id, domainEvents, cancellationToken); - await _reliableMarkProcessor.MarkPublishedWithSuccess(domainEvents); + await _reliableMarkProcessor.MarkEventsPublishedAsync(domainEvents); } } } \ No newline at end of file diff --git a/Source/EventFlow.MsSql/ReliablePublish/MsSqlReliableMarkProcessor.cs b/Source/EventFlow/PublishRecovery/ReliableMarkProcessor.cs similarity index 50% rename from Source/EventFlow.MsSql/ReliablePublish/MsSqlReliableMarkProcessor.cs rename to Source/EventFlow/PublishRecovery/ReliableMarkProcessor.cs index c36e93dec..978605546 100644 --- a/Source/EventFlow.MsSql/ReliablePublish/MsSqlReliableMarkProcessor.cs +++ b/Source/EventFlow/PublishRecovery/ReliableMarkProcessor.cs @@ -21,27 +21,23 @@ // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -using System; using System.Collections.Generic; using System.Linq; -using System.Threading; using System.Threading.Tasks; using EventFlow.Aggregates; -using EventFlow.Core; -using EventFlow.Subscribers; -namespace EventFlow.MsSql.ReliablePublish +namespace EventFlow.PublishRecovery { - public sealed class MsSqlReliableMarkProcessor : IReliableMarkProcessor + public sealed class ReliableMarkProcessor : IReliableMarkProcessor { - private readonly IMsSqlConnection _msSqlConnection; + private readonly IReliablePublishPersistence _reliablePublishPersistence; - public MsSqlReliableMarkProcessor(IMsSqlConnection msSqlConnection) + public ReliableMarkProcessor(IReliablePublishPersistence reliablePublishPersistence) { - _msSqlConnection = msSqlConnection; + _reliablePublishPersistence = reliablePublishPersistence; } - public async Task MarkPublishedWithSuccess(IReadOnlyCollection domainEvents) + public async Task MarkEventsPublishedAsync(IReadOnlyCollection domainEvents) { var count = domainEvents.Count; if (count == 0) @@ -49,29 +45,12 @@ public async Task MarkPublishedWithSuccess(IReadOnlyCollection dom return; } - var aggregateIdentity = domainEvents.First().GetIdentity(); - - if (domainEvents.Any(x => !Equals(x.GetIdentity(), aggregateIdentity))) + foreach (var domaiEnventsPerAggregate in domainEvents.GroupBy(x => x.GetIdentity())) { - throw new NotSupportedException("Mark events as published successfully for several aggregates is not supported"); - } + var aggregateIdentity = domaiEnventsPerAggregate.Key; - var item = new PublishLogItem - { - AggregateId = aggregateIdentity.Value, - MinAggregateSequenceNumber = domainEvents.Min(x => x.AggregateSequenceNumber), - MaxAggregateSequenceNumber = domainEvents.Max(x => x.AggregateSequenceNumber), - }; - - await _msSqlConnection.ExecuteAsync( - Label.Named("publishlog-commit"), - CancellationToken.None, // Unable to Cancel - @"INSERT INTO [dbo].[EventFlowPublishLog] - (AggregateId, MinAggregateSequenceNumber, MaxAggregateSequenceNumber) - VALUES - (@AggregateId, @MinAggregateSequenceNumber, @MaxAggregateSequenceNumber)", - item) - .ConfigureAwait(false); + await _reliablePublishPersistence.MarkPublishedAsync(aggregateIdentity, domainEvents).ConfigureAwait(false); + } } } } \ No newline at end of file diff --git a/Source/EventFlow.MsSql/ReliablePublish/TimeBasedRecoveryDetector.cs b/Source/EventFlow/PublishRecovery/TimeBasedRecoveryDetector.cs similarity index 88% rename from Source/EventFlow.MsSql/ReliablePublish/TimeBasedRecoveryDetector.cs rename to Source/EventFlow/PublishRecovery/TimeBasedRecoveryDetector.cs index 72c6f7ae1..ce36a9d11 100644 --- a/Source/EventFlow.MsSql/ReliablePublish/TimeBasedRecoveryDetector.cs +++ b/Source/EventFlow/PublishRecovery/TimeBasedRecoveryDetector.cs @@ -24,15 +24,15 @@ using System; using EventFlow.Aggregates; -namespace EventFlow.MsSql.ReliablePublish +namespace EventFlow.PublishRecovery { public sealed class TimeBasedRecoveryDetector : IRecoveryDetector { public TimeSpan DelayToNeedRecover { get; set; } = TimeSpan.FromMinutes(5); - public bool IsNeedRecovery(IDomainEvent evnt) + public bool IsNeedRecovery(IDomainEvent domainEvent) { - return DateTimeOffset.UtcNow - evnt.Timestamp > DelayToNeedRecover; + return DateTimeOffset.UtcNow - domainEvent.Timestamp > DelayToNeedRecover; } } } \ No newline at end of file diff --git a/Source/EventFlow/PublishRecovery/VerificationState.cs b/Source/EventFlow/PublishRecovery/VerificationState.cs new file mode 100644 index 000000000..b3e93e32e --- /dev/null +++ b/Source/EventFlow/PublishRecovery/VerificationState.cs @@ -0,0 +1,41 @@ +// The MIT License (MIT) +// +// Copyright (c) 2015-2018 Rasmus Mikkelsen +// Copyright (c) 2015-2018 eBay Software Foundation +// https://github.com/eventflow/EventFlow +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +using System.Collections.Generic; +using EventFlow.EventStores; + +namespace EventFlow.PublishRecovery +{ + public class VerificationState + { + public VerificationState(GlobalPosition position, IReadOnlyCollection items) + { + Position = position; + Items = items; + } + + public GlobalPosition Position { get; } + + public IReadOnlyCollection Items { get; } + } +} \ No newline at end of file From f0a36a2a4aba489a52d5008b8837b450ade14e27 Mon Sep 17 00:00:00 2001 From: Maxim Shoshin Date: Wed, 10 Jun 2020 18:46:46 +0300 Subject: [PATCH 4/9] Introducing IRecoveryHandlers --- .../IntegrationTests/CrashResilienceTests.cs | 52 ++++++----- .../AggregateReadStoreManagerTests.cs | 6 +- ...FlowOptionsReliablePublishingExtensions.cs | 11 ++- ...ryProcessor.cs => ApplyRecoveryHandler.cs} | 23 ++--- .../IReadModelRecoveryHandler.cs | 36 ++++++++ .../PublishRecovery/IRecoveryHandler.cs | 40 ++++++++ .../ReadModelRecoveryHandler.cs | 44 +++++++++ .../RecoveryHandlerProcessor.cs | 78 ++++++++++++++++ .../ReadStores/ReadModelEventHelper.cs | 91 +++++++++++++++++++ .../EventFlow/ReadStores/ReadStoreManager.cs | 34 +------ 10 files changed, 343 insertions(+), 72 deletions(-) rename Source/EventFlow/PublishRecovery/{PublishRecoveryProcessor.cs => ApplyRecoveryHandler.cs} (62%) create mode 100644 Source/EventFlow/PublishRecovery/IReadModelRecoveryHandler.cs create mode 100644 Source/EventFlow/PublishRecovery/IRecoveryHandler.cs create mode 100644 Source/EventFlow/PublishRecovery/ReadModelRecoveryHandler.cs create mode 100644 Source/EventFlow/PublishRecovery/RecoveryHandlerProcessor.cs create mode 100644 Source/EventFlow/ReadStores/ReadModelEventHelper.cs diff --git a/Source/EventFlow.MsSql.Tests/IntegrationTests/CrashResilienceTests.cs b/Source/EventFlow.MsSql.Tests/IntegrationTests/CrashResilienceTests.cs index e52270173..07affc027 100644 --- a/Source/EventFlow.MsSql.Tests/IntegrationTests/CrashResilienceTests.cs +++ b/Source/EventFlow.MsSql.Tests/IntegrationTests/CrashResilienceTests.cs @@ -23,7 +23,6 @@ using System; using System.Collections.Generic; -using System.Linq; using System.Threading; using System.Threading.Tasks; using EventFlow.Aggregates; @@ -36,7 +35,6 @@ using EventFlow.Subscribers; using EventFlow.TestHelpers; using EventFlow.TestHelpers.Aggregates; -using EventFlow.TestHelpers.Aggregates.Events; using EventFlow.TestHelpers.MsSql; using FluentAssertions; using NUnit.Framework; @@ -49,6 +47,7 @@ public sealed class CrashResilienceTests : IntegrationTest private IMsSqlDatabase _testDatabase; private TestPublisher _publisher; private PublishVerificator _publishVerificator; + private RecoveryHandlerForTest _recoveryHandler; protected override IRootResolver CreateRootResolver(IEventFlowOptions eventFlowOptions) { @@ -59,6 +58,7 @@ protected override IRootResolver CreateRootResolver(IEventFlowOptions eventFlowO var resolver = eventFlowOptions .ConfigureMsSql(MsSqlConfiguration.New.SetConnectionString(_testDatabase.ConnectionString.Value)) .UseMssqlReliablePublishing() + .UseRecoveryHandler(Lifetime.Singleton) .RegisterServices(sr => sr.Decorate( (r, dea) => _publisher ?? (_publisher = new TestPublisher(dea)))) @@ -71,6 +71,7 @@ protected override IRootResolver CreateRootResolver(IEventFlowOptions eventFlowO _publisher = (TestPublisher)resolver.Resolve(); _publishVerificator = (PublishVerificator)resolver.Resolve(); + _recoveryHandler = (RecoveryHandlerForTest)resolver.Resolve(); return resolver; } @@ -94,7 +95,8 @@ public async Task ShouldRecoverAfterFailure() await Verify().ConfigureAwait(false); // Assert - _publisher.PublishedEvents.Should().NotBeEmpty(); + _recoveryHandler.RecoveredEvents.Should() + .BeEquivalentTo(_publisher.NotPublishedEvents); } [Test] @@ -114,36 +116,23 @@ public async Task ShouldRetryVerification() } [Test] - public async Task ShouldRecoveredOnceForSimultaniousVerification() + public async Task ShouldNotRecoverAfterFailureWithoutVerificator() { // Arrange var id = ThingyId.New; _publisher.SimulatePublishFailure = true; - await PublishPingCommandsAsync(id, 1).ConfigureAwait(false); // Act - var tasks = Enumerable.Range(0, 10).Select(x => Verify()); - - _publisher.SimulatePublishFailure = false; - - await Task.WhenAll(tasks).ConfigureAwait(false); + await PublishPingCommandsAsync(id, 1).ConfigureAwait(false); // Assert - _publisher.PublishedEvents.Should().ContainSingle(x => x.GetAggregateEvent() is ThingyPingEvent); + _recoveryHandler.RecoveredEvents.Should().BeEmpty(); } [Test] - public async Task ShouldNotRecoverAfterFailureWithoutVerificator() + public void ShouldRemoveOutdatedLogItems() { - // Arrange - var id = ThingyId.New; - _publisher.SimulatePublishFailure = true; - - // Act - await PublishPingCommandsAsync(id, 1).ConfigureAwait(false); - - // Assert - _publisher.PublishedEvents.Should().BeEmpty(); + throw new NotImplementedException(); } private async Task Verify() @@ -155,6 +144,24 @@ private async Task Verify() } while (result != PublishVerificationResult.CompletedNoMoreDataToVerify); } + private class RecoveryHandlerForTest : IRecoveryHandler + { + private readonly List _recoveredEvents = new List(); + public IReadOnlyList RecoveredEvents => _recoveredEvents; + + public bool CanProcess(IDomainEvent domainEvent) + { + return true; + } + + public Task RecoverFromShutdownAsync(IReadOnlyCollection domainEvents, CancellationToken cancellationToken) + { + _recoveredEvents.AddRange(domainEvents); + + return Task.FromResult(0); + } + } + private class TestPublisher : IDomainEventPublisher { private readonly IDomainEventPublisher _inner; @@ -170,6 +177,8 @@ public TestPublisher(IDomainEventPublisher inner) public IReadOnlyList PublishedEvents => _publishedEvents; + public IReadOnlyList NotPublishedEvents => _notPublishedEvents; + public async Task PublishAsync(IReadOnlyCollection domainEvents, CancellationToken cancellationToken) { if (SimulatePublishFailure) @@ -180,7 +189,6 @@ public async Task PublishAsync(IReadOnlyCollection domainEvents, C await _inner.PublishAsync(domainEvents, cancellationToken); - _notPublishedEvents.RemoveAll(evnt => domainEvents.Contains(evnt, CompareEventById.Instance)); _publishedEvents.AddRange(domainEvents); } diff --git a/Source/EventFlow.Tests/UnitTests/ReadStores/AggregateReadStoreManagerTests.cs b/Source/EventFlow.Tests/UnitTests/ReadStores/AggregateReadStoreManagerTests.cs index 8fbad2b78..89330b961 100644 --- a/Source/EventFlow.Tests/UnitTests/ReadStores/AggregateReadStoreManagerTests.cs +++ b/Source/EventFlow.Tests/UnitTests/ReadStores/AggregateReadStoreManagerTests.cs @@ -161,7 +161,8 @@ public void ThrowsIfReadModelSubscribesNoEvents() ReadModelWithoutEvents>(null, null, null, null, null); }; - a.Should().Throw().WithInnerException().WithMessage("*does not implement any*"); + a.Should().Throw().WithInnerException() + .WithInnerException().WithMessage("*does not implement any*"); } [Test] @@ -173,7 +174,8 @@ public void ThrowsIfReadModelSubscribesSameEventTwice() ReadModelWithAmbigiousEvents>(null, null, null, null, null); }; - a.Should().Throw().WithInnerException().WithMessage("*implements ambiguous*"); + a.Should().Throw().WithInnerException() + .WithInnerException().WithMessage("*implements ambiguous*"); } private class ReadModelWithoutEvents : IReadModel diff --git a/Source/EventFlow/Extensions/EventFlowOptionsReliablePublishingExtensions.cs b/Source/EventFlow/Extensions/EventFlowOptionsReliablePublishingExtensions.cs index 00c6b0af7..e8cd03938 100644 --- a/Source/EventFlow/Extensions/EventFlowOptionsReliablePublishingExtensions.cs +++ b/Source/EventFlow/Extensions/EventFlowOptionsReliablePublishingExtensions.cs @@ -37,11 +37,20 @@ public static IEventFlowOptions UseReliablePublishing f.Register()) .RegisterServices(f => f.Register()) - .RegisterServices(f => f.Register()) + .RegisterServices(f => f.Register()) .RegisterServices(r => r.Register()) .RegisterServices(f => f.Register(lifetime)) .RegisterServices(f => f.Decorate( (context, inner) => new ReliableDomainEventPublisher(inner, context.Resolver.Resolve()))); } + + public static IEventFlowOptions UseRecoveryHandler( + this IEventFlowOptions eventFlowOptions, + Lifetime lifetime = Lifetime.AlwaysUnique) + where TRecoveryHandler : class, IRecoveryHandler + { + return eventFlowOptions + .RegisterServices(f => f.Register(lifetime)); + } } } \ No newline at end of file diff --git a/Source/EventFlow/PublishRecovery/PublishRecoveryProcessor.cs b/Source/EventFlow/PublishRecovery/ApplyRecoveryHandler.cs similarity index 62% rename from Source/EventFlow/PublishRecovery/PublishRecoveryProcessor.cs rename to Source/EventFlow/PublishRecovery/ApplyRecoveryHandler.cs index b4990f3b0..9ebe26f51 100644 --- a/Source/EventFlow/PublishRecovery/PublishRecoveryProcessor.cs +++ b/Source/EventFlow/PublishRecovery/ApplyRecoveryHandler.cs @@ -22,33 +22,26 @@ // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. using System.Collections.Generic; -using System.Linq; using System.Threading; using System.Threading.Tasks; using EventFlow.Aggregates; -using EventFlow.Subscribers; +using EventFlow.ReadStores; namespace EventFlow.PublishRecovery { - public sealed class PublishRecoveryProcessor : IPublishRecoveryProcessor + public class ApplyRecoveryHandler : ReadModelRecoveryHandler + where TReadModel : class, IReadModel { - private readonly IDomainEventPublisher _domainEventPublisher; + private readonly IReadStoreManager _storeManager; - public PublishRecoveryProcessor(IDomainEventPublisher domainEventPublisher) + public ApplyRecoveryHandler(IReadStoreManager storeManager) { - _domainEventPublisher = domainEventPublisher; + _storeManager = storeManager; } - public async Task RecoverEventsAsync(IReadOnlyList eventsForRecovery, CancellationToken cancellationToken) + public override Task RecoverFromShutdownAsync(IReadOnlyCollection domainEvents, CancellationToken cancellationToken) { - var groupByIdentities = eventsForRecovery.GroupBy(x => x.GetIdentity()); - - foreach (var groupByIdentity in groupByIdentities) - { - // Potentially it is possible to publish events simultaniously, - // but for stability results do it serially - await _domainEventPublisher.PublishAsync(groupByIdentity.ToList(), cancellationToken); - } + return _storeManager.UpdateReadStoresAsync(domainEvents, cancellationToken); } } } \ No newline at end of file diff --git a/Source/EventFlow/PublishRecovery/IReadModelRecoveryHandler.cs b/Source/EventFlow/PublishRecovery/IReadModelRecoveryHandler.cs new file mode 100644 index 000000000..b0ae2127d --- /dev/null +++ b/Source/EventFlow/PublishRecovery/IReadModelRecoveryHandler.cs @@ -0,0 +1,36 @@ +// The MIT License (MIT) +// +// Copyright (c) 2015-2018 Rasmus Mikkelsen +// Copyright (c) 2015-2018 eBay Software Foundation +// https://github.com/eventflow/EventFlow +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +using EventFlow.ReadStores; + +namespace EventFlow.PublishRecovery +{ + public interface IReadModelRecoveryHandler : IRecoveryHandler + { + } + + public interface IReadModelRecoveryHandler : IReadModelRecoveryHandler + where TReadModel : class, IReadModel + { + } +} \ No newline at end of file diff --git a/Source/EventFlow/PublishRecovery/IRecoveryHandler.cs b/Source/EventFlow/PublishRecovery/IRecoveryHandler.cs new file mode 100644 index 000000000..77d8473af --- /dev/null +++ b/Source/EventFlow/PublishRecovery/IRecoveryHandler.cs @@ -0,0 +1,40 @@ +// The MIT License (MIT) +// +// Copyright (c) 2015-2018 Rasmus Mikkelsen +// Copyright (c) 2015-2018 eBay Software Foundation +// https://github.com/eventflow/EventFlow +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using EventFlow.Aggregates; + +namespace EventFlow.PublishRecovery +{ + public interface IRecoveryHandler + { + bool CanProcess(IDomainEvent domainEvent); + + Task RecoverFromShutdownAsync(IReadOnlyCollection domainEvents, CancellationToken cancellationToken); + + // TODO: Uncomment when infrastructure ready + // Task RecoverFromErrorAsync(IReadOnlyCollection domainEvents, Exception exception, CancellationToken cancellationToken); + } +} \ No newline at end of file diff --git a/Source/EventFlow/PublishRecovery/ReadModelRecoveryHandler.cs b/Source/EventFlow/PublishRecovery/ReadModelRecoveryHandler.cs new file mode 100644 index 000000000..99d07df29 --- /dev/null +++ b/Source/EventFlow/PublishRecovery/ReadModelRecoveryHandler.cs @@ -0,0 +1,44 @@ +// The MIT License (MIT) +// +// Copyright (c) 2015-2018 Rasmus Mikkelsen +// Copyright (c) 2015-2018 eBay Software Foundation +// https://github.com/eventflow/EventFlow +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using EventFlow.Aggregates; +using EventFlow.ReadStores; + +namespace EventFlow.PublishRecovery +{ + public abstract class ReadModelRecoveryHandler : IReadModelRecoveryHandler + where TReadModel : class, IReadModel + { + public virtual bool CanProcess(IDomainEvent domainEvent) + { + return ReadModelEventHelper.CanApply(domainEvent); + } + + public abstract Task RecoverFromShutdownAsync( + IReadOnlyCollection domainEvents, + CancellationToken cancellationToken); + } +} \ No newline at end of file diff --git a/Source/EventFlow/PublishRecovery/RecoveryHandlerProcessor.cs b/Source/EventFlow/PublishRecovery/RecoveryHandlerProcessor.cs new file mode 100644 index 000000000..28a2af031 --- /dev/null +++ b/Source/EventFlow/PublishRecovery/RecoveryHandlerProcessor.cs @@ -0,0 +1,78 @@ +// The MIT License (MIT) +// +// Copyright (c) 2015-2018 Rasmus Mikkelsen +// Copyright (c) 2015-2018 eBay Software Foundation +// https://github.com/eventflow/EventFlow +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using EventFlow.Aggregates; +using EventFlow.Configuration; + +namespace EventFlow.PublishRecovery +{ + public sealed class RecoveryHandlerProcessor : IPublishRecoveryProcessor + { + private readonly IReliableMarkProcessor _markProcessor; + private readonly IResolver _resolver; + + public RecoveryHandlerProcessor(IResolver resolver, IReliableMarkProcessor markProcessor) + { + _resolver = resolver; + _markProcessor = markProcessor; + } + + public async Task RecoverEventsAsync(IReadOnlyList eventsForRecovery, CancellationToken cancellationToken) + { + var recoveryHandlers = _resolver.Resolve>(); + + if (!recoveryHandlers.Any()) + { + throw new Exception("No any recovery handlers registered."); + } + + var anyRecovered = false; + + foreach (var handler in recoveryHandlers) + { + var events = eventsForRecovery + .Where(evnt => handler.CanProcess(evnt)) + .ToList(); + + if (events.Any()) + { + anyRecovered = true; + await handler.RecoverFromShutdownAsync(events, cancellationToken).ConfigureAwait(false); + } + } + + if (!anyRecovered) + { + throw new Exception("No events recovered"); + } + + // TODO: Rethink as now we mark as recovered all events even no suitable recovery handler found. + await _markProcessor.MarkEventsPublishedAsync(eventsForRecovery).ConfigureAwait(false); + } + } +} \ No newline at end of file diff --git a/Source/EventFlow/ReadStores/ReadModelEventHelper.cs b/Source/EventFlow/ReadStores/ReadModelEventHelper.cs new file mode 100644 index 000000000..829a5e6ad --- /dev/null +++ b/Source/EventFlow/ReadStores/ReadModelEventHelper.cs @@ -0,0 +1,91 @@ +// The MIT License (MIT) +// +// Copyright (c) 2015-2018 Rasmus Mikkelsen +// Copyright (c) 2015-2018 eBay Software Foundation +// https://github.com/eventflow/EventFlow +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using EventFlow.Aggregates; +using EventFlow.Extensions; + +namespace EventFlow.ReadStores +{ + internal static class ReadModelEventHelper + where TReadModel : class, IReadModel + { + // ReSharper disable StaticMemberInGenericType + private static readonly ISet AggregateEventTypes; + // ReSharper enable StaticMemberInGenericType + + static ReadModelEventHelper() + { + var readModelType = typeof(TReadModel); + + var iAmReadModelForInterfaceTypes = readModelType + .GetTypeInfo() + .GetInterfaces() + .Where(IsReadModelFor) + .ToList(); + if (!iAmReadModelForInterfaceTypes.Any()) + { + throw new ArgumentException( + $"Read model type '{readModelType.PrettyPrint()}' does not implement any '{typeof(IAmReadModelFor<,,>).PrettyPrint()}'"); + } + + AggregateEventTypes = new HashSet(iAmReadModelForInterfaceTypes.Select(i => i.GetTypeInfo().GetGenericArguments()[2])); + if (AggregateEventTypes.Count != iAmReadModelForInterfaceTypes.Count) + { + throw new ArgumentException( + $"Read model type '{readModelType.PrettyPrint()}' implements ambiguous '{typeof(IAmReadModelFor<,,>).PrettyPrint()}' interfaces"); + } + } + + private static bool IsReadModelFor(Type i) + { + if (!i.GetTypeInfo().IsGenericType) + { + return false; + } + + var typeDefinition = i.GetGenericTypeDefinition(); + return typeDefinition == typeof(IAmReadModelFor<,,>) || + typeDefinition == typeof(IAmAsyncReadModelFor<,,>); + } + + public static bool CanApply(IDomainEvent domainEvent) + { + return CanApply(domainEvent.EventType); + } + + public static bool CanApply(Type domainEventType) + { + return AggregateEventTypes.Contains(domainEventType); + } + + // Dummy mehtod, all initialization performed in static constructor + public static void Nop() + { + } + } +} \ No newline at end of file diff --git a/Source/EventFlow/ReadStores/ReadStoreManager.cs b/Source/EventFlow/ReadStores/ReadStoreManager.cs index 4c5a2699b..82fa8064a 100644 --- a/Source/EventFlow/ReadStores/ReadStoreManager.cs +++ b/Source/EventFlow/ReadStores/ReadStoreManager.cs @@ -25,7 +25,6 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Reflection; using System.Threading; using System.Threading.Tasks; using EventFlow.Aggregates; @@ -41,7 +40,6 @@ public abstract class ReadStoreManager : IReadStore { // ReSharper disable StaticMemberInGenericType private static readonly Type StaticReadModelType = typeof(TReadModel); - private static readonly ISet AggregateEventTypes; // ReSharper enable StaticMemberInGenericType protected ILog Log { get; } @@ -54,35 +52,7 @@ public abstract class ReadStoreManager : IReadStore static ReadStoreManager() { - var iAmReadModelForInterfaceTypes = StaticReadModelType - .GetTypeInfo() - .GetInterfaces() - .Where(IsReadModelFor) - .ToList(); - if (!iAmReadModelForInterfaceTypes.Any()) - { - throw new ArgumentException( - $"Read model type '{StaticReadModelType.PrettyPrint()}' does not implement any '{typeof(IAmReadModelFor<,,>).PrettyPrint()}'"); - } - - AggregateEventTypes = new HashSet(iAmReadModelForInterfaceTypes.Select(i => i.GetTypeInfo().GetGenericArguments()[2])); - if (AggregateEventTypes.Count != iAmReadModelForInterfaceTypes.Count) - { - throw new ArgumentException( - $"Read model type '{StaticReadModelType.PrettyPrint()}' implements ambiguous '{typeof(IAmReadModelFor<,,>).PrettyPrint()}' interfaces"); - } - } - - private static bool IsReadModelFor(Type i) - { - if (!i.GetTypeInfo().IsGenericType) - { - return false; - } - - var typeDefinition = i.GetGenericTypeDefinition(); - return typeDefinition == typeof(IAmReadModelFor<,,>) || - typeDefinition == typeof(IAmAsyncReadModelFor<,,>); + ReadModelEventHelper.Nop(); } protected ReadStoreManager( @@ -104,7 +74,7 @@ public async Task UpdateReadStoresAsync( CancellationToken cancellationToken) { var relevantDomainEvents = domainEvents - .Where(e => AggregateEventTypes.Contains(e.EventType)) + .Where(e => ReadModelEventHelper.CanApply(e.EventType)) .ToList(); if (!relevantDomainEvents.Any()) { From 5b58b52ebc1987323561ce7e2b9cc995f74bd9fc Mon Sep 17 00:00:00 2001 From: Maxim Shoshin Date: Thu, 11 Jun 2020 10:22:50 +0300 Subject: [PATCH 5/9] Create explicit method ReadModelEventHelper.CheckReadModel --- .../AggregateReadStoreManagerTests.cs | 6 +- .../ReadStores/ReadModelEventHelper.cs | 58 ++++++++++--------- .../EventFlow/ReadStores/ReadStoreManager.cs | 3 +- 3 files changed, 35 insertions(+), 32 deletions(-) diff --git a/Source/EventFlow.Tests/UnitTests/ReadStores/AggregateReadStoreManagerTests.cs b/Source/EventFlow.Tests/UnitTests/ReadStores/AggregateReadStoreManagerTests.cs index 89330b961..8fbad2b78 100644 --- a/Source/EventFlow.Tests/UnitTests/ReadStores/AggregateReadStoreManagerTests.cs +++ b/Source/EventFlow.Tests/UnitTests/ReadStores/AggregateReadStoreManagerTests.cs @@ -161,8 +161,7 @@ public void ThrowsIfReadModelSubscribesNoEvents() ReadModelWithoutEvents>(null, null, null, null, null); }; - a.Should().Throw().WithInnerException() - .WithInnerException().WithMessage("*does not implement any*"); + a.Should().Throw().WithInnerException().WithMessage("*does not implement any*"); } [Test] @@ -174,8 +173,7 @@ public void ThrowsIfReadModelSubscribesSameEventTwice() ReadModelWithAmbigiousEvents>(null, null, null, null, null); }; - a.Should().Throw().WithInnerException() - .WithInnerException().WithMessage("*implements ambiguous*"); + a.Should().Throw().WithInnerException().WithMessage("*implements ambiguous*"); } private class ReadModelWithoutEvents : IReadModel diff --git a/Source/EventFlow/ReadStores/ReadModelEventHelper.cs b/Source/EventFlow/ReadStores/ReadModelEventHelper.cs index 829a5e6ad..ed21ad6df 100644 --- a/Source/EventFlow/ReadStores/ReadModelEventHelper.cs +++ b/Source/EventFlow/ReadStores/ReadModelEventHelper.cs @@ -20,7 +20,6 @@ // COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -// using System; using System.Collections.Generic; @@ -40,27 +39,47 @@ internal static class ReadModelEventHelper static ReadModelEventHelper() { - var readModelType = typeof(TReadModel); + var iAmReadModelForInterfaceTypes = GetIamReadModelInterfaces(); - var iAmReadModelForInterfaceTypes = readModelType - .GetTypeInfo() - .GetInterfaces() - .Where(IsReadModelFor) - .ToList(); - if (!iAmReadModelForInterfaceTypes.Any()) + AggregateEventTypes = new HashSet(iAmReadModelForInterfaceTypes.Select(i => i.GetTypeInfo().GetGenericArguments()[2])); + } + + public static bool CanApply(IDomainEvent domainEvent) + { + return CanApply(domainEvent.EventType); + } + + public static bool CanApply(Type domainEventType) + { + return AggregateEventTypes.Contains(domainEventType); + } + + public static void CheckReadModel() + { + if (!AggregateEventTypes.Any()) { throw new ArgumentException( - $"Read model type '{readModelType.PrettyPrint()}' does not implement any '{typeof(IAmReadModelFor<,,>).PrettyPrint()}'"); + $"Read model type '{typeof(TReadModel).PrettyPrint()}' does not implement any '{typeof(IAmReadModelFor<,,>).PrettyPrint()}'"); } - AggregateEventTypes = new HashSet(iAmReadModelForInterfaceTypes.Select(i => i.GetTypeInfo().GetGenericArguments()[2])); - if (AggregateEventTypes.Count != iAmReadModelForInterfaceTypes.Count) + if (AggregateEventTypes.Count != GetIamReadModelInterfaces().Count) { throw new ArgumentException( - $"Read model type '{readModelType.PrettyPrint()}' implements ambiguous '{typeof(IAmReadModelFor<,,>).PrettyPrint()}' interfaces"); + $"Read model type '{typeof(TReadModel).PrettyPrint()}' implements ambiguous '{typeof(IAmReadModelFor<,,>).PrettyPrint()}' interfaces"); } } + private static IReadOnlyList GetIamReadModelInterfaces() + { + var readModelType = typeof(TReadModel); + + return readModelType + .GetTypeInfo() + .GetInterfaces() + .Where(IsReadModelFor) + .ToList(); + } + private static bool IsReadModelFor(Type i) { if (!i.GetTypeInfo().IsGenericType) @@ -72,20 +91,5 @@ private static bool IsReadModelFor(Type i) return typeDefinition == typeof(IAmReadModelFor<,,>) || typeDefinition == typeof(IAmAsyncReadModelFor<,,>); } - - public static bool CanApply(IDomainEvent domainEvent) - { - return CanApply(domainEvent.EventType); - } - - public static bool CanApply(Type domainEventType) - { - return AggregateEventTypes.Contains(domainEventType); - } - - // Dummy mehtod, all initialization performed in static constructor - public static void Nop() - { - } } } \ No newline at end of file diff --git a/Source/EventFlow/ReadStores/ReadStoreManager.cs b/Source/EventFlow/ReadStores/ReadStoreManager.cs index 82fa8064a..4847460e1 100644 --- a/Source/EventFlow/ReadStores/ReadStoreManager.cs +++ b/Source/EventFlow/ReadStores/ReadStoreManager.cs @@ -25,6 +25,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; using EventFlow.Aggregates; @@ -52,7 +53,7 @@ public abstract class ReadStoreManager : IReadStore static ReadStoreManager() { - ReadModelEventHelper.Nop(); + ReadModelEventHelper.CheckReadModel(); } protected ReadStoreManager( From 98c882bc576bbdc259396ecc51587e6eb8b22061 Mon Sep 17 00:00:00 2001 From: Maxim Shoshin Date: Mon, 15 Jun 2020 14:37:19 +0300 Subject: [PATCH 6/9] Introduce RecovertyHandlers --- .../IntegrationTests/CrashResilienceTests.cs | 50 ++++++- Source/EventFlow/EventFlowOptions.cs | 4 + ...FlowOptionsReliablePublishingExtensions.cs | 7 +- .../IReadModelRecoveryHandler.cs | 33 ++++- ...andler.cs => IRecoveryHandlerProcessor.cs} | 38 ++++- ...ecoveryHandler.cs => NoRecoveryHandler.cs} | 16 +- ...ocessor.cs => NopReliableMarkProcessor.cs} | 8 +- .../PublishRecovery/PublishVerificator.cs | 8 +- .../ReadModelRecoveryHandler.cs | 35 ++++- .../RecoveryHandlerProcessor.cs | 139 +++++++++++++++--- Source/EventFlow/Sagas/DispatchToSagas.cs | 21 ++- .../Subscribers/DispatchToEventSubscribers.cs | 30 +++- .../Subscribers/DomainEventPublisher.cs | 80 +++++++++- 13 files changed, 395 insertions(+), 74 deletions(-) rename Source/EventFlow/PublishRecovery/{IRecoveryHandler.cs => IRecoveryHandlerProcessor.cs} (50%) rename Source/EventFlow/PublishRecovery/{ApplyRecoveryHandler.cs => NoRecoveryHandler.cs} (68%) rename Source/EventFlow/PublishRecovery/{IPublishRecoveryProcessor.cs => NopReliableMarkProcessor.cs} (85%) diff --git a/Source/EventFlow.MsSql.Tests/IntegrationTests/CrashResilienceTests.cs b/Source/EventFlow.MsSql.Tests/IntegrationTests/CrashResilienceTests.cs index 07affc027..37db681bd 100644 --- a/Source/EventFlow.MsSql.Tests/IntegrationTests/CrashResilienceTests.cs +++ b/Source/EventFlow.MsSql.Tests/IntegrationTests/CrashResilienceTests.cs @@ -32,6 +32,8 @@ using EventFlow.MsSql.Extensions; using EventFlow.MsSql.ReliablePublish; using EventFlow.PublishRecovery; +using EventFlow.ReadStores; +using EventFlow.Sagas; using EventFlow.Subscribers; using EventFlow.TestHelpers; using EventFlow.TestHelpers.Aggregates; @@ -58,7 +60,7 @@ protected override IRootResolver CreateRootResolver(IEventFlowOptions eventFlowO var resolver = eventFlowOptions .ConfigureMsSql(MsSqlConfiguration.New.SetConnectionString(_testDatabase.ConnectionString.Value)) .UseMssqlReliablePublishing() - .UseRecoveryHandler(Lifetime.Singleton) + .RegisterServices(sr => sr.Register(Lifetime.Singleton)) .RegisterServices(sr => sr.Decorate( (r, dea) => _publisher ?? (_publisher = new TestPublisher(dea)))) @@ -71,7 +73,7 @@ protected override IRootResolver CreateRootResolver(IEventFlowOptions eventFlowO _publisher = (TestPublisher)resolver.Resolve(); _publishVerificator = (PublishVerificator)resolver.Resolve(); - _recoveryHandler = (RecoveryHandlerForTest)resolver.Resolve(); + _recoveryHandler = (RecoveryHandlerForTest)resolver.Resolve(); return resolver; } @@ -144,21 +146,53 @@ private async Task Verify() } while (result != PublishVerificationResult.CompletedNoMoreDataToVerify); } - private class RecoveryHandlerForTest : IRecoveryHandler + private class RecoveryHandlerForTest : IRecoveryHandlerProcessor { private readonly List _recoveredEvents = new List(); + private readonly IReliableMarkProcessor _markProcessor; + + public RecoveryHandlerForTest(IReliableMarkProcessor markProcessor) + { + _markProcessor = markProcessor; + } + public IReadOnlyList RecoveredEvents => _recoveredEvents; - public bool CanProcess(IDomainEvent domainEvent) + public Task RecoverAfterUnexpectedShutdownAsync(IReadOnlyList eventsForRecovery, CancellationToken cancellationToken) { - return true; + _recoveredEvents.AddRange(eventsForRecovery); + + return _markProcessor.MarkEventsPublishedAsync(eventsForRecovery); } - public Task RecoverFromShutdownAsync(IReadOnlyCollection domainEvents, CancellationToken cancellationToken) + public Task RecoverReadModelUpdateErrorAsync(IReadStoreManager readModelType, IReadOnlyCollection eventsForRecovery, + Exception exception, CancellationToken cancellationToken) { - _recoveredEvents.AddRange(domainEvents); + throw new NotImplementedException(); + } - return Task.FromResult(0); + public Task RecoverAllSubscriberErrorAsync(IReadOnlyCollection eventsForRecovery, Exception exception, + CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + + public Task RecoverSubscriberErrorAsync(object subscriber, IDomainEvent eventForRecovery, Exception exception, + CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + + public Task RecoverScheduleSubscriberErrorAsync(IReadOnlyCollection eventsForRecovery, Exception exception, + CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + + public Task RecoverSagaErrorAsync(ISagaId eventsForRecovery, SagaDetails exception, IDomainEvent cancellationToken, + Exception exception1, CancellationToken cancellationToken1) + { + throw new NotImplementedException(); } } diff --git a/Source/EventFlow/EventFlowOptions.cs b/Source/EventFlow/EventFlowOptions.cs index 5ecfb33df..cb57c8d15 100644 --- a/Source/EventFlow/EventFlowOptions.cs +++ b/Source/EventFlow/EventFlowOptions.cs @@ -40,6 +40,7 @@ using EventFlow.Jobs; using EventFlow.Logs; using EventFlow.Provided; +using EventFlow.PublishRecovery; using EventFlow.Queries; using EventFlow.ReadStores; using EventFlow.Sagas; @@ -216,6 +217,9 @@ private void RegisterDefaults(IServiceRegistration serviceRegistration) serviceRegistration.Register(); serviceRegistration.Register(); serviceRegistration.Register(); + serviceRegistration.Register(); + serviceRegistration.Register(); + serviceRegistration.Register(); serviceRegistration.Register(); serviceRegistration.Register(Lifetime.Singleton); serviceRegistration.Register(); diff --git a/Source/EventFlow/Extensions/EventFlowOptionsReliablePublishingExtensions.cs b/Source/EventFlow/Extensions/EventFlowOptionsReliablePublishingExtensions.cs index e8cd03938..4b6d42dd0 100644 --- a/Source/EventFlow/Extensions/EventFlowOptionsReliablePublishingExtensions.cs +++ b/Source/EventFlow/Extensions/EventFlowOptionsReliablePublishingExtensions.cs @@ -37,20 +37,19 @@ public static IEventFlowOptions UseReliablePublishing f.Register()) .RegisterServices(f => f.Register()) - .RegisterServices(f => f.Register()) .RegisterServices(r => r.Register()) .RegisterServices(f => f.Register(lifetime)) .RegisterServices(f => f.Decorate( (context, inner) => new ReliableDomainEventPublisher(inner, context.Resolver.Resolve()))); } - public static IEventFlowOptions UseRecoveryHandler( + public static IEventFlowOptions UseReadModelRecoveryHandler( this IEventFlowOptions eventFlowOptions, Lifetime lifetime = Lifetime.AlwaysUnique) - where TRecoveryHandler : class, IRecoveryHandler + where TRecoveryHandler : class, IReadModelRecoveryHandler { return eventFlowOptions - .RegisterServices(f => f.Register(lifetime)); + .RegisterServices(f => f.Register(lifetime)); } } } \ No newline at end of file diff --git a/Source/EventFlow/PublishRecovery/IReadModelRecoveryHandler.cs b/Source/EventFlow/PublishRecovery/IReadModelRecoveryHandler.cs index b0ae2127d..9c16cf87e 100644 --- a/Source/EventFlow/PublishRecovery/IReadModelRecoveryHandler.cs +++ b/Source/EventFlow/PublishRecovery/IReadModelRecoveryHandler.cs @@ -21,16 +21,39 @@ // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using EventFlow.Aggregates; using EventFlow.ReadStores; namespace EventFlow.PublishRecovery { - public interface IReadModelRecoveryHandler : IRecoveryHandler - { - } + public delegate Task NextRecoverShutdownHandlerAsync( + IReadStoreManager readStoreManager, + IReadOnlyCollection domainEvents, + CancellationToken cancellationToken); - public interface IReadModelRecoveryHandler : IReadModelRecoveryHandler - where TReadModel : class, IReadModel + public delegate Task NextRecoverErrorHandlerAsync( + IReadStoreManager readStoreManager, + IReadOnlyCollection domainEvents, + Exception exception, + CancellationToken cancellationToken); + + public interface IReadModelRecoveryHandler { + Task RecoverFromShutdownAsync( + IReadStoreManager readStoreManager, + IReadOnlyCollection eventsForRecovery, + NextRecoverShutdownHandlerAsync nextHandler, + CancellationToken cancellationToken); + + Task RecoverFromErrorAsync( + IReadStoreManager readStoreManager, + IReadOnlyCollection eventsForRecovery, + Exception exception, + NextRecoverErrorHandlerAsync nextHandler, + CancellationToken cancellationToken); } } \ No newline at end of file diff --git a/Source/EventFlow/PublishRecovery/IRecoveryHandler.cs b/Source/EventFlow/PublishRecovery/IRecoveryHandlerProcessor.cs similarity index 50% rename from Source/EventFlow/PublishRecovery/IRecoveryHandler.cs rename to Source/EventFlow/PublishRecovery/IRecoveryHandlerProcessor.cs index 77d8473af..a15341d06 100644 --- a/Source/EventFlow/PublishRecovery/IRecoveryHandler.cs +++ b/Source/EventFlow/PublishRecovery/IRecoveryHandlerProcessor.cs @@ -21,20 +21,48 @@ // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using EventFlow.Aggregates; +using EventFlow.ReadStores; +using EventFlow.Sagas; namespace EventFlow.PublishRecovery { - public interface IRecoveryHandler + public interface IRecoveryHandlerProcessor { - bool CanProcess(IDomainEvent domainEvent); + Task RecoverAfterUnexpectedShutdownAsync( + IReadOnlyList eventsForRecovery, + CancellationToken cancellationToken); - Task RecoverFromShutdownAsync(IReadOnlyCollection domainEvents, CancellationToken cancellationToken); + Task RecoverReadModelUpdateErrorAsync(IReadStoreManager readModelType, + IReadOnlyCollection eventsForRecovery, + Exception exception, + CancellationToken cancellationToken); - // TODO: Uncomment when infrastructure ready - // Task RecoverFromErrorAsync(IReadOnlyCollection domainEvents, Exception exception, CancellationToken cancellationToken); + Task RecoverAllSubscriberErrorAsync( + IReadOnlyCollection eventsForRecovery, + Exception exception, + CancellationToken cancellationToken); + + Task RecoverSubscriberErrorAsync( + object subscriber, + IDomainEvent eventForRecovery, + Exception exception, + CancellationToken cancellationToken); + + Task RecoverScheduleSubscriberErrorAsync( + IReadOnlyCollection eventsForRecovery, + Exception exception, + CancellationToken cancellationToken); + + Task RecoverSagaErrorAsync( + ISagaId eventsForRecovery, + SagaDetails exception, + IDomainEvent cancellationToken, + Exception exception1, + CancellationToken cancellationToken1); } } \ No newline at end of file diff --git a/Source/EventFlow/PublishRecovery/ApplyRecoveryHandler.cs b/Source/EventFlow/PublishRecovery/NoRecoveryHandler.cs similarity index 68% rename from Source/EventFlow/PublishRecovery/ApplyRecoveryHandler.cs rename to Source/EventFlow/PublishRecovery/NoRecoveryHandler.cs index 9ebe26f51..04425b6d4 100644 --- a/Source/EventFlow/PublishRecovery/ApplyRecoveryHandler.cs +++ b/Source/EventFlow/PublishRecovery/NoRecoveryHandler.cs @@ -21,6 +21,7 @@ // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; @@ -29,19 +30,18 @@ namespace EventFlow.PublishRecovery { - public class ApplyRecoveryHandler : ReadModelRecoveryHandler - where TReadModel : class, IReadModel + public sealed class NoRecoveryHandler : IReadModelRecoveryHandler { - private readonly IReadStoreManager _storeManager; - - public ApplyRecoveryHandler(IReadStoreManager storeManager) + public Task RecoverFromShutdownAsync(IReadStoreManager readStoreManager, IReadOnlyCollection eventsForRecovery, + NextRecoverShutdownHandlerAsync nextHandler, CancellationToken cancellationToken) { - _storeManager = storeManager; + throw new NotSupportedException("Unable to recover after shutdown."); } - public override Task RecoverFromShutdownAsync(IReadOnlyCollection domainEvents, CancellationToken cancellationToken) + public Task RecoverFromErrorAsync(IReadStoreManager readStoreManager, IReadOnlyCollection eventsForRecovery, + Exception exception, NextRecoverErrorHandlerAsync nextHandler, CancellationToken cancellationToken) { - return _storeManager.UpdateReadStoresAsync(domainEvents, cancellationToken); + return Task.FromResult(false); } } } \ No newline at end of file diff --git a/Source/EventFlow/PublishRecovery/IPublishRecoveryProcessor.cs b/Source/EventFlow/PublishRecovery/NopReliableMarkProcessor.cs similarity index 85% rename from Source/EventFlow/PublishRecovery/IPublishRecoveryProcessor.cs rename to Source/EventFlow/PublishRecovery/NopReliableMarkProcessor.cs index 0bac92de7..b24525734 100644 --- a/Source/EventFlow/PublishRecovery/IPublishRecoveryProcessor.cs +++ b/Source/EventFlow/PublishRecovery/NopReliableMarkProcessor.cs @@ -22,14 +22,16 @@ // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. using System.Collections.Generic; -using System.Threading; using System.Threading.Tasks; using EventFlow.Aggregates; namespace EventFlow.PublishRecovery { - public interface IPublishRecoveryProcessor + public sealed class NopReliableMarkProcessor : IReliableMarkProcessor { - Task RecoverEventsAsync(IReadOnlyList eventsForRecovery, CancellationToken cancellationToken); + public Task MarkEventsPublishedAsync(IReadOnlyCollection domainEvents) + { + return Task.FromResult(0); + } } } \ No newline at end of file diff --git a/Source/EventFlow/PublishRecovery/PublishVerificator.cs b/Source/EventFlow/PublishRecovery/PublishVerificator.cs index c890bc9a0..f7f0b7d62 100644 --- a/Source/EventFlow/PublishRecovery/PublishVerificator.cs +++ b/Source/EventFlow/PublishRecovery/PublishVerificator.cs @@ -35,15 +35,15 @@ public sealed class PublishVerificator : IPublishVerificator private const int PageSize = 200; private readonly IEventPersistence _eventPersistence; - private readonly IPublishRecoveryProcessor _publishRecoveryProcessor; + private readonly IRecoveryHandlerProcessor _recoveryHandlerProcessor; private readonly IEventJsonSerializer _eventSerializer; private readonly IRecoveryDetector _recoveryDetector; private readonly IReliablePublishPersistence _reliablePublishPersistence; - public PublishVerificator(IEventPersistence eventPersistence, IPublishRecoveryProcessor publishRecoveryProcessor, IEventJsonSerializer eventSerializer, IRecoveryDetector recoveryDetector, IReliablePublishPersistence reliablePublishPersistence) + public PublishVerificator(IEventPersistence eventPersistence, IRecoveryHandlerProcessor recoveryHandlerProcessor, IEventJsonSerializer eventSerializer, IRecoveryDetector recoveryDetector, IReliablePublishPersistence reliablePublishPersistence) { _eventPersistence = eventPersistence; - _publishRecoveryProcessor = publishRecoveryProcessor; + _recoveryHandlerProcessor = recoveryHandlerProcessor; _eventSerializer = eventSerializer; _recoveryDetector = recoveryDetector; _reliablePublishPersistence = reliablePublishPersistence; @@ -69,7 +69,7 @@ public async Task VerifyOnceAsync(CancellationToken c { // Do it inside transaction to recover in single thread // success recovery should put LogItem - await _publishRecoveryProcessor.RecoverEventsAsync(eventsForRecovery, cancellationToken) + await _recoveryHandlerProcessor.RecoverAfterUnexpectedShutdownAsync(eventsForRecovery, cancellationToken) .ConfigureAwait(false); return PublishVerificationResult.RecoveredNeedVerify; diff --git a/Source/EventFlow/PublishRecovery/ReadModelRecoveryHandler.cs b/Source/EventFlow/PublishRecovery/ReadModelRecoveryHandler.cs index 99d07df29..4a0e59f8b 100644 --- a/Source/EventFlow/PublishRecovery/ReadModelRecoveryHandler.cs +++ b/Source/EventFlow/PublishRecovery/ReadModelRecoveryHandler.cs @@ -21,6 +21,7 @@ // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; @@ -29,16 +30,42 @@ namespace EventFlow.PublishRecovery { - public abstract class ReadModelRecoveryHandler : IReadModelRecoveryHandler + public abstract class ReadModelRecoveryHandler : IReadModelRecoveryHandler where TReadModel : class, IReadModel { - public virtual bool CanProcess(IDomainEvent domainEvent) + public Task RecoverFromShutdownAsync(IReadStoreManager readStoreManager, IReadOnlyCollection eventsForRecovery, + NextRecoverShutdownHandlerAsync nextHandler, CancellationToken cancellationToken) { - return ReadModelEventHelper.CanApply(domainEvent); + if (readStoreManager.ReadModelType == typeof(TReadModel)) + { + return InternalRecoverFromShutdownAsync(readStoreManager, eventsForRecovery, nextHandler, cancellationToken); + } + + return nextHandler(readStoreManager, eventsForRecovery, cancellationToken); } - public abstract Task RecoverFromShutdownAsync( + public Task RecoverFromErrorAsync(IReadStoreManager readStoreManager, IReadOnlyCollection eventsForRecovery, + Exception exception, NextRecoverErrorHandlerAsync nextHandler, CancellationToken cancellationToken) + { + if (readStoreManager.ReadModelType == typeof(TReadModel)) + { + return InternalRecoverFromErrorAsync(readStoreManager, eventsForRecovery, exception, nextHandler, cancellationToken); + } + + return nextHandler(readStoreManager, eventsForRecovery, exception, cancellationToken); + } + + protected abstract Task InternalRecoverFromShutdownAsync( + IReadStoreManager readStoreManager, IReadOnlyCollection domainEvents, + NextRecoverShutdownHandlerAsync nextHandler, + CancellationToken cancellationToken); + + protected abstract Task InternalRecoverFromErrorAsync( + IReadStoreManager readStoreManager, + IReadOnlyCollection eventsForRecovery, + Exception exception, + NextRecoverErrorHandlerAsync nextHandler, CancellationToken cancellationToken); } } \ No newline at end of file diff --git a/Source/EventFlow/PublishRecovery/RecoveryHandlerProcessor.cs b/Source/EventFlow/PublishRecovery/RecoveryHandlerProcessor.cs index 28a2af031..081ea15dd 100644 --- a/Source/EventFlow/PublishRecovery/RecoveryHandlerProcessor.cs +++ b/Source/EventFlow/PublishRecovery/RecoveryHandlerProcessor.cs @@ -28,51 +28,152 @@ using System.Threading.Tasks; using EventFlow.Aggregates; using EventFlow.Configuration; +using EventFlow.ReadStores; +using EventFlow.Sagas; namespace EventFlow.PublishRecovery { - public sealed class RecoveryHandlerProcessor : IPublishRecoveryProcessor + public sealed class RecoveryHandlerProcessor : IRecoveryHandlerProcessor { private readonly IReliableMarkProcessor _markProcessor; private readonly IResolver _resolver; + private readonly IReadOnlyCollection _readStoreManagers; - public RecoveryHandlerProcessor(IResolver resolver, IReliableMarkProcessor markProcessor) + public RecoveryHandlerProcessor( + IResolver resolver, + IReliableMarkProcessor markProcessor, + IEnumerable readStoreManagers) { _resolver = resolver; _markProcessor = markProcessor; + _readStoreManagers = readStoreManagers.ToList(); } - public async Task RecoverEventsAsync(IReadOnlyList eventsForRecovery, CancellationToken cancellationToken) + public async Task RecoverAfterUnexpectedShutdownAsync(IReadOnlyList eventsForRecovery, CancellationToken cancellationToken) { - var recoveryHandlers = _resolver.Resolve>(); + cancellationToken.ThrowIfCancellationRequested(); - if (!recoveryHandlers.Any()) + foreach (var readStoreManager in _readStoreManagers) { - throw new Exception("No any recovery handlers registered."); + await RecoverReadModelUpdateAfterShutdownAsync(readStoreManager, eventsForRecovery, cancellationToken) + .ConfigureAwait(false); } - var anyRecovered = false; + cancellationToken.ThrowIfCancellationRequested(); - foreach (var handler in recoveryHandlers) + await _markProcessor.MarkEventsPublishedAsync(eventsForRecovery).ConfigureAwait(false); + } + + public Task RecoverReadModelUpdateErrorAsync( + IReadStoreManager readModelType, + IReadOnlyCollection eventsForRecovery, + Exception exception, + CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + { + return Task.FromResult(false); + } + + var recoveryHandlers = _resolver.Resolve>().ToList(); + + var wrapper = new MiddlewareWrapper(recoveryHandlers); + + return wrapper.RecoverFromErrorAsync(readModelType, eventsForRecovery, exception, cancellationToken); + } + + public Task RecoverAllSubscriberErrorAsync(IReadOnlyCollection eventsForRecovery, Exception exception, + CancellationToken cancellationToken) + { + // TODO: Implement + return Task.FromResult(false); + } + + public Task RecoverSubscriberErrorAsync(object subscriber, IDomainEvent eventForRecovery, Exception exception, + CancellationToken cancellationToken) + { + // TODO: Implement + return Task.FromResult(false); + } + + public Task RecoverScheduleSubscriberErrorAsync(IReadOnlyCollection eventsForRecovery, Exception exception, + CancellationToken cancellationToken) + { + // TODO: Implement + return Task.FromResult(false); + } + + public Task RecoverSagaErrorAsync(ISagaId eventsForRecovery, SagaDetails exception, IDomainEvent cancellationToken, + Exception exception1, CancellationToken cancellationToken1) + { + // TODO: Implement + return Task.FromResult(false); + } + + private Task RecoverReadModelUpdateAfterShutdownAsync( + IReadStoreManager readModelType, + IReadOnlyCollection eventsForRecovery, + CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) { - var events = eventsForRecovery - .Where(evnt => handler.CanProcess(evnt)) - .ToList(); + return Task.FromResult(false); + } + + var recoveryHandlers = _resolver.Resolve>().ToList(); + + var wrapper = new MiddlewareWrapper(recoveryHandlers); - if (events.Any()) + return wrapper.RecoverFromShutdownAsync(readModelType, eventsForRecovery, cancellationToken); + } + + private sealed class MiddlewareWrapper + { + private readonly IReadOnlyList _handlers; + private readonly int _position; + + public MiddlewareWrapper(IReadOnlyList handlers, int position = 0) + { + _handlers = handlers; + _position = position; + } + + public Task RecoverFromShutdownAsync( + IReadStoreManager readStoreManager, + IReadOnlyCollection domainEvents, + CancellationToken cancellationToken) + { + if (_handlers.Count >= _position) { - anyRecovered = true; - await handler.RecoverFromShutdownAsync(events, cancellationToken).ConfigureAwait(false); + return Task.FromResult(false); } + + return _handlers[_position].RecoverFromShutdownAsync( + readStoreManager, + domainEvents, + new MiddlewareWrapper(_handlers, _position + 1).RecoverFromShutdownAsync, + cancellationToken); } - if (!anyRecovered) + + public Task RecoverFromErrorAsync( + IReadStoreManager readStoreManager, + IReadOnlyCollection eventsForRecovery, + Exception exception, + CancellationToken cancellationToken) { - throw new Exception("No events recovered"); - } + if (_handlers.Count >= _position) + { + return Task.FromResult(false); + } - // TODO: Rethink as now we mark as recovered all events even no suitable recovery handler found. - await _markProcessor.MarkEventsPublishedAsync(eventsForRecovery).ConfigureAwait(false); + return _handlers[_position].RecoverFromErrorAsync( + readStoreManager, + eventsForRecovery, + exception, + new MiddlewareWrapper(_handlers, _position + 1).RecoverFromErrorAsync, + cancellationToken); + } } } } \ No newline at end of file diff --git a/Source/EventFlow/Sagas/DispatchToSagas.cs b/Source/EventFlow/Sagas/DispatchToSagas.cs index f8928779c..4a601eab2 100644 --- a/Source/EventFlow/Sagas/DispatchToSagas.cs +++ b/Source/EventFlow/Sagas/DispatchToSagas.cs @@ -30,6 +30,7 @@ using EventFlow.Configuration; using EventFlow.Extensions; using EventFlow.Logs; +using EventFlow.PublishRecovery; namespace EventFlow.Sagas { @@ -40,19 +41,22 @@ public class DispatchToSagas : IDispatchToSagas private readonly ISagaStore _sagaStore; private readonly ISagaDefinitionService _sagaDefinitionService; private readonly ISagaErrorHandler _sagaErrorHandler; + private readonly IRecoveryHandlerProcessor _recoveryHandlerProcessor; public DispatchToSagas( ILog log, IResolver resolver, ISagaStore sagaStore, ISagaDefinitionService sagaDefinitionService, - ISagaErrorHandler sagaErrorHandler) + ISagaErrorHandler sagaErrorHandler, + IRecoveryHandlerProcessor recoveryHandlerProcessor) { _log = log; _resolver = resolver; _sagaStore = sagaStore; _sagaDefinitionService = sagaDefinitionService; _sagaErrorHandler = sagaErrorHandler; + _recoveryHandlerProcessor = recoveryHandlerProcessor; } public async Task ProcessAsync( @@ -111,7 +115,20 @@ await _sagaStore.UpdateAsync( } catch (Exception e) { - var handled = await _sagaErrorHandler.HandleAsync( + var handled = await _recoveryHandlerProcessor.RecoverSagaErrorAsync( + sagaId, + details, + domainEvent, + e, + cancellationToken) + .ConfigureAwait(false); + + if (handled) + { + return; + } + + handled = await _sagaErrorHandler.HandleAsync( sagaId, details, e, diff --git a/Source/EventFlow/Subscribers/DispatchToEventSubscribers.cs b/Source/EventFlow/Subscribers/DispatchToEventSubscribers.cs index 4ab22ba28..a24da23c1 100644 --- a/Source/EventFlow/Subscribers/DispatchToEventSubscribers.cs +++ b/Source/EventFlow/Subscribers/DispatchToEventSubscribers.cs @@ -33,6 +33,7 @@ using EventFlow.Core.Caching; using EventFlow.Extensions; using EventFlow.Logs; +using EventFlow.PublishRecovery; namespace EventFlow.Subscribers { @@ -45,6 +46,7 @@ public class DispatchToEventSubscribers : IDispatchToEventSubscribers private readonly IResolver _resolver; private readonly IEventFlowConfiguration _eventFlowConfiguration; private readonly IMemoryCache _memoryCache; + private readonly IRecoveryHandlerProcessor _recoveryHandlerProcessor; private class SubscriberInfomation { @@ -56,12 +58,14 @@ public DispatchToEventSubscribers( ILog log, IResolver resolver, IEventFlowConfiguration eventFlowConfiguration, - IMemoryCache memoryCache) + IMemoryCache memoryCache, + IRecoveryHandlerProcessor recoveryHandlerProcessor) { _log = log; _resolver = resolver; _eventFlowConfiguration = eventFlowConfiguration; _memoryCache = memoryCache; + _recoveryHandlerProcessor = recoveryHandlerProcessor; } public async Task DispatchToSynchronousSubscribersAsync( @@ -114,10 +118,28 @@ private async Task DispatchToSubscribersAsync( { await subscriberInfomation.HandleMethod(subscriber, domainEvent, cancellationToken).ConfigureAwait(false); } - catch (Exception e) when (swallowException) + catch (Exception e) { - _log.Error(e, $"Subscriber '{subscriberInfomation.SubscriberType.PrettyPrint()}' threw " + - $"'{e.GetType().PrettyPrint()}' while handling '{domainEvent.EventType.PrettyPrint()}': {e.Message}"); + var handled = await _recoveryHandlerProcessor.RecoverSubscriberErrorAsync( + subscriber, + domainEvent, + e, + cancellationToken) + .ConfigureAwait(false); + + if (handled) + { + continue; + } + + if (swallowException) + { + _log.Error(e, $"Subscriber '{subscriberInfomation.SubscriberType.PrettyPrint()}' threw " + + $"'{e.GetType().PrettyPrint()}' while handling '{domainEvent.EventType.PrettyPrint()}': {e.Message}"); + continue; + } + + throw; } } } diff --git a/Source/EventFlow/Subscribers/DomainEventPublisher.cs b/Source/EventFlow/Subscribers/DomainEventPublisher.cs index 2ae5f4b03..07d01e961 100644 --- a/Source/EventFlow/Subscribers/DomainEventPublisher.cs +++ b/Source/EventFlow/Subscribers/DomainEventPublisher.cs @@ -21,6 +21,7 @@ // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +using System; using System.Collections.Generic; using System.Linq; using System.Threading; @@ -31,6 +32,7 @@ using EventFlow.Core; using EventFlow.Jobs; using EventFlow.Provided.Jobs; +using EventFlow.PublishRecovery; using EventFlow.ReadStores; using EventFlow.Sagas; @@ -46,6 +48,7 @@ public class DomainEventPublisher : IDomainEventPublisher private readonly ICancellationConfiguration _cancellationConfiguration; private readonly IReadOnlyCollection _subscribeSynchronousToAlls; private readonly IReadOnlyCollection _readStoreManagers; + private readonly IRecoveryHandlerProcessor _recoveryHandlerProcessor; public DomainEventPublisher( IDispatchToEventSubscribers dispatchToEventSubscribers, @@ -55,7 +58,8 @@ public DomainEventPublisher( IEventFlowConfiguration eventFlowConfiguration, IEnumerable readStoreManagers, IEnumerable subscribeSynchronousToAlls, - ICancellationConfiguration cancellationConfiguration) + ICancellationConfiguration cancellationConfiguration, + IRecoveryHandlerProcessor recoveryHandlerProcessor) { _dispatchToEventSubscribers = dispatchToEventSubscribers; _dispatchToSagas = dispatchToSagas; @@ -63,6 +67,7 @@ public DomainEventPublisher( _resolver = resolver; _eventFlowConfiguration = eventFlowConfiguration; _cancellationConfiguration = cancellationConfiguration; + _recoveryHandlerProcessor = recoveryHandlerProcessor; _subscribeSynchronousToAlls = subscribeSynchronousToAlls.ToList(); _readStoreManagers = readStoreManagers.ToList(); } @@ -101,7 +106,26 @@ private async Task PublishToReadStoresAsync( CancellationToken cancellationToken) { var updateReadStoresTasks = _readStoreManagers - .Select(rsm => rsm.UpdateReadStoresAsync(domainEvents, cancellationToken)); + .Select(async rsm => + { + try + { + await rsm.UpdateReadStoresAsync(domainEvents, cancellationToken).ConfigureAwait(false); + } + catch (Exception ex) + { + var handled = await _recoveryHandlerProcessor + .RecoverReadModelUpdateErrorAsync(rsm, domainEvents, ex, cancellationToken) + .ConfigureAwait(false); + + if (handled) + { + return; + } + + throw; + } + }); await Task.WhenAll(updateReadStoresTasks).ConfigureAwait(false); } @@ -110,7 +134,28 @@ private async Task PublishToSubscribersOfAllEventsAsync( CancellationToken cancellationToken) { var handle = _subscribeSynchronousToAlls - .Select(s => s.HandleAsync(domainEvents, cancellationToken)); + .Select(async s => + { + try + { + await s.HandleAsync(domainEvents, cancellationToken).ConfigureAwait(false); + } + catch (Exception ex) + { + var handled = await _recoveryHandlerProcessor.RecoverAllSubscriberErrorAsync( + domainEvents, + ex, + cancellationToken) + .ConfigureAwait(false); + + if (handled) + { + return; + } + + throw; + } + }); await Task.WhenAll(handle).ConfigureAwait(false); } @@ -122,15 +167,34 @@ private async Task PublishToSynchronousSubscribersAsync( } private async Task PublishToAsynchronousSubscribersAsync( - IEnumerable domainEvents, + IReadOnlyCollection domainEvents, CancellationToken cancellationToken) { if (_eventFlowConfiguration.IsAsynchronousSubscribersEnabled) { - await Task.WhenAll(domainEvents.Select( - d => _jobScheduler.ScheduleNowAsync( - DispatchToAsynchronousEventSubscribersJob.Create(d, _resolver), cancellationToken))) - .ConfigureAwait(false); + try + { + await Task.WhenAll(domainEvents.Select( + d => _jobScheduler.ScheduleNowAsync( + DispatchToAsynchronousEventSubscribersJob.Create(d, _resolver), + cancellationToken))) + .ConfigureAwait(false); + } + catch (Exception ex) + { + var handled = await _recoveryHandlerProcessor.RecoverScheduleSubscriberErrorAsync( + domainEvents, + ex, + cancellationToken) + .ConfigureAwait(false); + + if (handled) + { + return; + } + + throw; + } } } From 24e4e339c300d3accefe1d8ff9bf689a1c57b7d6 Mon Sep 17 00:00:00 2001 From: Maxim Shoshin Date: Thu, 18 Jun 2020 17:09:36 +0300 Subject: [PATCH 7/9] Redesign to use IReadModelRecoveryHandler --- .../IntegrationTests/CrashResilienceTests.cs | 63 +------- .../ReadStores/ReadModelRecoveryTests.cs | 81 +++++++++++ Source/EventFlow/EventFlowOptions.cs | 1 - ...FlowOptionsReliablePublishingExtensions.cs | 25 +++- .../IReadModelRecoveryHandler.cs | 20 +-- .../IRecoveryHandlerProcessor.cs | 31 ---- .../ReadModelRecoveryHandler.cs | 71 --------- ...s => ReadStoreManagerWithErrorRecovery.cs} | 31 +++- .../RecoveryHandlerProcessor.cs | 137 ++---------------- .../ReadStores/ReadModelEventHelper.cs | 95 ------------ .../EventFlow/ReadStores/ReadStoreManager.cs | 35 ++++- Source/EventFlow/Sagas/DispatchToSagas.cs | 21 +-- .../Subscribers/DispatchToEventSubscribers.cs | 18 +-- .../Subscribers/DomainEventPublisher.cs | 78 +--------- 14 files changed, 190 insertions(+), 517 deletions(-) create mode 100644 Source/EventFlow.Tests/IntegrationTests/ReadStores/ReadModelRecoveryTests.cs delete mode 100644 Source/EventFlow/PublishRecovery/ReadModelRecoveryHandler.cs rename Source/EventFlow/PublishRecovery/{NoRecoveryHandler.cs => ReadStoreManagerWithErrorRecovery.cs} (57%) delete mode 100644 Source/EventFlow/ReadStores/ReadModelEventHelper.cs diff --git a/Source/EventFlow.MsSql.Tests/IntegrationTests/CrashResilienceTests.cs b/Source/EventFlow.MsSql.Tests/IntegrationTests/CrashResilienceTests.cs index 37db681bd..843b380f9 100644 --- a/Source/EventFlow.MsSql.Tests/IntegrationTests/CrashResilienceTests.cs +++ b/Source/EventFlow.MsSql.Tests/IntegrationTests/CrashResilienceTests.cs @@ -32,8 +32,6 @@ using EventFlow.MsSql.Extensions; using EventFlow.MsSql.ReliablePublish; using EventFlow.PublishRecovery; -using EventFlow.ReadStores; -using EventFlow.Sagas; using EventFlow.Subscribers; using EventFlow.TestHelpers; using EventFlow.TestHelpers.Aggregates; @@ -131,12 +129,6 @@ public async Task ShouldNotRecoverAfterFailureWithoutVerificator() _recoveryHandler.RecoveredEvents.Should().BeEmpty(); } - [Test] - public void ShouldRemoveOutdatedLogItems() - { - throw new NotImplementedException(); - } - private async Task Verify() { PublishVerificationResult result; @@ -164,36 +156,6 @@ public Task RecoverAfterUnexpectedShutdownAsync(IReadOnlyList even return _markProcessor.MarkEventsPublishedAsync(eventsForRecovery); } - - public Task RecoverReadModelUpdateErrorAsync(IReadStoreManager readModelType, IReadOnlyCollection eventsForRecovery, - Exception exception, CancellationToken cancellationToken) - { - throw new NotImplementedException(); - } - - public Task RecoverAllSubscriberErrorAsync(IReadOnlyCollection eventsForRecovery, Exception exception, - CancellationToken cancellationToken) - { - throw new NotImplementedException(); - } - - public Task RecoverSubscriberErrorAsync(object subscriber, IDomainEvent eventForRecovery, Exception exception, - CancellationToken cancellationToken) - { - throw new NotImplementedException(); - } - - public Task RecoverScheduleSubscriberErrorAsync(IReadOnlyCollection eventsForRecovery, Exception exception, - CancellationToken cancellationToken) - { - throw new NotImplementedException(); - } - - public Task RecoverSagaErrorAsync(ISagaId eventsForRecovery, SagaDetails exception, IDomainEvent cancellationToken, - Exception exception1, CancellationToken cancellationToken1) - { - throw new NotImplementedException(); - } } private class TestPublisher : IDomainEventPublisher @@ -213,7 +175,8 @@ public TestPublisher(IDomainEventPublisher inner) public IReadOnlyList NotPublishedEvents => _notPublishedEvents; - public async Task PublishAsync(IReadOnlyCollection domainEvents, CancellationToken cancellationToken) + public async Task PublishAsync(IReadOnlyCollection domainEvents, + CancellationToken cancellationToken) { if (SimulatePublishFailure) { @@ -227,27 +190,13 @@ public async Task PublishAsync(IReadOnlyCollection domainEvents, C } [Obsolete("Use PublishAsync (without generics and aggregate identity)")] - public Task PublishAsync(TIdentity id, IReadOnlyCollection domainEvents, - CancellationToken cancellationToken) where TAggregate : IAggregateRoot where TIdentity : IIdentity + public Task PublishAsync(TIdentity id, + IReadOnlyCollection domainEvents, + CancellationToken cancellationToken) where TAggregate : IAggregateRoot + where TIdentity : IIdentity { return _inner.PublishAsync(id, domainEvents, cancellationToken); } - - private sealed class CompareEventById : IEqualityComparer - { - public static CompareEventById Instance { get; } = new CompareEventById(); - - public bool Equals(IDomainEvent x, IDomainEvent y) - { - return Equals(x.GetIdentity(), y.GetIdentity()) && - x.AggregateSequenceNumber == y.AggregateSequenceNumber; - } - - public int GetHashCode(IDomainEvent obj) - { - return HashHelper.Combine(obj.AggregateSequenceNumber, obj.GetIdentity().GetHashCode()); - } - } } private sealed class AlwaysRecoverDetector : IRecoveryDetector diff --git a/Source/EventFlow.Tests/IntegrationTests/ReadStores/ReadModelRecoveryTests.cs b/Source/EventFlow.Tests/IntegrationTests/ReadStores/ReadModelRecoveryTests.cs new file mode 100644 index 000000000..d2addc246 --- /dev/null +++ b/Source/EventFlow.Tests/IntegrationTests/ReadStores/ReadModelRecoveryTests.cs @@ -0,0 +1,81 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using EventFlow.Aggregates; +using EventFlow.Configuration; +using EventFlow.Extensions; +using EventFlow.PublishRecovery; +using EventFlow.ReadStores; +using EventFlow.TestHelpers; +using EventFlow.TestHelpers.Aggregates; +using EventFlow.TestHelpers.Aggregates.Events; +using FluentAssertions; +using NUnit.Framework; + +namespace EventFlow.Tests.IntegrationTests.ReadStores +{ + public sealed class ReadModelRecoveryTests : IntegrationTest + { + protected override IRootResolver CreateRootResolver(IEventFlowOptions eventFlowOptions) + { + return eventFlowOptions + .UseInMemoryReadStoreFor() + .UseReadModelRecoveryHandler(Lifetime.Singleton) + .CreateResolver(); + } + + [Test] + public async Task ShouldRecoveryForExceptionInReadModel() + { + var recoveryHandler = (TestRecoveryHandler)Resolver.Resolve>(); + recoveryHandler.ShouldRecover = true; + + await PublishPingCommandAsync(ThingyId.New); + + recoveryHandler.LastRecoveredEvents.Should() + .ContainSingle(x => x.GetAggregateEvent() is ThingyPingEvent); + } + + [Test] + public async Task ShouldThrowOriginalErrorWhenNoRecovery() + { + var recoveryHandler = (TestRecoveryHandler)Resolver.Resolve>(); + recoveryHandler.ShouldRecover = false; + + Func publishPing = () => PublishPingCommandAsync(ThingyId.New); + + (await publishPing.Should().ThrowAsync().ConfigureAwait(false)) + .WithMessage("Read model exception. Should be recovered."); + } + + private sealed class FailingReadModel : IReadModel, + IAmReadModelFor + { + public void Apply(IReadModelContext context, IDomainEvent domainEvent) + { + throw new Exception("Read model exception. Should be recovered."); + } + } + + private sealed class TestRecoveryHandler : IReadModelRecoveryHandler + { + public IReadOnlyCollection LastRecoveredEvents { get; private set; } + + public bool ShouldRecover { get; set; } + + public Task RecoverFromShutdownAsync(IReadOnlyCollection eventsForRecovery, CancellationToken cancellationToken) + { + return Task.FromResult(0); + } + + public Task RecoverFromErrorAsync(IReadOnlyCollection eventsForRecovery, Exception exception, + CancellationToken cancellationToken) + { + LastRecoveredEvents = eventsForRecovery; + + return Task.FromResult(ShouldRecover); + } + } + } +} \ No newline at end of file diff --git a/Source/EventFlow/EventFlowOptions.cs b/Source/EventFlow/EventFlowOptions.cs index cb57c8d15..8afcb220d 100644 --- a/Source/EventFlow/EventFlowOptions.cs +++ b/Source/EventFlow/EventFlowOptions.cs @@ -219,7 +219,6 @@ private void RegisterDefaults(IServiceRegistration serviceRegistration) serviceRegistration.Register(); serviceRegistration.Register(); serviceRegistration.Register(); - serviceRegistration.Register(); serviceRegistration.Register(); serviceRegistration.Register(Lifetime.Singleton); serviceRegistration.Register(); diff --git a/Source/EventFlow/Extensions/EventFlowOptionsReliablePublishingExtensions.cs b/Source/EventFlow/Extensions/EventFlowOptionsReliablePublishingExtensions.cs index 4b6d42dd0..4d2601cdb 100644 --- a/Source/EventFlow/Extensions/EventFlowOptionsReliablePublishingExtensions.cs +++ b/Source/EventFlow/Extensions/EventFlowOptionsReliablePublishingExtensions.cs @@ -23,6 +23,7 @@ using EventFlow.Configuration; using EventFlow.PublishRecovery; +using EventFlow.ReadStores; using EventFlow.Subscribers; namespace EventFlow.Extensions @@ -43,13 +44,31 @@ public static IEventFlowOptions UseReliablePublishing new ReliableDomainEventPublisher(inner, context.Resolver.Resolve()))); } - public static IEventFlowOptions UseReadModelRecoveryHandler( + public static IEventFlowOptions UseReadModelRecoveryHandler( this IEventFlowOptions eventFlowOptions, Lifetime lifetime = Lifetime.AlwaysUnique) - where TRecoveryHandler : class, IReadModelRecoveryHandler + where TRecoveryHandler : class, IReadModelRecoveryHandler + where TReadModel : class, IReadModel { return eventFlowOptions - .RegisterServices(f => f.Register(lifetime)); + .RegisterServices(f => + { + f.Register, TRecoveryHandler>(lifetime); + + f.Register(ctx => (IReadModelRecoveryHandler)ctx.Resolver.Resolve>()); + + f.Decorate((ctx, inner) => + { + if (inner.ReadModelType == typeof(TReadModel)) + { + return new ReadStoreManagerWithErrorRecovery( + (IReadStoreManager)inner, + ctx.Resolver.Resolve>()); + } + + return inner; + }); + }); } } } \ No newline at end of file diff --git a/Source/EventFlow/PublishRecovery/IReadModelRecoveryHandler.cs b/Source/EventFlow/PublishRecovery/IReadModelRecoveryHandler.cs index 9c16cf87e..edfac1548 100644 --- a/Source/EventFlow/PublishRecovery/IReadModelRecoveryHandler.cs +++ b/Source/EventFlow/PublishRecovery/IReadModelRecoveryHandler.cs @@ -30,30 +30,20 @@ namespace EventFlow.PublishRecovery { - public delegate Task NextRecoverShutdownHandlerAsync( - IReadStoreManager readStoreManager, - IReadOnlyCollection domainEvents, - CancellationToken cancellationToken); - - public delegate Task NextRecoverErrorHandlerAsync( - IReadStoreManager readStoreManager, - IReadOnlyCollection domainEvents, - Exception exception, - CancellationToken cancellationToken); - public interface IReadModelRecoveryHandler { Task RecoverFromShutdownAsync( - IReadStoreManager readStoreManager, IReadOnlyCollection eventsForRecovery, - NextRecoverShutdownHandlerAsync nextHandler, CancellationToken cancellationToken); Task RecoverFromErrorAsync( - IReadStoreManager readStoreManager, IReadOnlyCollection eventsForRecovery, Exception exception, - NextRecoverErrorHandlerAsync nextHandler, CancellationToken cancellationToken); } + + public interface IReadModelRecoveryHandler : IReadModelRecoveryHandler + where TReadModel : class, IReadModel + { + } } \ No newline at end of file diff --git a/Source/EventFlow/PublishRecovery/IRecoveryHandlerProcessor.cs b/Source/EventFlow/PublishRecovery/IRecoveryHandlerProcessor.cs index a15341d06..83bb72bc5 100644 --- a/Source/EventFlow/PublishRecovery/IRecoveryHandlerProcessor.cs +++ b/Source/EventFlow/PublishRecovery/IRecoveryHandlerProcessor.cs @@ -21,13 +21,10 @@ // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using EventFlow.Aggregates; -using EventFlow.ReadStores; -using EventFlow.Sagas; namespace EventFlow.PublishRecovery { @@ -36,33 +33,5 @@ public interface IRecoveryHandlerProcessor Task RecoverAfterUnexpectedShutdownAsync( IReadOnlyList eventsForRecovery, CancellationToken cancellationToken); - - Task RecoverReadModelUpdateErrorAsync(IReadStoreManager readModelType, - IReadOnlyCollection eventsForRecovery, - Exception exception, - CancellationToken cancellationToken); - - Task RecoverAllSubscriberErrorAsync( - IReadOnlyCollection eventsForRecovery, - Exception exception, - CancellationToken cancellationToken); - - Task RecoverSubscriberErrorAsync( - object subscriber, - IDomainEvent eventForRecovery, - Exception exception, - CancellationToken cancellationToken); - - Task RecoverScheduleSubscriberErrorAsync( - IReadOnlyCollection eventsForRecovery, - Exception exception, - CancellationToken cancellationToken); - - Task RecoverSagaErrorAsync( - ISagaId eventsForRecovery, - SagaDetails exception, - IDomainEvent cancellationToken, - Exception exception1, - CancellationToken cancellationToken1); } } \ No newline at end of file diff --git a/Source/EventFlow/PublishRecovery/ReadModelRecoveryHandler.cs b/Source/EventFlow/PublishRecovery/ReadModelRecoveryHandler.cs deleted file mode 100644 index 4a0e59f8b..000000000 --- a/Source/EventFlow/PublishRecovery/ReadModelRecoveryHandler.cs +++ /dev/null @@ -1,71 +0,0 @@ -// The MIT License (MIT) -// -// Copyright (c) 2015-2018 Rasmus Mikkelsen -// Copyright (c) 2015-2018 eBay Software Foundation -// https://github.com/eventflow/EventFlow -// -// Permission is hereby granted, free of charge, to any person obtaining a copy of -// this software and associated documentation files (the "Software"), to deal in -// the Software without restriction, including without limitation the rights to -// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of -// the Software, and to permit persons to whom the Software is furnished to do so, -// subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS -// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR -// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER -// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -using System; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using EventFlow.Aggregates; -using EventFlow.ReadStores; - -namespace EventFlow.PublishRecovery -{ - public abstract class ReadModelRecoveryHandler : IReadModelRecoveryHandler - where TReadModel : class, IReadModel - { - public Task RecoverFromShutdownAsync(IReadStoreManager readStoreManager, IReadOnlyCollection eventsForRecovery, - NextRecoverShutdownHandlerAsync nextHandler, CancellationToken cancellationToken) - { - if (readStoreManager.ReadModelType == typeof(TReadModel)) - { - return InternalRecoverFromShutdownAsync(readStoreManager, eventsForRecovery, nextHandler, cancellationToken); - } - - return nextHandler(readStoreManager, eventsForRecovery, cancellationToken); - } - - public Task RecoverFromErrorAsync(IReadStoreManager readStoreManager, IReadOnlyCollection eventsForRecovery, - Exception exception, NextRecoverErrorHandlerAsync nextHandler, CancellationToken cancellationToken) - { - if (readStoreManager.ReadModelType == typeof(TReadModel)) - { - return InternalRecoverFromErrorAsync(readStoreManager, eventsForRecovery, exception, nextHandler, cancellationToken); - } - - return nextHandler(readStoreManager, eventsForRecovery, exception, cancellationToken); - } - - protected abstract Task InternalRecoverFromShutdownAsync( - IReadStoreManager readStoreManager, - IReadOnlyCollection domainEvents, - NextRecoverShutdownHandlerAsync nextHandler, - CancellationToken cancellationToken); - - protected abstract Task InternalRecoverFromErrorAsync( - IReadStoreManager readStoreManager, - IReadOnlyCollection eventsForRecovery, - Exception exception, - NextRecoverErrorHandlerAsync nextHandler, - CancellationToken cancellationToken); - } -} \ No newline at end of file diff --git a/Source/EventFlow/PublishRecovery/NoRecoveryHandler.cs b/Source/EventFlow/PublishRecovery/ReadStoreManagerWithErrorRecovery.cs similarity index 57% rename from Source/EventFlow/PublishRecovery/NoRecoveryHandler.cs rename to Source/EventFlow/PublishRecovery/ReadStoreManagerWithErrorRecovery.cs index 04425b6d4..6387a0cc9 100644 --- a/Source/EventFlow/PublishRecovery/NoRecoveryHandler.cs +++ b/Source/EventFlow/PublishRecovery/ReadStoreManagerWithErrorRecovery.cs @@ -30,18 +30,35 @@ namespace EventFlow.PublishRecovery { - public sealed class NoRecoveryHandler : IReadModelRecoveryHandler + public sealed class ReadStoreManagerWithErrorRecovery : IReadStoreManager + where TReadModel : class, IReadModel { - public Task RecoverFromShutdownAsync(IReadStoreManager readStoreManager, IReadOnlyCollection eventsForRecovery, - NextRecoverShutdownHandlerAsync nextHandler, CancellationToken cancellationToken) + private readonly IReadStoreManager _original; + private readonly IReadModelRecoveryHandler _recoveryHandler; + + public ReadStoreManagerWithErrorRecovery(IReadStoreManager original, IReadModelRecoveryHandler recoveryHandler) { - throw new NotSupportedException("Unable to recover after shutdown."); + _original = original; + _recoveryHandler = recoveryHandler; } - public Task RecoverFromErrorAsync(IReadStoreManager readStoreManager, IReadOnlyCollection eventsForRecovery, - Exception exception, NextRecoverErrorHandlerAsync nextHandler, CancellationToken cancellationToken) + public Type ReadModelType => _original.ReadModelType; + + public async Task UpdateReadStoresAsync(IReadOnlyCollection domainEvents, CancellationToken cancellationToken) { - return Task.FromResult(false); + try + { + await _original.UpdateReadStoresAsync(domainEvents, cancellationToken); + } + catch (Exception ex) + { + var handled = await _recoveryHandler.RecoverFromErrorAsync(domainEvents, ex, cancellationToken); + + if (!handled) + { + throw; + } + } } } } \ No newline at end of file diff --git a/Source/EventFlow/PublishRecovery/RecoveryHandlerProcessor.cs b/Source/EventFlow/PublishRecovery/RecoveryHandlerProcessor.cs index 081ea15dd..38a5e5e5d 100644 --- a/Source/EventFlow/PublishRecovery/RecoveryHandlerProcessor.cs +++ b/Source/EventFlow/PublishRecovery/RecoveryHandlerProcessor.cs @@ -21,159 +21,42 @@ // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -using System; using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; using EventFlow.Aggregates; -using EventFlow.Configuration; -using EventFlow.ReadStores; -using EventFlow.Sagas; namespace EventFlow.PublishRecovery { public sealed class RecoveryHandlerProcessor : IRecoveryHandlerProcessor { private readonly IReliableMarkProcessor _markProcessor; - private readonly IResolver _resolver; - private readonly IReadOnlyCollection _readStoreManagers; + private readonly IReadOnlyList _readModelRecoveryHandlers; public RecoveryHandlerProcessor( - IResolver resolver, IReliableMarkProcessor markProcessor, - IEnumerable readStoreManagers) + IEnumerable readModelRecoveryHandlers) { - _resolver = resolver; _markProcessor = markProcessor; - _readStoreManagers = readStoreManagers.ToList(); + _readModelRecoveryHandlers = readModelRecoveryHandlers.ToList(); } public async Task RecoverAfterUnexpectedShutdownAsync(IReadOnlyList eventsForRecovery, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); - foreach (var readStoreManager in _readStoreManagers) - { - await RecoverReadModelUpdateAfterShutdownAsync(readStoreManager, eventsForRecovery, cancellationToken) - .ConfigureAwait(false); - } + var recoveryTasks = _readModelRecoveryHandlers.Select( + handler => handler.RecoverFromShutdownAsync(eventsForRecovery, cancellationToken)); - cancellationToken.ThrowIfCancellationRequested(); - - await _markProcessor.MarkEventsPublishedAsync(eventsForRecovery).ConfigureAwait(false); - } - - public Task RecoverReadModelUpdateErrorAsync( - IReadStoreManager readModelType, - IReadOnlyCollection eventsForRecovery, - Exception exception, - CancellationToken cancellationToken) - { - if (cancellationToken.IsCancellationRequested) - { - return Task.FromResult(false); - } - - var recoveryHandlers = _resolver.Resolve>().ToList(); - - var wrapper = new MiddlewareWrapper(recoveryHandlers); - - return wrapper.RecoverFromErrorAsync(readModelType, eventsForRecovery, exception, cancellationToken); - } + await Task.WhenAll(recoveryTasks) + .ConfigureAwait(false); - public Task RecoverAllSubscriberErrorAsync(IReadOnlyCollection eventsForRecovery, Exception exception, - CancellationToken cancellationToken) - { - // TODO: Implement - return Task.FromResult(false); - } - - public Task RecoverSubscriberErrorAsync(object subscriber, IDomainEvent eventForRecovery, Exception exception, - CancellationToken cancellationToken) - { - // TODO: Implement - return Task.FromResult(false); - } - - public Task RecoverScheduleSubscriberErrorAsync(IReadOnlyCollection eventsForRecovery, Exception exception, - CancellationToken cancellationToken) - { - // TODO: Implement - return Task.FromResult(false); - } + // TODO: Recover Subscribers, Sagas - public Task RecoverSagaErrorAsync(ISagaId eventsForRecovery, SagaDetails exception, IDomainEvent cancellationToken, - Exception exception1, CancellationToken cancellationToken1) - { - // TODO: Implement - return Task.FromResult(false); - } - - private Task RecoverReadModelUpdateAfterShutdownAsync( - IReadStoreManager readModelType, - IReadOnlyCollection eventsForRecovery, - CancellationToken cancellationToken) - { - if (cancellationToken.IsCancellationRequested) - { - return Task.FromResult(false); - } - - var recoveryHandlers = _resolver.Resolve>().ToList(); - - var wrapper = new MiddlewareWrapper(recoveryHandlers); - - return wrapper.RecoverFromShutdownAsync(readModelType, eventsForRecovery, cancellationToken); - } - - private sealed class MiddlewareWrapper - { - private readonly IReadOnlyList _handlers; - private readonly int _position; - - public MiddlewareWrapper(IReadOnlyList handlers, int position = 0) - { - _handlers = handlers; - _position = position; - } - - public Task RecoverFromShutdownAsync( - IReadStoreManager readStoreManager, - IReadOnlyCollection domainEvents, - CancellationToken cancellationToken) - { - if (_handlers.Count >= _position) - { - return Task.FromResult(false); - } - - return _handlers[_position].RecoverFromShutdownAsync( - readStoreManager, - domainEvents, - new MiddlewareWrapper(_handlers, _position + 1).RecoverFromShutdownAsync, - cancellationToken); - } - - - public Task RecoverFromErrorAsync( - IReadStoreManager readStoreManager, - IReadOnlyCollection eventsForRecovery, - Exception exception, - CancellationToken cancellationToken) - { - if (_handlers.Count >= _position) - { - return Task.FromResult(false); - } + cancellationToken.ThrowIfCancellationRequested(); - return _handlers[_position].RecoverFromErrorAsync( - readStoreManager, - eventsForRecovery, - exception, - new MiddlewareWrapper(_handlers, _position + 1).RecoverFromErrorAsync, - cancellationToken); - } + await _markProcessor.MarkEventsPublishedAsync(eventsForRecovery).ConfigureAwait(false); } } } \ No newline at end of file diff --git a/Source/EventFlow/ReadStores/ReadModelEventHelper.cs b/Source/EventFlow/ReadStores/ReadModelEventHelper.cs deleted file mode 100644 index ed21ad6df..000000000 --- a/Source/EventFlow/ReadStores/ReadModelEventHelper.cs +++ /dev/null @@ -1,95 +0,0 @@ -// The MIT License (MIT) -// -// Copyright (c) 2015-2018 Rasmus Mikkelsen -// Copyright (c) 2015-2018 eBay Software Foundation -// https://github.com/eventflow/EventFlow -// -// Permission is hereby granted, free of charge, to any person obtaining a copy of -// this software and associated documentation files (the "Software"), to deal in -// the Software without restriction, including without limitation the rights to -// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of -// the Software, and to permit persons to whom the Software is furnished to do so, -// subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS -// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR -// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER -// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using EventFlow.Aggregates; -using EventFlow.Extensions; - -namespace EventFlow.ReadStores -{ - internal static class ReadModelEventHelper - where TReadModel : class, IReadModel - { - // ReSharper disable StaticMemberInGenericType - private static readonly ISet AggregateEventTypes; - // ReSharper enable StaticMemberInGenericType - - static ReadModelEventHelper() - { - var iAmReadModelForInterfaceTypes = GetIamReadModelInterfaces(); - - AggregateEventTypes = new HashSet(iAmReadModelForInterfaceTypes.Select(i => i.GetTypeInfo().GetGenericArguments()[2])); - } - - public static bool CanApply(IDomainEvent domainEvent) - { - return CanApply(domainEvent.EventType); - } - - public static bool CanApply(Type domainEventType) - { - return AggregateEventTypes.Contains(domainEventType); - } - - public static void CheckReadModel() - { - if (!AggregateEventTypes.Any()) - { - throw new ArgumentException( - $"Read model type '{typeof(TReadModel).PrettyPrint()}' does not implement any '{typeof(IAmReadModelFor<,,>).PrettyPrint()}'"); - } - - if (AggregateEventTypes.Count != GetIamReadModelInterfaces().Count) - { - throw new ArgumentException( - $"Read model type '{typeof(TReadModel).PrettyPrint()}' implements ambiguous '{typeof(IAmReadModelFor<,,>).PrettyPrint()}' interfaces"); - } - } - - private static IReadOnlyList GetIamReadModelInterfaces() - { - var readModelType = typeof(TReadModel); - - return readModelType - .GetTypeInfo() - .GetInterfaces() - .Where(IsReadModelFor) - .ToList(); - } - - private static bool IsReadModelFor(Type i) - { - if (!i.GetTypeInfo().IsGenericType) - { - return false; - } - - var typeDefinition = i.GetGenericTypeDefinition(); - return typeDefinition == typeof(IAmReadModelFor<,,>) || - typeDefinition == typeof(IAmAsyncReadModelFor<,,>); - } - } -} \ No newline at end of file diff --git a/Source/EventFlow/ReadStores/ReadStoreManager.cs b/Source/EventFlow/ReadStores/ReadStoreManager.cs index 4847460e1..c90cc10f5 100644 --- a/Source/EventFlow/ReadStores/ReadStoreManager.cs +++ b/Source/EventFlow/ReadStores/ReadStoreManager.cs @@ -25,7 +25,7 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Runtime.CompilerServices; +using System.Reflection; using System.Threading; using System.Threading.Tasks; using EventFlow.Aggregates; @@ -41,6 +41,7 @@ public abstract class ReadStoreManager : IReadStore { // ReSharper disable StaticMemberInGenericType private static readonly Type StaticReadModelType = typeof(TReadModel); + private static readonly ISet AggregateEventTypes; // ReSharper enable StaticMemberInGenericType protected ILog Log { get; } @@ -53,7 +54,35 @@ public abstract class ReadStoreManager : IReadStore static ReadStoreManager() { - ReadModelEventHelper.CheckReadModel(); + var iAmReadModelForInterfaceTypes = StaticReadModelType + .GetTypeInfo() + .GetInterfaces() + .Where(IsReadModelFor) + .ToList(); + if (!iAmReadModelForInterfaceTypes.Any()) + { + throw new ArgumentException( + $"Read model type '{StaticReadModelType.PrettyPrint()}' does not implement any '{typeof(IAmReadModelFor<,,>).PrettyPrint()}'"); + } + + AggregateEventTypes = new HashSet(iAmReadModelForInterfaceTypes.Select(i => i.GetTypeInfo().GetGenericArguments()[2])); + if (AggregateEventTypes.Count != iAmReadModelForInterfaceTypes.Count) + { + throw new ArgumentException( + $"Read model type '{StaticReadModelType.PrettyPrint()}' implements ambiguous '{typeof(IAmReadModelFor<,,>).PrettyPrint()}' interfaces"); + } + } + + private static bool IsReadModelFor(Type i) + { + if (!i.GetTypeInfo().IsGenericType) + { + return false; + } + + var typeDefinition = i.GetGenericTypeDefinition(); + return typeDefinition == typeof(IAmReadModelFor<,,>) || + typeDefinition == typeof(IAmAsyncReadModelFor<,,>); } protected ReadStoreManager( @@ -75,7 +104,7 @@ public async Task UpdateReadStoresAsync( CancellationToken cancellationToken) { var relevantDomainEvents = domainEvents - .Where(e => ReadModelEventHelper.CanApply(e.EventType)) + .Where(e => AggregateEventTypes.Contains(e.EventType)) .ToList(); if (!relevantDomainEvents.Any()) { diff --git a/Source/EventFlow/Sagas/DispatchToSagas.cs b/Source/EventFlow/Sagas/DispatchToSagas.cs index 4a601eab2..f8928779c 100644 --- a/Source/EventFlow/Sagas/DispatchToSagas.cs +++ b/Source/EventFlow/Sagas/DispatchToSagas.cs @@ -30,7 +30,6 @@ using EventFlow.Configuration; using EventFlow.Extensions; using EventFlow.Logs; -using EventFlow.PublishRecovery; namespace EventFlow.Sagas { @@ -41,22 +40,19 @@ public class DispatchToSagas : IDispatchToSagas private readonly ISagaStore _sagaStore; private readonly ISagaDefinitionService _sagaDefinitionService; private readonly ISagaErrorHandler _sagaErrorHandler; - private readonly IRecoveryHandlerProcessor _recoveryHandlerProcessor; public DispatchToSagas( ILog log, IResolver resolver, ISagaStore sagaStore, ISagaDefinitionService sagaDefinitionService, - ISagaErrorHandler sagaErrorHandler, - IRecoveryHandlerProcessor recoveryHandlerProcessor) + ISagaErrorHandler sagaErrorHandler) { _log = log; _resolver = resolver; _sagaStore = sagaStore; _sagaDefinitionService = sagaDefinitionService; _sagaErrorHandler = sagaErrorHandler; - _recoveryHandlerProcessor = recoveryHandlerProcessor; } public async Task ProcessAsync( @@ -115,20 +111,7 @@ await _sagaStore.UpdateAsync( } catch (Exception e) { - var handled = await _recoveryHandlerProcessor.RecoverSagaErrorAsync( - sagaId, - details, - domainEvent, - e, - cancellationToken) - .ConfigureAwait(false); - - if (handled) - { - return; - } - - handled = await _sagaErrorHandler.HandleAsync( + var handled = await _sagaErrorHandler.HandleAsync( sagaId, details, e, diff --git a/Source/EventFlow/Subscribers/DispatchToEventSubscribers.cs b/Source/EventFlow/Subscribers/DispatchToEventSubscribers.cs index a24da23c1..3703b539a 100644 --- a/Source/EventFlow/Subscribers/DispatchToEventSubscribers.cs +++ b/Source/EventFlow/Subscribers/DispatchToEventSubscribers.cs @@ -33,7 +33,6 @@ using EventFlow.Core.Caching; using EventFlow.Extensions; using EventFlow.Logs; -using EventFlow.PublishRecovery; namespace EventFlow.Subscribers { @@ -46,7 +45,6 @@ public class DispatchToEventSubscribers : IDispatchToEventSubscribers private readonly IResolver _resolver; private readonly IEventFlowConfiguration _eventFlowConfiguration; private readonly IMemoryCache _memoryCache; - private readonly IRecoveryHandlerProcessor _recoveryHandlerProcessor; private class SubscriberInfomation { @@ -58,14 +56,12 @@ public DispatchToEventSubscribers( ILog log, IResolver resolver, IEventFlowConfiguration eventFlowConfiguration, - IMemoryCache memoryCache, - IRecoveryHandlerProcessor recoveryHandlerProcessor) + IMemoryCache memoryCache) { _log = log; _resolver = resolver; _eventFlowConfiguration = eventFlowConfiguration; _memoryCache = memoryCache; - _recoveryHandlerProcessor = recoveryHandlerProcessor; } public async Task DispatchToSynchronousSubscribersAsync( @@ -120,18 +116,6 @@ private async Task DispatchToSubscribersAsync( } catch (Exception e) { - var handled = await _recoveryHandlerProcessor.RecoverSubscriberErrorAsync( - subscriber, - domainEvent, - e, - cancellationToken) - .ConfigureAwait(false); - - if (handled) - { - continue; - } - if (swallowException) { _log.Error(e, $"Subscriber '{subscriberInfomation.SubscriberType.PrettyPrint()}' threw " + diff --git a/Source/EventFlow/Subscribers/DomainEventPublisher.cs b/Source/EventFlow/Subscribers/DomainEventPublisher.cs index 07d01e961..52aa23aa8 100644 --- a/Source/EventFlow/Subscribers/DomainEventPublisher.cs +++ b/Source/EventFlow/Subscribers/DomainEventPublisher.cs @@ -21,7 +21,6 @@ // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -using System; using System.Collections.Generic; using System.Linq; using System.Threading; @@ -32,7 +31,6 @@ using EventFlow.Core; using EventFlow.Jobs; using EventFlow.Provided.Jobs; -using EventFlow.PublishRecovery; using EventFlow.ReadStores; using EventFlow.Sagas; @@ -48,7 +46,6 @@ public class DomainEventPublisher : IDomainEventPublisher private readonly ICancellationConfiguration _cancellationConfiguration; private readonly IReadOnlyCollection _subscribeSynchronousToAlls; private readonly IReadOnlyCollection _readStoreManagers; - private readonly IRecoveryHandlerProcessor _recoveryHandlerProcessor; public DomainEventPublisher( IDispatchToEventSubscribers dispatchToEventSubscribers, @@ -58,8 +55,7 @@ public DomainEventPublisher( IEventFlowConfiguration eventFlowConfiguration, IEnumerable readStoreManagers, IEnumerable subscribeSynchronousToAlls, - ICancellationConfiguration cancellationConfiguration, - IRecoveryHandlerProcessor recoveryHandlerProcessor) + ICancellationConfiguration cancellationConfiguration) { _dispatchToEventSubscribers = dispatchToEventSubscribers; _dispatchToSagas = dispatchToSagas; @@ -67,7 +63,6 @@ public DomainEventPublisher( _resolver = resolver; _eventFlowConfiguration = eventFlowConfiguration; _cancellationConfiguration = cancellationConfiguration; - _recoveryHandlerProcessor = recoveryHandlerProcessor; _subscribeSynchronousToAlls = subscribeSynchronousToAlls.ToList(); _readStoreManagers = readStoreManagers.ToList(); } @@ -106,26 +101,7 @@ private async Task PublishToReadStoresAsync( CancellationToken cancellationToken) { var updateReadStoresTasks = _readStoreManagers - .Select(async rsm => - { - try - { - await rsm.UpdateReadStoresAsync(domainEvents, cancellationToken).ConfigureAwait(false); - } - catch (Exception ex) - { - var handled = await _recoveryHandlerProcessor - .RecoverReadModelUpdateErrorAsync(rsm, domainEvents, ex, cancellationToken) - .ConfigureAwait(false); - - if (handled) - { - return; - } - - throw; - } - }); + .Select(rsm => rsm.UpdateReadStoresAsync(domainEvents, cancellationToken)); await Task.WhenAll(updateReadStoresTasks).ConfigureAwait(false); } @@ -134,28 +110,7 @@ private async Task PublishToSubscribersOfAllEventsAsync( CancellationToken cancellationToken) { var handle = _subscribeSynchronousToAlls - .Select(async s => - { - try - { - await s.HandleAsync(domainEvents, cancellationToken).ConfigureAwait(false); - } - catch (Exception ex) - { - var handled = await _recoveryHandlerProcessor.RecoverAllSubscriberErrorAsync( - domainEvents, - ex, - cancellationToken) - .ConfigureAwait(false); - - if (handled) - { - return; - } - - throw; - } - }); + .Select(s => s.HandleAsync(domainEvents, cancellationToken)); await Task.WhenAll(handle).ConfigureAwait(false); } @@ -172,29 +127,10 @@ private async Task PublishToAsynchronousSubscribersAsync( { if (_eventFlowConfiguration.IsAsynchronousSubscribersEnabled) { - try - { - await Task.WhenAll(domainEvents.Select( - d => _jobScheduler.ScheduleNowAsync( - DispatchToAsynchronousEventSubscribersJob.Create(d, _resolver), - cancellationToken))) - .ConfigureAwait(false); - } - catch (Exception ex) - { - var handled = await _recoveryHandlerProcessor.RecoverScheduleSubscriberErrorAsync( - domainEvents, - ex, - cancellationToken) - .ConfigureAwait(false); - - if (handled) - { - return; - } - - throw; - } + await Task.WhenAll(domainEvents.Select( + d => _jobScheduler.ScheduleNowAsync( + DispatchToAsynchronousEventSubscribersJob.Create(d, _resolver), cancellationToken))) + .ConfigureAwait(false); } } From 8bb53e292f77832b56fe9a0addb47df5f829e43d Mon Sep 17 00:00:00 2001 From: Maxim Shoshin Date: Thu, 18 Jun 2020 17:15:45 +0300 Subject: [PATCH 8/9] Revert unnecessary changes --- Source/EventFlow/ReadStores/ReadStoreManager.cs | 2 +- .../Subscribers/DispatchToEventSubscribers.cs | 12 +++--------- Source/EventFlow/Subscribers/DomainEventPublisher.cs | 2 +- 3 files changed, 5 insertions(+), 11 deletions(-) diff --git a/Source/EventFlow/ReadStores/ReadStoreManager.cs b/Source/EventFlow/ReadStores/ReadStoreManager.cs index c90cc10f5..4c5a2699b 100644 --- a/Source/EventFlow/ReadStores/ReadStoreManager.cs +++ b/Source/EventFlow/ReadStores/ReadStoreManager.cs @@ -79,7 +79,7 @@ private static bool IsReadModelFor(Type i) { return false; } - + var typeDefinition = i.GetGenericTypeDefinition(); return typeDefinition == typeof(IAmReadModelFor<,,>) || typeDefinition == typeof(IAmAsyncReadModelFor<,,>); diff --git a/Source/EventFlow/Subscribers/DispatchToEventSubscribers.cs b/Source/EventFlow/Subscribers/DispatchToEventSubscribers.cs index 3703b539a..4ab22ba28 100644 --- a/Source/EventFlow/Subscribers/DispatchToEventSubscribers.cs +++ b/Source/EventFlow/Subscribers/DispatchToEventSubscribers.cs @@ -114,16 +114,10 @@ private async Task DispatchToSubscribersAsync( { await subscriberInfomation.HandleMethod(subscriber, domainEvent, cancellationToken).ConfigureAwait(false); } - catch (Exception e) + catch (Exception e) when (swallowException) { - if (swallowException) - { - _log.Error(e, $"Subscriber '{subscriberInfomation.SubscriberType.PrettyPrint()}' threw " + - $"'{e.GetType().PrettyPrint()}' while handling '{domainEvent.EventType.PrettyPrint()}': {e.Message}"); - continue; - } - - throw; + _log.Error(e, $"Subscriber '{subscriberInfomation.SubscriberType.PrettyPrint()}' threw " + + $"'{e.GetType().PrettyPrint()}' while handling '{domainEvent.EventType.PrettyPrint()}': {e.Message}"); } } } diff --git a/Source/EventFlow/Subscribers/DomainEventPublisher.cs b/Source/EventFlow/Subscribers/DomainEventPublisher.cs index 52aa23aa8..2ae5f4b03 100644 --- a/Source/EventFlow/Subscribers/DomainEventPublisher.cs +++ b/Source/EventFlow/Subscribers/DomainEventPublisher.cs @@ -122,7 +122,7 @@ private async Task PublishToSynchronousSubscribersAsync( } private async Task PublishToAsynchronousSubscribersAsync( - IReadOnlyCollection domainEvents, + IEnumerable domainEvents, CancellationToken cancellationToken) { if (_eventFlowConfiguration.IsAsynchronousSubscribersEnabled) From db6129b657f7dfa5a6afe96e3fae2612f81a3663 Mon Sep 17 00:00:00 2001 From: Maxim Shoshin Date: Mon, 29 Jun 2020 19:30:19 +0300 Subject: [PATCH 9/9] Small refactoring --- .../ReadStores/ReadModelRecoveryTests.cs | 25 ++++++++++++- Source/EventFlow/EventFlowOptions.cs | 2 - ...FlowOptionsReliablePublishingExtensions.cs | 1 + .../NopReliableMarkProcessor.cs | 37 ------------------- .../PublishRecovery/PublishVerificator.cs | 4 +- .../PublishRecovery/VerificationState.cs | 6 +-- 6 files changed, 30 insertions(+), 45 deletions(-) delete mode 100644 Source/EventFlow/PublishRecovery/NopReliableMarkProcessor.cs diff --git a/Source/EventFlow.Tests/IntegrationTests/ReadStores/ReadModelRecoveryTests.cs b/Source/EventFlow.Tests/IntegrationTests/ReadStores/ReadModelRecoveryTests.cs index d2addc246..c492f7953 100644 --- a/Source/EventFlow.Tests/IntegrationTests/ReadStores/ReadModelRecoveryTests.cs +++ b/Source/EventFlow.Tests/IntegrationTests/ReadStores/ReadModelRecoveryTests.cs @@ -1,4 +1,27 @@ -using System; +// The MIT License (MIT) +// +// Copyright (c) 2015-2019 Rasmus Mikkelsen +// Copyright (c) 2015-2019 eBay Software Foundation +// https://github.com/eventflow/EventFlow +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; diff --git a/Source/EventFlow/EventFlowOptions.cs b/Source/EventFlow/EventFlowOptions.cs index 8afcb220d..f919f9c47 100644 --- a/Source/EventFlow/EventFlowOptions.cs +++ b/Source/EventFlow/EventFlowOptions.cs @@ -217,8 +217,6 @@ private void RegisterDefaults(IServiceRegistration serviceRegistration) serviceRegistration.Register(); serviceRegistration.Register(); serviceRegistration.Register(); - serviceRegistration.Register(); - serviceRegistration.Register(); serviceRegistration.Register(); serviceRegistration.Register(Lifetime.Singleton); serviceRegistration.Register(); diff --git a/Source/EventFlow/Extensions/EventFlowOptionsReliablePublishingExtensions.cs b/Source/EventFlow/Extensions/EventFlowOptionsReliablePublishingExtensions.cs index 4d2601cdb..00959fcaa 100644 --- a/Source/EventFlow/Extensions/EventFlowOptionsReliablePublishingExtensions.cs +++ b/Source/EventFlow/Extensions/EventFlowOptionsReliablePublishingExtensions.cs @@ -36,6 +36,7 @@ public static IEventFlowOptions UseReliablePublishing f.Register()) .RegisterServices(f => f.Register()) .RegisterServices(f => f.Register()) .RegisterServices(r => r.Register()) diff --git a/Source/EventFlow/PublishRecovery/NopReliableMarkProcessor.cs b/Source/EventFlow/PublishRecovery/NopReliableMarkProcessor.cs deleted file mode 100644 index b24525734..000000000 --- a/Source/EventFlow/PublishRecovery/NopReliableMarkProcessor.cs +++ /dev/null @@ -1,37 +0,0 @@ -// The MIT License (MIT) -// -// Copyright (c) 2015-2018 Rasmus Mikkelsen -// Copyright (c) 2015-2018 eBay Software Foundation -// https://github.com/eventflow/EventFlow -// -// Permission is hereby granted, free of charge, to any person obtaining a copy of -// this software and associated documentation files (the "Software"), to deal in -// the Software without restriction, including without limitation the rights to -// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of -// the Software, and to permit persons to whom the Software is furnished to do so, -// subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS -// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR -// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER -// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -using System.Collections.Generic; -using System.Threading.Tasks; -using EventFlow.Aggregates; - -namespace EventFlow.PublishRecovery -{ - public sealed class NopReliableMarkProcessor : IReliableMarkProcessor - { - public Task MarkEventsPublishedAsync(IReadOnlyCollection domainEvents) - { - return Task.FromResult(0); - } - } -} \ No newline at end of file diff --git a/Source/EventFlow/PublishRecovery/PublishVerificator.cs b/Source/EventFlow/PublishRecovery/PublishVerificator.cs index f7f0b7d62..d7655f824 100644 --- a/Source/EventFlow/PublishRecovery/PublishVerificator.cs +++ b/Source/EventFlow/PublishRecovery/PublishVerificator.cs @@ -56,7 +56,7 @@ public async Task VerifyOnceAsync(CancellationToken c var logItemLookup = state.Items.ToLookup(x => x.AggregateId); - var page = await _eventPersistence.LoadAllCommittedEvents(state.Position, PageSize, cancellationToken) + var page = await _eventPersistence.LoadAllCommittedEvents(state.LastVerifiedPosition, PageSize, cancellationToken) .ConfigureAwait(false); var verifyResult = VerifyDomainEvents(page, logItemLookup); @@ -65,7 +65,7 @@ public async Task VerifyOnceAsync(CancellationToken c // but we have to check them again on next iteration var eventsForRecovery = GetEventsForRecovery(verifyResult.UnpublishedEvents); - if (eventsForRecovery.Count > 0) + if (eventsForRecovery.Any()) { // Do it inside transaction to recover in single thread // success recovery should put LogItem diff --git a/Source/EventFlow/PublishRecovery/VerificationState.cs b/Source/EventFlow/PublishRecovery/VerificationState.cs index b3e93e32e..08d61579a 100644 --- a/Source/EventFlow/PublishRecovery/VerificationState.cs +++ b/Source/EventFlow/PublishRecovery/VerificationState.cs @@ -28,13 +28,13 @@ namespace EventFlow.PublishRecovery { public class VerificationState { - public VerificationState(GlobalPosition position, IReadOnlyCollection items) + public VerificationState(GlobalPosition lastVerifiedPosition, IReadOnlyCollection items) { - Position = position; + LastVerifiedPosition = lastVerifiedPosition; Items = items; } - public GlobalPosition Position { get; } + public GlobalPosition LastVerifiedPosition { get; } public IReadOnlyCollection Items { get; } }