├── .gitignore ├── config_test.go ├── message_test.go ├── docker-compose.yml ├── Makefile ├── message.go ├── Gopkg.lock ├── circle.yml ├── Gopkg.toml ├── LICENSE ├── test.sh ├── listener.go ├── README.md ├── listener_test.go ├── emitter.go ├── emitter_test.go └── config.go /.gitignore: -------------------------------------------------------------------------------- 1 | vendor/** 2 | -------------------------------------------------------------------------------- /config_test.go: -------------------------------------------------------------------------------- 1 | package bus 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestNewEmitterConfig(t *testing.T) { 8 | c := newEmitterConfig(EmitterConfig{}) 9 | 10 | if c == nil { 11 | t.Fail() 12 | } 13 | } 14 | 15 | func TestNewListenerConfig(t *testing.T) { 16 | c := newListenerConfig(ListenerConfig{}) 17 | 18 | if c == nil { 19 | t.Fail() 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /message_test.go: -------------------------------------------------------------------------------- 1 | package bus 2 | 3 | import ( 4 | "encoding/json" 5 | "testing" 6 | ) 7 | 8 | func TestMessageDecodePayload(t *testing.T) { 9 | type event struct{ Name string } 10 | 11 | e := &event{"event"} 12 | payload, err := json.Marshal(e) 13 | if err != nil { 14 | t.Fatal("expected to marshal event") 15 | } 16 | 17 | m := &Message{Payload: payload} 18 | v := &event{} 19 | if err := m.DecodePayload(v); err != nil { 20 | t.Fatalf("expected to decode payload message %s", err) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | services: 3 | nsqlookupd: 4 | image: nsqio/nsq 5 | command: /nsqlookupd 6 | ports: 7 | - "4160:4160" 8 | - "4161:4161" 9 | nsqd: 10 | image: nsqio/nsq 11 | command: /nsqd --lookupd-tcp-address=nsqlookupd:4160 --broadcast-address=localhost --data-path=/data 12 | ports: 13 | - "4150:4150" 14 | - "4151:4151" 15 | nsqadmin: 16 | image: nsqio/nsq 17 | command: /nsqadmin --lookupd-http-address=nsqlookupd:4161 18 | ports: 19 | - "4171:4171" 20 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | NO_COLOR=\033[0m 2 | OK_COLOR=\033[32;01m 3 | ERROR_COLOR=\033[31;01m 4 | WARN_COLOR=\033[33;01m 5 | 6 | IGNORED_PACKAGES := /vendor/ 7 | 8 | .PHONY: all deps test 9 | 10 | all: deps test 11 | 12 | deps: 13 | @echo "$(OK_COLOR)==> Installing dependencies$(NO_COLOR)" 14 | @go get -u github.com/golang/dep/cmd/dep 15 | @dep ensure 16 | 17 | test: 18 | @/bin/sh -c "./test.sh $(allpackages)" 19 | 20 | _allpackages = $(shell ( go list ./... 2>&1 1>&3 | \ 21 | grep -v -e "^$$" $(addprefix -e ,$(IGNORED_PACKAGES)) 1>&2 ) 3>&1 | \ 22 | grep -v -e "^$$" $(addprefix -e ,$(IGNORED_PACKAGES))) 23 | 24 | allpackages = $(if $(__allpackages),,$(eval __allpackages := $$(_allpackages)))$(__allpackages) 25 | -------------------------------------------------------------------------------- /message.go: -------------------------------------------------------------------------------- 1 | package bus 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | nsq "github.com/nsqio/go-nsq" 7 | ) 8 | 9 | // Message carries nsq.Message fields and methods and 10 | // adds extra fields for handling messages internally. 11 | type ( 12 | Message struct { 13 | *nsq.Message 14 | ReplyTo string 15 | Payload []byte 16 | } 17 | ) 18 | 19 | // NewMessage returns a new bus.Message. 20 | func NewMessage(p []byte, r string) *Message { 21 | return &Message{Payload: p, ReplyTo: r} 22 | } 23 | 24 | // DecodePayload deserializes data (as []byte) and creates a new struct passed by parameter. 25 | func (m *Message) DecodePayload(v interface{}) (err error) { 26 | return json.Unmarshal(m.Payload, v) 27 | } 28 | -------------------------------------------------------------------------------- /Gopkg.lock: -------------------------------------------------------------------------------- 1 | # This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'. 2 | 3 | 4 | [[projects]] 5 | branch = "master" 6 | name = "github.com/golang/snappy" 7 | packages = ["."] 8 | revision = "553a641470496b2327abcac10b36396bd98e45c9" 9 | 10 | [[projects]] 11 | name = "github.com/nsqio/go-nsq" 12 | packages = ["."] 13 | revision = "eee57a3ac4174c55924125bb15eeeda8cffb6e6f" 14 | version = "v1.0.7" 15 | 16 | [[projects]] 17 | name = "github.com/sony/gobreaker" 18 | packages = ["."] 19 | revision = "e9556a45379ef1da12e54847edb2fb3d7d566f36" 20 | version = "0.3.0" 21 | 22 | [solve-meta] 23 | analyzer-name = "dep" 24 | analyzer-version = 1 25 | inputs-digest = "f809d552b399c68ffa71039413bfe74a8a2316db0cbb328e9eb4f0f64cfd7cc0" 26 | solver-name = "gps-cdcl" 27 | solver-version = 1 28 | -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | machine: 2 | environment: 3 | IMPORT_PATH: "/home/ubuntu/.go_workspace/src/github.com/rafaeljesus" 4 | APP_PATH: "$IMPORT_PATH/nsq-event-bus" 5 | services: 6 | - docker 7 | 8 | dependencies: 9 | pre: 10 | - wget https://s3.amazonaws.com/bitly-downloads/nsq/nsq-0.3.8.linux-amd64.go1.6.2.tar.gz 11 | - tar xvzf nsq-0.3.8.linux-amd64.go1.6.2.tar.gz 12 | - cp nsq-0.3.8.linux-amd64.go1.6.2/bin/* . 13 | - ./nsqlookupd: 14 | background: true 15 | - ./nsqd --lookupd-tcp-address=127.0.0.1:4160 --broadcast-address=127.0.0.1: 16 | background: true 17 | - go get -x -u github.com/golang/lint/golint 18 | - mkdir -p "$IMPORT_PATH" 19 | cache_directories: 20 | - nsq-0.3.8.linux-amd64.go1.6.2 21 | override: 22 | - ln -sf "$(pwd)" "$APP_PATH" 23 | - cd "$APP_PATH" && make deps 24 | 25 | test: 26 | override: 27 | - cd "$APP_PATH" && make test 28 | -------------------------------------------------------------------------------- /Gopkg.toml: -------------------------------------------------------------------------------- 1 | # Gopkg.toml example 2 | # 3 | # Refer to https://github.com/golang/dep/blob/master/docs/Gopkg.toml.md 4 | # for detailed Gopkg.toml documentation. 5 | # 6 | # required = ["github.com/user/thing/cmd/thing"] 7 | # ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"] 8 | # 9 | # [[constraint]] 10 | # name = "github.com/user/project" 11 | # version = "1.0.0" 12 | # 13 | # [[constraint]] 14 | # name = "github.com/user/project2" 15 | # branch = "dev" 16 | # source = "github.com/myfork/project2" 17 | # 18 | # [[override]] 19 | # name = "github.com/x/y" 20 | # version = "2.4.0" 21 | # 22 | # [prune] 23 | # non-go = false 24 | # go-tests = true 25 | # unused-packages = true 26 | 27 | 28 | [[constraint]] 29 | name = "github.com/nsqio/go-nsq" 30 | version = "1.0.7" 31 | 32 | [[constraint]] 33 | name = "github.com/sony/gobreaker" 34 | version = "0.3.0" 35 | 36 | [prune] 37 | go-tests = true 38 | unused-packages = true 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Rafael Jesus 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 | -------------------------------------------------------------------------------- /test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # Copyright 2016 The Kubernetes Authors. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | set -e 18 | 19 | export CGO_ENABLED=1 20 | NO_COLOR='\033[0m' 21 | OK_COLOR='\033[32;01m' 22 | ERROR_COLOR='\033[31;01m' 23 | WARN_COLOR='\033[33;01m' 24 | PASS="${OK_COLOR}PASS ${NO_COLOR}" 25 | FAIL="${ERROR_COLOR}FAIL ${NO_COLOR}" 26 | 27 | TARGETS=$@ 28 | 29 | echo "${OK_COLOR}Running tests: ${NO_COLOR}" 30 | go test -v -race -cover ${TARGETS} -bench . 31 | 32 | echo "${OK_COLOR}Vetting: ${NO_COLOR}" 33 | ERRS=$(go vet ${TARGETS} 2>&1 || true) 34 | if [ -n "${ERRS}" ]; then 35 | echo ${FAIL} 36 | echo "${ERRS}" 37 | exit 1 38 | fi 39 | echo ${PASS} 40 | -------------------------------------------------------------------------------- /listener.go: -------------------------------------------------------------------------------- 1 | package bus 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | 7 | nsq "github.com/nsqio/go-nsq" 8 | ) 9 | 10 | var ( 11 | // ErrTopicRequired is returned when topic is not passed as parameter. 12 | ErrTopicRequired = errors.New("topic is mandatory") 13 | // ErrHandlerRequired is returned when handler is not passed as parameter. 14 | ErrHandlerRequired = errors.New("handler is mandatory") 15 | // ErrChannelRequired is returned when channel is not passed as parameter in bus.ListenerConfig. 16 | ErrChannelRequired = errors.New("channel is mandatory") 17 | ) 18 | 19 | // HandlerFunc is the handler function to handle the massage. 20 | type HandlerFunc func(m *Message) (interface{}, error) 21 | 22 | // On listen to a message from a specific topic using nsq consumer, returns 23 | // an error if topic and channel not passed or if an error occurred while creating 24 | // nsq consumer. 25 | func On(lc ListenerConfig) error { 26 | if len(lc.Topic) == 0 { 27 | return ErrTopicRequired 28 | } 29 | 30 | if len(lc.Channel) == 0 { 31 | return ErrChannelRequired 32 | } 33 | 34 | if lc.HandlerFunc == nil { 35 | return ErrHandlerRequired 36 | } 37 | 38 | if len(lc.Lookup) == 0 { 39 | lc.Lookup = []string{"localhost:4161"} 40 | } 41 | 42 | if lc.HandlerConcurrency == 0 { 43 | lc.HandlerConcurrency = 1 44 | } 45 | 46 | config := newListenerConfig(lc) 47 | consumer, err := nsq.NewConsumer(lc.Topic, lc.Channel, config) 48 | if err != nil { 49 | return err 50 | } 51 | 52 | handler := handleMessage(lc) 53 | consumer.AddConcurrentHandlers(handler, lc.HandlerConcurrency) 54 | return consumer.ConnectToNSQLookupds(lc.Lookup) 55 | } 56 | 57 | func handleMessage(lc ListenerConfig) nsq.HandlerFunc { 58 | return nsq.HandlerFunc(func(message *nsq.Message) error { 59 | m := Message{Message: message} 60 | if err := json.Unmarshal(message.Body, &m); err != nil { 61 | return err 62 | } 63 | 64 | res, err := lc.HandlerFunc(&m) 65 | if err != nil { 66 | return err 67 | } 68 | 69 | if m.ReplyTo == "" { 70 | return nil 71 | } 72 | 73 | emitter, err := NewEmitter(EmitterConfig{}) 74 | if err != nil { 75 | return err 76 | } 77 | 78 | return emitter.Emit(m.ReplyTo, res) 79 | }) 80 | } 81 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Event Bus NSQ 2 | 3 | * A tiny wrapper around [go-nsq](https://github.com/nsqio/go-nsq) topic and channel. 4 | * Protect nsq calls with [gobreaker](https://github.com/sony/gobreaker). 5 | 6 | ## Installation 7 | ```bash 8 | go get -u github.com/rafaeljesus/nsq-event-bus 9 | ``` 10 | 11 | ## Usage 12 | The nsq-event-bus package exposes a interface for emitting and listening events. 13 | 14 | ### Emitter 15 | ```go 16 | import "github.com/rafaeljesus/nsq-event-bus" 17 | 18 | topic := "events" 19 | emitter, err := bus.NewEmitter(bus.EmitterConfig{ 20 | Address: "localhost:4150", 21 | MaxInFlight: 25, 22 | }) 23 | 24 | e := event{} 25 | if err = emitter.Emit(topic, &e); err != nil { 26 | // handle failure to emit message 27 | } 28 | 29 | // emitting messages on a async fashion 30 | if err = emitter.EmitAsync(topic, &e); err != nil { 31 | // handle failure to emit message 32 | } 33 | 34 | ``` 35 | 36 | ### Listener 37 | ```go 38 | import "github.com/rafaeljesus/nsq-event-bus" 39 | 40 | if err = bus.On(bus.ListenerConfig{ 41 | Topic: "topic", 42 | Channel: "test_on", 43 | HandlerFunc: handler, 44 | HandlerConcurrency: 4, 45 | }); err != nil { 46 | // handle failure to listen a message 47 | } 48 | 49 | func handler(message *Message) (reply interface{}, err error) { 50 | e := event{} 51 | if err = message.DecodePayload(&e); err != nil { 52 | message.Finish() 53 | return 54 | } 55 | 56 | if message.Attempts > MAX_DELIVERY_ATTEMPTS { 57 | message.Finish() 58 | return 59 | } 60 | 61 | err, _ = doWork(&e) 62 | if err != nil { 63 | message.Requeue(BACKOFF_TIME) 64 | return 65 | } 66 | 67 | message.Finish() 68 | return 69 | } 70 | ``` 71 | 72 | ### Request (Request/Reply) 73 | ```go 74 | import "github.com/rafaeljesus/nsq-event-bus" 75 | 76 | topic := "user_signup" 77 | emitter, err = bus.NewEmitter(bus.EmitterConfig{}) 78 | 79 | e := event{Login: "rafa", Password: "ilhabela_is_the_place"} 80 | if err = bus.Request(topic, &e, handler); err != nil { 81 | // handle failure to listen a message 82 | } 83 | 84 | func handler(message *Message) (reply interface{}, err error) { 85 | e := event{} 86 | if err = message.DecodePayload(&e); err != nil { 87 | message.Finish() 88 | return 89 | } 90 | 91 | reply = &Reply{} 92 | message.Finish() 93 | return 94 | } 95 | ``` 96 | 97 | ## Contributing 98 | - Fork it 99 | - Create your feature branch (`git checkout -b my-new-feature`) 100 | - Commit your changes (`git commit -am 'Add some feature'`) 101 | - Push to the branch (`git push origin my-new-feature`) 102 | - Create new Pull Request 103 | 104 | ## Badges 105 | 106 | [![Build Status](https://circleci.com/gh/rafaeljesus/nsq-event-bus.svg?style=svg)](https://circleci.com/gh/rafaeljesus/nsq-event-bus) 107 | [![Go Report Card](https://goreportcard.com/badge/github.com/rafaeljesus/nsq-event-bus)](https://goreportcard.com/report/github.com/rafaeljesus/nsq-event-bus) 108 | [![Go Doc](https://godoc.org/github.com/rafaeljesus/nsq-event-bus?status.svg)](https://godoc.org/github.com/rafaeljesus/nsq-event-bus) 109 | 110 | --- 111 | 112 | > GitHub [@rafaeljesus](https://github.com/rafaeljesus)  ·  113 | > Medium [@_jesus_rafael](https://medium.com/@_jesus_rafael)  ·  114 | > Twitter [@_jesus_rafael](https://twitter.com/_jesus_rafael) 115 | -------------------------------------------------------------------------------- /listener_test.go: -------------------------------------------------------------------------------- 1 | package bus 2 | 3 | import ( 4 | "crypto/tls" 5 | "fmt" 6 | "sync" 7 | "testing" 8 | "time" 9 | ) 10 | 11 | func TestListener(t *testing.T) { 12 | t.Parallel() 13 | 14 | tests := []struct { 15 | scenario string 16 | function func(*testing.T) 17 | }{ 18 | { 19 | "listener on", 20 | testOn, 21 | }, 22 | { 23 | "listener on validation", 24 | testOnValidation, 25 | }, 26 | } 27 | 28 | for _, test := range tests { 29 | t.Run(test.scenario, func(t *testing.T) { 30 | test.function(t) 31 | }) 32 | } 33 | } 34 | 35 | func testOn(t *testing.T) { 36 | type event struct{ Name string } 37 | 38 | emitter, err := NewEmitter(EmitterConfig{}) 39 | if err != nil { 40 | t.Fatalf("expected to initialize emitter %v", err) 41 | } 42 | 43 | e := event{"event"} 44 | if err := emitter.Emit("ltopic", &e); err != nil { 45 | t.Fatalf("expected to emit message %v", err) 46 | } 47 | 48 | var wg sync.WaitGroup 49 | wg.Add(1) 50 | handler := func(message *Message) (reply interface{}, err error) { 51 | defer wg.Done() 52 | e := event{} 53 | if err = message.DecodePayload(&e); err != nil { 54 | t.Errorf("Expected to unmarshal payload") 55 | } 56 | if e.Name != "event" { 57 | t.Errorf("Expected name to be equal event %s", e.Name) 58 | } 59 | message.Finish() 60 | return 61 | } 62 | 63 | if err := On(ListenerConfig{ 64 | Topic: "ltopic", 65 | Channel: "test_on", 66 | HandlerFunc: handler, 67 | HandlerConcurrency: 1, 68 | Lookup: []string{"localhost:4161"}, 69 | DialTimeout: time.Second * 1, 70 | ReadTimeout: time.Second * 60, 71 | WriteTimeout: time.Second * 1, 72 | LookupdPollInterval: time.Second * 60, 73 | LookupdPollJitter: 0.3, 74 | MaxRequeueDelay: time.Second * 5, 75 | DefaultRequeueDelay: time.Second * 5, 76 | BackoffStrategy: &backoffStrategyMock{}, 77 | MaxBackoffDuration: time.Second * 5, 78 | BackoffMultiplier: time.Second * 5, 79 | MaxAttempts: 5, 80 | LowRdyIdleTimeout: time.Second * 5, 81 | RDYRedistributeInterval: time.Second * 5, 82 | ClientID: "foo", 83 | Hostname: "foo", 84 | UserAgent: "foo", 85 | HeartbeatInterval: time.Second * 30, 86 | SampleRate: 99, 87 | TLSV1: true, 88 | TLSConfig: &tls.Config{ 89 | InsecureSkipVerify: true, 90 | }, 91 | Deflate: true, 92 | DeflateLevel: 5, 93 | Snappy: true, 94 | OutputBufferSize: 16384, 95 | OutputBufferTimeout: time.Millisecond * 350, 96 | MaxInFlight: 2, 97 | MsgTimeout: time.Second * 5, 98 | AuthSecret: "foo", 99 | }); err != nil { 100 | t.Errorf("expected to listen a message %v", err) 101 | } 102 | 103 | wg.Wait() 104 | } 105 | 106 | func testOnValidation(t *testing.T) { 107 | cases := []struct { 108 | msg string 109 | config ListenerConfig 110 | }{ 111 | { 112 | "unexpected topic", 113 | ListenerConfig{ 114 | Topic: "", 115 | Channel: "test_on", 116 | HandlerFunc: func(message *Message) (reply interface{}, err error) { 117 | return 118 | }, 119 | }, 120 | }, 121 | { 122 | "unexpected channel", 123 | ListenerConfig{ 124 | Topic: "ltopic", 125 | Channel: "", 126 | HandlerFunc: func(message *Message) (reply interface{}, err error) { 127 | return 128 | }, 129 | }, 130 | }, 131 | { 132 | "unexpected handler", 133 | ListenerConfig{ 134 | Topic: "ltopic", 135 | Channel: "test_on", 136 | }, 137 | }, 138 | } 139 | 140 | for _, c := range cases { 141 | if err := On(c.config); err == nil { 142 | t.Fatalf(fmt.Sprintf("%s: %v", c.msg, err)) 143 | } 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /emitter.go: -------------------------------------------------------------------------------- 1 | package bus 2 | 3 | import ( 4 | "crypto/rand" 5 | "encoding/hex" 6 | "encoding/json" 7 | "fmt" 8 | "log" 9 | "net/http" 10 | "strconv" 11 | "strings" 12 | 13 | nsq "github.com/nsqio/go-nsq" 14 | "github.com/sony/gobreaker" 15 | ) 16 | 17 | type ( 18 | // Emitter is the emitter wrapper over nsq. 19 | Emitter struct { 20 | producer *nsq.Producer 21 | address string 22 | breaker *gobreaker.CircuitBreaker 23 | } 24 | ) 25 | 26 | // NewEmitter returns a new Emitter configured with the 27 | // variables from the config parameter, or returning an non-nil err 28 | // if an error occurred while creating nsq producer. 29 | func NewEmitter(ec EmitterConfig) (*Emitter, error) { 30 | config := newEmitterConfig(ec) 31 | 32 | address := ec.Address 33 | if len(address) == 0 { 34 | address = "localhost:4150" 35 | } 36 | 37 | producer, err := nsq.NewProducer(address, config) 38 | if err != nil { 39 | return nil, err 40 | } 41 | 42 | return &Emitter{ 43 | producer: producer, 44 | address: address, 45 | breaker: gobreaker.NewCircuitBreaker(newBreakerSettings(ec.Breaker)), 46 | }, nil 47 | } 48 | 49 | // Emit emits a message to a specific topic using nsq producer, returning 50 | // an error if encoding payload fails or if an error occurred while publishing 51 | // the message. 52 | func (e *Emitter) Emit(topic string, payload interface{}) error { 53 | if len(topic) == 0 { 54 | return ErrTopicRequired 55 | } 56 | 57 | body, err := e.encodeMessage(payload, "") 58 | if err != nil { 59 | return err 60 | } 61 | 62 | _, err = e.breaker.Execute(func() (interface{}, error) { 63 | return nil, e.producer.Publish(topic, body) 64 | }) 65 | 66 | return err 67 | } 68 | 69 | // Emit emits a message to a specific topic using nsq producer, but does not wait for 70 | // the response from `nsqd`. Returns an error if encoding payload fails and 71 | // logs to console if an error occurred while publishing the message. 72 | func (e *Emitter) EmitAsync(topic string, payload interface{}) error { 73 | if len(topic) == 0 { 74 | return ErrTopicRequired 75 | } 76 | 77 | body, err := e.encodeMessage(payload, "") 78 | if err != nil { 79 | return err 80 | } 81 | 82 | responseChan := make(chan *nsq.ProducerTransaction, 1) 83 | go func(responseChan chan *nsq.ProducerTransaction) { 84 | for { 85 | trans, ok := <-responseChan 86 | if ok && trans.Error != nil { 87 | log.Fatalf(trans.Error.Error()) 88 | } 89 | } 90 | }(responseChan) 91 | 92 | _, err = e.breaker.Execute(func() (interface{}, error) { 93 | return nil, e.producer.PublishAsync(topic, body, responseChan, "") 94 | }) 95 | 96 | return err 97 | } 98 | 99 | // Request a RPC like method which implements request/reply pattern using nsq producer and consumer. 100 | // Returns an non-nil err if an error occurred while creating or listening to the internal 101 | // reply topic or encoding the message payload fails or while publishing the message. 102 | func (e *Emitter) Request(topic string, payload interface{}, handler HandlerFunc) error { 103 | if len(topic) == 0 { 104 | return ErrTopicRequired 105 | } 106 | 107 | if handler == nil { 108 | return ErrHandlerRequired 109 | } 110 | 111 | replyTo, err := e.genReplyQueue() 112 | if err != nil { 113 | return err 114 | } 115 | 116 | if err := e.createTopic(replyTo); err != nil { 117 | return err 118 | } 119 | 120 | if err := On(ListenerConfig{ 121 | Topic: replyTo, 122 | Channel: replyTo, 123 | HandlerFunc: handler, 124 | }); err != nil { 125 | return err 126 | } 127 | 128 | body, err := e.encodeMessage(payload, replyTo) 129 | if err != nil { 130 | return err 131 | } 132 | 133 | _, err = e.breaker.Execute(func() (interface{}, error) { 134 | return nil, e.producer.Publish(topic, body) 135 | }) 136 | 137 | return err 138 | } 139 | 140 | func (e *Emitter) encodeMessage(payload interface{}, replyTo string) ([]byte, error) { 141 | p, err := json.Marshal(payload) 142 | if err != nil { 143 | return nil, err 144 | } 145 | 146 | message := NewMessage(p, replyTo) 147 | return json.Marshal(message) 148 | } 149 | 150 | func (e *Emitter) genReplyQueue() (string, error) { 151 | b := make([]byte, 8) 152 | if _, err := rand.Read(b); err != nil { 153 | return "", err 154 | } 155 | 156 | hash := hex.EncodeToString(b) 157 | return fmt.Sprint(hash, ".ephemeral"), nil 158 | } 159 | 160 | func (e *Emitter) createTopic(topic string) error { 161 | s := strings.Split(e.address, ":") 162 | port, err := strconv.Atoi(s[1]) 163 | if err != nil { 164 | return err 165 | } 166 | 167 | uri := fmt.Sprintf("http://%s:%s/topic/create?topic=%s", s[0], strconv.Itoa(port+1), topic) 168 | _, err = http.Post(uri, "application/json; charset=utf-8", nil) 169 | return err 170 | } 171 | 172 | func newBreakerSettings(c Breaker) gobreaker.Settings { 173 | return gobreaker.Settings{ 174 | Name: "nsq-emitter-circuit-breaker", 175 | Interval: c.Interval, 176 | Timeout: c.Timeout, 177 | ReadyToTrip: func(counts gobreaker.Counts) bool { 178 | return counts.ConsecutiveFailures > c.Threshold 179 | }, 180 | OnStateChange: func(name string, from gobreaker.State, to gobreaker.State) { 181 | c.OnStateChange(name, from.String(), to.String()) 182 | }, 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /emitter_test.go: -------------------------------------------------------------------------------- 1 | package bus 2 | 3 | import ( 4 | "crypto/tls" 5 | "sync" 6 | "testing" 7 | "time" 8 | ) 9 | 10 | func TestEmitter(t *testing.T) { 11 | t.Parallel() 12 | 13 | tests := []struct { 14 | scenario string 15 | function func(*testing.T) 16 | }{ 17 | { 18 | "create new emitter", 19 | testNewEmitter, 20 | }, 21 | { 22 | "emit message", 23 | testEmitMessage, 24 | }, 25 | { 26 | "emit async message", 27 | testEmitAsyncMessage, 28 | }, 29 | { 30 | "request message", 31 | testRequestMessage, 32 | }, 33 | } 34 | 35 | for _, test := range tests { 36 | t.Run(test.scenario, func(t *testing.T) { 37 | test.function(t) 38 | }) 39 | } 40 | } 41 | 42 | func testNewEmitter(t *testing.T) { 43 | _, err := NewEmitter(EmitterConfig{ 44 | Address: "localhost:4150", 45 | DialTimeout: time.Second * 5, 46 | ReadTimeout: time.Second * 5, 47 | WriteTimeout: time.Second * 5, 48 | LocalAddr: &localAddrMock{}, 49 | LookupdPollInterval: time.Second * 5, 50 | LookupdPollJitter: 1, 51 | MaxRequeueDelay: time.Second * 5, 52 | DefaultRequeueDelay: time.Second * 5, 53 | BackoffStrategy: &backoffStrategyMock{}, 54 | MaxBackoffDuration: time.Second * 5, 55 | BackoffMultiplier: time.Second * 5, 56 | MaxAttempts: 5, 57 | LowRdyIdleTimeout: time.Second * 5, 58 | RDYRedistributeInterval: time.Second * 5, 59 | ClientID: "foo", 60 | Hostname: "foo", 61 | UserAgent: "foo", 62 | HeartbeatInterval: time.Second * 5, 63 | SampleRate: 10, 64 | TLSV1: true, 65 | TLSConfig: &tls.Config{ 66 | InsecureSkipVerify: true, 67 | }, 68 | Deflate: true, 69 | DeflateLevel: 1, 70 | Snappy: true, 71 | OutputBufferSize: 1, 72 | OutputBufferTimeout: time.Second * 5, 73 | MaxInFlight: 1, 74 | MsgTimeout: time.Second * 5, 75 | AuthSecret: "foo", 76 | }) 77 | 78 | if err != nil { 79 | t.Fatalf("expected to initialize emitter %v", err) 80 | } 81 | } 82 | 83 | func testEmitMessage(t *testing.T) { 84 | emitter, err := NewEmitter(EmitterConfig{}) 85 | if err != nil { 86 | t.Fatalf("expected to initialize emitter %v", err) 87 | } 88 | 89 | type event struct{ Name string } 90 | e := event{"event"} 91 | if err := emitter.Emit("etopic", &e); err != nil { 92 | t.Fatalf("expected to emit message %v", err) 93 | } 94 | 95 | if err := emitter.Emit("", &e); err != ErrTopicRequired { 96 | t.Fatalf("unexpected error value %v", err) 97 | } 98 | } 99 | 100 | func testEmitAsyncMessage(t *testing.T) { 101 | emitter, err := NewEmitter(EmitterConfig{}) 102 | if err != nil { 103 | t.Fatalf("expected to initialize emitter %v", err) 104 | } 105 | 106 | type event struct{ Name string } 107 | e := event{"event"} 108 | if err := emitter.EmitAsync("etopic", &e); err != nil { 109 | t.Fatalf("expected to emit message %v", err) 110 | } 111 | 112 | if err := emitter.Emit("", &e); err != ErrTopicRequired { 113 | t.Fatalf("unexpected error value %v", err) 114 | } 115 | } 116 | 117 | func testRequestMessage(t *testing.T) { 118 | emitter, err := NewEmitter(EmitterConfig{}) 119 | if err != nil { 120 | t.Fatalf("expected to initialize emitter %v", err) 121 | } 122 | 123 | type event struct{ Name string } 124 | 125 | var wg sync.WaitGroup 126 | wg.Add(1) 127 | replyHandler := func(message *Message) (reply interface{}, err error) { 128 | defer wg.Done() 129 | e := event{} 130 | if err = message.DecodePayload(&e); err != nil { 131 | t.Errorf("Expected to unmarshal payload") 132 | } 133 | if e.Name != "event_reply" { 134 | t.Errorf("Expected name to be equal event %s", e.Name) 135 | } 136 | message.Finish() 137 | return 138 | } 139 | 140 | handler := func(message *Message) (reply interface{}, err error) { 141 | e := event{} 142 | if err = message.DecodePayload(&e); err != nil { 143 | t.Errorf("Expected to unmarshal payload") 144 | } 145 | reply = &event{"event_reply"} 146 | message.Finish() 147 | return 148 | } 149 | 150 | if err := On(ListenerConfig{ 151 | Topic: "etopic", 152 | Channel: "test_request", 153 | HandlerFunc: handler, 154 | }); err != nil { 155 | t.Fatalf("expected to listen a message %v", err) 156 | } 157 | 158 | cases := []struct { 159 | topic string 160 | event event 161 | replyh func(message *Message) (interface{}, error) 162 | wantErr bool 163 | }{ 164 | { 165 | "etopic", event{"event"}, replyHandler, true, 166 | }, 167 | { 168 | "", event{"event"}, replyHandler, false, 169 | }, 170 | { 171 | "etopic", event{"event"}, nil, false, 172 | }, 173 | } 174 | 175 | for _, c := range cases { 176 | if c.wantErr { 177 | if err := emitter.Request(c.topic, c.event, c.replyh); err != nil { 178 | t.Errorf("expected to request a message %v", err) 179 | } 180 | } else { 181 | if err := emitter.Request(c.topic, c.event, c.replyh); err == nil { 182 | t.Errorf("unexpected error value %v", err) 183 | } 184 | } 185 | } 186 | 187 | wg.Wait() 188 | } 189 | 190 | type localAddrMock struct{} 191 | 192 | func (a *localAddrMock) Network() (s string) { return } 193 | func (a *localAddrMock) String() (s string) { return } 194 | 195 | type backoffStrategyMock struct{} 196 | 197 | func (b *backoffStrategyMock) Calculate(attempt int) (v time.Duration) { return } 198 | -------------------------------------------------------------------------------- /config.go: -------------------------------------------------------------------------------- 1 | package bus 2 | 3 | import ( 4 | "crypto/tls" 5 | "net" 6 | "time" 7 | 8 | nsq "github.com/nsqio/go-nsq" 9 | ) 10 | 11 | // EmitterConfig carries the different variables to tune a newly started emitter, 12 | // it exposes the same configuration available from official nsq go client. 13 | type EmitterConfig struct { 14 | Address string 15 | DialTimeout time.Duration 16 | ReadTimeout time.Duration 17 | WriteTimeout time.Duration 18 | LocalAddr net.Addr 19 | LookupdPollInterval time.Duration 20 | LookupdPollJitter float64 21 | MaxRequeueDelay time.Duration 22 | DefaultRequeueDelay time.Duration 23 | BackoffStrategy nsq.BackoffStrategy 24 | MaxBackoffDuration time.Duration 25 | BackoffMultiplier time.Duration 26 | MaxAttempts uint16 27 | LowRdyIdleTimeout time.Duration 28 | RDYRedistributeInterval time.Duration 29 | ClientID string 30 | Hostname string 31 | UserAgent string 32 | HeartbeatInterval time.Duration 33 | SampleRate int32 34 | TLSV1 bool 35 | TLSConfig *tls.Config 36 | Deflate bool 37 | DeflateLevel int 38 | Snappy bool 39 | OutputBufferSize int64 40 | OutputBufferTimeout time.Duration 41 | MaxInFlight int 42 | MsgTimeout time.Duration 43 | AuthSecret string 44 | // Breaker circuit breaker configuration 45 | Breaker 46 | } 47 | 48 | // ListenerConfig carries the different variables to tune a newly started consumer, 49 | // it exposes the same configuration available from official nsq go client. 50 | type ListenerConfig struct { 51 | Topic string 52 | Channel string 53 | Lookup []string 54 | HandlerFunc HandlerFunc 55 | HandlerConcurrency int 56 | DialTimeout time.Duration 57 | ReadTimeout time.Duration 58 | WriteTimeout time.Duration 59 | LocalAddr net.Addr 60 | LookupdPollInterval time.Duration 61 | LookupdPollJitter float64 62 | MaxRequeueDelay time.Duration 63 | DefaultRequeueDelay time.Duration 64 | BackoffStrategy nsq.BackoffStrategy 65 | MaxBackoffDuration time.Duration 66 | BackoffMultiplier time.Duration 67 | MaxAttempts uint16 68 | LowRdyIdleTimeout time.Duration 69 | RDYRedistributeInterval time.Duration 70 | ClientID string 71 | Hostname string 72 | UserAgent string 73 | HeartbeatInterval time.Duration 74 | SampleRate int32 75 | TLSV1 bool 76 | TLSConfig *tls.Config 77 | Deflate bool 78 | DeflateLevel int 79 | Snappy bool 80 | OutputBufferSize int64 81 | OutputBufferTimeout time.Duration 82 | MaxInFlight int 83 | MsgTimeout time.Duration 84 | AuthSecret string 85 | } 86 | 87 | // Breaker carries the configuration for circuit breaker 88 | type Breaker struct { 89 | // Interval is the cyclic period of the closed state for CircuitBreaker to clear the internal counts, 90 | // If Interval is 0, CircuitBreaker doesn't clear the internal counts during the closed state. 91 | Interval time.Duration 92 | // Timeout is the period of the open state, after which the state of CircuitBreaker becomes half-open. 93 | // If Timeout is 0, the timeout value of CircuitBreaker is set to 60 seconds. 94 | Timeout time.Duration 95 | // Threshold when a threshold of failures has been reached, future calls to the broker will not run. 96 | // During this state, the circuit breaker will periodically allow the calls to run and, if it is successful, 97 | // will start running the function again. Default value is 5. 98 | Threshold uint32 99 | // OnStateChange is called whenever the state of CircuitBreaker changes. 100 | OnStateChange func(name, from, to string) 101 | } 102 | 103 | func newEmitterConfig(ec EmitterConfig) (config *nsq.Config) { 104 | config = nsq.NewConfig() 105 | 106 | setDialTimeout(config, ec.DialTimeout) 107 | setReadTimeout(config, ec.ReadTimeout) 108 | setLocalAddr(config, ec.LocalAddr) 109 | setLookupPollInterval(config, ec.LookupdPollInterval) 110 | setLookupPollJitter(config, ec.LookupdPollJitter) 111 | setMaxRequeueDelay(config, ec.MaxRequeueDelay) 112 | setDefaultRequeueDelay(config, ec.DefaultRequeueDelay) 113 | setBackoffStrategy(config, ec.BackoffStrategy) 114 | setMaxBackoffDuration(config, ec.MaxBackoffDuration) 115 | setBackoffMultiplier(config, ec.BackoffMultiplier) 116 | setMaxAttempts(config, ec.MaxAttempts) 117 | setLowRdyIdleTimeout(config, ec.LowRdyIdleTimeout) 118 | setRDYRedistributeInterval(config, ec.RDYRedistributeInterval) 119 | setClientID(config, ec.ClientID) 120 | setHostname(config, ec.Hostname) 121 | setUserAgent(config, ec.UserAgent) 122 | setHeartbeatInterval(config, ec.HeartbeatInterval) 123 | setSampleRate(config, ec.SampleRate) 124 | setTLSV1(config, ec.TLSV1) 125 | setTLSConfig(config, ec.TLSConfig) 126 | setDeflate(config, ec.Deflate) 127 | setOutputBufferSize(config, ec.OutputBufferSize) 128 | setOutputBufferTimeout(config, ec.OutputBufferTimeout) 129 | setMaxInFlight(config, ec.MaxInFlight) 130 | setMsgTimeout(config, ec.MsgTimeout) 131 | setAuthSecret(config, ec.AuthSecret) 132 | 133 | return 134 | } 135 | 136 | func newListenerConfig(lc ListenerConfig) (config *nsq.Config) { 137 | config = nsq.NewConfig() 138 | 139 | setDialTimeout(config, lc.DialTimeout) 140 | setReadTimeout(config, lc.ReadTimeout) 141 | setLocalAddr(config, lc.LocalAddr) 142 | setLookupPollInterval(config, lc.LookupdPollInterval) 143 | setLookupPollJitter(config, lc.LookupdPollJitter) 144 | setMaxRequeueDelay(config, lc.MaxRequeueDelay) 145 | setDefaultRequeueDelay(config, lc.DefaultRequeueDelay) 146 | setBackoffStrategy(config, lc.BackoffStrategy) 147 | setMaxBackoffDuration(config, lc.MaxBackoffDuration) 148 | setBackoffMultiplier(config, lc.BackoffMultiplier) 149 | setMaxAttempts(config, lc.MaxAttempts) 150 | setLowRdyIdleTimeout(config, lc.LowRdyIdleTimeout) 151 | setRDYRedistributeInterval(config, lc.RDYRedistributeInterval) 152 | setClientID(config, lc.ClientID) 153 | setHostname(config, lc.Hostname) 154 | setUserAgent(config, lc.UserAgent) 155 | setHeartbeatInterval(config, lc.HeartbeatInterval) 156 | setSampleRate(config, lc.SampleRate) 157 | setTLSV1(config, lc.TLSV1) 158 | setTLSConfig(config, lc.TLSConfig) 159 | setDeflate(config, lc.Deflate) 160 | setOutputBufferSize(config, lc.OutputBufferSize) 161 | setOutputBufferTimeout(config, lc.OutputBufferTimeout) 162 | setMaxInFlight(config, lc.MaxInFlight) 163 | setMsgTimeout(config, lc.MsgTimeout) 164 | setAuthSecret(config, lc.AuthSecret) 165 | 166 | return 167 | } 168 | 169 | func setDialTimeout(config *nsq.Config, dialTimeout time.Duration) { 170 | if dialTimeout != 0 { 171 | config.DialTimeout = dialTimeout 172 | } 173 | } 174 | 175 | func setReadTimeout(config *nsq.Config, readTimeout time.Duration) { 176 | if readTimeout != 0 { 177 | config.ReadTimeout = readTimeout 178 | } 179 | } 180 | 181 | func setLocalAddr(config *nsq.Config, localAddr net.Addr) { 182 | if localAddr != nil { 183 | config.LocalAddr = localAddr 184 | } 185 | } 186 | 187 | func setLookupPollInterval(config *nsq.Config, lookupdPollInterval time.Duration) { 188 | if lookupdPollInterval != 0 { 189 | config.LookupdPollInterval = lookupdPollInterval 190 | } 191 | } 192 | 193 | func setLookupPollJitter(config *nsq.Config, lookupdPollJitter float64) { 194 | if lookupdPollJitter != 0 { 195 | config.LookupdPollJitter = lookupdPollJitter 196 | } 197 | } 198 | 199 | func setMaxRequeueDelay(config *nsq.Config, maxRequeueDelay time.Duration) { 200 | if maxRequeueDelay != 0 { 201 | config.MaxRequeueDelay = maxRequeueDelay 202 | } 203 | } 204 | 205 | func setDefaultRequeueDelay(config *nsq.Config, defaultRequeueDelay time.Duration) { 206 | if defaultRequeueDelay != 0 { 207 | config.DefaultRequeueDelay = defaultRequeueDelay 208 | } 209 | } 210 | 211 | func setBackoffStrategy(config *nsq.Config, backoffStrategy nsq.BackoffStrategy) { 212 | if backoffStrategy != nil { 213 | config.BackoffStrategy = backoffStrategy 214 | } 215 | } 216 | 217 | func setMaxBackoffDuration(config *nsq.Config, maxBackoffDuration time.Duration) { 218 | if maxBackoffDuration != 0 { 219 | config.MaxBackoffDuration = maxBackoffDuration 220 | } 221 | } 222 | 223 | func setBackoffMultiplier(config *nsq.Config, backoffMultiplier time.Duration) { 224 | if backoffMultiplier != 0 { 225 | config.BackoffMultiplier = backoffMultiplier 226 | } 227 | } 228 | 229 | func setMaxAttempts(config *nsq.Config, maxAttempts uint16) { 230 | if maxAttempts != 0 { 231 | config.MaxAttempts = maxAttempts 232 | } 233 | } 234 | 235 | func setLowRdyIdleTimeout(config *nsq.Config, lowRdyIdleTimeout time.Duration) { 236 | if lowRdyIdleTimeout != 0 { 237 | config.LowRdyIdleTimeout = lowRdyIdleTimeout 238 | } 239 | } 240 | 241 | func setRDYRedistributeInterval(config *nsq.Config, rdyRedistributeInterval time.Duration) { 242 | if rdyRedistributeInterval != 0 { 243 | config.LowRdyIdleTimeout = rdyRedistributeInterval 244 | } 245 | } 246 | 247 | func setClientID(config *nsq.Config, clientID string) { 248 | if clientID != "" { 249 | config.ClientID = clientID 250 | } 251 | } 252 | 253 | func setHostname(config *nsq.Config, hostname string) { 254 | if hostname != "" { 255 | config.Hostname = hostname 256 | } 257 | } 258 | 259 | func setUserAgent(config *nsq.Config, userAgent string) { 260 | if userAgent != "" { 261 | config.UserAgent = userAgent 262 | } 263 | } 264 | 265 | func setHeartbeatInterval(config *nsq.Config, heartbeatInterval time.Duration) { 266 | if heartbeatInterval != 0 { 267 | config.HeartbeatInterval = heartbeatInterval 268 | } 269 | } 270 | 271 | func setSampleRate(config *nsq.Config, sampleRate int32) { 272 | if sampleRate != 0 { 273 | config.SampleRate = sampleRate 274 | } 275 | } 276 | 277 | func setTLSV1(config *nsq.Config, tlsv1 bool) { 278 | if tlsv1 { 279 | config.TlsV1 = tlsv1 280 | } 281 | } 282 | 283 | func setTLSConfig(config *nsq.Config, tlsConfig *tls.Config) { 284 | if tlsConfig != nil { 285 | config.TlsConfig = tlsConfig 286 | } 287 | } 288 | 289 | func setDeflate(config *nsq.Config, deflate bool) { 290 | if deflate { 291 | config.Deflate = deflate 292 | } 293 | } 294 | 295 | func setOutputBufferSize(config *nsq.Config, out int64) { 296 | if out != 0 { 297 | config.OutputBufferSize = out 298 | } 299 | } 300 | 301 | func setOutputBufferTimeout(config *nsq.Config, out time.Duration) { 302 | if out != 0 { 303 | config.OutputBufferTimeout = out 304 | } 305 | } 306 | 307 | func setMaxInFlight(config *nsq.Config, maxInFlight int) { 308 | if maxInFlight != 0 { 309 | config.MaxInFlight = maxInFlight 310 | } 311 | } 312 | 313 | func setMsgTimeout(config *nsq.Config, msgTimeout time.Duration) { 314 | if msgTimeout != 0 { 315 | config.MsgTimeout = msgTimeout 316 | } 317 | } 318 | 319 | func setAuthSecret(config *nsq.Config, authSecret string) { 320 | if authSecret != "" { 321 | config.AuthSecret = authSecret 322 | } 323 | } 324 | --------------------------------------------------------------------------------