//****************************************************************************************************** // IaonSession.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: // ---------------------------------------------------------------------------------------------------- // 08/23/2011 - J. Ritchie Carroll // Generated original version of source code. // 12/20/2012 - Starlynn Danyelle Gilliam // Modified Header. // //****************************************************************************************************** using System; using System.Data; using System.Linq; using System.Text; using GSF.Annotations; using GSF.Collections; using GSF.Configuration; using GSF.Diagnostics; using System.Collections.Generic; // ReSharper disable InconsistentlySynchronizedField namespace GSF.TimeSeries.Adapters { /// /// Represents a new Input, Action, Output interface session. /// public class IaonSession : IProvideStatus, IDisposable { #region [ Members ] // Events /// /// Provides status messages to consumer. /// /// /// is the new status message.
/// is the 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 every five seconds allowing consumer to track current number of unpublished seconds of data in the queue. /// /// /// is the total number of unpublished seconds of data. /// public event EventHandler> UnpublishedSamples; /// /// Event is raised every five seconds allowing host to track total number of unprocessed measurements. /// /// /// /// Each implementation reports its 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; /// /// Indicates to the host that processing for one of the input adapters has completed. /// /// /// This event is expected to only be raised when an input adapter has been designed to process /// a finite amount of data, e.g., reading a historical range of data during temporal processing. /// public event EventHandler ProcessingComplete; /// /// Event is raised when this is disposed or an in the collection is disposed. /// public event EventHandler Disposed; // Fields private Guid m_nodeID; private RoutingTables m_routingTables; private AllAdaptersCollection m_allAdapters; private FilterAdapterCollection m_filterAdapters; private InputAdapterCollection m_inputAdapters; private ActionAdapterCollection m_actionAdapters; private OutputAdapterCollection m_outputAdapters; private MeasurementKey[] m_inputMeasurementKeysRestriction; private readonly int m_measurementWarningThreshold; private readonly int m_measurementDumpingThreshold; private readonly int m_defaultSampleSizeWarningThreshold; private string m_name; private bool m_disposed; #endregion #region [ Constructors ] /// /// Creates a new . /// public IaonSession() { ConfigurationFile configFile = ConfigurationFile.Current; // Initialize system settings CategorizedSettingsElementCollection systemSettings = configFile.Settings["systemSettings"]; systemSettings.Add("NodeID", Guid.NewGuid().ToString(), "Unique Node ID"); m_nodeID = systemSettings["NodeID"].ValueAs(); // Initialize threshold settings CategorizedSettingsElementCollection thresholdSettings = configFile.Settings["thresholdSettings"]; thresholdSettings.Add("MeasurementWarningThreshold", "100000", "Number of unarchived measurements allowed in any output adapter queue before displaying a warning message"); thresholdSettings.Add("MeasurementDumpingThreshold", "500000", "Number of unarchived measurements allowed in any output adapter queue before taking evasive action and dumping data"); thresholdSettings.Add("DefaultSampleSizeWarningThreshold", "10", "Default number of unpublished samples (in seconds) allowed in any action adapter queue before displaying a warning message"); m_measurementWarningThreshold = thresholdSettings["MeasurementWarningThreshold"].ValueAsInt32(); m_measurementDumpingThreshold = thresholdSettings["MeasurementDumpingThreshold"].ValueAsInt32(); m_defaultSampleSizeWarningThreshold = thresholdSettings["DefaultSampleSizeWarningThreshold"].ValueAsInt32(); using (Logger.AppendStackMessages("HostAdapter","IaonSession")) { // Create a new set of routing tables switch (OptimizationOptions.DefaultRoutingMethod) { case OptimizationOptions.RoutingMethod.HighLatencyLowCpu: m_routingTables = new RoutingTables(new RouteMappingHighLatencyLowCpu()); break; default: m_routingTables = new RoutingTables(); break; } } m_routingTables.StatusMessage += m_routingTables_StatusMessage; m_routingTables.ProcessException += m_routingTables_ProcessException; // Create a collection to manage all input, action and output adapter collections as a unit m_allAdapters = new AllAdaptersCollection(); // Attach to common adapter events m_allAdapters.StatusMessage += StatusMessageHandler; m_allAdapters.ProcessException += ProcessExceptionHandler; m_allAdapters.InputMeasurementKeysUpdated += InputMeasurementKeysUpdatedHandler; m_allAdapters.OutputMeasurementsUpdated += OutputMeasurementsUpdatedHandler; m_allAdapters.ConfigurationChanged += ConfigurationChangedHandler; m_allAdapters.Disposed += DisposedHandler; // Create filter adapters collection m_filterAdapters = new FilterAdapterCollection(); // Create input adapters collection m_inputAdapters = new InputAdapterCollection(); m_inputAdapters.NewMeasurements += NewMeasurementsHandler; m_inputAdapters.ProcessingComplete += ProcessingCompleteHandler; // Create action adapters collection m_actionAdapters = new ActionAdapterCollection(); m_actionAdapters.NewMeasurements += NewMeasurementsHandler; m_actionAdapters.UnpublishedSamples += UnpublishedSamplesHandler; m_actionAdapters.RequestTemporalSupport += RequestTemporalSupportHandler; // Create output adapters collection m_outputAdapters = new OutputAdapterCollection(); m_outputAdapters.UnprocessedMeasurements += UnprocessedMeasurementsHandler; // Associate adapter collections with routing tables m_routingTables.InputAdapters = m_inputAdapters; m_routingTables.ActionAdapters = m_actionAdapters; m_routingTables.OutputAdapters = m_outputAdapters; // We group these adapters such that they are initialized in the following order: output, filter, input, action. // This is done so that the archival capabilities will be setup before we start receiving input and the input // data will be flowing before any actions get established for the input - at least generally. m_allAdapters.Add(m_outputAdapters); m_allAdapters.Add(m_filterAdapters); m_allAdapters.Add(m_inputAdapters); m_allAdapters.Add(m_actionAdapters); } /// /// Releases the unmanaged resources before the object is reclaimed by . /// ~IaonSession() => Dispose(false); #endregion #region [ Properties ] /// /// Gets the all adapters collection for this . /// public virtual AllAdaptersCollection AllAdapters => m_allAdapters; /// /// Gets the filter adapter collection for this . /// public virtual FilterAdapterCollection FilterAdapters => m_filterAdapters; /// /// Gets the input adapter collection for this . /// public virtual InputAdapterCollection InputAdapters => m_inputAdapters; /// /// Gets the action adapter collection for this . /// public virtual ActionAdapterCollection ActionAdapters => m_actionAdapters; /// /// Gets the output adapter collection for this . /// public virtual OutputAdapterCollection OutputAdapters => m_outputAdapters; /// /// Gets the routing tables for this . /// public virtual RoutingTables RoutingTables => m_routingTables; /// /// Gets or sets a routing table restriction for a collection of input measurement keys. /// public virtual MeasurementKey[] InputMeasurementKeysRestriction { get => m_inputMeasurementKeysRestriction; set => m_inputMeasurementKeysRestriction = value; } /// /// Gets or sets the configuration for this . /// public virtual DataSet DataSource { get => m_allAdapters.DataSource; set => m_allAdapters.DataSource = value; } /// /// Gets or sets the node ID for this . /// public virtual Guid NodeID { get => m_nodeID; set => m_nodeID = value; } /// /// Gets name assigned to this , if any. /// public virtual string Name { get => m_name.ToNonNullString(); set => m_name = value; } /// /// Gets the combined status of the adapters in this . /// public virtual string Status { get { StringBuilder status = new StringBuilder(); status.AppendLine(); status.AppendLine(">> Filter Adapters:"); status.AppendLine(); if (!(m_filterAdapters is null)) status.AppendLine(m_filterAdapters.Status); status.AppendLine(); status.AppendLine(">> Input Adapters:"); status.AppendLine(); if (!(m_inputAdapters is null)) status.AppendLine(m_inputAdapters.Status); status.AppendLine(); status.AppendLine(">> Action Adapters:"); status.AppendLine(); if (!(m_actionAdapters is null)) status.AppendLine(m_actionAdapters.Status); status.AppendLine(); status.AppendLine(">> Output Adapters:"); status.AppendLine(); if (!(m_outputAdapters is null)) status.AppendLine(m_outputAdapters.Status); 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) return; try { if (!disposing) return; DataSet dataSource = DataSource; // Dispose filter adapters collection if (!(m_filterAdapters is null)) { m_filterAdapters.Stop(); m_filterAdapters.Dispose(); } m_filterAdapters = null; // Dispose input adapters collection if (!(m_inputAdapters is null)) { m_inputAdapters.Stop(); m_inputAdapters.NewMeasurements -= NewMeasurementsHandler; m_inputAdapters.ProcessingComplete -= ProcessingCompleteHandler; m_inputAdapters.Dispose(); } m_inputAdapters = null; // Dispose action adapters collection if (!(m_actionAdapters is null)) { m_actionAdapters.Stop(); m_actionAdapters.NewMeasurements -= NewMeasurementsHandler; m_actionAdapters.UnpublishedSamples -= UnpublishedSamplesHandler; m_actionAdapters.RequestTemporalSupport -= RequestTemporalSupportHandler; m_actionAdapters.Dispose(); } m_actionAdapters = null; // Dispose output adapters collection if (!(m_outputAdapters is null)) { m_outputAdapters.Stop(); m_outputAdapters.UnprocessedMeasurements -= UnprocessedMeasurementsHandler; m_outputAdapters.Dispose(); } m_outputAdapters = null; // Dispose all adapters collection if (!(m_allAdapters is null)) { m_allAdapters.StatusMessage -= StatusMessageHandler; m_allAdapters.ProcessException -= ProcessExceptionHandler; m_allAdapters.InputMeasurementKeysUpdated -= InputMeasurementKeysUpdatedHandler; m_allAdapters.OutputMeasurementsUpdated -= OutputMeasurementsUpdatedHandler; m_allAdapters.ConfigurationChanged -= ConfigurationChangedHandler; m_allAdapters.Disposed -= DisposedHandler; m_allAdapters.Dispose(); } m_allAdapters = null; // Dispose of routing tables if (!(m_routingTables is null)) { m_routingTables.StatusMessage -= m_routingTables_StatusMessage; m_routingTables.ProcessException -= m_routingTables_ProcessException; m_routingTables.Dispose(); } m_routingTables = null; dataSource?.Dispose(); } finally { m_disposed = true; // Prevent duplicate dispose. Disposed?.Invoke(this, EventArgs.Empty); } } /// /// Initialize and start adapters. /// /// Sets flag that determines if adapters should be automatically started. public virtual void Initialize(bool autoStart = true) { // Initialize all adapters m_allAdapters.Initialize(); // Start all adapters if they // haven't started already if (autoStart) m_allAdapters.Start(); } /// /// Gets flag that determines if temporal processing is supported in this . /// /// Name of collection over which to check support (e.g., "InputAdapters"); or null for all collections. /// Flag that determines if temporal processing is supported in this . public virtual bool TemporalProcessingSupportExists(string collection = null) { try { DataTable temporalSupport = m_allAdapters?.DataSource?.Tables["TemporalSupport"]; if (temporalSupport is null) return false; if (string.IsNullOrWhiteSpace(collection)) return temporalSupport.Rows.Count > 0; return temporalSupport.Select($"Source = '{collection}'").Length > 0; } catch (Exception ex) { Logger.SwallowException(ex, "Ignored Exception", "Failures are non-catastrophic and should only result in false."); return false; } } /// /// Gets derived name of specified object. /// /// Sending object from which to derive name. /// Derived name of specified object. public virtual string GetDerivedName(object sender) { string name = null; switch (sender) { case IProvideStatus statusProvider: name = statusProvider.Name; break; case string nameValue: name = nameValue; break; } if (string.IsNullOrWhiteSpace(name)) name = sender.GetType().Name; if (!string.IsNullOrWhiteSpace(m_name)) name += "#" + m_name; return name; } /// /// Recalculates routing tables as long as all adapters have been initialized. /// public virtual void RecalculateRoutingTables() { if (m_routingTables is null || m_allAdapters is null || !m_allAdapters.Initialized) return; m_routingTables.CalculateRoutingTables(m_inputMeasurementKeysRestriction); } /// /// Raises the event. /// /// Object source raising the event. /// New status message. /// of status message. protected virtual void OnStatusMessage(object sender, string status, UpdateType type = UpdateType.Information) { if (StatusMessage is null) return; // When using default informational update type, see if an update type code was embedded in the status message - this allows for compatibility for event // handlers that are normally unaware of the update type if (type == UpdateType.Information && !(status is null) && status.Length > 3 && status.StartsWith("0x") && Enum.TryParse(status[2].ToString(), out type)) status = status.Substring(3); StatusMessage(sender, new EventArgs(status, type)); } /// /// Raises the event with a formatted status message. /// /// Object source raising the event. /// Formatted status message. /// of status message. /// Arguments for . /// /// This overload combines string.Format and SendStatusMessage for convenience. /// [StringFormatMethod("formattedStatus")] protected virtual void OnStatusMessage(object sender, string formattedStatus, UpdateType type, params object[] args) { if (!(StatusMessage is null)) OnStatusMessage(sender, string.Format(formattedStatus, args), type); } /// /// Raises event. /// /// Object source raising the event. /// Processing . protected virtual void OnProcessException(object sender, Exception ex) => ProcessException?.Invoke(sender, new EventArgs(ex)); /// /// Raises event. /// /// Object source raising the event. protected virtual void OnInputMeasurementKeysUpdated(object sender) => InputMeasurementKeysUpdated?.Invoke(sender, EventArgs.Empty); /// /// Raises event. /// /// Object source raising the event. protected virtual void OnOutputMeasurementsUpdated(object sender) => OutputMeasurementsUpdated?.Invoke(sender, EventArgs.Empty); /// /// Raises event. /// /// Object source raising the event. protected virtual void OnConfigurationChanged(object sender) => ConfigurationChanged?.Invoke(sender, EventArgs.Empty); /// /// Raises the event. /// /// Object source raising the event. /// Total number of unpublished seconds of data. protected virtual void OnUnpublishedSamples(object sender, int seconds) => UnpublishedSamples?.Invoke(sender, new EventArgs(seconds)); /// /// Raises the event. /// /// Object source raising the event. /// Total measurements in the queue that have not been processed. protected virtual void OnUnprocessedMeasurements(object sender, int unprocessedMeasurements) => UnprocessedMeasurements?.Invoke(sender, new EventArgs(unprocessedMeasurements)); /// /// Raises the event. /// /// Object source raising the event. /// , if any. protected virtual void OnProcessingComplete(object sender, EventArgs e = null) => ProcessingComplete?.Invoke(sender, e ?? EventArgs.Empty); /// /// Raises the event. /// /// Object source raising the event. protected virtual void OnDisposed(object sender) => Disposed?.Invoke(sender, EventArgs.Empty); /// /// Event handler for reporting status messages. /// /// Event source of the status message. /// Event arguments containing the status message to report. public virtual void StatusMessageHandler(object sender, EventArgs e) => // Bubble message up to any event subscribers OnStatusMessage(sender, "[{0}] {1}", UpdateType.Information, GetDerivedName(sender), e.Argument); /// /// Event handler for processing reported exceptions. /// /// Event source of the exception. /// Event arguments containing the exception to report. public virtual void ProcessExceptionHandler(object sender, EventArgs e) { OnStatusMessage(sender, "[{0}] {1}", UpdateType.Alarm, GetDerivedName(sender), e.Argument.Message); // Bubble message up to any event subscribers OnProcessException(sender, e.Argument); } /// /// Event handler for updates to adapter input measurement key definitions. /// /// Sending object. /// Event arguments, if any. public virtual void InputMeasurementKeysUpdatedHandler(object sender, EventArgs e) { // When adapter measurement keys are dynamically updated, routing tables need to be updated RecalculateRoutingTables(); // Bubble message up to any event subscribers OnInputMeasurementKeysUpdated(sender); } /// /// Event handler for updates to adapter output measurement definitions. /// /// Sending object. /// Event arguments, if any. public virtual void OutputMeasurementsUpdatedHandler(object sender, EventArgs e) { // When adapter measurement keys are dynamically updated, routing tables need to be updated RecalculateRoutingTables(); // Bubble message up to any event subscribers OnOutputMeasurementsUpdated(sender); } /// /// Event handler for adapter notifications about configuration changes. /// /// Sending object. /// Event arguments, if any. public virtual void ConfigurationChangedHandler(object sender, EventArgs e) => // Bubble message up to any event subscribers OnConfigurationChanged(sender); /// /// Event handler for monitoring unpublished samples. /// /// Event source reference to adapter, typically an action adapter, that is reporting the number of unpublished data samples. /// Event arguments containing number of samples, in seconds of data, of unpublished data in the source adapter. /// /// Time-series framework uses this handler to monitor the number of unpublished samples, in seconds of data, in action adapters.
/// This method is typically called once every five seconds. ///
public virtual void UnpublishedSamplesHandler(object sender, EventArgs e) { int secondsOfData = e.Argument; int threshold = m_defaultSampleSizeWarningThreshold; int processingInterval = -1; // Most action adapters will be based on a concentrator, if so we monitor the unpublished sample queue size compared to the defined // lag time - if the queue size is over twice the lag size, the action adapter could be falling behind if (sender is ConcentratorBase concentrator) threshold = (int)(2 * Math.Ceiling(concentrator.LagTime)); // Get processing interval for adapter if (sender is IAdapter adapter) processingInterval = adapter.ProcessingInterval; // Allow much more time before warning for when a fast processing interval has been defined if (processingInterval > -1 && processingInterval < 100) threshold *= 4; if (secondsOfData > threshold) OnStatusMessage(sender, "[{0}] There are {1} seconds of unpublished data in the action adapter concentration queue.", UpdateType.Warning, GetDerivedName(sender), secondsOfData); // Bubble message up to any event subscribers OnUnpublishedSamples(sender, e.Argument); } /// /// Event handler for requesting temporal support. /// /// Event source reference to adapter collection, typically an action adapter collection, that is requesting temporal support. /// Event arguments are not used. /// /// Action adapter collections use this handler to make sure temporal support is initialized before setting up temporal sessions. /// public virtual void RequestTemporalSupportHandler(object sender, EventArgs e) { lock (s_requestTemporalSupportLock) { if (m_allAdapters.DataSource.Tables.Contains("TemporalSupport")) return; // Create a new temporal support identification table DataTable temporalSupport = new DataTable("TemporalSupport"); temporalSupport.Columns.Add("Source", typeof(string)); temporalSupport.Columns.Add("ID", typeof(uint)); // Add rows for each Iaon adapter collection to identify which adapters support temporal processing lock (m_allAdapters) { foreach (IAdapterCollection collection in m_allAdapters) { foreach (IAdapter adapter in collection.Where(adapter => adapter.SupportsTemporalProcessing)) temporalSupport.Rows.Add(collection.DataMember, adapter.ID); } m_allAdapters.DataSource.Tables.Add(temporalSupport.Copy()); } } } /// /// Event handler for monitoring unprocessed measurements. /// /// Event source reference to adapter, typically an output adapter, that is reporting the number of unprocessed measurements. /// Event arguments containing number of queued (i.e., unprocessed) measurements in the source adapter. /// /// Time-series framework uses this handler to monitor the number of unprocessed measurements in output adapters.
/// This method is typically called once every five seconds. ///
public virtual void UnprocessedMeasurementsHandler(object sender, EventArgs e) { int unprocessedMeasurements = e.Argument; if (unprocessedMeasurements > m_measurementDumpingThreshold) { if (sender is IOutputAdapter outputAdapter) { // If an output adapter queue size exceeds the defined measurement dumping threshold, // then the queue will be truncated before system runs out of memory outputAdapter.RemoveMeasurements(m_measurementDumpingThreshold); OnStatusMessage(sender, "[{0}] System exercised evasive action to conserve memory and dumped {1:N0} unprocessed measurements from the output queue :(", UpdateType.Alarm, outputAdapter.Name, m_measurementDumpingThreshold); OnStatusMessage(sender, "[{0}] NOTICE: Adapter may be offline or processing data too slowly to keep up with incoming data volume. It may be necessary to adjust measurement threshold configuration settings and/or increase amount of available system memory.", UpdateType.Warning, outputAdapter.Name); } else { // It is only expected that output adapters will be mapped to this handler, but in case // another adapter type uses this handler we will still display a message OnStatusMessage(sender, "[{0}] CRITICAL: There are {1:N0} unprocessed measurements in the adapter queue - but sender \"{2}\" is not an IOutputAdapter, so no evasive action can be exercised.", UpdateType.Warning, GetDerivedName(sender), unprocessedMeasurements, sender.GetType().Name); } } else if (unprocessedMeasurements > m_measurementWarningThreshold) { if (unprocessedMeasurements >= m_measurementDumpingThreshold - m_measurementWarningThreshold) OnStatusMessage(sender, "[{0}] CRITICAL: There are {1:N0} unprocessed measurements in the output queue.", UpdateType.Warning, GetDerivedName(sender), unprocessedMeasurements); else OnStatusMessage(sender, "[{0}] There are {1:N0} unprocessed measurements in the output queue.", UpdateType.Warning, GetDerivedName(sender), unprocessedMeasurements); } // Bubble message up to any event subscribers OnUnprocessedMeasurements(sender, e.Argument); } /// /// Event handler for new measurement notifications from input adapters and action adapters. /// /// Event source reference to the adapter that is reporting new measurements. /// Event arguments for event that contains references to the new measurements. public virtual void NewMeasurementsHandler(object sender, EventArgs> e) => m_filterAdapters?.HandleNewMeasurements(e.Argument); /// /// Event handler for processing complete notifications from input adapters. /// /// Event source reference to input adapter that is reporting processing completion. /// Event arguments for event, if any; otherwise . public virtual void ProcessingCompleteHandler(object sender, EventArgs e) { OnStatusMessage(sender, "[{0}] Processing completed.", UpdateType.Information, GetDerivedName(sender)); // Bubble message up to any event subscribers OnProcessingComplete(sender, e); } /// /// Event handler for disposed events from all adapters. /// /// Sending object. /// Event arguments, if any. public virtual void DisposedHandler(object sender, EventArgs e) { OnStatusMessage(sender, "[{0}] Disposed.", UpdateType.Information, GetDerivedName(sender)); // Bubble message up to any event subscribers OnDisposed(sender); } private void m_routingTables_StatusMessage(object sender, EventArgs e) => // Bubble routing table messages out through Iaon session OnStatusMessage(this, e.Argument); private void m_routingTables_ProcessException(object sender, EventArgs e) => // Bubble routing table exceptions out through Iaon session ProcessExceptionHandler(sender, e); #endregion #region [ Static ] // Static Fields private static readonly object s_requestTemporalSupportLock = new object(); private static DataSet s_currentRealTimeConfiguration; private static DataSet s_currentTemporalConfiguration; // Static Methods /// /// Extracts a configuration that supports temporal processing from an existing real-time configuration. /// /// Real-time configuration. /// A new configuration for adapters that support temporal processing. public static DataSet ExtractTemporalConfiguration(DataSet realtimeConfiguration) { lock (s_requestTemporalSupportLock) { // Since the same real-time configuration is shared among all adapters we can return // existing cached temporal configuration if real-time configuration hasn't changed if (!(s_currentRealTimeConfiguration is null) && !(s_currentTemporalConfiguration is null) && ReferenceEquals(s_currentRealTimeConfiguration, realtimeConfiguration)) return s_currentTemporalConfiguration; try { // Duplicate current run-time session configuration that has temporal support DataSet temporalConfiguration = new DataSet("IaonTemporal"); DataTable temporalSupport = realtimeConfiguration.Tables["TemporalSupport"]; foreach (DataTable table in realtimeConfiguration.Tables) { string tableName = table.TableName; switch (tableName.ToLower()) { case "inputadapters": case "actionadapters": case "outputadapters": // For Iaon adapter tables, we only copy in adapters that support temporal processing temporalConfiguration.Tables.Add(table.Clone()); // Check for adapters with temporal support in this adapter collection DataRow[] temporalAdapters = temporalSupport?.Select($"Source = '{tableName}'") ?? Array.Empty(); // If any adapters support temporal processing, add them to the temporal configuration if (temporalAdapters.Length > 0) { DataTable realtimeTable = realtimeConfiguration.Tables[tableName]; DataTable temporalTable = temporalConfiguration.Tables[tableName]; foreach (DataRow row in realtimeTable.Select($"ID IN ({temporalAdapters.Select(row => row["ID"].ToString()).ToDelimitedString(',')})")) { DataRow newRow = temporalTable.NewRow(); for (int i = 0; i < realtimeTable.Columns.Count; i++) newRow[i] = row[i]; temporalConfiguration.Tables[tableName].Rows.Add(newRow); } } break; default: // For all other tables we add configuration information as-is temporalConfiguration.Tables.Add(table.Copy()); break; } } // Cache new temporal configuration s_currentRealTimeConfiguration = realtimeConfiguration; s_currentTemporalConfiguration = temporalConfiguration; return temporalConfiguration; } catch { s_currentRealTimeConfiguration = null; s_currentTemporalConfiguration = null; throw; } } } #endregion } }