2bec257289
iotlcm-worker-svr:异步执行中枢(RabbitMQ 消费者 + 全量同步编排)。 - Go module:iotlcm-worker-svr → git.5iot.com/iotlcm-public/iotlcm-worker-svr(214 文件) - 凭据与内部标识脱敏:MySQL/Redis 凭据、私网与生产 IP、内网域名、 库名 xiujie_admin → iotlcm、哨兵名占位 - 159 处真实手机号 → 合法 11 位编造号(保留号段与后 4 位,维持互异, 不破坏「校验 MSISDN 格式」等测试断言) - 移除内部资产:CI 触发器、Jenkins 触发测试文档、预演门禁与镜像 digest 记录 - Dockerfile 改用公共基础镜像(golang:1.25-alpine → alpine:3.23), 移除写死的 git 凭据 - README 重写(架构/全量同步模型/配置/已知失败),补 Apache-2.0 LICENSE,LF 行尾 + .gitattributes 已知:3 个测试包在源仓同一提交上同样失败(client/upstream、 service/allocate、service/inbound),属既有缺陷,本仓未引入亦未修复。
577 lines
17 KiB
Go
577 lines
17 KiB
Go
package imei
|
|
|
|
import (
|
|
"bufio"
|
|
"context"
|
|
"errors"
|
|
"io"
|
|
"net"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"sync/atomic"
|
|
"testing"
|
|
"time"
|
|
|
|
"git.5iot.com/iotlcm-public/iotlcm-worker-svr/internal/model"
|
|
queue "git.5iot.com/iotlcm-public/iotlcm-worker-svr/internal/queue/zset"
|
|
"git.5iot.com/iotlcm-public/iotlcm-worker-svr/internal/testutil"
|
|
"github.com/gogf/gf/v2/database/gredis"
|
|
goredis "github.com/redis/go-redis/v9"
|
|
)
|
|
|
|
// This proxy withholds a real Redis request/reply without closing its TCP socket.
|
|
func readLeaseRESP(r *bufio.Reader) ([]byte, error) {
|
|
line, err := r.ReadBytes('\n')
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
switch line[0] {
|
|
case '$':
|
|
n, err := strconv.Atoi(strings.TrimSpace(string(line[1:])))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if n >= 0 {
|
|
payload := make([]byte, n+2)
|
|
if _, err := io.ReadFull(r, payload); err != nil {
|
|
return nil, err
|
|
}
|
|
line = append(line, payload...)
|
|
}
|
|
case '*', '%', '~', '>', '|':
|
|
n, err := strconv.Atoi(strings.TrimSpace(string(line[1:])))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if line[0] == '%' || line[0] == '|' {
|
|
n *= 2
|
|
}
|
|
for i := 0; i < n; i++ {
|
|
payload, err := readLeaseRESP(r)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
line = append(line, payload...)
|
|
}
|
|
}
|
|
return line, nil
|
|
}
|
|
|
|
func TestProduceBoundsRealRedisRenewalIO(t *testing.T) {
|
|
for _, mode := range []string{"standalone", "sentinel"} {
|
|
t.Run(mode, func(t *testing.T) { testProduceBoundsRealRedisRenewalIO(t, mode) })
|
|
}
|
|
}
|
|
|
|
func testProduceBoundsRealRedisRenewalIO(t *testing.T, mode string) {
|
|
for _, callerCancel := range []bool{false, true} {
|
|
t.Run(strconv.FormatBool(callerCancel), func(t *testing.T) {
|
|
backend := testutil.Redis(t)
|
|
real, held, release := leaseRedisProxy(t, backend, "redis.call('PEXPIRE'", true)
|
|
// Release the gate even on a red-test failure so no worker leaks.
|
|
defer release()
|
|
opts := real.GetAdapter().Client().(*goredis.Client).Options()
|
|
if opts.ReadTimeout != 0 || opts.WriteTimeout != 0 || opts.ContextTimeoutEnabled {
|
|
t.Fatal("test must start with production-default unbounded socket I/O")
|
|
}
|
|
cause := errors.New("snapshot failed independently")
|
|
ctx, cancel := context.WithCancelCause(context.Background())
|
|
defer cancel(nil)
|
|
scanner := &lockTestScanner{snapshot: func(workCtx context.Context) (int64, error) {
|
|
select {
|
|
case <-held:
|
|
if callerCancel {
|
|
cancel(cause)
|
|
<-workCtx.Done()
|
|
return 0, workCtx.Err()
|
|
}
|
|
return 0, cause
|
|
case <-workCtx.Done():
|
|
return 0, workCtx.Err()
|
|
}
|
|
}}
|
|
store, err := queue.New(real, "imei:")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
config := Config{RunLockTTL: time.Second}
|
|
if mode == "sentinel" {
|
|
real, config.LeaseRedisConfig = sentinelLeaseRedis(t, real)
|
|
store, err = queue.New(real, "imei:")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
producer, err := New(scanner, store, config)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
result := make(chan error, 1)
|
|
go func() { result <- producer.Produce(ctx, model.ImeiQueueStartMessage{RunID: "lock-test"}) }()
|
|
select {
|
|
case <-held:
|
|
case <-time.After(2 * time.Second):
|
|
t.Fatal("proxy did not hold renewal response")
|
|
}
|
|
select {
|
|
case err := <-result:
|
|
if !errors.Is(err, cause) {
|
|
t.Fatalf("lost original cause: %v", err)
|
|
}
|
|
case <-time.After(600 * time.Millisecond):
|
|
release()
|
|
<-result
|
|
t.Fatal("canceled renewal socket I/O blocked cleanup beyond its lease budget")
|
|
}
|
|
if producer.running.Load() != 0 {
|
|
t.Fatal("running flag was not reset")
|
|
}
|
|
lock, err := backend.Do(context.Background(), "GET", producer.config.RunLockKey)
|
|
if err != nil || !lock.IsNil() {
|
|
t.Fatalf("lock not released: %v, %v", lock, err)
|
|
}
|
|
if opts.ReadTimeout != 0 || opts.WriteTimeout != 0 {
|
|
t.Fatal("lease client mutated the shared production client")
|
|
}
|
|
// A fresh run must start without opening the stalled gate.
|
|
producer.scanner = cardScanner{}
|
|
if err := producer.Produce(context.Background(), model.ImeiQueueStartMessage{RunID: "next-run"}); err != nil {
|
|
t.Fatalf("producer cannot restart: %v", err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestProduceFencesDelayedRealRedisWrites(t *testing.T) {
|
|
for _, mode := range []string{"standalone", "sentinel"} {
|
|
t.Run(mode, func(t *testing.T) { testProduceFencesDelayedRealRedisWrites(t, mode) })
|
|
}
|
|
}
|
|
|
|
func testProduceFencesDelayedRealRedisWrites(t *testing.T, mode string) {
|
|
for _, stage := range []string{"enqueue", "producer_done"} {
|
|
t.Run(stage, func(t *testing.T) {
|
|
backend := testutil.Redis(t)
|
|
match := "local taskID=ARGV[i]"
|
|
cards := []Card{{ID: 1, ICCID: "a", ChanKey: "ch", TempKey: "sim_yd_v5"}}
|
|
if stage == "producer_done" {
|
|
match = "'producerCompletedAt'"
|
|
cards = nil
|
|
}
|
|
real, held, release := leaseRedisProxy(t, backend, match, false)
|
|
defer release()
|
|
workContexts := make(chan context.Context, 1)
|
|
scanner := &lockTestScanner{cardScanner: cardScanner{cards: cards}}
|
|
scanner.snapshot = func(ctx context.Context) (int64, error) { workContexts <- ctx; return int64(len(cards)), nil }
|
|
store, err := queue.New(real, "imei:")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
config := Config{RunLockTTL: time.Second}
|
|
if mode == "sentinel" {
|
|
real, config.LeaseRedisConfig = sentinelLeaseRedis(t, real)
|
|
store, err = queue.New(real, "imei:")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
producer, err := New(scanner, store, config)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
result := startLockTestProducer(t, producer)
|
|
select {
|
|
case <-held:
|
|
case <-time.After(2 * time.Second):
|
|
t.Fatal("proxy did not hold old-owner EVAL")
|
|
}
|
|
workCtx := <-workContexts
|
|
observer, err := queue.New(backend, "imei:")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
ok, err := observer.AcquireLockOrTakeover(context.Background(), producer.config.RunLockKey, "new-owner", "lock-test", time.Minute)
|
|
if err != nil || !ok {
|
|
t.Fatalf("takeover: %v, %v", ok, err)
|
|
}
|
|
select {
|
|
case <-workCtx.Done():
|
|
case <-time.After(2 * time.Second):
|
|
t.Fatal("real renewal did not detect takeover")
|
|
}
|
|
if !errors.Is(context.Cause(workCtx), ErrAlreadyRunning) {
|
|
t.Fatalf("wrong loss cause: %v", context.Cause(workCtx))
|
|
}
|
|
release()
|
|
select {
|
|
case err := <-result:
|
|
if !errors.Is(err, ErrAlreadyRunning) {
|
|
t.Fatalf("wrong result: %v", err)
|
|
}
|
|
case <-time.After(2 * time.Second):
|
|
t.Fatal("producer did not finish")
|
|
}
|
|
meta, err := observer.GetRunMeta(context.Background(), "lock-test")
|
|
if err != nil || meta.ActiveCount != 0 || meta.ProducerDone {
|
|
t.Fatalf("stale owner changed run state: %+v, %v", meta, err)
|
|
}
|
|
channels, err := observer.RunChannels(context.Background(), "lock-test")
|
|
if err != nil || len(channels) != 0 {
|
|
t.Fatalf("stale owner indexed channel: %v, %v", channels, err)
|
|
}
|
|
ok, err = observer.RenewLock(context.Background(), producer.config.RunLockKey, "new-owner", time.Minute)
|
|
if err != nil || !ok {
|
|
t.Fatalf("old cleanup removed new lease: %v, %v", ok, err)
|
|
}
|
|
added, err := observer.EnqueueRunBatch(context.Background(), "lock-test", "ch", []queue.Task{{TaskID: "next", RunID: "lock-test", ChanKey: "ch", CardID: 2}}, time.Now())
|
|
if err != nil || added != 1 {
|
|
t.Fatalf("new work rejected: added=%d, %v", added, err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func leaseRedisProxy(t *testing.T, upstream *gredis.Redis, match string, holdReply bool) (*gredis.Redis, <-chan struct{}, func()) {
|
|
t.Helper()
|
|
raw := upstream.GetAdapter().Client().(*goredis.Client)
|
|
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
held, gate := make(chan struct{}), make(chan struct{})
|
|
var heldOnce, releaseOnce sync.Once
|
|
release := func() { releaseOnce.Do(func() { close(gate) }) }
|
|
var connections sync.Map
|
|
go func() {
|
|
for {
|
|
down, err := listener.Accept()
|
|
if err != nil {
|
|
return
|
|
}
|
|
connections.Store(down, struct{}{})
|
|
go func() {
|
|
defer down.Close()
|
|
defer connections.Delete(down)
|
|
up, err := net.Dial("tcp", raw.Options().Addr)
|
|
if err != nil {
|
|
return
|
|
}
|
|
defer up.Close()
|
|
dr, ur := bufio.NewReader(down), bufio.NewReader(up)
|
|
for {
|
|
request, err := readLeaseRESP(dr)
|
|
if err != nil {
|
|
return
|
|
}
|
|
holding := strings.Contains(string(request), match)
|
|
if holding && !holdReply {
|
|
heldOnce.Do(func() { close(held) })
|
|
<-gate
|
|
}
|
|
if _, err := up.Write(request); err != nil {
|
|
return
|
|
}
|
|
response, err := readLeaseRESP(ur)
|
|
if err != nil {
|
|
return
|
|
}
|
|
if holding && holdReply {
|
|
heldOnce.Do(func() { close(held) })
|
|
<-gate
|
|
}
|
|
if _, err := down.Write(response); err != nil {
|
|
return
|
|
}
|
|
}
|
|
}()
|
|
}
|
|
}()
|
|
t.Cleanup(func() {
|
|
release()
|
|
_ = listener.Close()
|
|
connections.Range(func(key, _ any) bool { _ = key.(net.Conn).Close(); return true })
|
|
})
|
|
client, err := gredis.New(&gredis.Config{Address: listener.Addr().String(), DialTimeout: 100 * time.Millisecond, Protocol: 2})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Cleanup(func() { _ = client.Close(context.Background()) })
|
|
if _, err := client.Do(context.Background(), "PING"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
opts := client.GetAdapter().Client().(*goredis.Client).Options()
|
|
t.Logf("real driver options: readTimeout=%s writeTimeout=%s contextTimeoutEnabled=%v", opts.ReadTimeout, opts.WriteTimeout, opts.ContextTimeoutEnabled)
|
|
return client, held, release
|
|
}
|
|
|
|
// A real disposable Sentinel discovers the proxy as its master; neither client
|
|
// configuration nor server state comes from the application's Redis settings.
|
|
func sentinelLeaseRedis(t *testing.T, master *gredis.Redis) (*gredis.Redis, *gredis.Config) {
|
|
t.Helper()
|
|
binary := os.Getenv("REDIS_SERVER_BINARY")
|
|
if binary == "" {
|
|
var err error
|
|
binary, err = exec.LookPath("redis-server")
|
|
if err != nil {
|
|
t.Skip("Sentinel tests require redis-server")
|
|
}
|
|
}
|
|
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
address := listener.Addr().String()
|
|
_, port, _ := net.SplitHostPort(address)
|
|
_ = listener.Close()
|
|
masterAddress := master.GetAdapter().Client().(*goredis.Client).Options().Addr
|
|
host, masterPort, err := net.SplitHostPort(masterAddress)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
dir := t.TempDir()
|
|
path := filepath.Join(dir, "sentinel.conf")
|
|
contents := "bind 127.0.0.1\nport " + port + "\nsentinel monitor lease-test " + host + " " + masterPort + " 1\nsentinel down-after-milliseconds lease-test 60000\n"
|
|
if err := os.WriteFile(path, []byte(contents), 0600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
command := exec.Command(binary, filepath.Base(path), "--sentinel")
|
|
command.Dir = dir
|
|
logPath := filepath.Join(dir, "sentinel.log")
|
|
logFile, err := os.Create(logPath)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer logFile.Close()
|
|
command.Stdout, command.Stderr = logFile, logFile
|
|
if err := command.Start(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Cleanup(func() { _ = command.Process.Kill(); _ = command.Wait() })
|
|
probe, err := gredis.New(&gredis.Config{Address: address, ReadTimeout: 100 * time.Millisecond, WriteTimeout: 100 * time.Millisecond, DialTimeout: 100 * time.Millisecond, Protocol: 2})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Cleanup(func() { _ = probe.Close(context.Background()) })
|
|
for deadline := time.Now().Add(5 * time.Second); ; {
|
|
if _, err = probe.Do(context.Background(), "SENTINEL", "get-master-addr-by-name", "lease-test"); err == nil {
|
|
break
|
|
}
|
|
if time.Now().After(deadline) {
|
|
log, _ := os.ReadFile(logPath)
|
|
t.Fatalf("isolated Sentinel did not start: %v; %s", err, log)
|
|
}
|
|
time.Sleep(20 * time.Millisecond)
|
|
}
|
|
config := &gredis.Config{Address: address, MasterName: "lease-test", DialTimeout: 100 * time.Millisecond, Protocol: 2}
|
|
client, err := gredis.New(config)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Cleanup(func() { _ = client.Close(context.Background()) })
|
|
return client, config
|
|
}
|
|
|
|
func TestLeaseClientBoundsSentinelDiscoveryIO(t *testing.T) {
|
|
master := testutil.Redis(t)
|
|
_, config := sentinelLeaseRedis(t, master)
|
|
sentinel, err := gredis.New(&gredis.Config{Address: config.Address, ReadTimeout: time.Second, WriteTimeout: time.Second, Protocol: 2})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Cleanup(func() { _ = sentinel.Close(context.Background()) })
|
|
proxy, held, release := leaseRedisProxy(t, sentinel, "get-master-addr-by-name", true)
|
|
defer release()
|
|
config.Address = proxy.GetAdapter().Client().(*goredis.Client).Options().Addr
|
|
shared, err := gredis.New(config)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Cleanup(func() { _ = shared.Close(context.Background()) })
|
|
factory, err := newLeaseClientFactory(shared, config, 100*time.Millisecond)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
client, closeClient, err := factory()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer closeClient()
|
|
result := make(chan error, 1)
|
|
go func() { _, err := client.Do(context.Background(), "PING"); result <- err }()
|
|
select {
|
|
case <-held:
|
|
case <-time.After(2 * time.Second):
|
|
t.Fatal("Sentinel discovery request was not held")
|
|
}
|
|
select {
|
|
case err := <-result:
|
|
if err == nil {
|
|
t.Fatal("stalled Sentinel discovery unexpectedly succeeded")
|
|
}
|
|
case <-time.After(600 * time.Millisecond):
|
|
release()
|
|
<-result
|
|
t.Fatal("Sentinel discovery has no socket I/O upper bound")
|
|
}
|
|
}
|
|
|
|
func TestLeaseClientRejectsClusterAndUnknownAdapter(t *testing.T) {
|
|
cluster, err := gredis.New(&gredis.Config{Address: "127.0.0.1:1", Cluster: true})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer cluster.Close(context.Background())
|
|
store, err := queue.New(cluster, "imei:")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := New(cardScanner{}, store, Config{}); err == nil || !strings.Contains(err.Error(), "Cluster") {
|
|
t.Fatalf("Cluster must be rejected at initialization before any write: %v", err)
|
|
}
|
|
if _, err := newLeaseClientFactory(&leaseRedis{}, nil, time.Second); err == nil {
|
|
t.Fatal("unknown adapter must not fall back to unbounded context-only I/O")
|
|
}
|
|
}
|
|
|
|
func TestProduceBoundsReleaseIO(t *testing.T) {
|
|
backend := testutil.Redis(t)
|
|
real, held, release := leaseRedisProxy(t, backend, "redis.call('DEL', KEYS[1])", true)
|
|
defer release()
|
|
store, err := queue.New(real, "imei:")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
producer, err := New(cardScanner{}, store, Config{RunLockTTL: time.Second})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
result := startLockTestProducer(t, producer)
|
|
select {
|
|
case <-held:
|
|
case <-time.After(2 * time.Second):
|
|
t.Fatal("release response was not held")
|
|
}
|
|
select {
|
|
case err := <-result:
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
case <-time.After(600 * time.Millisecond):
|
|
release()
|
|
<-result
|
|
t.Fatal("release socket I/O blocked cleanup")
|
|
}
|
|
if producer.running.Load() != 0 {
|
|
t.Fatal("cleanup did not reset running")
|
|
}
|
|
}
|
|
|
|
func TestLeaseClientBoundsSocketWriteIO(t *testing.T) {
|
|
backend := testutil.Redis(t)
|
|
upstream := backend.GetAdapter().Client().(*goredis.Client).Options().Addr
|
|
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var stopReading atomic.Bool
|
|
held, gate := make(chan struct{}), make(chan struct{})
|
|
var releaseOnce sync.Once
|
|
release := func() { releaseOnce.Do(func() { close(gate) }) }
|
|
defer release()
|
|
t.Cleanup(func() { _ = listener.Close() })
|
|
go func() {
|
|
down, err := listener.Accept()
|
|
if err != nil {
|
|
return
|
|
}
|
|
defer down.Close()
|
|
up, err := net.Dial("tcp", upstream)
|
|
if err != nil {
|
|
return
|
|
}
|
|
defer up.Close()
|
|
dr, ur := bufio.NewReader(down), bufio.NewReader(up)
|
|
for {
|
|
// Read only the array header once armed: the large EVAL payload
|
|
// cannot enter the upstream or fit in TCP buffers, so Write blocks.
|
|
header, err := dr.ReadBytes('\n')
|
|
if err != nil {
|
|
return
|
|
}
|
|
if stopReading.Load() {
|
|
close(held)
|
|
<-gate
|
|
return
|
|
}
|
|
n, err := strconv.Atoi(strings.TrimSpace(string(header[1:])))
|
|
if err != nil {
|
|
return
|
|
}
|
|
for i := 0; i < n; i++ {
|
|
part, err := readLeaseRESP(dr)
|
|
if err != nil {
|
|
return
|
|
}
|
|
header = append(header, part...)
|
|
}
|
|
if _, err := up.Write(header); err != nil {
|
|
return
|
|
}
|
|
response, err := readLeaseRESP(ur)
|
|
if err != nil {
|
|
return
|
|
}
|
|
if _, err := down.Write(response); err != nil {
|
|
return
|
|
}
|
|
}
|
|
}()
|
|
shared, err := gredis.New(&gredis.Config{Address: listener.Addr().String(), Protocol: 2})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer shared.Close(context.Background())
|
|
factory, err := newLeaseClientFactory(shared, nil, 100*time.Millisecond)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
client, closeClient, err := factory()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer closeClient()
|
|
if _, err := client.Do(context.Background(), "PING"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
opts := client.(*leaseRedis).client.Options()
|
|
if opts.ReadTimeout != 100*time.Millisecond || opts.WriteTimeout != 100*time.Millisecond || !opts.ContextTimeoutEnabled || opts.MaxRetries != 0 || opts.DialerRetries != 1 {
|
|
t.Fatalf("lease client has unsafe timeout/retry options")
|
|
}
|
|
stopReading.Store(true)
|
|
result := make(chan error, 1)
|
|
go func() {
|
|
_, err := client.Eval(context.Background(), "return 1 --"+strings.Repeat("x", 32<<20), 0, nil, nil)
|
|
result <- err
|
|
}()
|
|
select {
|
|
case <-held:
|
|
case <-time.After(2 * time.Second):
|
|
t.Fatal("write proxy did not stop reading")
|
|
}
|
|
select {
|
|
case err := <-result:
|
|
var timeout net.Error
|
|
if !errors.As(err, &timeout) || !timeout.Timeout() || !strings.Contains(err.Error(), "write") {
|
|
t.Fatalf("expected real socket write timeout, got %v", err)
|
|
}
|
|
case <-time.After(600 * time.Millisecond):
|
|
release()
|
|
<-result
|
|
t.Fatal("socket write has no upper bound")
|
|
}
|
|
}
|