//******************************************************************************************************
// RouteMappingDoubleBufferQueue.cs - Gbtc
//
// Copyright © 2016, 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:
// ----------------------------------------------------------------------------------------------------
// 06/29/2016 - Steven E. Chisholm
// Generated original version of source code.
// Derived from code existing in RoutingTables.cs
//
//******************************************************************************************************
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Threading;
using GSF.Collections;
namespace GSF.TimeSeries.Adapters
{
///
/// The standard and default routing table that uses double buffer queues to route measurements.
///
public class RouteMappingDoubleBufferQueue
: IRouteMappingTables
{
// Nested Types
private class GlobalCache
{
public Dictionary> GlobalSignalLookup;
public Dictionary GlobalDestinationLookup;
public List BroadcastConsumers;
public int Version;
public GlobalCache(Dictionary consumers, int version)
{
GlobalSignalLookup = new Dictionary>();
GlobalDestinationLookup = consumers;
BroadcastConsumers = new List();
Version = version;
// Generate routes for all signals received by each consumer adapter
foreach (var kvp in consumers)
{
var consumerAdapter = kvp.Key;
var consumer = kvp.Value;
if ((object)consumerAdapter.InputMeasurementKeys != null)
{
// Create routes for each of the consumer's input signals
foreach (Guid signalID in consumerAdapter.InputMeasurementKeys.Select(key => key.SignalID))
{
GlobalSignalLookup.GetOrAdd(signalID, id => new List()).Add(consumer);
}
}
else
{
// Add this consumer to the broadcast routes to begin receiving all measurements
BroadcastConsumers.Add(consumer);
}
}
// Broadcast consumers receive all measurements, so add them to every signal route
foreach (List consumerList in GlobalSignalLookup.Values)
{
consumerList.AddRange(BroadcastConsumers);
}
}
}
private class LocalCache
{
private Dictionary> m_localSignalLookup;
private Dictionary m_localDestinationLookup;
private RouteMappingDoubleBufferQueue m_routingTables;
private object m_localCacheLock;
private int m_version;
public LocalCache(RouteMappingDoubleBufferQueue routingTables, IAdapter producerAdapter)
{
m_localCacheLock = new object();
m_localSignalLookup = new Dictionary>();
m_localDestinationLookup = new Dictionary();
m_routingTables = routingTables;
IInputAdapter inputAdapter = producerAdapter as IInputAdapter;
IActionAdapter actionAdapter = producerAdapter as IActionAdapter;
if ((object)inputAdapter != null)
inputAdapter.NewMeasurements += Route;
else if ((object)actionAdapter != null)
actionAdapter.NewMeasurements += Route;
}
public void Route(object sender, EventArgs> e)
{
ICollection measurements = e?.Argument;
if (measurements == null)
return;
GlobalCache globalCache;
List producers;
List consumers;
// Get the global cache from the routing tables
globalCache = Interlocked.CompareExchange(ref m_routingTables.m_globalCache, null, null);
// Return if routes are still being calculated
if ((object)globalCache == null)
return;
lock (m_localCacheLock)
{
// Check the version of the local cache against that of the global cache.
// We need to clear the local cache if the versions don't match because
// that means routes have changed
if (m_version != globalCache.Version)
{
// Dump the signal lookup
m_localSignalLookup.Clear();
// Best if we hang onto producers for adapters that still have routes
foreach (Consumer consumer in m_localDestinationLookup.Keys.Where(consumer => !globalCache.GlobalDestinationLookup.ContainsKey(consumer.Adapter)).ToList())
{
m_localDestinationLookup[consumer].QueueProducer.Dispose();
m_localDestinationLookup.Remove(consumer);
}
// Update the local cache version
m_version = globalCache.Version;
}
foreach (IMeasurement measurement in measurements)
{
// Attempt to look up the signal in the local cache
if (!m_localSignalLookup.TryGetValue(measurement.ID, out producers))
{
// Not in the local cache - check the global cache and fall back on broadcast consumers
if (!globalCache.GlobalSignalLookup.TryGetValue(measurement.ID, out consumers))
consumers = globalCache.BroadcastConsumers;
// Get a producer for each of the consumers
producers = consumers
.Select(consumer => m_localDestinationLookup.GetOrAdd(consumer, c => new Producer(c.Manager)))
.ToList();
// Add this signal to the local cache
m_localSignalLookup.Add(measurement.ID, producers);
}
// Add this measurement to the producers' list
foreach (Producer producer in producers)
producer.Measurements.Add(measurement);
}
// Produce measurements to consumers in the local destination
// cache which have measurements to be received
foreach (Producer producer in m_localDestinationLookup.Values)
{
if (producer.Measurements.Count > 0)
{
producer.QueueProducer.Produce(producer.Measurements);
producer.Measurements.Clear();
}
}
}
}
}
private class Producer
{
[SuppressMessage("SonarQube.Immutability", "S3887", Justification = "Internal use only. Reference marked as read-only for future code update safety.")]
public readonly List Measurements;
public readonly DoubleBufferedQueueProducer QueueProducer;
public Producer(DoubleBufferedQueueManager manager)
{
Measurements = new List();
QueueProducer = manager.GetProducer();
}
}
private class Consumer
{
public readonly IAdapter Adapter;
public readonly DoubleBufferedQueueManager Manager;
public Consumer(IAdapter adapter, Action exceptionAction)
{
IActionAdapter actionAdapter = adapter as IActionAdapter;
IOutputAdapter outputAdapter;
Adapter = adapter;
if ((object)actionAdapter != null)
{
Manager = new DoubleBufferedQueueManager(measurements => actionAdapter.QueueMeasurementsForProcessing(new List(measurements)), exceptionAction);
}
else
{
outputAdapter = adapter as IOutputAdapter;
if ((object)outputAdapter != null)
Manager = new DoubleBufferedQueueManager(measurements => outputAdapter.QueueMeasurementsForProcessing(new List(measurements)), exceptionAction);
else
Manager = new DoubleBufferedQueueManager(() => { });
}
}
}
private GlobalCache m_globalCache;
private Action m_onStatusMessage;
private Action m_onProcessException;
private LocalCache m_injectMeasurementsLocalCache;
///
/// Instances a new .
///
public RouteMappingDoubleBufferQueue()
{
m_onStatusMessage = x => { };
m_onProcessException = x => { };
m_globalCache = new GlobalCache(new Dictionary(), 0);
m_injectMeasurementsLocalCache = new LocalCache(this, null);
}
///
/// Assigns the status messaging callbacks.
///
/// Raise status messages on this callback
/// Raise exceptions on this callback
public void Initialize(Action onStatusMessage, Action onProcessException)
{
if (onStatusMessage == null)
throw new ArgumentNullException(nameof(onStatusMessage));
if (onProcessException == null)
throw new ArgumentNullException(nameof(onProcessException));
m_onStatusMessage = onStatusMessage;
m_onProcessException = onProcessException;
}
///
/// Gets the number of routes in this routing table.
///
public int RouteCount => m_globalCache.GlobalSignalLookup.Count;
///
/// Patches the existing routing table with the supplied adapters.
///
/// all of the producers
/// all of the consumers
[SuppressMessage("SonarQube.UnusedObject", "S1848", Justification = "Class instantiation of \"LocalCache\" attaches event handlers for adapters.")]
public void PatchRoutingTable(RoutingTablesAdaptersList producerAdapters, RoutingTablesAdaptersList consumerAdapters)
{
if (producerAdapters == null)
throw new ArgumentNullException(nameof(producerAdapters));
if (consumerAdapters == null)
throw new ArgumentNullException(nameof(consumerAdapters));
// Attach to NewMeasurements event of all producer adapters that are new
foreach (IAdapter producerAdapter in producerAdapters.NewAdapter)
{
// ReSharper disable once ObjectCreationAsStatement
new LocalCache(this, producerAdapter);
}
Dictionary consumerLookup = new Dictionary(m_globalCache.GlobalDestinationLookup);
// Create new consumer adapters
foreach (var consumerAdapter in consumerAdapters.NewAdapter)
{
consumerLookup.Add(consumerAdapter, new Consumer(consumerAdapter, m_onProcessException));
}
// Remove old adapters
foreach (var consumerAdapter in consumerAdapters.OldAdapter)
{
consumerLookup.Remove(consumerAdapter);
}
Interlocked.Exchange(ref m_globalCache, new GlobalCache(consumerLookup, m_globalCache.Version + 1));
}
///
/// This method will directly inject measurements into the routing table and use a shared local input adapter. For
/// contention reasons, it is not recommended this be its default use case, but it is necessary at times.
///
/// the sender object
/// the event arguments
public void InjectMeasurements(object sender, EventArgs> measurements)
{
m_injectMeasurementsLocalCache.Route(sender, measurements);
}
}
}