fix: WithDefaultTimeout 不再套用到流式输出/双向流

流式输出(Invoke[chan T])返回的 channel 由调用方通过 range 自行决定
消费多久,典型场景是流式聊天回复,可能正常持续几十秒甚至更久。之前
WithDefaultTimeout 会不分场景地把这个全局默认超时套到整个调用生命周期
上(包括流还在正常输出的过程中),导致长时间运行的正常流式响应被腰斩。

改成流式输出/双向流完全不受 WithDefaultTimeout 影响,只认调用方显式传入
的 ctx deadline;需要超时保护的话必须自己 context.WithTimeout。普通调用
和流式输入不受影响,继续吃池子的默认超时。

新增 TestStreamOutIgnoresDefaultTimeout 验证;example/main.go 的
demoTimeout 补充示例4 演示这一行为;README 同步更新。
This commit is contained in:
2026-07-23 14:24:10 +08:00
parent 4f74627be1
commit 6ccec66a1e
4 changed files with 57 additions and 23 deletions
+6 -19
View File
@@ -180,20 +180,19 @@ func invokeRegular[R any](ctx context.Context, pool Pool, method string, args ..
return result, nil
}
// invokeStreamOut 不套用 WithDefaultTimeout:返回的 channel 生命周期由调用方通过
// range 消费决定,可能持续很久(比如流式聊天回复),套一个全局默认超时会在正常
// 流式输出过程中把它腰斩。想要超时保护的话,调用方必须显式传入带 deadline 的 ctx。
func invokeStreamOut[R any](ctx context.Context, pool Pool, method string, rt reflect.Type, args ...any) (R, error) {
var zero R
ctx, cancel := applyDefaultTimeout(ctx, pool)
argsJSON, err := json.Marshal(args)
if err != nil {
cancel()
return zero, fmt.Errorf("marshal args: %w", err)
}
conn, w, err := pool.acquire(ctx)
if err != nil {
cancel()
return zero, err
}
@@ -205,7 +204,6 @@ func invokeStreamOut[R any](ctx context.Context, pool Pool, method string, rt re
Args: argsJSON,
}); err != nil {
w.release(conn, false)
cancel()
return zero, contextErr(ctx, fmt.Errorf("write call: %w", err))
}
@@ -219,7 +217,6 @@ func invokeStreamOut[R any](ctx context.Context, pool Pool, method string, rt re
stop()
ch.Close()
w.release(conn, ctx.Err() == nil)
cancel()
}()
for {
msg, err := readResult(ctx, conn, pool, write)
@@ -328,24 +325,23 @@ func invokeStreamIn[R any](ctx context.Context, pool Pool, method string, stream
return result, nil
}
// invokeStreamBoth 同样不套用 WithDefaultTimeout,理由同 invokeStreamOut——
// 双向流的生命周期由输入/输出两端共同决定,可能持续很久,需要超时保护时调用方
// 必须显式传入带 deadline 的 ctx。
func invokeStreamBoth[R any](ctx context.Context, pool Pool, method string, streamArgIdx int, streamCh reflect.Value, rt reflect.Type, args ...any) (R, error) {
var zero R
ctx, cancel := applyDefaultTimeout(ctx, pool)
jsonArgs := make([]any, len(args))
copy(jsonArgs, args)
jsonArgs[streamArgIdx] = nil
argsJSON, err := json.Marshal(jsonArgs)
if err != nil {
cancel()
return zero, fmt.Errorf("marshal args: %w", err)
}
conn, w, err := pool.acquire(ctx)
if err != nil {
cancel()
return zero, err
}
@@ -359,7 +355,6 @@ func invokeStreamBoth[R any](ctx context.Context, pool Pool, method string, stre
StreamArgIdx: streamArgIdx,
}); err != nil {
w.release(conn, false)
cancel()
return zero, contextErr(ctx, fmt.Errorf("write call: %w", err))
}
@@ -368,15 +363,8 @@ func invokeStreamBoth[R any](ctx context.Context, pool Pool, method string, stre
var mu sync.Mutex
write := func(msg Message) { mu.Lock(); writeMsg(conn, msg); mu.Unlock() } //nolint
// 两个 goroutine 共用同一个 ctxdefaultTimeout 产生的 cancel 必须等两者都结束才能调用,
// 否则先完成的一方会提前取消掉另一方仍在进行的操作。
var wg sync.WaitGroup
wg.Add(2)
go func() { wg.Wait(); cancel() }()
// 写入 goroutine:输入 channel → Python chunks
go func() {
defer wg.Done()
for {
val, ok, cancelled := chanRecv(ctx, streamCh)
if cancelled || !ok {
@@ -398,7 +386,6 @@ func invokeStreamBoth[R any](ctx context.Context, pool Pool, method string, stre
// 读取 goroutinePython chunks → 输出 channel,内联处理 callback
go func() {
defer wg.Done()
stop := watchCtx(ctx, conn, id, write)
defer func() {
stop()