第 23 / 23 篇

附录:随书实验完整代码

以下文件在构建时从随书实验与测试目录读取,与下载区实验包同源。它们是本书教学实现;实际验证范围以实验章节和验证记录为准。

lab/EXPERIMENTS.md

# 实测记录

采集日期:2026-10-21,约 10:23—10:28 UTC。环境:`go version go1.24.3 darwin/arm64`,Apple M4,`CGO_ENABLED=1`。基准后缀 `-10` 表示当次 GOMAXPROCS=10。以下是本次实际输出,不是性能目标;读者机器上的数值会不同。

## 正确性与故障重复

```text
$ go test -race ./...
ok  example.com/go-service-engineering/lab/cmd/service       2.981s
ok  example.com/go-service-engineering/lab/internal/service  1.901s

$ go vet ./...
[无输出,退出状态 0]

$ go test -race ./internal/service -run 'Test(PoolQueueBound|PoolBlockedCancellationReleases|PoolConcurrentCloseAndSubmit|ConnectionExhaustionAndRecovery|FetcherBlockedBodyCancellationReleasesConnection|SlowStreamDeadlineReleasesHandler|DrainRequestsWaitsForHandlerAndGatesAdmission)$' -count=30 -timeout=60s
ok  example.com/go-service-engineering/lab/internal/service  8.224s
```

每轮涵盖:固定上限、满队列拒绝、阻塞任务到期释放、并发 Submit/Close、真实 TCP 连接占满后超时及恢复、读响应体阻塞时取消释放、完全不读响应的慢客户端写截止、handler 排空入口和等待。共 30 轮,测试本身的等待也有上限。

`cmd/service` 的两个 SIGTERM 测试分别检查:3 秒预算内,一个 150ms 在途 HTTP 请求、350ms 运行任务和 150ms 排队任务全部排空;80ms 预算内,一个 5s 请求和 5s 任务被取消,handler 和 worker 均释放,进程非零退出。重复执行命令:

```sh
go test -race ./cmd/service -run TestSIGTERM -count=10 -timeout=60s
```

最终输出:

```text
ok  example.com/go-service-engineering/lab/cmd/service  17.458s
```

## 基准

```text
$ go test ./internal/service -run '^$' -bench BenchmarkPoolSubmit -benchmem -count=3
goos: darwin
goarch: arm64
pkg: example.com/go-service-engineering/lab/internal/service
cpu: Apple M4
BenchmarkPoolSubmit-10      1279885       950.9 ns/op     304 B/op     8 allocs/op
BenchmarkPoolSubmit-10      1000000        1218 ns/op     304 B/op     8 allocs/op
BenchmarkPoolSubmit-10       910029        1176 ns/op     304 B/op     8 allocs/op
PASS
ok  example.com/go-service-engineering/lab/internal/service  3.830s
```

每个 op 都成功接受一个空任务;满队列重试、四个 worker、每任务 timeout context 和同步日志格式化都包含在测量中。日志实际写入 `io.Discard`,不包含终端或磁盘 I/O。基准结束后等待所有已接受任务退出。三次波动不能推出容量保证、HTTP p99 或线上性能。

## 实际 profile 摘要

```text
$ go test ./internal/service -run '^$' -bench BenchmarkPoolSubmit -benchtime=2s -benchmem -cpuprofile profiles/pool.cpu.pprof -memprofile profiles/pool.heap.pprof -o /tmp/go-service-engineering-profile.test
BenchmarkPoolSubmit-10      2343188       974.5 ns/op     305 B/op     8 allocs/op
PASS
ok  example.com/go-service-engineering/lab/internal/service  2.745s
```

本次采集的 `pool.cpu.pprof` 的 `go tool pprof -top -nodecount=12` 摘录:

```text
Duration: 2.41s, Total samples = 8.56s (355.91%)
flat     flat%    function
4.09s    47.78%   runtime.usleep
1.71s    19.98%   runtime.pthread_cond_wait
0.49s     5.72%   runtime.pthread_cond_signal
0.40s     4.67%   runtime.madvise
0.11s     1.29%   runtime.walltime
0.09s     1.05%   internal/sync.(*Mutex).Unlock
```

多核样本总量可以超过墙钟时长。这份 Darwin 样本主要落在 runtime/系统等待栈;不能据此把 `usleep` 当作业务 CPU 热点或给出锁优化结论。应结合更长采样、调用栈、block/mutex profile 和业务负载再判断。

本次采集的 `pool.heap.pprof` 的 `-alloc_space`:

```text
Showing nodes accounting for 663.06MB, 98.71% of 671.74MB total
flat        flat%    cum        function
344.03MB    51.21%   598.56MB   context.WithDeadlineCause
254.53MB    37.89%   254.53MB   time.newTimer
38MB         5.66%   651.06MB   service.(*Pool).run
13.50MB      2.01%   15MB       service.(*Pool).Submit
13MB         1.94%   13.50MB    fmt.Sprintf
```

这里是约 234 万个任务累积分配的采样量,绝不是 671.74MB 存活堆或泄漏证据。每任务 deadline context/timer 是此次累计分配的主要来源。

在线 profile 从实际编译的服务、真实 HTTP 管理端口采集:先提交两个 `delay=5s` 任务并等待 active=2,再提交八个任务;请求 `/debug/pprof/goroutine?debug=2` 和 `/debug/pprof/heap?gc=1`,分别保存为 `profiles/live.goroutine.txt`、`profiles/live.heap.pprof`。

```text
LIVE STATS {accepted:10 started:2 completed:0 failed:0 cancelled:0 active:2 queued:8}
SIGTERM, SHUTDOWN_TIMEOUT=100ms
shutdown complete {accepted:10 started:10 completed:10 failed:10 cancelled:10 active:0 queued:0}
process exit: 1, error: context deadline exceeded
```

取消后的 `started=10` 表示十个队列项目均被 worker 处理到完成状态,不代表被取消的八个排队业务函数执行过。goroutine 文本可直接定位两个 worker 的 `service.wait`/`select` 栈;它是单时刻栈快照。最终在线 heap 样本约 2051 KiB,主要为 runtime 与压缩编码初始化分配;小规模业务对象未必被默认采样率抽中。读取方法与重采命令见 README。

发布包包含摘要和 `profiles/live.goroutine.txt`;二进制采样保存在本次构建的本地 evidence,不随代码包发布。以下 SHA256 标识本次二进制采样,因时间戳和运行调度不同,重采不要求散列相同:

| 文件 | SHA256 |
| --- | --- |
| `pool.cpu.pprof` | `3efac2e4630bebfe7f81352fc2b2e3d17d6e343a602a8f4115853709d9023121` |
| `pool.heap.pprof` | `2deb44a62a2ab8b85aa6ef6af5c6b0c5163ff2d0787322ccc174144794266132` |
| `live.heap.pprof` | `5202267c274e0f830baf32e0328f0a210d94af6acac2845e74f37a84080b6f18` |

CI 应重新采样并验证 pprof 可以解析,而不检查固定百分比:

```sh
mkdir -p evidence
go test ./internal/service -run '^$' -bench BenchmarkPoolSubmit -benchtime=2s -benchmem -cpuprofile evidence/ci.cpu.pprof -memprofile evidence/ci.heap.pprof -o evidence/service.test
go tool pprof -top evidence/ci.cpu.pprof
go tool pprof -top -alloc_space evidence/ci.heap.pprof
```

lab/LICENSE

MIT License

Copyright (c) 2026 Go Service Engineering Lab contributors

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

lab/README.md

# Go 服务工程实验

《Go 服务工程:并发、资源管理与故障诊断》的独立实验工程。只依赖 Go 标准库,实测 Go 1.24.3;在 macOS/Linux 上解压本目录后直接执行:

```sh
go test -race ./...
```

Race 检测需要支持的平台、C 编译器和 `CGO_ENABLED=1`。普通测试可执行 `go test ./...`。进程集成测试发送真正的 SIGTERM,因此需要 Unix 信号语义。

## 启动与请求

```sh
go build -o /tmp/go-service-lab ./cmd/service
/tmp/go-service-lab
```

在另一个终端执行:

