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 }