diff --git a/Source/Libraries/Adapters/openHistorian.Adapters/LocalOutputAdapter.cs b/Source/Libraries/Adapters/openHistorian.Adapters/LocalOutputAdapter.cs
index fae8b16bc4..456fc33911 100755
--- a/Source/Libraries/Adapters/openHistorian.Adapters/LocalOutputAdapter.cs
+++ b/Source/Libraries/Adapters/openHistorian.Adapters/LocalOutputAdapter.cs
@@ -18,6 +18,9 @@
// ----------------------------------------------------------------------------------------------------
// 07/25/2013 - J. Ritchie Carroll
// Generated original version of source code.
+// 09/25/2026 - J. Ritchie Carroll
+// Updated to support optional point ID / time-bucket Bloom filter indexes on finalized archives
+// to support optimized nearest timestamp seek filters for sparse data reads.
//
//******************************************************************************************************
@@ -60,1337 +63,1433 @@
using GSFDataPublisher = GSF.TimeSeries.Transport.DataPublisher;
using STTPDataPublisher = sttp.DataPublisher;
-namespace openHistorian.Adapters
+namespace openHistorian.Adapters;
+
+///
+/// Represents an output adapter that archives measurements to a local archive.
+///
+[Description("openHistorian 2.0 (Local): archives measurements to a local in-process openHistorian.")]
+public class LocalOutputAdapter : OutputAdapterBase
{
+ #region [ Members ]
+
+ // Constants
+
+ ///
+ /// Defines the default listening port for the historian.
+ ///
+ public const int DefaultPort = 38402;
+
+ ///
+ /// Defines default value for .
+ ///
+ public const bool DefaultWatchAttachedPaths = false;
+
+ ///
+ /// Defines default value for .
+ ///
+ public const bool DefaultEnablePointIDIndex = true;
+
+ ///
+ /// Defines default value for .
+ ///
+ public const bool DefaultEnableTimeBucketBloomFilter = false;
+
+ ///
+ /// Defines default value for .
+ ///
+ public const int DefaultBloomFilterMaximumIndexSizeMB = 64;
+
+ ///
+ /// Defines default value for .
+ ///
+ public const int DefaultBloomFilterBucketDurationSeconds = 900;
+
+ ///
+ /// Defines default value for .
+ ///
+ public const double DefaultBloomFilterFalsePositiveProbability = 0.01D;
+
+ ///
+ /// Defines default value for .
+ ///
+ public const string DefaultDataChannel = "port=38402; interface=::0";
+
+ ///
+ /// Defines the default value for .
+ ///
+ public const double DefaultTargetFileSize = 2.0D;
+
+ ///
+ /// Defines the default value for .
+ ///
+ public const double DefaultDesiredRemainingSpace = 100.0D;
+
+ ///
+ /// Defines the default value for .
+ ///
+ public const int DefaultMaximumArchiveDays = 0;
+
+ ///
+ /// Defines the default value for .
+ ///
+ public const string DefaultDownsamplingIntervals = "";
+
+ ///
+ /// Defines the default value for .
+ ///
+ public const bool DefaultAutoRemoveOldestFilesBeforeFull = true;
+
+ ///
+ /// Defines the default value for .
+ ///
+ public const bool DefaultEnableTimeReasonabilityCheck = false;
+
///
- /// Represents an output adapter that archives measurements to a local archive.
+ /// Defines the default value for .
///
- [Description("openHistorian 2.0 (Local): archives measurements to a local in-process openHistorian.")]
- public class LocalOutputAdapter : OutputAdapterBase
+ public const double DefaultPastTimeReasonabilityLimit = 43200.0D;
+
+ ///
+ /// Defines the default value for .
+ ///
+ public const double DefaultFutureTimeReasonabilityLimit = 43200.0D;
+
+ ///
+ /// Defines the default value for .
+ ///
+ public const bool DefaultSwingingDoorCompressionEnabled = false;
+
+ ///
+ /// Defines the default value for .
+ ///
+ public const ArchiveDirectoryMethod DefaultDirectoryNamingMode = ArchiveDirectoryMethod.YearThenMonth;
+
+ ///
+ /// Defines the default value for .
+ ///
+ public const ArchiveDirectoryFillMethod DefaultDirectoryFillMethod = ArchiveDirectoryFillMethod.Sequential;
+
+ ///
+ /// Defines the default value for .
+ ///
+ public const int DefaultArchiveCurtailmentInterval = Time.SecondsPerDay;
+
+ // Fields
+ private HistorianIArchive m_archive;
+ private HistorianServerDatabaseConfig m_archiveInfo;
+ private string m_instanceName;
+ private string m_workingDirectory;
+ private string[] m_archiveDirectories;
+ private string[] m_attachedPaths;
+ private double m_targetFileSize;
+ private double m_desiredRemainingSpace;
+ private long m_pastTimeReasonabilityLimit;
+ private long m_futureTimeReasonabilityLimit;
+ private DataServices m_dataServices;
+ private ReplicationProviders m_replicationProviders;
+ private long m_archivedMeasurements;
+ private readonly HistorianKey m_key;
+ private readonly HistorianValue m_value;
+ private Dictionary m_measurements;
+ private Dictionary> m_compressionSettings;
+ private readonly Dictionary> m_swingingDoorStates;
+ private SortedList m_downsamplingIntervals;
+ private Timer m_archiveCurtailmentTimer;
+ private SafeFileWatcher[] m_attachedPathWatchers;
+ private bool m_disposed;
+
+ #endregion
+
+ #region [ Constructors ]
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public LocalOutputAdapter()
{
- #region [ Members ]
-
- // Constants
-
- ///
- /// Defines the default listening port for the historian.
- ///
- public const int DefaultPort = 38402;
-
- ///
- /// Defines default value for .
- ///
- public const bool DefaultWatchAttachedPaths = false;
-
- ///
- /// Defines default value for .
- ///
- public const string DefaultDataChannel = "port=38402; interface=::0";
-
- ///
- /// Defines the default value for .
- ///
- public const double DefaultTargetFileSize = 2.0D;
-
- ///
- /// Defines the default value for .
- ///
- public const double DefaultDesiredRemainingSpace = 100.0D;
-
- ///
- /// Defines the default value for .
- ///
- public const int DefaultMaximumArchiveDays = 0;
-
- ///
- /// Defines the default value for .
- ///
- public const string DefaultDownsamplingIntervals = "";
-
- ///
- /// Defines the default value for .
- ///
- public const bool DefaultAutoRemoveOldestFilesBeforeFull = true;
-
- ///
- /// Defines the default value for .
- ///
- public const bool DefaultEnableTimeReasonabilityCheck = false;
-
- ///
- /// Defines the default value for .
- ///
- public const double DefaultPastTimeReasonabilityLimit = 43200.0D;
-
- ///
- /// Defines the default value for .
- ///
- public const double DefaultFutureTimeReasonabilityLimit = 43200.0D;
-
- ///
- /// Defines the default value for .
- ///
- public const bool DefaultSwingingDoorCompressionEnabled = false;
-
- ///
- /// Defines the default value for .
- ///
- public const ArchiveDirectoryMethod DefaultDirectoryNamingMode = ArchiveDirectoryMethod.YearThenMonth;
-
- ///
- /// Defines the default value for .
- ///
- public const ArchiveDirectoryFillMethod DefaultDirectoryFillMethod = ArchiveDirectoryFillMethod.Sequential;
-
- ///
- /// Defines the default value for .
- ///
- public const int DefaultArchiveCurtailmentInterval = Time.SecondsPerDay;
-
- // Fields
- private HistorianIArchive m_archive;
- private HistorianServerDatabaseConfig m_archiveInfo;
- private string m_instanceName;
- private string m_workingDirectory;
- private string[] m_archiveDirectories;
- private string[] m_attachedPaths;
- private double m_targetFileSize;
- private double m_desiredRemainingSpace;
- private long m_pastTimeReasonabilityLimit;
- private long m_futureTimeReasonabilityLimit;
- private DataServices m_dataServices;
- private ReplicationProviders m_replicationProviders;
- private long m_archivedMeasurements;
- private readonly HistorianKey m_key;
- private readonly HistorianValue m_value;
- private Dictionary m_measurements;
- private Dictionary> m_compressionSettings;
- private readonly Dictionary> m_swingingDoorStates;
- private SortedList m_downsamplingIntervals;
- private Timer m_archiveCurtailmentTimer;
- private SafeFileWatcher[] m_attachedPathWatchers;
- private bool m_disposed;
-
- #endregion
-
- #region [ Constructors ]
-
- ///
- /// Initializes a new instance of the class.
- ///
- public LocalOutputAdapter()
- {
- m_key = new HistorianKey();
- m_value = new HistorianValue();
- m_swingingDoorStates = new Dictionary>();
- m_downsamplingIntervals = new SortedList();
- }
+ m_key = new HistorianKey();
+ m_value = new HistorianValue();
+ m_swingingDoorStates = new Dictionary>();
+ m_downsamplingIntervals = new SortedList();
+ }
- #endregion
+ #endregion
- #region [ Properties ]
+ #region [ Properties ]
- ///
- /// Gets or sets instance name defined for this .
- ///
- [ConnectionStringParameter]
- [Description("Define the instance name for the historian. Leave this value blank to default to the adapter name.")]
- [DefaultValue("")]
- public string InstanceName
- {
- get => string.IsNullOrEmpty(m_instanceName) ? Name.ToLower() : m_instanceName;
- set => m_instanceName = value;
- }
+ ///
+ /// Controls exact per-file point ID index generation on newly finalized archives.
+ ///
+ [ConnectionStringParameter]
+ [DefaultValue(DefaultEnablePointIDIndex)]
+ [Description("Generate exact point ID presence sidecars during rollover. Existing indexes remain readable when disabled.")]
+ public bool EnablePointIDIndex { get; set; } = DefaultEnablePointIDIndex;
+
+ ///
+ /// Enables time-bucket Bloom indexes on newly finalized archives.
+ ///
+ [ConnectionStringParameter]
+ [DefaultValue(DefaultEnableTimeBucketBloomFilter)]
+ [Description("Generate optional time-bucket PointID Bloom sidecars during rollover. Existing indexes remain readable when disabled.")]
+ public bool EnableTimeBucketBloomFilter { get; set; } = DefaultEnableTimeBucketBloomFilter;
+
+ ///
+ /// Maximum uncompressed Bloom index payload per archive in MB.
+ ///
+ [ConnectionStringParameter]
+ [DefaultValue(DefaultBloomFilterMaximumIndexSizeMB)]
+ [Description("Maximum uncompressed Bloom index payload per archive, 1 to 64 MB. If exceeded, omit the entire Bloom index and use ordinary reads.")]
+ public int BloomFilterMaximumIndexSizeMB { get; set; } = DefaultBloomFilterMaximumIndexSizeMB;
+
+ ///
+ /// Gets or sets Bloom bucket width in seconds (1 through 86400).
+ ///
+ [ConnectionStringParameter]
+ [DefaultValue(DefaultBloomFilterBucketDurationSeconds)]
+ [Description("Bloom bucket duration in seconds. Smaller buckets improve gap pruning but increase index size. Range: 1 to 86400.")]
+ public int BloomFilterBucketDurationSeconds { get; set; } = DefaultBloomFilterBucketDurationSeconds;
+
+ ///
+ /// Gets or sets the target per-bucket false-positive probability.
+ ///
+ [ConnectionStringParameter]
+ [DefaultValue(DefaultBloomFilterFalsePositiveProbability)]
+ [Description("Target per-bucket Bloom false-positive probability, e.g., 0.01 for 1%. Range: 0.0001 to 0.25. Lower values increase size and hashing work.")]
+ public double BloomFilterFalsePositiveProbability { get; set; } = DefaultBloomFilterFalsePositiveProbability;
+
+ ///
+ /// Gets or sets instance name defined for this .
+ ///
+ [ConnectionStringParameter]
+ [Description("Define the instance name for the historian. Leave this value blank to default to the adapter name.")]
+ [DefaultValue("")]
+ public string InstanceName
+ {
+ get => string.IsNullOrEmpty(m_instanceName) ? Name.ToLower() : m_instanceName;
+ set => m_instanceName = value;
+ }
+
+ ///
+ /// Gets or sets TCP server based connection string to use for the historian data channel.
+ ///
+ [ConnectionStringParameter]
+ [Description("Defines TCP server based connection string to use for the historian data channel.")]
+ [DefaultValue(DefaultDataChannel)]
+ public string DataChannel { get; set; }
- ///
- /// Gets or sets TCP server based connection string to use for the historian data channel.
- ///
- [ConnectionStringParameter]
- [Description("Defines TCP server based connection string to use for the historian data channel.")]
- [DefaultValue(DefaultDataChannel)]
- public string DataChannel { get; set; }
-
- ///
- /// Gets or sets the working directory which is used to write working data before it is moved into its permanent file.
- ///
- [ConnectionStringParameter]
- [Description("Define the working directory used for working data and intermediate files and before moving data to its permanent location (see ArchiveDirectories). Leave blank to default to \".\\Archive\\\".")]
- [DefaultValue("")]
- public string WorkingDirectory
+ ///
+ /// Gets or sets the working directory which is used to write working data before it is moved into its permanent file.
+ ///
+ [ConnectionStringParameter]
+ [Description("Define the working directory used for working data and intermediate files and before moving data to its permanent location (see ArchiveDirectories). Leave blank to default to \".\\Archive\\\".")]
+ [DefaultValue("")]
+ public string WorkingDirectory
+ {
+ get => m_workingDirectory;
+ set
{
- get => m_workingDirectory;
- set
- {
- string localPath = FilePath.GetAbsolutePath(value);
+ string localPath = FilePath.GetAbsolutePath(value);
- if (!Directory.Exists(localPath))
+ if (!Directory.Exists(localPath))
+ {
+ try
{
- try
- {
- Directory.CreateDirectory(localPath);
- }
- catch (Exception ex)
- {
- OnProcessException(MessageLevel.Error, new InvalidOperationException($"Failed to create working directory \"{localPath}\": {ex.Message}", ex));
- }
+ Directory.CreateDirectory(localPath);
+ }
+ catch (Exception ex)
+ {
+ OnProcessException(MessageLevel.Error, new InvalidOperationException($"Failed to create working directory \"{localPath}\": {ex.Message}", ex));
}
-
- m_workingDirectory = localPath;
}
+
+ m_workingDirectory = localPath;
}
+ }
- ///
- /// Gets or sets the write directories for the historian.
- ///
- [ConnectionStringParameter]
- [Description("Define the write directories for this historian instance. Leave empty to default to WorkingDirectory. Separate multiple directories with a semi-colon.")]
- [DefaultValue("")]
- public string ArchiveDirectories
+ ///
+ /// Gets or sets the write directories for the historian.
+ ///
+ [ConnectionStringParameter]
+ [Description("Define the write directories for this historian instance. Leave empty to default to WorkingDirectory. Separate multiple directories with a semi-colon.")]
+ [DefaultValue("")]
+ public string ArchiveDirectories
+ {
+ get => m_archiveDirectories is null ? "" : string.Join(";", m_archiveDirectories);
+ set
{
- get => m_archiveDirectories is null ? "" : string.Join(";", m_archiveDirectories);
- set
+ if (value is null)
{
- if (value is null)
- {
- m_archiveDirectories = null;
- }
- else
+ m_archiveDirectories = null;
+ }
+ else
+ {
+ List archivePaths = [];
+
+ foreach (string archivePath in value.Split(';'))
{
- List archivePaths = [];
+ string localPath = FilePath.GetAbsolutePath(archivePath.Trim());
- foreach (string archivePath in value.Split(';'))
+ if (!Directory.Exists(localPath))
{
- string localPath = FilePath.GetAbsolutePath(archivePath.Trim());
-
- if (!Directory.Exists(localPath))
+ try
{
- try
- {
- Directory.CreateDirectory(localPath);
- }
- catch (Exception ex)
- {
- OnProcessException(MessageLevel.Error, new InvalidOperationException($"Failed to create archive directory \"{localPath}\": {ex.Message}", ex));
- }
+ Directory.CreateDirectory(localPath);
+ }
+ catch (Exception ex)
+ {
+ OnProcessException(MessageLevel.Error, new InvalidOperationException($"Failed to create archive directory \"{localPath}\": {ex.Message}", ex));
}
-
- archivePaths.Add(localPath);
}
- m_archiveDirectories = archivePaths.ToArray();
+ archivePaths.Add(localPath);
}
+
+ m_archiveDirectories = archivePaths.ToArray();
}
}
+ }
+
+ ///
+ /// Gets or sets directory naming mode for archive directory files.
+ ///
+ [ConnectionStringParameter]
+ [Description("Define the directory naming mode for archive directory files.")]
+ [DefaultValue(DefaultDirectoryNamingMode)]
+ public ArchiveDirectoryMethod DirectoryNamingMode { get; set; }
+
+ ///
+ /// Gets or sets the method used to select a write directory when multiple archive directories are configured.
+ ///
+ ///
+ /// (default) fills each archive directory in order, only
+ /// advancing to the next when the current drive lacks space - this matches the historical behavior.
+ /// distributes new files across the configured directories.
+ ///
+ [ConnectionStringParameter]
+ [Description("Define how a write directory is selected when multiple archive directories are configured: 'Sequential' fills each directory in order until full (default), 'RoundRobin' distributes new files across directories.")]
+ [DefaultValue(DefaultDirectoryFillMethod)]
+ public ArchiveDirectoryFillMethod DirectoryFillMethod { get; set; }
- ///
- /// Gets or sets directory naming mode for archive directory files.
- ///
- [ConnectionStringParameter]
- [Description("Define the directory naming mode for archive directory files.")]
- [DefaultValue(DefaultDirectoryNamingMode)]
- public ArchiveDirectoryMethod DirectoryNamingMode { get; set; }
-
- ///
- /// Gets or sets the method used to select a write directory when multiple archive directories are configured.
- ///
- ///
- /// (default) fills each archive directory in order, only
- /// advancing to the next when the current drive lacks space - this matches the historical behavior.
- /// distributes new files across the configured directories.
- ///
- [ConnectionStringParameter]
- [Description("Define how a write directory is selected when multiple archive directories are configured: 'Sequential' fills each directory in order until full (default), 'RoundRobin' distributes new files across directories.")]
- [DefaultValue(DefaultDirectoryFillMethod)]
- public ArchiveDirectoryFillMethod DirectoryFillMethod { get; set; }
-
- ///
- /// Gets or sets the default interval, in seconds, over which the archive curtailment will operate. Set to zero to disable.
- ///
- [ConnectionStringParameter]
- [Description("Define the default interval, in seconds, over which the archive curtailment will operate. Set to zero to disable.")]
- [DefaultValue(DefaultArchiveCurtailmentInterval)]
- public int ArchiveCurtailmentInterval { get; set; }
-
- ///
- /// Gets or sets the directories and/or individual files to attach to the historian.
- ///
- [ConnectionStringParameter]
- [Description("Define the directories and/or individual files to attach to this historian instance. Separate multiple paths with a semi-colon.")]
- [DefaultValue("")]
- public string AttachedPaths
+ ///
+ /// Gets or sets the default interval, in seconds, over which the archive curtailment will operate. Set to zero to disable.
+ ///
+ [ConnectionStringParameter]
+ [Description("Define the default interval, in seconds, over which the archive curtailment will operate. Set to zero to disable.")]
+ [DefaultValue(DefaultArchiveCurtailmentInterval)]
+ public int ArchiveCurtailmentInterval { get; set; }
+
+ ///
+ /// Gets or sets the directories and/or individual files to attach to the historian.
+ ///
+ [ConnectionStringParameter]
+ [Description("Define the directories and/or individual files to attach to this historian instance. Separate multiple paths with a semi-colon.")]
+ [DefaultValue("")]
+ public string AttachedPaths
+ {
+ get => m_attachedPaths is null ? "" : string.Join(";", m_attachedPaths);
+ set
{
- get => m_attachedPaths is null ? "" : string.Join(";", m_attachedPaths);
- set
+ if (value is null)
{
- if (value is null)
+ m_attachedPaths = null;
+ }
+ else
+ {
+ List attachedPaths = [];
+
+ foreach (string archivePath in value.Split(';'))
{
- m_attachedPaths = null;
+ string localPath = FilePath.GetAbsolutePath(archivePath);
+
+ if (Directory.Exists(localPath) || File.Exists(localPath))
+ attachedPaths.Add(localPath);
+ else
+ OnProcessException(MessageLevel.Error, new InvalidOperationException($"Failed to locate \"{localPath}\""));
}
- else
- {
- List attachedPaths = [];
- foreach (string archivePath in value.Split(';'))
- {
- string localPath = FilePath.GetAbsolutePath(archivePath);
+ m_attachedPaths = attachedPaths.ToArray();
+ }
+ }
+ }
- if (Directory.Exists(localPath) || File.Exists(localPath))
- attachedPaths.Add(localPath);
- else
- OnProcessException(MessageLevel.Error, new InvalidOperationException($"Failed to locate \"{localPath}\""));
- }
+ ///
+ /// Gets or sets the flag which determines whether to set up file watchers to monitor the attached paths.
+ ///
+ [ConnectionStringParameter]
+ [Description("Determines whether to set up file watchers to monitor the attached paths.")]
+ [DefaultValue(DefaultWatchAttachedPaths)]
+ public bool WatchAttachedPaths { get; set; }
- m_attachedPaths = attachedPaths.ToArray();
- }
- }
+ ///
+ /// Gets or sets target file size, in GigaBytes.
+ ///
+ [ConnectionStringParameter]
+ [Description("Define desired target file size in GigaBytes.")]
+ [DefaultValue(DefaultTargetFileSize)]
+ public double TargetFileSize
+ {
+ get => m_targetFileSize;
+ set
+ {
+ if (value < 0.1D || value > SI2.Tera)
+ throw new ArgumentOutOfRangeException(nameof(value));
+
+ m_targetFileSize = value;
}
+ }
- ///
- /// Gets or sets the flag which determines whether to set up file watchers to monitor the attached paths.
- ///
- [ConnectionStringParameter]
- [Description("Determines whether to set up file watchers to monitor the attached paths.")]
- [DefaultValue(DefaultWatchAttachedPaths)]
- public bool WatchAttachedPaths { get; set; }
-
- ///
- /// Gets or sets target file size, in GigaBytes.
- ///
- [ConnectionStringParameter]
- [Description("Define desired target file size in GigaBytes.")]
- [DefaultValue(DefaultTargetFileSize)]
- public double TargetFileSize
+ ///
+ /// Gets or sets desired remaining space, in GigaBytes.
+ ///
+ [ConnectionStringParameter]
+ [Description("Define desired remaining disk space in GigaBytes.")]
+ [DefaultValue(DefaultDesiredRemainingSpace)]
+ public double DesiredRemainingSpace
+ {
+ get => m_desiredRemainingSpace;
+ set
{
- get => m_targetFileSize;
- set
- {
- if (value < 0.1D || value > SI2.Tera)
- throw new ArgumentOutOfRangeException(nameof(value));
+ if (value < 0.1D || value > SI2.Tera)
+ throw new ArgumentOutOfRangeException(nameof(value));
- m_targetFileSize = value;
- }
+ m_desiredRemainingSpace = value;
}
+ }
- ///
- /// Gets or sets desired remaining space, in GigaBytes.
- ///
- [ConnectionStringParameter]
- [Description("Define desired remaining disk space in GigaBytes.")]
- [DefaultValue(DefaultDesiredRemainingSpace)]
- public double DesiredRemainingSpace
+ ///
+ /// Gets or sets the maximum number of days of data to maintain in the archive.
+ ///
+ [ConnectionStringParameter]
+ [Description("Define the maximum number of days of data to maintain, i.e., any archives files with data older than current date minus value will be deleted daily. Defaults to zero meaning no maximum.")]
+ [DefaultValue(DefaultMaximumArchiveDays)]
+ public int MaximumArchiveDays { get; set; } = DefaultMaximumArchiveDays;
+
+ ///
+ /// Gets or sets the scheduled downsampling intervals for this archive.
+ ///
+ [ConnectionStringParameter]
+ [Description("Define the scheduled downsampling intervals for this archive. Format: any number of downsampling-start-days=samples-per-second separated by semi-colons, e.g., 180=10; 365=1; 650=0.1")]
+ [DefaultValue(DefaultDownsamplingIntervals)]
+ public string DownsamplingIntervals
+ {
+ get
{
- get => m_desiredRemainingSpace;
- set
+ if (m_downsamplingIntervals.Count == 0)
+ return string.Empty;
+
+ StringBuilder downsampling = new();
+
+ foreach (KeyValuePair interval in m_downsamplingIntervals)
{
- if (value < 0.1D || value > SI2.Tera)
- throw new ArgumentOutOfRangeException(nameof(value));
+ if (downsampling.Length > 0)
+ downsampling.Append("; ");
- m_desiredRemainingSpace = value;
+ downsampling.Append($"{interval.Key}={interval.Value}");
}
- }
- ///
- /// Gets or sets the maximum number of days of data to maintain in the archive.
- ///
- [ConnectionStringParameter]
- [Description("Define the maximum number of days of data to maintain, i.e., any archives files with data older than current date minus value will be deleted daily. Defaults to zero meaning no maximum.")]
- [DefaultValue(DefaultMaximumArchiveDays)]
- public int MaximumArchiveDays { get; set; } = DefaultMaximumArchiveDays;
-
- ///
- /// Gets or sets the scheduled downsampling intervals for this archive.
- ///
- [ConnectionStringParameter]
- [Description("Define the scheduled downsampling intervals for this archive. Format: any number of downsampling-start-days=samples-per-second separated by semi-colons, e.g., 180=10; 365=1; 650=0.1")]
- [DefaultValue(DefaultDownsamplingIntervals)]
- public string DownsamplingIntervals
+ return downsampling.ToString();
+ }
+ set
{
- get
- {
- if (m_downsamplingIntervals.Count == 0)
- return string.Empty;
+ m_downsamplingIntervals.Clear();
- StringBuilder downsampling = new();
+ if (string.IsNullOrWhiteSpace(value))
+ return;
- foreach (KeyValuePair interval in m_downsamplingIntervals)
- {
- if (downsampling.Length > 0)
- downsampling.Append("; ");
+ foreach (string interval in value.Split(';'))
+ {
+ string[] parts = interval.Split('=');
- downsampling.Append($"{interval.Key}={interval.Value}");
- }
+ if (parts.Length != 2)
+ continue;
- return downsampling.ToString();
+ if (int.TryParse(parts[0].Trim(), out int startDay) && startDay > 0 && double.TryParse(parts[1].Trim(), out double samplesPerSecond) && samplesPerSecond > 0.0D)
+ m_downsamplingIntervals[startDay] = samplesPerSecond;
}
- set
- {
- m_downsamplingIntervals.Clear();
+ }
+ }
- if (string.IsNullOrWhiteSpace(value))
- return;
+ ///
+ /// Gets or sets the flag that determines if oldest archive files should be removed before running out of archive space.
+ ///
+ [ConnectionStringParameter]
+ [Description("Define the flag that determines if oldest archive files should be removed before running out of archive space.")]
+ [DefaultValue(DefaultAutoRemoveOldestFilesBeforeFull)]
+ public bool AutoRemoveOldestFilesBeforeFull { get; set; } = DefaultAutoRemoveOldestFilesBeforeFull;
- foreach (string interval in value.Split(';'))
- {
- string[] parts = interval.Split('=');
+ ///
+ /// Gets or sets flag that indicates if incoming timestamps to the historian should be validated for reasonability.
+ ///
+ [ConnectionStringParameter]
+ [Description("Define the flag that indicates if incoming timestamps to the historian should be validated for reasonability.")]
+ [DefaultValue(DefaultEnableTimeReasonabilityCheck)]
+ public bool EnableTimeReasonabilityCheck { get; set; }
- if (parts.Length != 2)
- continue;
+ ///
+ /// Gets or sets the maximum number of seconds that a past timestamp, as compared to local clock, will be considered valid.
+ ///
+ [ConnectionStringParameter]
+ [Description("Define the maximum number of seconds that a past timestamp, as compared to local clock, will be considered valid.")]
+ [DefaultValue(DefaultPastTimeReasonabilityLimit)]
+ public double PastTimeReasonabilityLimit
+ {
+ get => new Ticks(m_pastTimeReasonabilityLimit).ToSeconds();
+ set => m_pastTimeReasonabilityLimit = Ticks.FromSeconds(Math.Abs(value));
+ }
- if (int.TryParse(parts[0].Trim(), out int startDay) && startDay > 0 && double.TryParse(parts[1].Trim(), out double samplesPerSecond) && samplesPerSecond > 0.0D)
- m_downsamplingIntervals[startDay] = samplesPerSecond;
- }
- }
- }
+ ///
+ /// Gets or sets the maximum number of seconds that a future timestamp, as compared to local clock, will be considered valid.
+ ///
+ [ConnectionStringParameter]
+ [Description("Define the maximum number of seconds that a future timestamp, as compared to local clock, will be considered valid.")]
+ [DefaultValue(DefaultFutureTimeReasonabilityLimit)]
+ public double FutureTimeReasonabilityLimit
+ {
+ get => new Ticks(m_futureTimeReasonabilityLimit).ToSeconds();
+ set => m_futureTimeReasonabilityLimit = Ticks.FromSeconds(Math.Abs(value));
+ }
- ///
- /// Gets or sets the flag that determines if oldest archive files should be removed before running out of archive space.
- ///
- [ConnectionStringParameter]
- [Description("Define the flag that determines if oldest archive files should be removed before running out of archive space.")]
- [DefaultValue(DefaultAutoRemoveOldestFilesBeforeFull)]
- public bool AutoRemoveOldestFilesBeforeFull { get; set; } = DefaultAutoRemoveOldestFilesBeforeFull;
-
- ///
- /// Gets or sets flag that indicates if incoming timestamps to the historian should be validated for reasonability.
- ///
- [ConnectionStringParameter]
- [Description("Define the flag that indicates if incoming timestamps to the historian should be validated for reasonability.")]
- [DefaultValue(DefaultEnableTimeReasonabilityCheck)]
- public bool EnableTimeReasonabilityCheck { get; set; }
-
- ///
- /// Gets or sets the maximum number of seconds that a past timestamp, as compared to local clock, will be considered valid.
- ///
- [ConnectionStringParameter]
- [Description("Define the maximum number of seconds that a past timestamp, as compared to local clock, will be considered valid.")]
- [DefaultValue(DefaultPastTimeReasonabilityLimit)]
- public double PastTimeReasonabilityLimit
- {
- get => new Ticks(m_pastTimeReasonabilityLimit).ToSeconds();
- set => m_pastTimeReasonabilityLimit = Ticks.FromSeconds(Math.Abs(value));
- }
+ ///
+ /// Gets or sets the flag that determines if swinging door compression is enabled for this historian instance.
+ ///
+ [ConnectionStringParameter]
+ [Description("Define the flag that determines if swinging door compression is enabled for this historian instance.")]
+ [DefaultValue(DefaultSwingingDoorCompressionEnabled)]
+ public bool SwingingDoorCompressionEnabled { get; set; }
- ///
- /// Gets or sets the maximum number of seconds that a future timestamp, as compared to local clock, will be considered valid.
- ///
- [ConnectionStringParameter]
- [Description("Define the maximum number of seconds that a future timestamp, as compared to local clock, will be considered valid.")]
- [DefaultValue(DefaultFutureTimeReasonabilityLimit)]
- public double FutureTimeReasonabilityLimit
- {
- get => new Ticks(m_futureTimeReasonabilityLimit).ToSeconds();
- set => m_futureTimeReasonabilityLimit = Ticks.FromSeconds(Math.Abs(value));
- }
+ ///
+ /// Returns a flag that determines if measurements sent to this are destined for archival.
+ ///
+ public override bool OutputIsForArchive => true;
- ///
- /// Gets or sets the flag that determines if swinging door compression is enabled for this historian instance.
- ///
- [ConnectionStringParameter]
- [Description("Define the flag that determines if swinging door compression is enabled for this historian instance.")]
- [DefaultValue(DefaultSwingingDoorCompressionEnabled)]
- public bool SwingingDoorCompressionEnabled { get; set; }
-
- ///
- /// Returns a flag that determines if measurements sent to this are destined for archival.
- ///
- public override bool OutputIsForArchive => true;
-
- ///
- /// Gets flag that determines if this uses an asynchronous connection.
- ///
- protected override bool UseAsyncConnect => true;
-
- ///
- /// Gets or sets based data source available to this .
- ///
- public override DataSet DataSource
+ ///
+ /// Gets flag that determines if this uses an asynchronous connection.
+ ///
+ protected override bool UseAsyncConnect => true;
+
+ ///
+ /// Gets or sets based data source available to this .
+ ///
+ public override DataSet DataSource
+ {
+ get => base.DataSource;
+ set
{
- get => base.DataSource;
- set
- {
- base.DataSource = value;
+ base.DataSource = value;
- if (value is null)
- return;
+ if (value is null)
+ return;
- Dictionary measurements = new();
- string instanceName = InstanceName;
+ Dictionary measurements = new();
+ string instanceName = InstanceName;
- // Create dictionary of metadata for this server instance
- foreach (DataRow row in value.Tables["ActiveMeasurements"].Rows)
- {
- if (MeasurementKey.TryParse(row["ID"].ToString(), out MeasurementKey key) && (key.Source?.Equals(instanceName, StringComparison.OrdinalIgnoreCase) ?? false))
- measurements[key.ID] = row;
- }
+ // Create dictionary of metadata for this server instance
+ foreach (DataRow row in value.Tables["ActiveMeasurements"].Rows)
+ {
+ if (MeasurementKey.TryParse(row["ID"].ToString(), out MeasurementKey key) && (key.Source?.Equals(instanceName, StringComparison.OrdinalIgnoreCase) ?? false))
+ measurements[key.ID] = row;
+ }
- Dictionary> compressionSettings = new();
+ Dictionary> compressionSettings = new();
- if (value.Tables.Contains("CompressionSettings"))
+ if (value.Tables.Contains("CompressionSettings"))
+ {
+ // Extract compression settings for defined measurements
+ foreach (DataRow row in value.Tables["CompressionSettings"].Rows)
{
- // Extract compression settings for defined measurements
- foreach (DataRow row in value.Tables["CompressionSettings"].Rows)
- {
- uint pointID = row.ConvertField("PointID");
+ uint pointID = row.ConvertField("PointID");
- if (InputMeasurementKeys.All(key => key.ID != pointID))
- continue;
+ if (InputMeasurementKeys.All(key => key.ID != pointID))
+ continue;
- // Get compression settings
- int compressionMinTime = row.ConvertField("CompressionMinTime");
- int compressionMaxTime = row.ConvertField("CompressionMaxTime");
- double compressionLimit = row.ConvertField("CompressionLimit");
+ // Get compression settings
+ int compressionMinTime = row.ConvertField("CompressionMinTime");
+ int compressionMaxTime = row.ConvertField("CompressionMaxTime");
+ double compressionLimit = row.ConvertField("CompressionLimit");
- compressionSettings[pointID] = new Tuple(compressionMinTime, compressionMaxTime, compressionLimit);
- }
+ compressionSettings[pointID] = new Tuple(compressionMinTime, compressionMaxTime, compressionLimit);
}
+ }
- Interlocked.Exchange(ref m_measurements, measurements);
- Interlocked.Exchange(ref m_compressionSettings, compressionSettings);
+ Interlocked.Exchange(ref m_measurements, measurements);
+ Interlocked.Exchange(ref m_compressionSettings, compressionSettings);
- // When metadata is updated for an output adapter, reset sliding memory caches for Grafana data sources
- TargetCaches.ResetAll();
- }
+ // When metadata is updated for an output adapter, reset sliding memory caches for Grafana data sources
+ TargetCaches.ResetAll();
}
+ }
- ///
- /// Returns the detailed status of the data output source.
- ///
- public override string Status
+ ///
+ /// Returns the detailed status of the data output source.
+ ///
+ public override string Status
+ {
+ get
{
- get
+ StringBuilder status = new();
+
+ status.Append(base.Status);
+ status.AppendLine();
+
+ status.AppendLine($" Historian instance name: {InstanceName}");
+ status.AppendLine($" Working directory: {FilePath.TrimFileName(WorkingDirectory, 51)}");
+ status.AppendLine($" Network data channel: {DataChannel.ToNonNullString(DefaultDataChannel)}");
+ status.AppendLine($" Target file size: {TargetFileSize:N4}GB");
+ status.AppendLine($" Desired remaining space: {DesiredRemainingSpace:N4}GB");
+ status.AppendLine($" Directory naming mode: {DirectoryNamingMode}");
+ status.AppendLine($" Disk flush interval: {m_archiveInfo.DiskFlushInterval:N0}ms");
+ status.AppendLine($" Cache flush interval: {m_archiveInfo.CacheFlushInterval:N0}ms");
+ status.AppendLine($" Staging count: {m_archiveInfo.StagingCount:N0}");
+ status.AppendLine($" Point ID index enabled: {EnablePointIDIndex}");
+ status.AppendLine($" Bloom filter enabled: {EnableTimeBucketBloomFilter}");
+
+ if (EnableTimeBucketBloomFilter)
{
- StringBuilder status = new();
-
- status.Append(base.Status);
- status.AppendLine();
-
- status.AppendLine($" Historian instance name: {InstanceName}");
- status.AppendLine($" Working directory: {FilePath.TrimFileName(WorkingDirectory, 51)}");
- status.AppendLine($" Network data channel: {DataChannel.ToNonNullString(DefaultDataChannel)}");
- status.AppendLine($" Target file size: {TargetFileSize:N4}GB");
- status.AppendLine($" Desired remaining space: {DesiredRemainingSpace:N4}GB");
- status.AppendLine($" Directory naming mode: {DirectoryNamingMode}");
- status.AppendLine($" Disk flush interval: {m_archiveInfo.DiskFlushInterval:N0}ms");
- status.AppendLine($" Cache flush interval: {m_archiveInfo.CacheFlushInterval:N0}ms");
- status.AppendLine($" Staging count: {m_archiveInfo.StagingCount:N0}");
- status.AppendLine($" Memory pool size: {Globals.MemoryPool.MaximumPoolSize / SI2.Giga:N4}GB");
- status.AppendLine($" Maximum archive days: {(MaximumArchiveDays < 1 ? "No limit" : MaximumArchiveDays.ToString("N0"))}");
- status.AppendLine($" Downsampling intervals: {(string.IsNullOrWhiteSpace(DownsamplingIntervals) ? "None defined" : DownsamplingIntervals)}");
- status.AppendLine($" Auto-remove old archives: {AutoRemoveOldestFilesBeforeFull}");
- status.AppendLine($" Time reasonability check: {(EnableTimeReasonabilityCheck ? "Enabled" : "Not Enabled")}");
- status.AppendLine($" Archive curtailment timer: {Time.ToElapsedTimeString(ArchiveCurtailmentInterval, 0)}");
-
- if (EnableTimeReasonabilityCheck)
- {
- status.AppendLine($" Maximum past time limit: {PastTimeReasonabilityLimit:N4}s, i.e., {new Ticks(m_pastTimeReasonabilityLimit).ToElapsedTimeString(4)}");
- status.AppendLine($" Maximum future time limit: {FutureTimeReasonabilityLimit:N4}s, i.e., {new Ticks(m_futureTimeReasonabilityLimit).ToElapsedTimeString(4)}");
- }
+ status.AppendLine($" Bloom index max size: {BloomFilterMaximumIndexSizeMB} MB/archive");
+ status.AppendLine($" Bloom bucket duration: {BloomFilterBucketDurationSeconds:N0} seconds");
+ status.AppendLine($" Bloom false-positive prob: {BloomFilterFalsePositiveProbability:P2}");
+ }
+
+ status.AppendLine($" Memory pool size: {Globals.MemoryPool.MaximumPoolSize / SI2.Giga:N4}GB");
+ status.AppendLine($" Maximum archive days: {(MaximumArchiveDays < 1 ? "No limit" : MaximumArchiveDays.ToString("N0"))}");
+ status.AppendLine($" Downsampling intervals: {(string.IsNullOrWhiteSpace(DownsamplingIntervals) ? "None defined" : DownsamplingIntervals)}");
+ status.AppendLine($" Auto-remove old archives: {AutoRemoveOldestFilesBeforeFull}");
+ status.AppendLine($" Time reasonability check: {(EnableTimeReasonabilityCheck ? "Enabled" : "Not Enabled")}");
+ status.AppendLine($" Archive curtailment timer: {Time.ToElapsedTimeString(ArchiveCurtailmentInterval, 0)}");
+
+ if (EnableTimeReasonabilityCheck)
+ {
+ status.AppendLine($" Maximum past time limit: {PastTimeReasonabilityLimit:N4}s, i.e., {new Ticks(m_pastTimeReasonabilityLimit).ToElapsedTimeString(4)}");
+ status.AppendLine($" Maximum future time limit: {FutureTimeReasonabilityLimit:N4}s, i.e., {new Ticks(m_futureTimeReasonabilityLimit).ToElapsedTimeString(4)}");
+ }
- if (m_dataServices is not null)
- status.Append(m_dataServices.Status);
+ if (m_dataServices is not null)
+ status.Append(m_dataServices.Status);
- if (m_replicationProviders is not null)
- status.Append(m_replicationProviders.Status);
+ if (m_replicationProviders is not null)
+ status.Append(m_replicationProviders.Status);
- Server?.Host?.GetFullStatus(status, 25);
+ Server?.Host?.GetFullStatus(status, 25);
- return status.ToString();
- }
+ return status.ToString();
}
+ }
- ///
- /// Historian server instance.
- ///
- public HistorianServer Server { get; private set; }
+ ///
+ /// Historian server instance.
+ ///
+ public HistorianServer Server { get; private set; }
- ///
- /// Active measurement metadata dictionary.
- ///
- public Dictionary Measurements => m_measurements;
+ ///
+ /// Active measurement metadata dictionary.
+ ///
+ public Dictionary Measurements => m_measurements;
- private SafeFileWatcher[] AttachedPathWatchers
+ private SafeFileWatcher[] AttachedPathWatchers
+ {
+ set
{
- set
+ if (m_attachedPathWatchers is not null)
{
- if (m_attachedPathWatchers is not null)
+ foreach (SafeFileWatcher watcher in m_attachedPathWatchers)
{
- foreach (SafeFileWatcher watcher in m_attachedPathWatchers)
- {
- watcher.EnableRaisingEvents = false;
- watcher.Dispose();
- }
+ watcher.EnableRaisingEvents = false;
+ watcher.Dispose();
}
-
- m_attachedPathWatchers = value;
}
+
+ m_attachedPathWatchers = value;
}
+ }
+
+ #endregion
- #endregion
+ #region [ Methods ]
- #region [ Methods ]
+ ///
+ /// Releases the unmanaged resources used by this and optionally releases the managed resources.
+ ///
+ /// true to release both managed and unmanaged resources; false to release only unmanaged resources.
+ protected override void Dispose(bool disposing)
+ {
+ if (m_disposed)
+ return;
- ///
- /// Releases the unmanaged resources used by this and optionally releases the managed resources.
- ///
- /// true to release both managed and unmanaged resources; false to release only unmanaged resources.
- protected override void Dispose(bool disposing)
+ try
{
- if (m_disposed)
+ if (!disposing)
return;
- try
- {
- if (!disposing)
- return;
-
- AttachedPathWatchers = null;
-
- if (m_dataServices is not null)
- {
- m_dataServices.AdapterCreated -= DataServices_AdapterCreated;
- m_dataServices.AdapterLoaded -= DataServices_AdapterLoaded;
- m_dataServices.AdapterUnloaded -= DataServices_AdapterUnloaded;
- m_dataServices.AdapterLoadException -= AdapterLoader_AdapterLoadException;
- m_dataServices.Dispose();
- }
+ AttachedPathWatchers = null;
- if (m_replicationProviders is not null)
- {
- m_replicationProviders.AdapterCreated -= ReplicationProviders_AdapterCreated;
- m_replicationProviders.AdapterLoaded -= ReplicationProviders_AdapterLoaded;
- m_replicationProviders.AdapterUnloaded -= ReplicationProviders_AdapterUnloaded;
- m_replicationProviders.AdapterLoadException -= AdapterLoader_AdapterLoadException;
- m_replicationProviders.Dispose();
- }
+ if (m_dataServices is not null)
+ {
+ m_dataServices.AdapterCreated -= DataServices_AdapterCreated;
+ m_dataServices.AdapterLoaded -= DataServices_AdapterLoaded;
+ m_dataServices.AdapterUnloaded -= DataServices_AdapterUnloaded;
+ m_dataServices.AdapterLoadException -= AdapterLoader_AdapterLoadException;
+ m_dataServices.Dispose();
+ }
- if (m_archiveCurtailmentTimer is not null)
- {
- m_archiveCurtailmentTimer.Stop();
- m_archiveCurtailmentTimer.Elapsed -= ArchiveCurtailmentTimerElapsed;
- m_archiveCurtailmentTimer.Dispose();
- }
+ if (m_replicationProviders is not null)
+ {
+ m_replicationProviders.AdapterCreated -= ReplicationProviders_AdapterCreated;
+ m_replicationProviders.AdapterLoaded -= ReplicationProviders_AdapterLoaded;
+ m_replicationProviders.AdapterUnloaded -= ReplicationProviders_AdapterUnloaded;
+ m_replicationProviders.AdapterLoadException -= AdapterLoader_AdapterLoadException;
+ m_replicationProviders.Dispose();
}
- finally
+
+ if (m_archiveCurtailmentTimer is not null)
{
- m_disposed = true; // Prevent duplicate dispose.
- base.Dispose(disposing); // Call base class Dispose().
+ m_archiveCurtailmentTimer.Stop();
+ m_archiveCurtailmentTimer.Elapsed -= ArchiveCurtailmentTimerElapsed;
+ m_archiveCurtailmentTimer.Dispose();
}
}
-
- ///
- /// Initializes this .
- ///
- public override void Initialize()
+ finally
{
- base.Initialize();
+ m_disposed = true; // Prevent duplicate dispose.
+ base.Dispose(disposing); // Call base class Dispose().
+ }
+ }
- //const string errorMessage = "{0} is missing from Settings - Example: instanceName=default; ArchiveDirectories={{c:\\Archive1\\;d:\\Backups2\\}}; dataChannel={{port=9591; interface=0.0.0.0}}";
- Dictionary settings = Settings;
+ ///
+ /// Initializes this .
+ ///
+ public override void Initialize()
+ {
+ base.Initialize();
- // Validate settings.
- if (!settings.TryGetValue(nameof(InstanceName), out m_instanceName) || string.IsNullOrWhiteSpace(m_instanceName))
- m_instanceName = Name;
+ //const string errorMessage = "{0} is missing from Settings - Example: instanceName=default; ArchiveDirectories={{c:\\Archive1\\;d:\\Backups2\\}}; dataChannel={{port=9591; interface=0.0.0.0}}";
+ Dictionary settings = Settings;
- // Track instance in static dictionary
- Instances[InstanceName] = this;
+ // Validate settings.
+ if (!settings.TryGetValue(nameof(InstanceName), out m_instanceName) || string.IsNullOrWhiteSpace(m_instanceName))
+ m_instanceName = Name;
- if (!settings.TryGetValue(nameof(WorkingDirectory), out string setting) || string.IsNullOrEmpty(setting))
- setting = "Archive";
+ // Track instance in static dictionary
+ Instances[InstanceName] = this;
- WorkingDirectory = setting;
+ if (!settings.TryGetValue(nameof(WorkingDirectory), out string setting) || string.IsNullOrEmpty(setting))
+ setting = "Archive";
- if (settings.TryGetValue(nameof(ArchiveDirectories), out setting))
- ArchiveDirectories = setting;
+ WorkingDirectory = setting;
- ArchiveCurtailmentInterval = settings.TryGetValue(nameof(ArchiveCurtailmentInterval), out setting) ? int.Parse(setting) : DefaultArchiveCurtailmentInterval;
+ if (settings.TryGetValue(nameof(ArchiveDirectories), out setting))
+ ArchiveDirectories = setting;
- if (settings.TryGetValue(nameof(AttachedPaths), out setting))
- AttachedPaths = setting;
+ ArchiveCurtailmentInterval = settings.TryGetValue(nameof(ArchiveCurtailmentInterval), out setting) ? int.Parse(setting) : DefaultArchiveCurtailmentInterval;
- WatchAttachedPaths = settings.TryGetValue(nameof(WatchAttachedPaths), out setting) && setting.ParseBoolean();
+ if (settings.TryGetValue(nameof(AttachedPaths), out setting))
+ AttachedPaths = setting;
- DataChannel = settings.TryGetValue(nameof(DataChannel), out setting) && !string.IsNullOrWhiteSpace(setting) ? setting : DefaultDataChannel;
+ WatchAttachedPaths = settings.TryGetValue(nameof(WatchAttachedPaths), out setting) && setting.ParseBoolean();
- if (!settings.TryGetValue(nameof(TargetFileSize), out setting) || !double.TryParse(setting, out double targetFileSize))
- targetFileSize = DefaultTargetFileSize;
+ DataChannel = settings.TryGetValue(nameof(DataChannel), out setting) && !string.IsNullOrWhiteSpace(setting) ? setting : DefaultDataChannel;
- if (targetFileSize < 0.1D || targetFileSize > SI2.Tera)
- targetFileSize = DefaultTargetFileSize;
+ if (!settings.TryGetValue(nameof(TargetFileSize), out setting) || !double.TryParse(setting, out double targetFileSize))
+ targetFileSize = DefaultTargetFileSize;
- if (!settings.TryGetValue(nameof(DesiredRemainingSpace), out setting) || !double.TryParse(setting, out double desiredRemainingSpace))
- desiredRemainingSpace = DefaultDesiredRemainingSpace;
+ if (targetFileSize < 0.1D || targetFileSize > SI2.Tera)
+ targetFileSize = DefaultTargetFileSize;
- if (desiredRemainingSpace < 0.1D || desiredRemainingSpace > SI2.Tera)
- desiredRemainingSpace = DefaultDesiredRemainingSpace;
+ if (!settings.TryGetValue(nameof(DesiredRemainingSpace), out setting) || !double.TryParse(setting, out double desiredRemainingSpace))
+ desiredRemainingSpace = DefaultDesiredRemainingSpace;
- if (settings.TryGetValue(nameof(MaximumArchiveDays), out setting) && int.TryParse(setting, out int maximumArchiveDays))
- MaximumArchiveDays = maximumArchiveDays;
+ if (desiredRemainingSpace < 0.1D || desiredRemainingSpace > SI2.Tera)
+ desiredRemainingSpace = DefaultDesiredRemainingSpace;
- if (settings.TryGetValue(nameof(AutoRemoveOldestFilesBeforeFull), out setting))
- AutoRemoveOldestFilesBeforeFull = setting.ParseBoolean();
+ if (settings.TryGetValue(nameof(MaximumArchiveDays), out setting) && int.TryParse(setting, out int maximumArchiveDays))
+ MaximumArchiveDays = maximumArchiveDays;
- if (MaximumArchiveDays < 1 && !AutoRemoveOldestFilesBeforeFull)
- OnStatusMessage(MessageLevel.Warning, "Maximum archive days not set and automated removal of oldest files before full disk is not enabled: system will not initiate archive file curtailment operations in this configuration. Disk space for target archive paths should be monitored externally.");
+ if (settings.TryGetValue(nameof(AutoRemoveOldestFilesBeforeFull), out setting))
+ AutoRemoveOldestFilesBeforeFull = setting.ParseBoolean();
- if (settings.TryGetValue(nameof(DownsamplingIntervals), out setting))
- DownsamplingIntervals = setting;
+ if (MaximumArchiveDays < 1 && !AutoRemoveOldestFilesBeforeFull)
+ OnStatusMessage(MessageLevel.Warning, "Maximum archive days not set and automated removal of oldest files before full disk is not enabled: system will not initiate archive file curtailment operations in this configuration. Disk space for target archive paths should be monitored externally.");
- if (MaximumArchiveDays > 0 && m_downsamplingIntervals.Any(kvp => kvp.Key >= MaximumArchiveDays))
- {
- OnStatusMessage(MessageLevel.Warning, $"Downsampling intervals defined for days greater than or equal to maximum archive days ({MaximumArchiveDays:N0}) will be ignored.");
- m_downsamplingIntervals = new SortedList(m_downsamplingIntervals.Where(kvp => kvp.Key < MaximumArchiveDays).ToDictionary(kvp => kvp.Key, kvp => kvp.Value));
- }
+ if (settings.TryGetValue(nameof(DownsamplingIntervals), out setting))
+ DownsamplingIntervals = setting;
- EnableTimeReasonabilityCheck = settings.TryGetValue(nameof(EnableTimeReasonabilityCheck), out setting) && setting.ParseBoolean();
+ if (MaximumArchiveDays > 0 && m_downsamplingIntervals.Any(kvp => kvp.Key >= MaximumArchiveDays))
+ {
+ OnStatusMessage(MessageLevel.Warning, $"Downsampling intervals defined for days greater than or equal to maximum archive days ({MaximumArchiveDays:N0}) will be ignored.");
+ m_downsamplingIntervals = new SortedList(m_downsamplingIntervals.Where(kvp => kvp.Key < MaximumArchiveDays).ToDictionary(kvp => kvp.Key, kvp => kvp.Value));
+ }
- if (settings.TryGetValue(nameof(PastTimeReasonabilityLimit), out setting) && double.TryParse(setting, out double value))
- PastTimeReasonabilityLimit = value;
- else
- PastTimeReasonabilityLimit = DefaultPastTimeReasonabilityLimit;
+ EnableTimeReasonabilityCheck = settings.TryGetValue(nameof(EnableTimeReasonabilityCheck), out setting) && setting.ParseBoolean();
- if (settings.TryGetValue(nameof(FutureTimeReasonabilityLimit), out setting) && double.TryParse(setting, out value))
- FutureTimeReasonabilityLimit = value;
- else
- FutureTimeReasonabilityLimit = DefaultFutureTimeReasonabilityLimit;
+ if (settings.TryGetValue(nameof(PastTimeReasonabilityLimit), out setting) && double.TryParse(setting, out double value))
+ PastTimeReasonabilityLimit = value;
+ else
+ PastTimeReasonabilityLimit = DefaultPastTimeReasonabilityLimit;
- SwingingDoorCompressionEnabled = settings.TryGetValue(nameof(SwingingDoorCompressionEnabled), out setting) && setting.ParseBoolean();
+ if (settings.TryGetValue(nameof(FutureTimeReasonabilityLimit), out setting) && double.TryParse(setting, out value))
+ FutureTimeReasonabilityLimit = value;
+ else
+ FutureTimeReasonabilityLimit = DefaultFutureTimeReasonabilityLimit;
- if (settings.TryGetValue(nameof(DirectoryNamingMode), out setting) && Enum.TryParse(setting, true, out ArchiveDirectoryMethod directoryNamingMode))
- DirectoryNamingMode = directoryNamingMode;
- else
- DirectoryNamingMode = DefaultDirectoryNamingMode;
+ SwingingDoorCompressionEnabled = settings.TryGetValue(nameof(SwingingDoorCompressionEnabled), out setting) && setting.ParseBoolean();
- if (settings.TryGetValue(nameof(DirectoryFillMethod), out setting) && Enum.TryParse(setting, true, out ArchiveDirectoryFillMethod directoryFillMethod))
- DirectoryFillMethod = directoryFillMethod;
- else
- DirectoryFillMethod = DefaultDirectoryFillMethod;
-
- // Handle advanced settings - there are hidden but available from manual entry into connection string
- if (!settings.TryGetValue("StagingCount", out setting) || !int.TryParse(setting, out int stagingCount))
- stagingCount = 3;
-
- if (!settings.TryGetValue("DiskFlushInterval", out setting) || !int.TryParse(setting, out int diskFlushInterval))
- diskFlushInterval = 10000;
-
- if (!settings.TryGetValue("CacheFlushInterval", out setting) || !int.TryParse(setting, out int cacheFlushInterval))
- cacheFlushInterval = 100;
-
- // Validate maximum downsampling intervals. Currently only 10 stages are defined, 0 to 9 - since configured
- // staging count is typically 3, this allows 6 total downsampling intervals (as extra stages):
- if (m_downsamplingIntervals.Count > 9 - stagingCount)
- throw new InvalidOperationException($"Maximum of {9 - stagingCount} downsampling intervals are allowed.");
-
- // Establish archive information for this historian instance
- m_archiveInfo = new HistorianServerDatabaseConfig(InstanceName, WorkingDirectory, true);
-
- if (m_archiveDirectories is not null)
- m_archiveInfo.FinalWritePaths.AddRange(m_archiveDirectories);
-
- if (m_attachedPaths is not null)
- m_archiveInfo.ImportPaths.AddRange(m_attachedPaths);
-
- m_archiveInfo.ImportAttachedPathsAtStartup = false;
- m_archiveInfo.TargetFileSize = (long)(targetFileSize * SI.Giga);
- m_archiveInfo.DesiredRemainingSpace = (long)(desiredRemainingSpace * SI.Giga);
- m_archiveInfo.DirectoryMethod = DirectoryNamingMode;
- m_archiveInfo.FillMethod = DirectoryFillMethod;
- m_archiveInfo.StagingCount = stagingCount;
- m_archiveInfo.DiskFlushInterval = diskFlushInterval;
- m_archiveInfo.CacheFlushInterval = cacheFlushInterval;
-
- // Provide web service support
- m_dataServices = new DataServices();
- m_dataServices.AdapterCreated += DataServices_AdapterCreated;
- m_dataServices.AdapterLoaded += DataServices_AdapterLoaded;
- m_dataServices.AdapterUnloaded += DataServices_AdapterUnloaded;
- m_dataServices.AdapterLoadException += AdapterLoader_AdapterLoadException;
-
- // Provide archive replication support
- m_replicationProviders = new ReplicationProviders();
- m_replicationProviders.AdapterCreated += ReplicationProviders_AdapterCreated;
- m_replicationProviders.AdapterLoaded += ReplicationProviders_AdapterLoaded;
- m_replicationProviders.AdapterUnloaded += ReplicationProviders_AdapterUnloaded;
- m_replicationProviders.AdapterLoadException += AdapterLoader_AdapterLoadException;
-
- if (MaximumArchiveDays > 0)
- {
- m_archiveCurtailmentTimer = new Timer(ArchiveCurtailmentInterval * 1000.0D);
- m_archiveCurtailmentTimer.AutoReset = true;
- m_archiveCurtailmentTimer.Elapsed += ArchiveCurtailmentTimerElapsed;
- m_archiveCurtailmentTimer.Enabled = true;
- }
+ if (settings.TryGetValue(nameof(DirectoryNamingMode), out setting) && Enum.TryParse(setting, true, out ArchiveDirectoryMethod directoryNamingMode))
+ DirectoryNamingMode = directoryNamingMode;
+ else
+ DirectoryNamingMode = DefaultDirectoryNamingMode;
- // Initialize the file watchers for attached paths
- AttachedPathWatchers = WatchAttachedPaths ? m_attachedPaths?.Select(WatchPath).ToArray() : null;
- }
+ if (settings.TryGetValue(nameof(DirectoryFillMethod), out setting) && Enum.TryParse(setting, true, out ArchiveDirectoryFillMethod directoryFillMethod))
+ DirectoryFillMethod = directoryFillMethod;
+ else
+ DirectoryFillMethod = DefaultDirectoryFillMethod;
- ///
- /// Gets a short one-line status of this .
- ///
- /// Maximum length of the status message.
- /// Text of the status message.
- public override string GetShortStatus(int maxLength) =>
- $"Archived {m_archivedMeasurements} measurements.".CenterText(maxLength);
-
- ///
- /// Detaches an archive file from the historian.
- ///
- /// Archive file name to detach.
- [AdapterCommand("Detaches an archive file from the historian. Wild cards are allowed in file name and folders to handle multiple files.", "Administrator", "Editor")]
- public void DetachFile(string fileName)
- {
- ClientDatabaseBase database = GetClientDatabase();
+ // Handle advanced settings - there are hidden but available from manual entry into connection string
+ if (!settings.TryGetValue("StagingCount", out setting) || !int.TryParse(setting, out int stagingCount))
+ stagingCount = 3;
- if (fileName.Contains('*'))
- ExecuteWildCardFileOperation(database, Path.GetFullPath(fileName), database.DetatchFiles);
- else
- ExecuteFileOperation(database, Path.GetFullPath(fileName), database.DetatchFiles);
- }
+ if (!settings.TryGetValue("DiskFlushInterval", out setting) || !int.TryParse(setting, out int diskFlushInterval))
+ diskFlushInterval = 10000;
- ///
- /// Deletes an archive file from the historian.
- ///
- /// Archive file name to delete.
- [AdapterCommand("Deletes an archive file from the historian. Wild cards are allowed in file name and folders to handle multiple files.", "Administrator", "Editor")]
- public void DeleteFile(string fileName)
- {
- ClientDatabaseBase database = GetClientDatabase();
+ if (!settings.TryGetValue("CacheFlushInterval", out setting) || !int.TryParse(setting, out int cacheFlushInterval))
+ cacheFlushInterval = 100;
- if (fileName.Contains('*'))
- ExecuteWildCardFileOperation(database, Path.GetFullPath(fileName), database.DeleteFiles);
- else
- ExecuteFileOperation(database, Path.GetFullPath(fileName), database.DeleteFiles);
- }
+ // Validate maximum downsampling intervals. Currently only 10 stages are defined, 0 to 9 - since configured
+ // staging count is typically 3, this allows 6 total downsampling intervals (as extra stages):
+ if (m_downsamplingIntervals.Count > 9 - stagingCount)
+ throw new InvalidOperationException($"Maximum of {9 - stagingCount} downsampling intervals are allowed.");
- ///
- /// Detaches files in an archive folder from the historian.
- ///
- /// Archive folder name to detach.
- [AdapterCommand("Detaches all archive files in a specified folder from the historian.", "Administrator", "Editor")]
- public void DetachFolder(string folderName)
- {
- ClientDatabaseBase database = GetClientDatabase();
- ExecuteFolderOperation(database, FilePath.GetDirectoryName(Path.GetFullPath(folderName)), database.DetatchFiles);
- }
+ // Establish archive information for this historian instance
+ if (settings.TryGetValue(nameof(EnablePointIDIndex), out setting))
+ EnablePointIDIndex = setting.ParseBoolean();
+
+ if (settings.TryGetValue(nameof(EnableTimeBucketBloomFilter), out setting))
+ EnableTimeBucketBloomFilter = setting.ParseBoolean();
+
+ if (settings.TryGetValue(nameof(BloomFilterMaximumIndexSizeMB), out setting) && int.TryParse(setting, out int bloomFilterMaximumIndexSizeMB))
+ BloomFilterMaximumIndexSizeMB = bloomFilterMaximumIndexSizeMB;
+
+ if (settings.TryGetValue(nameof(BloomFilterBucketDurationSeconds), out setting) && int.TryParse(setting, out int bloomFilterBucketDurationSeconds))
+ BloomFilterBucketDurationSeconds = bloomFilterBucketDurationSeconds;
+
+ if (settings.TryGetValue(nameof(BloomFilterFalsePositiveProbability), out setting) && double.TryParse(setting, out double bloomFilterFalsePositiveProbability))
+ BloomFilterFalsePositiveProbability = bloomFilterFalsePositiveProbability;
+
+ TimeBucketBloomFilterSettings bloomFilter = CreateBloomFilterSettings();
+
+ m_archiveInfo = new HistorianServerDatabaseConfig(InstanceName, WorkingDirectory, true)
+ {
+ BloomFilter = bloomFilter,
+ EnablePointIDIndex = EnablePointIDIndex
+ };
+
+ if (m_archiveDirectories is not null)
+ m_archiveInfo.FinalWritePaths.AddRange(m_archiveDirectories);
+
+ if (m_attachedPaths is not null)
+ m_archiveInfo.ImportPaths.AddRange(m_attachedPaths);
- ///
- /// Deletes files in an archive folder from the historian.
- ///
- /// Archive folder name to delete.
- [AdapterCommand("Deletes all archive files in a specified folder from the historian.", "Administrator", "Editor")]
- public void DeleteFolder(string folderName)
+ m_archiveInfo.ImportAttachedPathsAtStartup = false;
+ m_archiveInfo.TargetFileSize = (long)(targetFileSize * SI.Giga);
+ m_archiveInfo.DesiredRemainingSpace = (long)(desiredRemainingSpace * SI.Giga);
+ m_archiveInfo.DirectoryMethod = DirectoryNamingMode;
+ m_archiveInfo.FillMethod = DirectoryFillMethod;
+ m_archiveInfo.StagingCount = stagingCount;
+ m_archiveInfo.DiskFlushInterval = diskFlushInterval;
+ m_archiveInfo.CacheFlushInterval = cacheFlushInterval;
+
+ // Provide web service support
+ m_dataServices = new DataServices();
+ m_dataServices.AdapterCreated += DataServices_AdapterCreated;
+ m_dataServices.AdapterLoaded += DataServices_AdapterLoaded;
+ m_dataServices.AdapterUnloaded += DataServices_AdapterUnloaded;
+ m_dataServices.AdapterLoadException += AdapterLoader_AdapterLoadException;
+
+ // Provide archive replication support
+ m_replicationProviders = new ReplicationProviders();
+ m_replicationProviders.AdapterCreated += ReplicationProviders_AdapterCreated;
+ m_replicationProviders.AdapterLoaded += ReplicationProviders_AdapterLoaded;
+ m_replicationProviders.AdapterUnloaded += ReplicationProviders_AdapterUnloaded;
+ m_replicationProviders.AdapterLoadException += AdapterLoader_AdapterLoadException;
+
+ if (MaximumArchiveDays > 0)
{
- ClientDatabaseBase database = GetClientDatabase();
- ExecuteFolderOperation(database, FilePath.GetDirectoryName(Path.GetFullPath(folderName)), database.DeleteFiles);
+ m_archiveCurtailmentTimer = new Timer(ArchiveCurtailmentInterval * 1000.0D);
+ m_archiveCurtailmentTimer.AutoReset = true;
+ m_archiveCurtailmentTimer.Elapsed += ArchiveCurtailmentTimerElapsed;
+ m_archiveCurtailmentTimer.Enabled = true;
}
- ///
- /// Initiates archive file curtailment based on defined maximum archive days, auto removal of oldest files before full disk and defined downsampling intervals.
- ///
- [AdapterCommand("Initiates archive file curtailment based on defined maximum archive days, auto removal of oldest files before full disk and defined downsampling intervals.", "Administrator", "Editor")]
- public void CurtailArchiveFiles()
- {
- if (AutoRemoveOldestFilesBeforeFull)
- RemoveOldestFilesBeforeFull();
+ // Initialize the file watchers for attached paths
+ AttachedPathWatchers = WatchAttachedPaths ? m_attachedPaths?.Select(WatchPath).ToArray() : null;
+ }
- if (m_downsamplingIntervals.Count > 0)
- DownsampleArchiveFiles();
+ ///
+ /// Gets a short one-line status of this .
+ ///
+ /// Maximum length of the status message.
+ /// Text of the status message.
+ public override string GetShortStatus(int maxLength) =>
+ $"Archived {m_archivedMeasurements} measurements.".CenterText(maxLength);
- if (MaximumArchiveDays > 0)
- RemoveFilesOlderThanMaxArchiveDays();
- }
+ ///
+ /// Detaches an archive file from the historian.
+ ///
+ /// Archive file name to detach.
+ [AdapterCommand("Detaches an archive file from the historian. Wild cards are allowed in file name and folders to handle multiple files.", "Administrator", "Editor")]
+ public void DetachFile(string fileName)
+ {
+ ClientDatabaseBase database = GetClientDatabase();
- // Builds the set of normalized write directories (absolute, trimmed, with a trailing separator) used to scope
- // archive-reduction operations so that only files written by this instance are affected. Attached/import folders
- // live outside these directories and are therefore left untouched by all reduction paths: disk-pressure removal,
- // downsampling and maximum-archive-days deletion.
- private string[] GetNormalizedArchiveDirectories()
- {
- string[] archiveDirectories = (m_archiveDirectories ?? [WorkingDirectory]).ToArray();
+ if (fileName.Contains('*'))
+ ExecuteWildCardFileOperation(database, Path.GetFullPath(fileName), database.DetatchFiles);
+ else
+ ExecuteFileOperation(database, Path.GetFullPath(fileName), database.DetatchFiles);
+ }
- for (int i = 0; i < archiveDirectories.Length; i++)
- {
- string normalizedDirectory = Path.GetFullPath(archiveDirectories[i]).TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
+ ///
+ /// Deletes an archive file from the historian.
+ ///
+ /// Archive file name to delete.
+ [AdapterCommand("Deletes an archive file from the historian. Wild cards are allowed in file name and folders to handle multiple files.", "Administrator", "Editor")]
+ public void DeleteFile(string fileName)
+ {
+ ClientDatabaseBase database = GetClientDatabase();
- // Add trailing separator to ensure only subdirectory, not prefix match which might be a file
- if (!normalizedDirectory.EndsWith(Path.DirectorySeparatorChar.ToString()))
- normalizedDirectory += Path.DirectorySeparatorChar;
+ if (fileName.Contains('*'))
+ ExecuteWildCardFileOperation(database, Path.GetFullPath(fileName), database.DeleteFiles);
+ else
+ ExecuteFileOperation(database, Path.GetFullPath(fileName), database.DeleteFiles);
+ }
- archiveDirectories[i] = normalizedDirectory;
- }
+ ///
+ /// Detaches files in an archive folder from the historian.
+ ///
+ /// Archive folder name to detach.
+ [AdapterCommand("Detaches all archive files in a specified folder from the historian.", "Administrator", "Editor")]
+ public void DetachFolder(string folderName)
+ {
+ ClientDatabaseBase database = GetClientDatabase();
+ ExecuteFolderOperation(database, FilePath.GetDirectoryName(Path.GetFullPath(folderName)), database.DetatchFiles);
+ }
- return archiveDirectories;
- }
+ ///
+ /// Deletes files in an archive folder from the historian.
+ ///
+ /// Archive folder name to delete.
+ [AdapterCommand("Deletes all archive files in a specified folder from the historian.", "Administrator", "Editor")]
+ public void DeleteFolder(string folderName)
+ {
+ ClientDatabaseBase database = GetClientDatabase();
+ ExecuteFolderOperation(database, FilePath.GetDirectoryName(Path.GetFullPath(folderName)), database.DeleteFiles);
+ }
+
+ ///
+ /// Initiates archive file curtailment based on defined maximum archive days, auto removal of oldest files before full disk and defined downsampling intervals.
+ ///
+ [AdapterCommand("Initiates archive file curtailment based on defined maximum archive days, auto removal of oldest files before full disk and defined downsampling intervals.", "Administrator", "Editor")]
+ public void CurtailArchiveFiles()
+ {
+ if (AutoRemoveOldestFilesBeforeFull)
+ RemoveOldestFilesBeforeFull();
- // Determines whether the specified archive file resides within one of the supplied (normalized) write
- // directories. Used to scope reduction operations to written files and skip attached/non-write folders.
- private static bool FilePathTargetsArchiveDirectories(string fileName, string[] normalizedArchiveDirectories)
+ if (m_downsamplingIntervals.Count > 0)
+ DownsampleArchiveFiles();
+
+ if (MaximumArchiveDays > 0)
+ RemoveFilesOlderThanMaxArchiveDays();
+ }
+
+ // Builds the set of normalized write directories (absolute, trimmed, with a trailing separator) used to scope
+ // archive-reduction operations so that only files written by this instance are affected. Attached/import folders
+ // live outside these directories and are therefore left untouched by all reduction paths: disk-pressure removal,
+ // downsampling and maximum-archive-days deletion.
+ private string[] GetNormalizedArchiveDirectories()
+ {
+ string[] archiveDirectories = (m_archiveDirectories ?? [WorkingDirectory]).ToArray();
+
+ for (int i = 0; i < archiveDirectories.Length; i++)
{
- // Normalize file path: absolute, trimmed
- string filePath = Path.GetFullPath(fileName);
+ string normalizedDirectory = Path.GetFullPath(archiveDirectories[i]).TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
- // Case-insensitive compare on Windows, case-sensitive on Posix environments
- StringComparison comparison = Common.IsPosixEnvironment ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase;
+ // Add trailing separator to ensure only subdirectory, not prefix match which might be a file
+ if (!normalizedDirectory.EndsWith(Path.DirectorySeparatorChar.ToString()))
+ normalizedDirectory += Path.DirectorySeparatorChar;
- return normalizedArchiveDirectories.Any(directory => filePath.StartsWith(directory, comparison));
+ archiveDirectories[i] = normalizedDirectory;
}
- private void RemoveOldestFilesBeforeFull()
- {
- // Set target space to be three times the target file size plus minimum disk space before considered full,
- // this will allow three new final stage archive files to be written before next curtailment interval.
- // Curtailment interval, desired remaining space and target file size can all be adjusted to better
- // accommodate high volume data archiving in very low disk space environments.
- long neededSpace = m_archiveInfo.DesiredRemainingSpace + 3 * m_archiveInfo.TargetFileSize;
+ return archiveDirectories;
+ }
- string[] archiveDirectories = GetNormalizedArchiveDirectories();
+ // Determines whether the specified archive file resides within one of the supplied (normalized) write
+ // directories. Used to scope reduction operations to written files and skip attached/non-write folders.
+ private static bool FilePathTargetsArchiveDirectories(string fileName, string[] normalizedArchiveDirectories)
+ {
+ // Normalize file path: absolute, trimmed
+ string filePath = Path.GetFullPath(fileName);
- try
- {
- // Check if any target archive destination has enough disk space
- foreach (string path in archiveDirectories)
- {
- FilePath.GetAvailableFreeSpace(path, out long freeSpace, out _);
+ // Case-insensitive compare on Windows, case-sensitive on Posix environments
+ StringComparison comparison = Common.IsPosixEnvironment ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase;
- // If any path has needed space, then we are done
- if (freeSpace > neededSpace)
- return;
- }
- }
- catch (Exception ex)
+ return normalizedArchiveDirectories.Any(directory => filePath.StartsWith(directory, comparison));
+ }
+
+ private void RemoveOldestFilesBeforeFull()
+ {
+ // Set target space to be three times the target file size plus minimum disk space before considered full,
+ // this will allow three new final stage archive files to be written before next curtailment interval.
+ // Curtailment interval, desired remaining space and target file size can all be adjusted to better
+ // accommodate high volume data archiving in very low disk space environments.
+ long neededSpace = m_archiveInfo.DesiredRemainingSpace + 3 * m_archiveInfo.TargetFileSize;
+
+ string[] archiveDirectories = GetNormalizedArchiveDirectories();
+
+ try
+ {
+ // Check if any target archive destination has enough disk space
+ foreach (string path in archiveDirectories)
{
- OnProcessException(MessageLevel.Warning, new InvalidOperationException($"Oldest file removal operation cancelled: failed during check for full disk: {ex.Message}", ex));
+ FilePath.GetAvailableFreeSpace(path, out long freeSpace, out _);
- // Do not continue with archive curtailment if disk space check failed
- return;
+ // If any path has needed space, then we are done
+ if (freeSpace > neededSpace)
+ return;
}
+ }
+ catch (Exception ex)
+ {
+ OnProcessException(MessageLevel.Warning, new InvalidOperationException($"Oldest file removal operation cancelled: failed during check for full disk: {ex.Message}", ex));
- try
- {
- OnStatusMessage(MessageLevel.Warning, "Disk space is near full, scanning for oldest archive files...");
+ // Do not continue with archive curtailment if disk space check failed
+ return;
+ }
- ClientDatabaseBase database = GetClientDatabase();
+ try
+ {
+ OnStatusMessage(MessageLevel.Warning, "Disk space is near full, scanning for oldest archive files...");
- long fileSizeSum = 0;
+ ClientDatabaseBase database = GetClientDatabase();
- // Find the oldest archive files until we have reached target disk space. End time is preferred over
- // start time for sorting since devices with an inaccurate GPS clock can provide bad start times when
- // out of range timestamps are not configured to be filtered, and you don't want to accidentally delete
- // a file with otherwise in-range data.
- ArchiveDetails[] filesToDelete = database.GetAllAttachedFiles()
- .Where(file => FilePathTargetsArchiveDirectories(file.FileName, archiveDirectories))
- .OrderBy(file => file.EndTime)
- .TakeWhile(item =>
- {
- fileSizeSum += item.FileSize;
- return fileSizeSum < neededSpace;
- })
- .ToArray();
+ long fileSizeSum = 0;
- database.DeleteFiles(filesToDelete.Select(file => file.Id).ToList());
+ // Find the oldest archive files until we have reached target disk space. End time is preferred over
+ // start time for sorting since devices with an inaccurate GPS clock can provide bad start times when
+ // out of range timestamps are not configured to be filtered, and you don't want to accidentally delete
+ // a file with otherwise in-range data.
+ ArchiveDetails[] filesToDelete = database.GetAllAttachedFiles()
+ .Where(file => FilePathTargetsArchiveDirectories(file.FileName, archiveDirectories))
+ .OrderBy(file => file.EndTime)
+ .TakeWhile(item =>
+ {
+ fileSizeSum += item.FileSize;
+ return fileSizeSum < neededSpace;
+ })
+ .ToArray();
- OnStatusMessage(MessageLevel.Warning, $"Deleted the following oldest archive files in order to free disk space:\r\n {filesToDelete.Select(file => FilePath.TrimFileName(file.FileName, 75)).ToDelimitedString($"{Environment.NewLine} ")}");
- }
- catch (Exception ex)
- {
- OnProcessException(MessageLevel.Error, new InvalidOperationException($"Failed while attempting to delete oldest archive files in order to free disk space: {ex.Message}", ex));
- }
- }
+ database.DeleteFiles(filesToDelete.Select(file => file.Id).ToList());
- private void DownsampleArchiveFiles()
+ OnStatusMessage(MessageLevel.Warning, $"Deleted the following oldest archive files in order to free disk space:\r\n {filesToDelete.Select(file => FilePath.TrimFileName(file.FileName, 75)).ToDelimitedString($"{Environment.NewLine} ")}");
+ }
+ catch (Exception ex)
{
- ClientDatabaseBase database = GetClientDatabase();
- SortedList filesToDownsample = new();
+ OnProcessException(MessageLevel.Error, new InvalidOperationException($"Failed while attempting to delete oldest archive files in order to free disk space: {ex.Message}", ex));
+ }
+ }
- try
- {
- OnStatusMessage(MessageLevel.Info, "Scanning archive files for downsampling...");
+ private void DownsampleArchiveFiles()
+ {
+ ClientDatabaseBase database = GetClientDatabase();
+ SortedList filesToDownsample = new();
- // Scope downsampling to files within the write directories so attached/non-write folders are never
- // rewritten at a lower resolution.
- string[] normalizedArchiveDirectories = GetNormalizedArchiveDirectories();
+ try
+ {
+ OnStatusMessage(MessageLevel.Info, "Scanning archive files for downsampling...");
- List attachedFiles = database.GetAllAttachedFiles()
- .Where(file => FilePathTargetsArchiveDirectories(file.FileName, normalizedArchiveDirectories))
- .ToList();
+ // Scope downsampling to files within the write directories so attached/non-write folders are never
+ // rewritten at a lower resolution.
+ string[] normalizedArchiveDirectories = GetNormalizedArchiveDirectories();
- // Start with oldest files first, this way they can be excluded from further downsampling if they are already targeted
- int[] startDays = m_downsamplingIntervals.Keys.OrderByDescending(value => value).ToArray();
- HashSet targetedArchives = [];
+ List attachedFiles = database.GetAllAttachedFiles()
+ .Where(file => FilePathTargetsArchiveDirectories(file.FileName, normalizedArchiveDirectories))
+ .ToList();
- int downsamplingStage = m_archiveInfo.StagingCount + startDays.Length;
+ // Start with oldest files first, this way they can be excluded from further downsampling if they are already targeted
+ int[] startDays = m_downsamplingIntervals.Keys.OrderByDescending(value => value).ToArray();
+ HashSet targetedArchives = [];
- foreach (int startDay in startDays)
- {
- // Get list of files that have both a start time and an end time that are greater than the target
- // start day for a given downsampling interval. We check both start and end times since devices
- // with an inaccurate GPS clock can provide bad time when out of range timestamps are not configured
- // to be filtered and you don't want to accidentally delete a file with otherwise in-range data.
- // We also filter on stage number to avoid downsampling files that have already been downsampled.
- HashSet matchingFiles = new(attachedFiles.Where(file =>
- (DateTime.UtcNow - file.StartTime).TotalDays > startDay &&
- (DateTime.UtcNow - file.EndTime).TotalDays > startDay &&
- FileFlags.GetStageNumber(file.Flags) < downsamplingStage &&
- !file.Flags.Contains(FileFlags.IntermediateFile)));
-
- // Start days are ordered from highest to lowest, so we decrement the downsampling stage
- downsamplingStage--;
-
- if (matchingFiles.Count == 0)
- continue;
+ int downsamplingStage = m_archiveInfo.StagingCount + startDays.Length;
+
+ foreach (int startDay in startDays)
+ {
+ // Get list of files that have both a start time and an end time that are greater than the target
+ // start day for a given downsampling interval. We check both start and end times since devices
+ // with an inaccurate GPS clock can provide bad time when out of range timestamps are not configured
+ // to be filtered and you don't want to accidentally delete a file with otherwise in-range data.
+ // We also filter on stage number to avoid downsampling files that have already been downsampled.
+ HashSet matchingFiles = new(attachedFiles.Where(file =>
+ (DateTime.UtcNow - file.StartTime).TotalDays > startDay &&
+ (DateTime.UtcNow - file.EndTime).TotalDays > startDay &&
+ FileFlags.GetStageNumber(file.Flags) < downsamplingStage &&
+ !file.Flags.Contains(FileFlags.IntermediateFile)));
+
+ // Start days are ordered from highest to lowest, so we decrement the downsampling stage
+ downsamplingStage--;
+
+ if (matchingFiles.Count == 0)
+ continue;
- matchingFiles.ExceptWith(targetedArchives);
+ matchingFiles.ExceptWith(targetedArchives);
- if (matchingFiles.Count == 0)
- continue;
+ if (matchingFiles.Count == 0)
+ continue;
- filesToDownsample[startDay] = matchingFiles.ToArray();
- targetedArchives.UnionWith(matchingFiles);
- }
- }
- catch (Exception ex)
- {
- OnProcessException(MessageLevel.Warning, new InvalidOperationException($"Failed while scanning archive files for downsampling: {ex.Message}", ex));
+ filesToDownsample[startDay] = matchingFiles.ToArray();
+ targetedArchives.UnionWith(matchingFiles);
}
+ }
+ catch (Exception ex)
+ {
+ OnProcessException(MessageLevel.Warning, new InvalidOperationException($"Failed while scanning archive files for downsampling: {ex.Message}", ex));
+ }
- if (filesToDownsample.Count == 0)
- return;
+ if (filesToDownsample.Count == 0)
+ return;
- try
- {
- int downsamplingStage = 1;
+ try
+ {
+ int downsamplingStage = 1;
- foreach (KeyValuePair kvp in filesToDownsample)
- {
- int startDay = kvp.Key;
- ArchiveDetails[] files = kvp.Value;
- double samplesPerSecond = m_downsamplingIntervals[startDay];
+ foreach (KeyValuePair kvp in filesToDownsample)
+ {
+ int startDay = kvp.Key;
+ ArchiveDetails[] files = kvp.Value;
+ double samplesPerSecond = m_downsamplingIntervals[startDay];
- // Each successive downsampled file will be flagged with a higher stage count
- Guid stageFlags = FileFlags.GetStage(m_archiveInfo.StagingCount + downsamplingStage++);
+ // Each successive downsampled file will be flagged with a higher stage count
+ Guid stageFlags = FileFlags.GetStage(m_archiveInfo.StagingCount + downsamplingStage++);
- OnStatusMessage(MessageLevel.Info, $"Downsampling {files.Length:N0} archive files older than {startDay:N0} days to {samplesPerSecond} samples per second...");
+ OnStatusMessage(MessageLevel.Info, $"Downsampling {files.Length:N0} archive files older than {startDay:N0} days to {samplesPerSecond} samples per second...");
- foreach (ArchiveDetails file in files)
- {
- string downsampledFile = DownsampleArchiveFile(file, samplesPerSecond, stageFlags);
+ foreach (ArchiveDetails file in files)
+ {
+ string downsampledFile = DownsampleArchiveFile(file, samplesPerSecond, stageFlags);
- if (string.IsNullOrWhiteSpace(downsampledFile))
- continue;
+ if (string.IsNullOrWhiteSpace(downsampledFile))
+ continue;
- try
- {
- // Remove higher resolution file after successful downsampling
- database.DeleteFiles([file.Id]);
+ try
+ {
+ // Remove higher resolution file after successful downsampling
+ database.DeleteFiles([file.Id]);
- // Attach downsampled resolution file to active historian instance
- database.AttachFilesOrPaths([downsampledFile]);
+ // Attach downsampled resolution file to active historian instance
+ database.AttachFilesOrPaths([downsampledFile]);
- OnStatusMessage(MessageLevel.Info, $"Downsampled archive file \"{FilePath.GetFileName(file.FileName)}\" to {samplesPerSecond} samples per second.");
- }
- catch (Exception ex)
- {
- OnProcessException(MessageLevel.Warning, new InvalidOperationException($"Failed while attempting to transition to downsampled archive file \"{FilePath.GetFileName(downsampledFile)}\": {ex.Message}", ex));
- }
+ OnStatusMessage(MessageLevel.Info, $"Downsampled archive file \"{FilePath.GetFileName(file.FileName)}\" to {samplesPerSecond} samples per second.");
+ }
+ catch (Exception ex)
+ {
+ OnProcessException(MessageLevel.Warning, new InvalidOperationException($"Failed while attempting to transition to downsampled archive file \"{FilePath.GetFileName(downsampledFile)}\": {ex.Message}", ex));
}
}
}
- catch (Exception ex)
- {
- OnProcessException(MessageLevel.Warning, new InvalidOperationException($"Failed downsampling archive files: {ex.Message}", ex));
- }
}
-
- private string DownsampleArchiveFile(ArchiveDetails file, double samplesPerSecond, Guid stageFlags)
+ catch (Exception ex)
{
- try
- {
- DateTime startTime = file.StartTime;
- DateTime endTime = file.EndTime;
+ OnProcessException(MessageLevel.Warning, new InvalidOperationException($"Failed downsampling archive files: {ex.Message}", ex));
+ }
+ }
- // Interval is the time spacing between retained samples, i.e., the reciprocal of the target sample
- // rate (samplesPerSecond is validated > 0 by the DownsamplingIntervals parser, so this cannot divide
- // by zero).
- TimeSpan interval = new((long)(TimeSpan.TicksPerSecond / samplesPerSecond));
+ private string DownsampleArchiveFile(ArchiveDetails file, double samplesPerSecond, Guid stageFlags)
+ {
+ try
+ {
+ DateTime startTime = file.StartTime;
+ DateTime endTime = file.EndTime;
- using ArchiveList archiveList = new();
- archiveList.LoadFiles([file.FileName]);
+ // Interval is the time spacing between retained samples, i.e., the reciprocal of the target sample
+ // rate (samplesPerSecond is validated > 0 by the DownsamplingIntervals parser, so this cannot divide
+ // by zero).
+ TimeSpan interval = new((long)(TimeSpan.TicksPerSecond / samplesPerSecond));
- using SequentialReaderStream reader = new(archiveList, SortedTreeEngineReaderOptions.Default, TimestampSeekFilter.CreateFromIntervalData(startTime, endTime, interval, new TimeSpan(TimeSpan.TicksPerMillisecond)));
+ using ArchiveList archiveList = new();
+ archiveList.LoadFiles([file.FileName]);
- string sourcePath = FilePath.GetDirectoryName(file.FileName);
- string completeFileName = Path.Combine(sourcePath, $"{FilePath.GetFileNameWithoutExtension(file.FileName)}-{samplesPerSecond}sps.d2");
- string pendingFileName = Path.Combine(sourcePath, $"{FilePath.GetFileNameWithoutExtension(completeFileName)}.~d2i");
+ using SequentialReaderStream reader = new(archiveList, SortedTreeEngineReaderOptions.Default, TimestampSeekFilter.CreateFromIntervalData(startTime, endTime, interval, new TimeSpan(TimeSpan.TicksPerMillisecond)));
- SortedTreeFileSimpleWriter.Create(pendingFileName, completeFileName, 4096, null, HistorianFileEncodingDefinition.TypeGuid, reader, stageFlags);
+ string sourcePath = FilePath.GetDirectoryName(file.FileName);
+ string completeFileName = Path.Combine(sourcePath, $"{FilePath.GetFileNameWithoutExtension(file.FileName)}-{samplesPerSecond}sps.d2");
+ string pendingFileName = Path.Combine(sourcePath, $"{FilePath.GetFileNameWithoutExtension(completeFileName)}.~d2i");
- return completeFileName;
- }
- catch (Exception ex)
- {
- OnProcessException(MessageLevel.Warning, new InvalidOperationException($"Failed while attempting to downsample archive file \"{FilePath.GetFileName(file.FileName)}\" to {samplesPerSecond} samples per second: {ex.Message}", ex));
- }
+ SortedTreeFileSimpleWriter.CreateWithIndexes(pendingFileName, completeFileName, 4096, null, HistorianFileEncodingDefinition.TypeGuid, reader, CreateBloomFilterSettings(), EnablePointIDIndex, stageFlags);
- return null;
+ return completeFileName;
+ }
+ catch (Exception ex)
+ {
+ OnProcessException(MessageLevel.Warning, new InvalidOperationException($"Failed while attempting to downsample archive file \"{FilePath.GetFileName(file.FileName)}\" to {samplesPerSecond} samples per second: {ex.Message}", ex));
}
- private void RemoveFilesOlderThanMaxArchiveDays()
+ return null;
+ }
+
+ private void RemoveFilesOlderThanMaxArchiveDays()
+ {
+ try
{
- try
- {
- OnStatusMessage(MessageLevel.Info, $"Scanning for archive files older than {MaximumArchiveDays:N0} days...");
+ OnStatusMessage(MessageLevel.Info, $"Scanning for archive files older than {MaximumArchiveDays:N0} days...");
- ClientDatabaseBase database = GetClientDatabase();
+ ClientDatabaseBase database = GetClientDatabase();
- // Scope deletion to files within the write directories so attached/non-write folders are never
- // deleted by the maximum-archive-days retention window.
- string[] normalizedArchiveDirectories = GetNormalizedArchiveDirectories();
+ // Scope deletion to files within the write directories so attached/non-write folders are never
+ // deleted by the maximum-archive-days retention window.
+ string[] normalizedArchiveDirectories = GetNormalizedArchiveDirectories();
- // Get list of files that have both a start time and an end time that are greater than the maximum
- // archive days. We check both start and end times since devices with an inaccurate GPS clock can
- // provide bad time when out of range timestamps are not configured to be filtered and you don't
- // want to accidentally delete a file with otherwise in-range data.
- ArchiveDetails[] filesToDelete = database.GetAllAttachedFiles().Where(file =>
- FilePathTargetsArchiveDirectories(file.FileName, normalizedArchiveDirectories) &&
- (DateTime.UtcNow - file.StartTime).TotalDays > MaximumArchiveDays &&
- (DateTime.UtcNow - file.EndTime).TotalDays > MaximumArchiveDays).ToArray();
+ // Get list of files that have both a start time and an end time that are greater than the maximum
+ // archive days. We check both start and end times since devices with an inaccurate GPS clock can
+ // provide bad time when out of range timestamps are not configured to be filtered and you don't
+ // want to accidentally delete a file with otherwise in-range data.
+ ArchiveDetails[] filesToDelete = database.GetAllAttachedFiles().Where(file =>
+ FilePathTargetsArchiveDirectories(file.FileName, normalizedArchiveDirectories) &&
+ (DateTime.UtcNow - file.StartTime).TotalDays > MaximumArchiveDays &&
+ (DateTime.UtcNow - file.EndTime).TotalDays > MaximumArchiveDays).ToArray();
- if (filesToDelete.Length <= 0)
- return;
+ if (filesToDelete.Length <= 0)
+ return;
- database.DeleteFiles(filesToDelete.Select(file => file.Id).ToList());
+ database.DeleteFiles(filesToDelete.Select(file => file.Id).ToList());
- OnStatusMessage(MessageLevel.Info, $"Deleted the following archive files that were older than {MaximumArchiveDays:N0} days:\r\n {filesToDelete.Select(file => FilePath.TrimFileName(file.FileName, 75)).ToDelimitedString($"{Environment.NewLine} ")}");
- }
- catch (Exception ex)
- {
- OnProcessException(MessageLevel.Warning, new InvalidOperationException($"Failed while attempting to delete archive files older than {MaximumArchiveDays:N0} days: {ex.Message}", ex));
- }
+ OnStatusMessage(MessageLevel.Info, $"Deleted the following archive files that were older than {MaximumArchiveDays:N0} days:\r\n {filesToDelete.Select(file => FilePath.TrimFileName(file.FileName, 75)).ToDelimitedString($"{Environment.NewLine} ")}");
}
-
- private void ExecuteWildCardFileOperation(ClientDatabaseBase database, string fileName, Action> fileOperation)
+ catch (Exception ex)
{
- HashSet sourceFiles = new(FilePath.GetFileList(fileName).Select(Path.GetFullPath), StringComparer.OrdinalIgnoreCase);
- List files = database.GetAllAttachedFiles().Where(file => sourceFiles.Contains(Path.GetFullPath(file.FileName))).Select(file => file.Id).ToList();
- fileOperation(files);
+ OnProcessException(MessageLevel.Warning, new InvalidOperationException($"Failed while attempting to delete archive files older than {MaximumArchiveDays:N0} days: {ex.Message}", ex));
}
+ }
- private void ExecuteFileOperation(ClientDatabaseBase database, string fileName, Action> fileOperation)
- {
- List files = database.GetAllAttachedFiles().Where(file => Path.GetFullPath(file.FileName).Equals(fileName, StringComparison.OrdinalIgnoreCase)).Select(file => file.Id).ToList();
- fileOperation(files);
- }
+ private void ExecuteWildCardFileOperation(ClientDatabaseBase database, string fileName, Action> fileOperation)
+ {
+ HashSet sourceFiles = new(FilePath.GetFileList(fileName).Select(Path.GetFullPath), StringComparer.OrdinalIgnoreCase);
+ List files = database.GetAllAttachedFiles().Where(file => sourceFiles.Contains(Path.GetFullPath(file.FileName))).Select(file => file.Id).ToList();
+ fileOperation(files);
+ }
- private void ExecuteFolderOperation(ClientDatabaseBase database, string folderName, Action> folderOperation)
- {
- List files = database.GetAllAttachedFiles().Where(file => Path.GetFullPath(file.FileName).StartsWith(folderName, StringComparison.OrdinalIgnoreCase)).Select(file => file.Id).ToList();
- folderOperation(files);
- }
+ private void ExecuteFileOperation(ClientDatabaseBase database, string fileName, Action> fileOperation)
+ {
+ List files = database.GetAllAttachedFiles().Where(file => Path.GetFullPath(file.FileName).Equals(fileName, StringComparison.OrdinalIgnoreCase)).Select(file => file.Id).ToList();
+ fileOperation(files);
+ }
- private ClientDatabaseBase GetClientDatabase()
- {
- if (m_archive?.ClientDatabase is not null)
- return m_archive.ClientDatabase;
+ private void ExecuteFolderOperation(ClientDatabaseBase database, string folderName, Action> folderOperation)
+ {
+ List files = database.GetAllAttachedFiles().Where(file => Path.GetFullPath(file.FileName).StartsWith(folderName, StringComparison.OrdinalIgnoreCase)).Select(file => file.Id).ToList();
+ folderOperation(files);
+ }
- throw new InvalidOperationException("Cannot execute historian operation, archive database is not open.");
- }
+ private ClientDatabaseBase GetClientDatabase()
+ {
+ if (m_archive?.ClientDatabase is not null)
+ return m_archive.ClientDatabase;
- private void ArchiveCurtailmentTimerElapsed(object sender, ElapsedEventArgs e) => CurtailArchiveFiles();
+ throw new InvalidOperationException("Cannot execute historian operation, archive database is not open.");
+ }
- ///
- /// Attempts to connect to this .
- ///
- protected override void AttemptConnection()
- {
- // Open archive files
- Dictionary settings = (DataChannel ?? DefaultDataChannel).ParseKeyValuePairs();
+ private void ArchiveCurtailmentTimerElapsed(object sender, ElapsedEventArgs e) => CurtailArchiveFiles();
- if (!settings.TryGetValue("port", out string setting) || !int.TryParse(setting, out int port))
- port = DefaultPort;
+ ///
+ /// Attempts to connect to this .
+ ///
+ protected override void AttemptConnection()
+ {
+ // Open archive files
+ Dictionary settings = (DataChannel ?? DefaultDataChannel).ParseKeyValuePairs();
- if (!settings.TryGetValue("interface", out string networkInterfaceIP))
- networkInterfaceIP = "::0";
+ if (!settings.TryGetValue("port", out string setting) || !int.TryParse(setting, out int port))
+ port = DefaultPort;
- Server = new HistorianServer(m_archiveInfo, port, networkInterfaceIP);
- m_archive = Server[InstanceName];
+ if (!settings.TryGetValue("interface", out string networkInterfaceIP))
+ networkInterfaceIP = "::0";
- // Initialization of services needs to occur after files are open
- m_dataServices.Initialize();
- m_replicationProviders.Initialize();
+ Server = new HistorianServer(m_archiveInfo, port, networkInterfaceIP);
+ m_archive = Server[InstanceName];
- OnConnected();
+ // Initialization of services needs to occur after files are open
+ m_dataServices.Initialize();
+ m_replicationProviders.Initialize();
- if (m_attachedPathWatchers is not null)
- {
- foreach (SafeFileWatcher fileWatcher in m_attachedPathWatchers)
- fileWatcher.EnableRaisingEvents = true;
- }
+ OnConnected();
- new Thread(AttachArchiveDirectoriesAndAttachedPaths).Start();
+ if (m_attachedPathWatchers is not null)
+ {
+ foreach (SafeFileWatcher fileWatcher in m_attachedPathWatchers)
+ fileWatcher.EnableRaisingEvents = true;
}
- ///
- /// Attempts to disconnect from this .
- ///
- protected override void AttemptDisconnection()
+ new Thread(AttachArchiveDirectoriesAndAttachedPaths).Start();
+ }
+
+ ///
+ /// Attempts to disconnect from this .
+ ///
+ protected override void AttemptDisconnection()
+ {
+ if (m_attachedPathWatchers is not null)
{
- if (m_attachedPathWatchers is not null)
- {
- foreach (SafeFileWatcher fileWatcher in m_attachedPathWatchers)
- fileWatcher.EnableRaisingEvents = false;
- }
+ foreach (SafeFileWatcher fileWatcher in m_attachedPathWatchers)
+ fileWatcher.EnableRaisingEvents = false;
+ }
- m_archive = null;
- Server?.Dispose();
- Server = null;
+ m_archive = null;
+ Server?.Dispose();
+ Server = null;
- OnDisconnected();
- m_archivedMeasurements = 0;
- }
+ OnDisconnected();
+ m_archivedMeasurements = 0;
+ }
- ///
- /// Archives locally.
- ///
- /// Measurements to be archived.
- /// Local archive is closed.
- protected override void ProcessMeasurements(IMeasurement[] measurements)
+ ///
+ /// Archives locally.
+ ///
+ /// Measurements to be archived.
+ /// Local archive is closed.
+ protected override void ProcessMeasurements(IMeasurement[] measurements)
+ {
+ foreach (IMeasurement measurement in measurements)
{
- foreach (IMeasurement measurement in measurements)
+ // Validate timestamp reasonability as compared to local clock, when enabled
+ if (EnableTimeReasonabilityCheck)
{
- // Validate timestamp reasonability as compared to local clock, when enabled
- if (EnableTimeReasonabilityCheck)
- {
- long deviation = DateTime.UtcNow.Ticks - measurement.Timestamp.Value;
+ long deviation = DateTime.UtcNow.Ticks - measurement.Timestamp.Value;
- if (deviation < -m_futureTimeReasonabilityLimit || deviation > m_pastTimeReasonabilityLimit)
- continue;
- }
+ if (deviation < -m_futureTimeReasonabilityLimit || deviation > m_pastTimeReasonabilityLimit)
+ continue;
+ }
- m_key.Timestamp = (ulong)measurement.Timestamp.Value;
- m_key.PointID = measurement.Key.ID;
+ m_key.Timestamp = (ulong)measurement.Timestamp.Value;
+ m_key.PointID = measurement.Key.ID;
- // Since current time-series measurements are basically all floats - values fit into first value,
- // this will change as value types for time-series framework expands
- m_value.Value1 = BitConvert.ToUInt64((float)measurement.AdjustedValue);
- m_value.Value3 = (ulong)measurement.StateFlags;
+ // Since current time-series measurements are basically all floats - values fit into first value,
+ // this will change as value types for time-series framework expands
+ m_value.Value1 = BitConvert.ToUInt64((float)measurement.AdjustedValue);
+ m_value.Value3 = (ulong)measurement.StateFlags;
- // Check to see if swinging door compression is enabled
- if (SwingingDoorCompressionEnabled)
+ // Check to see if swinging door compression is enabled
+ if (SwingingDoorCompressionEnabled)
+ {
+ // Attempt to lookup compression settings for this measurement
+ if ((m_compressionSettings?.TryGetValue(m_key.PointID, out Tuple settings) ?? false) && settings is not null)
{
- // Attempt to lookup compression settings for this measurement
- if ((m_compressionSettings?.TryGetValue(m_key.PointID, out Tuple settings) ?? false) && settings is not null)
+ // Get compression settings
+ int compressionMinTime = settings.Item1;
+ int compressionMaxTime = settings.Item2;
+ double compressionLimit = settings.Item3;
+
+ // Get current swinging door compression state, creating state if needed
+ Tuple state = m_swingingDoorStates.GetOrAdd(m_key.PointID, _ => new Tuple(measurement, measurement, double.MinValue, double.MaxValue));
+ IMeasurement currentData = measurement;
+ IMeasurement archivedData = state.Item1;
+ IMeasurement previousData = state.Item2;
+ double lastHighSlope = state.Item3;
+ double lastLowSlope = state.Item4;
+ double highSlope = 0.0D;
+ double lowSlope = 0.0D;
+ bool archiveData;
+
+ // Data is to be compressed
+ if (compressionMinTime > 0 && currentData.Timestamp - archivedData.Timestamp < compressionMinTime)
{
- // Get compression settings
- int compressionMinTime = settings.Item1;
- int compressionMaxTime = settings.Item2;
- double compressionLimit = settings.Item3;
-
- // Get current swinging door compression state, creating state if needed
- Tuple state = m_swingingDoorStates.GetOrAdd(m_key.PointID, _ => new Tuple(measurement, measurement, double.MinValue, double.MaxValue));
- IMeasurement currentData = measurement;
- IMeasurement archivedData = state.Item1;
- IMeasurement previousData = state.Item2;
- double lastHighSlope = state.Item3;
- double lastLowSlope = state.Item4;
- double highSlope = 0.0D;
- double lowSlope = 0.0D;
- bool archiveData;
-
- // Data is to be compressed
- if (compressionMinTime > 0 && currentData.Timestamp - archivedData.Timestamp < compressionMinTime)
- {
- // CompressionMinTime is in effect
- archiveData = false;
- }
- else if (currentData.StateFlags != archivedData.StateFlags || currentData.StateFlags != previousData.StateFlags || compressionMaxTime > 0 && previousData.Value - archivedData.Timestamp > compressionMaxTime)
- {
- // Quality changed or CompressionMaxTime is exceeded
- archiveData = true;
- }
- else
- {
- // Perform a compression test
- highSlope = (currentData.Value - (archivedData.Value + compressionLimit)) / (currentData.Timestamp - archivedData.Timestamp);
- lowSlope = (currentData.Value - (archivedData.Value - compressionLimit)) / (currentData.Timestamp - archivedData.Timestamp);
- double slope = (currentData.Value - archivedData.Value) / (currentData.Timestamp - archivedData.Timestamp);
-
- if (highSlope >= lastHighSlope)
- lastHighSlope = highSlope;
+ // CompressionMinTime is in effect
+ archiveData = false;
+ }
+ else if (currentData.StateFlags != archivedData.StateFlags || currentData.StateFlags != previousData.StateFlags || compressionMaxTime > 0 && previousData.Value - archivedData.Timestamp > compressionMaxTime)
+ {
+ // Quality changed or CompressionMaxTime is exceeded
+ archiveData = true;
+ }
+ else
+ {
+ // Perform a compression test
+ highSlope = (currentData.Value - (archivedData.Value + compressionLimit)) / (currentData.Timestamp - archivedData.Timestamp);
+ lowSlope = (currentData.Value - (archivedData.Value - compressionLimit)) / (currentData.Timestamp - archivedData.Timestamp);
+ double slope = (currentData.Value - archivedData.Value) / (currentData.Timestamp - archivedData.Timestamp);
- if (lowSlope <= lastLowSlope)
- lastLowSlope = lowSlope;
+ if (highSlope >= lastHighSlope)
+ lastHighSlope = highSlope;
- archiveData = slope <= lastHighSlope || slope >= lastLowSlope;
- }
+ if (lowSlope <= lastLowSlope)
+ lastLowSlope = lowSlope;
- // Update swinging door compression state
- m_swingingDoorStates[m_key.PointID] = new Tuple
- (
- archiveData ? currentData : archivedData,
- currentData,
- archiveData ? highSlope: lastHighSlope,
- archiveData ? lowSlope : lastLowSlope
- );
-
- // Continue to next point if this point does not need to be archived
- if (!archiveData)
- continue;
+ archiveData = slope <= lastHighSlope || slope >= lastLowSlope;
}
- }
- m_archive.Write(m_key, m_value);
+ // Update swinging door compression state
+ m_swingingDoorStates[m_key.PointID] = new Tuple
+ (
+ archiveData ? currentData : archivedData,
+ currentData,
+ archiveData ? highSlope: lastHighSlope,
+ archiveData ? lowSlope : lastLowSlope
+ );
+
+ // Continue to next point if this point does not need to be archived
+ if (!archiveData)
+ continue;
+ }
}
- m_archivedMeasurements += measurements.Length;
+ m_archive.Write(m_key, m_value);
}
- private void AttachArchiveDirectoriesAndAttachedPaths()
+ m_archivedMeasurements += measurements.Length;
+ }
+
+ private void AttachArchiveDirectoriesAndAttachedPaths()
+ {
+ try
{
- try
- {
- ClientDatabaseBase clientDatabase = GetClientDatabase();
+ ClientDatabaseBase clientDatabase = GetClientDatabase();
- string[] archivedirectories = m_archiveDirectories ?? [];
- string[] attachedPaths = m_attachedPaths ?? [];
+ string[] archivedirectories = m_archiveDirectories ?? [];
+ string[] attachedPaths = m_attachedPaths ?? [];
- List files = archivedirectories
- .Concat(attachedPaths)
- .SelectMany(dir => FilePath.EnumerateFiles(dir, exceptionHandler: ex =>
- OnProcessException(MessageLevel.Error, ex, nameof(AttachArchiveDirectoriesAndAttachedPaths))))
- .ToList();
+ List files = archivedirectories
+ .Concat(attachedPaths)
+ .SelectMany(dir => FilePath.EnumerateFiles(dir, exceptionHandler: ex =>
+ OnProcessException(MessageLevel.Error, ex, nameof(AttachArchiveDirectoriesAndAttachedPaths))))
+ .ToList();
- string[] filePtr = new string[1];
+ string[] filePtr = new string[1];
- foreach (string file in files)
- {
- if (!Enabled)
- return;
+ foreach (string file in files)
+ {
+ if (!Enabled)
+ return;
- filePtr[0] = file;
+ filePtr[0] = file;
- try
- {
- clientDatabase.AttachFilesOrPaths(filePtr);
- }
- catch (Exception ex)
- {
- OnProcessException(MessageLevel.Error, new InvalidOperationException(
+ try
+ {
+ clientDatabase.AttachFilesOrPaths(filePtr);
+ }
+ catch (Exception ex)
+ {
+ OnProcessException(MessageLevel.Error, new InvalidOperationException(
$"{Name} failed while attempting to attach to file \"{file}\": {ex.Message}", ex),
- nameof(AttachArchiveDirectoriesAndAttachedPaths));
- }
+ nameof(AttachArchiveDirectoriesAndAttachedPaths));
}
}
- catch (Exception ex)
- {
- OnProcessException(MessageLevel.Critical, new InvalidOperationException($"{Name} failed while attempting to attach to existing archives: {ex.Message}", ex));
- }
}
-
- private SafeFileWatcher WatchPath(string path)
+ catch (Exception ex)
{
- SafeFileWatcher fileWatcher = new(path, "*.d2*");
+ OnProcessException(MessageLevel.Critical, new InvalidOperationException($"{Name} failed while attempting to attach to existing archives: {ex.Message}", ex));
+ }
+ }
+
+ private SafeFileWatcher WatchPath(string path)
+ {
+ SafeFileWatcher fileWatcher = new(path, "*.d2*");
- fileWatcher.Created += (_, args) => ThreadPool.QueueUserWorkItem(state =>
+ fileWatcher.Created += (_, args) => ThreadPool.QueueUserWorkItem(state =>
{
ClientDatabaseBase clientDatabase = GetClientDatabase();
string[] filePtr = [(string)state];
@@ -1398,539 +1497,543 @@ private SafeFileWatcher WatchPath(string path)
},
args.FullPath);
- fileWatcher.InternalBufferSize = 65536;
+ fileWatcher.InternalBufferSize = 65536;
- return fileWatcher;
- }
+ return fileWatcher;
+ }
+
+ private TimeBucketBloomFilterSettings CreateBloomFilterSettings()
+ {
+ return new TimeBucketBloomFilterSettings(
+ EnableTimeBucketBloomFilter,
+ BloomFilterBucketDurationSeconds,
+ BloomFilterFalsePositiveProbability,
+ BloomFilterMaximumIndexSizeMB);
+ }
- private void DataServices_AdapterCreated(object sender, EventArgs e) =>
- e.Argument.SettingsCategory = $"{InstanceName.ToLowerInvariant()}{e.Argument.SettingsCategory}";
+ private void DataServices_AdapterCreated(object sender, EventArgs e) =>
+ e.Argument.SettingsCategory = $"{InstanceName.ToLowerInvariant()}{e.Argument.SettingsCategory}";
- private void DataServices_AdapterLoaded(object sender, EventArgs e)
- {
- e.Argument.Archive = m_archive;
- e.Argument.ServiceProcessException += DataServices_ServiceProcessException;
+ private void DataServices_AdapterLoaded(object sender, EventArgs e)
+ {
+ e.Argument.Archive = m_archive;
+ e.Argument.ServiceProcessException += DataServices_ServiceProcessException;
- OnStatusMessage(MessageLevel.Info, $"{e.Argument.GetType().Name} has been loaded.");
- }
+ OnStatusMessage(MessageLevel.Info, $"{e.Argument.GetType().Name} has been loaded.");
+ }
- private void DataServices_AdapterUnloaded(object sender, EventArgs e)
- {
- e.Argument.Archive = null;
- e.Argument.ServiceProcessException -= DataServices_ServiceProcessException;
+ private void DataServices_AdapterUnloaded(object sender, EventArgs e)
+ {
+ e.Argument.Archive = null;
+ e.Argument.ServiceProcessException -= DataServices_ServiceProcessException;
- OnStatusMessage(MessageLevel.Info, $"{e.Argument.GetType().Name} has been unloaded.");
- }
+ OnStatusMessage(MessageLevel.Info, $"{e.Argument.GetType().Name} has been unloaded.");
+ }
- private void ReplicationProviders_AdapterCreated(object sender, EventArgs e) =>
- e.Argument.SettingsCategory = InstanceName.ToLowerInvariant() + e.Argument.SettingsCategory;
+ private void ReplicationProviders_AdapterCreated(object sender, EventArgs e) =>
+ e.Argument.SettingsCategory = InstanceName.ToLowerInvariant() + e.Argument.SettingsCategory;
- private void ReplicationProviders_AdapterLoaded(object sender, EventArgs e)
- {
- e.Argument.ReplicationStart += ReplicationProvider_ReplicationStart;
- e.Argument.ReplicationComplete += ReplicationProvider_ReplicationComplete;
- e.Argument.ReplicationProgress += ReplicationProvider_ReplicationProgress;
- e.Argument.ReplicationException += ReplicationProvider_ReplicationException;
+ private void ReplicationProviders_AdapterLoaded(object sender, EventArgs e)
+ {
+ e.Argument.ReplicationStart += ReplicationProvider_ReplicationStart;
+ e.Argument.ReplicationComplete += ReplicationProvider_ReplicationComplete;
+ e.Argument.ReplicationProgress += ReplicationProvider_ReplicationProgress;
+ e.Argument.ReplicationException += ReplicationProvider_ReplicationException;
- OnStatusMessage(MessageLevel.Info, $"{e.Argument.GetType().Name} has been loaded.");
- }
+ OnStatusMessage(MessageLevel.Info, $"{e.Argument.GetType().Name} has been loaded.");
+ }
- private void ReplicationProviders_AdapterUnloaded(object sender, EventArgs e)
- {
- e.Argument.ReplicationStart -= ReplicationProvider_ReplicationStart;
- e.Argument.ReplicationComplete -= ReplicationProvider_ReplicationComplete;
- e.Argument.ReplicationProgress -= ReplicationProvider_ReplicationProgress;
- e.Argument.ReplicationException -= ReplicationProvider_ReplicationException;
+ private void ReplicationProviders_AdapterUnloaded(object sender, EventArgs e)
+ {
+ e.Argument.ReplicationStart -= ReplicationProvider_ReplicationStart;
+ e.Argument.ReplicationComplete -= ReplicationProvider_ReplicationComplete;
+ e.Argument.ReplicationProgress -= ReplicationProvider_ReplicationProgress;
+ e.Argument.ReplicationException -= ReplicationProvider_ReplicationException;
- OnStatusMessage(MessageLevel.Info, $"{e.Argument.GetType().Name} has been unloaded.");
- }
+ OnStatusMessage(MessageLevel.Info, $"{e.Argument.GetType().Name} has been unloaded.");
+ }
- private void AdapterLoader_AdapterLoadException(object sender, EventArgs e) =>
- OnProcessException(MessageLevel.Warning, e.Argument);
+ private void AdapterLoader_AdapterLoadException(object sender, EventArgs e) =>
+ OnProcessException(MessageLevel.Warning, e.Argument);
- private void DataServices_ServiceProcessException(object sender, EventArgs e) =>
- OnProcessException(MessageLevel.Warning, e.Argument);
+ private void DataServices_ServiceProcessException(object sender, EventArgs e) =>
+ OnProcessException(MessageLevel.Warning, e.Argument);
- private void ReplicationProvider_ReplicationStart(object sender, EventArgs e) =>
- OnStatusMessage(MessageLevel.Info, $"{sender.GetType().Name} has started archive replication...");
+ private void ReplicationProvider_ReplicationStart(object sender, EventArgs e) =>
+ OnStatusMessage(MessageLevel.Info, $"{sender.GetType().Name} has started archive replication...");
- private void ReplicationProvider_ReplicationComplete(object sender, EventArgs e) =>
- OnStatusMessage(MessageLevel.Info, $"{sender.GetType().Name} has finished archive replication.");
+ private void ReplicationProvider_ReplicationComplete(object sender, EventArgs e) =>
+ OnStatusMessage(MessageLevel.Info, $"{sender.GetType().Name} has finished archive replication.");
- private void ReplicationProvider_ReplicationProgress(object sender, EventArgs> e) =>
- OnStatusMessage(MessageLevel.Info, $"{sender.GetType().Name} has replicated archive file {e.Argument.ProgressMessage}.");
+ private void ReplicationProvider_ReplicationProgress(object sender, EventArgs> e) =>
+ OnStatusMessage(MessageLevel.Info, $"{sender.GetType().Name} has replicated archive file {e.Argument.ProgressMessage}.");
- private void ReplicationProvider_ReplicationException(object sender, EventArgs e) =>
- OnProcessException(MessageLevel.Warning, e.Argument);
+ private void ReplicationProvider_ReplicationException(object sender, EventArgs e) =>
+ OnProcessException(MessageLevel.Warning, e.Argument);
- #endregion
+ #endregion
- #region [ Static ]
+ #region [ Static ]
- // Static Fields
+ // Static Fields
- ///
- /// Accesses local output adapter instances (normally only one).
- ///
- public static readonly ConcurrentDictionary Instances = new(StringComparer.OrdinalIgnoreCase);
+ ///
+ /// Accesses local output adapter instances (normally only one).
+ ///
+ public static readonly ConcurrentDictionary Instances = new(StringComparer.OrdinalIgnoreCase);
- private static int s_virtualProtocolID;
+ private static int s_virtualProtocolID;
- // Static Constructor
+ // Static Constructor
- static LocalOutputAdapter()
- {
- CategorizedSettingsElementCollection systemSettings = ConfigurationFile.Current.Settings["systemSettings"];
+ static LocalOutputAdapter()
+ {
+ CategorizedSettingsElementCollection systemSettings = ConfigurationFile.Current.Settings["systemSettings"];
- systemSettings.Add("MemoryPoolSize", "0.0", "The fixed memory pool size in Gigabytes. Leave at zero for dynamically calculated setting.");
- systemSettings.Add("MemoryPoolTargetUtilization", "Low", "The target utilization level for the memory pool. One of 'Low', 'Medium', or 'High'.");
+ systemSettings.Add("MemoryPoolSize", "0.0", "The fixed memory pool size in Gigabytes. Leave at zero for dynamically calculated setting.");
+ systemSettings.Add("MemoryPoolTargetUtilization", "Low", "The target utilization level for the memory pool. One of 'Low', 'Medium', or 'High'.");
- // Set maximum buffer size
- double memoryPoolSize = systemSettings["MemoryPoolSize"].ValueAs(0.0D);
+ // Set maximum buffer size
+ double memoryPoolSize = systemSettings["MemoryPoolSize"].ValueAs(0.0D);
- if (memoryPoolSize > 0.0D)
- Globals.MemoryPool.SetMaximumBufferSize((long)(memoryPoolSize * SI2.Giga));
+ if (memoryPoolSize > 0.0D)
+ Globals.MemoryPool.SetMaximumBufferSize((long)(memoryPoolSize * SI2.Giga));
- if (!Enum.TryParse(systemSettings["MemoryPoolTargetUtilization"].Value, false, out TargetUtilizationLevels targetLevel))
- targetLevel = TargetUtilizationLevels.High;
+ if (!Enum.TryParse(systemSettings["MemoryPoolTargetUtilization"].Value, false, out TargetUtilizationLevels targetLevel))
+ targetLevel = TargetUtilizationLevels.High;
- Globals.MemoryPool.SetTargetUtilizationLevel(targetLevel);
- }
+ Globals.MemoryPool.SetTargetUtilizationLevel(targetLevel);
+ }
- // Static Methods
+ // Static Methods
- // ReSharper disable UnusedMember.Local
- // ReSharper disable UnusedParameter.Local
- private static void OptimizeLocalHistorianSettings(AdoDataConnection connection, string nodeIDQueryString, ulong trackingVersion, string arguments, Action statusMessage, Action processException)
+ // ReSharper disable UnusedMember.Local
+ // ReSharper disable UnusedParameter.Local
+ private static void OptimizeLocalHistorianSettings(AdoDataConnection connection, string nodeIDQueryString, ulong trackingVersion, string arguments, Action statusMessage, Action processException)
+ {
+ TableOperations inputAdapterTable = new(connection);
+ TableOperations actionAdapterTable = new(connection);
+ TableOperations outputAdapterTable = new(connection);
+ TableOperations historianTable = new(connection);
+ TableOperations measurementTable = new(connection);
+ TableOperations deviceGroupClassTable = new(connection);
+
+ // Make sure setting exists to allow user to by-pass local historian optimizations at startup
+ ConfigurationFile configFile = ConfigurationFile.Current;
+ CategorizedSettingsElementCollection settings = configFile.Settings["systemSettings"];
+ settings.Add("OptimizeLocalHistorianSettings", true, "Determines if the defined local historians will have their settings optimized at startup");
+
+ // See if this node should optimize local historian settings
+ if (settings["OptimizeLocalHistorianSettings"].ValueAsBoolean())
{
- TableOperations inputAdapterTable = new(connection);
- TableOperations actionAdapterTable = new(connection);
- TableOperations outputAdapterTable = new(connection);
- TableOperations historianTable = new(connection);
- TableOperations measurementTable = new(connection);
- TableOperations deviceGroupClassTable = new(connection);
-
- // Make sure setting exists to allow user to by-pass local historian optimizations at startup
- ConfigurationFile configFile = ConfigurationFile.Current;
- CategorizedSettingsElementCollection settings = configFile.Settings["systemSettings"];
- settings.Add("OptimizeLocalHistorianSettings", true, "Determines if the defined local historians will have their settings optimized at startup");
-
- // See if this node should optimize local historian settings
- if (settings["OptimizeLocalHistorianSettings"].ValueAsBoolean())
- {
- statusMessage("Optimizing settings for local historians...");
+ statusMessage("Optimizing settings for local historians...");
- // Load the defined local system historians
- IEnumerable historians = connection.RetrieveData($"SELECT AdapterName FROM RuntimeHistorian WHERE NodeID = {nodeIDQueryString} AND TypeName = 'openHistorian.Adapters.LocalOutputAdapter'").AsEnumerable();
+ // Load the defined local system historians
+ IEnumerable historians = connection.RetrieveData($"SELECT AdapterName FROM RuntimeHistorian WHERE NodeID = {nodeIDQueryString} AND TypeName = 'openHistorian.Adapters.LocalOutputAdapter'").AsEnumerable();
- List validHistorians = [];
+ List validHistorians = [];
- // Apply settings optimizations to local historians
- foreach (DataRow row in historians)
- {
- string acronym = row.Field("AdapterName").ToLower();
- validHistorians.Add(acronym);
- }
+ // Apply settings optimizations to local historians
+ foreach (DataRow row in historians)
+ {
+ string acronym = row.Field("AdapterName").ToLower();
+ validHistorians.Add(acronym);
+ }
- // Local statics historian is valid regardless of historian type
- if (!validHistorians.Contains("stat"))
- validHistorians.Add("stat");
+ // Local statics historian is valid regardless of historian type
+ if (!validHistorians.Contains("stat"))
+ validHistorians.Add("stat");
- // Sort valid historians for binary search
- validHistorians.Sort();
+ // Sort valid historians for binary search
+ validHistorians.Sort();
- // Create a list to track categories to remove
- HashSet categoriesToRemove = [];
+ // Create a list to track categories to remove
+ HashSet categoriesToRemove = [];
- // Search for unused settings categories
- foreach (PropertyInformation info in configFile.Settings.ElementInformation.Properties)
- {
- string name = info.Name;
+ // Search for unused settings categories
+ foreach (PropertyInformation info in configFile.Settings.ElementInformation.Properties)
+ {
+ string name = info.Name;
- if (name.EndsWith("AdoMetadataProvider") && validHistorians.BinarySearch(name.Substring(0, name.IndexOf("AdoMetadataProvider", StringComparison.Ordinal))) < 0)
- categoriesToRemove.Add(name);
+ if (name.EndsWith("AdoMetadataProvider") && validHistorians.BinarySearch(name.Substring(0, name.IndexOf("AdoMetadataProvider", StringComparison.Ordinal))) < 0)
+ categoriesToRemove.Add(name);
- if (name.EndsWith("OleDbMetadataProvider") && validHistorians.BinarySearch(name.Substring(0, name.IndexOf("OleDbMetadataProvider", StringComparison.Ordinal))) < 0)
- categoriesToRemove.Add(name);
+ if (name.EndsWith("OleDbMetadataProvider") && validHistorians.BinarySearch(name.Substring(0, name.IndexOf("OleDbMetadataProvider", StringComparison.Ordinal))) < 0)
+ categoriesToRemove.Add(name);
- if (name.EndsWith("RestWebServiceMetadataProvider") && validHistorians.BinarySearch(name.Substring(0, name.IndexOf("RestWebServiceMetadataProvider", StringComparison.Ordinal))) < 0)
- categoriesToRemove.Add(name);
+ if (name.EndsWith("RestWebServiceMetadataProvider") && validHistorians.BinarySearch(name.Substring(0, name.IndexOf("RestWebServiceMetadataProvider", StringComparison.Ordinal))) < 0)
+ categoriesToRemove.Add(name);
- if (name.EndsWith("MetadataService") && validHistorians.BinarySearch(name.Substring(0, name.IndexOf("MetadataService", StringComparison.Ordinal))) < 0)
- categoriesToRemove.Add(name);
+ if (name.EndsWith("MetadataService") && validHistorians.BinarySearch(name.Substring(0, name.IndexOf("MetadataService", StringComparison.Ordinal))) < 0)
+ categoriesToRemove.Add(name);
- if (name.EndsWith("TimeSeriesDataService") && validHistorians.BinarySearch(name.Substring(0, name.IndexOf("TimeSeriesDataService", StringComparison.Ordinal))) < 0)
- categoriesToRemove.Add(name);
+ if (name.EndsWith("TimeSeriesDataService") && validHistorians.BinarySearch(name.Substring(0, name.IndexOf("TimeSeriesDataService", StringComparison.Ordinal))) < 0)
+ categoriesToRemove.Add(name);
- if (name.EndsWith("HadoopReplicationProvider") && validHistorians.BinarySearch(name.Substring(0, name.IndexOf("HadoopReplicationProvider", StringComparison.Ordinal))) < 0)
- categoriesToRemove.Add(name);
- }
+ if (name.EndsWith("HadoopReplicationProvider") && validHistorians.BinarySearch(name.Substring(0, name.IndexOf("HadoopReplicationProvider", StringComparison.Ordinal))) < 0)
+ categoriesToRemove.Add(name);
+ }
- if (categoriesToRemove.Count > 0)
- {
- statusMessage("Removing unused local historian configuration settings...");
+ if (categoriesToRemove.Count > 0)
+ {
+ statusMessage("Removing unused local historian configuration settings...");
- // Remove any unused settings categories
- foreach (string category in categoriesToRemove)
- configFile.Settings.Remove(category);
- }
+ // Remove any unused settings categories
+ foreach (string category in categoriesToRemove)
+ configFile.Settings.Remove(category);
+ }
+
+ // Save any applied changes
+ configFile.Save();
- // Save any applied changes
- configFile.Save();
+ // Convert valid historian list to proper acronym casing (and remove stat historian)
+ IEnumerable historianAcronyms = validHistorians.Where(value => !value.Equals("stat")).Select(value => value.ToUpperInvariant());
- // Convert valid historian list to proper acronym casing (and remove stat historian)
- IEnumerable historianAcronyms = validHistorians.Where(value => !value.Equals("stat")).Select(value => value.ToUpperInvariant());
+ foreach (string historianAcronym in historianAcronyms)
+ {
+ Historian historianAdapter = historianTable.QueryRecordWhere("Acronym = {0}", historianAcronym);
+ CustomOutputAdapter outputAdapter = null;
- foreach (string historianAcronym in historianAcronyms)
+ if (historianAdapter is null)
{
- Historian historianAdapter = historianTable.QueryRecordWhere("Acronym = {0}", historianAcronym);
- CustomOutputAdapter outputAdapter = null;
+ outputAdapter = outputAdapterTable.QueryRecordWhere("AdapterName = {0}", historianAcronym);
- if (historianAdapter is null)
+ if (outputAdapter is null)
{
- outputAdapter = outputAdapterTable.QueryRecordWhere("AdapterName = {0}", historianAcronym);
-
- if (outputAdapter is null)
- {
- statusMessage($"WARNING: Could not check for associated historian input reader after failing to find historian adapter \"{historianAcronym}\" in either \"Historian\" or \"CustomOutputAdapter\" table. Record was just found in the \"RuntimeHistorian\" view, verify schema integrity.");
- continue;
- }
+ statusMessage($"WARNING: Could not check for associated historian input reader after failing to find historian adapter \"{historianAcronym}\" in either \"Historian\" or \"CustomOutputAdapter\" table. Record was just found in the \"RuntimeHistorian\" view, verify schema integrity.");
+ continue;
}
+ }
- // Parse connection string to get adapter settings for historian
- Dictionary adapterSettings = (historianAdapter?.ConnectionString ?? outputAdapter?.ConnectionString ?? "").ParseKeyValuePairs();
-
- // Get instance name for historian adapter
- if (!adapterSettings.TryGetValue("instanceName", out string instanceName) || string.IsNullOrWhiteSpace(instanceName))
- instanceName = historianAcronym;
+ // Parse connection string to get adapter settings for historian
+ Dictionary adapterSettings = (historianAdapter?.ConnectionString ?? outputAdapter?.ConnectionString ?? "").ParseKeyValuePairs();
- // Reader name will always be associated with instance name of historian (this often matches historian adapter name)
- string readerName = $"{instanceName}READER";
- CustomInputAdapter inputAdapter = inputAdapterTable.QueryRecordWhere("AdapterName = {0}", readerName);
+ // Get instance name for historian adapter
+ if (!adapterSettings.TryGetValue("instanceName", out string instanceName) || string.IsNullOrWhiteSpace(instanceName))
+ instanceName = historianAcronym;
- if (inputAdapter is not null)
- continue;
+ // Reader name will always be associated with instance name of historian (this often matches historian adapter name)
+ string readerName = $"{instanceName}READER";
+ CustomInputAdapter inputAdapter = inputAdapterTable.QueryRecordWhere("AdapterName = {0}", readerName);
- ushort? port = null;
+ if (inputAdapter is not null)
+ continue;
- // Check if historian has defined a custom data channel, if so attempt to parse port number
- if (adapterSettings.ContainsKey(nameof(DataChannel)))
- {
- Dictionary configSettings = adapterSettings[nameof(DataChannel)].ParseKeyValuePairs();
+ ushort? port = null;
- if (configSettings.ContainsKey("port") && ushort.TryParse(configSettings["port"], out ushort value))
- port = value;
- }
+ // Check if historian has defined a custom data channel, if so attempt to parse port number
+ if (adapterSettings.ContainsKey(nameof(DataChannel)))
+ {
+ Dictionary configSettings = adapterSettings[nameof(DataChannel)].ParseKeyValuePairs();
- string serverConnection = port is null ? "127.0.0.1" : $"127.0.0.1:{port.Value}";
-
- // Add new associated historian input adapter reader to handle temporal queries
- inputAdapter = inputAdapterTable.NewRecord();
- inputAdapter.NodeID = new Guid(nodeIDQueryString.RemoveCharacter('\''));
- inputAdapter.AdapterName = $"{historianAcronym}READER";
- inputAdapter.AssemblyName = "openHistorian.Adapters.dll";
- inputAdapter.TypeName = "openHistorian.Adapters.LocalInputAdapter";
- inputAdapter.ConnectionString = $"{nameof(LocalInputAdapter.InstanceName)}={instanceName}; {nameof(LocalInputAdapter.HistorianServer)}={serverConnection}; ConnectOnDemand=true";
- inputAdapter.LoadOrder = 0;
- inputAdapter.Enabled = true;
- inputAdapterTable.AddNewRecord(inputAdapter);
+ if (configSettings.ContainsKey("port") && ushort.TryParse(configSettings["port"], out ushort value))
+ port = value;
}
- }
- // TODO: Remove this code when device IDs have been updated to use GUIDs:
- // Make sure gateway protocol data publishers filter out device groups from metadata since groups reference local device IDs
- const string DefaultDeviceFilter = "FROM DeviceDetail WHERE IsConcentrator = 0;";
- const string GSFDataPublisherTypeName = $"{nameof(GSF)}.{nameof(GSF.TimeSeries)}.{nameof(GSF.TimeSeries.Transport)}.{nameof(GSF.TimeSeries.Transport.DataPublisher)}";
- const string GSFMetadataTables = nameof(GSFDataPublisher.MetadataTables);
- const string STTPDataPublisherTypeName = $"{nameof(sttp)}.{nameof(sttp.DataPublisher)}";
- const string STTPMetadataTables = nameof(STTPDataPublisher.MetadataTables);
- int virtualProtocolID = s_virtualProtocolID != 0 ? s_virtualProtocolID : s_virtualProtocolID = connection.ExecuteScalar("SELECT ID FROM Protocol WHERE Acronym='VirtualInput'");
- string deviceGroupFilter = $"FROM DeviceDetail WHERE IsConcentrator = 0 AND NOT (ProtocolID = {virtualProtocolID} AND AccessID = {DeviceGroup.DefaultAccessID});";
+ string serverConnection = port is null ? "127.0.0.1" : $"127.0.0.1:{port.Value}";
+
+ // Add new associated historian input adapter reader to handle temporal queries
+ inputAdapter = inputAdapterTable.NewRecord();
+ inputAdapter.NodeID = new Guid(nodeIDQueryString.RemoveCharacter('\''));
+ inputAdapter.AdapterName = $"{historianAcronym}READER";
+ inputAdapter.AssemblyName = "openHistorian.Adapters.dll";
+ inputAdapter.TypeName = "openHistorian.Adapters.LocalInputAdapter";
+ inputAdapter.ConnectionString = $"{nameof(LocalInputAdapter.InstanceName)}={instanceName}; {nameof(LocalInputAdapter.HistorianServer)}={serverConnection}; ConnectOnDemand=true";
+ inputAdapter.LoadOrder = 0;
+ inputAdapter.Enabled = true;
+ inputAdapterTable.AddNewRecord(inputAdapter);
+ }
+ }
- IEnumerable dataPublisherAdapters = actionAdapterTable.QueryRecordsWhere($"NodeID = {nodeIDQueryString} AND TypeName LIKE '%.DataPublisher'");
+ // TODO: Remove this code when device IDs have been updated to use GUIDs:
+ // Make sure gateway protocol data publishers filter out device groups from metadata since groups reference local device IDs
+ const string DefaultDeviceFilter = "FROM DeviceDetail WHERE IsConcentrator = 0;";
+ const string GSFDataPublisherTypeName = $"{nameof(GSF)}.{nameof(GSF.TimeSeries)}.{nameof(GSF.TimeSeries.Transport)}.{nameof(GSF.TimeSeries.Transport.DataPublisher)}";
+ const string GSFMetadataTables = nameof(GSFDataPublisher.MetadataTables);
+ const string STTPDataPublisherTypeName = $"{nameof(sttp)}.{nameof(sttp.DataPublisher)}";
+ const string STTPMetadataTables = nameof(STTPDataPublisher.MetadataTables);
+ int virtualProtocolID = s_virtualProtocolID != 0 ? s_virtualProtocolID : s_virtualProtocolID = connection.ExecuteScalar("SELECT ID FROM Protocol WHERE Acronym='VirtualInput'");
+ string deviceGroupFilter = $"FROM DeviceDetail WHERE IsConcentrator = 0 AND NOT (ProtocolID = {virtualProtocolID} AND AccessID = {DeviceGroup.DefaultAccessID});";
- foreach (CustomActionAdapter actionAdapter in dataPublisherAdapters)
- {
- if (actionAdapter is null)
- continue;
+ IEnumerable dataPublisherAdapters = actionAdapterTable.QueryRecordsWhere($"NodeID = {nodeIDQueryString} AND TypeName LIKE '%.DataPublisher'");
- Dictionary connectionString = actionAdapter.ConnectionString?.ParseKeyValuePairs() ?? new Dictionary(StringComparer.OrdinalIgnoreCase);
- bool connectionStringUpdated = false;
+ foreach (CustomActionAdapter actionAdapter in dataPublisherAdapters)
+ {
+ if (actionAdapter is null)
+ continue;
- switch (actionAdapter.TypeName)
- {
- case GSFDataPublisherTypeName:
- if (connectionString.ContainsKey(GSFMetadataTables))
- continue;
-
- connectionString[GSFMetadataTables] = GSFDataPublisher.DefaultMetadataTables.Replace(DefaultDeviceFilter, deviceGroupFilter);
- connectionStringUpdated = true;
- break;
- case STTPDataPublisherTypeName:
- // For down-stream identity matched metadata replication, check option to allow device groups replicate - this will OK when device IDs match, e.g.,
- // when STTP data subscribers enable the "useIdentityInsertsForMetadata" option
- if (connectionString.TryGetValue("allowDeviceGroupReplication", out string setting) && setting.ParseBoolean())
- {
- connectionString.Remove(STTPMetadataTables);
- }
- else
- {
- if (connectionString.ContainsKey(STTPMetadataTables))
- continue;
+ Dictionary connectionString = actionAdapter.ConnectionString?.ParseKeyValuePairs() ?? new Dictionary(StringComparer.OrdinalIgnoreCase);
+ bool connectionStringUpdated = false;
- connectionString[STTPMetadataTables] = STTPDataPublisher.DefaultMetadataTables.Replace(DefaultDeviceFilter, deviceGroupFilter);
- connectionStringUpdated = true;
- }
- break;
- default:
+ switch (actionAdapter.TypeName)
+ {
+ case GSFDataPublisherTypeName:
+ if (connectionString.ContainsKey(GSFMetadataTables))
continue;
- }
+
+ connectionString[GSFMetadataTables] = GSFDataPublisher.DefaultMetadataTables.Replace(DefaultDeviceFilter, deviceGroupFilter);
+ connectionStringUpdated = true;
+ break;
+ case STTPDataPublisherTypeName:
+ // For down-stream identity matched metadata replication, check option to allow device groups replicate - this will OK when device IDs match, e.g.,
+ // when STTP data subscribers enable the "useIdentityInsertsForMetadata" option
+ if (connectionString.TryGetValue("allowDeviceGroupReplication", out string setting) && setting.ParseBoolean())
+ {
+ connectionString.Remove(STTPMetadataTables);
+ }
+ else
+ {
+ if (connectionString.ContainsKey(STTPMetadataTables))
+ continue;
- if (!connectionStringUpdated)
+ connectionString[STTPMetadataTables] = STTPDataPublisher.DefaultMetadataTables.Replace(DefaultDeviceFilter, deviceGroupFilter);
+ connectionStringUpdated = true;
+ }
+ break;
+ default:
continue;
-
- actionAdapter.ConnectionString = connectionString.JoinKeyValuePairs();
- actionAdapterTable.UpdateRecord(actionAdapter);
}
- // Make sure default device group classes are defined
- int count = deviceGroupClassTable.QueryRecordCountWhere("Longitude = {0} and Latitude = {0}", DeviceGroupClass.DefaultGeoID);
+ if (!connectionStringUpdated)
+ continue;
+
+ actionAdapter.ConnectionString = connectionString.JoinKeyValuePairs();
+ actionAdapterTable.UpdateRecord(actionAdapter);
+ }
+
+ // Make sure default device group classes are defined
+ int count = deviceGroupClassTable.QueryRecordCountWhere("Longitude = {0} and Latitude = {0}", DeviceGroupClass.DefaultGeoID);
- if (count == 0)
+ if (count == 0)
+ {
+ // Add default device group classes
+ List classOptions =
+ [
+ "Region",
+ "Substation",
+ "Generation",
+ "Other"
+ ];
+
+ foreach (string classOption in classOptions)
{
- // Add default device group classes
- List classOptions =
- [
- "Region",
- "Substation",
- "Generation",
- "Other"
- ];
-
- foreach (string classOption in classOptions)
- {
- DeviceGroupClass deviceGroupClass = deviceGroupClassTable.NewRecord();
- deviceGroupClass.Acronym = classOption.ToUpperInvariant();
- deviceGroupClass.Name = classOption;
- deviceGroupClassTable.AddNewRecord(deviceGroupClass);
- }
+ DeviceGroupClass deviceGroupClass = deviceGroupClassTable.NewRecord();
+ deviceGroupClass.Acronym = classOption.ToUpperInvariant();
+ deviceGroupClass.Name = classOption;
+ deviceGroupClassTable.AddNewRecord(deviceGroupClass);
}
+ }
- IEnumerable dynamicCalculators = actionAdapterTable.QueryRecordsWhere("TypeName = 'DynamicCalculator.DynamicCalculator'");
+ IEnumerable dynamicCalculators = actionAdapterTable.QueryRecordsWhere("TypeName = 'DynamicCalculator.DynamicCalculator'");
- foreach (CustomActionAdapter dynamicCalculator in dynamicCalculators)
- {
- if (dynamicCalculator is null)
- continue;
+ foreach (CustomActionAdapter dynamicCalculator in dynamicCalculators)
+ {
+ if (dynamicCalculator is null)
+ continue;
- Dictionary connectionString = dynamicCalculator.ConnectionString?.ParseKeyValuePairs() ?? new Dictionary(StringComparer.OrdinalIgnoreCase);
+ Dictionary connectionString = dynamicCalculator.ConnectionString?.ParseKeyValuePairs() ?? new Dictionary(StringComparer.OrdinalIgnoreCase);
- // variableList={FREQ=bef9f097-d6f8-4b4f-9ce4-97ffa30a08e9}; expressionText=FREQ-60; framesPerSecond=30; lagTime=6; leadTime=3; outputMeasurements=5966846f-156d-4a2d-a181-08c4875447a1; useLatestValues=false
- // variableList={VPOSA=62eb881c-4423-47bc-87bd-e3acfd5c7430; VAA=01564cc5-2451-4501-ba51-4fcea7173333}; expressionText=VPOSA-VAA; framesPerSecond=30; lagTime=6; leadTime=3; outputMeasurements=d59389a2-3816-4e84-a593-75f07369e582; useLatestValues=false
- // variableList={IANG_DIFF_CA[]=9108df49-4f96-4e8c-a23f-1eb07b281fa1,a66b1545-16f2-4f4a-8469-f3942cf27a56}; expressionText=IF(Any(IANG_DIFF_CA, "< 110") OR Any(IANG_DIFF_CA, "> 130"), 0, 1); framesPerSecond=30; lagTime=6; leadTime=3; outputMeasurements=174b312a-cc47-4390-b073-ee68033956e9; useLatestValues=false
-
- if (!connectionString.ContainsKey("variableList"))
- continue;
+ // variableList={FREQ=bef9f097-d6f8-4b4f-9ce4-97ffa30a08e9}; expressionText=FREQ-60; framesPerSecond=30; lagTime=6; leadTime=3; outputMeasurements=5966846f-156d-4a2d-a181-08c4875447a1; useLatestValues=false
+ // variableList={VPOSA=62eb881c-4423-47bc-87bd-e3acfd5c7430; VAA=01564cc5-2451-4501-ba51-4fcea7173333}; expressionText=VPOSA-VAA; framesPerSecond=30; lagTime=6; leadTime=3; outputMeasurements=d59389a2-3816-4e84-a593-75f07369e582; useLatestValues=false
+ // variableList={IANG_DIFF_CA[]=9108df49-4f96-4e8c-a23f-1eb07b281fa1,a66b1545-16f2-4f4a-8469-f3942cf27a56}; expressionText=IF(Any(IANG_DIFF_CA, "< 110") OR Any(IANG_DIFF_CA, "> 130"), 0, 1); framesPerSecond=30; lagTime=6; leadTime=3; outputMeasurements=174b312a-cc47-4390-b073-ee68033956e9; useLatestValues=false
- string variableList = connectionString["variableList"];
+ if (!connectionString.TryGetValue("variableList", out string variableList))
+ continue;
- if (string.IsNullOrWhiteSpace(variableList))
- continue;
+ if (string.IsNullOrWhiteSpace(variableList))
+ continue;
- Dictionary variables = variableList.ParseKeyValuePairs();
- HashSet signalIDs = new(variables.Values.SelectMany(ids => ids.Split(',')).Select(id => Guid.TryParse(id, out Guid signalID) ? signalID : Guid.Empty).Where(signalID => signalID != Guid.Empty));
+ Dictionary variables = variableList.ParseKeyValuePairs();
+ HashSet signalIDs = new(variables.Values.SelectMany(ids => ids.Split(',')).Select(id => Guid.TryParse(id, out Guid signalID) ? signalID : Guid.Empty).Where(signalID => signalID != Guid.Empty));
- if (signalIDs.Count == 0)
- continue;
+ if (signalIDs.Count == 0)
+ continue;
- int inputCount = connection.ExecuteScalar($"SELECT COUNT(*) FROM Measurement WHERE SignalID IN ({string.Join(",", signalIDs.Select(id => $"'{id}'"))})");
+ int inputCount = connection.ExecuteScalar($"SELECT COUNT(*) FROM Measurement WHERE SignalID IN ({string.Join(",", signalIDs.Select(id => $"'{id}'"))})");
- if (inputCount == signalIDs.Count)
- continue;
+ if (inputCount == signalIDs.Count)
+ continue;
- // Remove any calculations that reference non-existent input measurements
- int affectedRows = actionAdapterTable.DeleteRecord(dynamicCalculator);
+ // Remove any calculations that reference non-existent input measurements
+ int affectedRows = actionAdapterTable.DeleteRecord(dynamicCalculator);
- if (affectedRows > 0)
- statusMessage($"Removed dynamic calculation adapter \"{dynamicCalculator.AdapterName}\" that referenced non-existent input measurements.");
-
- // Remove result calculation output measurement
- if (!connectionString.ContainsKey("outputMeasurements"))
- continue;
+ if (affectedRows > 0)
+ statusMessage($"Removed dynamic calculation adapter \"{dynamicCalculator.AdapterName}\" that referenced non-existent input measurements.");
- string outputMeasurement = connectionString["outputMeasurements"];
+ // Remove result calculation output measurement
+ if (!connectionString.TryGetValue("outputMeasurements", out string outputMeasurement))
+ continue;
- if (string.IsNullOrWhiteSpace(outputMeasurement))
- continue;
+ if (string.IsNullOrWhiteSpace(outputMeasurement))
+ continue;
- if (Guid.TryParse(outputMeasurement, out Guid outputMeasurementID))
- measurementTable.DeleteRecordWhere($"SignalID = '{outputMeasurementID}'");
- }
+ if (Guid.TryParse(outputMeasurement, out Guid outputMeasurementID))
+ measurementTable.DeleteRecordWhere($"SignalID = '{outputMeasurementID}'");
}
-
- #endregion
-
- #region [ Future Code ]
-
- // This code can be used to refresh the local metadata repository when/if this gets implemented
-
- // >> members:
-
- //private MetadataProviders m_metadataProviders;
- //private readonly object m_queuedMetadataRefreshPending;
- //private AutoResetEvent m_metadataRefreshComplete;
- //private bool m_autoRefreshMetadata;
-
- // >> ctor:
-
- //m_autoRefreshMetadata = true;
- //m_queuedMetadataRefreshPending = new object();
- //m_metadataRefreshComplete = new AutoResetEvent(true);
-
- // >> Status:
-
- //status.Append(m_metadataProviders.Status);
- //status.AppendLine();
-
- // >> Initialize():
-
- //// Provide metadata sync support.
- //m_metadataProviders = new MetadataProviders();
- //m_metadataProviders.AdapterCreated += MetadataProviders_AdapterCreated;
- //m_metadataProviders.AdapterLoaded += MetadataProviders_AdapterLoaded;
- //m_metadataProviders.AdapterUnloaded += MetadataProviders_AdapterUnloaded;
- //m_metadataProviders.AdapterLoadException += AdapterLoader_AdapterLoadException;
-
- // >> Dispose():
-
- //if (m_metadataProviders != null)
- //{
- // m_metadataProviders.AdapterCreated -= MetadataProviders_AdapterCreated;
- // m_metadataProviders.AdapterLoaded -= MetadataProviders_AdapterLoaded;
- // m_metadataProviders.AdapterUnloaded -= MetadataProviders_AdapterUnloaded;
- // m_metadataProviders.AdapterLoadException -= AdapterLoader_AdapterLoadException;
- // m_metadataProviders.Dispose();
- //}
-
- // >> AttemptConnection():
-
- //m_metadataProviders.Initialize();
-
- //int waitCount = 0;
-
- //// Wait for adapter initialization to complete, up to 2 seconds
- //while (waitCount < 20 && m_adapterLoadedCount != m_dataServices.Adapters.Count + m_metadataProviders.Adapters.Count + m_replicationProviders.Adapters.Count)
- //{
- // Thread.Sleep(100);
- // waitCount++;
- //}
-
- //// Kick off a meta-data refresh...
- //if (m_autoRefreshMetadata)
- //{
- // RefreshMetadata();
- // m_autoRefreshMetadata = false;
- //}
-
- /////
- ///// Gets or sets a boolean indicating whether or not metadata is
- ///// refreshed when the adapter attempts to connect to the archive.
- /////
- //[ConnectionStringParameter,
- //Description("Define a boolean indicating whether to refresh metadata on connect."),
- //DefaultValue(true)]
- //public bool AutoRefreshMetadata
- //{
- // get
- // {
- // return m_autoRefreshMetadata;
- // }
- // set
- // {
- // m_autoRefreshMetadata = value;
- // }
- //}
-
- //private void MetadataProviders_AdapterCreated(object sender, EventArgs e)
- //{
- // e.Argument.SettingsCategory = InstanceName + e.Argument.SettingsCategory;
-
- // if (e.Argument.GetType() == typeof(AdoMetadataProvider))
- // {
- // // Populate the default configuration for AdoMetadataProvider.
- // AdoMetadataProvider provider = e.Argument as AdoMetadataProvider;
-
- // provider.Enabled = true;
- // provider.SelectString = string.Format("SELECT * FROM HistorianMetadata WHERE PlantCode='{0}'", Name);
-
- // // The following connection information is now provided via configuration Eval mappings
- // // provider.DataProviderString = config.Settings["SystemSettings"]["DataProviderString"].Value;
-
- // // ConfigurationFile config = ConfigurationFile.Current;
- // // string connectionString = config.Settings["SystemSettings"]["ConnectionString"].Value;
- // // Dictionary settings = connectionString.ParseKeyValuePairs();
- // // string setting;
-
- // // if (settings.TryGetValue("Provider", out setting))
- // // {
- // // // Check if provider is for Access
- // // if (setting.StartsWith("Microsoft.Jet.OLEDB", StringComparison.OrdinalIgnoreCase))
- // // {
- // // // Make sure path to Access database is fully qualified
- // // if (settings.TryGetValue("Data Source", out setting))
- // // {
- // // settings["Data Source"] = FilePath.GetAbsolutePath(setting);
- // // connectionString = settings.JoinKeyValuePairs();
- // // }
- // // }
- // // }
-
- // // provider.ConnectionString = connectionString;
- // }
- //}
-
- //private void MetadataProviders_AdapterLoaded(object sender, EventArgs e)
- //{
- // e.Argument.Metadata = m_archive.MetadataFile;
-
- // e.Argument.MetadataRefreshStart += MetadataProviders_MetadataRefreshStart;
- // e.Argument.MetadataRefreshComplete += MetadataProviders_MetadataRefreshComplete;
- // e.Argument.MetadataRefreshTimeout += MetadataProviders_MetadataRefreshTimeout;
- // e.Argument.MetadataRefreshException += MetadataProviders_MetadataRefreshException;
- // OnStatusMessage("{0} has been loaded.", e.Argument.GetType().Name);
-
- // m_adapterLoadedCount++;
- //}
-
- //private void MetadataProviders_AdapterUnloaded(object sender, EventArgs e)
- //{
- // e.Argument.Metadata = null;
- // e.Argument.MetadataRefreshStart -= MetadataProviders_MetadataRefreshStart;
- // e.Argument.MetadataRefreshComplete -= MetadataProviders_MetadataRefreshComplete;
- // e.Argument.MetadataRefreshTimeout -= MetadataProviders_MetadataRefreshTimeout;
- // e.Argument.MetadataRefreshException -= MetadataProviders_MetadataRefreshException;
- // OnStatusMessage("{0} has been unloaded.", e.Argument.GetType().Name);
- //}
-
- //private void MetadataProviders_MetadataRefreshStart(object sender, EventArgs e)
- //{
- // OnStatusMessage("{0} has started metadata refresh...", sender.GetType().Name);
- //}
-
- //private void MetadataProviders_MetadataRefreshComplete(object sender, EventArgs e)
- //{
- // OnStatusMessage("{0} has finished metadata refresh.", sender.GetType().Name);
- //}
-
- //private void MetadataProviders_MetadataRefreshTimeout(object sender, EventArgs e)
- //{
- // OnStatusMessage("{0} has timed-out on metadata refresh.", sender.GetType().Name);
- //}
-
- //private void MetadataProviders_MetadataRefreshException(object sender, EventArgs e)
- //{
- // OnProcessException(e.Argument);
- //}
- #endregion
}
-}
+
+ #endregion
+
+ #region [ Future Code ]
+
+ // This code can be used to refresh the local metadata repository when/if this gets implemented
+
+ // >> members:
+
+ //private MetadataProviders m_metadataProviders;
+ //private readonly object m_queuedMetadataRefreshPending;
+ //private AutoResetEvent m_metadataRefreshComplete;
+ //private bool m_autoRefreshMetadata;
+
+ // >> ctor:
+
+ //m_autoRefreshMetadata = true;
+ //m_queuedMetadataRefreshPending = new object();
+ //m_metadataRefreshComplete = new AutoResetEvent(true);
+
+ // >> Status:
+
+ //status.Append(m_metadataProviders.Status);
+ //status.AppendLine();
+
+ // >> Initialize():
+
+ //// Provide metadata sync support.
+ //m_metadataProviders = new MetadataProviders();
+ //m_metadataProviders.AdapterCreated += MetadataProviders_AdapterCreated;
+ //m_metadataProviders.AdapterLoaded += MetadataProviders_AdapterLoaded;
+ //m_metadataProviders.AdapterUnloaded += MetadataProviders_AdapterUnloaded;
+ //m_metadataProviders.AdapterLoadException += AdapterLoader_AdapterLoadException;
+
+ // >> Dispose():
+
+ //if (m_metadataProviders != null)
+ //{
+ // m_metadataProviders.AdapterCreated -= MetadataProviders_AdapterCreated;
+ // m_metadataProviders.AdapterLoaded -= MetadataProviders_AdapterLoaded;
+ // m_metadataProviders.AdapterUnloaded -= MetadataProviders_AdapterUnloaded;
+ // m_metadataProviders.AdapterLoadException -= AdapterLoader_AdapterLoadException;
+ // m_metadataProviders.Dispose();
+ //}
+
+ // >> AttemptConnection():
+
+ //m_metadataProviders.Initialize();
+
+ //int waitCount = 0;
+
+ //// Wait for adapter initialization to complete, up to 2 seconds
+ //while (waitCount < 20 && m_adapterLoadedCount != m_dataServices.Adapters.Count + m_metadataProviders.Adapters.Count + m_replicationProviders.Adapters.Count)
+ //{
+ // Thread.Sleep(100);
+ // waitCount++;
+ //}
+
+ //// Kick off a meta-data refresh...
+ //if (m_autoRefreshMetadata)
+ //{
+ // RefreshMetadata();
+ // m_autoRefreshMetadata = false;
+ //}
+
+ /////
+ ///// Gets or sets a boolean indicating whether or not metadata is
+ ///// refreshed when the adapter attempts to connect to the archive.
+ /////
+ //[ConnectionStringParameter,
+ //Description("Define a boolean indicating whether to refresh metadata on connect."),
+ //DefaultValue(true)]
+ //public bool AutoRefreshMetadata
+ //{
+ // get
+ // {
+ // return m_autoRefreshMetadata;
+ // }
+ // set
+ // {
+ // m_autoRefreshMetadata = value;
+ // }
+ //}
+
+ //private void MetadataProviders_AdapterCreated(object sender, EventArgs e)
+ //{
+ // e.Argument.SettingsCategory = InstanceName + e.Argument.SettingsCategory;
+
+ // if (e.Argument.GetType() == typeof(AdoMetadataProvider))
+ // {
+ // // Populate the default configuration for AdoMetadataProvider.
+ // AdoMetadataProvider provider = e.Argument as AdoMetadataProvider;
+
+ // provider.Enabled = true;
+ // provider.SelectString = string.Format("SELECT * FROM HistorianMetadata WHERE PlantCode='{0}'", Name);
+
+ // // The following connection information is now provided via configuration Eval mappings
+ // // provider.DataProviderString = config.Settings["SystemSettings"]["DataProviderString"].Value;
+
+ // // ConfigurationFile config = ConfigurationFile.Current;
+ // // string connectionString = config.Settings["SystemSettings"]["ConnectionString"].Value;
+ // // Dictionary settings = connectionString.ParseKeyValuePairs();
+ // // string setting;
+
+ // // if (settings.TryGetValue("Provider", out setting))
+ // // {
+ // // // Check if provider is for Access
+ // // if (setting.StartsWith("Microsoft.Jet.OLEDB", StringComparison.OrdinalIgnoreCase))
+ // // {
+ // // // Make sure path to Access database is fully qualified
+ // // if (settings.TryGetValue("Data Source", out setting))
+ // // {
+ // // settings["Data Source"] = FilePath.GetAbsolutePath(setting);
+ // // connectionString = settings.JoinKeyValuePairs();
+ // // }
+ // // }
+ // // }
+
+ // // provider.ConnectionString = connectionString;
+ // }
+ //}
+
+ //private void MetadataProviders_AdapterLoaded(object sender, EventArgs e)
+ //{
+ // e.Argument.Metadata = m_archive.MetadataFile;
+
+ // e.Argument.MetadataRefreshStart += MetadataProviders_MetadataRefreshStart;
+ // e.Argument.MetadataRefreshComplete += MetadataProviders_MetadataRefreshComplete;
+ // e.Argument.MetadataRefreshTimeout += MetadataProviders_MetadataRefreshTimeout;
+ // e.Argument.MetadataRefreshException += MetadataProviders_MetadataRefreshException;
+ // OnStatusMessage("{0} has been loaded.", e.Argument.GetType().Name);
+
+ // m_adapterLoadedCount++;
+ //}
+
+ //private void MetadataProviders_AdapterUnloaded(object sender, EventArgs e)
+ //{
+ // e.Argument.Metadata = null;
+ // e.Argument.MetadataRefreshStart -= MetadataProviders_MetadataRefreshStart;
+ // e.Argument.MetadataRefreshComplete -= MetadataProviders_MetadataRefreshComplete;
+ // e.Argument.MetadataRefreshTimeout -= MetadataProviders_MetadataRefreshTimeout;
+ // e.Argument.MetadataRefreshException -= MetadataProviders_MetadataRefreshException;
+ // OnStatusMessage("{0} has been unloaded.", e.Argument.GetType().Name);
+ //}
+
+ //private void MetadataProviders_MetadataRefreshStart(object sender, EventArgs e)
+ //{
+ // OnStatusMessage("{0} has started metadata refresh...", sender.GetType().Name);
+ //}
+
+ //private void MetadataProviders_MetadataRefreshComplete(object sender, EventArgs e)
+ //{
+ // OnStatusMessage("{0} has finished metadata refresh.", sender.GetType().Name);
+ //}
+
+ //private void MetadataProviders_MetadataRefreshTimeout(object sender, EventArgs e)
+ //{
+ // OnStatusMessage("{0} has timed-out on metadata refresh.", sender.GetType().Name);
+ //}
+
+ //private void MetadataProviders_MetadataRefreshException(object sender, EventArgs e)
+ //{
+ // OnProcessException(e.Argument);
+ //}
+ #endregion
+}
\ No newline at end of file
diff --git a/Source/Libraries/GSF.SortedTreeStore/GSF.SortedTreeStore.SqlClr.csproj b/Source/Libraries/GSF.SortedTreeStore/GSF.SortedTreeStore.SqlClr.csproj
index 9e6d84ea5b..7d2ab2090d 100755
--- a/Source/Libraries/GSF.SortedTreeStore/GSF.SortedTreeStore.SqlClr.csproj
+++ b/Source/Libraries/GSF.SortedTreeStore/GSF.SortedTreeStore.SqlClr.csproj
@@ -28,7 +28,7 @@
1.0.0.%2a
false
true
- 8
+ latest
AnyCPU
@@ -142,11 +142,18 @@
+
+
+
+
+
+
+
diff --git a/Source/Libraries/GSF.SortedTreeStore/GSF.SortedTreeStore.csproj b/Source/Libraries/GSF.SortedTreeStore/GSF.SortedTreeStore.csproj
index db5bb6eee4..0d9f08b072 100755
--- a/Source/Libraries/GSF.SortedTreeStore/GSF.SortedTreeStore.csproj
+++ b/Source/Libraries/GSF.SortedTreeStore/GSF.SortedTreeStore.csproj
@@ -28,7 +28,7 @@
1.0.0.%2a
false
true
- 8
+ latest
AnyCPU
@@ -272,6 +272,10 @@
+
+
+
+
@@ -279,6 +283,9 @@
+
+
+
diff --git a/Source/Libraries/GSF.SortedTreeStore/IO/FileStructure/SimplifiedFileWriter.cs b/Source/Libraries/GSF.SortedTreeStore/IO/FileStructure/SimplifiedFileWriter.cs
index ec6e2fe368..8bd582b5f7 100755
--- a/Source/Libraries/GSF.SortedTreeStore/IO/FileStructure/SimplifiedFileWriter.cs
+++ b/Source/Libraries/GSF.SortedTreeStore/IO/FileStructure/SimplifiedFileWriter.cs
@@ -33,22 +33,18 @@ namespace GSF.IO.FileStructure
/// Assists in the writing of a simplified file. This file can only be appended to
/// and it must be sequentially written.
///
- public class SimplifiedFileWriter
- : IDisposable
+ public class SimplifiedFileWriter : IDisposable
{
private static readonly LogPublisher Log = Logger.CreatePublisher(typeof(SimplifiedFileWriter), MessageClass.Component);
private bool m_disposed;
-
private readonly FileHeaderBlock m_fileHeaderBlock;
-
private SimplifiedSubFileStream m_subFileStream;
-
- public FileStream m_stream;
-
private readonly string m_pendingFileName;
private readonly string m_completeFileName;
+ public FileStream m_stream;
+
///
/// Creates a simplified file writer.
///
@@ -65,21 +61,25 @@ public SimplifiedFileWriter(string pendingFileName, string completeFileName, int
m_stream = new FileStream(pendingFileName, FileMode.CreateNew, FileAccess.ReadWrite, FileShare.None);
}
-#if DEBUG
+ #if DEBUG
~SimplifiedFileWriter()
{
Log.Publish(MessageLevel.Info, "Finalizer Called", GetType().FullName);
}
-#endif
+ #endif
public Guid ArchiveId => m_fileHeaderBlock.ArchiveId;
+ internal uint SnapshotSequenceNumber => m_fileHeaderBlock.SnapshotSequenceNumber;
+
private void CloseCurrentFile()
{
if (m_subFileStream is null)
return;
+
if (!m_subFileStream.IsDisposed)
throw new Exception("The previous file must be disposed before completing this action");
+
m_subFileStream = null;
}
@@ -91,10 +91,13 @@ public ISupportsBinaryStream CreateFile(SubFileName fileName)
{
if (m_disposed)
throw new ObjectDisposedException(GetType().FullName);
+
CloseCurrentFile();
SubFileHeader subFile = m_fileHeaderBlock.CreateNewFile(fileName);
+
subFile.DirectBlock = m_fileHeaderBlock.LastAllocatedBlock + 1;
+
m_subFileStream = new SimplifiedSubFileStream(m_stream, subFile, m_fileHeaderBlock);
return m_subFileStream;
}
@@ -106,15 +109,22 @@ public void Commit()
{
if (m_disposed)
return;
+
CloseCurrentFile();
+
m_stream.Position = 0;
m_stream.Write(m_fileHeaderBlock.GetBytes());
m_stream.Flush(true);
+
WinApi.FlushFileBuffers(m_stream.SafeFileHandle);
+
m_stream.Dispose();
m_stream = null;
+
File.Move(m_pendingFileName, m_completeFileName);
+
m_disposed = true;
+
Dispose();
}
@@ -133,31 +143,28 @@ public void Dispose()
/// true to release both managed and unmanaged resources; false to release only unmanaged resources.
protected virtual void Dispose(bool disposing)
{
- if (!m_disposed)
+ if (m_disposed)
+ return;
+
+ try
{
- try
- {
- // This will be done regardless of whether the object is finalized or disposed.
-
- if (disposing)
- {
- if (m_subFileStream != null)
- {
- m_subFileStream.Dispose();
- m_subFileStream = null;
- }
- if (m_stream != null)
- {
- m_stream.Dispose();
- }
- File.Delete(m_pendingFileName);
- // This will be done only when the object is disposed by calling Dispose().
- }
- }
- finally
+ // This will be done regardless of whether the object is finalized or disposed.
+ if (!disposing)
+ return;
+
+ if (m_subFileStream is not null)
{
- m_disposed = true; // Prevent duplicate dispose.
+ m_subFileStream.Dispose();
+ m_subFileStream = null;
}
+
+ m_stream?.Dispose();
+
+ File.Delete(m_pendingFileName);
+ }
+ finally
+ {
+ m_disposed = true; // Prevent duplicate dispose.
}
}
}
diff --git a/Source/Libraries/GSF.SortedTreeStore/Snap/Filters/NearestTimestampSeekFilter.cs b/Source/Libraries/GSF.SortedTreeStore/Snap/Filters/NearestTimestampSeekFilter.cs
new file mode 100644
index 0000000000..58eab9028a
--- /dev/null
+++ b/Source/Libraries/GSF.SortedTreeStore/Snap/Filters/NearestTimestampSeekFilter.cs
@@ -0,0 +1,259 @@
+//******************************************************************************************************
+// NearestTimestampSeekFilter.cs - Gbtc
+//
+// Copyright © 2026, Grid Protection Alliance. All Rights Reserved.
+//
+// Licensed to the Grid Protection Alliance (GPA) under one or more contributor license agreements. See
+// the NOTICE file distributed with this work for additional information regarding copyright ownership.
+// The GPA licenses this file to you under the MIT License (MIT), the "License"; you may not use this
+// file except in compliance with the License. You may obtain a copy of the License at:
+//
+// http://opensource.org/licenses/MIT
+//
+// Unless agreed to in writing, the subject software distributed under the License is distributed on an
+// "AS-IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. Refer to the
+// License for the specific language governing permissions and limitations.
+//
+// Code Modification History:
+// ----------------------------------------------------------------------------------------------------
+// 09/21/2026 - J. Ritchie Carroll
+// Generated original version of source code.
+//
+//******************************************************************************************************
+
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using GSF.IO;
+using GSF.Snap.Types;
+
+namespace GSF.Snap.Filters;
+
+///
+/// Selects the nearest eligible sample per point ID around regularly spaced target timestamps.
+/// Requires timestamp-first key ordering, then point ID (as used by HistorianKey).
+///
+///
+/// Targets start at firstTime and include lastTime when it falls on the grid. Samples must lie
+/// within the query range and maximumDistance of the target. Equal distances prefer the earlier
+/// timestamp, then the smaller key. Original keys and values are returned without interpolation.
+/// Windows cannot overlap except at their midpoint, which belongs to the earlier target.
+/// Consequently, results are sequential and a source sample is never returned twice. AtOrAfter
+/// mode instead selects the first eligible sample at or after each target, using the same
+/// distance limit. Missing IDs produce no result. Both endpoints must support the selected mode.
+///
+public sealed class NearestTimestampSeekFilter : SeekFilterBase where TKey : SnapTypeBase, new()
+{
+ private readonly ulong m_first;
+ private readonly ulong m_firstTarget;
+ private readonly bool m_singleTarget;
+ private readonly ulong m_last;
+ private readonly ulong m_interval;
+ private readonly ulong m_distance;
+ private readonly ulong[] m_pointIDs;
+ private bool m_started;
+ private bool m_finished;
+
+ ///
+ /// Creates a nearest-sample query. All time quantities are in timestamp ticks.
+ ///
+ /// Inclusive query start and first target.
+ /// Inclusive query end.
+ /// Positive target spacing.
+ /// Explicit set of IDs to sample.
+ /// Search radius, at most half the interval; defaults to half.
+ /// Search both sides, or only at/after each target using the same distance limit.
+ public NearestTimestampSeekFilter(ulong firstTime, ulong lastTime, ulong interval,
+ IEnumerable pointIDs, ulong? maximumDistance = null, TimestampSearchMode searchMode = TimestampSearchMode.Nearest)
+ {
+ if (new TKey() is not TimestampPointIDBase)
+ throw new NotSupportedException("Nearest sampling requires timestamp/point-ID keys.");
+
+ if (firstTime > lastTime)
+ throw new ArgumentOutOfRangeException(nameof(lastTime));
+
+ if (interval == 0)
+ throw new ArgumentOutOfRangeException(nameof(interval));
+
+ if (pointIDs == null)
+ throw new ArgumentNullException(nameof(pointIDs));
+
+ ValidateSearchMode(searchMode);
+
+ SearchMode = searchMode;
+ m_first = firstTime;
+ m_firstTarget = firstTime;
+ m_last = lastTime;
+ m_interval = interval;
+ m_distance = maximumDistance ?? interval / 2;
+
+ if (m_distance > interval / 2)
+ throw new ArgumentOutOfRangeException(nameof(maximumDistance), "Search windows must not overlap.");
+
+ m_pointIDs = pointIDs.Distinct().OrderBy(id => id).ToArray();
+ StartOfRange = new TKey();
+ EndOfRange = new TKey();
+ StartOfFrame = new TKey();
+ EndOfFrame = new TKey();
+
+ SetTimestamp(StartOfRange, firstTime, false);
+ SetTimestamp(EndOfRange, lastTime, true);
+
+ Reset();
+ }
+
+ ///
+ /// Finds one nearest sample for an ID, searching on both sides of a timestamp.
+ ///
+ /// Target timestamp.
+ /// ID to find.
+ /// Maximum distance in timestamp ticks, inclusive.
+ /// Search both sides, or only at/after the target.
+ public NearestTimestampSeekFilter(ulong timestamp, ulong pointID, ulong maximumDistance, TimestampSearchMode searchMode = TimestampSearchMode.Nearest)
+ : this(searchMode == TimestampSearchMode.AtOrAfter ? timestamp : timestamp - Math.Min(timestamp, maximumDistance),
+ timestamp + Math.Min(ulong.MaxValue - timestamp, maximumDistance),
+ ulong.MaxValue, [ pointID ], 0, searchMode)
+ {
+ m_distance = maximumDistance;
+ m_firstTarget = timestamp;
+ m_singleTarget = true;
+ Reset();
+ }
+
+ ///
+ public override Guid FilterType => NearestTimestampSeekFilterDefinition.FilterGuid;
+
+ /// The current target timestamp after NextWindow succeeds.
+ public ulong TargetTimestamp { get; private set; }
+
+ /// Gets whether samples are selected on both sides or only at/after the target.
+ public TimestampSearchMode SearchMode { get; }
+
+ internal IEnumerable PointIDs => m_pointIDs;
+
+ internal static TimestampPointIDBase TimeKey(TKey key) => (TimestampPointIDBase)(object)key;
+
+ internal static void SetTimestamp(TKey key, ulong timestamp, bool maximum)
+ {
+ if (maximum)
+ key.SetMax();
+ else
+ key.SetMin();
+
+ TimeKey(key).Timestamp = timestamp;
+ }
+
+ ///
+ public override void Reset()
+ {
+ m_started = false;
+ m_finished = false;
+ TargetTimestamp = m_firstTarget;
+ StartOfRange.CopyTo(StartOfFrame);
+ EndOfRange.CopyTo(EndOfFrame);
+ }
+
+ ///
+ public override bool NextWindow()
+ {
+ if (m_finished || m_pointIDs.Length == 0)
+ return false;
+
+ if (m_started)
+ {
+ if (m_singleTarget || m_last - TargetTimestamp < m_interval)
+ {
+ m_finished = true;
+ return false;
+ }
+
+ TargetTimestamp += m_interval;
+ }
+
+ ulong lower = SearchMode == TimestampSearchMode.AtOrAfter ? TargetTimestamp : TargetTimestamp - Math.Min(m_distance, TargetTimestamp - m_first);
+ ulong upper = TargetTimestamp + Math.Min(m_distance, m_last - TargetTimestamp);
+
+ // When the interval is even, the shared midpoint belongs to the previous target.
+ if (SearchMode == TimestampSearchMode.Nearest && m_started && m_distance == m_interval - m_distance)
+ lower++;
+
+ SetTimestamp(StartOfFrame, lower, false);
+ SetTimestamp(EndOfFrame, upper, true);
+
+ m_started = true;
+
+ return true;
+ }
+
+ ///
+ public override void Save(BinaryStreamBase stream)
+ {
+ // Keep the original payload for nearest queries; older readers reject the new version
+ // for directional queries rather than silently changing their meaning.
+ stream.Write((byte)(SearchMode == TimestampSearchMode.Nearest ? 1 : 2));
+ stream.Write((byte)(m_singleTarget ? 1 : 0));
+
+ if (SearchMode != TimestampSearchMode.Nearest)
+ stream.Write((byte)SearchMode);
+
+ if (m_singleTarget)
+ {
+ stream.Write(m_firstTarget);
+ stream.Write(m_pointIDs[0]);
+ stream.Write(m_distance);
+ return;
+ }
+
+ stream.Write(m_first);
+ stream.Write(m_last);
+ stream.Write(m_interval);
+ stream.Write(m_distance);
+ stream.Write(m_pointIDs.Length);
+
+ foreach (ulong id in m_pointIDs)
+ stream.Write(id);
+ }
+
+ private static void ValidateSearchMode(TimestampSearchMode searchMode)
+ {
+ if (searchMode != TimestampSearchMode.Nearest && searchMode != TimestampSearchMode.AtOrAfter)
+ throw new ArgumentOutOfRangeException(nameof(searchMode));
+ }
+ internal static NearestTimestampSeekFilter Load(BinaryStreamBase stream)
+ {
+ byte version = stream.ReadUInt8();
+
+ if (version != 1 && version != 2)
+ throw new InvalidDataException("Unknown nearest-timestamp filter version.");
+
+ byte mode = stream.ReadUInt8();
+ TimestampSearchMode searchMode = version == 1 ? TimestampSearchMode.Nearest : (TimestampSearchMode)stream.ReadUInt8();
+
+ if (searchMode != TimestampSearchMode.Nearest && searchMode != TimestampSearchMode.AtOrAfter)
+ throw new InvalidDataException("Unknown timestamp search mode.");
+
+ if (mode == 1)
+ return new NearestTimestampSeekFilter(stream.ReadUInt64(), stream.ReadUInt64(), stream.ReadUInt64(), searchMode);
+
+ if (mode != 0)
+ throw new InvalidDataException("Unknown nearest-timestamp filter mode.");
+
+ ulong first = stream.ReadUInt64();
+ ulong last = stream.ReadUInt64();
+ ulong interval = stream.ReadUInt64();
+ ulong distance = stream.ReadUInt64();
+ int count = stream.ReadInt32();
+
+ if (count < 0)
+ throw new InvalidDataException("Negative point-ID count.");
+
+ // Read incrementally: do not allocate an attacker-controlled array before reading payload.
+ List ids = [];
+
+ for (int i = 0; i < count; i++)
+ ids.Add(stream.ReadUInt64());
+
+ return new NearestTimestampSeekFilter(first, last, interval, ids, distance, searchMode);
+ }
+}
\ No newline at end of file
diff --git a/Source/Libraries/GSF.SortedTreeStore/Snap/Filters/NearestTimestampSeekFilterDefinition.cs b/Source/Libraries/GSF.SortedTreeStore/Snap/Filters/NearestTimestampSeekFilterDefinition.cs
new file mode 100644
index 0000000000..d0f5d43211
--- /dev/null
+++ b/Source/Libraries/GSF.SortedTreeStore/Snap/Filters/NearestTimestampSeekFilterDefinition.cs
@@ -0,0 +1,55 @@
+//******************************************************************************************************
+// NearestTimestampSeekFilterDefinition.cs - Gbtc
+//
+// Copyright © 2026, Grid Protection Alliance. All Rights Reserved.
+//
+// Licensed to the Grid Protection Alliance (GPA) under one or more contributor license agreements. See
+// the NOTICE file distributed with this work for additional information regarding copyright ownership.
+// The GPA licenses this file to you under the MIT License (MIT), the "License"; you may not use this
+// file except in compliance with the License. You may obtain a copy of the License at:
+//
+// http://opensource.org/licenses/MIT
+//
+// Unless agreed to in writing, the subject software distributed under the License is distributed on an
+// "AS-IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. Refer to the
+// License for the specific language governing permissions and limitations.
+//
+// Code Modification History:
+// ----------------------------------------------------------------------------------------------------
+// 09/21/2026 - J. Ritchie Carroll
+// Generated original version of source code.
+//
+//******************************************************************************************************
+
+using System;
+using System.Reflection;
+using GSF.IO;
+using GSF.Snap.Definitions;
+
+namespace GSF.Snap.Filters;
+
+///
+/// Wire definition for nearest timestamp sampling.
+///
+public sealed class NearestTimestampSeekFilterDefinition : SeekFilterDefinitionBase
+{
+ ///
+ /// Stable identifier for the nearest-sample wire format.
+ ///
+ public static readonly Guid FilterGuid = new("f906dfb8-692b-4a28-98ed-ff78ae74e4b5");
+
+ ///
+ public override Guid FilterType => FilterGuid;
+
+ ///
+ public override SeekFilterBase Create(BinaryStreamBase stream)
+ {
+ Type filterType = typeof(NearestTimestampSeekFilter<>).MakeGenericType(typeof(TKey));
+
+ MethodInfo method = filterType.GetMethod("Load", BindingFlags.Static | BindingFlags.NonPublic) ??
+ throw new InvalidOperationException("Nearest filter loader is missing.");
+
+ return (SeekFilterBase)(method.Invoke(null, [stream]) ??
+ throw new InvalidOperationException("Nearest filter loader returned no filter."));
+ }
+}
\ No newline at end of file
diff --git a/Source/Libraries/GSF.SortedTreeStore/Snap/Filters/TimestampPointIDSeekFilter.cs b/Source/Libraries/GSF.SortedTreeStore/Snap/Filters/TimestampPointIDSeekFilter.cs
index 823c6c3622..a14f0a4973 100755
--- a/Source/Libraries/GSF.SortedTreeStore/Snap/Filters/TimestampPointIDSeekFilter.cs
+++ b/Source/Libraries/GSF.SortedTreeStore/Snap/Filters/TimestampPointIDSeekFilter.cs
@@ -26,112 +26,126 @@
using GSF.IO;
using GSF.Snap.Types;
-namespace GSF.Snap.Filters
+namespace GSF.Snap.Filters;
+
+///
+/// Represents a seek filter for a specific timestamp and point ID.
+///
+public static class TimestampPointIDSeekFilter
{
///
- /// Represents a seek filter for a specific timestamp and point ID.
+ /// Creates a filter to find the nearest key for the specified timestamp and point ID within the specified maximum distance.
+ ///
+ /// The specific timestamp to find.
+ /// The specific point ID to find.
+ /// The maximum distance to search for the nearest key.
+ /// The mode to use when searching for the nearest key.
+ /// Seek filter to find specific key.
+ public static SeekFilterBase FindNearestKey(ulong timestamp, ulong pointID, ulong maximumDistance,
+ TimestampSearchMode searchMode = TimestampSearchMode.Nearest)
+ where TKey : TimestampPointIDBase, new()
+ {
+ return new NearestTimestampSeekFilter(timestamp, pointID, maximumDistance, searchMode);
+ }
+
+ ///
+ /// Creates a filter for the specified timestamp and point ID.
///
- public static class TimestampPointIDSeekFilter
+ /// The specific timestamp to find.
+ /// The specific point ID to find.
+ /// Seek filter to find specific key.
+ public static SeekFilterBase FindKey(ulong timestamp, ulong pointID)
+ where TKey : TimestampPointIDBase, new()
{
+ return new SeekToKey(timestamp, pointID);
+ }
+
+ ///
+ /// Loads a from the provided .
+ ///
+ /// The stream to load the filter from
+ /// Seek filter to find specific key.
+ [MethodImpl(MethodImplOptions.NoOptimization)]
+ private static SeekFilterBase CreateFromStream(BinaryStreamBase stream)
+ where TKey : TimestampPointIDBase, new()
+ {
+ return new SeekToKey(stream);
+ }
+
+ private class SeekToKey
+ : SeekFilterBase
+ where TKey : TimestampPointIDBase, new()
+ {
+ private readonly TKey m_keyToFind;
+ private bool m_isEndReached;
+
+ private SeekToKey()
+ {
+ m_keyToFind = new TKey();
+ StartOfFrame = new TKey();
+ EndOfFrame = new TKey();
+ StartOfRange = StartOfFrame;
+ EndOfRange = EndOfFrame;
+ }
+
///
- /// Creates a filter for the specified timestamp and point ID.
+ /// Creates a filter by reading from the stream.
///
- /// The specific timestamp to find.
- /// The specific point ID to find.
- /// Seek filter to find specific key.
- public static SeekFilterBase FindKey(ulong timestamp, ulong pointID)
- where TKey : TimestampPointIDBase, new()
+ /// the stream to read from
+ public SeekToKey(BinaryStreamBase stream)
+ : this()
{
- return new SeekToKey(timestamp, pointID);
+ m_keyToFind.Timestamp = stream.ReadUInt64();
+ m_keyToFind.PointID = stream.ReadUInt64();
+ m_keyToFind.CopyTo(StartOfRange);
+ m_keyToFind.CopyTo(EndOfRange);
}
///
- /// Loads a from the provided .
+ /// Creates a filter for the key.
///
- /// The stream to load the filter from
- /// Seek filter to find specific key.
- [MethodImpl(MethodImplOptions.NoOptimization)]
- private static SeekFilterBase CreateFromStream(BinaryStreamBase stream)
- where TKey : TimestampPointIDBase, new()
+ /// The specific timestamp to find.
+ /// The specific point ID to find.
+ public SeekToKey(ulong timestamp, ulong pointID)
+ : this()
{
- return new SeekToKey(stream);
+ m_keyToFind.Timestamp = timestamp;
+ m_keyToFind.PointID = pointID;
+ m_keyToFind.CopyTo(StartOfRange);
+ m_keyToFind.CopyTo(EndOfRange);
}
- private class SeekToKey
- : SeekFilterBase
- where TKey : TimestampPointIDBase, new()
+ ///
+ /// Gets the next search window.
+ ///
+ /// true if window exists, false if finished.
+ public override bool NextWindow()
{
- private readonly TKey m_keyToFind;
- private bool m_isEndReached;
-
- private SeekToKey()
- {
- m_keyToFind = new TKey();
- StartOfFrame = new TKey();
- EndOfFrame = new TKey();
- StartOfRange = StartOfFrame;
- EndOfRange = EndOfFrame;
- }
-
- ///
- /// Creates a filter by reading from the stream.
- ///
- /// the stream to read from
- public SeekToKey(BinaryStreamBase stream)
- : this()
- {
- m_keyToFind.Timestamp = stream.ReadUInt64();
- m_keyToFind.PointID = stream.ReadUInt64();
- m_keyToFind.CopyTo(StartOfRange);
- m_keyToFind.CopyTo(EndOfRange);
- }
-
- ///
- /// Creates a filter for the key.
- ///
- /// The specific timestamp to find.
- /// The specific point ID to find.
- public SeekToKey(ulong timestamp, ulong pointID)
- : this()
- {
- m_keyToFind.Timestamp = timestamp;
- m_keyToFind.PointID = pointID;
- m_keyToFind.CopyTo(StartOfRange);
- m_keyToFind.CopyTo(EndOfRange);
- }
-
- ///
- /// Gets the next search window.
- ///
- /// true if window exists, false if finished.
- public override bool NextWindow()
- {
- if (m_isEndReached)
- return false;
-
- m_isEndReached = true;
- return true;
- }
+ if (m_isEndReached)
+ return false;
- ///
- /// Resets the iterative nature of the filter.
- ///
- public override void Reset()
- {
- m_isEndReached = false;
- }
+ m_isEndReached = true;
+ return true;
+ }
- ///
- /// Serializes the filter to a stream
- ///
- /// the stream to write to
- public override void Save(BinaryStreamBase stream)
- {
- stream.Write(m_keyToFind.Timestamp);
- stream.Write(m_keyToFind.PointID);
- }
+ ///
+ /// Resets the iterative nature of the filter.
+ ///
+ public override void Reset()
+ {
+ m_isEndReached = false;
+ }
- public override Guid FilterType => TimestampPointIDSeekFilterDefinition.FilterGuid;
+ ///
+ /// Serializes the filter to a stream
+ ///
+ /// the stream to write to
+ public override void Save(BinaryStreamBase stream)
+ {
+ stream.Write(m_keyToFind.Timestamp);
+ stream.Write(m_keyToFind.PointID);
}
+
+ public override Guid FilterType => TimestampPointIDSeekFilterDefinition.FilterGuid;
}
-}
+}
\ No newline at end of file
diff --git a/Source/Libraries/GSF.SortedTreeStore/Snap/Filters/TimestampSearchMode.cs b/Source/Libraries/GSF.SortedTreeStore/Snap/Filters/TimestampSearchMode.cs
new file mode 100644
index 0000000000..ec6282ce86
--- /dev/null
+++ b/Source/Libraries/GSF.SortedTreeStore/Snap/Filters/TimestampSearchMode.cs
@@ -0,0 +1,34 @@
+//******************************************************************************************************
+// TimestampSearchMode.cs - Gbtc
+//
+// Copyright © 2026, Grid Protection Alliance. All Rights Reserved.
+//
+// Licensed to the Grid Protection Alliance (GPA) under one or more contributor license agreements. See
+// the NOTICE file distributed with this work for additional information regarding copyright ownership.
+// The GPA licenses this file to you under the MIT License (MIT), the "License"; you may not use this
+// file except in compliance with the License. You may obtain a copy of the License at:
+//
+// http://opensource.org/licenses/MIT
+//
+// Unless agreed to in writing, the subject software distributed under the License is distributed on an
+// "AS-IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. Refer to the
+// License for the specific language governing permissions and limitations.
+//
+// Code Modification History:
+// ----------------------------------------------------------------------------------------------------
+// 09/21/2026 - J. Ritchie Carroll
+// Generated original version of source code.
+//
+//******************************************************************************************************
+
+namespace GSF.Snap.Filters;
+
+/// Controls which side of a target timestamp is searched for a sample.
+public enum TimestampSearchMode : byte
+{
+ /// Search both sides and choose the closest eligible sample, preferring the earlier timestamp on ties.
+ Nearest = 0,
+
+ /// Choose the first eligible sample at or after the target; never return a preceding sample.
+ AtOrAfter = 1
+}
\ No newline at end of file
diff --git a/Source/Libraries/GSF.SortedTreeStore/Snap/Filters/TimestampSeekFilter.cs b/Source/Libraries/GSF.SortedTreeStore/Snap/Filters/TimestampSeekFilter.cs
index ec4d5ee6b6..377c53ef7a 100755
--- a/Source/Libraries/GSF.SortedTreeStore/Snap/Filters/TimestampSeekFilter.cs
+++ b/Source/Libraries/GSF.SortedTreeStore/Snap/Filters/TimestampSeekFilter.cs
@@ -27,136 +27,162 @@
using GSF.IO;
using GSF.Snap.Types;
-namespace GSF.Snap.Filters
+namespace GSF.Snap.Filters;
+
+public partial class TimestampSeekFilter
{
- public partial class TimestampSeekFilter
+ ///
+ /// Creates a nearest-sample filter over a set of date ranges
+ ///
+ /// the first time if the query (inclusive)
+ /// the last time of the query (inclusive)
+ /// the interval between samples
+ /// the point IDs to include in the filter
+ /// the optional maximum distance to search for the nearest key
+ /// the mode to use when searching for the nearest key
+ /// A seek filter to find the nearest key for the specified interval data.
+ ///
+ /// Thrown when the interval is less than or equal to zero or the maximumDistance is less than zero.
+ ///
+ public static SeekFilterBase CreateNearestFromIntervalData(DateTime firstTime, DateTime lastTime,
+ TimeSpan interval, System.Collections.Generic.IEnumerable pointIDs, TimeSpan? maximumDistance = null,
+ TimestampSearchMode searchMode = TimestampSearchMode.Nearest)
+ where TKey : TimestampPointIDBase, new()
{
+ if (interval.Ticks <= 0)
+ throw new ArgumentOutOfRangeException(nameof(interval));
+
+ if (maximumDistance is { Ticks: < 0 })
+ throw new ArgumentOutOfRangeException(nameof(maximumDistance));
- ///
- /// Creates a filter over a single date range. (Inclusive list)
- ///
- /// the first time if the query (inclusive)
- /// the last time of the query (inclusive)
- ///
- public static SeekFilterBase CreateFromRange(DateTime firstTime, DateTime lastTime)
- where TKey : TimestampPointIDBase, new()
- {
- return new FixedRange((ulong)firstTime.Ticks, (ulong)lastTime.Ticks);
- }
-
- ///
- /// Creates a filter over a single date range. (Inclusive list)
- ///
- /// the first time if the query (inclusive)
- /// the last time of the query (inclusive)
- ///
- public static SeekFilterBase CreateFromRange(ulong firstTime, ulong lastTime)
- where TKey : TimestampPointIDBase, new()
- {
- return new FixedRange(firstTime, lastTime);
- }
+ return new NearestTimestampSeekFilter(
+ (ulong)firstTime.Ticks,
+ (ulong)lastTime.Ticks,
+ (ulong)interval.Ticks,
+ pointIDs,
+ maximumDistance is null ? null : (ulong?)maximumDistance.Value.Ticks,
+ searchMode);
+ }
- ///
- /// Creates a filter over a set of date ranges (Similar to down sampled queries)
- ///
- /// the first time if the query (inclusive)
- /// the last time of the query (inclusive if contained in the intervals)
- /// the smallest interval that is exact
- /// the interval that will be parsed. Possible to be rounded
- /// the width of every window
- /// A that will be able to do this parsing
- ///
- /// Example uses. FirstTime = 1/1/2013. LastTime = 1/2/2013.
- /// MainInterval = 0.1 seconds. SubInterval = 0.0333333 seconds.
- /// Tolerance = 0.001 seconds.
- ///
- public static SeekFilterBase CreateFromIntervalData(ulong firstTime, ulong lastTime, ulong mainInterval, ulong subInterval, ulong tolerance)
- where TKey : TimestampPointIDBase, new()
- {
- return new IntervalRanges(firstTime, lastTime, mainInterval, subInterval, tolerance);
- }
+ ///
+ /// Creates a filter over a single date range. (Inclusive list)
+ ///
+ /// the first time if the query (inclusive)
+ /// the last time of the query (inclusive)
+ ///
+ public static SeekFilterBase CreateFromRange(DateTime firstTime, DateTime lastTime)
+ where TKey : TimestampPointIDBase, new()
+ {
+ return new FixedRange((ulong)firstTime.Ticks, (ulong)lastTime.Ticks);
+ }
- ///