Skip to content

Commit

Permalink
fix: improve perf for checking duplicated transactionId in incoming m…
Browse files Browse the repository at this point in the history
…essage (#1419)

* fix: improve perf for checking duplicated transactionId in incoming message
  • Loading branch information
MadsDue authored Dec 13, 2024
1 parent 41f9628 commit 9e0e5c6
Show file tree
Hide file tree
Showing 4 changed files with 78 additions and 36 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -171,12 +171,9 @@ private async Task<IReadOnlyCollection<ValidationError>> CheckTransactionIdsAsyn
{
var transactionId = series.TransactionId;

var errorsForSeries = await CheckTransactionIdAsync(
var errorsForSeries = CheckTransactionId(
transactionId,
message.SenderNumber,
transactionIdsToBeStored,
cancellationToken)
.ConfigureAwait(false);
transactionIdsToBeStored);

if (errorsForSeries is null)
{
Expand All @@ -188,34 +185,28 @@ private async Task<IReadOnlyCollection<ValidationError>> CheckTransactionIdsAsyn
}
}

var duplicatedTransactionIds = await transactionIdRepository
.GetDuplicatedTransactionIdsAsync(message.SenderNumber, transactionIdsToBeStored, cancellationToken)
.ConfigureAwait(false);
foreach (var duplicatedTransactionId in duplicatedTransactionIds)
{
errors.Add(new DuplicateTransactionIdDetected(duplicatedTransactionId));
}

return errors;
}

private async Task<ValidationError?> CheckTransactionIdAsync(
private ValidationError? CheckTransactionId(
string transactionId,
string senderNumber,
IReadOnlyCollection<string> transactionIdsToBeStored,
CancellationToken cancellationToken)
IReadOnlyCollection<string> transactionIdsToBeStored)
{
return transactionId switch
{
_ when string.IsNullOrEmpty(transactionId) => new EmptyTransactionId(),
_ when transactionId.Length > MaxTransactionIdLength => new InvalidTransactionIdSize(transactionId),
_ when await TransactionIdIsDuplicatedAsync(senderNumber, transactionId, cancellationToken)
.ConfigureAwait(false) => new DuplicateTransactionIdDetected(transactionId),
_ when transactionIdsToBeStored.Contains(transactionId) =>
new DuplicateTransactionIdDetected(transactionId),
_ => null,
};
}

