我目前正在维护一些基于标准库和 gorilla mux 并在 kubernetes (GKE) 中运行的 HTTP API。
我们采用http.TimeoutHandler
“标准”方式来进行一致的超时错误管理。典型的端点实现将使用以下“链”:
MonitoringMiddleware => TimeoutMiddleware => … => handler
这样我们就可以监控每个端点的一些关键指标。
我们的一个 API 通常用于“即发即弃”模式,这意味着客户端将推送一些数据而不关心 API 响应。我们面临的问题是
这意味着当客户端断开连接时我们不会处理完成的请求,这不是我们想要的,因此我正在寻找解决方案。
我能找到的唯一与我的问题有关的讨论是https://github.com/golang/go/issues/18527;然而
Handler
解决方法是您的应用程序可以忽略Request.Context()
将意味着监控中间件不会报告“正确”状态,因为Handler
它将在其 goroutine 中执行请求处理,但TimeoutHandler
将强制执行状态并且可观察性将被破坏。
目前,我不考虑删除我们的中间件,因为它们有助于我们的 API 在行为和可观察性方面保持一致性。到目前为止,我的结论是,TimeoutHandler
当处理程序不应依赖于等待响应的客户端时,我需要“分叉”并使用自定义上下文。
我目前想法的要点是:
type TimeoutHandler struct {
handler Handler
body string
dt time.Duration
// BaseContext optionally specifies a function that returns
// the base context for controling if the server request processing.
// If BaseContext is nil, the default is req.Context().
// If non-nil, it must return a non-nil context.
BaseContext func(*http.Request) context.Context
}
func (h *TimeoutHandler) ServeHTTP(w ResponseWriter, r *Request) {
reqCtx := r.Context()
if h.BaseContext != nil {
reqCtx = h.BaseContext(r)
}
ctx, cancelCtx := context.WithTimeout(reqCtx, h.dt)
defer cancelCtx()
r = r.WithContext(ctx)
...
case <-reqCtx.Done():
tw.mu.Lock()
defer tw.mu.Unlock()
w.WriteHeader(499) // write status for monitoring;
// no need to write a body since no client is listening.
case <-ctx.Done():
tw.mu.Lock()
defer tw.mu.Unlock()
w.WriteHeader(StatusServiceUnavailable)
io.WriteString(w, h.errorBody())
tw.timedOut = true
}
中间件BaseContext
回调将返回context.Background()
对“即发即弃”端点的请求。
我不喜欢的一件事是,这样做我会丢失任何编写的上下文键,因此这个新的中间件会有很强的使用限制。总的来说,我觉得这比它应该的要复杂。
我完全错过了一些明显的东西吗?欢迎任何关于 API 检测(也许我们的中间件是反模式)/fire and forget 实现的反馈!
编辑:由于大多数评论是客户端不等待响应的请求具有未指定的行为,因此我检查了有关发生这种情况的典型客户端的更多信息。
从我们的日志来看,这发生在看似移动设备的用户代理上。我可以想象连接可能会更加不稳定,问题可能不会消失。因此,我不会断定我不应该找到解决方案,因为这目前正在创建误报警报。