Poll and cache rates in parallel

This commit is contained in:
nicolas.dorier
2018-08-23 00:24:33 +09:00
parent 87d384dba5
commit f12114f9aa
12 changed files with 301 additions and 131 deletions

View File

@@ -120,36 +120,67 @@ namespace BTCPayServer.Tests
.Build(); .Build();
_Host.Start(); _Host.Start();
InvoiceRepository = (InvoiceRepository)_Host.Services.GetService(typeof(InvoiceRepository)); InvoiceRepository = (InvoiceRepository)_Host.Services.GetService(typeof(InvoiceRepository));
StoreRepository = (StoreRepository)_Host.Services.GetService(typeof(StoreRepository)); StoreRepository = (StoreRepository)_Host.Services.GetService(typeof(StoreRepository));
var rateProvider = (RateProviderFactory)_Host.Services.GetService(typeof(RateProviderFactory));
rateProvider.DirectProviders.Clear();
var coinAverageMock = new MockRateProvider(); if (MockRates)
coinAverageMock.ExchangeRates.Add(new Rating.ExchangeRate()
{ {
Exchange = "coinaverage", var rateProvider = (RateProviderFactory)_Host.Services.GetService(typeof(RateProviderFactory));
CurrencyPair = CurrencyPair.Parse("BTC_USD"), rateProvider.Providers.Clear();
BidAsk = new BidAsk(5000m)
}); var coinAverageMock = new MockRateProvider();
coinAverageMock.ExchangeRates.Add(new Rating.ExchangeRate() coinAverageMock.ExchangeRates.Add(new Rating.ExchangeRate()
{ {
Exchange = "coinaverage", Exchange = "coinaverage",
CurrencyPair = CurrencyPair.Parse("BTC_CAD"), CurrencyPair = CurrencyPair.Parse("BTC_USD"),
BidAsk = new BidAsk(4500m) BidAsk = new BidAsk(5000m)
}); });
coinAverageMock.ExchangeRates.Add(new Rating.ExchangeRate() coinAverageMock.ExchangeRates.Add(new Rating.ExchangeRate()
{ {
Exchange = "coinaverage", Exchange = "coinaverage",
CurrencyPair = CurrencyPair.Parse("LTC_BTC"), CurrencyPair = CurrencyPair.Parse("BTC_CAD"),
BidAsk = new BidAsk(0.001m) BidAsk = new BidAsk(4500m)
}); });
coinAverageMock.ExchangeRates.Add(new Rating.ExchangeRate() coinAverageMock.ExchangeRates.Add(new Rating.ExchangeRate()
{ {
Exchange = "coinaverage", Exchange = "coinaverage",
CurrencyPair = CurrencyPair.Parse("LTC_USD"), CurrencyPair = CurrencyPair.Parse("LTC_BTC"),
BidAsk = new BidAsk(500m) BidAsk = new BidAsk(0.001m)
}); });
rateProvider.DirectProviders.Add("coinaverage", coinAverageMock); coinAverageMock.ExchangeRates.Add(new Rating.ExchangeRate()
{
Exchange = "coinaverage",
CurrencyPair = CurrencyPair.Parse("LTC_USD"),
BidAsk = new BidAsk(500m)
});
rateProvider.Providers.Add("coinaverage", coinAverageMock);
var bitflyerMock = new MockRateProvider();
bitflyerMock.ExchangeRates.Add(new Rating.ExchangeRate()
{
Exchange = "bitflyer",
CurrencyPair = CurrencyPair.Parse("BTC_JPY"),
BidAsk = new BidAsk(700000m)
});
rateProvider.Providers.Add("bitflyer", bitflyerMock);
var quadrigacx = new MockRateProvider();
quadrigacx.ExchangeRates.Add(new Rating.ExchangeRate()
{
Exchange = "quadrigacx",
CurrencyPair = CurrencyPair.Parse("BTC_CAD"),
BidAsk = new BidAsk(6000m)
});
rateProvider.Providers.Add("quadrigacx", quadrigacx);
var bittrex = new MockRateProvider();
bittrex.ExchangeRates.Add(new Rating.ExchangeRate()
{
Exchange = "bittrex",
CurrencyPair = CurrencyPair.Parse("DOGE_BTC"),
BidAsk = new BidAsk(0.004m)
});
rateProvider.Providers.Add("bittrex", bittrex);
}
} }
public string HostName public string HostName
@@ -177,7 +208,7 @@ namespace BTCPayServer.Tests
{ {
context.User = new ClaimsPrincipal(new ClaimsIdentity(new[] { new Claim(ClaimTypes.NameIdentifier, userId) }, Policies.CookieAuthentication)); context.User = new ClaimsPrincipal(new ClaimsIdentity(new[] { new Claim(ClaimTypes.NameIdentifier, userId) }, Policies.CookieAuthentication));
} }
if(storeId != null) if (storeId != null)
{ {
context.SetStoreData(GetService<StoreRepository>().FindStore(storeId, userId).GetAwaiter().GetResult()); context.SetStoreData(GetService<StoreRepository>().FindStore(storeId, userId).GetAwaiter().GetResult());
} }

View File

