Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

scheduler improvements and API tweaks #286

Merged
merged 15 commits into from
Apr 24, 2017
Merged
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
httpcli: move APIError (and friends) to apierrors package; ErrorMappe…
…rFunc processes responses instead of status codes
James DeFelice committed Apr 24, 2017
commit 18db7e134d953c4d81400275810bf2dd67afb1b7
103 changes: 103 additions & 0 deletions api/v1/lib/httpcli/apierrors/apierrors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
package apierrors

import (
"io/ioutil"
"net/http"
)

var (
// MsgNotLeader is returned by Do calls that are sent to a non leading Mesos master. Deprecated.
MsgNotLeader = "call sent to a non-leading master"
// MsgAuth is returned by Do calls that are not successfully authenticated. Deprecated.
MsgAuth = "call not authenticated"
// MsgUnsubscribed is returned by Do calls that are sent before a subscription is established. Deprecated.
MsgUnsubscribed = "no subscription established"
// MsgVersion is returned by Do calls that are sent to an incompatible API version. Deprecated.
MsgVersion = "incompatible API version"
// MsgMalformed is returned by Do calls that are malformed. Deprecated.
MsgMalformed = "malformed request"
// MsgMediaType is returned by Do calls that are sent with an unsupported media type. Deprecated.
MsgMediaType = "unsupported media type"
// MsgRateLimit is returned by Do calls that are rate limited. This is a temporary condition
// that should clear. Deprecated.
MsgRateLimit = "rate limited"
// MsgUnavailable is returned by Do calls that are sent to a master or agent that's in recovery, or
// does not yet realize that it's the leader. This is a temporary condition that should clear. Deprecated.
MsgUnavailable = "mesos server unavailable"
// MsgNotFound could happen if the master or agent libprocess has not yet set up http routes. Deprecated.
MsgNotFound = "mesos http endpoint not found"

// ErrorTable maps HTTP response codes to their respective Mesos v1 API error messages.
ErrorTable = func() map[int]Error {
result := make(map[int]Error)
for code, msg := range map[int]string{
http.StatusTemporaryRedirect: MsgNotLeader,
http.StatusBadRequest: MsgMalformed,
http.StatusConflict: MsgVersion,
http.StatusForbidden: MsgUnsubscribed,
http.StatusUnauthorized: MsgAuth,
http.StatusNotAcceptable: MsgMediaType,
http.StatusNotFound: MsgNotFound,
http.StatusServiceUnavailable: MsgUnavailable,
http.StatusTooManyRequests: MsgRateLimit,
} {
result[code] = Error{Code: code, Message: msg}
}
return result
}()
)

// Error captures HTTP v1 API error codes and messages generated by Mesos.
type Error struct {
Code int // Code is the HTTP response status code generated by Mesos
Message string // Message briefly summarizes the nature of the error
Details string // Details captures the HTTP response entity, if any, supplied by Mesos
}

// IsErrorCode returns true for all HTTP status codes that are not considered informational or successful.
func IsErrorCode(code int) bool {
return code >= 300
}

// FromResponse returns an `*Error` for a response containing a status code that indicates an error condition.
// The response body (if any) is captured in the Error.Details field.
// Returns nil for nil responses and responses with non-error status codes.
// See IsErrorCode.
func FromResponse(res *http.Response) error {
if res == nil {
return nil
}

code := res.StatusCode
if !IsErrorCode(code) {
// non-error HTTP response codes don't generate errors
return nil
}

details := ""
if res.Body != nil {
buf, _ := ioutil.ReadAll(res.Body)
details = string(buf)
}

if msg, ok := ErrorTable[code]; ok {
// return a modified copy of whatever was in the error table
msg.Details = details
return &msg
}

// unmapped errors are OK, they're just not "interpreted" (with a Message)
return &Error{
Code: code,
Details: details,
}
}

func (err *Error) Error() string {
return err.Message
}