```sh
curl -i -X POST 'http://127.0.0.1:8080/tasks?delay=250ms'
curl 'http://127.0.0.1:8080/stats'
curl 'http://127.0.0.1:8080/work?delay=250ms'
curl -o /dev/null 'http://127.0.0.1:8080/stream?bytes=1048576'
curl -i -X POST 'http://127.0.0.1:8080/tasks?url=https%3A%2F%2Fexample.com'
```

`POST /tasks` 返回 202 和任务 ID;这是进入内存队列的确认,不是任务完成或持久化的确认。队列已满或已经关闭时返回 503 和 `Retry-After: 1`。ID 允许有空号。`GET /work` 在当前请求中等待;`GET /stream` 写指定字节数,默认 64 MiB,最大 256 MiB。`GET /healthz` 检查 HTTP 存活。`/stats` 是多个原子计数器的近似同时快照,运行中不保证跨字段等式;停稳后的计数可用于核对接收、完成和释放边界。

| 环境变量 | 默认值 | 含义 |
| --- | --- | --- |
| `LISTEN_ADDR` | `127.0.0.1:8080` | 业务 HTTP 监听地址;`:0` 分配临时端口 |
| `ADMIN_ADDR` | `127.0.0.1:6060` | 独立 pprof 监听地址 |
| `WORKERS` | `2` | 固定后台 worker 数 |
| `QUEUE_CAPACITY` | `8` | 等待队列上限,不含正在执行的任务 |
| `TASK_TIMEOUT` | `5s` | worker 开始执行后每个任务的超时 |
| `STREAM_WRITE_TIMEOUT` | `250ms` | 整段流式响应的绝对写截止 |
| `SHUTDOWN_TIMEOUT` | `5s` | HTTP 请求排空和后台任务排空共享的预算 |

将 `WORKERS=1 QUEUE_CAPACITY=1` 设置后并发提交多个 `delay=5s` 任务,可观察过载拒绝。实验的 URL 参数允许访问调用者提供的 HTTP(S) 地址;服务和 pprof 默认只监听本机。本工程不包含身份认证、URL 出站策略和磁盘队列。

## 所有权与退出约定

- `Pool.Submit(ctx, Work)` 中的 `ctx` 只控制入队。任务一旦接受,就由 Pool 的根 context 和每任务截止管理。任务闭包只保存参数,不保存请求、ResponseWriter 或请求 context。
- Pool 用固定 worker 和有界 channel 控制并发;入队时使用 `select/default`,不等待队列空位。并发关闭与入队由同一个锁保护。`Close` 停止接收并排空队列,`Shutdown(ctx)` 在预算用尽后发出取消,`Done()` 表示 worker 已经实际返回。
- 任务超时从执行开始计算,不包含排队时间。被强制取消的排队任务会被 worker 取出并记为取消完成,不会调用业务函数。`Work` 必须响应 context 并释放自己的资源;Go 无法强杀一个忽略取消的 goroutine。任务 panic 会传播并终止进程。
- SIGTERM/SIGINT 先停止 HTTP 监听并等待现有 handler,再关闭任务入口并排空已接受任务。共享预算到期后关闭 HTTP 连接、取消任务,然后额外等待最多 1 秒合作清理。`App.DrainRequests(ctx)` 显式等待 handler 返回,因为 `http.Server.Close` 本身不等待 handler;它先关闭 handler 入口,再等待 WaitGroup,避免零计数时 Add 与 Wait 的竞态。超出预算以非零状态退出;正常排空为零。
- Fetcher 的 Client/Transport 全程共享。所有响应体只有一个关闭者;小响应包括错误状态都读到 EOF 再 Close,以复用连接。响应最多消费 1 MiB+1 字节,超限关闭响应体并放弃该连接的复用。连接数、拨号、TLS 握手、响应头等待和任务总时长分别有界。
- `/stream` 使用 `http.ResponseController.SetWriteDeadline` 为整个流设绝对截止。HTTP/1.1 响应设置 `Connection: close`,让截止继续覆盖 net/http 的最后一次缓冲刷新;不在 handler 返回前清除截止。普通路由继续使用 keep-alive。这里的时间限制是整段流上限,不是每次成功写入后的空闲计时。

## 可重复故障实验

```sh
go test -race ./internal/service -run 'Test(PoolQueueBound|PoolBlockedCancellationReleases|PoolConcurrentCloseAndSubmit|ConnectionExhaustionAndRecovery|FetcherBlockedBodyCancellationReleasesConnection|SlowStreamDeadlineReleasesHandler|DrainRequestsWaitsForHandlerAndGatesAdmission)$' -count=30 -timeout=60s
go test -race ./cmd/service -run TestSIGTERM -count=10 -timeout=60s
go test ./internal/service -run '^$' -bench BenchmarkPoolSubmit -benchmem -count=3
```

所有故障测试有自己的等待上限。连接耗尽测试使用真实 TCP:故意占住一个未读响应体,让后续请求等待连接至 context 超时,然后关闭响应体并验证恢复。另一个测试让响应体读取阻塞,验证取消传播到服务端并恢复连接槽。慢客户端测试发送请求后完全不读 256 MiB 响应,验证写截止使 handler 返回。子进程测试直接运行生产 `main` 和 signal handler,检查在途请求、运行任务、排队任务,以及截止后请求/任务取消后的实际释放。

基准的每个 op 都最终成功入队;队列满时 `runtime.Gosched()` 重试的成本也计算在内。它测量包含 worker/context/日志格式化的池吞吐,不代表 HTTP 服务吞吐或单次 Submit 的延迟。结果、机器信息及采样摘要见 [EXPERIMENTS.md](EXPERIMENTS.md)。

## pprof

运行服务时可保存在线采样:

```sh
mkdir -p profiles
curl -o profiles/live.cpu.pprof 'http://127.0.0.1:6060/debug/pprof/profile?seconds=10'
curl -o profiles/live.heap.pprof 'http://127.0.0.1:6060/debug/pprof/heap?gc=1'
curl -o profiles/live.goroutine.txt 'http://127.0.0.1:6060/debug/pprof/goroutine?debug=2'
go tool pprof -top -inuse_space profiles/live.heap.pprof
```

采 CPU 时在另一个终端运行负载;空闲服务的 profile 可能几乎没有业务样本。代码包附带可读 goroutine 快照、profile 摘要和本次采样的 SHA256,二进制 profile 由以下命令重新生成:

```sh
go test ./internal/service -run '^$' -bench BenchmarkPoolSubmit -benchtime=2s -benchmem -cpuprofile profiles/pool.cpu.pprof -memprofile profiles/pool.heap.pprof -o /tmp/go-service-profile.test
go tool pprof -top profiles/pool.cpu.pprof
go tool pprof -top -alloc_space profiles/pool.heap.pprof
```

`alloc_space` 是累计分配采样,`inuse_space` 是采样时仍存活的堆,不能混用。采样受平台、时长和采样率影响;小负载的 heap profile 可能只显示 runtime,不能据此断言业务没有分配。

源代码及随附文档按 [MIT License](LICENSE) 发布。

lab/cmd/service/main.go

package main

import (
    "context"
    "errors"
    "fmt"
    "log/slog"
    "net"
    "net/http"
    _ "net/http/pprof"
    "os"
    "os/signal"
    "strconv"
    "syscall"
    "time"

    "example.com/go-service-engineering/lab/internal/service"
)

func env(key, fallback string) string {
    if value := os.Getenv(key); value != "" {
        return value
    }
    return fallback
}

func main() {
    logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
    ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
    defer stop()
    if err := run(ctx, logger); err != nil {
        logger.Error("service stopped", "error", err)
        os.Exit(1)
    }
}