@@ -14,6 +14,7 @@ using Xunit;
using NBXplorer.DerivationStrategy; using NBXplorer.DerivationStrategy;
using BTCPayServer.Payments; using BTCPayServer.Payments;
using BTCPayServer.Payments.Lightning; using BTCPayServer.Payments.Lightning;
using BTCPayServer.Tests.Logging;
namespace BTCPayServer.Tests namespace BTCPayServer.Tests
{ {

View File

@@ -398,6 +398,7 @@ namespace BTCPayServer.Tests
} }
[Fact] [Fact]
[Trait("Flaky", "Flaky")]
public void CanSetLightningServer() public void CanSetLightningServer()
{ {
using (var tester = ServerTester.Create()) using (var tester = ServerTester.Create())
@@ -546,18 +547,21 @@ namespace BTCPayServer.Tests
} }
[Fact] [Fact]
[Trait("Flaky", "Flaky")]
public void CanSendLightningPaymentCLightning() public void CanSendLightningPaymentCLightning()
{ {
ProcessLightningPayment(LightningConnectionType.CLightning); ProcessLightningPayment(LightningConnectionType.CLightning);
} }
[Fact] [Fact]
[Trait("Flaky", "Flaky")]
public void CanSendLightningPaymentCharge() public void CanSendLightningPaymentCharge()
{ {
ProcessLightningPayment(LightningConnectionType.Charge); ProcessLightningPayment(LightningConnectionType.Charge);
} }
[Fact] [Fact]
[Trait("Flaky", "Flaky")]
public void CanSendLightningPaymentLnd() public void CanSendLightningPaymentLnd()
{ {
ProcessLightningPayment(LightningConnectionType.LndREST); ProcessLightningPayment(LightningConnectionType.LndREST);
@@ -943,8 +947,8 @@ namespace BTCPayServer.Tests
user.RegisterDerivationScheme("BTC"); user.RegisterDerivationScheme("BTC");
List<decimal> rates = new List<decimal>(); List<decimal> rates = new List<decimal>();
rates.Add(CreateInvoice(tester, user, "coinaverage")); rates.Add(CreateInvoice(tester, user, "coinaverage"));
var bitflyer = CreateInvoice(tester, user, "bitflyer"); var bitflyer = CreateInvoice(tester, user, "bitflyer", "JPY");
var bitflyer2 = CreateInvoice(tester, user, "bitflyer"); var bitflyer2 = CreateInvoice(tester, user, "bitflyer", "JPY");
Assert.Equal(bitflyer, bitflyer2); // Should be equal because cache Assert.Equal(bitflyer, bitflyer2); // Should be equal because cache
rates.Add(bitflyer); rates.Add(bitflyer);
@@ -955,7 +959,7 @@ namespace BTCPayServer.Tests
} }
} }
private static decimal CreateInvoice(ServerTester tester, TestAccount user, string exchange) private static decimal CreateInvoice(ServerTester tester, TestAccount user, string exchange, string currency = "USD")
{ {
var storeController = user.GetController<StoresController>(); var storeController = user.GetController<StoresController>();
var vm = (RatesViewModel)((ViewResult)storeController.Rates()).Model; var vm = (RatesViewModel)((ViewResult)storeController.Rates()).Model;
@@ -964,7 +968,7 @@ namespace BTCPayServer.Tests
var invoice2 = user.BitPay.CreateInvoice(new Invoice() var invoice2 = user.BitPay.CreateInvoice(new Invoice()
{ {
Price = 5000.0m, Price = 5000.0m,
Currency = "USD", Currency = currency,
PosData = "posData", PosData = "posData",
OrderId = "orderId", OrderId = "orderId",
ItemDesc = "Some description", ItemDesc = "Some description",
@@ -978,12 +982,10 @@ namespace BTCPayServer.Tests
{ {
using (var tester = ServerTester.Create()) using (var tester = ServerTester.Create())
{ {
tester.PayTester.MockRates = false;
tester.Start(); tester.Start();
var user = tester.NewAccount(); var user = tester.NewAccount();
user.GrantAccess(); user.GrantAccess();
user.RegisterDerivationScheme("BTC"); user.RegisterDerivationScheme("BTC");
// First we try payment with a merchant having only BTC // First we try payment with a merchant having only BTC
var invoice1 = user.BitPay.CreateInvoice(new Invoice() var invoice1 = user.BitPay.CreateInvoice(new Invoice()
{ {
@@ -994,7 +996,7 @@ namespace BTCPayServer.Tests
ItemDesc = "Some description", ItemDesc = "Some description",
FullNotifications = true FullNotifications = true
}, Facade.Merchant); }, Facade.Merchant);
Assert.Equal(Money.Coins(1.0m), invoice1.BtcPrice);
var storeController = user.GetController<StoresController>(); var storeController = user.GetController<StoresController>();
var vm = (RatesViewModel)((ViewResult)storeController.Rates()).Model; var vm = (RatesViewModel)((ViewResult)storeController.Rates()).Model;
@@ -1013,11 +1015,9 @@ namespace BTCPayServer.Tests
FullNotifications = true FullNotifications = true
}, Facade.Merchant); }, Facade.Merchant);
// The rate was 5000 USD per BTC var expectedRate = 5000.0m * 0.6m;
// Now it should be 3000 USD per BTC var expectedCoins = invoice2.Price / expectedRate;
// So the expected price should be Assert.True(invoice2.BtcPrice.Almost(Money.Coins(expectedCoins), 0.00001m));
var expected = Money.Coins(5000m / 3000m);
Assert.True(invoice2.BtcPrice.Almost(expected, 0.00001m));
} }
} }
@@ -1132,7 +1132,7 @@ namespace BTCPayServer.Tests
rateVm.ScriptTest = "BTC_USD,BTC_CAD,DOGE_USD,DOGE_CAD"; rateVm.ScriptTest = "BTC_USD,BTC_CAD,DOGE_USD,DOGE_CAD";
rateVm.Script = "DOGE_X = bittrex(DOGE_BTC) * BTC_X;\n" + rateVm.Script = "DOGE_X = bittrex(DOGE_BTC) * BTC_X;\n" +
"X_CAD = quadrigacx(X_CAD);\n" + "X_CAD = quadrigacx(X_CAD);\n" +
"X_X = gdax(X_X);"; "X_X = coinaverage(X_X);";
rateVm.Spread = 50; rateVm.Spread = 50;
rateVm = Assert.IsType<RatesViewModel>(Assert.IsType<ViewResult>(store.Rates(rateVm, "Test").Result).Model); rateVm = Assert.IsType<RatesViewModel>(Assert.IsType<ViewResult>(store.Rates(rateVm, "Test").Result).Model);
Assert.True(rateVm.TestRateRules.All(t => !t.Error)); Assert.True(rateVm.TestRateRules.All(t => !t.Error));
@@ -1669,11 +1669,14 @@ namespace BTCPayServer.Tests
var factory = CreateBTCPayRateFactory(); var factory = CreateBTCPayRateFactory();
foreach (var result in factory foreach (var result in factory
.DirectProviders .Providers
.Select(p => (ExpectedName: p.Key, ResultAsync: p.Value.GetRatesAsync())) .Where(p => p.Value is BackgroundFetcherRateProvider)
.Select(p => (ExpectedName: p.Key, ResultAsync: p.Value.GetRatesAsync(), Fetcher: (BackgroundFetcherRateProvider)p.Value))
.ToList()) .ToList())
{ {
result.Fetcher.InvalidateCache();
var exchangeRates = result.ResultAsync.Result; var exchangeRates = result.ResultAsync.Result;
result.Fetcher.InvalidateCache();
Assert.NotNull(exchangeRates); Assert.NotNull(exchangeRates);
Assert.NotEmpty(exchangeRates); Assert.NotEmpty(exchangeRates);
Assert.NotEmpty(exchangeRates.ByExchange[result.ExpectedName]); Assert.NotEmpty(exchangeRates.ByExchange[result.ExpectedName]);
@@ -1687,7 +1690,7 @@ namespace BTCPayServer.Tests
); );
} }
// Kraken emit one request only after first GetRates // Kraken emit one request only after first GetRates
factory.DirectProviders["kraken"].GetRatesAsync().GetAwaiter().GetResult(); factory.Providers["kraken"].GetRatesAsync().GetAwaiter().GetResult();
} }
[Fact] [Fact]
@@ -1712,44 +1715,87 @@ namespace BTCPayServer.Tests
private static RateProviderFactory CreateBTCPayRateFactory() private static RateProviderFactory CreateBTCPayRateFactory()
{ {
return new RateProviderFactory(new MemoryCacheOptions() { ExpirationScanFrequency = TimeSpan.FromSeconds(1.0) }, null, new CoinAverageSettings()); return new RateProviderFactory(CreateMemoryCache(), null, new CoinAverageSettings());
}
private static MemoryCacheOptions CreateMemoryCache()
{
return new MemoryCacheOptions() { ExpirationScanFrequency = TimeSpan.FromSeconds(1.0) };
}
class SpyRateProvider : IRateProvider
{
public bool Hit { get; set; }
public Task<ExchangeRates> GetRatesAsync()
{
Hit = true;
var rates = new ExchangeRates();
rates.Add(new ExchangeRate("coinaverage", CurrencyPair.Parse("BTC_USD"), new BidAsk(5000)));
return Task.FromResult(rates);
}
public void AssertHit()
{
Assert.True(Hit, "Should have hit the provider");
Hit = false;
}
public void AssertNotHit()
{
Assert.False(Hit, "Should have not hit the provider");
Hit = false;
}
} }
[Fact] [Fact]
public void CheckRatesProvider() public void CheckRatesProvider()
{ {
var coinAverage = new CoinAverageRateProvider(); var spy = new SpyRateProvider();
var rates = coinAverage.GetRatesAsync().GetAwaiter().GetResult();
Assert.NotNull(rates.GetRate("coinaverage", new CurrencyPair("BTC", "JPY")));
var ratesBitpay = new BitpayRateProvider(new Bitpay(new Key(), new Uri("https://bitpay.com/"))).GetRatesAsync().GetAwaiter().GetResult();
Assert.NotNull(ratesBitpay.GetRate("bitpay", new CurrencyPair("BTC", "JPY")));
RateRules.TryParse("X_X = coinaverage(X_X);", out var rateRules); RateRules.TryParse("X_X = coinaverage(X_X);", out var rateRules);
var factory = CreateBTCPayRateFactory(); var factory = CreateBTCPayRateFactory();
var fetcher = new RateFetcher(CreateBTCPayRateFactory()); factory.Providers.Clear();
factory.CacheSpan = TimeSpan.FromSeconds(10); factory.Providers.Add("coinaverage", new CachedRateProvider("coinaverage", spy, new MemoryCache(CreateMemoryCache())));
factory.Providers.Add("bittrex", new CachedRateProvider("bittrex", spy, new MemoryCache(CreateMemoryCache())));
factory.CacheSpan = TimeSpan.FromSeconds(1);
var fetcher = new RateFetcher(factory);
var fetchedRate = fetcher.FetchRate(CurrencyPair.Parse("BTC_USD"), rateRules).GetAwaiter().GetResult(); var fetchedRate = fetcher.FetchRate(CurrencyPair.Parse("BTC_USD"), rateRules).GetAwaiter().GetResult();
Assert.False(fetchedRate.Cached); spy.AssertHit();
fetchedRate = fetcher.FetchRate(CurrencyPair.Parse("BTC_USD"), rateRules).GetAwaiter().GetResult(); fetchedRate = fetcher.FetchRate(CurrencyPair.Parse("BTC_USD"), rateRules).GetAwaiter().GetResult();
Assert.True(fetchedRate.Cached); spy.AssertNotHit();
Thread.Sleep(11000); Thread.Sleep(3000);
fetchedRate = fetcher.FetchRate(CurrencyPair.Parse("BTC_USD"), rateRules).GetAwaiter().GetResult(); fetchedRate = fetcher.FetchRate(CurrencyPair.Parse("BTC_USD"), rateRules).GetAwaiter().GetResult();
Assert.False(fetchedRate.Cached); spy.AssertHit();
fetchedRate = fetcher.FetchRate(CurrencyPair.Parse("BTC_USD"), rateRules).GetAwaiter().GetResult(); fetchedRate = fetcher.FetchRate(CurrencyPair.Parse("BTC_USD"), rateRules).GetAwaiter().GetResult();
Assert.True(fetchedRate.Cached); spy.AssertNotHit();
// Should cache at exchange level so this should hit the cache // Should cache at exchange level so this should hit the cache
var fetchedRate2 = fetcher.FetchRate(CurrencyPair.Parse("LTC_USD"), rateRules).GetAwaiter().GetResult(); var fetchedRate2 = fetcher.FetchRate(CurrencyPair.Parse("LTC_USD"), rateRules).GetAwaiter().GetResult();
Assert.True(fetchedRate.Cached); spy.AssertNotHit();
Assert.NotEqual(fetchedRate.BidAsk.Bid, fetchedRate2.BidAsk.Bid); Assert.Null(fetchedRate2.BidAsk);
Assert.Equal(RateRulesErrors.RateUnavailable, fetchedRate2.Errors.First());
// Should cache at exchange level this should not hit the cache as it is different exchange // Should cache at exchange level this should not hit the cache as it is different exchange
RateRules.TryParse("X_X = bittrex(X_X);", out rateRules); RateRules.TryParse("X_X = bittrex(X_X);", out rateRules);
fetchedRate = fetcher.FetchRate(CurrencyPair.Parse("BTC_USD"), rateRules).GetAwaiter().GetResult(); fetchedRate = fetcher.FetchRate(CurrencyPair.Parse("BTC_USD"), rateRules).GetAwaiter().GetResult();
Assert.False(fetchedRate.Cached); spy.AssertHit();
factory.Providers.Clear();
var fetch = new BackgroundFetcherRateProvider(spy);
factory.Providers.Add("bittrex", fetch);
fetchedRate = fetcher.FetchRate(CurrencyPair.Parse("BTC_USD"), rateRules).GetAwaiter().GetResult();
spy.AssertHit();
fetchedRate = fetcher.FetchRate(CurrencyPair.Parse("BTC_USD"), rateRules).GetAwaiter().GetResult();
spy.AssertNotHit();
fetch.UpdateIfNecessary().GetAwaiter().GetResult();
spy.AssertNotHit();
fetch.RefreshRate = TimeSpan.FromSeconds(1.0);
Thread.Sleep(1020);
fetchedRate = fetcher.FetchRate(CurrencyPair.Parse("BTC_USD"), rateRules).GetAwaiter().GetResult();
spy.AssertNotHit();
fetch.UpdateIfNecessary().GetAwaiter().GetResult();
spy.AssertHit();
} }
private static bool IsMapped(Invoice invoice, ApplicationDbContext ctx) private static bool IsMapped(Invoice invoice, ApplicationDbContext ctx)

