//****************************************************************************************************** // MeasurementKey.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.Concurrent; using System.Data; using System.Threading; using GSF.Data; namespace GSF.TimeSeries { /// /// Represents a primary key for a measurement. /// [Serializable] public class MeasurementKey { #region [ Members ] // Fields private readonly Guid m_signalID; private ulong m_id; private string m_source; private readonly int m_hashCode; private readonly int m_runtimeID; private MeasurementMetadata m_metadata; #endregion #region [ Constructors ] private MeasurementKey(Guid signalID, ulong id, string source) { m_signalID = signalID; m_id = id; m_source = source; m_hashCode = base.GetHashCode(); m_runtimeID = Interlocked.Increment(ref s_nextRuntimeID) - 1; // Returns the incremented value. Hints the -1 m_metadata = new MeasurementMetadata(this, null, 0, 1, null); } #endregion #region [ Properties ] /// /// Gets or sets ID of signal associated with this . /// public Guid SignalID => m_signalID; /// /// Gets or sets the numeric ID of this . /// public ulong ID => m_id; /// /// Gets or sets the source of this . /// /// /// This value is typically used to track the archive name in which the measurement, that this represents, is stored. /// public string Source => m_source; /// /// A unique ID that is assigned at runtime to identify this instance of . /// This value will change between life cycles, so it cannot be used to compare /// instances that are running out of process or in a separate . /// /// /// Since each is only tied to a single object, /// this provides another unique identifier that is zero indexed. /// This allows certain optimizations such as array lookups. /// public int RuntimeID => m_runtimeID; /// /// Gets the as they appear in the primary . /// /// /// This is to be considered the reference value. Adapters are free to change this inside specific instances /// This value should only be updated upon change in the primary data source using . /// public MeasurementMetadata Metadata => m_metadata; #endregion #region [ Methods ] /// /// Updates the values of the . /// /// Gets or sets the text based tag name. /// Defines an offset to add to the value. /// Defines a multiplicative offset to apply to the value. /// Defines the to use when downsampling this type of value. public void SetMeasurementMetadata(string tagName, double adder, double multiplier, MeasurementValueFilterFunction valueFilter) { if (this == Undefined) throw new NotSupportedException("Cannot set data source information for an undefined measurement."); if (m_metadata.TagName != tagName || m_metadata.Adder != adder || m_metadata.Multiplier != multiplier || m_metadata.MeasurementValueFilter != valueFilter) m_metadata = new MeasurementMetadata(this, tagName, adder, multiplier, valueFilter); } /// /// Returns a that represents the current . /// /// A that represents the current . public override string ToString() { return $"{m_source}:{m_id}"; } /// /// Serves as a hash function for the current . /// /// A hash code for the current . public override int GetHashCode() { return m_hashCode; } #endregion #region [ Static ] // Static Fields // All edits to as well as the ConcurrentDictionaries in s_keyCache should occur within a lock on s_syncEdits private static readonly ConcurrentDictionary s_idCache = new ConcurrentDictionary(); private static readonly ConcurrentDictionary> s_keyCache = new ConcurrentDictionary>(StringComparer.OrdinalIgnoreCase); private static readonly object s_syncEdits = new object(); private static int s_nextRuntimeID; /// /// Represents an undefined measurement key. /// public static readonly MeasurementKey Undefined; // Static Constructor static MeasurementKey() { // Order of operations is critical here since MeasurementKey and MeasurementMetadata have a circular reference Undefined = CreateUndefinedMeasurementKey(); MeasurementMetadata.CreateUndefinedMeasurementMetadata(); Undefined.m_metadata = MeasurementMetadata.Undefined; } // Static Methods /// /// Constructs a new given the specified parameters. /// /// ID of associated signal, if defined. /// A string representation of the . /// cannot be empty /// The value is not in the correct format for a value. public static MeasurementKey CreateOrUpdate(Guid signalID, string value) { string source; ulong id; if (!TrySplit(value, out source, out id)) throw new FormatException("The value is not in the correct format for a MeasurementKey value"); return CreateOrUpdate(signalID, source, id); } /// /// Constructs a new given the specified parameters. /// /// ID of associated signal, if defined. /// Source of the measurement that this represents (e.g., name of archive). /// Numeric ID of the measurement that this represents. /// cannot be empty. /// cannot be null. public static MeasurementKey CreateOrUpdate(Guid signalID, string source, ulong id) { Func addValueFactory; Func updateValueFactory; if (signalID == Guid.Empty) throw new ArgumentException("Unable to update undefined measurement key", nameof(signalID)); if (string.IsNullOrWhiteSpace(source)) throw new ArgumentNullException(nameof(source), "MeasurementKey source cannot be null or empty"); addValueFactory = guid => { // Create a new measurement key and add it to the KeyCache ConcurrentDictionary idLookup = s_keyCache.GetOrAdd(source, s => new ConcurrentDictionary()); return idLookup[id] = new MeasurementKey(guid, id, source); }; updateValueFactory = (guid, key) => { ConcurrentDictionary idLookup; // If existing measurement key is exactly the same as the // one we are trying to create, simply return that key if (key.ID == id && key.Source == source) return key; // Update source and ID and re-insert it into the KeyCache key.m_source = source; key.m_id = id; idLookup = s_keyCache.GetOrAdd(source, s => new ConcurrentDictionary()); idLookup[id] = key; return key; }; // https://msdn.microsoft.com/en-us/library/ee378675(v=vs.110).aspx // // If you call AddOrUpdate simultaneously on different threads, // addValueFactory may be called multiple times, but its key/value // pair might not be added to the dictionary for every call. // // This lock prevents race conditions that might occur in the addValueFactory that // could cause different MeasurementKey objects to be written to the KeyCache and IDCache lock (s_syncEdits) { return s_idCache.AddOrUpdate(signalID, addValueFactory, updateValueFactory); } } /// /// Constructs a new given the specified parameters. /// /// ID of associated signal, if defined. /// A string representation of the . /// The measurement key that was created or updated or . /// True if the measurement key was successfully created or updated, false otherwise. /// cannot be empty. /// Measurement key Source cannot be null. public static bool TryCreateOrUpdate(Guid signalID, string value, out MeasurementKey key) { try { key = CreateOrUpdate(signalID, value); return true; } catch { key = Undefined; return false; } } /// /// Constructs a new given the specified parameters. /// /// ID of associated signal, if defined. /// Source of the measurement that this represents (e.g., name of archive). /// Numeric ID of the measurement that this represents. /// The measurement key that was created or updated or . /// True if the measurement key was successfully created or updated, false otherwise. /// cannot be empty. /// cannot be null. public static bool TryCreateOrUpdate(Guid signalID, string source, ulong id, out MeasurementKey key) { try { key = CreateOrUpdate(signalID, source, id); return true; } catch { key = Undefined; return false; } } /// /// Looks up the measurement key associated with the given signal ID. /// /// The signal ID of the measurement key. /// The measurement key associated with the given signal ID. /// /// If no measurement key is found with the given signal ID, /// this method returns . /// public static MeasurementKey LookUpBySignalID(Guid signalID) { MeasurementKey key; // ReSharper disable once InconsistentlySynchronizedField if (signalID == Guid.Empty) key = Undefined; else if (!s_idCache.TryGetValue(signalID, out key)) key = Undefined; return key; } /// /// Looks up the measurement key associated with the given source and ID. /// /// The source of the signal. /// The source-specific unique integer identifier. /// The measurement key associated with the given source and ID. /// /// If no measurement key is found with the given source and ID, /// this method returns . /// public static MeasurementKey LookUpBySource(string source, ulong id) { ConcurrentDictionary idLookup; MeasurementKey key; if (!s_keyCache.TryGetValue(source, out idLookup)) return Undefined; if (!idLookup.TryGetValue(id, out key)) return Undefined; return key; } /// /// Performs a lookup by signal ID and, failing that, attempts to create /// the key using the given signal ID and the parsed source, and ID. /// /// The signal ID of the key to be looked up. /// A string representation of the . /// /// If the lookup succeeds, an existing measurement key with a matching signalID. /// If creation succeeds, a new measurement key with matching signal ID, source, and ID. /// Otherwise, . /// public static MeasurementKey LookUpOrCreate(Guid signalID, string value) { string source; ulong id; if (!TrySplit(value, out source, out id)) return LookUpOrCreate(signalID, Undefined.Source, Undefined.ID); return LookUpOrCreate(signalID, source, id); } /// /// Performs a lookup by signal ID and, failing that, attempts to /// create the key using the given signal ID, source, and ID. /// /// The signal ID of the key to be looked up. /// The source to use for the key if the lookup fails. /// The ID to use for the key if the lookup fails. /// /// If the lookup succeeds, an existing measurement key with a matching signalID. /// If creation succeeds, a new measurement key with matching signal ID, source, and ID. /// Otherwise, . /// public static MeasurementKey LookUpOrCreate(Guid signalID, string source, ulong id) { MeasurementKey key = LookUpBySignalID(signalID); if (key == Undefined && !TryCreateOrUpdate(signalID, source, id, out key)) return Undefined; return key; } /// /// Performs a lookup by source and, failing that, attempts to create /// the key using a newly generated signal ID and the parsed source and ID. /// /// A string representation of the . /// /// If the lookup succeeds, an existing measurement key with a matching signalID. /// If creation succeeds, a new measurement key with matching signal ID, source, and ID. /// Otherwise, . /// public static MeasurementKey LookUpOrCreate(string value) { string source; ulong id; if (!TrySplit(value, out source, out id)) return Undefined; return LookUpOrCreate(source, id); } /// /// Performs a lookup by source and, failing that, attempts to create /// the key using a newly generated signal ID and the given source and ID. /// /// The source to use for the key if the lookup fails. /// The ID to use for the key if the lookup fails. /// /// If the lookup succeeds, an existing measurement key with a matching signalID. /// If creation succeeds, a new measurement key with matching signal ID, source, and ID. /// Otherwise, . /// public static MeasurementKey LookUpOrCreate(string source, ulong id) { MeasurementKey key = LookUpBySource(source, id); if (key == Undefined && !TryCreateOrUpdate(Guid.NewGuid(), source, id, out key)) return Undefined; return key; } /// /// Converts the string representation of a into its value equivalent. /// /// A string representing the to convert. /// A value equivalent the representation contained in . /// The value is not in the correct format for a value. public static MeasurementKey Parse(string value) { MeasurementKey key; if (TryParse(value, out key)) return key; throw new FormatException("The value is not in the correct format for a MeasurementKey value"); } /// /// Attempts to convert the string representation of a into its value equivalent. /// /// A string representing the to convert. /// Output in which to stored parsed value. /// A true if representation contained in could be parsed; otherwise false. public static bool TryParse(string value, out MeasurementKey key) { string source; ulong id; // Split the input into source and ID if (TrySplit(value, out source, out id)) { // First attempt to look up an existing key key = LookUpBySource(source, id); if (key == Undefined) { try { // Lookup failed - attempt to create it with a newly generated signal ID key = CreateOrUpdate(Guid.NewGuid(), source, id); } catch { // source is null or empty key = Undefined; } } } else { // Incorrect format for a measurement key key = Undefined; } return key != Undefined; } /// /// Establish default cache. /// /// The database connection. /// The database adapter type. /// Measurement table name used to load measurement key cache. /// /// Source tables are expected to have at least the following fields: /// /// ID NVARCHAR Measurement key formatted as: ArchiveSource:PointID /// SignalID GUID Unique identification for measurement /// /// public static void EstablishDefaultCache(IDbConnection connection, Type adapterType, string measurementTable = "ActiveMeasurement") { string source; ulong id; // Establish default measurement key cache foreach (DataRow measurement in connection.RetrieveData(adapterType, $"SELECT ID, SignalID FROM {measurementTable}").Rows) { if (TrySplit(measurement["ID"].ToString(), out source, out id)) CreateOrUpdate(measurement["SignalID"].ToNonNullString(Guid.Empty.ToString()).ConvertToType(), source, id); } } /// /// Creates the undefined measurement key. Used to initialize . /// private static MeasurementKey CreateUndefinedMeasurementKey() { MeasurementKey key = new MeasurementKey(Guid.Empty, ulong.MaxValue, "__"); // Lock on s_syncEdits is not required since method is only called by the static constructor s_keyCache.GetOrAdd(key.Source, kcf => new ConcurrentDictionary())[ulong.MaxValue] = key; return key; } /// /// Attempts to split the given string representation /// of a measurement key into a source and ID pair. /// private static bool TrySplit(string value, out string source, out ulong id) { string[] elem; if (!string.IsNullOrEmpty(value)) { elem = value.Split(':'); if (elem.Length == 2 && ulong.TryParse(elem[1].Trim(), out id)) { source = elem[0].Trim(); return true; } } source = Undefined.Source; id = Undefined.ID; return false; } #endregion } }