func run(ctx context.Context, logger *slog.Logger) error {
    cfg := service.DefaultConfig()
    cfg.Logger = logger
    var err error
    if cfg.Workers, err = strconv.Atoi(env("WORKERS", "2")); err != nil {
        return fmt.Errorf("WORKERS: %w", err)
    }
    if cfg.QueueCapacity, err = strconv.Atoi(env("QUEUE_CAPACITY", "8")); err != nil {
        return fmt.Errorf("QUEUE_CAPACITY: %w", err)
    }
    if cfg.TaskTimeout, err = time.ParseDuration(env("TASK_TIMEOUT", "5s")); err != nil {
        return fmt.Errorf("TASK_TIMEOUT: %w", err)
    }
    if cfg.StreamWriteTimeout, err = time.ParseDuration(env("STREAM_WRITE_TIMEOUT", "250ms")); err != nil {
        return fmt.Errorf("STREAM_WRITE_TIMEOUT: %w", err)
    }
    grace, err := time.ParseDuration(env("SHUTDOWN_TIMEOUT", "5s"))
    if err != nil || grace <= 0 {
        return errors.New("SHUTDOWN_TIMEOUT must be a positive duration")
    }
    app, err := service.New(cfg)
    if err != nil {
        return err
    }
    defer app.Fetcher.Close()
    defer func() {
        cleanup, cancel := context.WithCancel(context.Background())
        cancel()
        _ = app.Pool.Shutdown(cleanup)
    }()
    listener, err := net.Listen("tcp", env("LISTEN_ADDR", "127.0.0.1:8080"))
    if err != nil {
        return err
    }
    defer listener.Close()
    adminListener, err := net.Listen("tcp", env("ADMIN_ADDR", "127.0.0.1:6060"))
    if err != nil {
        return err
    }
    defer adminListener.Close()
    server := &http.Server{Handler: app.Handler(), ReadHeaderTimeout: 2 * time.Second, ReadTimeout: 5 * time.Second, IdleTimeout: 30 * time.Second, MaxHeaderBytes: 16 << 10}
    admin := &http.Server{Handler: http.DefaultServeMux, ReadHeaderTimeout: 2 * time.Second, IdleTimeout: 30 * time.Second}
    defer server.Close()
    defer admin.Close()
    serveErrors := make(chan error, 2)
    go func() { serveErrors <- server.Serve(listener) }()
    go func() { serveErrors <- admin.Serve(adminListener) }()
    logger.Info("listening", "address", listener.Addr().String(), "admin", adminListener.Addr().String())
    select {
    case <-ctx.Done():
    case err := <-serveErrors:
        if !errors.Is(err, http.ErrServerClosed) {
            return err
        }
    }
    logger.Info("shutdown started")
    shutdown, cancel := context.WithTimeout(context.Background(), grace)
    defer cancel()
    // Shutdown first stops admission at the listener and waits for request
    // handlers. Only then close task admission and drain accepted background work.
    httpErr := server.Shutdown(shutdown)
    if httpErr != nil {
        _ = server.Close()
    }
    _ = admin.Close()
    poolErr := app.Pool.Shutdown(shutdown)
    // Closing sockets cancels request contexts, but Server.Close does not wait
    // for their handlers. Explicitly wait for cooperative cleanup as well.
    cleanup, cleanupCancel := context.WithTimeout(context.Background(), time.Second)
    defer cleanupCancel()
    requestErr := app.DrainRequests(cleanup)
    select {
    case <-app.Pool.Done():
    case <-cleanup.Done():
        return errors.New("workers ignored cancellation for one second")
    }
    logger.Info("shutdown complete", "stats", app.Pool.Stats(), "http_error", httpErr, "pool_error", poolErr, "request_error", requestErr)
    return errors.Join(httpErr, poolErr, requestErr)
}

lab/cmd/service/main_test.go

package main

import (
    "bufio"
    "bytes"
    "encoding/json"
    "fmt"
    "io"
    "net/http"
    "os"
    "os/exec"
    "strings"
    "sync"
    "syscall"
    "testing"
    "time"
)

// The helper is a real child process running the production main and signal
// handler. It also runs under the race runtime when invoked by go test -race.
func TestProcessHelper(t *testing.T) {
    if os.Getenv("GO_SERVICE_PROCESS_HELPER") != "1" {
        return
    }
    main()
    os.Exit(0)
}

type process struct {
    cmd    *exec.Cmd
    events chan map[string]any
    done   chan error
    mu     sync.Mutex
    logs   bytes.Buffer
}

func startProcess(t *testing.T, grace string) (*process, string) {
    t.Helper()
    cmd := exec.Command(os.Args[0], "-test.run=^TestProcessHelper$")
    cmd.Env = append(os.Environ(), "GO_SERVICE_PROCESS_HELPER=1", "LISTEN_ADDR=127.0.0.1:0", "ADMIN_ADDR=127.0.0.1:0", "WORKERS=1", "QUEUE_CAPACITY=2", "TASK_TIMEOUT=10s", "SHUTDOWN_TIMEOUT="+grace)
    stdout, err := cmd.StdoutPipe()
    if err != nil {
        t.Fatal(err)
    }
    cmd.Stderr = cmd.Stdout
    p := &process{cmd: cmd, events: make(chan map[string]any, 128), done: make(chan error, 1)}
    if err := cmd.Start(); err != nil {
        t.Fatal(err)
    }
    go func() {
        scanner := bufio.NewScanner(stdout)
        for scanner.Scan() {
            p.mu.Lock()
            _, _ = fmt.Fprintln(&p.logs, scanner.Text())
            p.mu.Unlock()
            var event map[string]any
            if json.Unmarshal(scanner.Bytes(), &event) == nil {
                p.events <- event
            }
        }
        p.done <- cmd.Wait()
    }()
    t.Cleanup(func() { _ = cmd.Process.Kill() })
    event := p.waitEvent(t, "listening")
    return p, "http://" + event["address"].(string)
}

func (p *process) output() string { p.mu.Lock(); defer p.mu.Unlock(); return p.logs.String() }

func (p *process) waitEvent(t *testing.T, message string) map[string]any {
    t.Helper()
    timer := time.NewTimer(5 * time.Second)
    defer timer.Stop()
    for {
        select {
        case event := <-p.events:
            if event["msg"] == message {
                return event
            }
        case <-timer.C:
            t.Fatalf("missing event %q; logs:\n%s", message, p.output())
            return nil
        }
    }
}

func postTask(t *testing.T, url string) {
    t.Helper()
    client := &http.Client{Timeout: 2 * time.Second}
    resp, err := client.Post(url, "", nil)
    if err != nil {
        t.Fatal(err)
    }
    defer resp.Body.Close()
    _, _ = io.Copy(io.Discard, resp.Body)
    if resp.StatusCode != http.StatusAccepted {
        t.Fatalf("task status: %d", resp.StatusCode)
    }
}

func TestSIGTERMDrainsRequestAndAcceptedTasks(t *testing.T) {
    if testing.Short() {
        t.Skip("real process integration test")
    }
    p, url := startProcess(t, "3s")
    postTask(t, url+"/tasks?delay=350ms")
    p.waitEvent(t, "task started")
    postTask(t, url+"/tasks?delay=150ms") // queued, also must drain
    requestDone := make(chan error, 1)
    go func() {
        client := &http.Client{Timeout: 3 * time.Second}
        resp, err := client.Get(url + "/work?delay=150ms")
        if err == nil {
            _, err = io.Copy(io.Discard, resp.Body)
            _ = resp.Body.Close()
            if resp.StatusCode != http.StatusOK {
                err = fmt.Errorf("request status %d", resp.StatusCode)
            }
        }
        requestDone <- err
    }()
    p.waitEvent(t, "request started")
    if err := p.cmd.Process.Signal(syscall.SIGTERM); err != nil {
        t.Fatal(err)
    }
    p.waitEvent(t, "shutdown started")
    select {
    case err := <-requestDone:
        if err != nil {
            t.Fatalf("in-flight request lost: %v", err)
        }
    case <-time.After(4 * time.Second):
        t.Fatal("request did not finish")
    }
    event := p.waitEvent(t, "shutdown complete")
    stats := event["stats"].(map[string]any)
    if stats["accepted"] != float64(2) || stats["completed"] != float64(2) || stats["failed"] != float64(0) || stats["active"] != float64(0) {
        t.Fatalf("task boundary violated: %v", stats)
    }
    select {
    case err := <-p.done:
        if err != nil {
            t.Fatalf("process: %v\n%s", err, p.output())
        }
    case <-time.After(4 * time.Second):
        t.Fatal("process did not exit")
    }
    // Listener must be gone after shutdown, not merely return a health error.
    client := &http.Client{Timeout: 200 * time.Millisecond}
    if resp, err := client.Get(url + "/healthz"); err == nil {
        resp.Body.Close()
        t.Fatal("listener still accepts after exit")
    }
    logs := p.output()
    if strings.Index(logs, "request finished") > strings.Index(logs, "shutdown complete") {
        t.Fatal("shutdown returned before handler completion")
    }
}

