├── .gitignore ├── Makefile ├── README.md ├── cmd ├── client │ └── main.go └── server │ └── main.go ├── docs ├── identity.seq ├── identity.seq.png ├── list.seq ├── list.seq.png ├── relay.seq └── relay.seq.png ├── go.mod ├── go.sum ├── internal ├── client │ └── client.go └── server │ └── server.go └── test ├── benchmark_test.go └── integration_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PKGS = $(shell go list ./... | grep -v /test) 2 | 3 | build-client: 4 | CGO_ENABLED=0 go build -o ./build/client ./cmd/client 5 | .PHONY: build-client 6 | 7 | build-server: 8 | CGO_ENABLED=0 go build -o ./build/server ./cmd/server 9 | .PHONY: build-server 10 | 11 | build: build-server build-client 12 | .PHONY: build 13 | 14 | lint: 15 | golint $(PKGS) 16 | .PHONY: lint 17 | 18 | test-unit: # TODO: Please implement 19 | go test --race --cover -v $(PKGS) 20 | .PHONY: test-unit 21 | 22 | test-integration: 23 | go test --race -v test/integration_test.go 24 | .PHONY: test-integration 25 | 26 | test-benchmark: 27 | go test -v -bench=. test/benchmark_test.go 28 | .PHONY: test-benchmark 29 | 30 | test: lint test-unit test-integration 31 | .PHONY: test 32 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Golang backend assignment 2 | 3 | ## Table of contents 4 | 5 | * [Assignment](#assignment) 6 | * [Running and building](#running-and-building) 7 | * [Testing](#testing) 8 | 9 | ## Assignment 10 | 11 | Return your answer as a zip file containing all relevant files _with tests_ (including `.git`, so that we can see your commit history). 12 | Do not fork this repo. 13 | If you create your own git repo, please make sure it is private, so that the other candidates cannot access your solution. 14 | 15 | Design and implement (with tests) a _message delivery system_ using [Go](http://golang.org/) programming language, 16 | including both the server and the client. 17 | You are free to use any external libs if needed. 18 | The protocol must be on top of pure TCP, don't use existing application level protocols like HTTP or WebSockets. 19 | 20 | We don't value over-engineering. 21 | Provide a readable minimalistic implementation that has understandable split to well-named source files and functions. 22 | Impress us with simplicity, good unit tests and a working solution. 23 | 24 | In this simplified scenario the message delivery system includes the following parts: 25 | 26 | ### Hub 27 | 28 | Hub relays incoming message bodies to receivers based on user ID(s) defined in the message. 29 | You don't need to implement authentication, hub can for example assign arbitrary (unique) user id to the client once its connected. 30 | 31 | - user_id - unsigned 64 bit integer 32 | - Connection to hub must be done using pure TCP. Protocol doesnt require multiplexing. 33 | 34 | ### Clients 35 | 36 | Clients are users who are connected to the hub. Client may send three types of messages which are described below. 37 | 38 | ### Identity message 39 | Client can send a identity message which the hub will answer with the user_id of the connected user. 40 | 41 | ![Identity](docs/identity.seq.png) 42 | 43 | ### List message 44 | Client can send a list message which the hub will answer with the list of all connected client user_id:s (excluding the requesting client). 45 | 46 | ![List](docs/list.seq.png) 47 | 48 | ### Relay message 49 | Client can send a relay messages which body is relayed to receivers marked in the message. 50 | Design the optimal data format for the message delivery system, so that it consumes minimal amount of resources (memory, cpu, etc.). 51 | Message body can be relayed to one or multiple receivers. 52 | 53 | - max 255 receivers (user_id:s) per message 54 | - message body - byte array (text, JSON, binary, or anything), max length 1024 kilobytes 55 | 56 | ![Relay](docs/relay.seq.png) 57 | 58 | *Relay example: receivers: 2 and 3, body: foobar* 59 | 60 | ## Running and building 61 | 62 | The project already includes necessary infrastructure for building and running the hub. 63 | It has Makefile with targets for building and testing both client and server. 64 | 65 | ## Testing 66 | 67 | The project contains integration and benchmark tests for the hub (including both client and server), 68 | so make sure your implementation is compatible and the tests pass without making major changes to them. 69 | Please add unit tests for you implementation, without them the assignment will be rejected. 70 | -------------------------------------------------------------------------------- /cmd/client/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "fmt" 4 | 5 | func main() { 6 | fmt.Println("Hello from client!") 7 | } 8 | -------------------------------------------------------------------------------- /cmd/server/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "fmt" 4 | 5 | func main() { 6 | fmt.Println("Hello from server!") 7 | } 8 | -------------------------------------------------------------------------------- /docs/identity.seq: -------------------------------------------------------------------------------- 1 | sequenceDiagram 2 | participant Client 1 3 | participant Hub 4 | Client 1->>Hub: Who Am I? 5 | Hub->>Client 1: 1 6 | -------------------------------------------------------------------------------- /docs/identity.seq.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Unity-Technologies/mz-golang-backend-assignment/79271c9aee5c761519a5a1fb51208f7d55993def/docs/identity.seq.png -------------------------------------------------------------------------------- /docs/list.seq: -------------------------------------------------------------------------------- 1 | sequenceDiagram 2 | participant Client 1 3 | participant Hub 4 | Client 1->>Hub: Who is here? 5 | Hub->>Client 1: 2 and 3 6 | -------------------------------------------------------------------------------- /docs/list.seq.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Unity-Technologies/mz-golang-backend-assignment/79271c9aee5c761519a5a1fb51208f7d55993def/docs/list.seq.png -------------------------------------------------------------------------------- /docs/relay.seq: -------------------------------------------------------------------------------- 1 | sequenceDiagram 2 | participant Client 1 3 | participant Client 2 4 | participant Client 3 5 | participant Hub 6 | Client 1->>Hub: Send foobar to 2 and 3 7 | Hub->>Client 2: foobar 8 | Hub->>Client 3: foobar 9 | -------------------------------------------------------------------------------- /docs/relay.seq.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Unity-Technologies/mz-golang-backend-assignment/79271c9aee5c761519a5a1fb51208f7d55993def/docs/relay.seq.png -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/Applifier/golang-backend-assignment 2 | 3 | go 1.12 4 | 5 | require github.com/stretchr/testify v1.4.0 6 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 2 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 4 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 5 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 6 | github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= 7 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 8 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 9 | gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= 10 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 11 | -------------------------------------------------------------------------------- /internal/client/client.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | ) 7 | 8 | type IncomingMessage struct { 9 | SenderID uint64 10 | Body []byte 11 | } 12 | 13 | type Client struct { 14 | } 15 | 16 | func New() *Client { 17 | return &Client{} 18 | } 19 | 20 | func (cli *Client) Connect(serverAddr *net.TCPAddr) error { 21 | fmt.Println("TODO: Connect to the server using the given address") 22 | return nil 23 | } 24 | 25 | func (cli *Client) Close() error { 26 | fmt.Println("TODO: Close the connection to the server") 27 | return nil 28 | } 29 | 30 | func (cli *Client) WhoAmI() (uint64, error) { 31 | fmt.Println("TODO: Fetch the ID from the server") 32 | return 0, nil 33 | } 34 | 35 | func (cli *Client) ListClientIDs() ([]uint64, error) { 36 | fmt.Println("TODO: Fetch the IDs from the server") 37 | return nil, nil 38 | } 39 | 40 | func (cli *Client) SendMsg(recipients []uint64, body []byte) error { 41 | fmt.Println("TODO: Send the message to the server") 42 | return nil 43 | } 44 | 45 | func (cli *Client) HandleIncomingMessages(writeCh chan<- IncomingMessage) { 46 | fmt.Println("TODO: Handle the messages from the server") 47 | } 48 | -------------------------------------------------------------------------------- /internal/server/server.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | ) 7 | 8 | type Server struct { 9 | } 10 | 11 | func New() *Server { 12 | return &Server{} 13 | } 14 | 15 | func (server *Server) Start(laddr *net.TCPAddr) error { 16 | fmt.Println("TODO: Start handling client connections and messages") 17 | return nil 18 | } 19 | 20 | func (server *Server) ListClientIDs() []uint64 { 21 | fmt.Println("TODO: Return the IDs of the connected clients") 22 | return []uint64{} 23 | } 24 | 25 | func (server *Server) Stop() error { 26 | fmt.Println("TODO: Stop accepting connections and close the existing ones") 27 | return nil 28 | } 29 | -------------------------------------------------------------------------------- /test/benchmark_test.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "github.com/Applifier/golang-backend-assignment/internal/client" 5 | "github.com/Applifier/golang-backend-assignment/internal/server" 6 | "github.com/stretchr/testify/assert" 7 | "github.com/stretchr/testify/require" 8 | "net" 9 | "testing" 10 | "time" 11 | ) 12 | 13 | const clientCount = 100 14 | const benchmarkServerPort = 50000 15 | 16 | func TestBenchmark(t *testing.T) { 17 | srv := server.New() 18 | serverAddr := net.TCPAddr{Port: benchmarkServerPort} 19 | require.NoError(t, srv.Start(&serverAddr)) 20 | 21 | var clients []*client.Client 22 | var clientChs []chan client.IncomingMessage 23 | for i := 0; i < clientCount; i++ { 24 | cli := client.New() 25 | require.NoError(t, cli.Connect(&serverAddr)) 26 | clientCh := make(chan client.IncomingMessage) 27 | go cli.HandleIncomingMessages(clientCh) 28 | defer func() { 29 | assert.NoError(t, cli.Close()) 30 | }() 31 | defer close(clientCh) 32 | clients = append(clients, cli) 33 | clientChs = append(clientChs, clientCh) 34 | } 35 | 36 | defer func() { 37 | assert.NoError(t, srv.Stop()) 38 | }() 39 | 40 | waitForClientsToConnect(t, srv) 41 | 42 | t.Run("short messages", func(t *testing.T) { 43 | payload := []byte("FOOBAR") 44 | result := testing.Benchmark(func(b *testing.B) { 45 | for i := 0; i < b.N; i++ { 46 | assert.NoError(b, clients[0].SendMsg(srv.ListClientIDs(), payload)) 47 | for j := 1; j < clientCount; j++ { 48 | <-clientChs[j] 49 | } 50 | } 51 | }) 52 | t.Logf("Short message benchmark\n%s\n", result.String()) 53 | }) 54 | 55 | t.Run("long messages", func(t *testing.T) { 56 | payload := []byte("Lorem ipsum dolor sit amet, consectetur adipiscing elit. Duis sed est id mi blandit fringilla vulputate nec urna. Duis non porttitor arcu. Mauris ac ullamcorper turpis, ac tincidunt risus. In rutrum efficitur porttitor. Cras scelerisque eu mi ut tristique. Phasellus enim elit, pretium ut mi vel, semper interdum nisl. Duis gravida blandit risus, a semper ipsum lacinia quis. Nam eros purus, congue in metus id, volutpat dapibus velit. Cras ut dictum libero, non placerat quam. Vivamus sem justo, varius at magna sed, blandit consequat mi. Cras viverra, orci nec feugiat ullamcorper, mauris erat tincidunt nisi, nec rutrum neque est a libero. Nullam pharetra dolor at erat elementum convallis. Phasellus dictum fermentum odio non eleifend. Etiam scelerisque, neque a fringilla molestie, purus turpis posuere erat, ut pulvinar nisl nisl nec nisl. In pellentesque risus sem, id pretium eros gravida sit amet. In vel massa justo. Fusce euismod mattis massa. Fusce at nibh in est condimentum luctus. Integer a molestie arcu. Suspendisse aliquam venenatis nisl, sit amet aliquam ante convallis quis. Praesent nec ipsum lectus. Ut elementum pretium mollis. Etiam tincidunt sapien felis, eget aliquet justo tincidunt at. Integer turpis sem, feugiat quis lorem sed, scelerisque lacinia massa. Aliquam vitae urna et erat sodales accumsan a a enim. Nunc eget diam tristique, ornare nibh sed, laoreet ligula. Mauris sollicitudin consectetur elit nec eleifend. Donec in diam ut ligula porttitor vulputate. Integer finibus, tellus vitae sagittis tincidunt, felis augue pulvinar enim, consectetur sollicitudin lorem lacus vel sem. Mauris condimentum et dolor ac interdum. Praesent bibendum nulla nec dui tempus, non blandit augue iaculis. In pretium erat vel odio dictum, et rhoncus urna tristique. Mauris ut risus orci. Mauris cursus posuere felis, et accumsan ante consequat ac. Cras convallis luctus consequat.") 57 | result := testing.Benchmark(func(b *testing.B) { 58 | for i := 0; i < b.N; i++ { 59 | assert.NoError(b, clients[0].SendMsg(srv.ListClientIDs(), payload)) 60 | for j := 1; j < clientCount; j++ { 61 | <-clientChs[j] 62 | } 63 | } 64 | }) 65 | t.Logf("Long message benchmark\n%s\n", result.String()) 66 | }) 67 | } 68 | 69 | func waitForClientsToConnect(t *testing.T, srv *server.Server) { 70 | for i := 0; i < 50; i++ { 71 | if clientCount != len(srv.ListClientIDs()) { 72 | time.Sleep(time.Millisecond * 200) 73 | } else { 74 | break 75 | } 76 | } 77 | // If even after polling for several seconds, not all clients have connected, stop the test 78 | require.Equal(t, clientCount, len(srv.ListClientIDs())) 79 | } 80 | -------------------------------------------------------------------------------- /test/integration_test.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "github.com/Applifier/golang-backend-assignment/internal/client" 5 | "github.com/Applifier/golang-backend-assignment/internal/server" 6 | "github.com/stretchr/testify/assert" 7 | "github.com/stretchr/testify/require" 8 | "net" 9 | "testing" 10 | ) 11 | 12 | const serverPort = 50000 13 | 14 | func TestIntegration(t *testing.T) { 15 | srv := server.New() 16 | 17 | serverAddr := net.TCPAddr{Port: serverPort} 18 | require.NoError(t, srv.Start(&serverAddr)) 19 | defer assertDoesNotError(t, srv.Stop) 20 | 21 | // Create clients 22 | client1 := createClientAndFetchID(t, 1) 23 | defer assertDoesNotError(t, client1.Close) 24 | client1Ch := make(chan client.IncomingMessage) 25 | defer close(client1Ch) 26 | 27 | client2 := createClientAndFetchID(t, 2) 28 | defer assertDoesNotError(t, client2.Close) 29 | client2Ch := make(chan client.IncomingMessage) 30 | defer close(client2Ch) 31 | 32 | client3 := createClientAndFetchID(t, 3) 33 | defer assertDoesNotError(t, client3.Close) 34 | client3Ch := make(chan client.IncomingMessage) 35 | defer close(client3Ch) 36 | 37 | t.Run("List other clients from each client", func(t *testing.T) { 38 | ids, err := client1.ListClientIDs() 39 | assert.NoError(t, err) 40 | assert.Equal(t, []uint64{2, 3}, ids) 41 | 42 | ids, err = client2.ListClientIDs() 43 | assert.NoError(t, err) 44 | assert.Equal(t, []uint64{1, 3}, ids) 45 | 46 | ids, err = client3.ListClientIDs() 47 | assert.NoError(t, err) 48 | assert.Equal(t, []uint64{1, 2}, ids) 49 | }) 50 | 51 | t.Run("Send message from the first client to the two other clients", func(t *testing.T) { 52 | body := []byte("Hello world!") 53 | assert.Equal(t, nil, client1.SendMsg([]uint64{2, 3}, body)) 54 | 55 | go client2.HandleIncomingMessages(client2Ch) 56 | incomingMessage := <-client2Ch 57 | assert.Equal(t, body, incomingMessage.Body) 58 | assert.Equal(t, uint64(1), incomingMessage.SenderID) 59 | 60 | go client3.HandleIncomingMessages(client3Ch) 61 | incomingMessage = <-client3Ch 62 | assert.Equal(t, body, incomingMessage.Body) 63 | assert.Equal(t, uint64(1), incomingMessage.SenderID) 64 | }) 65 | } 66 | 67 | func assertDoesNotError(tb testing.TB, fn func() error) { 68 | assert.NoError(tb, fn()) 69 | } 70 | 71 | func createClientAndFetchID(t *testing.T, expectedClientID uint64) *client.Client { 72 | cli := client.New() 73 | serverAddr := net.TCPAddr{Port: serverPort} 74 | require.NoError(t, cli.Connect(&serverAddr)) 75 | id, err := cli.WhoAmI() 76 | assert.NoError(t, err) 77 | assert.Equal(t, expectedClientID, id) 78 | return cli 79 | } 80 | --------------------------------------------------------------------------------