mirror of
https://github.com/GopeedLab/gopeed.git
synced 2026-11-04 15:49:49 +00:00
259 lines
6.1 KiB
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()
|
|
}
|