diff --git a/Documentation/api.md b/Documentation/api.md index 51db8eeaa..c2b4c44d3 100644 --- a/Documentation/api.md +++ b/Documentation/api.md @@ -658,6 +658,214 @@ curl -L http://127.0.0.1:4001/v2/keys/ Here we see the `/message` key but our hidden `/_message` key is not returned. + +## Lock Module + +The lock module is used to serialize access to resources used by clients. +Multiple clients can attempt to acquire a lock but only one can have it at a time. +Once the lock is released, the next client waiting for the lock will receive it. + + +### Acquiring a Lock + +To acquire a lock, simply send a `POST` request to the lock module with they lock name and TTL: + +```sh +curl -L http://127.0.0.1:4001/mod/v2/lock/mylock -XPOST -d ttl=20 +``` + +You will receive the lock index when you acquire the lock: + +``` +2 +``` + +If the TTL is not specified or is not a number then you'll receive the following error: + +```json +{ + "errorCode": 202, + "message": "The given TTL in POST form is not a number", + "cause": "Acquire", +} +``` + +If you specify a timeout that is not a number then you'll receive the following error: + +```json +{ + "errorCode": 205, + "message": "The given timeout in POST form is not a number", + "cause": "Acquire", +} +``` + + +### Renewing a Lock + +To extend the TTL of an already acquired lock, simply repeat your original request but with a `PUT` and the lock index instead: + +```sh +curl -L http://127.0.0.1:4001/mod/v2/lock/mylock -XPUT -d index=5 -d ttl=20 +``` + +If the index or value is not specified then you'll receive the following error: + +```json +{ + "errorCode": 207, + "message": "Index or value is required", + "cause": "Renew", +} +``` + +If the index or value does not exist then you'll receive the following error with a `404 Not Found` HTTP code: + +```json +{ + "errorCode": 100, + "message": "Key not found", + "index": 1 +} +``` + +If the TTL is not specified or is not a number then you'll receive the following error: + +```json +{ + "errorCode": 202, + "message": "The given TTL in POST form is not a number", + "cause": "Renew", +} +``` + +### Releasing a Lock + +When the client is finished with the lock, simply send a `DELETE` request to release the lock: + +```sh +curl -L http://127.0.0.1:4001/mod/v2/lock/mylock -XDELETE -d index=5 +``` + +If the index or value is not specified then you'll receive the following error: + +```json +{ + "errorCode": 207, + "message": "Index or value is required", + "cause": "Release", +} +``` + +If the index and value are both specified then you'll receive the following error: + +```json +{ + "errorCode": 208, + "message": "Index and value cannot both be specified", + "cause": "Release", +} +``` + +If the index or value does not exist then you'll receive the following error with a `404 Not Found` HTTP code: + +```json +{ + "errorCode": 100, + "message": "Key not found", + "index": 1 +} +``` + +### Retrieving a Lock + +To determine the current value or index of a lock, send a `GET` request to the lock. +You can specify a `field` of `index` or `value`. +The default is `value`. + +```sh +curl -L http://127.0.0.1:4001/mod/v2/lock/mylock?field=index +``` + +Will return the current index: + +```sh +2 +``` + +If you specify a field other than `field` or `value` then you'll receive the following error: + +```json +{ + "errorCode": 209, + "message": "Invalid field", + "cause": "Get", +} +``` + + +## Leader Module + +The leader module wraps the lock module to provide a simple interface for electing a single leader in a cluster. + + +### Setting the Leader + +A client can attempt to become leader by sending a `PUT` request to the leader module with the name of the leader to elect: + +```sh +curl -L http://127.0.0.1:4001/mod/v2/leader/myclustername -XPUT -d ttl=300 -d name=foo.mydomain.com +``` + +You will receive a successful `200` HTTP response code when the leader is elected. + +If the name is not specified then you'll receive the following error: + +```json +{ + "errorCode": 206, + "message": "Name is required in POST form", + "cause": "Set", +} +``` + +You can also receive any errors specified by the Lock module. + + +### Retrieving the Current Leader + +A client can check to determine if there is a current leader by sending a `GET` request to the leader module: + +```sh +curl -L http://127.0.0.1:4001/mod/v2/leader/myclustername +``` + +You will receive the name of the current leader: + +```sh +foo.mydomain.com +``` + + +### Relinquishing Leadership + +A client can give up leadership by sending a `DELETE` request with the leader name: + +```sh +curl -L http://127.0.0.1:4001/mod/v2/leader/myclustername?name=foo.mydomain.com -XDELETE +``` + +If the name is not specified then you'll receive the following error: + +```json +{ + "errorCode": 206, + "message": "Name is required in POST form", + "cause": "Set", +} +``` + + ## Statistics An etcd cluster keeps track of a number of stastics including latency, bandwidth and uptime. diff --git a/error/error.go b/error/error.go index dd06067ff..cc86c9117 100644 --- a/error/error.go +++ b/error/error.go @@ -35,11 +35,16 @@ const ( EcodeRootROnly = 107 EcodeDirNotEmpty = 108 - EcodeValueRequired = 200 - EcodePrevValueRequired = 201 - EcodeTTLNaN = 202 - EcodeIndexNaN = 203 - EcodeValueOrTTLRequired = 204 + EcodeValueRequired = 200 + EcodePrevValueRequired = 201 + EcodeTTLNaN = 202 + EcodeIndexNaN = 203 + EcodeValueOrTTLRequired = 204 + EcodeTimeoutNaN = 205 + EcodeNameRequired = 206 + EcodeIndexOrValueRequired = 207 + EcodeIndexValueMutex = 208 + EcodeInvalidField = 209 EcodeRaftInternal = 300 EcodeLeaderElect = 301 @@ -68,6 +73,11 @@ func init() { errors[EcodeTTLNaN] = "The given TTL in POST form is not a number" errors[EcodeIndexNaN] = "The given index in POST form is not a number" errors[EcodeValueOrTTLRequired] = "Value or TTL is required in POST form" + errors[EcodeTimeoutNaN] = "The given timeout in POST form is not a number" + errors[EcodeNameRequired] = "Name is required in POST form" + errors[EcodeIndexOrValueRequired] = "Index or value is required" + errors[EcodeIndexValueMutex] = "Index and value cannot both be specified" + errors[EcodeInvalidField] = "Invalid field" // raft related errors errors[EcodeRaftInternal] = "Raft Internal Error" diff --git a/mod/leader/v2/delete_handler.go b/mod/leader/v2/delete_handler.go index 266a96379..278ae34b2 100644 --- a/mod/leader/v2/delete_handler.go +++ b/mod/leader/v2/delete_handler.go @@ -7,22 +7,21 @@ import ( "net/url" "github.com/gorilla/mux" + etcdErr "github.com/coreos/etcd/error" ) -// deleteHandler remove a given leader leader. -func (h *handler) deleteHandler(w http.ResponseWriter, req *http.Request) { +// deleteHandler remove a given leader. +func (h *handler) deleteHandler(w http.ResponseWriter, req *http.Request) error { vars := mux.Vars(req) name := req.FormValue("name") if name == "" { - http.Error(w, "leader name required", http.StatusInternalServerError) - return + return etcdErr.NewError(etcdErr.EcodeNameRequired, "Delete", 0) } // Proxy the request to the the lock service. u, err := url.Parse(fmt.Sprintf("%s/mod/v2/lock/%s", h.addr, vars["key"])) if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return + return err } q := u.Query() q.Set("value", name) @@ -30,20 +29,17 @@ func (h *handler) deleteHandler(w http.ResponseWriter, req *http.Request) { r, err := http.NewRequest("DELETE", u.String(), nil) if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return + return err } // Read from the leader lock. resp, err := h.client.Do(r) if err != nil { - http.Error(w, "delete leader http error: " + err.Error(), http.StatusInternalServerError) - return + return err } defer resp.Body.Close() w.WriteHeader(resp.StatusCode) - if resp.StatusCode != http.StatusOK { - w.Write([]byte("delete leader error: ")) - } io.Copy(w, resp.Body) + + return nil } diff --git a/mod/leader/v2/get_handler.go b/mod/leader/v2/get_handler.go index 7914eb65a..7a96d0ead 100644 --- a/mod/leader/v2/get_handler.go +++ b/mod/leader/v2/get_handler.go @@ -9,21 +9,18 @@ import ( ) // getHandler retrieves the current leader. -func (h *handler) getHandler(w http.ResponseWriter, req *http.Request) { +func (h *handler) getHandler(w http.ResponseWriter, req *http.Request) error { vars := mux.Vars(req) // Proxy the request to the lock service. url := fmt.Sprintf("%s/mod/v2/lock/%s?field=value", h.addr, vars["key"]) resp, err := h.client.Get(url) if err != nil { - http.Error(w, "read leader error: " + err.Error(), http.StatusInternalServerError) - return + return err } defer resp.Body.Close() - if resp.StatusCode != http.StatusOK { - w.Write([]byte("get leader error: ")) - } w.WriteHeader(resp.StatusCode) io.Copy(w, resp.Body) + return nil } diff --git a/mod/leader/v2/handler.go b/mod/leader/v2/handler.go index 3c88278fb..665692b27 100644 --- a/mod/leader/v2/handler.go +++ b/mod/leader/v2/handler.go @@ -4,6 +4,7 @@ import ( "net/http" "github.com/gorilla/mux" + etcdErr "github.com/coreos/etcd/error" ) // prefix is appended to the lock's prefix since the leader mod uses the lock mod. @@ -27,8 +28,22 @@ func NewHandler(addr string) (http.Handler) { addr: addr, } h.StrictSlash(false) - h.HandleFunc("/{key:.*}", h.getHandler).Methods("GET") - h.HandleFunc("/{key:.*}", h.setHandler).Methods("PUT") - h.HandleFunc("/{key:.*}", h.deleteHandler).Methods("DELETE") + h.handleFunc("/{key:.*}", h.getHandler).Methods("GET") + h.handleFunc("/{key:.*}", h.setHandler).Methods("PUT") + h.handleFunc("/{key:.*}", h.deleteHandler).Methods("DELETE") return h } + +func (h *handler) handleFunc(path string, f func(http.ResponseWriter, *http.Request) error) *mux.Route { + return h.Router.HandleFunc(path, func(w http.ResponseWriter, req *http.Request) { + if err := f(w, req); err != nil { + switch err := err.(type) { + case *etcdErr.Error: + w.Header().Set("Content-Type", "application/json") + err.Write(w) + default: + http.Error(w, err.Error(), http.StatusInternalServerError) + } + } + }) +} diff --git a/mod/leader/v2/set_handler.go b/mod/leader/v2/set_handler.go index c517c7cc9..dd2e29702 100644 --- a/mod/leader/v2/set_handler.go +++ b/mod/leader/v2/set_handler.go @@ -7,22 +7,21 @@ import ( "net/url" "github.com/gorilla/mux" + etcdErr "github.com/coreos/etcd/error" ) // setHandler attempts to set the current leader. -func (h *handler) setHandler(w http.ResponseWriter, req *http.Request) { +func (h *handler) setHandler(w http.ResponseWriter, req *http.Request) error { vars := mux.Vars(req) name := req.FormValue("name") if name == "" { - http.Error(w, "leader name required", http.StatusInternalServerError) - return + return etcdErr.NewError(etcdErr.EcodeNameRequired, "Set", 0) } // Proxy the request to the the lock service. u, err := url.Parse(fmt.Sprintf("%s/mod/v2/lock/%s", h.addr, vars["key"])) if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return + return err } q := u.Query() q.Set("value", name) @@ -32,8 +31,7 @@ func (h *handler) setHandler(w http.ResponseWriter, req *http.Request) { r, err := http.NewRequest("POST", u.String(), nil) if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return + return err } // Close request if this connection disconnects. @@ -51,13 +49,10 @@ func (h *handler) setHandler(w http.ResponseWriter, req *http.Request) { // Read from the leader lock. resp, err := h.client.Do(r) if err != nil { - http.Error(w, "set leader http error: " + err.Error(), http.StatusInternalServerError) - return + return err } defer resp.Body.Close() w.WriteHeader(resp.StatusCode) - if resp.StatusCode != http.StatusOK { - w.Write([]byte("set leader error: ")) - } io.Copy(w, resp.Body) + return nil } diff --git a/mod/leader/v2/tests/mod_leader_test.go b/mod/leader/v2/tests/mod_leader_test.go index 71f3fc1ac..238805ce3 100644 --- a/mod/leader/v2/tests/mod_leader_test.go +++ b/mod/leader/v2/tests/mod_leader_test.go @@ -14,23 +14,27 @@ import ( func TestModLeaderSet(t *testing.T) { tests.RunServer(func(s *server.Server) { // Set leader. - body, err := testSetLeader(s, "foo", "xxx", 10) + body, status, err := testSetLeader(s, "foo", "xxx", 10) assert.NoError(t, err) + assert.Equal(t, status, 200) assert.Equal(t, body, "2") // Check that the leader is set. - body, err = testGetLeader(s, "foo") + body, status, err = testGetLeader(s, "foo") assert.NoError(t, err) + assert.Equal(t, status, 200) assert.Equal(t, body, "xxx") // Delete leader. - body, err = testDeleteLeader(s, "foo", "xxx") + body, status, err = testDeleteLeader(s, "foo", "xxx") assert.NoError(t, err) + assert.Equal(t, status, 200) assert.Equal(t, body, "") // Check that the leader is removed. - body, err = testGetLeader(s, "foo") + body, status, err = testGetLeader(s, "foo") assert.NoError(t, err) + assert.Equal(t, status, 200) assert.Equal(t, body, "") }) } @@ -39,42 +43,45 @@ func TestModLeaderSet(t *testing.T) { func TestModLeaderRenew(t *testing.T) { tests.RunServer(func(s *server.Server) { // Set leader. - body, err := testSetLeader(s, "foo", "xxx", 2) + body, status, err := testSetLeader(s, "foo", "xxx", 2) assert.NoError(t, err) + assert.Equal(t, status, 200) assert.Equal(t, body, "2") time.Sleep(1 * time.Second) // Renew leader. - body, err = testSetLeader(s, "foo", "xxx", 3) + body, status, err = testSetLeader(s, "foo", "xxx", 3) assert.NoError(t, err) + assert.Equal(t, status, 200) assert.Equal(t, body, "2") time.Sleep(2 * time.Second) // Check that the leader is set. - body, err = testGetLeader(s, "foo") + body, status, err = testGetLeader(s, "foo") assert.NoError(t, err) + assert.Equal(t, status, 200) assert.Equal(t, body, "xxx") }) } -func testSetLeader(s *server.Server, key string, name string, ttl int) (string, error) { +func testSetLeader(s *server.Server, key string, name string, ttl int) (string, int, error) { resp, err := tests.PutForm(fmt.Sprintf("%s/mod/v2/leader/%s?name=%s&ttl=%d", s.URL(), key, name, ttl), nil) ret := tests.ReadBody(resp) - return string(ret), err + return string(ret), resp.StatusCode, err } -func testGetLeader(s *server.Server, key string) (string, error) { +func testGetLeader(s *server.Server, key string) (string, int, error) { resp, err := tests.Get(fmt.Sprintf("%s/mod/v2/leader/%s", s.URL(), key)) ret := tests.ReadBody(resp) - return string(ret), err + return string(ret), resp.StatusCode, err } -func testDeleteLeader(s *server.Server, key string, name string) (string, error) { +func testDeleteLeader(s *server.Server, key string, name string) (string, int, error) { resp, err := tests.DeleteForm(fmt.Sprintf("%s/mod/v2/leader/%s?name=%s", s.URL(), key, name), nil) ret := tests.ReadBody(resp) - return string(ret), err + return string(ret), resp.StatusCode, err } diff --git a/mod/lock/v2/acquire_handler.go b/mod/lock/v2/acquire_handler.go index 09b50633c..1fd6aeadf 100644 --- a/mod/lock/v2/acquire_handler.go +++ b/mod/lock/v2/acquire_handler.go @@ -8,8 +8,9 @@ import ( "strconv" "time" - "github.com/coreos/go-etcd/etcd" "github.com/gorilla/mux" + "github.com/coreos/go-etcd/etcd" + etcdErr "github.com/coreos/etcd/error" ) // acquireHandler attempts to acquire a lock on the given key. @@ -17,7 +18,7 @@ import ( // The "value" parameter specifies a value to associate with the lock. // The "ttl" parameter specifies how long the lock will persist for. // The "timeout" parameter specifies how long the request should wait for the lock. -func (h *handler) acquireHandler(w http.ResponseWriter, req *http.Request) { +func (h *handler) acquireHandler(w http.ResponseWriter, req *http.Request) error { h.client.SyncCluster() // Setup connection watcher. @@ -36,16 +37,14 @@ func (h *handler) acquireHandler(w http.ResponseWriter, req *http.Request) { if req.FormValue("timeout") == "" { timeout = -1 } else if timeout, err = strconv.Atoi(req.FormValue("timeout")); err != nil { - http.Error(w, "invalid timeout: " + req.FormValue("timeout"), http.StatusInternalServerError) - return + return etcdErr.NewError(etcdErr.EcodeTimeoutNaN, "Acquire", 0) } timeout = timeout + 1 // Parse TTL. ttl, err := strconv.Atoi(req.FormValue("ttl")) if err != nil { - http.Error(w, "invalid ttl: " + req.FormValue("ttl"), http.StatusInternalServerError) - return + return etcdErr.NewError(etcdErr.EcodeTTLNaN, "Acquire", 0) } // If node exists then just watch it. Otherwise create the node and watch it. @@ -65,12 +64,14 @@ func (h *handler) acquireHandler(w http.ResponseWriter, req *http.Request) { // Stop all goroutines. close(stopChan) - // Write response. + // Check for an error. if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - } else { - w.Write([]byte(strconv.Itoa(index))) + return err } + + // Write response. + w.Write([]byte(strconv.Itoa(index))) + return nil } // createNode creates a new lock node and watches it until it is acquired or acquisition fails. @@ -83,7 +84,7 @@ func (h *handler) createNode(keypath string, value string, ttl int, closeChan <- // Create an incrementing id for the lock. resp, err := h.client.AddChild(keypath, value, uint64(ttl)) if err != nil { - return 0, errors.New("acquire lock index error: " + err.Error()) + return 0, err } indexpath := resp.Node.Key index, _ := strconv.Atoi(path.Base(indexpath)) @@ -98,7 +99,7 @@ func (h *handler) createNode(keypath string, value string, ttl int, closeChan <- if err != nil { select { case <-closeChan: - err = errors.New("acquire lock error: user interrupted") + err = errors.New("user interrupted") default: } } @@ -174,7 +175,7 @@ func (h *handler) watch(keypath string, index int, closeChan <- chan bool) error if err == etcd.ErrWatchStoppedByUser { return fmt.Errorf("lock watch closed") } else if err != nil { - return fmt.Errorf("lock watch error:%s", err.Error()) + return fmt.Errorf("lock watch error: %s", err.Error()) } } } diff --git a/mod/lock/v2/get_index_handler.go b/mod/lock/v2/get_index_handler.go index 3473defe4..a9fd4c551 100644 --- a/mod/lock/v2/get_index_handler.go +++ b/mod/lock/v2/get_index_handler.go @@ -5,11 +5,12 @@ import ( "path" "github.com/gorilla/mux" + etcdErr "github.com/coreos/etcd/error" ) // getIndexHandler retrieves the current lock index. // The "field" parameter specifies to read either the lock "index" or lock "value". -func (h *handler) getIndexHandler(w http.ResponseWriter, req *http.Request) { +func (h *handler) getIndexHandler(w http.ResponseWriter, req *http.Request) error { h.client.SyncCluster() vars := mux.Vars(req) @@ -22,8 +23,7 @@ func (h *handler) getIndexHandler(w http.ResponseWriter, req *http.Request) { // Read all indices. resp, err := h.client.Get(keypath, true, true) if err != nil { - http.Error(w, "read lock error: " + err.Error(), http.StatusInternalServerError) - return + return err } nodes := lockNodes{resp.Node.Nodes} @@ -37,7 +37,9 @@ func (h *handler) getIndexHandler(w http.ResponseWriter, req *http.Request) { w.Write([]byte(node.Value)) default: - http.Error(w, "read lock error: invalid field: " + field, http.StatusInternalServerError) + return etcdErr.NewError(etcdErr.EcodeInvalidField, "Get", 0) } } + + return nil } diff --git a/mod/lock/v2/handler.go b/mod/lock/v2/handler.go index 3a84e1b68..10f838635 100644 --- a/mod/lock/v2/handler.go +++ b/mod/lock/v2/handler.go @@ -5,6 +5,7 @@ import ( "github.com/gorilla/mux" "github.com/coreos/go-etcd/etcd" + etcdErr "github.com/coreos/etcd/error" ) const prefix = "/_etcd/mod/lock" @@ -22,9 +23,26 @@ func NewHandler(addr string) (http.Handler) { client: etcd.NewClient([]string{addr}), } h.StrictSlash(false) - h.HandleFunc("/{key:.*}", h.getIndexHandler).Methods("GET") - h.HandleFunc("/{key:.*}", h.acquireHandler).Methods("POST") - h.HandleFunc("/{key:.*}", h.renewLockHandler).Methods("PUT") - h.HandleFunc("/{key:.*}", h.releaseLockHandler).Methods("DELETE") + h.handleFunc("/{key:.*}", h.getIndexHandler).Methods("GET") + h.handleFunc("/{key:.*}", h.acquireHandler).Methods("POST") + h.handleFunc("/{key:.*}", h.renewLockHandler).Methods("PUT") + h.handleFunc("/{key:.*}", h.releaseLockHandler).Methods("DELETE") return h } + +func (h *handler) handleFunc(path string, f func(http.ResponseWriter, *http.Request) error) *mux.Route { + return h.Router.HandleFunc(path, func(w http.ResponseWriter, req *http.Request) { + if err := f(w, req); err != nil { + switch err := err.(type) { + case *etcdErr.Error: + w.Header().Set("Content-Type", "application/json") + err.Write(w) + case etcd.EtcdError: + w.Header().Set("Content-Type", "application/json") + etcdErr.NewError(err.ErrorCode, err.Cause, err.Index).Write(w) + default: + http.Error(w, err.Error(), http.StatusInternalServerError) + } + } + }) +} diff --git a/mod/lock/v2/release_handler.go b/mod/lock/v2/release_handler.go index b3e8344db..5a648c3f3 100644 --- a/mod/lock/v2/release_handler.go +++ b/mod/lock/v2/release_handler.go @@ -5,10 +5,11 @@ import ( "net/http" "github.com/gorilla/mux" + etcdErr "github.com/coreos/etcd/error" ) // releaseLockHandler deletes the lock. -func (h *handler) releaseLockHandler(w http.ResponseWriter, req *http.Request) { +func (h *handler) releaseLockHandler(w http.ResponseWriter, req *http.Request) error { h.client.SyncCluster() vars := mux.Vars(req) @@ -18,34 +19,30 @@ func (h *handler) releaseLockHandler(w http.ResponseWriter, req *http.Request) { index := req.FormValue("index") value := req.FormValue("value") if len(index) == 0 && len(value) == 0 { - http.Error(w, "release lock error: index or value required", http.StatusInternalServerError) - return + return etcdErr.NewError(etcdErr.EcodeIndexOrValueRequired, "Release", 0) } else if len(index) != 0 && len(value) != 0 { - http.Error(w, "release lock error: index and value cannot both be specified", http.StatusInternalServerError) - return + return etcdErr.NewError(etcdErr.EcodeIndexValueMutex, "Release", 0) } // Look up index by value if index is missing. if len(index) == 0 { resp, err := h.client.Get(keypath, true, true) if err != nil { - http.Error(w, "release lock index error: " + err.Error(), http.StatusInternalServerError) - return + return err } nodes := lockNodes{resp.Node.Nodes} node, _ := nodes.FindByValue(value) if node == nil { - http.Error(w, "release lock error: cannot find: " + value, http.StatusInternalServerError) - return + return etcdErr.NewError(etcdErr.EcodeKeyNotFound, "Release", 0) } index = path.Base(node.Key) } // Delete the lock. - _, err := h.client.Delete(path.Join(keypath, index), false) - if err != nil { - http.Error(w, "release lock error: " + err.Error(), http.StatusInternalServerError) - return + if _, err := h.client.Delete(path.Join(keypath, index), false); err != nil { + return err } + + return nil } diff --git a/mod/lock/v2/renew_handler.go b/mod/lock/v2/renew_handler.go index 9d209d582..c44ed51c5 100644 --- a/mod/lock/v2/renew_handler.go +++ b/mod/lock/v2/renew_handler.go @@ -6,11 +6,12 @@ import ( "strconv" "github.com/gorilla/mux" + etcdErr "github.com/coreos/etcd/error" ) // renewLockHandler attempts to update the TTL on an existing lock. // Returns a 200 OK if successful. Returns non-200 on error. -func (h *handler) renewLockHandler(w http.ResponseWriter, req *http.Request) { +func (h *handler) renewLockHandler(w http.ResponseWriter, req *http.Request) error { h.client.SyncCluster() // Read the lock path. @@ -20,31 +21,26 @@ func (h *handler) renewLockHandler(w http.ResponseWriter, req *http.Request) { // Parse new TTL parameter. ttl, err := strconv.Atoi(req.FormValue("ttl")) if err != nil { - http.Error(w, "invalid ttl: " + err.Error(), http.StatusInternalServerError) - return + return etcdErr.NewError(etcdErr.EcodeTTLNaN, "Renew", 0) } // Read and set defaults for index and value. index := req.FormValue("index") value := req.FormValue("value") if len(index) == 0 && len(value) == 0 { - // The index or value is required. - http.Error(w, "renew lock error: index or value required", http.StatusInternalServerError) - return + return etcdErr.NewError(etcdErr.EcodeIndexOrValueRequired, "Renew", 0) } if len(index) == 0 { // If index is not specified then look it up by value. resp, err := h.client.Get(keypath, true, true) if err != nil { - http.Error(w, "renew lock index error: " + err.Error(), http.StatusInternalServerError) - return + return err } nodes := lockNodes{resp.Node.Nodes} node, _ := nodes.FindByValue(value) if node == nil { - http.Error(w, "renew lock error: cannot find: " + value, http.StatusInternalServerError) - return + return etcdErr.NewError(etcdErr.EcodeKeyNotFound, "Renew", 0) } index = path.Base(node.Key) @@ -52,16 +48,15 @@ func (h *handler) renewLockHandler(w http.ResponseWriter, req *http.Request) { // If value is not specified then default it to the previous value. resp, err := h.client.Get(path.Join(keypath, index), true, false) if err != nil { - http.Error(w, "renew lock value error: " + err.Error(), http.StatusInternalServerError) - return + return err } value = resp.Node.Value } // Renew the lock, if it exists. - _, err = h.client.Update(path.Join(keypath, index), value, uint64(ttl)) - if err != nil { - http.Error(w, "renew lock error: " + err.Error(), http.StatusInternalServerError) - return + if _, err = h.client.Update(path.Join(keypath, index), value, uint64(ttl)); err != nil { + return err } + + return nil } diff --git a/mod/lock/v2/tests/mod_lock_test.go b/mod/lock/v2/tests/mod_lock_test.go index d135290f2..dd47a040d 100644 --- a/mod/lock/v2/tests/mod_lock_test.go +++ b/mod/lock/v2/tests/mod_lock_test.go @@ -14,23 +14,27 @@ import ( func TestModLockAcquireAndRelease(t *testing.T) { tests.RunServer(func(s *server.Server) { // Acquire lock. - body, err := testAcquireLock(s, "foo", "", 10) + body, status, err := testAcquireLock(s, "foo", "", 10) assert.NoError(t, err) + assert.Equal(t, status, 200) assert.Equal(t, body, "2") // Check that we have the lock. - body, err = testGetLockIndex(s, "foo") + body, status, err = testGetLockIndex(s, "foo") assert.NoError(t, err) + assert.Equal(t, status, 200) assert.Equal(t, body, "2") // Release lock. - body, err = testReleaseLock(s, "foo", "2", "") + body, status, err = testReleaseLock(s, "foo", "2", "") assert.NoError(t, err) + assert.Equal(t, status, 200) assert.Equal(t, body, "") // Check that we have the lock. - body, err = testGetLockIndex(s, "foo") + body, status, err = testGetLockIndex(s, "foo") assert.NoError(t, err) + assert.Equal(t, status, 200) assert.Equal(t, body, "") }) } @@ -42,8 +46,9 @@ func TestModLockBlockUntilAcquire(t *testing.T) { // Acquire lock #1. go func() { - body, err := testAcquireLock(s, "foo", "", 10) + body, status, err := testAcquireLock(s, "foo", "", 10) assert.NoError(t, err) + assert.Equal(t, status, 200) assert.Equal(t, body, "2") c <- true }() @@ -53,8 +58,9 @@ func TestModLockBlockUntilAcquire(t *testing.T) { waiting := true go func() { c <- true - body, err := testAcquireLock(s, "foo", "", 10) + body, status, err := testAcquireLock(s, "foo", "", 10) assert.NoError(t, err) + assert.Equal(t, status, 200) assert.Equal(t, body, "4") waiting = false }() @@ -63,29 +69,34 @@ func TestModLockBlockUntilAcquire(t *testing.T) { time.Sleep(1 * time.Second) // Check that we have the lock #1. - body, err := testGetLockIndex(s, "foo") + body, status, err := testGetLockIndex(s, "foo") assert.NoError(t, err) + assert.Equal(t, status, 200) assert.Equal(t, body, "2") // Check that we are still waiting for lock #2. assert.Equal(t, waiting, true) // Release lock #1. - body, err = testReleaseLock(s, "foo", "2", "") + _, status, err = testReleaseLock(s, "foo", "2", "") assert.NoError(t, err) + assert.Equal(t, status, 200) // Check that we have lock #2. - body, err = testGetLockIndex(s, "foo") + body, status, err = testGetLockIndex(s, "foo") assert.NoError(t, err) + assert.Equal(t, status, 200) assert.Equal(t, body, "4") // Release lock #2. - body, err = testReleaseLock(s, "foo", "4", "") + _, status, err = testReleaseLock(s, "foo", "4", "") assert.NoError(t, err) + assert.Equal(t, status, 200) // Check that we have no lock. - body, err = testGetLockIndex(s, "foo") + body, status, err = testGetLockIndex(s, "foo") assert.NoError(t, err) + assert.Equal(t, status, 200) assert.Equal(t, body, "") }) } @@ -97,8 +108,9 @@ func TestModLockExpireAndRelease(t *testing.T) { // Acquire lock #1. go func() { - body, err := testAcquireLock(s, "foo", "", 2) + body, status, err := testAcquireLock(s, "foo", "", 2) assert.NoError(t, err) + assert.Equal(t, status, 200) assert.Equal(t, body, "2") c <- true }() @@ -107,8 +119,9 @@ func TestModLockExpireAndRelease(t *testing.T) { // Acquire lock #2. go func() { c <- true - body, err := testAcquireLock(s, "foo", "", 10) + body, status, err := testAcquireLock(s, "foo", "", 10) assert.NoError(t, err) + assert.Equal(t, status, 200) assert.Equal(t, body, "4") }() <- c @@ -116,16 +129,18 @@ func TestModLockExpireAndRelease(t *testing.T) { time.Sleep(1 * time.Second) // Check that we have the lock #1. - body, err := testGetLockIndex(s, "foo") + body, status, err := testGetLockIndex(s, "foo") assert.NoError(t, err) + assert.Equal(t, status, 200) assert.Equal(t, body, "2") // Wait for lock #1 TTL. time.Sleep(2 * time.Second) // Check that we have lock #2. - body, err = testGetLockIndex(s, "foo") + body, status, err = testGetLockIndex(s, "foo") assert.NoError(t, err) + assert.Equal(t, status, 200) assert.Equal(t, body, "4") }) } @@ -134,34 +149,39 @@ func TestModLockExpireAndRelease(t *testing.T) { func TestModLockRenew(t *testing.T) { tests.RunServer(func(s *server.Server) { // Acquire lock. - body, err := testAcquireLock(s, "foo", "", 3) + body, status, err := testAcquireLock(s, "foo", "", 3) assert.NoError(t, err) + assert.Equal(t, status, 200) assert.Equal(t, body, "2") time.Sleep(2 * time.Second) // Check that we have the lock. - body, err = testGetLockIndex(s, "foo") + body, status, err = testGetLockIndex(s, "foo") assert.NoError(t, err) + assert.Equal(t, status, 200) assert.Equal(t, body, "2") // Renew lock. - body, err = testRenewLock(s, "foo", "2", "", 3) + body, status, err = testRenewLock(s, "foo", "2", "", 3) assert.NoError(t, err) + assert.Equal(t, status, 200) assert.Equal(t, body, "") time.Sleep(2 * time.Second) // Check that we still have the lock. - body, err = testGetLockIndex(s, "foo") + body, status, err = testGetLockIndex(s, "foo") assert.NoError(t, err) + assert.Equal(t, status, 200) assert.Equal(t, body, "2") time.Sleep(2 * time.Second) // Check that lock was released. - body, err = testGetLockIndex(s, "foo") + body, status, err = testGetLockIndex(s, "foo") assert.NoError(t, err) + assert.Equal(t, status, 200) assert.Equal(t, body, "") }) } @@ -170,55 +190,59 @@ func TestModLockRenew(t *testing.T) { func TestModLockAcquireAndReleaseByValue(t *testing.T) { tests.RunServer(func(s *server.Server) { // Acquire lock. - body, err := testAcquireLock(s, "foo", "XXX", 10) + body, status, err := testAcquireLock(s, "foo", "XXX", 10) assert.NoError(t, err) + assert.Equal(t, status, 200) assert.Equal(t, body, "2") // Check that we have the lock. - body, err = testGetLockValue(s, "foo") + body, status, err = testGetLockValue(s, "foo") assert.NoError(t, err) + assert.Equal(t, status, 200) assert.Equal(t, body, "XXX") // Release lock. - body, err = testReleaseLock(s, "foo", "", "XXX") + body, status, err = testReleaseLock(s, "foo", "", "XXX") assert.NoError(t, err) + assert.Equal(t, status, 200) assert.Equal(t, body, "") // Check that we released the lock. - body, err = testGetLockValue(s, "foo") + body, status, err = testGetLockValue(s, "foo") assert.NoError(t, err) + assert.Equal(t, status, 200) assert.Equal(t, body, "") }) } -func testAcquireLock(s *server.Server, key string, value string, ttl int) (string, error) { +func testAcquireLock(s *server.Server, key string, value string, ttl int) (string, int, error) { resp, err := tests.PostForm(fmt.Sprintf("%s/mod/v2/lock/%s?value=%s&ttl=%d", s.URL(), key, value, ttl), nil) ret := tests.ReadBody(resp) - return string(ret), err + return string(ret), resp.StatusCode, err } -func testGetLockIndex(s *server.Server, key string) (string, error) { +func testGetLockIndex(s *server.Server, key string) (string, int, error) { resp, err := tests.Get(fmt.Sprintf("%s/mod/v2/lock/%s?field=index", s.URL(), key)) ret := tests.ReadBody(resp) - return string(ret), err + return string(ret), resp.StatusCode, err } -func testGetLockValue(s *server.Server, key string) (string, error) { +func testGetLockValue(s *server.Server, key string) (string, int, error) { resp, err := tests.Get(fmt.Sprintf("%s/mod/v2/lock/%s", s.URL(), key)) ret := tests.ReadBody(resp) - return string(ret), err + return string(ret), resp.StatusCode, err } -func testReleaseLock(s *server.Server, key string, index string, value string) (string, error) { +func testReleaseLock(s *server.Server, key string, index string, value string) (string, int, error) { resp, err := tests.DeleteForm(fmt.Sprintf("%s/mod/v2/lock/%s?index=%s&value=%s", s.URL(), key, index, value), nil) ret := tests.ReadBody(resp) - return string(ret), err + return string(ret), resp.StatusCode, err } -func testRenewLock(s *server.Server, key string, index string, value string, ttl int) (string, error) { +func testRenewLock(s *server.Server, key string, index string, value string, ttl int) (string, int, error) { resp, err := tests.PutForm(fmt.Sprintf("%s/mod/v2/lock/%s?index=%s&value=%s&ttl=%d", s.URL(), key, index, value, ttl), nil) ret := tests.ReadBody(resp) - return string(ret), err + return string(ret), resp.StatusCode, err }