Files
ziti/controller/timeout/timeouts.go
T
2021-03-03 14:44:26 -05:00

179 lines
3.8 KiB
Go

package timeout
import (
"bytes"
"context"
"errors"
"fmt"
"github.com/go-openapi/runtime"
"github.com/michaelquigley/pfxlog"
"github.com/openziti/edge/controller/apierror"
"github.com/openziti/edge/controller/env"
"github.com/openziti/foundation/util/debugz"
"github.com/openziti/foundation/util/errorz"
"net/http"
"path"
goruntime "runtime"
"strings"
"sync"
"time"
)
func TimeoutHandler(h http.Handler, dt time.Duration, apiErr *errorz.ApiError) http.Handler {
return &timeoutHandler{
handler: h,
apiError: apiErr,
dt: dt,
producer: runtime.JSONProducer(),
}
}
type timeoutHandler struct {
handler http.Handler
dt time.Duration
apiError *errorz.ApiError
producer runtime.Producer
}
func (h *timeoutHandler) errorBody(w http.ResponseWriter, r *http.Request) error {
if h.apiError != nil {
requestId := ""
rc, _ := env.GetRequestContextFromHttpContext(r)
if rc != nil {
requestId = rc.Id
}
apiError := apierror.ToRestModel(h.apiError, requestId)
err := h.producer.Produce(w, apiError)
return err
}
return errors.New("no timout api error specified")
}
func (h *timeoutHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
ctx, cancelCtx := context.WithTimeout(r.Context(), h.dt)
defer cancelCtx()
r = r.WithContext(ctx)
done := make(chan struct{})
tw := &timeoutWriter{
w: w,
h: make(http.Header),
req: r,
}
panicChan := make(chan interface{}, 1)
go func() {
defer func() {
if p := recover(); p != nil {
pfxlog.Logger().Errorf("panic caught by timeout handler: %v\n%v", p, debugz.GenerateLocalStack())
panicChan <- p
}
}()
h.handler.ServeHTTP(tw, r)
close(done)
}()
select {
case <-panicChan:
case <-done:
tw.mu.Lock()
defer tw.mu.Unlock()
dst := w.Header()
for k, vv := range tw.h {
dst[k] = vv
}
if !tw.wroteHeader {
tw.code = http.StatusOK
}
w.WriteHeader(tw.code)
_, _ = w.Write(tw.wbuf.Bytes())
case <-ctx.Done():
tw.mu.Lock()
defer tw.mu.Unlock()
w.Header().Set("content-type", "application/json")
w.WriteHeader(http.StatusServiceUnavailable)
_ = h.errorBody(w, r)
tw.timedOut = true
}
}
type timeoutWriter struct {
w http.ResponseWriter
h http.Header
wbuf bytes.Buffer
req *http.Request
mu sync.Mutex
timedOut bool
wroteHeader bool
code int
}
var _ http.Pusher = (*timeoutWriter)(nil)
func (tw *timeoutWriter) Push(target string, opts *http.PushOptions) error {
if pusher, ok := tw.w.(http.Pusher); ok {
return pusher.Push(target, opts)
}
return http.ErrNotSupported
}
func (tw *timeoutWriter) Header() http.Header { return tw.h }
func (tw *timeoutWriter) Write(p []byte) (int, error) {
tw.mu.Lock()
defer tw.mu.Unlock()
if tw.timedOut {
return 0, http.ErrHandlerTimeout
}
if !tw.wroteHeader {
tw.writeHeaderLocked(http.StatusOK)
}
return tw.wbuf.Write(p)
}
func (tw *timeoutWriter) writeHeaderLocked(code int) {
checkWriteHeaderCode(code)
switch {
case tw.timedOut:
return
case tw.wroteHeader:
if tw.req != nil {
caller := relevantCaller()
pfxlog.Logger().Errorf("http: superfluous response.WriteHeader call from %s (%s:%d)", caller.Function, path.Base(caller.File), caller.Line)
}
default:
tw.wroteHeader = true
tw.code = code
}
}
func (tw *timeoutWriter) WriteHeader(code int) {
tw.mu.Lock()
defer tw.mu.Unlock()
tw.writeHeaderLocked(code)
}
func checkWriteHeaderCode(code int) {
if code < 100 || code > 999 {
panic(fmt.Sprintf("invalid WriteHeader code %v", code))
}
}
func relevantCaller() goruntime.Frame {
pc := make([]uintptr, 16)
n := goruntime.Callers(1, pc)
frames := goruntime.CallersFrames(pc[:n])
var frame goruntime.Frame
for {
frame, more := frames.Next()
if !strings.HasPrefix(frame.Function, "net/http.") && !strings.HasPrefix(frame.Function, "github.com/openziti/edge/controller/timeout") {
return frame
}
if !more {
break
}
}
return frame
}