This repository was archived by the owner on Jul 28, 2025. It is now read-only.
generated from nhs-england-tools/repository-template
-
Notifications
You must be signed in to change notification settings - Fork 1
feat: retry function #10
Merged
Merged
Changes from all commits
Commits
Show all changes
44 commits
Select commit
Hold shift + click to select a range
83e0f79
Add Mesh client to sln file
ianfnelson 1950834
wip
ianfnelson 5009f6c
feat: ExtractFunction attempts to upload file to blob storage
alex-clayton-1 f5adb31
Queue client implementations
ianfnelson 939f9ee
renames
ianfnelson e14e3f8
sketching out storage impl
ianfnelson 71c2f53
tweak
ianfnelson d682007
fix namespace
ianfnelson 0a4ce6b
fix file name
ianfnelson 4460867
fix compilation errors
ianfnelson 5044497
fix: fixing unit tests for discovery function
SamTyrrellNHS da5d59b
comment
ianfnelson 0d3a21d
feat: adding unit tests for file Extract function
SamTyrrellNHS 4f64ffd
feat: channging env var name
SamTyrrellNHS 29ea6ff
feat: adding Retry function
SamTyrrellNHS e70e497
feat: Implemented FileExtract Function
alex-clayton-1 26cddd0
introduce app configuration
ianfnelson 9317192
fix test compilation
ianfnelson c609736
configuration of queue clients and extract function
ianfnelson f4e9ccb
extract function configuration
ianfnelson 239523e
fix
ianfnelson f7f61fc
feat: added tests for File Retry function
SamTyrrellNHS 317fd0f
Merge branch 'feat/DTOSS-8739-extract-function' into feat/RetryFunction
SamTyrrellNHS 7e9de12
feat: Improved logging in FileExtractFunction
alex-clayton-1 b503cff
refactor: Split out functionality in FileExtractFunction into separat…
alex-clayton-1 ddd700d
style: Fixed formatting issues
alex-clayton-1 f464619
test: Fixed two of the FileExtractFunctionTests
alex-clayton-1 f2bb922
merging with feat/DTOSS-8739-extract-function
SamTyrrellNHS 5e3e932
feat: added configuration
SamTyrrellNHS b9c63f4
feat: fixing unit tests
SamTyrrellNHS a98b312
merging with main
SamTyrrellNHS 5f9fc5a
feat: removing merge issues
SamTyrrellNHS 5d31f45
feat: removing merge issues
SamTyrrellNHS d2efe6b
fix: line spacing
SamTyrrellNHS ba700a9
feat: responding to feedback, adding theory tests
SamTyrrellNHS 763d7a4
feat: adding test for when no files found
SamTyrrellNHS 4327eac
merging with main
SamTyrrellNHS 453bb58
Update host.json
SamTyrrellNHS 7ab6582
feat: env file update
SamTyrrellNHS 8817a92
Merge branch 'feat/RetryFunction' of https://github.com/NHSDigital/dt…
SamTyrrellNHS 6e60b52
feat: namespace change
SamTyrrellNHS df740df
feat: env change
SamTyrrellNHS 130ff2a
feat: tidying code
SamTyrrellNHS c9e3bc1
feat: fixing tests
SamTyrrellNHS File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
6 changes: 6 additions & 0 deletions
6
src/ServiceLayer.Mesh/Configuration/IFileRetryFunctionConfiguration.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| namespace ServiceLayer.Mesh.Configuration; | ||
|
|
||
| public interface IFileRetryFunctionConfiguration | ||
| { | ||
| int StaleHours { get; } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,67 @@ | ||
| using Microsoft.Azure.Functions.Worker; | ||
| using Microsoft.EntityFrameworkCore; | ||
| using Microsoft.Extensions.Logging; | ||
| using ServiceLayer.Mesh.Data; | ||
| using ServiceLayer.Mesh.Messaging; | ||
| using ServiceLayer.Mesh.Models; | ||
| using ServiceLayer.Mesh.Configuration; | ||
|
|
||
| namespace ServiceLayer.Mesh.Functions; | ||
|
|
||
| public class FileRetryFunction | ||
| public class FileRetryFunction( | ||
| ILogger<FileRetryFunction> logger, | ||
| ServiceLayerDbContext serviceLayerDbContext, | ||
| IFileExtractQueueClient fileExtractQueueClient, | ||
| IFileTransformQueueClient fileTransformQueueClient, | ||
| IFileRetryFunctionConfiguration configuration) | ||
| { | ||
| [Function("FileRetryFunction")] | ||
| public async Task Run([TimerTrigger("%FileRetryTimerExpression%")] TimerInfo myTimer) | ||
| { | ||
| logger.LogInformation("FileRetryFunction started"); | ||
|
|
||
| var staleDateTimeUtc = DateTime.UtcNow.AddHours(-configuration.StaleHours); | ||
|
|
||
| await Task.WhenAll( | ||
| RetryStaleExtractions(staleDateTimeUtc), | ||
SamTyrrellNHS marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| RetryStaleTransformations(staleDateTimeUtc)); | ||
| } | ||
|
|
||
| private async Task RetryStaleExtractions(DateTime staleDateTimeUtc) | ||
| { | ||
| var staleFiles = await serviceLayerDbContext.MeshFiles | ||
| .Where(f => | ||
| (f.Status == MeshFileStatus.Discovered || f.Status == MeshFileStatus.Extracting) | ||
| && f.LastUpdatedUtc <= staleDateTimeUtc) | ||
| .ToListAsync(); | ||
|
|
||
| logger.LogInformation($"FileRetryFunction: {staleFiles.Count} stale files found for extraction retry"); | ||
|
|
||
| foreach (var file in staleFiles) | ||
| { | ||
| await fileExtractQueueClient.EnqueueFileExtractAsync(file); | ||
| file.LastUpdatedUtc = DateTime.UtcNow; | ||
| await serviceLayerDbContext.SaveChangesAsync(); | ||
| logger.LogInformation($"FileRetryFunction: File {file.FileId} enqueued to Extract queue"); | ||
| } | ||
| } | ||
|
|
||
| private async Task RetryStaleTransformations(DateTime staleDateTimeUtc) | ||
| { | ||
| var staleFiles = await serviceLayerDbContext.MeshFiles | ||
| .Where(f => | ||
| (f.Status == MeshFileStatus.Extracted || f.Status == MeshFileStatus.Transforming) | ||
| && f.LastUpdatedUtc <= staleDateTimeUtc) | ||
| .ToListAsync(); | ||
|
|
||
| logger.LogInformation($"FileRetryFunction: {staleFiles.Count} stale files found for transforming retry"); | ||
|
|
||
| foreach (var file in staleFiles) | ||
| { | ||
| await fileTransformQueueClient.EnqueueFileTransformAsync(file); | ||
| file.LastUpdatedUtc = DateTime.UtcNow; | ||
| await serviceLayerDbContext.SaveChangesAsync(); | ||
| logger.LogInformation($"FileRetryFunction: File {file.FileId} enqueued to Transform queue"); | ||
| } | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| { | ||
| "version": "2.0", | ||
| "extensionBundle": { | ||
| "id": "Microsoft.Azure.Functions.ExtensionBundle", | ||
| "version": "[4.*, 5.0.0)" | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
218 changes: 218 additions & 0 deletions
218
tests/ServiceLayer.Mesh.Tests/Functions/FileRetryFunctionTests.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,218 @@ | ||
| using Microsoft.EntityFrameworkCore; | ||
| using Microsoft.Extensions.Logging; | ||
| using Moq; | ||
| using NHS.MESH.Client.Contracts.Services; | ||
| using ServiceLayer.Mesh.Data; | ||
| using ServiceLayer.Mesh.Functions; | ||
| using ServiceLayer.Mesh.Messaging; | ||
| using ServiceLayer.Mesh.Models; | ||
| using ServiceLayer.Mesh.Configuration; | ||
|
|
||
| namespace ServiceLayer.Mesh.Tests.Functions; | ||
|
|
||
| public class FileRetryFunctionTests | ||
| { | ||
| private readonly Mock<ILogger<FileRetryFunction>> _loggerMock; | ||
| private readonly Mock<IFileExtractQueueClient> _fileExtractQueueClientMock; | ||
| private readonly Mock<IFileTransformQueueClient> _fileTransformQueueClientMock; | ||
| private readonly Mock<IFileRetryFunctionConfiguration> _configuration; | ||
| private readonly ServiceLayerDbContext _dbContext; | ||
| private readonly FileRetryFunction _function; | ||
|
|
||
| public FileRetryFunctionTests() | ||
| { | ||
| _loggerMock = new Mock<ILogger<FileRetryFunction>>(); | ||
| _fileExtractQueueClientMock = new Mock<IFileExtractQueueClient>(); | ||
| _fileTransformQueueClientMock = new Mock<IFileTransformQueueClient>(); | ||
| _configuration = new Mock<IFileRetryFunctionConfiguration>(); | ||
|
|
||
| var options = new DbContextOptionsBuilder<ServiceLayerDbContext>() | ||
| .UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString()) | ||
| .ConfigureWarnings(warnings => | ||
| warnings.Ignore(Microsoft.EntityFrameworkCore.Diagnostics.InMemoryEventId.TransactionIgnoredWarning)) | ||
| .Options; | ||
|
|
||
| _dbContext = new ServiceLayerDbContext(options); | ||
|
|
||
| _configuration.Setup(c => c.StaleHours).Returns(12); | ||
|
|
||
| _function = new FileRetryFunction( | ||
| _loggerMock.Object, | ||
| _dbContext, | ||
| _fileExtractQueueClientMock.Object, | ||
| _fileTransformQueueClientMock.Object, | ||
| _configuration.Object | ||
| ); | ||
| } | ||
|
|
||
| [Theory] | ||
| [InlineData(MeshFileStatus.Discovered)] | ||
| [InlineData(MeshFileStatus.Extracting)] | ||
| public async Task Run_EnqueuesDiscoveredOrExtractingFilesOlderThan12Hours(MeshFileStatus testStatus) | ||
| { | ||
| var file = new MeshFile | ||
| { | ||
| FileType = MeshFileType.NbssAppointmentEvents, | ||
| MailboxId = "test-mailbox", | ||
| FileId = "file-1", | ||
| Status = testStatus, | ||
| LastUpdatedUtc = DateTime.UtcNow.AddHours(-13) | ||
| }; | ||
|
|
||
| _dbContext.MeshFiles.Add(file); | ||
| await _dbContext.SaveChangesAsync(); | ||
|
|
||
| // Act | ||
| await _function.Run(null); | ||
|
|
||
| // Assert | ||
| _fileExtractQueueClientMock.Verify(q => q.EnqueueFileExtractAsync(It.Is<MeshFile>(f => f.FileId == "file-1")), Times.Once); | ||
| _fileTransformQueueClientMock.Verify(q => q.EnqueueFileTransformAsync(It.Is<MeshFile>(f => f.FileId == "file-1")), Times.Never); | ||
|
|
||
| var updatedFile = await _dbContext.MeshFiles.FindAsync("file-1"); | ||
| Assert.True(updatedFile!.LastUpdatedUtc > DateTime.UtcNow.AddMinutes(-1)); | ||
| } | ||
|
|
||
| [Theory] | ||
| [InlineData(MeshFileStatus.Extracted)] | ||
| [InlineData(MeshFileStatus.Transforming)] | ||
| public async Task Run_EnqueuesExtractedOrTransformingFilesOlderThan12Hours(MeshFileStatus testStatus) | ||
| { | ||
| var file = new MeshFile | ||
| { | ||
| FileType = MeshFileType.NbssAppointmentEvents, | ||
| MailboxId = "test-mailbox", | ||
| FileId = "file-1", | ||
| Status = testStatus, | ||
| LastUpdatedUtc = DateTime.UtcNow.AddHours(-13) | ||
| }; | ||
|
|
||
| _dbContext.MeshFiles.Add(file); | ||
| await _dbContext.SaveChangesAsync(); | ||
|
|
||
| // Act | ||
| await _function.Run(null); | ||
|
|
||
| // Assert | ||
| _fileTransformQueueClientMock.Verify(q => q.EnqueueFileTransformAsync(It.Is<MeshFile>(f => f.FileId == "file-1")), Times.Once); | ||
| _fileExtractQueueClientMock.Verify(q => q.EnqueueFileExtractAsync(It.Is<MeshFile>(f => f.FileId == "file-1")), Times.Never); | ||
|
|
||
|
|
||
| var updatedFile = await _dbContext.MeshFiles.FindAsync("file-1"); | ||
| Assert.True(updatedFile!.LastUpdatedUtc > DateTime.UtcNow.AddMinutes(-1)); | ||
| } | ||
|
|
||
| [Theory] | ||
| [InlineData(MeshFileStatus.Discovered)] | ||
| [InlineData(MeshFileStatus.Extracting)] | ||
| [InlineData(MeshFileStatus.Extracted)] | ||
| [InlineData(MeshFileStatus.Transforming)] | ||
| public async Task Run_SkipsFreshFiles(MeshFileStatus testStatus) | ||
| { | ||
| // Arrange | ||
| var lastUpdatedUtc = DateTime.UtcNow.AddHours(-1); | ||
|
|
||
| var file = new MeshFile | ||
| { | ||
| FileType = MeshFileType.NbssAppointmentEvents, | ||
| MailboxId = "test-mailbox", | ||
| FileId = "file-2", | ||
| Status = testStatus, | ||
| LastUpdatedUtc = lastUpdatedUtc | ||
| }; | ||
| _dbContext.MeshFiles.Add(file); | ||
| await _dbContext.SaveChangesAsync(); | ||
|
|
||
| // Act | ||
| await _function.Run(null); | ||
|
|
||
| // Assert | ||
SamTyrrellNHS marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| _fileExtractQueueClientMock.Verify(q => q.EnqueueFileExtractAsync(It.IsAny<MeshFile>()), Times.Never); | ||
| _fileTransformQueueClientMock.Verify(q => q.EnqueueFileTransformAsync(It.IsAny<MeshFile>()), Times.Never); | ||
|
|
||
| var updatedFile = await _dbContext.MeshFiles.FindAsync("file-2"); | ||
| Assert.True(updatedFile!.LastUpdatedUtc == lastUpdatedUtc); | ||
| } | ||
|
|
||
| [Fact] | ||
| public async Task Run_IgnoresFilesInOtherStatuses() | ||
| { | ||
| // Arrange | ||
| var file = new MeshFile | ||
| { | ||
| FileType = MeshFileType.NbssAppointmentEvents, | ||
| MailboxId = "test-mailbox", | ||
| FileId = "file-5", | ||
| Status = MeshFileStatus.Transformed, | ||
| LastUpdatedUtc = DateTime.UtcNow.AddHours(-20) | ||
| }; | ||
| _dbContext.MeshFiles.Add(file); | ||
| await _dbContext.SaveChangesAsync(); | ||
|
|
||
| // Act | ||
| await _function.Run(null); | ||
|
|
||
| // Assert | ||
| _fileExtractQueueClientMock.Verify(q => q.EnqueueFileExtractAsync(It.IsAny<MeshFile>()), Times.Never); | ||
| } | ||
|
|
||
| [Fact] | ||
| public async Task Run_IfNoFilesFoundDoNothing() | ||
| { | ||
| // Act | ||
| await _function.Run(null); | ||
|
|
||
| // Assert | ||
| _fileExtractQueueClientMock.Verify(q => q.EnqueueFileExtractAsync(It.IsAny<MeshFile>()), Times.Never); | ||
| _fileTransformQueueClientMock.Verify(q => q.EnqueueFileTransformAsync(It.IsAny<MeshFile>()), Times.Never); | ||
| } | ||
|
|
||
| [Fact] | ||
| public async Task Run_ProcessesMultipleEligibleFiles() | ||
| { | ||
| // Arrange | ||
| var files = new[] | ||
| { | ||
| new MeshFile | ||
| { | ||
| FileType = MeshFileType.NbssAppointmentEvents, | ||
| MailboxId = "test-mailbox", | ||
| FileId = "file-6", | ||
| Status = MeshFileStatus.Discovered, | ||
| LastUpdatedUtc = DateTime.UtcNow.AddHours(-13) | ||
| }, | ||
| new MeshFile | ||
| { | ||
| FileType = MeshFileType.NbssAppointmentEvents, | ||
| MailboxId = "test-mailbox", | ||
| FileId = "file-7", | ||
| Status = MeshFileStatus.Extracted, | ||
| LastUpdatedUtc = DateTime.UtcNow.AddHours(-13) | ||
| }, | ||
| new MeshFile | ||
| { | ||
| FileType = MeshFileType.NbssAppointmentEvents, | ||
| MailboxId = "test-mailbox", | ||
| FileId = "file-8", | ||
| Status = MeshFileStatus.Transforming, | ||
| LastUpdatedUtc = DateTime.UtcNow.AddHours(-13) | ||
| } | ||
| }; | ||
|
|
||
| _dbContext.MeshFiles.AddRange(files); | ||
| await _dbContext.SaveChangesAsync(); | ||
|
|
||
| // Act | ||
| await _function.Run(null); | ||
|
|
||
| // Assert | ||
| _fileExtractQueueClientMock.Verify(q => q.EnqueueFileExtractAsync(It.Is<MeshFile>(f => f.FileId == "file-6")), Times.Once); | ||
| _fileExtractQueueClientMock.Verify(q => q.EnqueueFileExtractAsync(It.IsAny<MeshFile>()), Times.Once); | ||
|
|
||
| foreach (var fileId in new[] { "file-6", "file-7", "file-8" }) | ||
| { | ||
| var updated = await _dbContext.MeshFiles.FindAsync(fileId); | ||
| Assert.True(updated!.LastUpdatedUtc > DateTime.UtcNow.AddMinutes(-1)); | ||
| } | ||
| } | ||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.