feat: 添加 Python→Go 全双工回调支持(call_go)
- 新增 WithHandlers 选项,通过反射将 Go 结构体方法暴露给 Python - 新增 callback/callback_result 消息类型,支持 Python 在处理中回调 Go - client 侧新增 readResult,内联处理 callback,复用同一连接避免死锁 - Python 侧新增 call_go[T]() 泛型调用,支持 dataclass 自动构造 - 注入 GOBRIDGE_WORKER_ID/WORKER_COUNT 环境变量,支持多 worker 初始化分工 - 新增示例演示 Go→Python→Go→Python 四层全双工链路 - Python 包版本升至 0.1.1
This commit is contained in:
161
pool.go
161
pool.go
@@ -2,9 +2,12 @@ package gobridge
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"reflect"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
)
|
||||
|
||||
@@ -19,6 +22,7 @@ type poolConfig struct {
|
||||
socketDir string
|
||||
stdout io.Writer
|
||||
stderr io.Writer
|
||||
handler any
|
||||
}
|
||||
|
||||
// Option 是 NewPool 的函数选项
|
||||
@@ -41,7 +45,6 @@ func WithPythonExe(exe string) Option {
|
||||
}
|
||||
|
||||
// WithScriptArgs 设置脚本路径之后的附加参数
|
||||
// uv 模式示例:WithScriptArgs("run") → 执行 uv run <script>
|
||||
func WithScriptArgs(args ...string) Option {
|
||||
return func(c *poolConfig) { c.scriptArgs = args }
|
||||
}
|
||||
@@ -52,7 +55,6 @@ func WithWorkDir(workDir string) Option {
|
||||
}
|
||||
|
||||
// WithEnv 设置附加环境变量,格式为 "KEY=VALUE"
|
||||
// 与当前进程环境合并,同名时以此处为准
|
||||
func WithEnv(env ...string) Option {
|
||||
return func(c *poolConfig) { c.env = env }
|
||||
}
|
||||
@@ -72,29 +74,55 @@ func WithStderr(w io.Writer) Option {
|
||||
return func(c *poolConfig) { c.stderr = w }
|
||||
}
|
||||
|
||||
// WithHandlers 注册 Go handler struct,其所有公开方法自动暴露给 Python 通过 call_go() 调用。
|
||||
//
|
||||
// type MyService struct{}
|
||||
// func (s *MyService) Multiply(ctx context.Context, a, b int) (int, error) { ... }
|
||||
//
|
||||
// pool, err := gobridge.NewPool("worker.py", gobridge.WithHandlers(&MyService{}))
|
||||
// // Python: call_go("Multiply", 3, 4)
|
||||
func WithHandlers(h any) Option {
|
||||
return func(c *poolConfig) { c.handler = h }
|
||||
}
|
||||
|
||||
// ── Pool 接口 ────────────────────────────────────────────────────────────────
|
||||
|
||||
// Pool 是 Python worker 进程池的接口
|
||||
type Pool interface {
|
||||
// Close 关闭所有 worker 进程和连接
|
||||
Close()
|
||||
acquire(ctx context.Context) (net.Conn, *worker, error)
|
||||
nextReqID() uint64
|
||||
callbackDispatch(ctx context.Context, msg Message) (any, string)
|
||||
}
|
||||
|
||||
// pool 是 Pool 的具体实现
|
||||
// ── goHandler ────────────────────────────────────────────────────────────────
|
||||
|
||||
type goHandler struct {
|
||||
fn reflect.Value
|
||||
hasCtx bool
|
||||
inTypes []reflect.Type
|
||||
outType reflect.Type
|
||||
hasErr bool
|
||||
}
|
||||
|
||||
|
||||
// ── pool(内部实现)─────────────────────────────────────────────────────────
|
||||
|
||||
type pool struct {
|
||||
workers []*worker
|
||||
idx atomic.Uint64
|
||||
reqID atomic.Uint64
|
||||
workers []*worker
|
||||
idx atomic.Uint64
|
||||
reqID atomic.Uint64
|
||||
mu sync.RWMutex
|
||||
handlers map[string]goHandler
|
||||
}
|
||||
|
||||
// NewPool 创建并启动进程池
|
||||
// NewPool 创建并启动进程池。
|
||||
//
|
||||
// pool, err := gobridge.NewPool("worker.py")
|
||||
// pool, err := gobridge.NewPool("worker.py", gobridge.WithWorkers(4))
|
||||
// pool, err := gobridge.NewPool("run",
|
||||
// gobridge.WithPythonExe("uv"),
|
||||
// gobridge.WithScriptArgs("worker.py"),
|
||||
// gobridge.WithWorkDir("./worker"),
|
||||
// pool, err := gobridge.NewPool("worker.py",
|
||||
// gobridge.WithHandlers(&MyService{}),
|
||||
// gobridge.WithWorkers(2),
|
||||
// )
|
||||
func NewPool(script string, opts ...Option) (Pool, error) {
|
||||
cfg := &poolConfig{
|
||||
@@ -107,7 +135,7 @@ func NewPool(script string, opts ...Option) (Pool, error) {
|
||||
o(cfg)
|
||||
}
|
||||
if script == "" {
|
||||
return nil, fmt.Errorf("NewPool: script must not be empty")
|
||||
return nil, fmt.Errorf("gobridge: script must not be empty")
|
||||
}
|
||||
cfg.scriptArgs = append([]string{script}, cfg.scriptArgs...)
|
||||
|
||||
@@ -115,17 +143,72 @@ func NewPool(script string, opts ...Option) (Pool, error) {
|
||||
for i := range workers {
|
||||
w, err := newWorker(cfg, i)
|
||||
if err != nil {
|
||||
for j := 0; j < i; j++ {
|
||||
for j := range i {
|
||||
workers[j].stop()
|
||||
}
|
||||
return nil, fmt.Errorf("create worker %d: %w", i, err)
|
||||
}
|
||||
workers[i] = w
|
||||
}
|
||||
return &pool{workers: workers}, nil
|
||||
|
||||
p := &pool{workers: workers, handlers: make(map[string]goHandler)}
|
||||
|
||||
if cfg.handler != nil {
|
||||
p.bindHandlers(cfg.handler)
|
||||
}
|
||||
|
||||
return p, nil
|
||||
}
|
||||
|
||||
func (p *pool) bindHandlers(h any) {
|
||||
rv := reflect.ValueOf(h)
|
||||
rt := rv.Type()
|
||||
for i := range rt.NumMethod() {
|
||||
m := rt.Method(i)
|
||||
if !m.IsExported() {
|
||||
continue
|
||||
}
|
||||
p.bindHandler(m.Name, rv.Method(i))
|
||||
}
|
||||
}
|
||||
|
||||
func (p *pool) bindHandler(name string, fn reflect.Value) {
|
||||
errType := reflect.TypeFor[error]()
|
||||
ctxType := reflect.TypeFor[context.Context]()
|
||||
|
||||
ft := fn.Type()
|
||||
h := goHandler{fn: fn}
|
||||
|
||||
startIdx := 0
|
||||
if ft.NumIn() > 0 && ft.In(0).Implements(ctxType) {
|
||||
h.hasCtx = true
|
||||
startIdx = 1
|
||||
}
|
||||
for i := startIdx; i < ft.NumIn(); i++ {
|
||||
h.inTypes = append(h.inTypes, ft.In(i))
|
||||
}
|
||||
|
||||
switch ft.NumOut() {
|
||||
case 0:
|
||||
case 1:
|
||||
if ft.Out(0).Implements(errType) {
|
||||
h.hasErr = true
|
||||
} else {
|
||||
h.outType = ft.Out(0)
|
||||
}
|
||||
case 2:
|
||||
if !ft.Out(1).Implements(errType) {
|
||||
panic(fmt.Sprintf("gobridge: handler %s: second return value must implement error", name))
|
||||
}
|
||||
h.outType = ft.Out(0)
|
||||
h.hasErr = true
|
||||
default:
|
||||
panic(fmt.Sprintf("gobridge: handler %s: must return at most (value, error)", name))
|
||||
}
|
||||
|
||||
p.handlers[name] = h
|
||||
}
|
||||
|
||||
// acquire 以轮询方式从进程池取出一个可用连接
|
||||
func (p *pool) acquire(ctx context.Context) (net.Conn, *worker, error) {
|
||||
idx := p.idx.Add(1) % uint64(len(p.workers))
|
||||
w := p.workers[idx]
|
||||
@@ -137,7 +220,51 @@ func (p *pool) nextReqID() uint64 {
|
||||
return p.reqID.Add(1)
|
||||
}
|
||||
|
||||
// Close 关闭所有 worker 进程和连接
|
||||
func (p *pool) callbackDispatch(ctx context.Context, msg Message) (any, string) {
|
||||
p.mu.RLock()
|
||||
h, ok := p.handlers[msg.Method]
|
||||
p.mu.RUnlock()
|
||||
if !ok {
|
||||
return nil, fmt.Sprintf("unknown go handler: %s", msg.Method)
|
||||
}
|
||||
|
||||
var rawArgs []json.RawMessage
|
||||
if len(msg.Args) > 0 {
|
||||
if err := json.Unmarshal(msg.Args, &rawArgs); err != nil {
|
||||
return nil, fmt.Sprintf("unmarshal args: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
if len(rawArgs) != len(h.inTypes) {
|
||||
return nil, fmt.Sprintf("arg count mismatch: want %d got %d", len(h.inTypes), len(rawArgs))
|
||||
}
|
||||
|
||||
in := make([]reflect.Value, 0, len(h.inTypes)+1)
|
||||
if h.hasCtx {
|
||||
in = append(in, reflect.ValueOf(ctx))
|
||||
}
|
||||
for i, t := range h.inTypes {
|
||||
v := reflect.New(t)
|
||||
if err := json.Unmarshal(rawArgs[i], v.Interface()); err != nil {
|
||||
return nil, fmt.Sprintf("unmarshal arg %d: %v", i, err)
|
||||
}
|
||||
in = append(in, v.Elem())
|
||||
}
|
||||
|
||||
out := h.fn.Call(in)
|
||||
|
||||
if h.hasErr {
|
||||
errVal := out[len(out)-1]
|
||||
if !errVal.IsNil() {
|
||||
return nil, errVal.Interface().(error).Error()
|
||||
}
|
||||
}
|
||||
if h.outType == nil || len(out) == 0 {
|
||||
return nil, ""
|
||||
}
|
||||
return out[0].Interface(), ""
|
||||
}
|
||||
|
||||
func (p *pool) Close() {
|
||||
for _, w := range p.workers {
|
||||
w.stop()
|
||||
|
||||
Reference in New Issue
Block a user