feat: 添加 WithDefaultTimeout 默认超时配置
Invoke 之前完全依赖调用方传入的 ctx 控制超时,池子被占满或 Python 侧 handler 阻塞时,未设置 deadline 的调用会永久阻塞且不报错。新增 WithDefaultTimeout 选项,仅在 ctx 未设置 deadline 时兜底生效,调用方 显式设置的超时优先级更高。 同时补充 example 中的阻塞/超时演示(demoTimeout、demoBlocking)和 pool_test.go 集成测试,覆盖池占满排队、默认超时、显式 deadline 优先级、 流式输出超时后 channel 静默关闭等场景。
This commit is contained in:
+143
@@ -0,0 +1,143 @@
|
||||
package gobridge
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// testWorkerScript 返回 example/worker.py 的绝对路径,供集成测试复用其中的
|
||||
// sleep_seconds / add 等函数;没有 python3 时跳过测试。
|
||||
func testWorkerScript(t *testing.T) string {
|
||||
t.Helper()
|
||||
if _, err := exec.LookPath("python3"); err != nil {
|
||||
t.Skip("python3 not found in PATH, skipping integration test")
|
||||
}
|
||||
_, file, _, _ := runtime.Caller(0)
|
||||
return filepath.Join(filepath.Dir(file), "example", "worker.py")
|
||||
}
|
||||
|
||||
func newTestPool(t *testing.T, opts ...Option) Pool {
|
||||
t.Helper()
|
||||
script := testWorkerScript(t)
|
||||
// 注意:不要用 t.TempDir() 当 socketDir——macOS 的 AF_UNIX sun_path 只有 104
|
||||
// 字节长度限制,t.TempDir() 产出的路径很容易超限导致 bind 失败,所以沿用库默认的 /tmp。
|
||||
opts = append([]Option{
|
||||
WithStdout(io.Discard),
|
||||
WithStderr(io.Discard),
|
||||
}, opts...)
|
||||
pool, err := NewPool(script, opts...)
|
||||
if err != nil {
|
||||
t.Fatalf("NewPool: %v", err)
|
||||
}
|
||||
t.Cleanup(pool.Close)
|
||||
return pool
|
||||
}
|
||||
|
||||
// TestPoolExhaustionBlocks 验证:workers=2、maxConns=2(总容量 4)的池子被占满后,
|
||||
// 新的 Invoke 调用会排队阻塞等待连接释放,而不是立刻失败或被跳过;
|
||||
// 一旦有连接释放,新调用能正常拿到连接并执行。
|
||||
func TestPoolExhaustionBlocks(t *testing.T) {
|
||||
pool := newTestPool(t, WithWorkers(2), WithMaxConns(2))
|
||||
|
||||
const capacity = 4
|
||||
const taskSeconds = 5.0
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(capacity)
|
||||
for i := 0; i < capacity; i++ {
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
if _, err := Invoke[string](context.Background(), pool, "sleep_seconds", taskSeconds); err != nil {
|
||||
t.Errorf("occupy task failed: %v", err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// 留出时间让 4 个占位任务都真正 acquire 到连接、进入 Python 侧 time.sleep(),占满全部 4 个槽位
|
||||
time.Sleep(300 * time.Millisecond)
|
||||
|
||||
// 池子已被占满:用一个很短的超时验证新调用确实在排队等待连接(而非瞬间失败/跳过)
|
||||
shortCtx, cancel := context.WithTimeout(context.Background(), 300*time.Millisecond)
|
||||
defer cancel()
|
||||
start := time.Now()
|
||||
_, err := Invoke[string](shortCtx, pool, "add", 1, 2)
|
||||
elapsed := time.Since(start)
|
||||
|
||||
if !errors.Is(err, context.DeadlineExceeded) {
|
||||
t.Fatalf("want context.DeadlineExceeded while pool exhausted, got %v", err)
|
||||
}
|
||||
if elapsed < 250*time.Millisecond {
|
||||
t.Fatalf("acquire returned too fast (%v),说明没有真正排队等待连接释放", elapsed)
|
||||
}
|
||||
|
||||
// 等 4 个占位任务(各耗时 5s)全部结束、连接释放后,新调用应恢复正常
|
||||
wg.Wait()
|
||||
sum, err := Invoke[int](context.Background(), pool, "add", 3, 4)
|
||||
if err != nil {
|
||||
t.Fatalf("Invoke after release: %v", err)
|
||||
}
|
||||
if sum != 7 {
|
||||
t.Fatalf("want 7, got %d", sum)
|
||||
}
|
||||
}
|
||||
|
||||
// TestDefaultTimeout 验证 WithDefaultTimeout:仅在 ctx 未设置 deadline 时生效,
|
||||
// 调用方显式传入的 deadline 优先级更高,不会被覆盖。
|
||||
func TestDefaultTimeout(t *testing.T) {
|
||||
pool := newTestPool(t, WithWorkers(1), WithMaxConns(1), WithDefaultTimeout(300*time.Millisecond))
|
||||
|
||||
// ctx 未设置 deadline:池的默认超时应自动生效,而不是永久阻塞
|
||||
start := time.Now()
|
||||
_, err := Invoke[string](context.Background(), pool, "sleep_seconds", 5.0)
|
||||
elapsed := time.Since(start)
|
||||
if !errors.Is(err, context.DeadlineExceeded) {
|
||||
t.Fatalf("want context.DeadlineExceeded from default timeout, got %v", err)
|
||||
}
|
||||
if elapsed > 2*time.Second {
|
||||
t.Fatalf("default timeout did not kick in promptly, took %v", elapsed)
|
||||
}
|
||||
|
||||
// 显式传入的 deadline 优先级更高,不应被 300ms 的默认超时覆盖
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||
defer cancel()
|
||||
msg, err := Invoke[string](ctx, pool, "sleep_seconds", 1.0)
|
||||
if err != nil {
|
||||
t.Fatalf("explicit deadline should override default timeout: %v", err)
|
||||
}
|
||||
if msg != "slept 1s" {
|
||||
t.Fatalf("want %q, got %q", "slept 1s", msg)
|
||||
}
|
||||
}
|
||||
|
||||
// TestStreamTimeoutClosesChannelSilently 验证流式输出模式下,ctx 超时不会通过 error
|
||||
// 返回,而是静默关闭已返回的 channel,需要调用方自行检查 ctx.Err()。
|
||||
func TestStreamTimeoutClosesChannelSilently(t *testing.T) {
|
||||
pool := newTestPool(t, WithWorkers(1), WithMaxConns(1))
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 350*time.Millisecond)
|
||||
defer cancel()
|
||||
|
||||
ch, err := Invoke[chan int](ctx, pool, "slow_range_gen", 1, 10, 100)
|
||||
if err != nil {
|
||||
t.Fatalf("Invoke: %v", err)
|
||||
}
|
||||
|
||||
var got []int
|
||||
for v := range ch {
|
||||
got = append(got, v)
|
||||
}
|
||||
|
||||
if len(got) == 0 || len(got) >= 9 {
|
||||
t.Fatalf("want a partial stream (超时提前打断), got %v", got)
|
||||
}
|
||||
if ctx.Err() == nil {
|
||||
t.Fatalf("want ctx.Err() != nil after timeout-truncated stream")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user