├── .gitignore ├── .travis.yml ├── sema ├── sema14.go ├── sema15_arm.s ├── sema15_386.s ├── sema14_arm.s ├── sema15_amd64.s ├── sema14_386.s └── sema14_amd64.s ├── stack_race.go ├── _examples ├── arith │ ├── client │ │ ├── main.go │ │ ├── main_gen.go │ │ └── main_gen_test.go │ └── server │ │ ├── main.go │ │ ├── main_gen.go │ │ └── main_gen_test.go └── hello_world │ ├── client │ └── main.go │ └── server │ └── main.go ├── mux.go ├── LICENSE ├── request.go ├── jspipe.go ├── Protocol.md ├── response.go ├── command.go ├── handler.go ├── stack.go ├── setup_test.go ├── client_test.go ├── map_test.go ├── map.go ├── debug.go ├── README.md ├── server.go └── client.go /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | *# -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 4 | - 1.4 -------------------------------------------------------------------------------- /sema/sema14.go: -------------------------------------------------------------------------------- 1 | // +build go1.4 2 | 3 | package sema 4 | 5 | type Point uint32 6 | 7 | //go:noescape 8 | func Wait(p *Point) 9 | 10 | //go:noescape 11 | func Wake(p *Point) 12 | -------------------------------------------------------------------------------- /sema/sema15_arm.s: -------------------------------------------------------------------------------- 1 | // +build go1.5 2 | 3 | #define NOSPLIT 4 4 | 5 | TEXT ·Wait(SB),NOSPLIT,$0-0 6 | B sync·runtime_Semacquire(SB) 7 | 8 | TEXT ·Wake(SB),NOSPLIT,$0-0 9 | B runtime·semrelease(SB) 10 | -------------------------------------------------------------------------------- /sema/sema15_386.s: -------------------------------------------------------------------------------- 1 | // +build go1.5 2 | 3 | #define NOSPLIT 4 4 | 5 | TEXT ·Wait(SB),NOSPLIT,$0-0 6 | JMP sync·runtime_Semacquire(SB) 7 | 8 | TEXT ·Wake(SB),NOSPLIT,$0-0 9 | JMP runtime·semrelease(SB) 10 | -------------------------------------------------------------------------------- /sema/sema14_arm.s: -------------------------------------------------------------------------------- 1 | // +build go1.4,!go1.5 2 | 3 | #define NOSPLIT 4 4 | 5 | TEXT ·Wait(SB),NOSPLIT,$0-0 6 | B runtime·asyncsemacquire(SB) 7 | 8 | TEXT ·Wake(SB),NOSPLIT,$0-0 9 | B runtime·semrelease(SB) 10 | -------------------------------------------------------------------------------- /sema/sema15_amd64.s: -------------------------------------------------------------------------------- 1 | // +build go1.5 2 | 3 | #define NOSPLIT 4 4 | 5 | TEXT ·Wait(SB),NOSPLIT,$0-0 6 | JMP sync·runtime_Semacquire(SB) 7 | 8 | TEXT ·Wake(SB),NOSPLIT,$0-0 9 | JMP runtime·semrelease(SB) 10 | -------------------------------------------------------------------------------- /sema/sema14_386.s: -------------------------------------------------------------------------------- 1 | // +build go1.4,!go1.5 2 | 3 | #define NOSPLIT 4 4 | 5 | TEXT ·Wait(SB),NOSPLIT,$0-0 6 | JMP runtime·asyncsemacquire(SB) 7 | 8 | TEXT ·Wake(SB),NOSPLIT,$0-0 9 | JMP runtime·semrelease(SB) 10 | -------------------------------------------------------------------------------- /sema/sema14_amd64.s: -------------------------------------------------------------------------------- 1 | // +build go1.4,!go1.5 2 | 3 | #define NOSPLIT 4 4 | 5 | TEXT ·Wait(SB),NOSPLIT,$0-0 6 | JMP runtime·asyncsemacquire(SB) 7 | 8 | TEXT ·Wake(SB),NOSPLIT,$0-0 9 | JMP runtime·semrelease(SB) 10 | -------------------------------------------------------------------------------- /stack_race.go: -------------------------------------------------------------------------------- 1 | // +build race 2 | 3 | package synapse 4 | 5 | // this file exports 6 | // the same interface 7 | // as stack.go, but 8 | // no-ops instead. 9 | 10 | var ( 11 | waiters = waitStack{} 12 | wrappers = connStack{} 13 | ) 14 | 15 | type waitStack struct{} 16 | type connStack struct{} 17 | 18 | func (ws waitStack) push(_ *waiter) {} 19 | func (ws waitStack) pop(c *Client) *waiter { 20 | w := &waiter{parent: c} 21 | return w 22 | } 23 | 24 | func (cs connStack) pop() *connWrapper { return &connWrapper{} } 25 | func (cs connStack) push(_ *connWrapper) {} 26 | -------------------------------------------------------------------------------- /_examples/arith/client/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/tinylib/synapse" 6 | "os" 7 | "time" 8 | ) 9 | 10 | //go:generate msgp -io=false 11 | 12 | type Num struct { 13 | Value float64 `msg:"val"` 14 | } 15 | 16 | const Double synapse.Method = 0 17 | 18 | func main() { 19 | cl, err := synapse.Dial("tcp", "localhost:7000", time.Second) 20 | if err != nil { 21 | fmt.Println(err) 22 | return 23 | } 24 | defer cl.Close() 25 | 26 | pi := 3.14159 27 | num := Num{Value: pi} 28 | 29 | fmt.Println("Asking the remote server to double 3.14159 for us...") 30 | err = cl.Call(Double, &num, synapse.JSPipe(os.Stdout)) 31 | if err != nil { 32 | fmt.Println("ERROR:", err) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /mux.go: -------------------------------------------------------------------------------- 1 | package synapse 2 | 3 | // RouteTable is the simplest and fastest 4 | // form of routing. Request methods map 5 | // directly to an index in the routing 6 | // table. 7 | // 8 | // Route tables should be used by defining 9 | // your methods in a const-iota block, and 10 | // then using those as indices in the routing 11 | // table. 12 | type RouteTable []Handler 13 | 14 | func (r *RouteTable) ServeCall(req Request, res ResponseWriter) { 15 | m := req.Method() 16 | if int(m) > len(*r) { 17 | res.Error(StatusNotFound, "no such method") 18 | return 19 | } 20 | h := (*r)[m] 21 | if h == nil { 22 | res.Error(StatusNotFound, "no such method") 23 | return 24 | } 25 | h.ServeCall(req, res) 26 | return 27 | } 28 | -------------------------------------------------------------------------------- /_examples/arith/server/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/tinylib/synapse" 6 | "net" 7 | ) 8 | 9 | //go:generate msgp -io=false 10 | 11 | type Num struct { 12 | Value float64 `msg:"val"` 13 | } 14 | 15 | const Double synapse.Method = 0 16 | 17 | type doubleHandler struct{} 18 | 19 | func (d doubleHandler) ServeCall(req synapse.Request, res synapse.ResponseWriter) { 20 | var n Num 21 | err := req.Decode(&n) 22 | if err != nil { 23 | res.Error(synapse.StatusBadRequest, err.Error()) 24 | return 25 | } 26 | n.Value *= 2 27 | res.Send(&n) 28 | } 29 | 30 | func main() { 31 | rt := synapse.RouteTable{Double: doubleHandler{}} 32 | l, err := net.Listen("tcp", "localhost:7000") 33 | if err != nil { 34 | fmt.Println(err) 35 | return 36 | } 37 | fmt.Println("listening on :7070...") 38 | fmt.Println(synapse.Serve(l, &rt)) 39 | } 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 The Tinylib Authors 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /_examples/hello_world/client/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/tinylib/synapse" 6 | "os" 7 | "time" 8 | ) 9 | 10 | const ( 11 | Hello synapse.Method = 0 12 | ) 13 | 14 | func main() { 15 | // This sets up a TCP connection to 16 | // localhost:7000 and attaches a client 17 | // to it. Client creation fails if it 18 | // can't ping the server on the other 19 | // end. Additionally, calls will fail 20 | // if a response isn't received within 1 second. 21 | client, err := synapse.Dial("tcp", "localhost:7000", time.Second) 22 | if err != nil { 23 | fmt.Println(err) 24 | os.Exit(1) 25 | } 26 | 27 | // Here we make a remote call to 28 | // the method called "hello," and 29 | // we pass an object for the 30 | // response to be decoded into. 31 | // synapse.String is a convenience 32 | // provided for sending strings 33 | // back and forth. 34 | var res synapse.String 35 | err = client.Call(Hello, nil, &res) 36 | if err != nil { 37 | fmt.Println(err) 38 | os.Exit(1) 39 | } 40 | 41 | fmt.Println("response from server:", string(res)) 42 | } 43 | -------------------------------------------------------------------------------- /_examples/hello_world/server/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/tinylib/synapse" 5 | "log" 6 | ) 7 | 8 | const ( 9 | // Each route is simply 10 | // an index into a table, 11 | // much like a system call 12 | // or a file descriptor. 13 | // Generall, it's easiest 14 | // to define your routes 15 | // in a const-iota block. 16 | Hello synapse.Method = iota 17 | ) 18 | 19 | // helloHandler will implement 20 | // synapse.Handler 21 | type helloHandler struct{} 22 | 23 | func (h helloHandler) ServeCall(req synapse.Request, res synapse.ResponseWriter) { 24 | log.Println("received request from client with addr", req.RemoteAddr()) 25 | res.Send(synapse.String("Hello, World!")) 26 | } 27 | 28 | func main() { 29 | // RouteTable is the simplest 30 | // form of request routing: 31 | // it matches the method number 32 | // to an index into the table. 33 | rt := synapse.RouteTable{ 34 | Hello: helloHandler{}, 35 | } 36 | 37 | // ListenAndServe blocks forever 38 | // serving the provided handler. 39 | log.Fatalln(synapse.ListenAndServe("tcp", "localhost:7000", &rt)) 40 | } 41 | -------------------------------------------------------------------------------- /request.go: -------------------------------------------------------------------------------- 1 | package synapse 2 | 3 | import ( 4 | "github.com/tinylib/msgp/msgp" 5 | "net" 6 | ) 7 | 8 | type Method uint32 9 | 10 | // Request is the interface that 11 | // Handlers use to interact with 12 | // requests. 13 | type Request interface { 14 | // Method returns the 15 | // request method. 16 | Method() Method 17 | 18 | // RemoteAddr returns the remote 19 | // address that made the request. 20 | RemoteAddr() net.Addr 21 | 22 | // Decode reads the data of the request 23 | // into the argument. 24 | Decode(msgp.Unmarshaler) error 25 | 26 | // IsNil returns whether or not 27 | // the body of the request is 'nil'. 28 | IsNil() bool 29 | } 30 | 31 | // Request implementation passed 32 | // to the root handler of the server. 33 | type request struct { 34 | addr net.Addr // remote address 35 | in []byte // body 36 | mtd uint32 // method 37 | } 38 | 39 | func (r *request) Method() Method { return Method(r.mtd) } 40 | func (r *request) RemoteAddr() net.Addr { return r.addr } 41 | 42 | func (r *request) Decode(m msgp.Unmarshaler) error { 43 | if m != nil { 44 | _, err := m.UnmarshalMsg(r.in) 45 | return err 46 | } 47 | return nil 48 | } 49 | 50 | func (r *request) IsNil() bool { 51 | return msgp.IsNil(r.in) 52 | } 53 | -------------------------------------------------------------------------------- /_examples/arith/client/main_gen.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | // NOTE: THIS FILE WAS PRODUCED BY THE 4 | // MSGP CODE GENERATION TOOL (github.com/tinylib/msgp) 5 | // DO NOT EDIT 6 | 7 | import ( 8 | "github.com/tinylib/msgp/msgp" 9 | ) 10 | 11 | // MarshalMsg implements msgp.Marshaler 12 | func (z Num) MarshalMsg(b []byte) (o []byte, err error) { 13 | o = msgp.Require(b, z.Msgsize()) 14 | // map header, size 1 15 | // string "val" 16 | o = append(o, 0x81, 0xa3, 0x76, 0x61, 0x6c) 17 | o = msgp.AppendFloat64(o, z.Value) 18 | return 19 | } 20 | 21 | // UnmarshalMsg implements msgp.Unmarshaler 22 | func (z *Num) UnmarshalMsg(bts []byte) (o []byte, err error) { 23 | var field []byte 24 | _ = field 25 | var isz uint32 26 | isz, bts, err = msgp.ReadMapHeaderBytes(bts) 27 | if err != nil { 28 | return 29 | } 30 | for isz > 0 { 31 | isz-- 32 | field, bts, err = msgp.ReadMapKeyZC(bts) 33 | if err != nil { 34 | return 35 | } 36 | switch msgp.UnsafeString(field) { 37 | case "val": 38 | z.Value, bts, err = msgp.ReadFloat64Bytes(bts) 39 | if err != nil { 40 | return 41 | } 42 | default: 43 | bts, err = msgp.Skip(bts) 44 | if err != nil { 45 | return 46 | } 47 | } 48 | } 49 | o = bts 50 | return 51 | } 52 | 53 | func (z Num) Msgsize() (s int) { 54 | s = 1 + 4 + msgp.Float64Size 55 | return 56 | } 57 | -------------------------------------------------------------------------------- /_examples/arith/server/main_gen.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | // NOTE: THIS FILE WAS PRODUCED BY THE 4 | // MSGP CODE GENERATION TOOL (github.com/tinylib/msgp) 5 | // DO NOT EDIT 6 | 7 | import ( 8 | "github.com/tinylib/msgp/msgp" 9 | ) 10 | 11 | // MarshalMsg implements msgp.Marshaler 12 | func (z Num) MarshalMsg(b []byte) (o []byte, err error) { 13 | o = msgp.Require(b, z.Msgsize()) 14 | // map header, size 1 15 | // string "val" 16 | o = append(o, 0x81, 0xa3, 0x76, 0x61, 0x6c) 17 | o = msgp.AppendFloat64(o, z.Value) 18 | return 19 | } 20 | 21 | // UnmarshalMsg implements msgp.Unmarshaler 22 | func (z *Num) UnmarshalMsg(bts []byte) (o []byte, err error) { 23 | var field []byte 24 | _ = field 25 | var isz uint32 26 | isz, bts, err = msgp.ReadMapHeaderBytes(bts) 27 | if err != nil { 28 | return 29 | } 30 | for isz > 0 { 31 | isz-- 32 | field, bts, err = msgp.ReadMapKeyZC(bts) 33 | if err != nil { 34 | return 35 | } 36 | switch msgp.UnsafeString(field) { 37 | case "val": 38 | z.Value, bts, err = msgp.ReadFloat64Bytes(bts) 39 | if err != nil { 40 | return 41 | } 42 | default: 43 | bts, err = msgp.Skip(bts) 44 | if err != nil { 45 | return 46 | } 47 | } 48 | } 49 | o = bts 50 | return 51 | } 52 | 53 | func (z Num) Msgsize() (s int) { 54 | s = 1 + 4 + msgp.Float64Size 55 | return 56 | } 57 | -------------------------------------------------------------------------------- /jspipe.go: -------------------------------------------------------------------------------- 1 | package synapse 2 | 3 | import ( 4 | "github.com/tinylib/msgp/msgp" 5 | "io" 6 | ) 7 | 8 | // JSPipe is a decoder that can be used 9 | // to translate a messagepack object 10 | // directly into JSON as it is being 11 | // decoded. 12 | // 13 | // For example, you can trivially 14 | // print the response to a request 15 | // as JSON to stdout by writing 16 | // something like the following: 17 | // 18 | // client.Call("name", in, synapse.JSPipe(os.Stdout)) 19 | // 20 | func JSPipe(w io.Writer) msgp.Unmarshaler { return jsp{Writer: w} } 21 | 22 | // thin wrapper for io.Writer to use 23 | // as a MsgDecoder 24 | type jsp struct { 25 | io.Writer 26 | } 27 | 28 | func (j jsp) UnmarshalMsg(b []byte) ([]byte, error) { 29 | return msgp.UnmarshalAsJSON(j, b) 30 | } 31 | 32 | // String is a convenience type 33 | // for reading and writing go 34 | // strings to the wire. 35 | // 36 | // It can be used like: 37 | // 38 | // router.HandleFunc("ping", func(_ synapse.Request, res synapse.Response) { 39 | // res.Send(synapse.String("pong")) 40 | // }) 41 | // 42 | type String string 43 | 44 | // MarshalMsg implements msgp.Marshaler 45 | func (s String) MarshalMsg(b []byte) ([]byte, error) { 46 | return msgp.AppendString(b, string(s)), nil 47 | } 48 | 49 | // UnmarshalMsg implements msgp.Unmarshaler 50 | func (s *String) UnmarshalMsg(b []byte) ([]byte, error) { 51 | val, out, err := msgp.ReadStringBytes(b) 52 | if err != nil { 53 | return b, err 54 | } 55 | *s = String(val) 56 | return out, nil 57 | } 58 | -------------------------------------------------------------------------------- /_examples/arith/client/main_gen_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | // NOTE: THIS FILE WAS PRODUCED BY THE 4 | // MSGP CODE GENERATION TOOL (github.com/tinylib/msgp) 5 | // DO NOT EDIT 6 | 7 | import ( 8 | "testing" 9 | 10 | "github.com/tinylib/msgp/msgp" 11 | ) 12 | 13 | func TestMarshalUnmarshalNum(t *testing.T) { 14 | v := Num{} 15 | bts, err := v.MarshalMsg(nil) 16 | if err != nil { 17 | t.Fatal(err) 18 | } 19 | left, err := v.UnmarshalMsg(bts) 20 | if err != nil { 21 | t.Fatal(err) 22 | } 23 | if len(left) > 0 { 24 | t.Errorf("%d bytes left over after UnmarshalMsg(): %q", len(left), left) 25 | } 26 | 27 | left, err = msgp.Skip(bts) 28 | if err != nil { 29 | t.Fatal(err) 30 | } 31 | if len(left) > 0 { 32 | t.Errorf("%d bytes left over after Skip(): %q", len(left), left) 33 | } 34 | } 35 | 36 | func BenchmarkMarshalMsgNum(b *testing.B) { 37 | v := Num{} 38 | b.ReportAllocs() 39 | b.ResetTimer() 40 | for i := 0; i < b.N; i++ { 41 | v.MarshalMsg(nil) 42 | } 43 | } 44 | 45 | func BenchmarkAppendMsgNum(b *testing.B) { 46 | v := Num{} 47 | bts := make([]byte, 0, v.Msgsize()) 48 | bts, _ = v.MarshalMsg(bts[0:0]) 49 | b.SetBytes(int64(len(bts))) 50 | b.ReportAllocs() 51 | b.ResetTimer() 52 | for i := 0; i < b.N; i++ { 53 | bts, _ = v.MarshalMsg(bts[0:0]) 54 | } 55 | } 56 | 57 | func BenchmarkUnmarshalNum(b *testing.B) { 58 | v := Num{} 59 | bts, _ := v.MarshalMsg(nil) 60 | b.ReportAllocs() 61 | b.SetBytes(int64(len(bts))) 62 | b.ResetTimer() 63 | for i := 0; i < b.N; i++ { 64 | _, err := v.UnmarshalMsg(bts) 65 | if err != nil { 66 | b.Fatal(err) 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /_examples/arith/server/main_gen_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | // NOTE: THIS FILE WAS PRODUCED BY THE 4 | // MSGP CODE GENERATION TOOL (github.com/tinylib/msgp) 5 | // DO NOT EDIT 6 | 7 | import ( 8 | "testing" 9 | 10 | "github.com/tinylib/msgp/msgp" 11 | ) 12 | 13 | func TestMarshalUnmarshalNum(t *testing.T) { 14 | v := Num{} 15 | bts, err := v.MarshalMsg(nil) 16 | if err != nil { 17 | t.Fatal(err) 18 | } 19 | left, err := v.UnmarshalMsg(bts) 20 | if err != nil { 21 | t.Fatal(err) 22 | } 23 | if len(left) > 0 { 24 | t.Errorf("%d bytes left over after UnmarshalMsg(): %q", len(left), left) 25 | } 26 | 27 | left, err = msgp.Skip(bts) 28 | if err != nil { 29 | t.Fatal(err) 30 | } 31 | if len(left) > 0 { 32 | t.Errorf("%d bytes left over after Skip(): %q", len(left), left) 33 | } 34 | } 35 | 36 | func BenchmarkMarshalMsgNum(b *testing.B) { 37 | v := Num{} 38 | b.ReportAllocs() 39 | b.ResetTimer() 40 | for i := 0; i < b.N; i++ { 41 | v.MarshalMsg(nil) 42 | } 43 | } 44 | 45 | func BenchmarkAppendMsgNum(b *testing.B) { 46 | v := Num{} 47 | bts := make([]byte, 0, v.Msgsize()) 48 | bts, _ = v.MarshalMsg(bts[0:0]) 49 | b.SetBytes(int64(len(bts))) 50 | b.ReportAllocs() 51 | b.ResetTimer() 52 | for i := 0; i < b.N; i++ { 53 | bts, _ = v.MarshalMsg(bts[0:0]) 54 | } 55 | } 56 | 57 | func BenchmarkUnmarshalNum(b *testing.B) { 58 | v := Num{} 59 | bts, _ := v.MarshalMsg(nil) 60 | b.ReportAllocs() 61 | b.SetBytes(int64(len(bts))) 62 | b.ResetTimer() 63 | for i := 0; i < b.N; i++ { 64 | _, err := v.UnmarshalMsg(bts) 65 | if err != nil { 66 | b.Fatal(err) 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /Protocol.md: -------------------------------------------------------------------------------- 1 | Synapse Wire Protocol 2 | ===================== 3 | 4 | Synapse uses a simple framing protocol to transmit data. This document describes 5 | the fundamental structure of the protocol, although certain details have yet to be 6 | formalized. 7 | 8 | ## Lead Frame 9 | 10 | Every message on the wire begins with a "lead frame," which is formatted 11 | as follows: 12 | 13 | +-----------------+------------+----------------+==========+ 14 | | Sequence Number | Frame Type | Message Length | DATA | 15 | +-----------------+------------+----------------+==========+ 16 | | (8 bytes) | (1 byte) | (2 bytes) | N bytes | 17 | +-----------------+------------+----------------+==========+ 18 | | 64-bit usigned | uint8 | 16-bit unsigned| | 19 | | integer, big- | | integer, big- | | 20 | | endian | | endian | | 21 | +-----------------+------------+----------------+==========+ 22 | 23 | The sequence number is a per-connection request counter that is incremented 24 | atomically on the client side. It is used to uniquely identify each request. Responses 25 | have the same sequence number as their corresponding request. 26 | 27 | The frame types are defined as follows: 28 | 29 | | Value | Type | 30 | |:-----:|:----:| 31 | | 1 | Request | 32 | | 2 | Response | 33 | | 3 | Command | 34 | 35 | The message length is the number of bytes in the message *not including the lead frame.* 36 | 37 | ## Request Message 38 | 39 | | | Name | Message | 40 | |:-:|:----:|:-------:| 41 | | Value | MessagePack string | MessagePack object | 42 | 43 | ## Response Message 44 | 45 | | | Code | Message | 46 | |:-:|:----:|:-------:| 47 | | Value | MessagePack int | MessagePack object | 48 | 49 | ## Command Message 50 | 51 | | | Type | Body | 52 | |:-:|:----:|:----:| 53 | | Value | byte | N-1 bytes | 54 | 55 | TODO: document commands -------------------------------------------------------------------------------- /response.go: -------------------------------------------------------------------------------- 1 | package synapse 2 | 3 | import ( 4 | "github.com/tinylib/msgp/msgp" 5 | ) 6 | 7 | const outPrealloc = 256 8 | 9 | // A ResponseWriter is the interface through 10 | // which Handlers write responses. Handlers can 11 | // either return a body (through Send) or an 12 | // error (through Error); whichever call is 13 | // made first should 'win'. Subsequent calls 14 | // to either method should no-op. 15 | type ResponseWriter interface { 16 | // Error sets an error status 17 | // to be returned to the caller, 18 | // along with an explanation 19 | // of the error. 20 | Error(Status, string) 21 | 22 | // Send sets the body to be 23 | // returned to the caller. 24 | Send(msgp.Marshaler) error 25 | } 26 | 27 | // ResponseWriter implementation 28 | type response struct { 29 | out []byte // body 30 | wrote bool // written? 31 | _ [sizeofPtr - 1]byte // pad 32 | } 33 | 34 | func (r *response) resetLead() { 35 | // we need to save the lead bytes 36 | if cap(r.out) < leadSize { 37 | r.out = make([]byte, leadSize, outPrealloc) 38 | return 39 | } 40 | r.out = r.out[0:leadSize] 41 | return 42 | } 43 | 44 | // base Error implementation 45 | func (r *response) Error(s Status, expl string) { 46 | if r.wrote { 47 | return 48 | } 49 | r.wrote = true 50 | r.resetLead() 51 | r.out = msgp.AppendInt(r.out, int(s)) 52 | r.out = msgp.AppendString(r.out, expl) 53 | } 54 | 55 | // base Send implementation 56 | func (r *response) Send(msg msgp.Marshaler) error { 57 | if r.wrote { 58 | return nil 59 | } 60 | r.wrote = true 61 | 62 | var err error 63 | r.resetLead() 64 | r.out = msgp.AppendInt(r.out, int(StatusOK)) 65 | if msg != nil { 66 | r.out, err = msg.MarshalMsg(r.out) 67 | if err != nil { 68 | return err 69 | } 70 | if len(r.out) > maxMessageSize { 71 | return ErrTooLarge 72 | } 73 | return nil 74 | } 75 | r.out = msgp.AppendNil(r.out) 76 | return nil 77 | } 78 | -------------------------------------------------------------------------------- /command.go: -------------------------------------------------------------------------------- 1 | package synapse 2 | 3 | // Commands have request-response 4 | // semantics similar to the user-level 5 | // requests and responses. However, commands 6 | // are only sent only by internal processes. 7 | // Clients can use commands to poll for information 8 | // from the server, etc. 9 | // 10 | // The ordering of events is: 11 | // - client -> writeCmd([command]) -> server -> action.Server() -> [command] -> client -> action.Client() 12 | // 13 | // See the ping handler and client.ping() for an example. 14 | 15 | // frame type 16 | type fType byte 17 | 18 | // 19 | const ( 20 | // invalid frame 21 | fINVAL fType = iota 22 | 23 | // request frame 24 | fREQ 25 | 26 | // response frame 27 | fRES 28 | 29 | // command frame 30 | fCMD 31 | ) 32 | 33 | // command is a message 34 | // sent between the client 35 | // and server (either direction) 36 | // that allows either end 37 | // to communicate information 38 | // to the other 39 | type command uint8 40 | 41 | // cmdDirectory is a map of all the commands 42 | // to their respective actions 43 | var cmdDirectory = [_maxcommand]action{ 44 | cmdPing: ping{}, 45 | } 46 | 47 | // an action is the consequence 48 | // of a command - commands are 49 | // mapped to actions 50 | type action interface { 51 | // Client is the action carried out on the client side 52 | // when it receives a command response from a server 53 | Client(c *Client, msg []byte) 54 | 55 | // Sever is the action carried out on the server side. It 56 | // should return the reponse message (if any), and any 57 | // error encountered. Errors will result in cmdInvalid 58 | // sent to the client. 59 | Server(ch *connHandler, msg []byte) (res []byte, err error) 60 | } 61 | 62 | // list of commands 63 | const ( 64 | // cmdInvalid is used 65 | // to indicate a command 66 | // doesn't exist or is 67 | // formatted incorrectly 68 | cmdInvalid command = iota 69 | 70 | // ping is a 71 | // simple ping 72 | // command 73 | cmdPing 74 | 75 | // a command >= _maxcommand 76 | // is invalid 77 | _maxcommand 78 | ) 79 | 80 | // ping is a no-op on both sides 81 | type ping struct{} 82 | 83 | func (p ping) Client(cl *Client, res []byte) {} 84 | 85 | func (p ping) Server(ch *connHandler, body []byte) ([]byte, error) { return nil, nil } 86 | -------------------------------------------------------------------------------- /handler.go: -------------------------------------------------------------------------------- 1 | package synapse 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | // Handler is the interface used 8 | // to register server response callbacks. 9 | type Handler interface { 10 | 11 | // ServeCall handles a synapse request and 12 | // writes a response. It should be safe to 13 | // call ServeCall from multiple goroutines. 14 | // Both the request and response interfaces 15 | // become invalid after the call is returned; 16 | // no implementation of ServeCall should 17 | // maintain a reference to either object 18 | // after the function returns. 19 | ServeCall(req Request, res ResponseWriter) 20 | } 21 | 22 | // Status represents 23 | // a response status code 24 | type Status int 25 | 26 | // These are the status 27 | // codes that can be passed 28 | // to a ResponseWriter as 29 | // an error to send to the 30 | // client. 31 | const ( 32 | StatusInvalid Status = iota // zero-value for Status 33 | StatusOK // OK 34 | StatusNotFound // no handler for the request method 35 | StatusCondition // precondition failure 36 | StatusBadRequest // mal-formed request 37 | StatusNotAuthed // not authorized 38 | StatusServerError // server-side error 39 | StatusOther // other error 40 | ) 41 | 42 | // ResponseError is the type of error 43 | // returned by the client when the 44 | // server sends a response with 45 | // ResponseWriter.Error() 46 | type ResponseError struct { 47 | Code Status 48 | Expl string 49 | } 50 | 51 | // Error implements error 52 | func (e *ResponseError) Error() string { 53 | return fmt.Sprintf("synapse: response error (%s): %s", e.Code, e.Expl) 54 | } 55 | 56 | // String returns the string representation of the status 57 | func (s Status) String() string { 58 | switch s { 59 | case StatusOK: 60 | return "OK" 61 | case StatusNotFound: 62 | return "not found" 63 | case StatusCondition: 64 | return "precondition failed" 65 | case StatusBadRequest: 66 | return "bad request" 67 | case StatusNotAuthed: 68 | return "not authorized" 69 | case StatusServerError: 70 | return "server error" 71 | case StatusOther: 72 | return "other" 73 | default: 74 | return fmt.Sprintf("Status(%d)", s) 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /stack.go: -------------------------------------------------------------------------------- 1 | // +build !race 2 | 3 | package synapse 4 | 5 | import ( 6 | "github.com/tinylib/spin" 7 | ) 8 | 9 | // this file is just defines a thread-safe 10 | // stack to use as a free-list for *connWrapper 11 | // and *waiter structures, since they are allocated 12 | // and de-allocated frequently and predictably. 13 | // 14 | // we statically allocate 'arenaSize' waiters 15 | // and connWrappers in contiguous blocks, and 16 | // then create a LIFO out of them during 17 | // initialization. if we run out of elements 18 | // in the stack, we fall back to using the 19 | // heap. wrappers are heap-allocated if 20 | // c.next == c, and waiters are heap allocated 21 | // if (*waiter).static == false. 22 | 23 | const arenaSize = 512 24 | 25 | var ( 26 | waiters waitStack 27 | wrappers connStack 28 | 29 | waiterSlab [arenaSize]waiter 30 | wrapperSlab [arenaSize]connWrapper 31 | ) 32 | 33 | func init() { 34 | // set up the pointers and lock 35 | // the waiter semaphores 36 | waiterSlab[0].static = true 37 | for i := 0; i < (arenaSize - 1); i++ { 38 | waiterSlab[i].next = &waiterSlab[i+1] 39 | waiterSlab[i+1].static = true 40 | wrapperSlab[i].next = &wrapperSlab[i+1] 41 | } 42 | waiters.top = &waiterSlab[0] 43 | wrappers.top = &wrapperSlab[0] 44 | } 45 | 46 | type connStack struct { 47 | top *connWrapper 48 | lock uint32 49 | } 50 | 51 | type waitStack struct { 52 | top *waiter 53 | lock uint32 54 | } 55 | 56 | func (s *connStack) pop() (ptr *connWrapper) { 57 | spin.Lock(&s.lock) 58 | if s.top != nil { 59 | ptr, s.top = s.top, s.top.next 60 | spin.Unlock(&s.lock) 61 | return 62 | } 63 | spin.Unlock(&s.lock) 64 | ptr = &connWrapper{} 65 | ptr.next = ptr 66 | return 67 | } 68 | 69 | func (s *connStack) push(ptr *connWrapper) { 70 | if ptr.next == ptr { 71 | return 72 | } 73 | spin.Lock(&s.lock) 74 | s.top, ptr.next = ptr, s.top 75 | spin.Unlock(&s.lock) 76 | return 77 | } 78 | 79 | // the following should always hold: 80 | // - ptr.next = nil 81 | // - ptr.parent = c 82 | // - ptr.done is Lock()ed 83 | func (s *waitStack) pop(c *Client) (ptr *waiter) { 84 | spin.Lock(&s.lock) 85 | if s.top != nil { 86 | ptr, s.top = s.top, s.top.next 87 | spin.Unlock(&s.lock) 88 | ptr.parent = c 89 | ptr.next = nil 90 | return 91 | } 92 | spin.Unlock(&s.lock) 93 | ptr = &waiter{} 94 | ptr.parent = c 95 | return 96 | } 97 | 98 | func (s *waitStack) push(ptr *waiter) { 99 | ptr.parent = nil 100 | ptr.err = nil 101 | if !ptr.static { 102 | return 103 | } 104 | spin.Lock(&s.lock) 105 | s.top, ptr.next = ptr, s.top 106 | spin.Unlock(&s.lock) 107 | return 108 | } 109 | -------------------------------------------------------------------------------- /setup_test.go: -------------------------------------------------------------------------------- 1 | package synapse 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "log" 7 | "net" 8 | "os" 9 | "testing" 10 | "time" 11 | 12 | "github.com/tinylib/msgp/msgp" 13 | ) 14 | 15 | var ( 16 | // both the TCP and unix socket 17 | // client serve the same handler, 18 | // so in every case we expect 19 | // precisely the same behavior. 20 | 21 | // tcp client for testing 22 | tcpClient *Client 23 | 24 | // unix socket clinet for testing 25 | unxClient *Client 26 | 27 | // only global so that we 28 | // can attach handlers to it 29 | // during tests 30 | rt *RouteTable 31 | 32 | ct testing.T 33 | ) 34 | 35 | type testData []byte 36 | 37 | func (s *testData) MarshalMsg(b []byte) ([]byte, error) { 38 | return msgp.AppendBytes(b, []byte(*s)), nil 39 | } 40 | 41 | func (s *testData) UnmarshalMsg(b []byte) (o []byte, err error) { 42 | var t []byte 43 | t, o, err = msgp.ReadBytesBytes(b, []byte(*s)) 44 | *s = testData(t) 45 | return 46 | } 47 | 48 | type EchoHandler struct{} 49 | 50 | func (e EchoHandler) ServeCall(req Request, res ResponseWriter) { 51 | var s msgp.Raw 52 | err := req.Decode(&s) 53 | if err != nil { 54 | panic(err) 55 | } 56 | res.Send(&s) 57 | } 58 | 59 | type NopHandler struct{} 60 | 61 | func (n NopHandler) ServeCall(req Request, res ResponseWriter) { 62 | err := req.Decode(nil) 63 | if err != nil { 64 | panic(err) 65 | } 66 | res.Send(nil) 67 | } 68 | 69 | func finish(c io.Closer) { 70 | err := c.Close() 71 | if err != nil { 72 | fmt.Println("warning:", err) 73 | } 74 | time.Sleep(1 * time.Millisecond) 75 | } 76 | 77 | const ( 78 | Echo Method = iota 79 | Nop 80 | DebugEcho 81 | ) 82 | 83 | func TestMain(m *testing.M) { 84 | 85 | RegisterName(Echo, "echo") 86 | RegisterName(Nop, "nop") 87 | RegisterName(DebugEcho, "debug-echo") 88 | 89 | rt = &RouteTable{ 90 | Echo: EchoHandler{}, 91 | Nop: NopHandler{}, 92 | DebugEcho: Debug(EchoHandler{}, log.New(os.Stderr, "debug-echo :: ", log.LstdFlags)), 93 | } 94 | 95 | l, err := net.Listen("tcp", ":7070") 96 | if err != nil { 97 | panic(err) 98 | } 99 | 100 | go Serve(l, rt) 101 | 102 | ul, err := net.Listen("unix", "synapse") 103 | if err != nil { 104 | panic(err) 105 | } 106 | 107 | go Serve(ul, rt) 108 | 109 | tcpClient, err = Dial("tcp", ":7070", 5*time.Millisecond) 110 | if err != nil { 111 | panic(err) 112 | } 113 | 114 | unxClient, err = Dial("unix", "synapse", 5*time.Millisecond) 115 | if err != nil { 116 | panic(err) 117 | } 118 | 119 | ret := m.Run() 120 | 121 | // note: the unix socket 122 | // won't get cleaned up 123 | // if the client is 124 | // closed *after* the 125 | // listener, which will 126 | // cause subsequent tests 127 | // to fail to bind: "address 128 | // already in use" 129 | 130 | finish(tcpClient) 131 | finish(unxClient) 132 | finish(ul) 133 | finish(l) 134 | 135 | os.Exit(ret) 136 | } 137 | -------------------------------------------------------------------------------- /client_test.go: -------------------------------------------------------------------------------- 1 | package synapse 2 | 3 | import ( 4 | "bytes" 5 | "net" 6 | "sync" 7 | "testing" 8 | "time" 9 | ) 10 | 11 | func isCode(err error, c Status) bool { 12 | if reserr, ok := err.(*ResponseError); ok && reserr.Code == c { 13 | return true 14 | } 15 | return false 16 | } 17 | 18 | // open up a client and server; make 19 | // some concurrent requests 20 | func TestClient(t *testing.T) { 21 | 22 | const concurrent = 5 23 | wg := new(sync.WaitGroup) 24 | wg.Add(concurrent) 25 | 26 | for i := 0; i < concurrent; i++ { 27 | go func() { 28 | instr := testData("hello, world!") 29 | var outstr testData 30 | err := tcpClient.Call(Echo, &instr, &outstr) 31 | if err != nil { 32 | t.Fatal(err) 33 | } 34 | if !bytes.Equal([]byte(instr), []byte(outstr)) { 35 | t.Fatal("input and output not equal") 36 | } 37 | wg.Done() 38 | }() 39 | } 40 | 41 | wg.Wait() 42 | 43 | // test for NotFound 44 | err := tcpClient.Call(100, nil, nil) 45 | if !isCode(err, StatusNotFound) { 46 | t.Errorf("expected not-found error; got %#v", err) 47 | } 48 | } 49 | 50 | // the output of the debug handler 51 | // is only visible if '-v' is set 52 | func TestDebugClient(t *testing.T) { 53 | instr := String("here's a message body!") 54 | var outstr String 55 | err := tcpClient.Call(DebugEcho, &instr, &outstr) 56 | if err != nil { 57 | t.Fatal(err) 58 | } 59 | if instr != outstr { 60 | t.Fatal("input and output not equal") 61 | } 62 | } 63 | 64 | // test that 'nil' is a safe 65 | // argument to requests and responses 66 | func TestNop(t *testing.T) { 67 | err := tcpClient.Call(Nop, nil, nil) 68 | if err != nil { 69 | t.Fatal(err) 70 | } 71 | } 72 | 73 | // benchmarks the test case above 74 | func BenchmarkTCPEcho(b *testing.B) { 75 | l, err := net.Listen("tcp", "localhost:7000") 76 | if err != nil { 77 | b.Fatal(err) 78 | } 79 | defer func() { 80 | l.Close() 81 | time.Sleep(1 * time.Millisecond) 82 | }() 83 | 84 | go Serve(l, EchoHandler{}) 85 | cl, err := Dial("tcp", "localhost:7000", 50*time.Millisecond) 86 | if err != nil { 87 | b.Fatal(err) 88 | } 89 | defer cl.Close() 90 | 91 | b.ResetTimer() 92 | b.ReportAllocs() 93 | b.SetParallelism(20) 94 | b.RunParallel(func(pb *testing.PB) { 95 | instr := testData("hello, world!") 96 | var outstr testData 97 | for pb.Next() { 98 | err := cl.Call(Echo, &instr, &outstr) 99 | if err != nil { 100 | b.Fatal(err) 101 | } 102 | if !bytes.Equal([]byte(instr), []byte(outstr)) { 103 | b.Fatalf("%q in; %q out", instr, outstr) 104 | } 105 | } 106 | }) 107 | b.StopTimer() 108 | } 109 | 110 | func BenchmarkUnixNoop(b *testing.B) { 111 | l, err := net.Listen("unix", "bench") 112 | if err != nil { 113 | b.Fatal(err) 114 | } 115 | defer func() { 116 | l.Close() 117 | time.Sleep(1 * time.Millisecond) 118 | }() 119 | go Serve(l, NopHandler{}) 120 | cl, err := Dial("unix", "bench", 50*time.Millisecond) 121 | if err != nil { 122 | b.Fatal(err) 123 | } 124 | defer cl.Close() 125 | 126 | b.ResetTimer() 127 | b.ReportAllocs() 128 | b.SetParallelism(20) 129 | b.RunParallel(func(pb *testing.PB) { 130 | for pb.Next() { 131 | err := cl.Call(Echo, nil, nil) 132 | if err != nil { 133 | b.Fatal(err) 134 | } 135 | } 136 | }) 137 | b.StopTimer() 138 | } 139 | 140 | func BenchmarkPipeNoop(b *testing.B) { 141 | srv, cln := net.Pipe() 142 | 143 | go ServeConn(srv, NopHandler{}) 144 | 145 | defer srv.Close() 146 | 147 | cl, err := NewClient(cln, 50*time.Millisecond) 148 | if err != nil { 149 | b.Fatal(err) 150 | } 151 | 152 | defer cl.Close() 153 | 154 | b.ResetTimer() 155 | b.ReportAllocs() 156 | b.SetParallelism(20) 157 | b.RunParallel(func(pb *testing.PB) { 158 | for pb.Next() { 159 | err := cl.Call(0, nil, nil) 160 | if err != nil { 161 | b.Fatal(err) 162 | } 163 | } 164 | }) 165 | b.StopTimer() 166 | } 167 | -------------------------------------------------------------------------------- /map_test.go: -------------------------------------------------------------------------------- 1 | package synapse 2 | 3 | import ( 4 | "sync" 5 | "testing" 6 | ) 7 | 8 | func TestWaiterMap(t *testing.T) { 9 | mp := &wMap{} 10 | 11 | q := mp.remove(4309821) 12 | if q != nil { 13 | t.Error("remove() should return 'nil' from an empty map") 14 | } 15 | 16 | vals := make([]waiter, 1000) 17 | for i := range vals { 18 | vals[i].seq = uint64(i) * 17 19 | } 20 | 21 | for i := range vals { 22 | mp.insert(&vals[i]) 23 | } 24 | 25 | if mp.length() != 1000 { 26 | t.Errorf("expected map to have 1000 elements; found %d", mp.length()) 27 | } 28 | 29 | for i := range vals { 30 | q := mp.remove(vals[i].seq) 31 | if q != &vals[i] { 32 | t.Errorf("expected %v; got %v", &vals[i], q) 33 | } 34 | } 35 | 36 | l := mp.length() 37 | if l != 0 { 38 | t.Errorf("expected map to have 0 elements; found %d", l) 39 | } 40 | 41 | mp.insert(&vals[200]) 42 | 43 | mp.flush(nil) 44 | 45 | if mp.length() != 0 { 46 | t.Errorf("expected map length to be 0 after flush(); found %d", mp.length()) 47 | } 48 | 49 | for i := range vals { 50 | mp.insert(&vals[i]) 51 | } 52 | 53 | l = mp.length() 54 | if l != 1000 { 55 | t.Errorf("expected 1000 elements after re-insertion; found %d", l) 56 | } 57 | 58 | mp.reap() 59 | 60 | l = mp.length() 61 | if l != 1000 { 62 | t.Errorf("expected 1000 elements after first reap(); found %d", l) 63 | } 64 | 65 | mp.reap() 66 | 67 | l = mp.length() 68 | if l != 0 { 69 | t.Errorf("expected 0 elements after second reap(); found %d", l) 70 | } 71 | 72 | for i := range vals { 73 | vals[i].reap = false 74 | mp.insert(&vals[i]) 75 | } 76 | 77 | // non-sequential removal 78 | for i := range vals { 79 | x := &vals[len(vals)-i-1] 80 | v := mp.remove(x.seq) 81 | if v == nil { 82 | t.Errorf("index %d: no value returned", len(vals)-i-1) 83 | } 84 | if v != x { 85 | t.Errorf("expected %v out; got %v", x, v) 86 | } 87 | } 88 | } 89 | 90 | func seqInsert(mp *wMap, b *testing.B, num int) { 91 | vals := make([]waiter, num) 92 | for i := range vals { 93 | vals[i].seq = uint64(i) 94 | } 95 | b.ReportAllocs() 96 | b.ResetTimer() 97 | for i := 0; i < b.N; i++ { 98 | for j := range vals { 99 | mp.insert(&vals[j]) 100 | } 101 | for j := range vals { 102 | mp.remove(uint64(j)) 103 | } 104 | } 105 | } 106 | 107 | func stdInsert(mp map[uint64]*waiter, b *testing.B, num int) { 108 | vals := make([]waiter, num) 109 | for i := range vals { 110 | vals[i].seq = uint64(i) 111 | } 112 | // the mutex is just for symmetry 113 | // with the other map implementation, 114 | // as it does one lock per insert/delete 115 | var m sync.Mutex 116 | b.ReportAllocs() 117 | b.ResetTimer() 118 | for i := 0; i < b.N; i++ { 119 | for j := range vals { 120 | m.Lock() 121 | mp[vals[j].seq] = &vals[j] 122 | m.Unlock() 123 | } 124 | for j := range vals { 125 | m.Lock() 126 | if mp[vals[j].seq] != nil { 127 | delete(mp, vals[j].seq) 128 | } 129 | m.Unlock() 130 | } 131 | } 132 | } 133 | 134 | func BenchmarkMapInsertDelete(b *testing.B) { 135 | var mp wMap 136 | w := &waiter{} 137 | w.seq = 39082134 138 | b.ReportAllocs() 139 | b.ResetTimer() 140 | for i := 0; i < b.N; i++ { 141 | mp.insert(w) 142 | mp.remove(w.seq) 143 | } 144 | } 145 | 146 | func BenchmarkStdMapInsertDelete(b *testing.B) { 147 | mp := make(map[uint64]*waiter) 148 | w := &waiter{} 149 | w.seq = 39082134 150 | b.ReportAllocs() 151 | b.ResetTimer() 152 | var m sync.Mutex 153 | for i := 0; i < b.N; i++ { 154 | m.Lock() 155 | mp[w.seq] = w 156 | m.Unlock() 157 | m.Lock() 158 | w = mp[w.seq] 159 | delete(mp, w.seq) 160 | m.Unlock() 161 | } 162 | } 163 | 164 | func Benchmark1000Sequential(b *testing.B) { 165 | var mp wMap 166 | seqInsert(&mp, b, 1000) 167 | } 168 | 169 | func Benchmark500Sequential(b *testing.B) { 170 | var mp wMap 171 | seqInsert(&mp, b, 500) 172 | } 173 | 174 | func Benchmark1000StdSequential(b *testing.B) { 175 | mp := make(map[uint64]*waiter) 176 | stdInsert(mp, b, 1000) 177 | } 178 | 179 | func Benchmark500StdSequential(b *testing.B) { 180 | mp := make(map[uint64]*waiter) 181 | stdInsert(mp, b, 500) 182 | } 183 | -------------------------------------------------------------------------------- /map.go: -------------------------------------------------------------------------------- 1 | package synapse 2 | 3 | import ( 4 | "sync" 5 | 6 | "github.com/tinylib/synapse/sema" 7 | ) 8 | 9 | const ( 10 | // number of significant bits in 11 | // bucket addressing. more bits 12 | // means more memory and better 13 | // worst-case performance. 14 | bucketBits = 6 15 | 16 | // number of buckets per map 17 | mbuckets = 1 << bucketBits 18 | 19 | // bitmask for bucket index 20 | bucketMask = mbuckets - 1 21 | ) 22 | 23 | // wMap is really buckets of queues. 24 | // locking is done on the bucket level, so 25 | // concurrent reads and writes generally do 26 | // not contend with one another. the fact that 27 | // waiters are inserted and deleted in approximately 28 | // FIFO order means that we can often expect best-case 29 | // performance for removal. 30 | type wMap [mbuckets]mNode 31 | 32 | // returns a sequence number's canonical node 33 | func (w *wMap) node(seq uint64) *mNode { return &w[seq&bucketMask] } 34 | 35 | // a node is just a linked list of *waiter with 36 | // a mutex protecting access 37 | type mNode struct { 38 | sync.Mutex 39 | list *waiter // stack of *waiter 40 | tail *waiter // insert point 41 | } 42 | 43 | // pop searches the list from top to bottom 44 | // for the sequence number 'seq' and removes 45 | // the *waiter from the list if it exists. returns 46 | // nil otherwise. 47 | func (n *mNode) pop(seq uint64) *waiter { 48 | fwd := &n.list 49 | var prev *waiter 50 | for cur := n.list; cur != nil; cur = cur.next { 51 | if cur.seq == seq { 52 | if cur == n.tail { 53 | n.tail = prev 54 | } 55 | *fwd, cur.next = cur.next, nil 56 | return cur 57 | } 58 | fwd = &cur.next 59 | prev = cur 60 | } 61 | return nil 62 | } 63 | 64 | // insert puts a waiter at the tail of the list 65 | func (n *mNode) insert(q *waiter) { 66 | if n.list == nil { 67 | n.list = q 68 | } else { 69 | n.tail.next = q 70 | } 71 | n.tail = q 72 | } 73 | 74 | func (n *mNode) size() (i int) { 75 | for w := n.list; w != nil; w = w.next { 76 | i++ 77 | } 78 | return 79 | } 80 | 81 | // flush waiters marked with 'reap', 82 | // and mark unmarked waiters. 83 | func (n *mNode) reap() { 84 | fwd := &n.list 85 | var prev *waiter 86 | for cur := n.list; cur != nil; { 87 | if cur.reap { 88 | if cur == n.tail { 89 | n.tail = prev 90 | } 91 | *fwd, cur.next = cur.next, nil 92 | cur.err = ErrTimeout 93 | sema.Wake(&cur.done) 94 | cur = *fwd 95 | } else { 96 | cur.reap = true 97 | prev = cur 98 | fwd = &cur.next 99 | cur = cur.next 100 | } 101 | } 102 | } 103 | 104 | // flush the entire contents of the node 105 | func (n *mNode) flush(err error) { 106 | var next *waiter 107 | for l := n.list; l != nil; { 108 | next, l.next = l.next, nil 109 | l.err = err 110 | sema.Wake(&l.done) 111 | l = next 112 | } 113 | n.list = nil 114 | n.tail = nil 115 | } 116 | 117 | // insert inserts a waiter keyed on its 118 | // sequence number. this has undefined behavior 119 | // if the waiter is already in the map. (!!!) 120 | func (w *wMap) insert(q *waiter) { 121 | n := w.node(q.seq) 122 | n.Lock() 123 | n.insert(q) 124 | n.Unlock() 125 | } 126 | 127 | // remove gets a value and removes it if it exists. 128 | // it returns nil if it didn't exist in the first place. 129 | func (w *wMap) remove(seq uint64) *waiter { 130 | n := w.node(seq) 131 | n.Lock() 132 | wt := n.pop(seq) 133 | n.Unlock() 134 | return wt 135 | } 136 | 137 | // return the total size of the map... sort of. 138 | // this is only used for testing. during concurrent 139 | // access, the returned value may not be the size 140 | // of the map at *any* fixed point in time. it's 141 | // an approximation. 142 | func (w *wMap) length() (count int) { 143 | for i := range w { 144 | n := &w[i] 145 | n.Lock() 146 | count += n.size() 147 | n.Unlock() 148 | } 149 | return count 150 | } 151 | 152 | // unlock every waiter with the provided error, 153 | // and then zero out the entire contents of the map 154 | func (w *wMap) flush(err error) { 155 | for i := range w { 156 | n := &w[i] 157 | n.Lock() 158 | n.flush(err) 159 | n.Unlock() 160 | } 161 | } 162 | 163 | // reap carries out a timeout reap. 164 | // if (*waiter).reap==true, then delete it, 165 | // otherwise set (*waiter).reap to true. 166 | func (w *wMap) reap() { 167 | for i := range w { 168 | n := &w[i] 169 | n.Lock() 170 | n.reap() 171 | n.Unlock() 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /debug.go: -------------------------------------------------------------------------------- 1 | package synapse 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "log" 7 | "net" 8 | "time" 9 | 10 | "github.com/tinylib/msgp/msgp" 11 | ) 12 | 13 | var methodTab = map[Method]string{} 14 | 15 | func RegisterName(m Method, name string) { 16 | methodTab[m] = name 17 | } 18 | 19 | func (m Method) String() string { 20 | if str, ok := methodTab[m]; ok { 21 | return str 22 | } 23 | return fmt.Sprintf("Method(%d)", m) 24 | } 25 | 26 | type logger interface { 27 | Print(...interface{}) 28 | Printf(string, ...interface{}) 29 | } 30 | 31 | type debugh struct { 32 | inner Handler 33 | logger logger 34 | } 35 | 36 | // Debug wraps a handler and logs all of the incoming 37 | // and outgoing data using the provided logger. 38 | func Debug(h Handler, l *log.Logger) Handler { 39 | return &debugh{inner: h, logger: l} 40 | } 41 | 42 | func (d *debugh) ServeCall(req Request, res ResponseWriter) { 43 | // if we're handed the base types 44 | // used by the package, we can do this 45 | // with a lot less overhead 46 | if rq, ok := req.(*request); ok { 47 | if rs, ok := res.(*response); ok { 48 | d.serveBase(rq, rs) 49 | return 50 | } 51 | } 52 | 53 | var buf bytes.Buffer 54 | nm := req.Method() 55 | remote := req.RemoteAddr() 56 | 57 | var raw msgp.Raw 58 | err := req.Decode(&raw) 59 | if err != nil { 60 | d.logger.Printf("request from %s for method %s is malformed: %s\n", remote, nm, err) 61 | res.Error(StatusBadRequest, err.Error()) 62 | return 63 | } 64 | msgp.UnmarshalAsJSON(&buf, []byte(raw)) 65 | d.logger.Printf("request from %s:\n\tMETHOD: %s\n\tBODY: %s\n", remote, nm, buf.Bytes()) 66 | buf.Reset() 67 | r := &mockReq{ 68 | mtd: nm, 69 | remote: remote, 70 | raw: raw, 71 | } 72 | w := &mockRes{} 73 | start := time.Now() 74 | d.inner.ServeCall(r, w) 75 | ctime := time.Since(start) 76 | if !w.wrote { 77 | d.logger.Print("WARNING: handler for", nm, "did not write a valid response") 78 | res.Error(StatusServerError, "empty response") 79 | return 80 | } 81 | msgp.UnmarshalAsJSON(&buf, []byte(w.out)) 82 | d.logger.Printf("response to %s for method %s:\n\tSTATUS: %s\n\tBODY: %s\n\tDURATION: %s", remote, nm, w.status, buf.Bytes(), ctime) 83 | res.Send(msgp.Raw(w.out)) 84 | } 85 | 86 | type mockReq struct { 87 | remote net.Addr 88 | raw msgp.Raw 89 | mtd Method 90 | } 91 | 92 | func (m *mockReq) IsNil() bool { return msgp.IsNil([]byte(m.raw)) } 93 | func (m *mockReq) Method() Method { return m.mtd } 94 | func (m *mockReq) RemoteAddr() net.Addr { return m.remote } 95 | func (m *mockReq) Decode(u msgp.Unmarshaler) error { 96 | _, err := u.UnmarshalMsg([]byte(m.raw)) 97 | return err 98 | } 99 | 100 | type mockRes struct { 101 | wrote bool 102 | out []byte 103 | status Status 104 | } 105 | 106 | func (m *mockRes) Send(g msgp.Marshaler) error { 107 | if m.wrote { 108 | return nil 109 | } 110 | m.wrote = true 111 | var err error 112 | m.out, err = g.MarshalMsg(nil) 113 | if err != nil { 114 | return err 115 | } 116 | m.status = StatusOK 117 | return nil 118 | } 119 | 120 | func (m *mockRes) Error(s Status, r string) { 121 | if m.wrote { 122 | return 123 | } 124 | m.wrote = true 125 | m.status = s 126 | m.out = msgp.AppendString(m.out, r) 127 | } 128 | 129 | func (d *debugh) serveBase(req *request, res *response) { 130 | var buf bytes.Buffer 131 | _, err := msgp.UnmarshalAsJSON(&buf, req.in) 132 | remote := req.addr.String() 133 | if err != nil { 134 | d.logger.Printf("request from %s for %s was malformed: %s", remote, Method(req.mtd), err) 135 | res.Error(StatusBadRequest, err.Error()) 136 | return 137 | } 138 | d.logger.Printf("request from %s:\n\tMETHOD: %s\n\tREQUEST BODY: %s\n", remote, Method(req.mtd), buf.Bytes()) 139 | buf.Reset() 140 | start := time.Now() 141 | d.inner.ServeCall(req, res) 142 | ctime := time.Since(start) 143 | if !res.wrote || len(res.out) < leadSize { 144 | d.logger.Print("WARNING: handler for", Method(req.mtd), "did not write a valid response") 145 | res.Error(StatusServerError, "empty response") 146 | return 147 | } 148 | out := res.out[leadSize:] 149 | var stat int 150 | var body []byte 151 | stat, body, err = msgp.ReadIntBytes(out) 152 | if err != nil { 153 | d.logger.Printf("body of response to %s is malformed: %s", Method(req.mtd), err) 154 | return 155 | } 156 | status := Status(stat) 157 | _, err = msgp.UnmarshalAsJSON(&buf, body) 158 | if err != nil { 159 | d.logger.Printf("response for %s is malformed: %s", Method(req.mtd), err) 160 | return 161 | } 162 | d.logger.Printf("response to %s for %s:\n\tSTATUS: %s\n\tRESPONSE BODY: %s\n\tDURATION: %s\n", remote, Method(req.mtd), status, buf.Bytes(), ctime) 163 | } 164 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Synapse 2 | ======== 3 | 4 | ## What is this? 5 | 6 | Synapse is: 7 | 8 | - An RPC wire protocol based on [MessagePack](http://msgpack.org) 9 | - A library implementation of that protocol in go 10 | 11 | This library was designed to work with [tinylib/msgp](http://github.com/tinylib/msgp) for serialization. 12 | 13 | ## Why not MessagePack RPC? 14 | 15 | When we first started writing code for this library, it was supposed to be a MessagePack RPC implementation. 16 | However, we found that the nature of the protocol makes it difficult to recover from malformed or unexpected 17 | messages on the wire. The primary difference between the Synapse and MessagePack RPC protocols is that synapse 18 | uses length-prefixed frames to indicate the complete size of the message before it is read off of the wire. This 19 | has some benefits: 20 | 21 | - Serialization is done inside handlers, not in an I/O loop. The request/response body is copied into 22 | a reserved chunk of memory and then serialization is done in a separate goroutine. Thus, serialization can 23 | fail without interrupting other messages. 24 | - An unexpected or unwanted message can be *efficiently* skipped, because we do not have to 25 | traverse it to know its size. Being able to quickly discard unwanted or unexpected messages improves 26 | security against attacks designed to exhaust server resources. 27 | - Message size has a hard limit at 65535 bytes, which makes it significantly more difficult to exhaust server resources (either because of traffic anomalies or a deliberate attack.) 28 | 29 | Additionally, synapse includes a message type (called "command") that allows client and server implementations 30 | to probe one another programatically, which will allows us to add features (like service discovery) as the protocol 31 | evolves. 32 | 33 | ## Goals 34 | 35 | Synapse is designed to make it easy to write network applications that have request-response semantics, much 36 | like HTTP and (some) RPC protocols. Like `net/rpc`, synapse can operate over most network protocols (or any 37 | `net.Conn`), and, like `net/http`, provides a standardized way to write middlewares and routers around services. 38 | As an added bonus, synapse has a much smaller per-request and per-connection memory footprint than `net/rpc` or 39 | `net/http`. 40 | 41 | This repository contains only the "core" of the Synapse project. Over time, we will release middlewares in other repositories, but our intention is to keep the core as small as possible. 42 | 43 | #### Non-goals 44 | 45 | Synapse is not designed for large messages (there is a hard limit at 65kB), and it does not provide strong 46 | ordering guarantees. At the protocol level, there is no notion of CRUD operations or any other sort of stateful 47 | semantics; those are features that developers should provide at the application level. The same goes for auth. All 48 | of these features can effectively be implemented as wrappers of the core library. 49 | 50 | ## Hello World 51 | 52 | As a motivating example, let's consider a "hello world" program. (You can find the complete files in `_examples/hello_world/`.) 53 | 54 | Here's what the client code looks like: 55 | 56 | ```go 57 | func main() { 58 | // This sets up a TCP connection to 59 | // localhost:7000 and attaches a client 60 | // to it. Client creation fails if it 61 | // can't ping the server on the other 62 | // end. Additionally, calls will fail 63 | // if a response isn't received in one second. 64 | client, err := synapse.Dial("tcp", "localhost:7000", time.Second) 65 | if err != nil { 66 | fmt.Println(err) 67 | os.Exit(1) 68 | } 69 | 70 | // Here we make a remote call to 71 | // the method called "hello," and 72 | // we pass an object for the 73 | // response to be decoded into. 74 | // synapse.String is a convenience 75 | // provided for sending strings 76 | // back and forth. 77 | var res synapse.String 78 | err = client.Call("hello", nil, &res) 79 | if err != nil { 80 | fmt.Println(err) 81 | os.Exit(1) 82 | } 83 | 84 | fmt.Println("response from server:", string(res)) 85 | } 86 | ``` 87 | 88 | And here's the server code: 89 | 90 | ```go 91 | func main() { 92 | // Like net/http, synapse uses 93 | // routers to send requests to 94 | // the appropriate handler(s). 95 | // Here we'll use the one provided 96 | // by the synapse package, although 97 | // users can write their own. 98 | router := synapse.NewRouter() 99 | 100 | // Here we're registering the "hello" 101 | // route with a function that logs the 102 | // remote address of the caller and then 103 | // responds with "Hello, World!" 104 | router.HandleFunc("hello", func(req synapse.Request, res synapse.ResponseWriter) { 105 | log.Println("received request from client at", req.RemoteAddr()) 106 | res.Send(synapse.String("Hello, World!")) 107 | }) 108 | 109 | // ListenAndServe blocks forever 110 | // serving the provided handler. 111 | log.Fatalln(synapse.ListenAndServe("tcp", "localhost:7000", router)) 112 | } 113 | ``` 114 | 115 | ## Project Status 116 | 117 | Very alpha. Expect frequent breaking changes to the API. We're actively looking for community feedback. 118 | 119 | ## Performance, etc. 120 | 121 | Synapse is optimized for throughput over latency; in general, synapse is designed to perform well in adverse 122 | (high-load/high-concurrency) conditions over simple serial conditions. There are a number of implementation 123 | details that influence performance: 124 | 125 | - De-coupling of message body serialization/de-serialization from the main read/write loops reduces the 126 | amount of time spend in critical (blocking) sections, meaning that time spent blocking is both lower and 127 | more consistent. Additionally, malformed handlers cannot corrupt the state of the network I/O loop. 128 | However, this hurts performance in the simple (serial) case, because lots of time is spent copying memory 129 | rather than making forward progress. 130 | - Opportunistic coalescing of network writes reduces system call overhead, without dramatically affecting latency. 131 | - The objects used to maintain per-request state are arena allocated during initialization. Practically speaking, 132 | this means that synapse does 0 allocations per request on the client side, and 1 allocation on the 133 | server side (for `request.Name()`). 134 | - `tinylib/msgp` serialization is fast, compact, and versatile. 135 | 136 | -------------------------------------------------------------------------------- /server.go: -------------------------------------------------------------------------------- 1 | package synapse 2 | 3 | import ( 4 | "crypto/tls" 5 | "math" 6 | "net" 7 | "sync" 8 | "unsafe" 9 | 10 | "github.com/philhofer/fwd" 11 | "github.com/tinylib/msgp/msgp" 12 | ) 13 | 14 | const ( 15 | // used to compute pad sizes 16 | sizeofPtr = unsafe.Sizeof((*byte)(nil)) 17 | 18 | // leadSize is the size of a "lead frame" 19 | leadSize = 11 20 | 21 | // maxMessageSize is the maximum size of a message 22 | maxMessageSize = math.MaxUint16 23 | ) 24 | 25 | // All writes (on either side) need to be atomic; conn.Write() is called exactly once and 26 | // should contain the entirety of the request (client-side) or response (Server-side). 27 | // 28 | // In principle, the client can operate on any net.Conn, and the 29 | // Server can operate on any net.Listener. 30 | 31 | // Serve starts a Server on 'l' that serves 32 | // the supplied handler. It blocks until the 33 | // listener closes. 34 | func Serve(l net.Listener, h Handler) error { 35 | for { 36 | c, err := l.Accept() 37 | if err != nil { 38 | return err 39 | } 40 | go ServeConn(c, h) 41 | } 42 | } 43 | 44 | // ListenAndServeTLS acts identically to ListenAndServe, except that 45 | // it expects connections over TLS1.2 (see crypto/tls). Additionally, 46 | // files containing a certificate and matching private key for the 47 | // Server must be provided. If the certificate is signed by a 48 | // certificate authority, the certFile should be the concatenation of 49 | // the server's certificate followed by the CA's certificate. 50 | func ListenAndServeTLS(network, laddr string, certFile, keyFile string, h Handler) error { 51 | cert, err := tls.LoadX509KeyPair(certFile, keyFile) 52 | if err != nil { 53 | return err 54 | } 55 | 56 | l, err := tls.Listen(network, laddr, &tls.Config{ 57 | Certificates: []tls.Certificate{cert}, 58 | }) 59 | if err != nil { 60 | return err 61 | } 62 | return Serve(l, h) 63 | } 64 | 65 | // ListenAndServe opens up a network listener 66 | // on the provided network and local address 67 | // and begins serving with the provided handler. 68 | // ListenAndServe blocks until there is a fatal 69 | // listener error. 70 | func ListenAndServe(network string, laddr string, h Handler) error { 71 | l, err := net.Listen(network, laddr) 72 | if err != nil { 73 | return err 74 | } 75 | return Serve(l, h) 76 | } 77 | 78 | // ServeConn serves an individual network 79 | // connection. It blocks until the connection 80 | // is closed or it encounters a fatal error. 81 | func ServeConn(c net.Conn, h Handler) { 82 | ch := connHandler{ 83 | conn: c, 84 | h: h, 85 | remote: c.RemoteAddr(), 86 | writing: make(chan *connWrapper, 32), 87 | } 88 | go ch.writeLoop() 89 | ch.connLoop() // returns on connection close 90 | ch.wg.Wait() // wait for handlers to return 91 | close(ch.writing) // close output queue 92 | } 93 | 94 | func readFrame(lead [leadSize]byte) (seq uint64, ft fType, sz int) { 95 | seq = (uint64(lead[0]) << 56) | (uint64(lead[1]) << 48) | 96 | (uint64(lead[2]) << 40) | (uint64(lead[3]) << 32) | 97 | (uint64(lead[4]) << 24) | (uint64(lead[5]) << 16) | 98 | (uint64(lead[6]) << 8) | (uint64(lead[7])) 99 | ft = fType(lead[8]) 100 | sz = int((uint16(lead[9]) << 8) | uint16(lead[10])) 101 | return 102 | } 103 | 104 | func putFrame(bts []byte, seq uint64, ft fType, sz int) { 105 | bts[0] = byte(seq >> 56) 106 | bts[1] = byte(seq >> 48) 107 | bts[2] = byte(seq >> 40) 108 | bts[3] = byte(seq >> 32) 109 | bts[4] = byte(seq >> 24) 110 | bts[5] = byte(seq >> 16) 111 | bts[6] = byte(seq >> 8) 112 | bts[7] = byte(seq) 113 | bts[8] = byte(ft) 114 | usz := uint16(sz) 115 | bts[9] = byte(usz >> 8) 116 | bts[10] = byte(usz) 117 | } 118 | 119 | // connHandler handles network 120 | // connections and multiplexes requests 121 | // to connWrappers 122 | type connHandler struct { 123 | h Handler 124 | conn net.Conn 125 | remote net.Addr 126 | wg sync.WaitGroup // outstanding handlers 127 | writing chan *connWrapper // write queue 128 | } 129 | 130 | func (c *connHandler) writeLoop() error { 131 | bwr := fwd.NewWriterSize(c.conn, 4096) 132 | 133 | // this works the same way 134 | // as (*client).writeLoop() 135 | // 136 | // the goroutine wakes up when 137 | // there are pending messages 138 | // to write. it writes the first 139 | // one into the buffered writer, 140 | // and continues to fill the 141 | // buffered writer until 142 | // no more messages are pending, 143 | // and then flushes whatever is 144 | // left. (*connHandler).writing 145 | // is closed when there are no 146 | // more pending handlers. 147 | var err error 148 | for { 149 | cw, ok := <-c.writing 150 | if !ok { 151 | // this is the "normal" 152 | // exit point for this 153 | // goroutine. 154 | return nil 155 | } 156 | if !c.do(bwr.Write(cw.res.out)) { 157 | goto flush 158 | } 159 | 160 | wrappers.push(cw) 161 | more: 162 | select { 163 | case another, ok := <-c.writing: 164 | if ok { 165 | f := c.do(bwr.Write(another.res.out)) 166 | wrappers.push(another) 167 | if !f { 168 | goto flush 169 | } 170 | goto more 171 | } else { 172 | bwr.Flush() 173 | return nil 174 | } 175 | default: 176 | if !c.do(0, bwr.Flush()) { 177 | goto flush 178 | } 179 | } 180 | } 181 | flush: 182 | for w := range c.writing { 183 | wrappers.push(w) 184 | } 185 | return err 186 | } 187 | 188 | func (c *connHandler) do(i int, err error) bool { 189 | if err != nil { 190 | c.conn.Close() 191 | return false 192 | } 193 | return true 194 | } 195 | 196 | // connLoop continuously polls the connection. 197 | // requests are read synchronously; the responses 198 | // are written in a spawned goroutine 199 | func (c *connHandler) connLoop() { 200 | brd := fwd.NewReaderSize(c.conn, 4096) 201 | 202 | var ( 203 | lead [leadSize]byte 204 | seq uint64 205 | sz int 206 | frame fType 207 | err error 208 | ) 209 | 210 | for { 211 | // loop: 212 | // - read seq, type, sz 213 | // - call handler asynchronously 214 | 215 | if !c.do(brd.ReadFull(lead[:])) { 216 | return 217 | } 218 | seq, frame, sz = readFrame(lead) 219 | 220 | // handle commands 221 | if frame == fCMD { 222 | var body []byte // command body; may be nil 223 | var cmd command // command byte 224 | 225 | // the 1-byte body case 226 | // is pretty common for fCMD 227 | if sz == 1 { 228 | var bt byte 229 | bt, err = brd.ReadByte() 230 | cmd = command(bt) 231 | } else { 232 | body = make([]byte, sz) 233 | _, err = brd.ReadFull(body) 234 | cmd = command(body[0]) 235 | body = body[1:] 236 | } 237 | if err != nil { 238 | c.conn.Close() 239 | return 240 | } 241 | c.wg.Add(1) 242 | go handleCmd(c, seq, cmd, body) 243 | continue 244 | } 245 | 246 | // the only valid frame 247 | // type left is fREQ 248 | if frame != fREQ { 249 | if !c.do(brd.Skip(sz)) { 250 | return 251 | } 252 | continue 253 | } 254 | 255 | w := wrappers.pop() 256 | 257 | if cap(w.in) >= sz { 258 | w.in = w.in[0:sz] 259 | } else { 260 | w.in = make([]byte, sz) 261 | } 262 | 263 | if !c.do(brd.ReadFull(w.in)) { 264 | return 265 | } 266 | 267 | // trigger handler 268 | w.seq = seq 269 | c.wg.Add(1) 270 | go c.handleReq(w) 271 | } 272 | } 273 | 274 | // connWrapper contains all the resources 275 | // necessary to execute a Handler on a request 276 | type connWrapper struct { 277 | next *connWrapper // only used by slab 278 | seq uint64 // sequence number 279 | req request // (8w) 280 | res response // (4w) 281 | in []byte // incoming message 282 | } 283 | 284 | // handleconn sets up the Request and ResponseWriter 285 | // interfaces and calls the handler. 286 | func (c *connHandler) handleReq(cw *connWrapper) { 287 | // clear/reset everything 288 | cw.req.addr = c.remote 289 | cw.res.wrote = false 290 | 291 | var err error 292 | 293 | // split request into 'name' and body 294 | cw.req.mtd, cw.req.in, err = msgp.ReadUint32Bytes(cw.in) 295 | if err != nil { 296 | cw.res.Error(StatusBadRequest, "malformed request method") 297 | } else { 298 | c.h.ServeCall(&cw.req, &cw.res) 299 | // if the handler didn't write a body, 300 | // write 'nil' 301 | if !cw.res.wrote { 302 | cw.res.Send(nil) 303 | } 304 | } 305 | 306 | blen := len(cw.res.out) - leadSize // length minus frame length 307 | putFrame(cw.res.out, cw.seq, fRES, blen) 308 | c.writing <- cw 309 | c.wg.Done() 310 | } 311 | 312 | func handleCmd(c *connHandler, seq uint64, cmd command, body []byte) { 313 | if cmd == cmdInvalid || cmd >= _maxcommand { 314 | return 315 | } 316 | 317 | act := cmdDirectory[cmd] 318 | resbyte := byte(cmd) 319 | var res []byte 320 | var err error 321 | if act == nil { 322 | resbyte = byte(cmdInvalid) 323 | } else { 324 | res, err = act.Server(c, body) 325 | if err != nil { 326 | resbyte = byte(cmdInvalid) 327 | } 328 | } 329 | 330 | // for now, we'll use one of the 331 | // connection wrappers 332 | wr := wrappers.pop() 333 | 334 | sz := len(res) + 1 335 | need := sz + leadSize 336 | if cap(wr.res.out) < need { 337 | wr.res.out = make([]byte, need) 338 | } else { 339 | wr.res.out = wr.res.out[:need] 340 | } 341 | 342 | putFrame(wr.res.out[:], seq, fCMD, sz) 343 | wr.res.out[leadSize] = resbyte 344 | if res != nil { 345 | copy(wr.res.out[leadSize+1:], res) 346 | } 347 | c.writing <- wr 348 | c.wg.Done() 349 | } 350 | -------------------------------------------------------------------------------- /client.go: -------------------------------------------------------------------------------- 1 | package synapse 2 | 3 | import ( 4 | "crypto/tls" 5 | "errors" 6 | "fmt" 7 | "net" 8 | "sync" 9 | "sync/atomic" 10 | "time" 11 | 12 | "github.com/philhofer/fwd" 13 | "github.com/tinylib/msgp/msgp" 14 | "github.com/tinylib/synapse/sema" 15 | ) 16 | 17 | const ( 18 | // waiter "high water mark" 19 | // TODO(maybe): make this adjustable. 20 | waiterHWM = 32 21 | ) 22 | 23 | const ( 24 | clientClosed = iota 25 | clientOpen 26 | ) 27 | 28 | var ( 29 | // ErrClosed is returns when a call is attempted 30 | // on a closed client. 31 | ErrClosed = errors.New("synapse: client is closed") 32 | 33 | // ErrTimeout is returned when a server 34 | // doesn't respond to a request before 35 | // the client's timeout scavenger can 36 | // free the waiting goroutine 37 | ErrTimeout = errors.New("synapse: the server didn't respond in time") 38 | 39 | // ErrTooLarge is returned when the message 40 | // size is larger than 65,535 bytes. 41 | ErrTooLarge = errors.New("synapse: message body too large") 42 | ) 43 | 44 | // Dial creates a new client by dialing 45 | // the provided network and remote address. 46 | // The provided timeout is used as the timeout 47 | // for requests, in milliseconds. 48 | func Dial(network string, raddr string, timeout time.Duration) (*Client, error) { 49 | conn, err := net.Dial(network, raddr) 50 | if err != nil { 51 | return nil, err 52 | } 53 | return NewClient(conn, timeout) 54 | } 55 | 56 | // DialTLS acts identically to Dial, except that it dials the connection 57 | // over TLS using the provided *tls.Config. 58 | func DialTLS(network, raddr string, timeout time.Duration, config *tls.Config) (*Client, error) { 59 | conn, err := tls.Dial(network, raddr, config) 60 | if err != nil { 61 | return nil, err 62 | } 63 | return NewClient(conn, timeout) 64 | } 65 | 66 | // NewClient creates a new client from an 67 | // existing net.Conn. Timeout is the maximum time, 68 | // in milliseconds, to wait for server responses 69 | // before sending an error to the caller. NewClient 70 | // fails with an error if it cannot ping the server 71 | // over the connection. 72 | func NewClient(c net.Conn, timeout time.Duration) (*Client, error) { 73 | cl := &Client{ 74 | conn: c, 75 | writing: make(chan *waiter, waiterHWM), 76 | done: make(chan struct{}), 77 | state: clientOpen, 78 | } 79 | go cl.readLoop() 80 | go cl.writeLoop() 81 | go cl.timeoutLoop(timeout) 82 | 83 | // do a ping to check 84 | // for sanity 85 | err := cl.ping() 86 | if err != nil { 87 | cl.Close() 88 | return nil, fmt.Errorf("synapse: ping failed: %s", err) 89 | } 90 | 91 | return cl, nil 92 | } 93 | 94 | // Client is a client to 95 | // a single synapse server. 96 | type Client struct { 97 | conn net.Conn // connection 98 | wlock sync.Mutex // write lock 99 | csn uint64 // sequence number; atomic 100 | writing chan *waiter // queue to write to conn; size is effectively HWM 101 | done chan struct{} // closed during (*Client).Close to shut down timeoutloop 102 | wg sync.WaitGroup // outstanding client procs 103 | state uint32 // open, closed, etc. 104 | pending wMap // map seq number to waiting handler 105 | } 106 | 107 | // used to transfer control 108 | // flow to blocking goroutines 109 | type waiter struct { 110 | next *waiter // next in linked list, or nil 111 | parent *Client // parent *client 112 | seq uint64 // sequence number 113 | done sema.Point // for notifying response 114 | err error // response error on wakeup, if applicable 115 | in []byte // response body 116 | reap bool // can reap for timeout 117 | static bool // is part of the statically allocated arena 118 | } 119 | 120 | // Close idempotently closes the 121 | // client's connection to the server. 122 | // Close returns once every waiting 123 | // goroutine has either received a 124 | // response or timed out; goroutines 125 | // with requests in progress are not interrupted. 126 | func (c *Client) Close() error { 127 | // there can only be one winner of the Close() race 128 | if !atomic.CompareAndSwapUint32(&c.state, clientOpen, clientClosed) { 129 | return ErrClosed 130 | } 131 | 132 | c.wg.Wait() 133 | close(c.done) 134 | close(c.writing) 135 | return c.conn.Close() 136 | } 137 | 138 | // close with error 139 | // sets the status of every waiting 140 | // goroutine to 'err' and unblocks it. 141 | func (c *Client) closeError(err error) { 142 | if !atomic.CompareAndSwapUint32(&c.state, clientOpen, clientClosed) { 143 | return 144 | } 145 | 146 | err = fmt.Errorf("synapse: fatal error: %s", err) 147 | 148 | // we can't actually guarantee that we will preempt 149 | // every goroutine, but we can try. 150 | c.pending.flush(err) 151 | for c.pending.length() > 0 { 152 | c.pending.flush(err) 153 | } 154 | 155 | c.wg.Wait() 156 | close(c.done) 157 | close(c.writing) 158 | c.conn.Close() 159 | } 160 | 161 | // a handler for io.Read() and io.Write(), 162 | // e.g. 163 | // if !c.do(w.Write(data)) { goto fail } 164 | func (c *Client) do(_ int, err error) bool { 165 | if err != nil { 166 | c.closeError(err) 167 | return false 168 | } 169 | return true 170 | } 171 | 172 | // readLoop continuously polls 173 | // the connection for server 174 | // responses. responses are then 175 | // filled into the appropriate 176 | // waiter's input buffer. it returns 177 | // on the first error returned by Read() 178 | func (c *Client) readLoop() { 179 | var seq uint64 180 | var sz int 181 | var frame fType 182 | var lead [leadSize]byte 183 | bwr := fwd.NewReaderSize(c.conn, 4096) 184 | 185 | for { 186 | if !c.do(bwr.ReadFull(lead[:])) { 187 | return 188 | } 189 | 190 | seq, frame, sz = readFrame(lead) 191 | 192 | // only accept fCMD and fRES frames; 193 | // they are routed to waiters 194 | // precisely the same way 195 | if frame != fCMD && frame != fRES { 196 | // ignore 197 | if !c.do(bwr.Skip(sz)) { 198 | return 199 | } 200 | continue 201 | } 202 | 203 | w := c.pending.remove(seq) 204 | if w == nil { 205 | if !c.do(bwr.Skip(sz)) { 206 | return 207 | } 208 | continue 209 | } 210 | 211 | // fill the waiters input 212 | // buffer and then notify 213 | if cap(w.in) >= sz { 214 | w.in = w.in[:sz] 215 | } else { 216 | w.in = make([]byte, sz) 217 | } 218 | 219 | if !c.do(bwr.ReadFull(w.in)) { 220 | return 221 | } 222 | 223 | // wakeup waiter w/ 224 | // error from last 225 | // read call (usually nil) 226 | w.err = nil 227 | sema.Wake(&w.done) 228 | } 229 | } 230 | 231 | // once every 'msec' milliseconds, reap 232 | // every pending item with reap=true, and 233 | // set all others to reap=true. 234 | // 235 | // NOTE(pmh): this doesn't actually guarantee 236 | // de-queing after the given duration; 237 | // rather, it limits max wait time to 2*msec. 238 | func (c *Client) timeoutLoop(d time.Duration) { 239 | tick := time.Tick(d) 240 | for { 241 | select { 242 | case <-c.done: 243 | return 244 | case <-tick: 245 | c.pending.reap() 246 | } 247 | } 248 | } 249 | 250 | func (c *Client) writeLoop() { 251 | bwr := fwd.NewWriterSize(c.conn, 4096) 252 | 253 | for { 254 | wt, ok := <-c.writing 255 | if !ok { 256 | // this is the "normal" 257 | // exit point for this 258 | // goroutine. 259 | return 260 | } 261 | if !c.do(bwr.Write(wt.in)) { 262 | return 263 | } 264 | more: 265 | select { 266 | case another, ok := <-c.writing: 267 | if ok { 268 | if !c.do(bwr.Write(another.in)) { 269 | return 270 | } 271 | goto more 272 | } else { 273 | bwr.Flush() 274 | return 275 | } 276 | default: 277 | if !c.do(0, bwr.Flush()) { 278 | return 279 | } 280 | } 281 | } 282 | } 283 | 284 | // write a command to the connection - works 285 | // similarly to standard write() 286 | func (w *waiter) writeCommand(cmd command, msg []byte) error { 287 | w.parent.wg.Add(1) 288 | if atomic.LoadUint32(&w.parent.state) == clientClosed { 289 | return ErrClosed 290 | } 291 | seqn := atomic.AddUint64(&w.parent.csn, 1) 292 | 293 | cmdlen := len(msg) + 1 294 | if cmdlen > maxMessageSize { 295 | return ErrTooLarge 296 | } 297 | 298 | // write frame + message 299 | need := leadSize + cmdlen 300 | if cap(w.in) >= need { 301 | w.in = w.in[:need] 302 | } else { 303 | w.in = make([]byte, need) 304 | } 305 | putFrame(w.in, seqn, fCMD, cmdlen) 306 | w.in[leadSize] = byte(cmd) 307 | copy(w.in[leadSize+1:], msg) 308 | 309 | w.writebody(seqn) 310 | return nil 311 | } 312 | 313 | func (w *waiter) writebody(seq uint64) { 314 | w.seq = seq 315 | w.reap = false 316 | p := w.parent 317 | p.pending.insert(w) 318 | p.writing <- w 319 | } 320 | 321 | func (w *waiter) write(method Method, in msgp.Marshaler) error { 322 | w.parent.wg.Add(1) 323 | if atomic.LoadUint32(&w.parent.state) == clientClosed { 324 | return ErrClosed 325 | } 326 | sn := atomic.AddUint64(&w.parent.csn, 1) 327 | 328 | var err error 329 | 330 | // save bytes up front 331 | if cap(w.in) < leadSize { 332 | w.in = make([]byte, leadSize, 256) 333 | } else { 334 | w.in = w.in[:leadSize] 335 | } 336 | 337 | // write body 338 | w.in = msgp.AppendUint32(w.in, uint32(method)) 339 | // handle nil body 340 | if in != nil { 341 | w.in, err = in.MarshalMsg(w.in) 342 | if err != nil { 343 | return err 344 | } 345 | } else { 346 | w.in = msgp.AppendMapHeader(w.in, 0) 347 | } 348 | 349 | // raw request body 350 | olen := len(w.in) - leadSize 351 | 352 | if olen > maxMessageSize { 353 | return ErrTooLarge 354 | } 355 | 356 | putFrame(w.in, sn, fREQ, olen) 357 | 358 | w.writebody(sn) 359 | return nil 360 | } 361 | 362 | func (w *waiter) read(out msgp.Unmarshaler) error { 363 | code, body, err := msgp.ReadIntBytes(w.in) 364 | if err != nil { 365 | return err 366 | } 367 | if Status(code) != StatusOK { 368 | str, _, err := msgp.ReadStringBytes(w.in) 369 | if err != nil { 370 | str = "" 371 | } 372 | return &ResponseError{Code: Status(code), Expl: str} 373 | } 374 | if out != nil { 375 | _, err = out.UnmarshalMsg(body) 376 | } 377 | return err 378 | } 379 | 380 | func (w *waiter) call(method Method, in msgp.Marshaler, out msgp.Unmarshaler) error { 381 | err := w.write(method, in) 382 | if err != nil { 383 | w.parent.wg.Done() 384 | return err 385 | } 386 | // wait for response 387 | sema.Wait(&w.done) 388 | w.parent.wg.Done() 389 | if w.err != nil { 390 | return w.err 391 | } 392 | return w.read(out) 393 | } 394 | 395 | // Call sends a request to the server with 'in' as the body, 396 | // and then decodes the response into 'out'. Call is safe 397 | // to call from multiple goroutines simultaneously. 398 | func (c *Client) Call(method Method, in msgp.Marshaler, out msgp.Unmarshaler) error { 399 | w := waiters.pop(c) 400 | err := w.call(method, in, out) 401 | waiters.push(w) 402 | return err 403 | } 404 | 405 | // errors specific to commands 406 | var ( 407 | errNoCmd = errors.New("synapse: no response CMD code") 408 | errInvalidCmd = errors.New("synapse: invalid command") 409 | errUnknownCmd = errors.New("synapse: unknown command") 410 | ) 411 | 412 | // doCommand executes a command from the client 413 | func (c *Client) sendCommand(cmd command, msg []byte) error { 414 | w := waiters.pop(c) 415 | err := w.writeCommand(cmd, msg) 416 | if err != nil { 417 | c.wg.Done() 418 | return err 419 | } 420 | 421 | // wait 422 | sema.Wait(&w.done) 423 | c.wg.Done() 424 | if w.err != nil { 425 | return w.err 426 | } 427 | 428 | // bad response 429 | if len(w.in) == 0 { 430 | waiters.push(w) 431 | return errNoCmd 432 | } 433 | 434 | ret := command(w.in[0]) 435 | 436 | if ret == cmdInvalid || ret >= _maxcommand { 437 | waiters.push(w) 438 | return errInvalidCmd 439 | } 440 | 441 | act := cmdDirectory[ret] 442 | if act == nil { 443 | waiters.push(w) 444 | return errUnknownCmd 445 | } 446 | 447 | act.Client(c, w.in[1:]) 448 | waiters.push(w) 449 | return nil 450 | } 451 | 452 | // perform the ping command; 453 | // returns an error if the server 454 | // didn't respond appropriately 455 | func (c *Client) ping() error { 456 | return c.sendCommand(cmdPing, nil) 457 | } 458 | --------------------------------------------------------------------------------