fix(management): invalidate ExternalDefinitionLookupCache on external-system / DB-connection edits (ScadaBridge#65)
ManagementActor now drops the central definition cache post-commit (via the existing AfterConfigurationCommit seam) after every external system, external-system method and database connection create/update/delete, so an edit takes effect on the editing node immediately instead of after DefinitionCacheTtl. ManagementService gains a project reference to ExternalSystemGateway. The other central node still converges within the TTL.
This commit is contained in:
+106
@@ -0,0 +1,106 @@
|
||||
using Akka.Actor;
|
||||
using Akka.TestKit.Xunit2;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using NSubstitute;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Entities.ExternalSystems;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Repositories;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Services;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Messages.Management;
|
||||
using ZB.MOM.WW.ScadaBridge.ExternalSystemGateway;
|
||||
using ZB.MOM.WW.ScadaBridge.Security;
|
||||
|
||||
namespace ZB.MOM.WW.ScadaBridge.ManagementService.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// ScadaBridge#65: the central <see cref="ExternalDefinitionLookupCache"/> used to be dropped only
|
||||
/// by its TTL, so an external-system / method / database-connection edit made through the management
|
||||
/// path was invisible to inbound-method calls on the same node for up to <c>DefinitionCacheTtl</c>.
|
||||
/// Every such mutation now invalidates it once the change is committed.
|
||||
/// </summary>
|
||||
public class ExternalDefinitionCacheInvalidationTests : TestKit
|
||||
{
|
||||
private readonly IExternalSystemRepository _repo = Substitute.For<IExternalSystemRepository>();
|
||||
// A TTL far longer than the test: only an explicit InvalidateAll can make the cache re-fetch.
|
||||
private readonly ExternalDefinitionLookupCache _cache = new(TimeSpan.FromHours(1));
|
||||
private readonly ServiceCollection _services = new ServiceCollection().AddTestConfigurationTransaction();
|
||||
|
||||
/// <summary>Initializes the harness with one external system, one method and one DB connection.</summary>
|
||||
public ExternalDefinitionCacheInvalidationTests()
|
||||
{
|
||||
_repo.GetExternalSystemByIdAsync(1, Arg.Any<CancellationToken>())
|
||||
.Returns(_ => new ExternalSystemDefinition("Erp", "http://erp", "None") { Id = 1 });
|
||||
_repo.GetExternalSystemMethodByIdAsync(2, Arg.Any<CancellationToken>())
|
||||
.Returns(_ => new ExternalSystemMethod("Get", "GET", "/x") { Id = 2, ExternalSystemDefinitionId = 1 });
|
||||
_repo.GetDatabaseConnectionByIdAsync(3, Arg.Any<CancellationToken>())
|
||||
.Returns(_ => new DatabaseConnectionDefinition("Db", "Server=x") { Id = 3 });
|
||||
_services.AddScoped(_ => _repo);
|
||||
_services.AddScoped(_ => Substitute.For<IAuditService>());
|
||||
_services.AddSingleton(_cache);
|
||||
}
|
||||
|
||||
private IActorRef CreateActor() =>
|
||||
Sys.ActorOf(Props.Create(() => new ManagementActor(
|
||||
_services.BuildServiceProvider(), NullLogger<ManagementActor>.Instance)));
|
||||
|
||||
private static ManagementEnvelope Admin(object command) =>
|
||||
new(new AuthenticatedUser("admin", "Admin", new[] { "Designer" }, Array.Empty<string>()),
|
||||
command, Guid.NewGuid().ToString("N"));
|
||||
|
||||
private async Task<int> PrimeAndCountFetches()
|
||||
{
|
||||
var fetches = 0;
|
||||
Task<ExternalSystemDefinition?> Fetch()
|
||||
{
|
||||
fetches++;
|
||||
return Task.FromResult<ExternalSystemDefinition?>(new ExternalSystemDefinition("Erp", "http://erp", "None"));
|
||||
}
|
||||
|
||||
await _cache.GetSystemAsync("Erp", Fetch);
|
||||
await _cache.GetSystemAsync("Erp", Fetch);
|
||||
Assert.Equal(1, fetches); // second read was served from the cache
|
||||
return fetches;
|
||||
}
|
||||
|
||||
private async Task<bool> IsStillCached()
|
||||
{
|
||||
var refetched = false;
|
||||
await _cache.GetSystemAsync("Erp", () =>
|
||||
{
|
||||
refetched = true;
|
||||
return Task.FromResult<ExternalSystemDefinition?>(new ExternalSystemDefinition("Erp", "http://erp", "None"));
|
||||
});
|
||||
return !refetched;
|
||||
}
|
||||
|
||||
/// <summary>Every external-system / method / database-connection mutation drops the cached definitions.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task EveryDefinitionMutation_InvalidatesTheCache()
|
||||
{
|
||||
object[] commands =
|
||||
[
|
||||
new CreateExternalSystemCommand("New", "http://n", "None", null),
|
||||
new UpdateExternalSystemCommand(1, "Erp", "http://erp2", "None", null),
|
||||
new DeleteExternalSystemCommand(1),
|
||||
new CreateExternalSystemMethodCommand(1, "M", "GET", "/m"),
|
||||
new UpdateExternalSystemMethodCommand(2, Path: "/y"),
|
||||
new DeleteExternalSystemMethodCommand(2),
|
||||
new CreateDatabaseConnectionDefCommand("Db2", "Server=y"),
|
||||
new UpdateDatabaseConnectionDefCommand(3, "Db", "Server=z"),
|
||||
new DeleteDatabaseConnectionDefCommand(3),
|
||||
];
|
||||
var actor = CreateActor();
|
||||
foreach (var command in commands)
|
||||
{
|
||||
_cache.InvalidateAll();
|
||||
await PrimeAndCountFetches();
|
||||
Assert.True(await IsStillCached());
|
||||
|
||||
actor.Tell(Admin(command));
|
||||
ExpectMsg<ManagementSuccess>(TimeSpan.FromSeconds(5));
|
||||
|
||||
Assert.False(await IsStillCached()); // dropped by the edit, so the lookup re-fetched
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user