From 142951affd48ff776df19bdb9082b96545e27334 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Ja=C5=A1ek?= Date: Mon, 29 Aug 2022 07:24:06 +0200 Subject: [PATCH 1/2] feature: background job cache invalidation --- .../CacheInvalidationService.cs | 94 +++++++++++++++++++ .../ChangeFeedResponseItem.cs | 5 + .../Kontent.Ai.Boilerplate/Constants.cs | 6 ++ src/content/Kontent.Ai.Boilerplate/Startup.cs | 5 +- 4 files changed, 108 insertions(+), 2 deletions(-) create mode 100644 src/content/Kontent.Ai.Boilerplate/CacheInvalidation/CacheInvalidationService.cs create mode 100644 src/content/Kontent.Ai.Boilerplate/CacheInvalidation/ChangeFeedResponseItem.cs create mode 100644 src/content/Kontent.Ai.Boilerplate/Constants.cs diff --git a/src/content/Kontent.Ai.Boilerplate/CacheInvalidation/CacheInvalidationService.cs b/src/content/Kontent.Ai.Boilerplate/CacheInvalidation/CacheInvalidationService.cs new file mode 100644 index 0000000..7a7a336 --- /dev/null +++ b/src/content/Kontent.Ai.Boilerplate/CacheInvalidation/CacheInvalidationService.cs @@ -0,0 +1,94 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using Kontent.Ai.Delivery.Abstractions; +using Kontent.Ai.Delivery.Caching; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Options; + +namespace Kontent.Ai.Boilerplate.CacheInvalidation; + +internal class CacheInvalidationService : IHostedService +{ + private readonly IDeliveryCacheManager _cacheManager; + private readonly IOptions _options; + private string? _continuationToken; + private readonly HttpClient _client; + private Timer? _timer; + + + public CacheInvalidationService(IDeliveryCacheManager cacheManager, IOptions options) + { + _cacheManager = cacheManager ?? throw new ArgumentNullException(nameof(cacheManager)); + _options = options ?? throw new ArgumentNullException(nameof(options)); + _client = new HttpClient(); + _continuationToken = CheckChangeFeed().Result.Item2; + } + + + public Task StartAsync(CancellationToken cancellationToken) + { + _timer = new Timer(TryCacheInvalidation, null, TimeSpan.Zero, + TimeSpan.FromMinutes(5)); + + return Task.CompletedTask; + } + + + public Task StopAsync(CancellationToken cancellationToken) + { + _timer?.Dispose(); + return Task.CompletedTask; + } + + private async Task?, string?>> CheckChangeFeed() + { + var changeFeedResponse = await _client.SendAsync( + new HttpRequestMessage(method: HttpMethod.Get, + requestUri: + $"{_options.Value.ProductionEndpoint}/{_options.Value.ProjectId}/change-feed") + { Headers = { { HeaderNames.Continuation, _continuationToken } } }); + var changeFeedItems = changeFeedResponse.StatusCode == HttpStatusCode.OK + ? await JsonSerializer.DeserializeAsync>( + await changeFeedResponse.Content.ReadAsStreamAsync()) + : null; + + return new Tuple?, string?>(changeFeedItems, + changeFeedResponse.Headers.GetValues(HeaderNames.Continuation).FirstOrDefault()); + } + + private async void TryCacheInvalidation(object? state) + { + IEnumerable? changeFeed; + do + { + (changeFeed, var continuationToken) = await CheckChangeFeed(); + if (continuationToken != null && continuationToken != _continuationToken) + _continuationToken = continuationToken; + if (changeFeed != null) await InvalidateCache(itemsChanged: changeFeed); + } while (changeFeed != null); + } + + private async Task InvalidateCache(IEnumerable itemsChanged) + { + var dependencies = new HashSet(); + { + foreach (var item in itemsChanged) + { + dependencies.Add(CacheHelpers.GetItemDependencyKey(item.Codename)); + } + + dependencies.Add(CacheHelpers.GetItemsDependencyKey()); + } + + foreach (var dependency in dependencies) + { + await _cacheManager.InvalidateDependencyAsync(dependency); + } + } +} \ No newline at end of file diff --git a/src/content/Kontent.Ai.Boilerplate/CacheInvalidation/ChangeFeedResponseItem.cs b/src/content/Kontent.Ai.Boilerplate/CacheInvalidation/ChangeFeedResponseItem.cs new file mode 100644 index 0000000..8d2dc4f --- /dev/null +++ b/src/content/Kontent.Ai.Boilerplate/CacheInvalidation/ChangeFeedResponseItem.cs @@ -0,0 +1,5 @@ +using System.Text.Json.Serialization; + +namespace Kontent.Ai.Boilerplate.CacheInvalidation; + +internal record ChangeFeedResponseItem([property: JsonPropertyName("codename")]string Codename); diff --git a/src/content/Kontent.Ai.Boilerplate/Constants.cs b/src/content/Kontent.Ai.Boilerplate/Constants.cs new file mode 100644 index 0000000..b1487a0 --- /dev/null +++ b/src/content/Kontent.Ai.Boilerplate/Constants.cs @@ -0,0 +1,6 @@ +namespace Kontent.Ai.Boilerplate; + +public static class HeaderNames +{ + public const string Continuation = "X-Continuation"; +} \ No newline at end of file diff --git a/src/content/Kontent.Ai.Boilerplate/Startup.cs b/src/content/Kontent.Ai.Boilerplate/Startup.cs index 07453fd..b21df63 100644 --- a/src/content/Kontent.Ai.Boilerplate/Startup.cs +++ b/src/content/Kontent.Ai.Boilerplate/Startup.cs @@ -13,6 +13,7 @@ using Kontent.Ai.Delivery.Abstractions; using Kontent.Ai.Delivery.Extensions; using Kontent.Ai.AspNetCore.Webhooks; +using Kontent.Ai.Boilerplate.CacheInvalidation; namespace Kontent.Ai.Boilerplate { @@ -37,7 +38,7 @@ public void ConfigureServices(IServiceCollection services) services.AddSingleton(); services.AddSingleton(); services.AddDeliveryClient(Configuration); - + services.AddHostedService(); // Use cached client decorator services.AddDeliveryClientCache(new DeliveryCacheOptions() { @@ -74,7 +75,7 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) // Register webhook-based cache invalidation controller app.UseWebhookSignatureValidator(context => context.Request.Path.StartsWithSegments("/webhooks/webhooks", StringComparison.OrdinalIgnoreCase), Configuration.GetSection(nameof(WebhookOptions))); - + app.UseEndpoints(endpoints => { endpoints.MapControllerRoute( From 718e6f67c51f080933ab2ec4ed3ed3c22e591a43 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Ja=C5=A1ek?= Date: Tue, 30 Aug 2022 08:15:56 +0200 Subject: [PATCH 2/2] refactor: abstract logic to static helper --- .../CacheInvalidationService.cs | 47 +++-------------- .../CacheInvalidationServiceHelper.cs | 51 +++++++++++++++++++ 2 files changed, 58 insertions(+), 40 deletions(-) create mode 100644 src/content/Kontent.Ai.Boilerplate/CacheInvalidation/CacheInvalidationServiceHelper.cs diff --git a/src/content/Kontent.Ai.Boilerplate/CacheInvalidation/CacheInvalidationService.cs b/src/content/Kontent.Ai.Boilerplate/CacheInvalidation/CacheInvalidationService.cs index 7a7a336..135eb3f 100644 --- a/src/content/Kontent.Ai.Boilerplate/CacheInvalidation/CacheInvalidationService.cs +++ b/src/content/Kontent.Ai.Boilerplate/CacheInvalidation/CacheInvalidationService.cs @@ -1,13 +1,9 @@ using System; using System.Collections.Generic; -using System.Linq; -using System.Net; using System.Net.Http; -using System.Text.Json; using System.Threading; using System.Threading.Tasks; using Kontent.Ai.Delivery.Abstractions; -using Kontent.Ai.Delivery.Caching; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Options; @@ -27,7 +23,8 @@ public CacheInvalidationService(IDeliveryCacheManager cacheManager, IOptions?, string?>> CheckChangeFeed() - { - var changeFeedResponse = await _client.SendAsync( - new HttpRequestMessage(method: HttpMethod.Get, - requestUri: - $"{_options.Value.ProductionEndpoint}/{_options.Value.ProjectId}/change-feed") - { Headers = { { HeaderNames.Continuation, _continuationToken } } }); - var changeFeedItems = changeFeedResponse.StatusCode == HttpStatusCode.OK - ? await JsonSerializer.DeserializeAsync>( - await changeFeedResponse.Content.ReadAsStreamAsync()) - : null; - return new Tuple?, string?>(changeFeedItems, - changeFeedResponse.Headers.GetValues(HeaderNames.Continuation).FirstOrDefault()); - } - private async void TryCacheInvalidation(object? state) { IEnumerable? changeFeed; do { - (changeFeed, var continuationToken) = await CheckChangeFeed(); + (changeFeed, var continuationToken) = await CacheInvalidationServiceHelper.CheckChangeFeed(client: _client, + options: _options, continuationToken: _continuationToken); if (continuationToken != null && continuationToken != _continuationToken) _continuationToken = continuationToken; - if (changeFeed != null) await InvalidateCache(itemsChanged: changeFeed); + if (changeFeed != null) + await CacheInvalidationServiceHelper.InvalidateCache(itemsChanged: changeFeed, + cacheManager: _cacheManager); } while (changeFeed != null); } - - private async Task InvalidateCache(IEnumerable itemsChanged) - { - var dependencies = new HashSet(); - { - foreach (var item in itemsChanged) - { - dependencies.Add(CacheHelpers.GetItemDependencyKey(item.Codename)); - } - - dependencies.Add(CacheHelpers.GetItemsDependencyKey()); - } - - foreach (var dependency in dependencies) - { - await _cacheManager.InvalidateDependencyAsync(dependency); - } - } } \ No newline at end of file diff --git a/src/content/Kontent.Ai.Boilerplate/CacheInvalidation/CacheInvalidationServiceHelper.cs b/src/content/Kontent.Ai.Boilerplate/CacheInvalidation/CacheInvalidationServiceHelper.cs new file mode 100644 index 0000000..bdc76f2 --- /dev/null +++ b/src/content/Kontent.Ai.Boilerplate/CacheInvalidation/CacheInvalidationServiceHelper.cs @@ -0,0 +1,51 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Text.Json; +using System.Threading.Tasks; +using Kontent.Ai.Delivery.Abstractions; +using Kontent.Ai.Delivery.Caching; +using Microsoft.Extensions.Options; + +namespace Kontent.Ai.Boilerplate.CacheInvalidation; + +internal static class CacheInvalidationServiceHelper +{ + internal static async Task?, string?>> CheckChangeFeed( + IOptions options, string? continuationToken, HttpClient client) + { + var changeFeedResponse = await client.SendAsync( + new HttpRequestMessage(method: HttpMethod.Get, + requestUri: + $"{options.Value.ProductionEndpoint}/{options.Value.ProjectId}/change-feed") + { Headers = { { HeaderNames.Continuation, continuationToken } } }); + var changeFeedItems = changeFeedResponse.StatusCode == HttpStatusCode.OK + ? await JsonSerializer.DeserializeAsync>( + await changeFeedResponse.Content.ReadAsStreamAsync()) + : null; + + return new Tuple?, string?>(changeFeedItems, + changeFeedResponse.Headers.GetValues(HeaderNames.Continuation).FirstOrDefault()); + } + + internal static async Task InvalidateCache(IEnumerable itemsChanged, + IDeliveryCacheManager cacheManager) + { + var dependencies = new HashSet(); + { + foreach (var item in itemsChanged) + { + dependencies.Add(CacheHelpers.GetItemDependencyKey(item.Codename)); + } + + dependencies.Add(CacheHelpers.GetItemsDependencyKey()); + } + + foreach (var dependency in dependencies) + { + await cacheManager.InvalidateDependencyAsync(dependency); + } + } +} \ No newline at end of file