Add management of the retries via headers.

This commit is contained in:
2023-04-10 03:12:39 +01:00
parent a7b385f0b7
commit 21c9119bcb
8 changed files with 95 additions and 153 deletions
+4 -6
View File
@@ -6,7 +6,6 @@ import (
"time" "time"
"github.com/akyoto/cache" "github.com/akyoto/cache"
"github.com/lukaszraczylo/go-simple-graphql/utils/concurrency"
"github.com/lukaszraczylo/go-simple-graphql/utils/logger" "github.com/lukaszraczylo/go-simple-graphql/utils/logger"
"golang.org/x/net/http2" "golang.org/x/net/http2"
@@ -22,11 +21,10 @@ import (
func NewConnection() *BaseClient { func NewConnection() *BaseClient {
b := &BaseClient{ b := &BaseClient{
client: &http.Client{}, client: &http.Client{},
concurrencyManager: concurrency.NewPool(-1), MaxGoRoutines: -1,
MaxGoRoutines: -1, LoggingLevel: logger.Warn,
LoggingLevel: logger.Warn, LoggerColorful: true,
LoggerColorful: true,
} }
if b.LoggerWriter == nil { if b.LoggerWriter == nil {
+29 -5
View File
@@ -10,16 +10,32 @@ import (
func TestNewConnection(t *testing.T) { func TestNewConnection(t *testing.T) {
tests := []struct { tests := []struct {
want *BaseClient want *BaseClient
name string cache bool
endpoint string
output string
name string
}{ }{
{ {
name: "Test NewConnection", name: "Test NewConnection https",
endpoint: "https://api.github.com/graphql",
output: "string",
cache: true,
want: &BaseClient{ want: &BaseClient{
endpoint: "https://api.github.com/graphql", endpoint: "https://api.github.com/graphql",
responseType: "string", responseType: "string",
}, },
}, },
{
name: "Test NewConnection http",
endpoint: "http://api.github.com/graphql",
output: "mapstring",
cache: false,
want: &BaseClient{
endpoint: "http://api.github.com/graphql",
responseType: "mapstring",
},
},
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
@@ -27,11 +43,19 @@ func TestNewConnection(t *testing.T) {
// and GRAPHQL_OUTPUT to string // and GRAPHQL_OUTPUT to string
current := os.Getenv("GRAPHQL_ENDPOINT") current := os.Getenv("GRAPHQL_ENDPOINT")
defer os.Setenv("GRAPHQL_ENDPOINT", current) defer os.Setenv("GRAPHQL_ENDPOINT", current)
os.Setenv("GRAPHQL_ENDPOINT", "https://api.github.com/graphql") os.Setenv("GRAPHQL_ENDPOINT", tt.endpoint)
got := NewConnection() got := NewConnection()
got.SetEndpoint(tt.endpoint)
got.SetOutput(tt.output)
if tt.cache {
got.enableCache()
} else {
got.disableCache()
}
assert.Equal(t, tt.want.endpoint, got.endpoint) assert.Equal(t, tt.want.endpoint, got.endpoint)
assert.Equal(t, tt.want.responseType, got.responseType) assert.Equal(t, tt.want.responseType, got.responseType)
assert.Equal(t, tt.cache, got.cache.enabled)
}) })
} }
} }
+16 -15
View File
@@ -70,15 +70,17 @@ func (c *BaseClient) NewQuery(q ...any) *Query {
func (c *BaseClient) Query(queryContent string, queryVariables interface{}, queryHeaders map[string]interface{}) (any, error) { func (c *BaseClient) Query(queryContent string, queryVariables interface{}, queryHeaders map[string]interface{}) (any, error) {
compiledQuery := c.NewQuery(queryContent, queryVariables) compiledQuery := c.NewQuery(queryContent, queryVariables)
parseQueryHeaders, enabledCache, headersModified := c.parseQueryHeaders(queryHeaders) parseQueryHeaders, enabledCache, headersModified, enabledRetries := compiledQuery.parseQueryHeaders(queryHeaders)
var should_cache bool var should_cache, retries_enabled bool
var queryHash string var queryHash string
if headersModified { if headersModified {
should_cache = enabledCache should_cache = enabledCache
retries_enabled = enabledRetries
} else { } else {
should_cache = c.cache.enabled should_cache = c.cache.enabled
retries_enabled = c.retries.enabled
} }
// if queryContent does not start with `query` then we don't want to cache it // if queryContent does not start with `query` then we don't want to cache it
@@ -92,12 +94,13 @@ func (c *BaseClient) Query(queryContent string, queryVariables interface{}, quer
} }
q := &queryExecutor{ q := &queryExecutor{
client: c, client: c,
query: compiledQuery.compiledQuery, query: compiledQuery.compiledQuery,
headers: parseQueryHeaders, headers: parseQueryHeaders,
context: context.Background(), context: context.Background(),
should_cache: should_cache, should_cache: should_cache,
hash: queryHash, retries_enabled: retries_enabled,
hash: queryHash,
} }
q.execute() q.execute()
@@ -126,7 +129,7 @@ func (c *BaseClient) decodeResponse(jsonData []byte) any {
} }
} }
func (c *BaseClient) parseQueryHeaders(queryHeaders map[string]interface{}) (returnHeaders map[string]interface{}, cache_enabled bool, headers_modified bool) { func (q *Query) parseQueryHeaders(queryHeaders map[string]interface{}) (returnHeaders map[string]interface{}, cache_enabled bool, headers_modified bool, retries_enabled bool) {
returnHeaders = make(map[string]interface{}) returnHeaders = make(map[string]interface{})
var err error var err error
@@ -134,18 +137,16 @@ func (c *BaseClient) parseQueryHeaders(queryHeaders map[string]interface{}) (ret
if k == "gqlcache" { if k == "gqlcache" {
cache_enabled, err = strconv.ParseBool(fmt.Sprintf("%v", v)) cache_enabled, err = strconv.ParseBool(fmt.Sprintf("%v", v))
if err != nil { if err != nil {
c.Logger.Error(c, "Can't parse gqlcache header", "error", err.Error()) panic(fmt.Sprintf("Unable to parse gqlcache value %s", err.Error()))
}
if cache_enabled != c.cache.enabled {
headers_modified = true
} }
headers_modified = true
continue continue
} }
if k == "gqlretries" { if k == "gqlretries" {
c.retries.enabled, _ = strconv.ParseBool(fmt.Sprintf("%v", v)) retries_enabled, _ = strconv.ParseBool(fmt.Sprintf("%v", v))
continue continue
} }
returnHeaders[k] = v returnHeaders[k] = v
} }
return returnHeaders, cache_enabled, headers_modified return returnHeaders, cache_enabled, headers_modified, retries_enabled
} }
+16 -2
View File
@@ -134,6 +134,20 @@ func TestBaseClient_decodeResponse(t *testing.T) {
}, },
want: []byte(`{"data":{"hello":"world"}}`), want: []byte(`{"data":{"hello":"world"}}`),
}, },
{
name: "Test decodeResponse - mapstring",
fields: fields{
responseType: "mapstring",
},
args: args{
jsonData: []byte(`{"data":{"hello":"world"}}`),
},
want: map[string]interface{}{
"data": map[string]interface{}{
"hello": "world",
},
},
},
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
@@ -333,8 +347,8 @@ func TestBaseClient_parseQueryHeaders(t *testing.T) {
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
c := NewConnection() q := &Query{}
if gotReturnHeaders, _, _ := c.parseQueryHeaders(tt.args.queryHeaders); !reflect.DeepEqual(gotReturnHeaders, tt.wantReturnHeaders) { if gotReturnHeaders, _, _, _ := q.parseQueryHeaders(tt.args.queryHeaders); !reflect.DeepEqual(gotReturnHeaders, tt.wantReturnHeaders) {
t.Errorf("BaseClient.parseQueryHeaders() = %v, want %v", gotReturnHeaders, tt.wantReturnHeaders) t.Errorf("BaseClient.parseQueryHeaders() = %v, want %v", gotReturnHeaders, tt.wantReturnHeaders)
} }
}) })
+11 -1
View File
@@ -23,6 +23,11 @@ func (c *BaseClient) executeQuery(query []byte, headers any) (result any, err er
httpRequest.Header.Add(key, fmt.Sprintf("%s", value)) httpRequest.Header.Add(key, fmt.Sprintf("%s", value))
} }
var retries_available = c.retries.max
if !c.retries.enabled {
retries_available = 1
}
var httpResponse *http.Response var httpResponse *http.Response
err = retry.Do( err = retry.Do(
@@ -56,7 +61,7 @@ func (c *BaseClient) executeQuery(query []byte, headers any) (result any, err er
retry.OnRetry(func(n uint, err error) { retry.OnRetry(func(n uint, err error) {
c.Logger.Error(c, "Retrying query", "error", err.Error()) c.Logger.Error(c, "Retrying query", "error", err.Error())
}), }),
retry.Attempts(uint(c.retries.max)), retry.Attempts(uint(retries_available)),
retry.DelayType(retry.BackOffDelay), retry.DelayType(retry.BackOffDelay),
retry.Delay(time.Duration(c.retries.delay)*time.Second), retry.Delay(time.Duration(c.retries.delay)*time.Second),
retry.LastErrorOnly(true), retry.LastErrorOnly(true),
@@ -80,6 +85,11 @@ func (q *queryExecutor) execute() {
} }
} }
if q.retries_enabled {
q.client.Logger.Debug(q.client, "Retries enabled")
q.client.retries.enabled = true
}
response, err := q.client.executeQuery(q.query, q.headers) response, err := q.client.executeQuery(q.query, q.headers)
if err != nil { if err != nil {
q.client.Logger.Error(q.client, "Error while executing query;", "error", err.Error()) q.client.Logger.Error(q.client, "Error while executing query;", "error", err.Error())
+19 -20
View File
@@ -5,7 +5,6 @@ import (
"net/http" "net/http"
"github.com/akyoto/cache" "github.com/akyoto/cache"
"github.com/lukaszraczylo/go-simple-graphql/utils/concurrency"
"github.com/lukaszraczylo/go-simple-graphql/utils/logger" "github.com/lukaszraczylo/go-simple-graphql/utils/logger"
) )
@@ -22,18 +21,17 @@ type retriesConfig struct {
} }
type BaseClient struct { type BaseClient struct {
Logger Logger Logger Logger
LoggerWriter Writer LoggerWriter Writer
client *http.Client client *http.Client
concurrencyManager *concurrency.Pool endpoint string
endpoint string responseType string
responseType string cache cacheStore
cache cacheStore MaxGoRoutines int
MaxGoRoutines int LoggingLevel logger.LogLevel
LoggingLevel logger.LogLevel LoggerColorful bool
LoggerColorful bool validate bool
validate bool retries retriesConfig
retries retriesConfig
} }
type Query struct { type Query struct {
@@ -44,13 +42,14 @@ type Query struct {
} }
type queryExecutor struct { type queryExecutor struct {
result queryExecutorResult result queryExecutorResult
context context.Context context context.Context
client *BaseClient client *BaseClient
headers map[string]interface{} headers map[string]interface{}
hash string hash string
query []byte query []byte
should_cache bool should_cache bool
retries_enabled bool
} }
type queryExecutorResult struct { type queryExecutorResult struct {
-30
View File
@@ -1,30 +0,0 @@
package concurrency
type Pool struct {
queue chan struct{}
jobs int
}
func NewPool(size int) *Pool {
if size < 0 {
size = 0
}
return &Pool{
jobs: size,
queue: make(chan struct{}, size),
}
}
func (p *Pool) Enqueue(job func(params ...any), params ...any) {
if p.jobs == 0 {
go job(params...)
return
}
p.queue <- struct{}{}
go func() {
defer func() {
<-p.queue
}()
job(params...)
}()
}
-74
View File
@@ -1,74 +0,0 @@
package concurrency
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestNewPool(t *testing.T) {
type args struct {
size int
}
tests := []struct {
want *Pool
name string
args args
}{
{
name: "TestNewPool",
args: args{
size: 0,
},
want: &Pool{
jobs: 0,
queue: make(chan struct{}, 0),
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := NewPool(tt.args.size)
assert.Equal(t, got.jobs, tt.want.jobs)
assert.Equal(t, cap(got.queue), cap(tt.want.queue))
})
}
}
func TestPool_Enqueue(t *testing.T) {
type fields struct {
queue chan struct{}
jobs int
}
type args struct {
job func(params ...any)
params []any
}
tests := []struct {
name string
fields fields
args args
}{
{
name: "TestPool_Enqueue",
fields: fields{
queue: make(chan struct{}, 0),
jobs: 0,
},
args: args{
job: func(params ...any) {
return
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
p := &Pool{
queue: tt.fields.queue,
jobs: tt.fields.jobs,
}
p.Enqueue(tt.args.job, tt.args.params...)
})
}
}