// 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
// 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) {
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 {
http.NotFound(w, r)
return
}
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
}