feat: 添加 WithDefaultTimeout 默认超时配置

Invoke 之前完全依赖调用方传入的 ctx 控制超时,池子被占满或 Python 侧
handler 阻塞时,未设置 deadline 的调用会永久阻塞且不报错。新增
WithDefaultTimeout 选项,仅在 ctx 未设置 deadline 时兜底生效,调用方
显式设置的超时优先级更高。

同时补充 example 中的阻塞/超时演示(demoTimeout、demoBlocking)和
pool_test.go 集成测试,覆盖池占满排队、默认超时、显式 deadline 优先级、
流式输出超时后 channel 静默关闭等场景。
This commit is contained in:
2026-07-23 10:37:24 +08:00
parent db71a904e0
commit 0f4a4ded53
7 changed files with 367 additions and 7 deletions
+91
View File
@@ -6,6 +6,8 @@ import (
"log"
"path/filepath"
"runtime"
"sync"
"time"
"git.fsdpf.net/go/gobridge"
)
@@ -35,6 +37,8 @@ func main() {
demoPool(ctx, pool)
demoServer(ctx, script)
demoSession(ctx, script)
demoTimeout(script)
demoBlocking(script)
}
func demoPool(ctx context.Context, pool gobridge.Pool) {
@@ -277,6 +281,93 @@ func demoSession(ctx context.Context, script string) {
}
}
func demoTimeout(script string) {
fmt.Println("\n── 默认超时示例(WithDefaultTimeout)─────────────────────────────")
// workers=1, maxConns=1:整个池子只有一个连接槽位,便于演示"占满后阻塞/超时"
pool, err := gobridge.NewPool(script,
gobridge.WithWorkers(1),
gobridge.WithMaxConns(1),
gobridge.WithDefaultTimeout(500*time.Millisecond),
)
if err != nil {
log.Fatal(err)
}
defer pool.Close()
// ── 示例1ctx 未设置 deadline,池的默认超时自动生效 ─────────────────────
// sleep_seconds(2) 耗时 2s,远超 500ms 默认超时,Invoke 会在 500ms 后返回
// context.DeadlineExceeded,而不是永久阻塞。
_, err = gobridge.Invoke[string](context.Background(), pool, "sleep_seconds", 2.0)
fmt.Println("sleep_seconds(2.0)(无 ctx deadline,池默认超时 500ms)→ err =", err)
// ── 示例2:调用方显式传入的 deadline 优先级更高,不会被默认超时覆盖 ───────
ctx2, cancel2 := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel2()
msg, err := gobridge.Invoke[string](ctx2, pool, "sleep_seconds", 1.0)
fmt.Println("sleep_seconds(1.0)(显式 3s ctx)→", msg, err)
// ── 示例3:流式输出模式下,超时只会静默关闭 channel,不返回 error
// 需要调用方自行在 range 结束后检查 ctx.Err() 才能区分"正常结束"还是"被打断"。
ctx3, cancel3 := context.WithTimeout(context.Background(), 350*time.Millisecond)
defer cancel3()
ch, err := gobridge.Invoke[chan int](ctx3, pool, "slow_range_gen", 1, 10, 100)
if err != nil {
log.Fatal(err)
}
fmt.Print("slow_range_gen(1, 10, 100ms)350ms ctx 超时)=")
for v := range ch {
fmt.Print(" ", v)
}
fmt.Println()
if ctx3.Err() != nil {
fmt.Println("channel 提前关闭,ctx.Err() =", ctx3.Err())
}
}
func demoBlocking(script string) {
fmt.Println("\n── 连接池占满后阻塞示例(不设超时)───────────────────────────")
// workers=2, maxConns=2:总容量只有 2*2=4 个连接槽位,便于演示"占满后新调用会怎样"
pool, err := gobridge.NewPool(script,
gobridge.WithWorkers(2),
gobridge.WithMaxConns(2),
)
if err != nil {
log.Fatal(err)
}
defer pool.Close()
const capacity = 4 // 2 workers * 2 maxConns
const taskSeconds = 2.0 // 每个占位任务耗时 2s
// 并发发起 4 个耗时调用,占满全部连接槽位
var wg sync.WaitGroup
wg.Add(capacity)
for i := 0; i < capacity; i++ {
go func(n int) {
defer wg.Done()
if _, err := gobridge.Invoke[string](context.Background(), pool, "sleep_seconds", taskSeconds); err != nil {
log.Printf("occupy task %d failed: %v", n, err)
}
}(i)
}
// 留出时间让 4 个占位任务都真正进入 Python 侧 time.sleep(),占满全部槽位
time.Sleep(200 * time.Millisecond)
fmt.Printf("池子已被 %d 个任务占满(各耗时 %.0fs),此时发起第 5 个调用(ctx 不设超时)……\n", capacity, taskSeconds)
// 池子被占满时,新调用既不会立刻失败,也不会被跳过,而是排队阻塞等待连接释放。
// 这里故意不设超时,用等待耗时证明它确实在阻塞排队,而不是瞬间返回。
callStart := time.Now()
sum, err := gobridge.Invoke[int](context.Background(), pool, "add", 1, 2)
fmt.Printf("第 5 个调用等待 %v 后返回:add(1,2)=%d, err=%v\n", time.Since(callStart).Round(time.Millisecond), sum, err)
// 预期:等待时长接近"占满起 ~taskSeconds 秒后有槽位释放"的剩余时间,
// 说明调用被真正阻塞排队,直到某个占位任务结束、连接被释放后才继续执行。
wg.Wait()
}
func demoServer(ctx context.Context, script string) {
fmt.Println("\n── Server 全双工示例 ─────────────────────────────────────────────")