-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Consumer API (Synchronization): save encrypted payload in the databas…
…e instead of in the blob storage (#450) * chore: generate db migration to add encrypted payload column to the datawallet modification table * feat: change EncryptedPayload from ignored to property persisted in the db * chore: update compiled models * feat: add LoadEncryptedPayload method * feat: add SynchronizationDbContextSeeder * feat: add SynchronizationDbContextSeeder class * feat: remove saving encrypted content to blob storage * chore: remove blob storage configuration * chore: update check if blob reference is present * chore: update handler instantiation * chore: fix formatting * chore: put back blob storage configuration for testing purposes * feat: remove fetching payloads from blob storage * chore: remove unnecessary method arguments * chore: update handler instantiation * chore: remove unused directives * chore: remove empty lines * chore: remove blob storage configuration * chore: remove BlobStorageOptions default values and make them required * feat: add blob storage configuration only if the blob storage options are present chore: remove BlobStorageOptions default value * chore: remove the [Required] annotation from BlobStorageConfiguration * feat: only add blob storage options if the configuration is present * ci: trigger pipelines * chore: revert BlobStorageOptions properties to non required due to failing tests * chore: fix copy/paste error * chore: fix copy/paste error * chore: remove unnecessary properties * chore: remove unnecessary line and remove the unnecessary async keywords * chore: add pagination * chore: update code so that all items are migrated and set page size to 500 * fix: handle modifications with missing payload which is also missing from the blob storage * fix: track number of entries whose payload is missing from the storage in order to skip them chore: increase page size to 500 * refactor: performance optimizations * refactor: use string.IsNullOrWhiteSpace --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Timo Notheisen <[email protected]>
- Loading branch information
1 parent
9224fc0
commit 5de8243
Showing
27 changed files
with
3,055 additions
and
876 deletions.
There are no files selected for viewing
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
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,139 @@ | ||
using System.Text.Json; | ||
using Backbone.BuildingBlocks.API.Extensions; | ||
using Backbone.BuildingBlocks.Application.Abstractions.Exceptions; | ||
using Backbone.BuildingBlocks.Application.Abstractions.Infrastructure.Persistence.BlobStorage; | ||
using Backbone.Modules.Synchronization.Application.Infrastructure; | ||
using Backbone.Modules.Synchronization.Domain.Entities; | ||
using Backbone.Modules.Synchronization.Infrastructure.Persistence.Database; | ||
using Backbone.Tooling.Extensions; | ||
using Microsoft.Extensions.Options; | ||
|
||
namespace Backbone.ConsumerApi; | ||
|
||
public class SynchronizationDbContextSeeder : IDbSeeder<SynchronizationDbContext> | ||
{ | ||
private const int PAGE_SIZE = 500; | ||
|
||
private readonly IBlobStorage? _blobStorage; | ||
private readonly string? _blobRootFolder; | ||
private readonly ILogger<SynchronizationDbContextSeeder> _logger; | ||
private int _numberOfModificationsWithoutPayload; | ||
|
||
public SynchronizationDbContextSeeder(IServiceProvider serviceProvider, ILogger<SynchronizationDbContextSeeder> logger) | ||
{ | ||
_blobStorage = serviceProvider.GetService<IBlobStorage>(); | ||
_blobRootFolder = serviceProvider.GetService<IOptions<BlobOptions>>()!.Value.RootFolder; | ||
_logger = logger; | ||
} | ||
|
||
public async Task SeedAsync(SynchronizationDbContext context) | ||
{ | ||
await FillEncryptedPayloadColumnFromBlobStorage(context); | ||
} | ||
|
||
private async Task FillEncryptedPayloadColumnFromBlobStorage(SynchronizationDbContext context) | ||
{ | ||
// _blobRootFolder is null when blob storage configuration is not provided, meaning the content of database entries should not be loaded from blob storage | ||
if (_blobRootFolder == null) | ||
return; | ||
|
||
var hasMorePages = true; | ||
|
||
|
||
while (hasMorePages) | ||
{ | ||
var modificationsWithoutEncryptedPayload = context.DatawalletModifications | ||
.Where(m => m.EncryptedPayload == null) | ||
.OrderBy(m => m.Index) | ||
.Skip(_numberOfModificationsWithoutPayload) | ||
.Take(PAGE_SIZE) | ||
.ToList(); | ||
|
||
var blobReferences = modificationsWithoutEncryptedPayload | ||
.Where(m => !string.IsNullOrWhiteSpace(m.BlobReference)) | ||
.Select(m => m.BlobReference) | ||
.ToList(); | ||
|
||
var blobsFromReferences = await FindBlobsByReferences(blobReferences); | ||
|
||
await FillPayloads(context, modificationsWithoutEncryptedPayload, blobsFromReferences); | ||
|
||
await context.SaveChangesAsync(); | ||
|
||
hasMorePages = modificationsWithoutEncryptedPayload.Count != 0; | ||
} | ||
} | ||
|
||
private async Task<Dictionary<string, Dictionary<long, byte[]>>> FindBlobsByReferences(IEnumerable<string> blobReferences) | ||
{ | ||
var blobs = await Task.WhenAll(blobReferences.Select(async r => | ||
{ | ||
try | ||
{ | ||
var blobFromReference = await _blobStorage!.FindAsync(_blobRootFolder!, r); | ||
return new KeyValuePair<string, byte[]?>(r, blobFromReference); | ||
} | ||
catch (NotFoundException) | ||
{ | ||
return new KeyValuePair<string, byte[]?>(r, null); | ||
} | ||
})); | ||
|
||
var deserialized = blobs | ||
.Where(b => b.Value != null) | ||
.Select(b => new KeyValuePair<string, Dictionary<long, byte[]>>(b.Key, JsonSerializer.Deserialize<Dictionary<long, byte[]>>(b.Value!)!)) | ||
.ToDictionary(b => b.Key, b => b.Value); | ||
|
||
return deserialized; | ||
} | ||
|
||
private async Task FillPayloads(SynchronizationDbContext context, List<DatawalletModification> modifications, Dictionary<string, Dictionary<long, byte[]>> blobsFromReferences) | ||
{ | ||
await Task.WhenAll(modifications.Select(async m => await FillPayload(context, m, blobsFromReferences))); | ||
} | ||
|
||
private async Task FillPayload(SynchronizationDbContext context, DatawalletModification modification, Dictionary<string, Dictionary<long, byte[]>> blobsFromReferences) | ||
{ | ||
var hadContent = await FillPayload(modification, blobsFromReferences); | ||
|
||
if (hadContent) | ||
context.DatawalletModifications.Update(modification); | ||
else | ||
Interlocked.Increment(ref _numberOfModificationsWithoutPayload); | ||
} | ||
|
||
private async Task<bool> FillPayload(DatawalletModification modification, Dictionary<string, Dictionary<long, byte[]>> blobsFromReferences) | ||
{ | ||
if (string.IsNullOrWhiteSpace(modification.BlobReference)) | ||
{ | ||
// fill via blob id | ||
try | ||
{ | ||
var blobContent = await _blobStorage!.FindAsync(_blobRootFolder!, modification.Id); | ||
modification.LoadEncryptedPayload(blobContent); | ||
} | ||
catch (NotFoundException) | ||
{ | ||
_logger.LogInformation("Blob with Id '{id}' not found. As the encrypted payload of a datawallet modification is not required, this is probably not an error.", modification.Id); | ||
return false; | ||
} | ||
} | ||
|
||
// fill via blob reference | ||
if (!blobsFromReferences.TryGetValue(modification.BlobReference, out var blob)) | ||
{ | ||
_logger.LogError("Blob with reference '{blobReference}' not found.", modification.BlobReference); | ||
return false; | ||
} | ||
|
||
if (!blob.TryGetValue(modification.Index, out var payload)) | ||
{ | ||
_logger.LogInformation("Blob with Id '{id}' not found in blob reference. As the encrypted payload of a datawallet modification is not required, this is probably not an error.", modification.Id); | ||
return false; | ||
} | ||
|
||
modification.LoadEncryptedPayload(payload); | ||
|
||
return true; | ||
} | ||
} |
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
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
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
Oops, something went wrong.