func TestSIGTERMDeadlineCancelsBlockedTask(t *testing.T) {
    if testing.Short() {
        t.Skip("real process integration test")
    }
    p, url := startProcess(t, "80ms")
    postTask(t, url+"/tasks?delay=5s")
    p.waitEvent(t, "task started")
    requestDone := make(chan error, 1)
    go func() {
        client := &http.Client{Timeout: 3 * time.Second}
        resp, err := client.Get(url + "/work?delay=5s")
        if err == nil {
            _, err = io.Copy(io.Discard, resp.Body)
            _ = resp.Body.Close()
        }
        requestDone <- err
    }()
    p.waitEvent(t, "request started")
    if err := p.cmd.Process.Signal(syscall.SIGTERM); err != nil {
        t.Fatal(err)
    }
    event := p.waitEvent(t, "shutdown complete")
    stats := event["stats"].(map[string]any)
    if stats["cancelled"] != float64(1) || stats["completed"] != float64(1) || stats["active"] != float64(0) {
        t.Fatalf("cancelled task did not release: %v", stats)
    }
    select {
    case err := <-requestDone:
        if err == nil {
            t.Fatal("deadline-expired request unexpectedly succeeded")
        }
    case <-time.After(3 * time.Second):
        t.Fatal("closed request did not return")
    }
    logs := p.output()
    if !strings.Contains(logs, `"msg":"request finished","path":"/work","error":"context canceled"`) {
        t.Fatalf("request cleanup was not observed before shutdown completion:\n%s", logs)
    }
    select {
    case err := <-p.done:
        if err == nil {
            t.Fatal("deadline must produce nonzero exit status")
        }
    case <-time.After(4 * time.Second):
        t.Fatal("process did not exit after cancellation")
    }
}

lab/go.mod

module example.com/go-service-engineering/lab

go 1.24.3

lab/internal/service/app.go

package service

import (
    "context"
    "encoding/json"
    "errors"
    "fmt"
    "log/slog"
    "net/http"
    "net/url"
    "strconv"
    "sync"
    "time"
)

type Config struct {
    Workers            int
    QueueCapacity      int
    TaskTimeout        time.Duration
    StreamWriteTimeout time.Duration
    Logger             *slog.Logger
}

func DefaultConfig() Config {
    return Config{Workers: 2, QueueCapacity: 8, TaskTimeout: 5 * time.Second, StreamWriteTimeout: 250 * time.Millisecond}
}

type App struct {
    Pool           *Pool
    Fetcher        *Fetcher
    cfg            Config
    log            *slog.Logger
    requestMu      sync.Mutex
    requestsClosed bool
    requests       sync.WaitGroup
}

func New(cfg Config) (*App, error) {
    if cfg.StreamWriteTimeout <= 0 {
        return nil, fmt.Errorf("stream write timeout must be positive")
    }
    if cfg.Logger == nil {
        cfg.Logger = slog.Default()
    }
    pool, err := NewPool(cfg.Workers, cfg.QueueCapacity, cfg.TaskTimeout, cfg.Logger)
    if err != nil {
        return nil, err
    }
    return &App{Pool: pool, Fetcher: NewFetcher(2), cfg: cfg, log: cfg.Logger}, nil
}

func (a *App) Handler() http.Handler {
    mux := http.NewServeMux()
    mux.HandleFunc("POST /tasks", a.tasks)
    mux.HandleFunc("GET /work", a.work)
    mux.HandleFunc("GET /stream", a.stream)
    mux.HandleFunc("GET /stats", func(w http.ResponseWriter, r *http.Request) { writeJSON(w, http.StatusOK, a.Pool.Stats()) })
    mux.HandleFunc("GET /healthz", func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) })
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        a.requestMu.Lock()
        if a.requestsClosed {
            a.requestMu.Unlock()
            http.Error(w, "requests draining", http.StatusServiceUnavailable)
            return
        }
        a.requests.Add(1)
        a.requestMu.Unlock()
        defer a.requests.Done()
        mux.ServeHTTP(w, r)
    })
}

// DrainRequests gates new handlers and waits for existing handlers to return.
// Call it after stopping the listener. The gate prevents Add racing with Wait
// when the previous request count is zero. It does not itself cancel requests.
func (a *App) DrainRequests(ctx context.Context) error {
    a.requestMu.Lock()
    a.requestsClosed = true
    a.requestMu.Unlock()
    done := make(chan struct{})
    go func() { a.requests.Wait(); close(done) }()
    select {
    case <-done:
        return nil
    case <-ctx.Done():
        return ctx.Err()
    }
}

func duration(r *http.Request) (time.Duration, error) {
    s := r.URL.Query().Get("delay")
    if s == "" {
        return 100 * time.Millisecond, nil
    }
    d, err := time.ParseDuration(s)
    if err != nil || d < 0 || d > 30*time.Second {
        return 0, errors.New("delay must be between 0 and 30s")
    }
    return d, nil
}

func wait(ctx context.Context, d time.Duration) error {
    timer := time.NewTimer(d)
    defer timer.Stop()
    select {
    case <-timer.C:
        return nil
    case <-ctx.Done():
        return ctx.Err()
    }
}

func (a *App) tasks(w http.ResponseWriter, r *http.Request) {
    d, err := duration(r)
    if err != nil {
        http.Error(w, err.Error(), http.StatusBadRequest)
        return
    }
    target := r.URL.Query().Get("url")
    if target != "" {
        u, parseErr := url.Parse(target)
        if parseErr != nil || (u.Scheme != "http" && u.Scheme != "https") || u.Host == "" {
            http.Error(w, "url must be an absolute http(s) URL", http.StatusBadRequest)
            return
        }
    }
    // Capture values only. Do not retain r, ResponseWriter, or r.Context().
    id, err := a.Pool.Submit(r.Context(), func(ctx context.Context) error {
        if target != "" {
            _, err := a.Fetcher.Fetch(ctx, target)
            return err
        }
        return wait(ctx, d)
    })
    if err != nil {
        status := http.StatusServiceUnavailable
        if errors.Is(err, context.Canceled) {
            status = http.StatusRequestTimeout
        }
        w.Header().Set("Retry-After", "1")
        http.Error(w, err.Error(), status)
        return
    }
    writeJSON(w, http.StatusAccepted, map[string]uint64{"id": id})
}

func (a *App) work(w http.ResponseWriter, r *http.Request) {
    d, err := duration(r)
    if err != nil {
        http.Error(w, err.Error(), http.StatusBadRequest)
        return
    }
    a.log.Info("request started", "path", "/work")
    err = wait(r.Context(), d)
    a.log.Info("request finished", "path", "/work", "error", err)
    if err != nil {
        return
    }
    writeJSON(w, http.StatusOK, map[string]string{"status": "done"})
}

func (a *App) stream(w http.ResponseWriter, r *http.Request) {
    n := int64(64 << 20)
    if raw := r.URL.Query().Get("bytes"); raw != "" {
        value, err := strconv.ParseInt(raw, 10, 64)
        if err != nil || value < 1 || value > 256<<20 {
            http.Error(w, "bytes must be 1..268435456", http.StatusBadRequest)
            return
        }
        n = value
    }
    controller := http.NewResponseController(w)
    // One absolute deadline bounds the entire stream, including blocked writes.
    if err := controller.SetWriteDeadline(time.Now().Add(a.cfg.StreamWriteTimeout)); err != nil {
        http.Error(w, "write deadlines unavailable", http.StatusInternalServerError)
        return
    }
    // Keep the deadline active through net/http's final buffered flush. This
    // HTTP/1.1 demo closes streaming connections rather than reusing one with a
    // stale deadline. Other endpoints retain ordinary keep-alive behavior.
    w.Header().Set("Connection", "close")
    w.Header().Set("Content-Type", "application/octet-stream")
    chunk := make([]byte, 32<<10)
    var sent int64
    for sent < n {
        if err := r.Context().Err(); err != nil {
            a.log.Info("stream finished", "bytes", sent, "error", err)
            return
        }
        size := min(int64(len(chunk)), n-sent)
        written, err := w.Write(chunk[:size])
        sent += int64(written)
        if err != nil {
            a.log.Info("stream finished", "bytes", sent, "error", err)
            return
        }
    }
    a.log.Info("stream finished", "bytes", sent)
}

