fix(historian): #668 review follow-ups (explicit modified source time, paging tests, ACL test, QuestDB orphan warning, docs)
- HistoryModifiedValue gains explicit SourceTimeUtc (SqlServer + QuestDB populate it); the node manager's resume cursor uses it instead of the prior value's nullable source timestamp (MinValue for inserts). - ACL test now uses a trail-returning data source and asserts Good + data on the allow leg. - Node-manager tests: end-only backward open-ended paging, reverse resume cursor, null-prior cursor. - QuestDB updater logs a Warning (tag + time range) when a sample write fails after its trail rows landed. - docs/Historian.md: rolling-upgrade window, IDENTITY-vs-commit ordering, QuestDB cross-node ModifiedId clock ordering.
This commit is contained in:
@@ -101,7 +101,7 @@ public sealed class NodeManagerAclEnforcementTests : IDisposable
|
||||
try
|
||||
{
|
||||
var nm = server.NodeManager!;
|
||||
nm.HistorianDataSource = NullHistorianDataSource.Instance;
|
||||
nm.HistorianDataSource = new OneModificationTrail();
|
||||
var nodeId = Historized(nm, "Plant/Modbus/dev1/Speed");
|
||||
|
||||
nm.Authorizer = Authorizer(enabled: true, GrantOn(NodePermissions.Read));
|
||||
@@ -110,8 +110,14 @@ public sealed class NodeManagerAclEnforcementTests : IDisposable
|
||||
deniedResults[0].StatusCode.Code.ShouldBe(StatusCodes.BadUserAccessDenied);
|
||||
|
||||
nm.Authorizer = Authorizer(enabled: true, GrantOn(NodePermissions.HistoryRead));
|
||||
var (_, allowedErrors) = ReadRaw(nm, Identity(), modified: true, nodeId);
|
||||
allowedErrors[0].StatusCode.Code.ShouldBe(StatusCodes.BadHistoryOperationUnsupported);
|
||||
var (allowedResults, allowedErrors) = ReadRaw(nm, Identity(), modified: true, nodeId);
|
||||
// A real trail entry must come back: Null's BadHistoryOperationUnsupported is also what the SDK pre-seeds,
|
||||
// so asserting it would pass even if the grant never reached the modified branch.
|
||||
allowedErrors[0].StatusCode.Code.ShouldBe(StatusCodes.Good);
|
||||
allowedResults[0].StatusCode.Code.ShouldBe(StatusCodes.Good);
|
||||
var data = ExtensionObject.ToEncodeable(allowedResults[0].HistoryData).ShouldBeOfType<HistoryModifiedData>();
|
||||
data.DataValues.Count.ShouldBe(1);
|
||||
data.ModificationInfos[0].UserName.ShouldBe("alice");
|
||||
}
|
||||
finally { await host.DisposeAsync(); }
|
||||
}
|
||||
@@ -327,6 +333,32 @@ public sealed class NodeManagerAclEnforcementTests : IDisposable
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>A backend whose modified-value trail holds exactly one entry; every other read is empty.</summary>
|
||||
private sealed class OneModificationTrail : IHistorianDataSource
|
||||
{
|
||||
public Task<HistoryModifiedReadResult> ReadModifiedAsync(
|
||||
string fullReference, DateTime startUtc, DateTime endUtc, uint maxValues, HistoryModifiedCursor? resumeAfter,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var t = DateTime.UtcNow.AddMinutes(-5);
|
||||
var prior = new DataValueSnapshot(1.5, StatusCodes.Good, t, t);
|
||||
return Task.FromResult(new HistoryModifiedReadResult(
|
||||
[new HistoryModifiedValue(prior, DateTime.UtcNow, HistoryModificationType.Replace, "alice", 1, t)], false));
|
||||
}
|
||||
|
||||
public Task<ZB.MOM.WW.OtOpcUa.Core.Abstractions.HistoryReadResult> ReadRawAsync(string r, DateTime s, DateTime e, uint m, CancellationToken ct) =>
|
||||
NullHistorianDataSource.Instance.ReadRawAsync(r, s, e, m, ct);
|
||||
public Task<ZB.MOM.WW.OtOpcUa.Core.Abstractions.HistoryReadResult> ReadProcessedAsync(
|
||||
string r, DateTime s, DateTime e, TimeSpan i, HistoryAggregateType a, CancellationToken ct) =>
|
||||
NullHistorianDataSource.Instance.ReadProcessedAsync(r, s, e, i, a, ct);
|
||||
public Task<ZB.MOM.WW.OtOpcUa.Core.Abstractions.HistoryReadResult> ReadAtTimeAsync(string r, IReadOnlyList<DateTime> t, CancellationToken ct) =>
|
||||
NullHistorianDataSource.Instance.ReadAtTimeAsync(r, t, ct);
|
||||
public Task<HistoricalEventsResult> ReadEventsAsync(string? n, DateTime s, DateTime e, int m, CancellationToken ct) =>
|
||||
NullHistorianDataSource.Instance.ReadEventsAsync(n, s, e, m, ct);
|
||||
public HistorianHealthSnapshot GetHealthSnapshot() => NullHistorianDataSource.Instance.GetHealthSnapshot();
|
||||
public void Dispose() { }
|
||||
}
|
||||
|
||||
private static NodeId Historized(OtOpcUaNodeManager nm, string rawPath)
|
||||
{
|
||||
nm.EnsureVariable(rawPath, parentFolderNodeId: null, displayName: rawPath, dataType: "Float",
|
||||
|
||||
Reference in New Issue
Block a user