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:
86
client.go
86
client.go
@@ -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})
|
||||
}()
|
||||
|
||||
// 读取 goroutine:Python chunks → 输出 channel
|
||||
// 读取 goroutine:Python 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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user