func writeJSON(w http.ResponseWriter, status int, value any) {
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(status)
    _ = json.NewEncoder(w).Encode(value)
}

lab/internal/service/app_test.go

package service

import (
    "context"
    "errors"
    "fmt"
    "io"
    "log/slog"
    "net"
    "net/http"
    "net/http/httptest"
    "strings"
    "sync"
    "testing"
    "time"
)

func app(t *testing.T, cfg Config) *App {
    t.Helper()
    a, err := New(cfg)
    if err != nil {
        t.Fatal(err)
    }
    t.Cleanup(func() {
        ctx, cancel := context.WithTimeout(context.Background(), time.Second)
        defer cancel()
        _ = a.Pool.Shutdown(ctx)
        receive(t, a.Pool.Done())
        a.Fetcher.Close()
    })
    return a
}

func eventually(t *testing.T, condition func() bool) {
    t.Helper()
    deadline := time.NewTimer(3 * time.Second)
    defer deadline.Stop()
    tick := time.NewTicker(time.Millisecond)
    defer tick.Stop()
    for !condition() {
        select {
        case <-tick.C:
        case <-deadline.C:
            t.Fatal("condition did not become true")
        }
    }
}

func TestHTTPOverloadAndTaskOwnership(t *testing.T) {
    cfg := DefaultConfig()
    cfg.Workers, cfg.QueueCapacity, cfg.Logger = 1, 1, quiet()
    a := app(t, cfg)
    srv := httptest.NewServer(a.Handler())
    defer srv.Close()
    ctx, cancel := context.WithCancel(context.Background())
    req, _ := http.NewRequestWithContext(ctx, http.MethodPost, srv.URL+"/tasks?delay=150ms", nil)
    resp, err := srv.Client().Do(req)
    if err != nil {
        t.Fatal(err)
    }
    _, _ = io.Copy(io.Discard, resp.Body)
    _ = resp.Body.Close()
    if resp.StatusCode != http.StatusAccepted {
        t.Fatalf("first task: %d", resp.StatusCode)
    }
    cancel() // Accepted task is owned by the pool, not this now-ended request.
    eventually(t, func() bool { return a.Pool.Stats().Active == 1 })
    for _, want := range []int{http.StatusAccepted, http.StatusServiceUnavailable} {
        resp, err := srv.Client().Post(srv.URL+"/tasks?delay=150ms", "", nil)
        if err != nil {
            t.Fatal(err)
        }
        _, _ = io.Copy(io.Discard, resp.Body)
        _ = resp.Body.Close()
        if resp.StatusCode != want {
            t.Fatalf("got %d, want %d", resp.StatusCode, want)
        }
        if want == http.StatusServiceUnavailable && resp.Header.Get("Retry-After") != "1" {
            t.Fatal("missing retry hint")
        }
    }
    a.Pool.Close()
    receive(t, a.Pool.Done())
    if s := a.Pool.Stats(); s.Completed != 2 || s.Failed != 0 {
        t.Fatalf("request cancellation leaked into task: %+v", s)
    }
}

type lockedBuffer struct {
    mu   sync.Mutex
    text strings.Builder
}

func (b *lockedBuffer) Write(p []byte) (int, error) {
    b.mu.Lock()
    defer b.mu.Unlock()
    return b.text.Write(p)
}
func (b *lockedBuffer) String() string { b.mu.Lock(); defer b.mu.Unlock(); return b.text.String() }

func TestSlowStreamDeadlineReleasesHandler(t *testing.T) {
    var logs lockedBuffer
    cfg := DefaultConfig()
    cfg.StreamWriteTimeout = 80 * time.Millisecond
    cfg.Logger = slog.New(slog.NewTextHandler(&logs, nil))
    a := app(t, cfg)
    finished := make(chan struct{})
    srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        a.Handler().ServeHTTP(w, r)
        close(finished)
    }))
    defer srv.Close()
    conn, err := net.DialTimeout("tcp", srv.Listener.Addr().String(), time.Second)
    if err != nil {
        t.Fatal(err)
    }
    defer conn.Close()
    if tcp, ok := conn.(*net.TCPConn); ok {
        _ = tcp.SetReadBuffer(1024)
    }
    // A real peer sends its request and never reads the 256 MiB response.
    if _, err := fmt.Fprint(conn, "GET /stream?bytes=268435456 HTTP/1.1\r\nHost: localhost\r\n\r\n"); err != nil {
        t.Fatal(err)
    }
    receive(t, finished)
    if text := logs.String(); !strings.Contains(text, "stream finished") || !strings.Contains(text, "i/o timeout") {
        t.Fatalf("stream did not hit its write deadline: %s", text)
    }
}

func TestHTTPInvalidInput(t *testing.T) {
    cfg := DefaultConfig()
    cfg.Logger = quiet()
    a := app(t, cfg)
    for _, path := range []string{"/tasks?delay=-1s", "/tasks?url=file:///etc/passwd", "/stream?bytes=999999999"} {
        method := http.MethodPost
        if strings.HasPrefix(path, "/stream") {
            method = http.MethodGet
        }
        rr := httptest.NewRecorder()
        a.Handler().ServeHTTP(rr, httptest.NewRequest(method, path, nil))
        if rr.Code != http.StatusBadRequest {
            t.Errorf("%s: %d", path, rr.Code)
        }
    }
}

func TestDrainRequestsWaitsForHandlerAndGatesAdmission(t *testing.T) {
    var logs lockedBuffer
    cfg := DefaultConfig()
    cfg.Logger = slog.New(slog.NewTextHandler(&logs, nil))
    a := app(t, cfg)
    ctx, cancel := context.WithCancel(context.Background())
    defer cancel()
    finished := make(chan struct{})
    go func() {
        a.Handler().ServeHTTP(httptest.NewRecorder(), httptest.NewRequest(http.MethodGet, "/work?delay=5s", nil).WithContext(ctx))
        close(finished)
    }()
    eventually(t, func() bool { return strings.Contains(logs.String(), "request started") })
    short, stop := context.WithTimeout(context.Background(), 20*time.Millisecond)
    defer stop()
    if err := a.DrainRequests(short); !errors.Is(err, context.DeadlineExceeded) {
        t.Fatalf("drain returned while handler still owned resources: %v", err)
    }
    cancel()
    receive(t, finished)
    long, end := context.WithTimeout(context.Background(), time.Second)
    defer end()
    if err := a.DrainRequests(long); err != nil {
        t.Fatal(err)
    }
    rr := httptest.NewRecorder()
    a.Handler().ServeHTTP(rr, httptest.NewRequest(http.MethodGet, "/healthz", nil))
    if rr.Code != http.StatusServiceUnavailable {
        t.Fatalf("new handler admitted during drain: %d", rr.Code)
    }
}

lab/internal/service/client.go

package service

import (
    "context"
    "fmt"
    "io"
    "net"
    "net/http"
    "time"
)

// Fetcher reuses one transport. Every response body has exactly one owner,
// including error-status bodies; reading EOF permits HTTP/1.1 connection reuse.
type Fetcher struct {
    Client    *http.Client
    Transport *http.Transport
}

func NewFetcher(maxConns int) *Fetcher {
    tr := &http.Transport{
        Proxy:                 http.ProxyFromEnvironment,
        DialContext:           (&net.Dialer{Timeout: 2 * time.Second, KeepAlive: 30 * time.Second}).DialContext,
        MaxConnsPerHost:       maxConns,
        MaxIdleConns:          32,
        MaxIdleConnsPerHost:   maxConns,
        IdleConnTimeout:       30 * time.Second,
        TLSHandshakeTimeout:   2 * time.Second,
        ResponseHeaderTimeout: 2 * time.Second,
        ForceAttemptHTTP2:     true,
    }
    return &Fetcher{Client: &http.Client{Transport: tr}, Transport: tr}
}

// Fetch limits response bytes. An oversized response is closed before EOF,
// intentionally sacrificing reuse rather than consuming unbounded bandwidth.
func (f *Fetcher) Fetch(ctx context.Context, url string) (int64, error) {
    req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
    if err != nil {
        return 0, err
    }
    resp, err := f.Client.Do(req)
    if err != nil {
        return 0, err
    }
    defer resp.Body.Close()
    const maxBody = 1 << 20
    n, err := io.Copy(io.Discard, io.LimitReader(resp.Body, maxBody+1))
    if err != nil {
        return n, err
    }
    if n > maxBody {
        return n, fmt.Errorf("response exceeds %d bytes", maxBody)
    }
    if resp.StatusCode < 200 || resp.StatusCode > 299 {
        return n, fmt.Errorf("upstream status %d", resp.StatusCode)
    }
    return n, nil
}

