Cache calls to SecurityTypeByISIN #18
@@ -6,6 +6,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/biter777/countries"
|
"github.com/biter777/countries"
|
||||||
@@ -16,16 +17,41 @@ import (
|
|||||||
type OpenFIGI struct {
|
type OpenFIGI struct {
|
||||||
client *http.Client
|
client *http.Client
|
||||||
mappingLimiter *rate.Limiter
|
mappingLimiter *rate.Limiter
|
||||||
|
|
||||||
|
mu sync.RWMutex
|
||||||
|
// TODO: there's no eviction policy at the moment as this is only used by short-lived application
|
||||||
|
// which processes a relatively small amount of records. We need to consider using an external
|
||||||
|
// cache lib (like golang-lru or go-cache) if this becomes a problem or implement this ourselves.
|
||||||
|
securityTypeCache map[string]string
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewOpenFIGI(c *http.Client) *OpenFIGI {
|
func NewOpenFIGI(c *http.Client) *OpenFIGI {
|
||||||
return &OpenFIGI{
|
return &OpenFIGI{
|
||||||
client: c,
|
client: c,
|
||||||
mappingLimiter: rate.NewLimiter(rate.Every(time.Minute), 25), // https://www.openfigi.com/api/documentation#rate-limits
|
mappingLimiter: rate.NewLimiter(rate.Every(time.Minute), 25), // https://www.openfigi.com/api/documentation#rate-limits
|
||||||
|
|
||||||
|
securityTypeCache: make(map[string]string),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (of *OpenFIGI) SecurityTypeByISIN(ctx context.Context, isin string) (string, error) {
|
func (of *OpenFIGI) SecurityTypeByISIN(ctx context.Context, isin string) (string, error) {
|
||||||
|
of.mu.RLock()
|
||||||
|
if secType, ok := of.securityTypeCache[isin]; ok {
|
||||||
|
of.mu.RUnlock()
|
||||||
|
return secType, nil
|
||||||
|
}
|
||||||
|
of.mu.RUnlock()
|
||||||
|
|
||||||
|
of.mu.Lock()
|
||||||
|
defer of.mu.Unlock()
|
||||||
|
|
||||||
|
// we check again because there could be more than one concurrent cache miss and we want only one
|
||||||
|
// of them to result in an actual request. When the first one releases the lock the following
|
||||||
|
// reads will hit the cache.
|
||||||
|
if secType, ok := of.securityTypeCache[isin]; ok {
|
||||||
|
return secType, nil
|
||||||
|
}
|
||||||
|
|
||||||
if len(isin) != 12 || countries.ByName(isin[:2]) == countries.Unknown {
|
if len(isin) != 12 || countries.ByName(isin[:2]) == countries.Unknown {
|
||||||
return "", fmt.Errorf("invalid ISIN: %s", isin)
|
return "", fmt.Errorf("invalid ISIN: %s", isin)
|
||||||
}
|
}
|
||||||
@@ -76,7 +102,14 @@ func (of *OpenFIGI) SecurityTypeByISIN(ctx context.Context, isin string) (string
|
|||||||
|
|
||||||
// It is not possible that an isin is assign to diferent security types, therefore we can assume
|
// It is not possible that an isin is assign to diferent security types, therefore we can assume
|
||||||
// all entries have the same securityType value.
|
// all entries have the same securityType value.
|
||||||
return resBody[0].Data[0].SecurityType, nil
|
secType := resBody[0].Data[0].SecurityType
|
||||||
|
if secType == "" {
|
||||||
|
return "", fmt.Errorf("empty security type returned for ISIN: %s", isin)
|
||||||
|
}
|
||||||
|
|
||||||
|
of.securityTypeCache[isin] = secType
|
||||||
|
|
||||||
|
return secType, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type mappingRequestBody struct {
|
type mappingRequestBody struct {
|
||||||
|
|||||||
@@ -79,6 +79,18 @@ func TestOpenFIGI_SecurityTypeByISIN(t *testing.T) {
|
|||||||
isin: "NL0000235190",
|
isin: "NL0000235190",
|
||||||
wantErr: true,
|
wantErr: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "empty securityType",
|
||||||
|
client: NewTestClient(t, func(req *http.Request) (*http.Response, error) {
|
||||||
|
return &http.Response{
|
||||||
|
Status: http.StatusText(http.StatusOK),
|
||||||
|
StatusCode: http.StatusOK,
|
||||||
|
Body: io.NopCloser(bytes.NewBufferString(`[{"data":[{"securityType":""}]}]`)),
|
||||||
|
}, nil
|
||||||
|
}),
|
||||||
|
isin: "NL0000235190",
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "client error",
|
name: "client error",
|
||||||
client: NewTestClient(t, func(req *http.Request) (*http.Response, error) {
|
client: NewTestClient(t, func(req *http.Request) (*http.Response, error) {
|
||||||
@@ -118,6 +130,42 @@ func TestOpenFIGI_SecurityTypeByISIN(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestOpenFIGI_SecurityTypeByISIN_Cache(t *testing.T) {
|
||||||
|
var alreadyCalled bool
|
||||||
|
c := NewTestClient(t, func(req *http.Request) (*http.Response, error) {
|
||||||
|
if alreadyCalled {
|
||||||
|
t.Fatalf("want requests to be cached")
|
||||||
|
}
|
||||||
|
|
||||||
|
alreadyCalled = true
|
||||||
|
return &http.Response{
|
||||||
|
Status: http.StatusText(http.StatusOK),
|
||||||
|
StatusCode: http.StatusOK,
|
||||||
|
Body: io.NopCloser(bytes.NewBufferString(`[{"data":[{"securityType":"Common Stock"}]}]`)),
|
||||||
|
}, nil
|
||||||
|
})
|
||||||
|
|
||||||
|
of := internal.NewOpenFIGI(c)
|
||||||
|
|
||||||
|
got, gotErr := of.SecurityTypeByISIN(t.Context(), "NL0000235190")
|
||||||
|
if gotErr != nil {
|
||||||
|
t.Fatalf("want 1st success call but got error: %v", gotErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
if got != "Common Stock" {
|
||||||
|
t.Fatalf("want 1st securityType to be %q but got %q", "Common Stock", got)
|
||||||
|
}
|
||||||
|
|
||||||
|
got, gotErr = of.SecurityTypeByISIN(t.Context(), "NL0000235190")
|
||||||
|
if gotErr != nil {
|
||||||
|
t.Fatalf("want 2nd success call but got error: %v", gotErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
if got != "Common Stock" {
|
||||||
|
t.Fatalf("want 2nd securityType to be %q but got %q", "Common Stock", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type RoundTripFunc func(req *http.Request) (*http.Response, error)
|
type RoundTripFunc func(req *http.Request) (*http.Response, error)
|
||||||
|
|
||||||
func (f RoundTripFunc) RoundTrip(req *http.Request) (*http.Response, error) {
|
func (f RoundTripFunc) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||||
|
|||||||
Reference in New Issue
Block a user