From 6c126675b0af102292cf540a99db5582e82aaea6 Mon Sep 17 00:00:00 2001 From: Daniel Marbach Date: Fri, 23 Jun 2023 16:25:22 +0200 Subject: [PATCH] Recreate outbox index also when the expiryAfterSeconds column is missing (#521) (#523) * Reproduction test * Make sure index creation takes missing expireAfterSeconds into account --- .../Outbox/OutboxInitializationTests.cs | 94 +++++++++++++++++++ .../Outbox/OutboxTestsConfiguration.cs | 1 - .../Outbox/OutboxStorage.cs | 36 +++---- 3 files changed, 113 insertions(+), 18 deletions(-) create mode 100644 src/NServiceBus.Storage.MongoDB.Tests/Outbox/OutboxInitializationTests.cs diff --git a/src/NServiceBus.Storage.MongoDB.Tests/Outbox/OutboxInitializationTests.cs b/src/NServiceBus.Storage.MongoDB.Tests/Outbox/OutboxInitializationTests.cs new file mode 100644 index 00000000..199d57f0 --- /dev/null +++ b/src/NServiceBus.Storage.MongoDB.Tests/Outbox/OutboxInitializationTests.cs @@ -0,0 +1,94 @@ +namespace NServiceBus.Storage.MongoDB.Tests +{ + using System; + using System.Globalization; + using System.Linq; + using System.Threading.Tasks; + using global::MongoDB.Bson; + using global::MongoDB.Driver; + using NUnit.Framework; + + [TestFixture] + public class OutboxInitializationTests + { + [OneTimeSetUp] + public async Task OneTimeSetUp() + { + databaseName = "Test_" + DateTime.UtcNow.Ticks.ToString(CultureInfo.InvariantCulture); + + var databaseSettings = new MongoDatabaseSettings + { + ReadConcern = ReadConcern.Majority, + ReadPreference = ReadPreference.Primary, + WriteConcern = WriteConcern.WMajority + }; + + var database = ClientProvider.Client.GetDatabase(databaseName, databaseSettings); + + await database.CreateCollectionAsync(CollectionNamingConvention()); + + outboxCollection = ClientProvider.Client.GetDatabase(databaseName).GetCollection(CollectionNamingConvention()); + } + + static string CollectionNamingConvention() => CollectionNamingConvention(typeof(T)); + + static string CollectionNamingConvention(Type type) => type.Name.ToLower(); + + [SetUp] + public async Task Setup() => await outboxCollection.Indexes.DropAllAsync(); + + [Theory] + public async Task Should_create_index_when_it_doesnt_exist(TimeSpan timeToKeepOutboxDeduplicationData) + { + OutboxStorage.InitializeOutboxTypes(ClientProvider.Client, databaseName, CollectionNamingConvention, timeToKeepOutboxDeduplicationData); + + await AssertIndexCorrect(outboxCollection, timeToKeepOutboxDeduplicationData); + } + + [Theory] + public async Task Should_recreate_when_expiry_drifts(TimeSpan timeToKeepOutboxDeduplicationData) + { + OutboxStorage.InitializeOutboxTypes(ClientProvider.Client, databaseName, CollectionNamingConvention, timeToKeepOutboxDeduplicationData.Add(TimeSpan.FromSeconds(30))); + + OutboxStorage.InitializeOutboxTypes(ClientProvider.Client, databaseName, CollectionNamingConvention, timeToKeepOutboxDeduplicationData); + + await AssertIndexCorrect(outboxCollection, timeToKeepOutboxDeduplicationData); + } + + [Theory] + public async Task Should_recreate_when_expiry_column_dropped(TimeSpan timeToKeepOutboxDeduplicationData) + { + var indexModel = new CreateIndexModel(Builders.IndexKeys.Ascending(record => record.Dispatched), new CreateIndexOptions + { + Name = OutboxStorage.OutboxCleanupIndexName, + Background = true + }); + await outboxCollection.Indexes.CreateOneAsync(indexModel); + + OutboxStorage.InitializeOutboxTypes(ClientProvider.Client, databaseName, CollectionNamingConvention, timeToKeepOutboxDeduplicationData); + + await AssertIndexCorrect(outboxCollection, timeToKeepOutboxDeduplicationData); + } + + [DatapointSource] + public TimeSpan[] Expiry = new TimeSpan[] { TimeSpan.FromHours(1), TimeSpan.FromHours(3), TimeSpan.FromDays(1) }; + + static async Task AssertIndexCorrect(IMongoCollection outboxCollection, TimeSpan expiry) + { + var outboxCleanupIndex = (await outboxCollection.Indexes.ListAsync()).ToList().SingleOrDefault(indexDocument => indexDocument.GetElement("name").Value == OutboxStorage.OutboxCleanupIndexName); + + Assert.IsNotNull(outboxCleanupIndex); + + BsonElement bsonElement = outboxCleanupIndex.GetElement("expireAfterSeconds"); + Assert.NotNull(bsonElement); + + Assert.AreEqual(expiry, TimeSpan.FromSeconds(bsonElement.Value.ToInt32())); + } + + [OneTimeTearDown] + public async Task OneTimeTearDown() => await ClientProvider.Client.DropDatabaseAsync(databaseName); + + IMongoCollection outboxCollection; + string databaseName; + } +} \ No newline at end of file diff --git a/src/NServiceBus.Storage.MongoDB.Tests/Outbox/OutboxTestsConfiguration.cs b/src/NServiceBus.Storage.MongoDB.Tests/Outbox/OutboxTestsConfiguration.cs index 744a0120..a873449a 100644 --- a/src/NServiceBus.Storage.MongoDB.Tests/Outbox/OutboxTestsConfiguration.cs +++ b/src/NServiceBus.Storage.MongoDB.Tests/Outbox/OutboxTestsConfiguration.cs @@ -5,7 +5,6 @@ using System.Threading.Tasks; using Extensibility; using global::MongoDB.Driver; - using MongoDB; using NServiceBus.Outbox; public class OutboxTestsConfiguration diff --git a/src/NServiceBus.Storage.MongoDB/Outbox/OutboxStorage.cs b/src/NServiceBus.Storage.MongoDB/Outbox/OutboxStorage.cs index 20ba7e5e..45a31d7f 100644 --- a/src/NServiceBus.Storage.MongoDB/Outbox/OutboxStorage.cs +++ b/src/NServiceBus.Storage.MongoDB/Outbox/OutboxStorage.cs @@ -62,30 +62,32 @@ internal static void InitializeOutboxTypes(IMongoClient client, string databaseN }; var outboxCollection = client.GetDatabase(databaseName).GetCollection(collectionNamingConvention(typeof(OutboxRecord)), collectionSettings); - var outboxCleanupIndex = outboxCollection.Indexes.List().ToList().SingleOrDefault(indexDocument => indexDocument.GetElement("name").Value == outboxCleanupIndexName); - var existingExpiration = outboxCleanupIndex?.GetElement("expireAfterSeconds").Value.ToInt32(); - - var createIndex = outboxCleanupIndex is null; - - if (existingExpiration.HasValue && TimeSpan.FromSeconds(existingExpiration.Value) != timeToKeepOutboxDeduplicationData) + var outboxCleanupIndex = outboxCollection.Indexes.List().ToList().SingleOrDefault(indexDocument => indexDocument.GetElement("name").Value == OutboxCleanupIndexName); + var createIndex = false; + if (outboxCleanupIndex is null) { - outboxCollection.Indexes.DropOne(outboxCleanupIndexName); createIndex = true; } - - if (createIndex) + else if (!outboxCleanupIndex.TryGetElement("expireAfterSeconds", out var existingExpiration) || TimeSpan.FromSeconds(existingExpiration.Value.ToInt32()) != timeToKeepOutboxDeduplicationData) { - var indexModel = new CreateIndexModel(Builders.IndexKeys.Ascending(record => record.Dispatched), new CreateIndexOptions - { - ExpireAfter = timeToKeepOutboxDeduplicationData, - Name = outboxCleanupIndexName, - Background = true - }); + outboxCollection.Indexes.DropOne(OutboxCleanupIndexName); + createIndex = true; + } - outboxCollection.Indexes.CreateOne(indexModel); + if (!createIndex) + { + return; } + + var indexModel = new CreateIndexModel(Builders.IndexKeys.Ascending(record => record.Dispatched), new CreateIndexOptions + { + ExpireAfter = timeToKeepOutboxDeduplicationData, + Name = OutboxCleanupIndexName, + Background = true + }); + outboxCollection.Indexes.CreateOne(indexModel); } - const string outboxCleanupIndexName = "OutboxCleanup"; + internal const string OutboxCleanupIndexName = "OutboxCleanup"; } } \ No newline at end of file