//****************************************************************************************************** // OutputAdapterBase.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.ComponentModel; using System.Data; using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Text; using GSF.Collections; using GSF.Diagnostics; using GSF.Threading; namespace GSF.TimeSeries.Adapters { /// /// Represents the base class for any outgoing data stream. /// /// /// This base class acts as a measurement queue so that output adapters can temporarily go /// offline without losing any measurements to be processed. Derived classes are expected to /// override to handle queued measurements. /// public abstract class OutputAdapterBase : AdapterBase, IOutputAdapter { #region [ Members ] // Events /// /// Event is raised every five seconds allowing host to track total number of unprocessed measurements. /// /// /// /// Base class reports current queue size of unprocessed measurements so that if queue size reaches /// an unhealthy threshold, host can take evasive action. /// /// /// is total number of unprocessed measurements. /// /// public event EventHandler> UnprocessedMeasurements; // Fields private readonly LongSynchronizedOperation m_metadataRefreshOperation; private ProcessQueue m_measurementQueue; private List m_inputSourceIDs; private MeasurementKey[] m_requestedInputMeasurementKeys; private readonly LongSynchronizedOperation m_connectionOperation; private SharedTimer m_connectionTimer; private SharedTimer m_monitorTimer; private string m_connectionInfo; private bool m_disposed; #endregion #region [ Constructors ] /// /// Constructs a new instance of the . /// protected OutputAdapterBase() { m_metadataRefreshOperation = new LongSynchronizedOperation(ExecuteMetadataRefresh) { IsBackground = true }; m_measurementQueue = ProcessQueue.CreateRealTimeQueue(ProcessMeasurements); m_measurementQueue.ProcessException += m_measurementQueue_ProcessException; m_connectionOperation = new LongSynchronizedOperation(AttemptConnectionOperation) { IsBackground = true }; m_connectionTimer = Common.TimerScheduler.CreateTimer(2000); m_connectionTimer.Elapsed += m_connectionTimer_Elapsed; m_connectionTimer.AutoReset = false; m_connectionTimer.Enabled = false; // We monitor total number of unarchived measurements every 5 seconds - this is a useful statistic to monitor, if // total number of unarchived measurements gets very large, measurement archival could be falling behind m_monitorTimer = Common.TimerScheduler.CreateTimer(5000); m_monitorTimer.Elapsed += m_monitorTimer_Elapsed; m_monitorTimer.AutoReset = true; m_monitorTimer.Enabled = false; } #endregion #region [ Properties ] /// /// Gets or sets based data source available to this . /// public override DataSet DataSource { get { return base.DataSource; } set { base.DataSource = value; RefreshMetadata(); } } /// /// Gets or sets whether or not to automatically place measurements back into the processing /// queue if an exception occurs while processing. Defaults to false. /// [ConnectionStringParameter, Description("Defines whether or not to automatically place measurements back into the processing queue if an exception occurs while processing. Defaults to false."), DefaultValue(false)] public virtual bool RequeueOnException { get { return m_measurementQueue.RequeueOnException; } set { m_measurementQueue.RequeueOnException = value; } } /// /// Gets or sets values used to filter input measurements. /// /// /// This allows an adapter to associate itself with entire collections of measurements based on the source of the measurement keys. /// Set to null apply no filter. /// public virtual string[] InputSourceIDs { get { if ((object)m_inputSourceIDs == null) return null; return m_inputSourceIDs.ToArray(); } set { if ((object)value == null) { m_inputSourceIDs = null; } else { m_inputSourceIDs = new List(value); m_inputSourceIDs.Sort(); } // Filter measurements to list of specified source IDs LoadInputSourceIDs(this); } } /// /// 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 { return m_requestedInputMeasurementKeys; } set { m_requestedInputMeasurementKeys = value; } } /// /// Gets the flag that determines if measurements sent to this are destined for archival. /// /// /// This property allows the to calculate statistics on how many measurements have /// been archived per minute. Historians would normally set this property to true; other custom exports would set /// this property to false. /// public abstract bool OutputIsForArchive { get; } /// /// Gets the flag indicating if this implementation supports temporal processing. /// /// /// For output adapters that archive data it is assumed that the desired behavior will be to not support temporal processing /// since the data being processed has already been archived (i.e., no need to attempt to rearchive old data). As a result /// the default behavior for an output adapter is to not support temporal processing when /// is true. If you have an output adapter that you want to support temporal data processing independent of the /// value, then override this property and force the base value to the desired state. /// public override bool SupportsTemporalProcessing { get { return !OutputIsForArchive; } } /// /// Gets or sets the desired processing interval, in milliseconds, for the output 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 override int ProcessingInterval { get { return base.ProcessingInterval; } set { if (base.ProcessingInterval != value) { base.ProcessingInterval = value; bool enabled = false; bool requeueOnException = ProcessQueue.DefaultRequeueOnException; IMeasurement[] unprocessedMeasurements = null; if (m_measurementQueue != null) { enabled = m_measurementQueue.Enabled; requeueOnException = m_measurementQueue.RequeueOnException; if (m_measurementQueue.Count > 0) { m_measurementQueue.Stop(); unprocessedMeasurements = m_measurementQueue.ToArray(); } m_measurementQueue.ProcessException -= m_measurementQueue_ProcessException; m_measurementQueue.Dispose(); } if (value <= 0) { // The default processing interval is "as fast as possible" m_measurementQueue = ProcessQueue.CreateRealTimeQueue(ProcessMeasurements); } else { // Set the desired processing interval m_measurementQueue = ProcessQueue.CreateSynchronousQueue(ProcessMeasurements); m_measurementQueue.ProcessInterval = value; } m_measurementQueue.ProcessException += m_measurementQueue_ProcessException; m_measurementQueue.RequeueOnException = requeueOnException; // Requeue any existing measurements if (unprocessedMeasurements != null && unprocessedMeasurements.Length > 0) m_measurementQueue.AddRange(unprocessedMeasurements); m_measurementQueue.Enabled = enabled; } } } /// /// Gets flag that determines if the data output stream connects asynchronously. /// /// /// Derived classes should return true when data output stream connects asynchronously, otherwise return false. /// protected abstract bool UseAsyncConnect { get; } /// /// Gets or sets the connection attempt interval, in milliseconds, for the data output adapter. /// protected double ConnectionAttemptInterval { get { if (m_connectionTimer != null) return m_connectionTimer.Interval; return 2000.0D; } set { if (m_connectionTimer != null) m_connectionTimer.Interval = (int)value; } } /// /// Gets the operation that calls . /// protected LongSynchronizedOperation MetadataRefreshOperation { get { return m_metadataRefreshOperation; } } /// /// Allows derived class access to internal processing queue. /// protected ProcessQueue InternalProcessQueue { get { return m_measurementQueue; } } /// /// Returns the detailed status of the data input source. Derived classes should extend status with implementation specific information. /// public override string Status { get { const int MaxMeasurementsToShow = 10; StringBuilder status = new StringBuilder(); status.Append(base.Status); if (RequestedInputMeasurementKeys != null && RequestedInputMeasurementKeys.Length > 0) { status.AppendFormat(" Requested input keys: {0:N0} defined measurements", RequestedInputMeasurementKeys.Length); status.AppendLine(); status.AppendLine(); for (int i = 0; i < Math.Min(RequestedInputMeasurementKeys.Length, MaxMeasurementsToShow); i++) status.AppendLine(RequestedInputMeasurementKeys[i].ToString().TruncateRight(25).CenterText(50)); if (RequestedInputMeasurementKeys.Length > MaxMeasurementsToShow) status.AppendLine("...".CenterText(50)); status.AppendLine(); } status.AppendFormat(" Source ID filter list: {0}", (object)m_inputSourceIDs == null ? "[No filter applied]" : m_inputSourceIDs.ToDelimitedString(',')); status.AppendLine(); status.AppendFormat(" Asynchronous connection: {0}", UseAsyncConnect); status.AppendLine(); status.AppendFormat(" Output is for archive: {0}", OutputIsForArchive); status.AppendLine(); status.Append(m_measurementQueue.Status); return status.ToString(); } } /// /// Gets or sets output measurements that the will produce, if any. /// /// /// Redefined to hide attributes defined in the base class. /// public new virtual IMeasurement[] OutputMeasurements { get { return base.OutputMeasurements; } set { base.OutputMeasurements = value; } } #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) { try { if (disposing) { if (m_connectionTimer != null) { m_connectionTimer.Elapsed -= m_connectionTimer_Elapsed; m_connectionTimer.Dispose(); } m_connectionTimer = null; if (m_monitorTimer != null) { m_monitorTimer.Elapsed -= m_monitorTimer_Elapsed; m_monitorTimer.Dispose(); } m_monitorTimer = null; if (m_measurementQueue != null) { m_measurementQueue.ProcessException -= m_measurementQueue_ProcessException; m_measurementQueue.Dispose(); } m_measurementQueue = null; } } finally { m_disposed = true; // Prevent duplicate dispose. base.Dispose(disposing); // Call base class Dispose(). } } } /// /// Initializes . /// public override void Initialize() { base.Initialize(); Dictionary settings = Settings; string setting; // Load optional parameters if (settings.TryGetValue("inputSourceIDs", out setting) || settings.TryGetValue("sourceids", out setting)) InputSourceIDs = setting.Split(','); else InputSourceIDs = null; if (settings.TryGetValue("requeueOnException", out setting)) RequeueOnException = setting.ParseBoolean(); // Start data monitor... if (m_monitorTimer != null) m_monitorTimer.Start(); } /// /// Initiates request for metadata refresh for , if implemented. /// [AdapterCommand("Requests metadata refresh of output adapter.", "Administrator", "Editor")] [SuppressMessage("SonarQube.Miscellaneous", "S1656", Justification = "Self assignment of \"InputSourceIDs\" initiates parse and load of source IDs.")] public void RefreshMetadata() { // Force a recalculation of input measurement keys so that system can appropriately update routing tables string setting; if (Settings.TryGetValue("inputMeasurementKeys", out setting)) InputMeasurementKeys = ParseInputMeasurementKeys(DataSource, true, setting); else InputMeasurementKeys = new MeasurementKey[0]; InputSourceIDs = InputSourceIDs; m_metadataRefreshOperation.RunOnceAsync(); } /// /// Executes the metadata refresh in a synchronous fashion. /// protected virtual void ExecuteMetadataRefresh() { } /// /// Starts this and initiates connection cycle to data output stream. /// public override void Start() { base.Start(); // Start the connection cycle if (m_connectionTimer != null) m_connectionTimer.Enabled = true; // Make sure data monitor is started... if (m_monitorTimer != null && !m_monitorTimer.Enabled) m_monitorTimer.Start(); } /// /// Attempts to connect to data output stream. /// /// /// Derived classes should attempt connection to data output stream here. Any exceptions thrown /// by this implementation will result in restart of the connection cycle. /// protected abstract void AttemptConnection(); /// /// Called when data output source connection is established. /// /// /// Derived classes should call this method manually if is true. /// protected virtual void OnConnected() { // Start data processing thread if (m_measurementQueue != null) m_measurementQueue.Start(); OnStatusMessage(MessageLevel.Info, "Connection established.", "Connecting"); } /// /// Stops this and disconnects from data output stream. /// public override void Stop() { try { bool performedDisconnect = Enabled; // Stop the connection cycle if (m_connectionTimer != null) m_connectionTimer.Enabled = false; base.Stop(); // Stop data processing thread m_measurementQueue.Stop(); // Attempt disconnection from historian (e.g., consumer to call historian API disconnect function) AttemptDisconnection(); if (performedDisconnect && !UseAsyncConnect) OnDisconnected(); } catch (Exception ex) { OnProcessException(MessageLevel.Info, new InvalidOperationException($"Exception occurred during disconnect: {ex.Message}", ex)); } } /// /// Attempts to disconnect from data output stream. /// /// /// Derived classes should attempt disconnect from data output stream here. Any exceptions thrown /// by this implementation will be reported to host via event. /// protected abstract void AttemptDisconnection(); /// /// Called when data input source is disconnected. /// /// /// Derived classes should call this method manually if is true. /// protected virtual void OnDisconnected() { OnStatusMessage(MessageLevel.Info, $"Disconnected from {m_connectionInfo ?? ConnectionInfo}."); } /// /// Queues a single measurement for processing. Measurement is automatically filtered to the defined . /// /// Measurement to queue for processing. public virtual void QueueMeasurementForProcessing(IMeasurement measurement) { QueueMeasurementsForProcessing(new[] { measurement }); } /// /// Queues a collection of measurements for processing. Measurements are automatically filtered to the defined . /// /// Measurements to queue for processing. public virtual void QueueMeasurementsForProcessing(IEnumerable measurements) { if (m_disposed) return; m_measurementQueue.AddRange(measurements); IncrementProcessedMeasurements(measurements.Count()); } /// /// Serializes measurements to data output stream. /// /// /// /// Derived classes must implement this function to process queued measurements. /// For example, this function would "archive" measurements if output adapter is for a historian. /// /// /// It is important that consumers "resume" connection cycle if processing fails (e.g., connection /// to archive is lost). Here is an example: /// /// /// protected virtual void ProcessMeasurements(IMeasurement[] measurements) /// { /// try /// { /// // Process measurements... /// foreach (IMeasurement measurement in measurement) /// { /// ArchiveMeasurement(measurement); /// } /// } /// catch (Exception) /// { /// // So long as user hasn't requested to stop, restart connection cycle /// if (Enabled) /// Start(); /// } /// } /// /// /// /// protected abstract void ProcessMeasurements(IMeasurement[] measurements); /// /// This removes a range of measurements from the internal measurement queue. /// /// /// This method is typically only used to curtail size of measurement queue if it's getting too large. If more points are /// requested than there are points available - all points in the queue will be removed. /// public virtual void RemoveMeasurements(int total) { if (m_disposed) return; lock (m_measurementQueue.SyncRoot) { m_measurementQueue.RemoveRange(0, Math.Min(total, m_measurementQueue.Count)); } } /// /// Blocks the current thread, if the is connected, until all items /// in queue are processed, and then stops processing. /// /// /// /// It is possible for items to be added to the queue while the flush is executing. The flush will continue to /// process items as quickly as possible until the queue is empty. Unless the user stops queuing items to be /// processed, the flush call may never return (not a happy situation on shutdown). /// /// /// The does not clear queue prior to destruction. If the user fails to call /// this method before the class is destructed, there may be items that remain unprocessed in the queue. /// /// public virtual void Flush() { if (m_measurementQueue != null) m_measurementQueue.Flush(); } /// /// Raises the event. /// /// Total measurements in the queue that have not been processed. protected virtual void OnUnprocessedMeasurements(int unprocessedMeasurements) { try { UnprocessedMeasurements?.Invoke(this, new EventArgs(unprocessedMeasurements)); } catch (Exception ex) { // We protect our code from consumer thrown exceptions OnProcessException(MessageLevel.Info, new InvalidOperationException($"Exception in consumer handler for UnprocessedMeasurements event: {ex.Message}", ex), "ConsumerEventException"); } } private void AttemptConnectionOperation() { m_connectionInfo = null; try { // Cache connection info before possible failure in case connection switches to another target m_connectionInfo = ConnectionInfo; // So long as user hasn't requested to stop, attempt connection if (Enabled) { OnStatusMessage(MessageLevel.Info, $"Attempting connection{(m_connectionInfo is null ? "" : $" to {m_connectionInfo}")}..."); // Attempt connection to data output adapter (e.g., call historian API connect function). AttemptConnection(); // Try to update connection info after successful connection attempt in case info is now available if (m_connectionInfo is null) m_connectionInfo = ConnectionInfo; if (!UseAsyncConnect) OnConnected(); } } catch (Exception ex) { OnProcessException(MessageLevel.Info, new ConnectionException($"Connection attempt failed{(m_connectionInfo is null ? "" : $" for {m_connectionInfo}")}: {ex.Message}", ex)); // So long as user hasn't requested to stop, keep trying connection if (Enabled) Start(); } } private void m_connectionTimer_Elapsed(object sender, EventArgs e) { m_connectionOperation.TryRunOnceAsync(); } // All we do here is expose the total number of unarchived measurements in the queue private void m_monitorTimer_Elapsed(object sender, EventArgs e) { OnUnprocessedMeasurements(m_measurementQueue.Count); } // Bubble any exceptions occurring in the process queue to the base class event private void m_measurementQueue_ProcessException(object sender, EventArgs e) { OnProcessException(MessageLevel.Info, e.Argument); } #endregion } }