Browse Source

client: introduce Error type

Brian Waldon 11 years ago
parent
commit
b174732812
8 changed files with 77 additions and 124 deletions
  1. 5 12
      client/client.go
  2. 6 6
      client/client_test.go
  3. 0 24
      client/error.go
  4. 44 20
      client/keys.go
  5. 1 50
      client/keys_test.go
  6. 12 3
      client/members.go
  7. 5 5
      discovery/discovery.go
  8. 4 4
      discovery/discovery_test.go

+ 5 - 12
client/client.go

@@ -28,19 +28,12 @@ import (
 )
 )
 
 
 var (
 var (
-	ErrTimeout          = context.DeadlineExceeded
-	ErrCanceled         = context.Canceled
-	ErrUnavailable      = errors.New("client: no available etcd endpoints")
-	ErrNoLeader         = errors.New("client: no leader")
-	ErrNoEndpoints      = errors.New("no endpoints available")
-	ErrTooManyRedirects = errors.New("too many redirects")
-
-	ErrKeyNoExist = errors.New("client: key does not exist")
-	ErrKeyExists  = errors.New("client: key already exists")
-
-	DefaultRequestTimeout = 5 * time.Second
+	ErrNoEndpoints      = errors.New("client: no endpoints available")
+	ErrTooManyRedirects = errors.New("client: too many redirects")
 )
 )
 
 
