//******************************************************************************************************
// 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
}
}