etcdhttp: add test for streaming watches
parent
ddc30c0a33
commit
a9caa24f8a
|
@ -7,7 +7,6 @@ import (
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httputil"
|
|
||||||
"net/url"
|
"net/url"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
@ -324,23 +323,18 @@ func handleWatch(ctx context.Context, w http.ResponseWriter, wa store.Watcher, s
|
||||||
}
|
}
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
// WriteHeader will do this implicitly, but best to be explicit.
|
|
||||||
w.Header().Set("Transfer-Encoding", "chunked")
|
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
|
|
||||||
// Ensure headers are flushed early, in case of long polling
|
// Ensure headers are flushed early, in case of long polling
|
||||||
w.(http.Flusher).Flush()
|
w.(http.Flusher).Flush()
|
||||||
|
|
||||||
cw := httputil.NewChunkedWriter(w)
|
|
||||||
|
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-nch:
|
case <-nch:
|
||||||
// Client closed connection. Nothing to do.
|
// Client closed connection. Nothing to do.
|
||||||
return
|
return
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
// Timed out. Close the connection gracefully.
|
// Timed out. net/http will close the connection for us, so nothing to do.
|
||||||
cw.Close()
|
|
||||||
return
|
return
|
||||||
case ev, ok := <-ech:
|
case ev, ok := <-ech:
|
||||||
if !ok {
|
if !ok {
|
||||||
|
@ -349,7 +343,7 @@ func handleWatch(ctx context.Context, w http.ResponseWriter, wa store.Watcher, s
|
||||||
// send to the client in time. Then we simply end streaming.
|
// send to the client in time. Then we simply end streaming.
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err := json.NewEncoder(cw).Encode(ev); err != nil {
|
if err := json.NewEncoder(w).Encode(ev); err != nil {
|
||||||
// Should never be reached
|
// Should never be reached
|
||||||
log.Println("error writing event: %v", err)
|
log.Println("error writing event: %v", err)
|
||||||
return
|
return
|
||||||
|
@ -360,7 +354,6 @@ func handleWatch(ctx context.Context, w http.ResponseWriter, wa store.Watcher, s
|
||||||
w.(http.Flusher).Flush()
|
w.(http.Flusher).Flush()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// allowMethod verifies that the given method is one of the allowed methods,
|
// allowMethod verifies that the given method is one of the allowed methods,
|
||||||
|
|
|
@ -4,7 +4,6 @@ import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
|
@ -964,7 +963,6 @@ func TestServeKeysWatch(t *testing.T) {
|
||||||
Node: &store.NodeExtern{},
|
Node: &store.NodeExtern{},
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
wbody = fmt.Sprintf("%x\r\n%s\r\n", len(wbody), wbody)
|
|
||||||
|
|
||||||
if rw.Code != wcode {
|
if rw.Code != wcode {
|
||||||
t.Errorf("got code=%d, want %d", rw.Code, wcode)
|
t.Errorf("got code=%d, want %d", rw.Code, wcode)
|
||||||
|
@ -988,7 +986,6 @@ func TestHandleWatch(t *testing.T) {
|
||||||
handleWatch(context.Background(), rw, wa, false)
|
handleWatch(context.Background(), rw, wa, false)
|
||||||
|
|
||||||
wcode := http.StatusOK
|
wcode := http.StatusOK
|
||||||
wte := "chunked"
|
|
||||||
wct := "application/json"
|
wct := "application/json"
|
||||||
wbody := mustMarshalEvent(
|
wbody := mustMarshalEvent(
|
||||||
t,
|
t,
|
||||||
|
@ -997,7 +994,6 @@ func TestHandleWatch(t *testing.T) {
|
||||||
Node: &store.NodeExtern{},
|
Node: &store.NodeExtern{},
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
wbody = fmt.Sprintf("%x\r\n%s\r\n", len(wbody), wbody)
|
|
||||||
|
|
||||||
if rw.Code != wcode {
|
if rw.Code != wcode {
|
||||||
t.Errorf("got code=%d, want %d", rw.Code, wcode)
|
t.Errorf("got code=%d, want %d", rw.Code, wcode)
|
||||||
|
@ -1006,9 +1002,6 @@ func TestHandleWatch(t *testing.T) {
|
||||||
if ct := h.Get("Content-Type"); ct != wct {
|
if ct := h.Get("Content-Type"); ct != wct {
|
||||||
t.Errorf("Content-Type=%q, want %q", ct, wct)
|
t.Errorf("Content-Type=%q, want %q", ct, wct)
|
||||||
}
|
}
|
||||||
if te := h.Get("Transfer-Encoding"); te != wte {
|
|
||||||
t.Errorf("Transfer-Encoding=%q, want %q", te, wte)
|
|
||||||
}
|
|
||||||
g := rw.Body.String()
|
g := rw.Body.String()
|
||||||
if g != wbody {
|
if g != wbody {
|
||||||
t.Errorf("got body=%#v, want %#v", g, wbody)
|
t.Errorf("got body=%#v, want %#v", g, wbody)
|
||||||
|
@ -1025,7 +1018,6 @@ func TestHandleWatchNoEvent(t *testing.T) {
|
||||||
handleWatch(context.Background(), rw, wa, false)
|
handleWatch(context.Background(), rw, wa, false)
|
||||||
|
|
||||||
wcode := http.StatusOK
|
wcode := http.StatusOK
|
||||||
wte := "chunked"
|
|
||||||
wct := "application/json"
|
wct := "application/json"
|
||||||
wbody := ""
|
wbody := ""
|
||||||
|
|
||||||
|
@ -1036,9 +1028,6 @@ func TestHandleWatchNoEvent(t *testing.T) {
|
||||||
if ct := h.Get("Content-Type"); ct != wct {
|
if ct := h.Get("Content-Type"); ct != wct {
|
||||||
t.Errorf("Content-Type=%q, want %q", ct, wct)
|
t.Errorf("Content-Type=%q, want %q", ct, wct)
|
||||||
}
|
}
|
||||||
if te := h.Get("Transfer-Encoding"); te != wte {
|
|
||||||
t.Errorf("Transfer-Encoding=%q, want %q", te, wte)
|
|
||||||
}
|
|
||||||
g := rw.Body.String()
|
g := rw.Body.String()
|
||||||
if g != wbody {
|
if g != wbody {
|
||||||
t.Errorf("got body=%#v, want %#v", g, wbody)
|
t.Errorf("got body=%#v, want %#v", g, wbody)
|
||||||
|
@ -1065,7 +1054,6 @@ func TestHandleWatchCloseNotified(t *testing.T) {
|
||||||
handleWatch(context.Background(), rw, wa, false)
|
handleWatch(context.Background(), rw, wa, false)
|
||||||
|
|
||||||
wcode := http.StatusOK
|
wcode := http.StatusOK
|
||||||
wte := "chunked"
|
|
||||||
wct := "application/json"
|
wct := "application/json"
|
||||||
wbody := ""
|
wbody := ""
|
||||||
|
|
||||||
|
@ -1076,9 +1064,6 @@ func TestHandleWatchCloseNotified(t *testing.T) {
|
||||||
if ct := h.Get("Content-Type"); ct != wct {
|
if ct := h.Get("Content-Type"); ct != wct {
|
||||||
t.Errorf("Content-Type=%q, want %q", ct, wct)
|
t.Errorf("Content-Type=%q, want %q", ct, wct)
|
||||||
}
|
}
|
||||||
if te := h.Get("Transfer-Encoding"); te != wte {
|
|
||||||
t.Errorf("Transfer-Encoding=%q, want %q", te, wte)
|
|
||||||
}
|
|
||||||
g := rw.Body.String()
|
g := rw.Body.String()
|
||||||
if g != wbody {
|
if g != wbody {
|
||||||
t.Errorf("got body=%#v, want %#v", g, wbody)
|
t.Errorf("got body=%#v, want %#v", g, wbody)
|
||||||
|
@ -1095,9 +1080,8 @@ func TestHandleWatchTimeout(t *testing.T) {
|
||||||
handleWatch(ctx, rw, wa, false)
|
handleWatch(ctx, rw, wa, false)
|
||||||
|
|
||||||
wcode := http.StatusOK
|
wcode := http.StatusOK
|
||||||
wte := "chunked"
|
|
||||||
wct := "application/json"
|
wct := "application/json"
|
||||||
wbody := "0\r\n"
|
wbody := ""
|
||||||
|
|
||||||
if rw.Code != wcode {
|
if rw.Code != wcode {
|
||||||
t.Errorf("got code=%d, want %d", rw.Code, wcode)
|
t.Errorf("got code=%d, want %d", rw.Code, wcode)
|
||||||
|
@ -1106,15 +1090,123 @@ func TestHandleWatchTimeout(t *testing.T) {
|
||||||
if ct := h.Get("Content-Type"); ct != wct {
|
if ct := h.Get("Content-Type"); ct != wct {
|
||||||
t.Errorf("Content-Type=%q, want %q", ct, wct)
|
t.Errorf("Content-Type=%q, want %q", ct, wct)
|
||||||
}
|
}
|
||||||
if te := h.Get("Transfer-Encoding"); te != wte {
|
|
||||||
t.Errorf("Transfer-Encoding=%q, want %q", te, wte)
|
|
||||||
}
|
|
||||||
g := rw.Body.String()
|
g := rw.Body.String()
|
||||||
if g != wbody {
|
if g != wbody {
|
||||||
t.Errorf("got body=%#v, want %#v", g, wbody)
|
t.Errorf("got body=%#v, want %#v", g, wbody)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestHandleWatchStreaming(t *testing.T) {
|
// flushingRecorder provides a channel to allow users to block until the Recorder is Flushed()
|
||||||
// TODO(jonboulle): me
|
type flushingRecorder struct {
|
||||||
|
*httptest.ResponseRecorder
|
||||||
|
ch chan struct{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fr *flushingRecorder) Flush() {
|
||||||
|
fr.ResponseRecorder.Flush()
|
||||||
|
fr.ch <- struct{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHandleWatchStreaming(t *testing.T) {
|
||||||
|
rw := &flushingRecorder{
|
||||||
|
httptest.NewRecorder(),
|
||||||
|
make(chan struct{}, 1),
|
||||||
|
}
|
||||||
|
wa := &dummyWatcher{
|
||||||
|
echan: make(chan *store.Event),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Launch the streaming handler in the background with a cancellable context
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
done := make(chan struct{})
|
||||||
|
go func() {
|
||||||
|
handleWatch(ctx, rw, wa, true)
|
||||||
|
close(done)
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Expect one Flush for the headers etc.
|
||||||
|
select {
|
||||||
|
case <-rw.ch:
|
||||||
|
case <-time.After(time.Second):
|
||||||
|
t.Fatalf("timed out waiting for flush")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Expect headers but no body
|
||||||
|
wcode := http.StatusOK
|
||||||
|
wct := "application/json"
|
||||||
|
wbody := ""
|
||||||
|
|
||||||
|
if rw.Code != wcode {
|
||||||
|
t.Errorf("got code=%d, want %d", rw.Code, wcode)
|
||||||
|
}
|
||||||
|
h := rw.Header()
|
||||||
|
if ct := h.Get("Content-Type"); ct != wct {
|
||||||
|
t.Errorf("Content-Type=%q, want %q", ct, wct)
|
||||||
|
}
|
||||||
|
g := rw.Body.String()
|
||||||
|
if g != wbody {
|
||||||
|
t.Errorf("got body=%#v, want %#v", g, wbody)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now send the first event
|
||||||
|
select {
|
||||||
|
case wa.echan <- &store.Event{
|
||||||
|
Action: store.Get,
|
||||||
|
Node: &store.NodeExtern{},
|
||||||
|
}:
|
||||||
|
case <-time.After(time.Second):
|
||||||
|
t.Fatal("timed out waiting for send")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for it to be flushed...
|
||||||
|
select {
|
||||||
|
case <-rw.ch:
|
||||||
|
case <-time.After(time.Second):
|
||||||
|
t.Fatalf("timed out waiting for flush")
|
||||||
|
}
|
||||||
|
|
||||||
|
// And check the body is as expected
|
||||||
|
wbody = mustMarshalEvent(
|
||||||
|
t,
|
||||||
|
&store.Event{
|
||||||
|
Action: store.Get,
|
||||||
|
Node: &store.NodeExtern{},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
g = rw.Body.String()
|
||||||
|
if g != wbody {
|
||||||
|
t.Errorf("got body=%#v, want %#v", g, wbody)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rinse and repeat
|
||||||
|
select {
|
||||||
|
case wa.echan <- &store.Event{
|
||||||
|
Action: store.Get,
|
||||||
|
Node: &store.NodeExtern{},
|
||||||
|
}:
|
||||||
|
case <-time.After(time.Second):
|
||||||
|
t.Fatal("timed out waiting for send")
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-rw.ch:
|
||||||
|
case <-time.After(time.Second):
|
||||||
|
t.Fatalf("timed out waiting for flush")
|
||||||
|
}
|
||||||
|
|
||||||
|
// This time, we expect to see both events
|
||||||
|
wbody = wbody + wbody
|
||||||
|
g = rw.Body.String()
|
||||||
|
if g != wbody {
|
||||||
|
t.Errorf("got body=%#v, want %#v", g, wbody)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Finally, time out the connection and ensure the serving goroutine returns
|
||||||
|
cancel()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-done:
|
||||||
|
case <-time.After(time.Second):
|
||||||
|
t.Fatalf("timed out waiting for done")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue