From 0e62ffe537fed9f6889d095ae2856ea47491a6a8 Mon Sep 17 00:00:00 2001 From: Guilherme Branco Stracini Date: Sun, 6 Sep 2020 22:24:17 -0300 Subject: [PATCH 1/4] Add Redis support --- CrispyWaffle.sln | 7 + README.md | 1 + .../Cache/RedisCacheRepository.cs | 441 ++++++++++++++++++ .../CrispyWaffle.Redis.csproj | 17 + .../Telemetry/RedisTelemetryClient.cs | 229 +++++++++ .../Utils/Communications/RedisConnector.cs | 262 +++++++++++ .../Utils/MasterSlaveConfiguration.cs | 51 ++ .../Utils/RedisExtensions.cs | 51 ++ .../Infrastructure/EnvironmentHelper.cs | 12 + .../CrispyWaffle.Tests.csproj | 4 +- appveyor.yml | 21 +- docs/changelog.md | 7 +- docs/user-guide/basic-usage.md | 1 + docs/user-guide/caching.md | 48 ++ docs/user-guide/events.md | 4 +- docs/user-guide/logging.md | 2 +- docs/user-guide/scheduled-jobs.md | 1 - 17 files changed, 1152 insertions(+), 7 deletions(-) create mode 100644 Src/CrispyWaffle.Redis/Cache/RedisCacheRepository.cs create mode 100644 Src/CrispyWaffle.Redis/CrispyWaffle.Redis.csproj create mode 100644 Src/CrispyWaffle.Redis/Telemetry/RedisTelemetryClient.cs create mode 100644 Src/CrispyWaffle.Redis/Utils/Communications/RedisConnector.cs create mode 100644 Src/CrispyWaffle.Redis/Utils/MasterSlaveConfiguration.cs create mode 100644 Src/CrispyWaffle.Redis/Utils/RedisExtensions.cs create mode 100644 docs/user-guide/caching.md diff --git a/CrispyWaffle.sln b/CrispyWaffle.sln index ebb8552c..2c633779 100644 --- a/CrispyWaffle.sln +++ b/CrispyWaffle.sln @@ -23,6 +23,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CrispyWaffle.Log4Net", "Src EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CrispyWaffle.Tests", "Tests\CrispyWaffle.Tests\CrispyWaffle.Tests.csproj", "{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CrispyWaffle.Redis", "Src\CrispyWaffle.Redis\CrispyWaffle.Redis.csproj", "{47DB173E-BC2B-4C16-B2A9-9506E57D6D9E}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -49,6 +51,10 @@ Global {FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}.Debug|Any CPU.Build.0 = Debug|Any CPU {FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}.Release|Any CPU.ActiveCfg = Release|Any CPU {FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}.Release|Any CPU.Build.0 = Release|Any CPU + {47DB173E-BC2B-4C16-B2A9-9506E57D6D9E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {47DB173E-BC2B-4C16-B2A9-9506E57D6D9E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {47DB173E-BC2B-4C16-B2A9-9506E57D6D9E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {47DB173E-BC2B-4C16-B2A9-9506E57D6D9E}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -59,6 +65,7 @@ Global {687F6153-2295-40A6-8673-1359482F928B} = {2FD912BD-34A5-4F9E-8192-CD7AEFD572B5} {D6164E04-F1C9-4D86-9B9C-41036B816C4A} = {2FD912BD-34A5-4F9E-8192-CD7AEFD572B5} {FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} = {AA14B952-E519-4983-BF49-E106490AAF1F} + {47DB173E-BC2B-4C16-B2A9-9506E57D6D9E} = {2FD912BD-34A5-4F9E-8192-CD7AEFD572B5} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {FAE960DB-B996-4FB3-858C-61BA7C7CD86E} diff --git a/README.md b/README.md index 8166a2da..ce9cb18c 100644 --- a/README.md +++ b/README.md @@ -52,6 +52,7 @@ Download the latest zip file from the [Release](https://github.com/GuiBranco/Cri | **CrispyWaffle.Configuration** | [![CrispyWaffle Configuration NuGet Version](https://img.shields.io/nuget/v/CrispyWaffle.Configuration.svg?style=flat)](https://www.nuget.org/packages/CrispyWaffle.Configuration/) | [![CrispyWaffle Configuration NuGet Downloads](https://img.shields.io/nuget/dt/CrispyWaffle.Configuration.svg?style=flat)](https://www.nuget.org/packages/CrispyWaffle.Configuration/) | | **CrispyWaffle.Elmah** | [![CrispyWaffle ELMAH NuGet Version](https://img.shields.io/nuget/v/CrispyWaffle.Elmah.svg?style=flat)](https://www.nuget.org/packages/CrispyWaffle.Elmah/) | [![CrispyWaffle ELMAH NuGet Downloads](https://img.shields.io/nuget/dt/CrispyWaffle.Elmah.svg?style=flat)](https://www.nuget.org/packages/CrispyWaffle.Elmah/) | | **CrispyWaffle.Log4Net** | [![CrispyWaffle Log4Net NuGet Version](https://img.shields.io/nuget/v/CrispyWaffle.Log4Net.svg?style=flat)](https://www.nuget.org/packages/CrispyWaffle.Log4Net/) | [![CrispyWaffle Log4Net NuGet Downloads](https://img.shields.io/nuget/dt/CrispyWaffle.Log4Net.svg?style=flat)](https://www.nuget.org/packages/CrispyWaffle.Log4Net/) | +| **CrispyWaffle.Redis** | [![CrispyWaffle Redis NuGet Version](https://img.shields.io/nuget/v/CrispyWaffle.Redis.svg?style=flat)](https://www.nuget.org/packages/CrispyWaffle.Redis/) | [![CrispyWaffle Redis NuGet Downloads](https://img.shields.io/nuget/dt/CrispyWaffle.Redis.svg?style=flat)](https://www.nuget.org/packages/CrispyWaffle.Redis/) | More information avaiable [here](https://guibranco.github.io/CrispyWaffle/installation/). diff --git a/Src/CrispyWaffle.Redis/Cache/RedisCacheRepository.cs b/Src/CrispyWaffle.Redis/Cache/RedisCacheRepository.cs new file mode 100644 index 00000000..7d05d7cc --- /dev/null +++ b/Src/CrispyWaffle.Redis/Cache/RedisCacheRepository.cs @@ -0,0 +1,441 @@ +// *********************************************************************** +// Assembly : CrispyWaffle.Redis +// Author : Guilherme Branco Stracini +// Created : 09-06-2020 +// +// Last Modified By : Guilherme Branco Stracini +// Last Modified On : 09-06-2020 +// *********************************************************************** +// +// Copyright (c) Guilherme Branco Stracini ME. All rights reserved. +// +// +// *********************************************************************** +using CrispyWaffle.Cache; +using CrispyWaffle.Log; +using CrispyWaffle.Redis.Utils.Communications; +using StackExchange.Redis; +using StackExchange.Redis.Extensions.Core.Abstractions; +using System; +using System.Collections.Generic; +using System.Globalization; + +namespace CrispyWaffle.Redis.Cache +{ + /// + /// Class RedisCacheRepository. + /// Implements the + /// Implements the + /// + /// + /// + public class RedisCacheRepository : ICacheRepository, IDisposable + { + #region Private fields + + /// + /// The connector + /// + private readonly RedisConnector _connector; + + /// + /// The cache client + /// + private readonly IRedisCacheClient _cacheClient; + + #endregion + + #region ~Ctor + + /// + /// Initializes a new instance of the class. + /// + /// The connector. + public RedisCacheRepository(RedisConnector connector) + { + _connector = connector; + _cacheClient = connector.Cache; + } + + #endregion + + #region Private methods + + /// + /// Handles the exception. + /// + /// The e. + private static void HandleException(Exception e) + { + if (e.Message.IndexOf("timeout", StringComparison.InvariantCultureIgnoreCase) != -1) + { + LogConsumer.Trace(e); + } + else + { + LogConsumer.Handle(e); + } + } + + #endregion + + #region Public methods + + /// + /// Sets to database. + /// + /// + /// The value. + /// The key. + /// The database number. + /// The TTL. + /// if set to true [fire and forget]. + public void SetToDatabase(T value, string key, int databaseNumber, TimeSpan? ttl = null, bool fireAndForget = false) + { + var flags = CommandFlags.None; + if (fireAndForget) + { + flags = CommandFlags.FireAndForget; + } + + var inputBytes = _connector.Serializer.Serialize(value); + _connector.GetDatabase(databaseNumber).StringSet(key, inputBytes, ttl, When.Always, flags); + } + + /// + /// Gets from database. + /// + /// + /// The key. + /// The database number. + /// T. + /// + /// + public T GetFromDatabase(string key, int databaseNumber) + { + var valueBytes = _connector.GetDatabase(databaseNumber).StringGet(key, CommandFlags.PreferReplica); + if (!valueBytes.HasValue) + { + throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture, "Unable to get the item with key {0}", key)); + } + + return _connector.Serializer.Deserialize(valueBytes); + } + + /// + /// Tries the get from database. + /// + /// + /// The key. + /// The database number. + /// The value. + /// true if get from database, false otherwise. + public bool TryGetFromDatabase(string key, int databaseNumber, out T value) + { + value = default; + var valueBytes = _connector.GetDatabase(databaseNumber).StringGet(key, CommandFlags.PreferReplica); + if (!valueBytes.HasValue) + { + return false; + } + + value = _connector.Serializer.Deserialize(valueBytes); + return true; + } + + /// + /// Removes from database. + /// + /// The key. + /// The database number. + public void RemoveFromDatabase(string key, int databaseNumber) + { + _connector.GetDatabase(databaseNumber).KeyDelete(key); + } + + #endregion + + #region Implementation of ICacheRepository + + /// + /// Gets or sets a value indicating whether [should propagate exceptions]. + /// + /// true if [should propagate exceptions]; otherwise, false. + public bool ShouldPropagateExceptions { get; set; } + + /// + /// Sets the specified value. + /// + /// The type of the value + /// The value. + /// The key. + /// The TTL. + public void Set(T value, string key, TimeSpan? ttl = null) + { + try + { + if (ttl.HasValue) + { + _cacheClient.Db0.AddAsync(key, value, ttl.Value).Wait(); + } + else + { + _cacheClient.Db0.AddAsync(key, value).Wait(); + } + } + catch (Exception e) + { + if (ShouldPropagateExceptions) + { + throw; + } + + HandleException(e); + } + } + + /// + /// Sets the specified value. + /// + /// + /// The value. + /// The key. + /// The sub key. + public void Set(T value, string key, string subKey) + { + try + { + var allValues = _cacheClient.Db0.ExistsAsync(key).Result + ? _cacheClient.Db0.HashGetAllAsync(key, CommandFlags.PreferReplica).Result + : new Dictionary(); + allValues[subKey] = value; + _cacheClient.Db0.HashSetAsync(key, allValues, CommandFlags.FireAndForget).Wait(); + } + catch (Exception e) + { + if (ShouldPropagateExceptions) + { + throw; + } + + HandleException(e); + } + } + + /// + /// Gets the specified key. + /// + /// The type of object (the object will be cast to this type) + /// The key. + /// T. + /// + /// + public T Get(string key) + { + try + { + if (_cacheClient.Db0.ExistsAsync(key).Result) + { + return _cacheClient.Db0.GetAsync(key, CommandFlags.PreferReplica).Result; + } + } + catch (Exception e) + { + if (ShouldPropagateExceptions) + { + throw; + } + + HandleException(e); + } + throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture, "Unable to get the item with key {0}", key)); + } + + /// + /// Gets the specified key. + /// + /// + /// The key. + /// The sub key. + /// T. + /// + /// + public T Get(string key, string subKey) + { + try + { + if (_cacheClient.Db0.HashExistsAsync(key, subKey, CommandFlags.PreferReplica).Result) + { + return _cacheClient.Db0.HashGetAsync(key, subKey, CommandFlags.PreferReplica).Result; + } + } + catch (Exception e) + { + if (ShouldPropagateExceptions) + { + throw; + } + + HandleException(e); + } + throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture, "Unable to get the item with key {0} and sub key {1}", key, subKey)); + } + + /// + /// Tries to get a value based on its key, if exists return true, else false. + /// The out parameter value is the object requested. + /// + /// The type of object (the object will be cast to this type) + /// The key. + /// The value. + /// Returns True if the object with the key exists, false otherwise + public bool TryGet(string key, out T value) + { + value = default; + try + { + value = _cacheClient.Db0.GetAsync(key, CommandFlags.PreferReplica).Result; + return _cacheClient.Db0.ExistsAsync(key).Result; + } + catch (Exception e) + { + if (ShouldPropagateExceptions) + { + throw; + } + + HandleException(e); + } + return false; + } + + /// + /// Tries the get. + /// + /// + /// The key. + /// The sub key. + /// The value. + /// true if get the key, false otherwise. + public bool TryGet(string key, string subKey, out T value) + { + value = default; + try + { + value = _cacheClient.Db0.HashGetAsync(key, subKey, CommandFlags.PreferReplica).Result; + return _cacheClient.Db0.HashExistsAsync(key, subKey).Result; + } + catch (Exception e) + { + if (ShouldPropagateExceptions) + { + throw; + } + + HandleException(e); + } + + return false; + } + + /// + /// Removes the specified key from the cache. + /// + /// The key. + public void Remove(string key) + { + try + { + _cacheClient.Db0.RemoveAsync(key).Wait(); + } + catch (Exception e) + { + if (ShouldPropagateExceptions) + { + throw; + } + + HandleException(e); + } + } + + /// + /// Removes the specified key. + /// + /// The key. + /// The sub key. + public void Remove(string key, string subKey) + { + try + { + _cacheClient.Db0.HashDeleteAsync(key, subKey, CommandFlags.FireAndForget).Wait(); + } + catch (Exception e) + { + if (ShouldPropagateExceptions) + { + throw; + } + + HandleException(e); + } + } + + /// + /// Returns the time to live of the specified key. + /// + /// The key. + /// The timespan until this key is expired from the cache or 0 if it's already expired or doesn't exists. + public TimeSpan TTL(string key) + { + try + { + return _cacheClient.Db0.Database.KeyTimeToLive(key, CommandFlags.PreferReplica) ?? TimeSpan.Zero; + } + catch (Exception e) + { + if (ShouldPropagateExceptions) + { + throw; + } + + HandleException(e); + } + return TimeSpan.Zero; + } + + /// + /// Clears this instance. + /// + public void Clear() + { + try + { + _cacheClient.Db0.FlushDbAsync().Wait(); + } + catch (Exception e) + { + if (ShouldPropagateExceptions) + { + throw; + } + + HandleException(e); + } + } + + #endregion + + #region Implementation of IDisposable + + /// + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + /// + public void Dispose() + { + _connector.Dispose(); + } + + #endregion + } +} diff --git a/Src/CrispyWaffle.Redis/CrispyWaffle.Redis.csproj b/Src/CrispyWaffle.Redis/CrispyWaffle.Redis.csproj new file mode 100644 index 00000000..e83aa74a --- /dev/null +++ b/Src/CrispyWaffle.Redis/CrispyWaffle.Redis.csproj @@ -0,0 +1,17 @@ + + + + netstandard2.0 + + + + + + + + + + + + + diff --git a/Src/CrispyWaffle.Redis/Telemetry/RedisTelemetryClient.cs b/Src/CrispyWaffle.Redis/Telemetry/RedisTelemetryClient.cs new file mode 100644 index 00000000..78bf5343 --- /dev/null +++ b/Src/CrispyWaffle.Redis/Telemetry/RedisTelemetryClient.cs @@ -0,0 +1,229 @@ +// *********************************************************************** +// Assembly : CrispyWaffle.Redis +// Author : Guilherme Branco Stracini +// Created : 09-06-2020 +// +// Last Modified By : Guilherme Branco Stracini +// Last Modified On : 09-06-2020 +// *********************************************************************** +// +// Copyright (c) Guilherme Branco Stracini ME. All rights reserved. +// +// +// *********************************************************************** +using CrispyWaffle.Composition; +using CrispyWaffle.Extensions; +using CrispyWaffle.Redis.Utils.Communications; +using CrispyWaffle.Serialization; +using CrispyWaffle.Telemetry; +using StackExchange.Redis; +using System; + +namespace CrispyWaffle.Redis.Telemetry +{ + /// + /// The Redis telemetry client class. + /// + /// + + public sealed class RedisTelemetryClient : ITelemetryClient + { + #region Private fields + + /// + /// The redis + /// + private readonly RedisConnector _redis; + + /// + /// The default TTL + /// + private readonly TimeSpan _defaultTTL; + + /// + /// The suffix + /// + private readonly string _suffix; + + #endregion + + #region ~Ctor + + /// + /// Initializes a new instance of the class. + /// + public RedisTelemetryClient() + { + _redis = ServiceLocator.Resolve(); + _defaultTTL = new TimeSpan(30, 0, 0, 0); + } + + /// + /// Initializes a new instance of the class. + /// + /// The redis. + /// The key suffix + public RedisTelemetryClient(RedisConnector redis, string suffix) + { + _redis = redis; + _suffix = suffix; + } + + #endregion + + #region Public properties + + /// + /// Gets or sets the hit database. + /// + /// The hit database. + public int HitDatabase { get; set; } + + /// + /// Gets or sets the metric database. + /// + /// The metric database. + public int MetricDatabase { get; set; } + + #endregion + + #region Implementation of ITelemetryClient + + /// + /// Gets the hit. + /// + /// Name of the hit. + /// System.Int32. + public int GetHit(string hitName) + { + var result = _redis.GetDatabase(HitDatabase).StringGet(hitName, CommandFlags.PreferReplica); + return result.HasValue ? result.ToString().ToInt32() : 0; + } + + /// + /// Tracks the hit. + /// + /// Name of the hit. + public void TrackHit(string hitName) + { + _redis.GetDatabase(HitDatabase).StringIncrement(hitName, 1, CommandFlags.FireAndForget); + } + + /// + /// Removes the hit. + /// + /// Name of the hit. + /// true if remove hit, false otherwise. + public bool RemoveHit(string hitName) + { + return _redis.GetDatabase(HitDatabase).KeyDelete(hitName); + } + + /// + /// Gets the event. + /// + /// The type of the event. + /// The event. + /// TEvent. + public TEvent GetEvent(ITelemetryEvent @event) where TEvent : class, new() + { + var valueBytes = _redis.GetDatabase(@event.Category).StringGet(@event.Name, CommandFlags.PreferReplica); + return !valueBytes.HasValue + ? null + : _redis.Serializer.Deserialize(valueBytes); + } + + /// + /// Tracks the event. + /// + /// The type of the t event. + /// Name of the event. + public void TrackEvent(ITelemetryEvent @event) where TEvent : class, new() + { + _redis.GetDatabase(@event.Category).StringSet( + @event.Name, + (string)@event.Event.GetSerializer(), + _defaultTTL, + When.Always, + CommandFlags.FireAndForget); + } + + /// + /// Tracks the event. + /// + /// The type of the event. + /// The event. + /// The TTL. + public void TrackEvent(ITelemetryEvent @event, TimeSpan ttl) where TEvent : class, new() + { + _redis.GetDatabase(@event.Category).StringSet( + @event.Name, + (string)@event.Event.GetSerializer(), + ttl, + When.Always, + CommandFlags.FireAndForget); + } + + /// + /// Gets the metric. + /// + /// Name of the metric. + /// The variation. + /// System.Int32. + public int GetMetric(string metricName, string variation) + { + var value = _redis.GetDatabase(MetricDatabase).HashGet(metricName, variation, CommandFlags.PreferReplica); + return value.HasValue ? value.ToString().ToInt32() : 0; + } + + /// + /// Tracks the metric. + /// + /// Name of the metric. + /// The variation. + public void TrackMetric(string metricName, string variation) + { + _redis.GetDatabase(MetricDatabase).HashIncrement(metricName, variation, 1, CommandFlags.FireAndForget); + } + + /// + /// Removes the metric. + /// + /// Name of the metric. + /// The variation. + /// true if remove metric, false otherwise. + public bool RemoveMetric(string metricName, string variation) + { + return _redis.GetDatabase(MetricDatabase).HashDelete(metricName, variation); + } + + /// + /// Tracks the exception. + /// + /// Type of the exception. + public void TrackException(Type exceptionType) + { + _redis.GetDatabase(MetricDatabase).HashIncrement( + $"Exceptions_{_suffix}", + exceptionType.FullName, + 1, + CommandFlags.FireAndForget); + } + + /// + /// Tracks the dependency. + /// + /// Type of the interface. + /// The resolved times. + public void TrackDependency(Type interfaceType, int resolvedTimes) + { + _redis.GetDatabase(MetricDatabase).HashIncrement( + $"Dependencies_{_suffix}", + interfaceType.FullName, + resolvedTimes, + CommandFlags.FireAndForget); + } + + #endregion + } +} diff --git a/Src/CrispyWaffle.Redis/Utils/Communications/RedisConnector.cs b/Src/CrispyWaffle.Redis/Utils/Communications/RedisConnector.cs new file mode 100644 index 00000000..cc442319 --- /dev/null +++ b/Src/CrispyWaffle.Redis/Utils/Communications/RedisConnector.cs @@ -0,0 +1,262 @@ +// *********************************************************************** +// Assembly : CrispyWaffle.Redis +// Author : Guilherme Branco Stracini +// Created : 09-06-2020 +// +// Last Modified By : Guilherme Branco Stracini +// Last Modified On : 09-06-2020 +// *********************************************************************** +// +// Copyright (c) Guilherme Branco Stracini ME. All rights reserved. +// +// +// *********************************************************************** +using CrispyWaffle.Configuration; +using StackExchange.Redis; +using StackExchange.Redis.Extensions.Core; +using StackExchange.Redis.Extensions.Core.Abstractions; +using StackExchange.Redis.Extensions.Core.Configuration; +using StackExchange.Redis.Extensions.Core.Implementations; +using System; +using System.Linq; + +namespace CrispyWaffle.Redis.Utils.Communications +{ + /// + /// Class RedisConnector. + /// Implements the + /// + /// + [ConnectionName("Redis")] + public class RedisConnector : IDisposable + { + #region Private fields + + /// + /// The disposed + /// + private bool _disposed; + + /// + /// The connection pool manager + /// + private readonly IRedisCacheConnectionPoolManager _connectionPoolManager; + + #endregion + + #region ~Ctor + + /// + /// Initializes a new instance of the class. + /// + /// The configuration. + /// The serializer. + /// Name of the client. + public RedisConnector(MasterSlaveConfiguration configuration, ISerializer serializer, string clientName) + : this(configuration.HostsList, configuration.Password, clientName, serializer) + { } + + /// + /// Initializes a new instance of the class. + /// + /// The configuration. + /// The serializer. + /// The queue prefix. + /// Name of the client. + /// queuePrefix + /// queuePrefix + public RedisConnector(MasterSlaveConfiguration configuration, ISerializer serializer, string queuePrefix, string clientName) + : this(configuration.HostsList, configuration.Password, clientName, serializer) => + QueuePrefix = queuePrefix ?? throw new ArgumentNullException(nameof(queuePrefix)); + + /// + /// Initializes a new instance of the class. + /// + /// The connection. + /// The serializer. + /// Name of the client. + public RedisConnector(IConnection connection, ISerializer serializer, string clientName) + : this(string.Concat(connection.Host, @":", connection.Port), connection.Credentials.Password, clientName, serializer) + { } + + /// + /// Initializes a new instance of the class. + /// + /// The connection. + /// The serializer. + /// The queue prefix. + /// Name of the client. + /// queuePrefix + /// queuePrefix + public RedisConnector(IConnection connection, ISerializer serializer, string queuePrefix, string clientName) + : this(string.Concat(connection.Host, @":", connection.Port), connection.Credentials.Password, clientName, serializer) + => QueuePrefix = queuePrefix ?? throw new ArgumentNullException(nameof(queuePrefix)); + + /// + /// Initializes a new instance of the class. + /// + /// The host. + /// The port. + /// The password. + /// The serializer. + /// Name of the client. + public RedisConnector(string host, int port, string password, ISerializer serializer, string clientName) + : this(string.Concat(host, @":", port), password, clientName, serializer) + { } + + /// + /// Initializes a new instance of the class. + /// + /// The host. + /// The port. + /// The password. + /// The serializer. + /// The queue prefix. + /// Name of the client. + /// queuePrefix + /// queuePrefix + public RedisConnector(string host, int port, string password, ISerializer serializer, string queuePrefix, string clientName) + : this(host, port, password, serializer, clientName) => + QueuePrefix = queuePrefix ?? throw new ArgumentNullException(nameof(queuePrefix)); + + /// + /// Initializes a new instance of the class. + /// + /// The hosts list. + /// The password. + /// The serializer. + /// The queue prefix. + /// Name of the client. + /// queuePrefix + /// queuePrefix + public RedisConnector(string hostsList, string password, ISerializer serializer, string queuePrefix, string clientName) + : this(hostsList, password, clientName, serializer) => + QueuePrefix = queuePrefix ?? throw new ArgumentNullException(nameof(queuePrefix)); + + /// + /// Initializes a new instance of the class. + /// + /// The hosts list. + /// The password. + /// Name of the client. + /// The serializer. + public RedisConnector(string hostsList, string password, string clientName, ISerializer serializer) + { + var redisConfiguration = new RedisConfiguration + { + AbortOnConnectFail = false, + AllowAdmin = true, + Password = password, + ServiceName = clientName, + Hosts = hostsList.Split(',').Select(host => + { + var split = host.Split(':'); + + var hostname = split[0]; + var port = split.Length > 1 ? int.Parse(split[1]) : 6379; + return new RedisHost { Host = hostname, Port = port }; + }).ToArray() + }; + + + //TODO add LogConsumer to logger parameter. + _connectionPoolManager = new RedisCacheConnectionPoolManager(redisConfiguration, null); + + + Serializer = serializer; + Cache = new RedisCacheClient(_connectionPoolManager, serializer, redisConfiguration); + QueuePrefix = "crispy-waffle"; + } + + /// + /// Finalizes an instance of the class. + /// + ~RedisConnector() + { + Dispose(false); + } + + /// + /// Disposes the specified disposing. + /// + /// if set to true [disposing]. + private void Dispose(bool disposing) + { + if (!disposing) + { + return; + } + + if (_disposed) + { + return; + } + + _connectionPoolManager.Dispose(); + _disposed = true; + } + + #endregion + + #region Implementation of IDisposable + + /// + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + #endregion + + #region Public properties + + /// + /// Gets the serializer. + /// + /// The serializer. + public ISerializer Serializer { get; } + + /// + /// Gets the cache. + /// + /// The cache. + public IRedisCacheClient Cache { get; } + + /// + /// Gets the subscriber. + /// + /// The subscriber. + public ISubscriber Subscriber => _connectionPoolManager.GetConnection().GetSubscriber(); + + /// + /// Gets the default server. + /// + /// IServer. + public IServer GetDefaultServer() => _connectionPoolManager.GetConnection().GetServer(_connectionPoolManager.GetConnection().GetEndPoints(true)[0]); + + /// + /// Gets the default database. + /// + /// The default database. + public IDatabase DefaultDatabase => _connectionPoolManager.GetConnection().GetDatabase(); + + /// + /// Gets the queue prefix. + /// + /// The queue prefix. + public string QueuePrefix { get; } + + /// + /// Gets the database. + /// + /// The database number. + /// State of the asynchronous. + /// IDatabase. + public IDatabase GetDatabase(int databaseNumber, object asyncState = null) => _connectionPoolManager.GetConnection().GetDatabase(databaseNumber, asyncState); + + #endregion + } +} diff --git a/Src/CrispyWaffle.Redis/Utils/MasterSlaveConfiguration.cs b/Src/CrispyWaffle.Redis/Utils/MasterSlaveConfiguration.cs new file mode 100644 index 00000000..aa8e97bc --- /dev/null +++ b/Src/CrispyWaffle.Redis/Utils/MasterSlaveConfiguration.cs @@ -0,0 +1,51 @@ +using CrispyWaffle.Configuration; +using System; + +namespace CrispyWaffle.Redis.Utils +{ + /// + /// The Redis master/slave connection configuration class. + /// + [ConnectionName("RedisMaster", Order = 0), ConnectionName("RedisSlave", Order = 1)] + public sealed class MasterSlaveConfiguration + { + /// + /// Gets the hosts list. + /// + /// The hosts list. + public string HostsList { get; } + + /// + /// Gets the password. + /// + /// The password. + public string Password { get; } + + /// + /// Initializes a new instance of the class. + /// + /// The master. + /// The slave. + /// master + /// slave + public MasterSlaveConfiguration(IConnection master, IConnection slave) + { + if (master == null) + { + throw new ArgumentNullException(nameof(master)); + } + + if (slave == null) + { + throw new ArgumentNullException(nameof(slave)); + } + + HostsList = string.Concat(master.Host, @":", master.Port, @",", slave.Host, @":", slave.Port); + + if (master.Credentials != null || slave.Credentials != null) + { + Password = master.Credentials?.Password ?? slave.Credentials.Password; + } + } + } +} diff --git a/Src/CrispyWaffle.Redis/Utils/RedisExtensions.cs b/Src/CrispyWaffle.Redis/Utils/RedisExtensions.cs new file mode 100644 index 00000000..477d365f --- /dev/null +++ b/Src/CrispyWaffle.Redis/Utils/RedisExtensions.cs @@ -0,0 +1,51 @@ +using CrispyWaffle.Composition; +using CrispyWaffle.Extensions; +using CrispyWaffle.Redis.Utils.Communications; +using StackExchange.Redis; +using System; + +namespace CrispyWaffle.Redis.Utils +{ + /// + /// The Redis extensions class. + /// + public static class RedisExtensions + { + #region Private fields + + /// + /// The connector + /// + private static readonly RedisConnector Connector = ServiceLocator.Resolve(); + + #endregion + + #region Public methods + + /// + /// Temporaries increase the desired cache key in the default connector database. + /// + /// The cache key. + /// The time to live. + /// System.Int32. + public static int TemporaryIncrease(string cacheKey, TimeSpan ttl) + { + Connector.DefaultDatabase.StringIncrement(cacheKey, 1, CommandFlags.FireAndForget); + + Connector.DefaultDatabase.KeyExpire(cacheKey, ttl, CommandFlags.FireAndForget); + + return Connector.DefaultDatabase.StringGet(cacheKey, CommandFlags.PreferReplica).ToString().ToInt32(); + } + + /// + /// Flushes the database. + /// + /// The database. + public static void FlushDatabase(int database) + { + Connector.GetDefaultServer().FlushDatabase(database); + } + + #endregion + } +} diff --git a/Src/CrispyWaffle/Infrastructure/EnvironmentHelper.cs b/Src/CrispyWaffle/Infrastructure/EnvironmentHelper.cs index 3c58bac5..e3edca44 100644 --- a/Src/CrispyWaffle/Infrastructure/EnvironmentHelper.cs +++ b/Src/CrispyWaffle/Infrastructure/EnvironmentHelper.cs @@ -122,6 +122,12 @@ private static string GetIpAddressExternal() /// Display name of the application. public static void SetDisplayApplicationName(string displayApplicationName) => DisplayApplicationName = displayApplicationName; + /// + /// Sets the operation. + /// + /// The operation. + public static void SetOperation(string operation) => Operation = operation; + #endregion #region Public properties @@ -168,6 +174,12 @@ private static string GetIpAddressExternal() /// The display name of the application. public static string DisplayApplicationName { get; private set; } + /// + /// Gets the operation. + /// + /// The operation. + public static string Operation { get; private set; } + /// /// Gets the version. /// diff --git a/Tests/CrispyWaffle.Tests/CrispyWaffle.Tests.csproj b/Tests/CrispyWaffle.Tests/CrispyWaffle.Tests.csproj index fa6e85ef..95507968 100644 --- a/Tests/CrispyWaffle.Tests/CrispyWaffle.Tests.csproj +++ b/Tests/CrispyWaffle.Tests/CrispyWaffle.Tests.csproj @@ -11,10 +11,10 @@ runtime; build; native; contentfiles; analyzers all - + - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/appveyor.yml b/appveyor.yml index 2d1c17b0..210d1eaa 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,4 +1,4 @@ -version: 2.3.{build} +version: 2.4.{build} skip_tags: true image: Visual Studio 2019 configuration: Release @@ -71,10 +71,12 @@ after_build: - xcopy %CD%\Src\%SOLUTION_NAME%.Configuration\bin\Release\netstandard2.0\*.* %CD%\Build\Configuration\ - xcopy %CD%\Src\%SOLUTION_NAME%.Elmah\bin\Release\netstandard2.0\*.* %CD%\Build\Elmah\ - xcopy %CD%\Src\%SOLUTION_NAME%.Log4Net\bin\Release\netstandard2.0\*.* %CD%\Build\Log4Net\ +- xcopy %CD%\Src\%SOLUTION_NAME%.Redis\bin\Release\netstandard2.0\*.* %CD%\Build\Redis\ - copy %CD%\Src\%SOLUTION_NAME%\bin\Release\%SOLUTION_NAME%.%APPVEYOR_BUILD_VERSION%.nupkg %SOLUTION_NAME%.%APPVEYOR_BUILD_VERSION%.nupkg - copy %CD%\Src\%SOLUTION_NAME%.Configuration\bin\Release\%SOLUTION_NAME%.Configuration.%APPVEYOR_BUILD_VERSION%.nupkg %SOLUTION_NAME%.Configuration.%APPVEYOR_BUILD_VERSION%.nupkg - copy %CD%\Src\%SOLUTION_NAME%.Elmah\bin\Release\%SOLUTION_NAME%.Elmah.%APPVEYOR_BUILD_VERSION%.nupkg %SOLUTION_NAME%.Elmah.%APPVEYOR_BUILD_VERSION%.nupkg - copy %CD%\Src\%SOLUTION_NAME%.Log4Net\bin\Release\%SOLUTION_NAME%.Log4Net.%APPVEYOR_BUILD_VERSION%.nupkg %SOLUTION_NAME%.Log4Net.%APPVEYOR_BUILD_VERSION%.nupkg +- copy %CD%\Src\%SOLUTION_NAME%.Redis\bin\Release\%SOLUTION_NAME%.Redis.%APPVEYOR_BUILD_VERSION%.nupkg %SOLUTION_NAME%.Redis.%APPVEYOR_BUILD_VERSION%.nupkg - rd /s /q %CD%\Src\%SOLUTION_NAME%\bin\Release\ - xcopy %CD%\Tests\%SOLUTION_NAME%.Tests\*.xml %CD%\Coverage\ - xcopy %CD%\Tests\%SOLUTION_NAME%.Tests\*.json %CD%\Coverage\ @@ -83,6 +85,7 @@ after_build: - 7z a -tzip -mx9 "%SOLUTION_NAME%.%APPVEYOR_BUILD_VERSION%.Configuration.zip" Build\Configuration - 7z a -tzip -mx9 "%SOLUTION_NAME%.%APPVEYOR_BUILD_VERSION%.Elmah.zip" Build\Elmah - 7z a -tzip -mx9 "%SOLUTION_NAME%.%APPVEYOR_BUILD_VERSION%.Log4Net.zip" Build\Log4Net +- 7z a -tzip -mx9 "%SOLUTION_NAME%.%APPVEYOR_BUILD_VERSION%.Redis.zip" Build\Log4Net - 7z a -tzip -mx9 "%SOLUTION_NAME%.%APPVEYOR_BUILD_VERSION%.Coverage.zip" Coverage artifacts: @@ -94,6 +97,8 @@ artifacts: name: ZipFileElmah - path: $(SOLUTION_NAME).%APPVEYOR_BUILD_VERSION%.Log4Net.zip name: ZipFileLog4Net +- path: $(SOLUTION_NAME).%APPVEYOR_BUILD_VERSION%.Redis.zip + name: ZipFileRedis - path: $(SOLUTION_NAME).%APPVEYOR_BUILD_VERSION%.nupkg name: PackageCore - path: $(SOLUTION_NAME).Configuration.%APPVEYOR_BUILD_VERSION%.nupkg @@ -102,6 +107,8 @@ artifacts: name: PackageElmah - path: $(SOLUTION_NAME).Log4Net.%APPVEYOR_BUILD_VERSION%.nupkg name: PackageLog4Net +- path: $(SOLUTION_NAME).Redis.%APPVEYOR_BUILD_VERSION%.nupkg + name: PackageRedis - path: $(SOLUTION_NAME).%APPVEYOR_BUILD_VERSION%.Coverage.zip name: Coverage @@ -126,6 +133,11 @@ deploy: on: branch: master artifact: PackageLog4Net +- provider: Environment + name: NuGet + on: + branch: master + artifact: PackageRedis - provider: GitHub on: branch: master @@ -154,6 +166,13 @@ deploy: auth_token: $(GITHUB_TOKEN) force_update: true artifact: ZipFileLog4Net +- provider: GitHub + on: + branch: master + tag: $(appveyor_build_version) + auth_token: $(GITHUB_TOKEN) + force_update: true + artifact: ZipFileRedis - provider: GitHub on: branch: master diff --git a/docs/changelog.md b/docs/changelog.md index 7c612aaf..e8745762 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,6 +1,11 @@ # Changelog -## Version 2.3 [2020-06-06] +## Version 2.4 [2020-09-06] + +- Add CrispyWaffle.Redis project & package +- Add documentation for caching. + +## Version 2.3 [2020-09-06] - Add FailoverExceptionHandler class - [issue #73](https://github.com/guibranco/CrispyWaffle/issues/73). - Add Environment Helper class - [issue #75](https://github.com/guibranco/CrispyWaffle/issues/75). diff --git a/docs/user-guide/basic-usage.md b/docs/user-guide/basic-usage.md index d28cf5c6..b19ebb9b 100644 --- a/docs/user-guide/basic-usage.md +++ b/docs/user-guide/basic-usage.md @@ -4,6 +4,7 @@ The Crispy Waffle has the following features: - [Logging](logging.md) +- [Caching](caching.md) - [Events dispatching](events.md) - Conversion extensions - String extensions diff --git a/docs/user-guide/caching.md b/docs/user-guide/caching.md new file mode 100644 index 00000000..27fb3afb --- /dev/null +++ b/docs/user-guide/caching.md @@ -0,0 +1,48 @@ +# Caching + +## About + +Cache data is easy with Crispy Waffle, there are available two **repositories** for cache: + +- **MemoryCacheRepository**: A cache repository that store data in application memory. +- **RedisCacheRepository**: A cache repositority that use Redis as persistence mechanism. + +There is also a helper class, **CacheManager** that make cache usage easy. + +## Examples + +### Single cache repository + CacheManager helper class + +The following example uses the **RedisCacheRepository** and the **CacheManager** helper class: + +Process #1: + +```cs +ServiceLocator.Register(LifeStyle.SINGLETON); +CacheManager.AddRepository(); + +CacheManager.Set("some string text", "MyKey"); +``` +Process #2 +```cs +ceLocator.Register(LifeStyle.SINGLETON); +CacheManager.AddRepository(); + +var cachedValueFromRedis = CacheManager.Get("MyKey"); //some string text +``` + +### Multiple cache repositories (registration precedence) + +Using multiple cache repositories + +```cs +ServiceLocator.Register(LifeStyle.SINGLETON); +ServiceLocator.Register(LifeStyle.SINGLETON); + +CacheManager.AddRepository(); //or directly: CacheManager.AddRepository(); +CacheManager.AddRepository(); //RedisCacheRepository + +var cachedValue = CacheManager.Get("MyKey"); //first will lookup in MemoryCacheRepository then RedisCacheRepository + +var cachedValueFromSpecificRepository = CachemManager.GetFrom("MyKey"); //will get the value only from RedisCacheRepository. +``` \ No newline at end of file diff --git a/docs/user-guide/events.md b/docs/user-guide/events.md index f8dc2232..9e80ad15 100644 --- a/docs/user-guide/events.md +++ b/docs/user-guide/events.md @@ -41,7 +41,9 @@ Event handling is currently done synchronously. There are plans to do async, iss --- -## Multiple event handlers +## Examples + +### Multiple event handlers for the same event A more complex example using `Events` & `EventsHandlers`. In this example, there are two event handlers and the event class has some properties: diff --git a/docs/user-guide/logging.md b/docs/user-guide/logging.md index 6973a356..d5f70fe9 100644 --- a/docs/user-guide/logging.md +++ b/docs/user-guide/logging.md @@ -19,7 +19,7 @@ Default adapters: - RollingTextFileLogAdapter: Text file output, rolling to another file every time each reaches defined size. - ... -## Example +## Examples A simple `console application` with simple (*colored console output*) logging example: diff --git a/docs/user-guide/scheduled-jobs.md b/docs/user-guide/scheduled-jobs.md index d7efa4e2..ad5afe14 100644 --- a/docs/user-guide/scheduled-jobs.md +++ b/docs/user-guide/scheduled-jobs.md @@ -17,7 +17,6 @@ Currently supports the following formats: Check the [Wikipedia's `CRON`](https://en.wikipedia.org/wiki/Cron) page for more examples and details. - ## Examples Using [`cron` expression](https://en.wikipedia.org/wiki/Cron) to schedule tasks/jobs inside a program. From ffd0b5bee64c0a9eb762f8363f3077fee2ceb65b Mon Sep 17 00:00:00 2001 From: Guilherme Branco Stracini Date: Sun, 6 Sep 2020 22:25:05 -0300 Subject: [PATCH 2/4] RedisConnector --- Src/CrispyWaffle.Redis/Utils/Communications/RedisConnector.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Src/CrispyWaffle.Redis/Utils/Communications/RedisConnector.cs b/Src/CrispyWaffle.Redis/Utils/Communications/RedisConnector.cs index cc442319..340c4c69 100644 --- a/Src/CrispyWaffle.Redis/Utils/Communications/RedisConnector.cs +++ b/Src/CrispyWaffle.Redis/Utils/Communications/RedisConnector.cs @@ -160,7 +160,7 @@ public RedisConnector(string hostsList, string password, string clientName, ISer //TODO add LogConsumer to logger parameter. - _connectionPoolManager = new RedisCacheConnectionPoolManager(redisConfiguration, null); + _connectionPoolManager = new RedisCacheConnectionPoolManager(redisConfiguration); Serializer = serializer; From df68c13d64f7dd1175ebbb456510dc78b03b65b4 Mon Sep 17 00:00:00 2001 From: Guilherme Branco Stracini Date: Sun, 6 Sep 2020 22:28:55 -0300 Subject: [PATCH 3/4] Redis log classes --- .../GeneralPropagationStrategy.cs | 48 +++ .../IPropagationStrategy.cs | 30 ++ .../OperationPropagationStrategy.cs | 51 +++ .../ProcessPropagationStrategy.cs | 50 +++ .../Log/PubSubRedisLogProvider.cs | 339 ++++++++++++++++++ 5 files changed, 518 insertions(+) create mode 100644 Src/CrispyWaffle.Redis/Log/PropagationStrategy/GeneralPropagationStrategy.cs create mode 100644 Src/CrispyWaffle.Redis/Log/PropagationStrategy/IPropagationStrategy.cs create mode 100644 Src/CrispyWaffle.Redis/Log/PropagationStrategy/OperationPropagationStrategy.cs create mode 100644 Src/CrispyWaffle.Redis/Log/PropagationStrategy/ProcessPropagationStrategy.cs create mode 100644 Src/CrispyWaffle.Redis/Log/PubSubRedisLogProvider.cs diff --git a/Src/CrispyWaffle.Redis/Log/PropagationStrategy/GeneralPropagationStrategy.cs b/Src/CrispyWaffle.Redis/Log/PropagationStrategy/GeneralPropagationStrategy.cs new file mode 100644 index 00000000..9de58283 --- /dev/null +++ b/Src/CrispyWaffle.Redis/Log/PropagationStrategy/GeneralPropagationStrategy.cs @@ -0,0 +1,48 @@ +using StackExchange.Redis; +using System.Threading; +using System.Threading.Tasks; + +namespace CrispyWaffle.Redis.Log.PropagationStrategy +{ + /// + /// The general propagation strategy class. + /// + /// + public sealed class GeneralPropagation : IPropagationStrategy + { + #region Implementation of IPropagationStrategy + + /// + /// Propagates the specified message using specific strategy to the publisher. + /// + /// The message. + /// The queue prefix. + /// The publisher. + public void Propagate(string message, string queuePrefix, ISubscriber publisher) + { + publisher.Publish($"{queuePrefix}-queues", "general"); + publisher.Publish($"{queuePrefix}-general", message); + } + + /// + /// propagate as an asynchronous operation. + /// + /// The message. + /// The queue prefix. + /// The publisher. + /// The cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// Task. + public async Task PropagateAsync(string message, string queuePrefix, ISubscriber publisher, CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + { + return; + } + + await publisher.PublishAsync($"{queuePrefix}-queues", "general").ConfigureAwait(false); + await publisher.PublishAsync($"{queuePrefix}-general", message).ConfigureAwait(false); + } + + #endregion + } +} diff --git a/Src/CrispyWaffle.Redis/Log/PropagationStrategy/IPropagationStrategy.cs b/Src/CrispyWaffle.Redis/Log/PropagationStrategy/IPropagationStrategy.cs new file mode 100644 index 00000000..bd3b5d32 --- /dev/null +++ b/Src/CrispyWaffle.Redis/Log/PropagationStrategy/IPropagationStrategy.cs @@ -0,0 +1,30 @@ +using StackExchange.Redis; +using System.Threading; +using System.Threading.Tasks; + +namespace CrispyWaffle.Redis.Log.PropagationStrategy +{ + /// + /// The propagation strategy interface + /// + public interface IPropagationStrategy + { + /// + /// Propagates the specified message using specific strategy to the publisher. + /// + /// The message. + /// The queue prefix. + /// The publisher. + void Propagate(string message, string queuePrefix, ISubscriber publisher); + + /// + /// Propagates the asynchronous. + /// + /// The message. + /// The queue prefix. + /// The publisher. + /// The cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// Task. + Task PropagateAsync(string message, string queuePrefix, ISubscriber publisher, CancellationToken cancellationToken); + } +} diff --git a/Src/CrispyWaffle.Redis/Log/PropagationStrategy/OperationPropagationStrategy.cs b/Src/CrispyWaffle.Redis/Log/PropagationStrategy/OperationPropagationStrategy.cs new file mode 100644 index 00000000..7fcfd43c --- /dev/null +++ b/Src/CrispyWaffle.Redis/Log/PropagationStrategy/OperationPropagationStrategy.cs @@ -0,0 +1,51 @@ +using CrispyWaffle.Infrastructure; +using StackExchange.Redis; +using System.Threading; +using System.Threading.Tasks; + +namespace CrispyWaffle.Redis.Log.PropagationStrategy +{ + /// + /// The operation propagation strategy class. + /// + /// + + public sealed class OperationPropagationStrategy : IPropagationStrategy + { + #region Implementation of IPropagationStrategy + + /// + /// Propagates the specified message using specific strategy to the publisher. + /// + /// The message. + /// The queue prefix. + /// The publisher. + public void Propagate(string message, string queuePrefix, ISubscriber publisher) + { + publisher.Publish($"{queuePrefix}-queues", EnvironmentHelper.Operation); + publisher.Publish($"{queuePrefix}-{EnvironmentHelper.Operation}", message); + } + + /// + /// propagate as an asynchronous operation. + /// + /// The message. + /// The queue prefix. + /// The publisher. + /// The cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// Task. + public async Task PropagateAsync(string message, string queuePrefix, ISubscriber publisher, + CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + { + return; + } + + await publisher.PublishAsync($"{queuePrefix}-queues", EnvironmentHelper.Operation).ConfigureAwait(false); + await publisher.PublishAsync($"{queuePrefix}-{EnvironmentHelper.Operation}", message).ConfigureAwait(false); + } + + #endregion + } +} diff --git a/Src/CrispyWaffle.Redis/Log/PropagationStrategy/ProcessPropagationStrategy.cs b/Src/CrispyWaffle.Redis/Log/PropagationStrategy/ProcessPropagationStrategy.cs new file mode 100644 index 00000000..e852bcb8 --- /dev/null +++ b/Src/CrispyWaffle.Redis/Log/PropagationStrategy/ProcessPropagationStrategy.cs @@ -0,0 +1,50 @@ +using CrispyWaffle.Infrastructure; +using StackExchange.Redis; +using System.Threading; +using System.Threading.Tasks; + +namespace CrispyWaffle.Redis.Log.PropagationStrategy +{ + /// + /// The process propagation strategy + /// + /// + + public sealed class ProcessPropagation : IPropagationStrategy + { + #region Implementation of IPropagationStrategy + + /// + /// Propagates the specified message using specific strategy to the publisher. + /// + /// The message. + /// The queue prefix. + /// The publisher. + public void Propagate(string message, string queuePrefix, ISubscriber publisher) + { + publisher.Publish($"{queuePrefix}-queues", EnvironmentHelper.ProcessId); + publisher.Publish($"{queuePrefix}-{EnvironmentHelper.ProcessId}", message); + } + + /// + /// propagate as an asynchronous operation. + /// + /// The message. + /// The queue prefix. + /// The publisher. + /// The cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// Task. + public async Task PropagateAsync(string message, string queuePrefix, ISubscriber publisher, CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + { + return; + } + + await publisher.PublishAsync($"{queuePrefix}-queues", EnvironmentHelper.ProcessId).ConfigureAwait(false); + await publisher.PublishAsync($"{queuePrefix}-{EnvironmentHelper.ProcessId}", message).ConfigureAwait(false); + } + + #endregion + } +} diff --git a/Src/CrispyWaffle.Redis/Log/PubSubRedisLogProvider.cs b/Src/CrispyWaffle.Redis/Log/PubSubRedisLogProvider.cs new file mode 100644 index 00000000..a27081ed --- /dev/null +++ b/Src/CrispyWaffle.Redis/Log/PubSubRedisLogProvider.cs @@ -0,0 +1,339 @@ +using System; +using System.Collections.Concurrent; +using System.Threading; +using CrispyWaffle.Extensions; +using CrispyWaffle.Infrastructure; +using CrispyWaffle.Log; +using CrispyWaffle.Log.Providers; +using CrispyWaffle.Redis.Log.PropagationStrategy; +using CrispyWaffle.Redis.Utils.Communications; +using CrispyWaffle.Serialization; + +namespace CrispyWaffle.Redis.Log +{ + public class PubSubRedisLogProvider : ILogProvider + { + #region Private fields + + /// + /// The Redis connector + /// + private readonly RedisConnector _redis; + + /// + /// The propagation strategy + /// + private readonly IPropagationStrategy _propagationStrategy; + + /// + /// The level + /// + private LogLevel _level; + + /// + /// The cancellation token + /// + private readonly CancellationToken _cancellationToken; + + /// + /// The queue + /// + private readonly ConcurrentQueue _queue; + + #endregion + + #region ~Ctor + + /// + /// Initializes a new instance of the class. + /// + /// The redis. + /// The propagation strategy. + /// The cancellation token. + public PubSubRedisLogProvider(RedisConnector redis, IPropagationStrategy propagationStrategy, CancellationToken cancellationToken) + { + _redis = redis; + _propagationStrategy = propagationStrategy; + _cancellationToken = cancellationToken; + _queue = new ConcurrentQueue(); + var thread = new Thread(Worker); + thread.Start(); + } + + #endregion + + #region Private methods + + /// + /// Workers this instance. + /// + private void Worker() + { + Thread.CurrentThread.Name = "Message queue Redis log provider worker"; + Thread.Sleep(1000); + + while (true) + { + while (_queue.Count > 0) + { + if (!_queue.TryDequeue(out var message)) + { + break; + } + + PropagateMessageInternal(message); + } + + if (_cancellationToken.IsCancellationRequested) + { + break; + } + } + } + + /// + /// Serializes the specified level. + /// + /// The level. + /// The category. + /// The message. + /// The identifier. + /// System.String. + private static string Serialize(LogLevel level, string category, string message, string identifier = null) + { + return (string)new LogMessage + { + Category = category, + Date = DateTime.Now, + Hostname = EnvironmentHelper.Host, + Id = Guid.NewGuid().ToString(), + IpAddress = EnvironmentHelper.IpAddress, + IpAddressRemote = EnvironmentHelper.IpAddressExternal, + Level = level.GetHumanReadableValue(), + Message = message, + MessageIdentifier = identifier, + Operation = EnvironmentHelper.Operation, + ProcessId = EnvironmentHelper.ProcessId, + UserAgent = EnvironmentHelper.UserAgent, + ThreadId = Thread.CurrentThread.ManagedThreadId, + ThreadName = Thread.CurrentThread.Name + }.GetSerializer(); + } + + /// + /// Propagates the message internal. + /// + /// The message. + private void PropagateMessageInternal(string message) + { + try + { + _propagationStrategy.Propagate(message, _redis.QueuePrefix, _redis.Subscriber); + } + catch (Exception e) + { + LogConsumer.Debug("Message: {0} | Stack Trace: {1}", e.Message, e.StackTrace); + } + } + + /// + /// Propagates the internal. + /// + /// The message. + private void PropagateInternal(string message) + { + _queue.Enqueue(message); + } + + #endregion + + #region Implementation of ILogProvider + + /// + /// Sets the level. + /// + /// The level. + public void SetLevel(LogLevel level) + { + _level = level; + } + + /// + /// Logs the message with fatal level. + /// + /// The category. + /// The message. + public void Fatal(string category, string message) + { + if (!_level.HasFlag(LogLevel.FATAL)) + { + return; + } + + PropagateInternal(Serialize(LogLevel.FATAL, category, message)); + } + + /// + /// Logs the message with error level. + /// + /// The category. + /// The message. + public void Error(string category, string message) + { + if (!_level.HasFlag(LogLevel.ERROR)) + { + return; + } + + PropagateInternal(Serialize(LogLevel.ERROR, category, message)); + } + + /// + /// Logs the message with warning level. + /// + /// The category + /// The message to be logged + public void Warning(string category, string message) + { + if (!_level.HasFlag(LogLevel.WARNING)) + { + return; + } + + PropagateInternal(Serialize(LogLevel.WARNING, category, message)); + } + + /// + /// Logs the message with info level. + /// + /// The category + /// The message to be logged + public void Info(string category, string message) + { + if (!_level.HasFlag(LogLevel.INFO)) + { + return; + } + + PropagateInternal(Serialize(LogLevel.INFO, category, message)); + } + + /// + /// Logs the message with trace level. + /// + /// The category + /// The message to be logged + public void Trace(string category, string message) + { + if (!_level.HasFlag(LogLevel.TRACE)) + { + return; + } + + PropagateInternal(Serialize(LogLevel.TRACE, category, message)); + } + + /// + /// Traces the specified category. + /// + /// The category. + /// The message. + /// The exception. + public void Trace(string category, string message, Exception exception) + { + if (!_level.HasFlag(LogLevel.TRACE)) + { + return; + } + + PropagateInternal(Serialize(LogLevel.TRACE, category, message)); + + Trace(category, exception); + } + + /// + /// Traces the specified category. + /// + /// The category. + /// The exception. + public void Trace(string category, Exception exception) + { + if (!_level.HasFlag(LogLevel.TRACE)) + { + return; + } + + do + { + + PropagateInternal(Serialize(LogLevel.TRACE, category, exception.Message)); + PropagateInternal(Serialize(LogLevel.TRACE, category, exception.StackTrace)); + + exception = exception.InnerException; + + } while (exception != null); + + } + + /// + /// Logs the message with debug level. + /// + /// The category + /// The message to be logged + public void Debug(string category, string message) + { + if (!_level.HasFlag(LogLevel.DEBUG)) + { + return; + } + + PropagateInternal(Serialize(LogLevel.DEBUG, category, message)); + } + + /// + /// Logs the message as a file/attachment with a file name/identifier with debug level + /// + /// The category + /// The content to be stored + /// The file name of the content. This can be a filename, a key, a identifier. Depends upon each implementation + public void Debug(string category, string content, string identifier) + { + if (!_level.HasFlag(LogLevel.DEBUG)) + { + return; + } + + PropagateInternal(Serialize(LogLevel.DEBUG, category, content, identifier)); + } + + /// + /// Logs the message as a file/attachment with a file name/identifier with debug level using a custom serializer or default. + /// + /// any class that can be serialized to the serializer format + /// The category + /// The object to be serialized + /// The filename/attachment identifier (file name or key) + /// (Optional) the custom serializer format + public void Debug(string category, T content, string identifier, SerializerFormat customFormat = SerializerFormat.NONE) where T : class, new() + { + if (!_level.HasFlag(LogLevel.DEBUG)) + { + return; + } + + string serialized; + if (customFormat == SerializerFormat.NONE) + { + serialized = (string)content.GetSerializer(); + } + else + { + serialized = (string)content.GetCustomSerializer(customFormat); + } + + PropagateInternal(Serialize(LogLevel.DEBUG, category, serialized, identifier)); + } + + #endregion + } +} From bf9ebd6090ac7fe5ace7e715c72de92c451592ef Mon Sep 17 00:00:00 2001 From: Guilherme Branco Stracini Date: Sun, 6 Sep 2020 22:30:58 -0300 Subject: [PATCH 4/4] Add Nuget package --- CrispyWaffle.sln | 4 +-- .../CrispyWaffle.Redis.csproj | 28 ++++++++++++++++++- 2 files changed, 29 insertions(+), 3 deletions(-) diff --git a/CrispyWaffle.sln b/CrispyWaffle.sln index 2c633779..b6554138 100644 --- a/CrispyWaffle.sln +++ b/CrispyWaffle.sln @@ -21,10 +21,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CrispyWaffle.Elmah", "Src\C EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CrispyWaffle.Log4Net", "Src\CrispyWaffle.Log4Net\CrispyWaffle.Log4Net.csproj", "{D6164E04-F1C9-4D86-9B9C-41036B816C4A}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CrispyWaffle.Tests", "Tests\CrispyWaffle.Tests\CrispyWaffle.Tests.csproj", "{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}" -EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CrispyWaffle.Redis", "Src\CrispyWaffle.Redis\CrispyWaffle.Redis.csproj", "{47DB173E-BC2B-4C16-B2A9-9506E57D6D9E}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CrispyWaffle.Tests", "Tests\CrispyWaffle.Tests\CrispyWaffle.Tests.csproj", "{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU diff --git a/Src/CrispyWaffle.Redis/CrispyWaffle.Redis.csproj b/Src/CrispyWaffle.Redis/CrispyWaffle.Redis.csproj index e83aa74a..aca43217 100644 --- a/Src/CrispyWaffle.Redis/CrispyWaffle.Redis.csproj +++ b/Src/CrispyWaffle.Redis/CrispyWaffle.Redis.csproj @@ -1,7 +1,22 @@ - netstandard2.0 + 47DB173E-BC2B-4C16-B2A9-9506E57D6D9E + netstandard2.0;netstandard2.1 + true + Guilherme Branco Stracini + © 2020 Guilherme Branco Stracini. All rights reserved. + The CrispyWaffle Redis package + https://guibranco.github.io/CrispyWaffle + https://github.com/guibranco/CrispyWaffle + GIT + toolkit sdk framework library lib redis cache telemetry log provider + logo.png + 1.0.0 + 1.0.0.0 + LICENSE + Guilherme Branco Stracini ME + Redis release @@ -13,5 +28,16 @@ + + + + True + + + + True + + +