+var DefaultRequestTimeout = 5 * time.Second
+
 var DefaultTransport CancelableTransport = &http.Transport{
 var DefaultTransport CancelableTransport = &http.Transport{
 	Proxy: http.ProxyFromEnvironment,
 	Proxy: http.ProxyFromEnvironment,
 	Dial: (&net.Dialer{
 	Dial: (&net.Dialer{
@@ -203,7 +196,7 @@ func (c *httpClusterClient) Do(ctx context.Context, act httpAction) (resp *http.
 		hc := c.clientFactory(ep)
 		hc := c.clientFactory(ep)
 		resp, body, err = hc.Do(ctx, act)
 		resp, body, err = hc.Do(ctx, act)
 		if err != nil {
 		if err != nil {
-			if err == ErrTimeout || err == ErrCanceled {
+			if err == context.DeadlineExceeded || err == context.Canceled {
 				return nil, nil, err
 				return nil, nil, err
 			}
 			}
 			continue
 			continue

+ 6 - 6
client/client_test.go

@@ -226,32 +226,32 @@ func TestHTTPClusterClientDo(t *testing.T) {
 			wantCode: http.StatusTeapot,
 			wantCode: http.StatusTeapot,
 		},
 		},
 
 
-		// ErrTimeout short-circuits Do
+		// context.DeadlineExceeded short-circuits Do
 		{
 		{
 			client: &httpClusterClient{
 			client: &httpClusterClient{
 				endpoints: []url.URL{fakeURL, fakeURL},
 				endpoints: []url.URL{fakeURL, fakeURL},
 				clientFactory: newStaticHTTPClientFactory(
 				clientFactory: newStaticHTTPClientFactory(
 					[]staticHTTPResponse{
 					[]staticHTTPResponse{
-						staticHTTPResponse{err: ErrTimeout},
+						staticHTTPResponse{err: context.DeadlineExceeded},
 						staticHTTPResponse{resp: http.Response{StatusCode: http.StatusTeapot}},
 						staticHTTPResponse{resp: http.Response{StatusCode: http.StatusTeapot}},
 					},
 					},
 				),
 				),
 			},
 			},
-			wantErr: ErrTimeout,
+			wantErr: context.DeadlineExceeded,
 		},
 		},
 
 
-		// ErrCanceled short-circuits Do
+		// context.Canceled short-circuits Do
 		{
 		{
 			client: &httpClusterClient{
 			client: &httpClusterClient{
 				endpoints: []url.URL{fakeURL, fakeURL},
 				endpoints: []url.URL{fakeURL, fakeURL},
 				clientFactory: newStaticHTTPClientFactory(
 				clientFactory: newStaticHTTPClientFactory(
 					[]staticHTTPResponse{
 					[]staticHTTPResponse{
-						staticHTTPResponse{err: ErrCanceled},
+						staticHTTPResponse{err: context.Canceled},
 						staticHTTPResponse{resp: http.Response{StatusCode: http.StatusTeapot}},
 						staticHTTPResponse{resp: http.Response{StatusCode: http.StatusTeapot}},
 					},
 					},
 				),
 				),
 			},
 			},
-			wantErr: ErrCanceled,
+			wantErr: context.Canceled,
 		},
 		},
 
 
 		// return err if there are no endpoints
 		// return err if there are no endpoints

+ 0 - 24
client/error.go

@@ -1,24 +0,0 @@
-// Copyright 2015 CoreOS, Inc.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//     http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package client
-
-type HTTPError struct {
-	Message string `json:"message"`
-	Code    int    `json:"-"`
-}
-
-func (e HTTPError) Error() string {
-	return e.Message
-}

+ 44 - 20
client/keys.go

@@ -27,6 +27,39 @@ import (
 	"github.com/coreos/etcd/Godeps/_workspace/src/golang.org/x/net/context"
 	"github.com/coreos/etcd/Godeps/_workspace/src/golang.org/x/net/context"
 )
 )
 
 
+const (
+	ErrorCodeKeyNotFound = 100
+	ErrorCodeTestFailed  = 101
+	ErrorCodeNotFile     = 102
+	ErrorCodeNotDir      = 104
+	ErrorCodeNodeExist   = 105
+	ErrorCodeRootROnly   = 107
+	ErrorCodeDirNotEmpty = 108
+
+	ErrorCodePrevValueRequired = 201
+	ErrorCodeTTLNaN            = 202
+	ErrorCodeIndexNaN          = 203
+	ErrorCodeInvalidField      = 209
+	ErrorCodeInvalidForm       = 210
+
+	ErrorCodeRaftInternal = 300
+	ErrorCodeLeaderElect  = 301
+
+	ErrorCodeWatcherCleared    = 400
+	ErrorCodeEventIndexCleared = 401
+)
+
+type Error struct {
+	Code    int    `json:"errorCode"`
+	Message string `json:"message"`
+	Cause   string `json:"cause"`
+	Index   uint64 `json:"index"`
+}
+
+func (e Error) Error() string {
+	return fmt.Sprintf("%v: %v (%v) [%v]", e.Code, e.Message, e.Cause, e.Index)
+}
+
 // PrevExistType is used to define an existence condition when setting
 // PrevExistType is used to define an existence condition when setting
 // or deleting Nodes.
 // or deleting Nodes.
 type PrevExistType string
 type PrevExistType string
@@ -452,15 +485,15 @@ func (a *deleteAction) HTTPRequest(ep url.URL) *http.Request {
 func unmarshalHTTPResponse(code int, header http.Header, body []byte) (res *Response, err error) {
 func unmarshalHTTPResponse(code int, header http.Header, body []byte) (res *Response, err error) {
 	switch code {
 	switch code {
 	case http.StatusOK, http.StatusCreated:
 	case http.StatusOK, http.StatusCreated:
-		res, err = unmarshalSuccessfulResponse(header, body)
+		res, err = unmarshalSuccessfulKeysResponse(header, body)
 	default:
 	default:
-		err = unmarshalErrorResponse(code)
+		err = unmarshalFailedKeysResponse(body)
 	}
 	}
 
 
 	return
 	return
 }
 }
 
 
-func unmarshalSuccessfulResponse(header http.Header, body []byte) (*Response, error) {
+func unmarshalSuccessfulKeysResponse(header http.Header, body []byte) (*Response, error) {
 	var res Response
 	var res Response
 	err := json.Unmarshal(body, &res)
 	err := json.Unmarshal(body, &res)
 	if err != nil {
 	if err != nil {
@@ -468,26 +501,17 @@ func unmarshalSuccessfulResponse(header http.Header, body []byte) (*Response, er
 	}
 	}
 	if header.Get("X-Etcd-Index") != "" {
 	if header.Get("X-Etcd-Index") != "" {
 		res.Index, err = strconv.ParseUint(header.Get("X-Etcd-Index"), 10, 64)
 		res.Index, err = strconv.ParseUint(header.Get("X-Etcd-Index"), 10, 64)
-	}
-	if err != nil {
-		return nil, err
+		if err != nil {
+			return nil, err
+		}
 	}
 	}
 	return &res, nil
 	return &res, nil
 }
 }
 
 
-func unmarshalErrorResponse(code int) error {
-	switch code {
-	case http.StatusNotFound:
-		return ErrKeyNoExist
-	case http.StatusPreconditionFailed:
-		return ErrKeyExists
-	case http.StatusInternalServerError:
-		// this isn't necessarily true
-		return ErrNoLeader
-	case http.StatusGatewayTimeout:
-		return ErrTimeout
-	default:
+func unmarshalFailedKeysResponse(body []byte) error {
+	var etcdErr Error
+	if err := json.Unmarshal(body, &etcdErr); err != nil {
+		return err
 	}
 	}
-
-	return fmt.Errorf("unrecognized HTTP status code %d", code)
+	return etcdErr
 }
 }

+ 1 - 50
client/keys_test.go

@@ -15,7 +15,6 @@
 package client
 package client
 
 
 import (
 import (
-	"errors"
 	"fmt"
 	"fmt"
 	"io/ioutil"
 	"io/ioutil"
 	"net/http"
 	"net/http"
@@ -531,7 +530,7 @@ func TestUnmarshalSuccessfulResponse(t *testing.T) {
 	for i, tt := range tests {
 	for i, tt := range tests {
 		h := make(http.Header)
 		h := make(http.Header)
 		h.Add("X-Etcd-Index", tt.indexHeader)
 		h.Add("X-Etcd-Index", tt.indexHeader)
-		res, err := unmarshalSuccessfulResponse(h, []byte(tt.body))
+		res, err := unmarshalSuccessfulKeysResponse(h, []byte(tt.body))
 		if tt.expectError != (err != nil) {
 		if tt.expectError != (err != nil) {
 			t.Errorf("#%d: expectError=%t, err=%v", i, tt.expectError, err)
 			t.Errorf("#%d: expectError=%t, err=%v", i, tt.expectError, err)
 		}
 		}
@@ -555,51 +554,3 @@ func TestUnmarshalSuccessfulResponse(t *testing.T) {
 		}
 		}
 	}
 	}
 }
 }
-
-func TestUnmarshalErrorResponse(t *testing.T) {
-	unrecognized := errors.New("test fixture")
-
-	tests := []struct {
-		code int
-		want error
-	}{
-		{http.StatusBadRequest, unrecognized},
-		{http.StatusUnauthorized, unrecognized},
-		{http.StatusPaymentRequired, unrecognized},
-		{http.StatusForbidden, unrecognized},
-		{http.StatusNotFound, ErrKeyNoExist},
-		{http.StatusMethodNotAllowed, unrecognized},
-		{http.StatusNotAcceptable, unrecognized},
-		{http.StatusProxyAuthRequired, unrecognized},
-		{http.StatusRequestTimeout, unrecognized},
-		{http.StatusConflict, unrecognized},
-		{http.StatusGone, unrecognized},
-		{http.StatusLengthRequired, unrecognized},
-		{http.StatusPreconditionFailed, ErrKeyExists},
-		{http.StatusRequestEntityTooLarge, unrecognized},
-		{http.StatusRequestURITooLong, unrecognized},
-		{http.StatusUnsupportedMediaType, unrecognized},
-		{http.StatusRequestedRangeNotSatisfiable, unrecognized},
-		{http.StatusExpectationFailed, unrecognized},
-		{http.StatusTeapot, unrecognized},
-
-		{http.StatusInternalServerError, ErrNoLeader},
-		{http.StatusNotImplemented, unrecognized},
-		{http.StatusBadGateway, unrecognized},
-		{http.StatusServiceUnavailable, unrecognized},
-		{http.StatusGatewayTimeout, ErrTimeout},
-		{http.StatusHTTPVersionNotSupported, unrecognized},
-	}
-
-	for i, tt := range tests {
-		want := tt.want
-		if reflect.DeepEqual(unrecognized, want) {
-			want = fmt.Errorf("unrecognized HTTP status code %d", tt.code)
-		}
-
-		got := unmarshalErrorResponse(tt.code)
-		if !reflect.DeepEqual(want, got) {
-			t.Errorf("#%d: want=%v, got=%v", i, want, got)
-		}
-	}
-}

+ 12 - 3
client/members.go

@@ -144,11 +144,11 @@ func (m *httpMembersAPI) Add(ctx context.Context, peerURL string) (*Member, erro
 	}
 	}
 
 
 	if resp.StatusCode != http.StatusCreated {
 	if resp.StatusCode != http.StatusCreated {
-		var httperr HTTPError
-		if err := json.Unmarshal(body, &httperr); err != nil {
+		var merr membersError
+		if err := json.Unmarshal(body, &merr); err != nil {
 			return nil, err
 			return nil, err
 		}
 		}
-		return nil, httperr
+		return nil, merr
 	}
 	}
 
 
 	var memb Member
 	var memb Member
@@ -216,3 +216,12 @@ func v2MembersURL(ep url.URL) *url.URL {
 	ep.Path = path.Join(ep.Path, defaultV2MembersPrefix)
 	ep.Path = path.Join(ep.Path, defaultV2MembersPrefix)
 	return &ep
 	return &ep
 }
 }
+
+type membersError struct {
+	Message string `json:"message"`
+	Code    int    `json:"-"`
+}
+
+func (e membersError) Error() string {
+	return e.Message
+}

+ 5 - 5
discovery/discovery.go

@@ -183,7 +183,7 @@ func (d *discovery) createSelf(contents string) error {
 	resp, err := d.c.Create(ctx, d.selfKey(), contents)
 	resp, err := d.c.Create(ctx, d.selfKey(), contents)
 	cancel()
 	cancel()
 	if err != nil {
 	if err != nil {
-		if err == client.ErrKeyExists {
+		if eerr, ok := err.(*client.Error); ok && eerr.Code == client.ErrorCodeNodeExist {
 			return ErrDuplicateID
 			return ErrDuplicateID
 		}
 		}
 		return err
 		return err
@@ -202,10 +202,10 @@ func (d *discovery) checkCluster() ([]*client.Node, int, uint64, error) {
 	resp, err := d.c.Get(ctx, path.Join(configKey, "size"), nil)
 	resp, err := d.c.Get(ctx, path.Join(configKey, "size"), nil)
 	cancel()
 	cancel()
 	if err != nil {
 	if err != nil {
-		if err == client.ErrKeyNoExist {
+		if eerr, ok := err.(*client.Error); ok && eerr.Code == client.ErrorCodeKeyNotFound {
 			return nil, 0, 0, ErrSizeNotFound
 			return nil, 0, 0, ErrSizeNotFound
 		}
 		}
-		if err == client.ErrTimeout {
+		if err == context.DeadlineExceeded {
 			return d.checkClusterRetry()
 			return d.checkClusterRetry()
 		}
 		}
 		return nil, 0, 0, err
 		return nil, 0, 0, err
@@ -219,7 +219,7 @@ func (d *discovery) checkCluster() ([]*client.Node, int, uint64, error) {
 	resp, err = d.c.Get(ctx, d.cluster, nil)
 	resp, err = d.c.Get(ctx, d.cluster, nil)
 	cancel()
 	cancel()
 	if err != nil {
 	if err != nil {
-		if err == client.ErrTimeout {
+		if err == context.DeadlineExceeded {
 			return d.checkClusterRetry()
 			return d.checkClusterRetry()
 		}
 		}
 		return nil, 0, 0, err
 		return nil, 0, 0, err
@@ -295,7 +295,7 @@ func (d *discovery) waitNodes(nodes []*client.Node, size int, index uint64) ([]*
 		log.Printf("discovery: found %d peer(s), waiting for %d more", len(all), size-len(all))
 		log.Printf("discovery: found %d peer(s), waiting for %d more", len(all), size-len(all))
 		resp, err := w.Next(context.Background())
 		resp, err := w.Next(context.Background())
 		if err != nil {
 		if err != nil {
-			if err == client.ErrTimeout {
+			if err == context.DeadlineExceeded {
 				return d.waitNodesRetry()
 				return d.waitNodesRetry()
 			}
 			}
 			return nil, err
 			return nil, err

+ 4 - 4
discovery/discovery_test.go

@@ -424,7 +424,7 @@ func (c *clientWithResp) Create(ctx context.Context, key string, value string) (
 
 
 func (c *clientWithResp) Get(ctx context.Context, key string, opts *client.GetOptions) (*client.Response, error) {
 func (c *clientWithResp) Get(ctx context.Context, key string, opts *client.GetOptions) (*client.Response, error) {
 	if len(c.rs) == 0 {
 	if len(c.rs) == 0 {
-		return &client.Response{}, client.ErrKeyNoExist
+		return &client.Response{}, &client.Error{Code: client.ErrorCodeKeyNotFound}
 	}
 	}
 	r := c.rs[0]
 	r := c.rs[0]
 	c.rs = append(c.rs[1:], r)
 	c.rs = append(c.rs[1:], r)
@@ -485,7 +485,7 @@ type clientWithRetry struct {
 func (c *clientWithRetry) Create(ctx context.Context, key string, value string) (*client.Response, error) {
 func (c *clientWithRetry) Create(ctx context.Context, key string, value string) (*client.Response, error) {
 	if c.failCount < c.failTimes {
 	if c.failCount < c.failTimes {
 		c.failCount++
 		c.failCount++
-		return nil, client.ErrTimeout
+		return nil, context.DeadlineExceeded
 	}
 	}
 	return c.clientWithResp.Create(ctx, key, value)
 	return c.clientWithResp.Create(ctx, key, value)
 }
 }
@@ -493,7 +493,7 @@ func (c *clientWithRetry) Create(ctx context.Context, key string, value string)
 func (c *clientWithRetry) Get(ctx context.Context, key string, opts *client.GetOptions) (*client.Response, error) {
 func (c *clientWithRetry) Get(ctx context.Context, key string, opts *client.GetOptions) (*client.Response, error) {
 	if c.failCount < c.failTimes {
 	if c.failCount < c.failTimes {
 		c.failCount++
 		c.failCount++
-		return nil, client.ErrTimeout
+		return nil, context.DeadlineExceeded
 	}
 	}
 	return c.clientWithResp.Get(ctx, key, opts)
 	return c.clientWithResp.Get(ctx, key, opts)
 }
 }
@@ -508,7 +508,7 @@ type watcherWithRetry struct {
 func (w *watcherWithRetry) Next(context.Context) (*client.Response, error) {
 func (w *watcherWithRetry) Next(context.Context) (*client.Response, error) {
 	if w.failCount < w.failTimes {
 	if w.failCount < w.failTimes {
 		w.failCount++
 		w.failCount++
-		return nil, client.ErrTimeout
+		return nil, context.DeadlineExceeded
 	}
 	}
 	if len(w.rs) == 0 {
 	if len(w.rs) == 0 {
 		return &client.Response{}, nil
 		return &client.Response{}, nil