/// <summary> /// Read a value from the cache /// </summary> /// <typeparam name="T">Type to read</typeparam> /// <param name="key">Key</param> /// <param name="value">Value</param> /// <param name="notFound">Create T if not found, null to not do this. Item1 = value, Item2 = expiration.</param> public async Task <CachedItem <T> > Get <T>(string key, Func <Task <CachedItem <T> > > notFound) where T : class { using (var lockRead = cacheTimerLock.LockRead()) { if (cache.TryGetValue(key, out KeyValuePair <DateTime, object> cacheValue)) { return(new CachedItem <T>((T)cacheValue.Value, cacheValue.Key)); } } // most likely the callback needs to make a network request, so don't do it in a lock // it's ok if multiple calls stack on the same cache key, the last one to finish will win CachedItem <T> newItem = await notFound(); // don't add null values to the cache if (newItem.Value != null) { using (var lockWrite = cacheTimerLock.LockWrite()) { cache[key] = new KeyValuePair <DateTime, object>(newItem.Expiration, newItem.Value); } } return(newItem); }
/// <summary> /// Get cache of symbols metadata and put into a dictionary. This method looks in the cache first, and if found, returns immediately, otherwise makes a network request and puts it in the cache /// </summary> /// <param name="api">Exchange API</param> /// <returns>Dictionary of symbol name and market, or null if there was an error</returns> public static async Task <Dictionary <string, ExchangeMarket> > GetExchangeMarketDictionaryFromCacheAsync(this ExchangeAPI api) { await new SynchronizationContextRemover(); CachedItem <Dictionary <string, ExchangeMarket> > cacheResult = await api.Cache.Get <Dictionary <string, ExchangeMarket> >(nameof(GetExchangeMarketDictionaryFromCacheAsync), async() => { try { Dictionary <string, ExchangeMarket> symbolsMetadataDictionary = new Dictionary <string, ExchangeMarket>(StringComparer.OrdinalIgnoreCase); IEnumerable <ExchangeMarket> symbolsMetadata = await api.GetMarketSymbolsMetadataAsync(); // build a new lookup dictionary foreach (ExchangeMarket symbolMetadata in symbolsMetadata) { symbolsMetadataDictionary[symbolMetadata.MarketSymbol] = symbolMetadata; } // return the cached dictionary for 4 hours return(new CachedItem <Dictionary <string, ExchangeMarket> >(symbolsMetadataDictionary, CryptoUtility.UtcNow.AddHours(4.0))); } catch// (Exception ex) { // if the network goes down this could log quite a lot of exceptions... //Logger.Error(ex); return(new CachedItem <Dictionary <string, ExchangeMarket> >()); } }); if (cacheResult.Found) { return(cacheResult.Value); } return(null); }
/// <summary> /// Gets the exchange market from this exchange's SymbolsMetadata cache. This will make a network request if needed to retrieve fresh markets from the exchange using GetSymbolsMetadataAsync(). /// Please note that sending a symbol that is not found over and over will result in many network requests. Only send symbols that you are confident exist on the exchange. /// </summary> /// <param name="marketSymbol">The market symbol. Ex. ADA/BTC. This is assumed to be normalized and already correct for the exchange.</param> /// <returns>The ExchangeMarket or null if it doesn't exist in the cache or there was an error</returns> public virtual async Task <ExchangeMarket> GetExchangeMarketFromCacheAsync(string marketSymbol) { try { // *NOTE*: custom caching, do not wrap in CacheMethodCall... // *NOTE*: vulnerability exists where if spammed with not found symbols, lots of network calls will happen, stalling the application // TODO: Add not found dictionary, or some mechanism to mitigate this risk // not sure if this is needed, but adding it just in case await new SynchronizationContextRemover(); CachedItem <Dictionary <string, ExchangeMarket> > cacheResult = await Cache.Get <Dictionary <string, ExchangeMarket> >(nameof(GetExchangeMarketFromCacheAsync), null); if (cacheResult.Found && cacheResult.Value.TryGetValue(marketSymbol, out ExchangeMarket market)) { return(market); } // try again with a fresh request Cache.Remove(nameof(GetExchangeMarketFromCacheAsync)); Cache.Remove(nameof(GetMarketSymbolsMetadataAsync)); cacheResult = await Cache.Get <Dictionary <string, ExchangeMarket> >(nameof(GetExchangeMarketFromCacheAsync), async() => { Dictionary <string, ExchangeMarket> symbolsMetadataDictionary = new Dictionary <string, ExchangeMarket>(StringComparer.OrdinalIgnoreCase); IEnumerable <ExchangeMarket> symbolsMetadata = await GetMarketSymbolsMetadataAsync(); // build a new lookup dictionary foreach (ExchangeMarket symbolMetadata in symbolsMetadata) { symbolsMetadataDictionary[symbolMetadata.MarketSymbol] = symbolMetadata; } // return the cached dictionary for 4 hours return(new CachedItem <Dictionary <string, ExchangeMarket> >(symbolsMetadataDictionary, CryptoUtility.UtcNow.AddHours(4.0))); }); // attempt to lookup one more time in the dictionary if (cacheResult.Found && cacheResult.Value.TryGetValue(marketSymbol, out market)) { return(market); } } catch { // TODO: Report the error somehow, for now a failed network request will just return null symbol which will force the caller to use default handling } return(null); }
/// <summary> /// Read a value from the cache /// </summary> /// <typeparam name="T">Type to read</typeparam> /// <param name="key">Key</param> /// <param name="value">Value</param> /// <param name="notFound">Create T if not found, null to not do this. Item1 = value, Item2 = expiration.</param> public async Task <CachedItem <T> > GetOrCreate <T>(string key, Func <Task <CachedItem <T> > > notFound) where T : class { CachedItem <T> newItem = default; cacheTimerLock.EnterReadLock(); try { if (cache.TryGetValue(key, out KeyValuePair <DateTime, object> cacheValue)) { return(new CachedItem <T>((T)cacheValue.Value, cacheValue.Key)); } } finally { cacheTimerLock.ExitReadLock(); } if (notFound == null) { return(new CachedItem <T>()); } // most likely the callback needs to make a network request, so don't do it in a lock // it's ok if multiple calls stack on the same cache key, the last one to finish will win newItem = await notFound(); // don't add null values to the cache if (newItem.Value != null) { cacheTimerLock.EnterWriteLock(); try { cache[key] = new KeyValuePair <DateTime, object>(newItem.Expiration, newItem.Value); } finally { cacheTimerLock.ExitWriteLock(); } } return(newItem); }