feat(reconcile): site-reconcile messages + expected-set/stage-if-absent repo
- Commons: ReconcileSiteRequest / ReconcileSiteResponse / ReconcileGapItem message contracts (site→central ClusterClient on startup; central reply with gap fetch tokens + orphan list + base URL). - Commons: ExpectedDeployment projection record (Commons/Types/Deployment/), lightweight join of DeployedConfigSnapshot + Instance (no ConfigJson). - IDeploymentManagerRepository: GetExpectedDeploymentsForSiteAsync (inner-join query returning deployed set for a site, excluding snapshot-less instances) + StagePendingIfAbsentAsync (insert-if-absent, self-contained save, returns bool; does NOT supersede — an existing pending row signals in-flight delivery). - DeploymentManagerRepository: implement both methods; StagePendingIfAbsent commits internally (matches PurgeExpiredPendingDeployments convention). - ReconcileRepositoryTests: 4 tests covering expected-set filter/IsEnabled/ cross-site isolation, empty-site, stage-absent (true + row retrievable), stage-present (false + existing row unchanged); all pass.
This commit is contained in:
+49
@@ -2,6 +2,8 @@ using Microsoft.EntityFrameworkCore;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Deployment;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Instances;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Repositories;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Types.Deployment;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;
|
||||
|
||||
namespace ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Repositories;
|
||||
|
||||
@@ -251,6 +253,53 @@ public class DeploymentManagerRepository : IDeploymentManagerRepository
|
||||
return expired.Count;
|
||||
}
|
||||
|
||||
// --- Startup reconciliation ---
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<IReadOnlyList<ExpectedDeployment>> GetExpectedDeploymentsForSiteAsync(int siteId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
// Inner join: only instances that have a DeployedConfigSnapshot are returned.
|
||||
// ConfigurationJson is intentionally excluded — the caller fetches the full config
|
||||
// on demand via the HTTP endpoint for gap instances only.
|
||||
return await (
|
||||
from snap in _dbContext.Set<DeployedConfigSnapshot>()
|
||||
join inst in _dbContext.Set<Instance>() on snap.InstanceId equals inst.Id
|
||||
where inst.SiteId == siteId
|
||||
select new ExpectedDeployment(
|
||||
inst.Id,
|
||||
inst.UniqueName,
|
||||
snap.RevisionHash,
|
||||
snap.DeploymentId,
|
||||
inst.State == InstanceState.Enabled))
|
||||
.ToListAsync(cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<bool> StagePendingIfAbsentAsync(
|
||||
int instanceId, string deploymentId, string revisionHash,
|
||||
string configurationJson, string token,
|
||||
DateTimeOffset createdAtUtc, DateTimeOffset expiresAtUtc,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
// Insert-if-absent: do NOT supersede an existing pending row. An existing row means an
|
||||
// in-flight deploy is already delivering to the node; clobbering it could cause the node
|
||||
// to fetch the reconcile token while the original deliver is mid-flight.
|
||||
// Self-contained (commits internally), matching PurgeExpiredPendingDeploymentsAsync.
|
||||
var alreadyPending = await _dbContext.Set<PendingDeployment>()
|
||||
.AnyAsync(p => p.InstanceId == instanceId, cancellationToken);
|
||||
if (alreadyPending)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var pending = new PendingDeployment(
|
||||
deploymentId, instanceId, revisionHash,
|
||||
configurationJson, token, createdAtUtc, expiresAtUtc);
|
||||
await _dbContext.Set<PendingDeployment>().AddAsync(pending, cancellationToken);
|
||||
await _dbContext.SaveChangesAsync(cancellationToken);
|
||||
return true;
|
||||
}
|
||||
|
||||
// --- Instance lookups for deployment pipeline ---
|
||||
|
||||
/// <inheritdoc />
|
||||
|
||||
Reference in New Issue
Block a user