Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor rate converter separating scheduler from converter logic to improve testability #1394

Merged
merged 21 commits into from
Aug 12, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
9bbed41
Separate rate converter logic from periodic task scheduling.
bsardo Jul 7, 2020
cc006d0
Refactor rate converter splitting the ticker from the converter logic
bsardo Jul 7, 2020
32b95b8
Move clock interfaces and real clock to their own files
bsardo Jul 13, 2020
ef89867
Remove unused clock methods
bsardo Jul 13, 2020
7ad09b0
Only export rate converter methods needed outside the currencies pack…
bsardo Jul 13, 2020
b8f6d36
Add mock clock tests
bsardo Jul 13, 2020
2b638ad
Move clock files into clock package
bsardo Jul 13, 2020
abad978
Move clock package to timeutil package
bsardo Jul 17, 2020
6afc23f
Move ticker task channel notifier onto the task Runner interface so l…
bsardo Jul 20, 2020
e63bb59
Hardcode clock in rate converter factory function and move simplified…
bsardo Jul 20, 2020
5615880
Shorten ticker task module variable name from tt to t
bsardo Jul 21, 2020
29ca564
Remove notifier from rate converter
bsardo Jul 31, 2020
c124fc9
Remove wrapped rate converter constructor and rename ticker task run …
bsardo Aug 4, 2020
243e512
Small adjustments to the rate converter race test
bsardo Aug 4, 2020
babff17
Make rate converter time field private and move corresponding test fi…
bsardo Aug 4, 2020
e19c428
Consolidate ticker task tests
bsardo Aug 7, 2020
d75fff7
Removed alternate rate converter constructor used just for tests and …
bsardo Aug 7, 2020
f2c7a0d
Revert ticker task test consolidation and add Stop call to clean up
bsardo Aug 7, 2020
3518ff7
Remove fetching interval from rate converter and pass rate converter …
bsardo Aug 10, 2020
de7e4a0
Pass fetching interval instead of ticker task to currency rates debug…
bsardo Aug 11, 2020
d74cf9a
Simplify rate converter GetInfo endpoint to only call GetRates once
bsardo Aug 11, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 4 additions & 11 deletions currencies/converter_info.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,30 +5,23 @@ import "time"
// ConverterInfo holds information about converter setup
type ConverterInfo interface {
Source() string
FetchingInterval() time.Duration
LastUpdated() time.Time
Rates() *map[string]map[string]float64
AdditionalInfo() interface{}
}

type converterInfo struct {
source string
fetchingInterval time.Duration
lastUpdated time.Time
rates *map[string]map[string]float64
additionalInfo interface{}
source string
lastUpdated time.Time
rates *map[string]map[string]float64
additionalInfo interface{}
}

// Source returns converter's URL source
func (ci converterInfo) Source() string {
return ci.source
}

// FetchingInterval returns converter's fetching interval in nanoseconds
func (ci converterInfo) FetchingInterval() time.Duration {
return ci.fetchingInterval
}

