mirror of
https://github.com/GopeedLab/gopeed.git
synced 2026-11-14 12:23:43 +00:00
refactor: share async invoke bridge across native platforms
This commit is contained in:
1 parent
d12122ea86
commit
607d1729e6
17 files changed
+464
-222
No files matched your search
@@ -1,123 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"runtime"
|
||||
"sync"
|
||||
)
|
||||
|
||||
const (
|
||||
minInvokeConcurrency = 4
|
||||
maxInvokeConcurrency = 32
|
||||
)
|
||||
|
||||
type invokeRequest struct {
|
||||
method string
|
||||
path string
|
||||
query string
|
||||
body string
|
||||
}
|
||||
|
||||
type invokeTask struct {
|
||||
request invokeRequest
|
||||
complete func(result string, err error)
|
||||
}
|
||||
|
||||
// invokeExecutor keeps native API work concurrent without ever rejecting a
|
||||
// request because all workers are busy. submit only appends to the waiting
|
||||
// queue; callers observe backpressure through the Future completed by the
|
||||
// callback rather than by blocking the FFI entry point.
|
||||
type invokeExecutor struct {
|
||||
mu sync.Mutex
|
||||
ready *sync.Cond
|
||||
queue []invokeTask
|
||||
closed bool
|
||||
workers sync.WaitGroup
|
||||
handler func(invokeRequest) string
|
||||
}
|
||||
|
||||
func invokeConcurrency(cpuCount int) int {
|
||||
concurrency := cpuCount * 2
|
||||
if concurrency < minInvokeConcurrency {
|
||||
return minInvokeConcurrency
|
||||
}
|
||||
if concurrency > maxInvokeConcurrency {
|
||||
return maxInvokeConcurrency
|
||||
}
|
||||
return concurrency
|
||||
}
|
||||
|
||||
func newInvokeExecutor(workerCount int, handler func(invokeRequest) string) *invokeExecutor {
|
||||
if workerCount < 1 {
|
||||
workerCount = 1
|
||||
}
|
||||
executor := &invokeExecutor{handler: handler}
|
||||
executor.ready = sync.NewCond(&executor.mu)
|
||||
executor.workers.Add(workerCount)
|
||||
for range workerCount {
|
||||
go executor.run()
|
||||
}
|
||||
return executor
|
||||
}
|
||||
|
||||
func newDefaultInvokeExecutor(handler func(invokeRequest) string) *invokeExecutor {
|
||||
return newInvokeExecutor(invokeConcurrency(runtime.GOMAXPROCS(0)), handler)
|
||||
}
|
||||
|
||||
func (e *invokeExecutor) submit(task invokeTask) bool {
|
||||
e.mu.Lock()
|
||||
defer e.mu.Unlock()
|
||||
if e.closed {
|
||||
return false
|
||||
}
|
||||
e.queue = append(e.queue, task)
|
||||
e.ready.Signal()
|
||||
return true
|
||||
}
|
||||
|
||||
func (e *invokeExecutor) close() {
|
||||
e.mu.Lock()
|
||||
e.closed = true
|
||||
e.ready.Broadcast()
|
||||
e.mu.Unlock()
|
||||
e.workers.Wait()
|
||||
}
|
||||
|
||||
func (e *invokeExecutor) run() {
|
||||
defer e.workers.Done()
|
||||
for {
|
||||
task, ok := e.take()
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
result, err := e.execute(task.request)
|
||||
task.complete(result, err)
|
||||
}
|
||||
}
|
||||
|
||||
func (e *invokeExecutor) take() (invokeTask, bool) {
|
||||
e.mu.Lock()
|
||||
defer e.mu.Unlock()
|
||||
for len(e.queue) == 0 && !e.closed {
|
||||
e.ready.Wait()
|
||||
}
|
||||
if len(e.queue) == 0 {
|
||||
return invokeTask{}, false
|
||||
}
|
||||
task := e.queue[0]
|
||||
e.queue[0] = invokeTask{}
|
||||
e.queue = e.queue[1:]
|
||||
if len(e.queue) == 0 {
|
||||
e.queue = nil
|
||||
}
|
||||
return task, true
|
||||
}
|
||||
|
||||
func (e *invokeExecutor) execute(request invokeRequest) (result string, err error) {
|
||||
defer func() {
|
||||
if recovered := recover(); recovered != nil {
|
||||
err = fmt.Errorf("invoke panic: %v", recovered)
|
||||
}
|
||||
}()
|
||||
return e.handler(request), nil
|
||||
}
|
||||
@@ -1,102 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"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()
|
||||
}
|
||||
+10
-34
@@ -20,29 +20,15 @@ import "C"
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sync"
|
||||
"unsafe"
|
||||
|
||||
nativebridge "github.com/GopeedLab/gopeed/bind/native"
|
||||
"github.com/GopeedLab/gopeed/pkg/rest"
|
||||
"github.com/GopeedLab/gopeed/pkg/rest/model"
|
||||
)
|
||||
|
||||
func main() {}
|
||||
|
||||
var (
|
||||
invokeExecutorOnce sync.Once
|
||||
desktopInvoker *invokeExecutor
|
||||
)
|
||||
|
||||
func getInvokeExecutor() *invokeExecutor {
|
||||
invokeExecutorOnce.Do(func() {
|
||||
desktopInvoker = newDefaultInvokeExecutor(func(request invokeRequest) string {
|
||||
return rest.Invoke(request.method, request.path, request.query, request.body)
|
||||
})
|
||||
})
|
||||
return desktopInvoker
|
||||
}
|
||||
|
||||
//export Start
|
||||
func Start(cfg *C.char) (int, *C.char) {
|
||||
var config model.StartConfig
|
||||
@@ -56,11 +42,13 @@ func Start(cfg *C.char) (int, *C.char) {
|
||||
if err != nil {
|
||||
return 0, C.CString(err.Error())
|
||||
}
|
||||
nativebridge.ResumeInvokes()
|
||||
return realPort, nil
|
||||
}
|
||||
|
||||
//export Stop
|
||||
func Stop() {
|
||||
nativebridge.PauseInvokesAndWait()
|
||||
rest.Stop()
|
||||
}
|
||||
|
||||
@@ -96,16 +84,6 @@ func apiServerResult(state *model.APIServerState, err error) string {
|
||||
return string(data)
|
||||
}
|
||||
|
||||
//export Invoke
|
||||
func Invoke(method *C.char, path *C.char, query *C.char, body *C.char) *C.char {
|
||||
return C.CString(rest.Invoke(
|
||||
C.GoString(method),
|
||||
C.GoString(path),
|
||||
C.GoString(query),
|
||||
C.GoString(body),
|
||||
))
|
||||
}
|
||||
|
||||
//export InvokeAsync
|
||||
func InvokeAsync(
|
||||
method *C.char,
|
||||
@@ -118,12 +96,6 @@ func InvokeAsync(
|
||||
if callback == 0 {
|
||||
return
|
||||
}
|
||||
request := invokeRequest{
|
||||
method: C.GoString(method),
|
||||
path: C.GoString(path),
|
||||
query: C.GoString(query),
|
||||
body: C.GoString(body),
|
||||
}
|
||||
complete := func(result string, err error) {
|
||||
success := C.int(1)
|
||||
payload := result
|
||||
@@ -138,9 +110,13 @@ func InvokeAsync(
|
||||
C.CString(payload),
|
||||
)
|
||||
}
|
||||
if !getInvokeExecutor().submit(invokeTask{request: request, complete: complete}) {
|
||||
complete("", fmt.Errorf("invoke executor is closed"))
|
||||
}
|
||||
nativebridge.InvokeAsync(
|
||||
C.GoString(method),
|
||||
C.GoString(path),
|
||||
C.GoString(query),
|
||||
C.GoString(body),
|
||||
complete,
|
||||
)
|
||||
}
|
||||
|
||||
//export SubscribeTaskEvents
|
||||
|
||||
Reference in new issue
Block a user