-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: Move Redis dependency management to background tasks (#915)
* feat: Process Redis dependency setting operation in backgronud task tests and refactor chore: Linted code for plan-technology-for-your-school.sln solution chore: remove stopwatch chore: reawait task chore: add options to program extensions chore: amend how default options work * chore: fire and forget * fix: Update tests
- Loading branch information
1 parent
b8beefd
commit ab1db97
Showing
17 changed files
with
559 additions
and
228 deletions.
There are no files selected for viewing
29 changes: 29 additions & 0 deletions
29
src/Dfe.PlanTech.Application/Background/BackgroundTaskQueue.cs
This file contains 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,29 @@ | ||
using System.Threading.Channels; | ||
using Dfe.PlanTech.Domain.Background; | ||
using Microsoft.Extensions.Options; | ||
|
||
namespace Dfe.PlanTech.Application.Background; | ||
|
||
/// <inheritdoc cref="IBackgroundTaskQueue" /> | ||
public class BackgroundTaskQueue(IOptions<BackgroundTaskQueueOptions> options) : IBackgroundTaskQueue | ||
{ | ||
private readonly Channel<Func<CancellationToken, Task>> _queue = Channel.CreateBounded<Func<CancellationToken, Task>>(CreateChannelOptions(options.Value)); | ||
|
||
/// <inheritdoc cref="IBackgroundTaskQueue" /> | ||
public async Task QueueBackgroundWorkItemAsync(Func<CancellationToken, Task> workItem) | ||
{ | ||
ArgumentNullException.ThrowIfNull(workItem); | ||
await _queue.Writer.WriteAsync(workItem); | ||
} | ||
|
||
/// <inheritdoc cref="IBackgroundTaskQueue" /> | ||
public async Task<Func<CancellationToken, Task>> DequeueAsync( | ||
CancellationToken cancellationToken) | ||
{ | ||
var workItem = await _queue.Reader.ReadAsync(cancellationToken); | ||
|
||
return workItem; | ||
} | ||
|
||
private static BoundedChannelOptions CreateChannelOptions(BackgroundTaskQueueOptions options) => new(options.MaxQueueSize) { FullMode = options.FullMode }; | ||
} |
This file contains 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 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
13 changes: 13 additions & 0 deletions
13
src/Dfe.PlanTech.Domain/Background/BackgroundTaskQueueOptions.cs
This file contains 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,13 @@ | ||
using System.Threading.Channels; | ||
|
||
namespace Dfe.PlanTech.Domain.Background; | ||
|
||
/// <summary> | ||
/// Options for <see cref="IBackgroundTaskQueue"/> | ||
/// </summary> | ||
/// <param name="MaxQueueSize">Maximum number of tasks that can be enqueued before the queue is full. Defaults to 10.</param> | ||
/// <param name="FullMode">What to do when the queue is full. Defaults to wait. See <see cref="BoundedChannelFullMode" /> for more details.</param> | ||
public record BackgroundTaskQueueOptions(int MaxQueueSize, BoundedChannelFullMode FullMode) | ||
{ | ||
public BackgroundTaskQueueOptions() : this(10, BoundedChannelFullMode.Wait) { } | ||
} |
24 changes: 24 additions & 0 deletions
24
src/Dfe.PlanTech.Domain/Background/IBackgroundTaskQueue.cs
This file contains 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,24 @@ | ||
namespace Dfe.PlanTech.Domain.Background; | ||
|
||
/// <summary> | ||
/// Queue for tasks to be ran in background | ||
/// </summary> | ||
public interface IBackgroundTaskQueue | ||
{ | ||
/// <summary> | ||
/// Add an async operation to the queue for background processing. | ||
/// </summary> | ||
/// <param name="workItem"></param> | ||
/// <returns></returns> | ||
Task QueueBackgroundWorkItemAsync(Func<CancellationToken, Task> workItem); | ||
|
||
/// <summary> | ||
/// Removes an item from the queue | ||
/// </summary> | ||
/// <remarks> | ||
/// Will wait until an item exists | ||
/// </remarks> | ||
/// <param name="cancellationToken"></param> | ||
/// <returns></returns> | ||
Task<Func<CancellationToken, Task>> DequeueAsync(CancellationToken cancellationToken); | ||
} |
34 changes: 34 additions & 0 deletions
34
src/Dfe.PlanTech.Infrastructure.Redis/IRedisDependencyManager.cs
This file contains 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,34 @@ | ||
using StackExchange.Redis; | ||
|
||
namespace Dfe.PlanTech.Infrastructure.Redis; | ||
|
||
/// <summary> | ||
/// Manages Redis dependencies for <see cref="IContentComponent"> and their contents. | ||
/// </summary> | ||
public interface IRedisDependencyManager | ||
{ | ||
/// <summary> | ||
/// Find and set dependencies for a given <see cref="IContentComponent"/> | ||
/// </summary> | ||
/// <typeparam name="T">Type of the value to register as a dependency.</typeparam> | ||
/// <param name="database">The database where dependencies are stored.</param> | ||
/// <param name="key">Key for the parent of the dependencies.</param> | ||
/// <param name="value">The <see cref="IContentComponent"/> parent of the dependencies </param> | ||
/// <param name="cancellationToken"></param> | ||
/// <returns>A task that represents the asynchronous operation.</returns> | ||
Task RegisterDependenciesAsync<T>(IDatabase database, string key, T value, CancellationToken cancellationToken = default); | ||
|
||
/// <summary> | ||
/// Generates a key for the given content component ID. | ||
/// </summary> | ||
/// <param name="contentComponentId">The ID of the content component.</param> | ||
/// <returns>The generated key string.</returns> | ||
/// <example> | ||
/// <code> | ||
/// var contentComponentId = "example_id"; | ||
/// var dependencyKey = GetDependencyKey(contentComponentId); | ||
/// Console.WriteLine(dependencyKey); // Output: "dependency:example_id" | ||
/// </code> | ||
/// </example> | ||
string GetDependencyKey(string contentComponentId); | ||
} |
This file contains 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
72 changes: 72 additions & 0 deletions
72
src/Dfe.PlanTech.Infrastructure.Redis/RedisDependencyManager.cs
This file contains 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,72 @@ | ||
using Dfe.PlanTech.Domain.Background; | ||
using Dfe.PlanTech.Domain.Content.Interfaces; | ||
using StackExchange.Redis; | ||
|
||
namespace Dfe.PlanTech.Infrastructure.Redis; | ||
|
||
/// <inheritdoc cref="IRedisDependencyManager"/> | ||
/// <param name="backgroundTaskQueue">To add dependency set operations to a queue for background processing</param> | ||
public class RedisDependencyManager(IBackgroundTaskQueue backgroundTaskQueue) : IRedisDependencyManager | ||
{ | ||
/// <inheritdoc cref="IRedisDependencyManager"/> | ||
public Task RegisterDependenciesAsync<T>(IDatabase database, string key, T value, CancellationToken cancellationToken = default) | ||
=> backgroundTaskQueue.QueueBackgroundWorkItemAsync((cancellationToken) => GetAndSetDependencies(database, key, value)); | ||
|
||
/// <inheritdoc cref="IRedisDependencyManager"/> | ||
public string GetDependencyKey(string contentComponentId) => $"Dependency:{contentComponentId}"; | ||
|
||
/// <summary> | ||
/// Retrieves dependencies for the given <see cref="IContentComponent"/> and registers them in the Redis cache. | ||
/// </summary> | ||
/// <typeparam name="T">Type of the value whose dependencies are to be retrieved.</typeparam> | ||
/// <param name="database">The database where dependencies are stored.</param> | ||
/// <param name="key">Key for the parent of the dependencies.</param> | ||
/// <param name="value">The <see cref="IContentComponent"/> parent of the dependencies.</param> | ||
private async Task GetAndSetDependencies<T>(IDatabase database, string key, T value) | ||
{ | ||
var batch = database.CreateBatch(); | ||
var tasks = GetDependencies(value).Select(dependency => batch.SetAddAsync(GetDependencyKey(dependency), key, CommandFlags.FireAndForget)).ToArray(); | ||
batch.Execute(); | ||
await Task.WhenAll(tasks); | ||
} | ||
|
||
/// <summary> | ||
/// Retrieves dependencies, in the form of the Id of the <see cref="IContentComponent"/> | ||
/// </summary> | ||
/// <typeparam name="T"></typeparam> | ||
/// <param name="value"></param> | ||
/// <returns></returns> | ||
/// <exception cref="InvalidOperationException">Thrown when the value object is not a <see cref="IContentComponent"/> or <see cref="IEnumerable{IContentComponent}"/> </exception> | ||
private IEnumerable<string> GetDependencies<T>(T? value) | ||
=> value switch | ||
{ | ||
null => [], | ||
IEnumerable<IContentComponent> collection => collection.SelectMany(GetDependencies), | ||
IContentComponent item => GetContentDependenciesAsync(item), | ||
_ => throw new InvalidOperationException($"{value!.GetType()} is not a {typeof(IContentComponent)} or a {typeof(IEnumerable<IContentComponent>)}"), | ||
}; | ||
|
||
/// <summary> | ||
/// Uses reflection to check for any ContentIds within the <see cref="IContentComponent">, and returns the Id value of any found | ||
/// </summary> | ||
/// <param name="value"></param> | ||
private IEnumerable<string> GetContentDependenciesAsync(IContentComponent value) | ||
{ | ||
// RichText is a sub-component that doesn't have SystemDetails, exit for such types | ||
if (value.Sys is null) | ||
yield break; | ||
|
||
yield return value.Sys.Id; | ||
var properties = value.GetType().GetProperties(); | ||
foreach (var property in properties) | ||
{ | ||
if (typeof(IContentComponent).IsAssignableFrom(property.PropertyType) || typeof(IEnumerable<IContentComponent>).IsAssignableFrom(property.PropertyType)) | ||
{ | ||
foreach (var dependency in GetDependencies(property.GetValue(value))) | ||
{ | ||
yield return dependency; | ||
} | ||
} | ||
} | ||
} | ||
} |
Oops, something went wrong.