Skip to content

Commit

Permalink
Merge pull request #2 from phlogistonjohn/jjm-libext
Browse files Browse the repository at this point in the history
extend the asynchttp library to make it more flexible
  • Loading branch information
obnoxxx committed Apr 4, 2018
2 parents 738570e + a382a24 commit aa6a652
Show file tree
Hide file tree
Showing 3 changed files with 177 additions and 24 deletions.
9 changes: 2 additions & 7 deletions .travis.yml
Expand Up @@ -8,16 +8,11 @@ env:
secure: ENMA5XgK92+t5tia+tgiCSx2mrXSEwLKgbRkj7ZKezdCdH2Hq9/waf7zWyXxXZ3ZQOzvQEkzHDRULwoaareST4cTY365euHT7UW4uFEqFOfz2kk8HrCFkHPJnkXaosgBqGfwB7uxFpxqcFzaZ4YPcXdYdKEfqaz2Q7MJsQdXpqlFRWa3pmwMj8nu//b/0MRfRvQUya420jhHqyt4G8os+2/VcTt8XCi83nr/RaKSyAyVZLl/5zwvQPmv0TRt/VobvnxNGBpwFxzFhxF9JH0VS/h4Mu/j0QNSQ0Rg4ip8JzQxjzvdXmT4CVQX9f+42NrI7MmserpjSy8OZBRpUkfX0eTFWJL/pzgpYxdhW3/8dA0/IGoCj0kGmvoW0x4KSn8XKOPKJYZkdZwKSzNYMbqEDZY0rXS7AAnqVkoMjgz8JQcSfo17Lwvu+7bztK2/JnMQlkOxY+HNSaeACeKKxco24vrtvfoUW4xx0c3scju82NrngaiXrffLymbN4U5DoEP70Hd/BLeMfcP+34MDn+AOLBOeGTgU6nbziY9gZhlPdBGO6aG5PrwXEJDtnpQNU1Pe8/b8QYvIkWAY77cZlQfg6eQ27a/u5U5MLfCIH9kebERtsNyXqIoOnQXbIFma9m1UWr4L7FQSndmYnkbrMhIXvXEJdDghUtGEwvAaOlyj5MI=
matrix:
include:
- go: 1.2.2
env: OPTIONS=""
- go: 1.3.3
env: OPTIONS="-race"
- go: 1.4.2
- go: "1.10"
env: COVERAGE="true" GOTOOLS="yes"
- go: 1.5
- go: 1.8.7
env: OPTIONS="-race" GOTOOLS="yes"
before_script:
- if [[ "$GOTOOLS" = "yes" ]] ; then go get golang.org/x/tools/cmd/vet; fi
- if [[ "$GOTOOLS" = "yes" ]] ; then go get golang.org/x/tools/cmd/cover; fi
script:
- if [[ "$GOTOOLS" = "yes" ]] ; then go fmt ./... | wc -l | grep 0 ; fi
Expand Down
67 changes: 50 additions & 17 deletions asynchttp.go
Expand Up @@ -16,12 +16,13 @@
package rest

import (
"github.com/gorilla/mux"
"github.com/heketi/utils"
"github.com/lpabon/godbc"
"net/http"
"sync"
"time"

"github.com/gorilla/mux"
"github.com/heketi/utils"
"github.com/lpabon/godbc"
)

