mirror of
https://github.com/d0zingcat/gocryptotrader.git
synced 2026-06-04 15:10:54 +00:00
(Engine) Bugfix: Unlocking an unlocked mutex PANIC + Increase dispatcher job capacity via commandline (#371)
* Removes lock unlock timer and instead sets unlocks between getting a nonce and sending a payload. Increases dispatch channel buffer to deal with len(enabledCurrencies) > ~100 * Adds additional comments to help explain the situation * Fixes bug that could unlock mutex too early * Fixes LIES where Gemini gets a nonce and then proceeds to declare it doesn't get a nonce causing an unrecoverable lock * Fun new concept! The creation of a tested timed mutex. Unlocking an unlocked mutex cannot occur and response can be checked to verify whether the mutex was unlocked from timeout or command. * Adds new cmd parameter "dispatchjobbuffer" * Expands comments and renames benchmark. Makes `Timer` property private * Happy little linters * Renames jobBuffer and all related instances to jobs limit * Tiny error message update * Grammatical fix and setting dispatch.Start to use defaults
This commit is contained in:
@@ -397,7 +397,7 @@ func (g *Gemini) SendAuthenticatedHTTPRequest(method, path string, params map[st
|
||||
nil,
|
||||
result,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
g.Verbose,
|
||||
g.HTTPDebugging,
|
||||
g.HTTPRecording)
|
||||
|
||||
@@ -15,7 +15,7 @@ import (
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
err := dispatch.Start(1)
|
||||
err := dispatch.Start(1, dispatch.DefaultJobsLimit)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -11,80 +11,15 @@ import (
|
||||
"net/http/httputil"
|
||||
"net/url"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/thrasher-corp/gocryptotrader/common"
|
||||
"github.com/thrasher-corp/gocryptotrader/common/timedmutex"
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/mock"
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/nonce"
|
||||
log "github.com/thrasher-corp/gocryptotrader/logger"
|
||||
)
|
||||
|
||||
var supportedMethods = []string{http.MethodGet, http.MethodPost, http.MethodHead,
|
||||
http.MethodPut, http.MethodDelete, http.MethodOptions, http.MethodConnect}
|
||||
|
||||
// Const vars for rate limiter
|
||||
const (
|
||||
DefaultMaxRequestJobs = 50
|
||||
DefaultTimeoutRetryAttempts = 3
|
||||
|
||||
proxyTLSTimeout = 15 * time.Second
|
||||
)
|
||||
|
||||
// Vars for rate limiter
|
||||
var (
|
||||
MaxRequestJobs = DefaultMaxRequestJobs
|
||||
TimeoutRetryAttempts = DefaultTimeoutRetryAttempts
|
||||
DisableRateLimiter bool
|
||||
)
|
||||
|
||||
// Requester struct for the request client
|
||||
type Requester struct {
|
||||
HTTPClient *http.Client
|
||||
UnauthLimit *RateLimit
|
||||
AuthLimit *RateLimit
|
||||
Name string
|
||||
UserAgent string
|
||||
Cycle time.Time
|
||||
timeoutRetryAttempts int
|
||||
m sync.Mutex
|
||||
Jobs chan Job
|
||||
disengage chan struct{}
|
||||
WorkerStarted bool
|
||||
Nonce nonce.Nonce
|
||||
fifoLock sync.Mutex
|
||||
DisableRateLimiter bool
|
||||
}
|
||||
|
||||
// RateLimit struct
|
||||
type RateLimit struct {
|
||||
Duration time.Duration
|
||||
Rate int
|
||||
Requests int
|
||||
Mutex sync.Mutex
|
||||
}
|
||||
|
||||
// JobResult holds a request job result
|
||||
type JobResult struct {
|
||||
Error error
|
||||
Result interface{}
|
||||
}
|
||||
|
||||
// Job holds a request job
|
||||
type Job struct {
|
||||
Request *http.Request
|
||||
Method string
|
||||
Path string
|
||||
Headers map[string]string
|
||||
Body io.Reader
|
||||
Result interface{}
|
||||
JobResult chan *JobResult
|
||||
AuthRequest bool
|
||||
Verbose bool
|
||||
HTTPDebugging bool
|
||||
Record bool
|
||||
}
|
||||
|
||||
// NewRateLimit creates a new RateLimit
|
||||
func NewRateLimit(d time.Duration, rate int) *RateLimit {
|
||||
return &RateLimit{Duration: d, Rate: rate}
|
||||
@@ -237,8 +172,8 @@ func New(name string, authLimit, unauthLimit *RateLimit, httpRequester *http.Cli
|
||||
AuthLimit: authLimit,
|
||||
Name: name,
|
||||
Jobs: make(chan Job, MaxRequestJobs),
|
||||
disengage: make(chan struct{}, 1),
|
||||
timeoutRetryAttempts: TimeoutRetryAttempts,
|
||||
timedLock: timedmutex.NewTimedMutex(DefaultMutexLockTimeout),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -443,27 +378,27 @@ func (r *Requester) worker() {
|
||||
// SendPayload handles sending HTTP/HTTPS requests
|
||||
func (r *Requester) SendPayload(method, path string, headers map[string]string, body io.Reader, result interface{}, authRequest, nonceEnabled, verbose, httpDebugging, record bool) error {
|
||||
if !nonceEnabled {
|
||||
r.lock()
|
||||
r.timedLock.LockForDuration()
|
||||
}
|
||||
|
||||
if r == nil || r.Name == "" {
|
||||
r.unlock()
|
||||
r.timedLock.UnlockIfLocked()
|
||||
return errors.New("not initiliased, SetDefaults() called before making request?")
|
||||
}
|
||||
|
||||
if !IsValidMethod(method) {
|
||||
r.unlock()
|
||||
r.timedLock.UnlockIfLocked()
|
||||
return fmt.Errorf("incorrect method supplied %s: supported %s", method, supportedMethods)
|
||||
}
|
||||
|
||||
if path == "" {
|
||||
r.unlock()
|
||||
r.timedLock.UnlockIfLocked()
|
||||
return errors.New("invalid path")
|
||||
}
|
||||
|
||||
req, err := r.checkRequest(method, path, body, headers)
|
||||
if err != nil {
|
||||
r.unlock()
|
||||
r.timedLock.UnlockIfLocked()
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -478,12 +413,12 @@ func (r *Requester) SendPayload(method, path string, headers map[string]string,
|
||||
}
|
||||
|
||||
if !r.RequiresRateLimiter() {
|
||||
r.unlock()
|
||||
r.timedLock.UnlockIfLocked()
|
||||
return r.DoRequest(req, path, body, result, authRequest, verbose, httpDebugging, record)
|
||||
}
|
||||
|
||||
if len(r.Jobs) == MaxRequestJobs {
|
||||
r.unlock()
|
||||
r.timedLock.UnlockIfLocked()
|
||||
return errors.New("max request jobs reached")
|
||||
}
|
||||
|
||||
@@ -515,7 +450,7 @@ func (r *Requester) SendPayload(method, path string, headers map[string]string,
|
||||
log.Debugf(log.ExchangeSys, "%s request. Attaching new job.", r.Name)
|
||||
}
|
||||
r.Jobs <- newJob
|
||||
r.unlock()
|
||||
r.timedLock.UnlockIfLocked()
|
||||
|
||||
if verbose {
|
||||
log.Debugf(log.ExchangeSys, "%s request. Waiting for job to complete.", r.Name)
|
||||
@@ -532,7 +467,7 @@ func (r *Requester) SendPayload(method, path string, headers map[string]string,
|
||||
// GetNonce returns a nonce for requests. This locks and enforces concurrent
|
||||
// nonce FIFO on the buffered job channel
|
||||
func (r *Requester) GetNonce(isNano bool) nonce.Value {
|
||||
r.lock()
|
||||
r.timedLock.LockForDuration()
|
||||
if r.Nonce.Get() == 0 {
|
||||
if isNano {
|
||||
r.Nonce.Set(time.Now().UnixNano())
|
||||
@@ -548,7 +483,7 @@ func (r *Requester) GetNonce(isNano bool) nonce.Value {
|
||||
// GetNonceMilli returns a nonce for requests. This locks and enforces concurrent
|
||||
// nonce FIFO on the buffered job channel this is for millisecond
|
||||
func (r *Requester) GetNonceMilli() nonce.Value {
|
||||
r.lock()
|
||||
r.timedLock.LockForDuration()
|
||||
if r.Nonce.Get() == 0 {
|
||||
r.Nonce.Set(time.Now().UnixNano() / int64(time.Millisecond))
|
||||
return r.Nonce.Get()
|
||||
@@ -569,33 +504,3 @@ func (r *Requester) SetProxy(p *url.URL) error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// lock locks and sets up an issue timer, if something errors out of scope it
|
||||
// automatically unlocks
|
||||
func (r *Requester) lock() {
|
||||
if r.disengage == nil {
|
||||
r.disengage = make(chan struct{}, 1)
|
||||
}
|
||||
var wg sync.WaitGroup
|
||||
r.fifoLock.Lock()
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
timer := time.NewTimer(50 * time.Millisecond)
|
||||
wg.Done()
|
||||
select {
|
||||
case <-timer.C:
|
||||
log.Errorf(log.ExchangeSys, "Unlocking due to possible error for %s", r.Name)
|
||||
r.fifoLock.Unlock()
|
||||
|
||||
case <-r.disengage:
|
||||
return
|
||||
}
|
||||
}()
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
// unlock unlocks mtx and shuts down a timer
|
||||
func (r *Requester) unlock() {
|
||||
r.disengage <- struct{}{}
|
||||
r.fifoLock.Unlock()
|
||||
}
|
||||
|
||||
@@ -199,16 +199,9 @@ func TestCheckRequest(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestDoRequest(t *testing.T) {
|
||||
var test = new(Requester)
|
||||
err := test.SendPayload(http.MethodGet, "https://www.google.com", nil, nil, nil, false, false, true, false, false)
|
||||
if err == nil {
|
||||
t.Fatal("Expected error")
|
||||
}
|
||||
|
||||
r := New("", NewRateLimit(time.Second*10, 5), NewRateLimit(time.Second*20, 100), new(http.Client))
|
||||
|
||||
r.Name = "bitfinex"
|
||||
err = r.SendPayload("BLAH", "https://www.google.com", nil, nil, nil, false, false, true, false, false)
|
||||
err := r.SendPayload("BLAH", "https://www.google.com", nil, nil, nil, false, false, true, false, false)
|
||||
if err == nil {
|
||||
t.Fatal("Expected error")
|
||||
}
|
||||
@@ -321,7 +314,7 @@ func TestDoRequest(t *testing.T) {
|
||||
}
|
||||
|
||||
func BenchmarkRequestLockMech(b *testing.B) {
|
||||
var r = new(Requester)
|
||||
r := New("", NewRateLimit(time.Second*10, 5), NewRateLimit(time.Second*20, 100), new(http.Client))
|
||||
var meep interface{}
|
||||
for n := 0; n < b.N; n++ {
|
||||
r.SendPayload(http.MethodGet, "127.0.0.1", nil, nil, &meep, false, false, false, false, false)
|
||||
|
||||
75
exchanges/request/request_types.go
Normal file
75
exchanges/request/request_types.go
Normal file
@@ -0,0 +1,75 @@
|
||||
package request
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/thrasher-corp/gocryptotrader/common/timedmutex"
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/nonce"
|
||||
)
|
||||
|
||||
var supportedMethods = []string{http.MethodGet, http.MethodPost, http.MethodHead,
|
||||
http.MethodPut, http.MethodDelete, http.MethodOptions, http.MethodConnect}
|
||||
|
||||
// Const vars for rate limiter
|
||||
const (
|
||||
DefaultMaxRequestJobs = 50
|
||||
DefaultTimeoutRetryAttempts = 3
|
||||
DefaultMutexLockTimeout = 50 * time.Millisecond
|
||||
proxyTLSTimeout = 15 * time.Second
|
||||
)
|
||||
|
||||
// Vars for rate limiter
|
||||
var (
|
||||
MaxRequestJobs = DefaultMaxRequestJobs
|
||||
TimeoutRetryAttempts = DefaultTimeoutRetryAttempts
|
||||
DisableRateLimiter bool
|
||||
)
|
||||
|
||||
// Requester struct for the request client
|
||||
type Requester struct {
|
||||
HTTPClient *http.Client
|
||||
UnauthLimit *RateLimit
|
||||
AuthLimit *RateLimit
|
||||
Name string
|
||||
UserAgent string
|
||||
Cycle time.Time
|
||||
timeoutRetryAttempts int
|
||||
m sync.Mutex
|
||||
Jobs chan Job
|
||||
WorkerStarted bool
|
||||
Nonce nonce.Nonce
|
||||
DisableRateLimiter bool
|
||||
timedLock *timedmutex.TimedMutex
|
||||
}
|
||||
|
||||
// RateLimit struct
|
||||
type RateLimit struct {
|
||||
Duration time.Duration
|
||||
Rate int
|
||||
Requests int
|
||||
Mutex sync.Mutex
|
||||
}
|
||||
|
||||
// JobResult holds a request job result
|
||||
type JobResult struct {
|
||||
Error error
|
||||
Result interface{}
|
||||
}
|
||||
|
||||
// Job holds a request job
|
||||
type Job struct {
|
||||
Request *http.Request
|
||||
Method string
|
||||
Path string
|
||||
Headers map[string]string
|
||||
Body io.Reader
|
||||
Result interface{}
|
||||
JobResult chan *JobResult
|
||||
AuthRequest bool
|
||||
Verbose bool
|
||||
HTTPDebugging bool
|
||||
Record bool
|
||||
}
|
||||
@@ -15,7 +15,7 @@ import (
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
err := dispatch.Start(1)
|
||||
err := dispatch.Start(1, dispatch.DefaultJobsLimit)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user