Compare commits

...
11 Commits
Author SHA1 Message Date
lukaszraczylo 21c9119bcb Add management of the retries via headers. 2023-04-10 03:12:39 +01:00
lukaszraczylo a7b385f0b7 Do not cache mutations. 2023-04-10 01:46:25 +01:00
lukaszraczylo 7d3abf5464 Replace allegro/bigcache with something lighter.
In high frequency environment the allegro bigcache caused significant
memory usage overhead. This commit replaces it with something much lighter.
2023-04-08 21:36:11 +01:00
lukaszraczylo 7af4e1f96a Memleak fix 2023-04-08 20:45:05 +01:00
lukaszraczylo 7623410bfe fixup! fixup! ... 2023-04-08 06:20:11 +01:00
lukaszraczylo 8e26d79df7 fixup! ... 2023-04-08 05:58:11 +01:00
lukaszraczylo aa25c43795 ... 2023-04-08 05:44:23 +01:00
lukaszraczylo dfb082ad51 fixup! fixup! fixup! Avoid unnecessary copying of the whole struct. 2023-04-08 05:19:57 +01:00
lukaszraczylo ce736958c6 fixup! fixup! Avoid unnecessary copying of the whole struct. 2023-04-08 05:18:30 +01:00
lukaszraczylo 4472041b21 fixup! Avoid unnecessary copying of the whole struct. 2023-04-08 05:17:09 +01:00
lukaszraczylo b81e612f51 Avoid unnecessary copying of the whole struct. 2023-04-08 05:14:14 +01:00
13 changed files with 269 additions and 218 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)
+1 -1
View File
@@ -3,7 +3,7 @@ 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.3.3
github.com/gookit/goutil v0.6.8 github.com/gookit/goutil v0.6.8
github.com/json-iterator/go v1.1.12 github.com/json-iterator/go v1.1.12
+2 -2
View File
@@ -1,5 +1,5 @@
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.3.3 h1:G56Bp6mU0b5HE1SkaoVjscZjlQb0oy4mezwY/cGH19w=
github.com/avast/retry-go/v4 v4.3.3/go.mod h1:rg6XFaiuFYII0Xu3RDbZQkxCofFwruZKW8oEF1jpWiU= github.com/avast/retry-go/v4 v4.3.3/go.mod h1:rg6XFaiuFYII0Xu3RDbZQkxCofFwruZKW8oEF1jpWiU=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
+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")
c.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 cacheBaseClient.cache.enabled && jsonData == nil {
cacheBaseClient.Logger.Warn(cacheBaseClient, "Response is empty")
} else if cacheBaseClient.cache.enabled && 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
} }