func IsErrNotLeader(err error) bool {
apiErr, ok := err.(*Error)
return ok && apiErr.Code == http.StatusTemporaryRedirect
}
122 changes: 27 additions & 95 deletions api/v1/lib/httpcli/http.go
Original file line number Diff line number Diff line change
@@ -3,98 +3,29 @@ package httpcli
import (
"bytes"
"crypto/tls"
"errors"
"fmt"
"io"
"io/ioutil"
"log"
"net"
"net/http"
"time"

"github.com/mesos/mesos-go/api/v1/lib"
"github.com/mesos/mesos-go/api/v1/lib/encoding"
"github.com/mesos/mesos-go/api/v1/lib/httpcli/apierrors"
"github.com/mesos/mesos-go/api/v1/lib/recordio"
)

// Deprecation notice: these error variables are no longer returned by this module. Use APIError instead.
// TODO(jdef) remove these error vars after v0.0.3
var (
// ErrNotLeader is returned by Do calls that are sent to a non leading Mesos master. Deprecated.
ErrNotLeader = errors.New("mesos: call sent to a non-leading master")
// ErrAuth is returned by Do calls that are not successfully authenticated. Deprecated.
ErrAuth = errors.New("mesos: call not authenticated")
// ErrUnsubscribed is returned by Do calls that are sent before a subscription is established. Deprecated.
ErrUnsubscribed = errors.New("mesos: no subscription established")
// ErrVersion is returned by Do calls that are sent to an incompatible API version. Deprecated.
ErrVersion = errors.New("mesos: incompatible API version")
// ErrMalformed is returned by Do calls that are malformed. Deprecated.
ErrMalformed = errors.New("mesos: malformed request")
// ErrMediaType is returned by Do calls that are sent with an unsupported media type. Deprecated.
ErrMediaType = errors.New("mesos: unsupported media type")
// ErrRateLimit is returned by Do calls that are rate limited. This is a temporary condition
// that should clear. Deprecated.
ErrRateLimit = errors.New("mesos: rate limited")
// ErrUnavailable is returned by Do calls that are sent to a master or agent that's in recovery, or
// does not yet realize that it's the leader. This is a temporary condition that should clear. Deprecated.
ErrUnavailable = errors.New("mesos: mesos server unavailable")
// ErrNotFound could happen if the master or agent libprocess has not yet set up http routes. Deprecated.
ErrNotFound = errors.New("mesos: mesos http endpoint not found")

// codeErrors maps HTTP response codes to their respective errors.
codeErrors = map[int]error{
http.StatusOK: nil,
http.StatusAccepted: nil,
http.StatusTemporaryRedirect: ErrNotLeader,
http.StatusBadRequest: ErrMalformed,
http.StatusConflict: ErrVersion,
http.StatusForbidden: ErrUnsubscribed,
http.StatusUnauthorized: ErrAuth,
http.StatusNotAcceptable: ErrMediaType,
http.StatusNotFound: ErrNotFound,
http.StatusServiceUnavailable: ErrUnavailable,
http.StatusTooManyRequests: ErrRateLimit,
}

defaultErrorMapper = ErrorMapperFunc(func(code int) error {
// for now, just scrape the string of the deprecated error var and use that
// as the APIError.Message. Eventually we'll get rid of the Err* variables.
// TODO(jdef) simplify this after v0.0.3
err, ok := codeErrors[code]
if !ok {
err = &APIError{Code: code}
}
return &APIError{
Code: code,
Message: err.Error(),
}
})
)

// ProtocolError is a generic error type returned for expected status codes
// received from Mesos.
// Deprecated: no longer used in favor of APIError.
// TODO(jdef) remove this after v0.0.3
type ProtocolError int
// ProtocolError is returned when we receive a response from Mesos that is outside of the HTTP API specification.
// Receipt of the following will yield protocol errors:
// - any unexpected non-error HTTP response codes (e.g. 199)
// - any unexpected Content-Type
type ProtocolError string

// Error implements error interface
func (pe ProtocolError) Error() string { return fmt.Sprintf("Unexpected Mesos HTTP error: %d", int(pe)) }

// APIError captures HTTP error codes and messages generated by Mesos.
type APIError struct {
Code int // Code is the HTTP response status code generated by Mesos
Message string // Message briefly summarizes the nature of the error
Details string // Details captures the HTTP response entity, if any, supplied by Mesos
}

