2018-08-22 16:53:40 +09:00
using System ;
2018-08-25 15:09:42 +09:00
using Microsoft.Extensions.Logging ;
2018-08-22 16:53:40 +09:00
using System.Collections.Generic ;
using System.Linq ;
using System.Runtime.ExceptionServices ;
using System.Threading.Tasks ;
using BTCPayServer.Rating ;
2019-03-05 17:09:17 +09:00
using System.Threading ;
2019-05-24 18:42:22 +09:00
using Microsoft.Extensions.Logging.Abstractions ;
using BTCPayServer.Logging ;
2018-08-22 16:53:40 +09:00
namespace BTCPayServer.Services.Rates
{
public class BackgroundFetcherRateProvider : IRateProvider
{
2018-08-23 00:24:33 +09:00
public class LatestFetch
2018-08-22 16:53:40 +09:00
{
public ExchangeRates Latest ;
2018-08-25 14:44:19 +09:00
public DateTimeOffset NextRefresh ;
2019-02-24 21:57:25 +09:00
public TimeSpan Backoff = TimeSpan . FromSeconds ( 5.0 ) ;
2018-08-23 13:47:56 +09:00
public DateTimeOffset Expiration ;
2018-08-22 16:53:40 +09:00
public Exception Exception ;
2018-08-31 10:45:21 +09:00
public string ExchangeName ;
2018-08-22 16:53:40 +09:00
internal ExchangeRates GetResult ( )
{
2018-08-25 14:44:19 +09:00
if ( Expiration < = DateTimeOffset . UtcNow )
2018-08-22 16:53:40 +09:00
{
2018-08-25 14:44:19 +09:00
if ( Exception ! = null )
2018-08-23 13:47:56 +09:00
{
ExceptionDispatchInfo . Capture ( Exception ) . Throw ( ) ;
}
else
{
2018-08-31 10:45:21 +09:00
throw new InvalidOperationException ( $"The rate has expired ({ExchangeName})" ) ;
2018-08-23 13:47:56 +09:00
}
2018-08-22 16:53:40 +09:00
}
return Latest ;
}
}
IRateProvider _Inner ;
2019-05-24 18:42:22 +09:00
2018-08-22 16:53:40 +09:00
public BackgroundFetcherRateProvider ( IRateProvider inner )
{
if ( inner = = null )
throw new ArgumentNullException ( nameof ( inner ) ) ;
_Inner = inner ;
}
2018-08-25 15:49:04 +09:00
TimeSpan _RefreshRate = TimeSpan . FromSeconds ( 30 ) ;
public TimeSpan RefreshRate
{
get
{
return _RefreshRate ;
}
set
{
var diff = value - _RefreshRate ;
var latest = _Latest ;
if ( latest ! = null )
latest . NextRefresh + = diff ;
_RefreshRate = value ;
}
}
TimeSpan _ValidatyTime = TimeSpan . FromMinutes ( 10 ) ;
public TimeSpan ValidatyTime
{
get
{
return _ValidatyTime ;
}
set
{
var diff = value - _ValidatyTime ;
var latest = _Latest ;
if ( latest ! = null )
latest . Expiration + = diff ;
_ValidatyTime = value ;
}
}
2018-08-22 16:53:40 +09:00
public DateTimeOffset NextUpdate
{
get
{
var latest = _Latest ;
2018-08-25 14:44:19 +09:00
if ( latest = = null )
2018-08-22 16:53:40 +09:00
return DateTimeOffset . UtcNow ;
2018-08-25 14:44:19 +09:00
return latest . NextRefresh ;
2018-08-22 16:53:40 +09:00
}
}
2018-08-25 15:49:04 +09:00
public bool DoNotAutoFetchIfExpired { get ; set ; }
2019-02-24 21:57:25 +09:00
readonly static TimeSpan MaxBackoff = TimeSpan . FromMinutes ( 5.0 ) ;
2018-08-25 15:49:04 +09:00
2019-03-05 17:09:17 +09:00
public async Task < LatestFetch > UpdateIfNecessary ( CancellationToken cancellationToken )
2018-08-22 16:53:40 +09:00
{
if ( NextUpdate < = DateTimeOffset . UtcNow )
{
2018-08-23 00:24:33 +09:00
try
{
2019-03-05 17:09:17 +09:00
await Fetch ( cancellationToken ) ;
2018-08-23 00:24:33 +09:00
}
catch { } // Exception is inside _Latest
return _Latest ;
2018-08-22 16:53:40 +09:00
}
2018-08-23 00:24:33 +09:00
return _Latest ;
2018-08-22 16:53:40 +09:00
}
LatestFetch _Latest ;
2019-03-05 17:09:17 +09:00
public async Task < ExchangeRates > GetRatesAsync ( CancellationToken cancellationToken )
2018-08-22 16:53:40 +09:00
{
2018-08-25 15:09:42 +09:00
var latest = _Latest ;
2018-08-25 15:49:04 +09:00
if ( ! DoNotAutoFetchIfExpired & & latest ! = null & & latest . Expiration < = DateTimeOffset . UtcNow + TimeSpan . FromSeconds ( 1.0 ) )
2018-08-25 15:09:42 +09:00
{
Logs . PayServer . LogWarning ( $"GetRatesAsync was called on {GetExchangeName()} when the rate is outdated. It should never happen, let BTCPayServer developers know about this." ) ;
latest = null ;
}
2019-03-05 17:09:17 +09:00
return ( latest ? ? ( await Fetch ( cancellationToken ) ) ) . GetResult ( ) ;
2018-08-25 15:09:42 +09:00
}
private string GetExchangeName ( )
{
if ( _Inner is IHasExchangeName exchangeName )
return exchangeName . ExchangeName ? ? "???" ;
return "???" ;
2018-08-22 16:53:40 +09:00
}
2019-03-05 17:09:17 +09:00
private async Task < LatestFetch > Fetch ( CancellationToken cancellationToken )
2018-08-22 16:53:40 +09:00
{
2018-08-23 13:47:56 +09:00
var previous = _Latest ;
2018-08-22 16:53:40 +09:00
var fetch = new LatestFetch ( ) ;
2018-08-31 10:45:21 +09:00
fetch . ExchangeName = GetExchangeName ( ) ;
2018-08-22 16:53:40 +09:00
try
{
2019-03-05 17:09:17 +09:00
var rates = await _Inner . GetRatesAsync ( cancellationToken ) ;
2018-08-22 16:53:40 +09:00
fetch . Latest = rates ;
2018-08-23 13:47:56 +09:00
fetch . Expiration = DateTimeOffset . UtcNow + ValidatyTime ;
2018-08-25 14:44:19 +09:00
fetch . NextRefresh = DateTimeOffset . UtcNow + RefreshRate ;
2018-08-22 16:53:40 +09:00
}
catch ( Exception ex )
{
2018-08-25 14:44:19 +09:00
if ( previous ! = null )
{
fetch . Latest = previous . Latest ;
fetch . Expiration = previous . Expiration ;
2019-02-24 21:57:25 +09:00
fetch . Backoff = previous . Backoff * 2 ;
if ( fetch . Backoff > MaxBackoff )
fetch . Backoff = MaxBackoff ;
2018-08-25 14:44:19 +09:00
}
else
2018-08-23 13:47:56 +09:00
{
2018-08-25 14:44:19 +09:00
fetch . Expiration = DateTimeOffset . UtcNow ;
2018-08-23 13:47:56 +09:00
}
2019-02-24 21:57:25 +09:00
fetch . NextRefresh = DateTimeOffset . UtcNow + fetch . Backoff ;
2018-08-22 16:53:40 +09:00
fetch . Exception = ex ;
}
_Latest = fetch ;
2018-08-23 13:47:56 +09:00
fetch . GetResult ( ) ; // Will throw if not valid
2018-08-22 16:53:40 +09:00
return fetch ;
}
2018-08-23 00:24:33 +09:00
public void InvalidateCache ( )
{
_Latest = null ;
}
2018-08-22 16:53:40 +09:00
}
}