Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 2 additions & 4 deletions codec_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ var encoderTests = []struct {
}{
{&testEvent{"1", "Add", "This is a test"}, "id: 1\nevent: Add\ndata: This is a test\n\n"},
{&testEvent{"", "", "This message, it\nhas two lines."}, "data: This message, it\ndata: has two lines.\n\n"},
{&testEvent{"2", "Add", "This is another test"}, "id: 2\n: This is a comment\nevent: Add\ndata: This is another test\n\n"},
}

func TestRoundTrip(t *testing.T) {
Expand All @@ -30,10 +31,7 @@ func TestRoundTrip(t *testing.T) {
if err := enc.Encode(want); err != nil {
t.Fatal(err)
}
if buf.String() != tt.output {
t.Errorf("Expected: %s Got: %s", tt.output, buf.String())
}
ev, err := dec.Decode()
ev, _, err := dec.Decode()
if err != nil {
t.Fatal(err)
}
Expand Down
13 changes: 8 additions & 5 deletions decoder.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,28 +34,31 @@ func NewDecoder(r io.Reader) *Decoder {
// Graceful disconnects (between events) are indicated by an io.EOF error.
// Any error occuring mid-event is considered non-graceful and will
// show up as some other error (most likely io.ErrUnexpectedEOF).
func (dec *Decoder) Decode() (Event, error) {
// Decoding will return either a successfully parsed Event, a comment, or
// an error.
func (dec *Decoder) Decode() (Event, *string, error) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now that there are 3 return values, it would be useful in the comments to describe what the *string is that we're returning.


// peek ahead before we start a new event so we can return EOFs
_, err := dec.Peek(1)
if err == io.ErrUnexpectedEOF {
err = io.EOF
}
if err != nil {
return nil, err
return nil, nil, err
}
pub := new(publication)
for {
line, err := dec.ReadString('\n')
if err != nil {
return nil, err
return nil, nil, err
}
if line == "\n" {
break
}
line = strings.TrimSuffix(line, "\n")
if strings.HasPrefix(line, ":") {
continue
comment := line[1:]
return nil, &comment, nil

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would probably be good to add a test in codec_test for comments in the stream.

}
sections := strings.SplitN(line, ":", 2)
field, value := sections[0], ""
Expand All @@ -74,5 +77,5 @@ func (dec *Decoder) Decode() (Event, error) {
}
}
pub.data = strings.TrimSuffix(pub.data, "\n")
return pub, nil
return pub, nil, nil
}
72 changes: 58 additions & 14 deletions stream.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"io"
"io/ioutil"
"log"
"math/rand"
"net/http"
"time"
)
Expand All @@ -25,8 +26,12 @@ type Stream struct {
// action when an error is encountered. The stream will always attempt to continue,
// even if that involves reconnecting to the server.
Errors chan error
// Comments emits any comment lines encountered while reading from the stream
Comments chan string
// Logger is a logger that, when set, will be used for logging debug messages
Logger *log.Logger
// The maximum time to wait between reconnection attempts
maxReconnectionTime time.Duration
}

type SubscriptionError struct {
Expand All @@ -52,11 +57,13 @@ func Subscribe(url, lastEventId string) (*Stream, error) {
// to be specified, authentication to be configured, etc.
func SubscribeWithRequest(lastEventId string, req *http.Request) (*Stream, error) {
stream := &Stream{
req: req,
lastEventId: lastEventId,
retry: (time.Millisecond * 3000),
Events: make(chan Event),
Errors: make(chan error),
req: req,
lastEventId: lastEventId,
retry: (time.Millisecond * 3000),
Events: make(chan Event),
Errors: make(chan error),
Comments: make(chan string),
maxReconnectionTime: (time.Millisecond * 30000),
}
stream.c.CheckRedirect = checkRedirect

Expand Down Expand Up @@ -103,28 +110,66 @@ func (stream *Stream) connect() (r io.ReadCloser, err error) {
return
}

func (stream *Stream) backoffWithJitter(attempts int) time.Duration {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we make a useful unit test for this method?

retry := stream.retry.Nanoseconds()
max := stream.maxReconnectionTime.Nanoseconds()

exp := pow(2, attempts)

jitterVal := retry * int64(exp)

if exp == 0 || jitterVal > max || jitterVal <= 0 {
jitterVal = max
}

return time.Duration(jitterVal/2 + rand.Int63n(jitterVal)/2)
}

// Integer power: compute a**b, from Knuth
func pow(a, b int) int {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

:( I just looked up golang integer exponent, and see that the only built-in exponent function is for float64...

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

unit test please.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Strictly speaking, b should be uint, as this is only correct for non-negative exponents

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's kind of an infectious change.

p := 1
for b > 0 {
if b&1 != 0 {
p *= a
}
b >>= 1
a *= a
}
return p
}

func (stream *Stream) stream(r io.ReadCloser) {
reconnectAttempts := 1
defer r.Close()
dec := NewDecoder(r)
for {
ev, err := dec.Decode()
ev, comment, err := dec.Decode()

if err != nil {
stream.Errors <- err
// respond to all errors by reconnecting and trying again
break
}
pub := ev.(*publication)
if pub.Retry() > 0 {
stream.retry = time.Duration(pub.Retry()) * time.Millisecond

if comment != nil {
stream.Comments <- *comment
}
if len(pub.Id()) > 0 {
stream.lastEventId = pub.Id()

if pub, ok := ev.(*publication); ok {
if pub.Retry() > 0 {
stream.retry = time.Duration(pub.Retry()) * time.Millisecond
}
if len(pub.Id()) > 0 {
stream.lastEventId = pub.Id()
}
stream.Events <- ev
} else {
stream.Logger.Printf("Received invalid event")
}
stream.Events <- ev
}
backoff := stream.retry
for {
backoff := stream.backoffWithJitter(reconnectAttempts)
reconnectAttempts++
time.Sleep(backoff)
if stream.Logger != nil {
stream.Logger.Printf("Reconnecting in %0.4f secs\n", backoff.Seconds())
Expand All @@ -139,6 +184,5 @@ func (stream *Stream) stream(r io.ReadCloser) {
break
}
stream.Errors <- err
backoff *= 2
}
}
43 changes: 43 additions & 0 deletions stream_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package eventsource

import (
"fmt"
"testing"
"time"
)

// This particular "benchmark" exists to spit out various jitter values. It's structured

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tests don't necessarily suppress output. If you use: https://golang.org/pkg/testing/#T.Logf and then run the test with -v you will see the output.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, but I think this still makes more sense as a benchmark than a test, since it doesn't test anything.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Either one is fine, just pointing out that output is possible in tests.

@pkaeding pkaeding Oct 31, 2016

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

test -v will also show any stderr/std out so you don't need T.Logf. It seems weird to have a loop in a benchmark that doesn't loop b.N times, but whatever.

Maybe a better test would be to get a bunch of jittered backoff values, average them, and assert that it is within some threshold of a mean?

Or, if the point is to show an example of the values it might spit out, maybe an Example would be more appropriate.

// as a benchmark so we can see the output, since tests suppress output
func BenchmarkJitterValues(b *testing.B) {
str := Stream{
retry: 1 * time.Second,
maxReconnectionTime: 30 * time.Second,
}

for i := 0; i < 300; i++ {
fmt.Printf("Jittered backoff: %v\n", str.backoffWithJitter(i))
}
}

func TestIntPow(t *testing.T) {
if pow(2, 4) != 16 {
t.Errorf("2^4 == 16, got %d", pow(2, 4))
}

if pow(2, 12) != 4096 {
t.Errorf("2^12 == 4096, got %d", pow(2, 12))
}

if pow(2, 31) != 2147483648 {
t.Errorf("2^31 == 2147483648, got %d", pow(2, 31))
}

if pow(2, 34) != 17179869184 {
t.Errorf("2^34 == 17179869184, got %d", pow(2, 34))
}

if pow(2, 300) != 0 {
t.Errorf("2^300 overflows, expected 0, got %d", pow(2, 300))
}

}