private async Task<bool> TransactionIdIsDuplicatedAsync(
string senderNumber,
string transactionId,
CancellationToken cancellationToken)
{
return await transactionIdRepository
.TransactionIdExistsAsync(senderNumber, transactionId, cancellationToken)
.ConfigureAwait(false);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,12 @@ namespace Energinet.DataHub.EDI.IncomingMessages.Infrastructure.Repositories.Tra
public interface ITransactionIdRepository
{
/// <summary>
/// Checks if <paramref name="transactionId"/> is already registered by the sender <paramref name="senderId"/>
/// Returns a list of existing <paramref name="transactionIds"/> if they already is registered by the sender <paramref name="senderId"/>
/// </summary>
/// <param name="senderId"></param>
/// <param name="transactionId"></param>
/// <param name="transactionIds"></param>
/// <param name="cancellationToken"></param>
Task<bool> TransactionIdExistsAsync(string senderId, string transactionId, CancellationToken cancellationToken);
Task<IReadOnlyList<string>> GetDuplicatedTransactionIdsAsync(string senderId, IReadOnlyCollection<string> transactionIds, CancellationToken cancellationToken);

/// <summary>
/// Store transaction ids for the specified sender
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,15 +26,17 @@ public TransactionIdRepository(IncomingMessagesContext incomingMessagesContext)
_incomingMessagesContext = incomingMessagesContext;
}

public async Task<bool> TransactionIdExistsAsync(
public async Task<IReadOnlyList<string>> GetDuplicatedTransactionIdsAsync(
string senderId,
string transactionId,
IReadOnlyCollection<string> transactionIds,
CancellationToken cancellationToken)
{
var transaction = await GetTransactionFromDbAsync(senderId, transactionId, cancellationToken).ConfigureAwait(false)
?? GetTransactionFromInMemoryCollection(senderId, transactionId);
var duplicatedTransactionIdsForSender = await GetDuplicatedTransactionsFromDbAsync(senderId, transactionIds, cancellationToken)
.ConfigureAwait(false);
if (!transactionIds.Any())
duplicatedTransactionIdsForSender = GetDuplicatedTransactionsFromInMemoryCollection(senderId, transactionIds);

return transaction != null;
return duplicatedTransactionIdsForSender.Select(x => x.TransactionId).ToList();
}

public async Task AddAsync(
Expand All @@ -50,19 +52,26 @@ public async Task AddAsync(
}
}

private TransactionIdForSender? GetTransactionFromInMemoryCollection(string senderId, string transactionId)
private IReadOnlyList<TransactionIdForSender> GetDuplicatedTransactionsFromInMemoryCollection(
string senderId,
IReadOnlyCollection<string> transactionIds)
{
return _incomingMessagesContext.TransactionIdForSenders.Local
.FirstOrDefault(x => x.TransactionId == transactionId && x.SenderId == senderId);
.Where(
transactionIdForSender => transactionIds.Contains(transactionIdForSender.TransactionId)
&& transactionIdForSender.SenderId == senderId)
.ToList();
}

private async Task<TransactionIdForSender?> GetTransactionFromDbAsync(string senderId, string transactionId, CancellationToken cancellationToken)
private async Task<IReadOnlyList<TransactionIdForSender>> GetDuplicatedTransactionsFromDbAsync(
string senderId,
IReadOnlyCollection<string> transactionIds,
CancellationToken cancellationToken)
{
return await _incomingMessagesContext.TransactionIdForSenders
.FirstOrDefaultAsync(
transactionIdForSender => transactionIdForSender.TransactionId == transactionId
&& transactionIdForSender.SenderId == senderId,
cancellationToken)
.Where(transactionIdForSender => transactionIds.Contains(transactionIdForSender.TransactionId)
&& transactionIdForSender.SenderId == senderId)
.ToListAsync(cancellationToken)
.ConfigureAwait(false);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,37 @@ public async Task When_MultipleTransactionsWithSameId_Then_ResultContainExcepted
result.Errors.Should().Contain(error => error is DuplicateTransactionIdDetected);
}

[Fact]
public async Task When_MultipleTransactionsWithSameIdAsExisting_Then_ResultContainExceptedValidationError()
{
var documentFormat = DocumentFormat.Ebix;
var existingTransactionIdForSender = "123456";
var newTransactionIdForSender = "654321";
await StoreTransactionIdForActorAsync(existingTransactionIdForSender, _actorIdentity.ActorNumber.Value);
var message = MeteredDataForMeasurementPointBuilder.CreateIncomingMessage(
documentFormat,
_actorIdentity.ActorNumber,
[
(existingTransactionIdForSender,
Instant.FromUtc(2024, 1, 1, 0, 0),
Instant.FromUtc(2024, 1, 2, 0, 0),
Resolution.QuarterHourly),
(newTransactionIdForSender,
Instant.FromUtc(2024, 1, 1, 0, 0),
Instant.FromUtc(2024, 1, 2, 0, 0),
Resolution.QuarterHourly),
]);

var (incomingMessage, _) = await ParseMessageAsync(message.Stream, documentFormat);
var result = await _validateIncomingMessage.ValidateAsync(
incomingMessage!,
documentFormat,
CancellationToken.None);

result.Success.Should().BeFalse();
result.Errors.Should().Contain(error => error is DuplicateTransactionIdDetected);
}

[Fact]
public async Task When_TransactionIdIsEmpty_Then_ResultContainExceptedValidationError()
{
Expand Down Expand Up @@ -618,6 +649,17 @@ public async Task When_BusinessTypeIsNotAllowed_Then_ExpectedValidationError()
throw new NotSupportedException($"No message parser found for message format '{documentFormat}' and document type '{IncomingDocumentType.NotifyValidatedMeasureData}'");
}

private async Task StoreTransactionIdForActorAsync(string existingTransactionIdForSender, string senderActorNumber)
{
var databaseConnectionFactory = GetService<IDatabaseConnectionFactory>();
using var dbConnection = await databaseConnectionFactory.GetConnectionAndOpenAsync(CancellationToken.None).ConfigureAwait(false);

await dbConnection.ExecuteAsync(
"INSERT INTO [dbo].[TransactionRegistry] ([TransactionId], [SenderId]) VALUES (@TransactionId, @SenderId)",
new { TransactionId = existingTransactionIdForSender, SenderId = senderActorNumber })
.ConfigureAwait(false);
}

private async Task StoreMessageIdForActorAsync(string messageId, string senderActorNumber)
{
var databaseConnectionFactory = GetService<IDatabaseConnectionFactory>();
Expand Down

0 comments on commit 9e0e5c6

Please sign in to comment.