├── .github └── workflows │ ├── main-test.yaml │ └── pr-test.yaml ├── .gitignore ├── LICENSE ├── README.md ├── docker-compose.yaml ├── examples └── main.go ├── go.mod ├── go.sum ├── log.go ├── rabbit.go ├── rabbit_suite_test.go ├── rabbit_test.go └── retry.go /.github/workflows/main-test.yaml: -------------------------------------------------------------------------------- 1 | name: main 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | test: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@main 13 | - name: Start up dependencies 14 | run: docker compose up -d 15 | - name: Wait for dependencies to start up 16 | uses: jakejarvis/wait-action@master 17 | with: 18 | time: '30s' 19 | - name: Master buld tests 20 | run: | 21 | go test ./... 22 | - name: Bump version and push tag 23 | uses: mathieudutour/github-tag-action@v4.5 24 | with: 25 | github_token: ${{ secrets.GITHUB_TOKEN }} 26 | release_branches: main 27 | -------------------------------------------------------------------------------- /.github/workflows/pr-test.yaml: -------------------------------------------------------------------------------- 1 | on: pull_request 2 | name: Pull Request 3 | jobs: 4 | test: 5 | name: Run tests 6 | runs-on: ubuntu-latest 7 | steps: 8 | - uses: actions/checkout@v2 9 | - name: Start up dependencies 10 | run: docker compose up -d 11 | - name: Wait for dependencies to start up 12 | uses: jakejarvis/wait-action@master 13 | with: 14 | time: '30s' 15 | - name: Test 16 | run: | 17 | go test ./... 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2024 Streamdal.com, Inc. https://streamdal.com 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | rabbit 2 | ====== 3 | [![](https://godoc.org/github.com/streamdal/rabbit?status.svg)](http://godoc.org/github.com/streamdal/rabbit) [![Master build status](https://github.com/streamdal/rabbit/workflows/main/badge.svg)](https://github.com/streamdal/rabbit/actions) [![Go Report Card](https://goreportcard.com/badge/github.com/streamdal/rabbit)](https://goreportcard.com/report/github.com/streamdal/rabbit) 4 | 5 | A RabbitMQ wrapper lib around ~[streadway/amqp](https://github.com/streadway/amqp)~ [rabbitmq/amqp091-go](https://github.com/rabbitmq/amqp091-go) 6 | with some bells and whistles. 7 | 8 | NOTE: `streadway/amqp` is no longer maintained and RabbitMQ team have forked `streadway/amqp` and created `rabbitmq/amqp091-go`. You can read about this change [here](https://github.com/streadway/amqp/issues/497). This library uses `rabbitmq/amqp091-go`. 9 | 10 | * Support for auto-reconnect 11 | * Support for context (ie. cancel/timeout) 12 | * Support for using multiple binding keys 13 | * Support Producer, Consumer or both modes 14 | 15 | # Motivation 16 | 17 | We (Streamdal, formerly Batch.sh), make heavy use of RabbitMQ - we use it as 18 | the primary method for facilitating inter-service communication. Due to this, 19 | all services make use of RabbitMQ and are both publishers and consumers. 20 | 21 | We wrote this lib to ensure that all of our services make use of Rabbit in a 22 | consistent, predictable way AND are able to survive network blips. 23 | 24 | **NOTE**: This library works only with non-default exchanges. If you need support 25 | for default exchange - open a PR! 26 | 27 | # Usage 28 | ```go 29 | package main 30 | 31 | import ( 32 | "fmt" 33 | "log" 34 | 35 | "github.com/streamdal/rabbit" 36 | ) 37 | 38 | func main() { 39 | r, err := rabbit.New(&rabbit.Options{ 40 | URL: "amqp://localhost", 41 | QueueName: "my-queue", 42 | ExchangeName: "messages", 43 | BindingKeys: []string{"messages"}, 44 | }) 45 | if err != nil { 46 | log.Fatalf("unable to instantiate rabbit: %s", err) 47 | } 48 | 49 | routingKey := "messages" 50 | data := []byte("pumpkins") 51 | 52 | // Publish something 53 | if err := r.Publish(context.Background(), routingKey, data); err != nil { 54 | log.Fatalf("unable to publish message: ") 55 | } 56 | 57 | // Consume once 58 | if err := r.ConsumeOnce(nil, func(amqp.Delivery) error { 59 | fmt.Printf("Received new message: %+v\n", msg) 60 | }); err != nil { 61 | log.Fatalf("unable to consume once: %s", err), 62 | } 63 | 64 | var numReceived int 65 | 66 | // Consume forever (blocks) 67 | ctx, cancel := context.WithCancel(context.Background()) 68 | 69 | r.Consume(ctx, nil, func(msg amqp.Delivery) error { 70 | fmt.Printf("Received new message: %+v\n", msg) 71 | 72 | numReceived++ 73 | 74 | if numReceived > 1 { 75 | r.Stop() 76 | } 77 | }) 78 | 79 | // Or stop via ctx 80 | r.Consume(..) 81 | cancel() 82 | } 83 | ``` 84 | 85 | ### Retry Policies 86 | 87 | You can specify a retry policy for the consumer. 88 | A pre-made ACK retry policy is available in the library at `rp := rabbit.DefaultAckPolicy()`. This policy will retry 89 | acknowledgement unlimited times 90 | 91 | You can also create a new policy using the `rabbit.NewRetryPolicy(maxAttempts, time.Millisecond * 200, time.Second, ...)` function. 92 | 93 | The retry policy can then be passed to consume functions as an argument: 94 | 95 | ```go 96 | consumeFunc := func(msg amqp.Delivery) error { 97 | fmt.Printf("Received new message: %+v\n", msg) 98 | 99 | numReceived++ 100 | 101 | if numReceived > 1 { 102 | r.Stop() 103 | } 104 | } 105 | 106 | rp := rabbit.DefaultAckPolicy() 107 | 108 | r.Consume(ctx, nil, consumeFunc, rp) 109 | ``` -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | services: 3 | rabbitmq: 4 | image: rabbitmq:3.7.8-management-alpine 5 | ports: 6 | - "5672:5672" 7 | - "15672:15672" 8 | # volumes: 9 | # - ./backend-data:/var/lib/rabbitmq 10 | container_name: rabbit_test 11 | -------------------------------------------------------------------------------- /examples/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | 6 | amqp "github.com/rabbitmq/amqp091-go" 7 | "github.com/sirupsen/logrus" 8 | 9 | "github.com/streamdal/rabbit" 10 | ) 11 | 12 | func main() { 13 | llog := logrus.New() 14 | llog.SetLevel(logrus.DebugLevel) 15 | 16 | // Create rabbit instance 17 | r, err := setup(llog) 18 | if err != nil { 19 | llog.Fatalf("Unable to setup rabbit: %s", err) 20 | } 21 | 22 | errChan := make(chan *rabbit.ConsumeError, 1) 23 | 24 | llog.Debug("Starting error listener...") 25 | 26 | // Launch an error listener 27 | go func() { 28 | for { 29 | select { 30 | case err := <-errChan: 31 | llog.Debugf("Received rabbit error: %v", err) 32 | } 33 | } 34 | }() 35 | 36 | llog.Debug("Running consumer...") 37 | 38 | // Run a consumer 39 | r.Consume(context.Background(), errChan, func(d amqp.Delivery) error { 40 | llog.Debugf("[Received message]\nHeaders: %v\nBody: %s\n", d.Headers, d.Body) 41 | 42 | // Acknowledge the message 43 | if err := d.Ack(false); err != nil { 44 | llog.Errorf("Error acknowledging message: %s", err) 45 | } 46 | 47 | return nil 48 | }) 49 | } 50 | 51 | func setup(logger *logrus.Logger) (*rabbit.Rabbit, error) { 52 | return rabbit.New(&rabbit.Options{ 53 | URLs: []string{"amqp://guest:guest@localhost:5672/"}, 54 | Mode: rabbit.Both, 55 | QueueName: "test-queue", 56 | Bindings: []rabbit.Binding{ 57 | { 58 | ExchangeName: "test-exchange", 59 | BindingKeys: []string{"test-key"}, 60 | ExchangeDeclare: true, 61 | ExchangeType: "topic", 62 | ExchangeDurable: true, 63 | ExchangeAutoDelete: true, 64 | }, 65 | }, 66 | RetryReconnectSec: 1, 67 | QueueDurable: true, 68 | QueueExclusive: false, 69 | QueueAutoDelete: true, 70 | QueueDeclare: true, 71 | AutoAck: false, 72 | ConsumerTag: "rabbit-example", 73 | Log: logger, 74 | }) 75 | } 76 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/streamdal/rabbit 2 | 3 | go 1.14 4 | 5 | require ( 6 | github.com/kr/pretty v0.2.0 // indirect 7 | github.com/onsi/ginkgo v1.14.1 8 | github.com/onsi/gomega v1.10.2 9 | github.com/pkg/errors v0.9.1 10 | github.com/rabbitmq/amqp091-go v1.10.0 11 | github.com/satori/go.uuid v1.2.0 12 | github.com/sirupsen/logrus v1.9.3 13 | google.golang.org/protobuf v1.24.0 // indirect 14 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect 15 | ) 16 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 2 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 3 | github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= 4 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 5 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 6 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 7 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 8 | github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 9 | github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= 10 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 11 | github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= 12 | github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= 13 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 14 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 15 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 16 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 17 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 18 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 19 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 20 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 21 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 22 | github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= 23 | github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0= 24 | github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 25 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 26 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 27 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 28 | github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4= 29 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 30 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= 31 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 32 | github.com/kr/pretty v0.2.0 h1:s5hAObm+yFO5uHYt5dYjxi2rXrsnmRpJx4OYvIWUaQs= 33 | github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 34 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 35 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 36 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 37 | github.com/nxadm/tail v1.4.4 h1:DQuhQpB1tVlglWS2hLQ5OV6B5r8aGxSrPc5Qo6uTN78= 38 | github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= 39 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 40 | github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= 41 | github.com/onsi/ginkgo v1.14.1 h1:jMU0WaQrP0a/YAEq8eJmJKjBoMs+pClEr1vDMlM/Do4= 42 | github.com/onsi/ginkgo v1.14.1/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= 43 | github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= 44 | github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= 45 | github.com/onsi/gomega v1.10.2 h1:aY/nuoWlKJud2J6U0E3NWsjlg+0GtwXxgEqthRdzlcs= 46 | github.com/onsi/gomega v1.10.2/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= 47 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 48 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 49 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 50 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 51 | github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 52 | github.com/rabbitmq/amqp091-go v1.10.0 h1:STpn5XsHlHGcecLmMFCtg7mqq0RnD+zFr4uzukfVhBw= 53 | github.com/rabbitmq/amqp091-go v1.10.0/go.mod h1:Hy4jKW5kQART1u+JkDTF9YYOQUHXqMuhrgxOEeS7G4o= 54 | github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww= 55 | github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= 56 | github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= 57 | github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= 58 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 59 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 60 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 61 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 62 | github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= 63 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 64 | go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= 65 | go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= 66 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 67 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 68 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 69 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= 70 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 71 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 72 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 73 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 74 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 75 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 76 | golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7 h1:AeiKBIuRw3UomYXSbLy0Mc2dDLfdtbT/IVn4keq83P0= 77 | golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 78 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 79 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 80 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 81 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 82 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 83 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 84 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 85 | golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 86 | golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 87 | golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 88 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 89 | golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 90 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 h1:0A+M6Uqn+Eje4kHMK80dtF3JCXC4ykBgQG4Fe06QRhQ= 91 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 92 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 93 | golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= 94 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 95 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 96 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 97 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= 98 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 99 | golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 100 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= 101 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 102 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 103 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 104 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 105 | google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 106 | google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= 107 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 108 | google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= 109 | google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 110 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 111 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 112 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 113 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 114 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 115 | google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 116 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 117 | google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 118 | google.golang.org/protobuf v1.24.0 h1:UhZDfRO8JRQru4/+LlLE0BRKGF8L+PICnvYZmx/fEGA= 119 | google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= 120 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 121 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 122 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= 123 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 124 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 125 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= 126 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 127 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 128 | gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= 129 | gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 130 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 131 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 132 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 133 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 134 | honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 135 | -------------------------------------------------------------------------------- /log.go: -------------------------------------------------------------------------------- 1 | package rabbit 2 | 3 | // Logger is the common interface for user-provided loggers. 4 | type Logger interface { 5 | // Debug sends out a debug message with the given arguments to the logger. 6 | Debug(args ...interface{}) 7 | // Debugf formats a debug message using the given arguments and sends it to the logger. 8 | Debugf(format string, args ...interface{}) 9 | // Info sends out an informational message with the given arguments to the logger. 10 | Info(args ...interface{}) 11 | // Infof formats an informational message using the given arguments and sends it to the logger. 12 | Infof(format string, args ...interface{}) 13 | // Warn sends out a warning message with the given arguments to the logger. 14 | Warn(args ...interface{}) 15 | // Warnf formats a warning message using the given arguments and sends it to the logger. 16 | Warnf(format string, args ...interface{}) 17 | // Error sends out an error message with the given arguments to the logger. 18 | Error(args ...interface{}) 19 | // Errorf formats an error message using the given arguments and sends it to the logger. 20 | Errorf(format string, args ...interface{}) 21 | } 22 | 23 | // NoOpLogger is a do-nothing logger; it is used internally 24 | // as the default Logger when none is provided in the Options. 25 | type NoOpLogger struct { 26 | } 27 | 28 | // Debug is no-op implementation of Logger's Debug. 29 | func (l *NoOpLogger) Debug(args ...interface{}) { 30 | } 31 | 32 | // Debugf is no-op implementation of Logger's Debugf. 33 | func (l *NoOpLogger) Debugf(format string, args ...interface{}) { 34 | } 35 | 36 | // Info is no-op implementation of Logger's Info. 37 | func (l *NoOpLogger) Info(args ...interface{}) { 38 | } 39 | 40 | // Infof is no-op implementation of Logger's Infof. 41 | func (l *NoOpLogger) Infof(format string, args ...interface{}) { 42 | } 43 | 44 | // Warn is no-op implementation of Logger's Warn. 45 | func (l *NoOpLogger) Warn(args ...interface{}) { 46 | } 47 | 48 | // Warnf is no-op implementation of Logger's Warnf. 49 | func (l *NoOpLogger) Warnf(format string, args ...interface{}) { 50 | } 51 | 52 | // Error is no-op implementation of Logger's Error. 53 | func (l *NoOpLogger) Error(args ...interface{}) { 54 | } 55 | 56 | // Errorf is no-op implementation of Logger's Errorf. 57 | func (l *NoOpLogger) Errorf(format string, args ...interface{}) { 58 | } 59 | -------------------------------------------------------------------------------- /rabbit.go: -------------------------------------------------------------------------------- 1 | // Package rabbit is a simple streadway/amqp wrapper library that comes with: 2 | // 3 | // * Auto-reconnect support 4 | // 5 | // * Context support 6 | // 7 | // * Helpers for consuming once or forever and publishing 8 | // 9 | // The library is used internally at https://batch.sh where it powers most of 10 | // the platform's backend services. 11 | // 12 | // For an example, refer to the README.md. 13 | package rabbit 14 | 15 | import ( 16 | "context" 17 | "crypto/tls" 18 | "fmt" 19 | "sync" 20 | "time" 21 | 22 | "github.com/pkg/errors" 23 | amqp "github.com/rabbitmq/amqp091-go" 24 | uuid "github.com/satori/go.uuid" 25 | ) 26 | 27 | const ( 28 | // DefaultRetryReconnectSec determines how long to wait before attempting 29 | // to reconnect to a rabbit server 30 | DefaultRetryReconnectSec = 60 31 | 32 | // DefaultStopTimeout is the default amount of time Stop() will wait for 33 | // consume function(s) to exit. 34 | DefaultStopTimeout = 5 * time.Second 35 | 36 | // Both means that the client is acting as both a consumer and a producer. 37 | Both Mode = 0 38 | // Consumer means that the client is acting as a consumer. 39 | Consumer Mode = 1 40 | // Producer means that the client is acting as a producer. 41 | Producer Mode = 2 42 | 43 | ForceReconnectHeader = "rabbit-force-reconnect" 44 | ) 45 | 46 | var ( 47 | // ErrShutdown will be returned if the client is shutdown via Stop() or Close() 48 | ErrShutdown = errors.New("client is shutdown") 49 | 50 | // DefaultConsumerTag is used for identifying consumer 51 | DefaultConsumerTag = "c-rabbit-" + uuid.NewV4().String()[0:8] 52 | 53 | // DefaultAppID is used for identifying the producer 54 | DefaultAppID = "p-rabbit-" + uuid.NewV4().String()[0:8] 55 | ) 56 | 57 | // IRabbit is the interface that the `rabbit` library implements. It's here as 58 | // convenience. 59 | type IRabbit interface { 60 | Consume(ctx context.Context, errChan chan *ConsumeError, f func(msg amqp.Delivery) error, rp ...*RetryPolicy) 61 | ConsumeOnce(ctx context.Context, runFunc func(msg amqp.Delivery) error, rp ...*RetryPolicy) error 62 | Publish(ctx context.Context, routingKey string, payload []byte, headers ...amqp.Table) error 63 | Stop(timeout ...time.Duration) error 64 | Close() error 65 | } 66 | 67 | // Rabbit struct that is instantiated via `New()`. You should not instantiate 68 | // this struct by hand (unless you have a really good reason to do so). 69 | type Rabbit struct { 70 | Conn *amqp.Connection 71 | ConsumerDeliveryChannel <-chan amqp.Delivery 72 | ConsumerRWMutex *sync.RWMutex 73 | ConsumerWG *sync.WaitGroup 74 | NotifyCloseChan chan *amqp.Error 75 | ReconnectChan chan struct{} 76 | ReconnectInProgress bool 77 | ReconnectInProgressMtx *sync.RWMutex 78 | ProducerServerChannel *amqp.Channel 79 | ProducerRWMutex *sync.RWMutex 80 | Options *Options 81 | 82 | shutdown bool 83 | ctx context.Context 84 | cancel func() 85 | log Logger 86 | } 87 | 88 | // Mode is the type used to represent whether the RabbitMQ 89 | // clients is acting as a consumer, a producer, or both. 90 | type Mode int 91 | 92 | // Binding represents the information needed to bind a queue to 93 | // an Exchange. 94 | type Binding struct { 95 | // Required 96 | ExchangeName string 97 | 98 | // Bind a queue to one or more routing keys 99 | BindingKeys []string 100 | 101 | // Whether to declare/create exchange on connect 102 | ExchangeDeclare bool 103 | 104 | // Required if declaring queue (valid: direct, fanout, topic, headers) 105 | ExchangeType string 106 | 107 | // Whether exchange should survive/persist server restarts 108 | ExchangeDurable bool 109 | 110 | // Whether to delete exchange when its no longer used; used only if ExchangeDeclare set to true 111 | ExchangeAutoDelete bool 112 | } 113 | 114 | // Options determines how the `rabbit` library will behave and should be passed 115 | // in to rabbit via `New()`. Many of the options are optional (and will fall 116 | // back to sane defaults). 117 | type Options struct { 118 | // Required; format "amqp://user:pass@host:port" 119 | URLs []string 120 | 121 | // In what mode does the library operate (Both, Consumer, Producer) 122 | Mode Mode 123 | 124 | // If left empty, server will auto generate queue name 125 | QueueName string 126 | 127 | // Bindings is the set of information need to bind a queue to one or 128 | // more exchanges, specifying one or more binding (routing) keys. 129 | Bindings []Binding 130 | 131 | // https://godoc.org/github.com/streadway/amqp#Channel.Qos 132 | // Leave unset if no QoS preferences 133 | QosPrefetchCount int 134 | QosPrefetchSize int 135 | 136 | // How long to wait before we retry connecting to a server (after disconnect) 137 | RetryReconnectSec int 138 | 139 | // Whether queue should survive/persist server restarts (and there are no remaining bindings) 140 | QueueDurable bool 141 | 142 | // Whether consumer should be the sole consumer of the queue; used only if 143 | // QueueDeclare set to true 144 | QueueExclusive bool 145 | 146 | // Whether to delete queue on consumer disconnect; used only if QueueDeclare set to true 147 | QueueAutoDelete bool 148 | 149 | // Whether to declare/create queue on connect; used only if QueueDeclare set to true 150 | QueueDeclare bool 151 | 152 | // Additional arguments to pass to the queue declaration or binding 153 | // https://github.com/streamdal/plumber/issues/210 154 | QueueArgs map[string]interface{} 155 | 156 | // Whether to automatically acknowledge consumed message(s) 157 | AutoAck bool 158 | 159 | // Used for identifying consumer 160 | ConsumerTag string 161 | 162 | // Used as a property to identify producer 163 | AppID string 164 | 165 | // Use TLS 166 | UseTLS bool 167 | 168 | // Skip cert verification (only applies if UseTLS is true) 169 | SkipVerifyTLS bool 170 | 171 | // Log is the (optional) logger to use for writing out log messages. 172 | Log Logger 173 | } 174 | 175 | // ConsumeError will be passed down the error channel if/when `f()` func runs 176 | // into an error during `Consume()`. 177 | type ConsumeError struct { 178 | Message *amqp.Delivery 179 | Error error 180 | } 181 | 182 | // New is used for instantiating the library. 183 | func New(opts *Options) (*Rabbit, error) { 184 | if err := ValidateOptions(opts); err != nil { 185 | return nil, errors.Wrap(err, "invalid options") 186 | } 187 | 188 | var ac *amqp.Connection 189 | var err error 190 | 191 | // try all available URLs in a loop and quit as soon as it 192 | // can successfully establish a connection to one of them 193 | for _, url := range opts.URLs { 194 | if opts.UseTLS { 195 | tlsConfig := &tls.Config{} 196 | 197 | if opts.SkipVerifyTLS { 198 | tlsConfig.InsecureSkipVerify = true 199 | } 200 | 201 | ac, err = amqp.DialTLS(url, tlsConfig) 202 | } else { 203 | ac, err = amqp.Dial(url) 204 | } 205 | 206 | if err == nil { 207 | // yes, we made it! 208 | break 209 | } 210 | } 211 | 212 | if err != nil { 213 | return nil, errors.Wrap(err, "unable to dial server") 214 | } 215 | 216 | ctx, cancel := context.WithCancel(context.Background()) 217 | 218 | r := &Rabbit{ 219 | Conn: ac, 220 | ConsumerRWMutex: &sync.RWMutex{}, 221 | ConsumerWG: &sync.WaitGroup{}, 222 | NotifyCloseChan: make(chan *amqp.Error), 223 | ReconnectChan: make(chan struct{}, 1), 224 | ReconnectInProgress: false, 225 | ReconnectInProgressMtx: &sync.RWMutex{}, 226 | ProducerRWMutex: &sync.RWMutex{}, 227 | Options: opts, 228 | 229 | ctx: ctx, 230 | cancel: cancel, 231 | log: opts.Log, 232 | } 233 | 234 | if opts.Mode != Producer { 235 | if err := r.newConsumerChannel(); err != nil { 236 | return nil, errors.Wrap(err, "unable to get initial delivery channel") 237 | } 238 | } 239 | 240 | ac.NotifyClose(r.NotifyCloseChan) 241 | 242 | // Launch connection watcher/reconnect 243 | go r.runWatcher() 244 | 245 | return r, nil 246 | } 247 | 248 | // ValidateOptions validates various combinations of options. 249 | func ValidateOptions(opts *Options) error { 250 | if opts == nil { 251 | return errors.New("Options cannot be nil") 252 | } 253 | 254 | validURL := false 255 | for _, url := range opts.URLs { 256 | if len(url) > 0 { 257 | validURL = true 258 | break 259 | } 260 | } 261 | 262 | if !validURL { 263 | return errors.New("At least one non-empty URL must be provided") 264 | } 265 | 266 | if len(opts.Bindings) == 0 { 267 | return errors.New("At least one Exchange must be specified") 268 | } 269 | 270 | if err := validateBindings(opts); err != nil { 271 | return errors.Wrap(err, "binding validation failed") 272 | } 273 | 274 | applyDefaults(opts) 275 | 276 | if err := validMode(opts.Mode); err != nil { 277 | return err 278 | } 279 | 280 | return nil 281 | } 282 | 283 | func validateBindings(opts *Options) error { 284 | if opts.Mode == Producer || opts.Mode == Both { 285 | if len(opts.Bindings) > 1 { 286 | return errors.New("Exactly one Exchange must be specified when publishing messages") 287 | } 288 | } 289 | 290 | for _, binding := range opts.Bindings { 291 | if binding.ExchangeDeclare { 292 | if binding.ExchangeType == "" { 293 | return errors.New("ExchangeType cannot be empty if ExchangeDeclare set to true") 294 | } 295 | } 296 | if binding.ExchangeName == "" { 297 | return errors.New("ExchangeName cannot be empty") 298 | } 299 | 300 | // BindingKeys are only needed if Consumer or Both 301 | if opts.Mode != Producer { 302 | if len(binding.BindingKeys) < 1 { 303 | return errors.New("At least one BindingKeys must be specified") 304 | } 305 | } 306 | } 307 | 308 | return nil 309 | } 310 | 311 | func applyDefaults(opts *Options) { 312 | if opts == nil { 313 | return 314 | } 315 | 316 | if opts.RetryReconnectSec == 0 { 317 | opts.RetryReconnectSec = DefaultRetryReconnectSec 318 | } 319 | 320 | if opts.AppID == "" { 321 | opts.AppID = DefaultAppID 322 | } 323 | 324 | if opts.ConsumerTag == "" { 325 | opts.ConsumerTag = DefaultConsumerTag 326 | } 327 | 328 | if opts.Log == nil { 329 | opts.Log = &NoOpLogger{} 330 | } 331 | 332 | if opts.QueueArgs == nil { 333 | opts.QueueArgs = make(map[string]interface{}) 334 | } 335 | } 336 | 337 | func validMode(mode Mode) error { 338 | validModes := []Mode{Both, Producer, Consumer} 339 | 340 | var found bool 341 | 342 | for _, validMode := range validModes { 343 | if validMode == mode { 344 | found = true 345 | } 346 | } 347 | 348 | if !found { 349 | return fmt.Errorf("invalid mode '%d'", mode) 350 | } 351 | 352 | return nil 353 | } 354 | 355 | // Consume consumes messages from the configured queue (`Options.QueueName`) and 356 | // executes `f` for every received message. 357 | // 358 | // `Consume()` will block until it is stopped either via the passed in `ctx` OR 359 | // by calling `Stop()` 360 | // 361 | // It is also possible to see the errors that `f()` runs into by passing in an 362 | // error channel (`chan *ConsumeError`). 363 | // 364 | // Both `ctx` and `errChan` can be `nil`. 365 | // 366 | // If the server goes away, `Consume` will automatically attempt to reconnect. 367 | // Subsequent reconnect attempts will sleep/wait for `DefaultRetryReconnectSec` 368 | // between attempts. 369 | func (r *Rabbit) Consume(ctx context.Context, errChan chan *ConsumeError, f func(msg amqp.Delivery) error, rp ...*RetryPolicy) { 370 | var retry *RetryPolicy 371 | if len(rp) > 0 { 372 | retry = rp[0] 373 | } 374 | 375 | if r.shutdown { 376 | r.log.Error(ErrShutdown) 377 | return 378 | } 379 | 380 | if r.Options.Mode == Producer { 381 | r.log.Error("unable to Consume() - library is configured in Producer mode") 382 | return 383 | } 384 | 385 | r.ConsumerWG.Add(1) 386 | defer r.ConsumerWG.Done() 387 | 388 | if ctx == nil { 389 | ctx = context.Background() 390 | } 391 | 392 | r.log.Debug("waiting for messages from rabbit ...") 393 | 394 | var retries int 395 | 396 | MAIN: 397 | for { 398 | select { 399 | case msg := <-r.delivery(): 400 | if _, ok := msg.Headers[ForceReconnectHeader]; ok || msg.Acknowledger == nil { 401 | r.writeError(errChan, &ConsumeError{ 402 | Message: &msg, 403 | Error: errors.New("nil acknowledger detected - sending reconnect signal"), 404 | }) 405 | 406 | r.ReconnectChan <- struct{}{} 407 | 408 | // No point in continuing execution of consumer func as the 409 | // delivery msg is incomplete/invalid. 410 | continue 411 | } 412 | 413 | RETRY: 414 | for { 415 | if err := f(msg); err != nil { 416 | if retry != nil && retry.ShouldRetry() { 417 | dur := retry.Duration(retries) 418 | 419 | r.writeError(errChan, &ConsumeError{ 420 | Message: &msg, 421 | Error: fmt.Errorf("[Retry %s] error during consume: %s", retry.AttemptCount(), err), 422 | }) 423 | 424 | time.Sleep(dur) 425 | retries++ 426 | continue RETRY 427 | } 428 | 429 | r.writeError(errChan, &ConsumeError{ 430 | Message: &msg, 431 | Error: fmt.Errorf("error during consume: %s", err), 432 | }) 433 | 434 | // We're not retrying here, break out of retry loop and return 435 | // control flow to MAIN's loop 436 | break 437 | } 438 | 439 | // Exit retry loop on success 440 | break 441 | } 442 | case <-ctx.Done(): 443 | r.log.Warn("Consume stopped via local context") 444 | break MAIN 445 | case <-r.ctx.Done(): 446 | r.log.Warn("Consume stopped via global context") 447 | break MAIN 448 | } 449 | } 450 | 451 | r.log.Debug("Consume finished - exiting") 452 | } 453 | 454 | func (r *Rabbit) writeError(errChan chan *ConsumeError, err *ConsumeError) { 455 | if err == nil { 456 | r.log.Error("nil 'err' passed to writeError - bug?") 457 | return 458 | } 459 | 460 | r.log.Warnf("writeError(): %s", err.Error) 461 | 462 | if errChan == nil { 463 | // Don't have an error channel, nothing else to do 464 | return 465 | } 466 | 467 | // Only write to errChan if it's not full (to avoid goroutine leak) 468 | if len(errChan) > 0 { 469 | r.log.Warn("errChan is full - dropping message") 470 | return 471 | } 472 | 473 | go func() { 474 | errChan <- err 475 | }() 476 | } 477 | 478 | // ConsumeOnce will consume exactly one message from the configured queue, 479 | // execute `runFunc()` on the message and return. 480 | // 481 | // Same as with `Consume()`, you can pass in a context to cancel `ConsumeOnce()` 482 | // or run `Stop()`. 483 | func (r *Rabbit) ConsumeOnce(ctx context.Context, runFunc func(msg amqp.Delivery) error, rp ...*RetryPolicy) error { 484 | var retry *RetryPolicy 485 | if len(rp) > 0 { 486 | retry = rp[0] 487 | } 488 | 489 | if r.shutdown { 490 | return ErrShutdown 491 | } 492 | 493 | if r.Options.Mode == Producer { 494 | return errors.New("unable to ConsumeOnce - library is configured in Producer mode") 495 | } 496 | 497 | if ctx == nil { 498 | ctx = context.Background() 499 | } 500 | 501 | r.log.Debug("waiting for a single message from rabbit ...") 502 | 503 | var retries int 504 | 505 | select { 506 | case msg := <-r.delivery(): 507 | if msg.Acknowledger == nil { 508 | r.log.Warn("Detected nil acknowledger - sending signal to rabbit lib to reconnect") 509 | 510 | r.ReconnectChan <- struct{}{} 511 | 512 | return errors.New("detected nil acknowledger - sent signal to reconnect to RabbitMQ") 513 | } 514 | 515 | RETRY: 516 | for { 517 | if err := runFunc(msg); err != nil { 518 | if retry != nil && retry.ShouldRetry() { 519 | dur := retry.Duration(retries) 520 | 521 | r.log.Warnf("[Retry %s] error during consume: %s", retry.AttemptCount(), err) 522 | 523 | time.Sleep(dur) 524 | retries++ 525 | continue RETRY 526 | } 527 | 528 | r.log.Debug("ConsumeOnce finished - exiting") 529 | return err 530 | } 531 | 532 | break 533 | } 534 | case <-ctx.Done(): 535 | r.log.Warn("ConsumeOnce stopped via local context") 536 | 537 | return nil 538 | case <-r.ctx.Done(): 539 | r.log.Warn("ConsumeOnce stopped via global context") 540 | return nil 541 | } 542 | 543 | r.log.Debug("ConsumeOnce finished - exiting") 544 | 545 | return nil 546 | } 547 | 548 | // Publish publishes one message to the configured exchange, using the specified 549 | // routing key. 550 | func (r *Rabbit) Publish(ctx context.Context, routingKey string, body []byte, headers ...amqp.Table) error { 551 | if ctx == nil { 552 | ctx = context.Background() 553 | } 554 | 555 | if r.shutdown { 556 | return ErrShutdown 557 | } 558 | 559 | if r.Options.Mode == Consumer { 560 | return errors.New("unable to Publish - library is configured in Consumer mode") 561 | } 562 | 563 | // Is this the first time we're publishing? 564 | if r.ProducerServerChannel == nil { 565 | ch, err := r.newServerChannel() 566 | if err != nil { 567 | return errors.Wrap(err, "unable to create server channel") 568 | } 569 | 570 | r.ProducerRWMutex.Lock() 571 | r.ProducerServerChannel = ch 572 | r.ProducerRWMutex.Unlock() 573 | } 574 | 575 | r.ProducerRWMutex.RLock() 576 | defer r.ProducerRWMutex.RUnlock() 577 | 578 | // Create channels for error and done signals 579 | chanErr := make(chan error) 580 | chanDone := make(chan struct{}) 581 | 582 | go func() { 583 | var realHeaders amqp.Table 584 | 585 | if len(headers) > 0 { 586 | realHeaders = headers[0] 587 | } 588 | 589 | if err := r.ProducerServerChannel.Publish(r.Options.Bindings[0].ExchangeName, routingKey, false, false, amqp.Publishing{ 590 | DeliveryMode: amqp.Persistent, 591 | Body: body, 592 | AppId: r.Options.AppID, 593 | Headers: realHeaders, 594 | }); err != nil { 595 | // Signal there is an error 596 | chanErr <- err 597 | } 598 | 599 | // Signal we are done 600 | chanDone <- struct{}{} 601 | }() 602 | 603 | select { 604 | case <-chanDone: 605 | // We did it! 606 | return nil 607 | case err := <-chanErr: 608 | return errors.Wrap(err, "failed to publish message") 609 | case <-ctx.Done(): 610 | r.log.Warn("stopped via context") 611 | err := r.ProducerServerChannel.Close() 612 | if err != nil { 613 | return errors.Wrap(err, "failed to close producer channel") 614 | } 615 | return errors.New("context cancelled") 616 | } 617 | } 618 | 619 | // Stop stops an in-progress `Consume()` or `ConsumeOnce()` 620 | func (r *Rabbit) Stop(timeout ...time.Duration) error { 621 | r.cancel() 622 | 623 | doneCh := make(chan struct{}) 624 | 625 | // This will leak if consumer(s) don't exit within timeout 626 | go func() { 627 | r.ConsumerWG.Wait() 628 | doneCh <- struct{}{} 629 | }() 630 | 631 | stopTimeout := DefaultStopTimeout 632 | 633 | if len(timeout) > 0 { 634 | stopTimeout = timeout[0] 635 | } 636 | 637 | select { 638 | case <-doneCh: 639 | return nil 640 | case <-time.After(stopTimeout): 641 | return fmt.Errorf("timeout waiting for consumer to stop after '%v'", stopTimeout) 642 | } 643 | } 644 | 645 | // Close stops any active Consume and closes the amqp connection (and channels using the conn) 646 | // 647 | // You should re-instantiate the rabbit lib once this is called. 648 | func (r *Rabbit) Close() error { 649 | if r.shutdown { 650 | return ErrShutdown 651 | } 652 | 653 | r.cancel() 654 | 655 | if err := r.Conn.Close(); err != nil { 656 | return fmt.Errorf("unable to close amqp connection: %s", err) 657 | } 658 | 659 | r.shutdown = true 660 | 661 | return nil 662 | } 663 | 664 | func (r *Rabbit) getReconnectInProgress() bool { 665 | r.ReconnectInProgressMtx.RLock() 666 | defer r.ReconnectInProgressMtx.RUnlock() 667 | 668 | return r.ReconnectInProgress 669 | } 670 | 671 | func (r *Rabbit) runWatcher() { 672 | for { 673 | select { 674 | case closeErr := <-r.NotifyCloseChan: 675 | r.log.Debugf("received message on notify close channel: '%+v' (reconnecting)", closeErr) 676 | case <-r.ReconnectChan: 677 | if r.getReconnectInProgress() { 678 | // Already reconnecting, nothing to do 679 | r.log.Debug("received reconnect signal (already reconnecting)") 680 | return 681 | } 682 | 683 | r.ReconnectInProgressMtx.Lock() 684 | r.ReconnectInProgress = true 685 | 686 | r.log.Debug("received reconnect signal (reconnecting)") 687 | } 688 | 689 | // Acquire mutex to pause all consumers/producers while we reconnect AND prevent 690 | // access to the channel map 691 | r.ConsumerRWMutex.Lock() 692 | r.ProducerRWMutex.Lock() 693 | 694 | var attempts int 695 | 696 | for { 697 | attempts++ 698 | if err := r.reconnect(); err != nil { 699 | r.log.Warnf("unable to complete reconnect: %s; retrying in %d", err, r.Options.RetryReconnectSec) 700 | time.Sleep(time.Duration(r.Options.RetryReconnectSec) * time.Second) 701 | continue 702 | } 703 | 704 | r.log.Debugf("successfully reconnected after %d attempts", attempts) 705 | 706 | break 707 | } 708 | 709 | // Create and set a new notify close channel (since old one may have gotten shutdown) 710 | r.NotifyCloseChan = make(chan *amqp.Error, 0) 711 | r.Conn.NotifyClose(r.NotifyCloseChan) 712 | 713 | // Update channel 714 | if r.Options.Mode == Producer { 715 | serverChannel, err := r.newServerChannel() 716 | if err != nil { 717 | r.log.Errorf("unable to set new channel: %s", err) 718 | panic(fmt.Sprintf("unable to set new channel: %s", err)) 719 | } 720 | 721 | r.ProducerServerChannel = serverChannel 722 | } else { 723 | if err := r.newConsumerChannel(); err != nil { 724 | r.log.Errorf("unable to set new channel: %s", err) 725 | 726 | // TODO: This is super shitty. Should address this. 727 | panic(fmt.Sprintf("unable to set new channel: %s", err)) 728 | } 729 | } 730 | 731 | // Unlock so that consumers/producers can begin reading messages from a new channel 732 | r.ConsumerRWMutex.Unlock() 733 | r.ProducerRWMutex.Unlock() 734 | 735 | // If this was a requested reconnect - reset in progress flag 736 | if r.ReconnectInProgress { 737 | r.ReconnectInProgress = false 738 | r.ReconnectInProgressMtx.Unlock() 739 | } 740 | 741 | r.log.Debug("runWatcher iteration has completed successfully") 742 | } 743 | } 744 | 745 | func (r *Rabbit) newServerChannel() (*amqp.Channel, error) { 746 | if r.Conn == nil { 747 | return nil, errors.New("r.Conn is nil - did this get instantiated correctly? bug?") 748 | } 749 | 750 | ch, err := r.Conn.Channel() 751 | if err != nil { 752 | return nil, errors.Wrap(err, "unable to instantiate channel") 753 | } 754 | 755 | if err := ch.Qos(r.Options.QosPrefetchCount, r.Options.QosPrefetchSize, false); err != nil { 756 | return nil, errors.Wrap(err, "unable to set qos policy") 757 | } 758 | 759 | // Only declare queue if in Both or Consumer mode 760 | if r.Options.Mode != Producer { 761 | if r.Options.QueueDeclare { 762 | if _, err := ch.QueueDeclare( 763 | r.Options.QueueName, 764 | r.Options.QueueDurable, 765 | r.Options.QueueAutoDelete, 766 | r.Options.QueueExclusive, 767 | false, 768 | r.Options.QueueArgs, 769 | ); err != nil { 770 | return nil, err 771 | } 772 | } 773 | } 774 | 775 | for _, binding := range r.Options.Bindings { 776 | if binding.ExchangeDeclare { 777 | if err := ch.ExchangeDeclare( 778 | binding.ExchangeName, 779 | binding.ExchangeType, 780 | binding.ExchangeDurable, 781 | binding.ExchangeAutoDelete, 782 | false, 783 | false, 784 | nil, 785 | ); err != nil { 786 | return nil, errors.Wrap(err, "unable to declare exchange") 787 | } 788 | } 789 | 790 | // Only bind queue if in Both or Consumer mode 791 | if r.Options.Mode != Producer { 792 | for _, bindingKey := range binding.BindingKeys { 793 | if err := ch.QueueBind( 794 | r.Options.QueueName, 795 | bindingKey, 796 | binding.ExchangeName, 797 | false, 798 | r.Options.QueueArgs, 799 | ); err != nil { 800 | return nil, errors.Wrap(err, "unable to bind queue") 801 | } 802 | } 803 | } 804 | } 805 | 806 | return ch, nil 807 | } 808 | 809 | func (r *Rabbit) newConsumerChannel() error { 810 | serverChannel, err := r.newServerChannel() 811 | if err != nil { 812 | return errors.Wrap(err, "unable to create new server channel") 813 | } 814 | 815 | deliveryChannel, err := serverChannel.Consume( 816 | r.Options.QueueName, 817 | r.Options.ConsumerTag, 818 | r.Options.AutoAck, 819 | r.Options.QueueExclusive, 820 | false, 821 | false, 822 | nil, 823 | ) 824 | if err != nil { 825 | return errors.Wrap(err, "unable to create delivery channel") 826 | } 827 | 828 | r.ProducerServerChannel = serverChannel 829 | r.ConsumerDeliveryChannel = deliveryChannel 830 | 831 | return nil 832 | } 833 | 834 | func (r *Rabbit) reconnect() error { 835 | var ac *amqp.Connection 836 | var err error 837 | 838 | // try all available URLs in a loop and quit as soon as it 839 | // can successfully establish a connection to one of them 840 | for _, url := range r.Options.URLs { 841 | if r.Options.UseTLS { 842 | tlsConfig := &tls.Config{} 843 | 844 | if r.Options.SkipVerifyTLS { 845 | tlsConfig.InsecureSkipVerify = true 846 | } 847 | 848 | ac, err = amqp.DialTLS(url, tlsConfig) 849 | } else { 850 | ac, err = amqp.Dial(url) 851 | } 852 | 853 | if err == nil { 854 | // yes, we made it! 855 | break 856 | } 857 | } 858 | 859 | if err != nil { 860 | return errors.Wrap(err, "all servers failed on reconnect") 861 | } 862 | 863 | r.Conn = ac 864 | 865 | return nil 866 | } 867 | 868 | func (r *Rabbit) delivery() <-chan amqp.Delivery { 869 | // Acquire lock (in case we are reconnecting and channels are being swapped) 870 | r.ConsumerRWMutex.RLock() 871 | defer r.ConsumerRWMutex.RUnlock() 872 | 873 | return r.ConsumerDeliveryChannel 874 | } 875 | -------------------------------------------------------------------------------- /rabbit_suite_test.go: -------------------------------------------------------------------------------- 1 | package rabbit 2 | 3 | import ( 4 | "testing" 5 | 6 | . "github.com/onsi/ginkgo" 7 | . "github.com/onsi/gomega" 8 | ) 9 | 10 | func TestRabbitSuite(t *testing.T) { 11 | 12 | RegisterFailHandler(Fail) 13 | RunSpecs(t, "Rabbit Suite") 14 | } 15 | -------------------------------------------------------------------------------- /rabbit_test.go: -------------------------------------------------------------------------------- 1 | // NOTE: These tests require RabbitMQ to be available on "amqp://localhost" 2 | // 3 | // Make sure that you do `docker-compose up` before running tests 4 | package rabbit 5 | 6 | import ( 7 | "context" 8 | "log" 9 | "sync" 10 | "time" 11 | 12 | . "github.com/onsi/ginkgo" 13 | . "github.com/onsi/gomega" 14 | "github.com/pkg/errors" 15 | uuid "github.com/satori/go.uuid" 16 | // to test with logrus, uncomment the following 17 | // and the log initialiser in generateOptions() 18 | // "github.com/sirupsen/logrus" 19 | amqp "github.com/rabbitmq/amqp091-go" 20 | ) 21 | 22 | var _ = Describe("Rabbit", func() { 23 | var ( 24 | opts *Options 25 | r *Rabbit 26 | ch *amqp.Channel 27 | ) 28 | 29 | // This runs *after* all of the BeforeEach's AND *before* each It() block 30 | JustBeforeEach(func() { 31 | var err error 32 | 33 | opts = generateOptions() 34 | 35 | r, err = New(opts) 36 | 37 | Expect(err).ToNot(HaveOccurred()) 38 | Expect(r).ToNot(BeNil()) 39 | 40 | ch, err = connect(opts) 41 | Expect(err).ToNot(HaveOccurred()) 42 | Expect(ch).ToNot(BeNil()) 43 | }) 44 | 45 | Describe("New", func() { 46 | When("instantiating rabbit", func() { 47 | It("happy: should return a rabbit instance", func() { 48 | opts := generateOptions() 49 | 50 | r, err := New(opts) 51 | 52 | Expect(err).ToNot(HaveOccurred()) 53 | Expect(r).ToNot(BeNil()) 54 | }) 55 | 56 | It("by default, uses Both mode", func() { 57 | Expect(opts.Mode).To(Equal(Both)) 58 | Expect(r.Options.Mode).To(Equal(Both)) 59 | }) 60 | 61 | It("should error with missing options", func() { 62 | r, err := New(nil) 63 | 64 | Expect(err).ToNot(BeNil()) 65 | Expect(err.Error()).To(ContainSubstring("cannot be nil")) 66 | Expect(r).To(BeNil()) 67 | }) 68 | 69 | It("should error with unreachable rabbit server", func() { 70 | opts := generateOptions() 71 | opts.URLs = []string{"amqp://bad-url"} 72 | 73 | r, err := New(opts) 74 | 75 | Expect(err).ToNot(BeNil()) 76 | Expect(err.Error()).To(ContainSubstring("unable to dial server")) 77 | Expect(r).To(BeNil()) 78 | }) 79 | 80 | It("happy: it should succeed after having failed on unreachable rabbit server", func() { 81 | opts := generateOptions() 82 | opts.URLs = []string{"amqp://bad-url", "amqp://localhost"} 83 | 84 | r, err := New(opts) 85 | 86 | Expect(err).To(BeNil()) 87 | Expect(r).ToNot(BeNil()) 88 | }) 89 | 90 | It("instantiates various internals", func() { 91 | opts := generateOptions() 92 | 93 | r, err := New(opts) 94 | 95 | Expect(err).To(BeNil()) 96 | Expect(r).ToNot(BeNil()) 97 | 98 | Expect(r.ctx).ToNot(BeNil()) 99 | Expect(r.cancel).ToNot(BeNil()) 100 | Expect(r.Conn).ToNot(BeNil()) 101 | Expect(r.ConsumerRWMutex).ToNot(BeNil()) 102 | Expect(r.NotifyCloseChan).ToNot(BeNil()) 103 | Expect(r.ProducerRWMutex).ToNot(BeNil()) 104 | Expect(r.Options).ToNot(BeNil()) 105 | }) 106 | 107 | It("launches NotifyCloseChan watcher", func() { 108 | opts := generateOptions() 109 | 110 | r, err := New(opts) 111 | 112 | Expect(err).To(BeNil()) 113 | Expect(r).ToNot(BeNil()) 114 | 115 | // Before we write errors to the notify channel, copy previous 116 | // conn and channels so we can compare them after reconnect 117 | oldConn := r.Conn 118 | oldNotifyCloseChan := r.NotifyCloseChan 119 | oldConsumerDeliveryChannel := r.ConsumerDeliveryChannel 120 | 121 | // Write an error to the NotifyCloseChan 122 | r.NotifyCloseChan <- &amqp.Error{ 123 | Code: 0, 124 | Reason: "Test failure", 125 | Server: false, 126 | Recover: false, 127 | } 128 | 129 | // Give our watcher a moment to see the msg and cause a reconnect 130 | time.Sleep(100 * time.Millisecond) 131 | 132 | // We should've reconnected and got a new conn 133 | Expect(r.Conn).ToNot(BeNil()) 134 | Expect(r.Conn).To(BeAssignableToTypeOf(&amqp.Connection{})) 135 | Expect(oldConn).ToNot(Equal(r.Conn)) 136 | 137 | // We should also get new channels 138 | Expect(r.NotifyCloseChan).ToNot(BeNil()) 139 | Expect(r.ConsumerDeliveryChannel).ToNot(BeNil()) 140 | Expect(oldNotifyCloseChan).ToNot(Equal(r.NotifyCloseChan)) 141 | Expect(oldConsumerDeliveryChannel).ToNot(Equal(r.ConsumerDeliveryChannel)) 142 | }) 143 | }) 144 | }) 145 | 146 | Describe("Consume", func() { 147 | var ( 148 | errChan = make(chan *ConsumeError, 1) 149 | ) 150 | 151 | When("attempting to consume messages in producer mode", func() { 152 | It("Consume should not block and immediately return", func() { 153 | opts.Mode = Producer 154 | ra, err := New(opts) 155 | 156 | Expect(err).ToNot(HaveOccurred()) 157 | Expect(ra).ToNot(BeNil()) 158 | 159 | var exit bool 160 | 161 | go func() { 162 | r.Consume(nil, nil, func(m amqp.Delivery) error { 163 | return nil 164 | }) 165 | 166 | exit = true 167 | }() 168 | 169 | // Give the goroutine a little to start up 170 | time.Sleep(50 * time.Millisecond) 171 | 172 | Expect(exit).To(BeTrue()) 173 | }) 174 | }) 175 | 176 | When("consuming messages with a context", func() { 177 | It("run function is executed with inbound message", func() { 178 | receivedMessages := make([]amqp.Delivery, 0) 179 | 180 | // Launch consumer 181 | go func() { 182 | r.Consume(context.Background(), errChan, func(msg amqp.Delivery) error { 183 | receivedMessages = append(receivedMessages, msg) 184 | return nil 185 | }) 186 | }() 187 | 188 | messages := generateRandomStrings(10) 189 | 190 | // Publish messages 191 | publishErr := publishMessages(ch, opts, messages) 192 | Expect(publishErr).To(BeNil()) 193 | 194 | // Wait 195 | time.Sleep(100 * time.Millisecond) 196 | 197 | // Verify messages we received 198 | stopErr := r.Stop() 199 | Expect(stopErr).ToNot(HaveOccurred()) 200 | 201 | var data []string 202 | 203 | // Verify message attributes 204 | for _, msg := range receivedMessages { 205 | Expect(msg.Exchange).To(Equal(opts.Bindings[0].ExchangeName)) 206 | Expect(msg.RoutingKey).To(Equal(opts.Bindings[0].BindingKeys[0])) 207 | Expect(msg.ConsumerTag).To(Equal(opts.ConsumerTag)) 208 | 209 | data = append(data, string(msg.Body)) 210 | } 211 | 212 | Expect(messages).To(Equal(data)) 213 | }) 214 | 215 | It("context can be used to cancel consume", func() { 216 | ctx, cancel := context.WithCancel(context.Background()) 217 | receivedMessages := make([]amqp.Delivery, 0) 218 | 219 | var exit bool 220 | 221 | // Launch consumer 222 | go func() { 223 | r.Consume(ctx, errChan, func(msg amqp.Delivery) error { 224 | receivedMessages = append(receivedMessages, msg) 225 | return nil 226 | }) 227 | 228 | exit = true 229 | }() 230 | 231 | messages := generateRandomStrings(20) 232 | 233 | // Publish 10 messages -> cancel -> publish remainder of messages -> 234 | // verify runfunc was hit only 10 times 235 | publishErr1 := publishMessages(ch, opts, messages[0:10]) 236 | Expect(publishErr1).ToNot(HaveOccurred()) 237 | 238 | // Wait a moment for consumer to pick up messages 239 | time.Sleep(100 * time.Millisecond) 240 | 241 | cancel() 242 | 243 | // Wait a moment for consumer to quit 244 | time.Sleep(100 * time.Millisecond) 245 | 246 | publishErr2 := publishMessages(ch, opts, messages[10:]) 247 | Expect(publishErr2).ToNot(HaveOccurred()) 248 | 249 | Expect(len(receivedMessages)).To(Equal(10)) 250 | Expect(exit).To(BeTrue()) 251 | }) 252 | }) 253 | 254 | When("consuming messages with an error channel", func() { 255 | It("any errors returned by run func are passed to error channel", func() { 256 | go func() { 257 | r.Consume(context.Background(), errChan, func(msg amqp.Delivery) error { 258 | return errors.New("stuff broke") 259 | }) 260 | }() 261 | 262 | messages := generateRandomStrings(1) 263 | 264 | publishErr := publishMessages(ch, opts, messages) 265 | Expect(publishErr).ToNot(HaveOccurred()) 266 | 267 | Eventually(func() string { 268 | consumeErr := <-errChan 269 | return consumeErr.Error.Error() 270 | }).Should(ContainSubstring("stuff broke")) 271 | }) 272 | }) 273 | 274 | When("when a nil error channel is passed in", func() { 275 | It("errors are discarded and Consume() continues to work", func() { 276 | receivedMessages := make([]string, 0) 277 | 278 | go func() { 279 | r.Consume(context.Background(), nil, func(msg amqp.Delivery) error { 280 | receivedMessages = append(receivedMessages, string(msg.Body)) 281 | return errors.New("stuff broke") 282 | }) 283 | }() 284 | 285 | // Publish a handful of messages 286 | messages := generateRandomStrings(10) 287 | 288 | publishErr := publishMessages(ch, opts, messages) 289 | Expect(publishErr).To(BeNil()) 290 | 291 | // Wait 292 | time.Sleep(100 * time.Millisecond) 293 | 294 | // Verify messages we received 295 | stopErr := r.Stop() 296 | Expect(stopErr).ToNot(HaveOccurred()) 297 | 298 | // Verify message attributes 299 | Expect(messages).To(Equal(receivedMessages)) 300 | }) 301 | }) 302 | 303 | When("a nil context is passed in", func() { 304 | It("Consume() continues to work", func() { 305 | receivedMessages := make([]string, 0) 306 | 307 | go func() { 308 | r.Consume(nil, nil, func(msg amqp.Delivery) error { 309 | receivedMessages = append(receivedMessages, string(msg.Body)) 310 | return errors.New("stuff broke") 311 | }) 312 | }() 313 | 314 | // Publish a handful of messages 315 | messages := generateRandomStrings(10) 316 | 317 | publishErr := publishMessages(ch, opts, messages) 318 | Expect(publishErr).To(BeNil()) 319 | 320 | // Wait 321 | time.Sleep(100 * time.Millisecond) 322 | 323 | // Verify messages we received 324 | stopErr := r.Stop() 325 | Expect(stopErr).ToNot(HaveOccurred()) 326 | 327 | // Verify message attributes 328 | Expect(messages).To(Equal(receivedMessages)) 329 | }) 330 | }) 331 | }) 332 | 333 | Describe("ConsumeOnce", func() { 334 | When("Mode is Producer", func() { 335 | It("will return an error", func() { 336 | opts.Mode = Producer 337 | ra, err := New(opts) 338 | 339 | Expect(err).ToNot(HaveOccurred()) 340 | Expect(ra).ToNot(BeNil()) 341 | 342 | err = ra.ConsumeOnce(nil, func(m amqp.Delivery) error { return nil }) 343 | 344 | Expect(err).To(HaveOccurred()) 345 | Expect(err.Error()).To(ContainSubstring("library is configured in Producer mode")) 346 | }) 347 | }) 348 | 349 | When("passed context is nil", func() { 350 | It("will continue to work", func() { 351 | var receivedMessage string 352 | var consumeErr error 353 | var exit bool 354 | 355 | go func() { 356 | consumeErr = r.ConsumeOnce(nil, func(msg amqp.Delivery) error { 357 | receivedMessage = string(msg.Body) 358 | return nil 359 | }) 360 | 361 | exit = true 362 | }() 363 | 364 | // Wait a moment for consumer to start 365 | time.Sleep(100 * time.Millisecond) 366 | 367 | // Generate a handful of messages 368 | messages := generateRandomStrings(10) 369 | 370 | publishErr := publishMessages(ch, opts, messages) 371 | 372 | Expect(publishErr).ToNot(HaveOccurred()) 373 | 374 | // Wait a moment for consumer to get the message 375 | time.Sleep(100 * time.Millisecond) 376 | 377 | Expect(consumeErr).ToNot(HaveOccurred()) 378 | 379 | // Received message should be the same as the first message 380 | Expect(receivedMessage).To(Equal(messages[0])) 381 | 382 | // Goroutine should've exited 383 | Expect(exit).To(BeTrue()) 384 | }) 385 | }) 386 | 387 | When("context is passed", func() { 388 | It("will listen for cancellation", func() { 389 | var consumeErr error 390 | var exit bool 391 | var receivedMessage string 392 | 393 | ctx, cancel := context.WithCancel(context.Background()) 394 | 395 | go func() { 396 | consumeErr = r.ConsumeOnce(ctx, func(msg amqp.Delivery) error { 397 | receivedMessage = string(msg.Body) 398 | return nil 399 | }) 400 | 401 | exit = true 402 | }() 403 | 404 | // Wait a moment for consumer to connect 405 | time.Sleep(100 * time.Millisecond) 406 | 407 | // Consumer should not have received a message or exited 408 | Expect(receivedMessage).To(BeEmpty()) 409 | Expect(exit).To(BeFalse()) 410 | 411 | cancel() 412 | 413 | // Wait for cancel to kick in 414 | time.Sleep(100 * time.Millisecond) 415 | 416 | // Goroutine should've exited 417 | Expect(exit).To(BeTrue()) 418 | Expect(consumeErr).ToNot(HaveOccurred()) 419 | }) 420 | }) 421 | 422 | When("run func gets an error", func() { 423 | It("will return the error to the user", func() { 424 | var consumeErr error 425 | var exit bool 426 | 427 | go func() { 428 | consumeErr = r.ConsumeOnce(nil, func(msg amqp.Delivery) error { 429 | return errors.New("something broke") 430 | }) 431 | 432 | exit = true 433 | }() 434 | 435 | // Wait a moment for consumer to connect 436 | time.Sleep(100 * time.Millisecond) 437 | 438 | // Generate and send a message 439 | messages := generateRandomStrings(1) 440 | publishErr := publishMessages(ch, opts, messages) 441 | 442 | Expect(publishErr).ToNot(HaveOccurred()) 443 | 444 | // Wait a moment for consumer to receive the message 445 | time.Sleep(100 * time.Millisecond) 446 | 447 | // Goroutine should've exited 448 | Expect(exit).To(BeTrue()) 449 | 450 | // Consumer should've returned correct error 451 | Expect(consumeErr).To(HaveOccurred()) 452 | Expect(consumeErr.Error()).To(ContainSubstring("something broke")) 453 | 454 | }) 455 | }) 456 | }) 457 | 458 | Describe("Publish", func() { 459 | Context("happy path", func() { 460 | It("correctly publishes message", func() { 461 | var receivedMessage *amqp.Delivery 462 | 463 | go func() { 464 | var err error 465 | receivedMessage, err = receiveMessage(ch, opts) 466 | 467 | Expect(err).ToNot(HaveOccurred()) 468 | }() 469 | 470 | time.Sleep(25 * time.Millisecond) 471 | 472 | testMessage := []byte(uuid.NewV4().String()) 473 | publishErr := r.Publish(nil, opts.Bindings[0].BindingKeys[0], testMessage) 474 | 475 | Expect(publishErr).ToNot(HaveOccurred()) 476 | 477 | // Give our consumer some time to receive the message 478 | time.Sleep(100 * time.Millisecond) 479 | 480 | Expect(receivedMessage.Body).To(Equal(testMessage)) 481 | Expect(receivedMessage.AppId).To(Equal(opts.AppID)) 482 | }) 483 | 484 | When("Mode is Consumer", func() { 485 | It("should return an error", func() { 486 | opts.Mode = Consumer 487 | ra, err := New(opts) 488 | 489 | Expect(err).ToNot(HaveOccurred()) 490 | Expect(ra).ToNot(BeNil()) 491 | 492 | err = ra.Publish(nil, "messages", []byte("test")) 493 | 494 | Expect(err).To(HaveOccurred()) 495 | Expect(err.Error()).To(ContainSubstring("library is configured in Consumer mode")) 496 | }) 497 | }) 498 | }) 499 | 500 | Context("context timeout", func() { 501 | It("returns an error on context timeout", func() { 502 | ctx, cancel := context.WithCancel(context.Background()) 503 | cancel() 504 | 505 | testMessage := []byte(uuid.NewV4().String()) 506 | publishErr := r.Publish(ctx, opts.Bindings[0].BindingKeys[0], testMessage) 507 | 508 | Expect(publishErr).To(HaveOccurred()) 509 | }) 510 | }) 511 | 512 | When("producer server channel is nil", func() { 513 | It("will generate a new server channel", func() { 514 | r.ProducerServerChannel = nil 515 | 516 | var receivedMessage *amqp.Delivery 517 | 518 | go func() { 519 | var err error 520 | receivedMessage, err = receiveMessage(ch, opts) 521 | 522 | Expect(err).ToNot(HaveOccurred()) 523 | }() 524 | 525 | time.Sleep(25 * time.Millisecond) 526 | 527 | testMessage := []byte(uuid.NewV4().String()) 528 | publishErr := r.Publish(nil, opts.Bindings[0].BindingKeys[0], testMessage) 529 | 530 | Expect(publishErr).ToNot(HaveOccurred()) 531 | 532 | // Give our consumer some time to receive the message 533 | time.Sleep(100 * time.Millisecond) 534 | 535 | Expect(receivedMessage.Body).To(Equal(testMessage)) 536 | }) 537 | }) 538 | }) 539 | 540 | Describe("Stop", func() { 541 | When("consuming messages via Consume()", func() { 542 | It("Stop() should release Consume() and return", func() { 543 | var receivedMessage string 544 | var exit bool 545 | 546 | go func() { 547 | r.Consume(nil, nil, func(msg amqp.Delivery) error { 548 | receivedMessage = string(msg.Body) 549 | return nil 550 | }) 551 | 552 | exit = true 553 | }() 554 | 555 | // Wait a moment for consumer to start 556 | time.Sleep(100 * time.Millisecond) 557 | 558 | // Stop the consumer 559 | stopErr := r.Stop() 560 | 561 | time.Sleep(100 * time.Millisecond) 562 | 563 | // Verify that stop did not error and the goroutine exited 564 | Expect(stopErr).ToNot(HaveOccurred()) 565 | Expect(receivedMessage).To(BeEmpty()) // jic 566 | Expect(exit).To(BeTrue()) 567 | }) 568 | }) 569 | }) 570 | 571 | Describe("Close", func() { 572 | When("called after instantiating new rabbit", func() { 573 | It("does not error", func() { 574 | err := r.Close() 575 | Expect(err).ToNot(HaveOccurred()) 576 | }) 577 | }) 578 | 579 | When("called before Consume", func() { 580 | It("should cause Consume to immediately return", func() { 581 | err := r.Close() 582 | Expect(err).ToNot(HaveOccurred()) 583 | 584 | // This shouldn't block because internal ctx func should have been called 585 | r.Consume(nil, nil, func(m amqp.Delivery) error { 586 | return nil 587 | }) 588 | 589 | Expect(true).To(BeTrue()) 590 | }) 591 | }) 592 | 593 | When("called before ConsumeOnce", func() { 594 | It("ConsumeOnce should timeout", func() { 595 | err := r.Close() 596 | Expect(err).ToNot(HaveOccurred()) 597 | 598 | // This shouldn't block because internal ctx func should have been called 599 | err = r.ConsumeOnce(nil, func(m amqp.Delivery) error { 600 | return nil 601 | }) 602 | 603 | Expect(err).To(HaveOccurred()) 604 | Expect(err).To(Equal(ErrShutdown)) 605 | }) 606 | }) 607 | 608 | When("called before Publish", func() { 609 | It("Publish should error", func() { 610 | err := r.Close() 611 | Expect(err).ToNot(HaveOccurred()) 612 | 613 | err = r.Publish(nil, "messages", []byte("testing")) 614 | 615 | Expect(err).To(HaveOccurred()) 616 | Expect(err).To(Equal(ErrShutdown)) 617 | }) 618 | }) 619 | }) 620 | 621 | Describe("validateOptions", func() { 622 | Context("validation combinations", func() { 623 | BeforeEach(func() { 624 | opts = generateOptions() 625 | }) 626 | 627 | It("errors with nil options", func() { 628 | err := ValidateOptions(nil) 629 | Expect(err).To(HaveOccurred()) 630 | }) 631 | 632 | It("should error on invalid mode", func() { 633 | opts.Mode = 15 634 | err := ValidateOptions(opts) 635 | Expect(err).To(HaveOccurred()) 636 | Expect(err.Error()).To(ContainSubstring("invalid mode")) 637 | }) 638 | 639 | It("errors when no valid URL is set", func() { 640 | opts.URLs = []string{"", "", ""} 641 | 642 | err := ValidateOptions(opts) 643 | Expect(err).To(HaveOccurred()) 644 | Expect(err.Error()).To(ContainSubstring("At least one non-empty URL must be provided")) 645 | }) 646 | 647 | It("errors when no URL is set", func() { 648 | opts.URLs = []string{} 649 | 650 | err := ValidateOptions(opts) 651 | Expect(err).To(HaveOccurred()) 652 | Expect(err.Error()).To(ContainSubstring("At least one non-empty URL must be provided")) 653 | }) 654 | 655 | It("errors when no exchange is specified", func() { 656 | opts.URLs = []string{ 657 | "amqp://whatever", 658 | } 659 | // empty/nil bindings 660 | opts.Bindings = []Binding{} 661 | err := ValidateOptions(opts) 662 | Expect(err).To(HaveOccurred()) 663 | Expect(err.Error()).To(ContainSubstring("At least one Exchange must be specified")) 664 | }) 665 | 666 | It("errors when multiple exchanges are specified in Producer/Both mode", func() { 667 | opts.Bindings = []Binding{ 668 | { 669 | ExchangeName: "exchange1", 670 | ExchangeDeclare: false, 671 | ExchangeType: "", 672 | }, 673 | { 674 | ExchangeName: "exchange2", 675 | ExchangeDeclare: false, 676 | ExchangeType: "", 677 | }, 678 | } 679 | opts.URLs = []string{ 680 | "amqp://whatever", 681 | } 682 | 683 | opts.Mode = Producer 684 | err := ValidateOptions(opts) 685 | Expect(err).To(HaveOccurred()) 686 | Expect(err.Error()).To(ContainSubstring("Exactly one Exchange must be specified when publishing messages")) 687 | 688 | opts.Mode = Both 689 | err = ValidateOptions(opts) 690 | Expect(err).To(HaveOccurred()) 691 | Expect(err.Error()).To(ContainSubstring("Exactly one Exchange must be specified when publishing messages")) 692 | }) 693 | 694 | It("only checks ExchangeType if ExchangeDeclare is true", func() { 695 | opts.Bindings = []Binding{ 696 | { 697 | ExchangeName: "exchange1", 698 | ExchangeDeclare: false, 699 | ExchangeType: "", 700 | BindingKeys: []string{ 701 | "routingeKey1", 702 | "routingeKey2", 703 | }, 704 | }, 705 | } 706 | opts.URLs = []string{ 707 | "amqp://whatever", 708 | } 709 | opts.Mode = Consumer 710 | err := ValidateOptions(opts) 711 | Expect(err).ToNot(HaveOccurred()) 712 | 713 | opts.Bindings[0].ExchangeDeclare = true 714 | opts.Bindings[0].ExchangeType = "" 715 | 716 | err = ValidateOptions(opts) 717 | Expect(err).To(HaveOccurred()) 718 | Expect(err.Error()).To(ContainSubstring("ExchangeType cannot be empty")) 719 | }) 720 | 721 | It("errors if ExchangeName is unset", func() { 722 | opts.Bindings = []Binding{ 723 | { 724 | ExchangeName: "", 725 | }, 726 | } 727 | opts.URLs = []string{ 728 | "amqp://whatever", 729 | } 730 | err := ValidateOptions(opts) 731 | Expect(err).To(HaveOccurred()) 732 | Expect(err.Error()).To(ContainSubstring("ExchangeName cannot be empty")) 733 | }) 734 | 735 | It("errors if BindingKeys is unset", func() { 736 | opts.Bindings = []Binding{ 737 | { 738 | ExchangeName: "exchange1", 739 | BindingKeys: []string{}, 740 | }, 741 | } 742 | opts.URLs = []string{ 743 | "amqp://whatever", 744 | } 745 | 746 | err := ValidateOptions(opts) 747 | Expect(err).To(HaveOccurred()) 748 | Expect(err.Error()).To(ContainSubstring("At least one BindingKeys must be specified")) 749 | }) 750 | 751 | It("sets RetryConnect to default if unset", func() { 752 | opts.RetryReconnectSec = 0 753 | 754 | err := ValidateOptions(opts) 755 | 756 | Expect(err).ToNot(HaveOccurred()) 757 | Expect(opts.RetryReconnectSec).To(Equal(DefaultRetryReconnectSec)) 758 | }) 759 | 760 | It("sets AppID and ConsumerTag to default if unset", func() { 761 | opts.AppID = "" 762 | opts.ConsumerTag = "" 763 | 764 | err := ValidateOptions(opts) 765 | 766 | Expect(err).ToNot(HaveOccurred()) 767 | Expect(opts.ConsumerTag).To(ContainSubstring("c-rabbit-")) 768 | Expect(opts.AppID).To(ContainSubstring("p-rabbit-")) 769 | }) 770 | }) 771 | }) 772 | 773 | Describe("Reconnect testing", func() { 774 | When("runWatcher receives reconnect signal", func() { 775 | It("performs a reconnect", func() { 776 | // TODO: Implement 777 | }) 778 | }) 779 | 780 | When("runWatcher receives a notify close signal", func() { 781 | It("performs a reconnect", func() { 782 | // TODO: Implement 783 | }) 784 | }) 785 | 786 | When("consumer receives amqp.Delivery with nil acknowledger", func() { 787 | It("will send reconnect signal to ReconnectChan + send err to errCh", func() { 788 | // Create our own rabbit instance with mock delivery channel 789 | ctx, cancel := context.WithCancel(context.Background()) 790 | 791 | ac, err := amqp.Dial(opts.URLs[0]) 792 | Expect(err).ToNot(HaveOccurred()) 793 | notifyCloseCh := make(chan *amqp.Error) 794 | reconnectCh := make(chan struct{}, 1) 795 | deliveryCh := make(chan amqp.Delivery, 1) 796 | errCh := make(chan *ConsumeError, 1) 797 | 798 | r := &Rabbit{ 799 | Conn: ac, 800 | ConsumerRWMutex: &sync.RWMutex{}, 801 | ConsumerWG: &sync.WaitGroup{}, 802 | NotifyCloseChan: notifyCloseCh, 803 | ReconnectChan: reconnectCh, 804 | ConsumerDeliveryChannel: deliveryCh, 805 | ReconnectInProgressMtx: &sync.RWMutex{}, 806 | ProducerRWMutex: &sync.RWMutex{}, 807 | Options: opts, 808 | 809 | log: &NoOpLogger{}, 810 | ctx: ctx, 811 | cancel: cancel, 812 | } 813 | 814 | receivedMessage := false 815 | 816 | // Listen for exactly one message 817 | go func() { 818 | r.Consume(ctx, errCh, func(msg amqp.Delivery) error { 819 | receivedMessage = true 820 | return nil 821 | }) 822 | }() 823 | 824 | // Write message to delivery channel 825 | deliveryCh <- amqp.Delivery{ 826 | Acknowledger: nil, 827 | MessageId: "reconnect-test-id-123", 828 | Timestamp: time.Now(), 829 | ConsumerTag: "reconnect-test", 830 | Exchange: "fake-exchange", 831 | RoutingKey: "fake-routing-key", 832 | Body: []byte("foo"), 833 | } 834 | 835 | go func() { 836 | defer GinkgoRecover() 837 | 838 | consumeErr := <-errCh 839 | Expect(consumeErr).ToNot(BeNil()) 840 | Expect(consumeErr.Error.Error()).To(ContainSubstring("nil acknowledger detected - sending reconnect signal")) 841 | }() 842 | 843 | // Give consume error goroutine enough time to start up 844 | time.Sleep(time.Second) 845 | 846 | Eventually(reconnectCh).Should(Receive()) 847 | Eventually(notifyCloseCh).ShouldNot(Receive()) 848 | 849 | // Consume func should NOT be executed (library should treat as error) 850 | Eventually(receivedMessage).Should(BeFalse()) 851 | }) 852 | }) 853 | }) 854 | }) 855 | 856 | func generateOptions() *Options { 857 | exchangeName := "rabbit-" + uuid.NewV4().String() 858 | 859 | return &Options{ 860 | URLs: []string{"amqp://localhost"}, 861 | QueueName: "rabbit-" + uuid.NewV4().String(), 862 | Bindings: []Binding{ 863 | { 864 | ExchangeName: exchangeName, 865 | ExchangeType: "topic", 866 | ExchangeDeclare: true, 867 | ExchangeDurable: false, 868 | ExchangeAutoDelete: true, 869 | BindingKeys: []string{exchangeName}, 870 | }, 871 | }, 872 | QosPrefetchCount: 0, 873 | QosPrefetchSize: 0, 874 | RetryReconnectSec: 10, 875 | QueueDeclare: true, 876 | QueueDurable: false, 877 | QueueExclusive: false, 878 | QueueAutoDelete: true, 879 | AppID: "rabbit-test-producer", 880 | ConsumerTag: "rabbit-test-consumer", 881 | //Log: logrus.WithField("pkg", "rabbit"), 882 | } 883 | } 884 | 885 | func generateRandomStrings(num int) []string { 886 | generated := make([]string, 0) 887 | 888 | for i := 0; i != num; i++ { 889 | generated = append(generated, uuid.NewV4().String()) 890 | } 891 | 892 | return generated 893 | } 894 | 895 | func connect(opts *Options) (*amqp.Channel, error) { 896 | var err error 897 | var ac *amqp.Connection 898 | for _, url := range opts.URLs { 899 | ac, err = amqp.Dial(url) 900 | if err != nil { 901 | ac = nil 902 | } else { 903 | break 904 | } 905 | } 906 | if err != nil { 907 | return nil, errors.Wrap(err, "unable to dial rabbit server") 908 | } 909 | 910 | ch, err := ac.Channel() 911 | if err != nil { 912 | return nil, errors.Wrap(err, "unable to instantiate channel") 913 | } 914 | 915 | return ch, nil 916 | } 917 | 918 | func publishMessages(ch *amqp.Channel, opts *Options, messages []string) error { 919 | for _, v := range messages { 920 | if err := ch.Publish(opts.Bindings[0].ExchangeName, opts.Bindings[0].BindingKeys[0], false, false, amqp.Publishing{ 921 | DeliveryMode: amqp.Persistent, 922 | Body: []byte(v), 923 | }); err != nil { 924 | return err 925 | } 926 | } 927 | 928 | return nil 929 | } 930 | 931 | func receiveMessage(ch *amqp.Channel, opts *Options) (*amqp.Delivery, error) { 932 | tmpQueueName := "rabbit-receiveMessages-" + uuid.NewV4().String() 933 | 934 | if _, err := ch.QueueDeclare( 935 | tmpQueueName, 936 | false, 937 | true, 938 | false, 939 | false, 940 | nil, 941 | ); err != nil { 942 | return nil, errors.Wrap(err, "unable to declare queue") 943 | } 944 | 945 | if err := ch.QueueBind(tmpQueueName, opts.Bindings[0].BindingKeys[0], opts.Bindings[0].ExchangeName, false, nil); err != nil { 946 | return nil, errors.Wrap(err, "unable to bind queue") 947 | } 948 | 949 | deliveryChan, err := ch.Consume(tmpQueueName, "", true, false, false, false, nil) 950 | if err != nil { 951 | return nil, errors.Wrap(err, "unable to create delivery channel") 952 | } 953 | 954 | select { 955 | case m := <-deliveryChan: 956 | log.Println("Test: received message in receiveMessage()") 957 | return &m, nil 958 | case <-time.After(5 * time.Second): 959 | log.Println("Test: timed out waiting for message in receiveMessage()") 960 | return nil, errors.New("timed out") 961 | } 962 | } 963 | -------------------------------------------------------------------------------- /retry.go: -------------------------------------------------------------------------------- 1 | package rabbit 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | ) 7 | 8 | const RetryUnlimited = -1 9 | 10 | type RetryPolicy struct { 11 | DelayMS []time.Duration 12 | MaxAttempts int 13 | RetryCount int // Default: unlimited (-1) 14 | } 15 | 16 | // DefaultAckPolicy is the default backoff policy for acknowledging messages. 17 | func DefaultAckPolicy() *RetryPolicy { 18 | return &RetryPolicy{ 19 | DelayMS: []time.Duration{ 20 | 50 * time.Millisecond, 21 | 100 * time.Millisecond, 22 | 500 * time.Millisecond, 23 | }, 24 | MaxAttempts: RetryUnlimited, 25 | } 26 | } 27 | 28 | // NewRetryPolicy returns a new backoff policy with the given delays. 29 | func NewRetryPolicy(maxAttempts int, t ...time.Duration) *RetryPolicy { 30 | times := make([]time.Duration, 0) 31 | for _, d := range t { 32 | times = append(times, d) 33 | } 34 | return &RetryPolicy{ 35 | DelayMS: times, 36 | MaxAttempts: maxAttempts, 37 | } 38 | } 39 | 40 | // Duration returns the duration for the given attempt number 41 | // If the attempt number exceeds the number of delays, the last delay is returned 42 | func (b *RetryPolicy) Duration(n int) time.Duration { 43 | b.RetryCount++ 44 | if n >= len(b.DelayMS) { 45 | n = len(b.DelayMS) - 1 46 | } 47 | 48 | return b.DelayMS[n] 49 | } 50 | 51 | // ShouldRetry returns true if the current retry count is less than the max attempts 52 | func (b *RetryPolicy) ShouldRetry() bool { 53 | return b.MaxAttempts == RetryUnlimited || b.RetryCount < b.MaxAttempts 54 | } 55 | 56 | // Reset resets the current retry count to 0 57 | func (b *RetryPolicy) Reset() { 58 | b.RetryCount = 0 59 | } 60 | 61 | // AttemptCount returns the current attempt count as a string, for use with log messages 62 | func (b *RetryPolicy) AttemptCount() string { 63 | maxAttempts := fmt.Sprintf("%d", b.MaxAttempts) 64 | if b.MaxAttempts == RetryUnlimited { 65 | maxAttempts = "Unlimited" 66 | } 67 | 68 | return fmt.Sprintf("%d/%s", b.RetryCount+1, maxAttempts) 69 | } 70 | --------------------------------------------------------------------------------