Skip to content

Commit 346617b

Browse files
authored
Refactor rate converter separating scheduler from converter logic to improve testability (prebid#1394)
1 parent cce4967 commit 346617b

16 files changed

+431
-645
lines changed

currencies/converter_info.go

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,30 +5,23 @@ import "time"
55
// ConverterInfo holds information about converter setup
66
type ConverterInfo interface {
77
Source() string
8-
FetchingInterval() time.Duration
98
LastUpdated() time.Time
109
Rates() *map[string]map[string]float64
1110
AdditionalInfo() interface{}
1211
}
1312

1413
type converterInfo struct {
15-
source string
16-
fetchingInterval time.Duration
17-
lastUpdated time.Time
18-
rates *map[string]map[string]float64
19-
additionalInfo interface{}
14+
source string
15+
lastUpdated time.Time
16+
rates *map[string]map[string]float64
17+
additionalInfo interface{}
2018
}
2119

2220
// Source returns converter's URL source
2321
func (ci converterInfo) Source() string {
2422
return ci.source
2523
}
2624

27-
// FetchingInterval returns converter's fetching interval in nanoseconds
28-
func (ci converterInfo) FetchingInterval() time.Duration {
29-
return ci.fetchingInterval
30-
}
31-
3225
// LastUpdated returns converter's last updated time
3326
func (ci converterInfo) LastUpdated() time.Time {
3427
return ci.lastUpdated

currencies/rate_converter.go

Lines changed: 27 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -2,83 +2,43 @@ package currencies
22

33
import (
44
"encoding/json"
5+
"fmt"
56
"io/ioutil"
67
"net/http"
78
"sync/atomic"
89
"time"
910

1011
"github.com/golang/glog"
12+
"github.com/prebid/prebid-server/errortypes"
13+
"github.com/prebid/prebid-server/util/timeutil"
1114
)
1215

1316
// RateConverter holds the currencies conversion rates dictionary
1417
type RateConverter struct {
1518
httpClient httpClient
16-
done chan bool
17-
updateNotifier chan<- int
18-
fetchingInterval time.Duration
1919
staleRatesThreshold time.Duration
2020
syncSourceURL string
2121
rates atomic.Value // Should only hold Rates struct
2222
lastUpdated atomic.Value // Should only hold time.Time
2323
constantRates Conversions
24+
time timeutil.Time
2425
}
2526

2627
// NewRateConverter returns a new RateConverter
2728
func NewRateConverter(
2829
httpClient httpClient,
2930
syncSourceURL string,
30-
fetchingInterval time.Duration,
3131
staleRatesThreshold time.Duration,
3232
) *RateConverter {
33-
return NewRateConverterWithNotifier(
34-
httpClient,
35-
syncSourceURL,
36-
fetchingInterval,
37-
staleRatesThreshold,
38-
nil, // no notifier channel specified, won't send any notifications
39-
)
40-
}
41-
42-
// NewRateConverterDefault returns a RateConverter with default values.
43-
// By default there will be no currencies conversions done.
44-
// `currencies.ConstantRate` will be used.
45-
func NewRateConverterDefault() *RateConverter {
46-
return NewRateConverter(&http.Client{}, "", time.Duration(0), time.Duration(0))
47-
}
48-
49-
// NewRateConverterWithNotifier returns a new RateConverter
50-
// it allow to pass an update chan in which the number of ticks will be passed after each tick
51-
// allowing clients to listen on updates
52-
// Do not use this method
53-
func NewRateConverterWithNotifier(
54-
httpClient httpClient,
55-
syncSourceURL string,
56-
fetchingInterval time.Duration,
57-
staleRatesThreshold time.Duration,
58-
updateNotifier chan<- int,
59-
) *RateConverter {
60-
rc := &RateConverter{
33+
return &RateConverter{
6134
httpClient: httpClient,
62-
done: make(chan bool),
63-
updateNotifier: updateNotifier,
64-
fetchingInterval: fetchingInterval,
6535
staleRatesThreshold: staleRatesThreshold,
6636
syncSourceURL: syncSourceURL,
6737
rates: atomic.Value{},
6838
lastUpdated: atomic.Value{},
6939
constantRates: NewConstantRates(),
40+
time: &timeutil.RealTime{},
7041
}
71-
72-
// In case host do not want to support currency lookup
73-
// we just stop here and do nothing
74-
if rc.fetchingInterval == time.Duration(0) {
75-
return rc
76-
}
77-
78-
rc.Update() // Make sure to populate data before to return the converter
79-
go rc.startPeriodicFetching() // Start periodic ticking
80-
81-
return rc
8242
}
8343

8444
// fetch allows to retrieve the currencies rates from the syncSourceURL provided
@@ -93,6 +53,11 @@ func (rc *RateConverter) fetch() (*Rates, error) {
9353
return nil, err
9454
}
9555

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+
9661
defer response.Body.Close()
9762

9863
bytesJSON, err := ioutil.ReadAll(response.Body)
@@ -110,14 +75,14 @@ func (rc *RateConverter) fetch() (*Rates, error) {
11075
}
11176

11277
// Update updates the internal currencies rates from remote sources
113-
func (rc *RateConverter) Update() error {
78+
func (rc *RateConverter) update() error {
11479
rates, err := rc.fetch()
11580
if err == nil {
11681
rc.rates.Store(rates)
117-
rc.lastUpdated.Store(time.Now())
82+
rc.lastUpdated.Store(rc.time.Now())
11883
} else {
119-
if rc.CheckStaleRates() {
120-
rc.ClearRates()
84+
if rc.checkStaleRates() {
85+
rc.clearRates()
12186
glog.Errorf("Error updating conversion rates, falling back to constant rates: %v", err)
12287
} else {
12388
glog.Errorf("Error updating conversion rates: %v", err)
@@ -127,37 +92,8 @@ func (rc *RateConverter) Update() error {
12792
return err
12893
}
12994

130-
// startPeriodicFetching starts the periodic fetching at the given interval
131-
// triggers a first fetch when called before the first tick happen in order to initialize currencies rates map
132-
// returns a chan in which the number of data updates everytime a new update was done
133-
func (rc *RateConverter) startPeriodicFetching() {
134-
135-
ticker := time.NewTicker(rc.fetchingInterval)
136-
updatesTicksCount := 0
137-
138-
for {
139-
select {
140-
case <-ticker.C:
141-
// Retries are handled by clients directly.
142-
rc.Update()
143-
updatesTicksCount++
144-
if rc.updateNotifier != nil {
145-
rc.updateNotifier <- updatesTicksCount
146-
}
147-
case <-rc.done:
148-
if ticker != nil {
149-
ticker.Stop()
150-
ticker = nil
151-
}
152-
return
153-
}
154-
}
155-
}
156-
157-
// StopPeriodicFetching stops the periodic fetching while keeping the latest currencies rates map
158-
func (rc *RateConverter) StopPeriodicFetching() {
159-
rc.done <- true
160-
close(rc.done)
95+
func (rc *RateConverter) Run() error {
96+
return rc.update()
16197
}
16298

16399
// LastUpdated returns time when currencies rates were updated
@@ -178,18 +114,19 @@ func (rc *RateConverter) Rates() Conversions {
178114
return rc.constantRates
179115
}
180116

181-
// ClearRates sets the rates to nil
182-
func (rc *RateConverter) ClearRates() {
117+
// clearRates sets the rates to nil
118+
func (rc *RateConverter) clearRates() {
183119
// atomic.Value field rates must be of type *Rates so we cast nil to that type
184120
rc.rates.Store((*Rates)(nil))
185121
}
186122

187-
// CheckStaleRates checks if loaded third party conversion rates are stale
188-
func (rc *RateConverter) CheckStaleRates() bool {
123+
// checkStaleRates checks if loaded third party conversion rates are stale
124+
func (rc *RateConverter) checkStaleRates() bool {
189125
if rc.staleRatesThreshold <= 0 {
190126
return false
191127
}
192-
currentTime := time.Now().UTC()
128+
129+
currentTime := rc.time.Now().UTC()
193130
if lastUpdated := rc.lastUpdated.Load(); lastUpdated != nil {
194131
delta := currentTime.Sub(lastUpdated.(time.Time).UTC())
195132
if delta.Seconds() > rc.staleRatesThreshold.Seconds() {
@@ -202,14 +139,11 @@ func (rc *RateConverter) CheckStaleRates() bool {
202139
// GetInfo returns setup information about the converter
203140
func (rc *RateConverter) GetInfo() ConverterInfo {
204141
var rates *map[string]map[string]float64
205-
if rc.Rates() != nil {
206-
rates = rc.Rates().GetRates()
207-
}
142+
rates = rc.Rates().GetRates()
208143
return converterInfo{
209-
source: rc.syncSourceURL,
210-
fetchingInterval: rc.fetchingInterval,
211-
lastUpdated: rc.LastUpdated(),
212-
rates: rates,
144+
source: rc.syncSourceURL,
145+
lastUpdated: rc.LastUpdated(),
146+
rates: rates,
213147
}
214148
}
215149

0 commit comments

Comments
 (0)