a8f0a04c3d
* Common race condition is fixed by identifying that Client.respHandler can be completely removed since all respHandler operations (get, put and invocation) can be moved into the Client.processLoop goroutine, meaning that zero locking is required. This race condition resulted in a deadlock that was resolved by the response timeout at the end of client.Do, returning ErrLostConn * Rare race condition is fixed by changing responseHandlerMap.get to .getAndRemove. This race condition resulted in the innerHandler for a new dtJobCreated assigned in client.Do overriding a stale older dtJobCreated request, and the newer innerHandler being removed by an older dtJobCreated in client.processLoop > client.handleInner. When the newer dtJobCreated response was received, the handler for it had already been deleted. This was resolved by the response timeout at the end of client.Do, returning ErrLostConn
207 lines
4.3 KiB
Go
207 lines
4.3 KiB
Go
package client
|
|
|
|
import (
|
|
"errors"
|
|
"flag"
|
|
"os"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
const (
|
|
TestStr = "Hello world"
|
|
)
|
|
|
|
var (
|
|
client *Client
|
|
runIntegrationTests bool
|
|
)
|
|
|
|
func TestMain(m *testing.M) {
|
|
integrationsTestFlag := flag.Bool("integration", false, "Run the integration tests (in addition to the unit tests)")
|
|
flag.Parse()
|
|
if integrationsTestFlag != nil {
|
|
runIntegrationTests = *integrationsTestFlag
|
|
}
|
|
code := m.Run()
|
|
os.Exit(code)
|
|
}
|
|
|
|
func TestClientAddServer(t *testing.T) {
|
|
if !runIntegrationTests {
|
|
t.Skip("To run this test, use: go test -integration")
|
|
}
|
|
t.Log("Add local server 127.0.0.1:4730")
|
|
var err error
|
|
if client, err = New(Network, "127.0.0.1:4730"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
client.ErrorHandler = func(e error) {
|
|
t.Log(e)
|
|
}
|
|
}
|
|
|
|
func TestClientEcho(t *testing.T) {
|
|
if !runIntegrationTests {
|
|
t.Skip("To run this test, use: go test -integration")
|
|
}
|
|
echo, err := client.Echo([]byte(TestStr))
|
|
if err != nil {
|
|
t.Error(err)
|
|
return
|
|
}
|
|
if string(echo) != TestStr {
|
|
t.Errorf("Echo error, %s expected, %s got", TestStr, echo)
|
|
return
|
|
}
|
|
}
|
|
|
|
func TestClientDoBg(t *testing.T) {
|
|
if !runIntegrationTests {
|
|
t.Skip("To run this test, use: go test -integration")
|
|
}
|
|
handle, err := client.DoBg("ToUpper", []byte("abcdef"), JobLow)
|
|
if err != nil {
|
|
t.Error(err)
|
|
return
|
|
}
|
|
if handle == "" {
|
|
t.Error("Handle is empty.")
|
|
} else {
|
|
t.Log(handle)
|
|
}
|
|
}
|
|
|
|
func TestClientDo(t *testing.T) {
|
|
if !runIntegrationTests {
|
|
t.Skip("To run this test, use: go test -integration")
|
|
}
|
|
jobHandler := func(job *Response) {
|
|
str := string(job.Data)
|
|
if str == "ABCDEF" {
|
|
t.Log(str)
|
|
} else {
|
|
t.Errorf("Invalid data: %s", job.Data)
|
|
}
|
|
return
|
|
}
|
|
handle, err := client.Do("ToUpper", []byte("abcdef"),
|
|
JobLow, jobHandler)
|
|
if err != nil {
|
|
t.Error(err)
|
|
return
|
|
}
|
|
if handle == "" {
|
|
t.Error("Handle is empty.")
|
|
} else {
|
|
t.Log(handle)
|
|
}
|
|
}
|
|
|
|
func TestClientMultiDo(t *testing.T) {
|
|
if !runIntegrationTests {
|
|
t.Skip("To run this test, use: go test -integration")
|
|
}
|
|
|
|
// This integration test requires that examples/pl/worker_multi.pl be running.
|
|
//
|
|
// Test invocation is:
|
|
// go test -integration -timeout 10s -run '^TestClient(AddServer|MultiDo)$'
|
|
//
|
|
// Send 1000 requests to go through all race conditions
|
|
const nreqs = 1000
|
|
errCh := make(chan error)
|
|
gotCh := make(chan string, nreqs)
|
|
|
|
olderrh := client.ErrorHandler
|
|
client.ErrorHandler = func(e error) { errCh <- e }
|
|
client.ResponseTimeout = 5 * time.Second
|
|
defer func() { client.ErrorHandler = olderrh }()
|
|
|
|
nextJobCh := make(chan struct{})
|
|
defer close(nextJobCh)
|
|
go func() {
|
|
for range nextJobCh {
|
|
start := time.Now()
|
|
handle, err := client.Do("PerlToUpper", []byte("abcdef"), JobNormal, func(r *Response) { gotCh <- string(r.Data) })
|
|
if err == ErrLostConn && time.Since(start) > client.ResponseTimeout {
|
|
errCh <- errors.New("Impossible 'lost conn', deadlock bug detected")
|
|
} else if err != nil {
|
|
errCh <- err
|
|
}
|
|
if handle == "" {
|
|
errCh <- errors.New("Handle is empty.")
|
|
}
|
|
}
|
|
}()
|
|
|
|
for i := 0; i < nreqs; i++ {
|
|
select {
|
|
case err := <-errCh:
|
|
t.Fatal(err)
|
|
case nextJobCh <- struct{}{}:
|
|
}
|
|
}
|
|
|
|
remaining := nreqs
|
|
for remaining > 0 {
|
|
select {
|
|
case err := <-errCh:
|
|
t.Fatal(err)
|
|
case got := <-gotCh:
|
|
if got != "ABCDEF" {
|
|
t.Error("Unexpected response from PerlDoUpper: ", got)
|
|
}
|
|
remaining--
|
|
t.Logf("%d response remaining", remaining)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestClientStatus(t *testing.T) {
|
|
if !runIntegrationTests {
|
|
t.Skip("To run this test, use: go test -integration")
|
|
}
|
|
status, err := client.Status("handle not exists")
|
|
if err != nil {
|
|
t.Error(err)
|
|
return
|
|
}
|
|
if status.Known {
|
|
t.Errorf("The job (%s) shouldn't be known.", status.Handle)
|
|
return
|
|
}
|
|
if status.Running {
|
|
t.Errorf("The job (%s) shouldn't be running.", status.Handle)
|
|
return
|
|
}
|
|
|
|
handle, err := client.Do("Delay5sec", []byte("abcdef"), JobLow, nil)
|
|
if err != nil {
|
|
t.Error(err)
|
|
return
|
|
}
|
|
status, err = client.Status(handle)
|
|
if err != nil {
|
|
t.Error(err)
|
|
return
|
|
}
|
|
if !status.Known {
|
|
t.Errorf("The job (%s) should be known.", status.Handle)
|
|
return
|
|
}
|
|
if status.Running {
|
|
t.Errorf("The job (%s) shouldn't be running.", status.Handle)
|
|
return
|
|
}
|
|
}
|
|
|
|
func TestClientClose(t *testing.T) {
|
|
if !runIntegrationTests {
|
|
t.Skip("To run this test, use: go test -integration")
|
|
}
|
|
if err := client.Close(); err != nil {
|
|
t.Error(err)
|
|
}
|
|
}
|