diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/Historian/AlarmHistorianOptions.cs b/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/Historian/AlarmHistorianOptions.cs
index aec679e5..b99d55fa 100644
--- a/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/Historian/AlarmHistorianOptions.cs
+++ b/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/Historian/AlarmHistorianOptions.cs
@@ -1,5 +1,6 @@
using System.Collections.Generic;
using System.IO;
+using ZB.MOM.WW.OtOpcUa.Core.AlarmHistorian;
namespace ZB.MOM.WW.OtOpcUa.Runtime.Historian;
@@ -38,7 +39,7 @@ public sealed class AlarmHistorianOptions
/// Maximum queued rows before the sink evicts the oldest. Defaults to 1,000,000
/// (matches SqliteStoreAndForwardSink's DefaultCapacity).
- public long Capacity { get; init; } = 1_000_000;
+ public long Capacity { get; init; } = SqliteStoreAndForwardSink.DefaultCapacity;
/// Days to retain dead-lettered rows before purge. Defaults to 30.
public int DeadLetterRetentionDays { get; init; } = 30;
@@ -54,6 +55,12 @@ public sealed class AlarmHistorianOptions
warnings.Add("AlarmHistorian:SharedSecret is empty while the historian is enabled — the Wonderware sidecar Hello frame will carry an empty secret.");
if (!Path.IsPathRooted(DatabasePath))
warnings.Add($"AlarmHistorian:DatabasePath '{DatabasePath}' is relative — it resolves against the process working directory (e.g. System32 for a Windows service). Set an absolute path.");
+ if (DrainIntervalSeconds <= 0)
+ warnings.Add($"AlarmHistorian:DrainIntervalSeconds is {DrainIntervalSeconds} — must be > 0; the drain timer will throw or spin at startup.");
+ if (Capacity <= 0)
+ warnings.Add($"AlarmHistorian:Capacity is {Capacity} — must be > 0; the sink constructor will throw at startup.");
+ if (DeadLetterRetentionDays <= 0)
+ warnings.Add($"AlarmHistorian:DeadLetterRetentionDays is {DeadLetterRetentionDays} — must be > 0; dead-lettered rows would be purged on every drain tick.");
return warnings;
}
}
diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/ServiceCollectionExtensions.cs b/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/ServiceCollectionExtensions.cs
index 67003c07..fc89162c 100644
--- a/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/ServiceCollectionExtensions.cs
+++ b/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/ServiceCollectionExtensions.cs
@@ -73,7 +73,7 @@ public static class ServiceCollectionExtensions
if (opts is not { Enabled: true }) return services; // leave the Null default from AddOtOpcUaRuntime
foreach (var warning in opts.Validate())
- Serilog.Log.Logger.ForContext().Warning("{HistorianConfigWarning}", warning);
+ Serilog.Log.Logger.ForContext().Warning("Historian config: {HistorianConfigWarning}", warning);
services.AddSingleton(sp =>
{
diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/Historian/AlarmHistorianRegistrationTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/Historian/AlarmHistorianRegistrationTests.cs
index b1d0b9e6..de3b44a2 100644
--- a/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/Historian/AlarmHistorianRegistrationTests.cs
+++ b/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/Historian/AlarmHistorianRegistrationTests.cs
@@ -137,4 +137,29 @@ public sealed class AlarmHistorianRegistrationTests
{
new AlarmHistorianOptions { Enabled = false, SharedSecret = "" }.Validate().ShouldBeEmpty();
}
+
+ [Fact]
+ public void Validate_warns_on_non_positive_drain_interval()
+ {
+ var opts = new AlarmHistorianOptions { Enabled = true, SharedSecret = "s", DatabasePath = "/abs/h.db", DrainIntervalSeconds = 0 };
+ opts.Validate().ShouldContain(w => w.Contains("DrainIntervalSeconds"));
+ }
+
+ [Fact]
+ public void Validate_warns_on_non_positive_capacity()
+ {
+ var opts = new AlarmHistorianOptions { Enabled = true, SharedSecret = "s", DatabasePath = "/abs/h.db", Capacity = 0 };
+ opts.Validate().ShouldContain(w => w.Contains("Capacity"));
+ }
+
+ [Fact]
+ public void Validate_accumulates_multiple_warnings()
+ {
+ // relative path + empty secret ⇒ both warnings, not short-circuited on the first.
+ var opts = new AlarmHistorianOptions { Enabled = true, SharedSecret = "", DatabasePath = "alarm-historian.db" };
+ var warnings = opts.Validate();
+ warnings.ShouldContain(w => w.Contains("SharedSecret"));
+ warnings.ShouldContain(w => w.Contains("DatabasePath"));
+ warnings.Count.ShouldBeGreaterThanOrEqualTo(2);
+ }
}