//****************************************************************************************************** // AdapterCollectionBase.cs - Gbtc // // Copyright © 2012, 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://www.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/02/2010 - J. Ritchie Carroll // Generated original version of source code. // 12/20/2012 - Starlynn Danyelle Gilliam // Modified Header. // //****************************************************************************************************** using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.ComponentModel; using System.Data; using System.IO; using System.Linq; using System.Reflection; using System.Text; using GSF.Annotations; using GSF.Collections; using GSF.Diagnostics; using GSF.IO; using GSF.Threading; using GSF.Units; namespace GSF.TimeSeries.Adapters { /// /// Represents a collection of implementations. /// /// Type of this collection contains. [EditorBrowsable(EditorBrowsableState.Never)] public abstract class AdapterCollectionBase : ListCollection, IAdapterCollection where T : class, IAdapter { #region [ Members ] // Events /// /// Provides status messages to consumer. /// /// /// is new status message. /// public event EventHandler> StatusMessage; /// /// Event is raised when there is an exception encountered while processing. /// /// /// is the exception that was thrown. /// public event EventHandler> ProcessException; /// /// Event is raised when are updated. /// public event EventHandler InputMeasurementKeysUpdated; /// /// Event is raised when are updated. /// public event EventHandler OutputMeasurementsUpdated; /// /// Event is raised when adapter is aware of a configuration change. /// public event EventHandler ConfigurationChanged; /// /// Event is raised when this is disposed or an in the collection is disposed. /// public event EventHandler Disposed; // Fields private string m_name; private uint m_id; private bool m_initialized; private string m_connectionString; private Dictionary m_settings; private DataSet m_dataSource; private string m_dataMember; private int m_initializationTimeout; private bool m_autoStart; private IMeasurement[] m_outputMeasurements; private MeasurementKey[] m_inputMeasurementKeys; private string[] m_inputSourceIDs; private string[] m_outputSourceIDs; private MeasurementKey[] m_requestedInputMeasurementKeys; private MeasurementKey[] m_requestedOutputMeasurementKeys; private Ticks m_lastProcessTime; private Time m_totalProcessTime; private long m_processedMeasurements; private DateTime m_startTimeConstraint; private DateTime m_stopTimeConstraint; private int m_processingInterval; private SharedTimer m_monitorTimer; private bool m_monitorTimerEnabled; private readonly LogicalThreadScheduler m_lifecycleThreadScheduler; private readonly Dictionary m_lifecycleThreads; private readonly LogicalThreadLocal m_activeItem; private bool m_enabled; private long m_startTime; private long m_stopTime; private bool m_disposed; #endregion #region [ Constructors ] /// /// Constructs a new instance of the . /// protected AdapterCollectionBase() { Log = Logger.CreatePublisher(GetType(), MessageClass.Application); m_name = GetType().Name; Log.InitialStackMessages = Log.InitialStackMessages.Union("AdapterName", m_name); m_settings = new Dictionary(); m_startTimeConstraint = DateTime.MinValue; m_stopTimeConstraint = DateTime.MaxValue; m_processingInterval = -1; m_initializationTimeout = AdapterBase.DefaultInitializationTimeout; m_autoStart = true; // We monitor total number of processed measurements every minute m_monitorTimer = Common.TimerScheduler.CreateTimer(60000); m_monitorTimer.Elapsed += m_monitorTimer_Elapsed; m_monitorTimer.AutoReset = true; m_monitorTimer.Enabled = false; m_lifecycleThreadScheduler = new LogicalThreadScheduler(); m_lifecycleThreads = new Dictionary(); m_activeItem = new LogicalThreadLocal(); // Even on a single processor system we want a few threads such that if an // adapter is taking a long time to initialize, other adapters can still be // initializing in the meanwhile if (m_lifecycleThreadScheduler.MaxThreadCount < 4) m_lifecycleThreadScheduler.MaxThreadCount = 4; m_lifecycleThreadScheduler.UnhandledException += (sender, args) => OnProcessException(MessageLevel.Warning, args.Argument); } /// /// Releases the unmanaged resources before the object is reclaimed by . /// ~AdapterCollectionBase() { Dispose(false); } #endregion #region [ Properties ] /// /// Log messages generated by an adapter. /// protected LogPublisher Log { get; } /// /// Gets or sets the name of this . /// public virtual string Name { get { return m_name; } set { m_name = value; Log.InitialStackMessages = Log.InitialStackMessages.Union("AdapterName", m_name); } } /// /// Gets or sets numeric ID associated with this . /// public virtual uint ID { get { return m_id; } set { m_id = value; } } /// /// Gets or sets flag indicating if the adapter collection has been initialized successfully. /// public virtual bool Initialized { get { return m_initialized; } set { m_initialized = value; } } /// /// Gets or sets key/value pair connection information specific to this . /// public virtual string ConnectionString { get { return m_connectionString; } set { m_connectionString = value; // Pre-parse settings upon connection string assignment if (string.IsNullOrWhiteSpace(m_connectionString)) m_settings = new Dictionary(); else m_settings = m_connectionString.ParseKeyValuePairs(); } } string IAdapter.ConnectionInfo => null; /// /// Gets or sets based data source used to load each . /// Updates to this property will cascade to all items in this . /// /// /// Table name specified in from is expected /// to have the following table column names:
/// ID, AdapterName, AssemblyName, TypeName, ConnectionString
/// ID column type should be integer based, all other column types are expected to be string based. ///
public virtual DataSet DataSource { get { return m_dataSource; } set { m_dataSource = value; // Update data source for items in this collection lock (this) { foreach (T item in this) { item.DataSource = m_dataSource; } } } } /// /// Gets or sets specific data member (e.g., table name) in used to this . /// /// /// Table name specified in from is expected /// to have the following table column names:
/// ID, AdapterName, AssemblyName, TypeName, ConnectionString
/// ID column type should be integer based, all other column types are expected to be string based. ///
public virtual string DataMember { get { return m_dataMember; } set { m_dataMember = value; } } /// /// Gets or sets the default adapter time that represents the maximum time system will wait during for initialization. /// /// /// Set to to wait indefinitely. /// public virtual int InitializationTimeout { get { return m_initializationTimeout; } set { m_initializationTimeout = value; } } /// /// Gets or sets flag indicating if adapter collection should automatically start items when is false. /// public virtual bool AutoStart { get { return m_autoStart; } set { m_autoStart = value; } } /// /// Gets or sets primary keys of input measurements the expects, if any. /// public virtual MeasurementKey[] InputMeasurementKeys { get { // If a specific set of input measurement keys has been assigned, use that set if (m_inputMeasurementKeys != null) return m_inputMeasurementKeys; List cumulativeKeys = new List(); // Otherwise return cumulative results of all child adapters lock (this) { foreach (T adapter in this) { if ((object)adapter != null) { MeasurementKey[] inputMeasurementKeys = adapter.InputMeasurementKeys; // If any of the children expects all measurements (i.e., null InputMeasurementKeys) // then the parent collection must expect all measurements if ((object)inputMeasurementKeys == null) return null; if (inputMeasurementKeys.Length > 0) cumulativeKeys.AddRange(inputMeasurementKeys); } } } return cumulativeKeys.Distinct().ToArray(); } set { m_inputMeasurementKeys = value; OnInputMeasurementKeysUpdated(); } } /// /// Gets or sets output measurements that the will produce, if any. /// public virtual IMeasurement[] OutputMeasurements { get { // If a specific set of output measurements has been assigned, use that set if (m_outputMeasurements != null) return m_outputMeasurements; // Otherwise return cumulative results of all child adapters List cumulativeMeasurements = new List(); // Otherwise return cumulative results of all child adapters lock (this) { foreach (T adapter in this) { if ((object)adapter != null) { IMeasurement[] outputMeasurements = adapter.OutputMeasurements; if (outputMeasurements != null && outputMeasurements.Length > 0) cumulativeMeasurements.AddRange(outputMeasurements); } } } return cumulativeMeasurements.Distinct().ToArray(); } set { m_outputMeasurements = value; OnOutputMeasurementsUpdated(); } } /// /// Gets or sets values used to filter input measurement keys. /// /// /// The collection classes simply track this value if assigned, no automatic action is taken. /// public virtual string[] InputSourceIDs { get { return m_inputSourceIDs; } set { m_inputSourceIDs = value; } } /// /// Gets or sets values used to filter output measurements. /// /// /// The collection classes simply track this value if assigned, no automatic action is taken. /// public virtual string[] OutputSourceIDs { get { return m_outputSourceIDs; } set { m_outputSourceIDs = value; } } /// /// Gets or sets input measurement keys that are requested by other adapters based on what adapter says it can provide. /// public virtual MeasurementKey[] RequestedInputMeasurementKeys { get { // If a specific set of input measurement keys has been assigned, use that set if (m_requestedInputMeasurementKeys != null) return m_requestedInputMeasurementKeys; // Otherwise return cumulative results of all child adapters lock (this) { if (typeof(IActionAdapter).IsAssignableFrom(typeof(T))) return this.Cast().Where(item => item.RequestedInputMeasurementKeys != null).SelectMany(item => item.RequestedInputMeasurementKeys).Distinct().ToArray(); if (typeof(IOutputAdapter).IsAssignableFrom(typeof(T))) return this.Cast().Where(item => item.RequestedInputMeasurementKeys != null).SelectMany(item => item.RequestedInputMeasurementKeys).Distinct().ToArray(); } return null; } set { m_requestedInputMeasurementKeys = value; } } /// /// Gets or sets output measurement keys that are requested by other adapters based on what adapter says it can provide. /// public virtual MeasurementKey[] RequestedOutputMeasurementKeys { get { // If a specific set of output measurement keys has been assigned, use that set if (m_requestedOutputMeasurementKeys != null) return m_requestedOutputMeasurementKeys; // Otherwise return cumulative results of all child adapters lock (this) { if (typeof(IActionAdapter).IsAssignableFrom(typeof(T))) return this.Cast().Where(item => item.RequestedOutputMeasurementKeys != null).SelectMany(item => item.RequestedOutputMeasurementKeys).Distinct().ToArray(); if (typeof(IInputAdapter).IsAssignableFrom(typeof(T))) return this.Cast().Where(item => item.RequestedOutputMeasurementKeys != null).SelectMany(item => item.RequestedOutputMeasurementKeys).Distinct().ToArray(); } return null; } set { m_requestedOutputMeasurementKeys = value; } } /// /// Gets the flag indicating if this adapter collection supports temporal processing. /// /// /// For collections this defaults to false. /// public virtual bool SupportsTemporalProcessing => false; /// /// Gets the start time temporal processing constraint defined by call to . /// /// /// This value will be when start time constraint is not set - meaning the adapter /// is processing data in real-time. /// public virtual DateTime StartTimeConstraint => m_startTimeConstraint; /// /// Gets the stop time temporal processing constraint defined by call to . /// /// /// This value will be when stop time constraint is not set - meaning the adapter /// is processing data in real-time. /// public virtual DateTime StopTimeConstraint => m_stopTimeConstraint; /// /// Gets or sets the desired processing interval, in milliseconds, for the adapter collection and applies this interval to each adapter. /// /// /// With the exception of the values of -1 and 0, this value specifies the desired processing interval for data, i.e., /// basically a delay, or timer interval, over which to process data. A value of -1 means to use the default processing /// interval while a value of 0 means to process data as fast as possible. /// public virtual int ProcessingInterval { get { return m_processingInterval; } set { m_processingInterval = value; if (m_processingInterval < -1) m_processingInterval = -1; // Apply this new processing interval for all adapters in the collection lock (this) { foreach (T item in this) { item.ProcessingInterval = m_processingInterval; } } } } /// /// Gets the total amount of time, in seconds, that the adapter has been active. /// public virtual Time RunTime { get { Ticks processingTime = 0; if (m_startTime > 0) { if (m_stopTime > 0) processingTime = m_stopTime - m_startTime; else processingTime = DateTime.UtcNow.Ticks - m_startTime; } if (processingTime < 0) processingTime = 0; return processingTime.ToSeconds(); } } /// /// Gets the total number of measurements processed thus far by each implementation /// in the . /// public virtual long ProcessedMeasurements { get { long processedMeasurements = 0; // Calculate new total for all adapters lock (this) { foreach (T item in this) { processedMeasurements += item.ProcessedMeasurements; } } return processedMeasurements; } } /// /// Gets or sets enabled state of this . /// public virtual bool Enabled { get { return m_enabled; } set { if (m_enabled && !value) Stop(); else if (!m_enabled && value) Start(); } } /// /// Gets the UTC time this was started. /// public Ticks StartTime => m_startTime; /// /// Gets the UTC time this was stopped. /// public Ticks StopTime => m_stopTime; /// /// Gets a flag that indicates whether the object has been disposed. /// public bool IsDisposed => m_disposed; /// /// Gets a value indicating whether the is read-only. /// public virtual bool IsReadOnly => false; /// /// Gets or sets flag that determines if monitor timer should be used for monitoring processed measurement statistics for the . /// protected virtual bool MonitorTimerEnabled { get { return m_monitorTimerEnabled; } set { m_monitorTimerEnabled = value; if (m_monitorTimer != null) m_monitorTimer.Enabled = value && Enabled; } } /// /// Gets flag that determines if implementations are automatically initialized /// when they are added to the collection. /// protected virtual bool AutoInitialize => true; /// /// Gets settings parsed when was assigned. /// public Dictionary Settings => m_settings; /// /// Gets the descriptive status of this . /// public virtual string Status { get { StringBuilder status = new StringBuilder(); DataSet dataSource = DataSource; // Show collection status status.AppendFormat(" Total adapter components: {0}", Count); status.AppendLine(); status.AppendFormat(" Collection initialized: {0}", Initialized); status.AppendLine(); status.AppendFormat(" Initialization timeout: {0}", InitializationTimeout < 0 ? "Infinite" : InitializationTimeout.ToString() + " milliseconds"); status.AppendLine(); status.AppendFormat(" Current operational state: {0}", Enabled ? "Enabled" : "Disabled"); status.AppendLine(); status.AppendFormat(" Temporal processing: {0}", SupportsTemporalProcessing ? "Supported" : "Unsupported"); status.AppendLine(); if (SupportsTemporalProcessing) { status.AppendFormat(" Start time constraint: {0}", StartTimeConstraint == DateTime.MinValue ? "Unspecified" : StartTimeConstraint.ToString("yyyy-MM-dd HH:mm:ss.fff")); status.AppendLine(); status.AppendFormat(" Stop time constraint: {0}", StopTimeConstraint == DateTime.MaxValue ? "Unspecified" : StopTimeConstraint.ToString("yyyy-MM-dd HH:mm:ss.fff")); status.AppendLine(); status.AppendFormat(" Processing interval: {0}", ProcessingInterval < 0 ? "Default" : (ProcessingInterval == 0 ? "As fast as possible" : ProcessingInterval + " milliseconds")); status.AppendLine(); } if (MonitorTimerEnabled) { status.AppendFormat(" Processed measurements: {0:N0}", m_processedMeasurements); status.AppendLine(); status.AppendFormat(" Average processing rate: {0:N0} measurements / second", ((int)(m_processedMeasurements / m_totalProcessTime))); status.AppendLine(); } status.AppendFormat(" Data source defined: {0}", dataSource != null); status.AppendLine(); if (dataSource != null) { status.AppendFormat(" Referenced data source: {0}, {1} tables", dataSource.DataSetName, dataSource.Tables.Count); status.AppendLine(); } status.AppendFormat(" Data source table name: {0}", DataMember); status.AppendLine(); Dictionary keyValuePairs = Settings; char[] keyChars; string value; status.AppendFormat(" Connection string: {0} key/value pairs", keyValuePairs.Count); // 1 2 3 4 5 6 7 // 123456789012345678901234567890123456789012345678901234567890123456789012345678 // Key = Value // 1 2 3 4 5 // 12345678901234567890123456789012345678901234567890 status.AppendLine(); status.AppendLine(); foreach (KeyValuePair item in keyValuePairs) { keyChars = item.Key.Trim().ToCharArray(); keyChars[0] = char.ToUpper(keyChars[0]); value = item.Value.Trim(); if (value.Length > 50) value = value.TruncateRight(47) + "..."; status.AppendFormat("{0} = {1}", new string(keyChars).TruncateRight(25).PadLeft(25), value.PadRight(50)); status.AppendLine(); } status.AppendLine(); if (Count > 0) { int index = 0; status.AppendLine(); status.AppendFormat("Status of each {0} component:", Name); status.AppendLine(); status.Append(new string('-', 79)); status.AppendLine(); // Show the status of registered components. lock (this) { foreach (T item in this) { IProvideStatus statusProvider = item; if (statusProvider != null) { // This component provides status information. status.AppendLine(); status.AppendFormat("Status of {0} component {1}, {2}:", typeof(T).Name, ++index, statusProvider.Name); status.AppendLine(); try { status.Append(statusProvider.Status); } catch (Exception ex) { status.AppendFormat("Failed to retrieve status due to exception: {0}", ex.Message); status.AppendLine(); } } } } status.AppendLine(); status.Append(new string('-', 79)); status.AppendLine(); } return status.ToString(); } } #endregion #region [ Methods ] /// /// Releases all the resources used by the object. /// public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } /// /// 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 virtual void Dispose(bool disposing) { if (!m_disposed) { try { if (disposing) { if (m_monitorTimer != null) { m_monitorTimer.Elapsed -= m_monitorTimer_Elapsed; m_monitorTimer.Dispose(); } m_monitorTimer = null; Clear(); // This disposes all items in collection... } } finally { m_disposed = true; // Prevent duplicate dispose. if (Disposed != null) Disposed(this, EventArgs.Empty); } } } /// /// Loads all implementations defined in . /// /// /// /// Table name specified in from is expected /// to have the following table column names:
/// ID, AdapterName, AssemblyName, TypeName, ConnectionString
/// ID column type should be integer based, all other column types are expected to be string based. ///
/// /// Note that when calling this method any existing items will be cleared allowing a "re-initialize". /// ///
/// DataSource is null. /// DataMember is null or empty. public virtual void Initialize() { if ((object)DataSource == null) throw new NullReferenceException($"DataSource is null, cannot load {Name}"); if (string.IsNullOrWhiteSpace(DataMember)) throw new InvalidOperationException($"DataMember is null or empty, cannot load {Name}"); Initialized = false; Dictionary settings = Settings; string setting; T item; // Load the default initialization parameter for adapters in this collection if (settings.TryGetValue("initializationTimeout", out setting)) InitializationTimeout = int.Parse(setting); lock (this) { Clear(); if (DataSource.Tables.Contains(DataMember)) { foreach (DataRow adapterRow in DataSource.Tables[DataMember].Rows) { if (TryCreateAdapter(adapterRow, out item)) Add(item); } Initialized = true; } else throw new InvalidOperationException($"Data set member \"{DataMember}\" was not found in data source, check ConfigurationEntity. Failed to initialize {Name}."); } } /// /// Attempts to create an from the specified . /// /// containing item information to initialize. /// Initialized adapter if successful; otherwise null. /// true if item was successfully initialized; otherwise false. /// /// See property for expected column names. /// /// is null. public virtual bool TryCreateAdapter(DataRow adapterRow, out T adapter) { if ((object)adapterRow == null) throw new NullReferenceException("Cannot initialize from null adapter DataRow"); Assembly assembly; string name = "", assemblyName = "", typeName = "", connectionString, setting; uint id; try { name = adapterRow["AdapterName"].ToNonNullString("[IAdapter]"); assemblyName = FilePath.GetAbsolutePath(adapterRow["AssemblyName"].ToNonNullString()); typeName = adapterRow["TypeName"].ToNonNullString(); connectionString = adapterRow["ConnectionString"].ToNonNullString(); id = uint.Parse(adapterRow["ID"].ToNonNullString("0")); if (string.IsNullOrWhiteSpace(typeName)) throw new InvalidOperationException("No adapter type was defined"); if (!File.Exists(assemblyName)) throw new InvalidOperationException("Specified adapter assembly does not exist"); assembly = Assembly.LoadFrom(assemblyName); adapter = (T)Activator.CreateInstance(assembly.GetType(typeName)); // Assign critical adapter properties adapter.Name = name; adapter.ID = id; adapter.ConnectionString = connectionString; adapter.DataSource = DataSource; // Assign adapter initialization timeout if (adapter.Settings.TryGetValue("initializationTimeout", out setting)) adapter.InitializationTimeout = int.Parse(setting); else adapter.InitializationTimeout = InitializationTimeout; return true; } catch (Exception ex) { // We report any errors encountered during type creation... OnProcessException(MessageLevel.Warning, new InvalidOperationException($"Failed to load adapter \"{name}\" [{typeName}] from \"{assemblyName}\": {ex.Message}", ex)); } adapter = default(T); return false; } // Explicit IAdapter implementation of TryCreateAdapter bool IAdapterCollection.TryCreateAdapter(DataRow adapterRow, out IAdapter adapter) { T adapterT; bool result = TryCreateAdapter(adapterRow, out adapterT); adapter = adapterT as IAdapter; return result; } /// /// Attempts to get the adapter with the specified . /// /// ID of adapter to get. /// Adapter reference if found; otherwise null. /// true if adapter with the specified was found; otherwise false. public virtual bool TryGetAdapterByID(uint id, out T adapter) { return TryGetAdapter(id, (item, value) => item.ID == value, out adapter); } /// /// Attempts to get the adapter with the specified . /// /// Name of adapter to get. /// Adapter reference if found; otherwise null. /// true if adapter with the specified was found; otherwise false. public virtual bool TryGetAdapterByName(string name, out T adapter) { return TryGetAdapter(name, (item, value) => item.Name.Equals(value, StringComparison.OrdinalIgnoreCase), out adapter); } /// /// Attempts to get the adapter with the specified given function. /// /// Value of adapter to get. /// Function delegate used to test item . /// Adapter reference if found; otherwise null. /// true if adapter with the specified was found; otherwise false. protected virtual bool TryGetAdapter(TValue value, Func testItem, out T adapter) { lock (this) { foreach (T item in this) { if (testItem(item, value)) { adapter = item; return true; } } } adapter = default(T); return false; } // Explicit IAdapter implementation of TryGetAdapterByID bool IAdapterCollection.TryGetAdapterByID(uint id, out IAdapter adapter) { T adapterT; bool result = TryGetAdapterByID(id, out adapterT); adapter = adapterT as IAdapter; return result; } // Explicit IAdapter implementation of TryGetAdapterByName bool IAdapterCollection.TryGetAdapterByName(string name, out IAdapter adapter) { T adapterT; bool result = TryGetAdapterByName(name, out adapterT); adapter = adapterT as IAdapter; return result; } /// /// Attempts to initialize (or reinitialize) an individual based on its ID. /// /// The numeric ID associated with the to be initialized. /// true if item was successfully initialized; otherwise false. public virtual bool TryInitializeAdapterByID(uint id) { T newAdapter, oldAdapter; uint rowID; foreach (DataRow adapterRow in DataSource.Tables[DataMember].Rows) { rowID = uint.Parse(adapterRow["ID"].ToNonNullString("0")); if (rowID == id) { if (TryCreateAdapter(adapterRow, out newAdapter)) { // Found and created new item - update collection reference bool foundItem = false; lock (this) { for (int i = 0; i < Count; i++) { oldAdapter = this[i]; if (oldAdapter.ID == id) { // Dispose old item, initialize new item this[i] = newAdapter; foundItem = true; break; } } // Add item to collection if it didn't exist if (!foundItem) Add(newAdapter); return true; } } break; } } return false; } /// /// Starts, or restarts, each implementation in this . /// [AdapterCommand("Starts, or restarts, each adapter in the collection.", "Administrator", "Editor")] public virtual void Start() { LogicalThread lifecycleThread; // Make sure we are stopped (e.g., disconnected) before attempting to start (e.g., connect) if (!m_enabled) { m_enabled = true; m_stopTime = 0; m_startTime = DateTime.UtcNow.Ticks; ResetStatistics(); lock (this) { foreach (T item in this) { if (item.Initialized && item.AutoStart && !item.Enabled) { // Create local reference to the foreach // variable to be accessed in the lambda function T itemRef = item; // Push start command to the lifecycle thread for the adapter lifecycleThread = m_lifecycleThreads.GetOrAdd(item.ID, id => m_lifecycleThreadScheduler.CreateThread()); lifecycleThread.Push(() => Start(itemRef)); } } } // Start data monitor... if (MonitorTimerEnabled) m_monitorTimer.Start(); } } /// /// Stops each implementation in this . /// [AdapterCommand("Stops each adapter in the collection.", "Administrator", "Editor")] public virtual void Stop() { LogicalThread lifecycleThread; if (m_enabled) { m_enabled = false; m_stopTime = DateTime.UtcNow.Ticks; lock (this) { foreach (T item in this) { if (item.Initialized && item.Enabled) { // Create local reference to the foreach // variable to be accessed in the lambda function T itemRef = item; // Push stop command to the lifecycle thread for the adapter lifecycleThread = m_lifecycleThreads.GetOrAdd(item.ID, id => m_lifecycleThreadScheduler.CreateThread()); lifecycleThread.Push(() => Stop(itemRef)); } } } // Stop data monitor... m_monitorTimer.Stop(); } } /// /// Resets the statistics of this collection. /// [AdapterCommand("Resets the statistics of this collection.", "Administrator", "Editor")] public void ResetStatistics() { m_processedMeasurements = 0; m_totalProcessTime = 0.0D; m_lastProcessTime = DateTime.UtcNow.Ticks; OnStatusMessage(MessageLevel.Info, "Statistics reset for this collection."); } /// /// 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 this . public virtual string GetShortStatus(int maxLength) { return $"Total components: {Count:N0}".CenterText(maxLength); } /// /// Defines a temporal processing constraint for the adapter collection and applies this constraint to each adapter. /// /// Defines a relative or exact start time for the temporal constraint. /// Defines a relative or exact stop time for the temporal constraint. /// Defines any temporal parameters related to the constraint. /// /// /// This method defines a temporal processing constraint for an adapter, i.e., the start and stop time over which an /// adapter will process data. Actual implementation of the constraint will be adapter specific. Implementations /// should be able to dynamically handle multiple calls to this function with new constraints. Passing in null /// for the and should cancel the temporal constraint and /// return the adapter to standard / real-time operation. /// /// /// The and parameters can be specified in one of the /// following formats: /// /// /// Time Format /// Format Description /// /// /// 12-30-2000 23:59:59.033 /// Absolute date and time. /// /// /// * /// Evaluates to . /// /// /// *-20s /// Evaluates to 20 seconds before . /// /// /// *-10m /// Evaluates to 10 minutes before . /// /// /// *-1h /// Evaluates to 1 hour before . /// /// /// *-1d /// Evaluates to 1 day before . /// /// /// /// [AdapterCommand("Defines a temporal processing constraint for each adapter in the collection.", "Administrator", "Editor", "Viewer")] public virtual void SetTemporalConstraint(string startTime, string stopTime, string constraintParameters) { if (!string.IsNullOrWhiteSpace(startTime)) m_startTimeConstraint = AdapterBase.ParseTimeTag(startTime); else m_startTimeConstraint = DateTime.MinValue; if (!string.IsNullOrWhiteSpace(stopTime)) m_stopTimeConstraint = AdapterBase.ParseTimeTag(stopTime); else m_stopTimeConstraint = DateTime.MaxValue; // Apply temporal constraint to all adapters in this collection lock (this) { foreach (T adapter in this) { adapter.SetTemporalConstraint(startTime, stopTime, constraintParameters); } } } /// /// Raises the event. /// /// New status message. [Obsolete("Switch to using overload with MessageLevel parameter - this method may be removed from future builds.", false)] protected void OnStatusMessage(string status) { OnStatusMessage(MessageLevel.Info, status, "Unclassified Status"); } /// /// Raises the event with a formatted status message. /// /// Formatted status message. /// Arguments for . /// /// This overload combines string.Format and SendStatusMessage for convenience. /// [StringFormatMethod("formattedStatus"), Obsolete("Switch to using overload with MessageLevel parameter - this method may be removed from future builds.", false)] protected void OnStatusMessage(string formattedStatus, params object[] args) { OnStatusMessage(MessageLevel.Info, string.Format(formattedStatus, args), "Unclassified Status"); } /// /// Raises the event and sends this data to the . /// /// The to assign to this message /// New status message. /// A fixed string to classify this event; defaults to null. /// to use, if any; defaults to . /// /// should be a constant string value associated with what type of message is being /// generated. In general, there should only be a few dozen distinct event names per class. Exceeding this /// threshold will cause the EventName to be replaced with a general warning that a usage issue has occurred. /// protected internal virtual void OnStatusMessage(MessageLevel level, string status, string eventName = null, MessageFlags flags = MessageFlags.None) { try { Log.Publish(level, flags, eventName, status); using (Logger.SuppressLogMessages()) StatusMessage?.Invoke(this, new EventArgs(AdapterBase.GetStatusWithMessageLevelPrefix(status, level))); } catch (Exception ex) { // We protect our code from consumer thrown exceptions OnProcessException(MessageLevel.Info, new InvalidOperationException($"Exception in consumer handler for StatusMessage event: {ex.Message}", ex), "ConsumerEventException"); } } /// /// Raises the event. /// /// Processing . [Obsolete("Switch to using overload with MessageLevel parameter - this method may be removed from future builds.", false)] protected void OnProcessException(Exception ex) { OnProcessException(MessageLevel.Info, ex, "Unclassified Exception"); } /// /// Raises the event. /// /// The to assign to this message /// Processing . /// A fixed string to classify this event; defaults to null. /// to use, if any; defaults to . /// /// should be a constant string value associated with what type of message is being /// generated. In general, there should only be a few dozen distinct event names per class. Exceeding this /// threshold will cause the EventName to be replaced with a general warning that a usage issue has occurred. /// protected internal virtual void OnProcessException(MessageLevel level, Exception exception, string eventName = null, MessageFlags flags = MessageFlags.None) { try { Log.Publish(level, flags, eventName, exception?.Message, null, exception); using (Logger.SuppressLogMessages()) ProcessException?.Invoke(this, new EventArgs(exception)); } catch (Exception ex) { // We protect our code from consumer thrown exceptions Log.Publish(MessageLevel.Info, "ConsumerEventException", $"Exception in consumer handler for ProcessException event: {ex.Message}", null, ex); } } /// /// Raises event. /// protected virtual void OnInputMeasurementKeysUpdated() { try { InputMeasurementKeysUpdated?.Invoke(this, EventArgs.Empty); } catch (Exception ex) { // We protect our code from consumer thrown exceptions OnProcessException(MessageLevel.Info, new InvalidOperationException($"Exception in consumer handler for InputMeasurementKeysUpdated event: {ex.Message}", ex), "ConsumerEventException"); } } /// /// Raises event. /// protected virtual void OnOutputMeasurementsUpdated() { try { OutputMeasurementsUpdated?.Invoke(this, EventArgs.Empty); } catch (Exception ex) { // We protect our code from consumer thrown exceptions OnProcessException(MessageLevel.Info, new InvalidOperationException($"Exception in consumer handler for OutputMeasurementsUpdated event: {ex.Message}", ex), "ConsumerEventException"); } } /// /// Raises event. /// protected virtual void OnConfigurationChanged() { try { ConfigurationChanged?.Invoke(this, EventArgs.Empty); } catch (Exception ex) { // We protect our code from consumer thrown exceptions OnProcessException(MessageLevel.Info, new InvalidOperationException($"Exception in consumer handler for ConfigurationChanged event: {ex.Message}", ex), "ConsumerEventException"); } } /// /// Removes all elements from the . /// protected override void ClearItems() { // Dispose each item before clearing the collection lock (this) { foreach (T item in this) DisposeItem(item); base.ClearItems(); } } /// /// Inserts an element into the the specified index. /// /// The zero-based index at which item should be inserted. /// The implementation to insert. protected override void InsertItem(int index, T item) { lock (this) { // Wire up item events and handle item initialization InitializeItem(item); base.InsertItem(index, item); } } /// /// Assigns a new element to the at the specified index. /// /// The zero-based index for which item should be assigned. /// The implementation to assign. protected override void SetItem(int index, T item) { lock (this) { // Dispose of existing item DisposeItem(this[index]); // Wire up item events and handle initialization of new item InitializeItem(item); base.SetItem(index, item); } } /// /// Removes the element at the specified index of the . /// /// The zero-based index of the element to remove. protected override void RemoveItem(int index) { // Dispose of item before removing it from the collection lock (this) { DisposeItem(this[index]); base.RemoveItem(index); } } /// /// Wires events and initializes new implementation. /// /// New implementation. /// /// Derived classes should override if more events are defined. /// protected virtual void InitializeItem(T item) { LogicalThread lifecycleThread; if ((object)item != null) { // Wire up events item.StatusMessage += item_StatusMessage; item.ProcessException += item_ProcessException; item.ConfigurationChanged += item_ConfigurationChanged; item.Disposed += item_Disposed; try { // If automatically initializing new elements, handle object initialization on // its own thread so it can take needed amount of time if (AutoInitialize) { lifecycleThread = GetLifecycleThread(item); lifecycleThread.Push(() => { m_activeItem.Value = item; LogicalThread.CurrentThread.Push(() => Initialize(item)); }); } } catch (Exception ex) { // Process exception for logging string errorMessage = $"Failed to queue initialize operation for adapter {item.Name}: {ex.Message}"; OnProcessException(MessageLevel.Warning, new InvalidOperationException(errorMessage, ex)); } } } /// /// Un-wires events and disposes of implementation. /// /// to dispose. /// /// Derived classes should override if more events are defined. /// protected virtual void DisposeItem(T item) { LogicalThread lifecycleThread; if ((object)item != null) { lifecycleThread = GetLifecycleThread(item); lifecycleThread.Push(() => Dispose(item)); } } // Handle item initialization private void Initialize(T item) { Action initializationTimeoutAction = null; ICancellationToken initializationTimeoutToken = null; if (m_activeItem.Value != item) return; try { // Initialize item if not initialized already if (!item.Initialized) { // If initialization timeout is specified for this item, start the initialization timeout timer if (item.InitializationTimeout > 0) { initializationTimeoutAction = () => { const string MessageFormat = "Initialization of adapter {0} has exceeded" + " its timeout of {1} seconds. The adapter may still initialize, however this" + " may indicate a problem with the adapter. If you consider this to be normal," + " try adjusting the initialization timeout to suppress this message during" + " normal operations."; OnStatusMessage(MessageLevel.Warning, string.Format(MessageFormat, item.Name, item.InitializationTimeout / 1000.0), "Initialization"); // ReSharper disable once AccessToModifiedClosure initializationTimeoutToken = initializationTimeoutAction?.DelayAndExecute(item.InitializationTimeout); }; initializationTimeoutToken = initializationTimeoutAction.DelayAndExecute(item.InitializationTimeout); } // Initialize the item item.Initialize(); // Initialization successfully completed, so stop the timeout timer initializationTimeoutToken?.Cancel(); } // If the item is set to auto-start and not already started, start it now if (item.AutoStart && !item.Enabled) { LogicalThread.CurrentThread.Push(() => { Start(item); // Set item to its final initialized state so that // start and stop commands may be issued to the adapter item.Initialized = true; // Now that the adapter is fully initialized, // attach to these events to react to updates at runtime item.InputMeasurementKeysUpdated += item_InputMeasurementKeysUpdated; item.OutputMeasurementsUpdated += item_OutputMeasurementsUpdated; // If input measurement keys were not updated during initialize of the adapter, // make sure to notify routing tables that adapter is ready for broadcast OnInputMeasurementKeysUpdated(); }); } else { // Set item to its final initialized state so that // start and stop commands may be issued to the adapter item.Initialized = true; // Now that the adapter is fully initialized, // attach to these events to react to updates at runtime item.InputMeasurementKeysUpdated += item_InputMeasurementKeysUpdated; item.OutputMeasurementsUpdated += item_OutputMeasurementsUpdated; // If input measurement keys were not updated during initialize of the adapter, // make sure to notify routing tables that adapter is ready for broadcast OnInputMeasurementKeysUpdated(); } } catch (Exception ex) { // We report any errors encountered during initialization... OnProcessException(MessageLevel.Warning, new InvalidOperationException($"Failed to initialize adapter {item.Name}: {ex.Message}", ex), "Initialization"); // Initialization failed, so stop the timeout timer initializationTimeoutToken?.Cancel(); } } // Handle item startup private void Start(T item) { if (m_activeItem.Value != item) return; try { item.Start(); } catch (Exception ex) { // We report any errors encountered during startup... OnProcessException(MessageLevel.Warning, new InvalidOperationException($"Failed to start adapter {item.Name}: {ex.Message}", ex), "Startup"); } } // Handle item stop private void Stop(T item) { if (m_activeItem.Value != item) return; if (item.Initialized && item.Enabled) item.Stop(); } // Handles item disposal private void Dispose(T item) { // Stop item, then un-wire events item.Stop(); item.StatusMessage -= item_StatusMessage; item.ProcessException -= item_ProcessException; item.InputMeasurementKeysUpdated -= item_InputMeasurementKeysUpdated; item.OutputMeasurementsUpdated -= item_OutputMeasurementsUpdated; item.ConfigurationChanged -= item_ConfigurationChanged; // Dispose of item, then un-wire disposed event item.Dispose(); item.Disposed -= item_Disposed; } // Gets the lifecycle thread for the given item private LogicalThread GetLifecycleThread(T item) { return m_lifecycleThreads.GetOrAdd(item.ID, id => { LogicalThread thread = m_lifecycleThreadScheduler.CreateThread(); thread.UnhandledException += (sender, args) => item_ProcessException(item, args); return thread; }); } // Raise status message event on behalf of each item in collection private void item_StatusMessage(object sender, EventArgs e) => StatusMessage?.Invoke(sender, e); // Raise process exception event on behalf of each item in collection private void item_ProcessException(object sender, EventArgs e) => ProcessException?.Invoke(sender, e); // Raise input measurement keys updated event on behalf of each item in collection private void item_InputMeasurementKeysUpdated(object sender, EventArgs e) => InputMeasurementKeysUpdated?.Invoke(sender, e); // Raise output measurements updated event on behalf of each item in collection private void item_OutputMeasurementsUpdated(object sender, EventArgs e) => OutputMeasurementsUpdated?.Invoke(sender, e); // Raise configuration changed event on behalf of each item in collection private void item_ConfigurationChanged(object sender, EventArgs e) => ConfigurationChanged?.Invoke(sender, e); // Raise disposed event on behalf of each item in collection private void item_Disposed(object sender, EventArgs e) => Disposed?.Invoke(sender, e); // We monitor the total number of measurements destined for archival here... private void m_monitorTimer_Elapsed(object sender, EventArgs e) { StringBuilder status = new StringBuilder(); long totalNew, processedMeasurements = ProcessedMeasurements; // Calculate time since last call Ticks currentTime = DateTime.UtcNow.Ticks; Ticks totalProcessTime = currentTime - m_lastProcessTime; m_totalProcessTime += totalProcessTime.ToSeconds(); m_lastProcessTime = currentTime; // Calculate how many new measurements have been received in the last minute... totalNew = processedMeasurements - m_processedMeasurements; m_processedMeasurements = processedMeasurements; // Process statistics for 12 hours total runtime: // // 1 1 1 // 12345678901234 12345678901234567 1234567890 // Time span Measurements Per second // -------------- ----------------- ---------- // Entire runtime 9,999,999,999,999 99,999,999 // Last minute 4,985 83 status.AppendFormat("\r\nProcess statistics for {0} total runtime:\r\n\r\n", m_totalProcessTime.ToString(3).ToLower()); status.Append("Time span".PadRight(14)); status.Append(' '); status.Append("Measurements".CenterText(17)); status.Append(' '); status.Append("Per second".CenterText(10)); status.AppendLine(); status.Append(new string('-', 14)); status.Append(' '); status.Append(new string('-', 17)); status.Append(' '); status.Append(new string('-', 10)); status.AppendLine(); status.Append("Entire runtime".PadRight(14)); status.Append(' '); status.Append(m_processedMeasurements.ToString("N0").CenterText(17)); status.Append(' '); status.Append(((int)(m_processedMeasurements / m_totalProcessTime)).ToString("N0").CenterText(10)); status.AppendLine(); status.Append("Last minute".PadRight(14)); status.Append(' '); status.Append(totalNew.ToString("N0").CenterText(17)); status.Append(' '); status.Append(((int)(totalNew / totalProcessTime.ToSeconds())).ToString("N0").CenterText(10)); // Report updated statistics every minute... OnStatusMessage(MessageLevel.Info, "AdapterCollectionBase", status.ToString()); } #region [ Explicit IList Implementation ] void ICollection.Add(IAdapter item) { lock (this) { Add((T)item); } } bool ICollection.Contains(IAdapter item) { lock (this) { return Contains((T)item); } } void ICollection.CopyTo(IAdapter[] array, int arrayIndex) { lock (this) { CopyTo(array.Cast().ToArray(), arrayIndex); } } bool ICollection.Remove(IAdapter item) { lock (this) { return Remove((T)item); } } IEnumerator IEnumerable.GetEnumerator() { IAdapter[] adapters; lock (this) { adapters = new IAdapter[Count]; for (int i = 0; i < Count; i++) adapters[i] = this[i]; } foreach (IAdapter item in adapters) { yield return item; } } int IList.IndexOf(IAdapter item) { lock (this) { return IndexOf((T)item); } } void IList.Insert(int index, IAdapter item) { lock (this) { Insert(index, (T)item); } } IAdapter IList.this[int index] { get { lock (this) { return this[index]; } } set { lock (this) { this[index] = (T)value; } } } #endregion #endregion } }