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
+45
View File
@@ -242,6 +242,51 @@ if ctx.Err() != nil {
完整可运行示例见 [example/main.go](example/main.go) 中的 `demoTimeout`(默认超时 / 显式 deadline 优先级 / 流式超时静默关闭 channel)和 `demoBlocking`(池子占满后阻塞排队)。
### 查询流式调用执行过程中的错误(WithStreamErrors
流式输出/双向流的 handler(Python 生成器)如果**执行过程中**抛异常(不是等全部 yield 完才失败,是还没跑完就失败),默认情况下调用方完全无法感知——`Invoke[chan T]` 返回的那个 `err` 只描述"调用有没有成功发起"(参数序列化、抢连接、写 `call` 消息),跟 Python 函数体最终是正常结束还是执行过程中抛异常没有任何关系,因为 handler 真正执行是在 `Invoke` 返回之后的后台 goroutine 里才发生的。Python 侧异常只会让 `ch` 静默提前关闭,现象上跟"正常读完"一模一样,`ctx.Err()` 也查不出来(不是 ctx 取消导致的)。
想知道是不是这种情况,用 `WithStreamErrors` 包一层 ctx(opt-in),它会额外返回一个查询函数,之后不需要再传 ctx:
```go
ctx, streamErr := gobridge.WithStreamErrors(ctx) // opt-in,不包这一层就是上面说的默认行为
ch, err := gobridge.Invoke[chan int](ctx, pool, "range_gen", 1, 10)
if err != nil {
// 建立阶段失败,比如池子占满/参数错误
}
for v := range ch {
fmt.Println(v)
}
if err := streamErr(ch); err != nil {
// channel 是因为 Python 侧执行过程中抛异常提前关闭的,不是正常 yield 完
}
```
错误记录挂在 `WithStreamErrors` 返回的那个 `ctx` 的对象图里(通过 `context.WithValue` 携带一个可变的存储,`streamErr` 直接持有这块存储,不需要再查 ctx),不是全局表——调用方不再引用这个 `ctx`(和对应的 `ch`)时,整条链会被 GC 自然回收,**不需要任何显式清理逻辑,也不依赖 `ctx.Done()`**,即使用 `context.Background()` 也能正常释放。`WithStreamErrors` 之后即使 `ctx` 又被别的 `context.With*`(包括库里自己的 `StickyCtx`)再包一层,也不影响——`context.Value()` 的查找是逐层往上找的,不会因为后续再包装而丢失(`streamErr` 是从最初那次 `WithStreamErrors` 调用里直接拿到的闭包,跟后续怎么包 ctx 完全无关)。
不调用 `WithStreamErrors` 是完全零成本的默认行为,现有的流式调用代码不需要做任何改动。完整可运行示例见 `example/main.go``demoTimeout` 的示例5。
### 查询流式调用中途的错误(StreamError
流式输出/双向流的 handler(Python 生成器)如果**执行过程中**抛异常(不是等全部 yield 完才失败,是还没跑完就失败),默认情况下调用方完全无法感知——`Invoke[chan T]` 返回的那个 `err` 只描述"调用有没有成功发起"(参数序列化、抢连接、写 `call` 消息),跟 Python 函数体最终是正常结束还是中途抛异常没有任何关系,因为 handler 真正执行是在 `Invoke` 返回之后的后台 goroutine 里才发生的。Python 侧异常只会让 `ch` 静默提前关闭,现象上跟"正常读完"一模一样,`ctx.Err()` 也查不出来(不是 ctx 取消导致的)。
想知道是不是这种情况,用 `StreamError` 查——不需要任何额外配置,直接传入 `Invoke[chan T]` 返回的那个 channel
```go
ch, err := gobridge.Invoke[chan int](ctx, pool, "range_gen", 1, 10)
if err != nil {
// 建立阶段失败,比如池子占满/参数错误
}
for v := range ch {
fmt.Println(v)
}
if streamErr := gobridge.StreamError(ch); streamErr != nil {
// channel 是因为 Python 侧执行过程中抛异常提前关闭的,不是正常 yield 完
}
```
`StreamError` 内部用调用方传给 `Invoke` 的那个 `ctx` 挂了 `context.AfterFunc``ctx` 结束(取消/超时/调用方自己 `cancel()`)时会自动清理对应记录,不需要调用方主动查询才释放——前提是 ctx 本身会结束(`context.Background()` 永不 `Done()`,如果还从来不查 `StreamError`,记录会一直留着,这也是本文档反复强调"别用没有 deadline 的 ctx"的场景之一)。
## Session 亲和路由
默认情况下,每次 `Invoke` 通过轮询分配 worker 进程。当多次调用需要共享同一 Python 进程的状态时,可以使用 Session 或 StickyCtx 将调用固定到同一进程。