Compare commits

...
14 Commits
13 changed files with 296 additions and 251 deletions
+29 -13
View File
@@ -2,10 +2,10 @@ package gql
import ( import (
"crypto/tls" "crypto/tls"
"fmt"
"time" "time"
"github.com/allegro/bigcache" "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"
@@ -21,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 {
@@ -42,9 +41,7 @@ func NewConnection() *BaseClient {
b.retries.max = envutil.GetInt("GRAPHQL_RETRIES_MAX", 1) b.retries.max = envutil.GetInt("GRAPHQL_RETRIES_MAX", 1)
b.retries.delay = envutil.GetInt("GRAPHQL_RETRIES_DELAY", 300) b.retries.delay = envutil.GetInt("GRAPHQL_RETRIES_DELAY", 300)
if b.cache.enabled { b.enableCache()
b.enableCache()
}
b_tmp_log_level := envutil.Getenv("LOG_LEVEL", "info") b_tmp_log_level := envutil.Getenv("LOG_LEVEL", "info")
@@ -67,9 +64,20 @@ func NewConnection() *BaseClient {
}) })
var httpClient *http.Client var httpClient *http.Client
httpTransport := &http.Transport{
MaxIdleConns: 10,
IdleConnTimeout: 15 * time.Second,
ResponseHeaderTimeout: 15 * time.Second,
DisableKeepAlives: false,
}
if strings.HasPrefix(b.endpoint, "http://") { if strings.HasPrefix(b.endpoint, "http://") {
httpClient = &http.Client{ httpClient = &http.Client{
Transport: http.DefaultTransport, Transport: httpTransport,
CheckRedirect: func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
},
} }
} else { } else {
tlsClientConfig := &tls.Config{} tlsClientConfig := &tls.Config{}
@@ -79,6 +87,8 @@ func NewConnection() *BaseClient {
http2Transport := &http2.Transport{ http2Transport := &http2.Transport{
AllowHTTP: true, AllowHTTP: true,
TLSClientConfig: tlsClientConfig, TLSClientConfig: tlsClientConfig,
ReadIdleTimeout: 15 * time.Second,
PingTimeout: 15 * time.Second,
} }
httpClient = &http.Client{ httpClient = &http.Client{
Transport: http2Transport, Transport: http2Transport,
@@ -101,8 +111,14 @@ func (b *BaseClient) SetOutput(output string) {
func (b *BaseClient) enableCache() { func (b *BaseClient) enableCache() {
var err error var err error
b.cache.client, err = bigcache.NewBigCache(bigcache.DefaultConfig(time.Duration(b.cache.ttl) * time.Second)) b.cache.client = cache.New(time.Duration(b.cache.ttl) * time.Second * 2)
if err != nil { if err != nil {
b.Logger.Error(b, "Error while creating cache client;", "error", err.Error()) fmt.Println(">> Error while creating cache client;", "error", err.Error())
panic(err)
} }
} }
func (b *BaseClient) disableCache() {
b.Logger.Debug(b, "Disabling cache")
b.cache.enabled = false
}
+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)
}) })
} }
} }
+3 -3
View File
@@ -2,10 +2,10 @@ package gql
func (c *BaseClient) cacheLookup(hash string) []byte { func (c *BaseClient) cacheLookup(hash string) []byte {
if c.cache.client != nil { if c.cache.client != nil {
g, err := c.cache.client.Get(hash) obj, found := c.cache.client.Get(hash)
if err == nil { if found {
c.Logger.Debug(c, "Cache hit;", "hash", hash) c.Logger.Debug(c, "Cache hit;", "hash", hash)
return g return obj.([]byte)
} }
// if error is not nil it means that hash is not present in cache // if error is not nil it means that hash is not present in cache
c.Logger.Debug(c, "Cache miss;", "hash", hash) c.Logger.Debug(c, "Cache miss;", "hash", hash)
+10 -10
View File
@@ -3,27 +3,27 @@ module github.com/lukaszraczylo/go-simple-graphql
go 1.20 go 1.20
require ( require (
github.com/allegro/bigcache v1.2.1 github.com/akyoto/cache v1.0.6
github.com/avast/retry-go/v4 v4.3.3 github.com/avast/retry-go/v4 v4.5.0
github.com/gookit/goutil v0.6.8 github.com/gookit/goutil v0.6.12
github.com/json-iterator/go v1.1.12 github.com/json-iterator/go v1.1.12
github.com/stretchr/testify v1.8.2 github.com/stretchr/testify v1.8.4
golang.org/x/net v0.9.0 golang.org/x/net v0.15.0
) )
require ( require (
github.com/davecgh/go-spew v1.1.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect
github.com/gookit/color v1.5.3 // indirect github.com/gookit/color v1.5.4 // indirect
github.com/kr/pretty v0.3.0 // indirect github.com/kr/pretty v0.3.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
golang.org/x/exp v0.0.0-20221126150942-6ab00d035af9 // indirect golang.org/x/exp v0.0.0-20221126150942-6ab00d035af9 // indirect
golang.org/x/sync v0.1.0 // indirect golang.org/x/sync v0.3.0 // indirect
golang.org/x/sys v0.7.0 // indirect golang.org/x/sys v0.12.0 // indirect
golang.org/x/term v0.7.0 // indirect golang.org/x/term v0.12.0 // indirect
golang.org/x/text v0.9.0 // indirect golang.org/x/text v0.13.0 // indirect
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
) )
+20 -26
View File
@@ -1,16 +1,16 @@
github.com/allegro/bigcache v1.2.1 h1:hg1sY1raCwic3Vnsvje6TT7/pnZba83LeFck5NrFKSc= github.com/akyoto/cache v1.0.6 h1:5XGVVYoi2i+DZLLPuVIXtsNIJ/qaAM16XT0LaBaXd2k=
github.com/allegro/bigcache v1.2.1/go.mod h1:Cb/ax3seSYIx7SuZdm2G2xzfwmv3TPSk2ucNfQESPXM= github.com/akyoto/cache v1.0.6/go.mod h1:WfxTRqKhfgAG71Xh6E3WLpjhBtZI37O53G4h5s+3iM4=
github.com/avast/retry-go/v4 v4.3.3 h1:G56Bp6mU0b5HE1SkaoVjscZjlQb0oy4mezwY/cGH19w= github.com/avast/retry-go/v4 v4.5.0 h1:QoRAZZ90cj5oni2Lsgl2GW8mNTnUCnmpx/iKpwVisHg=
github.com/avast/retry-go/v4 v4.3.3/go.mod h1:rg6XFaiuFYII0Xu3RDbZQkxCofFwruZKW8oEF1jpWiU= github.com/avast/retry-go/v4 v4.5.0/go.mod h1:7hLEXp0oku2Nir2xBAsg0PTphp9z71bN5Aq1fboC3+I=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/gookit/color v1.5.3 h1:twfIhZs4QLCtimkP7MOxlF3A0U/5cDPseRT9M/+2SCE= github.com/gookit/color v1.5.4 h1:FZmqs7XOyGgCAxmWyPslpiok1k05wmY3SJTytgvYFs0=
github.com/gookit/color v1.5.3/go.mod h1:NUzwzeehUfl7GIb36pqId+UGmRfQcU/WiiyTTeNjHtE= github.com/gookit/color v1.5.4/go.mod h1:pZJOeOS8DM43rXbp4AZo1n9zCU2qjpcRko0b6/QJi9w=
github.com/gookit/goutil v0.6.8 h1:B2XXSCGav5TXWtKRT9i/s/owOLXXB7sY6UsfqeSLroE= github.com/gookit/goutil v0.6.12 h1:73vPUcTtVGXbhSzBOFcnSB1aJl7Jq9np3RAE50yIDZc=
github.com/gookit/goutil v0.6.8/go.mod h1:u+Isykc6RQcZ4GQzulsaGm+Famd97U5Tzp3aQyo+jyA= github.com/gookit/goutil v0.6.12/go.mod h1:g6krlFib8xSe3G1h02IETowOtrUGpAmetT8IevDpvpM=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
@@ -30,32 +30,26 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN
github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k= github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k=
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
golang.org/x/exp v0.0.0-20221126150942-6ab00d035af9 h1:yZNXmy+j/JpX19vZkVktWqAo7Gny4PBWYYK3zskGpx4= golang.org/x/exp v0.0.0-20221126150942-6ab00d035af9 h1:yZNXmy+j/JpX19vZkVktWqAo7Gny4PBWYYK3zskGpx4=
golang.org/x/exp v0.0.0-20221126150942-6ab00d035af9/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= golang.org/x/exp v0.0.0-20221126150942-6ab00d035af9/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
golang.org/x/net v0.9.0 h1:aWJ/m6xSmxWBx+V0XRHTlrYrPG56jKsLdTFmsSsCzOM= golang.org/x/net v0.15.0 h1:ugBLEUaxABaB5AJqW9enI0ACdci2RUd4eP51NTBvuJ8=
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU= golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o=
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.7.0 h1:BEvjmm5fURWqcfbSKTdpkDXYBrUS1c0m8agp14W48vQ= golang.org/x/term v0.12.0 h1:/ZfYdc3zq+q02Rv9vGqTeSItdzZTSNDmfTi0mBAuidU=
golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY= golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE= golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+39 -51
View File
@@ -3,12 +3,10 @@ package gql
import ( import (
"context" "context"
"fmt" "fmt"
"reflect"
"strconv" "strconv"
"github.com/lukaszraczylo/go-simple-graphql/utils/helpers"
"github.com/gookit/goutil/strutil" "github.com/gookit/goutil/strutil"
"github.com/lukaszraczylo/go-simple-graphql/utils/helpers"
jsoniter "github.com/json-iterator/go" jsoniter "github.com/json-iterator/go"
) )
@@ -70,61 +68,45 @@ func (c *BaseClient) NewQuery(q ...any) *Query {
// It looks a bit weird because of the backward compatibility // It looks a bit weird because of the backward compatibility
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)
parseQueryHeaders, enabledCache, headersModified, enabledRetries := compiledQuery.parseQueryHeaders(queryHeaders)
var should_cache, retries_enabled bool
var queryHash string var queryHash string
var cachedResponse []byte
cbc := reflect.ValueOf(*c).Interface().(BaseClient) if headersModified {
cacheBaseClient := &cbc should_cache = enabledCache
retries_enabled = enabledRetries
query := cacheBaseClient.NewQuery(queryContent, queryVariables) } else {
should_cache = c.cache.enabled
// Check for library specific headers retries_enabled = c.retries.enabled
if len(queryHeaders) > 0 {
queryHeadersModified := cacheBaseClient.parseQueryHeaders(queryHeaders)
// compare if there are any changes
if !reflect.DeepEqual(queryHeadersModified, queryHeaders) {
if cacheBaseClient.cache.enabled != c.cache.enabled && cacheBaseClient.cache.enabled {
cacheBaseClient.Logger.Debug(cacheBaseClient, "Switching cache on as per single-request header")
cacheBaseClient.enableCache()
}
}
} }
if cacheBaseClient.cache.enabled { // if queryContent does not start with `query` then we don't want to cache it
queryHash = strutil.Md5(fmt.Sprintf("%s-%+v", query.compiledQuery, queryHeaders)) // because it's probably a mutation or subscription.
cacheBaseClient.Logger.Debug(cacheBaseClient, "Hash calculated;", "hash:", queryHash) if !strutil.HasPrefix(queryContent, "query") {
should_cache = false
cachedResponse = c.cacheLookup(queryHash)
if cachedResponse != nil {
cacheBaseClient.Logger.Debug(cacheBaseClient, "Found cached response")
return cacheBaseClient.decodeResponse(cachedResponse), nil
} else {
cacheBaseClient.Logger.Debug(cacheBaseClient, "No cached response found")
}
} }
response, err := cacheBaseClient.executeQuery(query.compiledQuery, queryHeaders) if should_cache {
if err != nil { queryHash = strutil.Md5(fmt.Sprintf("%s-%+v", compiledQuery.compiledQuery, queryHeaders))
cacheBaseClient.Logger.Error(cacheBaseClient, "Error while executing query;", "error", err.Error())
return nil, err
} }
jsonData, err := json.Marshal(response) q := &queryExecutor{
if err != nil { client: c,
cacheBaseClient.Logger.Error(cacheBaseClient, "Error while converting to json;", "error", err.Error()) query: compiledQuery.compiledQuery,
return nil, err headers: parseQueryHeaders,
context: context.Background(),
should_cache: should_cache,
retries_enabled: retries_enabled,
hash: queryHash,
} }
if cacheBaseClient.cache.enabled && jsonData != nil && queryHash != "" { q.execute()
c.cache.client.Set(queryHash, jsonData) defer q.done()
} else if jsonData == nil {
cacheBaseClient.Logger.Warn(cacheBaseClient, "Response is empty")
} else if queryHash == "" {
cacheBaseClient.Logger.Warn(cacheBaseClient, "Query hash is empty")
}
return cacheBaseClient.decodeResponse(jsonData), err return q.result.data, q.result.errors
} }
func (c *BaseClient) decodeResponse(jsonData []byte) any { func (c *BaseClient) decodeResponse(jsonData []byte) any {
@@ -147,18 +129,24 @@ func (c *BaseClient) decodeResponse(jsonData []byte) any {
} }
} }
func (c *BaseClient) parseQueryHeaders(queryHeaders map[string]interface{}) (returnHeaders map[string]interface{}) { 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
for k, v := range queryHeaders { for k, v := range queryHeaders {
if k == "gqlcache" { if k == "gqlcache" {
c.cache.enabled, _ = strconv.ParseBool(fmt.Sprintf("%v", v)) cache_enabled, err = strconv.ParseBool(fmt.Sprintf("%v", v))
if err != nil {
panic(fmt.Sprintf("Unable to parse gqlcache value %s", err.Error()))
}
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 return returnHeaders, cache_enabled, headers_modified, retries_enabled
} }
+18 -4
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) {
@@ -298,10 +312,10 @@ func TestBaseClient_parseQueryHeaders(t *testing.T) {
queryHeaders map[string]interface{} queryHeaders map[string]interface{}
} }
tests := []struct { tests := []struct {
name string
fields fields fields fields
args args args args
wantReturnHeaders map[string]interface{} wantReturnHeaders map[string]interface{}
name string
}{ }{
{ {
name: "Test parseQueryHeaders - no change", name: "Test parseQueryHeaders - no change",
@@ -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)
} }
}) })
@@ -517,8 +531,8 @@ func TestBaseClient_QueryCacheRandomizedRaceViaHeader(t *testing.T) {
} }
for i := 0; i < 10; i++ { for i := 0; i < 10; i++ {
tt.args.queryHeaders["gqlcache"] = rand.Intn(2) == 0
go func() { go func() {
tt.args.queryHeaders["gqlcache"] = rand.Intn(2) == 0
_, err := c.Query(tt.args.queryContent, tt.args.queryVariables, tt.args.queryHeaders) _, err := c.Query(tt.args.queryContent, tt.args.queryVariables, tt.args.queryHeaders)
if !tt.wantErr { if !tt.wantErr {
assert.NoError(t, err) assert.NoError(t, err)
+50 -1
View File
@@ -3,6 +3,7 @@ package gql
import ( import (
"bytes" "bytes"
"fmt" "fmt"
"io"
"io/ioutil" "io/ioutil"
"net/http" "net/http"
"time" "time"
@@ -22,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(
@@ -31,6 +37,7 @@ func (c *BaseClient) executeQuery(query []byte, headers any) (result any, err er
c.Logger.Error(c, "Error while executing http request;", "error", err.Error()) c.Logger.Error(c, "Error while executing http request;", "error", err.Error())
return err return err
} }
defer io.Copy(io.Discard, httpResponse.Body) // equivalent to `cp body /dev/null`
defer httpResponse.Body.Close() defer httpResponse.Body.Close()
if httpResponse.StatusCode <= 200 && httpResponse.StatusCode >= 204 { if httpResponse.StatusCode <= 200 && httpResponse.StatusCode >= 204 {
@@ -54,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),
@@ -65,3 +72,45 @@ func (c *BaseClient) executeQuery(query []byte, headers any) (result any, err er
} }
return queryResult.Data, nil return queryResult.Data, nil
} }
func (q *queryExecutor) execute() {
if q.should_cache {
cachedResponse := q.client.cacheLookup(q.hash)
if cachedResponse != nil {
q.client.Logger.Debug(q.client, "Found cached response")
q.result.data = q.client.decodeResponse(cachedResponse)
return // return cached response
} else {
q.client.Logger.Debug(q.client, "No cached response found")
}
}
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)
if err != nil {
q.client.Logger.Error(q.client, "Error while executing query;", "error", err.Error())
q.result.errors = err
return
}
jsonData, err := json.Marshal(response)
if err != nil {
q.client.Logger.Error(q.client, "Error while converting to json;", "error", err.Error())
q.result.errors = err
return
}
if q.should_cache {
q.client.cache.client.Set(q.hash, jsonData, time.Duration(q.client.cache.ttl)*time.Second)
}
q.result.data = q.client.decodeResponse(jsonData)
}
func (q *queryExecutor) done() {
q = nil
}
+29 -16
View File
@@ -4,14 +4,12 @@ import (
"context" "context"
"net/http" "net/http"
"github.com/lukaszraczylo/go-simple-graphql/utils/concurrency" "github.com/akyoto/cache"
"github.com/lukaszraczylo/go-simple-graphql/utils/logger" "github.com/lukaszraczylo/go-simple-graphql/utils/logger"
"github.com/allegro/bigcache"
) )
type cacheStore struct { type cacheStore struct {
client *bigcache.BigCache client *cache.Cache
ttl int ttl int
enabled bool enabled bool
} }
@@ -23,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,6 +41,22 @@ type Query struct {
compiledQuery []byte `json:"compiledQuery"` compiledQuery []byte `json:"compiledQuery"`
} }
type queryExecutor struct {
result queryExecutorResult
context context.Context
client *BaseClient
headers map[string]interface{}
hash string
query []byte
should_cache bool
retries_enabled bool
}
type queryExecutorResult struct {
data interface{}
errors error
}
type request struct { type request struct {
Variables any `json:"variables"` Variables any `json:"variables"`
Query string `json:"query"` Query string `json:"query"`
-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...)
})
}
}
+10 -16
View File
@@ -1,16 +1,10 @@
package logger package logger
import ( import (
"encoding/json"
"reflect" "reflect"
"github.com/lukaszraczylo/go-simple-graphql/utils/helpers"
"github.com/lukaszraczylo/go-simple-graphql/utils/ordered_map"
jsoniter "github.com/json-iterator/go"
) )
var json = jsoniter.ConfigCompatibleWithStandardLibrary
const ( const (
reset = "\033[0m" reset = "\033[0m"
redBold = "\033[1;31m" redBold = "\033[1;31m"
@@ -34,25 +28,25 @@ type Config struct {
LogLevel LogLevel LogLevel LogLevel
} }
func Serialize(v any) (string, error) { func Serialize(v interface{}) ([]byte, error) {
marshal, err := json.Marshal(internalSerialize(v)) b, err := json.Marshal(internalSerialize(v))
if err != nil { if err != nil {
return "", err return nil, err
} }
return helpers.BytesToString(marshal), nil return b, nil
} }
func internalSerialize(v any) any { func internalSerialize(v interface{}) interface{} {
if v != nil { if v != nil {
switch reflect.TypeOf(v).Kind() { switch reflect.TypeOf(v).Kind() {
case reflect.Struct: case reflect.Struct:
r := ordered_map.New() r := make(map[string]interface{})
name := reflect.TypeOf(v).Name() name := reflect.TypeOf(v).Name()
if name != "" { if name != "" {
r.Set("_", reflect.TypeOf(v).String()) r["_"] = reflect.TypeOf(v).String()
} }
for i := 0; i < reflect.ValueOf(v).NumField(); i++ { for i := 0; i < reflect.ValueOf(v).NumField(); i++ {
r.Set(reflect.TypeOf(v).Field(i).Name, internalSerialize(reflect.ValueOf(v).Field(i).Interface())) r[reflect.TypeOf(v).Field(i).Name] = internalSerialize(reflect.ValueOf(v).Field(i).Interface())
} }
return r return r
case reflect.Ptr: case reflect.Ptr:
@@ -60,7 +54,7 @@ func internalSerialize(v any) any {
return internalSerialize(reflect.ValueOf(v).Elem().Interface()) return internalSerialize(reflect.ValueOf(v).Elem().Interface())
} }
case reflect.Slice: case reflect.Slice:
var r []any var r []interface{}
tmpSlice := reflect.ValueOf(v) tmpSlice := reflect.ValueOf(v)
for i := 0; i < tmpSlice.Len(); i++ { for i := 0; i < tmpSlice.Len(); i++ {
r = append(r, internalSerialize(tmpSlice.Index(i).Interface())) r = append(r, internalSerialize(tmpSlice.Index(i).Interface()))
+59 -2
View File
@@ -55,10 +55,67 @@ type customJsonEncoder struct {
} }
func (e *customJsonEncoder) Encode(v interface{}) error { func (e *customJsonEncoder) Encode(v interface{}) error {
data, err := json.Marshal(v) if v == nil {
e.buf.WriteString("null")
return nil
}
switch val := v.(type) {
case *OrderedMap:
data, err := json.Marshal(val.values)
if err != nil {
return err
}
e.buf.Write(data)
default:
data, err := json.Marshal(val)
if err != nil {
return err
}
e.buf.Write(data)
}
return nil
}
func (o OrderedMap) Keys() []string {
return o.keys
}
func (o OrderedMap) Values() []interface{} {
var values []interface{}
for _, k := range o.keys {
values = append(values, o.values[k])
}
return values
}
func (o *OrderedMap) Delete(key string) {
delete(o.values, key)
for i, k := range o.keys {
if k == key {
o.keys = append(o.keys[:i], o.keys[i+1:]...)
break
}
}
}
func (o OrderedMap) Exists(key string) bool {
_, exists := o.values[key]
return exists
}
func (o *OrderedMap) UnmarshalJSON(data []byte) error {
var values map[string]interface{}
err := json.Unmarshal(data, &values)
if err != nil { if err != nil {
return err return err
} }
e.buf.Write(data)
o.keys = []string{}
o.values = map[string]interface{}{}
for k, v := range values {
o.Set(k, v)
}
return nil return nil
} }