├── go.mod ├── .github └── workflows │ ├── test.yaml │ └── coverage.yml ├── go.sum ├── LICENSE ├── README.md ├── main.go └── main_test.go /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/davidscottmills/pubsub 2 | 3 | go 1.14 4 | 5 | require github.com/stretchr/testify v1.6.1 6 | -------------------------------------------------------------------------------- /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - master 5 | name: test and build 6 | jobs: 7 | test: 8 | strategy: 9 | matrix: 10 | go-version: [1.14.x, 1.15.x] 11 | os: [ubuntu-latest] 12 | runs-on: ${{ matrix.os }} 13 | steps: 14 | - name: Install Go 15 | uses: actions/setup-go@v2 16 | with: 17 | go-version: ${{ matrix.go-version }} 18 | - name: Checkout code 19 | uses: actions/checkout@v2 20 | - name: Test 21 | run: go test -v -race ./... 22 | -------------------------------------------------------------------------------- /.github/workflows/coverage.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - master 5 | name: coverage 6 | jobs: 7 | coverage: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Install Go 11 | if: success() 12 | uses: actions/setup-go@v2 13 | with: 14 | go-version: 1.14.x 15 | - name: Checkout code 16 | uses: actions/checkout@v2 17 | - name: Calc coverage 18 | run: go test -v -covermode=count -coverprofile=coverage.out 19 | - name: Convert coverage to lcov 20 | uses: jandelgado/gcov2lcov-action@v1.0.0 21 | with: 22 | infile: coverage.out 23 | outfile: coverage.lcov 24 | - name: Coveralls 25 | uses: coverallsapp/github-action@v1.1.2 26 | with: 27 | github-token: ${{ secrets.github_token }} 28 | path-to-lcov: coverage.lcov 29 | -------------------------------------------------------------------------------- /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 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4= 6 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 7 | github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= 8 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 9 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 10 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 11 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= 12 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 davidscottmills 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PubSub 2 | 3 | PubSub is a Go Package for in-memory publish/subscribe. 4 | 5 | [![test and build](https://github.com/davidscottmills/pubsub/workflows/test%20and%20build/badge.svg)](https://github.com/davidscottmills/pubsub/actions?query=workflow%3A%22test+and+build%22) 6 | [![Coverage Status](https://coveralls.io/repos/github/davidscottmills/pubsub/badge.svg)](https://coveralls.io/github/davidscottmills/pubsub) 7 | [![Go Report Card](https://goreportcard.com/badge/github.com/davidscottmills/pubsub)](https://goreportcard.com/report/github.com/davidscottmills/pubsub) 8 | [![Documentation](https://godoc.org/github.com/davidscottmills/pubsub?status.svg)](http://godoc.org/github.com/davidscottmills/pubsub) 9 | [![GitHub issues](https://img.shields.io/github/issues/davidscottmills/pubsub.svg)](https://github.com/davidscottmills/pubsub/issues) 10 | [![license](https://img.shields.io/github/license/davidscottmills/pubsub.svg?maxAge=2592000)](https://github.com/davidscottmills/pubsub/LICENSE.md) 11 | [![GitHub go.mod Go version of a Go module](https://img.shields.io/github/go-mod/go-version/davidscottmills/pubsub.svg)](https://github.com/davidscottmills/pubsub) 12 | 13 | ## Installation 14 | 15 | ```bash 16 | go get github.com/davidscottmills/pubsub 17 | ``` 18 | 19 | ## Usage 20 | 21 | ```go 22 | package main() 23 | 24 | import ( 25 | "fmt" 26 | "github.com/davidscottmills/pubsub" 27 | ) 28 | 29 | var ps *PubSub 30 | 31 | func main() { 32 | ps := NewPubSub() 33 | 34 | handerFunc := func(m *pubsub.Msg) { 35 | fmt.Println(m.Data) 36 | } 37 | 38 | // Subscribe to a subject 39 | s, err := ps.Subscribe("subject.name", handerFunc) 40 | defer s.Unsubscribe() 41 | 42 | // Publish 43 | err := ps.Publish("subject.name", "Hello, world!") 44 | 45 | someStruct := struct{}{} 46 | 47 | // Publish any type you'd like 48 | ps.Publish("subject.name", someStruct) 49 | 50 | runAndBlock() 51 | } 52 | ``` 53 | 54 | ## Subject Routing 55 | 56 | Subject naming matches that of NATS. 57 | 58 | ### Subject Names 59 | 60 | - All ascii alphanumeric characters except spaces/tabs and separators which are "." and ">" are allowed. Subject names can be optionally token-delimited using the dot character (.) 61 | - Subjects are case sensative 62 | 63 | ### Subject Wildcards 64 | - The asterisk character (*) matches a single token at any level of the subject. 65 | - The greater than symbol (>), also known as the full wildcard, matches one or more tokens at the tail of a subject, and must be the last token. The wildcarded subject foo.> will match foo.bar or foo.bar.baz.1, but not foo. 66 | - Wildcards must be a separate token (foo.*.baz or foo.> are syntactically valid; foo*.bar, f*o.b*r and foo> are not) 67 | 68 | ## TODO 69 | 70 | - Implement and test close or drain on PubSub 71 | - Implement and test errors 72 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | // Package pubsub provides a light weight in-memory way of doing subject based publish-subscribe. 2 | package pubsub 3 | 4 | import ( 5 | "errors" 6 | "strings" 7 | "sync" 8 | "unicode" 9 | ) 10 | 11 | // A PubSub represents the client that mangages subscribing, publishing and routing of messages to subject handlers. 12 | type PubSub struct { 13 | mu *sync.RWMutex 14 | subscriptions map[int]*Subscription 15 | ssid int 16 | } 17 | 18 | // A Subscription is a subscription to a specific subject. 19 | type Subscription struct { 20 | mu *sync.Mutex 21 | sid int 22 | subject string 23 | ps *PubSub 24 | mh MsgHandler 25 | mch chan *Msg //message channel 26 | uch chan bool //unsubscribe channel 27 | } 28 | 29 | // A Msg is a message that is to be handled by subscribers when data is published to a subject. 30 | type Msg struct { 31 | subject string 32 | // The data that the message is meant for a handler to use. 33 | Data interface{} 34 | } 35 | 36 | // A MsgHandler is a function that subject subscribers must pass to handle messages. 37 | type MsgHandler func(m *Msg) 38 | 39 | var ( 40 | //ErrInvalidSubject is returned when a subject name is invalid 41 | ErrInvalidSubject = errors.New("Subject name was invalid") 42 | ) 43 | 44 | // NewPubSub instantiates a new PubSub client. 45 | func NewPubSub() *PubSub { 46 | subs := make(map[int]*Subscription) 47 | ps := PubSub{mu: &sync.RWMutex{}, subscriptions: subs, ssid: 0} 48 | return &ps 49 | } 50 | 51 | // Subscribe subscribes to a subject. 52 | // subject - The subject you want to subscribe to. 53 | // mh - The message handler. 54 | // args - Number of allowed concurrent go routines. Default is not to throttle. 55 | // Returns ErrInvalidSubject if the subject is invalid 56 | func (ps *PubSub) Subscribe(subject string, mh MsgHandler) (*Subscription, error) { 57 | err := validateSubject(subject) 58 | if err != nil { 59 | return nil, err 60 | } 61 | ps.mu.Lock() 62 | s := newSubscription(ps.ssid, subject, ps, mh) 63 | ps.subscriptions[ps.ssid] = s 64 | ps.ssid++ 65 | go ps.subListen(s) 66 | ps.mu.Unlock() 67 | return s, nil 68 | } 69 | 70 | // Unsubscribe unsubscribes from a subject. 71 | func (s *Subscription) Unsubscribe() { 72 | s.ps.mu.Lock() 73 | s.uch <- true 74 | delete(s.ps.subscriptions, s.sid) 75 | s.ps.mu.Unlock() 76 | } 77 | 78 | // Publish publishes data to a subject. 79 | // subject - the subject you want to pass the data to 80 | // data - the data you want to pass 81 | // Returns ErrInvalidSubject if the subject is invalid 82 | func (ps *PubSub) Publish(subject string, data interface{}) error { 83 | err := validateSubject(subject) 84 | if err != nil { 85 | return err 86 | } 87 | msg := newMessage(subject, data) 88 | ps.mu.RLock() 89 | for _, s := range ps.subscriptions { 90 | if subjectMatches(s.subject, msg.subject) { 91 | s.mch <- msg 92 | } 93 | } 94 | ps.mu.RUnlock() 95 | return nil 96 | } 97 | 98 | func (ps *PubSub) subListen(s *Subscription) { 99 | L: 100 | for { 101 | select { 102 | case msg := <-s.mch: 103 | s.mh(msg) 104 | case <-s.uch: 105 | break L 106 | } 107 | } 108 | } 109 | 110 | func newMessage(subject string, data interface{}) *Msg { 111 | return &Msg{subject: subject, Data: data} 112 | } 113 | 114 | func newSubscription(sid int, subject string, ps *PubSub, mh MsgHandler) *Subscription { 115 | nmh := newMessageHandlerWrapper(mh) 116 | 117 | return &Subscription{mu: &sync.Mutex{}, sid: sid, subject: subject, ps: ps, mh: nmh, mch: make(chan *Msg), uch: make(chan bool)} 118 | } 119 | 120 | func newMessageHandlerWrapper(mh MsgHandler) MsgHandler { 121 | nmh := func(m *Msg) { 122 | go mh(m) 123 | } 124 | 125 | return nmh 126 | } 127 | 128 | func subjectMatches(subscribeSubject, msgSubject string) bool { 129 | if subscribeSubject == msgSubject { 130 | return true 131 | } 132 | 133 | subjectTokens := strings.Split(subscribeSubject, ".") 134 | msgSubjectTokens := strings.Split(msgSubject, ".") 135 | 136 | subTokensLen := len(subjectTokens) 137 | 138 | if subTokensLen > len(msgSubjectTokens) { 139 | return false 140 | } 141 | 142 | for i, token := range msgSubjectTokens { 143 | if (i+1 == subTokensLen || i+1 > subTokensLen) && subjectTokens[subTokensLen-1] == ">" { 144 | return true 145 | } 146 | 147 | if i+1 > subTokensLen { 148 | return false 149 | } 150 | subjectToken := subjectTokens[i] 151 | 152 | if subjectToken == "*" { 153 | continue 154 | } 155 | 156 | if token != subjectToken { 157 | return false 158 | } 159 | } 160 | 161 | return true 162 | } 163 | 164 | func validateSubject(subject string) error { 165 | if subject == "" { 166 | return ErrInvalidSubject 167 | } 168 | tokens := strings.Split(subject, ".") 169 | lenTokens := len(tokens) 170 | for i, token := range tokens { 171 | if token == ">" && i+1 != lenTokens { 172 | return ErrInvalidSubject 173 | } 174 | 175 | if token == "*" || token == ">" { 176 | continue 177 | } 178 | 179 | if !isASCII(token) { 180 | return ErrInvalidSubject 181 | } 182 | 183 | if strings.Contains(token, "*") || strings.Contains(token, ">") || strings.Contains(token, " ") { 184 | return ErrInvalidSubject 185 | } 186 | } 187 | return nil 188 | } 189 | 190 | func isASCII(s string) bool { 191 | for i := 0; i < len(s); i++ { 192 | if s[i] > unicode.MaxASCII { 193 | return false 194 | } 195 | } 196 | return true 197 | } 198 | -------------------------------------------------------------------------------- /main_test.go: -------------------------------------------------------------------------------- 1 | package pubsub 2 | 3 | import ( 4 | "strconv" 5 | "testing" 6 | "time" 7 | 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func Test_PubSub_Subscribe_Invalid_Subject(t *testing.T) { 12 | mh := func(m *Msg) {} 13 | ps := NewPubSub() 14 | _, err := ps.Subscribe("test.>.test", mh) 15 | require.Equal(t, ErrInvalidSubject, err) 16 | } 17 | 18 | func Test_PubSub_Publish_Invalid_Subject(t *testing.T) { 19 | ps := NewPubSub() 20 | err := ps.Publish("test.>.test", "some message") 21 | require.Equal(t, ErrInvalidSubject, err) 22 | } 23 | 24 | func Test_PubSub_Subscribe_Unsubscribe(t *testing.T) { 25 | mh := func(m *Msg) {} 26 | ps := NewPubSub() 27 | 28 | s1, _ := ps.Subscribe("test.test", mh) 29 | require.Same(t, ps, s1.ps) 30 | require.Equal(t, 1, len(ps.subscriptions)) 31 | 32 | s2, _ := ps.Subscribe("test.test", mh) 33 | require.Same(t, ps, s2.ps) 34 | require.Equal(t, 2, len(ps.subscriptions)) 35 | require.NotEqual(t, s1.sid, s2.sid) 36 | 37 | s1.Unsubscribe() 38 | require.Equal(t, 1, len(ps.subscriptions)) 39 | 40 | s2.Unsubscribe() 41 | require.Equal(t, 0, len(ps.subscriptions)) 42 | } 43 | 44 | func Test_PubSub_Listen(t *testing.T) { 45 | mh3called := false 46 | mhch1, mhch2 := make(chan bool), make(chan bool) 47 | mh1 := func(m *Msg) { 48 | mhch1 <- true 49 | } 50 | mh2 := func(m *Msg) { 51 | mhch2 <- true 52 | } 53 | mh3 := func(m *Msg) { mh3called = true } 54 | 55 | subject := "test.test" 56 | ps := NewPubSub() 57 | 58 | s1, _ := ps.Subscribe(subject, mh1) 59 | defer s1.Unsubscribe() 60 | s2, _ := ps.Subscribe(subject, mh2) 61 | defer s2.Unsubscribe() 62 | s3, _ := ps.Subscribe("not.test", mh3) 63 | defer s3.Unsubscribe() 64 | ps.Publish(subject, "Hello, world!") 65 | 66 | for i := 0; i < 2; i++ { 67 | select { 68 | case <-mhch1: 69 | continue 70 | case <-mhch2: 71 | continue 72 | } 73 | } 74 | 75 | require.False(t, mh3called) 76 | } 77 | 78 | func Test_PubSub_Listen_Unsubscribe_Publish(t *testing.T) { 79 | mh2called := false 80 | mhch1 := make(chan bool) 81 | mh1 := func(m *Msg) { 82 | mhch1 <- true 83 | } 84 | mh2 := func(m *Msg) { mh2called = true } 85 | 86 | subject := "test.test" 87 | ps := NewPubSub() 88 | s1, _ := ps.Subscribe(subject, mh1) 89 | defer s1.Unsubscribe() 90 | s2, _ := ps.Subscribe(subject, mh2) 91 | s2.Unsubscribe() 92 | ps.Publish(subject, "Hello, world!") 93 | 94 | <-mhch1 95 | 96 | require.False(t, mh2called) 97 | } 98 | 99 | func Test_PubSub_Listen_Publish_Unsubscribe(t *testing.T) { 100 | mhch1, mhch2 := make(chan bool), make(chan bool) 101 | mh1 := func(m *Msg) { mhch1 <- true } 102 | mh2 := func(m *Msg) { mhch2 <- true } 103 | 104 | subject := "test.test" 105 | ps := NewPubSub() 106 | s1, _ := ps.Subscribe(subject, mh1) 107 | defer s1.Unsubscribe() 108 | s2, _ := ps.Subscribe(subject, mh2) 109 | // If we publish before unsubscribe is called, 110 | // we should expect that the subscriber will receive the message. 111 | ps.Publish(subject, "Hello, world!") 112 | go s2.Unsubscribe() 113 | 114 | for i := 0; i < 2; i++ { 115 | select { 116 | case <-mhch1: 117 | case <-mhch2: 118 | } 119 | } 120 | } 121 | 122 | func Test_PubSub_Multiple_Messages(t *testing.T) { 123 | mhch1 := make(chan bool) 124 | mh1 := func(m *Msg) { 125 | mhch1 <- true 126 | time.Sleep(2 * time.Second) 127 | } 128 | 129 | subject := "test.test" 130 | ps := NewPubSub() 131 | s1, _ := ps.Subscribe(subject, mh1) 132 | defer s1.Unsubscribe() 133 | for i := 0; i < 100; i++ { 134 | ps.Publish(subject, "Hello, world!") 135 | } 136 | 137 | for i := 0; i < 100; i++ { 138 | <-mhch1 139 | } 140 | } 141 | 142 | func Test_subjectMatches_Wildcard_Routing(t *testing.T) { 143 | testData := []struct { 144 | testCaseID int 145 | subscribeSubject string 146 | msgSubject string 147 | expected bool 148 | }{ 149 | { 150 | testCaseID: 1, 151 | subscribeSubject: "foo.bar", 152 | msgSubject: "foo.bar", 153 | expected: true, 154 | }, 155 | { 156 | testCaseID: 2, 157 | subscribeSubject: ">", 158 | msgSubject: "foo.bar", 159 | expected: true, 160 | }, 161 | { 162 | testCaseID: 3, 163 | subscribeSubject: "foo.>", 164 | msgSubject: "foo.bar", 165 | expected: true, 166 | }, 167 | { 168 | testCaseID: 4, 169 | subscribeSubject: "foo.>", 170 | msgSubject: "foo.bar.1234", 171 | expected: true, 172 | }, 173 | { 174 | testCaseID: 5, 175 | subscribeSubject: "foo.bar.*", 176 | msgSubject: "foo.bar.1234", 177 | expected: true, 178 | }, 179 | { 180 | testCaseID: 6, 181 | subscribeSubject: "foo.*.bar", 182 | msgSubject: "foo.bar.bar", 183 | expected: true, 184 | }, 185 | { 186 | testCaseID: 7, 187 | subscribeSubject: "*.bar.bar", 188 | msgSubject: "foo.bar.bar", 189 | expected: true, 190 | }, 191 | { 192 | testCaseID: 8, 193 | subscribeSubject: "foo.*.>", 194 | msgSubject: "foo.bar.baz.1234", 195 | expected: true, 196 | }, 197 | { 198 | testCaseID: 9, 199 | subscribeSubject: "foo", 200 | msgSubject: "bar", 201 | expected: false, 202 | }, 203 | { 204 | testCaseID: 10, 205 | subscribeSubject: "foo.*", 206 | msgSubject: "food.bar.baz", 207 | expected: false, 208 | }, 209 | { 210 | testCaseID: 11, 211 | subscribeSubject: "foo.>", 212 | msgSubject: "food.bar.baz", 213 | expected: false, 214 | }, 215 | { 216 | testCaseID: 12, 217 | subscribeSubject: "foo.bar", 218 | msgSubject: "food.bar.baz", 219 | expected: false, 220 | }, 221 | { 222 | testCaseID: 13, 223 | subscribeSubject: "foo.*.*.>", 224 | msgSubject: "foo.bar.bar", 225 | expected: false, 226 | }, 227 | { 228 | testCaseID: 14, 229 | subscribeSubject: "foo.*.*.>", 230 | msgSubject: "foo.bar.bar.baz.12345", 231 | expected: true, 232 | }, 233 | { 234 | testCaseID: 15, 235 | subscribeSubject: "foo.*.bar.*", 236 | msgSubject: "foo.buzz.bar.fizz", 237 | expected: true, 238 | }, 239 | { 240 | testCaseID: 16, 241 | subscribeSubject: "foo.*.*.fizz.>", 242 | msgSubject: "foo.buzz.bar.fizz.12345", 243 | expected: true, 244 | }, 245 | { 246 | testCaseID: 17, 247 | subscribeSubject: "foo.*.*.*", 248 | msgSubject: "foo.bar.baz.buzz.bizz", 249 | expected: false, 250 | }, 251 | } 252 | 253 | for _, td := range testData { 254 | result := subjectMatches(td.subscribeSubject, td.msgSubject) 255 | if result != td.expected { 256 | t.Logf("TestCaseID: %d, Expected %t, got %t", td.testCaseID, td.expected, result) 257 | t.Fail() 258 | } 259 | } 260 | } 261 | 262 | func Test_validateSubject(t *testing.T) { 263 | testData := []struct { 264 | testCaseID int 265 | subject string 266 | expectErr bool 267 | }{ 268 | { 269 | testCaseID: 1, 270 | subject: ">", 271 | expectErr: false, 272 | }, 273 | { 274 | testCaseID: 2, 275 | subject: "*", 276 | expectErr: false, 277 | }, 278 | { 279 | testCaseID: 3, 280 | subject: "*.*", 281 | expectErr: false, 282 | }, 283 | { 284 | testCaseID: 4, 285 | subject: "foo.>", 286 | expectErr: false, 287 | }, 288 | { 289 | testCaseID: 5, 290 | subject: "foo.bar.*", 291 | expectErr: false, 292 | }, 293 | { 294 | testCaseID: 6, 295 | subject: "foo.*.bar", 296 | expectErr: false, 297 | }, 298 | { 299 | testCaseID: 7, 300 | subject: "*.bar.bar", 301 | expectErr: false, 302 | }, 303 | { 304 | testCaseID: 8, 305 | subject: "foo.*.>", 306 | expectErr: false, 307 | }, 308 | { 309 | testCaseID: 9, 310 | subject: "foo", 311 | expectErr: false, 312 | }, 313 | { 314 | testCaseID: 10, 315 | subject: "foo.bar", 316 | expectErr: false, 317 | }, 318 | { 319 | testCaseID: 12, 320 | subject: "fo*o", 321 | expectErr: true, 322 | }, 323 | { 324 | testCaseID: 13, 325 | subject: "fo>o", 326 | expectErr: true, 327 | }, 328 | { 329 | testCaseID: 14, 330 | subject: "fo o", 331 | expectErr: true, 332 | }, 333 | { 334 | testCaseID: 15, 335 | subject: "", 336 | expectErr: true, 337 | }, 338 | { 339 | testCaseID: 16, 340 | subject: "foo.bar.>.foo", 341 | expectErr: true, 342 | }, 343 | { 344 | testCaseID: 17, 345 | subject: "भारत.bar..foo", 346 | expectErr: true, 347 | }, 348 | } 349 | 350 | for _, td := range testData { 351 | err := validateSubject(td.subject) 352 | 353 | if td.expectErr && err == nil { 354 | t.Logf("TestCaseID: %d, Expected error but did not get one", td.testCaseID) 355 | t.Fail() 356 | } 357 | 358 | if !td.expectErr && err != nil { 359 | t.Logf("TestCaseID: %d, Did not expect error, but got one.", td.testCaseID) 360 | t.Fail() 361 | } 362 | } 363 | } 364 | 365 | func Benchmark_HelloWorld_TenSubscriptions_OneMessagesPerSubscription(b *testing.B) { 366 | // Setup 367 | ps := NewPubSub() 368 | mch := make(chan bool) 369 | 370 | subs := []string{} 371 | for i := 0; i < 10; i++ { 372 | sub := "sub." + strconv.Itoa(i) 373 | mh := func(m *Msg) { mch <- true } 374 | s, _ := ps.Subscribe(sub, mh) 375 | defer s.Unsubscribe() 376 | subs = append(subs, sub) 377 | } 378 | nmps := 100 379 | 380 | // Start the test 381 | b.ResetTimer() 382 | for i := 0; i < nmps; i++ { 383 | for _, sub := range subs { 384 | ps.Publish(sub, "Hello World!") 385 | } 386 | } 387 | 388 | // Ensure all go routines complete 389 | for i := 0; i < nmps*len(subs); i++ { 390 | <-mch 391 | } 392 | } 393 | 394 | func Benchmark_HelloWorld_TenByTenSubscriptions_OneMessagesPerSubscription(b *testing.B) { 395 | // Setup 396 | ps := NewPubSub() 397 | mch := make(chan bool) 398 | 399 | subs := []string{} 400 | subsPerSub := 10 401 | for i := 0; i < 10; i++ { 402 | sub := "sub." + strconv.Itoa(i) 403 | subs = append(subs, sub) 404 | for j := 0; j < subsPerSub; j++ { 405 | mh := func(m *Msg) { mch <- true } 406 | s, _ := ps.Subscribe(sub, mh) 407 | defer s.Unsubscribe() 408 | } 409 | } 410 | nmps := 100 411 | 412 | // Start the test 413 | b.ResetTimer() 414 | for i := 0; i < nmps; i++ { 415 | for _, sub := range subs { 416 | ps.Publish(sub, "Hello World!") 417 | } 418 | } 419 | 420 | // Ensure all go routines complete 421 | for i := 0; i < nmps*len(subs)*subsPerSub; i++ { 422 | <-mch 423 | } 424 | } 425 | 426 | func Fib(n int) int { 427 | if n < 2 { 428 | return n 429 | } 430 | return Fib(n-1) + Fib(n-2) 431 | } 432 | 433 | func Benchmark_Fibonacci_TenByTenSubscriptions_OneMessagesPerSubscription(b *testing.B) { 434 | // Setup 435 | ps := NewPubSub() 436 | mch := make(chan bool) 437 | 438 | subs := []string{} 439 | subsPerSub := 10 440 | for i := 0; i < 10; i++ { 441 | sub := "sub." + strconv.Itoa(i) 442 | subs = append(subs, sub) 443 | for j := 0; j < subsPerSub; j++ { 444 | mh := func(m *Msg) { 445 | // Try m.Data cast to int 446 | mi, ok := m.Data.(int) 447 | if !ok { 448 | panic("m.Data was not an int") 449 | } 450 | // Do expensive Fibonacci computation 451 | Fib(mi) 452 | mch <- true 453 | } 454 | s, _ := ps.Subscribe(sub, mh) 455 | defer s.Unsubscribe() 456 | } 457 | } 458 | nmps := 100 459 | 460 | // Start the test 461 | b.ResetTimer() 462 | for i := 0; i < nmps; i++ { 463 | for _, sub := range subs { 464 | // Calculating 20th Fibonacci number should be sufficiently difficult 465 | ps.Publish(sub, 20) 466 | } 467 | } 468 | 469 | // Ensure all go routines complete 470 | for i := 0; i < nmps*len(subs)*subsPerSub; i++ { 471 | <-mch 472 | } 473 | } 474 | --------------------------------------------------------------------------------