func (f *Fetcher) Close() { f.Transport.CloseIdleConnections() }

lab/internal/service/client_test.go

package service

import (
    "context"
    "errors"
    "io"
    "net"
    "net/http"
    "net/http/httptest"
    "sync/atomic"
    "testing"
    "time"
)

func TestConnectionExhaustionAndRecovery(t *testing.T) {
    released := make(chan struct{})
    var okRequests atomic.Int64
    srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        if r.URL.Path == "/hold" {
            w.WriteHeader(http.StatusOK)
            w.(http.Flusher).Flush()
            <-r.Context().Done()
            close(released)
            return
        }
        okRequests.Add(1)
        _, _ = io.WriteString(w, "recovered")
    }))
    defer srv.Close()
    f := NewFetcher(1)
    defer f.Close()
    resp, err := f.Client.Get(srv.URL + "/hold")
    if err != nil {
        t.Fatal(err)
    }
    defer resp.Body.Close()
    ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
    defer cancel()
    if _, err := f.Fetch(ctx, srv.URL+"/ok"); !errors.Is(err, context.DeadlineExceeded) {
        t.Fatalf("waiting for occupied connection: %v", err)
    }
    if got := okRequests.Load(); got != 0 {
        t.Fatalf("request bypassed connection bound: %d", got)
    }
    _ = resp.Body.Close()
    receive(t, released)
    recovery, stop := context.WithTimeout(context.Background(), time.Second)
    defer stop()
    if n, err := f.Fetch(recovery, srv.URL+"/ok"); err != nil || n != 9 {
        t.Fatalf("recovery: bytes=%d error=%v", n, err)
    }
}

func TestFetcherReusesConnectionIncludingErrorStatus(t *testing.T) {
    var connections atomic.Int64
    srv := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        if r.URL.Path == "/error" {
            w.WriteHeader(http.StatusBadGateway)
        }
        _, _ = io.WriteString(w, "small body")
    }))
    srv.Config.ConnState = func(_ net.Conn, state http.ConnState) {
        if state == http.StateNew {
            connections.Add(1)
        }
    }
    srv.Start()
    defer srv.Close()
    f := NewFetcher(1)
    defer f.Close()
    ctx, cancel := context.WithTimeout(context.Background(), time.Second)
    defer cancel()
    if _, err := f.Fetch(ctx, srv.URL+"/error"); err == nil {
        t.Fatal("expected status error")
    }
    for range 3 {
        if _, err := f.Fetch(ctx, srv.URL); err != nil {
            t.Fatal(err)
        }
    }
    if n := connections.Load(); n != 1 {
        t.Fatalf("body was not drained/reused: connections=%d", n)
    }
}

func TestFetcherBlockedBodyCancellationReleasesConnection(t *testing.T) {
    released := make(chan struct{})
    srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        if r.URL.Path == "/blocked" {
            w.WriteHeader(http.StatusOK)
            w.(http.Flusher).Flush()
            <-r.Context().Done()
            close(released)
            return
        }
        _, _ = io.WriteString(w, "ok")
    }))
    defer srv.Close()
    f := NewFetcher(1)
    defer f.Close()
    ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
    defer cancel()
    if _, err := f.Fetch(ctx, srv.URL+"/blocked"); !errors.Is(err, context.DeadlineExceeded) {
        t.Fatalf("blocked body: %v", err)
    }
    receive(t, released)
    recovery, stop := context.WithTimeout(context.Background(), time.Second)
    defer stop()
    if _, err := f.Fetch(recovery, srv.URL); err != nil {
        t.Fatalf("connection slot not recovered: %v", err)
    }
}

lab/internal/service/pool.go

// Package service demonstrates bounded concurrency and explicit task ownership.
package service

import (
    "context"
    "errors"
    "fmt"
    "log/slog"
    "sync"
    "sync/atomic"
    "time"
)

var (
    ErrFull   = errors.New("task queue full")
    ErrClosed = errors.New("task pool closed")
)

type Work func(context.Context) error

type task struct {
    id   uint64
    work Work
}

type Stats struct {
    Accepted  uint64 `json:"accepted"`
    Started   uint64 `json:"started"`
    Completed uint64 `json:"completed"`
    Failed    uint64 `json:"failed"`
    Cancelled uint64 `json:"cancelled"`
    Active    int64  `json:"active"`
    Queued    int    `json:"queued"`
}

// Pool owns every accepted task. A caller's context controls admission only;
// after admission, only the pool lifetime and per-task deadline cancel the task.
type Pool struct {
    mu        sync.Mutex
    closed    bool
    queue     chan task
    ctx       context.Context
    cancel    context.CancelFunc
    done      chan struct{}
    timeout   time.Duration
    log       *slog.Logger
    next      atomic.Uint64
    accepted  atomic.Uint64
    started   atomic.Uint64
    completed atomic.Uint64
    failed    atomic.Uint64
    cancelled atomic.Uint64
    active    atomic.Int64
}

func NewPool(workers, capacity int, timeout time.Duration, logger *slog.Logger) (*Pool, error) {
    if workers < 1 || capacity < 1 || timeout <= 0 {
        return nil, fmt.Errorf("workers, capacity, and timeout must be positive")
    }
    if logger == nil {
        logger = slog.Default()
    }
    ctx, cancel := context.WithCancel(context.Background())
    p := &Pool{queue: make(chan task, capacity), ctx: ctx, cancel: cancel, done: make(chan struct{}), timeout: timeout, log: logger}
    var wg sync.WaitGroup
    wg.Add(workers)
    for range workers {
        go func() {
            defer wg.Done()
            for t := range p.queue {
                p.run(t)
            }
        }()
    }
    go func() { wg.Wait(); cancel(); close(p.done) }()
    return p, nil
}

func (p *Pool) run(t task) {
    ctx, cancel := context.WithTimeout(p.ctx, p.timeout)
    defer cancel()
    p.active.Add(1)
    p.started.Add(1)
    p.log.Info("task started", "id", t.id)
    var err error
    if err = ctx.Err(); err == nil {
        err = t.work(ctx)
    }
    if err != nil {
        p.failed.Add(1)
        if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
            p.cancelled.Add(1)
        }
    }
    p.completed.Add(1)
    p.active.Add(-1)
    p.log.Info("task finished", "id", t.id, "error", err)
}

// Submit never waits for queue space. The queue bound excludes running tasks.
func (p *Pool) Submit(ctx context.Context, work Work) (uint64, error) {
    if work == nil {
        return 0, errors.New("nil work")
    }
    p.mu.Lock()
    defer p.mu.Unlock()
    if err := ctx.Err(); err != nil {
        return 0, err
    }
    if p.closed {
        return 0, ErrClosed
    }
    id := p.next.Add(1)
    select {
    case p.queue <- task{id: id, work: work}:
        p.accepted.Add(1)
        p.log.Info("task accepted", "id", id)
        return id, nil
    default:
        return 0, ErrFull
    }
}

// Close stops admission and lets workers drain the queue. It is idempotent.
func (p *Pool) Close() {
    p.mu.Lock()
    defer p.mu.Unlock()
    if !p.closed {
        p.closed = true
        close(p.queue)
    }
}

// Shutdown drains until the deadline, then signals cancellation. It cannot
// force a Work function that ignores context to stop. Done reports actual exit.
func (p *Pool) Shutdown(ctx context.Context) error {
    p.Close()
    select {
    case <-p.done:
        return nil
    case <-ctx.Done():
        p.cancel()
        return ctx.Err()
    }
}

func (p *Pool) Done() <-chan struct{} { return p.done }

func (p *Pool) Stats() Stats {
    return Stats{Accepted: p.accepted.Load(), Started: p.started.Load(), Completed: p.completed.Load(), Failed: p.failed.Load(), Cancelled: p.cancelled.Load(), Active: p.active.Load(), Queued: len(p.queue)}
}

lab/internal/service/pool_test.go

package service

import (
    "context"
    "errors"
    "io"
    "log/slog"
    "runtime"
    "sync"
    "testing"
    "time"
)

