// cms is a free and open source content management system. package main import ( "context" "log" "net/http" "strings" "time" "git.sr.ht/~evanj/cms/pkg/flushresp" ) type App struct { log *log.Logger dynamicHandler http.Handler // NOTE: Concurrent read (only) is OK. This is never wrote (but defined on // server startup). handlers map[string]http.Handler } func (a *App) ServeHTTP(w http.ResponseWriter, r *http.Request) { if r.URL.Path == "/favicon.ico" { // TODO: Handle favicon. http.NotFound(w, r) return } parts := strings.Split(r.URL.Path, "/") if len(parts) < 2 { http.NotFound(w, r) return } namespace := parts[1] if namespace == "" { namespace = "user" } h, ok := a.handlers[namespace] if !ok { h = a.dynamicHandler } a.handleWithRetry(w, r, h) } // handleWithRetry will retry requests when we're approaching long request // times in an effort to lower tail latency. func (a *App) handleWithRetry(w http.ResponseWriter, r *http.Request, h http.Handler) { var ( // Effective max request time is 30 seconds (ten seconds will be retried // three times). maxAttempts = 3 maxTime = 10 * time.Second err error ) for i := 0; i < maxAttempts; i++ { var ( ctx, cancel = context.WithTimeout(context.Background(), maxTime) wWrap = flushresp.New(w) successCh = handle(wWrap, r.WithContext(ctx), h) ) defer cancel() select { case <-ctx.Done(): err = ctx.Err() case <-successCh: if _, err := wWrap.Flush(); err != nil { // TODO: Is there a more appropriate way to handle an error here?... a.log.Println(err) } return } } // TODO: Probably want a better error message. w.WriteHeader(http.StatusInternalServerError) w.Write([]byte(err.Error())) } func handle(w http.ResponseWriter, r *http.Request, h http.Handler) chan struct{} { c := make(chan struct{}) go func(c chan struct{}) { h.ServeHTTP(w, r) c <- struct{}{} }(c) return c }