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鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add concurrency to GetUSD #766

Merged
merged 4 commits into from Feb 13, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 3 additions & 0 deletions core/resty/resty.go
Expand Up @@ -218,6 +218,9 @@ func (r *Resty) Wait() []error {
for {
select {
case <-r.ctx.Done():
if r.ctx.Err() == context.DeadlineExceeded {
return []error{r.ctx.Err()}
}
return errs

case result := <-r.done:
Expand Down
2 changes: 0 additions & 2 deletions core/tokenrate/bancor.go
Expand Up @@ -35,7 +35,6 @@ func (qq *bancorQuoteQuery) getUSD(ctx context.Context, symbol string) (float64,
return 0, errors.New("bancor: please configure dlt_id on environment variable [" + evnName + "] first")
}
dltId = id

}

r := resty.New()
Expand Down Expand Up @@ -79,7 +78,6 @@ func (qq *bancorQuoteQuery) getUSD(ctx context.Context, symbol string) (float64,
if ok && rate.Value > 0 {
return rate.Value, nil
}

}

return 0, fmt.Errorf("bancor: %s price is not provided on bancor apis", symbol)
Expand Down
5 changes: 2 additions & 3 deletions core/tokenrate/coingecko.go
Expand Up @@ -5,12 +5,11 @@ import (
"encoding/json"
"errors"
"fmt"
"github.com/0chain/gosdk/core/resty"
"net/http"
"os"
"strconv"
"strings"

"github.com/0chain/gosdk/core/resty"
)

type coingeckoQuoteQuery struct {
Expand All @@ -32,7 +31,7 @@ func (qq *coingeckoQuoteQuery) getUSD(ctx context.Context, symbol string) (float
envName := "COINGECKO_COINID_" + strings.ToUpper(symbol)
id, ok := os.LookupEnv(envName)
if !ok {
return 0, errors.New("coingecko: please configure coinid on environment variable [" + envName + "' first")
return 0, errors.New("coingecko: please configure coinid on environment variable [" + envName + "] first")
}
coinID = id

Expand Down
177 changes: 88 additions & 89 deletions core/tokenrate/coinmarketcap.go
Expand Up @@ -5,12 +5,11 @@ import (
"encoding/json"
"errors"
"fmt"
"github.com/0chain/gosdk/core/resty"
"net/http"
"os"
"strconv"
"strings"

"github.com/0chain/gosdk/core/resty"
)

type coinmarketcapQuoteQuery struct {
Expand Down Expand Up @@ -82,93 +81,93 @@ func (qq *coinmarketcapQuoteQuery) getUSD(ctx context.Context, symbol string) (f
return 0, errors.New("coinmarketcap: " + symbol + " to USD quote is not provided on coinmarketcap apis")
}

// {
// "status": {
// "timestamp": "2022-06-03T02:18:34.093Z",
// "error_code": 0,
// "error_message": null,
// "elapsed": 50,
// "credit_count": 1,
// "notice": null
// },
// "data": {
// "ZCN": [
// {
// "id": 2882,
// "name": "0Chain",
// "symbol": "ZCN",
// "slug": "0chain",
// "num_market_pairs": 8,
// "date_added": "2018-07-02T00:00:00.000Z",
// "tags": [
// {
// "slug": "platform",
// "name": "Platform",
// "category": "PROPERTY"
// },
// {
// "slug": "ai-big-data",
// "name": "AI & Big Data",
// "category": "PROPERTY"
// },
// {
// "slug": "distributed-computing",
// "name": "Distributed Computing",
// "category": "PROPERTY"
// },
// {
// "slug": "filesharing",
// "name": "Filesharing",
// "category": "PROPERTY"
// },
// {
// "slug": "iot",
// "name": "IoT",
// "category": "PROPERTY"
// },
// {
// "slug": "storage",
// "name": "Storage",
// "category": "PROPERTY"
// }
// ],
// "max_supply": 400000000,
// "circulating_supply": 48400982,
// "total_supply": 200000000,
// "platform": {
// "id": 1027,
// "name": "Ethereum",
// "symbol": "ETH",
// "slug": "ethereum",
// "token_address": "0xb9ef770b6a5e12e45983c5d80545258aa38f3b78"
// },
// "is_active": 1,
// "cmc_rank": 782,
// "is_fiat": 0,
// "self_reported_circulating_supply": 115000000,
// "self_reported_market_cap": 25409234.858036295,
// "last_updated": "2022-06-03T02:17:00.000Z",
// "quote": {
// "USD": {
// "price": 0.2209498683307504,
// "volume_24h": 28807.79174117,
// "volume_change_24h": -78.341,
// "percent_change_1h": 0.09600341,
// "percent_change_24h": 0.1834049,
// "percent_change_7d": 24.08736297,
// "percent_change_30d": -43.56084388,
// "percent_change_60d": -63.69787917,
// "percent_change_90d": -27.17695342,
// "market_cap": 10694190.59997902,
// "market_cap_dominance": 0.0008,
// "fully_diluted_market_cap": 88379947.33,
// "last_updated": "2022-06-03T02:17:00.000Z"
// }
// }
// }
// ]
// }
// }
// {
// "status": {
// "timestamp": "2022-06-03T02:18:34.093Z",
// "error_code": 0,
// "error_message": null,
// "elapsed": 50,
// "credit_count": 1,
// "notice": null
// },
// "data": {
// "ZCN": [
// {
// "id": 2882,
// "name": "0Chain",
// "symbol": "ZCN",
// "slug": "0chain",
// "num_market_pairs": 8,
// "date_added": "2018-07-02T00:00:00.000Z",
// "tags": [
// {
// "slug": "platform",
// "name": "Platform",
// "category": "PROPERTY"
// },
// {
// "slug": "ai-big-data",
// "name": "AI & Big Data",
// "category": "PROPERTY"
// },
// {
// "slug": "distributed-computing",
// "name": "Distributed Computing",
// "category": "PROPERTY"
// },
// {
// "slug": "filesharing",
// "name": "Filesharing",
// "category": "PROPERTY"
// },
// {
// "slug": "iot",
// "name": "IoT",
// "category": "PROPERTY"
// },
// {
// "slug": "storage",
// "name": "Storage",
// "category": "PROPERTY"
// }
// ],
// "max_supply": 400000000,
// "circulating_supply": 48400982,
// "total_supply": 200000000,
// "platform": {
// "id": 1027,
// "name": "Ethereum",
// "symbol": "ETH",
// "slug": "ethereum",
// "token_address": "0xb9ef770b6a5e12e45983c5d80545258aa38f3b78"
// },
// "is_active": 1,
// "cmc_rank": 782,
// "is_fiat": 0,
// "self_reported_circulating_supply": 115000000,
// "self_reported_market_cap": 25409234.858036295,
// "last_updated": "2022-06-03T02:17:00.000Z",
// "quote": {
// "USD": {
// "price": 0.2209498683307504,
// "volume_24h": 28807.79174117,
// "volume_change_24h": -78.341,
// "percent_change_1h": 0.09600341,
// "percent_change_24h": 0.1834049,
// "percent_change_7d": 24.08736297,
// "percent_change_30d": -43.56084388,
// "percent_change_60d": -63.69787917,
// "percent_change_90d": -27.17695342,
// "market_cap": 10694190.59997902,
// "market_cap_dominance": 0.0008,
// "fully_diluted_market_cap": 88379947.33,
// "last_updated": "2022-06-03T02:17:00.000Z"
// }
// }
// }
// ]
// }
// }
type coinmarketcapResponse struct {
Data map[string][]coinmarketcapCurrency `json:"data"`
Raw string `json:"-"`
Expand Down
48 changes: 42 additions & 6 deletions core/tokenrate/tokenrate.go
Expand Up @@ -4,6 +4,8 @@ import (
"context"
"errors"
"fmt"
"sort"
"sync"
)

var ErrNoAvailableQuoteQuery = errors.New("token: no available quote query service")
Expand All @@ -21,18 +23,52 @@ func init() {
}

func GetUSD(ctx context.Context, symbol string) (float64, error) {
var mu sync.Mutex
done := false

errs := make([]error, 0, len(quotes))
errCh := make(chan error, len(quotes))
successCh := make(chan float64)

ctx, cancel := context.WithCancel(ctx)
defer func() {
cancel()
}()

for _, q := range quotes {
r, err := q.getUSD(ctx, symbol)
if err == nil {
go func(q quoteQuery) {
val, err := q.getUSD(ctx, symbol)
if err != nil {
errCh <- err
} else {
mu.Lock()
if !done {
// we don't want to send result again if it has already been sent
successCh <- val
done = true
}
mu.Unlock()
}
}(q)
}

for {
select {
case e := <-errCh:
errs = append(errs, e)
if len(errs) >= len(quotes) {
if errs[0] == context.DeadlineExceeded {
return 0, context.DeadlineExceeded
}
sort.Slice(errs, func(i, j int) bool {
return errs[i].Error() < errs[j].Error()
})
return 0, fmt.Errorf("%w: %s", ErrNoAvailableQuoteQuery, errs)
}
case r := <-successCh:
return r, nil
}

errs = append(errs, err)
}

return 0, fmt.Errorf("%w: %s", ErrNoAvailableQuoteQuery, errs)
}

type quoteQuery interface {
Expand Down
67 changes: 67 additions & 0 deletions core/tokenrate/tokenrate_test.go
@@ -0,0 +1,67 @@
package tokenrate

import (
"context"
"fmt"
"github.com/stretchr/testify/require"
"testing"
"time"
)

func TestGetUSD(t *testing.T) {
for _, tc := range []struct {
name string
expectedErr error
setup func() context.Context
symbol string
}{
{
name: "context deadline exceeded",
expectedErr: context.DeadlineExceeded,
setup: getRequestContext(10 * time.Millisecond),
symbol: "eth",
},
{
name: "all success case",
expectedErr: nil,
setup: getRequestContext(10 * time.Second),
symbol: "eth",
},
{
name: "error wrong symbol",
expectedErr: getErrorForWrongSymbol(),
setup: getRequestContext(10 * time.Second),
symbol: "wrong",
},
} {
t.Run(tc.name, func(t *testing.T) {
ctx := tc.setup()
val, err := GetUSD(ctx, tc.symbol)
if tc.expectedErr != nil {
require.EqualError(t, err, tc.expectedErr.Error())
} else {
require.NoError(t, err)
require.Greater(t, val, 0.0)
}
})
}
}

func getRequestContext(d time.Duration) func() context.Context {
return func() context.Context {
ctx, cancel := context.WithTimeout(context.TODO(), d)
go func() {
<-ctx.Done()
cancel()
}()
return ctx
}
}

func getErrorForWrongSymbol() error {
errs := make([]error, 0, 3)
errs = append(errs, fmt.Errorf("bancor: please configure dlt_id on environment variable [%v] first", "BANCOR_DLTID_WRONG"))
errs = append(errs, fmt.Errorf("coingecko: please configure coinid on environment variable [%v] first", "COINGECKO_COINID_WRONG"))
errs = append(errs, fmt.Errorf("coinmarketcap: wrong is not provided on coinmarketcap apis"))
return fmt.Errorf("%w: %s", ErrNoAvailableQuoteQuery, errs)
}