From cff67fd5e2129f7dac52f00aef5355ab4e253c09 Mon Sep 17 00:00:00 2001 From: Rafael Lillo Date: Wed, 15 Jan 2025 13:13:14 +0000 Subject: [PATCH] Outbox Improvements for Relation Database (#3464) * Fixes MySQL queries for Dispatched Commands * Fixes MySQL Outbox queries * Fixes Postgres outbox * Fix Sqlite outbox * Fixes MsSQL outbox * Fixes compilation issue * Fixes MSSQL when convert the message id --- .../WebAPI_Common/DbMaker/SchemaCreation.cs | 4 +- .../SqlQueues/MsSqlMessageQueue.cs | 2 +- .../MsSqlOutbox.cs | 163 +++++++----- .../MsSqlQueries.cs | 24 +- .../MySqlOutbox.cs | 91 ++++--- .../MySqlQueries.cs | 25 +- .../PostgreSqlOutbox.cs | 103 +++++--- ...oxBulder.cs => PostgreSqlOutboxBuilder.cs} | 2 +- .../PostgreSqlQueries.cs | 24 +- .../SqliteOutbox.cs | 99 ++++--- .../SqliteQueries.cs | 23 +- src/Paramore.Brighter/IAmAnOutboxSync.cs | 4 +- .../IRelationDatabaseOutboxQueries.cs | 1 - src/Paramore.Brighter/InMemoryOutbox.cs | 4 +- .../RelationDatabaseOutbox.cs | 244 +++++++++++------- .../Outbox/When_retrieving_messages.cs | 90 +++++++ .../Outbox/When_retrieving_messages_async.cs | 90 +++++++ .../When_retrieving_messages_to_archive.cs | 76 ++++++ ...n_retrieving_messages_to_archive_async.cs} | 53 ++-- .../When_retrieving_outstanding_messages.cs | 63 +++++ ...n_retrieving_outstanding_messages_async.cs | 64 +++++ .../Outbox/When_retrieving_messages.cs | 91 +++++++ .../Outbox/When_retrieving_messages_async.cs | 90 +++++++ .../When_retrieving_messages_to_archive.cs | 79 ++++++ ...en_retrieving_messages_to_archive_async.cs | 84 ++++++ .../When_retrieving_outstanding_messages.cs | 65 +++++ ...n_retrieving_outstanding_messages_async.cs | 65 +++++ .../Outbox/When_retrieving_messages.cs | 90 +++++++ .../Outbox/When_retrieving_messages_async.cs | 90 +++++++ .../When_retrieving_messages_to_archive.cs | 76 ++++++ ...en_retrieving_messages_to_archive_async.cs | 84 ++++++ .../When_retrieving_outstanding_messages.cs | 63 +++++ ...n_retrieving_outstanding_messages_async.cs | 64 +++++ .../PostgresSqlTestHelper.cs | 2 +- .../Outbox/When_retrieving_messages.cs | 90 +++++++ .../Outbox/When_retrieving_messages_async.cs | 90 +++++++ .../When_retrieving_messages_to_archive.cs | 77 ++++++ ...en_retrieving_messages_to_archive_async.cs | 87 +++++++ .../When_retrieving_outstanding_messages.cs | 64 +++++ ...n_retrieving_outstanding_messages_async.cs | 64 +++++ 40 files changed, 2331 insertions(+), 333 deletions(-) rename src/Paramore.Brighter.Outbox.PostgreSql/{PostgreSqlOutboxBulder.cs => PostgreSqlOutboxBuilder.cs} (98%) create mode 100644 tests/Paramore.Brighter.MSSQL.Tests/Outbox/When_retrieving_messages.cs create mode 100644 tests/Paramore.Brighter.MSSQL.Tests/Outbox/When_retrieving_messages_async.cs create mode 100644 tests/Paramore.Brighter.MSSQL.Tests/Outbox/When_retrieving_messages_to_archive.cs rename tests/Paramore.Brighter.MSSQL.Tests/Outbox/{When_Retrieving_Messages_To_Archive.cs => When_retrieving_messages_to_archive_async.cs} (50%) create mode 100644 tests/Paramore.Brighter.MSSQL.Tests/Outbox/When_retrieving_outstanding_messages.cs create mode 100644 tests/Paramore.Brighter.MSSQL.Tests/Outbox/When_retrieving_outstanding_messages_async.cs create mode 100644 tests/Paramore.Brighter.MySQL.Tests/Outbox/When_retrieving_messages.cs create mode 100644 tests/Paramore.Brighter.MySQL.Tests/Outbox/When_retrieving_messages_async.cs create mode 100644 tests/Paramore.Brighter.MySQL.Tests/Outbox/When_retrieving_messages_to_archive.cs create mode 100644 tests/Paramore.Brighter.MySQL.Tests/Outbox/When_retrieving_messages_to_archive_async.cs create mode 100644 tests/Paramore.Brighter.MySQL.Tests/Outbox/When_retrieving_outstanding_messages.cs create mode 100644 tests/Paramore.Brighter.MySQL.Tests/Outbox/When_retrieving_outstanding_messages_async.cs create mode 100644 tests/Paramore.Brighter.PostgresSQL.Tests/Outbox/When_retrieving_messages.cs create mode 100644 tests/Paramore.Brighter.PostgresSQL.Tests/Outbox/When_retrieving_messages_async.cs create mode 100644 tests/Paramore.Brighter.PostgresSQL.Tests/Outbox/When_retrieving_messages_to_archive.cs create mode 100644 tests/Paramore.Brighter.PostgresSQL.Tests/Outbox/When_retrieving_messages_to_archive_async.cs create mode 100644 tests/Paramore.Brighter.PostgresSQL.Tests/Outbox/When_retrieving_outstanding_messages.cs create mode 100644 tests/Paramore.Brighter.PostgresSQL.Tests/Outbox/When_retrieving_outstanding_messages_async.cs create mode 100644 tests/Paramore.Brighter.Sqlite.Tests/Outbox/When_retrieving_messages.cs create mode 100644 tests/Paramore.Brighter.Sqlite.Tests/Outbox/When_retrieving_messages_async.cs create mode 100644 tests/Paramore.Brighter.Sqlite.Tests/Outbox/When_retrieving_messages_to_archive.cs create mode 100644 tests/Paramore.Brighter.Sqlite.Tests/Outbox/When_retrieving_messages_to_archive_async.cs create mode 100644 tests/Paramore.Brighter.Sqlite.Tests/Outbox/When_retrieving_outstanding_messages.cs create mode 100644 tests/Paramore.Brighter.Sqlite.Tests/Outbox/When_retrieving_outstanding_messages_async.cs diff --git a/samples/WebAPI/WebAPI_Common/DbMaker/SchemaCreation.cs b/samples/WebAPI/WebAPI_Common/DbMaker/SchemaCreation.cs index fc61582d3c..8d9eee4c2a 100644 --- a/samples/WebAPI/WebAPI_Common/DbMaker/SchemaCreation.cs +++ b/samples/WebAPI/WebAPI_Common/DbMaker/SchemaCreation.cs @@ -388,14 +388,14 @@ private static void CreateOutboxPostgres(string? connectionString, bool hasBinar sqlConnection.Open(); using NpgsqlCommand existsQuery = sqlConnection.CreateCommand(); - existsQuery.CommandText = PostgreSqlOutboxBulder.GetExistsQuery(OUTBOX_TABLE_NAME); + existsQuery.CommandText = PostgreSqlOutboxBuilder.GetExistsQuery(OUTBOX_TABLE_NAME); object? findOutbox = existsQuery.ExecuteScalar(); bool exists = findOutbox is long and > 0; if (exists) return; using NpgsqlCommand command = sqlConnection.CreateCommand(); - command.CommandText = PostgreSqlOutboxBulder.GetDDL(OUTBOX_TABLE_NAME, hasBinaryPayload); + command.CommandText = PostgreSqlOutboxBuilder.GetDDL(OUTBOX_TABLE_NAME, hasBinaryPayload); command.ExecuteScalar(); } diff --git a/src/Paramore.Brighter.MessagingGateway.MsSql/SqlQueues/MsSqlMessageQueue.cs b/src/Paramore.Brighter.MessagingGateway.MsSql/SqlQueues/MsSqlMessageQueue.cs index 995c0252fc..2179d3e59f 100644 --- a/src/Paramore.Brighter.MessagingGateway.MsSql/SqlQueues/MsSqlMessageQueue.cs +++ b/src/Paramore.Brighter.MessagingGateway.MsSql/SqlQueues/MsSqlMessageQueue.cs @@ -157,7 +157,7 @@ public async Task> TryReceiveAsync(string topic, return ReceivedResult.Empty; var json = (string) reader[0]; var messageType = (string) reader[1]; - var id = (int) reader[3]; + var id = (long) reader[3]; var message = JsonSerializer.Deserialize(json, JsonSerialisationOptions.Options); return new ReceivedResult(true, json, topic, messageType, id, message); } diff --git a/src/Paramore.Brighter.Outbox.MsSql/MsSqlOutbox.cs b/src/Paramore.Brighter.Outbox.MsSql/MsSqlOutbox.cs index 50ae369d94..bf143a2fb9 100644 --- a/src/Paramore.Brighter.Outbox.MsSql/MsSqlOutbox.cs +++ b/src/Paramore.Brighter.Outbox.MsSql/MsSqlOutbox.cs @@ -45,13 +45,14 @@ public class MsSqlOutbox : RelationDatabaseOutbox private const int MsSqlDuplicateKeyError_UniqueConstraintViolation = 2627; private readonly IAmARelationalDatabaseConfiguration _configuration; private readonly IAmARelationalDbConnectionProvider _connectionProvider; - + /// /// Initializes a new instance of the class. /// /// The configuration. /// The connection factory. - public MsSqlOutbox(IAmARelationalDatabaseConfiguration configuration, IAmARelationalDbConnectionProvider connectionProvider) : base( + public MsSqlOutbox(IAmARelationalDatabaseConfiguration configuration, + IAmARelationalDbConnectionProvider connectionProvider) : base( configuration.OutBoxTableName, new MsSqlQueries(), ApplicationLogging.CreateLogger()) { _configuration = configuration; @@ -70,7 +71,7 @@ public MsSqlOutbox(IAmARelationalDatabaseConfiguration configuration) : this(con protected override void WriteToStore( IAmABoxTransactionProvider transactionProvider, - Func commandFunc, + Func commandFunc, Action loggingAction) { var connection = GetOpenConnection(_connectionProvider, transactionProvider); @@ -86,7 +87,6 @@ protected override void WriteToStore( if (sqlException.Number != MsSqlDuplicateKeyError_UniqueIndexViolation && sqlException.Number != MsSqlDuplicateKeyError_UniqueConstraintViolation) throw; loggingAction.Invoke(); - } finally { @@ -96,8 +96,8 @@ protected override void WriteToStore( protected override async Task WriteToStoreAsync( IAmABoxTransactionProvider transactionProvider, - Func commandFunc, - Action loggingAction, + Func commandFunc, + Action loggingAction, CancellationToken cancellationToken) { var connection = await GetOpenConnectionAsync(_connectionProvider, transactionProvider, cancellationToken); @@ -128,7 +128,7 @@ protected override async Task WriteToStoreAsync( protected override T ReadFromStore( Func commandFunc, Func resultFunc - ) + ) { var connection = _connectionProvider.GetConnection(); @@ -149,7 +149,7 @@ protected override async Task ReadFromStoreAsync( Func commandFunc, Func> resultFunc, CancellationToken cancellationToken - ) + ) { var connection = await _connectionProvider.GetConnectionAsync(cancellationToken); @@ -167,11 +167,11 @@ CancellationToken cancellationToken } protected override DbCommand CreateCommand( - DbConnection connection, - string sqlText, + DbConnection connection, + string sqlText, int outBoxTimeout, params IDbDataParameter[] parameters - ) + ) { var command = connection.CreateCommand(); @@ -182,18 +182,32 @@ params IDbDataParameter[] parameters return command; } - protected override IDbDataParameter[] CreatePagedOutstandingParameters(double milliSecondsSinceAdded, int pageSize, + protected override IDbDataParameter[] CreatePagedOutstandingParameters(TimeSpan since, int pageSize, int pageNumber) { var parameters = new IDbDataParameter[3]; - parameters[0] = - new SqlParameter { ParameterName = "PageNumber", Value = (object)pageNumber ?? DBNull.Value }; - parameters[1] = new SqlParameter { ParameterName = "PageSize", Value = (object)pageSize ?? DBNull.Value }; - parameters[2] = new SqlParameter + parameters[0] = new SqlParameter { ParameterName = "PageNumber", Value = pageNumber }; + parameters[1] = new SqlParameter { ParameterName = "PageSize", Value = pageSize }; + parameters[2] = CreateSqlParameter("DispatchedSince", DateTimeOffset.UtcNow.Subtract(since)); + return parameters; + } + + protected override IDbDataParameter[] CreatePagedDispatchedParameters(TimeSpan dispatchedSince, int pageSize, + int pageNumber) { - ParameterName = "OutstandingSince", Value = (object)milliSecondsSinceAdded ?? DBNull.Value - }; + var parameters = new IDbDataParameter[3]; + parameters[0] = new SqlParameter { ParameterName = "PageNumber", Value = pageNumber }; + parameters[1] = new SqlParameter { ParameterName = "PageSize", Value = pageSize }; + parameters[2] = CreateSqlParameter("DispatchedSince", DateTimeOffset.UtcNow.Subtract(dispatchedSince)); + + return parameters; + } + protected override IDbDataParameter[] CreatePagedReadParameters(int pageSize, int pageNumber) + { + var parameters = new IDbDataParameter[2]; + parameters[0] = new SqlParameter { ParameterName = "PageNumber", Value = pageNumber }; + parameters[1] = new SqlParameter { ParameterName = "PageSize", Value = pageSize }; return parameters; } @@ -203,7 +217,7 @@ protected override IDbDataParameter CreateSqlParameter(string parameterName, obj { return new SqlParameter { ParameterName = parameterName, Value = value ?? DBNull.Value }; } - + protected override IDbDataParameter[] InitAddDbParameters(Message message, int? position = null) { var prefix = position.HasValue ? $"p{position}_" : ""; @@ -212,7 +226,7 @@ protected override IDbDataParameter[] InitAddDbParameters(Message message, int? { new SqlParameter { - ParameterName = $"{prefix}MessageId", + ParameterName = $"{prefix}MessageId", DbType = DbType.String, Value = (object)message.Id ?? DBNull.Value }, @@ -224,7 +238,7 @@ protected override IDbDataParameter[] InitAddDbParameters(Message message, int? }, new SqlParameter { - ParameterName = $"{prefix}Topic", + ParameterName = $"{prefix}Topic", DbType = DbType.String, Value = (object)message.Header.Topic.Value ?? DBNull.Value }, @@ -242,7 +256,7 @@ protected override IDbDataParameter[] InitAddDbParameters(Message message, int? }, new SqlParameter { - ParameterName = $"{prefix}ReplyTo", + ParameterName = $"{prefix}ReplyTo", DbType = DbType.String, Value = (object)message.Header.ReplyTo ?? DBNull.Value }, @@ -258,24 +272,23 @@ protected override IDbDataParameter[] InitAddDbParameters(Message message, int? DbType = DbType.String, Value = (object)message.Header.PartitionKey ?? DBNull.Value }, - new SqlParameter { ParameterName = $"{prefix}HeaderBag", - Value = (object)bagJson ?? DBNull.Value }, + new SqlParameter { ParameterName = $"{prefix}HeaderBag", Value = (object)bagJson ?? DBNull.Value }, _configuration.BinaryMessagePayload ? new SqlParameter { - ParameterName = $"{prefix}Body", + ParameterName = $"{prefix}Body", DbType = DbType.Binary, Value = (object)message.Body?.Bytes ?? DBNull.Value } : new SqlParameter { - ParameterName = $"{prefix}Body", + ParameterName = $"{prefix}Body", DbType = DbType.String, Value = (object)message.Body?.Value ?? DBNull.Value } }; } - + #endregion #region Property Extractors @@ -287,22 +300,22 @@ private static MessageType GetMessageType(DbDataReader dr) => private static string GetMessageId(DbDataReader dr) => dr.GetString(dr.GetOrdinal("MessageId")); - private string GetContentType(DbDataReader dr) + private static string GetContentType(DbDataReader dr) { var ordinal = dr.GetOrdinal("ContentType"); - if (dr.IsDBNull(ordinal)) return null; - + if (dr.IsDBNull(ordinal)) return null; + var contentType = dr.GetString(ordinal); return contentType; } - private string GetReplyTo(DbDataReader dr) + private static string GetReplyTo(DbDataReader dr) { - var ordinal = dr.GetOrdinal("ReplyTo"); - if (dr.IsDBNull(ordinal)) return null; - - var replyTo = dr.GetString(ordinal); - return replyTo; + var ordinal = dr.GetOrdinal("ReplyTo"); + if (dr.IsDBNull(ordinal)) return null; + + var replyTo = dr.GetString(ordinal); + return replyTo; } private static Dictionary GetContextBag(DbDataReader dr) @@ -314,11 +327,11 @@ private static Dictionary GetContextBag(DbDataReader dr) return dictionaryBag; } - private string GetCorrelationId(DbDataReader dr) + private static string GetCorrelationId(DbDataReader dr) { var ordinal = dr.GetOrdinal("CorrelationId"); - if (dr.IsDBNull(ordinal)) return null; - + if (dr.IsDBNull(ordinal)) return null; + var correlationId = dr.GetString(ordinal); return correlationId; } @@ -332,7 +345,7 @@ private static DateTimeOffset GetTimeStamp(DbDataReader dr) return timeStamp; } - private string GetPartitionKey(DbDataReader dr) + private static string GetPartitionKey(DbDataReader dr) { var ordinal = dr.GetOrdinal("PartitionKey"); if (dr.IsDBNull(ordinal)) return null; @@ -341,7 +354,7 @@ private string GetPartitionKey(DbDataReader dr) return partitionKey; } - private byte[] GetBodyAsBytes(DbDataReader dr) + private static byte[] GetBodyAsBytes(DbDataReader dr) { var ordinal = dr.GetOrdinal("Body"); if (dr.IsDBNull(ordinal)) return null; @@ -355,10 +368,10 @@ private byte[] GetBodyAsBytes(DbDataReader dr) var bytesRead = body.Read(buffer, 0, (int)bodyLength); bytesRemaining -= bytesRead; } - + return buffer; } - + private static string GetBodyAsText(DbDataReader dr) { var ordinal = dr.GetOrdinal("Body"); @@ -381,7 +394,7 @@ protected override Message MapFunction(DbDataReader dr) return message ?? new Message(); } - + protected override async Task MapFunctionAsync(DbDataReader dr, CancellationToken cancellationToken) { Message message = null; @@ -390,7 +403,7 @@ protected override async Task MapFunctionAsync(DbDataReader dr, Cancell message = MapAMessage(dr); } - dr.Close(); + await dr.CloseAsync(); return message ?? new Message(); } @@ -411,7 +424,7 @@ protected override IEnumerable MapListFunction(DbDataReader dr) protected override async Task> MapListFunctionAsync( DbDataReader dr, CancellationToken cancellationToken - ) + ) { var messages = new List(); while (await dr.ReadAsync(cancellationToken)) @@ -419,18 +432,32 @@ CancellationToken cancellationToken messages.Add(MapAMessage(dr)); } - dr.Close(); + await dr.CloseAsync(); return messages; } - protected override async Task MapOutstandingCountAsync(DbDataReader dr, CancellationToken cancellationToken) + protected override async Task MapOutstandingCountAsync(DbDataReader dr, + CancellationToken cancellationToken) + { + int outstandingMessages = -1; + if (await dr.ReadAsync(cancellationToken)) + { + outstandingMessages = dr.GetInt32(0); + } + + await dr.CloseAsync(); + return outstandingMessages; + } + + protected override int MapOutstandingCount(DbDataReader dr) { int outstandingMessages = -1; - if(await dr.ReadAsync(cancellationToken)) + if (dr.Read()) { outstandingMessages = dr.GetInt32(0); } + dr.Close(); return outstandingMessages; } @@ -445,32 +472,32 @@ private Message MapAMessage(DbDataReader dr) var header = new MessageHeader(id, topic, messageType); - DateTimeOffset timeStamp = GetTimeStamp(dr); - var correlationId = GetCorrelationId(dr); - var replyTo = GetReplyTo(dr); - var contentType = GetContentType(dr); + DateTimeOffset timeStamp = GetTimeStamp(dr); + var correlationId = GetCorrelationId(dr); + var replyTo = GetReplyTo(dr); + var contentType = GetContentType(dr); var partitionKey = GetPartitionKey(dr); - header = new MessageHeader( - messageId: id, - topic: topic, - messageType: messageType, - timeStamp: timeStamp, - handledCount: 0, - delayed: TimeSpan.Zero, - correlationId: correlationId, - replyTo: new RoutingKey(replyTo), + header = new MessageHeader( + messageId: id, + topic: topic, + messageType: messageType, + timeStamp: timeStamp, + handledCount: 0, + delayed: TimeSpan.Zero, + correlationId: correlationId, + replyTo: new RoutingKey(replyTo), contentType: contentType, partitionKey: partitionKey); - Dictionary dictionaryBag = GetContextBag(dr); - if (dictionaryBag != null) + Dictionary dictionaryBag = GetContextBag(dr); + if (dictionaryBag != null) + { + foreach (var key in dictionaryBag.Keys) { - foreach (var key in dictionaryBag.Keys) - { - header.Bag.Add(key, dictionaryBag[key]); - } + header.Bag.Add(key, dictionaryBag[key]); } + } var bodyOrdinal = dr.GetOrdinal("Body"); string messageBody = string.Empty; diff --git a/src/Paramore.Brighter.Outbox.MsSql/MsSqlQueries.cs b/src/Paramore.Brighter.Outbox.MsSql/MsSqlQueries.cs index 95153c5b9d..9f78317016 100644 --- a/src/Paramore.Brighter.Outbox.MsSql/MsSqlQueries.cs +++ b/src/Paramore.Brighter.Outbox.MsSql/MsSqlQueries.cs @@ -2,18 +2,16 @@ { public class MsSqlQueries : IRelationDatabaseOutboxQueries { - public string PagedDispatchedCommand { get; } = "SELECT * FROM (SELECT ROW_NUMBER() OVER(ORDER BY Timestamp DESC) AS NUMBER, * FROM {0}) AS TBL WHERE DISPATCHED IS NOT NULL AND DISPATCHED < DATEADD(millisecond, @OutStandingSince, getutcdate()) AND NUMBER BETWEEN ((@PageNumber-1)*@PageSize+1) AND (@PageNumber*@PageSize) ORDER BY Timestamp DESC"; - public string PagedReadCommand { get; } = "SELECT * FROM (SELECT ROW_NUMBER() OVER(ORDER BY Timestamp DESC) AS NUMBER, * FROM {0}) AS TBL WHERE NUMBER BETWEEN ((@PageNumber-1)*@PageSize+1) AND (@PageNumber*@PageSize) ORDER BY Timestamp DESC"; - public string PagedOutstandingCommand { get; } = "SELECT * FROM (SELECT ROW_NUMBER() OVER(ORDER BY Timestamp ASC) AS NUMBER, * FROM {0} WHERE DISPATCHED IS NULL) AS TBL WHERE TIMESTAMP < DATEADD(millisecond, -@OutStandingSince, getutcdate()) AND NUMBER BETWEEN ((@PageNumber-1)*@PageSize+1) AND (@PageNumber*@PageSize) ORDER BY Timestamp ASC"; - public string AddCommand { get; } = "INSERT INTO {0} (MessageId, MessageType, Topic, Timestamp, CorrelationId, ReplyTo, ContentType, PartitionKey, HeaderBag, Body) VALUES (@MessageId, @MessageType, @Topic, @Timestamp, @CorrelationId, @ReplyTo, @ContentType, @PartitionKey, @HeaderBag, @Body)"; - public string BulkAddCommand { get; } = "INSERT INTO {0} (MessageId, MessageType, Topic, Timestamp, CorrelationId, ReplyTo, ContentType, PartitionKey, HeaderBag, Body) VALUES {1}"; - public string MarkDispatchedCommand { get; } = "UPDATE {0} SET Dispatched = @DispatchedAt WHERE MessageId = @MessageId"; - public string MarkMultipleDispatchedCommand { get; } = "UPDATE {0} SET Dispatched = @DispatchedAt WHERE MessageId in ( {1} )"; - public string GetMessageCommand { get; } = "SELECT * FROM {0} WHERE MessageId = @MessageId"; - public string GetMessagesCommand { get; } = "SELECT * FROM {0} WHERE MessageId IN ( {1} )"; - public string DeleteMessagesCommand { get; } = "DELETE FROM {0} WHERE MessageId IN ( {1} )"; - public string DispatchedCommand { get; } = "Select top(@PageSize) * FROM {0} WHERE Dispatched is not NULL and Dispatched < DATEADD(hour, @DispatchedSince, getutcdate()) Order BY Dispatched"; - - public string GetNumberOfOutstandingMessagesCommand { get; } = "Select count(1) FROM {0} where Dispatched is NULL"; + public string PagedDispatchedCommand { get; } = "SELECT * FROM (SELECT ROW_NUMBER() OVER(ORDER BY [Timestamp] DESC) AS NUMBER, * FROM {0}) AS TBL WHERE [Dispatched] IS NOT NULL AND [Dispatched] < @DispatchedSince AND NUMBER BETWEEN ((@PageNumber-1)*@PageSize+1) AND (@PageNumber*@PageSize) ORDER BY [Timestamp] DESC"; + public string PagedReadCommand { get; } = "SELECT * FROM (SELECT ROW_NUMBER() OVER(ORDER BY [Timestamp] DESC) AS NUMBER, * FROM {0}) AS TBL WHERE NUMBER BETWEEN ((@PageNumber-1)*@PageSize+1) AND (@PageNumber*@PageSize) ORDER BY Timestamp DESC"; + public string PagedOutstandingCommand { get; } = "SELECT * FROM (SELECT ROW_NUMBER() OVER(ORDER BY [Timestamp] ASC) AS NUMBER, * FROM {0} WHERE [Dispatched] IS NULL) AS TBL WHERE [Timestamp] < @DispatchedSince AND NUMBER BETWEEN ((@PageNumber-1)*@PageSize+1) AND (@PageNumber*@PageSize) ORDER BY [Timestamp] ASC"; + public string AddCommand { get; } = "INSERT INTO {0} ([MessageId], [MessageType], [Topic], [Timestamp], [CorrelationId], [ReplyTo], [ContentType], [PartitionKey], [HeaderBag], [Body]) VALUES (@MessageId, @MessageType, @Topic, @Timestamp, @CorrelationId, @ReplyTo, @ContentType, @PartitionKey, @HeaderBag, @Body)"; + public string BulkAddCommand { get; } = "INSERT INTO {0} ([MessageId], [MessageType], [Topic], [Timestamp], [CorrelationId], [ReplyTo], [ContentType], [PartitionKey], [HeaderBag], [Body]) VALUES {1}"; + public string MarkDispatchedCommand { get; } = "UPDATE {0} SET [Dispatched] = @DispatchedAt WHERE [MessageId] = @MessageId"; + public string MarkMultipleDispatchedCommand { get; } = "UPDATE {0} SET [Dispatched] = @DispatchedAt WHERE [MessageId] IN ( {1} )"; + public string GetMessageCommand { get; } = "SELECT * FROM {0} WHERE [MessageId] = @MessageId"; + public string GetMessagesCommand { get; } = "SELECT * FROM {0} WHERE [MessageId] IN ( {1} )"; + public string DeleteMessagesCommand { get; } = "DELETE FROM {0} WHERE [MessageId] IN ( {1} )"; + public string GetNumberOfOutstandingMessagesCommand { get; } = "SELECT COUNT(1) FROM {0} WHERE [Dispatched] IS NULL"; } } diff --git a/src/Paramore.Brighter.Outbox.MySql/MySqlOutbox.cs b/src/Paramore.Brighter.Outbox.MySql/MySqlOutbox.cs index c83e19a9c1..9351163956 100644 --- a/src/Paramore.Brighter.Outbox.MySql/MySqlOutbox.cs +++ b/src/Paramore.Brighter.Outbox.MySql/MySqlOutbox.cs @@ -47,14 +47,15 @@ public class MySqlOutbox : RelationDatabaseOutbox private const int MySqlDuplicateKeyError = 1062; private readonly IAmARelationalDatabaseConfiguration _configuration; - private readonly IAmARelationalDbConnectionProvider _connectionProvider; + private readonly IAmARelationalDbConnectionProvider _connectionProvider; /// /// Initializes a new instance of the class. /// /// The configuration to connect to this data store /// Provides a connection to the Db that allows us to enlist in an ambient transaction - public MySqlOutbox(IAmARelationalDatabaseConfiguration configuration, IAmARelationalDbConnectionProvider connectionProvider) + public MySqlOutbox(IAmARelationalDatabaseConfiguration configuration, + IAmARelationalDbConnectionProvider connectionProvider) : base(configuration.OutBoxTableName, new MySqlQueries(), ApplicationLogging.CreateLogger()) { _configuration = configuration; @@ -66,7 +67,7 @@ public MySqlOutbox(IAmARelationalDatabaseConfiguration configuration, IAmARelati /// Initializes a new instance of the class. /// /// The configuration to connect to this data store - public MySqlOutbox(IAmARelationalDatabaseConfiguration configuration) + public MySqlOutbox(IAmARelationalDatabaseConfiguration configuration) : this(configuration, new MySqlConnectionProvider(configuration)) { } @@ -75,7 +76,7 @@ protected override void WriteToStore( IAmABoxTransactionProvider transactionProvider, Func commandFunc, Action loggingAction - ) + ) { var connection = GetOpenConnection(_connectionProvider, transactionProvider); using var command = commandFunc.Invoke(connection); @@ -90,7 +91,6 @@ Action loggingAction if (!IsExceptionUnqiueOrDuplicateIssue(sqlException)) throw; s_logger.LogWarning( "MsSqlOutbox: A duplicate was detected in the batch"); - } finally { @@ -101,9 +101,9 @@ Action loggingAction protected override async Task WriteToStoreAsync( IAmABoxTransactionProvider transactionProvider, Func commandFunc, - Action loggingAction, + Action loggingAction, CancellationToken cancellationToken - ) + ) { var connection = await GetOpenConnectionAsync(_connectionProvider, transactionProvider, cancellationToken); using var command = commandFunc.Invoke(connection); @@ -133,7 +133,7 @@ CancellationToken cancellationToken protected override T ReadFromStore( Func commandFunc, Func resultFunc - ) + ) { var connection = _connectionProvider.GetConnection(); @@ -152,27 +152,27 @@ Func resultFunc protected override async Task ReadFromStoreAsync( Func commandFunc, - Func> resultFunc, + Func> resultFunc, CancellationToken cancellationToken) { var connection = await _connectionProvider.GetConnectionAsync(cancellationToken); if (connection.State != ConnectionState.Open) await connection.OpenAsync(cancellationToken); - using var command = commandFunc.Invoke(connection); + await using var command = commandFunc.Invoke(connection); try { return await resultFunc.Invoke(await command.ExecuteReaderAsync(cancellationToken)); } finally { - connection.Close(); + await connection.CloseAsync(); } } protected override DbCommand CreateCommand( - DbConnection connection, - string sqlText, + DbConnection connection, + string sqlText, int outBoxTimeout, params IDbDataParameter[] parameters) { @@ -185,6 +185,26 @@ protected override DbCommand CreateCommand( return command; } + protected override IDbDataParameter[] CreatePagedDispatchedParameters(TimeSpan dispatchedSince, int pageSize, + int pageNumber) + { + var parameters = new IDbDataParameter[3]; + parameters[0] = CreateSqlParameter("Skip", Math.Max(pageNumber - 1, 0) * pageSize); + parameters[1] = CreateSqlParameter("Take", pageSize); + parameters[2] = CreateSqlParameter("DispatchedSince", DateTimeOffset.UtcNow.Subtract(dispatchedSince)); + + return parameters; + } + + protected override IDbDataParameter[] CreatePagedReadParameters(int pageSize, int pageNumber) + { + var parameters = new IDbDataParameter[2]; + parameters[0] = CreateSqlParameter("Skip", Math.Max(pageNumber - 1, 0) * pageSize); + parameters[1] = CreateSqlParameter("Take", pageSize); + + return parameters; + } + protected override IDbDataParameter CreateSqlParameter(string parameterName, object value) { return new MySqlParameter { ParameterName = parameterName, Value = value }; @@ -252,17 +272,13 @@ protected override IDbDataParameter[] InitAddDbParameters(Message message, int? }; } - protected override IDbDataParameter[] CreatePagedOutstandingParameters( - double milliSecondsSinceAdded, - int pageSize, - int pageNumber - ) + protected override IDbDataParameter[] CreatePagedOutstandingParameters(TimeSpan since, int pageSize, + int pageNumber) { - var offset = (pageNumber - 1) * pageSize; var parameters = new IDbDataParameter[3]; - parameters[0] = CreateSqlParameter("OffsetValue", offset); - parameters[1] = CreateSqlParameter("PageSize", pageSize); - parameters[2] = CreateSqlParameter("OutstandingSince", milliSecondsSinceAdded); + parameters[0] = CreateSqlParameter("Skip", Math.Max(pageNumber - 1, 0) * pageSize); + parameters[1] = CreateSqlParameter("Take", pageSize); + parameters[2] = CreateSqlParameter("TimestampSince", DateTimeOffset.UtcNow.Subtract(since)); return parameters; } @@ -316,13 +332,27 @@ protected override async Task> MapListFunctionAsync( } - protected override async Task MapOutstandingCountAsync(DbDataReader dr, CancellationToken cancellationToken) + protected override async Task MapOutstandingCountAsync(DbDataReader dr, + CancellationToken cancellationToken) { int outstandingMessages = -1; if (await dr.ReadAsync(cancellationToken)) { outstandingMessages = dr.GetInt32(0); } + + await dr.CloseAsync(); + return outstandingMessages; + } + + protected override int MapOutstandingCount(DbDataReader dr) + { + int outstandingMessages = -1; + if (dr.Read()) + { + outstandingMessages = dr.GetInt32(0); + } + dr.Close(); return outstandingMessages; } @@ -371,13 +401,14 @@ private Message MapAMessage(IDataReader dr) } var body = _configuration.BinaryMessagePayload - ? new MessageBody(GetBodyAsBytes((MySqlDataReader)dr), "application/octet-stream", CharacterEncoding.Raw) + ? new MessageBody(GetBodyAsBytes((MySqlDataReader)dr), "application/octet-stream", + CharacterEncoding.Raw) : new MessageBody(GetBodyAsString(dr), "application/json", CharacterEncoding.UTF8); return new Message(header, body); } - private byte[] GetBodyAsBytes(MySqlDataReader dr) + private static byte[] GetBodyAsBytes(MySqlDataReader dr) { var i = dr.GetOrdinal("Body"); using var ms = new MemoryStream(); @@ -401,7 +432,7 @@ private static string GetBodyAsString(IDataReader dr) return dr.GetString(dr.GetOrdinal("Body")); } - private static Dictionary GetContextBag(IDataReader dr) + private static Dictionary GetContextBag(IDataReader dr) { var i = dr.GetOrdinal("HeaderBag"); var headerBag = dr.IsDBNull(i) ? "" : dr.GetString(i); @@ -410,7 +441,7 @@ private static Dictionary GetContextBag(IDataReader dr) return dictionaryBag; } - private string GetContentType(IDataReader dr) + private static string GetContentType(IDataReader dr) { var ordinal = dr.GetOrdinal("ContentType"); if (dr.IsDBNull(ordinal)) return null; @@ -419,7 +450,7 @@ private string GetContentType(IDataReader dr) return contentType; } - private string GetCorrelationId(IDataReader dr) + private static string GetCorrelationId(IDataReader dr) { var ordinal = dr.GetOrdinal("CorrelationId"); if (dr.IsDBNull(ordinal)) return null; @@ -438,7 +469,7 @@ private static string GetMessageId(IDataReader dr) return dr.GetString(dr.GetOrdinal("MessageId")); } - private string GetPartitionKey(IDataReader dr) + private static string GetPartitionKey(IDataReader dr) { var ordinal = dr.GetOrdinal("PartitionKey"); if (dr.IsDBNull(ordinal)) return null; @@ -447,7 +478,7 @@ private string GetPartitionKey(IDataReader dr) return partitionKey; } - private string GetReplyTo(IDataReader dr) + private static string GetReplyTo(IDataReader dr) { var ordinal = dr.GetOrdinal("ReplyTo"); if (dr.IsDBNull(ordinal)) return null; diff --git a/src/Paramore.Brighter.Outbox.MySql/MySqlQueries.cs b/src/Paramore.Brighter.Outbox.MySql/MySqlQueries.cs index 412257a19c..542d238c15 100644 --- a/src/Paramore.Brighter.Outbox.MySql/MySqlQueries.cs +++ b/src/Paramore.Brighter.Outbox.MySql/MySqlQueries.cs @@ -1,18 +1,17 @@ namespace Paramore.Brighter.Outbox.MySql { public class MySqlQueries : IRelationDatabaseOutboxQueries - { - public string PagedDispatchedCommand { get; } = "SELECT * FROM {0} AS TBL WHERE `CreatedID` BETWEEN ((?PageNumber-1)*?PageSize+1) AND (?PageNumber*?PageSize) AND DISPATCHED IS NOT NULL AND DISPATCHED < DATE_ADD(UTC_TIMESTAMP(), INTERVAL ?OutstandingSince MICROSECOND) AND NUMBER BETWEEN ((?PageNumber-1)*?PageSize+1) AND (?PageNumber*?PageSize) ORDER BY Timestamp DESC"; - public string PagedReadCommand { get; } = "SELECT * FROM {0} AS TBL WHERE `CreatedID` BETWEEN ((?PageNumber-1)*?PageSize+1) AND (?PageNumber*?PageSize) ORDER BY Timestamp ASC"; - public string PagedOutstandingCommand { get; } = "SELECT * FROM {0} WHERE DISPATCHED IS NULL AND Timestamp < DATE_ADD(UTC_TIMESTAMP(), INTERVAL -?OutStandingSince SECOND) ORDER BY Timestamp DESC LIMIT ?PageSize OFFSET ?OffsetValue"; - public string AddCommand { get; } = "INSERT INTO {0} (MessageId, MessageType, Topic, Timestamp, CorrelationId, ReplyTo, ContentType, PartitionKey, HeaderBag, Body) VALUES (?MessageId, ?MessageType, ?Topic, ?Timestamp, ?CorrelationId, ?ReplyTo, ?ContentType, ?PartitionKey, ?HeaderBag, ?Body)"; - public string BulkAddCommand { get; } = "INSERT INTO {0} (MessageId, MessageType, Topic, Timestamp, CorrelationId, ReplyTo, ContentType, PartitionKey, HeaderBag, Body) VALUES {1}"; - public string MarkDispatchedCommand { get; } = "UPDATE {0} SET Dispatched = ?DispatchedAt WHERE MessageId = ?MessageId"; - public string MarkMultipleDispatchedCommand { get; } = "UPDATE {0} SET Dispatched = ?DispatchedAt WHERE MessageId IN ( {1} )"; - public string GetMessageCommand { get; } = "SELECT * FROM {0} WHERE MessageId = ?MessageId"; - public string GetMessagesCommand { get; } = "SELECT * FROM {0} WHERE `MessageID` IN ( {1} )ORDER BY Timestamp ASC"; - public string DeleteMessagesCommand { get; } = "DELETE FROM {0} WHERE MessageId IN ( {1} )"; - public string DispatchedCommand { get; } = "Select * FROM {0} WHERE Dispatched is not NULL and Dispatched < DATEADD(hour, @DispatchedSince, getutcdate()) LIMIT @PageSize Order BY Dispatched"; - public string GetNumberOfOutstandingMessagesCommand { get; } = "Select count(1) FROM {0} where Dispatched is NULL"; + { + public string PagedDispatchedCommand { get; } = "SELECT * FROM {0} WHERE `Dispatched` IS NOT NULL AND `Dispatched` < @DispatchedSince ORDER BY `Timestamp` DESC LIMIT @Take OFFSET @Skip"; + public string PagedReadCommand { get; } = "SELECT * FROM {0} ORDER BY `Timestamp` ASC LIMIT @Take OFFSET @Skip"; + public string PagedOutstandingCommand { get; } = "SELECT * FROM {0} WHERE `Dispatched` IS NULL AND `Timestamp` < @TimestampSince ORDER BY Timestamp DESC LIMIT @Take OFFSET @Skip"; + public string AddCommand { get; } = "INSERT INTO {0} (`MessageId`, `MessageType`, `Topic`, `Timestamp`, `CorrelationId`, `ReplyTo`, `ContentType`, `PartitionKey`, `HeaderBag`, `Body`) VALUES (@MessageId, @MessageType, @Topic, @Timestamp, @CorrelationId, @ReplyTo, @ContentType, @PartitionKey, @HeaderBag, @Body)"; + public string BulkAddCommand { get; } = "INSERT INTO {0} (`MessageId`, `MessageType`, `Topic`, `Timestamp`, `CorrelationId`, `ReplyTo`, `ContentType`, `PartitionKey`, `HeaderBag`, `Body`) VALUES {1}"; + public string MarkDispatchedCommand { get; } = "UPDATE {0} SET `Dispatched` = @DispatchedAt WHERE `MessageId` = @MessageId"; + public string MarkMultipleDispatchedCommand { get; } = "UPDATE {0} SET `Dispatched` = @DispatchedAt WHERE `MessageId` IN ( {1} )"; + public string GetMessageCommand { get; } = "SELECT * FROM {0} WHERE `MessageId` = @MessageId"; + public string GetMessagesCommand { get; } = "SELECT * FROM {0} WHERE `MessageId` IN ( {1} ) ORDER BY `Timestamp` ASC"; + public string DeleteMessagesCommand { get; } = "DELETE FROM {0} WHERE `MessageId` IN ( {1} )"; + public string GetNumberOfOutstandingMessagesCommand { get; } = "SELECT COUNT(1) FROM {0} WHERE `Dispatched` IS NULL"; } } diff --git a/src/Paramore.Brighter.Outbox.PostgreSql/PostgreSqlOutbox.cs b/src/Paramore.Brighter.Outbox.PostgreSql/PostgreSqlOutbox.cs index 994166358c..94840bd24c 100644 --- a/src/Paramore.Brighter.Outbox.PostgreSql/PostgreSqlOutbox.cs +++ b/src/Paramore.Brighter.Outbox.PostgreSql/PostgreSqlOutbox.cs @@ -53,7 +53,6 @@ public class PostgreSqlOutbox : RelationDatabaseOutbox /// /// The configuration to connect to this data store /// Provides a connection to the Db that allows us to enlist in an ambient transaction - public PostgreSqlOutbox( IAmARelationalDatabaseConfiguration configuration, IAmARelationalDbConnectionProvider connectionProvider) : base( @@ -74,7 +73,8 @@ public PostgreSqlOutbox( IAmARelationalDatabaseConfiguration configuration, NpgsqlDataSource dataSource = null) : this(configuration, new PostgreSqlConnectionProvider(configuration, dataSource)) - { } + { + } protected override void WriteToStore( IAmABoxTransactionProvider transactionProvider, @@ -112,8 +112,7 @@ protected override async Task WriteToStoreAsync( CancellationToken cancellationToken) { var connection = await GetOpenConnectionAsync(_connectionProvider, transactionProvider, cancellationToken); - await connection.OpenAsync(cancellationToken); - using var command = commandFunc.Invoke(connection); + await using var command = commandFunc.Invoke(connection); try { if (transactionProvider is { HasOpenTransaction: true }) @@ -140,7 +139,7 @@ protected override async Task WriteToStoreAsync( protected override T ReadFromStore( Func commandFunc, Func resultFunc - ) + ) { var connection = _connectionProvider.GetConnection(); @@ -159,22 +158,22 @@ Func resultFunc protected override async Task ReadFromStoreAsync( Func commandFunc, - Func> resultFunc, + Func> resultFunc, CancellationToken cancellationToken - ) + ) { var connection = await _connectionProvider.GetConnectionAsync(cancellationToken); if (connection.State != ConnectionState.Open) await connection.OpenAsync(cancellationToken); - using var command = commandFunc.Invoke(connection); + await using var command = commandFunc.Invoke(connection); try { return await resultFunc.Invoke(await command.ExecuteReaderAsync(cancellationToken)); } finally { - connection.Close(); + await connection.CloseAsync(); } } @@ -193,15 +192,33 @@ protected override DbCommand CreateCommand( return command; } - protected override IDbDataParameter[] CreatePagedOutstandingParameters( - double milliSecondsSinceAdded, - int pageSize, + protected override IDbDataParameter[] CreatePagedOutstandingParameters(TimeSpan since, int pageSize, + int pageNumber) + { + var parameters = new IDbDataParameter[3]; + parameters[0] = CreateSqlParameter("TimestampSince", DateTimeOffset.UtcNow.Subtract(since)); + parameters[1] = CreateSqlParameter("Take", pageSize); + parameters[2] = CreateSqlParameter("Skip", Math.Max(pageNumber - 1, 0) * pageSize); + + return parameters; + } + + protected override IDbDataParameter[] CreatePagedDispatchedParameters(TimeSpan dispatchedSince, int pageSize, int pageNumber) { var parameters = new IDbDataParameter[3]; - parameters[0] = CreateSqlParameter("OutstandingSince", milliSecondsSinceAdded); - parameters[1] = CreateSqlParameter("PageSize", pageSize); - parameters[2] = CreateSqlParameter("PageNumber", pageNumber); + parameters[0] = CreateSqlParameter("DispatchedSince", DateTimeOffset.UtcNow.Subtract(dispatchedSince)); + parameters[1] = CreateSqlParameter("Take", pageSize); + parameters[2] = CreateSqlParameter("Skip", Math.Max(pageNumber - 1, 0) * pageSize); + + return parameters; + } + + protected override IDbDataParameter[] CreatePagedReadParameters(int pageSize, int pageNumber) + { + var parameters = new IDbDataParameter[2]; + parameters[0] = CreateSqlParameter("Take", pageSize); + parameters[1] = CreateSqlParameter("Skip", Math.Max(pageNumber - 1, 0) * pageSize); return parameters; } @@ -267,18 +284,19 @@ protected override IDbDataParameter[] InitAddDbParameters(Message message, int? { ParameterName = $"{prefix}HeaderBag", NpgsqlDbType = NpgsqlDbType.Text, Value = bagjson }, - _configuration.BinaryMessagePayload ? new NpgsqlParameter - { - ParameterName = $"{prefix}Body", - NpgsqlDbType = NpgsqlDbType.Bytea, - Value = message.Body.Bytes - } - : new NpgsqlParameter - { - ParameterName = $"{prefix}Body", - NpgsqlDbType = NpgsqlDbType.Text, - Value = message.Body.Value - } + _configuration.BinaryMessagePayload + ? new NpgsqlParameter + { + ParameterName = $"{prefix}Body", + NpgsqlDbType = NpgsqlDbType.Bytea, + Value = message.Body.Bytes + } + : new NpgsqlParameter + { + ParameterName = $"{prefix}Body", + NpgsqlDbType = NpgsqlDbType.Text, + Value = message.Body.Value + } }; } @@ -318,7 +336,7 @@ protected override IEnumerable MapListFunction(DbDataReader dr) protected override async Task> MapListFunctionAsync( DbDataReader dr, CancellationToken cancellationToken - ) + ) { var messages = new List(); while (await dr.ReadAsync(cancellationToken)) @@ -331,19 +349,34 @@ CancellationToken cancellationToken return messages; } - protected override async Task MapOutstandingCountAsync(DbDataReader dr, CancellationToken cancellationToken) + protected override async Task MapOutstandingCountAsync(DbDataReader dr, + CancellationToken cancellationToken) { int outstandingMessages = -1; if (await dr.ReadAsync(cancellationToken)) { outstandingMessages = dr.GetInt32(0); } + + await dr.CloseAsync(); + + return outstandingMessages; + } + + protected override int MapOutstandingCount(DbDataReader dr) + { + int outstandingMessages = -1; + if (dr.Read()) + { + outstandingMessages = dr.GetInt32(0); + } + dr.Close(); return outstandingMessages; } - public Message MapAMessage(DbDataReader dr) + private Message MapAMessage(DbDataReader dr) { var id = GetMessageId(dr); var messageType = GetMessageType(dr); @@ -378,12 +411,12 @@ public Message MapAMessage(DbDataReader dr) var body = _configuration.BinaryMessagePayload ? new MessageBody(((NpgsqlDataReader)dr).GetFieldValue(dr.GetOrdinal("Body"))) - :new MessageBody(dr.GetString(dr.GetOrdinal("Body"))); + : new MessageBody(dr.GetString(dr.GetOrdinal("Body"))); return new Message(header, body); } - private string GetContentType(DbDataReader dr) + private static string GetContentType(DbDataReader dr) { var ordinal = dr.GetOrdinal("ContentType"); if (dr.IsDBNull(ordinal)) @@ -402,7 +435,7 @@ private static Dictionary GetContextBag(DbDataReader dr) return dictionaryBag; } - private string GetCorrelationId(DbDataReader dr) + private static string GetCorrelationId(DbDataReader dr) { var ordinal = dr.GetOrdinal("CorrelationId"); if (dr.IsDBNull(ordinal)) @@ -427,7 +460,7 @@ private static string GetMessageId(DbDataReader dr) return dr.GetString(dr.GetOrdinal("MessageId")); } - private string GetPartitionKey(DbDataReader dr) + private static string GetPartitionKey(DbDataReader dr) { var ordinal = dr.GetOrdinal("PartitionKey"); if (dr.IsDBNull(ordinal)) return null; @@ -436,7 +469,7 @@ private string GetPartitionKey(DbDataReader dr) return partitionKey; } - private string GetReplyTo(DbDataReader dr) + private static string GetReplyTo(DbDataReader dr) { var ordinal = dr.GetOrdinal("ReplyTo"); if (dr.IsDBNull(ordinal)) diff --git a/src/Paramore.Brighter.Outbox.PostgreSql/PostgreSqlOutboxBulder.cs b/src/Paramore.Brighter.Outbox.PostgreSql/PostgreSqlOutboxBuilder.cs similarity index 98% rename from src/Paramore.Brighter.Outbox.PostgreSql/PostgreSqlOutboxBulder.cs rename to src/Paramore.Brighter.Outbox.PostgreSql/PostgreSqlOutboxBuilder.cs index 9a168d4d02..4ccffa29f4 100644 --- a/src/Paramore.Brighter.Outbox.PostgreSql/PostgreSqlOutboxBulder.cs +++ b/src/Paramore.Brighter.Outbox.PostgreSql/PostgreSqlOutboxBuilder.cs @@ -27,7 +27,7 @@ namespace Paramore.Brighter.Outbox.PostgreSql /// /// Provide SQL statement helpers for creation of an Outbox /// - public class PostgreSqlOutboxBulder + public class PostgreSqlOutboxBuilder { const string TextOutboxDdl = @" CREATE TABLE {0} diff --git a/src/Paramore.Brighter.Outbox.PostgreSql/PostgreSqlQueries.cs b/src/Paramore.Brighter.Outbox.PostgreSql/PostgreSqlQueries.cs index 7d8cf45082..2372b233ab 100644 --- a/src/Paramore.Brighter.Outbox.PostgreSql/PostgreSqlQueries.cs +++ b/src/Paramore.Brighter.Outbox.PostgreSql/PostgreSqlQueries.cs @@ -2,17 +2,17 @@ { public class PostgreSqlQueries : IRelationDatabaseOutboxQueries { - public string PagedDispatchedCommand { get; } = "SELECT * FROM (SELECT ROW_NUMBER() OVER(ORDER BY Timestamp DESC) AS NUMBER, * FROM {0}) AS TBL WHERE DISPATCHED IS NOT NULL AND DISPATCHED < (CURRENT_TIMESTAMP + (@OutstandingSince || ' millisecond')::INTERVAL) AND NUMBER BETWEEN ((@PageNumber-1)*@PageSize+1) AND (@PageNumber*@PageSize) ORDER BY Timestamp DESC"; - public string PagedReadCommand { get; } = "SELECT * FROM (SELECT ROW_NUMBER() OVER(ORDER BY Timestamp ASC) AS NUMBER, * FROM {0}) AS TBL WHERE NUMBER BETWEEN ((@PageNumber-1)*@PageSize+1) AND (@PageNumber*@PageSize) ORDER BY Timestamp ASC"; - public string PagedOutstandingCommand { get; } = "SELECT * FROM (SELECT ROW_NUMBER() OVER(ORDER BY Timestamp ASC) AS NUMBER, * FROM {0} WHERE DISPATCHED IS NULL) AS TBL WHERE TIMESTAMP < (CURRENT_TIMESTAMP + (@OutstandingSince || ' millisecond')::INTERVAL) AND NUMBER BETWEEN ((@PageNumber-1)*@PageSize+1) AND (@PageNumber*@PageSize) ORDER BY Timestamp ASC"; - public string AddCommand { get; } = "INSERT INTO {0} (MessageId, MessageType, Topic, Timestamp, CorrelationId, ReplyTo, ContentType, PartitionKey, HeaderBag, Body) VALUES (@MessageId, @MessageType, @Topic, @Timestamp::timestamptz, @CorrelationId, @ReplyTo, @ContentType, @PartitionKey, @HeaderBag, @Body)"; - public string BulkAddCommand { get; } = "INSERT INTO {0} (MessageId, MessageType, Topic, Timestamp, CorrelationId, ReplyTo, ContentType, PartitionKey, HeaderBag, Body) VALUES {1}"; - public string MarkDispatchedCommand { get; } = "UPDATE {0} SET Dispatched = @DispatchedAt WHERE MessageId = @MessageId"; - public string MarkMultipleDispatchedCommand { get; } = "UPDATE {0} SET Dispatched = @DispatchedAt WHERE MessageId IN ( {1} )"; - public string GetMessageCommand { get; } = "SELECT * FROM {0} WHERE MessageId = @MessageId"; - public string GetMessagesCommand { get; } = "SELECT * FROM {0} WHERE MessageID IN ( {1} )ORDER BY Timestamp ASC"; - public string DeleteMessagesCommand { get; }= "DELETE FROM {0} WHERE MessageId IN ( {1} )"; - public string DispatchedCommand { get; }="SELECT * FROM {0} WHERE Dispatched IS NOT NULL AND Dispatched < (CURRENT_TIMESTAMP - INTERVAL '@DispatchedSince' HOUR) ORDER BY Dispatched LIMIT @PageSize;"; - public string GetNumberOfOutstandingMessagesCommand => "SELECT COUNT(*) FROM {0} WHERE Dispatched IS NULL"; + // All column are created with lower case because during table creating we didn't scape colum + public string PagedDispatchedCommand { get; } = "SELECT * FROM {0} WHERE \"dispatched\" IS NOT NULL AND \"dispatched\" < @DispatchedSince ORDER BY \"timestamp\" DESC LIMIT @Take OFFSET @Skip"; + public string PagedReadCommand { get; } = "SELECT * FROM {0} ORDER BY \"timestamp\" ASC LIMIT @Take OFFSET @Skip"; + public string PagedOutstandingCommand { get; } = "SELECT * FROM {0} WHERE \"dispatched\" IS NULL AND \"timestamp\" < @TimestampSince ORDER BY \"timestamp\" DESC LIMIT @Take OFFSET @Skip"; + public string AddCommand { get; } = "INSERT INTO {0} (\"messageid\", \"messagetype\", \"topic\", \"timestamp\", \"correlationid\", \"replyto\", \"contenttype\", \"partitionkey\", \"headerbag\", \"body\") VALUES (@MessageId, @MessageType, @Topic, @Timestamp, @CorrelationId, @ReplyTo, @ContentType, @PartitionKey, @HeaderBag, @Body)"; + public string BulkAddCommand { get; } = "INSERT INTO {0} (\"messageid\", \"messagetype\", \"topic\", \"timestamp\", \"correlationid\", \"replyto\", \"contenttype\", \"partitionkey\", \"headerbag\", \"body\") VALUES {1}"; + public string MarkDispatchedCommand { get; } = "UPDATE {0} SET \"dispatched\" = @DispatchedAt WHERE \"messageid\" = @MessageId"; + public string MarkMultipleDispatchedCommand { get; } = "UPDATE {0} SET \"dispatched\" = @DispatchedAt WHERE \"messageid\" IN ( {1} )"; + public string GetMessageCommand { get; } = "SELECT * FROM {0} WHERE \"messageid\" = @MessageId"; + public string GetMessagesCommand { get; } = "SELECT * FROM {0} WHERE \"messageid\" IN ( {1} ) ORDER BY \"timestamp\" ASC"; + public string DeleteMessagesCommand { get; } = "DELETE FROM {0} WHERE \"messageid\" IN ( {1} )"; + public string GetNumberOfOutstandingMessagesCommand { get; } = "SELECT COUNT(1) FROM {0} WHERE \"dispatched\" IS NULL"; } } diff --git a/src/Paramore.Brighter.Outbox.Sqlite/SqliteOutbox.cs b/src/Paramore.Brighter.Outbox.Sqlite/SqliteOutbox.cs index 9f2c32f65b..9ad00882c0 100644 --- a/src/Paramore.Brighter.Outbox.Sqlite/SqliteOutbox.cs +++ b/src/Paramore.Brighter.Outbox.Sqlite/SqliteOutbox.cs @@ -52,7 +52,8 @@ public class SqliteOutbox : RelationDatabaseOutbox /// /// The configuration to connect to this data store /// Provides a connection to the Db that allows us to enlist in an ambient transaction - public SqliteOutbox(IAmARelationalDatabaseConfiguration configuration, IAmARelationalDbConnectionProvider connectionProvider) + public SqliteOutbox(IAmARelationalDatabaseConfiguration configuration, + IAmARelationalDbConnectionProvider connectionProvider) : base(configuration.OutBoxTableName, new SqliteQueries(), ApplicationLogging.CreateLogger()) { _configuration = configuration; @@ -64,7 +65,7 @@ public SqliteOutbox(IAmARelationalDatabaseConfiguration configuration, IAmARelat /// Initializes a new instance of the class. /// /// The configuration to connect to this data store - public SqliteOutbox(IAmARelationalDatabaseConfiguration configuration) + public SqliteOutbox(IAmARelationalDatabaseConfiguration configuration) : this(configuration, new SqliteConnectionProvider(configuration)) { } @@ -73,7 +74,7 @@ protected override void WriteToStore( IAmABoxTransactionProvider transactionProvider, Func commandFunc, Action loggingAction - ) + ) { var connection = GetOpenConnection(_connectionProvider, transactionProvider); using var command = commandFunc.Invoke(connection); @@ -87,7 +88,6 @@ Action loggingAction { if (!IsExceptionUnqiueOrDuplicateIssue(sqlException)) throw; loggingAction.Invoke(); - } finally { @@ -98,11 +98,11 @@ Action loggingAction protected override async Task WriteToStoreAsync( IAmABoxTransactionProvider transactionProvider, Func commandFunc, - Action loggingAction, + Action loggingAction, CancellationToken cancellationToken) { var connection = await GetOpenConnectionAsync(_connectionProvider, transactionProvider, cancellationToken); - using var command = commandFunc.Invoke(connection); + await using var command = commandFunc.Invoke(connection); try { if (transactionProvider != null && transactionProvider.HasOpenTransaction) @@ -113,7 +113,6 @@ protected override async Task WriteToStoreAsync( { if (!IsExceptionUnqiueOrDuplicateIssue(sqlException)) throw; loggingAction.Invoke(); - } finally { @@ -131,7 +130,7 @@ protected override async Task WriteToStoreAsync( protected override T ReadFromStore( Func commandFunc, Func resultFunc - ) + ) { var connection = _connectionProvider.GetConnection(); @@ -150,7 +149,7 @@ Func resultFunc protected override async Task ReadFromStoreAsync( Func commandFunc, - Func> resultFunc, + Func> resultFunc, CancellationToken cancellationToken) { var connection = await _connectionProvider.GetConnectionAsync(cancellationToken); @@ -171,10 +170,10 @@ protected override async Task ReadFromStoreAsync( #endif } } - - protected override DbCommand CreateCommand( - DbConnection connection, - string sqlText, + + protected override DbCommand CreateCommand( + DbConnection connection, + string sqlText, int outBoxTimeout, params IDbDataParameter[] parameters) { @@ -187,15 +186,33 @@ protected override DbCommand CreateCommand( return command; } - protected override IDbDataParameter[] CreatePagedOutstandingParameters( - double milliSecondsSinceAdded, - int pageSize, int pageNumber - ) + protected override IDbDataParameter[] CreatePagedOutstandingParameters(TimeSpan since, int pageSize, + int pageNumber) + { + var parameters = new IDbDataParameter[3]; + parameters[0] = CreateSqlParameter("TimestampSince", DateTimeOffset.UtcNow.Subtract(since)); + parameters[1] = CreateSqlParameter("Take", pageSize); + parameters[2] = CreateSqlParameter("Skip", Math.Max(pageNumber - 1, 0) * pageSize); + + return parameters; + } + + protected override IDbDataParameter[] CreatePagedDispatchedParameters(TimeSpan dispatchedSince, int pageSize, + int pageNumber) { var parameters = new IDbDataParameter[3]; - parameters[0] = CreateSqlParameter("PageNumber", pageNumber); - parameters[1] = CreateSqlParameter("PageSize", pageSize); - parameters[2] = CreateSqlParameter("OutstandingSince", milliSecondsSinceAdded); + parameters[0] = CreateSqlParameter("DispatchedSince", DateTimeOffset.UtcNow.Subtract(dispatchedSince)); + parameters[1] = CreateSqlParameter("Take", pageSize); + parameters[2] = CreateSqlParameter("Skip", Math.Max(pageNumber - 1, 0) * pageSize); + + return parameters; + } + + protected override IDbDataParameter[] CreatePagedReadParameters(int pageSize, int pageNumber) + { + var parameters = new IDbDataParameter[2]; + parameters[0] = CreateSqlParameter("Take", pageSize); + parameters[1] = CreateSqlParameter("Skip", Math.Max(pageNumber - 1, 0) * pageSize); return parameters; } @@ -213,9 +230,7 @@ protected override IDbDataParameter[] InitAddDbParameters(Message message, int? { new SqliteParameter { - ParameterName = $"@{prefix}MessageId", - SqliteType = SqliteType.Text, - Value = message.Id + ParameterName = $"@{prefix}MessageId", SqliteType = SqliteType.Text, Value = message.Id }, new SqliteParameter { @@ -225,13 +240,16 @@ protected override IDbDataParameter[] InitAddDbParameters(Message message, int? }, new SqliteParameter { - ParameterName = $"@{prefix}Topic", SqliteType = SqliteType.Text, Value = message.Header.Topic.Value + ParameterName = $"@{prefix}Topic", + SqliteType = SqliteType.Text, + Value = message.Header.Topic.Value }, new SqliteParameter { ParameterName = $"@{prefix}Timestamp", SqliteType = SqliteType.Text, - Value = message.Header.TimeStamp.ToString("yyyy-MM-dd HH:mm:ss.fff", CultureInfo.InvariantCulture) + Value = + message.Header.TimeStamp.ToString("yyyy-MM-dd HH:mm:ss.fff", CultureInfo.InvariantCulture) }, new SqliteParameter { @@ -312,7 +330,8 @@ protected override IEnumerable MapListFunction(DbDataReader dr) return messages; } - protected override async Task> MapListFunctionAsync(DbDataReader dr, CancellationToken cancellationToken) + protected override async Task> MapListFunctionAsync(DbDataReader dr, + CancellationToken cancellationToken) { var messages = new List(); while (await dr.ReadAsync(cancellationToken)) @@ -320,18 +339,32 @@ protected override async Task> MapListFunctionAsync(DbDataR messages.Add(MapAMessage(dr)); } - dr.Close(); + await dr.CloseAsync(); return messages; } - protected override async Task MapOutstandingCountAsync(DbDataReader dr, CancellationToken cancellationToken) + protected override async Task MapOutstandingCountAsync(DbDataReader dr, + CancellationToken cancellationToken) { int outstandingMessages = -1; if (await dr.ReadAsync(cancellationToken)) { outstandingMessages = dr.GetInt32(0); } + + await dr.CloseAsync(); + return outstandingMessages; + } + + protected override int MapOutstandingCount(DbDataReader dr) + { + int outstandingMessages = -1; + if ( dr.Read()) + { + outstandingMessages = dr.GetInt32(0); + } + dr.Close(); return outstandingMessages; } @@ -390,7 +423,7 @@ private Message MapAMessage(IDataReader dr) } - private byte[] GetBodyAsBytes(DbDataReader dr) + private static byte[] GetBodyAsBytes(DbDataReader dr) { var i = dr.GetOrdinal("Body"); var body = dr.GetStream(i); @@ -416,8 +449,8 @@ private string GetContentType(IDataReader dr) var contentType = dr.GetString(ordinal); return contentType; } - - private string GetCorrelationId(IDataReader dr) + + private static string GetCorrelationId(IDataReader dr) { var ordinal = dr.GetOrdinal("CorrelationId"); if (dr.IsDBNull(ordinal)) return null; @@ -436,7 +469,7 @@ private static string GetMessageId(IDataReader dr) return dr.GetString(dr.GetOrdinal("MessageId")); } - private string GetPartitionKey(IDataReader dr) + private static string GetPartitionKey(IDataReader dr) { var ordinal = dr.GetOrdinal("PartitionKey"); if (dr.IsDBNull(ordinal)) return null; @@ -446,7 +479,7 @@ private string GetPartitionKey(IDataReader dr) } - private string GetReplyTo(IDataReader dr) + private static string GetReplyTo(IDataReader dr) { var ordinal = dr.GetOrdinal("ReplyTo"); if (dr.IsDBNull(ordinal)) return null; diff --git a/src/Paramore.Brighter.Outbox.Sqlite/SqliteQueries.cs b/src/Paramore.Brighter.Outbox.Sqlite/SqliteQueries.cs index 8c17f6ed49..694bf91862 100644 --- a/src/Paramore.Brighter.Outbox.Sqlite/SqliteQueries.cs +++ b/src/Paramore.Brighter.Outbox.Sqlite/SqliteQueries.cs @@ -2,17 +2,16 @@ { public class SqliteQueries : IRelationDatabaseOutboxQueries { - public string PagedDispatchedCommand { get; } = "SELECT * FROM {0} WHERE DISPATCHED IS NOT NULL AND (strftime('%s', 'now') - strftime('%s', Dispatched)) * 1000 < @OutstandingSince ORDER BY Timestamp ASC LIMIT @PageSize OFFSET (@PageNumber-1) * @PageSize"; - public string PagedReadCommand { get; } = "SELECT * FROM {0} ORDER BY Timestamp ASC LIMIT @PageSize OFFSET (@PageNumber-1) * @PageSize"; - public string PagedOutstandingCommand { get; } = "SELECT * FROM {0} WHERE DISPATCHED IS NULL AND (strftime('%s', 'now') - strftime('%s', TimeStamp)) * 1000 > @OutstandingSince ORDER BY Timestamp ASC LIMIT @PageSize OFFSET (@PageNumber-1) * @PageSize"; - public string AddCommand { get; } = "INSERT INTO {0} (MessageId, MessageType, Topic, Timestamp, CorrelationId, ReplyTo, ContentType, PartitionKey, HeaderBag, Body) VALUES (@MessageId, @MessageType, @Topic, @Timestamp, @CorrelationId, @ReplyTo, @ContentType, @PartitionKey, @HeaderBag, @Body)"; - public string BulkAddCommand { get; } = "INSERT INTO {0} (MessageId, MessageType, Topic, Timestamp, CorrelationId, ReplyTo, ContentType, PartitionKey, HeaderBag, Body) VALUES {1}"; - public string MarkDispatchedCommand { get; } = "UPDATE {0} SET Dispatched = @DispatchedAt WHERE MessageId = @MessageId"; - public string MarkMultipleDispatchedCommand { get; } = "UPDATE {0} SET Dispatched = @DispatchedAt WHERE MessageId in ( {1} )"; - public string GetMessageCommand { get; } = "SELECT * FROM {0} WHERE MessageId = @MessageId"; - public string GetMessagesCommand { get; } = "SELECT * FROM {0} WHERE MessageId IN ( {1} )"; - public string DeleteMessagesCommand { get; } = "DELETE FROM {0} WHERE MessageId IN ( {1} )"; - public string DispatchedCommand { get; } = "Select top(@PageSize) * FROM {0} WHERE Dispatched is not NULL and Dispatched < DATEADD(hour, @DispatchedSince, getutcdate()) Order BY Dispatched"; - public string GetNumberOfOutstandingMessagesCommand { get; } = "Select count(1) FROM {0} where Dispatched is NULL"; + public string PagedDispatchedCommand { get; } = "SELECT * FROM {0} WHERE [Dispatched] IS NOT NULL AND [Dispatched] < @DispatchedSince ORDER BY [Timestamp] DESC LIMIT @Take OFFSET @Skip"; + public string PagedReadCommand { get; } = "SELECT * FROM {0} ORDER BY [Timestamp] ASC LIMIT @Take OFFSET @Skip"; + public string PagedOutstandingCommand { get; } = "SELECT * FROM {0} WHERE [Dispatched] IS NULL AND `Timestamp` < @TimestampSince ORDER BY Timestamp DESC LIMIT @Take OFFSET @Skip"; + public string AddCommand { get; } = "INSERT INTO {0} ([MessageId], [MessageType], [Topic], [Timestamp], [CorrelationId], [ReplyTo], [ContentType], [PartitionKey], [HeaderBag], [Body]) VALUES (@MessageId, @MessageType, @Topic, @Timestamp, @CorrelationId, @ReplyTo, @ContentType, @PartitionKey, @HeaderBag, @Body)"; + public string BulkAddCommand { get; } = "INSERT INTO {0} ([MessageId], [MessageType], [Topic], [Timestamp], [CorrelationId], [ReplyTo], [ContentType], [PartitionKey], [HeaderBag], [Body]) VALUES {1}"; + public string MarkDispatchedCommand { get; } = "UPDATE {0} SET [Dispatched] = @DispatchedAt WHERE [MessageId] = @MessageId"; + public string MarkMultipleDispatchedCommand { get; } = "UPDATE {0} SET [Dispatched] = @DispatchedAt WHERE [MessageId] IN ( {1} )"; + public string GetMessageCommand { get; } = "SELECT * FROM {0} WHERE [MessageId] = @MessageId"; + public string GetMessagesCommand { get; } = "SELECT * FROM {0} WHERE [MessageId] IN ( {1} ) ORDER BY [Timestamp] ASC"; + public string DeleteMessagesCommand { get; } = "DELETE FROM {0} WHERE [MessageId] IN ( {1} )"; + public string GetNumberOfOutstandingMessagesCommand { get; } = "SELECT COUNT(1) FROM {0} WHERE [Dispatched] IS NULL"; } } diff --git a/src/Paramore.Brighter/IAmAnOutboxSync.cs b/src/Paramore.Brighter/IAmAnOutboxSync.cs index 716302e92d..49294d9067 100644 --- a/src/Paramore.Brighter/IAmAnOutboxSync.cs +++ b/src/Paramore.Brighter/IAmAnOutboxSync.cs @@ -70,7 +70,7 @@ public interface IAmAnOutboxSync : IAmAnOutbox where T : Messag /// What is the context for this request; used to access the Span /// The number of messages to fetch. /// The page number. - /// Timeout of sql call. + /// Timeout of sql call. /// Additional parameters required for search, if any /// List of messages that need to be dispatched. IEnumerable DispatchedMessages( @@ -78,7 +78,7 @@ IEnumerable DispatchedMessages( RequestContext requestContext, int pageSize = 100, int pageNumber = 1, - int outboxTimeout = -1, + int outBoxTimeout = -1, Dictionary? args = null); /// diff --git a/src/Paramore.Brighter/IRelationDatabaseOutboxQueries.cs b/src/Paramore.Brighter/IRelationDatabaseOutboxQueries.cs index 6c809bfc9d..79e51956f5 100644 --- a/src/Paramore.Brighter/IRelationDatabaseOutboxQueries.cs +++ b/src/Paramore.Brighter/IRelationDatabaseOutboxQueries.cs @@ -12,7 +12,6 @@ public interface IRelationDatabaseOutboxQueries string GetMessageCommand { get; } string GetMessagesCommand { get; } string DeleteMessagesCommand { get; } - string DispatchedCommand { get; } string GetNumberOfOutstandingMessagesCommand { get; } } } diff --git a/src/Paramore.Brighter/InMemoryOutbox.cs b/src/Paramore.Brighter/InMemoryOutbox.cs index 6e54fa33f4..8354408584 100644 --- a/src/Paramore.Brighter/InMemoryOutbox.cs +++ b/src/Paramore.Brighter/InMemoryOutbox.cs @@ -272,7 +272,7 @@ public Task DeleteAsync( /// What is the context for this request; used to access the Span /// How many messages in a page /// Which page of messages to get - /// + /// /// Additional parameters required for search, if any /// A list of dispatched messages public IEnumerable DispatchedMessages( @@ -280,7 +280,7 @@ public IEnumerable DispatchedMessages( RequestContext requestContext, int pageSize = 100, int pageNumber = 1, - int outboxTimeout = -1, + int outBoxTimeout = -1, Dictionary? args = null) { ClearExpiredMessages(); diff --git a/src/Paramore.Brighter/RelationDatabaseOutbox.cs b/src/Paramore.Brighter/RelationDatabaseOutbox.cs index 1a35b9814e..42ab9ae5a1 100644 --- a/src/Paramore.Brighter/RelationDatabaseOutbox.cs +++ b/src/Paramore.Brighter/RelationDatabaseOutbox.cs @@ -24,13 +24,13 @@ public abstract class RelationDatabaseOutbox( /// thread specific storage such as HTTPContext /// public bool ContinueOnCapturedContext { get; set; } - + /// /// The Tracer that we want to use to capture telemetry /// We inject this so that we can use the same tracer as the calling application /// You do not need to set this property as we will set it when setting up the External Service Bus /// - public IAmABrighterTracer? Tracer { private get; set; } + public IAmABrighterTracer? Tracer { private get; set; } /// /// Adds the specified message. @@ -41,7 +41,7 @@ public abstract class RelationDatabaseOutbox( /// Connection Provider to use for this call /// Task. public void Add( - Message message, + Message message, RequestContext requestContext, int outBoxTimeout = -1, IAmABoxTransactionProvider? transactionProvider = null) @@ -60,15 +60,15 @@ public void Add( /// /// The message. /// What is the context for this request; used to access the Span - /// + /// How long to wait for the message before timing out /// Connection Provider to use for this call /// Task. public void Add( - IEnumerable messages, + IEnumerable messages, RequestContext? requestContext, int outBoxTimeout = -1, IAmABoxTransactionProvider? transactionProvider = null - ) + ) { WriteToStore(transactionProvider, connection => InitBulkAddDbCommand(messages.ToList(), connection), @@ -80,7 +80,7 @@ public void Add( /// /// The message. /// What is the context for this request; used to access the Span - /// + /// How long to wait for the message before timing out /// Connection Provider to use for this call /// Cancellation Token /// Task<Message>. @@ -132,7 +132,7 @@ public Task AddAsync( /// Additional parameters required for search, if any public void Delete(string[] messageIds, RequestContext? requestContext, Dictionary? args = null) { - if(messageIds.Any()) + if (messageIds.Any()) WriteToStore(null, connection => InitDeleteDispatchedCommand(connection, messageIds), null); } @@ -144,14 +144,14 @@ public void Delete(string[] messageIds, RequestContext? requestContext, Dictiona /// Additional parameters required for search, if any /// The Cancellation Token public Task DeleteAsync( - string[] messageIds, + string[] messageIds, RequestContext? requestContext, Dictionary? args = null, CancellationToken cancellationToken = default) { - if(!messageIds.Any()) + if (!messageIds.Any()) return Task.CompletedTask; - + return WriteToStoreAsync(null, connection => InitDeleteDispatchedCommand(connection, messageIds), null, cancellationToken); } @@ -159,7 +159,7 @@ public Task DeleteAsync( /// /// Retrieves messages that have been sent within the window /// - /// How long ago would the message have been dispatched in milliseconds + /// How long ago would the message have been dispatched. /// What is the context for this request; used to access the Span /// How many messages in a page /// Which page of messages to get @@ -172,16 +172,38 @@ public Task> DispatchedMessagesAsync( RequestContext? requestContext, int pageSize = 100, int pageNumber = 1, - int outboxTimeout = -1, + int outboxTimeout = 0, Dictionary? args = null, CancellationToken cancellationToken = default) { return ReadFromStoreAsync( connection => - CreatePagedDispatchedCommand(connection, dispatchedSince, pageSize, pageNumber), + CreatePagedDispatchedCommand(connection, dispatchedSince, pageSize, pageNumber, outboxTimeout), dr => MapListFunctionAsync(dr, cancellationToken), cancellationToken); } + /// + /// Get the messages that have been dispatched + /// + /// The number of hours since the message was dispatched + /// What is the context for this request; used to access the Span + /// The amount to return + /// Additional parameters required for search, if any + /// The Cancellation Token + /// Messages that have already been dispatched + public Task> DispatchedMessagesAsync( + int hoursDispatchedSince, + RequestContext? requestContext, + int pageSize = 100, + Dictionary? args = null, + CancellationToken cancellationToken = default) + { + return DispatchedMessagesAsync(TimeSpan.FromHours(hoursDispatchedSince), + requestContext, + pageSize, + args: args, cancellationToken: cancellationToken); + } + /// /// Retrieves messages that have been sent within the window /// @@ -189,7 +211,7 @@ public Task> DispatchedMessagesAsync( /// What is the context for this request; used to access the Span /// How many messages in a page /// Which page of messages to get - /// + /// /// Additional parameters required for search, if any /// A list of dispatched messages public IEnumerable DispatchedMessages( @@ -197,33 +219,54 @@ public IEnumerable DispatchedMessages( RequestContext? requestContext, int pageSize = 100, int pageNumber = 1, - int outboxTimeout = -1, + int outBoxTimeout = 0, Dictionary? args = null) { return ReadFromStore( connection => - CreatePagedDispatchedCommand(connection, dispatchedSince, pageSize, pageNumber), - dr => MapListFunction(dr)); + CreatePagedDispatchedCommand(connection, dispatchedSince, pageSize, pageNumber, outBoxTimeout), + MapListFunction); } - + /// - /// Get the messages that have been dispatched + /// Retrieves messages that have been sent within the window /// - /// The number of hours since the message was dispatched + /// The number of hours since the message was dispatched /// What is the context for this request; used to access the Span - /// The amount to return + /// How many messages in a page + /// Which page of messages to get + /// How long to wait for the message before timing out /// Additional parameters required for search, if any - /// The Cancellation Token - /// Messages that have already been dispatched - public Task> DispatchedMessagesAsync( - int millisecondsDispatchedSince, + /// A list of dispatched messages + public IEnumerable DispatchedMessages( + int hoursDispatchedSince, RequestContext? requestContext, int pageSize = 100, - Dictionary? args = null, - CancellationToken cancellationToken = default) + int pageNumber = 1, + int outBoxTimeout = 0, + Dictionary? args = null) { - return ReadFromStoreAsync(connection => CreateDispatchedCommand(connection, millisecondsDispatchedSince, pageSize), - dr => MapListFunctionAsync(dr, cancellationToken), cancellationToken); + return ReadFromStore( + connection => + CreatePagedDispatchedCommand(connection, TimeSpan.FromHours(hoursDispatchedSince), pageSize, + pageNumber, outBoxTimeout), + MapListFunction); + } + + /// + /// Gets the specified message + /// + /// The Ids of the messages + /// What is the context for this request; used to access the Span + /// How long to wait for the message before timing out + /// For outboxes that require additional parameters such as topic, provide an optional arg + /// The message + public IEnumerable Get(IEnumerable messageIds, RequestContext requestContext, + int outBoxTimeout = -1, + Dictionary? args = null) + { + return ReadFromStore(connection => InitGetMessagesCommand(connection, messageIds.ToList(), outBoxTimeout), + MapListFunction); } /// @@ -234,10 +277,11 @@ public Task> DispatchedMessagesAsync( /// How long to wait for the message before timing out /// For outboxes that require additional parameters such as topic, provide an optional arg /// The message - public Message Get(string messageId, RequestContext requestContext, int outBoxTimeout = -1, Dictionary? args = null) + public Message Get(string messageId, RequestContext requestContext, int outBoxTimeout = -1, + Dictionary? args = null) { var message = ReadFromStore(connection => InitGetMessageCommand(connection, messageId, outBoxTimeout), - dr => MapFunction(dr)); + MapFunction); return message; } @@ -258,7 +302,8 @@ public async Task GetAsync( Dictionary? args = null, CancellationToken cancellationToken = default) { - var message = await ReadFromStoreAsync(connection => InitGetMessageCommand(connection, messageId, outBoxTimeout), + var message = await ReadFromStoreAsync( + connection => InitGetMessageCommand(connection, messageId, outBoxTimeout), dr => MapFunctionAsync(dr, cancellationToken), cancellationToken); return message; @@ -273,11 +318,11 @@ public async Task GetAsync( /// Cancellation Token. /// public Task> GetAsync( - IEnumerable messageIds, + IEnumerable messageIds, RequestContext requestContext, int outBoxTimeout = -1, CancellationToken cancellationToken = default - ) + ) { return ReadFromStoreAsync( connection => InitGetMessagesCommand(connection, messageIds.ToList(), outBoxTimeout), @@ -294,7 +339,7 @@ public Task> GetAsync( public IList Get(int pageSize = 100, int pageNumber = 1, Dictionary? args = null) { return ReadFromStore(connection => CreatePagedReadCommand(connection, pageSize, pageNumber), - dr => MapListFunction(dr)).ToList(); + MapListFunction).ToList(); } /// @@ -320,13 +365,21 @@ public async Task> GetAsync( /// /// Cancel the async operation /// - public Task GetNumberOfOutstandingMessagesAsync(CancellationToken cancellationToken) + public Task GetNumberOfOutstandingMessagesAsync(CancellationToken cancellationToken = default) { - return ReadFromStoreAsync( - connection => CreateRemainingOutstandingCommand(connection), + return ReadFromStoreAsync(CreateRemainingOutstandingCommand, dr => MapOutstandingCountAsync(dr, cancellationToken), cancellationToken); } + /// + /// Get the number of messages in the Outbox that are not dispatched + /// + /// + public int GetNumberOfOutstandingMessages() + { + return ReadFromStore(CreateRemainingOutstandingCommand, MapOutstandingCount); + } + /// /// Update a message to show it is dispatched @@ -344,7 +397,7 @@ public Task MarkDispatchedAsync( CancellationToken cancellationToken = default) { return WriteToStoreAsync(null, - connection => InitMarkDispatchedCommand(connection, id, dispatchedAt ?? DateTime.UtcNow), null, + connection => InitMarkDispatchedCommand(connection, id, dispatchedAt ?? DateTimeOffset.UtcNow), null, cancellationToken); } @@ -375,7 +428,8 @@ public Task MarkDispatchedAsync( /// What is the context for this request; used to access the Span /// When was the message dispatched, defaults to UTC now /// Allows additional arguments to be provided for specific Outbox Db providers - public void MarkDispatched(string id, RequestContext requestContext, DateTimeOffset? dispatchedAt = null, Dictionary? args = null) + public void MarkDispatched(string id, RequestContext requestContext, DateTimeOffset? dispatchedAt = null, + Dictionary? args = null) { WriteToStore(null, connection => InitMarkDispatchedCommand(connection, id, dispatchedAt ?? DateTime.UtcNow), null); @@ -398,8 +452,8 @@ public IEnumerable OutstandingMessages( Dictionary? args = null) { return ReadFromStore( - connection => CreatePagedOutstandingCommand(connection, dispatchedSince, pageSize, pageNumber), - dr => MapListFunction(dr)); + connection => CreatePagedOutstandingCommand(connection, dispatchedSince, pageSize, pageNumber, -1), + MapListFunction); } /// @@ -421,33 +475,33 @@ public Task> OutstandingMessagesAsync( CancellationToken cancellationToken = default) { return ReadFromStoreAsync( - connection => CreatePagedOutstandingCommand(connection, dispatchedSince, pageSize, pageNumber), + connection => CreatePagedOutstandingCommand(connection, dispatchedSince, pageSize, pageNumber, -1), dr => MapListFunctionAsync(dr, cancellationToken), cancellationToken); } protected abstract void WriteToStore( IAmABoxTransactionProvider? transactionProvider, - Func commandFunc, + Func commandFunc, Action? loggingAction - ); + ); protected abstract Task WriteToStoreAsync( IAmABoxTransactionProvider? transactionProvider, - Func commandFunc, - Action? loggingAction, + Func commandFunc, + Action? loggingAction, CancellationToken cancellationToken - ); + ); protected abstract T ReadFromStore( Func commandFunc, Func resultFunc - ); + ); protected abstract Task ReadFromStoreAsync( Func commandFunc, - Func> resultFunc, + Func> resultFunc, CancellationToken cancellationToken - ); + ); protected DbConnection GetOpenConnection(IAmARelationalDbConnectionProvider defaultConnectionProvider, IAmABoxTransactionProvider? transactionProvider) @@ -472,8 +526,9 @@ protected void FinishWrite(DbConnection connection, else connection.Close(); } - - protected async Task GetOpenConnectionAsync(IAmARelationalDbConnectionProvider defaultConnectionProvider, + + protected async Task GetOpenConnectionAsync( + IAmARelationalDbConnectionProvider defaultConnectionProvider, IAmABoxTransactionProvider? transactionProvider, CancellationToken cancellationToken) { var connectionProvider = defaultConnectionProvider; @@ -489,43 +544,38 @@ protected async Task GetOpenConnectionAsync(IAmARelationalDbConnec } private DbCommand CreatePagedDispatchedCommand( - DbConnection connection, + DbConnection connection, TimeSpan timeDispatchedSince, - int pageSize, - int pageNumber) - => CreateCommand(connection, GenerateSqlText(queries.PagedDispatchedCommand), 0, - CreateSqlParameter("PageNumber", pageNumber), CreateSqlParameter("PageSize", pageSize), - CreateSqlParameter("OutstandingSince", -1 * Convert.ToInt32(timeDispatchedSince.TotalMilliseconds))); - - private DbCommand CreateDispatchedCommand(DbConnection connection, int hoursDispatchedSince, int pageSize) - => CreateCommand(connection, GenerateSqlText(queries.DispatchedCommand), 0, - CreateSqlParameter("PageSize", pageSize), - CreateSqlParameter("DispatchedSince", -1 * hoursDispatchedSince) - ); + int pageSize, + int pageNumber, + int outboxTimeout) + => CreateCommand(connection, GenerateSqlText(queries.PagedDispatchedCommand), outboxTimeout, + CreatePagedDispatchedParameters(timeDispatchedSince, pageSize, pageNumber)); private DbCommand CreatePagedReadCommand( - DbConnection connection, - int pageSize, + DbConnection connection, + int pageSize, int pageNumber - ) + ) => CreateCommand(connection, GenerateSqlText(queries.PagedReadCommand), 0, - CreateSqlParameter("PageNumber", pageNumber), CreateSqlParameter("PageSize", pageSize)); + CreatePagedReadParameters(pageSize, pageNumber)); private DbCommand CreatePagedOutstandingCommand( - DbConnection connection, + DbConnection connection, TimeSpan timeSinceAdded, - int pageSize, - int pageNumber) - => CreateCommand(connection, GenerateSqlText(queries.PagedOutstandingCommand), 0, - CreatePagedOutstandingParameters(Convert.ToInt32(timeSinceAdded.Milliseconds), pageSize, pageNumber)); - + int pageSize, + int pageNumber, + int outboxTimeout) + => CreateCommand(connection, GenerateSqlText(queries.PagedOutstandingCommand), outboxTimeout, + CreatePagedOutstandingParameters(timeSinceAdded, pageSize, pageNumber)); + private DbCommand CreateRemainingOutstandingCommand(DbConnection connection) => CreateCommand(connection, GenerateSqlText(queries.GetNumberOfOutstandingMessagesCommand), 0); private DbCommand InitAddDbCommand( - DbConnection connection, + DbConnection connection, IDbDataParameter[] parameters - ) + ) => CreateCommand(connection, GenerateSqlText(queries.AddCommand), 0, parameters); private DbCommand InitBulkAddDbCommand(List messages, DbConnection connection) @@ -535,7 +585,8 @@ private DbCommand InitBulkAddDbCommand(List messages, DbConnection conn insertClause.parameters); } - private DbCommand InitMarkDispatchedCommand(DbConnection connection, string messageId, DateTimeOffset? dispatchedAt) + private DbCommand InitMarkDispatchedCommand(DbConnection connection, string messageId, + DateTimeOffset? dispatchedAt) => CreateCommand(connection, GenerateSqlText(queries.MarkDispatchedCommand), 0, CreateSqlParameter("MessageId", messageId), CreateSqlParameter("DispatchedAt", dispatchedAt?.ToUniversalTime())); @@ -544,7 +595,8 @@ private DbCommand InitMarkDispatchedCommand(DbConnection connection, IEnumerable DateTimeOffset? dispatchedAt) { var inClause = GenerateInClauseAndAddParameters(messageIds.ToList()); - return CreateCommand(connection, GenerateSqlText(queries.MarkMultipleDispatchedCommand, inClause.inClause), 0, + return CreateCommand(connection, GenerateSqlText(queries.MarkMultipleDispatchedCommand, inClause.inClause), + 0, inClause.parameters.Append(CreateSqlParameter("DispatchedAt", dispatchedAt?.ToUniversalTime())) .ToArray()); } @@ -553,10 +605,12 @@ private DbCommand InitGetMessageCommand(DbConnection connection, string messageI => CreateCommand(connection, GenerateSqlText(queries.GetMessageCommand), outBoxTimeout, CreateSqlParameter("MessageId", messageId)); - private DbCommand InitGetMessagesCommand(DbConnection connection, List messageIds, int outBoxTimeout = -1) + private DbCommand InitGetMessagesCommand(DbConnection connection, List messageIds, + int outBoxTimeout = -1) { var inClause = GenerateInClauseAndAddParameters(messageIds); - return CreateCommand(connection, GenerateSqlText(queries.GetMessagesCommand, inClause.inClause), outBoxTimeout, + return CreateCommand(connection, GenerateSqlText(queries.GetMessagesCommand, inClause.inClause), + outBoxTimeout, inClause.parameters); } @@ -574,8 +628,13 @@ protected abstract DbCommand CreateCommand(DbConnection connection, string sqlTe params IDbDataParameter[] parameters); - protected abstract IDbDataParameter[] CreatePagedOutstandingParameters(double milliSecondsSinceAdded, - int pageSize, int pageNumber); + protected abstract IDbDataParameter[] CreatePagedOutstandingParameters(TimeSpan since, int pageSize, + int pageNumber); + + protected abstract IDbDataParameter[] CreatePagedDispatchedParameters(TimeSpan dispatchedSince, int pageSize, + int pageNumber); + + protected abstract IDbDataParameter[] CreatePagedReadParameters(int pageSize, int pageNumber); protected abstract IDbDataParameter CreateSqlParameter(string parameterName, object? value); protected abstract IDbDataParameter[] InitAddDbParameters(Message message, int? position = null); @@ -586,11 +645,14 @@ protected abstract IDbDataParameter[] CreatePagedOutstandingParameters(double mi protected abstract IEnumerable MapListFunction(DbDataReader dr); - protected abstract Task> MapListFunctionAsync(DbDataReader dr, CancellationToken cancellationToken); - + protected abstract Task> MapListFunctionAsync(DbDataReader dr, + CancellationToken cancellationToken); + protected abstract Task MapOutstandingCountAsync(DbDataReader dr, CancellationToken cancellationToken); - - private (string inClause, IDbDataParameter[] parameters) GenerateInClauseAndAddParameters(List messageIds) + protected abstract int MapOutstandingCount(DbDataReader dr); + + private (string inClause, IDbDataParameter[] parameters) GenerateInClauseAndAddParameters( + List messageIds) { var paramNames = messageIds.Select((s, i) => "@p" + i).ToArray(); @@ -603,19 +665,19 @@ protected abstract IDbDataParameter[] CreatePagedOutstandingParameters(double mi return (string.Join(",", paramNames), parameters); } - private (string insertClause, IDbDataParameter[] parameters) GenerateBulkInsert(List messages) + private (string insertClause, IDbDataParameter[] parameters) GenerateBulkInsert(List messages) { var messageParams = new List(); var parameters = new List(); for (int i = 0; i < messages.Count(); i++) { - messageParams.Add($"(@p{i}_MessageId, @p{i}_MessageType, @p{i}_Topic, @p{i}_Timestamp, @p{i}_CorrelationId, @p{i}_ReplyTo, @p{i}_ContentType, @p{i}_PartitionKey, @p{i}_HeaderBag, @p{i}_Body)"); + messageParams.Add( + $"(@p{i}_MessageId, @p{i}_MessageType, @p{i}_Topic, @p{i}_Timestamp, @p{i}_CorrelationId, @p{i}_ReplyTo, @p{i}_ContentType, @p{i}_PartitionKey, @p{i}_HeaderBag, @p{i}_Body)"); parameters.AddRange(InitAddDbParameters(messages[i], i)); } return (string.Join(",", messageParams), parameters.ToArray()); } - } } diff --git a/tests/Paramore.Brighter.MSSQL.Tests/Outbox/When_retrieving_messages.cs b/tests/Paramore.Brighter.MSSQL.Tests/Outbox/When_retrieving_messages.cs new file mode 100644 index 0000000000..ab36484711 --- /dev/null +++ b/tests/Paramore.Brighter.MSSQL.Tests/Outbox/When_retrieving_messages.cs @@ -0,0 +1,90 @@ +using System; +using System.Linq; +using FluentAssertions; +using Paramore.Brighter.Outbox.MsSql; +using Xunit; + +namespace Paramore.Brighter.MSSQL.Tests.Outbox; + +[Trait("Category", "MSSQL")] +public class MsSqlFetchMessageTests : IDisposable +{ + private readonly MsSqlTestHelper _msSqlTestHelper; + private readonly Message _messageEarliest; + private readonly Message _messageDispatched; + private readonly Message _messageUnDispatched; + private readonly MsSqlOutbox _sqlOutbox; + + public MsSqlFetchMessageTests() + { + _msSqlTestHelper = new MsSqlTestHelper(); + _msSqlTestHelper.SetupMessageDb(); + + _sqlOutbox = new MsSqlOutbox(_msSqlTestHelper.OutboxConfiguration); + var routingKey = new RoutingKey("test_topic"); + + _messageEarliest = new Message( + new MessageHeader(Guid.NewGuid().ToString(), routingKey, MessageType.MT_DOCUMENT), + new MessageBody("message body")); + _messageDispatched = new Message( + new MessageHeader(Guid.NewGuid().ToString(), routingKey, MessageType.MT_DOCUMENT), + new MessageBody("message body")); + _messageUnDispatched = new Message( + new MessageHeader(Guid.NewGuid().ToString(), routingKey, MessageType.MT_DOCUMENT), + new MessageBody("message body")); + } + + [Fact] + public void When_Retrieving_Messages() + { + var context = new RequestContext(); + _sqlOutbox.Add([_messageEarliest, _messageDispatched, _messageUnDispatched], context); + _sqlOutbox.MarkDispatched(_messageEarliest.Id, context, DateTime.UtcNow.AddHours(-3)); + _sqlOutbox.MarkDispatched(_messageDispatched.Id, context); + + var messages = _sqlOutbox.Get(); + + //Assert + messages.Should().HaveCount(3); + } + + [Fact] + public void When_Retrieving_Messages_By_Id() + { + var context = new RequestContext(); + _sqlOutbox.Add([_messageEarliest, _messageDispatched, _messageUnDispatched], context); + _sqlOutbox.MarkDispatched(_messageEarliest.Id, context, DateTime.UtcNow.AddHours(-3)); + _sqlOutbox.MarkDispatched(_messageDispatched.Id, context); + + var messages = _sqlOutbox.Get( + [_messageEarliest.Id, _messageUnDispatched.Id], + context); + + //Assert + messages = messages.ToList(); + messages.Should().HaveCount(2); + messages.Should().Contain(x => x.Id == _messageEarliest.Id); + messages.Should().Contain(x => x.Id == _messageUnDispatched.Id); + messages.Should().NotContain(x => x.Id == _messageDispatched.Id); + } + + [Fact] + public void When_Retrieving_Message_By_Id() + { + var context = new RequestContext(); + _sqlOutbox.Add([_messageEarliest, _messageDispatched, _messageUnDispatched], context); + _sqlOutbox.MarkDispatched(_messageEarliest.Id, context, DateTime.UtcNow.AddHours(-3)); + _sqlOutbox.MarkDispatched(_messageDispatched.Id, context); + + var messages = _sqlOutbox.Get(_messageDispatched.Id, context); + + //Assert + messages.Id.Should().Be(_messageDispatched.Id); + } + + + public void Dispose() + { + _msSqlTestHelper.CleanUpDb(); + } +} diff --git a/tests/Paramore.Brighter.MSSQL.Tests/Outbox/When_retrieving_messages_async.cs b/tests/Paramore.Brighter.MSSQL.Tests/Outbox/When_retrieving_messages_async.cs new file mode 100644 index 0000000000..d682158d71 --- /dev/null +++ b/tests/Paramore.Brighter.MSSQL.Tests/Outbox/When_retrieving_messages_async.cs @@ -0,0 +1,90 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using FluentAssertions; +using Paramore.Brighter.Outbox.MsSql; +using Xunit; + +namespace Paramore.Brighter.MSSQL.Tests.Outbox; + +[Trait("Category", "MSSQL")] +public class MsSqlFetchMessageAsyncTests : IDisposable +{ + private readonly MsSqlTestHelper _msSqlTestHelper; + private readonly Message _messageEarliest; + private readonly Message _messageDispatched; + private readonly Message _messageUnDispatched; + private readonly MsSqlOutbox _sqlOutbox; + + public MsSqlFetchMessageAsyncTests() + { + _msSqlTestHelper = new MsSqlTestHelper(); + _msSqlTestHelper.SetupMessageDb(); + + _sqlOutbox = new MsSqlOutbox(_msSqlTestHelper.OutboxConfiguration); + var routingKey = new RoutingKey("test_topic"); + + _messageEarliest = new Message( + new MessageHeader(Guid.NewGuid().ToString(), routingKey, MessageType.MT_DOCUMENT), + new MessageBody("message body")); + _messageDispatched = new Message( + new MessageHeader(Guid.NewGuid().ToString(), routingKey, MessageType.MT_DOCUMENT), + new MessageBody("message body")); + _messageUnDispatched = new Message( + new MessageHeader(Guid.NewGuid().ToString(), routingKey, MessageType.MT_DOCUMENT), + new MessageBody("message body")); + } + + [Fact] + public async Task When_Retrieving_Messages_Async() + { + var context = new RequestContext(); + await _sqlOutbox.AddAsync([_messageEarliest, _messageDispatched, _messageUnDispatched], context); + await _sqlOutbox.MarkDispatchedAsync(_messageEarliest.Id, context, DateTime.UtcNow.AddHours(-3)); + await _sqlOutbox.MarkDispatchedAsync(_messageDispatched.Id, context); + + var messages = await _sqlOutbox.GetAsync(); + + //Assert + messages.Should().HaveCount(3); + } + + [Fact] + public async Task When_Retrieving_Messages_By_Id_Async() + { + var context = new RequestContext(); + await _sqlOutbox.AddAsync([_messageEarliest, _messageDispatched, _messageUnDispatched], context); + await _sqlOutbox.MarkDispatchedAsync(_messageEarliest.Id, context, DateTime.UtcNow.AddHours(-3)); + await _sqlOutbox.MarkDispatchedAsync(_messageDispatched.Id, context); + + var messages = await _sqlOutbox.GetAsync( + [_messageEarliest.Id, _messageUnDispatched.Id], + context); + + //Assert + messages = messages.ToList(); + messages.Should().HaveCount(2); + messages.Should().Contain(x => x.Id == _messageEarliest.Id); + messages.Should().Contain(x => x.Id == _messageUnDispatched.Id); + messages.Should().NotContain(x => x.Id == _messageDispatched.Id); + } + + [Fact] + public async Task When_Retrieving_Message_By_Id_Async() + { + var context = new RequestContext(); + await _sqlOutbox.AddAsync([_messageEarliest, _messageDispatched, _messageUnDispatched], context); + await _sqlOutbox.MarkDispatchedAsync(_messageEarliest.Id, context, DateTime.UtcNow.AddHours(-3)); + await _sqlOutbox.MarkDispatchedAsync(_messageDispatched.Id, context); + + var messages = await _sqlOutbox.GetAsync(_messageDispatched.Id, context); + + //Assert + messages.Id.Should().Be(_messageDispatched.Id); + } + + public void Dispose() + { + _msSqlTestHelper.CleanUpDb(); + } +} diff --git a/tests/Paramore.Brighter.MSSQL.Tests/Outbox/When_retrieving_messages_to_archive.cs b/tests/Paramore.Brighter.MSSQL.Tests/Outbox/When_retrieving_messages_to_archive.cs new file mode 100644 index 0000000000..493e2a2cf6 --- /dev/null +++ b/tests/Paramore.Brighter.MSSQL.Tests/Outbox/When_retrieving_messages_to_archive.cs @@ -0,0 +1,76 @@ +using System; +using FluentAssertions; +using Paramore.Brighter.Outbox.MsSql; +using Xunit; + +namespace Paramore.Brighter.MSSQL.Tests.Outbox; + +[Trait("Category", "MSSQL")] +public class MsSqlArchiveFetchTests : IDisposable +{ + private readonly MsSqlTestHelper _msSqlTestHelper; + private readonly Message _messageEarliest; + private readonly Message _messageDispatched; + private readonly Message _messageUnDispatched; + private readonly MsSqlOutbox _sqlOutbox; + + public MsSqlArchiveFetchTests() + { + _msSqlTestHelper = new MsSqlTestHelper(); + _msSqlTestHelper.SetupMessageDb(); + + _sqlOutbox = new MsSqlOutbox(_msSqlTestHelper.OutboxConfiguration); + var routingKey = new RoutingKey("test_topic"); + + _messageEarliest = new Message( + new MessageHeader(Guid.NewGuid().ToString(), routingKey, MessageType.MT_DOCUMENT), + new MessageBody("message body")); + _messageDispatched = new Message( + new MessageHeader(Guid.NewGuid().ToString(), routingKey, MessageType.MT_DOCUMENT), + new MessageBody("message body")); + _messageUnDispatched = new Message( + new MessageHeader(Guid.NewGuid().ToString(), routingKey, MessageType.MT_DOCUMENT), + new MessageBody("message body")); + } + + [Fact] + public void When_Retrieving_Messages_To_Archive() + { + var context = new RequestContext(); + _sqlOutbox.Add([_messageEarliest, _messageDispatched, _messageUnDispatched], context); + _sqlOutbox.MarkDispatched(_messageEarliest.Id, context, DateTimeOffset.UtcNow.AddHours(-3)); + _sqlOutbox.MarkDispatched(_messageDispatched.Id, context); + + var allDispatched = _sqlOutbox.DispatchedMessages(0, context); + var messagesOverAnHour = _sqlOutbox.DispatchedMessages(1, context); + var messagesOver4Hours = _sqlOutbox.DispatchedMessages(4, context); + + //Assert + allDispatched.Should().HaveCount(2); + messagesOverAnHour.Should().ContainSingle(); + messagesOver4Hours.Should().BeEmpty(); + } + + [Fact] + public void When_Retrieving_Messages_To_Archive_UsingTimeSpan() + { + var context = new RequestContext(); + _sqlOutbox.Add([_messageEarliest, _messageDispatched, _messageUnDispatched], context); + _sqlOutbox.MarkDispatched(_messageEarliest.Id, context, DateTime.UtcNow.AddHours(-3)); + _sqlOutbox.MarkDispatched(_messageDispatched.Id, context); + + var allDispatched = _sqlOutbox.DispatchedMessages(TimeSpan.Zero, context); + var messagesOverAnHour = _sqlOutbox.DispatchedMessages(TimeSpan.FromHours(2), context); + var messagesOver4Hours = _sqlOutbox.DispatchedMessages(TimeSpan.FromHours(4), context); + + //Assert + allDispatched.Should().HaveCount(2); + messagesOverAnHour.Should().ContainSingle(); + messagesOver4Hours.Should().BeEmpty(); + } + + public void Dispose() + { + _msSqlTestHelper.CleanUpDb(); + } +} diff --git a/tests/Paramore.Brighter.MSSQL.Tests/Outbox/When_Retrieving_Messages_To_Archive.cs b/tests/Paramore.Brighter.MSSQL.Tests/Outbox/When_retrieving_messages_to_archive_async.cs similarity index 50% rename from tests/Paramore.Brighter.MSSQL.Tests/Outbox/When_Retrieving_Messages_To_Archive.cs rename to tests/Paramore.Brighter.MSSQL.Tests/Outbox/When_retrieving_messages_to_archive_async.cs index 94b929c1e7..bea2b1656c 100644 --- a/tests/Paramore.Brighter.MSSQL.Tests/Outbox/When_Retrieving_Messages_To_Archive.cs +++ b/tests/Paramore.Brighter.MSSQL.Tests/Outbox/When_retrieving_messages_to_archive_async.cs @@ -2,22 +2,22 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; +using FluentAssertions; using Paramore.Brighter.Outbox.MsSql; using Xunit; namespace Paramore.Brighter.MSSQL.Tests.Outbox; [Trait("Category", "MSSQL")] -public class MsSqlArchiveFetchTests : IDisposable +public class MsSqlArchiveFetchAsyncTests : IDisposable { - private readonly MsSqlTestHelper _msSqlTestHelper; private readonly Message _messageEarliest; private readonly Message _messageDispatched; private readonly Message _messageUnDispatched; private readonly MsSqlOutbox _sqlOutbox; - public MsSqlArchiveFetchTests() + public MsSqlArchiveFetchAsyncTests() { _msSqlTestHelper = new MsSqlTestHelper(); _msSqlTestHelper.SetupMessageDb(); @@ -33,25 +33,46 @@ public MsSqlArchiveFetchTests() new MessageBody("message body")); } - [Fact] + [Fact] public async Task When_Retrieving_Messages_To_Archive_Async() { var context = new RequestContext(); - await _sqlOutbox.AddAsync(new []{_messageEarliest, _messageDispatched, _messageUnDispatched}, context); + await _sqlOutbox.AddAsync([_messageEarliest, _messageDispatched, _messageUnDispatched], context); await _sqlOutbox.MarkDispatchedAsync(_messageEarliest.Id, context, DateTime.UtcNow.AddHours(-3)); await _sqlOutbox.MarkDispatchedAsync(_messageDispatched.Id, context); - - var allDispatched = - await _sqlOutbox.DispatchedMessagesAsync(0, context, 100, cancellationToken: CancellationToken.None); - var messagesOverAnHour = - await _sqlOutbox.DispatchedMessagesAsync(1,context, 100, cancellationToken: CancellationToken.None); - var messagesOver4Hours = - await _sqlOutbox.DispatchedMessagesAsync(4, context, 100, cancellationToken: CancellationToken.None); - + + var allDispatched = + await _sqlOutbox.DispatchedMessagesAsync(0, context, cancellationToken: CancellationToken.None); + var messagesOverAnHour = + await _sqlOutbox.DispatchedMessagesAsync(1, context, cancellationToken: CancellationToken.None); + var messagesOver4Hours = + await _sqlOutbox.DispatchedMessagesAsync(4, context, cancellationToken: CancellationToken.None); + + //Assert + allDispatched.Should().HaveCount(2); + messagesOverAnHour.Should().ContainSingle(); + messagesOver4Hours.Should().BeEmpty(); + } + + [Fact] + public async Task When_Retrieving_Messages_To_Archive_UsingTimeSpan_Async() + { + var context = new RequestContext(); + await _sqlOutbox.AddAsync([_messageEarliest, _messageDispatched, _messageUnDispatched], context); + await _sqlOutbox.MarkDispatchedAsync(_messageEarliest.Id, context, DateTime.UtcNow.AddHours(-3)); + await _sqlOutbox.MarkDispatchedAsync(_messageDispatched.Id, context); + + var allDispatched = + await _sqlOutbox.DispatchedMessagesAsync(TimeSpan.Zero, context, 100, cancellationToken: CancellationToken.None); + var messagesOverAnHour = + await _sqlOutbox.DispatchedMessagesAsync(TimeSpan.FromHours(2), context, 100, cancellationToken: CancellationToken.None); + var messagesOver4Hours = + await _sqlOutbox.DispatchedMessagesAsync(TimeSpan.FromHours(4), context, 100, cancellationToken: CancellationToken.None); + //Assert - Assert.Equal(2, allDispatched.Count()); - Assert.Single(messagesOverAnHour); - Assert.Empty(messagesOver4Hours); + allDispatched.Should().HaveCount(2); + messagesOverAnHour.Should().ContainSingle(); + messagesOver4Hours.Should().BeEmpty(); } public void Dispose() diff --git a/tests/Paramore.Brighter.MSSQL.Tests/Outbox/When_retrieving_outstanding_messages.cs b/tests/Paramore.Brighter.MSSQL.Tests/Outbox/When_retrieving_outstanding_messages.cs new file mode 100644 index 0000000000..da8735add0 --- /dev/null +++ b/tests/Paramore.Brighter.MSSQL.Tests/Outbox/When_retrieving_outstanding_messages.cs @@ -0,0 +1,63 @@ +using System; +using FluentAssertions; +using Paramore.Brighter.Outbox.MsSql; +using Xunit; + +namespace Paramore.Brighter.MSSQL.Tests.Outbox; + +[Trait("Category", "MSSQL")] +public class MsSqlFetchOutStandingMessageTests : IDisposable +{ + private readonly MsSqlTestHelper _msSqlTestHelper; + private readonly Message _messageEarliest; + private readonly Message _messageDispatched; + private readonly Message _messageUnDispatched; + private readonly MsSqlOutbox _sqlOutbox; + + public MsSqlFetchOutStandingMessageTests() + { + _msSqlTestHelper = new MsSqlTestHelper(); + _msSqlTestHelper.SetupMessageDb(); + + _sqlOutbox = new MsSqlOutbox(_msSqlTestHelper.OutboxConfiguration); + var routingKey = new RoutingKey("test_topic"); + + _messageEarliest = new Message( + new MessageHeader(Guid.NewGuid().ToString(), routingKey, MessageType.MT_DOCUMENT) + { + TimeStamp = DateTimeOffset.UtcNow.AddHours(-3) + }, + new MessageBody("message body")); + _messageDispatched = new Message( + new MessageHeader(Guid.NewGuid().ToString(), routingKey, MessageType.MT_DOCUMENT), + new MessageBody("message body")); + _messageUnDispatched = new Message( + new MessageHeader(Guid.NewGuid().ToString(), routingKey, MessageType.MT_DOCUMENT), + new MessageBody("message body")); + } + + [Fact] + public void When_Retrieving_Not_Dispatched_Messages() + { + var context = new RequestContext(); + _sqlOutbox.Add([_messageEarliest, _messageDispatched, _messageUnDispatched], context); + _sqlOutbox.MarkDispatched(_messageDispatched.Id, context); + + var total = _sqlOutbox.GetNumberOfOutstandingMessages(); + + var allUnDispatched = _sqlOutbox.OutstandingMessages(TimeSpan.Zero, context); + var messagesOverAnHour = _sqlOutbox.OutstandingMessages(TimeSpan.FromHours(1), context); + var messagesOver4Hours = _sqlOutbox.OutstandingMessages(TimeSpan.FromHours(4), context); + + //Assert + total.Should().Be(2); + allUnDispatched.Should().HaveCount(2); + messagesOverAnHour.Should().ContainSingle(); + messagesOver4Hours.Should().BeEmpty(); + } + + public void Dispose() + { + _msSqlTestHelper.CleanUpDb(); + } +} diff --git a/tests/Paramore.Brighter.MSSQL.Tests/Outbox/When_retrieving_outstanding_messages_async.cs b/tests/Paramore.Brighter.MSSQL.Tests/Outbox/When_retrieving_outstanding_messages_async.cs new file mode 100644 index 0000000000..5426ddafe7 --- /dev/null +++ b/tests/Paramore.Brighter.MSSQL.Tests/Outbox/When_retrieving_outstanding_messages_async.cs @@ -0,0 +1,64 @@ +using System; +using System.Threading.Tasks; +using FluentAssertions; +using Paramore.Brighter.Outbox.MsSql; +using Xunit; + +namespace Paramore.Brighter.MSSQL.Tests.Outbox; + +[Trait("Category", "MSSQL")] +public class MsSqlFetchOutStandingMessageAsyncTests : IDisposable +{ + private readonly MsSqlTestHelper _msSqlTestHelper; + private readonly Message _messageEarliest; + private readonly Message _messageDispatched; + private readonly Message _messageUnDispatched; + private readonly MsSqlOutbox _sqlOutbox; + + public MsSqlFetchOutStandingMessageAsyncTests() + { + _msSqlTestHelper = new MsSqlTestHelper(); + _msSqlTestHelper.SetupMessageDb(); + + _sqlOutbox = new MsSqlOutbox(_msSqlTestHelper.OutboxConfiguration); + var routingKey = new RoutingKey("test_topic"); + + _messageEarliest = new Message( + new MessageHeader(Guid.NewGuid().ToString(), routingKey, MessageType.MT_DOCUMENT) + { + TimeStamp = DateTimeOffset.UtcNow.AddHours(-3) + }, + new MessageBody("message body")); + _messageDispatched = new Message( + new MessageHeader(Guid.NewGuid().ToString(), routingKey, MessageType.MT_DOCUMENT), + new MessageBody("message body")); + _messageUnDispatched = new Message( + new MessageHeader(Guid.NewGuid().ToString(), routingKey, MessageType.MT_DOCUMENT), + new MessageBody("message body")); + } + + [Fact] + public async Task When_Retrieving_Not_Dispatched_Messages_Async() + { + var context = new RequestContext(); + await _sqlOutbox.AddAsync([_messageEarliest, _messageDispatched, _messageUnDispatched], context); + await _sqlOutbox.MarkDispatchedAsync(_messageDispatched.Id, context); + + var total = await _sqlOutbox.GetNumberOfOutstandingMessagesAsync(); + + var allUnDispatched = await _sqlOutbox.OutstandingMessagesAsync(TimeSpan.Zero, context); + var messagesOverAnHour = await _sqlOutbox.OutstandingMessagesAsync(TimeSpan.FromHours(1), context); + var messagesOver4Hours = await _sqlOutbox.OutstandingMessagesAsync(TimeSpan.FromHours(4), context); + + //Assert + total.Should().Be(2); + allUnDispatched.Should().HaveCount(2); + messagesOverAnHour.Should().ContainSingle(); + messagesOver4Hours.Should().BeEmpty(); + } + + public void Dispose() + { + _msSqlTestHelper.CleanUpDb(); + } +} diff --git a/tests/Paramore.Brighter.MySQL.Tests/Outbox/When_retrieving_messages.cs b/tests/Paramore.Brighter.MySQL.Tests/Outbox/When_retrieving_messages.cs new file mode 100644 index 0000000000..9db0ded8ac --- /dev/null +++ b/tests/Paramore.Brighter.MySQL.Tests/Outbox/When_retrieving_messages.cs @@ -0,0 +1,91 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using FluentAssertions; +using Paramore.Brighter.Outbox.MySql; +using Xunit; + +namespace Paramore.Brighter.MySQL.Tests.Outbox; + +[Trait("Category", "MySql")] +public class MySqlFetchMessageTests : IDisposable +{ + private readonly MySqlTestHelper _mySqlTestHelper; + private readonly Message _messageEarliest; + private readonly Message _messageDispatched; + private readonly Message _messageUnDispatched; + private readonly MySqlOutbox _sqlOutbox; + + public MySqlFetchMessageTests() + { + _mySqlTestHelper = new MySqlTestHelper(); + _mySqlTestHelper.SetupMessageDb(); + + _sqlOutbox = new MySqlOutbox(_mySqlTestHelper.OutboxConfiguration); + var routingKey = new RoutingKey("test_topic"); + + _messageEarliest = new Message( + new MessageHeader(Guid.NewGuid().ToString(), routingKey, MessageType.MT_DOCUMENT), + new MessageBody("message body")); + _messageDispatched = new Message( + new MessageHeader(Guid.NewGuid().ToString(), routingKey, MessageType.MT_DOCUMENT), + new MessageBody("message body")); + _messageUnDispatched = new Message( + new MessageHeader(Guid.NewGuid().ToString(), routingKey, MessageType.MT_DOCUMENT), + new MessageBody("message body")); + } + + [Fact] + public void When_Retrieving_Messages() + { + var context = new RequestContext(); + _sqlOutbox.Add([_messageEarliest, _messageDispatched, _messageUnDispatched], context); + _sqlOutbox.MarkDispatched(_messageEarliest.Id, context, DateTime.UtcNow.AddHours(-3)); + _sqlOutbox.MarkDispatched(_messageDispatched.Id, context); + + var messages = _sqlOutbox.Get(); + + //Assert + messages.Should().HaveCount(3); + } + + [Fact] + public void When_Retrieving_Messages_By_Id() + { + var context = new RequestContext(); + _sqlOutbox.Add([_messageEarliest, _messageDispatched, _messageUnDispatched], context); + _sqlOutbox.MarkDispatched(_messageEarliest.Id, context, DateTime.UtcNow.AddHours(-3)); + _sqlOutbox.MarkDispatched(_messageDispatched.Id, context); + + var messages = _sqlOutbox.Get( + [_messageEarliest.Id, _messageUnDispatched.Id], + context); + + //Assert + messages = messages.ToList(); + messages.Should().HaveCount(2); + messages.Should().Contain(x => x.Id == _messageEarliest.Id); + messages.Should().Contain(x => x.Id == _messageUnDispatched.Id); + messages.Should().NotContain(x => x.Id == _messageDispatched.Id); + } + + [Fact] + public void When_Retrieving_Message_By_Id() + { + var context = new RequestContext(); + _sqlOutbox.Add([_messageEarliest, _messageDispatched, _messageUnDispatched], context); + _sqlOutbox.MarkDispatched(_messageEarliest.Id, context, DateTime.UtcNow.AddHours(-3)); + _sqlOutbox.MarkDispatched(_messageDispatched.Id, context); + + var messages = _sqlOutbox.Get(_messageDispatched.Id, context); + + //Assert + messages.Id.Should().Be(_messageDispatched.Id); + } + + + public void Dispose() + { + _mySqlTestHelper.CleanUpDb(); + } +} diff --git a/tests/Paramore.Brighter.MySQL.Tests/Outbox/When_retrieving_messages_async.cs b/tests/Paramore.Brighter.MySQL.Tests/Outbox/When_retrieving_messages_async.cs new file mode 100644 index 0000000000..b9990d9215 --- /dev/null +++ b/tests/Paramore.Brighter.MySQL.Tests/Outbox/When_retrieving_messages_async.cs @@ -0,0 +1,90 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using FluentAssertions; +using Paramore.Brighter.Outbox.MySql; +using Xunit; + +namespace Paramore.Brighter.MySQL.Tests.Outbox; + +[Trait("Category", "MySql")] +public class MySqlFetchMessageAsyncTests : IDisposable +{ + private readonly MySqlTestHelper _mySqlTestHelper; + private readonly Message _messageEarliest; + private readonly Message _messageDispatched; + private readonly Message _messageUnDispatched; + private readonly MySqlOutbox _sqlOutbox; + + public MySqlFetchMessageAsyncTests() + { + _mySqlTestHelper = new MySqlTestHelper(); + _mySqlTestHelper.SetupMessageDb(); + + _sqlOutbox = new MySqlOutbox(_mySqlTestHelper.OutboxConfiguration); + var routingKey = new RoutingKey("test_topic"); + + _messageEarliest = new Message( + new MessageHeader(Guid.NewGuid().ToString(), routingKey, MessageType.MT_DOCUMENT), + new MessageBody("message body")); + _messageDispatched = new Message( + new MessageHeader(Guid.NewGuid().ToString(), routingKey, MessageType.MT_DOCUMENT), + new MessageBody("message body")); + _messageUnDispatched = new Message( + new MessageHeader(Guid.NewGuid().ToString(), routingKey, MessageType.MT_DOCUMENT), + new MessageBody("message body")); + } + + [Fact] + public async Task When_Retrieving_Messages_Async() + { + var context = new RequestContext(); + await _sqlOutbox.AddAsync([_messageEarliest, _messageDispatched, _messageUnDispatched], context); + await _sqlOutbox.MarkDispatchedAsync(_messageEarliest.Id, context, DateTime.UtcNow.AddHours(-3)); + await _sqlOutbox.MarkDispatchedAsync(_messageDispatched.Id, context); + + var messages = await _sqlOutbox.GetAsync(); + + //Assert + messages.Should().HaveCount(3); + } + + [Fact] + public async Task When_Retrieving_Messages_By_Id_Async() + { + var context = new RequestContext(); + await _sqlOutbox.AddAsync([_messageEarliest, _messageDispatched, _messageUnDispatched], context); + await _sqlOutbox.MarkDispatchedAsync(_messageEarliest.Id, context, DateTime.UtcNow.AddHours(-3)); + await _sqlOutbox.MarkDispatchedAsync(_messageDispatched.Id, context); + + var messages = await _sqlOutbox.GetAsync( + [_messageEarliest.Id, _messageUnDispatched.Id], + context); + + //Assert + messages = messages.ToList(); + messages.Should().HaveCount(2); + messages.Should().Contain(x => x.Id == _messageEarliest.Id); + messages.Should().Contain(x => x.Id == _messageUnDispatched.Id); + messages.Should().NotContain(x => x.Id == _messageDispatched.Id); + } + + [Fact] + public async Task When_Retrieving_Message_By_Id_Async() + { + var context = new RequestContext(); + await _sqlOutbox.AddAsync([_messageEarliest, _messageDispatched, _messageUnDispatched], context); + await _sqlOutbox.MarkDispatchedAsync(_messageEarliest.Id, context, DateTime.UtcNow.AddHours(-3)); + await _sqlOutbox.MarkDispatchedAsync(_messageDispatched.Id, context); + + var messages = await _sqlOutbox.GetAsync(_messageDispatched.Id, context); + + //Assert + messages.Id.Should().Be(_messageDispatched.Id); + } + + public void Dispose() + { + _mySqlTestHelper.CleanUpDb(); + } +} diff --git a/tests/Paramore.Brighter.MySQL.Tests/Outbox/When_retrieving_messages_to_archive.cs b/tests/Paramore.Brighter.MySQL.Tests/Outbox/When_retrieving_messages_to_archive.cs new file mode 100644 index 0000000000..1c7ef7ea58 --- /dev/null +++ b/tests/Paramore.Brighter.MySQL.Tests/Outbox/When_retrieving_messages_to_archive.cs @@ -0,0 +1,79 @@ +using System; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; +using FluentAssertions; +using Paramore.Brighter.Outbox.MySql; +using Xunit; + +namespace Paramore.Brighter.MySQL.Tests.Outbox; + +[Trait("Category", "MySql")] +public class MySqlArchiveFetchTests : IDisposable +{ + private readonly MySqlTestHelper _mySqlTestHelper; + private readonly Message _messageEarliest; + private readonly Message _messageDispatched; + private readonly Message _messageUnDispatched; + private readonly MySqlOutbox _sqlOutbox; + + public MySqlArchiveFetchTests() + { + _mySqlTestHelper = new MySqlTestHelper(); + _mySqlTestHelper.SetupMessageDb(); + + _sqlOutbox = new MySqlOutbox(_mySqlTestHelper.OutboxConfiguration); + var routingKey = new RoutingKey("test_topic"); + + _messageEarliest = new Message( + new MessageHeader(Guid.NewGuid().ToString(), routingKey, MessageType.MT_DOCUMENT), + new MessageBody("message body")); + _messageDispatched = new Message( + new MessageHeader(Guid.NewGuid().ToString(), routingKey, MessageType.MT_DOCUMENT), + new MessageBody("message body")); + _messageUnDispatched = new Message( + new MessageHeader(Guid.NewGuid().ToString(), routingKey, MessageType.MT_DOCUMENT), + new MessageBody("message body")); + } + + [Fact] + public void When_Retrieving_Messages_To_Archive() + { + var context = new RequestContext(); + _sqlOutbox.Add([_messageEarliest, _messageDispatched, _messageUnDispatched], context); + _sqlOutbox.MarkDispatched(_messageEarliest.Id, context, DateTimeOffset.UtcNow.AddHours(-3)); + _sqlOutbox.MarkDispatched(_messageDispatched.Id, context); + + var allDispatched = _sqlOutbox.DispatchedMessages(0, context); + var messagesOverAnHour = _sqlOutbox.DispatchedMessages(1, context); + var messagesOver4Hours = _sqlOutbox.DispatchedMessages(4, context); + + //Assert + allDispatched.Should().HaveCount(2); + messagesOverAnHour.Should().ContainSingle(); + messagesOver4Hours.Should().BeEmpty(); + } + + [Fact] + public void When_Retrieving_Messages_To_Archive_UsingTimeSpan() + { + var context = new RequestContext(); + _sqlOutbox.Add([_messageEarliest, _messageDispatched, _messageUnDispatched], context); + _sqlOutbox.MarkDispatched(_messageEarliest.Id, context, DateTime.UtcNow.AddHours(-3)); + _sqlOutbox.MarkDispatched(_messageDispatched.Id, context); + + var allDispatched = _sqlOutbox.DispatchedMessages(TimeSpan.Zero, context); + var messagesOverAnHour = _sqlOutbox.DispatchedMessages(TimeSpan.FromHours(2), context); + var messagesOver4Hours = _sqlOutbox.DispatchedMessages(TimeSpan.FromHours(4), context); + + //Assert + allDispatched.Should().HaveCount(2); + messagesOverAnHour.Should().ContainSingle(); + messagesOver4Hours.Should().BeEmpty(); + } + + public void Dispose() + { + _mySqlTestHelper.CleanUpDb(); + } +} diff --git a/tests/Paramore.Brighter.MySQL.Tests/Outbox/When_retrieving_messages_to_archive_async.cs b/tests/Paramore.Brighter.MySQL.Tests/Outbox/When_retrieving_messages_to_archive_async.cs new file mode 100644 index 0000000000..796b951199 --- /dev/null +++ b/tests/Paramore.Brighter.MySQL.Tests/Outbox/When_retrieving_messages_to_archive_async.cs @@ -0,0 +1,84 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using FluentAssertions; +using Paramore.Brighter.Outbox.MySql; +using Xunit; + +namespace Paramore.Brighter.MySQL.Tests.Outbox; + +[Trait("Category", "MySql")] +public class MySqlArchiveFetchAsyncTests : IDisposable +{ + private readonly MySqlTestHelper _mySqlTestHelper; + private readonly Message _messageEarliest; + private readonly Message _messageDispatched; + private readonly Message _messageUnDispatched; + private readonly MySqlOutbox _sqlOutbox; + + public MySqlArchiveFetchAsyncTests() + { + _mySqlTestHelper = new MySqlTestHelper(); + _mySqlTestHelper.SetupMessageDb(); + + _sqlOutbox = new MySqlOutbox(_mySqlTestHelper.OutboxConfiguration); + var routingKey = new RoutingKey("test_topic"); + + _messageEarliest = new Message( + new MessageHeader(Guid.NewGuid().ToString(), routingKey, MessageType.MT_DOCUMENT), + new MessageBody("message body")); + _messageDispatched = new Message( + new MessageHeader(Guid.NewGuid().ToString(), routingKey, MessageType.MT_DOCUMENT), + new MessageBody("message body")); + _messageUnDispatched = new Message( + new MessageHeader(Guid.NewGuid().ToString(), routingKey, MessageType.MT_DOCUMENT), + new MessageBody("message body")); + } + + [Fact] + public async Task When_Retrieving_Messages_To_Archive_Async() + { + var context = new RequestContext(); + await _sqlOutbox.AddAsync([_messageEarliest, _messageDispatched, _messageUnDispatched], context); + await _sqlOutbox.MarkDispatchedAsync(_messageEarliest.Id, context, DateTime.UtcNow.AddHours(-3)); + await _sqlOutbox.MarkDispatchedAsync(_messageDispatched.Id, context); + + var allDispatched = + await _sqlOutbox.DispatchedMessagesAsync(0, context, cancellationToken: CancellationToken.None); + var messagesOverAnHour = + await _sqlOutbox.DispatchedMessagesAsync(1, context, cancellationToken: CancellationToken.None); + var messagesOver4Hours = + await _sqlOutbox.DispatchedMessagesAsync(4, context, cancellationToken: CancellationToken.None); + + //Assert + allDispatched.Should().HaveCount(2); + messagesOverAnHour.Should().ContainSingle(); + messagesOver4Hours.Should().BeEmpty(); + } + + [Fact] + public async Task When_Retrieving_Messages_To_Archive_UsingTimeSpan_Async() + { + var context = new RequestContext(); + await _sqlOutbox.AddAsync([_messageEarliest, _messageDispatched, _messageUnDispatched], context); + await _sqlOutbox.MarkDispatchedAsync(_messageEarliest.Id, context, DateTime.UtcNow.AddHours(-3)); + await _sqlOutbox.MarkDispatchedAsync(_messageDispatched.Id, context); + + var allDispatched = + await _sqlOutbox.DispatchedMessagesAsync(TimeSpan.Zero, context, 100, cancellationToken: CancellationToken.None); + var messagesOverAnHour = + await _sqlOutbox.DispatchedMessagesAsync(TimeSpan.FromHours(2), context, 100, cancellationToken: CancellationToken.None); + var messagesOver4Hours = + await _sqlOutbox.DispatchedMessagesAsync(TimeSpan.FromHours(4), context, 100, cancellationToken: CancellationToken.None); + + //Assert + allDispatched.Should().HaveCount(2); + messagesOverAnHour.Should().ContainSingle(); + messagesOver4Hours.Should().BeEmpty(); + } + + public void Dispose() + { + _mySqlTestHelper.CleanUpDb(); + } +} diff --git a/tests/Paramore.Brighter.MySQL.Tests/Outbox/When_retrieving_outstanding_messages.cs b/tests/Paramore.Brighter.MySQL.Tests/Outbox/When_retrieving_outstanding_messages.cs new file mode 100644 index 0000000000..07814a978a --- /dev/null +++ b/tests/Paramore.Brighter.MySQL.Tests/Outbox/When_retrieving_outstanding_messages.cs @@ -0,0 +1,65 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using FluentAssertions; +using Paramore.Brighter.Outbox.MySql; +using Xunit; + +namespace Paramore.Brighter.MySQL.Tests.Outbox; + +[Trait("Category", "MySql")] +public class MySqlFetchOutStandingMessageTests : IDisposable +{ + private readonly MySqlTestHelper _mySqlTestHelper; + private readonly Message _messageEarliest; + private readonly Message _messageDispatched; + private readonly Message _messageUnDispatched; + private readonly MySqlOutbox _sqlOutbox; + + public MySqlFetchOutStandingMessageTests() + { + _mySqlTestHelper = new MySqlTestHelper(); + _mySqlTestHelper.SetupMessageDb(); + + _sqlOutbox = new MySqlOutbox(_mySqlTestHelper.OutboxConfiguration); + var routingKey = new RoutingKey("test_topic"); + + _messageEarliest = new Message( + new MessageHeader(Guid.NewGuid().ToString(), routingKey, MessageType.MT_DOCUMENT) + { + TimeStamp = DateTimeOffset.UtcNow.AddHours(-3) + }, + new MessageBody("message body")); + _messageDispatched = new Message( + new MessageHeader(Guid.NewGuid().ToString(), routingKey, MessageType.MT_DOCUMENT), + new MessageBody("message body")); + _messageUnDispatched = new Message( + new MessageHeader(Guid.NewGuid().ToString(), routingKey, MessageType.MT_DOCUMENT), + new MessageBody("message body")); + } + + [Fact] + public void When_Retrieving_Not_Dispatched_Messages() + { + var context = new RequestContext(); + _sqlOutbox.Add([_messageEarliest, _messageDispatched, _messageUnDispatched], context); + _sqlOutbox.MarkDispatched(_messageDispatched.Id, context); + + var total = _sqlOutbox.GetNumberOfOutstandingMessages(); + + var allUnDispatched = _sqlOutbox.OutstandingMessages(TimeSpan.Zero, context); + var messagesOverAnHour = _sqlOutbox.OutstandingMessages(TimeSpan.FromHours(1), context); + var messagesOver4Hours = _sqlOutbox.OutstandingMessages(TimeSpan.FromHours(4), context); + + //Assert + total.Should().Be(2); + allUnDispatched.Should().HaveCount(2); + messagesOverAnHour.Should().ContainSingle(); + messagesOver4Hours.Should().BeEmpty(); + } + + public void Dispose() + { + _mySqlTestHelper.CleanUpDb(); + } +} diff --git a/tests/Paramore.Brighter.MySQL.Tests/Outbox/When_retrieving_outstanding_messages_async.cs b/tests/Paramore.Brighter.MySQL.Tests/Outbox/When_retrieving_outstanding_messages_async.cs new file mode 100644 index 0000000000..b4ac2969c5 --- /dev/null +++ b/tests/Paramore.Brighter.MySQL.Tests/Outbox/When_retrieving_outstanding_messages_async.cs @@ -0,0 +1,65 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using FluentAssertions; +using Paramore.Brighter.Outbox.MySql; +using Xunit; + +namespace Paramore.Brighter.MySQL.Tests.Outbox; + +[Trait("Category", "MySql")] +public class MySqlFetchOutStandingMessageAsyncTests : IDisposable +{ + private readonly MySqlTestHelper _mySqlTestHelper; + private readonly Message _messageEarliest; + private readonly Message _messageDispatched; + private readonly Message _messageUnDispatched; + private readonly MySqlOutbox _sqlOutbox; + + public MySqlFetchOutStandingMessageAsyncTests() + { + _mySqlTestHelper = new MySqlTestHelper(); + _mySqlTestHelper.SetupMessageDb(); + + _sqlOutbox = new MySqlOutbox(_mySqlTestHelper.OutboxConfiguration); + var routingKey = new RoutingKey("test_topic"); + + _messageEarliest = new Message( + new MessageHeader(Guid.NewGuid().ToString(), routingKey, MessageType.MT_DOCUMENT) + { + TimeStamp = DateTimeOffset.UtcNow.AddHours(-3) + }, + new MessageBody("message body")); + _messageDispatched = new Message( + new MessageHeader(Guid.NewGuid().ToString(), routingKey, MessageType.MT_DOCUMENT), + new MessageBody("message body")); + _messageUnDispatched = new Message( + new MessageHeader(Guid.NewGuid().ToString(), routingKey, MessageType.MT_DOCUMENT), + new MessageBody("message body")); + } + + [Fact] + public async Task When_Retrieving_Not_Dispatched_Messages_Async() + { + var context = new RequestContext(); + await _sqlOutbox.AddAsync([_messageEarliest, _messageDispatched, _messageUnDispatched], context); + await _sqlOutbox.MarkDispatchedAsync(_messageDispatched.Id, context); + + var total = await _sqlOutbox.GetNumberOfOutstandingMessagesAsync(); + + var allUnDispatched = await _sqlOutbox.OutstandingMessagesAsync(TimeSpan.Zero, context); + var messagesOverAnHour = await _sqlOutbox.OutstandingMessagesAsync(TimeSpan.FromHours(1), context); + var messagesOver4Hours = await _sqlOutbox.OutstandingMessagesAsync(TimeSpan.FromHours(4), context); + + //Assert + total.Should().Be(2); + allUnDispatched.Should().HaveCount(2); + messagesOverAnHour.Should().ContainSingle(); + messagesOver4Hours.Should().BeEmpty(); + } + + public void Dispose() + { + _mySqlTestHelper.CleanUpDb(); + } +} diff --git a/tests/Paramore.Brighter.PostgresSQL.Tests/Outbox/When_retrieving_messages.cs b/tests/Paramore.Brighter.PostgresSQL.Tests/Outbox/When_retrieving_messages.cs new file mode 100644 index 0000000000..28a3ede7f1 --- /dev/null +++ b/tests/Paramore.Brighter.PostgresSQL.Tests/Outbox/When_retrieving_messages.cs @@ -0,0 +1,90 @@ +using System; +using System.Linq; +using FluentAssertions; +using Paramore.Brighter.Outbox.PostgreSql; +using Xunit; + +namespace Paramore.Brighter.PostgresSQL.Tests.Outbox; + +[Trait("Category", "PostgresSql")] +public class PostgresSqlFetchMessageTests : IDisposable +{ + private readonly PostgresSqlTestHelper _postgresSqlTestHelper; + private readonly Message _messageEarliest; + private readonly Message _messageDispatched; + private readonly Message _messageUnDispatched; + private readonly PostgreSqlOutbox _sqlOutbox; + + public PostgresSqlFetchMessageTests() + { + _postgresSqlTestHelper = new PostgresSqlTestHelper(); + _postgresSqlTestHelper.SetupMessageDb(); + + _sqlOutbox = new PostgreSqlOutbox(_postgresSqlTestHelper.Configuration); + var routingKey = new RoutingKey("test_topic"); + + _messageEarliest = new Message( + new MessageHeader(Guid.NewGuid().ToString(), routingKey, MessageType.MT_DOCUMENT), + new MessageBody("message body")); + _messageDispatched = new Message( + new MessageHeader(Guid.NewGuid().ToString(), routingKey, MessageType.MT_DOCUMENT), + new MessageBody("message body")); + _messageUnDispatched = new Message( + new MessageHeader(Guid.NewGuid().ToString(), routingKey, MessageType.MT_DOCUMENT), + new MessageBody("message body")); + } + + [Fact] + public void When_Retrieving_Messages() + { + var context = new RequestContext(); + _sqlOutbox.Add([_messageEarliest, _messageDispatched, _messageUnDispatched], context); + _sqlOutbox.MarkDispatched(_messageEarliest.Id, context, DateTime.UtcNow.AddHours(-3)); + _sqlOutbox.MarkDispatched(_messageDispatched.Id, context); + + var messages = _sqlOutbox.Get(); + + //Assert + messages.Should().HaveCount(3); + } + + [Fact] + public void When_Retrieving_Messages_By_Id() + { + var context = new RequestContext(); + _sqlOutbox.Add([_messageEarliest, _messageDispatched, _messageUnDispatched], context); + _sqlOutbox.MarkDispatched(_messageEarliest.Id, context, DateTime.UtcNow.AddHours(-3)); + _sqlOutbox.MarkDispatched(_messageDispatched.Id, context); + + var messages = _sqlOutbox.Get( + [_messageEarliest.Id, _messageUnDispatched.Id], + context); + + //Assert + messages = messages.ToList(); + messages.Should().HaveCount(2); + messages.Should().Contain(x => x.Id == _messageEarliest.Id); + messages.Should().Contain(x => x.Id == _messageUnDispatched.Id); + messages.Should().NotContain(x => x.Id == _messageDispatched.Id); + } + + [Fact] + public void When_Retrieving_Message_By_Id() + { + var context = new RequestContext(); + _sqlOutbox.Add([_messageEarliest, _messageDispatched, _messageUnDispatched], context); + _sqlOutbox.MarkDispatched(_messageEarliest.Id, context, DateTime.UtcNow.AddHours(-3)); + _sqlOutbox.MarkDispatched(_messageDispatched.Id, context); + + var messages = _sqlOutbox.Get(_messageDispatched.Id, context); + + //Assert + messages.Id.Should().Be(_messageDispatched.Id); + } + + + public void Dispose() + { + _postgresSqlTestHelper.CleanUpDb(); + } +} diff --git a/tests/Paramore.Brighter.PostgresSQL.Tests/Outbox/When_retrieving_messages_async.cs b/tests/Paramore.Brighter.PostgresSQL.Tests/Outbox/When_retrieving_messages_async.cs new file mode 100644 index 0000000000..3e0aecde60 --- /dev/null +++ b/tests/Paramore.Brighter.PostgresSQL.Tests/Outbox/When_retrieving_messages_async.cs @@ -0,0 +1,90 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using FluentAssertions; +using Paramore.Brighter.Outbox.PostgreSql; +using Xunit; + +namespace Paramore.Brighter.PostgresSQL.Tests.Outbox; + +[Trait("Category", "PostgresSql")] +public class PostgresSqlFetchMessageAsyncTests : IDisposable +{ + private readonly PostgresSqlTestHelper _postgresSqlTestHelper; + private readonly Message _messageEarliest; + private readonly Message _messageDispatched; + private readonly Message _messageUnDispatched; + private readonly PostgreSqlOutbox _sqlOutbox; + + public PostgresSqlFetchMessageAsyncTests() + { + _postgresSqlTestHelper = new PostgresSqlTestHelper(); + _postgresSqlTestHelper.SetupMessageDb(); + + _sqlOutbox = new PostgreSqlOutbox(_postgresSqlTestHelper.Configuration); + var routingKey = new RoutingKey("test_topic"); + + _messageEarliest = new Message( + new MessageHeader(Guid.NewGuid().ToString(), routingKey, MessageType.MT_DOCUMENT), + new MessageBody("message body")); + _messageDispatched = new Message( + new MessageHeader(Guid.NewGuid().ToString(), routingKey, MessageType.MT_DOCUMENT), + new MessageBody("message body")); + _messageUnDispatched = new Message( + new MessageHeader(Guid.NewGuid().ToString(), routingKey, MessageType.MT_DOCUMENT), + new MessageBody("message body")); + } + + [Fact] + public async Task When_Retrieving_Messages_Async() + { + var context = new RequestContext(); + await _sqlOutbox.AddAsync([_messageEarliest, _messageDispatched, _messageUnDispatched], context); + await _sqlOutbox.MarkDispatchedAsync(_messageEarliest.Id, context, DateTime.UtcNow.AddHours(-3)); + await _sqlOutbox.MarkDispatchedAsync(_messageDispatched.Id, context); + + var messages = await _sqlOutbox.GetAsync(); + + //Assert + messages.Should().HaveCount(3); + } + + [Fact] + public async Task When_Retrieving_Messages_By_Id_Async() + { + var context = new RequestContext(); + await _sqlOutbox.AddAsync([_messageEarliest, _messageDispatched, _messageUnDispatched], context); + await _sqlOutbox.MarkDispatchedAsync(_messageEarliest.Id, context, DateTime.UtcNow.AddHours(-3)); + await _sqlOutbox.MarkDispatchedAsync(_messageDispatched.Id, context); + + var messages = await _sqlOutbox.GetAsync( + [_messageEarliest.Id, _messageUnDispatched.Id], + context); + + //Assert + messages = messages.ToList(); + messages.Should().HaveCount(2); + messages.Should().Contain(x => x.Id == _messageEarliest.Id); + messages.Should().Contain(x => x.Id == _messageUnDispatched.Id); + messages.Should().NotContain(x => x.Id == _messageDispatched.Id); + } + + [Fact] + public async Task When_Retrieving_Message_By_Id_Async() + { + var context = new RequestContext(); + await _sqlOutbox.AddAsync([_messageEarliest, _messageDispatched, _messageUnDispatched], context); + await _sqlOutbox.MarkDispatchedAsync(_messageEarliest.Id, context, DateTime.UtcNow.AddHours(-3)); + await _sqlOutbox.MarkDispatchedAsync(_messageDispatched.Id, context); + + var messages = await _sqlOutbox.GetAsync(_messageDispatched.Id, context); + + //Assert + messages.Id.Should().Be(_messageDispatched.Id); + } + + public void Dispose() + { + _postgresSqlTestHelper.CleanUpDb(); + } +} diff --git a/tests/Paramore.Brighter.PostgresSQL.Tests/Outbox/When_retrieving_messages_to_archive.cs b/tests/Paramore.Brighter.PostgresSQL.Tests/Outbox/When_retrieving_messages_to_archive.cs new file mode 100644 index 0000000000..c2aecc9118 --- /dev/null +++ b/tests/Paramore.Brighter.PostgresSQL.Tests/Outbox/When_retrieving_messages_to_archive.cs @@ -0,0 +1,76 @@ +using System; +using FluentAssertions; +using Paramore.Brighter.Outbox.PostgreSql; +using Xunit; + +namespace Paramore.Brighter.PostgresSQL.Tests.Outbox; + +[Trait("Category", "PostgresSql")] +public class PostgresSqlArchiveFetchTests : IDisposable +{ + private readonly PostgresSqlTestHelper _postgresSqlTestHelper; + private readonly Message _messageEarliest; + private readonly Message _messageDispatched; + private readonly Message _messageUnDispatched; + private readonly PostgreSqlOutbox _sqlOutbox; + + public PostgresSqlArchiveFetchTests() + { + _postgresSqlTestHelper = new PostgresSqlTestHelper(); + _postgresSqlTestHelper.SetupMessageDb(); + + _sqlOutbox = new PostgreSqlOutbox(_postgresSqlTestHelper.Configuration); + var routingKey = new RoutingKey("test_topic"); + + _messageEarliest = new Message( + new MessageHeader(Guid.NewGuid().ToString(), routingKey, MessageType.MT_DOCUMENT), + new MessageBody("message body")); + _messageDispatched = new Message( + new MessageHeader(Guid.NewGuid().ToString(), routingKey, MessageType.MT_DOCUMENT), + new MessageBody("message body")); + _messageUnDispatched = new Message( + new MessageHeader(Guid.NewGuid().ToString(), routingKey, MessageType.MT_DOCUMENT), + new MessageBody("message body")); + } + + [Fact] + public void When_Retrieving_Messages_To_Archive() + { + var context = new RequestContext(); + _sqlOutbox.Add([_messageEarliest, _messageDispatched, _messageUnDispatched], context); + _sqlOutbox.MarkDispatched(_messageEarliest.Id, context, DateTimeOffset.UtcNow.AddHours(-3)); + _sqlOutbox.MarkDispatched(_messageDispatched.Id, context); + + var allDispatched = _sqlOutbox.DispatchedMessages(0, context); + var messagesOverAnHour = _sqlOutbox.DispatchedMessages(1, context); + var messagesOver4Hours = _sqlOutbox.DispatchedMessages(4, context); + + //Assert + allDispatched.Should().HaveCount(2); + messagesOverAnHour.Should().ContainSingle(); + messagesOver4Hours.Should().BeEmpty(); + } + + [Fact] + public void When_Retrieving_Messages_To_Archive_UsingTimeSpan() + { + var context = new RequestContext(); + _sqlOutbox.Add([_messageEarliest, _messageDispatched, _messageUnDispatched], context); + _sqlOutbox.MarkDispatched(_messageEarliest.Id, context, DateTime.UtcNow.AddHours(-3)); + _sqlOutbox.MarkDispatched(_messageDispatched.Id, context); + + var allDispatched = _sqlOutbox.DispatchedMessages(TimeSpan.Zero, context); + var messagesOverAnHour = _sqlOutbox.DispatchedMessages(TimeSpan.FromHours(2), context); + var messagesOver4Hours = _sqlOutbox.DispatchedMessages(TimeSpan.FromHours(4), context); + + //Assert + allDispatched.Should().HaveCount(2); + messagesOverAnHour.Should().ContainSingle(); + messagesOver4Hours.Should().BeEmpty(); + } + + public void Dispose() + { + _postgresSqlTestHelper.CleanUpDb(); + } +} diff --git a/tests/Paramore.Brighter.PostgresSQL.Tests/Outbox/When_retrieving_messages_to_archive_async.cs b/tests/Paramore.Brighter.PostgresSQL.Tests/Outbox/When_retrieving_messages_to_archive_async.cs new file mode 100644 index 0000000000..32c0b13d54 --- /dev/null +++ b/tests/Paramore.Brighter.PostgresSQL.Tests/Outbox/When_retrieving_messages_to_archive_async.cs @@ -0,0 +1,84 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using FluentAssertions; +using Paramore.Brighter.Outbox.PostgreSql; +using Xunit; + +namespace Paramore.Brighter.PostgresSQL.Tests.Outbox; + +[Trait("Category", "PostgresSql")] +public class PostgresSqlArchiveFetchAsyncTests : IDisposable +{ + private readonly PostgresSqlTestHelper _postgresSqlTestHelper; + private readonly Message _messageEarliest; + private readonly Message _messageDispatched; + private readonly Message _messageUnDispatched; + private readonly PostgreSqlOutbox _sqlOutbox; + + public PostgresSqlArchiveFetchAsyncTests() + { + _postgresSqlTestHelper = new PostgresSqlTestHelper(); + _postgresSqlTestHelper.SetupMessageDb(); + + _sqlOutbox = new PostgreSqlOutbox(_postgresSqlTestHelper.Configuration); + var routingKey = new RoutingKey("test_topic"); + + _messageEarliest = new Message( + new MessageHeader(Guid.NewGuid().ToString(), routingKey, MessageType.MT_DOCUMENT), + new MessageBody("message body")); + _messageDispatched = new Message( + new MessageHeader(Guid.NewGuid().ToString(), routingKey, MessageType.MT_DOCUMENT), + new MessageBody("message body")); + _messageUnDispatched = new Message( + new MessageHeader(Guid.NewGuid().ToString(), routingKey, MessageType.MT_DOCUMENT), + new MessageBody("message body")); + } + + [Fact] + public async Task When_Retrieving_Messages_To_Archive_Async() + { + var context = new RequestContext(); + await _sqlOutbox.AddAsync([_messageEarliest, _messageDispatched, _messageUnDispatched], context); + await _sqlOutbox.MarkDispatchedAsync(_messageEarliest.Id, context, DateTime.UtcNow.AddHours(-3)); + await _sqlOutbox.MarkDispatchedAsync(_messageDispatched.Id, context); + + var allDispatched = + await _sqlOutbox.DispatchedMessagesAsync(0, context, cancellationToken: CancellationToken.None); + var messagesOverAnHour = + await _sqlOutbox.DispatchedMessagesAsync(1, context, cancellationToken: CancellationToken.None); + var messagesOver4Hours = + await _sqlOutbox.DispatchedMessagesAsync(4, context, cancellationToken: CancellationToken.None); + + //Assert + allDispatched.Should().HaveCount(2); + messagesOverAnHour.Should().ContainSingle(); + messagesOver4Hours.Should().BeEmpty(); + } + + [Fact] + public async Task When_Retrieving_Messages_To_Archive_UsingTimeSpan_Async() + { + var context = new RequestContext(); + await _sqlOutbox.AddAsync([_messageEarliest, _messageDispatched, _messageUnDispatched], context); + await _sqlOutbox.MarkDispatchedAsync(_messageEarliest.Id, context, DateTime.UtcNow.AddHours(-3)); + await _sqlOutbox.MarkDispatchedAsync(_messageDispatched.Id, context); + + var allDispatched = + await _sqlOutbox.DispatchedMessagesAsync(TimeSpan.Zero, context, 100, cancellationToken: CancellationToken.None); + var messagesOverAnHour = + await _sqlOutbox.DispatchedMessagesAsync(TimeSpan.FromHours(2), context, 100, cancellationToken: CancellationToken.None); + var messagesOver4Hours = + await _sqlOutbox.DispatchedMessagesAsync(TimeSpan.FromHours(4), context, 100, cancellationToken: CancellationToken.None); + + //Assert + allDispatched.Should().HaveCount(2); + messagesOverAnHour.Should().ContainSingle(); + messagesOver4Hours.Should().BeEmpty(); + } + + public void Dispose() + { + _postgresSqlTestHelper.CleanUpDb(); + } +} diff --git a/tests/Paramore.Brighter.PostgresSQL.Tests/Outbox/When_retrieving_outstanding_messages.cs b/tests/Paramore.Brighter.PostgresSQL.Tests/Outbox/When_retrieving_outstanding_messages.cs new file mode 100644 index 0000000000..220f30782e --- /dev/null +++ b/tests/Paramore.Brighter.PostgresSQL.Tests/Outbox/When_retrieving_outstanding_messages.cs @@ -0,0 +1,63 @@ +using System; +using FluentAssertions; +using Paramore.Brighter.Outbox.PostgreSql; +using Xunit; + +namespace Paramore.Brighter.PostgresSQL.Tests.Outbox; + +[Trait("Category", "PostgresSql")] +public class PostgresSqlFetchOutStandingMessageTests : IDisposable +{ + private readonly PostgresSqlTestHelper _postgresSqlTestHelper; + private readonly Message _messageEarliest; + private readonly Message _messageDispatched; + private readonly Message _messageUnDispatched; + private readonly PostgreSqlOutbox _sqlOutbox; + + public PostgresSqlFetchOutStandingMessageTests() + { + _postgresSqlTestHelper = new PostgresSqlTestHelper(); + _postgresSqlTestHelper.SetupMessageDb(); + + _sqlOutbox = new PostgreSqlOutbox(_postgresSqlTestHelper.Configuration); + var routingKey = new RoutingKey("test_topic"); + + _messageEarliest = new Message( + new MessageHeader(Guid.NewGuid().ToString(), routingKey, MessageType.MT_DOCUMENT) + { + TimeStamp = DateTimeOffset.UtcNow.AddHours(-3) + }, + new MessageBody("message body")); + _messageDispatched = new Message( + new MessageHeader(Guid.NewGuid().ToString(), routingKey, MessageType.MT_DOCUMENT), + new MessageBody("message body")); + _messageUnDispatched = new Message( + new MessageHeader(Guid.NewGuid().ToString(), routingKey, MessageType.MT_DOCUMENT), + new MessageBody("message body")); + } + + [Fact] + public void When_Retrieving_Not_Dispatched_Messages() + { + var context = new RequestContext(); + _sqlOutbox.Add([_messageEarliest, _messageDispatched, _messageUnDispatched], context); + _sqlOutbox.MarkDispatched(_messageDispatched.Id, context); + + var total = _sqlOutbox.GetNumberOfOutstandingMessages(); + + var allUnDispatched = _sqlOutbox.OutstandingMessages(TimeSpan.Zero, context); + var messagesOverAnHour = _sqlOutbox.OutstandingMessages(TimeSpan.FromHours(1), context); + var messagesOver4Hours = _sqlOutbox.OutstandingMessages(TimeSpan.FromHours(4), context); + + //Assert + total.Should().Be(2); + allUnDispatched.Should().HaveCount(2); + messagesOverAnHour.Should().ContainSingle(); + messagesOver4Hours.Should().BeEmpty(); + } + + public void Dispose() + { + _postgresSqlTestHelper.CleanUpDb(); + } +} diff --git a/tests/Paramore.Brighter.PostgresSQL.Tests/Outbox/When_retrieving_outstanding_messages_async.cs b/tests/Paramore.Brighter.PostgresSQL.Tests/Outbox/When_retrieving_outstanding_messages_async.cs new file mode 100644 index 0000000000..165c9f787c --- /dev/null +++ b/tests/Paramore.Brighter.PostgresSQL.Tests/Outbox/When_retrieving_outstanding_messages_async.cs @@ -0,0 +1,64 @@ +using System; +using System.Threading.Tasks; +using FluentAssertions; +using Paramore.Brighter.Outbox.PostgreSql; +using Xunit; + +namespace Paramore.Brighter.PostgresSQL.Tests.Outbox; + +[Trait("Category", "PostgresSql")] +public class PostgresSqlFetchOutStandingMessageAsyncTests : IDisposable +{ + private readonly PostgresSqlTestHelper _postgresSqlTestHelper; + private readonly Message _messageEarliest; + private readonly Message _messageDispatched; + private readonly Message _messageUnDispatched; + private readonly PostgreSqlOutbox _sqlOutbox; + + public PostgresSqlFetchOutStandingMessageAsyncTests() + { + _postgresSqlTestHelper = new PostgresSqlTestHelper(); + _postgresSqlTestHelper.SetupMessageDb(); + + _sqlOutbox = new PostgreSqlOutbox(_postgresSqlTestHelper.Configuration); + var routingKey = new RoutingKey("test_topic"); + + _messageEarliest = new Message( + new MessageHeader(Guid.NewGuid().ToString(), routingKey, MessageType.MT_DOCUMENT) + { + TimeStamp = DateTimeOffset.UtcNow.AddHours(-3) + }, + new MessageBody("message body")); + _messageDispatched = new Message( + new MessageHeader(Guid.NewGuid().ToString(), routingKey, MessageType.MT_DOCUMENT), + new MessageBody("message body")); + _messageUnDispatched = new Message( + new MessageHeader(Guid.NewGuid().ToString(), routingKey, MessageType.MT_DOCUMENT), + new MessageBody("message body")); + } + + [Fact] + public async Task When_Retrieving_Not_Dispatched_Messages_Async() + { + var context = new RequestContext(); + await _sqlOutbox.AddAsync([_messageEarliest, _messageDispatched, _messageUnDispatched], context); + await _sqlOutbox.MarkDispatchedAsync(_messageDispatched.Id, context); + + var total = await _sqlOutbox.GetNumberOfOutstandingMessagesAsync(); + + var allUnDispatched = await _sqlOutbox.OutstandingMessagesAsync(TimeSpan.Zero, context); + var messagesOverAnHour = await _sqlOutbox.OutstandingMessagesAsync(TimeSpan.FromHours(1), context); + var messagesOver4Hours = await _sqlOutbox.OutstandingMessagesAsync(TimeSpan.FromHours(4), context); + + //Assert + total.Should().Be(2); + allUnDispatched.Should().HaveCount(2); + messagesOverAnHour.Should().ContainSingle(); + messagesOver4Hours.Should().BeEmpty(); + } + + public void Dispose() + { + _postgresSqlTestHelper.CleanUpDb(); + } +} diff --git a/tests/Paramore.Brighter.PostgresSQL.Tests/PostgresSqlTestHelper.cs b/tests/Paramore.Brighter.PostgresSQL.Tests/PostgresSqlTestHelper.cs index 38be6873f6..a686aeb47f 100644 --- a/tests/Paramore.Brighter.PostgresSQL.Tests/PostgresSqlTestHelper.cs +++ b/tests/Paramore.Brighter.PostgresSQL.Tests/PostgresSqlTestHelper.cs @@ -95,7 +95,7 @@ private void CreateOutboxTable() { using var connection = new NpgsqlConnection(_postgreSqlSettings.TestsBrighterConnectionString); _tableName = $"message_{_tableName}"; - var createTableSql = PostgreSqlOutboxBulder.GetDDL(_tableName, Configuration.BinaryMessagePayload); + var createTableSql = PostgreSqlOutboxBuilder.GetDDL(_tableName, Configuration.BinaryMessagePayload); connection.Open(); using var command = connection.CreateCommand(); diff --git a/tests/Paramore.Brighter.Sqlite.Tests/Outbox/When_retrieving_messages.cs b/tests/Paramore.Brighter.Sqlite.Tests/Outbox/When_retrieving_messages.cs new file mode 100644 index 0000000000..e3d11b04c8 --- /dev/null +++ b/tests/Paramore.Brighter.Sqlite.Tests/Outbox/When_retrieving_messages.cs @@ -0,0 +1,90 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using FluentAssertions; +using Paramore.Brighter.Outbox.Sqlite; +using Xunit; + +namespace Paramore.Brighter.Sqlite.Tests.Outbox; + +[Trait("Category", "Sqlite")] +public class SqliteFetchMessageTests : IAsyncDisposable +{ + private readonly SqliteTestHelper _sqliteTestHelper; + private readonly Message _messageEarliest; + private readonly Message _messageDispatched; + private readonly Message _messageUnDispatched; + private readonly SqliteOutbox _sqlOutbox; + + public SqliteFetchMessageTests() + { + _sqliteTestHelper = new SqliteTestHelper(); + _sqliteTestHelper.SetupMessageDb(); + + _sqlOutbox = new SqliteOutbox(_sqliteTestHelper.OutboxConfiguration); + var routingKey = new RoutingKey("test_topic"); + + _messageEarliest = new Message( + new MessageHeader(Guid.NewGuid().ToString(), routingKey, MessageType.MT_DOCUMENT), + new MessageBody("message body")); + _messageDispatched = new Message( + new MessageHeader(Guid.NewGuid().ToString(), routingKey, MessageType.MT_DOCUMENT), + new MessageBody("message body")); + _messageUnDispatched = new Message( + new MessageHeader(Guid.NewGuid().ToString(), routingKey, MessageType.MT_DOCUMENT), + new MessageBody("message body")); + } + + [Fact] + public void When_Retrieving_Messages() + { + var context = new RequestContext(); + _sqlOutbox.Add([_messageEarliest, _messageDispatched, _messageUnDispatched], context); + _sqlOutbox.MarkDispatched(_messageEarliest.Id, context, DateTime.UtcNow.AddHours(-3)); + _sqlOutbox.MarkDispatched(_messageDispatched.Id, context); + + var messages = _sqlOutbox.Get(); + + //Assert + messages.Should().HaveCount(3); + } + + [Fact] + public void When_Retrieving_Messages_By_Id() + { + var context = new RequestContext(); + _sqlOutbox.Add([_messageEarliest, _messageDispatched, _messageUnDispatched], context); + _sqlOutbox.MarkDispatched(_messageEarliest.Id, context, DateTime.UtcNow.AddHours(-3)); + _sqlOutbox.MarkDispatched(_messageDispatched.Id, context); + + var messages = _sqlOutbox.Get( + [_messageEarliest.Id, _messageUnDispatched.Id], + context); + + //Assert + messages = messages.ToList(); + messages.Should().HaveCount(2); + messages.Should().Contain(x => x.Id == _messageEarliest.Id); + messages.Should().Contain(x => x.Id == _messageUnDispatched.Id); + messages.Should().NotContain(x => x.Id == _messageDispatched.Id); + } + + [Fact] + public void When_Retrieving_Message_By_Id() + { + var context = new RequestContext(); + _sqlOutbox.Add([_messageEarliest, _messageDispatched, _messageUnDispatched], context); + _sqlOutbox.MarkDispatched(_messageEarliest.Id, context, DateTime.UtcNow.AddHours(-3)); + _sqlOutbox.MarkDispatched(_messageDispatched.Id, context); + + var messages = _sqlOutbox.Get(_messageDispatched.Id, context); + + //Assert + messages.Id.Should().Be(_messageDispatched.Id); + } + + public async ValueTask DisposeAsync() + { + await _sqliteTestHelper.CleanUpDbAsync(); + } +} diff --git a/tests/Paramore.Brighter.Sqlite.Tests/Outbox/When_retrieving_messages_async.cs b/tests/Paramore.Brighter.Sqlite.Tests/Outbox/When_retrieving_messages_async.cs new file mode 100644 index 0000000000..28b71f40c1 --- /dev/null +++ b/tests/Paramore.Brighter.Sqlite.Tests/Outbox/When_retrieving_messages_async.cs @@ -0,0 +1,90 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using FluentAssertions; +using Paramore.Brighter.Outbox.Sqlite; +using Xunit; + +namespace Paramore.Brighter.Sqlite.Tests.Outbox; + +[Trait("Category", "Sqlite")] +public class SqliteFetchMessageAsyncTests : IAsyncDisposable +{ + private readonly SqliteTestHelper _sqliteTestHelper; + private readonly Message _messageEarliest; + private readonly Message _messageDispatched; + private readonly Message _messageUnDispatched; + private readonly SqliteOutbox _sqlOutbox; + + public SqliteFetchMessageAsyncTests() + { + _sqliteTestHelper = new SqliteTestHelper(); + _sqliteTestHelper.SetupMessageDb(); + + _sqlOutbox = new SqliteOutbox(_sqliteTestHelper.OutboxConfiguration); + var routingKey = new RoutingKey("test_topic"); + + _messageEarliest = new Message( + new MessageHeader(Guid.NewGuid().ToString(), routingKey, MessageType.MT_DOCUMENT), + new MessageBody("message body")); + _messageDispatched = new Message( + new MessageHeader(Guid.NewGuid().ToString(), routingKey, MessageType.MT_DOCUMENT), + new MessageBody("message body")); + _messageUnDispatched = new Message( + new MessageHeader(Guid.NewGuid().ToString(), routingKey, MessageType.MT_DOCUMENT), + new MessageBody("message body")); + } + + [Fact] + public async Task When_Retrieving_Messages_Async() + { + var context = new RequestContext(); + await _sqlOutbox.AddAsync([_messageEarliest, _messageDispatched, _messageUnDispatched], context); + await _sqlOutbox.MarkDispatchedAsync(_messageEarliest.Id, context, DateTime.UtcNow.AddHours(-3)); + await _sqlOutbox.MarkDispatchedAsync(_messageDispatched.Id, context); + + var messages = await _sqlOutbox.GetAsync(); + + //Assert + messages.Should().HaveCount(3); + } + + [Fact] + public async Task When_Retrieving_Messages_By_Id_Async() + { + var context = new RequestContext(); + await _sqlOutbox.AddAsync([_messageEarliest, _messageDispatched, _messageUnDispatched], context); + await _sqlOutbox.MarkDispatchedAsync(_messageEarliest.Id, context, DateTime.UtcNow.AddHours(-3)); + await _sqlOutbox.MarkDispatchedAsync(_messageDispatched.Id, context); + + var messages = await _sqlOutbox.GetAsync( + [_messageEarliest.Id, _messageUnDispatched.Id], + context); + + //Assert + messages = messages.ToList(); + messages.Should().HaveCount(2); + messages.Should().Contain(x => x.Id == _messageEarliest.Id); + messages.Should().Contain(x => x.Id == _messageUnDispatched.Id); + messages.Should().NotContain(x => x.Id == _messageDispatched.Id); + } + + [Fact] + public async Task When_Retrieving_Message_By_Id_Async() + { + var context = new RequestContext(); + await _sqlOutbox.AddAsync([_messageEarliest, _messageDispatched, _messageUnDispatched], context); + await _sqlOutbox.MarkDispatchedAsync(_messageEarliest.Id, context, DateTime.UtcNow.AddHours(-3)); + await _sqlOutbox.MarkDispatchedAsync(_messageDispatched.Id, context); + + var messages = await _sqlOutbox.GetAsync(_messageDispatched.Id, context); + + //Assert + messages.Id.Should().Be(_messageDispatched.Id); + } + + public async ValueTask DisposeAsync() + { + await _sqliteTestHelper.CleanUpDbAsync(); + } +} diff --git a/tests/Paramore.Brighter.Sqlite.Tests/Outbox/When_retrieving_messages_to_archive.cs b/tests/Paramore.Brighter.Sqlite.Tests/Outbox/When_retrieving_messages_to_archive.cs new file mode 100644 index 0000000000..1234a49c88 --- /dev/null +++ b/tests/Paramore.Brighter.Sqlite.Tests/Outbox/When_retrieving_messages_to_archive.cs @@ -0,0 +1,77 @@ +using System; +using System.Threading.Tasks; +using FluentAssertions; +using Paramore.Brighter.Outbox.Sqlite; +using Xunit; + +namespace Paramore.Brighter.Sqlite.Tests.Outbox; + +[Trait("Category", "Sqlite")] +public class SqliteArchiveFetchTests : IAsyncDisposable +{ + private readonly SqliteTestHelper _sqliteTestHelper; + private readonly Message _messageEarliest; + private readonly Message _messageDispatched; + private readonly Message _messageUnDispatched; + private readonly SqliteOutbox _sqlOutbox; + + public SqliteArchiveFetchTests() + { + _sqliteTestHelper = new SqliteTestHelper(); + _sqliteTestHelper.SetupMessageDb(); + + _sqlOutbox = new SqliteOutbox(_sqliteTestHelper.OutboxConfiguration); + var routingKey = new RoutingKey("test_topic"); + + _messageEarliest = new Message( + new MessageHeader(Guid.NewGuid().ToString(), routingKey, MessageType.MT_DOCUMENT), + new MessageBody("message body")); + _messageDispatched = new Message( + new MessageHeader(Guid.NewGuid().ToString(), routingKey, MessageType.MT_DOCUMENT), + new MessageBody("message body")); + _messageUnDispatched = new Message( + new MessageHeader(Guid.NewGuid().ToString(), routingKey, MessageType.MT_DOCUMENT), + new MessageBody("message body")); + } + + [Fact] + public void When_Retrieving_Messages_To_Archive() + { + var context = new RequestContext(); + _sqlOutbox.Add([_messageEarliest, _messageDispatched, _messageUnDispatched], context); + _sqlOutbox.MarkDispatched(_messageEarliest.Id, context, DateTimeOffset.UtcNow.AddHours(-3)); + _sqlOutbox.MarkDispatched(_messageDispatched.Id, context); + + var allDispatched = _sqlOutbox.DispatchedMessages(0, context); + var messagesOverAnHour = _sqlOutbox.DispatchedMessages(1, context); + var messagesOver4Hours = _sqlOutbox.DispatchedMessages(4, context); + + //Assert + allDispatched.Should().HaveCount(2); + messagesOverAnHour.Should().ContainSingle(); + messagesOver4Hours.Should().BeEmpty(); + } + + [Fact] + public void When_Retrieving_Messages_To_Archive_UsingTimeSpan() + { + var context = new RequestContext(); + _sqlOutbox.Add([_messageEarliest, _messageDispatched, _messageUnDispatched], context); + _sqlOutbox.MarkDispatched(_messageEarliest.Id, context, DateTime.UtcNow.AddHours(-3)); + _sqlOutbox.MarkDispatched(_messageDispatched.Id, context); + + var allDispatched = _sqlOutbox.DispatchedMessages(TimeSpan.Zero, context); + var messagesOverAnHour = _sqlOutbox.DispatchedMessages(TimeSpan.FromHours(2), context); + var messagesOver4Hours = _sqlOutbox.DispatchedMessages(TimeSpan.FromHours(4), context); + + //Assert + allDispatched.Should().HaveCount(2); + messagesOverAnHour.Should().ContainSingle(); + messagesOver4Hours.Should().BeEmpty(); + } + + public async ValueTask DisposeAsync() + { + await _sqliteTestHelper.CleanUpDbAsync(); + } +} diff --git a/tests/Paramore.Brighter.Sqlite.Tests/Outbox/When_retrieving_messages_to_archive_async.cs b/tests/Paramore.Brighter.Sqlite.Tests/Outbox/When_retrieving_messages_to_archive_async.cs new file mode 100644 index 0000000000..b71ee6daa3 --- /dev/null +++ b/tests/Paramore.Brighter.Sqlite.Tests/Outbox/When_retrieving_messages_to_archive_async.cs @@ -0,0 +1,87 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using FluentAssertions; +using Paramore.Brighter.Outbox.Sqlite; +using Xunit; + +namespace Paramore.Brighter.Sqlite.Tests.Outbox; + +[Trait("Category", "Sqlite")] +public class SqliteArchiveFetchAsyncTests : IAsyncDisposable +{ + private readonly SqliteTestHelper _sqliteTestHelper; + private readonly Message _messageEarliest; + private readonly Message _messageDispatched; + private readonly Message _messageUnDispatched; + private readonly SqliteOutbox _sqlOutbox; + + public SqliteArchiveFetchAsyncTests() + { + _sqliteTestHelper = new(); + _sqliteTestHelper.SetupMessageDb(); + + _sqlOutbox = new SqliteOutbox(_sqliteTestHelper.OutboxConfiguration); + var routingKey = new RoutingKey("test_topic"); + + _messageEarliest = new Message( + new MessageHeader(Guid.NewGuid().ToString(), routingKey, MessageType.MT_DOCUMENT), + new MessageBody("message body")); + _messageDispatched = new Message( + new MessageHeader(Guid.NewGuid().ToString(), routingKey, MessageType.MT_DOCUMENT), + new MessageBody("message body")); + _messageUnDispatched = new Message( + new MessageHeader(Guid.NewGuid().ToString(), routingKey, MessageType.MT_DOCUMENT), + new MessageBody("message body")); + } + + [Fact] + public async Task When_Retrieving_Messages_To_Archive_Async() + { + var context = new RequestContext(); + await _sqlOutbox.AddAsync([_messageEarliest, _messageDispatched, _messageUnDispatched], context); + await _sqlOutbox.MarkDispatchedAsync(_messageEarliest.Id, context, DateTime.UtcNow.AddHours(-3)); + await _sqlOutbox.MarkDispatchedAsync(_messageDispatched.Id, context); + + var allDispatched = + await _sqlOutbox.DispatchedMessagesAsync(0, context, cancellationToken: CancellationToken.None); + var messagesOverAnHour = + await _sqlOutbox.DispatchedMessagesAsync(1, context, cancellationToken: CancellationToken.None); + var messagesOver4Hours = + await _sqlOutbox.DispatchedMessagesAsync(4, context, cancellationToken: CancellationToken.None); + + //Assert + allDispatched.Should().HaveCount(2); + messagesOverAnHour.Should().ContainSingle(); + messagesOver4Hours.Should().BeEmpty(); + } + + [Fact] + public async Task When_Retrieving_Messages_To_Archive_UsingTimeSpan_Async() + { + var context = new RequestContext(); + await _sqlOutbox.AddAsync([_messageEarliest, _messageDispatched, _messageUnDispatched], context); + await _sqlOutbox.MarkDispatchedAsync(_messageEarliest.Id, context, DateTime.UtcNow.AddHours(-3)); + await _sqlOutbox.MarkDispatchedAsync(_messageDispatched.Id, context); + + var allDispatched = + await _sqlOutbox.DispatchedMessagesAsync(TimeSpan.Zero, context, 100, + cancellationToken: CancellationToken.None); + var messagesOverAnHour = + await _sqlOutbox.DispatchedMessagesAsync(TimeSpan.FromHours(2), context, 100, + cancellationToken: CancellationToken.None); + var messagesOver4Hours = + await _sqlOutbox.DispatchedMessagesAsync(TimeSpan.FromHours(4), context, 100, + cancellationToken: CancellationToken.None); + + //Assert + allDispatched.Should().HaveCount(2); + messagesOverAnHour.Should().ContainSingle(); + messagesOver4Hours.Should().BeEmpty(); + } + + public async ValueTask DisposeAsync() + { + await _sqliteTestHelper.CleanUpDbAsync(); + } +} diff --git a/tests/Paramore.Brighter.Sqlite.Tests/Outbox/When_retrieving_outstanding_messages.cs b/tests/Paramore.Brighter.Sqlite.Tests/Outbox/When_retrieving_outstanding_messages.cs new file mode 100644 index 0000000000..505ad4bd11 --- /dev/null +++ b/tests/Paramore.Brighter.Sqlite.Tests/Outbox/When_retrieving_outstanding_messages.cs @@ -0,0 +1,64 @@ +using System; +using System.Threading.Tasks; +using FluentAssertions; +using Paramore.Brighter.Outbox.Sqlite; +using Xunit; + +namespace Paramore.Brighter.Sqlite.Tests.Outbox; + +[Trait("Category", "Sqlite")] +public class SqliteFetchOutStandingMessageTests : IAsyncDisposable +{ + private readonly SqliteTestHelper _sqliteTestHelper; + private readonly Message _messageEarliest; + private readonly Message _messageDispatched; + private readonly Message _messageUnDispatched; + private readonly SqliteOutbox _sqlOutbox; + + public SqliteFetchOutStandingMessageTests() + { + _sqliteTestHelper = new SqliteTestHelper(); + _sqliteTestHelper.SetupMessageDb(); + + _sqlOutbox = new SqliteOutbox(_sqliteTestHelper.OutboxConfiguration); + var routingKey = new RoutingKey("test_topic"); + + _messageEarliest = new Message( + new MessageHeader(Guid.NewGuid().ToString(), routingKey, MessageType.MT_DOCUMENT) + { + TimeStamp = DateTimeOffset.UtcNow.AddHours(-3) + }, + new MessageBody("message body")); + _messageDispatched = new Message( + new MessageHeader(Guid.NewGuid().ToString(), routingKey, MessageType.MT_DOCUMENT), + new MessageBody("message body")); + _messageUnDispatched = new Message( + new MessageHeader(Guid.NewGuid().ToString(), routingKey, MessageType.MT_DOCUMENT), + new MessageBody("message body")); + } + + [Fact] + public void When_Retrieving_Not_Dispatched_Messages() + { + var context = new RequestContext(); + _sqlOutbox.Add([_messageEarliest, _messageDispatched, _messageUnDispatched], context); + _sqlOutbox.MarkDispatched(_messageDispatched.Id, context); + + var total = _sqlOutbox.GetNumberOfOutstandingMessages(); + + var allUnDispatched = _sqlOutbox.OutstandingMessages(TimeSpan.Zero, context); + var messagesOverAnHour = _sqlOutbox.OutstandingMessages(TimeSpan.FromHours(1), context); + var messagesOver4Hours = _sqlOutbox.OutstandingMessages(TimeSpan.FromHours(4), context); + + //Assert + total.Should().Be(2); + allUnDispatched.Should().HaveCount(2); + messagesOverAnHour.Should().ContainSingle(); + messagesOver4Hours.Should().BeEmpty(); + } + + public async ValueTask DisposeAsync() + { + await _sqliteTestHelper.CleanUpDbAsync(); + } +} diff --git a/tests/Paramore.Brighter.Sqlite.Tests/Outbox/When_retrieving_outstanding_messages_async.cs b/tests/Paramore.Brighter.Sqlite.Tests/Outbox/When_retrieving_outstanding_messages_async.cs new file mode 100644 index 0000000000..0c574e81b8 --- /dev/null +++ b/tests/Paramore.Brighter.Sqlite.Tests/Outbox/When_retrieving_outstanding_messages_async.cs @@ -0,0 +1,64 @@ +using System; +using System.Threading.Tasks; +using FluentAssertions; +using Paramore.Brighter.Outbox.Sqlite; +using Xunit; + +namespace Paramore.Brighter.Sqlite.Tests.Outbox; + +[Trait("Category", "Sqlite")] +public class PostgresSqlFetchOutStandingMessageAsyncTests : IAsyncDisposable +{ + private readonly SqliteTestHelper _sqliteTestHelper; + private readonly Message _messageEarliest; + private readonly Message _messageDispatched; + private readonly Message _messageUnDispatched; + private readonly SqliteOutbox _sqlOutbox; + + public PostgresSqlFetchOutStandingMessageAsyncTests() + { + _sqliteTestHelper = new SqliteTestHelper(); + _sqliteTestHelper.SetupMessageDb(); + + _sqlOutbox = new SqliteOutbox(_sqliteTestHelper.OutboxConfiguration); + var routingKey = new RoutingKey("test_topic"); + + _messageEarliest = new Message( + new MessageHeader(Guid.NewGuid().ToString(), routingKey, MessageType.MT_DOCUMENT) + { + TimeStamp = DateTimeOffset.UtcNow.AddHours(-3) + }, + new MessageBody("message body")); + _messageDispatched = new Message( + new MessageHeader(Guid.NewGuid().ToString(), routingKey, MessageType.MT_DOCUMENT), + new MessageBody("message body")); + _messageUnDispatched = new Message( + new MessageHeader(Guid.NewGuid().ToString(), routingKey, MessageType.MT_DOCUMENT), + new MessageBody("message body")); + } + + [Fact] + public async Task When_Retrieving_Not_Dispatched_Messages_Async() + { + var context = new RequestContext(); + await _sqlOutbox.AddAsync([_messageEarliest, _messageDispatched, _messageUnDispatched], context); + await _sqlOutbox.MarkDispatchedAsync(_messageDispatched.Id, context); + + var total = await _sqlOutbox.GetNumberOfOutstandingMessagesAsync(); + + var allUnDispatched = await _sqlOutbox.OutstandingMessagesAsync(TimeSpan.Zero, context); + var messagesOverAnHour = await _sqlOutbox.OutstandingMessagesAsync(TimeSpan.FromHours(1), context); + var messagesOver4Hours = await _sqlOutbox.OutstandingMessagesAsync(TimeSpan.FromHours(4), context); + + //Assert + total.Should().Be(2); + allUnDispatched.Should().HaveCount(2); + messagesOverAnHour.Should().ContainSingle(); + messagesOver4Hours.Should().BeEmpty(); + } + + public async ValueTask DisposeAsync() + { + await _sqliteTestHelper.CleanUpDbAsync(); + } +}