//****************************************************************************************************** // OutputAdapterBase.cs - Gbtc // // Copyright © 2010, 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 Eclipse Public License -v 1.0 (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/eclipse-1.0.php // // 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. // //****************************************************************************************************** using GSF.Collections; using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.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 ProcessQueue m_measurementQueue; private List m_inputSourceIDs; private MeasurementKey[] m_requestedInputMeasurementKeys; private System.Timers.Timer m_connectionTimer; private System.Timers.Timer m_monitorTimer; private bool m_disposed; #endregion #region [ Constructors ] /// /// Constructs a new instance of the . /// protected OutputAdapterBase() { m_measurementQueue = ProcessQueue.CreateRealTimeQueue(StartMeasurementProcessing); m_measurementQueue.ProcessException += m_measurementQueue_ProcessException; m_connectionTimer = new System.Timers.Timer(); m_connectionTimer.Elapsed += m_connectionTimer_Elapsed; m_connectionTimer.AutoReset = false; m_connectionTimer.Interval = 2000; m_connectionTimer.Enabled = false; m_monitorTimer = new System.Timers.Timer(); m_monitorTimer.Elapsed += m_monitorTimer_Elapsed; // 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.Interval = 5000; m_monitorTimer.AutoReset = true; m_monitorTimer.Enabled = false; } #endregion #region [ Properties ] /// /// Gets or sets whether or not to automatically place measurements back into the processing /// queue if an exception occurs while processing. Defaults to false. /// /// /// Note that items being requeued will be added to the bottom of queue by default. /// 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 (m_inputSourceIDs == null) return null; return m_inputSourceIDs.ToArray(); } set { if (value == null) { m_inputSourceIDs = null; } else { m_inputSourceIDs = new List(value); m_inputSourceIDs.Sort(); } // Filter measurements to list of specified source IDs AdapterBase.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, overwhich 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(StartMeasurementProcessing); } else { // Set the desired processing interval m_measurementQueue = ProcessQueue.CreateSynchronousQueue(StartMeasurementProcessing); 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 = value; } } /// /// 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} defined measurements", RequestedInputMeasurementKeys.Length); status.AppendLine(); status.AppendLine(); for (int i = 0; i < Common.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}", (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.AppendFormat(" Item reporting interval: {0}", MeasurementReportingInterval); status.AppendLine(); status.Append(m_measurementQueue.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) { 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; // 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.")] public virtual 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 = AdapterBase.ParseInputMeasurementKeys(DataSource, setting); else InputMeasurementKeys = null; InputSourceIDs = InputSourceIDs; } /// /// 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 input 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("Connection established."); } /// /// 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 (ThreadAbortException) { // This exception can be safely ignored... } catch (Exception ex) { OnProcessException(new InvalidOperationException(string.Format("Exception occured during disconnect: {0}", 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("Disconnected."); } /// /// 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 IMeasurement[] { 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; if (!ProcessMeasurementFilter || InputMeasurementKeys == null) { // No further filtering of incoming measurement required m_measurementQueue.AddRange(measurements); IncrementProcessedMeasurements(measurements.Count()); } else { // Filter measurements down to specified input measurements List filteredMeasurements = new List(); foreach (IMeasurement measurement in measurements) { if (IsInputMeasurement(measurement.Key)) filteredMeasurements.Add(measurement); } if (filteredMeasurements.Count > 0) { m_measurementQueue.AddRange(filteredMeasurements); IncrementProcessedMeasurements(filteredMeasurements.Count()); } } } // Proxy function to process measurements after waiting for any external events, if needed private void StartMeasurementProcessing(IMeasurement[] measurements) { // Wait for any defined external events WaitForExternalEvents(); // Call user measurement processing function ProcessMeasurements(measurements); } /// /// 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; if (total > m_measurementQueue.Count) total = m_measurementQueue.Count; IMeasurement measurement; for (int i = 0; i < total; i++) { m_measurementQueue.TryTake(out measurement); } } /// /// 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 queueing items to be /// processed, the flush call may never return (not a happy situtation 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 { if (UnprocessedMeasurements != null) UnprocessedMeasurements(this, new EventArgs(unprocessedMeasurements)); } catch (Exception ex) { // We protect our code from consumer thrown exceptions OnProcessException(new InvalidOperationException(string.Format("Exception in consumer handler for UnprocessedMeasurements event: {0}", ex.Message), ex)); } } /// /// Raises event. /// /// Processing . protected override void OnProcessException(Exception ex) { base.OnProcessException(ex); } private void m_connectionTimer_Elapsed(object sender, System.Timers.ElapsedEventArgs e) { try { // So long as user hasn't requested to stop, attempt connection if (Enabled) { OnStatusMessage("Attempting connection..."); // Attempt connection to data output adapter (e.g., call historian API connect function). AttemptConnection(); if (!UseAsyncConnect) OnConnected(); } } catch (ThreadAbortException) { // This exception can be safely ignored... } catch (Exception ex) { OnProcessException(new InvalidOperationException(string.Format("Connection attempt failed: {0}", ex.Message), ex)); // So long as user hasn't requested to stop, keep trying connection if (Enabled) Start(); } } // All we do here is expose the total number of unarchived measurements in the queue private void m_monitorTimer_Elapsed(object sender, System.Timers.ElapsedEventArgs e) { OnUnprocessedMeasurements(m_measurementQueue.Count); } // Bubble any exceptions occuring in the process queue to the base class event private void m_measurementQueue_ProcessException(object sender, EventArgs e) { OnProcessException(e.Argument); } #endregion } }