mirror of
https://github.com/GopeedLab/gopeed.git
synced 2026-11-04 23:57:54 +00:00
197 lines
4.3 KiB
Go
197 lines
4.3 KiB
Go
package nativebridge
|
|
|
|
import (
|
|
"runtime"
|
|
"strconv"
|
|
"sync"
|
|
"sync/atomic"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestInvokeConcurrency(t *testing.T) {
|
|
tests := []struct {
|
|
cpus int
|
|
want int
|
|
}{
|
|
{cpus: 1, want: 4},
|
|
{cpus: 2, want: 4},
|
|
{cpus: 4, want: 8},
|
|
{cpus: 8, want: 16},
|
|
{cpus: 16, want: 32},
|
|
{cpus: 64, want: 32},
|
|
}
|
|
for _, test := range tests {
|
|
t.Run(strconv.Itoa(test.cpus), func(t *testing.T) {
|
|
if got := invokeConcurrency(test.cpus); got != test.want {
|
|
t.Fatalf("invokeConcurrency(%d) = %d, want %d", test.cpus, got, test.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestInvokeExecutorWaitsWithoutRejecting(t *testing.T) {
|
|
const (
|
|
workerCount = 2
|
|
taskCount = 6
|
|
)
|
|
release := make(chan struct{})
|
|
started := make(chan struct{}, taskCount)
|
|
var active atomic.Int32
|
|
var maxActive atomic.Int32
|
|
executor := newInvokeExecutor(workerCount, func(request invokeRequest) string {
|
|
current := active.Add(1)
|
|
for {
|
|
maximum := maxActive.Load()
|
|
if current <= maximum || maxActive.CompareAndSwap(maximum, current) {
|
|
break
|
|
}
|
|
}
|
|
started <- struct{}{}
|
|
<-release
|
|
active.Add(-1)
|
|
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("task %d was rejected while all workers were busy", i)
|
|
}
|
|
}
|
|
|
|
for range workerCount {
|
|
select {
|
|
case <-started:
|
|
case <-time.After(time.Second):
|
|
t.Fatal("workers did not start queued tasks")
|
|
}
|
|
}
|
|
if got := maxActive.Load(); got != workerCount {
|
|
t.Fatalf("maximum active tasks = %d, want %d", got, workerCount)
|
|
}
|
|
|
|
close(release)
|
|
completed.Wait()
|
|
executor.close()
|
|
}
|
|
|
|
func TestInvokeExecutorConvertsPanicsToErrors(t *testing.T) {
|
|
executor := newInvokeExecutor(1, 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(1, 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(1, 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()
|
|
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()
|
|
}
|