schedule,shared: move concurrency 429 limits into scheduler code (#849)
- make concurrency limiting the scheduler.Scheduler's responsibility - eliminate the separate concurrency limit middleware - move concurrencyLimit logic into scheduler.FIFO to maintain backwards compatibility - add HTTPError from #834 Updates #834
This commit is contained in:
@@ -37,6 +37,16 @@ var (
|
||||
)
|
||||
|
||||
func SendError(w http.ResponseWriter, r *http.Request, err error) {
|
||||
var httpErr HTTPError
|
||||
if errors.As(err, &httpErr) {
|
||||
for k, v := range httpErr.Header() {
|
||||
w.Header()[k] = v
|
||||
}
|
||||
w.WriteHeader(httpErr.StatusCode())
|
||||
w.Write(httpErr.Body())
|
||||
return
|
||||
}
|
||||
|
||||
switch {
|
||||
case errors.Is(err, ErrNoModelInContext):
|
||||
SendResponse(w, r, http.StatusNotFound, "no model id could be identified")
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
package shared
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// HTTPError is an error that carries a complete HTTP response. A producer (e.g.
|
||||
// a scheduler shedding a request) returns one of these; a renderer (e.g.
|
||||
// router.SendError) writes the status, headers, and body verbatim instead of
|
||||
// mapping the error to a generic status. It is the seam that lets a component
|
||||
// shed a request with a rich response (e.g. a 429 with rate-limit headers and a
|
||||
// JSON hint body) without the renderer knowing the producer's internals.
|
||||
type HTTPError interface {
|
||||
error
|
||||
StatusCode() int
|
||||
Header() http.Header
|
||||
Body() []byte
|
||||
}
|
||||
|
||||
// ConcurrencyLimitError is an HTTPError for a 429 concurrency-limit rejection.
|
||||
// Zero-value fields fall back to sensible defaults: a 1-second Retry-After and a
|
||||
// JSON hint body.
|
||||
type ConcurrencyLimitError struct {
|
||||
// RetryAfter, when > 0, is sent as the Retry-After header (in seconds).
|
||||
// Defaults to 1.
|
||||
RetryAfter int
|
||||
|
||||
// Message overrides the JSON body's "error" field. Defaults to
|
||||
// "Too many requests".
|
||||
Message string
|
||||
}
|
||||
|
||||
func (e ConcurrencyLimitError) Error() string { return "concurrency limit reached" }
|
||||
|
||||
func (e ConcurrencyLimitError) StatusCode() int { return http.StatusTooManyRequests }
|
||||
|
||||
func (e ConcurrencyLimitError) Header() http.Header {
|
||||
h := http.Header{}
|
||||
h.Set("Content-Type", "application/json")
|
||||
h.Set("Retry-After", e.retryAfter())
|
||||
return h
|
||||
}
|
||||
|
||||
func (e ConcurrencyLimitError) Body() []byte {
|
||||
b, _ := json.Marshal(map[string]string{"error": e.message()})
|
||||
return b
|
||||
}
|
||||
|
||||
func (e ConcurrencyLimitError) retryAfter() string {
|
||||
if e.RetryAfter > 0 {
|
||||
return strconv.Itoa(e.RetryAfter)
|
||||
}
|
||||
return "1"
|
||||
}
|
||||
|
||||
func (e ConcurrencyLimitError) message() string {
|
||||
if e.Message != "" {
|
||||
return e.Message
|
||||
}
|
||||
return "Too many requests"
|
||||
}
|
||||
Reference in New Issue
Block a user