fix(historian): QuestDB modified reads keep a landed change whose marker was lost; H2 review NITs (lmxopcua#778)
QuestDB modified-history reads dropped every still-pending trail row that was not the latest at its source time, so V0->V1 (applied marker never written) followed by V1->V2 hid the V0->V1 change for good. A pending row below the latest is now served when the NEXT trail row at that time recorded a different prior value (kind, value, status or the prior's server_ts) - the next change saw what this one wrote; an orphan of a change that never landed leaves the same prior for the next row and stays hidden. QuestDB window functions reject VARCHAR, so the next row's prior comes from a LEFT JOIN on (ts, lead(id)). The updater's comment and marker-failure log no longer claim modified reads "still serve it" unconditionally. Regression tests (QuestDB 10.0.1 throwaway container): marker lost then a replace / delete at the same time; an orphan followed by a marker-lost retry then a later change (orphan still dropped); the random marker-mix fuzz now varies the recorded priors. All four fail on the old filter. NITs: - SqlServerHistorianValueWriter: a SqlException raised while the token is cancelled (SqlClient's "Operation cancelled by user") is rethrown as OperationCanceledException instead of booked as a failed write. Integration test holds a TABLOCKX on Sample and cancels mid-INSERT; it returned false before. - SqlServerHistorianDataUpdater.DeleteRawBatches is an Interlocked counter. - HistoryReadEvents: an event filter whose SDK validation throws (unknown operator) failed with the generic BadUnexpectedError; the node manager now re-throws any failed event-filter validation as BadEventFilterInvalid. Claude-Session: https://claude.ai/code/session_01GT67AEHd2Acv1YPwGfXEoV
This commit is contained in:
@@ -5703,6 +5703,56 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2
|
||||
NormalizeHistoryValues(nodesToProcess, results);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
/// <remarks>
|
||||
/// The SDK validates a HistoryReadEvents event filter before <see cref="HistoryReadEvents"/> runs and throws the
|
||||
/// validation's status — <c>BadEventFilterInvalid</c> for a structural fault (operand count, a dangling element
|
||||
/// operand) — but when the validation itself throws (an unknown filter operator) the request failed with the generic
|
||||
/// <c>BadUnexpectedError</c>. Any failure of that validation is re-thrown as <c>BadEventFilterInvalid</c> (#778);
|
||||
/// every other refusal (a bad time range…) and a valid filter pass through unchanged.
|
||||
/// </remarks>
|
||||
protected override void HistoryRead(
|
||||
ServerSystemContext context,
|
||||
HistoryReadDetails details,
|
||||
TimestampsToReturn timestampsToReturn,
|
||||
bool releaseContinuationPoints,
|
||||
IList<HistoryReadValueId> nodesToRead,
|
||||
IList<SdkHistoryReadResult> results,
|
||||
IList<ServiceResult> errors,
|
||||
List<NodeHandle> nodesToProcess,
|
||||
IDictionary<NodeId, NodeState> cache)
|
||||
{
|
||||
try
|
||||
{
|
||||
base.HistoryRead(context, details, timestampsToReturn, releaseContinuationPoints, nodesToRead, results, errors,
|
||||
nodesToProcess, cache);
|
||||
}
|
||||
catch (ServiceResultException ex) when (ex.StatusCode != StatusCodes.BadEventFilterInvalid
|
||||
&& details is ReadEventDetails { Filter: { } filter }
|
||||
&& !IsValidEventFilter(filter, context))
|
||||
{
|
||||
throw ServiceResultException.Create(StatusCodes.BadEventFilterInvalid, ex,
|
||||
"The HistoryReadEvents event filter is invalid: {0}", ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>The SDK's validation of a HistoryReadEvents event filter; a throw counts as invalid.</summary>
|
||||
/// <param name="filter">The event filter.</param>
|
||||
/// <param name="context">The request's context.</param>
|
||||
/// <returns><c>true</c> when the filter validates Good.</returns>
|
||||
private bool IsValidEventFilter(EventFilter filter, ServerSystemContext context)
|
||||
{
|
||||
try
|
||||
{
|
||||
var filterContext = new FilterContext(Server.NamespaceUris, Server.TypeTree, context.PreferredLocales, Server.Telemetry);
|
||||
return ServiceResult.IsGood(filter.Validate(filterContext).Status);
|
||||
}
|
||||
catch (ServiceResultException)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void HistoryReadEvents(
|
||||
ServerSystemContext context,
|
||||
|
||||
Reference in New Issue
Block a user