func quiet() *slog.Logger { return slog.New(slog.NewTextHandler(io.Discard, nil)) }

func receive(t *testing.T, ch <-chan struct{}) {
    t.Helper()
    select {
    case <-ch:
    case <-time.After(3 * time.Second):
        t.Fatal("timed out waiting for completion")
    }
}

func pool(t *testing.T, workers, capacity int, timeout time.Duration) *Pool {
    t.Helper()
    p, err := NewPool(workers, capacity, timeout, quiet())
    if err != nil {
        t.Fatal(err)
    }
    t.Cleanup(func() {
        ctx, cancel := context.WithTimeout(context.Background(), time.Second)
        defer cancel()
        _ = p.Shutdown(ctx)
        receive(t, p.Done())
    })
    return p
}

func TestPoolQueueBound(t *testing.T) {
    p := pool(t, 1, 2, time.Second)
    started, release := make(chan struct{}), make(chan struct{})
    _, err := p.Submit(context.Background(), func(ctx context.Context) error {
        close(started)
        select {
        case <-release:
            return nil
        case <-ctx.Done():
            return ctx.Err()
        }
    })
    if err != nil {
        t.Fatal(err)
    }
    receive(t, started)
    for range 2 {
        if _, err := p.Submit(context.Background(), func(context.Context) error { return nil }); err != nil {
            t.Fatal(err)
        }
    }
    if _, err := p.Submit(context.Background(), func(context.Context) error { return nil }); !errors.Is(err, ErrFull) {
        t.Fatalf("got %v, want ErrFull", err)
    }
    if s := p.Stats(); s.Queued != 2 || s.Active != 1 || s.Accepted != 3 {
        t.Fatalf("unexpected stats: %+v", s)
    }
    close(release)
    p.Close()
    receive(t, p.Done())
    if s := p.Stats(); s.Completed != 3 || s.Active != 0 || s.Queued != 0 {
        t.Fatalf("not drained: %+v", s)
    }
}

func TestAcceptedTaskOutlivesCaller(t *testing.T) {
    p := pool(t, 1, 1, time.Second)
    caller, cancel := context.WithCancel(context.Background())
    started, release, finished := make(chan struct{}), make(chan struct{}), make(chan struct{})
    _, err := p.Submit(caller, func(ctx context.Context) error {
        close(started)
        select {
        case <-release:
            close(finished)
            return nil
        case <-ctx.Done():
            return ctx.Err()
        }
    })
    if err != nil {
        t.Fatal(err)
    }
    receive(t, started)
    cancel()
    close(release)
    receive(t, finished)
    if _, err := p.Submit(caller, func(context.Context) error { return nil }); !errors.Is(err, context.Canceled) {
        t.Fatalf("cancelled admission: %v", err)
    }
}

func TestPoolBlockedCancellationReleases(t *testing.T) {
    p := pool(t, 1, 2, 5*time.Second)
    started, released := make(chan struct{}), make(chan struct{})
    _, err := p.Submit(context.Background(), func(ctx context.Context) error { close(started); <-ctx.Done(); close(released); return ctx.Err() })
    if err != nil {
        t.Fatal(err)
    }
    receive(t, started)
    // A queued function must never execute after forced cancellation.
    if _, err := p.Submit(context.Background(), func(context.Context) error { t.Error("cancelled queued work executed"); return nil }); err != nil {
        t.Fatal(err)
    }
    ctx, cancel := context.WithTimeout(context.Background(), 20*time.Millisecond)
    defer cancel()
    if err := p.Shutdown(ctx); !errors.Is(err, context.DeadlineExceeded) {
        t.Fatalf("shutdown: %v", err)
    }
    receive(t, released)
    receive(t, p.Done())
    if s := p.Stats(); s.Completed != 2 || s.Cancelled != 2 || s.Active != 0 {
        t.Fatalf("resources not released: %+v", s)
    }
}

func TestPoolTaskDeadline(t *testing.T) {
    p := pool(t, 1, 1, 20*time.Millisecond)
    _, err := p.Submit(context.Background(), func(ctx context.Context) error { <-ctx.Done(); return ctx.Err() })
    if err != nil {
        t.Fatal(err)
    }
    p.Close()
    receive(t, p.Done())
    if s := p.Stats(); s.Cancelled != 1 {
        t.Fatalf("deadline not observed: %+v", s)
    }
}

func TestPoolConcurrentCloseAndSubmit(t *testing.T) {
    p := pool(t, 4, 8, time.Second)
    var wg sync.WaitGroup
    for range 16 {
        wg.Add(1)
        go func() {
            defer wg.Done()
            for range 100 {
                _, err := p.Submit(context.Background(), func(context.Context) error { return nil })
                if err != nil && !errors.Is(err, ErrFull) && !errors.Is(err, ErrClosed) {
                    t.Errorf("submit: %v", err)
                }
            }
        }()
    }
    p.Close()
    wg.Wait()
    receive(t, p.Done())
    if s := p.Stats(); s.Completed != s.Accepted {
        t.Fatalf("accepted tasks lost: %+v", s)
    }
}

func BenchmarkPoolSubmit(b *testing.B) {
    p, _ := NewPool(4, 128, time.Second, quiet())
    defer func() { p.Close(); <-p.Done() }()
    b.ReportAllocs()
    for b.Loop() {
        for {
            _, err := p.Submit(context.Background(), func(context.Context) error { return nil })
            if err == nil {
                break
            }
            if !errors.Is(err, ErrFull) {
                b.Fatal(err)
            }
            runtime.Gosched()
        }
    }
}

lab/profiles/live.goroutine.txt

goroutine 49 [running]:
runtime/pprof.writeGoroutineStacks({0x104a93460, 0x140000e6380})
    /Users/aias/.local/share/go1.24.3/src/runtime/pprof/pprof.go:764 +0x6c
runtime/pprof.writeGoroutine({0x104a93460?, 0x140000e6380?}, 0x140000fa848?)
    /Users/aias/.local/share/go1.24.3/src/runtime/pprof/pprof.go:753 +0x2c
runtime/pprof.(*Profile).WriteTo(0x104cbf2f0?, {0x104a93460?, 0x140000e6380?}, 0xc?)
    /Users/aias/.local/share/go1.24.3/src/runtime/pprof/pprof.go:377 +0x14c
net/http/pprof.handler.ServeHTTP({0x14000202011, 0x9}, {0x104a95a68, 0x140000e6380}, 0x140000d8640)
    /Users/aias/.local/share/go1.24.3/src/net/http/pprof/pprof.go:272 +0x444
net/http/pprof.Index({0x104a95a68, 0x140000e6380}, 0x140000d8640?)
    /Users/aias/.local/share/go1.24.3/src/net/http/pprof/pprof.go:389 +0xc8
net/http.HandlerFunc.ServeHTTP(0x104ccbdc0?, {0x104a95a68?, 0x140000e6380?}, 0x1048be9b4?)
    /Users/aias/.local/share/go1.24.3/src/net/http/server.go:2294 +0x38
net/http.(*ServeMux).ServeHTTP(0x0?, {0x104a95a68, 0x140000e6380}, 0x140000d8640)
    /Users/aias/.local/share/go1.24.3/src/net/http/server.go:2822 +0x1b4
net/http.serverHandler.ServeHTTP({0x140000acf90?}, {0x104a95a68?, 0x140000e6380?}, 0x1?)
    /Users/aias/.local/share/go1.24.3/src/net/http/server.go:3301 +0xbc
net/http.(*conn).serve(0x140000aa750, {0x104a95d50, 0x14000101110})
    /Users/aias/.local/share/go1.24.3/src/net/http/server.go:2102 +0x52c
created by net/http.(*Server).Serve in goroutine 23
    /Users/aias/.local/share/go1.24.3/src/net/http/server.go:3454 +0x3d8

goroutine 1 [select]:
main.run({0x104a95ff0, 0x140001063c0}, 0x140000102f0)
    /Volumes/data/go-service-engineering-book/lab/cmd/service/main.go:85 +0x870
main.main()
    /Volumes/data/go-service-engineering-book/lab/cmd/service/main.go:31 +0x1e4

goroutine 17 [syscall]:
os/signal.signal_recv()
    /Users/aias/.local/share/go1.24.3/src/runtime/sigqueue.go:149 +0x2c