func (err *APIError) Error() string {
return err.Message
}
func (pe ProtocolError) Error() string { return string(pe) }

func IsErrNotLeader(err error) bool {
apiErr, ok := err.(*APIError)
return ok && apiErr.Code == http.StatusTemporaryRedirect
}
var defaultErrorMapper = ErrorMapperFunc(apierrors.FromResponse)

const (
debug = false // TODO(jdef) kill me at some point
@@ -124,16 +55,16 @@ type DoFunc func(*http.Request) (*http.Response, error)
// Close when they're finished processing the response otherwise there may be connection leaks.
type Response struct {
io.Closer
Header http.Header

decoder encoding.Decoder
Header http.Header
}

// implements mesos.Response
func (r *Response) Decoder() encoding.Decoder { return r.decoder }

// ErrorMapperFunc generates an error for the given statusCode.
// WARNING: this API will change in an upcoming release.
type ErrorMapperFunc func(statusCode int) error
// ErrorMapperFunc generates an error for the given response.
type ErrorMapperFunc func(*http.Response) error

// ResponseHandler is invoked to process an HTTP response
type ResponseHandler func(*http.Response, error) (mesos.Response, error)
@@ -242,7 +173,14 @@ func (c *Client) HandleResponse(res *http.Response, err error) (mesos.Response,
return nil, err
}

var events encoding.Decoder
result := &Response{
Closer: res.Body,
Header: res.Header,
}
if err = c.errorMapper(res); err != nil {
return result, err
}

switch res.StatusCode {
case http.StatusOK:
if debug {
@@ -251,27 +189,21 @@ func (c *Client) HandleResponse(res *http.Response, err error) (mesos.Response,
ct := res.Header.Get("Content-Type")
if ct != c.codec.MediaTypes[indexResponseContentType] {
res.Body.Close()
return nil, fmt.Errorf("unexpected content type: %q", ct) //TODO(jdef) extact this into a typed error
return nil, ProtocolError(fmt.Sprintf("unexpected content type: %q", ct))
}
events = c.codec.NewDecoder(recordio.NewFrameReader(res.Body))
result.decoder = c.codec.NewDecoder(recordio.NewFrameReader(res.Body))

case http.StatusAccepted:
if debug {
log.Println("request Accepted")
}
// noop; no decoder for these types of calls

default:
err = c.errorMapper(res.StatusCode)
if apiErr, ok := err.(*APIError); ok && res.Body != nil {
// Annotate the APIError with Details from the response
buf, _ := ioutil.ReadAll(res.Body)
apiErr.Details = string(buf)
}
return result, ProtocolError(fmt.Sprintf("unexpected mesos HTTP response code: %d", res.StatusCode))
}
return &Response{
decoder: events,
Closer: res.Body,
Header: res.Header,
}, err

return result, nil
}

// Do sends a Call and returns (a) a Response (should be closed when finished) that
3 changes: 2 additions & 1 deletion api/v1/lib/httpcli/httpsched/httpsched.go
Original file line number Diff line number Diff line change
@@ -11,6 +11,7 @@ import (
"github.com/mesos/mesos-go/api/v1/lib/backoff"
"github.com/mesos/mesos-go/api/v1/lib/encoding"
"github.com/mesos/mesos-go/api/v1/lib/httpcli"
"github.com/mesos/mesos-go/api/v1/lib/httpcli/apierrors"
"github.com/mesos/mesos-go/api/v1/lib/scheduler"
"github.com/mesos/mesos-go/api/v1/lib/scheduler/calls"
)
@@ -164,7 +165,7 @@ func (mre *mesosRedirectionError) Error() string {
func (cli *client) redirectHandler() httpcli.Opt {
return httpcli.HandleResponse(func(hres *http.Response, err error) (mesos.Response, error) {
resp, err := cli.HandleResponse(hres, err) // default response handler
if err == nil || (err != nil && !httpcli.IsErrNotLeader(err)) {
if err == nil || (err != nil && !apierrors.IsErrNotLeader(err)) {
return resp, err
}
res, ok := resp.(*httpcli.Response)