diff --git a/src/DotNetCore.CAP.SqlServer/SqlServerMonitoringApi.cs b/src/DotNetCore.CAP.SqlServer/SqlServerMonitoringApi.cs index 8f8d183..ef74ede 100644 --- a/src/DotNetCore.CAP.SqlServer/SqlServerMonitoringApi.cs +++ b/src/DotNetCore.CAP.SqlServer/SqlServerMonitoringApi.cs @@ -55,7 +55,6 @@ _options.Schema); } return stats; }); - statistics.Servers = 1; return statistics; } diff --git a/src/DotNetCore.CAP/CapCache.cs b/src/DotNetCore.CAP/CapCache.cs new file mode 100644 index 0000000..df2f844 --- /dev/null +++ b/src/DotNetCore.CAP/CapCache.cs @@ -0,0 +1,299 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; + +namespace DotNetCore.CAP +{ + #region Cache class + /// + /// This is a generic cache subsystem based on key/value pairs, where key is generic, too. Key must be unique. + /// Every cache entry has its own timeout. + /// Cache is thread safe and will delete expired entries on its own using System.Threading.Timers (which run on threads). + /// + public class Cache : IDisposable + { + #region Constructor and class members + /// + /// Initializes a new instance of the class. + /// + public Cache() { } + + private Dictionary cache = new Dictionary(); + private Dictionary timers = new Dictionary(); + private ReaderWriterLockSlim locker = new ReaderWriterLockSlim(); + #endregion + + #region IDisposable implementation & Clear + private bool disposed = false; + + /// + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + /// + /// Releases unmanaged and - optionally - managed resources. + /// + /// + /// true to release both managed and unmanaged resources; false to release only unmanaged resources. + protected virtual void Dispose(bool disposing) + { + if (!disposed) + { + disposed = true; + + if (disposing) + { + // Dispose managed resources. + Clear(); + locker.Dispose(); + } + // Dispose unmanaged resources + } + } + + /// + /// Clears the entire cache and disposes all active timers. + /// + public void Clear() + { + locker.EnterWriteLock(); + try + { + try + { + foreach (Timer t in timers.Values) + t.Dispose(); + } + catch + { } + + timers.Clear(); + cache.Clear(); + } + finally { locker.ExitWriteLock(); } + } + #endregion + + #region CheckTimer + // Checks whether a specific timer already exists and adds a new one, if not + private void CheckTimer(K key, TimeSpan? cacheTimeout, bool restartTimerIfExists) + { + Timer timer; + + if (timers.TryGetValue(key, out timer)) + { + if (restartTimerIfExists) + { + timer.Change( + (cacheTimeout == null ? Timeout.InfiniteTimeSpan : cacheTimeout.Value), + Timeout.InfiniteTimeSpan); + } + } + else + timers.Add( + key, + new Timer( + new TimerCallback(RemoveByTimer), + key, + (cacheTimeout == null ? Timeout.InfiniteTimeSpan : cacheTimeout.Value), + Timeout.InfiniteTimeSpan)); + } + + private void RemoveByTimer(object state) + { + Remove((K)state); + } + #endregion + + #region AddOrUpdate, Get, Remove, Exists, Clear + /// + /// Adds or updates the specified cache-key with the specified cacheObject and applies a specified timeout (in seconds) to this key. + /// + /// The cache-key to add or update. + /// The cache object to store. + /// The cache timeout (lifespan) of this object. Must be 1 or greater. + /// Specify Timeout.Infinite to keep the entry forever. + /// (Optional). If set to true, the timer for this cacheObject will be reset if the object already + /// exists in the cache. (Default = false). + public void AddOrUpdate(K key, T cacheObject, TimeSpan? cacheTimeout, bool restartTimerIfExists = false) + { + if (disposed) return; + + locker.EnterWriteLock(); + try + { + CheckTimer(key, cacheTimeout, restartTimerIfExists); + + if (!cache.ContainsKey(key)) + cache.Add(key, cacheObject); + else + cache[key] = cacheObject; + } + finally { locker.ExitWriteLock(); } + } + + /// + /// Adds or updates the specified cache-key with the specified cacheObject and applies Timeout.Infinite to this key. + /// + /// The cache-key to add or update. + /// The cache object to store. + public void AddOrUpdate(K key, T cacheObject) + { + AddOrUpdate(key, cacheObject, Timeout.InfiniteTimeSpan); + } + + /// + /// Gets the cache entry with the specified key or returns default(T) if the key is not found. + /// + /// The cache-key to retrieve. + /// The object from the cache or default(T), if not found. + public T this[K key] => Get(key); + + /// + /// Gets the cache entry with the specified key or return default(T) if the key is not found. + /// + /// The cache-key to retrieve. + /// The object from the cache or default(T), if not found. + public T Get(K key) + { + if (disposed) return default(T); + + locker.EnterReadLock(); + try + { + T rv; + return (cache.TryGetValue(key, out rv) ? rv : default(T)); + } + finally { locker.ExitReadLock(); } + } + + /// + /// Tries to gets the cache entry with the specified key. + /// + /// The key. + /// (out) The value, if found, or default(T), if not. + /// True, if key exists, otherwise false. + public bool TryGet(K key, out T value) + { + if (disposed) + { + value = default(T); + return false; + } + + locker.EnterReadLock(); + try + { + return cache.TryGetValue(key, out value); + } + finally { locker.ExitReadLock(); } + } + + /// + /// Removes a series of cache entries in a single call for all key that match the specified key pattern. + /// + /// The key pattern to remove. The Predicate has to return true to get key removed. + public void Remove(Predicate keyPattern) + { + if (disposed) return; + + locker.EnterWriteLock(); + try + { + var removers = (from k in cache.Keys.Cast() + where keyPattern(k) + select k).ToList(); + + foreach (K workKey in removers) + { + try { timers[workKey].Dispose(); } + catch { } + timers.Remove(workKey); + cache.Remove(workKey); + } + } + finally { locker.ExitWriteLock(); } + } + + /// + /// Removes the specified cache entry with the specified key. + /// If the key is not found, no exception is thrown, the statement is just ignored. + /// + /// The cache-key to remove. + public void Remove(K key) + { + if (disposed) return; + + locker.EnterWriteLock(); + try + { + if (cache.ContainsKey(key)) + { + try { timers[key].Dispose(); } + catch { } + timers.Remove(key); + cache.Remove(key); + } + } + finally { locker.ExitWriteLock(); } + } + + /// + /// Checks if a specified key exists in the cache. + /// + /// The cache-key to check. + /// True if the key exists in the cache, otherwise False. + public bool Exists(K key) + { + if (disposed) return false; + + locker.EnterReadLock(); + try + { + return cache.ContainsKey(key); + } + finally { locker.ExitReadLock(); } + } + #endregion + } + #endregion + + #region Other Cache classes (derived) + /// + /// This is a generic cache subsystem based on key/value pairs, where key is a string. + /// You can add any item to this cache as long as the key is unique, so treat keys as something like namespaces and build them with a + /// specific system/syntax in your application. + /// Every cache entry has its own timeout. + /// Cache is thread safe and will delete expired entries on its own using System.Threading.Timers (which run on threads). + /// + //public class Cache : Cache + //{ + //} + + /// + /// The non-generic Cache class instanciates a Cache{object} that can be used with any type of (mixed) contents. + /// It also publishes a static .Global member, so a cache can be used even without creating a dedicated instance. + /// The .Global member is lazy instanciated. + /// + public class CapCache : Cache + { + #region Static Global Cache instance + private static Lazy global = new Lazy(); + /// + /// Gets the global shared cache instance valid for the entire process. + /// + /// + /// The global shared cache instance. + /// + public static CapCache Global => global.Value; + #endregion + } + #endregion + +} diff --git a/src/DotNetCore.CAP/Dashboard/RazorPage.cs b/src/DotNetCore.CAP/Dashboard/RazorPage.cs index a0dd09e..39cd76d 100644 --- a/src/DotNetCore.CAP/Dashboard/RazorPage.cs +++ b/src/DotNetCore.CAP/Dashboard/RazorPage.cs @@ -84,7 +84,14 @@ namespace DotNetCore.CAP.Dashboard _statisticsLazy = new Lazy(() => { var monitoring = Storage.GetMonitoringApi(); - return monitoring.GetStatistics(); + var dto = monitoring.GetStatistics(); + + if (CapCache.Global.TryGet("cap.nodes.count", out object count)) + { + dto.Servers = (int)count; + } + + return dto; }); } diff --git a/src/DotNetCore.CAP/NodeDiscovery/INodeDiscoveryProvider.Consul.cs b/src/DotNetCore.CAP/NodeDiscovery/INodeDiscoveryProvider.Consul.cs index e0f197b..6e53442 100644 --- a/src/DotNetCore.CAP/NodeDiscovery/INodeDiscoveryProvider.Consul.cs +++ b/src/DotNetCore.CAP/NodeDiscovery/INodeDiscoveryProvider.Consul.cs @@ -1,10 +1,8 @@ using System; using System.Linq; using System.Collections.Generic; -using System.Text; using System.Threading.Tasks; using Consul; -using System.Net; namespace DotNetCore.CAP.NodeDiscovery { @@ -24,24 +22,31 @@ namespace DotNetCore.CAP.NodeDiscovery { _consul = new ConsulClient(config => { + config.WaitTime = TimeSpan.FromSeconds(5); config.Address = new Uri($"http://{_options.DiscoveryServerHostName}:{_options.DiscoveryServerProt}"); }); } public async Task> GetNodes() { - var services = await _consul.Agent.Services(); + try { + var services = await _consul.Agent.Services(); - var nodes = services.Response.Select(x => new Node - { - Id = x.Key, - Name = x.Value.Service, - Address = x.Value.Address, - Port = x.Value.Port, - Tags = string.Join(", ", x.Value.Tags) - }); + var nodes = services.Response.Select(x => new Node { + Id = x.Key, + Name = x.Value.Service, + Address = x.Value.Address, + Port = x.Value.Port, + Tags = string.Join(", ", x.Value.Tags) + }); + + CapCache.Global.AddOrUpdate("cap.nodes.count", nodes.Count(), TimeSpan.FromSeconds(30),true); - return nodes.ToList(); + return nodes.ToList(); + } + catch (Exception) { + return null; + } } public Task RegisterNode()