├── .travis.yml ├── README.md ├── doc.go ├── example_test.go ├── pubsub.go └── pubsub_test.go /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 4 | - "1.10" 5 | - tip 6 | 7 | before_install: 8 | 9 | script: 10 | - go test 11 | # - $HOME/gopath/bin/goveralls -coverprofile=coverage.cov -service=travis-ci 12 | # - bash <(curl -s https://codecov.io/bash) 13 | # - go test -bench=. -benchmem . 14 | #- sh ./install_all_cmd.sh 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Pubsub 2 | ============== 3 | 4 | [![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://raw.githubusercontent.com/kkdai/pubsub/master/LICENSE) [![GoDoc](https://godoc.org/github.com/kkdai/pubsub?status.svg)](https://godoc.org/github.com/kkdai/pubsub) [![Build Status](https://travis-ci.org/kkdai/pubsub.svg?branch=master)](https://travis-ci.org/kkdai/pubsub) 5 | 6 | 7 | 8 | What is Pubsub 9 | ============= 10 | Pubsub is prove of concept implement for [Redis](http://redis.io/) "Pub/Sub" messaging management feature. SUBSCRIBE, UNSUBSCRIBE and PUBLISH implement the Publish/Subscribe messaging paradigm where (citing Wikipedia) senders (publishers) are not programmed to send their messages to specific receivers (subscribers). (sited from [here](http://redis.io/topics/pubsub)) 11 | 12 | 13 | Installation and Usage 14 | ============= 15 | 16 | 17 | Install 18 | --------------- 19 | go get github.com/kkdai/pubsub 20 | 21 | 22 | Usage 23 | --------------- 24 | 25 | ```go 26 | package main 27 | 28 | import ( 29 | "fmt" 30 | 31 | . "github.com/kkdai/pubsub" 32 | ) 33 | 34 | func main() { 35 | ser := NewPubsub(1) 36 | c1 := ser.Subscribe("topic1") 37 | c2 := ser.Subscribe("topic2") 38 | ser.Publish("test1", "topic1") 39 | ser.Publish("test2", "topic2") 40 | fmt.Println(<-c1) 41 | //Got "test1" 42 | fmt.Println(<-c2) 43 | //Got "test2" 44 | 45 | 46 | // Add subscription "topic2" for c1. 47 | ser.AddSubscription(c1, "topic2") 48 | 49 | // Publish new content in topic2 50 | ser.Publish("test3", "topic2") 51 | 52 | fmt.Println(<-c1) 53 | //Got "test3" 54 | 55 | // Remove subscription "topic2" in c1 56 | ser.RemoveSubscription(c1, "topic2") 57 | 58 | // Publish new content in topic2 59 | ser.Publish("test4", "topic2") 60 | 61 | select { 62 | case val := <-c1: 63 | fmt.Printf("Should not get %v notify on remove topic\n", val) 64 | break 65 | case <-time.After(time.Second): 66 | //Will go here, because we remove subscription topic2 in c1. 67 | break 68 | } 69 | } 70 | ``` 71 | 72 | Benchmark 73 | --------------- 74 | 75 | Benchmark include memory usage. 76 | 77 | ``` 78 | BenchmarkAddSub-4 500 2906467 ns/op 1605949 B/op 3 allocs/op 79 | BenchmarkRemoveSub-4 10000 232910 ns/op 174260 B/op 16 allocs/op 80 | BenchmarkBasicFunction-4 5000000 232 ns/op 19 B/op 1 allocs/op 81 | ``` 82 | 83 | Inspired By 84 | --------------- 85 | 86 | 87 | - [Redis: Pubsub](http://redis.io/topics/pubsub) 88 | - [chandru/pubsub](https://github.com/tuxychandru/pubsub) 89 | 90 | 91 | Project52 92 | --------------- 93 | 94 | It is one of my [project 52](https://github.com/kkdai/project52). 95 | 96 | 97 | License 98 | --------------- 99 | 100 | This package is licensed under MIT license. See LICENSE for details. 101 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package Pubsub is a implement for Redis massaging management feature call Pub/Sub. 3 | The interface of this package is experimental and may change. 4 | 5 | */ 6 | package pubsub 7 | -------------------------------------------------------------------------------- /example_test.go: -------------------------------------------------------------------------------- 1 | package pubsub_test 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | . "github.com/kkdai/pubsub" 8 | ) 9 | 10 | func ExamplePubsub() { 11 | ser := NewPubsub(1) 12 | c1 := ser.Subscribe("topic1") 13 | c2 := ser.Subscribe("topic2") 14 | ser.Publish("test1", "topic1") 15 | ser.Publish("test2", "topic2") 16 | fmt.Println(<-c1) 17 | //Got "test1" 18 | fmt.Println(<-c2) 19 | //Got "test2" 20 | 21 | // Add subscription "topic2" for c1. 22 | ser.AddSubscription(c1, "topic2") 23 | 24 | // Publish new content in topic2 25 | ser.Publish("test3", "topic2") 26 | 27 | fmt.Println(<-c1) 28 | //Got "test3" 29 | 30 | // Remove subscription "topic2" in c1 31 | ser.RemoveSubscription(c1, "topic2") 32 | 33 | // Publish new content in topic2 34 | ser.Publish("test4", "topic2") 35 | 36 | select { 37 | case val := <-c1: 38 | fmt.Printf("Should not get %v notify on remove topic\n", val) 39 | break 40 | case <-time.After(time.Second): 41 | //Will go here, because we remove subscription topic2 in c1. 42 | break 43 | } 44 | } 45 | 46 | func ExamplePubsub_Subscribe() { 47 | ser := NewPubsub(1) 48 | //Subscribe topic1 in c1 49 | c1 := ser.Subscribe("topic1") 50 | fmt.Printf("c1 is a channel %v\n", c1) 51 | //Subscribe multiple topic "topic1", "topic2" and "topic3" in c2 52 | c2 := ser.Subscribe("topic1", "topic2", "topic3") 53 | fmt.Printf("c2 is a channel %v\n", c2) 54 | } 55 | 56 | func ExamplePubsub_AddSubscription() { 57 | ser := NewPubsub(1) 58 | //Subscribe topic1 in c1 59 | c1 := ser.Subscribe("topic1") 60 | //Add more Subscription "topic2" and "topic3" for c1 61 | ser.AddSubscription(c1, "topic2", "topic3") 62 | } 63 | -------------------------------------------------------------------------------- /pubsub.go: -------------------------------------------------------------------------------- 1 | package pubsub 2 | 3 | type chanMapStringList map[chan interface{}][]string 4 | type stringMapChanList map[string][]chan interface{} 5 | 6 | // Pubsub struct: Only content a userIndex and accessDB which content a chan map 7 | type Pubsub struct { 8 | //Capacity for each chan buffer 9 | capacity int 10 | 11 | //map to store "chan -> Topic List" for find subscription 12 | clientMapTopics chanMapStringList 13 | //map to store "topic -> chan List" for publish 14 | topicMapClients stringMapChanList 15 | } 16 | 17 | //Sub: Subscribe channels, the channels could be a list of channels name 18 | // The channel name could be any, without define in server 19 | func (p *Pubsub) Subscribe(topics ...string) chan interface{} { 20 | //init new chan using capacity as channel buffer 21 | workChan := make(chan interface{}, p.capacity) 22 | p.updateTopicMapClient(workChan, topics) 23 | return workChan 24 | } 25 | 26 | func (p *Pubsub) updateTopicMapClient(clientChan chan interface{}, topics []string) { 27 | var updateChanList []chan interface{} 28 | for _, topic := range topics { 29 | updateChanList, _ = p.topicMapClients[topic] 30 | updateChanList = append(updateChanList, clientChan) 31 | p.topicMapClients[topic] = updateChanList 32 | } 33 | p.clientMapTopics[clientChan] = topics 34 | } 35 | 36 | //AddSubscription: Add a new topic subscribe to specific client channel. 37 | func (p *Pubsub) AddSubscription(clientChan chan interface{}, topics ...string) { 38 | p.updateTopicMapClient(clientChan, topics) 39 | } 40 | 41 | //RemoveSubscription: Remove sub topic list on specific chan 42 | func (p *Pubsub) RemoveSubscription(clientChan chan interface{}, topics ...string) { 43 | 44 | for _, topic := range topics { 45 | //Remove from topic->chan map 46 | if chanList, ok := p.topicMapClients[topic]; ok { 47 | //remove one client chan in chan List 48 | var updateChanList []chan interface{} 49 | for _, client := range chanList { 50 | if client != clientChan { 51 | updateChanList = append(updateChanList, client) 52 | } 53 | } 54 | p.topicMapClients[topic] = updateChanList 55 | } 56 | 57 | //Remove from chan->topic map 58 | if topicList, ok := p.clientMapTopics[clientChan]; ok { 59 | var updateTopicList []string 60 | for _, updateTopic := range topicList { 61 | if updateTopic != topic { 62 | updateTopicList = append(updateTopicList, topic) 63 | } 64 | } 65 | p.clientMapTopics[clientChan] = updateTopicList 66 | } 67 | } 68 | } 69 | 70 | //Publish: Publish a content to a list of channels 71 | // The content could be any type. 72 | func (p *Pubsub) Publish(content interface{}, topics ...string) { 73 | for _, topic := range topics { 74 | if chanList, ok := p.topicMapClients[topic]; ok { 75 | //Someone has subscribed this topic 76 | for _, channel := range chanList { 77 | channel <- content 78 | } 79 | } 80 | } 81 | } 82 | 83 | // Create a pubsub with expect init size, but the size could be extend. 84 | func NewPubsub(initChanCapacity int) *Pubsub { 85 | initClientMapTopics := make(chanMapStringList) 86 | initTopicMapClients := make(stringMapChanList) 87 | 88 | server := Pubsub{clientMapTopics: initClientMapTopics, topicMapClients: initTopicMapClients} 89 | server.capacity = initChanCapacity 90 | return &server 91 | } 92 | -------------------------------------------------------------------------------- /pubsub_test.go: -------------------------------------------------------------------------------- 1 | package pubsub 2 | 3 | import ( 4 | "log" 5 | "testing" 6 | "time" 7 | ) 8 | 9 | func TestBasicFunction(t *testing.T) { 10 | ser := NewPubsub(1) 11 | c1 := ser.Subscribe("ch1") 12 | ser.Publish("test1", "ch1") 13 | 14 | if _, ok := <-c1; !ok { 15 | t.Error(" Error found on subscribed.\n") 16 | } 17 | } 18 | 19 | func TestTwoSubscribetor(t *testing.T) { 20 | ser := NewPubsub(1) 21 | c1 := ser.Subscribe("ch1") 22 | c2 := ser.Subscribe("ch2") 23 | 24 | ser.Publish("test2", "ch1") 25 | ser.Publish("test1", "ch2") 26 | 27 | val, ok := <-c1 28 | if !ok || val != "test2" { 29 | t.Errorf("Error found \n") 30 | } 31 | 32 | val, ok = <-c2 33 | if !ok || val != "test1" { 34 | t.Errorf("Error found \n") 35 | } 36 | } 37 | 38 | func TestAddSub(t *testing.T) { 39 | ser := NewPubsub(10) 40 | c1 := ser.Subscribe("ch1") 41 | ser.AddSubscription(c1, "ch2") 42 | ser.Publish("test2", "ch2") 43 | 44 | if val, ok := <-c1; !ok { 45 | t.Errorf("error on c1:%v", val) 46 | } 47 | } 48 | 49 | func TestRemoveSub(t *testing.T) { 50 | ser := NewPubsub(10) 51 | c1 := ser.Subscribe("ch1", "ch2") 52 | ser.Publish("test1", "ch2") 53 | 54 | if val, ok := <-c1; !ok { 55 | t.Errorf("error on addsub c1:%v", val) 56 | } 57 | 58 | ser.RemoveSubscription(c1, "ch1") 59 | ser.Publish("test2", "ch1") 60 | 61 | select { 62 | case val := <-c1: 63 | t.Errorf("Should not get %v notify on remove topic\n", val) 64 | break 65 | case <-time.After(time.Second): 66 | break 67 | } 68 | } 69 | 70 | func BenchmarkAddSub(b *testing.B) { 71 | big := NewPubsub(100000) 72 | b.ResetTimer() 73 | for i := 0; i < b.N; i++ { 74 | big.Subscribe("1234567890") 75 | } 76 | } 77 | 78 | func BenchmarkRemoveSub(b *testing.B) { 79 | big := NewPubsub(100000) 80 | var subChans []chan interface{} 81 | b.ResetTimer() 82 | for i := 0; i < b.N; i++ { 83 | c1 := big.Subscribe("1234567890") 84 | subChans = append(subChans, c1) 85 | } 86 | 87 | b.ResetTimer() 88 | for _, v := range subChans { 89 | big.RemoveSubscription(v, "1234567890") 90 | } 91 | } 92 | 93 | func BenchmarkBasicFunction(b *testing.B) { 94 | ser := NewPubsub(1000000) 95 | c1 := ser.Subscribe("ch1") 96 | 97 | for i := 0; i < b.N; i++ { 98 | ser.Publish("test1", "ch1") 99 | 100 | if _, ok := <-c1; !ok { 101 | log.Println(" Error found on subscribed.") 102 | } 103 | } 104 | } 105 | --------------------------------------------------------------------------------