var (
Expand All @@ -38,6 +39,7 @@ type AsyncHttpHandler struct {

// Manager of asynchronous operations
type AsyncHttpManager struct {
IdGen func() string
lock sync.RWMutex
route string
handlers map[string]*AsyncHttpHandler
Expand All @@ -48,26 +50,42 @@ func NewAsyncHttpManager(route string) *AsyncHttpManager {
return &AsyncHttpManager{
route: route,
handlers: make(map[string]*AsyncHttpHandler),
IdGen: utils.GenUUID,
}
}

// Use to create a new asynchronous operation handler.
// Only use this function if you need to do every step by hand.
// It is recommended to use AsyncHttpRedirectFunc() instead
func (a *AsyncHttpManager) NewHandler() *AsyncHttpHandler {
return a.NewHandlerWithId(a.NewId())
}

// NewHandlerWithId constructs and returns an AsyncHttpHandler with the
// given ID. Compare to NewHandler() which automatically generates its
// own ID.
func (a *AsyncHttpManager) NewHandlerWithId(id string) *AsyncHttpHandler {
handler := &AsyncHttpHandler{
manager: a,
id: utils.GenUUID(),
id: id,
}

a.lock.Lock()
defer a.lock.Unlock()

_, idPresent := a.handlers[handler.id]
godbc.Require(!idPresent)
a.handlers[handler.id] = handler

return handler
}

// NewId returns a new string id for a handler. This string is not preserved
// internally and must be passed to another function to be used.
func (a *AsyncHttpManager) NewId() string {
return a.IdGen()
}

// Create an asynchronous operation handler and return the appropiate
// information the caller.
// This function will call handlerfunc() in a new go routine, then
Expand Down Expand Up @@ -121,21 +139,17 @@ func (a *AsyncHttpManager) AsyncHttpRedirectFunc(w http.ResponseWriter,
handlerfunc func() (string, error)) {

handler := a.NewHandler()
go func() {
logger.Info("Started job %v", handler.id)
handler.handle(handlerfunc)
http.Redirect(w, r, handler.Url(), http.StatusAccepted)
}

ts := time.Now()
url, err := handlerfunc()
logger.Info("Completed job %v in %v", handler.id, time.Since(ts))
func (a *AsyncHttpManager) AsyncHttpRedirectUsing(w http.ResponseWriter,
r *http.Request,
id string,
handlerfunc func() (string, error)) {

if err != nil {
handler.CompletedWithError(err)
} else if url != "" {
handler.CompletedWithLocation(url)
} else {
handler.Completed()
}
}()
handler := a.NewHandlerWithId(id)
handler.handle(handlerfunc)
http.Redirect(w, r, handler.Url(), http.StatusAccepted)
}

Expand Down Expand Up @@ -267,3 +281,22 @@ func (h *AsyncHttpHandler) Completed() {
godbc.Ensure(h.location == "")
godbc.Ensure(h.err == nil)
}

// handle starts running the given function in the background (goroutine).
func (h *AsyncHttpHandler) handle(f func() (string, error)) {
go func() {
logger.Info("Started job %v", h.id)

ts := time.Now()
url, err := f()
logger.Info("Completed job %v in %v", h.id, time.Since(ts))

if err != nil {
h.CompletedWithError(err)
} else if url != "" {
h.CompletedWithLocation(url)
} else {
h.Completed()
}
}()
}
125 changes: 125 additions & 0 deletions asynchttp_test.go
Expand Up @@ -24,6 +24,7 @@ import (
"io/ioutil"
"net/http"
"net/http/httptest"
"strings"
"sync"
"testing"
"time"
Expand Down Expand Up @@ -418,3 +419,127 @@ func TestApplicationWithRedirectFunc(t *testing.T) {
}

}

func TestAsyncHttpRedirectUsing(t *testing.T) {

// Setup asynchronous manager
route := "/x"
manager := NewAsyncHttpManager(route)

// Setup the route
router := mux.NewRouter()
router.HandleFunc(route+"/{id}", manager.HandlerStatus).Methods("GET")
router.HandleFunc("/result", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/plain; charset=UTF-8")
w.WriteHeader(http.StatusOK)
fmt.Fprint(w, "HelloWorld")
}).Methods("GET")

router.HandleFunc("/app", func(w http.ResponseWriter, r *http.Request) {
manager.AsyncHttpRedirectUsing(w, r, "bob", func() (string, error) {
time.Sleep(500 * time.Millisecond)
return "/result", nil
})
}).Methods("GET")

// Setup the server
ts := httptest.NewServer(router)
defer ts.Close()

r, err := http.Get(ts.URL + "/app")
tests.Assert(t, r.StatusCode == http.StatusAccepted)
tests.Assert(t, err == nil)
location, err := r.Location()
tests.Assert(t, err == nil)

tests.Assert(t, strings.Contains(location.String(), "bob"),
`expected "bob" in newloc, got:`, location)

for {
// Since Get automatically redirects, we will
// just keep asking until we get a body
r, err := http.Get(location.String())
tests.Assert(t, err == nil)
tests.Assert(t, r.StatusCode == http.StatusOK)
if r.ContentLength > 0 {
body, err := ioutil.ReadAll(r.Body)
r.Body.Close()
tests.Assert(t, err == nil)
tests.Assert(t, string(body) == "HelloWorld")
break
} else {
tests.Assert(t, r.Header.Get("X-Pending") == "true")
time.Sleep(time.Millisecond)
}
}

}

func TestAsyncHttpRedirectFuncCustomIds(t *testing.T) {

// Setup asynchronous manager
route := "/x"
manager := NewAsyncHttpManager(route)

i := 0
manager.IdGen = func() string {
s := fmt.Sprintf("abc-%v%v%v", i, i, i)
i += 1
return s
}

// Setup the route
router := mux.NewRouter()
router.HandleFunc(route+"/{id}", manager.HandlerStatus).Methods("GET")
router.HandleFunc("/result", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/plain; charset=UTF-8")
w.WriteHeader(http.StatusOK)
fmt.Fprint(w, "HelloWorld")
}).Methods("GET")

router.HandleFunc("/app", func(w http.ResponseWriter, r *http.Request) {
manager.AsyncHttpRedirectFunc(w, r, func() (string, error) {
time.Sleep(500 * time.Millisecond)
return "/result", nil
})
}).Methods("GET")

// Setup the server
ts := httptest.NewServer(router)
defer ts.Close()

for j := 0; j < 4; j++ {
r, err := http.Get(ts.URL + "/app")
tests.Assert(t, r.StatusCode == http.StatusAccepted)
tests.Assert(t, err == nil)
location, err := r.Location()
tests.Assert(t, err == nil)

// test that our custom id generator generated the IDs
// according to our pattern
tests.Assert(t, strings.Contains(location.String(), "abc-"),
`expected "abc-" in newloc, got:`, location)
part := fmt.Sprintf("-%v%v%v", j, j, j)
tests.Assert(t, strings.Contains(location.String(), part),
"expected", part, "in newloc, got:", location)

for {
// Since Get automatically redirects, we will
// just keep asking until we get a body
r, err := http.Get(location.String())
tests.Assert(t, err == nil)
tests.Assert(t, r.StatusCode == http.StatusOK)
if r.ContentLength > 0 {
body, err := ioutil.ReadAll(r.Body)
r.Body.Close()
tests.Assert(t, err == nil)
tests.Assert(t, string(body) == "HelloWorld")
break
} else {
tests.Assert(t, r.Header.Get("X-Pending") == "true")
time.Sleep(time.Millisecond)
}
}
}

}

0 comments on commit aa6a652

Please sign in to comment.