Files
gopeed/bind/native/invoke_test.go

259 lines
6.1 KiB
Go

package nativebridge
import (
"runtime"
"strconv"
"sync"
"testing"
"time"
)
func TestInvokeExecutorStartsEveryRequestIndependently(t *testing.T) {
const taskCount = 64
release := make(chan struct{})
started := make(chan struct{}, taskCount)
executor := newInvokeExecutor(func(request invokeRequest) string {
started <- struct{}{}
<-release
return request.path
})
var completed sync.WaitGroup
completed.Add(taskCount)
for i := range taskCount {
accepted := executor.submit(invokeTask{
request: invokeRequest{path: strconv.Itoa(i)},
complete: func(string, error) {
completed.Done()
},
})
if !accepted {
t.Fatalf("request %d was rejected", i)
}
}
// Every handler must start before any of them is released. A worker pool
// would stall here once all of its workers were occupied.
for range taskCount {
select {
case <-started:
case <-time.After(time.Second):
t.Fatal("a request waited behind other blocked requests")
}
}
close(release)
completed.Wait()
executor.close()
}
func TestInvokeExecutorConvertsPanicsToErrors(t *testing.T) {
executor := newInvokeExecutor(func(invokeRequest) string {
panic("boom")
})
completed := make(chan error, 1)
executor.submit(invokeTask{complete: func(_ string, err error) {
completed <- err
}})
select {
case err := <-completed:
if err == nil || err.Error() != "invoke panic: boom" {
t.Fatalf("unexpected error: %v", err)
}
case <-time.After(time.Second):
t.Fatal("panic result was not completed")
}
executor.close()
}
func TestInvokeExecutorSurvivesCallbackPanic(t *testing.T) {
executor := newInvokeExecutor(func(request invokeRequest) string {
return request.path
})
executor.submit(invokeTask{complete: func(string, error) {
panic("callback failed")
}})
completed := make(chan string, 1)
executor.submit(invokeTask{
request: invokeRequest{path: "next"},
complete: func(result string, _ error) {
completed <- result
},
})
select {
case result := <-completed:
if result != "next" {
t.Fatalf("unexpected result: %q", result)
}
case <-time.After(time.Second):
t.Fatal("worker stopped after callback panic")
}
executor.close()
}
func TestInvokeExecutorPausesAndDrains(t *testing.T) {
release := make(chan struct{})
started := make(chan struct{})
executor := newInvokeExecutor(func(request invokeRequest) string {
if request.path == "running" {
close(started)
<-release
}
return request.path
})
completed := make(chan struct{})
if !executor.submit(invokeTask{
request: invokeRequest{path: "running"},
complete: func(string, error) {
close(completed)
},
}) {
t.Fatal("initial task was rejected")
}
<-started
drained := make(chan struct{})
go func() {
executor.pauseAndWait(time.Second)
close(drained)
}()
deadline := time.Now().Add(time.Second)
for {
executor.mu.Lock()
paused := !executor.accepting
executor.mu.Unlock()
if paused {
break
}
if time.Now().After(deadline) {
t.Fatal("executor did not pause")
}
runtime.Gosched()
}
select {
case <-drained:
t.Fatal("pause returned before the running request completed")
default:
}
if executor.submit(invokeTask{}) {
t.Fatal("paused executor accepted a new request")
}
close(release)
<-completed
select {
case <-drained:
case <-time.After(time.Second):
t.Fatal("pause did not return after pending requests drained")
}
executor.resume()
resumed := make(chan struct{})
if !executor.submit(invokeTask{complete: func(string, error) {
close(resumed)
}}) {
t.Fatal("resumed executor rejected a request")
}
<-resumed
executor.close()
}
func TestInvokeExecutorPauseTimesOutAndFailsPendingRequests(t *testing.T) {
release := make(chan struct{})
started := make(chan struct{})
executor := newInvokeExecutor(func(request invokeRequest) string {
started <- struct{}{}
<-release
return request.path
})
errors := make(chan error, 2)
for _, path := range []string{"first", "second"} {
if !executor.submit(invokeTask{
request: invokeRequest{path: path},
complete: func(_ string, err error) {
errors <- err
},
}) {
t.Fatalf("request %q was rejected", path)
}
}
for range 2 {
<-started
}
const timeout = 30 * time.Millisecond
startedAt := time.Now()
if executor.pauseAndWait(timeout) {
t.Fatal("pause unexpectedly reported a complete drain")
}
if elapsed := time.Since(startedAt); elapsed < timeout || elapsed > 10*timeout {
t.Fatalf("pause returned after %s, want close to %s", elapsed, timeout)
}
for range 2 {
select {
case err := <-errors:
if err == nil || err.Error() != errNativeBridgeStopping.Error() {
t.Fatalf("unexpected timeout error: %v", err)
}
case <-time.After(time.Second):
t.Fatal("pending request was not failed after the timeout")
}
}
close(release)
executor.close()
select {
case err := <-errors:
t.Fatalf("request callback ran more than once: %v", err)
default:
}
}
func TestStopWithinUsesOneDeadlineForRequestsAndCleanup(t *testing.T) {
releaseRequest := make(chan struct{})
requestStarted := make(chan struct{})
executor := newInvokeExecutor(func(invokeRequest) string {
close(requestStarted)
<-releaseRequest
return "done"
})
requestResult := make(chan error, 1)
executor.submit(invokeTask{complete: func(_ string, err error) {
requestResult <- err
}})
<-requestStarted
releaseCleanup := make(chan struct{})
cleanupStarted := make(chan struct{})
cleanup := func() {
close(cleanupStarted)
<-releaseCleanup
}
const timeout = 40 * time.Millisecond
startedAt := time.Now()
stopWithin(executor, cleanup, timeout)
if elapsed := time.Since(startedAt); elapsed < timeout || elapsed > 10*timeout {
t.Fatalf("stop returned after %s, want close to %s", elapsed, timeout)
}
select {
case <-cleanupStarted:
case <-time.After(time.Second):
t.Fatal("cleanup was not started after the request deadline")
}
select {
case err := <-requestResult:
if err == nil || err.Error() != errNativeBridgeStopping.Error() {
t.Fatalf("unexpected request result: %v", err)
}
case <-time.After(time.Second):
t.Fatal("timed-out request was not failed")
}
close(releaseRequest)
close(releaseCleanup)
executor.close()
}