decred.org/dcrdex@v1.0.5/server/dex/feemgr.go (about)

     1  package dex
     2  
     3  import (
     4  	"context"
     5  	"sync"
     6  	"sync/atomic"
     7  	"time"
     8  
     9  	"decred.org/dcrdex/server/asset"
    10  	"decred.org/dcrdex/server/market"
    11  )
    12  
    13  // FeeManager manages FeeFetchers.
    14  type FeeManager struct {
    15  	fetchers map[uint32]*feeFetcher
    16  }
    17  
    18  var _ market.FeeSource = (*FeeManager)(nil)
    19  
    20  // NewFeeManager is the constructor for a FeeManager.
    21  func NewFeeManager() *FeeManager {
    22  	return &FeeManager{
    23  		fetchers: make(map[uint32]*feeFetcher),
    24  	}
    25  }
    26  
    27  // AddFetcher adds a FeeFetcher for an asset.
    28  func (m *FeeManager) AddFetcher(asset *asset.BackedAsset) {
    29  	m.fetchers[asset.ID] = newFeeFetcher(asset)
    30  }
    31  
    32  // FeeFetcher returns the specified asset's FeeFetcher.
    33  func (m *FeeManager) FeeFetcher(assetID uint32) market.FeeFetcher {
    34  	return m.fetchers[assetID]
    35  }
    36  
    37  // LastRate is the last rate cached for the specified asset.
    38  func (m *FeeManager) LastRate(assetID uint32) uint64 {
    39  	return m.fetchers[assetID].LastRate()
    40  }
    41  
    42  // feeFetcher implements market.FeeFetcher and updates the last fee rate cache.
    43  type feeFetcher struct {
    44  	*asset.BackedAsset
    45  	lastRate *uint64
    46  
    47  	// Stash the old rate for a short time to avoid a race condition where
    48  	// a client gets a rate right before the server gets a higher rate, then the
    49  	// client tries to use the old rate and is rejected.
    50  	stashedRate struct {
    51  		sync.RWMutex
    52  		rate  uint64
    53  		stamp time.Time
    54  	}
    55  }
    56  
    57  var _ market.FeeFetcher = (*feeFetcher)(nil)
    58  
    59  // newFeeFetcher is the constructor for a *feeFetcher.
    60  func newFeeFetcher(asset *asset.BackedAsset) *feeFetcher {
    61  	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    62  	defer cancel()
    63  	r, err := asset.Backend.FeeRate(ctx)
    64  	if err != nil {
    65  		log.Warnf("Error priming fee cache for %s: %v", asset.Symbol, err)
    66  	}
    67  	if r > asset.MaxFeeRate {
    68  		r = asset.MaxFeeRate
    69  	}
    70  	return &feeFetcher{
    71  		BackedAsset: asset,
    72  		lastRate:    &r,
    73  	}
    74  }
    75  
    76  // Use the lower rate for a minute after a rate increase to avoid races.
    77  const stashedRateExpiry = time.Minute
    78  
    79  // FeeRate fetches a new fee rate and updates the cache.
    80  func (f *feeFetcher) FeeRate(ctx context.Context) uint64 {
    81  	r, err := f.Backend.FeeRate(ctx)
    82  	if err != nil {
    83  		log.Errorf("Error retrieving fee rate for %s: %v", f.Symbol, err)
    84  		return 0 // Do not store as last rate.
    85  	}
    86  	if r <= 0 {
    87  		return 0
    88  	}
    89  	if r > f.Asset.MaxFeeRate {
    90  		r = f.Asset.MaxFeeRate
    91  	}
    92  	oldRate := atomic.SwapUint64(f.lastRate, r)
    93  	if oldRate < r {
    94  		f.stashedRate.Lock()
    95  		f.stashedRate.rate = oldRate
    96  		f.stashedRate.stamp = time.Now()
    97  		f.stashedRate.Unlock()
    98  		return oldRate
    99  	}
   100  	f.stashedRate.RLock()
   101  	if time.Since(f.stashedRate.stamp) < stashedRateExpiry && f.stashedRate.rate < r {
   102  		r = f.stashedRate.rate
   103  	}
   104  	f.stashedRate.RUnlock()
   105  	return r
   106  }
   107  
   108  // LastRate is the last rate cached. This may be used as a fallback if FeeRate
   109  // times out, or as a quick rate when rate freshness is not critical.
   110  func (f *feeFetcher) LastRate() uint64 {
   111  	r := atomic.LoadUint64(f.lastRate)
   112  	f.stashedRate.RLock()
   113  	if time.Since(f.stashedRate.stamp) < stashedRateExpiry && f.stashedRate.rate < r {
   114  		r = f.stashedRate.rate
   115  	}
   116  	f.stashedRate.RUnlock()
   117  	return r
   118  }
   119  
   120  // MaxFeeRate is a getter for the BackedAsset's dex.Asset.MaxFeeRate. This is
   121  // provided so consumers that operate on the returned FeeRate can respect the
   122  // configured limit e.g. ScaleFeeRate in (*Market).processReadyEpoch.
   123  func (f *feeFetcher) MaxFeeRate() uint64 {
   124  	return f.Asset.MaxFeeRate
   125  }
   126  
   127  // SwapFeeRate returns the tx fee that needs to be used to initiate a swap.
   128  // This fee will be the max fee rate if the asset supports dynamic tx fees,
   129  // and otherwise it will be the current market fee rate.
   130  func (f *feeFetcher) SwapFeeRate(ctx context.Context) uint64 {
   131  	if f.Backend.Info().SupportsDynamicTxFee {
   132  		return f.MaxFeeRate()
   133  	}
   134  	return f.FeeRate(ctx)
   135  }