github.com/prebid/prebid-server/v2@v2.18.0/currency/rate_converter.go (about) 1 package currency 2 3 import ( 4 "fmt" 5 "io" 6 "net/http" 7 "sync/atomic" 8 "time" 9 10 "github.com/golang/glog" 11 "github.com/prebid/prebid-server/v2/errortypes" 12 "github.com/prebid/prebid-server/v2/util/jsonutil" 13 "github.com/prebid/prebid-server/v2/util/timeutil" 14 ) 15 16 // RateConverter holds the currencies conversion rates dictionary 17 type RateConverter struct { 18 httpClient httpClient 19 staleRatesThreshold time.Duration 20 syncSourceURL string 21 rates atomic.Value // Should only hold Rates struct 22 lastUpdated atomic.Value // Should only hold time.Time 23 constantRates Conversions 24 time timeutil.Time 25 } 26 27 // NewRateConverter returns a new RateConverter 28 func NewRateConverter( 29 httpClient httpClient, 30 syncSourceURL string, 31 staleRatesThreshold time.Duration, 32 ) *RateConverter { 33 return &RateConverter{ 34 httpClient: httpClient, 35 staleRatesThreshold: staleRatesThreshold, 36 syncSourceURL: syncSourceURL, 37 rates: atomic.Value{}, 38 lastUpdated: atomic.Value{}, 39 constantRates: NewConstantRates(), 40 time: &timeutil.RealTime{}, 41 } 42 } 43 44 // fetch allows to retrieve the currencies rates from the syncSourceURL provided 45 func (rc *RateConverter) fetch() (*Rates, error) { 46 request, err := http.NewRequest("GET", rc.syncSourceURL, nil) 47 if err != nil { 48 return nil, err 49 } 50 51 response, err := rc.httpClient.Do(request) 52 if err != nil { 53 return nil, err 54 } 55 56 if response.StatusCode >= 400 { 57 message := fmt.Sprintf("The currency rates request failed with status code %d", response.StatusCode) 58 return nil, &errortypes.BadServerResponse{Message: message} 59 } 60 61 defer response.Body.Close() 62 63 bytesJSON, err := io.ReadAll(response.Body) 64 if err != nil { 65 return nil, err 66 } 67 68 updatedRates := &Rates{} 69 err = jsonutil.UnmarshalValid(bytesJSON, updatedRates) 70 if err != nil { 71 return nil, err 72 } 73 74 return updatedRates, err 75 } 76 77 // Update updates the internal currencies rates from remote sources 78 func (rc *RateConverter) update() error { 79 rates, err := rc.fetch() 80 if err == nil { 81 rc.rates.Store(rates) 82 rc.lastUpdated.Store(rc.time.Now()) 83 } else { 84 if rc.checkStaleRates() { 85 rc.clearRates() 86 glog.Errorf("Error updating conversion rates, falling back to constant rates: %v", err) 87 } else { 88 glog.Errorf("Error updating conversion rates: %v", err) 89 } 90 } 91 92 return err 93 } 94 95 func (rc *RateConverter) Run() error { 96 return rc.update() 97 } 98 99 // LastUpdated returns time when currencies rates were updated 100 func (rc *RateConverter) LastUpdated() time.Time { 101 if lastUpdated := rc.lastUpdated.Load(); lastUpdated != nil { 102 return lastUpdated.(time.Time) 103 } 104 return time.Time{} 105 } 106 107 // Rates returns current conversions rates 108 func (rc *RateConverter) Rates() Conversions { 109 // atomic.Value field rates is an empty interface and will be of type *Rates the first time rates are stored 110 // or nil if the rates have never been stored 111 if rates := rc.rates.Load(); rates != (*Rates)(nil) && rates != nil { 112 return rates.(*Rates) 113 } 114 return rc.constantRates 115 } 116 117 // clearRates sets the rates to nil 118 func (rc *RateConverter) clearRates() { 119 // atomic.Value field rates must be of type *Rates so we cast nil to that type 120 rc.rates.Store((*Rates)(nil)) 121 } 122 123 // checkStaleRates checks if loaded third party conversion rates are stale 124 func (rc *RateConverter) checkStaleRates() bool { 125 if rc.staleRatesThreshold <= 0 { 126 return false 127 } 128 129 currentTime := rc.time.Now().UTC() 130 if lastUpdated := rc.lastUpdated.Load(); lastUpdated != nil { 131 delta := currentTime.Sub(lastUpdated.(time.Time).UTC()) 132 if delta.Seconds() > rc.staleRatesThreshold.Seconds() { 133 return true 134 } 135 } 136 return false 137 } 138 139 // GetInfo returns setup information about the converter 140 func (rc *RateConverter) GetInfo() ConverterInfo { 141 var rates *map[string]map[string]float64 142 rates = rc.Rates().GetRates() 143 return converterInfo{ 144 source: rc.syncSourceURL, 145 lastUpdated: rc.LastUpdated(), 146 rates: rates, 147 } 148 } 149 150 type httpClient interface { 151 Do(req *http.Request) (*http.Response, error) 152 } 153 154 // Conversions allows to get a conversion rate between two currencies. 155 // if one of the currency string is not a currency or if there is not conversion between those 156 // currencies, then an err is returned and rate is 0. 157 type Conversions interface { 158 GetRate(from string, to string) (float64, error) 159 GetRates() *map[string]map[string]float64 160 }