复审 p2-2items(P2-1 跨进程去重按裁决只报不修):
(a) **关闭期 ctx 取消 ≠ 没起**:后端关闭时 ctx 被取消,探活返回
`context.Canceled`;原实现把它判成"没起" ⇒ **在关闭过程中拉起 TTS**。
现在按 `errors.Is(err, context.Canceled)` 判为跳过:不记 markAttempt(不消耗
冷却)、不拉、也不 WARN 成故障。探活**自身超时**(DeadlineExceeded)仍是"没起",
照常拉 —— 两者用 errors.Is 区分,不按文案。新增 OutcomeCanceled。
(b) **探活地址必须合法**:写成 `127.0.0.1:9880`(漏 scheme)会让 http.NewRequest
每次都失败 ⇒「没起」永远成立 ⇒ 每次启动都拉一次进程。新增
`ttslaunch.ValidateProbeURL`(url.Parse + scheme ∈ {http,https} + host 非空):
- `New` 里校验 ⇒ 不合法即 WARN + 不启用(兜底闸,覆盖任何调用方);
- 组合根 `validateResolvedProbeURL` 里同一把闸再校验一次,WARN 带
`probe_url_source` 指明该改哪个文件 —— **baseUrl 兜底那条路也被覆盖**。
顺带按复审记录项 1 修正 `Proxy: nil` 的注释:net/http 本来就不给回环 IP 走代理,
这行真正保护的是 probeUrl 被配成主机名的情况(原注释高估了它)。
用例(均变异验证打红):
- TestCheck_ParentCanceled_SkipsLaunch:取消 ⇒ OutcomeCanceled + 0 次拉起 + 不进冷却
(变异:跳过改回"没起" ⇒ Outcome=launched、marker 落地)
- TestCheck_ProbeOwnTimeout_StillLaunches:探活自身超时仍照常拉
- TestNew_InvalidProbeURL_Disables:7 种非法形状 ⇒ 不接单、0 次拉起、有 WARN
- TestStartVoiceTTSLaunch_{ShutdownCancelDoesNotLaunch,InvalidProbeURLStaysOff,
InvalidBaseURLFallbackStaysOff}:组合根三条,均用真 .bat marker 观察"有没有拉起"
79 lines
3.0 KiB
Go
79 lines
3.0 KiB
Go
package ttslaunch
|
||
|
||
import (
|
||
"context"
|
||
"fmt"
|
||
"io"
|
||
"log/slog"
|
||
"net/http"
|
||
"net/url"
|
||
"strings"
|
||
"time"
|
||
)
|
||
|
||
// probeBodyLimit 是探活响应的最大读取量:探活只关心"有没有人应答",回应体一律丢弃,
|
||
// 但也要有界(别被一个话痨服务的无限流拖住)。
|
||
const probeBodyLimit = 4 << 10
|
||
|
||
// httpProbe 用一次 HTTP GET 回答「这个端口上已经有人在应答吗」。
|
||
//
|
||
// 判定口径(刻意宽松):**任何** HTTP 回应都算"在跑",只有传输层错误(连不上 /
|
||
// 超时 / 连接被拒)才算"没起"。理由是这个服务的根路径回什么状态码并不稳定
|
||
// (404/405/200 都可能),而"端口有人说话"已经足以证明不该再拉第二个实例 ——
|
||
// 误判成"没起"的代价是去起一个抢同一个端口的进程,代价明显更大。
|
||
//
|
||
// `Proxy: nil` 的实际作用范围(复审记录项 1:别高估它):net/http 本来就不给
|
||
// **回环 IP** 走代理,所以探 `127.0.0.1` 时这行可有可无(去掉再设 HTTP_PROXY,
|
||
// 用例照样全绿)。它真正保护的是 `probeUrl` 被配成**主机名**(如
|
||
// `http://tts.local:9880`)的情况:那种地址会被环境里的 HTTP_PROXY 接管,从而
|
||
// 得到"有人在跑"的假结论。要覆盖这一点得用主机名 probeUrl 写用例。
|
||
func httpProbe(url string, timeout time.Duration) probeFunc {
|
||
client := &http.Client{
|
||
Timeout: timeout,
|
||
Transport: &http.Transport{
|
||
Proxy: nil,
|
||
DisableKeepAlives: true,
|
||
},
|
||
}
|
||
return func(ctx context.Context) error {
|
||
reqCtx, cancel := context.WithTimeout(ctx, timeout)
|
||
defer cancel()
|
||
req, err := http.NewRequestWithContext(reqCtx, http.MethodGet, url, nil)
|
||
if err != nil {
|
||
return fmt.Errorf("ttslaunch: build probe request: %w", err)
|
||
}
|
||
resp, err := client.Do(req)
|
||
if err != nil {
|
||
return fmt.Errorf("ttslaunch: probe %s: %w", url, err)
|
||
}
|
||
defer func() { _ = resp.Body.Close() }()
|
||
_, _ = io.Copy(io.Discard, io.LimitReader(resp.Body, probeBodyLimit))
|
||
return nil
|
||
}
|
||
}
|
||
|
||
// ValidateProbeURL 校验探活地址的形状:必须是**绝对 http(s) URL 且带主机**。
|
||
//
|
||
// 为什么值得单独一道闸(复审 P2-2b):探活地址写错(`127.0.0.1:9880` 漏 scheme、
|
||
// 拼错主机)不是"报个错就完了" —— 它会让探活**每次都失败**,于是"没起"永远成立,
|
||
// 后端每启动一次就拉一次进程。所以调用方应当把非法地址当作"不启用"。
|
||
func ValidateProbeURL(raw string) error {
|
||
trimmed := strings.TrimSpace(raw)
|
||
u, err := url.Parse(trimmed)
|
||
if err != nil {
|
||
return fmt.Errorf("ttslaunch: parse probe url %q: %w", raw, err)
|
||
}
|
||
if u.Scheme != "http" && u.Scheme != "https" {
|
||
return fmt.Errorf("ttslaunch: probe url %q is not an absolute http(s) url", raw)
|
||
}
|
||
if u.Host == "" {
|
||
return fmt.Errorf("ttslaunch: probe url %q has no host", raw)
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// discardLogger 是没注入日志器时的兜底(绝不 nil 解引用)。
|
||
func discardLogger() *slog.Logger {
|
||
return slog.New(slog.NewTextHandler(io.Discard, nil))
|
||
}
|