Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Experimental download-style streaming responses #110

Merged
merged 7 commits into from
Jun 22, 2018
7 changes: 5 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,11 @@ generate:
PATH=$(CURDIR)/_tools/bin:$(PATH) GOBIN="${PWD}/bin" go install -v ./protoc-gen-...
$(RETOOL) do go generate ./...

test_all: setup test_core test_clients
test_all: setup test_core test_clients test_example

test_core: generate
# $(RETOOL) do errcheck -blank ./internal/twirptest
go test -race $(shell go list ./... | grep -v /vendor/ | grep -v /_tools/)
go test -race $(shell go list ./... | grep -v /vendor/ | grep -v /_tools/ | grep -v /example/)

test_clients: test_go_client test_python_client

Expand All @@ -25,6 +25,9 @@ test_go_client: generate build/clientcompat build/gocompat
test_python_client: generate build/clientcompat build/pycompat
./build/clientcompat -client ./build/pycompat

test_example: generate
go test -race -bench=. $(shell go list ./example/...)

setup:
./install_proto.bash
GOPATH=$(CURDIR)/_tools go install github.com/twitchtv/retool/...
Expand Down
22 changes: 16 additions & 6 deletions clientcompat/internal/clientcompat/clientcompat.twirp.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

47 changes: 44 additions & 3 deletions example/cmd/client/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,21 +15,25 @@ package main

import (
"context"
"fmt"
"log"
"net/http"
"time"

"github.com/twitchtv/twirp"
"github.com/twitchtv/twirp/example"
)

func main() {
client := example.NewHaberdasherJSONClient("http://localhost:8080", &http.Client{})
client := example.NewHaberdasherProtobufClient("http://localhost:8080", &http.Client{})

var (
hat *example.Hat
err error
)

//
// Call the MakeHat rpc
//
for i := 0; i < 5; i++ {
hat, err = client.MakeHat(context.Background(), &example.Size{Inches: 12})
if err != nil {
Expand All @@ -43,6 +47,43 @@ func main() {
// This was some fatal error!
log.Fatal(err)
}
break
}
log.Println(`Response from MakeHat:`)
log.Printf("\t%+v\n", hat)

//
// Call the MakeHats streaming rpc
//
const (
printEvery = 50000
quantity = int32(300000)
)
reqSentAt := time.Now()
hatStream, err := client.MakeHats(
context.Background(),
&example.MakeHatsReq{Inches: 12, Quantity: quantity},
)
if err != nil {
log.Fatal(err)
}
log.Printf("Response from MakeHats:\n")
ii := 1
printResults := func() {
took := time.Now().Sub(reqSentAt)
khps := float64(ii-1) / took.Seconds() / 1000
log.Printf("Received %.1f kHats per second (%d hats in %f seconds)\n", khps, ii-1, took.Seconds())
}
for hatOrErr := range hatStream {
if hatOrErr.Err != nil {
printResults()
log.Fatal(hatOrErr.Err)
}
if ii%printEvery == 0 {
khps := float64(ii) / time.Now().Sub(reqSentAt).Seconds() / 1000
log.Printf("\t[%4.1f khps] %6d %+v\n", khps, ii, hatOrErr.Msg)
}
ii++
}
fmt.Printf("%+v", hat)
printResults()
}
45 changes: 39 additions & 6 deletions example/cmd/server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,17 +27,50 @@ import (

type randomHaberdasher struct{}

func (h *randomHaberdasher) MakeHat(ctx context.Context, size *example.Size) (*example.Hat, error) {
if size.Inches <= 0 {
return nil, twirp.InvalidArgumentError("Inches", "I can't make a hat that small!")
var (
errTooSmall = twirp.InvalidArgumentError("Inches", "I can't make hats that small!")
errNegativeQuantity = twirp.InvalidArgumentError("Quantity", "I can't make a negative quantity of hats!")
)

func newRandomHat(inches int32) (*example.Hat, error) {
if inches <= 0 {
return nil, errTooSmall
}
return &example.Hat{
Size: size.Inches,
Color: []string{"white", "black", "brown", "red", "blue"}[rand.Intn(4)],
Name: []string{"bowler", "baseball cap", "top hat", "derby"}[rand.Intn(3)],
Size: inches,
Color: []string{"white", "black", "brown", "red", "blue"}[rand.Intn(5)],
Name: []string{"bowler", "baseball cap", "top hat", "derby"}[rand.Intn(4)],
}, nil
}

func (h *randomHaberdasher) MakeHat(ctx context.Context, size *example.Size) (*example.Hat, error) {
return newRandomHat(size.Inches)
}

func (h *randomHaberdasher) MakeHats(ctx context.Context, req *example.MakeHatsReq) (<-chan example.HatOrError, error) {
if req.Quantity < 0 {
return nil, errNegativeQuantity
}
// Normally we'd validate Inches here as well, but we let it fall through to error on newRandomHat to demonstrate mid-stream errors
// if req.Inches <= 0 {
// return nil, errTooSmall
// }

ch := make(chan example.HatOrError, 100) // NB: the size of this buffer can make a big difference!
go func() {
defer close(ch)
for ii := int32(0); ii < req.Quantity; ii++ {
hat, err := newRandomHat(req.Inches)
select {
case <-ctx.Done():
return
case ch <- example.HatOrError{Msg: hat, Err: err}:
}
}
}()
return ch, nil
}

func main() {
hook := statsd.NewStatsdServerHooks(LoggingStatter{os.Stderr})
server := example.NewHaberdasherServer(&randomHaberdasher{}, hook)
Expand Down
Loading