/// <summary> /// Loads a state set from the Redis database. /// </summary> /// <param name="historian">The historian to load the state set from.</param> /// <param name="name">The name of the state set to load.</param> /// <param name="cancellationToken">The cancellation token for the request.</param> /// <returns> /// A task that will return the loaded state set. /// </returns> internal static async Task <StateSet> Load(RedisHistorian historian, string name, CancellationToken cancellationToken) { var values = await historian.Connection.GetDatabase().HashGetAllAsync(historian.GetKeyForStateSetDefinition(name)).ConfigureAwait(false); cancellationToken.ThrowIfCancellationRequested(); string description = null; var states = new List <StateSetItem>(); foreach (var item in values) { if (item.Name == "DESC") { description = item.Value; continue; } var hName = item.Name.ToString(); if (hName.StartsWith("S_")) { states.Add(new StateSetItem(hName.Substring(2), (int)item.Value)); } } return(new StateSet(name, description, states)); }
/// <summary> /// Loads all tag definitions for the specified historian. /// </summary> /// <param name="historian">The historian.</param> /// <param name="callback">A callback function that is invoked every time a tag definition is loaded from Redis.</param> /// <param name="cancellationToken">The cancellation token for the request.</param> /// <returns> /// A task that will load all tags into the historian. /// </returns> internal static async Task LoadAll(RedisHistorian historian, Action <RedisTagDefinition> callback, CancellationToken cancellationToken) { var key = historian.GetKeyForTagIdsList(); const int pageSize = 100; var page = 0; bool @continue; // Load tag definitions 100 at a time. do { @continue = false; ++page; long start = (page - 1) * pageSize; long end = start + pageSize - 1; // -1 because right-most item is included when getting the list range. var tagIds = await historian.Connection.GetDatabase().ListRangeAsync(key, start, end).ConfigureAwait(false); @continue = tagIds.Length == pageSize; if (tagIds.Length == 0) { continue; } var tasks = tagIds.Select(x => Task.Run(async() => { var tag = await Load(historian, x, cancellationToken).ConfigureAwait(false); callback(tag); })).ToArray(); await Task.WhenAny(Task.WhenAll(tasks), Task.Delay(-1, cancellationToken)).ConfigureAwait(false); cancellationToken.ThrowIfCancellationRequested(); } while (@continue); }
/// <summary> /// Saves a state set definition to the Redis database. /// </summary> /// <param name="historian">The historian to load the state set from.</param> /// <param name="stateSet">The state set to save.</param> /// <param name="addToMasterList"> /// Specify <see langword="true"/> when the state set is being created and <see langword="false"/> /// when it is being updated. /// </param> /// <param name="cancellationToken">The cancellation token for the request.</param> /// <returns> /// A task that will save the tag definition to the Redis database. /// </returns> internal static async Task Save(RedisHistorian historian, StateSet stateSet, bool addToMasterList, CancellationToken cancellationToken) { var key = historian.GetKeyForStateSetDefinition(stateSet.Name); var tasks = new List <Task>(); var db = historian.Connection.GetDatabase(); var hashes = new List <HashEntry>() { new HashEntry("DESC", stateSet.Description) }; hashes.AddRange(stateSet.Select(x => new HashEntry($"S_{x.Name}", x.Value))); tasks.Add(db.HashSetAsync(key, hashes.ToArray())); if (addToMasterList) { var listKey = historian.GetKeyForStateSetNamesList(); tasks.Add(db.ListRightPushAsync(listKey, stateSet.Name)); } await Task.WhenAny(Task.WhenAll(tasks), Task.Delay(-1, cancellationToken)).ConfigureAwait(false); cancellationToken.ThrowIfCancellationRequested(); }
/// <summary> /// Creates a new <see cref="RedisTagDefinition"/> object. /// </summary> /// <param name="historian">The owning historian.</param> /// <param name="id">The tag ID.</param> /// <param name="settings">The tag settings.</param> /// <param name="metadata">The metadata for the tag.</param> /// <param name="initialTagValues">The initial tag values, used to prime the exception and compression filters for the tag.</param> /// <param name="changeHistory">The change history for the tag.</param> private RedisTagDefinition(RedisHistorian historian, string id, TagSettings settings, TagMetadata metadata, InitialTagValues initialTagValues, IEnumerable <TagChangeHistoryEntry> changeHistory) : base(historian, id, settings, metadata, CreateTagSecurity(), initialTagValues, changeHistory) { _historian = historian ?? throw new ArgumentNullException(nameof(historian)); _tagDefinitionKey = _historian.GetKeyForTagDefinition(Id); _snapshotKey = _historian.GetKeyForSnapshotData(Id); _archiveKey = _historian.GetKeyForRawData(Id); _archiveCandidateKey = _historian.GetKeyForArchiveCandidateData(Id); _archiveCandidateValue = new ArchiveCandidateValue(initialTagValues?.LastExceptionValue, initialTagValues?.CompressionAngleMinimum ?? Double.NaN, initialTagValues?.CompressionAngleMaximum ?? Double.NaN); Updated += TagUpdated; }
/// <summary> /// Creates a new <see cref="RedisTagDefinition"/>. /// </summary> /// <param name="historian">The historian for the tag.</param> /// <param name="settings">The tag settings.</param> /// <param name="creator">The tag's creator.</param> /// <param name="cancellationToken">The cancellation token for the request.</param> /// <returns> /// A task that will create a new <see cref="RedisTagDefinition"/> and save it to the /// historian's Redis server. /// </returns> internal static async Task <RedisTagDefinition> Create(RedisHistorian historian, TagSettings settings, ClaimsPrincipal creator, CancellationToken cancellationToken) { var now = DateTime.UtcNow; var result = new RedisTagDefinition(historian, null, settings, new TagMetadata(DateTime.UtcNow, creator?.Identity.Name), null, new[] { TagChangeHistoryEntry.Created(creator) }); var key = historian.GetKeyForTagIdsList(); await Task.WhenAny(Task.WhenAll(result.Save(cancellationToken), historian.Connection.GetDatabase().ListRightPushAsync(key, result.Id)), Task.Delay(-1, cancellationToken)).ConfigureAwait(false); cancellationToken.ThrowIfCancellationRequested(); return(result); }
/// <summary> /// Loads the last-archived value for the specified tag. /// </summary> /// <param name="historian">The historian.</param> /// <param name="tagId">The tag ID.</param> /// <param name="cancellationToken">The cancellation token for the request.</param> /// <returns> /// A task that will return the last-archived value for the tag. /// </returns> private static async Task <TagValue> LoadLastArchivedValue(RedisHistorian historian, string tagId, CancellationToken cancellationToken) { var key = historian.GetKeyForRawData(tagId); var lastArchivedValueTask = historian.Connection.GetDatabase().SortedSetRangeByScoreAsync(key, Double.NegativeInfinity, Double.PositiveInfinity, Exclude.None, Order.Descending, 0, 1, CommandFlags.None); await Task.WhenAny(lastArchivedValueTask, Task.Delay(-1, cancellationToken)).ConfigureAwait(false); cancellationToken.ThrowIfCancellationRequested(); var value = lastArchivedValueTask.Result.FirstOrDefault(); return(value == RedisValue.Null ? null : JsonConvert.DeserializeObject <RedisTagValue>(value)?.ToTagValue()); }
/// <summary> /// Loads the archive candidate value for the specified tag from Redis. /// </summary> /// <param name="historian">The historian.</param> /// <param name="tagId">The tag ID.</param> /// <param name="cancellationToken">The cancellation token for the request.</param> /// <returns> /// A task that will return the archive candidate value. /// </returns> private static async Task <ArchiveCandidateValue> LoadArchiveCandidateValue(RedisHistorian historian, string tagId, CancellationToken cancellationToken) { var key = historian.GetKeyForArchiveCandidateData(tagId); var valuesTask = historian.Connection.GetDatabase().HashGetAllAsync(key); await Task.WhenAny(valuesTask, Task.Delay(-1, cancellationToken)).ConfigureAwait(false); cancellationToken.ThrowIfCancellationRequested(); if (valuesTask.Result.Length == 0) { return(null); } return(GetArchiveCandidateValueFromHashValues(valuesTask.Result)); }
/// <summary> /// Deletes a state set definition. /// </summary> /// <param name="historian">The historian to delete the state set from.</param> /// <param name="name">The name of the state set to delete.</param> /// <param name="cancellationToken">The cancellation token for the request.</param> /// <returns> /// A task that will delete the tag. /// </returns> internal static async Task Delete(RedisHistorian historian, string name, CancellationToken cancellationToken) { var tasks = new List <Task>(); // Delete the state set definition. tasks.Add(historian.Connection.GetDatabase().KeyDeleteAsync(historian.GetKeyForStateSetDefinition(name))); // Remove the name from the list of state set names. var nameListKey = historian.GetKeyForStateSetNamesList(); tasks.Add(historian.Connection.GetDatabase().ListRemoveAsync(nameListKey, name)); await Task.WhenAny(Task.WhenAll(tasks), Task.Delay(-1, cancellationToken)).ConfigureAwait(false); cancellationToken.ThrowIfCancellationRequested(); }
/// <summary> /// Gets the Redis key for the list that contains all of the defined tag IDs. /// </summary> /// <param name="historian">The historian.</param> /// <returns> /// The Redis key for the tag IDs list. /// </returns> /// <remarks> /// We use a separate list to store the tag IDs rather than doing a wildcard match on keys (e.g. /// searching for keys matching "aika:tags:*:definition"), /// because this involves searching through all keys on a specific server, rather than on the /// database in general (which may be split across multiple nodes). See here for an /// explanation: https://stackexchange.github.io/StackExchange.Redis/KeysScan /// </remarks> internal static string GetKeyForStateSetNamesList(this RedisHistorian historian) { return(historian.GetKeyPrefixForStateSet("names")); }
/// <summary> /// Gets the Redis key for the hash that defines the state set with the provided name. /// </summary> /// <param name="historian">The historian.</param> /// <param name="name">The state set name.</param> /// <returns> /// The Redis key for the state set definition hash. /// </returns> internal static string GetKeyForStateSetDefinition(this RedisHistorian historian, string name) { return($"{historian.GetKeyPrefixForStateSet(name)}:definition"); }
/// <summary> /// Loads a tag definition from the Redis database. /// </summary> /// <param name="historian">The Redis historian to load the tag from.</param> /// <param name="tagId">The ID of the tag to load.</param> /// <param name="cancellationToken">The cancellation token for the request.</param> /// <returns> /// A task that will return the loaded tag definition. /// </returns> internal static async Task <RedisTagDefinition> Load(RedisHistorian historian, string tagId, CancellationToken cancellationToken) { var values = await historian.Connection.GetDatabase().HashGetAllAsync(historian.GetKeyForTagDefinition(tagId)).ConfigureAwait(false); cancellationToken.ThrowIfCancellationRequested(); string name = null; string description = null; string units = null; TagDataType dataType = default(TagDataType); string stateSet = null; bool exceptionFilterEnabled = false; TagValueFilterDeviationType exceptionFilterLimitType = default(TagValueFilterDeviationType); double exceptionFilterLimit = 0; TimeSpan exceptionFilterWindowSize = default(TimeSpan); bool compressionFilterEnabled = false; TagValueFilterDeviationType compressionFilterLimitType = default(TagValueFilterDeviationType); double compressionFilterLimit = 0; TimeSpan compressionFilterWindowSize = default(TimeSpan); DateTime createdAt = DateTime.MinValue; string creator = null; DateTime modifiedAt = DateTime.MinValue; string modifiedBy = null; foreach (var item in values) { switch (item.Name.ToString()) { case "NAME": name = item.Value; break; case "DESC": description = item.Value; break; case "UNITS": units = item.Value; break; case "TYPE": dataType = (TagDataType)((int)item.Value); break; case "SSET": stateSet = item.Value; break; case "EXC_ENABLED": exceptionFilterEnabled = Convert.ToBoolean((int)item.Value); break; case "EXC_LIMIT_TYPE": exceptionFilterLimitType = (TagValueFilterDeviationType)((int)item.Value); break; case "EXC_LIMIT": exceptionFilterLimit = (double)item.Value; break; case "EXC_WINDOW": exceptionFilterWindowSize = TimeSpan.Parse(item.Value); break; case "COM_ENABLED": compressionFilterEnabled = Convert.ToBoolean((int)item.Value); break; case "COM_LIMIT_TYPE": compressionFilterLimitType = (TagValueFilterDeviationType)((int)item.Value); break; case "COM_LIMIT": compressionFilterLimit = (double)item.Value; break; case "COM_WINDOW": compressionFilterWindowSize = TimeSpan.Parse(item.Value); break; case "MD_CREATEDAT": createdAt = new DateTime((long)item.Value, DateTimeKind.Utc); break; case "MD_CREATEDBY": creator = item.Value; break; case "MD_MODIFIEDAT": modifiedAt = new DateTime((long)item.Value, DateTimeKind.Utc); break; case "MD_MODIFIEDBY": modifiedBy = item.Value; break; } } if (String.IsNullOrWhiteSpace(name)) { name = tagId; } var settings = new TagSettings() { Name = name, Description = description, Units = units, DataType = dataType, StateSet = stateSet, ExceptionFilterSettings = new TagValueFilterSettingsUpdate() { IsEnabled = exceptionFilterEnabled, LimitType = exceptionFilterLimitType, Limit = exceptionFilterLimit, WindowSize = exceptionFilterWindowSize }, CompressionFilterSettings = new TagValueFilterSettingsUpdate() { IsEnabled = compressionFilterEnabled, LimitType = compressionFilterLimitType, Limit = compressionFilterLimit, WindowSize = compressionFilterWindowSize } }; var metadata = new TagMetadata(createdAt, creator, modifiedAt, modifiedBy); var snapshotTask = LoadSnapshotValue(historian, tagId, cancellationToken); var lastArchivedTask = LoadLastArchivedValue(historian, tagId, cancellationToken); var archiveCandidateTask = LoadArchiveCandidateValue(historian, tagId, cancellationToken); await Task.WhenAll(snapshotTask, lastArchivedTask, archiveCandidateTask).ConfigureAwait(false); cancellationToken.ThrowIfCancellationRequested(); var initialValues = new InitialTagValues(snapshotTask.Result, lastArchivedTask.Result, archiveCandidateTask.Result.Value, archiveCandidateTask.Result.CompressionAngleMinimum, archiveCandidateTask.Result.CompressionAngleMaximum); var result = new RedisTagDefinition(historian, tagId, settings, metadata, initialValues, null); return(result); }
/// <summary> /// Gets the Redis key for the list that contains all of the defined tag IDs. /// </summary> /// <param name="historian">The historian.</param> /// <returns> /// The Redis key for the tag IDs list. /// </returns> /// <remarks> /// We use a separate list to store the tag IDs rather than doing a wildcard match on keys (e.g. /// searching for keys matching "aika:tags:*:definition"), /// because this involves searching through all keys on a specific server, rather than on the /// database in general (which may be split across multiple nodes). See here for an /// explanation: https://stackexchange.github.io/StackExchange.Redis/KeysScan /// </remarks> internal static string GetKeyForTagIdsList(this RedisHistorian historian) { return(historian.GetKeyPrefixForTag("tagIds")); }
/// <summary> /// Gets the Redis key prefix that is used for all things related to a single tag (the definition, the snapshot value, and the raw history). /// </summary> /// <param name="historian">The historian.</param> /// <param name="tagId">The tag ID.</param> /// <returns>The key prefix.</returns> internal static string GetKeyPrefixForTag(this RedisHistorian historian, string tagId) { return($"{historian.RedisKeyPrefix}:tags:{tagId}"); }
/// <summary> /// Gets the Redis key prefix that is used for all things related to a state set. /// </summary> /// <param name="historian">The historian.</param> /// <param name="name">The state set name.</param> /// <returns> /// The key prefix. /// </returns> internal static string GetKeyPrefixForStateSet(this RedisHistorian historian, string name) { return($"{historian.RedisKeyPrefix}:stateSets:{name}"); }
/// <summary> /// Gets the Redis key for the tag's current archive candidate value. /// </summary> /// <param name="historian">The historian.</param> /// <param name="tagId">The tag ID.</param> /// <returns> /// The Redis key for the tag's snapshot value. /// </returns> internal static string GetKeyForArchiveCandidateData(this RedisHistorian historian, string tagId) { return($"{historian.GetKeyPrefixForTag(tagId)}:archiveCandidate"); }
/// <summary> /// Gets the Redis key for the tag's snapshot value. /// </summary> /// <param name="historian">The historian.</param> /// <param name="tagId">The tag ID.</param> /// <returns> /// The Redis key for the tag's snapshot value. /// </returns> internal static string GetKeyForSnapshotData(this RedisHistorian historian, string tagId) { return($"{historian.GetKeyPrefixForTag(tagId)}:snapshot"); }
/// <summary> /// Gets the Redis key for the hash that defines the tag with the provided tag ID. /// </summary> /// <param name="historian">The historian.</param> /// <param name="tagId">The tag ID.</param> /// <returns> /// The Redis key for the tag definition hash. /// </returns> internal static string GetKeyForTagDefinition(this RedisHistorian historian, string tagId) { return($"{historian.GetKeyPrefixForTag(tagId)}:definition"); }