os/signal.loop()
    /Users/aias/.local/share/go1.24.3/src/os/signal/signal_unix.go:23 +0x1c
created by os/signal.Notify.func1.1 in goroutine 1
    /Users/aias/.local/share/go1.24.3/src/os/signal/signal.go:152 +0x28

goroutine 18 [select]:
os/signal.NotifyContext.func1()
    /Users/aias/.local/share/go1.24.3/src/os/signal/signal.go:289 +0x68
created by os/signal.NotifyContext in goroutine 1
    /Users/aias/.local/share/go1.24.3/src/os/signal/signal.go:288 +0x148

goroutine 19 [select]:
example.com/go-service-engineering/lab/internal/service.wait({0x104a95df8, 0x14000188070}, 0x1?)
    /Volumes/data/go-service-engineering-book/lab/internal/service/app.go:105 +0xa0
example.com/go-service-engineering/lab/internal/service.(*App).tasks.func1({0x104a95df8?, 0x14000188070?})
    /Volumes/data/go-service-engineering-book/lab/internal/service/app.go:133 +0x60
example.com/go-service-engineering/lab/internal/service.(*Pool).run(0x1400009a100, {0x0?, 0x140000ac510?})
    /Volumes/data/go-service-engineering-book/lab/internal/service/pool.go:87 +0x174
example.com/go-service-engineering/lab/internal/service.NewPool.func1()
    /Volumes/data/go-service-engineering-book/lab/internal/service/pool.go:71 +0x70
created by example.com/go-service-engineering/lab/internal/service.NewPool in goroutine 1
    /Volumes/data/go-service-engineering-book/lab/internal/service/pool.go:68 +0x184

goroutine 20 [select]:
example.com/go-service-engineering/lab/internal/service.wait({0x104a95df8, 0x1400009e150}, 0x1?)
    /Volumes/data/go-service-engineering-book/lab/internal/service/app.go:105 +0xa0
example.com/go-service-engineering/lab/internal/service.(*App).tasks.func1({0x104a95df8?, 0x1400009e150?})
    /Volumes/data/go-service-engineering-book/lab/internal/service/app.go:133 +0x60
example.com/go-service-engineering/lab/internal/service.(*Pool).run(0x1400009a100, {0x0?, 0x14000184300?})
    /Volumes/data/go-service-engineering-book/lab/internal/service/pool.go:87 +0x174
example.com/go-service-engineering/lab/internal/service.NewPool.func1()
    /Volumes/data/go-service-engineering-book/lab/internal/service/pool.go:71 +0x70
created by example.com/go-service-engineering/lab/internal/service.NewPool in goroutine 1
    /Volumes/data/go-service-engineering-book/lab/internal/service/pool.go:68 +0x184

goroutine 21 [sync.WaitGroup.Wait]:
sync.runtime_SemacquireWaitGroup(0x0?)
    /Users/aias/.local/share/go1.24.3/src/runtime/sema.go:110 +0x2c
sync.(*WaitGroup).Wait(0x140000a2000)
    /Users/aias/.local/share/go1.24.3/src/sync/waitgroup.go:118 +0x70
example.com/go-service-engineering/lab/internal/service.NewPool.func2()
    /Volumes/data/go-service-engineering-book/lab/internal/service/pool.go:75 +0x30
created by example.com/go-service-engineering/lab/internal/service.NewPool in goroutine 1
    /Volumes/data/go-service-engineering-book/lab/internal/service/pool.go:75 +0x240

goroutine 22 [IO wait]:
internal/poll.runtime_pollWait(0x105124ef8, 0x72)
    /Users/aias/.local/share/go1.24.3/src/runtime/netpoll.go:351 +0xa0
internal/poll.(*pollDesc).wait(0x1400009a180?, 0x104762be8?, 0x0)
    /Users/aias/.local/share/go1.24.3/src/internal/poll/fd_poll_runtime.go:84 +0x28
internal/poll.(*pollDesc).waitRead(...)
    /Users/aias/.local/share/go1.24.3/src/internal/poll/fd_poll_runtime.go:89
internal/poll.(*FD).Accept(0x1400009a180)
    /Users/aias/.local/share/go1.24.3/src/internal/poll/fd_unix.go:620 +0x24c
net.(*netFD).accept(0x1400009a180)
    /Users/aias/.local/share/go1.24.3/src/net/fd_unix.go:172 +0x28
net.(*TCPListener).accept(0x140000bc000)
    /Users/aias/.local/share/go1.24.3/src/net/tcpsock_posix.go:159 +0x24
net.(*TCPListener).Accept(0x140000bc000)
    /Users/aias/.local/share/go1.24.3/src/net/tcpsock.go:380 +0x2c
net/http.(*Server).Serve(0x140000c8000, {0x104a958e8, 0x140000bc000})
    /Users/aias/.local/share/go1.24.3/src/net/http/server.go:3424 +0x290
main.run.func2()
    /Volumes/data/go-service-engineering-book/lab/cmd/service/main.go:82 +0x30
created by main.run in goroutine 1
    /Volumes/data/go-service-engineering-book/lab/cmd/service/main.go:82 +0x6b4

goroutine 23 [IO wait]:
internal/poll.runtime_pollWait(0x105124de0, 0x72)
    /Users/aias/.local/share/go1.24.3/src/runtime/netpoll.go:351 +0xa0
internal/poll.(*pollDesc).wait(0x1400009a200?, 0x104762be8?, 0x0)
    /Users/aias/.local/share/go1.24.3/src/internal/poll/fd_poll_runtime.go:84 +0x28
internal/poll.(*pollDesc).waitRead(...)
    /Users/aias/.local/share/go1.24.3/src/internal/poll/fd_poll_runtime.go:89
internal/poll.(*FD).Accept(0x1400009a200)
    /Users/aias/.local/share/go1.24.3/src/internal/poll/fd_unix.go:620 +0x24c
net.(*netFD).accept(0x1400009a200)
    /Users/aias/.local/share/go1.24.3/src/net/fd_unix.go:172 +0x28
net.(*TCPListener).accept(0x140000bc040)
    /Users/aias/.local/share/go1.24.3/src/net/tcpsock_posix.go:159 +0x24
net.(*TCPListener).Accept(0x140000bc040)
    /Users/aias/.local/share/go1.24.3/src/net/tcpsock.go:380 +0x2c
net/http.(*Server).Serve(0x140000c8100, {0x104a958e8, 0x140000bc040})
    /Users/aias/.local/share/go1.24.3/src/net/http/server.go:3424 +0x290
main.run.func3()
    /Volumes/data/go-service-engineering-book/lab/cmd/service/main.go:83 +0x30
created by main.run in goroutine 1
    /Volumes/data/go-service-engineering-book/lab/cmd/service/main.go:83 +0x71c

goroutine 50 [IO wait]:
internal/poll.runtime_pollWait(0x105124cc8, 0x72)
    /Users/aias/.local/share/go1.24.3/src/runtime/netpoll.go:351 +0xa0
internal/poll.(*pollDesc).wait(0x1400009a580?, 0x140000acfa1?, 0x0)
    /Users/aias/.local/share/go1.24.3/src/internal/poll/fd_poll_runtime.go:84 +0x28
internal/poll.(*pollDesc).waitRead(...)
    /Users/aias/.local/share/go1.24.3/src/internal/poll/fd_poll_runtime.go:89
internal/poll.(*FD).Read(0x1400009a580, {0x140000acfa1, 0x1, 0x1})
    /Users/aias/.local/share/go1.24.3/src/internal/poll/fd_unix.go:165 +0x1fc
net.(*netFD).Read(0x1400009a580, {0x140000acfa1?, 0x1400004bf58?, 0x1048bec74?})
    /Users/aias/.local/share/go1.24.3/src/net/fd_posix.go:55 +0x28
net.(*conn).Read(0x140000c6070, {0x140000acfa1?, 0x0?, 0x0?})
    /Users/aias/.local/share/go1.24.3/src/net/net.go:194 +0x34
net/http.(*connReader).backgroundRead(0x140000acf90)
    /Users/aias/.local/share/go1.24.3/src/net/http/server.go:690 +0x40
created by net/http.(*connReader).startBackgroundRead in goroutine 49
    /Users/aias/.local/share/go1.24.3/src/net/http/server.go:686 +0xc4