Code

Add an initial version of sample code for my Go talk.
authorSebastian Harl <sh@tokkee.org>
Wed, 4 May 2016 07:48:01 +0000 (09:48 +0200)
committerSebastian 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.

LICENSE [new file with mode: 0644]
README [new file with mode: 0644]
grpc/client/main.go [new file with mode: 0644]
grpc/integration/backend/backend_test.go [new file with mode: 0644]
grpc/integration/ui/ui_test.go [new file with mode: 0644]
grpc/proto/backend/backend.pb.go [new file with mode: 0644]
grpc/proto/backend/backend.proto [new file with mode: 0644]
grpc/server/main.go [new file with mode: 0644]
grpc/ui/main.go [new file with mode: 0644]

diff --git a/LICENSE b/LICENSE
new file mode 100644 (file)
index 0000000..cd0f3d8
--- /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
new file mode 100644 (file)
index 0000000..351ecbc
--- /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
new file mode 100644 (file)
index 0000000..8feb1e2
--- /dev/null
@@ -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
new file mode 100644 (file)
index 0000000..7000226
--- /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
new file mode 100644 (file)
index 0000000..fda93c8
--- /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
new file mode 100644 (file)
index 0000000..91609b5
--- /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
new file mode 100644 (file)
index 0000000..b1fdde2
--- /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
new file mode 100644 (file)
index 0000000..83ccfb5
--- /dev/null
@@ -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
new file mode 100644 (file)
index 0000000..88442df
--- /dev/null
@@ -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))
+}