//****************************************************************************************************** // TimeSeriesMetadata.cs - Gbtc // // Copyright © 2015, 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://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: // ---------------------------------------------------------------------------------------------------- // 10/28/2015 - Ritchie Carroll // Generated original version of source code. // //****************************************************************************************************** using System; using System.Collections.Generic; using System.Configuration; using System.IO; using System.Linq; using System.Text; using System.Xml.Serialization; using GSF; using GSF.Configuration; using GSF.IO; using Misakai.Kafka; namespace KafkaAdapters { /// /// Represents a set of Kafka time-series metadata. /// [XmlRoot("TimeSeriesMetadata", Namespace="http://www.gridprotectionalliance.org/gsf/KafkaAdapters", IsNullable=false)] public class TimeSeriesMetadata { #region [ Members ] // Constants private const int SizeMessage = 0; private const int ValueMessage = 1; // Fields /// /// Defines the version of this time-series metadata instance. /// public long Version; /// /// Defines the records of this time-series metadata instance. /// [XmlArrayItem("Record")] public List Records = new List(); #endregion #region [ Properties ] /// /// Gets the count of records in the time-series metadata. /// public int Count => Records?.Count ?? 0; #endregion #region [ Methods ] /// /// Calculates a CRC-32 based check-sum on this instance. /// /// CRC-32 based check-sum on this instance. public long CalculateChecksum() { return Records.Sum(record => record.CalculateChecksum()); } #endregion #region [ Static ] // Static Methods /// /// Serializes instance to a stream of XML. /// /// Source time-series metadata object to serialize. /// Destination stream to hold serialized data. /// Determines of metadata version should be incremented. public static void Serialize(TimeSeriesMetadata metadata, Stream serializationStream, bool incrementVersion) { if ((object)metadata == null) throw new ArgumentNullException(nameof(metadata)); if ((object)serializationStream == null) throw new ArgumentNullException(nameof(serializationStream)); // Increment serialization version if (incrementVersion) metadata.Version++; XmlSerializer serializer = new XmlSerializer(typeof(TimeSeriesMetadata)); using (TextWriter writer = new StreamWriter(serializationStream, Encoding.UTF8, 8192, true)) serializer.Serialize(writer, metadata); } /// /// Deserializes a stream of XML metadata as a instance. /// /// Source stream of serialized data. /// Deserialized instance. public static TimeSeriesMetadata Deserialize(Stream serializationStream) { if ((object)serializationStream == null) throw new ArgumentNullException(nameof(serializationStream)); XmlSerializer serializer = new XmlSerializer(typeof(TimeSeriesMetadata)); return serializer.Deserialize(serializationStream) as TimeSeriesMetadata; } /// /// Writes time-series metadata to specified Kafka . /// /// Source time-series metadata object to write to Kafka. /// Kafka router connection. /// Kafka topic. public static void WriteToKafka(TimeSeriesMetadata metadata, BrokerRouter router, string topic) { if ((object)metadata == null) throw new ArgumentNullException(nameof(metadata)); if ((object)router == null) throw new ArgumentNullException(nameof(router)); if (string.IsNullOrWhiteSpace(topic)) throw new ArgumentNullException(nameof(topic)); using (MemoryStream stream = new MemoryStream()) { Serialize(metadata, stream, true); using (Producer producer = new Producer(router)) { Message[] messages = new Message[2]; byte[] timeKey = BitConverter.GetBytes(DateTime.UtcNow.Ticks); // First message used to serialize metadata size (since metadata messages can be large) messages[SizeMessage] = new Message { Key = timeKey, Value = BitConverter.GetBytes(stream.Length) }; // Second message used to serialize metadata value messages[ValueMessage] = new Message { Key = timeKey, Value = stream.ToArray() }; // Send meta-data to Kafka producer.SendMessageAsync(topic, messages).Wait(); } } } /// /// Reads latest time-series metadata from specified Kafka . /// /// Kafka router connection. /// Kafka topic. /// Serialization time. /// Status message function. /// Latest instance read from Kafka. public static TimeSeriesMetadata ReadFromKafka(BrokerRouter router, string topic, Action statusMessage, out Ticks serializationTime) { if ((object)router == null) throw new ArgumentNullException(nameof(router)); if (string.IsNullOrWhiteSpace(topic)) throw new ArgumentNullException(nameof(topic)); Message sizeMessage = ReadMessage(router, topic, SizeMessage); serializationTime = BitConverter.ToInt64(sizeMessage.Key, 0); Message valueMessage = ReadMessage(router, topic, ValueMessage, (int)Math.Ceiling(BitConverter.ToInt64(sizeMessage.Value, 0) / 4096.0D) * 4096); if (serializationTime != BitConverter.ToInt64(valueMessage.Key, 0)) statusMessage?.Invoke("WARNING: Timestamp keys for metadata size and value records are mismatched..."); using (MemoryStream stream = new MemoryStream(valueMessage.Value)) return Deserialize(stream); } private static Message ReadMessage(BrokerRouter router, string topic, int messageIndex, int maxBytes = 32768) { Message message = null; long offset = router.LatestOffset(topic) - (2 - messageIndex); List messages = router.Fetch(topic, 0, offset, maxBytes)?.Messages; if ((messages?.Count ?? 0) > 0) message = messages?[0]; if ((object)message == null) throw new InvalidOperationException("No Kafka record to consume"); return message; } /// /// Caches meta-data locally. /// /// Source time-series metadata object to cache. /// Kafka topic. /// Status message function. public static void CacheLocally(TimeSeriesMetadata metadata, string topic, Action statusMessage) { // Cache meta-data locally so it can be reviewed string cacheFileName = "undefined"; try { // Define default cache path string cachePath = null; try { // Attempt to retrieve configuration cache path as defined in the config file ConfigurationFile configFile = ConfigurationFile.Current; CategorizedSettingsElementCollection systemSettings = configFile.Settings["systemSettings"]; CategorizedSettingsElement configurationCachePathSetting = systemSettings["ConfigurationCachePath"]; if ((object)configurationCachePathSetting != null) cachePath = FilePath.GetAbsolutePath(systemSettings["ConfigurationCachePath"].Value); if (string.IsNullOrEmpty(cachePath)) cachePath = $"{FilePath.GetAbsolutePath("")}{Path.DirectorySeparatorChar}ConfigurationCache{Path.DirectorySeparatorChar}"; } catch (ConfigurationErrorsException) { cachePath = $"{FilePath.GetAbsolutePath("")}{Path.DirectorySeparatorChar}ConfigurationCache{Path.DirectorySeparatorChar}"; } cacheFileName = Path.Combine(cachePath, $"{topic}.xml"); using (FileStream stream = File.Create(cacheFileName)) Serialize(metadata, stream, false); } catch (Exception ex) { statusMessage?.Invoke($"WARNING: Failed to locally cache current metadata to \"{cacheFileName}\": {ex.Message}"); } } #endregion } }