author | Sebastian Harl <sh@tokkee.org> | |
Wed, 4 May 2016 07:48:01 +0000 (09:48 +0200) | ||
committer | Sebastian Harl <sh@tokkee.org> | |
Wed, 4 May 2016 07:48:01 +0000 (09:48 +0200) |
This includes a sample RPC server and client along with a sample HTTP server
based on the examples from the talk.
In addition, there are sample integration tests.
based on the examples from the talk.
In addition, there are sample integration tests.
LICENSE | [new file with mode: 0644] | patch | blob |
README | [new file with mode: 0644] | patch | blob |
grpc/client/main.go | [new file with mode: 0644] | patch | blob |
grpc/integration/backend/backend_test.go | [new file with mode: 0644] | patch | blob |
grpc/integration/ui/ui_test.go | [new file with mode: 0644] | patch | blob |
grpc/proto/backend/backend.pb.go | [new file with mode: 0644] | patch | blob |
grpc/proto/backend/backend.proto | [new file with mode: 0644] | patch | blob |
grpc/server/main.go | [new file with mode: 0644] | patch | blob |
grpc/ui/main.go | [new file with mode: 0644] | patch | blob |
diff --git a/LICENSE b/LICENSE
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,24 @@
+Copyright (c) 2016 Sebastian 'tokkee' Harl <sh@tokkee.org>
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions
+are met:
+1. Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+2. Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR
+CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
+OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
+OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
diff --git a/README b/README
--- /dev/null
+++ b/README
@@ -0,0 +1,54 @@
+ tokkee's Go and gRPC talk
+ ===========================
+
+ Supporting material for a talk about Go and gRPC.
+
+Setup
+-----
+
+ Set up the Go workspace in a directory of your choice (<...>):
+
+ $ export GOPATH=<...>
+ $ export PATH=$GOPATH/bin:$PATH
+
+ Get the source:
+
+ $ go get -u tokkee.org/go-talk/grpc/...
+
+ Install the program binaries:
+
+ $ go install tokkee.org/go-talk/grpc/server
+ $ go install tokkee.org/go-talk/grpc/client
+ $ go install tokkee.org/go-talk/grpc/ui
+
+Testing
+-------
+
+ The programs come with a set of sample integration tests which will spin up
+ test instances of the respective tools and run queries against them.
+
+ Run all tests:
+
+ $ go test tokkee.org/go-talk/grpc/integration/...
+
+Rebuilding the generated proto / gRPC code
+------------------------------------------
+
+ Install protoc from https://github.com/google/protobuf/. We need version 3.
+
+ Install the Go gRPC generator plugin:
+
+ $ go get github.com/golang/protobuf/protoc-gen-go
+
+ Rebuild the code:
+
+ $ cd $GOPATH
+ $ protoc src/tokkee.org/go-talk/grpc/proto/backend/backend.proto \
+ --go_out=plugins=grpc:.
+
+Author
+------
+
+ Sebastian 'tokkee' Harl <sh@tokkee.org>
+
+ Licensed under the 2-clause BSD license. See LICENSE for details.
diff --git a/grpc/client/main.go b/grpc/client/main.go
--- /dev/null
+++ b/grpc/client/main.go
@@ -0,0 +1,63 @@
+// Copyright (C) 2016 Sebastian 'tokkee' Harl <sh@tokkee.org>
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions
+// are met:
+// 1. Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// 2. Redistributions in binary form must reproduce the above copyright
+// notice, this list of conditions and the following disclaimer in the
+// documentation and/or other materials provided with the distribution.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+// TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+// PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR
+// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
+// OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
+// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+// ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+// client implements a simple client for the backend protocol.
+package main
+
+import (
+ "flag"
+ "log"
+
+ "golang.org/x/net/context"
+ "google.golang.org/grpc"
+
+ pb "tokkee.org/go-talk/grpc/proto/backend"
+)
+
+var (
+ server = flag.String("server", "localhost:50051", "server address")
+)
+
+func main() {
+ ctx := context.Background()
+ flag.Parse()
+
+ if flag.NArg() != 1 {
+ log.Fatal("Missing query.")
+ }
+ query := flag.Arg(0)
+
+ conn, err := grpc.Dial(*server, grpc.WithInsecure())
+ if err != nil {
+ log.Fatalf("Failed to connect to server at %s: %v", *server, err)
+ }
+ defer conn.Close()
+
+ c := pb.NewBackendClient(conn)
+ res, err := c.Query(ctx, &pb.QueryRequest{Query: query})
+ if err != nil {
+ log.Fatalf("Query failed: %v", err)
+ }
+ log.Printf("%s: %d\n", res.Type, res.N)
+}
diff --git a/grpc/integration/backend/backend_test.go b/grpc/integration/backend/backend_test.go
--- /dev/null
@@ -0,0 +1,225 @@
+// Copyright (C) 2016 Sebastian 'tokkee' Harl <sh@tokkee.org>
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions
+// are met:
+// 1. Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// 2. Redistributions in binary form must reproduce the above copyright
+// notice, this list of conditions and the following disclaimer in the
+// documentation and/or other materials provided with the distribution.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+// TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+// PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR
+// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
+// OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
+// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+// ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+package backend_test
+
+import (
+ "fmt"
+ "log"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "strings"
+ "testing"
+
+ "github.com/golang/protobuf/proto"
+ "golang.org/x/net/context"
+ "google.golang.org/grpc"
+
+ pb "tokkee.org/go-talk/grpc/proto/backend"
+)
+
+var (
+ server string
+ client string
+
+ // TODO: probe for an unused port
+ port = "50051"
+)
+
+func init() {
+ gopath := os.Getenv("GOPATH")
+ if gopath == "" {
+ panic("GOPATH not set")
+ }
+
+ server = filepath.Join(gopath, "bin", "server")
+ client = filepath.Join(gopath, "bin", "client")
+
+ for _, path := range []string{server, client} {
+ if _, err := os.Stat(path); err != nil {
+ if !os.IsNotExist(err) {
+ panic(err)
+ }
+ panic(fmt.Sprintf("%s and/or %s not found; run 'go install tokkee.org/go-talk/grpc/server tokkee.org/go-talk/grpc/client'", server, client))
+ }
+ }
+}
+
+// setup sets up a Backend test instance and returns a client connected to it
+// and a cleanup function to be called when done.
+func setup() (pb.BackendClient, func(), error) {
+ srv := exec.Command(server, "--listen=:"+port)
+ if err := srv.Start(); err != nil {
+ return nil, nil, fmt.Errorf("failed to start server: %v", err)
+ }
+
+ conn, err := grpc.Dial("localhost:"+port, grpc.WithInsecure())
+ if err != nil {
+ srv.Process.Kill()
+ return nil, nil, fmt.Errorf("failed to connect to server: %v", err)
+ }
+
+ return pb.NewBackendClient(conn), func() {
+ conn.Close()
+
+ if err := srv.Process.Kill(); err != nil {
+ log.Printf("Failed to kill server process: %v", err)
+ }
+ srv.Wait()
+ }, nil
+}
+
+// TestServer runs sample queries against the backend server using the RPC
+// interface and checks the results.
+func TestServer(t *testing.T) {
+ ctx := context.Background()
+ c, cleanup, err := setup()
+ if err != nil {
+ t.Fatalf("Setup failed: %v", err)
+ }
+ defer cleanup()
+
+ for _, test := range []struct {
+ query string
+ wantErr bool
+ expected *pb.QueryReply
+ }{
+ {
+ query: "CounT 123456",
+ expected: &pb.QueryReply{
+ Type: "COUNT",
+ N: 6,
+ },
+ },
+ {
+ query: "count abc",
+ expected: &pb.QueryReply{
+ Type: "COUNT",
+ N: 3,
+ },
+ },
+ {
+ query: "count multiple words are supported as well",
+ expected: &pb.QueryReply{
+ Type: "COUNT",
+ N: 36,
+ },
+ },
+ {
+ query: "RANDOM 7",
+ expected: &pb.QueryReply{
+ Type: "RANDOM",
+ N: 4,
+ },
+ },
+ {
+ query: "RANDOM 4",
+ expected: &pb.QueryReply{
+ Type: "RANDOM",
+ N: 0,
+ },
+ },
+ {
+ query: "RANDOM NAN",
+ wantErr: true,
+ },
+ {
+ query: "COUNT",
+ wantErr: true,
+ },
+ {
+ query: "INVALID COMMAND",
+ wantErr: true,
+ },
+ } {
+ req := &pb.QueryRequest{Query: test.query}
+ res, err := c.Query(ctx, req)
+ if (err != nil) != test.wantErr || !proto.Equal(res, test.expected) {
+ e := "<nil>"
+ if test.wantErr {
+ e = "<ERR>"
+ }
+ t.Errorf("c.Query(%v) = %v, %v; want %v, %s", req, res, err, test.expected, e)
+ }
+ }
+}
+
+// TestClient runs sample queries against the backend server using the client
+// program and checks the results.
+func TestClient(t *testing.T) {
+ _, cleanup, err := setup()
+ if err != nil {
+ t.Fatalf("Setup failed: %v", err)
+ }
+ defer cleanup()
+
+ for _, test := range []struct {
+ query string
+ wantErr bool
+ expected string
+ }{
+ {
+ query: "count 123456",
+ expected: "COUNT: 6",
+ },
+ {
+ query: "count abc",
+ expected: "COUNT: 3",
+ },
+ {
+ query: "count multiple words are supported as well",
+ expected: "COUNT: 36",
+ },
+ {
+ query: "RANDOM 7",
+ expected: "RANDOM: 4",
+ },
+ {
+ query: "RANDOM 4",
+ expected: "RANDOM: 0",
+ },
+ {
+ query: "RANDOM NAN",
+ wantErr: true,
+ },
+ {
+ query: "COUNT",
+ wantErr: true,
+ },
+ {
+ query: "INVALID COMMAND",
+ wantErr: true,
+ },
+ } {
+ out, err := exec.Command(client, "--server=localhost:"+port, test.query).CombinedOutput()
+ if (err != nil) != test.wantErr || !strings.HasSuffix(string(out), test.expected+"\n") {
+ e := "<nil>"
+ if test.wantErr {
+ e = "<ERR>"
+ }
+ t.Errorf("%s %s returned %q, %v; want %q, %s", client, test.query, string(out), err, test.expected, e)
+ }
+ }
+}
diff --git a/grpc/integration/ui/ui_test.go b/grpc/integration/ui/ui_test.go
--- /dev/null
@@ -0,0 +1,189 @@
+// Copyright (C) 2016 Sebastian 'tokkee' Harl <sh@tokkee.org>
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions
+// are met:
+// 1. Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// 2. Redistributions in binary form must reproduce the above copyright
+// notice, this list of conditions and the following disclaimer in the
+// documentation and/or other materials provided with the distribution.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+// TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+// PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR
+// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
+// OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
+// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+// ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+package ui_test
+
+import (
+ "fmt"
+ "io/ioutil"
+ "log"
+ "net/http"
+ "net/url"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "strings"
+ "testing"
+ "time"
+
+ "google.golang.org/grpc"
+)
+
+var (
+ server string
+ ui string
+
+ // TODO: probe for unused ports
+ serverPort = "50051"
+ uiPort = "9999"
+)
+
+func init() {
+ gopath := os.Getenv("GOPATH")
+ if gopath == "" {
+ panic("GOPATH not set")
+ }
+
+ server = filepath.Join(gopath, "bin", "server")
+ ui = filepath.Join(gopath, "bin", "ui")
+
+ for _, path := range []string{server, ui} {
+ if _, err := os.Stat(path); err != nil {
+ if !os.IsNotExist(err) {
+ panic(err)
+ }
+ panic(fmt.Sprintf("%s and/or %s not found; run 'go install tokkee.org/go-talk/grpc/server tokkee.org/go-talk/grpc/ui'", server, ui))
+ }
+ }
+}
+
+// setup sets up a Backend test instance and UI and returns the UI address and
+// a cleanup function to be called when done.
+func setup() (string, func(), error) {
+ srv := exec.Command(server, "--listen=:"+serverPort)
+ if err := srv.Start(); err != nil {
+ return "", nil, fmt.Errorf("failed to start server: %v", err)
+ }
+ // Wait for the server to be ready.
+ conn, err := grpc.Dial("localhost:"+serverPort, grpc.WithInsecure())
+ if err != nil {
+ srv.Process.Kill()
+ return "", nil, fmt.Errorf("failed to connect to server: %v", err)
+ }
+ conn.Close()
+
+ u := exec.Command(ui, "--listen=:"+uiPort, "--backend=localhost:"+serverPort)
+ if err := u.Start(); err != nil {
+ srv.Process.Kill()
+ return "", nil, fmt.Errorf("failed to start UI: %v", err)
+ }
+ // Wait for the UI to be ready.
+ for {
+ if _, err := http.Get("http://localhost:" + uiPort + "/query"); err == nil {
+ break
+ }
+ time.Sleep(10 * time.Millisecond)
+ }
+
+ return "http://localhost:" + uiPort, func() {
+ if err := u.Process.Kill(); err != nil {
+ log.Printf("Failed to kill UI process: %v", err)
+ }
+ u.Wait()
+
+ if err := srv.Process.Kill(); err != nil {
+ log.Printf("Failed to kill server process: %v", err)
+ }
+ srv.Wait()
+ }, nil
+}
+
+// TestQuery runs sample queries against the /query endpoint of the UI and
+// checks the results.
+func TestQuery(t *testing.T) {
+ addr, cleanup, err := setup()
+ if err != nil {
+ t.Fatalf("Setup failed: %v", err)
+ }
+ defer cleanup()
+
+ for _, test := range []struct {
+ query string
+ status int
+ expected []string
+ }{
+ {
+ query: "CounT 123456",
+ status: 200,
+ expected: []string{
+ "<b>CounT 123456 =></b> COUNT: 6",
+ },
+ },
+ {
+ query: "count abc",
+ status: 200,
+ expected: []string{
+ "<b>count abc =></b> COUNT: 3",
+ },
+ },
+ {
+ query: "count multiple words are supported as well; RANDOM 7; RANDOM 4",
+ status: 200,
+ expected: []string{
+ "<b>count multiple words are supported as well =></b> COUNT: 36",
+ "<b>RANDOM 7 =></b> RANDOM: 4",
+ "<b>RANDOM 4 =></b> RANDOM: 0",
+ },
+ },
+ {
+ query: "RANDOM NAN",
+ status: 400,
+ },
+ {
+ query: "COUNT",
+ status: 400,
+ },
+ {
+ query: "INVALID COMMAND",
+ status: 400,
+ },
+ } {
+ params := make(url.Values)
+ params.Add("q", test.query)
+ res, err := http.PostForm(addr+"/query", params)
+ if err != nil {
+ t.Errorf("PostForm(%q, %v) = %v", addr+"/query", params, err)
+ continue
+ }
+ defer res.Body.Close()
+
+ raw, err := ioutil.ReadAll(res.Body)
+ if err != nil {
+ t.Errorf("Failed to read response body: %v", err)
+ continue
+ }
+ body := string(raw)
+
+ ok := true
+ for _, expected := range test.expected {
+ if !strings.Contains(body, expected) {
+ ok = false
+ break
+ }
+ }
+ if res.StatusCode != test.status || !ok {
+ t.Errorf("POST(%q, %v) = %v:\n%s\nwant status=%d; matches=%v", addr+"/query", params, res, body, test.status, test.expected)
+ }
+ }
+}
diff --git a/grpc/proto/backend/backend.pb.go b/grpc/proto/backend/backend.pb.go
--- /dev/null
@@ -0,0 +1,143 @@
+// Code generated by protoc-gen-go.
+// source: src/tokkee.org/go-talk/grpc/proto/backend/backend.proto
+// DO NOT EDIT!
+
+/*
+Package backend is a generated protocol buffer package.
+
+It is generated from these files:
+ src/tokkee.org/go-talk/grpc/proto/backend/backend.proto
+
+It has these top-level messages:
+ QueryRequest
+ QueryReply
+*/
+package backend
+
+import proto "github.com/golang/protobuf/proto"
+import fmt "fmt"
+import math "math"
+
+import (
+ context "golang.org/x/net/context"
+ grpc "google.golang.org/grpc"
+)
+
+// Reference imports to suppress errors if they are not otherwise used.
+var _ = proto.Marshal
+var _ = fmt.Errorf
+var _ = math.Inf
+
+// This is a compile-time assertion to ensure that this generated file
+// is compatible with the proto package it is being compiled against.
+const _ = proto.ProtoPackageIsVersion1
+
+type QueryRequest struct {
+ Query string `protobuf:"bytes,1,opt,name=query" json:"query,omitempty"`
+}
+
+func (m *QueryRequest) Reset() { *m = QueryRequest{} }
+func (m *QueryRequest) String() string { return proto.CompactTextString(m) }
+func (*QueryRequest) ProtoMessage() {}
+func (*QueryRequest) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{0} }
+
+type QueryReply struct {
+ Type string `protobuf:"bytes,1,opt,name=type" json:"type,omitempty"`
+ N int64 `protobuf:"varint,2,opt,name=n" json:"n,omitempty"`
+}
+
+func (m *QueryReply) Reset() { *m = QueryReply{} }
+func (m *QueryReply) String() string { return proto.CompactTextString(m) }
+func (*QueryReply) ProtoMessage() {}
+func (*QueryReply) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{1} }
+
+func init() {
+ proto.RegisterType((*QueryRequest)(nil), "backend.QueryRequest")
+ proto.RegisterType((*QueryReply)(nil), "backend.QueryReply")
+}
+
+// Reference imports to suppress errors if they are not otherwise used.
+var _ context.Context
+var _ grpc.ClientConn
+
+// This is a compile-time assertion to ensure that this generated file
+// is compatible with the grpc package it is being compiled against.
+const _ = grpc.SupportPackageIsVersion2
+
+// Client API for Backend service
+
+type BackendClient interface {
+ Query(ctx context.Context, in *QueryRequest, opts ...grpc.CallOption) (*QueryReply, error)
+}
+
+type backendClient struct {
+ cc *grpc.ClientConn
+}
+
+func NewBackendClient(cc *grpc.ClientConn) BackendClient {
+ return &backendClient{cc}
+}
+
+func (c *backendClient) Query(ctx context.Context, in *QueryRequest, opts ...grpc.CallOption) (*QueryReply, error) {
+ out := new(QueryReply)
+ err := grpc.Invoke(ctx, "/backend.Backend/Query", in, out, c.cc, opts...)
+ if err != nil {
+ return nil, err
+ }
+ return out, nil
+}
+
+// Server API for Backend service
+
+type BackendServer interface {
+ Query(context.Context, *QueryRequest) (*QueryReply, error)
+}
+
+func RegisterBackendServer(s *grpc.Server, srv BackendServer) {
+ s.RegisterService(&_Backend_serviceDesc, srv)
+}
+
+func _Backend_Query_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+ in := new(QueryRequest)
+ if err := dec(in); err != nil {
+ return nil, err
+ }
+ if interceptor == nil {
+ return srv.(BackendServer).Query(ctx, in)
+ }
+ info := &grpc.UnaryServerInfo{
+ Server: srv,
+ FullMethod: "/backend.Backend/Query",
+ }
+ handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+ return srv.(BackendServer).Query(ctx, req.(*QueryRequest))
+ }
+ return interceptor(ctx, in, info, handler)
+}
+
+var _Backend_serviceDesc = grpc.ServiceDesc{
+ ServiceName: "backend.Backend",
+ HandlerType: (*BackendServer)(nil),
+ Methods: []grpc.MethodDesc{
+ {
+ MethodName: "Query",
+ Handler: _Backend_Query_Handler,
+ },
+ },
+ Streams: []grpc.StreamDesc{},
+}
+
+var fileDescriptor0 = []byte{
+ // 174 bytes of a gzipped FileDescriptorProto
+ 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0xe2, 0x32, 0x2f, 0x2e, 0x4a, 0xd6,
+ 0x2f, 0xc9, 0xcf, 0xce, 0x4e, 0x4d, 0xd5, 0xcb, 0x2f, 0x4a, 0xd7, 0x4f, 0xcf, 0xd7, 0x2d, 0x49,
+ 0xcc, 0xc9, 0xd6, 0x4f, 0x2f, 0x2a, 0x48, 0xd6, 0x2f, 0x28, 0xca, 0x2f, 0xc9, 0xd7, 0x4f, 0x4a,
+ 0x4c, 0xce, 0x4e, 0xcd, 0x4b, 0x81, 0xd1, 0x7a, 0x60, 0x51, 0x21, 0x76, 0x28, 0x57, 0x49, 0x85,
+ 0x8b, 0x27, 0xb0, 0x34, 0xb5, 0xa8, 0x32, 0x28, 0xb5, 0xb0, 0x34, 0xb5, 0xb8, 0x44, 0x48, 0x84,
+ 0x8b, 0xb5, 0x10, 0xc4, 0x97, 0x60, 0x54, 0x60, 0xd4, 0xe0, 0x0c, 0x82, 0x70, 0x94, 0xf4, 0xb8,
+ 0xb8, 0xa0, 0xaa, 0x0a, 0x72, 0x2a, 0x85, 0x84, 0xb8, 0x58, 0x4a, 0x2a, 0x0b, 0x52, 0xa1, 0x4a,
+ 0xc0, 0x6c, 0x21, 0x1e, 0x2e, 0xc6, 0x3c, 0x09, 0x26, 0xa0, 0x00, 0x73, 0x10, 0x63, 0x9e, 0x91,
+ 0x03, 0x17, 0xbb, 0x13, 0xc4, 0x02, 0x21, 0x53, 0x2e, 0x56, 0xb0, 0x56, 0x21, 0x51, 0x3d, 0x98,
+ 0x13, 0x90, 0x2d, 0x94, 0x12, 0x46, 0x17, 0x06, 0xda, 0xa0, 0xc4, 0x90, 0xc4, 0x06, 0x76, 0xa7,
+ 0x31, 0x20, 0x00, 0x00, 0xff, 0xff, 0x9b, 0xe0, 0x69, 0x75, 0xe2, 0x00, 0x00, 0x00,
+}
diff --git a/grpc/proto/backend/backend.proto b/grpc/proto/backend/backend.proto
--- /dev/null
@@ -0,0 +1,43 @@
+// Copyright (C) 2016 Sebastian 'tokkee' Harl <sh@tokkee.org>
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions
+// are met:
+// 1. Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// 2. Redistributions in binary form must reproduce the above copyright
+// notice, this list of conditions and the following disclaimer in the
+// documentation and/or other materials provided with the distribution.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+// TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+// PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR
+// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
+// OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
+// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+// ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+syntax = "proto3";
+
+package backend;
+
+service Backend {
+ // Execute a query.
+ rpc Query(QueryRequest) returns (QueryReply) {};
+}
+
+message QueryRequest {
+ // The query string: <cmd> <arg>...
+ string query = 1;
+}
+
+// A query result.
+message QueryReply {
+ string type = 1;
+ int64 n = 2;
+}
diff --git a/grpc/server/main.go b/grpc/server/main.go
--- /dev/null
+++ b/grpc/server/main.go
@@ -0,0 +1,113 @@
+// Copyright (C) 2016 Sebastian 'tokkee' Harl <sh@tokkee.org>
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions
+// are met:
+// 1. Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// 2. Redistributions in binary form must reproduce the above copyright
+// notice, this list of conditions and the following disclaimer in the
+// documentation and/or other materials provided with the distribution.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+// TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+// PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR
+// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
+// OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
+// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+// ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+// server is the backend server implementation.
+package main
+
+import (
+ "flag"
+ "log"
+ "net"
+ "strconv"
+ "strings"
+
+ "golang.org/x/net/context"
+
+ "google.golang.org/grpc"
+ "google.golang.org/grpc/codes"
+ "google.golang.org/grpc/peer"
+
+ pb "tokkee.org/go-talk/grpc/proto/backend"
+)
+
+var (
+ listen = flag.String("listen", ":50051", "listening address")
+)
+
+// A server implements the Backend service.
+type server struct{}
+
+// Query runs the user-provided query and returns the result.
+func (*server) Query(ctx context.Context, in *pb.QueryRequest) (*pb.QueryReply, error) {
+ if peer, ok := peer.FromContext(ctx); ok {
+ log.Printf("Query() called from %v", peer)
+ } else {
+ log.Print("No peer information available")
+ }
+
+ t, n, err := runQuery(in.Query)
+ if err != nil {
+ return nil, err
+ }
+
+ return &pb.QueryReply{
+ Type: t,
+ N: n,
+ }, nil
+}
+
+// runQuery executes a query. It implements a highly sophisticated query
+// engine.
+func runQuery(q string) (string, int64, error) {
+ fields := strings.SplitN(q, " ", 2)
+ if len(fields) != 2 {
+ return "", 0, grpc.Errorf(codes.InvalidArgument, "invalid query %q: want <cmd> <arg>", q)
+ }
+
+ var n int64
+ cmd, arg := strings.ToUpper(fields[0]), fields[1]
+ switch cmd {
+ case "COUNT":
+ n = int64(len(arg))
+ case "RANDOM":
+ i, err := strconv.Atoi(arg)
+ if err != nil {
+ return "", 0, grpc.Errorf(codes.InvalidArgument, "RANDOM: %v", err)
+ }
+
+ // Chosen by fair dice roll.
+ n = 4
+ if i <= 4 {
+ n = 0
+ }
+ default:
+ return "", 0, grpc.Errorf(codes.InvalidArgument, "unknown query command %q", cmd)
+ }
+
+ return cmd, n, nil
+}
+
+func main() {
+ flag.Parse()
+
+ l, err := net.Listen("tcp", *listen)
+ if err != nil {
+ log.Fatalf("Failed to listen on %s: %v", *listen, err)
+ }
+
+ log.Printf("Listening on %s ...", *listen)
+ s := grpc.NewServer()
+ pb.RegisterBackendServer(s, &server{})
+ s.Serve(l)
+}
diff --git a/grpc/ui/main.go b/grpc/ui/main.go
--- /dev/null
+++ b/grpc/ui/main.go
@@ -0,0 +1,228 @@
+// Copyright (C) 2016 Sebastian 'tokkee' Harl <sh@tokkee.org>
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions
+// are met:
+// 1. Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// 2. Redistributions in binary form must reproduce the above copyright
+// notice, this list of conditions and the following disclaimer in the
+// documentation and/or other materials provided with the distribution.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+// TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+// PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR
+// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
+// OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
+// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+// ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+// ui is a web-based UI interacting with the backend server.
+package main
+
+import (
+ "bytes"
+ "errors"
+ "flag"
+ "fmt"
+ "html/template"
+ "io"
+ "log"
+ "net/http"
+ "strings"
+ "sync"
+ "time"
+
+ "golang.org/x/net/context"
+ "google.golang.org/grpc"
+
+ pb "tokkee.org/go-talk/grpc/proto/backend"
+)
+
+var (
+ listen = flag.String("listen", ":8080", "listen address")
+ backend = flag.String("backend", "localhost:50051", "backend server address")
+
+ queryTmpl = template.Must(template.New("query").Parse(`<html>
+<head><title>Backend Query</title></head>
+<body>
+ <form action="/query" method="POST">
+ Query: <input type="text" name="q" placeholder="<cmd> <arg>..." value="{{.Query}}" />
+ <input type="submit" value="Submit">
+ </form>
+{{range .Responses}}
+ <p><b>{{.Request}} =></b> {{.Type}}: {{.N}}</p>
+{{end}}
+</body>
+</html>
+`))
+)
+
+type server struct {
+ c pb.BackendClient
+}
+
+type queryResponse struct {
+ Request string
+ Type string
+ N int64
+}
+
+// runQueries executes multiple queries, separated by semicolon, in parallel.
+func (s *server) runQueries(ctx context.Context, query string) ([]queryResponse, error) {
+ requests := strings.Split(query, ";")
+ for i, r := range requests {
+ requests[i] = strings.TrimSpace(r)
+ }
+
+ responses := make([]queryResponse, len(requests))
+ errCh := make(chan error, len(requests))
+
+ for i, req := range requests {
+ go func(i int, req string) {
+ res, err := s.c.Query(ctx, &pb.QueryRequest{Query: req})
+ defer func() { errCh <- err }()
+ if err != nil {
+ return
+ }
+
+ responses[i] = queryResponse{
+ Request: req,
+ Type: res.Type,
+ N: res.N,
+ }
+ }(i, req)
+ }
+
+ timeout := time.After(50 * time.Millisecond)
+
+ for _ = range requests {
+ select {
+ case err := <-errCh:
+ if err != nil {
+ return nil, err
+ }
+
+ case <-timeout:
+ return nil, errors.New("request timed out")
+ }
+ }
+
+ return responses, nil
+}
+
+func (s *server) query(ctx context.Context, w http.ResponseWriter, r *http.Request) {
+ data := &struct {
+ Query string
+ Responses []queryResponse
+ }{}
+
+ data.Query = r.Form.Get("q")
+ if data.Query != "" {
+ if r.Method != "POST" {
+ // RFC 2616 requires us to set the "Allow" header.
+ w.Header().Add("Allow", "POST")
+ http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
+ return
+ }
+ // TODO: use and validate XSRF tokens
+
+ res, err := s.runQueries(ctx, data.Query)
+ if err != nil {
+ http.Error(w, fmt.Sprintf("query failed: %v", err), http.StatusBadRequest)
+ return
+ }
+ data.Responses = res
+ }
+
+ var buf bytes.Buffer
+ if err := queryTmpl.Execute(&buf, data); err != nil {
+ http.Error(w, fmt.Sprintf("template error: %v", err), http.StatusInternalServerError)
+ return
+ }
+ io.Copy(w, &buf)
+}
+
+type handler func(context.Context, http.ResponseWriter, *http.Request)
+
+// A mux multiplexes incoming requests based on the first part of the incoming
+// request URI. It differs from http.ServerMux in that it only support routing
+// based on the first part of the request path and it adds context handling
+// and form parsing. mux implements http.Handler.
+type mux struct {
+ mu sync.RWMutex
+ handlers map[string]handler
+}
+
+// Handle registers a handler for the specified path. It panics if a handler
+// has already been registered for the same path or if the path includes a
+// slash.
+func (m *mux) Handle(path string, h handler) {
+ if strings.Index(path, "/") != -1 {
+ panic("invalid path: " + path)
+ }
+ if h == nil {
+ panic("invalid nil handler")
+ }
+
+ m.mu.Lock()
+ defer m.mu.Unlock()
+
+ if m.handlers == nil {
+ m.handlers = make(map[string]handler)
+ }
+ if m.handlers[path] != nil {
+ panic(fmt.Sprintf("duplicate handlers registered for %q", path))
+ }
+ m.handlers[path] = h
+}
+
+// ServeHTTP handles incoming requests using the registered handlers and takes
+// care of request-specific setup (context management and form parsing).
+func (m *mux) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+ path := r.URL.Path
+ if len(path) > 0 && path[0] == '/' {
+ path = path[1:]
+ }
+ if f := strings.Split(path, "/"); len(f) > 1 {
+ path = f[0]
+ }
+
+ if err := r.ParseForm(); err != nil {
+ log.Printf("Invalid URL query: %v", err)
+ http.Error(w, fmt.Sprintf("invalid URL query: %v", err), http.StatusBadRequest)
+ return
+ }
+
+ m.mu.RLock()
+ h := m.handlers[path]
+ m.mu.RUnlock()
+
+ if h == nil {
+ log.Printf("Not found: %q (%v)", path, r.URL)
+ http.NotFound(w, r)
+ return
+ }
+ h(context.Background(), w, r)
+}
+
+func main() {
+ flag.Parse()
+
+ conn, err := grpc.Dial(*backend, grpc.WithInsecure())
+ if err != nil {
+ log.Fatalf("Failed to connect to backend server at %s: %v", *backend, err)
+ }
+ defer conn.Close()
+
+ s := &server{c: pb.NewBackendClient(conn)}
+ m := &mux{}
+ m.Handle("query", s.query)
+ log.Printf("Listening on %s ...", *listen)
+ log.Fatal(http.ListenAndServe(*listen, m))
+}