View File

@@ -195,12 +195,9 @@ namespace BTCPayServer.Controllers
var allRateRuleErrors = string.Join(", ", rateResult.Errors.ToArray()); var allRateRuleErrors = string.Join(", ", rateResult.Errors.ToArray());
logs.Write($"{pair.Key}: Rate rule error ({allRateRuleErrors})"); logs.Write($"{pair.Key}: Rate rule error ({allRateRuleErrors})");
} }
if (rateResult.ExchangeExceptions.Count != 0) foreach (var ex in rateResult.ExchangeExceptions)
{ {
foreach (var ex in rateResult.ExchangeExceptions) logs.Write($"{pair.Key}: Exception reaching exchange {ex.ExchangeName} ({ex.Exception.Message})");
{
logs.Write($"{pair.Key}: Exception reaching exchange {ex.ExchangeName} ({ex.Exception.Message})");
}
} }
}).ToArray()); }).ToArray());
} }

View File

@@ -33,16 +33,41 @@ namespace BTCPayServer.HostedServices
return new[] return new[]
{ {
CreateLoopTask(RefreshCoinAverageSupportedExchanges), CreateLoopTask(RefreshCoinAverageSupportedExchanges),
CreateLoopTask(RefreshCoinAverageSettings) CreateLoopTask(RefreshCoinAverageSettings),
CreateLoopTask(RefreshRates)
}; };
} }
async Task RefreshRates()
{
using (var timeout = CancellationTokenSource.CreateLinkedTokenSource(Cancellation))
{
timeout.CancelAfter(TimeSpan.FromSeconds(20.0));
try
{
await Task.WhenAll(_RateProviderFactory.Providers
.Select(p => (Fetcher: p.Value as BackgroundFetcherRateProvider, ExchangeName: p.Key)).Where(p => p.Fetcher != null)
.Select(p => p.Fetcher.UpdateIfNecessary().ContinueWith(t =>
{
if (t.Result.Exception != null)
{
Logs.PayServer.LogWarning($"Error while contacting {p.ExchangeName}: {t.Result.Exception.Message}");
}
}, TaskScheduler.Default))
.ToArray()).WithCancellation(timeout.Token);
}
catch (OperationCanceledException) when (timeout.IsCancellationRequested)
{
}
}
await Task.Delay(TimeSpan.FromSeconds(30), Cancellation);
}
async Task RefreshCoinAverageSupportedExchanges() async Task RefreshCoinAverageSupportedExchanges()
{ {
await new SynchronizationContextRemover();
var tickers = await new CoinAverageRateProvider() { Authenticator = _coinAverageSettings }.GetExchangeTickersAsync(); var tickers = await new CoinAverageRateProvider() { Authenticator = _coinAverageSettings }.GetExchangeTickersAsync();
var exchanges = new CoinAverageExchanges(); var exchanges = new CoinAverageExchanges();
foreach(var item in tickers foreach (var item in tickers
.Exchanges .Exchanges
.Select(c => new CoinAverageExchange(c.Name, c.DisplayName))) .Select(c => new CoinAverageExchange(c.Name, c.DisplayName)))
{ {
@@ -54,7 +79,6 @@ namespace BTCPayServer.HostedServices
async Task RefreshCoinAverageSettings() async Task RefreshCoinAverageSettings()
{ {
await new SynchronizationContextRemover();
var rates = (await _SettingsRepository.GetSettingAsync<RatesSetting>()) ?? new RatesSetting(); var rates = (await _SettingsRepository.GetSettingAsync<RatesSetting>()) ?? new RatesSetting();
_RateProviderFactory.CacheSpan = TimeSpan.FromMinutes(rates.CacheInMinutes); _RateProviderFactory.CacheSpan = TimeSpan.FromMinutes(rates.CacheInMinutes);
if (!string.IsNullOrWhiteSpace(rates.PrivateKey) && !string.IsNullOrWhiteSpace(rates.PublicKey)) if (!string.IsNullOrWhiteSpace(rates.PrivateKey) && !string.IsNullOrWhiteSpace(rates.PublicKey))

View File

@@ -10,7 +10,7 @@ namespace BTCPayServer.Services.Rates
{ {
public class BackgroundFetcherRateProvider : IRateProvider public class BackgroundFetcherRateProvider : IRateProvider
{ {
class LatestFetch public class LatestFetch
{ {
public ExchangeRates Latest; public ExchangeRates Latest;
public DateTimeOffset Timestamp; public DateTimeOffset Timestamp;
@@ -41,20 +41,24 @@ namespace BTCPayServer.Services.Rates
get get
{ {
var latest = _Latest; var latest = _Latest;
if (latest == null) if (latest == null || latest.Exception != null)
return DateTimeOffset.UtcNow; return DateTimeOffset.UtcNow;
return latest.Timestamp + RefreshRate; return latest.Timestamp + RefreshRate;
} }
} }
public async Task<bool> UpdateIfNecessary() public async Task<LatestFetch> UpdateIfNecessary()
{ {
if (NextUpdate <= DateTimeOffset.UtcNow) if (NextUpdate <= DateTimeOffset.UtcNow)
{ {
await Fetch(); try
return true; {
await Fetch();
}
catch { } // Exception is inside _Latest
return _Latest;
} }
return false; return _Latest;
} }
LatestFetch _Latest; LatestFetch _Latest;
@@ -77,7 +81,14 @@ namespace BTCPayServer.Services.Rates
} }
fetch.Timestamp = DateTimeOffset.UtcNow; fetch.Timestamp = DateTimeOffset.UtcNow;
_Latest = fetch; _Latest = fetch;
if (fetch.Exception != null)
ExceptionDispatchInfo.Capture(fetch.Exception).Throw();
return fetch; return fetch;
} }
public void InvalidateCache()
{
_Latest = null;
}
} }
} }

View File

@@ -38,7 +38,7 @@ namespace BTCPayServer.Services.Rates
get; get;
set; set;
} = TimeSpan.FromMinutes(1.0); } = TimeSpan.FromMinutes(1.0);
public IMemoryCache MemoryCache { get => _MemoryCache; private set => _MemoryCache = value; } public IMemoryCache MemoryCache { get => _MemoryCache; set => _MemoryCache = value; }
public Task<ExchangeRates> GetRatesAsync() public Task<ExchangeRates> GetRatesAsync()
{ {

View File

@@ -9,7 +9,6 @@ namespace BTCPayServer.Services.Rates
public class FallbackRateProvider : IRateProvider public class FallbackRateProvider : IRateProvider
{ {
IRateProvider[] _Providers; IRateProvider[] _Providers;
public bool Used { get; set; }
public FallbackRateProvider(IRateProvider[] providers) public FallbackRateProvider(IRateProvider[] providers)
{ {
if (providers == null) if (providers == null)
@@ -19,7 +18,6 @@ namespace BTCPayServer.Services.Rates
public async Task<ExchangeRates> GetRatesAsync() public async Task<ExchangeRates> GetRatesAsync()
{ {
Used = true;
foreach (var p in _Providers) foreach (var p in _Providers)
{ {
try try

View File

@@ -0,0 +1,28 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Rating;
namespace BTCPayServer.Services.Rates
{
public class NullRateProvider : IRateProvider
{
private NullRateProvider()
{
}
private static readonly NullRateProvider _Instance = new NullRateProvider();
public static NullRateProvider Instance
{
get
{
return _Instance;
}
}
public Task<ExchangeRates> GetRatesAsync()
{
return Task.FromResult(new ExchangeRates());
}
}
}

View File

@@ -24,7 +24,7 @@ namespace BTCPayServer.Services.Rates
public string EvaluatedRule { get; set; } public string EvaluatedRule { get; set; }
public HashSet<RateRulesErrors> Errors { get; set; } public HashSet<RateRulesErrors> Errors { get; set; }
public BidAsk BidAsk { get; set; } public BidAsk BidAsk { get; set; }
public bool Cached { get; internal set; } public TimeSpan Latency { get; internal set; }
} }
public class RateFetcher public class RateFetcher
@@ -72,13 +72,12 @@ namespace BTCPayServer.Services.Rates
private async Task<RateResult> GetRuleValue(List<Task<QueryRateResult>> dependentQueries, RateRule rateRule) private async Task<RateResult> GetRuleValue(List<Task<QueryRateResult>> dependentQueries, RateRule rateRule)
{ {
var result = new RateResult(); var result = new RateResult();
result.Cached = true;
foreach (var queryAsync in dependentQueries) foreach (var queryAsync in dependentQueries)
{ {
var query = await queryAsync; var query = await queryAsync;
if (!query.CachedResult) result.Latency = query.Latency;
result.Cached = false; if (query.Exception != null)
result.ExchangeExceptions.AddRange(query.Exceptions); result.ExchangeExceptions.Add(query.Exception);
foreach (var rule in query.ExchangeRates) foreach (var rule in query.ExchangeRates)
{ {
rateRule.ExchangeRates.SetRate(rule.Exchange, rule.CurrencyPair, rule.BidAsk); rateRule.ExchangeRates.SetRate(rule.Exchange, rule.CurrencyPair, rule.BidAsk);

View File

@@ -12,11 +12,38 @@ namespace BTCPayServer.Services.Rates
{ {
public class RateProviderFactory public class RateProviderFactory
{ {
class WrapperRateProvider : IRateProvider
{
private readonly IRateProvider _inner;
public Exception Exception { get; private set; }
public TimeSpan Latency { get; set; }
public WrapperRateProvider(IRateProvider inner)
{
_inner = inner;
}
public Task<ExchangeRates> GetRatesAsync()
{
DateTimeOffset now = DateTimeOffset.UtcNow;
try
{
return _inner.GetRatesAsync();
}
catch (Exception ex)
{
Exception = ex;
return Task.FromResult(new ExchangeRates());
}
finally
{
Latency = DateTimeOffset.UtcNow - now;
}
}
}
public class QueryRateResult public class QueryRateResult
{ {
public bool CachedResult { get; set; } public TimeSpan Latency { get; set; }
public List<ExchangeException> Exceptions { get; set; }
public ExchangeRates ExchangeRates { get; set; } public ExchangeRates ExchangeRates { get; set; }
public ExchangeException Exception { get; internal set; }
} }
public RateProviderFactory(IOptions<MemoryCacheOptions> cacheOptions, public RateProviderFactory(IOptions<MemoryCacheOptions> cacheOptions,
IHttpClientFactory httpClientFactory, IHttpClientFactory httpClientFactory,
@@ -24,22 +51,12 @@ namespace BTCPayServer.Services.Rates
{ {
_httpClientFactory = httpClientFactory; _httpClientFactory = httpClientFactory;
_CoinAverageSettings = coinAverageSettings; _CoinAverageSettings = coinAverageSettings;
_Cache = new MemoryCache(cacheOptions);
_CacheOptions = cacheOptions; _CacheOptions = cacheOptions;
// We use 15 min because of limits with free version of bitcoinaverage // We use 15 min because of limits with free version of bitcoinaverage
CacheSpan = TimeSpan.FromMinutes(15.0); CacheSpan = TimeSpan.FromMinutes(15.0);
InitExchanges(); InitExchanges();
} }
IMemoryCache _Cache;
private IOptions<MemoryCacheOptions> _CacheOptions; private IOptions<MemoryCacheOptions> _CacheOptions;
public IMemoryCache Cache
{
get
{
return _Cache;
}
}
TimeSpan _CacheSpan; TimeSpan _CacheSpan;
public TimeSpan CacheSpan public TimeSpan CacheSpan
{ {
@@ -55,12 +72,19 @@ namespace BTCPayServer.Services.Rates
} }
public void InvalidateCache() public void InvalidateCache()
{ {
_Cache = new MemoryCache(_CacheOptions); var cache = new MemoryCache(_CacheOptions);
foreach (var provider in Providers.Select(p => p.Value as CachedRateProvider).Where(p => p != null))
{
provider.CacheSpan = CacheSpan;
provider.MemoryCache = cache;
}
if (Providers.TryGetValue(CoinAverageRateProvider.CoinAverageName, out var coinAverage) && coinAverage is BackgroundFetcherRateProvider c)
c.RefreshRate = CacheSpan;
} }
CoinAverageSettings _CoinAverageSettings; CoinAverageSettings _CoinAverageSettings;
private readonly IHttpClientFactory _httpClientFactory; private readonly IHttpClientFactory _httpClientFactory;
private readonly Dictionary<string, IRateProvider> _DirectProviders = new Dictionary<string, IRateProvider>(); private readonly Dictionary<string, IRateProvider> _DirectProviders = new Dictionary<string, IRateProvider>();
public Dictionary<string, IRateProvider> DirectProviders public Dictionary<string, IRateProvider> Providers
{ {
get get
{ {
@@ -71,24 +95,50 @@ namespace BTCPayServer.Services.Rates
private void InitExchanges() private void InitExchanges()
{ {
// We need to be careful to only add exchanges which OnGetTickers implementation make only 1 request // We need to be careful to only add exchanges which OnGetTickers implementation make only 1 request
DirectProviders.Add("binance", new ExchangeSharpRateProvider("binance", new ExchangeBinanceAPI(), true)); Providers.Add("binance", new ExchangeSharpRateProvider("binance", new ExchangeBinanceAPI(), true));
DirectProviders.Add("bittrex", new ExchangeSharpRateProvider("bittrex", new ExchangeBittrexAPI(), true)); Providers.Add("bittrex", new ExchangeSharpRateProvider("bittrex", new ExchangeBittrexAPI(), true));
DirectProviders.Add("poloniex", new ExchangeSharpRateProvider("poloniex", new ExchangePoloniexAPI(), true)); Providers.Add("poloniex", new ExchangeSharpRateProvider("poloniex", new ExchangePoloniexAPI(), true));
DirectProviders.Add("hitbtc", new ExchangeSharpRateProvider("hitbtc", new ExchangeHitbtcAPI(), false)); Providers.Add("hitbtc", new ExchangeSharpRateProvider("hitbtc", new ExchangeHitbtcAPI(), false));
DirectProviders.Add("cryptopia", new ExchangeSharpRateProvider("cryptopia", new ExchangeCryptopiaAPI(), false)); Providers.Add("cryptopia", new ExchangeSharpRateProvider("cryptopia", new ExchangeCryptopiaAPI(), false));
// Handmade providers // Handmade providers
DirectProviders.Add("bitpay", new BitpayRateProvider(new NBitpayClient.Bitpay(new NBitcoin.Key(), new Uri("https://bitpay.com/")))); Providers.Add("bitpay", new BitpayRateProvider(new NBitpayClient.Bitpay(new NBitcoin.Key(), new Uri("https://bitpay.com/"))));
DirectProviders.Add(QuadrigacxRateProvider.QuadrigacxName, new QuadrigacxRateProvider()); Providers.Add(QuadrigacxRateProvider.QuadrigacxName, new QuadrigacxRateProvider());
DirectProviders.Add(CoinAverageRateProvider.CoinAverageName, new CoinAverageRateProvider() { Exchange = CoinAverageRateProvider.CoinAverageName, HttpClient = _httpClientFactory?.CreateClient(), Authenticator = _CoinAverageSettings }); Providers.Add(CoinAverageRateProvider.CoinAverageName, new CoinAverageRateProvider() { Exchange = CoinAverageRateProvider.CoinAverageName, HttpClient = _httpClientFactory?.CreateClient(), Authenticator = _CoinAverageSettings });
Providers.Add("kraken", new KrakenExchangeRateProvider() { HttpClient = _httpClientFactory?.CreateClient() });
// Those exchanges make multiple requests when calling GetTickers so we remove them // Those exchanges make multiple requests when calling GetTickers so we remove them
DirectProviders.Add("kraken", new KrakenExchangeRateProvider() { HttpClient = _httpClientFactory?.CreateClient() });
//DirectProviders.Add("gdax", new ExchangeSharpRateProvider("gdax", new ExchangeGdaxAPI())); //DirectProviders.Add("gdax", new ExchangeSharpRateProvider("gdax", new ExchangeGdaxAPI()));
//DirectProviders.Add("gemini", new ExchangeSharpRateProvider("gemini", new ExchangeGeminiAPI())); //DirectProviders.Add("gemini", new ExchangeSharpRateProvider("gemini", new ExchangeGeminiAPI()));
//DirectProviders.Add("bitfinex", new ExchangeSharpRateProvider("bitfinex", new ExchangeBitfinexAPI())); //DirectProviders.Add("bitfinex", new ExchangeSharpRateProvider("bitfinex", new ExchangeBitfinexAPI()));
//DirectProviders.Add("okex", new ExchangeSharpRateProvider("okex", new ExchangeOkexAPI())); //DirectProviders.Add("okex", new ExchangeSharpRateProvider("okex", new ExchangeOkexAPI()));
//DirectProviders.Add("bitstamp", new ExchangeSharpRateProvider("bitstamp", new ExchangeBitstampAPI())); //DirectProviders.Add("bitstamp", new ExchangeSharpRateProvider("bitstamp", new ExchangeBitstampAPI()));
foreach (var provider in Providers.ToArray())
{
var prov = new BackgroundFetcherRateProvider(Providers[provider.Key]);
prov.RefreshRate = provider.Key == CoinAverageRateProvider.CoinAverageName ? CacheSpan : TimeSpan.FromMinutes(1.0);
Providers[provider.Key] = prov;
}
var cache = new MemoryCache(_CacheOptions);
foreach (var supportedExchange in GetSupportedExchanges())
{
if (!Providers.ContainsKey(supportedExchange.Key))
{
var coinAverage = new CoinAverageRateProvider()
{
Exchange = supportedExchange.Key,
HttpClient = _httpClientFactory?.CreateClient(),
Authenticator = _CoinAverageSettings
};
var cached = new CachedRateProvider(supportedExchange.Key, coinAverage, cache)
{
CacheSpan = CacheSpan
};
Providers.Add(supportedExchange.Key, cached);
}
}
} }
public CoinAverageExchanges GetSupportedExchanges() public CoinAverageExchanges GetSupportedExchanges()
@@ -106,33 +156,18 @@ namespace BTCPayServer.Services.Rates
return exchanges; return exchanges;
} }
public bool UseCoinAverageAsFallback { get; set; } = true;
public async Task<QueryRateResult> QueryRates(string exchangeName) public async Task<QueryRateResult> QueryRates(string exchangeName)
{ {
List<IRateProvider> providers = new List<IRateProvider>(); Providers.TryGetValue(exchangeName, out var directProvider);
if (DirectProviders.TryGetValue(exchangeName, out var directProvider)) directProvider = directProvider ?? NullRateProvider.Instance;
providers.Add(directProvider);
if (UseCoinAverageAsFallback && _CoinAverageSettings.AvailableExchanges.ContainsKey(exchangeName)) var wrapper = new WrapperRateProvider(directProvider);
{ var value = await wrapper.GetRatesAsync();
providers.Add(new CoinAverageRateProvider()
{
Exchange = exchangeName,
HttpClient = _httpClientFactory?.CreateClient(),
Authenticator = _CoinAverageSettings
});
}
var fallback = new FallbackRateProvider(providers.ToArray());
var cached = new CachedRateProvider(exchangeName, fallback, _Cache)
{
CacheSpan = CacheSpan
};
var value = await cached.GetRatesAsync();
return new QueryRateResult() return new QueryRateResult()
{ {
CachedResult = !fallback.Used, Latency = wrapper.Latency,
ExchangeRates = value, ExchangeRates = value,
Exceptions = fallback.Exceptions Exception = wrapper.Exception != null ? new ExchangeException() { Exception = wrapper.Exception, ExchangeName = exchangeName } : null
.Select(c => new ExchangeException() { Exception = c, ExchangeName = exchangeName }).ToList()
}; };
} }
} }

View File

@@ -83,7 +83,7 @@ namespace BTCPayServer.Services
MultiValueDictionary<Type, TaskCompletionSource<bool>> _Subscriptions = new MultiValueDictionary<Type, TaskCompletionSource<bool>>(); MultiValueDictionary<Type, TaskCompletionSource<bool>> _Subscriptions = new MultiValueDictionary<Type, TaskCompletionSource<bool>>();
public async Task WaitSettingsChanged<T>(CancellationToken cancellation) public async Task WaitSettingsChanged<T>(CancellationToken cancellation)
{ {
var tcs = new TaskCompletionSource<bool>(); var tcs = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
using (cancellation.Register(() => using (cancellation.Register(() =>
{ {
try try