Add mod/lock connection monitoring.

release-0.4
Ben Johnson 2013-12-04 16:23:27 -07:00
parent df20be775c
commit f3d438a93f
2 changed files with 76 additions and 25 deletions

View File

@ -6,13 +6,22 @@ import (
"strconv" "strconv"
"time" "time"
"github.com/coreos/go-etcd/etcd"
"github.com/gorilla/mux" "github.com/gorilla/mux"
) )
// acquireHandler attempts to acquire a lock on the given key. // acquireHandler attempts to acquire a lock on the given key.
// The "key" parameter specifies the resource to 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) {
h.client.SyncCluster() h.client.SyncCluster()
// Setup connection watcher.
closeNotifier, _ := w.(http.CloseNotifier)
closeChan := closeNotifier.CloseNotify()
// Parse "key" and "ttl" query parameters.
vars := mux.Vars(req) vars := mux.Vars(req)
keypath := path.Join(prefix, vars["key"]) keypath := path.Join(prefix, vars["key"])
ttl, err := strconv.Atoi(req.FormValue("ttl")) ttl, err := strconv.Atoi(req.FormValue("ttl"))
@ -20,6 +29,16 @@ func (h *handler) acquireHandler(w http.ResponseWriter, req *http.Request) {
http.Error(w, "invalid ttl: " + err.Error(), http.StatusInternalServerError) http.Error(w, "invalid ttl: " + err.Error(), http.StatusInternalServerError)
return return
} }
// Parse "timeout" parameter.
var timeout int
if len(req.FormValue("timeout")) == 0 {
timeout = -1
} else if timeout, err = strconv.Atoi(req.FormValue("timeout")); err != nil {
http.Error(w, "invalid timeout: " + err.Error(), http.StatusInternalServerError)
return
}
timeout = timeout + 1
// Create an incrementing id for the lock. // Create an incrementing id for the lock.
resp, err := h.client.AddChild(keypath, "-", uint64(ttl)) resp, err := h.client.AddChild(keypath, "-", uint64(ttl))
@ -30,32 +49,31 @@ func (h *handler) acquireHandler(w http.ResponseWriter, req *http.Request) {
indexpath := resp.Key indexpath := resp.Key
// Keep updating TTL to make sure lock request is not expired before acquisition. // Keep updating TTL to make sure lock request is not expired before acquisition.
stopChan := make(chan bool) stop := make(chan bool)
defer close(stopChan) go h.ttlKeepAlive(indexpath, ttl, stop)
go func(k string) {
stopped := false // Monitor for broken connection.
for { stopWatchChan := make(chan bool)
select { go func() {
case <-time.After(time.Duration(ttl / 2) * time.Second): select {
case <-stopChan: case <-closeChan:
stopped = true stopWatchChan <- true
} case <-stop:
h.client.Update(k, "-", uint64(ttl)) // Stop watching for connection disconnect.
if stopped {
break
}
} }
}(indexpath) }()
// Extract the lock index. // Extract the lock index.
index, _ := strconv.Atoi(path.Base(resp.Key)) index, _ := strconv.Atoi(path.Base(resp.Key))
// Wait until we successfully get a lock or we get a failure.
var success bool
for { for {
// Read all indices. // Read all indices.
resp, err = h.client.GetAll(keypath, true) resp, err = h.client.GetAll(keypath, true)
if err != nil { if err != nil {
http.Error(w, "lock children lookup error: " + err.Error(), http.StatusInternalServerError) http.Error(w, "lock children lookup error: " + err.Error(), http.StatusInternalServerError)
return break
} }
indices := extractResponseIndices(resp) indices := extractResponseIndices(resp)
waitIndex := resp.ModifiedIndex waitIndex := resp.ModifiedIndex
@ -63,17 +81,48 @@ func (h *handler) acquireHandler(w http.ResponseWriter, req *http.Request) {
// If there is no previous index then we have the lock. // If there is no previous index then we have the lock.
if prevIndex == 0 { if prevIndex == 0 {
success = true
break break
} }
// Otherwise watch previous index until it's gone. // Otherwise watch previous index until it's gone.
_, err = h.client.Watch(path.Join(keypath, strconv.Itoa(prevIndex)), waitIndex, nil, nil) _, err = h.client.Watch(path.Join(keypath, strconv.Itoa(prevIndex)), waitIndex, nil, stopWatchChan)
if err != nil { if err == etcd.ErrWatchStoppedByUser {
break
} else if err != nil {
http.Error(w, "lock watch error: " + err.Error(), http.StatusInternalServerError) http.Error(w, "lock watch error: " + err.Error(), http.StatusInternalServerError)
return break
} }
} }
// Write lock index to response body. // Check for connection disconnect before we write the lock index.
w.Write([]byte(strconv.Itoa(index))) select {
case <-stopWatchChan:
success = false
default:
}
// Stop the ttl keep-alive.
close(stop)
if success {
// Write lock index to response body if we acquire the lock.
h.client.Update(indexpath, "-", uint64(ttl))
w.Write([]byte(strconv.Itoa(index)))
} else {
// Make sure key is deleted if we couldn't acquire.
h.client.Delete(indexpath)
}
}
// ttlKeepAlive continues to update a key's TTL until the stop channel is closed.
func (h *handler) ttlKeepAlive(k string, ttl int, stop chan bool) {
for {
select {
case <-time.After(time.Duration(ttl / 2) * time.Second):
h.client.Update(k, "-", uint64(ttl))
case <-stop:
return
}
}
} }

10
test.sh
View File

@ -1,7 +1,9 @@
#!/bin/sh #!/bin/sh
set -e set -e
PKGS="./store ./server ./server/v2/tests ./mod/lock/tests" if [ -z "$PKG" ]; then
PKG="./store ./server ./server/v2/tests ./mod/lock/tests"
fi
# Get GOPATH, etc from build # Get GOPATH, etc from build
. ./build . ./build
@ -10,10 +12,10 @@ PKGS="./store ./server ./server/v2/tests ./mod/lock/tests"
export GOPATH="${PWD}" export GOPATH="${PWD}"
# Unit tests # Unit tests
for PKG in $PKGS for i in $PKG
do do
go test -i $PKG go test -i $i
go test -v $PKG go test -v $i
done done
# Functional tests # Functional tests