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:
2026-04-14 13:06:50 +08:00
parent 07e9239ac5
commit b390effd8e
8 changed files with 490 additions and 54 deletions

View File

@@ -53,14 +53,15 @@ func Invoke[R any](ctx context.Context, pool Pool, method string, args ...any) (
// - ctx 取消时先发送 cancel 消息Python 侧收到后注入 InterruptedError
// - 再关闭连接,解除阻塞中的读写操作
//
// write 是调用方提供的互斥写函数,保证与其他写操作不并发。
// 返回 stop 函数,必须在 conn 归还连接池前调用,可安全多次调用。
func watchCtx(ctx context.Context, conn net.Conn, id uint64) (stop func()) {
func watchCtx(ctx context.Context, conn net.Conn, id uint64, write func(Message)) (stop func()) {
done := make(chan struct{})
var once sync.Once
go func() {
select {
case <-ctx.Done():
writeMsg(conn, Message{ID: id, Type: TypeCancel}) //nolint
write(Message{ID: id, Type: TypeCancel})
conn.Close()
case <-done:
}
@@ -89,6 +90,30 @@ func contextErr(ctx context.Context, err error) error {
return err
}
// readResult 读取下一条非 callback 消息,期间内联处理所有 Python→Go 回调。
// 保证 py→go→py→go→... 全链路复用同一条连接,不产生额外线程。
// write 是调用方提供的互斥写函数,与 watchCtx 共享同一把锁,避免并发写。
func readResult(ctx context.Context, conn net.Conn, pool Pool, write func(Message)) (Message, error) {
for {
msg, err := readMsg(conn)
if err != nil {
return Message{}, err
}
if msg.Type != TypeCallback {
return msg, nil
}
result, errStr := pool.callbackDispatch(ctx, msg)
var resp Message
if errStr != "" {
resp = Message{ID: msg.ID, Type: TypeError, Error: errStr}
} else {
data, _ := json.Marshal(result)
resp = Message{ID: msg.ID, Type: TypeCallbackResult, Data: data}
}
write(resp)
}
}
func invokeRegular[R any](ctx context.Context, pool Pool, method string, args ...any) (R, error) {
var zero R
@@ -102,21 +127,22 @@ func invokeRegular[R any](ctx context.Context, pool Pool, method string, args ..
return zero, err
}
var mu sync.Mutex
write := func(msg Message) { mu.Lock(); writeMsg(conn, msg); mu.Unlock() } //nolint
id := pool.nextReqID()
stop := watchCtx(ctx, conn, id)
stop := watchCtx(ctx, conn, id, write)
defer stop()
if err := writeMsg(conn, Message{
ID: id,
Type: TypeCall,
Method: method,
Args: argsJSON,
}); err != nil {
mu.Lock()
err = writeMsg(conn, Message{ID: id, Type: TypeCall, Method: method, Args: argsJSON})
mu.Unlock()
if err != nil {
w.release(conn, false)
return zero, contextErr(ctx, fmt.Errorf("write call: %w", err))
}
resp, err := readMsg(conn)
resp, err := readResult(ctx, conn, pool, write)
if err != nil {
w.release(conn, false)
return zero, contextErr(ctx, fmt.Errorf("read response: %w", err))
@@ -163,14 +189,16 @@ func invokeStreamOut[R any](ctx context.Context, pool Pool, method string, rt re
ch := reflect.MakeChan(rt, 64)
go func() {
stop := watchCtx(ctx, conn, id)
var mu sync.Mutex
write := func(msg Message) { mu.Lock(); writeMsg(conn, msg); mu.Unlock() } //nolint
stop := watchCtx(ctx, conn, id, write)
defer func() {
stop()
ch.Close()
w.release(conn, ctx.Err() == nil)
}()
for {
msg, err := readMsg(conn)
msg, err := readResult(ctx, conn, pool, write)
if err != nil || msg.Type == TypeEnd || msg.Type == TypeError {
return
}
@@ -204,11 +232,19 @@ func invokeStreamIn[R any](ctx context.Context, pool Pool, method string, stream
return zero, err
}
var mu sync.Mutex
writeErr := func(msg Message) error {
mu.Lock()
defer mu.Unlock()
return writeMsg(conn, msg)
}
write := func(msg Message) { writeErr(msg) } //nolint
id := pool.nextReqID()
stop := watchCtx(ctx, conn, id)
stop := watchCtx(ctx, conn, id, write)
defer stop()
if err := writeMsg(conn, Message{
if err := writeErr(Message{
ID: id,
Type: TypeCall,
Method: method,
@@ -234,18 +270,18 @@ func invokeStreamIn[R any](ctx context.Context, pool Pool, method string, stream
w.release(conn, false)
return zero, fmt.Errorf("marshal chunk: %w", err)
}
if err := writeMsg(conn, Message{ID: id, Type: TypeChunk, Data: chunkData}); err != nil {
if err := writeErr(Message{ID: id, Type: TypeChunk, Data: chunkData}); err != nil {
w.release(conn, false)
return zero, contextErr(ctx, fmt.Errorf("write chunk: %w", err))
}
}
if err := writeMsg(conn, Message{ID: id, Type: TypeEnd}); err != nil {
if err := writeErr(Message{ID: id, Type: TypeEnd}); err != nil {
w.release(conn, false)
return zero, contextErr(ctx, fmt.Errorf("write end: %w", err))
}
resp, err := readMsg(conn)
resp, err := readResult(ctx, conn, pool, write)
if err != nil {
w.release(conn, false)
return zero, contextErr(ctx, fmt.Errorf("read response: %w", err))
@@ -297,6 +333,9 @@ func invokeStreamBoth[R any](ctx context.Context, pool Pool, method string, stre
outCh := reflect.MakeChan(rt, 64)
var mu sync.Mutex
write := func(msg Message) { mu.Lock(); writeMsg(conn, msg); mu.Unlock() } //nolint
// 写入 goroutine输入 channel → Python chunks
go func() {
for {
@@ -308,23 +347,26 @@ func invokeStreamBoth[R any](ctx context.Context, pool Pool, method string, stre
if err != nil {
break
}
if err := writeMsg(conn, Message{ID: id, Type: TypeChunk, Data: data}); err != nil {
mu.Lock()
err = writeMsg(conn, Message{ID: id, Type: TypeChunk, Data: data})
mu.Unlock()
if err != nil {
break
}
}
writeMsg(conn, Message{ID: id, Type: TypeEnd}) //nolint
write(Message{ID: id, Type: TypeEnd})
}()
// 读取 goroutinePython chunks → 输出 channel
// 读取 goroutinePython chunks → 输出 channel,内联处理 callback
go func() {
stop := watchCtx(ctx, conn, id)
stop := watchCtx(ctx, conn, id, write)
defer func() {
stop()
outCh.Close()
w.release(conn, ctx.Err() == nil)
}()
for {
msg, err := readMsg(conn)
msg, err := readResult(ctx, conn, pool, write)
if err != nil || msg.Type == TypeEnd || msg.Type == TypeError {
return
}