// LastUpdated returns converter's last updated time
func (ci converterInfo) LastUpdated() time.Time {
return ci.lastUpdated
Expand Down
120 changes: 27 additions & 93 deletions currencies/rate_converter.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,83 +2,43 @@ package currencies

import (
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"sync/atomic"
"time"

"github.com/golang/glog"
"github.com/prebid/prebid-server/errortypes"
"github.com/prebid/prebid-server/util/timeutil"
)

// RateConverter holds the currencies conversion rates dictionary
type RateConverter struct {
httpClient httpClient
done chan bool
updateNotifier chan<- int
fetchingInterval time.Duration
staleRatesThreshold time.Duration
syncSourceURL string
rates atomic.Value // Should only hold Rates struct
lastUpdated atomic.Value // Should only hold time.Time
constantRates Conversions
time timeutil.Time
}

// NewRateConverter returns a new RateConverter
func NewRateConverter(
httpClient httpClient,
syncSourceURL string,
fetchingInterval time.Duration,
staleRatesThreshold time.Duration,
) *RateConverter {
return NewRateConverterWithNotifier(
httpClient,
syncSourceURL,
fetchingInterval,
staleRatesThreshold,
nil, // no notifier channel specified, won't send any notifications
)
}

// NewRateConverterDefault returns a RateConverter with default values.
// By default there will be no currencies conversions done.
// `currencies.ConstantRate` will be used.
func NewRateConverterDefault() *RateConverter {
return NewRateConverter(&http.Client{}, "", time.Duration(0), time.Duration(0))
}

// NewRateConverterWithNotifier returns a new RateConverter
// it allow to pass an update chan in which the number of ticks will be passed after each tick
// allowing clients to listen on updates
// Do not use this method
func NewRateConverterWithNotifier(
httpClient httpClient,
syncSourceURL string,
fetchingInterval time.Duration,
staleRatesThreshold time.Duration,
updateNotifier chan<- int,
) *RateConverter {
rc := &RateConverter{
return &RateConverter{
httpClient: httpClient,
done: make(chan bool),
updateNotifier: updateNotifier,
fetchingInterval: fetchingInterval,
staleRatesThreshold: staleRatesThreshold,
syncSourceURL: syncSourceURL,
rates: atomic.Value{},
lastUpdated: atomic.Value{},
constantRates: NewConstantRates(),
time: &timeutil.RealTime{},
}

// In case host do not want to support currency lookup
SyntaxNode marked this conversation as resolved.
Show resolved Hide resolved
// we just stop here and do nothing
if rc.fetchingInterval == time.Duration(0) {
return rc
}

rc.Update() // Make sure to populate data before to return the converter
go rc.startPeriodicFetching() // Start periodic ticking

return rc
}

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

if response.StatusCode >= 400 {
SyntaxNode marked this conversation as resolved.
Show resolved Hide resolved
message := fmt.Sprintf("The currency rates request failed with status code %d", response.StatusCode)
return nil, &errortypes.BadServerResponse{Message: message}
}

defer response.Body.Close()

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

// Update updates the internal currencies rates from remote sources
func (rc *RateConverter) Update() error {
func (rc *RateConverter) update() error {
rates, err := rc.fetch()
if err == nil {
rc.rates.Store(rates)
rc.lastUpdated.Store(time.Now())
rc.lastUpdated.Store(rc.time.Now())
} else {
if rc.CheckStaleRates() {
rc.ClearRates()
if rc.checkStaleRates() {
rc.clearRates()
glog.Errorf("Error updating conversion rates, falling back to constant rates: %v", err)
} else {
glog.Errorf("Error updating conversion rates: %v", err)
Expand All @@ -127,37 +92,8 @@ func (rc *RateConverter) Update() error {
return err
}

// startPeriodicFetching starts the periodic fetching at the given interval
// triggers a first fetch when called before the first tick happen in order to initialize currencies rates map
// returns a chan in which the number of data updates everytime a new update was done
func (rc *RateConverter) startPeriodicFetching() {

ticker := time.NewTicker(rc.fetchingInterval)
updatesTicksCount := 0

for {
select {
case <-ticker.C:
// Retries are handled by clients directly.
rc.Update()
updatesTicksCount++
if rc.updateNotifier != nil {
rc.updateNotifier <- updatesTicksCount
}
case <-rc.done:
if ticker != nil {
ticker.Stop()
ticker = nil
}
return
}
}
}

// StopPeriodicFetching stops the periodic fetching while keeping the latest currencies rates map
func (rc *RateConverter) StopPeriodicFetching() {
rc.done <- true
close(rc.done)
func (rc *RateConverter) Run() error {
return rc.update()
}

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

// ClearRates sets the rates to nil
func (rc *RateConverter) ClearRates() {
// clearRates sets the rates to nil
func (rc *RateConverter) clearRates() {
// atomic.Value field rates must be of type *Rates so we cast nil to that type
rc.rates.Store((*Rates)(nil))
}

// CheckStaleRates checks if loaded third party conversion rates are stale
func (rc *RateConverter) CheckStaleRates() bool {
// checkStaleRates checks if loaded third party conversion rates are stale
func (rc *RateConverter) checkStaleRates() bool {
if rc.staleRatesThreshold <= 0 {
return false
}
currentTime := time.Now().UTC()

currentTime := rc.time.Now().UTC()
if lastUpdated := rc.lastUpdated.Load(); lastUpdated != nil {
delta := currentTime.Sub(lastUpdated.(time.Time).UTC())
if delta.Seconds() > rc.staleRatesThreshold.Seconds() {
Expand All @@ -202,14 +139,11 @@ func (rc *RateConverter) CheckStaleRates() bool {
// GetInfo returns setup information about the converter
func (rc *RateConverter) GetInfo() ConverterInfo {
var rates *map[string]map[string]float64
if rc.Rates() != nil {
rates = rc.Rates().GetRates()
}
rates = rc.Rates().GetRates()
return converterInfo{
source: rc.syncSourceURL,
fetchingInterval: rc.fetchingInterval,
lastUpdated: rc.LastUpdated(),
rates: rates,
source: rc.syncSourceURL,
lastUpdated: rc.LastUpdated(),
rates: rates,
}
}

Expand Down
Loading