feat: 添加 WithStreamErrors 查询流式调用执行过程中的异常

流式输出/双向流的 handler(Python 生成器)如果执行过程中抛异常,
Invoke[chan T] 本身的 err 只描述"调用有没有发起成功",跟这个异常
无关(永远是 nil),channel 只会静默提前关闭,调用方原本完全无法
感知。新增 WithStreamErrors(ctx) 返回一个包过的 ctx 和一个查询函数
streamErr,opt-in 之后可以查到具体错误。

错误记录挂在 WithStreamErrors 返回的 ctx 的对象图里(context.WithValue),
不是全局表——调用方不再引用 ctx/channel 时会被 GC 自然回收,不需要
任何显式清理逻辑,也不依赖 ctx.Done(),即使用 context.Background()
也能正常释放;ctx 之后被别的 context.With*(包括 StickyCtx)再包一层
也不影响查询。

同时补充完整的自动化测试覆盖 example/main.go 里演示过的所有功能:
四种调用模式 × int/struct/slice/[]byte 的组合(client_test.go)、
WithHandlers/call_go 全双工(handlers_test.go)、NewSession 隔离性
与 StickyCtx 路由(session_test.go),之前这些只能靠人肉跑 go run
看输出,现在都有真实断言。
This commit is contained in:
2026-07-23 16:24:05 +08:00
parent 6ccec66a1e
commit ee5e5b96af
9 changed files with 717 additions and 5 deletions
+62
View File
@@ -0,0 +1,62 @@
package gobridge
import (
"context"
"sync"
)
// streamErrBox 是挂在 ctx.Value 树上的一块可变存储,key 是具体的 channel 值本身。
// 生命周期完全跟着调用方持有的 ctx/channel 走,不需要任何显式清理:
// 调用方不再引用它们时,整棵对象图(ctx → streamErrBox → errs → error
// 会被 GC 自然回收——不依赖 ctx.Done()context.Background() 一样能正常释放。
type streamErrBox struct {
mu sync.Mutex
errs map[any]error
}
func (b *streamErrBox) get(ch any) error {
b.mu.Lock()
defer b.mu.Unlock()
return b.errs[ch]
}
func (b *streamErrBox) set(ch any, err error) {
b.mu.Lock()
defer b.mu.Unlock()
b.errs[ch] = err
}
type streamErrBoxKey struct{}
// WithStreamErrors 返回一个包过的 ctx,以及一个用于查询流式调用错误的函数 streamErr。
// 之后用这个 ctx(或它的子 ctx,即使被别的 context.With* 再包一层也不受影响)发起的
// 流式输出/双向流调用,如果 Python handler 执行过程中抛异常,可以用 streamErr(ch)
// 查到具体错误,不需要再传一次 ctx。
//
// 不调用这个函数是默认行为:流式调用中途出错只会让 channel 静默提前关闭,
// 调用方拿不到任何错误信息。这是因为 Invoke[chan T] 在建立调用阶段就已经成功
// 返回了 (ch, nil),后续 Python 生成器内部的异常没有天然的返回值可以携带。
//
// ctx, streamErr := gobridge.WithStreamErrors(ctx)
// ch, err := gobridge.Invoke[chan int](ctx, pool, "range_gen", 1, 10)
// for v := range ch {
// fmt.Println(v)
// }
// if err := streamErr(ch); err != nil {
// // channel 是因为 Python 侧异常提前关闭的,而不是正常 yield 完
// }
func WithStreamErrors(parent context.Context) (ctx context.Context, streamErr func(ch any) error) {
box := &streamErrBox{errs: make(map[any]error)}
ctx = context.WithValue(parent, streamErrBoxKey{}, box)
return ctx, box.get
}
// recordStreamError 供 invokeStreamOut/invokeStreamBoth 在读到 TypeError 时调用。
// 如果调用方没有用 WithStreamErrors 包过 ctx,直接是个空操作。
func recordStreamError(ctx context.Context, ch any, err error) {
box, ok := ctx.Value(streamErrBoxKey{}).(*streamErrBox)
if !ok {
return
}
box.set(ch, err)
}