|
1 | 1 | using Cognite.Extractor.Common; |
2 | 2 | using Cognite.Extractor.Configuration; |
| 3 | +using Cognite.OpcUa; |
3 | 4 | using Cognite.OpcUa.Config; |
4 | 5 | using Cognite.OpcUa.History; |
5 | 6 | using Cognite.OpcUa.Nodes; |
@@ -1157,5 +1158,168 @@ public async Task TestHistoryReadFailureThreshold() |
1157 | 1158 | var exc = await Assert.ThrowsAsync<SmartAggregateException>(() => CommonTestUtils.RunHistory(reader, states, HistoryReadType.BackfillData)); |
1158 | 1159 | Assert.Equal("2 errors of type Opc.Ua.ServiceResultException. StatusCode: BadInvalidArgument", exc.Message); |
1159 | 1160 | } |
| 1161 | + |
| 1162 | + /// <summary> |
| 1163 | + /// Test that if EnqueueAsync fails (simulating a crash), the state is NOT updated. |
| 1164 | + /// This verifies the fix where we enqueue BEFORE updating state to ensure at-least-once delivery. |
| 1165 | + /// </summary> |
| 1166 | + [Fact] |
| 1167 | + public async Task TestHistoryDataHandlerEnqueueFailure() |
| 1168 | + { |
| 1169 | + await using var extractor = tester.BuildExtractor(); |
| 1170 | + var cfg = new HistoryConfig |
| 1171 | + { |
| 1172 | + Data = true |
| 1173 | + }; |
| 1174 | + |
| 1175 | + tester.Config.History = cfg; |
| 1176 | + |
| 1177 | + // Inject a mock streamer that throws on EnqueueAsync |
| 1178 | + var mockStreamer = new FailingStreamer( |
| 1179 | + tester.Provider.GetRequiredService<ILogger<Streamer>>(), |
| 1180 | + extractor, |
| 1181 | + tester.Config); |
| 1182 | + var streamerField = typeof(UAExtractor).GetField("<Streamer>k__BackingField", BindingFlags.Instance | BindingFlags.NonPublic); |
| 1183 | + streamerField?.SetValue(extractor, mockStreamer); |
| 1184 | + |
| 1185 | + using var throttler = new TaskThrottler(2, false); |
| 1186 | + var cps = new BlockingResourceCounter(1000); |
| 1187 | + var dummyState = new UAHistoryExtractionState(tester.Client, new NodeId("test", 0), true, true); |
| 1188 | + var log = tester.Provider.GetRequiredService<ILogger<HistoryReaderTest>>(); |
| 1189 | + |
| 1190 | + using var reader = new HistoryScheduler(log, tester.Client, extractor, extractor.TypeManager, tester.Config, HistoryReadType.FrontfillData, |
| 1191 | + throttler, cps, new[] { dummyState }, tester.Source.Token); |
| 1192 | + |
| 1193 | + var dt = new UADataType(DataTypeIds.Double); |
| 1194 | + var var1 = new UAVariable(new NodeId("state1", 0), "state1", null, null, NodeId.Null, null); |
| 1195 | + var1.FullAttributes.DataType = dt; |
| 1196 | + var state1 = new VariableExtractionState(extractor, var1, true, true, true); |
| 1197 | + extractor.State.SetNodeState(state1, "state1"); |
| 1198 | + state1.FinalizeRangeInit(); |
| 1199 | + |
| 1200 | + // Capture initial state |
| 1201 | + Assert.True(state1.IsFrontfilling); |
| 1202 | + var initialRange = state1.SourceExtractedRange; |
| 1203 | + |
| 1204 | + var start = DateTime.UtcNow; |
| 1205 | + var dataValues = new DataValueCollection(Enumerable.Range(0, 10) |
| 1206 | + .Select(idx => new DataValue(idx, StatusCodes.Good, start.AddSeconds(idx)))); |
| 1207 | + var historyData = new HistoryData { DataValues = dataValues }; |
| 1208 | + |
| 1209 | + var node = new HistoryReadNode(HistoryReadType.FrontfillData, new NodeId("state1", 0)) |
| 1210 | + { |
| 1211 | + LastResult = historyData, |
| 1212 | + ContinuationPoint = null |
| 1213 | + }; |
| 1214 | + |
| 1215 | + var historyDataHandler = reader.GetType().GetMethod("HistoryDataHandler", BindingFlags.NonPublic | BindingFlags.Instance); |
| 1216 | + |
| 1217 | + // The handler should throw because EnqueueAsync fails |
| 1218 | + var ex = await Assert.ThrowsAsync<Exception>(() => (Task)historyDataHandler!.Invoke(reader, new object[] { node })!); |
| 1219 | + Assert.Contains("Simulated enqueue failure", ex.Message, StringComparison.OrdinalIgnoreCase); |
| 1220 | + |
| 1221 | + // CRITICAL: State should NOT have been updated because the exception happened before state update |
| 1222 | + Assert.Equal(initialRange.Last, state1.SourceExtractedRange.Last); |
| 1223 | + Assert.Equal(initialRange.First, state1.SourceExtractedRange.First); |
| 1224 | + Assert.True(state1.IsFrontfilling, "State should still be frontfilling since update was never called"); |
| 1225 | + } |
| 1226 | + |
| 1227 | + /// <summary> |
| 1228 | + /// Test that if EnqueueAsync fails for events (simulating a crash), the state is NOT updated. |
| 1229 | + /// This verifies the fix where we enqueue BEFORE updating state to ensure at-least-once delivery. |
| 1230 | + /// </summary> |
| 1231 | + [Fact] |
| 1232 | + public async Task TestHistoryEventHandlerEnqueueFailure() |
| 1233 | + { |
| 1234 | + await using var extractor = tester.BuildExtractor(); |
| 1235 | + var cfg = new HistoryConfig |
| 1236 | + { |
| 1237 | + Backfill = true |
| 1238 | + }; |
| 1239 | + |
| 1240 | + tester.Config.History = cfg; |
| 1241 | + |
| 1242 | + // Inject a mock streamer that throws on EnqueueAsync for events |
| 1243 | + var mockStreamer = new FailingStreamer( |
| 1244 | + tester.Provider.GetRequiredService<ILogger<Streamer>>(), |
| 1245 | + extractor, |
| 1246 | + tester.Config); |
| 1247 | + var streamerField = typeof(UAExtractor).GetField("<Streamer>k__BackingField", BindingFlags.Instance | BindingFlags.NonPublic); |
| 1248 | + streamerField?.SetValue(extractor, mockStreamer); |
| 1249 | + |
| 1250 | + var log = tester.Provider.GetRequiredService<ILogger<HistoryReaderTest>>(); |
| 1251 | + |
| 1252 | + using var throttler = new TaskThrottler(2, false); |
| 1253 | + var cps = new BlockingResourceCounter(1000); |
| 1254 | + var dummyState = new UAHistoryExtractionState(tester.Client, new NodeId("test", 0), true, true); |
| 1255 | + |
| 1256 | + using var reader = new HistoryScheduler(log, tester.Client, extractor, extractor.TypeManager, tester.Config, HistoryReadType.FrontfillEvents, |
| 1257 | + throttler, cps, new[] { dummyState }, tester.Source.Token); |
| 1258 | + |
| 1259 | + var state = EventUtils.PopulateEventData(extractor, tester, false); |
| 1260 | + state.FinalizeRangeInit(); |
| 1261 | + |
| 1262 | + // Capture initial state |
| 1263 | + Assert.True(state.IsFrontfilling); |
| 1264 | + var initialRange = state.SourceExtractedRange; |
| 1265 | + |
| 1266 | + var filter = new EventFilter { SelectClauses = EventUtils.GetSelectClause(tester) }; |
| 1267 | + var details = new ReadEventDetails { Filter = filter }; |
| 1268 | + |
| 1269 | + var start = DateTime.UtcNow; |
| 1270 | + var frontfillEvents = new HistoryEventFieldListCollection(Enumerable.Range(0, 10) |
| 1271 | + .Select(idx => EventUtils.GetEventValues(start.AddSeconds(idx))) |
| 1272 | + .Select(values => new HistoryEventFieldList { EventFields = values })); |
| 1273 | + var historyEvents = new HistoryEvent { Events = frontfillEvents }; |
| 1274 | + |
| 1275 | + var node = new HistoryReadNode(HistoryReadType.FrontfillEvents, new NodeId("emitter", 0)) |
| 1276 | + { |
| 1277 | + LastResult = historyEvents, |
| 1278 | + ContinuationPoint = null |
| 1279 | + }; |
| 1280 | + |
| 1281 | + var historyEventHandler = reader.GetType().GetMethod("HistoryEventHandler", BindingFlags.NonPublic | BindingFlags.Instance); |
| 1282 | + |
| 1283 | + // The handler should throw because EnqueueAsync fails |
| 1284 | + var ex = await Assert.ThrowsAsync<Exception>(() => (Task)historyEventHandler!.Invoke(reader, new object[] { node, details })!); |
| 1285 | + Assert.Contains("Simulated enqueue failure", ex.Message, StringComparison.OrdinalIgnoreCase); |
| 1286 | + |
| 1287 | + // CRITICAL: State should NOT have been updated because the exception happened before state update |
| 1288 | + Assert.Equal(initialRange.Last, state.SourceExtractedRange.Last); |
| 1289 | + Assert.Equal(initialRange.First, state.SourceExtractedRange.First); |
| 1290 | + Assert.True(state.IsFrontfilling, "State should still be frontfilling since update was never called"); |
| 1291 | + } |
| 1292 | + |
| 1293 | + /// <summary> |
| 1294 | + /// Mock Streamer that throws on EnqueueAsync to simulate a crash during enqueue. |
| 1295 | + /// Used to verify that state updates happen AFTER successful enqueue. |
| 1296 | + /// </summary> |
| 1297 | + private class FailingStreamer : Streamer |
| 1298 | + { |
| 1299 | + public FailingStreamer(ILogger<Streamer> log, UAExtractor extractor, FullConfig config) |
| 1300 | + : base(log, extractor, config) |
| 1301 | + { |
| 1302 | + } |
| 1303 | + |
| 1304 | + public override Task EnqueueAsync(UADataPoint dp) |
| 1305 | + { |
| 1306 | + throw new Exception("Simulated enqueue failure for datapoint"); |
| 1307 | + } |
| 1308 | + |
| 1309 | + public override Task EnqueueAsync(IEnumerable<UADataPoint> dps) |
| 1310 | + { |
| 1311 | + throw new Exception("Simulated enqueue failure for datapoints"); |
| 1312 | + } |
| 1313 | + |
| 1314 | + public override Task EnqueueAsync(UAEvent evt) |
| 1315 | + { |
| 1316 | + throw new Exception("Simulated enqueue failure for event"); |
| 1317 | + } |
| 1318 | + |
| 1319 | + public override Task EnqueueAsync(IEnumerable<UAEvent> events) |
| 1320 | + { |
| 1321 | + throw new Exception("Simulated enqueue failure for events"); |
| 1322 | + } |
| 1323 | + } |
1160 | 1324 | } |
1161 | 1325 | } |
0 commit comments