//****************************************************************************************************** // IndependentActionAdapterManagerBase.cs - Gbtc // // Copyright © 2020, 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: // ---------------------------------------------------------------------------------------------------- // 02/13/2020 - J. Ritchie Carroll // Generated original version of source code. // //****************************************************************************************************** using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.ComponentModel; using System.Data; using System.Linq; using System.Reflection; using System.Text; using System.Threading; using GSF.Data; using GSF.Data.Model; using GSF.Diagnostics; using GSF.Reflection; using GSF.Threading; using GSF.TimeSeries.Data; using GSF.Units.EE; using MeasurementRecord = GSF.TimeSeries.Model.Measurement; using DeviceRecord = GSF.TimeSeries.Model.Device; using static GSF.TimeSeries.Adapters.IndependentAdapterManagerExtensions; namespace GSF.TimeSeries.Adapters { /// /// Represents an adapter base class that provides functionality to manage and distribute measurements to a collection of action adapters. /// public abstract class IndependentActionAdapterManagerBase : ActionAdapterCollection, IIndependentAdapterManager where TAdapter : IAdapter, new() { #region [ Members ] // Constants /// /// Defines the default value for the . /// public const int DefaultFramesPerSecond = 30; /// /// Defines the default value for the . /// public const double DefaultLagTime = 5.0D; /// /// Defines the default value for the . /// public const double DefaultLeadTime = 5.0D; // Fields private readonly LongSynchronizedOperation m_parseConnectionString; private bool m_disposed; #endregion #region [ Constructors ] /// /// Creates a new . /// protected IndependentActionAdapterManagerBase() { this.HandleConstruct(); // Define a synchronized operation to manage parsing connection string operations sequentially m_parseConnectionString = new LongSynchronizedOperation(ParseConnectionString, ex => OnProcessException(MessageLevel.Warning, ex, "ParseConnectionString")) { IsBackground = true }; } #endregion #region [ Properties ] /// /// Gets or sets primary keys of input measurements for the . /// [ConnectionStringParameter] [Description("Defines primary keys of input measurements the adapter expects; can be one of a filter expression, measurement key, point tag or Guid.")] [CustomConfigurationEditor("GSF.TimeSeries.UI.WPF.dll", "GSF.TimeSeries.UI.Editors.MeasurementEditor")] [DefaultValue(null)] public override MeasurementKey[] InputMeasurementKeys { get => base.InputMeasurementKeys; set { base.InputMeasurementKeys = value; InputMeasurementKeyTypes = DataSource.GetSignalTypes(value, SourceMeasurementTable); } } /// /// Gets or sets output measurements that the will produce, if any. /// [ConnectionStringParameter] [Description("Defines primary keys of output measurements the adapter expects; can be one of a filter expression, measurement key, point tag or Guid.")] [CustomConfigurationEditor("GSF.TimeSeries.UI.WPF.dll", "GSF.TimeSeries.UI.Editors.MeasurementEditor")] [DefaultValue(null)] public override IMeasurement[] OutputMeasurements { get => base.OutputMeasurements; set { base.OutputMeasurements = value; OutputMeasurementTypes = DataSource.GetSignalTypes(value, SourceMeasurementTable); } } /// /// Gets or sets the number of frames per second applied to each adapter. /// /// /// Valid frame rates for a are greater than 0 frames per second. /// [ConnectionStringParameter] [Description("Defines the number of frames per second applied to each adapter.")] [DefaultValue(DefaultFramesPerSecond)] public virtual int FramesPerSecond { get; set; } = DefaultFramesPerSecond; /// /// Gets or sets the allowed past time deviation tolerance, in seconds (can be sub-second) applied to each adapter. /// /// /// Defines the time sensitivity to past measurement timestamps. /// The number of seconds allowed before assuming a measurement timestamp is too old. /// This becomes the amount of delay introduced by the concentrator to allow time for data to flow into the system. /// [ConnectionStringParameter] [Description("Defines the allowed past time deviation tolerance, in seconds (can be sub-second) applied to each adapter.")] [DefaultValue(DefaultLagTime)] public virtual double LagTime { get; set; } = DefaultLagTime; /// /// Gets or sets the allowed future time deviation tolerance, in seconds (can be sub-second) applied to each adapter. /// /// /// Defines the time sensitivity to future measurement timestamps. /// The number of seconds allowed before assuming a measurement timestamp is too advanced. /// This becomes the tolerated +/- accuracy of the local clock to real-time. /// [ConnectionStringParameter] [Description("Defines the allowed future time deviation tolerance, in seconds (can be sub-second) applied to each adapter.")] [DefaultValue(DefaultLeadTime)] public virtual double LeadTime { get; set; } = DefaultLeadTime; /// /// Gets or sets the wait timeout, in milliseconds, that system wait for system configuration reload to complete. /// [ConnectionStringParameter] [Description("Defines the wait timeout, in milliseconds, that system wait for system configuration reload to complete.")] [DefaultValue(DefaultConfigurationReloadWaitTimeout)] public int ConfigurationReloadWaitTimeout { get; set; } = DefaultConfigurationReloadWaitTimeout; /// /// Gets or sets the total number of attempts to wait for system configuration reloads when waiting for configuration updates to be available. /// [ConnectionStringParameter] [Description("Defines the total number of attempts to wait for system configuration reloads when waiting for configuration updates to be available.")] [DefaultValue(DefaultConfigurationReloadWaitAttempts)] public virtual int ConfigurationReloadWaitAttempts { get; set; } = DefaultConfigurationReloadWaitAttempts; /// /// Gets or sets the connection string used for database operations. Leave blank to use local configuration database defined in "systemSettings". /// [ConnectionStringParameter] [Description("Defines the connection string used for database operations. Leave blank to use local configuration database defined in \"systemSettings\".")] [DefaultValue(DefaultDatabaseConnectionString)] public virtual string DatabaseConnectionString { get; set; } /// /// Gets or sets the provider string used for database operations. Defaults to a SQL Server provider string. /// [ConnectionStringParameter] [Description("Defines the provider string used for database operations. Defaults to a SQL Server provider string.")] [DefaultValue(DefaultDatabaseProviderString)] public virtual string DatabaseProviderString { get; set; } /// /// Gets or sets template for output measurement point tag names. /// [ConnectionStringParameter] [Description("Defines template for output measurement point tag names, typically an expression like \"" + DefaultPointTagTemplate + "\" where \"{0}\" is substituted with this adapter name, a dash and then the PerAdapterOutputNames value for the current measurement. Note that \"{0}\" token is not required, property can be overridden to provide desired value.")] [DefaultValue(DefaultPointTagTemplate)] public virtual string PointTagTemplate { get; set; } = DefaultPointTagTemplate; /// /// Gets or sets template for output measurement alternate tag names. /// [ConnectionStringParameter] [Description("Defines template for output measurement alternate tag names, typically an expression where \"{0}\" is substituted with this adapter name, a dash and then the PerAdapterOutputNames value for the current measurement. Note that \"{0}\" token is not required, property can be overridden to provide desired value.")] [DefaultValue(DefaultAlternateTagTemplate)] public virtual string AlternateTagTemplate { get; set; } = DefaultAlternateTagTemplate; /// /// Gets or sets template for output measurement signal reference names. /// [ConnectionStringParameter] [Description("Defines template for output measurement signal reference names, typically an expression like \"" + DefaultSignalReferenceTemplate + "\" where \"{0}\" is substituted with this adapter name, a dash and then the PerAdapterOutputNames value for the current measurement. Note that \"{0}\" token is not required, property can be overridden to provide desired value.")] [DefaultValue(DefaultSignalReferenceTemplate)] public virtual string SignalReferenceTemplate { get; set; } = DefaultSignalReferenceTemplate; /// /// Gets or sets template for output measurement descriptions. /// [ConnectionStringParameter] [Description("Defines template for output measurement descriptions, typically an expression like \"" + DefaultDescriptionTemplate + "\".")] [DefaultValue(DefaultDescriptionTemplate)] public virtual string DescriptionTemplate { get; set; } = DefaultDescriptionTemplate; /// /// Gets or sets template for the parent device acronym used to group associated output measurements. /// [ConnectionStringParameter] [Description("Defines template for the parent device acronym used to group associated output measurements, typically an expression like \"" + DefaultParentDeviceAcronymTemplate + "\" where \"{0}\" is substituted with this adapter name. Set to blank value to create no parent device associated output measurements. Note that \"{0}\" token is not required, you can simply use a specific device acronym.")] [DefaultValue(DefaultParentDeviceAcronymTemplate)] public virtual string ParentDeviceAcronymTemplate { get; set; } = DefaultParentDeviceAcronymTemplate; /// /// Gets or sets default signal type to use for all output measurements when array is not defined. /// [ConnectionStringParameter] [Description("Defines the default signal type to use for all output measurements. Used when per output measurement SignalTypes array is not defined.")] [DefaultValue(typeof(SignalType), DefaultSignalType)] public virtual SignalType SignalType { get; set; } = (SignalType)Enum.Parse(typeof(SignalType), DefaultSignalType); /// /// Gets per adapter signal type for output measurements, used when each output needs to be a different type. /// public virtual SignalType[] SignalTypes { get; } = null; /// /// Gets any custom adapter settings to be added to each adapter connection string. Can be used to add /// settings that are custom per adapter. /// public virtual string CustomAdapterSettings { get; } = null; /// /// Gets or sets the target historian acronym for output measurements. /// [ConnectionStringParameter] [Description("Defines the target historian acronym for output measurements.")] [DefaultValue(DefaultTargetHistorianAcronym)] public virtual string TargetHistorianAcronym { get; set; } = DefaultTargetHistorianAcronym; /// /// Gets or sets the source measurement table to use for configuration. /// [ConnectionStringParameter] [Description("Defines the source measurement table to use for configuration.")] [DefaultValue(DefaultSourceMeasurementTable)] public virtual string SourceMeasurementTable { get; set; } = DefaultSourceMeasurementTable; /// /// Gets or sets based data source used to load each . Updates /// to this property will cascade to all adapters in this . /// public override DataSet DataSource { get => base.DataSource; set { if (!this.DataSourceChanged(value)) return; base.DataSource = value; this.HandleUpdateDataSource(); } } /// /// Gets number of input measurement required by each adapter. /// public abstract int PerAdapterInputCount { get; } /// /// Gets or sets the index into the per adapter input measurements to use for target adapter name. /// [ConnectionStringParameter] [Description("Defines the index into the per adapter input measurements to use for target adapter name.")] public virtual int InputMeasurementIndexUsedForName { get; set; } /// /// Gets output measurement names to use for each adapter. /// public abstract ReadOnlyCollection PerAdapterOutputNames { get; } /// /// Gets or sets flag that determines if the adapter /// should be automatically parsed every time the /// is updated without requiring adapter to be reinitialized. Defaults to true /// to allow child adapters to come and go based on updates to system configuration. /// /// /// When this value is true (the default), implementors must call the /// method an overridden method. /// protected virtual bool AutoReparseConnectionString { get; set; } = true; /// /// Gets input measurement 's for each of the , if any. /// public virtual SignalType[] InputMeasurementKeyTypes { get; private set; } /// /// Gets output measurement 's for each of the , if any. /// public virtual SignalType[] OutputMeasurementTypes { get; private set; } /// /// Gets or sets current adapter ID counter. /// public uint AdapterIDCounter { get; set; } /// /// Gets adapter index currently being processed. /// public int CurrentAdapterIndex { get; protected set; } /// /// Gets adapter output index currently being processed. /// public int CurrentOutputIndex { get; protected set; } /// /// Gets associated device ID for , if any, for measurement generation. If overridden to provide custom /// device ID, should be set to null so no parent device is created. /// public virtual int CurrentDeviceID { get; } = 0; /// /// Returns the detailed status of the . /// public override string Status { get { StringBuilder status = new StringBuilder(); status.AppendLine($" Frames Per Second: {FramesPerSecond:N0}"); status.AppendLine($" Lag Time / Lead Time: {LagTime:N3} / {LeadTime:N3}"); status.Append(this.HandleStatus()); status.Append(base.Status); return status.ToString(); } } #endregion #region [ Methods ] /// /// Releases the unmanaged resources used by the object 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; try { if (!disposing) return; this.HandleDispose(); } finally { m_disposed = true; // Prevent duplicate dispose. base.Dispose(disposing); // Call base class Dispose(). } } /// /// Initializes the . /// public override void Initialize() => this.HandleInitialize(); /// /// Initializes management operations for child adapters based on inputs. Derived classes should override /// and call this method when is /// true (the default). /// /// Input measurement keys to use for initialization. /// /// When is true (the default), implementors must override /// the method and at least call this method. /// protected virtual void InitializeChildAdapterManagement(MeasurementKey[] inputMeasurementKeys) { try { // If no inputs are defined, skip setup if (inputMeasurementKeys.Length == 0) { OnStatusMessage(MessageLevel.Warning, "No inputs were configured. Cannot initialize child adapters."); return; } ReadOnlyCollection perAdapterOutputNames = PerAdapterOutputNames; int inputsPerAdapter = PerAdapterInputCount; int outputsPerAdapter = perAdapterOutputNames?.Count ?? 0; if (perAdapterOutputNames is null || inputsPerAdapter <= 0 || outputsPerAdapter <= 0) { OnStatusMessage(MessageLevel.Warning, "No PerAdapterInputCount or PerAdapterOutputNames where defined. Cannot initialize child adapters."); return; } int nameIndex = InputMeasurementIndexUsedForName; int? currentDeviceID = null; // Create associated parent device for output measurements if parent device acronym template is defined if (!string.IsNullOrWhiteSpace(ParentDeviceAcronymTemplate)) { if (CurrentDeviceID > 0) OnStatusMessage(MessageLevel.Warning, $"WARNING: Creating a parent device for \"{Name}\" [{GetType().Name}] based on specified template \"{ParentDeviceAcronymTemplate}\", but overridden CurrentDeviceID property reports non-zero value: {CurrentDeviceID:N0}"); using (AdoDataConnection connection = GetConfiguredConnection()) { TableOperations deviceTable = new TableOperations(connection); string deviceAcronym = string.Format(ParentDeviceAcronymTemplate, Name); DeviceRecord device = deviceTable.QueryRecordWhere("Acronym = {0}", deviceAcronym) ?? deviceTable.NewRecord(); int protocolID = connection.ExecuteScalar("SELECT ID FROM Protocol WHERE Acronym = 'VirtualInput'") ?? 15; device.Acronym = deviceAcronym; device.Name = deviceAcronym; device.ProtocolID = protocolID; device.Enabled = true; deviceTable.AddNewOrUpdateRecord(device); currentDeviceID = deviceTable.QueryRecordWhere("Acronym = {0}", deviceAcronym)?.ID; } } HashSet activeAdapterNames = new HashSet(StringComparer.Ordinal); List adapters = new List(); HashSet signalIDs = new HashSet(); // Create settings dictionary for connection string to use with primary child adapters Dictionary settings = new Dictionary(StringComparer.OrdinalIgnoreCase); foreach (PropertyInfo property in GetType().GetProperties()) { if (property.AttributeExists()) settings[property.Name] = $"{property.GetValue(this)}"; } // Create child adapter for provided inputs to the parent bulk collection-based adapter for (int i = 0; i < inputMeasurementKeys.Length; i += inputsPerAdapter) { CurrentAdapterIndex = adapters.Count; Guid[] inputs = new Guid[inputsPerAdapter]; Guid[] outputs = new Guid[outputsPerAdapter]; // Adapter inputs are presumed to be grouped together for (int j = 0; j < inputsPerAdapter && i + j < inputMeasurementKeys.Length; j++) inputs[j] = inputMeasurementKeys[i + j].SignalID; string inputName = this.LookupPointTag(inputs[nameIndex], SourceMeasurementTable); string adapterName = $"{Name}!{inputName}"; string alternateTagPrefix = null; if (!string.IsNullOrWhiteSpace(AlternateTagTemplate)) { string deviceName = this.LookupDevice(inputs[nameIndex], SourceMeasurementTable); string phasorLabel = this.LookupPhasorLabel(inputs[nameIndex], SourceMeasurementTable); alternateTagPrefix = $"{deviceName}-{phasorLabel}"; } // Track active adapter names so that adapters that no longer have sources can be removed activeAdapterNames.Add(adapterName); // See if child adapter already exists if (this.FindAdapter(adapterName) != null) continue; // Setup output measurements for new child adapter for (int j = 0; j < outputsPerAdapter; j++) { CurrentOutputIndex = j; string perAdapterOutputName = perAdapterOutputNames[j].ToUpper(); string outputPrefix = $"{adapterName}-{perAdapterOutputName}"; string outputPointTag = string.Format(PointTagTemplate, outputPrefix); string outputAlternateTag = string.Format(AlternateTagTemplate, alternateTagPrefix is null ? "" : $"{alternateTagPrefix}-{perAdapterOutputName}"); string signalReference = string.Format(SignalReferenceTemplate, outputPrefix); SignalType signalType = SignalTypes?[j] ?? SignalType; string description = string.Format(DescriptionTemplate, outputPrefix, signalType, Name, GetType().Name); // Get output measurement record, creating a new one if needed MeasurementRecord measurement = this.GetMeasurementRecord(currentDeviceID ?? CurrentDeviceID, outputPointTag, outputAlternateTag, signalReference, description, signalType, TargetHistorianAcronym); // Track output signal IDs signalIDs.Add(measurement.SignalID); outputs[j] = measurement.SignalID; } // Add inputs and outputs to connection string settings for child adapter settings[nameof(InputMeasurementKeys)] = string.Join(";", inputs); settings[nameof(OutputMeasurements)] = string.Join(";", outputs); string connectionString = settings.JoinKeyValuePairs(); if (!string.IsNullOrWhiteSpace(CustomAdapterSettings)) connectionString = $"{connectionString}; {CustomAdapterSettings}"; adapters.Add(new TAdapter { Name = adapterName, ID = AdapterIDCounter++, ConnectionString = connectionString, DataSource = DataSource }); } // Check for adapters that are no longer referenced and need to be removed IEnumerable adaptersToRemove = this.Where(adapter => !activeAdapterNames.Contains(adapter.Name)); foreach (IActionAdapter adapter in adaptersToRemove) Remove(adapter); // Host system was notified about configuration changes, i.e., new or updated output measurements. // Before initializing child adapters, we wait for this process to complete. this.WaitForSignalsToLoad(signalIDs.ToArray(), SourceMeasurementTable); // Add new adapters to parent bulk adapter collection, this will auto-initialize each child adapter foreach (TAdapter adapter in adapters) Add(adapter as IActionAdapter); RecalculateRoutingTables(); } catch (Exception ex) { OnProcessException(MessageLevel.Error, new InvalidOperationException($"Failed while initializing child adapters: {ex.Message}", ex)); } } /// /// Validates that an even number of inputs are provided for specified . /// protected void ValidateEvenInputCount() => this.HandleValidateEvenInputCount(); /// /// Parses connection string. Derived classes should override for custom connection string parsing and call the /// method when is /// true (the default). /// /// /// When is true (the default), implementors must override /// this method and at least call the method. /// public virtual void ParseConnectionString() { this.HandleParseConnectionString(); if (FramesPerSecond < 1) FramesPerSecond = DefaultFramesPerSecond; if (LagTime < 0.0D) LagTime = DefaultLagTime; if (LeadTime < 0.0D) LeadTime = DefaultLeadTime; } /// /// Notifies derived classes that configuration has been reloaded /// public virtual void ConfigurationReloaded() { if (AutoReparseConnectionString || !Initialized) return; ThreadPool.QueueUserWorkItem(_ => InitializeChildAdapterManagement(InputMeasurementKeys)); } /// /// Recalculates routing tables. /// public virtual void RecalculateRoutingTables() => this.HandleRecalculateRoutingTables(); /// /// Queues a collection of measurements for processing to each connected to this . /// /// Measurements to queue for processing. public override void QueueMeasurementsForProcessing(IEnumerable measurements) => this.HandleQueueMeasurementsForProcessing(measurements); /// /// Gets a short one-line status of this . /// /// Maximum number of available characters for display. /// A short one-line summary of the current status of the . public override string GetShortStatus(int maxLength) => this.HandleGetShortStatus(maxLength); /// /// Enumerates child adapters. /// [AdapterCommand("Enumerates child adapters.")] public virtual void EnumerateAdapters() => this.HandleEnumerateAdapters(); /// /// Gets subscriber information for specified client connection. /// /// Enumerated index for child adapter. /// Status for adapter with specified . [AdapterCommand("Gets subscriber information for specified client connection.")] public virtual string GetAdapterStatus(int adapterIndex) => this.HandleGetAdapterStatus(adapterIndex); /// /// Gets configured database connection. /// /// New ADO data connection based on configured settings. public AdoDataConnection GetConfiguredConnection() => this.HandleGetConfiguredConnection(); #endregion #region [ IIndependentAdapterManager Implementation ] RoutingTables IIndependentAdapterManager.RoutingTables { get; set; } string IIndependentAdapterManager.OriginalDataMember { get; set; } ManualResetEventSlim IIndependentAdapterManager.ConfigurationReloadedWaitHandle { get; set; } bool IIndependentAdapterManager.AutoReparseConnectionString { get => AutoReparseConnectionString; set => AutoReparseConnectionString = value; } void IIndependentAdapterManager.OnConfigurationChanged() => OnConfigurationChanged(); void IIndependentAdapterManager.OnInputMeasurementKeysUpdated() => OnInputMeasurementKeysUpdated(); void IIndependentAdapterManager.OnStatusMessage(MessageLevel level, string status, string eventName, MessageFlags flags) => OnStatusMessage(level, status, eventName, flags); void IIndependentAdapterManager.OnProcessException(MessageLevel level, Exception exception, string eventName, MessageFlags flags) => OnProcessException(level, exception, eventName, flags); void IIndependentAdapterManager.ParseConnectionString() => m_parseConnectionString.RunOnceAsync(); #endregion } }