decred.org/dcrdex@v1.0.5/dex/txfee/feefetcher.go (about)

     1  // This code is available on the terms of the project LICENSE.md file,
     2  // also available online at https://blueoakcouncil.org/license/1.0.0.
     3  
     4  package txfee
     5  
     6  import (
     7  	"context"
     8  	"math"
     9  	"sort"
    10  	"sync"
    11  	"time"
    12  
    13  	"decred.org/dcrdex/dex"
    14  	"decred.org/dcrdex/dex/utils"
    15  )
    16  
    17  // FeeFetchFunc is a function that fetches a fee rate. If an error is
    18  // encountered the FeeFetchFunc will indicate how long to wait until trying
    19  // again.
    20  type FeeFetchFunc func(ctx context.Context) (rate uint64, errDelay time.Duration, err error)
    21  
    22  // SourceConfig defines a fee rate source.
    23  type SourceConfig struct {
    24  	F      FeeFetchFunc
    25  	Name   string
    26  	Period time.Duration
    27  	// Rank controls which priority group the source is in. Lower Rank is higher
    28  	// priority. Fee rates from similarly-ranked sources are grouped together
    29  	// for a composite rate. Lower-ranked groups are not considered at all
    30  	// until all sources from higher ranked groups are error or expired.
    31  	Rank uint
    32  }
    33  
    34  type feeFetchSource struct {
    35  	*SourceConfig
    36  
    37  	log       dex.Logger
    38  	rate      uint64
    39  	stamp     time.Time
    40  	failUntil time.Time
    41  }
    42  
    43  type FeeFetcher struct {
    44  	log     dex.Logger
    45  	c       chan uint64
    46  	sources [][]*feeFetchSource
    47  }
    48  
    49  func groupedSources(sources []*SourceConfig, log dex.Logger) [][]*feeFetchSource {
    50  	srcs := make([]*feeFetchSource, len(sources))
    51  	for i, cfg := range sources {
    52  		srcs[i] = &feeFetchSource{SourceConfig: cfg, log: log.SubLogger(cfg.Name)}
    53  	}
    54  	return groupSources(srcs)
    55  }
    56  
    57  func groupSources(sources []*feeFetchSource) [][]*feeFetchSource {
    58  	groupedSources := make([][]*feeFetchSource, 0)
    59  next:
    60  	for _, src := range sources {
    61  		for i, group := range groupedSources {
    62  			if group[0].Rank == src.Rank {
    63  				groupedSources[i] = append(group, src)
    64  				continue next
    65  			}
    66  		}
    67  		groupedSources = append(groupedSources, []*feeFetchSource{src})
    68  	}
    69  	sort.Slice(groupedSources, func(i, j int) bool {
    70  		return groupedSources[i][0].Rank < groupedSources[j][0].Rank
    71  	})
    72  	return groupedSources
    73  }
    74  
    75  // NewFeeFetcher creates and returns a new FeeFetcher.
    76  func NewFeeFetcher(sources []*SourceConfig, log dex.Logger) *FeeFetcher {
    77  	return &FeeFetcher{
    78  		log:     log,
    79  		sources: groupedSources(sources, log),
    80  		c:       make(chan uint64, 1),
    81  	}
    82  }
    83  
    84  const (
    85  	feeFetchTimeout       = time.Second * 10
    86  	feeFetchDefaultTick   = time.Minute
    87  	minFeeFetchErrorDelay = time.Minute
    88  	// Fees are fully weighted for 5 minutes, then the weight decays to zero
    89  	// over the next 10 minutes. Fee rates older than 15 minutes are not
    90  	// considered valid.
    91  	feeFetchFullValidityPeriod  = time.Minute * 5
    92  	feeFetchValidityDecayPeriod = time.Minute * 10
    93  	feeExpiration               = feeFetchFullValidityPeriod + feeFetchValidityDecayPeriod
    94  )
    95  
    96  func prioritizedFeeRate(sources [][]*feeFetchSource) uint64 {
    97  	for _, group := range sources {
    98  		var weightedRate float64
    99  		var weight float64
   100  		for _, src := range group {
   101  			age := time.Since(src.stamp)
   102  			if !src.failUntil.IsZero() || age >= feeExpiration {
   103  				continue
   104  			}
   105  			if age < feeFetchFullValidityPeriod {
   106  				weight += 1
   107  				weightedRate += float64(src.rate)
   108  				continue
   109  			}
   110  			decayAge := age - feeFetchFullValidityPeriod
   111  			w := 1 - (float64(decayAge) / float64(feeFetchValidityDecayPeriod))
   112  			weight += w
   113  			weightedRate += w * float64(src.rate)
   114  		}
   115  		if weightedRate != 0 {
   116  			return utils.Max(1, uint64(math.Round(weightedRate/weight)))
   117  		}
   118  	}
   119  	return 0
   120  }
   121  
   122  func nextSource(sources [][]*feeFetchSource) (src *feeFetchSource, delay time.Duration) {
   123  	delay = time.Duration(math.MaxInt64)
   124  	for _, group := range sources {
   125  		for _, s := range group {
   126  			if !s.failUntil.IsZero() {
   127  				if until := time.Until(s.failUntil); until < delay {
   128  					delay = until
   129  					src = s
   130  				}
   131  			} else if until := s.Period - time.Since(s.stamp); until < delay {
   132  				delay = until
   133  				src = s
   134  			}
   135  		}
   136  		if src != nil {
   137  			return src, delay
   138  		}
   139  	}
   140  	return
   141  }
   142  
   143  func (f *FeeFetcher) Connect(ctx context.Context) (*sync.WaitGroup, error) {
   144  	reportCompositeRate := func() {
   145  		r := prioritizedFeeRate(f.sources)
   146  		if r == 0 {
   147  			// Probably not possible if we have things right below.
   148  			f.log.Critical("Goose egg for prioritized fee rate")
   149  			return
   150  		}
   151  		select {
   152  		case f.c <- r:
   153  		default:
   154  			f.log.Meter("blocking-channel", time.Minute*5).Errorf("Blocking report channel")
   155  		}
   156  	}
   157  
   158  	updateSource := func(src *feeFetchSource) bool {
   159  		ctx, cancel := context.WithTimeout(ctx, feeFetchTimeout)
   160  		defer cancel()
   161  		r, errDelay, err := src.F(ctx)
   162  		if err != nil {
   163  			src.log.Meter("fetch-error", time.Minute*30).Errorf("Fetch error: %v", err)
   164  			src.failUntil = time.Now().Add(utils.Max(minFeeFetchErrorDelay, errDelay))
   165  			return false
   166  		}
   167  		if r == 0 {
   168  			src.log.Meter("zero-rate", time.Minute*30).Error("Fee rate of zero")
   169  			src.failUntil = time.Now().Add(minFeeFetchErrorDelay)
   170  			return false
   171  		}
   172  		src.failUntil = time.Time{}
   173  		src.stamp = time.Now()
   174  		src.rate = r
   175  		src.log.Tracef("New rate %d", r)
   176  		return true
   177  	}
   178  
   179  	var wg sync.WaitGroup
   180  	wg.Add(1)
   181  	go func() {
   182  		// Prime the sources.
   183  		var any bool
   184  		for _, group := range f.sources {
   185  			for _, src := range group {
   186  				any = updateSource(src) || any
   187  			}
   188  		}
   189  		if any {
   190  			reportCompositeRate()
   191  		}
   192  
   193  		// Start the fetch loop.
   194  		defer wg.Done()
   195  		for {
   196  			if ctx.Err() != nil {
   197  				return
   198  			}
   199  			src, delay := nextSource(f.sources)
   200  			var timeout *time.Timer
   201  			if src == nil {
   202  				f.log.Meter("all-failed", time.Minute*10).Error("All sources failed")
   203  				timeout = time.NewTimer(feeFetchDefaultTick)
   204  			} else {
   205  				timeout = time.NewTimer(utils.Max(0, delay))
   206  			}
   207  			select {
   208  			case <-timeout.C:
   209  				if src == nil || !updateSource(src) {
   210  					continue
   211  				}
   212  				reportCompositeRate()
   213  			case <-ctx.Done():
   214  				return
   215  			}
   216  		}
   217  	}()
   218  
   219  	return &wg, nil
   220  }
   221  
   222  func (f *FeeFetcher) Next() <-chan uint64 {
   223  	return f.c
   224  }