mirror of
https://github.com/aljazceru/btcpayserver.git
synced 2025-12-18 22:44:29 +01:00
Do not save cache of rates in the database (#6978)
We were previously saving the rates in the database in a JSONB blob column. However, the volume of data ise consequential enough for provoking timeouts during update. Due to how postgres works, this also create bloat in the database that isn't cleaned immediately. This PR fixes this issue by saving the cache in files instead.
This commit is contained in:
@@ -38,6 +38,7 @@ using Microsoft.AspNetCore.Http;
|
|||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||||
using NBitcoin;
|
using NBitcoin;
|
||||||
using NBitcoin.Payment;
|
using NBitcoin.Payment;
|
||||||
using NBitcoin.RPC;
|
using NBitcoin.RPC;
|
||||||
@@ -448,7 +449,7 @@ namespace BTCPayServer
|
|||||||
public static IServiceCollection AddScheduledTask<T>(this IServiceCollection services, TimeSpan every)
|
public static IServiceCollection AddScheduledTask<T>(this IServiceCollection services, TimeSpan every)
|
||||||
where T : class, IPeriodicTask
|
where T : class, IPeriodicTask
|
||||||
{
|
{
|
||||||
services.AddSingleton<T>();
|
services.TryAddSingleton<T>();
|
||||||
services.AddTransient<ScheduledTask>(o => new ScheduledTask(typeof(T), every));
|
services.AddTransient<ScheduledTask>(o => new ScheduledTask(typeof(T), every));
|
||||||
return services;
|
return services;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,47 +1,34 @@
|
|||||||
|
#nullable enable
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using BTCPayServer.Logging;
|
using BTCPayServer.Configuration;
|
||||||
using BTCPayServer.Services;
|
|
||||||
using BTCPayServer.Services.Rates;
|
using BTCPayServer.Services.Rates;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Hosting;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
using NBitcoin;
|
using NBitcoin;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
namespace BTCPayServer.HostedServices
|
namespace BTCPayServer.HostedServices
|
||||||
{
|
{
|
||||||
public class RatesHostedService : BaseAsyncService
|
public class RatesHostedService(
|
||||||
|
IOptions<DataDirectories> dataDirectories,
|
||||||
|
RateProviderFactory rateProviderFactory) : IHostedService, IPeriodicTask
|
||||||
{
|
{
|
||||||
public class ExchangeRatesCache
|
public class ExchangeRatesCache
|
||||||
{
|
{
|
||||||
[JsonConverter(typeof(NBitcoin.JsonConverters.DateTimeToUnixTimeConverter))]
|
[JsonConverter(typeof(NBitcoin.JsonConverters.DateTimeToUnixTimeConverter))]
|
||||||
public DateTimeOffset Created { get; set; }
|
public DateTimeOffset Created { get; set; }
|
||||||
public List<BackgroundFetcherState> States { get; set; }
|
public List<BackgroundFetcherState>? States { get; set; }
|
||||||
public override string ToString()
|
public override string ToString()
|
||||||
{
|
{
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
private readonly SettingsRepository _SettingsRepository;
|
|
||||||
readonly RateProviderFactory _RateProviderFactory;
|
|
||||||
|
|
||||||
public RatesHostedService(SettingsRepository repo,
|
|
||||||
RateProviderFactory rateProviderFactory,
|
|
||||||
Logs logs) : base(logs)
|
|
||||||
{
|
|
||||||
this._SettingsRepository = repo;
|
|
||||||
_RateProviderFactory = rateProviderFactory;
|
|
||||||
}
|
|
||||||
|
|
||||||
internal override Task[] InitializeTasks()
|
|
||||||
{
|
|
||||||
return new Task[]
|
|
||||||
{
|
|
||||||
CreateLoopTask(RefreshRates)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
bool IsStillUsed(BackgroundFetcherRateProvider fetcher)
|
bool IsStillUsed(BackgroundFetcherRateProvider fetcher)
|
||||||
{
|
{
|
||||||
@@ -51,7 +38,7 @@ namespace BTCPayServer.HostedServices
|
|||||||
|
|
||||||
IEnumerable<(string ExchangeName, BackgroundFetcherRateProvider Fetcher)> GetStillUsedProviders()
|
IEnumerable<(string ExchangeName, BackgroundFetcherRateProvider Fetcher)> GetStillUsedProviders()
|
||||||
{
|
{
|
||||||
foreach (var kv in _RateProviderFactory.Providers)
|
foreach (var kv in rateProviderFactory.Providers)
|
||||||
{
|
{
|
||||||
if (kv.Value is BackgroundFetcherRateProvider fetcher && IsStillUsed(fetcher))
|
if (kv.Value is BackgroundFetcherRateProvider fetcher && IsStillUsed(fetcher))
|
||||||
{
|
{
|
||||||
@@ -59,33 +46,23 @@ namespace BTCPayServer.HostedServices
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
async Task RefreshRates()
|
public async Task Do(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var usedProviders = GetStillUsedProviders().ToArray();
|
var usedProviders = GetStillUsedProviders().ToArray();
|
||||||
if (usedProviders.Length == 0)
|
if (usedProviders.Length == 0)
|
||||||
{
|
|
||||||
await Task.Delay(TimeSpan.FromSeconds(30), CancellationToken);
|
|
||||||
return;
|
return;
|
||||||
}
|
using var timeout = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
|
||||||
using (var timeout = CancellationTokenSource.CreateLinkedTokenSource(CancellationToken))
|
|
||||||
{
|
|
||||||
timeout.CancelAfter(TimeSpan.FromSeconds(20.0));
|
timeout.CancelAfter(TimeSpan.FromSeconds(20.0));
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await Task.WhenAll(usedProviders
|
await Task.WhenAll(usedProviders
|
||||||
.Select(p => p.Fetcher.UpdateIfNecessary(timeout.Token).ContinueWith(t =>
|
.Select(p => p.Fetcher.UpdateIfNecessary(timeout.Token))
|
||||||
{
|
|
||||||
if (t.Result.Exception != null && t.Result.Exception is not NotSupportedException)
|
|
||||||
{
|
|
||||||
Logs.PayServer.LogWarning($"Error while contacting exchange {p.ExchangeName}: {t.Result.Exception.Message}");
|
|
||||||
}
|
|
||||||
}, TaskScheduler.Default))
|
|
||||||
.ToArray()).WithCancellation(timeout.Token);
|
.ToArray()).WithCancellation(timeout.Token);
|
||||||
}
|
}
|
||||||
catch (OperationCanceledException) when (timeout.IsCancellationRequested)
|
catch (OperationCanceledException) when (timeout.IsCancellationRequested && !cancellationToken.IsCancellationRequested)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
if (_LastCacheDate is DateTimeOffset lastCache)
|
if (_lastCacheDate is DateTimeOffset lastCache)
|
||||||
{
|
{
|
||||||
if (DateTimeOffset.UtcNow - lastCache > TimeSpan.FromMinutes(8.0))
|
if (DateTimeOffset.UtcNow - lastCache > TimeSpan.FromMinutes(8.0))
|
||||||
{
|
{
|
||||||
@@ -97,51 +74,47 @@ namespace BTCPayServer.HostedServices
|
|||||||
await SaveRateCache();
|
await SaveRateCache();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
await Task.Delay(TimeSpan.FromSeconds(30), CancellationToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
public override async Task StartAsync(CancellationToken cancellationToken)
|
public async Task StartAsync(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
await TryLoadRateCache();
|
await TryLoadRateCache();
|
||||||
await base.StartAsync(cancellationToken);
|
|
||||||
}
|
}
|
||||||
public override async Task StopAsync(CancellationToken cancellationToken)
|
public async Task StopAsync(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
await SaveRateCache();
|
await SaveRateCache();
|
||||||
await base.StopAsync(cancellationToken);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task TryLoadRateCache()
|
private async Task TryLoadRateCache()
|
||||||
{
|
{
|
||||||
|
ExchangeRatesCache? cache = null;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var cache = await _SettingsRepository.GetSettingAsync<ExchangeRatesCache>();
|
cache = JsonConvert.DeserializeObject<ExchangeRatesCache>(await File.ReadAllTextAsync(GetRatesCacheFilePath(), new UTF8Encoding(false)));
|
||||||
if (cache != null)
|
}
|
||||||
|
catch
|
||||||
{
|
{
|
||||||
_LastCacheDate = cache.Created;
|
}
|
||||||
|
if (cache is { States: not null })
|
||||||
|
{
|
||||||
|
_lastCacheDate = cache.Created;
|
||||||
var stateByExchange = cache.States.ToDictionary(o => o.ExchangeName);
|
var stateByExchange = cache.States.ToDictionary(o => o.ExchangeName);
|
||||||
foreach (var provider in _RateProviderFactory.Providers)
|
foreach (var kv in stateByExchange)
|
||||||
{
|
{
|
||||||
if (stateByExchange.TryGetValue(provider.Key, out var state) &&
|
if (rateProviderFactory.Providers.TryGetValue(kv.Key, out var prov) &&
|
||||||
provider.Value is BackgroundFetcherRateProvider fetcher)
|
prov is BackgroundFetcherRateProvider fetcher)
|
||||||
{
|
{
|
||||||
fetcher.LoadState(state);
|
fetcher.LoadState(kv.Value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Logs.PayServer.LogWarning(ex, "Warning: Error while trying to load rates from cache");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DateTimeOffset? _LastCacheDate;
|
DateTimeOffset? _lastCacheDate;
|
||||||
private async Task SaveRateCache()
|
private async Task SaveRateCache()
|
||||||
{
|
{
|
||||||
var cache = new ExchangeRatesCache();
|
var cache = new ExchangeRatesCache();
|
||||||
cache.Created = DateTimeOffset.UtcNow;
|
cache.Created = DateTimeOffset.UtcNow;
|
||||||
_LastCacheDate = cache.Created;
|
_lastCacheDate = cache.Created;
|
||||||
|
|
||||||
var usedProviders = GetStillUsedProviders().ToArray();
|
var usedProviders = GetStillUsedProviders().ToArray();
|
||||||
cache.States = new List<BackgroundFetcherState>(usedProviders.Length);
|
cache.States = new List<BackgroundFetcherState>(usedProviders.Length);
|
||||||
@@ -151,7 +124,10 @@ namespace BTCPayServer.HostedServices
|
|||||||
state.ExchangeName = provider.ExchangeName;
|
state.ExchangeName = provider.ExchangeName;
|
||||||
cache.States.Add(state);
|
cache.States.Add(state);
|
||||||
}
|
}
|
||||||
await _SettingsRepository.UpdateSetting(cache);
|
|
||||||
}
|
await File.WriteAllTextAsync(GetRatesCacheFilePath(), JsonConvert.SerializeObject(cache), new UTF8Encoding(false));
|
||||||
|
}
|
||||||
|
|
||||||
|
private string GetRatesCacheFilePath() => Path.Combine(dataDirectories.Value.DataDir, "rates-cache.json");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -427,7 +427,9 @@ o.GetRequiredService<IEnumerable<IPaymentLinkExtension>>().ToDictionary(o => o.P
|
|||||||
services.AddSingleton<IHostedService, InvoiceEventSaverService>();
|
services.AddSingleton<IHostedService, InvoiceEventSaverService>();
|
||||||
services.AddSingleton<IHostedService, BitpayIPNSender>();
|
services.AddSingleton<IHostedService, BitpayIPNSender>();
|
||||||
services.AddSingleton<IHostedService, InvoiceWatcher>();
|
services.AddSingleton<IHostedService, InvoiceWatcher>();
|
||||||
services.AddSingleton<IHostedService, RatesHostedService>();
|
services.AddSingleton<RatesHostedService>();
|
||||||
|
services.AddSingleton<IHostedService>(s => s.GetRequiredService<RatesHostedService>());
|
||||||
|
services.AddScheduledTask<RatesHostedService>(TimeSpan.FromSeconds(30));
|
||||||
services.AddSingleton<IHostedService, BackgroundJobSchedulerHostedService>();
|
services.AddSingleton<IHostedService, BackgroundJobSchedulerHostedService>();
|
||||||
services.AddSingleton<IHostedService, AppHubStreamer>();
|
services.AddSingleton<IHostedService, AppHubStreamer>();
|
||||||
services.AddSingleton<IHostedService, AppInventoryUpdaterHostedService>();
|
services.AddSingleton<IHostedService, AppInventoryUpdaterHostedService>();
|
||||||
|
|||||||
Reference in New Issue
Block a user