├── .github ├── FUNDING.yml ├── dependabot.yml └── workflows │ ├── lint.yml │ ├── release.yml │ └── test.yml ├── LICENSE ├── Makefile ├── README.md ├── TESTING.md ├── connection.go ├── consumer.go ├── consumer_metrics.go ├── doc ├── defer.key ├── defer.png ├── retry.key └── retry.png ├── docker-compose.yml ├── examples ├── consumer-single-active │ ├── go.mod │ ├── go.sum │ └── main.go ├── consumer-with-delay │ ├── go.mod │ ├── go.sum │ └── main.go ├── consumer-with-dynamic-binding │ ├── go.mod │ ├── go.sum │ └── main.go ├── consumer-with-graceful-shutdown │ ├── go.mod │ ├── go.sum │ └── main.go ├── consumer-with-lazy-retry │ ├── go.mod │ ├── go.sum │ └── main.go ├── consumer-with-metrics │ ├── go.mod │ ├── go.sum │ └── main.go ├── consumer-with-pool-and-batch │ ├── go.mod │ ├── go.sum │ └── main.go ├── consumer-with-retry │ ├── go.mod │ ├── go.sum │ └── main.go ├── consumer │ ├── go.mod │ ├── go.sum │ └── main.go ├── producer │ ├── go.mod │ ├── go.sum │ └── main.go └── rabbitmq.conf ├── go.mod ├── go.sum ├── helper.go ├── lo.go ├── logger.go ├── main_test.go ├── producer.go ├── retry_acknowledgement.go ├── retry_strategies.go └── types.go /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [samber] 2 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: github-actions 4 | directory: / 5 | schedule: 6 | interval: weekly 7 | - package-ecosystem: gomod 8 | directory: / 9 | schedule: 10 | interval: weekly 11 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | 3 | on: 4 | push: 5 | pull_request: 6 | 7 | jobs: 8 | golangci: 9 | name: lint 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/setup-go@v5 13 | with: 14 | go-version: "1.20" 15 | stable: false 16 | - uses: actions/checkout@v4 17 | - name: golangci-lint 18 | uses: golangci/golangci-lint-action@v8 19 | with: 20 | args: --timeout 120s --max-same-issues 50 21 | 22 | - name: Bearer 23 | uses: bearer/bearer-action@v2 24 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | semver: 7 | type: string 8 | description: 'Semver (eg: v1.2.3)' 9 | required: true 10 | 11 | jobs: 12 | release: 13 | if: github.triggering_actor == 'samber' 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v4 17 | 18 | - name: Set up Go 19 | uses: actions/setup-go@v5 20 | with: 21 | go-version: "1.20" 22 | stable: false 23 | 24 | - name: Test 25 | run: make test 26 | 27 | # remove tests in order to clean dependencies 28 | - name: Remove xxx_test.go files 29 | run: rm -rf *_test.go ./examples ./images 30 | 31 | # cleanup test dependencies 32 | - name: Cleanup dependencies 33 | run: go mod tidy 34 | 35 | - name: List files 36 | run: tree -Cfi 37 | - name: Write new go.mod into logs 38 | run: cat go.mod 39 | - name: Write new go.sum into logs 40 | run: cat go.sum 41 | 42 | - name: Create tag 43 | run: | 44 | git config --global user.name '${{ github.triggering_actor }}' 45 | git config --global user.email "${{ github.triggering_actor}}@users.noreply.github.com" 46 | 47 | git add . 48 | git commit --allow-empty -m 'bump ${{ inputs.semver }}' 49 | git tag ${{ inputs.semver }} 50 | git push origin ${{ inputs.semver }} 51 | 52 | - name: Release 53 | uses: softprops/action-gh-release@v2 54 | with: 55 | name: ${{ inputs.semver }} 56 | tag_name: ${{ inputs.semver }} 57 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | push: 5 | tags: 6 | branches: 7 | pull_request: 8 | 9 | jobs: 10 | 11 | test: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v4 15 | 16 | - name: Set up Go 17 | uses: actions/setup-go@v5 18 | with: 19 | go-version: "1.20" 20 | stable: false 21 | 22 | - name: Build 23 | run: make build 24 | 25 | - name: Test 26 | run: make test 27 | 28 | - name: Test 29 | run: make coverage 30 | 31 | - name: Codecov 32 | uses: codecov/codecov-action@v5 33 | with: 34 | token: ${{ secrets.CODECOV_TOKEN }} 35 | file: ./cover.out 36 | flags: unittests 37 | verbose: true 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Samuel Berthe 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 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | 2 | all: build 3 | re: clean all 4 | 5 | # 6 | # Build 7 | # 8 | build: 9 | go build -v ./... 10 | watch-build: deps 11 | reflex -t 50ms -s -- sh -c 'echo \\nBUILDING && CGO_ENABLED=0 dlv --listen=:1234 --headless=true --accept-multiclient --api-version=2 debug ./example/producer/*.go --continue && echo Exited' 12 | 13 | # 14 | # Deps 15 | # 16 | deps-tools: 17 | go install github.com/cespare/reflex@latest 18 | go install github.com/rakyll/gotest@latest 19 | go install github.com/go-delve/delve/cmd/dlv@latest 20 | go install github.com/psampaz/go-mod-outdated@latest 21 | go install github.com/jondot/goweight@latest 22 | go install golang.org/x/tools/cmd/cover@latest 23 | go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest 24 | go install golang.org/x/vuln/cmd/govulncheck@latest 25 | go install github.com/sonatype-nexus-community/nancy@latest 26 | go mod tidy 27 | 28 | deps: deps-tools 29 | go mod download -x 30 | 31 | cleanup-deps: 32 | go mod tidy 33 | 34 | audit: cleanup-deps 35 | go list -json -m all | nancy sleuth 36 | 37 | outdated: cleanup-deps 38 | go list -u -m -json all | go-mod-outdated -update -direct 39 | 40 | vulncheck: 41 | govulncheck ./... 42 | 43 | # 44 | # Quality 45 | # 46 | lint: 47 | golangci-lint run --timeout 600s --max-same-issues 50 --path-prefix=./ ./... 48 | lint-fix: 49 | golangci-lint run --fix --timeout 600s --max-same-issues 50 --path-prefix=./ ./... 50 | 51 | test: 52 | go test -race -v ./... 53 | watch-test: deps 54 | reflex -t 50ms -s -- sh -c 'gotest -race -v ./...' 55 | 56 | weight: 57 | goweight 58 | 59 | coverage: 60 | go test -v -coverprofile=cover.out -covermode=atomic ./... 61 | go tool cover -html=cover.out -o cover.html 62 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Resilient Pub/Sub framework for RabbitMQ and Go 2 | 3 | [![tag](https://img.shields.io/github/tag/samber/go-amqp-pubsub.svg)](https://github.com/samber/go-amqp-pubsub/releases) 4 | ![Go Version](https://img.shields.io/badge/Go-%3E%3D%201.20.3-%23007d9c) 5 | [![GoDoc](https://godoc.org/github.com/samber/go-amqp-pubsub?status.svg)](https://pkg.go.dev/github.com/samber/go-amqp-pubsub) 6 | ![Build Status](https://github.com/samber/go-amqp-pubsub/actions/workflows/test.yml/badge.svg) 7 | [![Go report](https://goreportcard.com/badge/github.com/samber/go-amqp-pubsub)](https://goreportcard.com/report/github.com/samber/go-amqp-pubsub) 8 | [![Coverage](https://img.shields.io/codecov/c/github/samber/go-amqp-pubsub)](https://codecov.io/gh/samber/go-amqp-pubsub) 9 | [![Contributors](https://img.shields.io/github/contributors/samber/go-amqp-pubsub)](https://github.com/samber/go-amqp-pubsub/graphs/contributors) 10 | [![License](https://img.shields.io/github/license/samber/go-amqp-pubsub)](./LICENSE) 11 | 12 | - Based on github.com/rabbitmq/amqp091-go driver 13 | - Resilient to network failure 14 | - Auto reconnect: recreate channels, bindings, producers, consumers... 15 | - Hot update of queue bindings (thread-safe) 16 | - Optional retry queue on message rejection 17 | - Optional dead letter queue on message rejection 18 | - Optional deferred message consumption 19 | 20 | ## How to 21 | 22 | During your tests, feel free to restart Rabbitmq. This library will reconnect automatically. 23 | 24 | ### Connection 25 | 26 | ```go 27 | import pubsub "github.com/samber/go-amqp-pubsub" 28 | 29 | conn, err := pubsub.NewConnection("connection-1", pubsub.ConnectionOptions{ 30 | URI: "amqp://dev:dev@localhost:5672", 31 | Config: amqp.Config{ 32 | Dial: amqp.DefaultDial(time.Second), 33 | }, 34 | }) 35 | 36 | // ... 37 | 38 | conn.Close() 39 | ``` 40 | 41 | ### Producer 42 | 43 | ```go 44 | import ( 45 | pubsub "github.com/samber/go-amqp-pubsub" 46 | "github.com/samber/lo" 47 | "github.com/samber/mo" 48 | ) 49 | 50 | // `err` can be ignored since it will connect lazily to rabbitmq 51 | conn, err := pubsub.NewConnection("connection-1", pubsub.ConnectionOptions{ 52 | URI: "amqp://dev:dev@localhost:5672", 53 | LazyConnection: mo.Some(true), 54 | }) 55 | 56 | producer := pubsub.NewProducer(conn, "producer-1", pubsub.ProducerOptions{ 57 | Exchange: pubsub.ProducerOptionsExchange{ 58 | Name: "product.event", 59 | Kind: pubsub.ExchangeKindTopic, 60 | }, 61 | }) 62 | 63 | err := producer.Publish(routingKey, false, false, amqp.Publishing{ 64 | ContentType: "application/json", 65 | DeliveryMode: amqp.Persistent, 66 | Body: []byte(`{"hello": "world"}`), 67 | }) 68 | 69 | producer.Close() 70 | conn.Close() 71 | ``` 72 | 73 | ### Consumer 74 | 75 | ```go 76 | import ( 77 | pubsub "github.com/samber/go-amqp-pubsub" 78 | "github.com/samber/lo" 79 | "github.com/samber/mo" 80 | ) 81 | 82 | // `err` can be ignore since it will connect lazily to rabbitmq 83 | conn, err := pubsub.NewConnection("connection-1", pubsub.ConnectionOptions{ 84 | URI: "amqp://dev:dev@localhost:5672", 85 | LazyConnection: mo.Some(true), 86 | }) 87 | 88 | consumer := pubsub.NewConsumer(conn, "consumer-1", pubsub.ConsumerOptions{ 89 | Queue: pubsub.ConsumerOptionsQueue{ 90 | Name: "product.onEdit", 91 | }, 92 | Bindings: []pubsub.ConsumerOptionsBinding{ 93 | {ExchangeName: "product.event", RoutingKey: "product.created"}, 94 | {ExchangeName: "product.event", RoutingKey: "product.updated"}, 95 | }, 96 | Message: pubsub.ConsumerOptionsMessage{ 97 | PrefetchCount: mo.Some(100), 98 | }, 99 | EnableDeadLetter: mo.Some(true), // will create a "product.onEdit.deadLetter" DL queue 100 | }) 101 | 102 | for msg := range consumer.Consume() { 103 | lo.Try0(func() { // handle exceptions 104 | // ... 105 | msg.Ack(false) 106 | }) 107 | } 108 | 109 | consumer.Close() 110 | conn.Close() 111 | ``` 112 | 113 | ### Consumer with pooling and batching 114 | 115 | See [examples/consumer-with-pool-and-batch](examples/consumer-with-pool-and-batch/main.go). 116 | 117 | ### Consumer with retry strategy 118 | 119 | ![Retry architecture](doc/retry.png) 120 | 121 | See [examples/consumer-with-retry](examples/consumer-with-retry/main.go). 122 | 123 | 3 retry strategies are available: 124 | 125 | - Exponential backoff 126 | - Constant interval 127 | - Lazy retry 128 | 129 | #### Examples 130 | 131 | Exponential backoff: 132 | 133 | ```go 134 | consumer := pubsub.NewConsumer(conn, "example-consumer-1", pubsub.ConsumerOptions{ 135 | Queue: pubsub.ConsumerOptionsQueue{ 136 | Name: "product.onEdit", 137 | }, 138 | // ... 139 | RetryStrategy: mo.Some(pubsub.NewExponentialRetryStrategy(3, 3*time.Second, 2)), // will create a "product.onEdit.retry" queue 140 | }) 141 | 142 | for msg := range consumer.Consume() { 143 | // ... 144 | msg.Reject(false) // will retry 3 times with exponential backoff 145 | } 146 | ``` 147 | 148 | Lazy retry: 149 | 150 | ```go 151 | consumer := pubsub.NewConsumer(conn, "example-consumer-1", pubsub.ConsumerOptions{ 152 | Queue: pubsub.ConsumerOptionsQueue{ 153 | Name: "product.onEdit", 154 | }, 155 | // ... 156 | RetryStrategy: mo.Some(pubsub.NewLazyRetryStrategy(3)), // will create a "product.onEdit.retry" queue 157 | }) 158 | 159 | for msg := range consumer.Consume() { 160 | // ... 161 | 162 | err := json.Unmarshal(body, &object) 163 | if err != nil { 164 | // retry is not necessary 165 | msg.Reject(false) 166 | continue 167 | } 168 | 169 | // ... 170 | 171 | err = sql.Exec(query) 172 | if err != nil { 173 | // retry on network error 174 | pubsub.RejectWithRetry(msg, 10*time.Second) 175 | continue 176 | } 177 | 178 | // ... 179 | msg.Ack(false) 180 | } 181 | ``` 182 | 183 | #### Custom retry strategy 184 | 185 | Custom strategies can be provided to the consumer. 186 | 187 | ```go 188 | type MyCustomRetryStrategy struct {} 189 | 190 | func NewMyCustomRetryStrategy() RetryStrategy { 191 | return &MyCustomRetryStrategy{} 192 | } 193 | 194 | func (rs *MyCustomRetryStrategy) NextBackOff(msg *amqp.Delivery, attempts int) (time.Duration, bool) { 195 | // retries every 10 seconds, until message get older than 5 minutes 196 | if msg.Timestamp.Add(5*time.Minute).After(time.Now()) { 197 | return 10 * time.Second, true 198 | } 199 | 200 | return time.Duration{}, false 201 | } 202 | ``` 203 | 204 | #### Consistency 205 | 206 | On retry, the message is published into the retry queue then is acked from the initial queue. This 2 phases delivery is unsafe, since connection could drop during operation. With the `ConsistentRetry` policy, the steps will be embbeded into a transaction. Use it carefully because the delivery rate will be reduced by an order of magnitude. 207 | 208 | ```go 209 | consumer := pubsub.NewConsumer(conn, "example-consumer-1", pubsub.ConsumerOptions{ 210 | Queue: pubsub.ConsumerOptionsQueue{ 211 | Name: "product.onEdit", 212 | }, 213 | // ... 214 | RetryStrategy: mo.Some(pubsub.NewExponentialRetryStrategy(3, 3*time.Second, 2)), 215 | RetryConsistency: mo.Some(pubsub.ConsistentRetry), 216 | }) 217 | ``` 218 | 219 | ### Defer message consumption 220 | 221 | ![](./doc/defer.png) 222 | 223 | See [examples/consumer-with-delay](examples/consumer-with-delay/main.go). 224 | 225 | On publishing, the first consumption of the message can be delayed. The message will instead be sent to the .defer queue, expire, and then go to the initial queue. 226 | 227 | ```go 228 | consumer := pubsub.NewConsumer(conn, "example-consumer-1", pubsub.ConsumerOptions{ 229 | Queue: pubsub.ConsumerOptionsQueue{ 230 | Name: "product.onEdit", 231 | }, 232 | // ... 233 | Defer: mo.Some(5 * time.Second), 234 | }) 235 | ``` 236 | 237 | ## Run examples 238 | 239 | ```sh 240 | # run rabbitmq 241 | docker-compose up rabbitmq 242 | ``` 243 | 244 | ```sh 245 | # run producer 246 | cd examples/producer/ 247 | go mod download 248 | go run main.go --rabbitmq-uri amqp://dev:dev@localhost:5672 249 | ``` 250 | 251 | ```sh 252 | # run consumer 253 | cd examples/consumer/ 254 | go mod download 255 | go run main.go --rabbitmq-uri amqp://dev:dev@localhost:5672 256 | ``` 257 | 258 | Then trigger network failure, by restarting rabbitmq: 259 | 260 | ```sh 261 | docker-compose restart rabbitmq 262 | ``` 263 | 264 | ## 🤝 Contributing 265 | 266 | - Ping me on Twitter [@samuelberthe](https://twitter.com/samuelberthe) (DMs, mentions, whatever :)) 267 | - Fork the [project](https://github.com/samber/oops) 268 | - Fix [open issues](https://github.com/samber/oops/issues) or request new features 269 | 270 | Don't hesitate ;) 271 | 272 | ```bash 273 | # Install some dev dependencies 274 | make tools 275 | 276 | # Run tests 277 | make test 278 | # or 279 | make watch-test 280 | ``` 281 | 282 | ### Todo 283 | 284 | - Connection pooling (eg: 10 connections, 100 channels per connection) 285 | - Better documentation 286 | - Testing + CI 287 | - BatchPublish + PublishWithConfirmation + BatchPublishWithConfirmation 288 | 289 | ## 👤 Contributors 290 | 291 | ![Contributors](https://contrib.rocks/image?repo=samber/oops) 292 | 293 | ## 💫 Show your support 294 | 295 | Give a ⭐️ if this project helped you! 296 | 297 | [![GitHub Sponsors](https://img.shields.io/github/sponsors/samber?style=for-the-badge)](https://github.com/sponsors/samber) 298 | 299 | ## 📝 License 300 | 301 | Copyright © 2023 [Samuel Berthe](https://github.com/samber). 302 | 303 | This project is [MIT](./LICENSE) licensed. 304 | -------------------------------------------------------------------------------- /TESTING.md: -------------------------------------------------------------------------------- 1 | 2 | # Some manual tests 3 | 4 | ## Handle rabbitmq restart 5 | 6 | - start consumers 7 | - start producers 8 | - stop rabbitmq properly 9 | - start rabbitmq 10 | 11 | ## Handle short network failure 12 | 13 | - start consumers 14 | - start producers 15 | - kill rabbitmq 16 | - start rabbitmq 17 | 18 | ## Handle network failure on app start 19 | 20 | - stop rabbitmq 21 | - start consumers 22 | - start producers 23 | - start rabbitmq 24 | 25 | ## Handle message timeout 26 | 27 | - insert a timeout in a message 28 | 29 | ## Handle consumer stop 30 | 31 | - create a consumer catching ctrl-c event 32 | - run producer 33 | - run a slow consumer (with buffered messages) 34 | - ctrl-c 35 | 36 | ## Handle exchange/queue removal 37 | 38 | - run producer 39 | - run consumer 40 | - remove some bindings 41 | - remove queue 42 | - remove exchange 43 | -------------------------------------------------------------------------------- /connection.go: -------------------------------------------------------------------------------- 1 | package pubsub 2 | 3 | import ( 4 | "sync" 5 | "time" 6 | 7 | "github.com/google/uuid" 8 | amqp "github.com/rabbitmq/amqp091-go" 9 | "github.com/samber/mo" 10 | ) 11 | 12 | type ConnectionOptions struct { 13 | URI string 14 | Config amqp.Config 15 | 16 | // optional arguments 17 | ReconnectInterval mo.Option[time.Duration] // default 2s 18 | LazyConnection mo.Option[bool] // default false 19 | } 20 | 21 | type Connection struct { 22 | conn *amqp.Connection 23 | name string 24 | options ConnectionOptions 25 | 26 | // should be a generic sync.Map 27 | channelsMutex sync.Mutex 28 | channels map[string]chan *amqp.Connection 29 | closeOnce sync.Once 30 | done *rpc[struct{}, struct{}] 31 | } 32 | 33 | func NewConnection(name string, opt ConnectionOptions) (*Connection, error) { 34 | doneCh := make(chan struct{}) 35 | 36 | c := &Connection{ 37 | conn: nil, 38 | name: name, 39 | options: opt, 40 | 41 | channelsMutex: sync.Mutex{}, 42 | channels: map[string]chan *amqp.Connection{}, 43 | closeOnce: sync.Once{}, 44 | done: newRPC[struct{}, struct{}](doneCh), 45 | } 46 | 47 | err := c.lifecycle() 48 | 49 | return c, err 50 | } 51 | 52 | func (c *Connection) lifecycle() error { 53 | lazyConnection := c.options.LazyConnection.OrElse(false) 54 | 55 | if !lazyConnection { 56 | err := c.redial() 57 | if err != nil { 58 | return err 59 | } 60 | } 61 | 62 | ticker := time.NewTicker(c.options.ReconnectInterval.OrElse(2 * time.Second)) 63 | 64 | go func() { 65 | if lazyConnection { 66 | _ = c.redial() // don't wait for the first tick 67 | } 68 | 69 | for { 70 | select { 71 | case <-ticker.C: 72 | if c.IsClosed() { 73 | _ = c.redial() 74 | } 75 | 76 | case req := <-c.done.C: 77 | ticker.Stop() 78 | 79 | // disconnect 80 | if !c.IsClosed() { 81 | err := c.conn.Close() 82 | if err != nil { 83 | logger(ScopeConnection, c.name, "Disconnection failure", map[string]any{"error": err.Error()}) 84 | } 85 | 86 | c.conn = nil 87 | } 88 | 89 | c.notifyChannels(nil) 90 | 91 | // @TODO we should requeue messages 92 | 93 | req.B(struct{}{}) 94 | 95 | return 96 | } 97 | } 98 | }() 99 | 100 | return nil 101 | } 102 | 103 | func (c *Connection) Close() error { 104 | c.closeOnce.Do(func() { 105 | _ = c.done.Send(struct{}{}) 106 | safeCloseChan(c.done.C) 107 | }) 108 | 109 | return nil 110 | } 111 | 112 | // ListenConnection implements the Observable pattern. 113 | func (c *Connection) ListenConnection() (func(), <-chan *amqp.Connection) { 114 | id := uuid.New().String() 115 | ch := make(chan *amqp.Connection, 42) 116 | 117 | cancel := func() { 118 | c.channelsMutex.Lock() 119 | defer c.channelsMutex.Unlock() 120 | 121 | delete(c.channels, id) 122 | close(ch) 123 | } 124 | 125 | c.channelsMutex.Lock() 126 | c.channels[id] = ch 127 | c.channelsMutex.Unlock() 128 | 129 | ch <- c.conn 130 | 131 | return cancel, ch 132 | } 133 | 134 | func (c *Connection) IsClosed() bool { 135 | c.channelsMutex.Lock() 136 | defer c.channelsMutex.Unlock() 137 | 138 | return c.conn == nil || c.conn.IsClosed() 139 | } 140 | 141 | func (c *Connection) redial() error { 142 | c.channelsMutex.Lock() 143 | bak := c.conn 144 | c.channelsMutex.Unlock() 145 | 146 | if bak != nil { 147 | _ = bak.Close() 148 | } 149 | 150 | conn, err := amqp.DialConfig(c.options.URI, c.options.Config) 151 | 152 | if err != nil { 153 | logger(ScopeConnection, c.name, "Connection failure", map[string]any{"error": err.Error()}) 154 | 155 | if conn != nil { 156 | _ = conn.Close() 157 | } 158 | if bak != nil { 159 | c.notifyChannels(nil) 160 | } 161 | c.conn = nil 162 | } else { 163 | c.notifyChannels(conn) 164 | c.conn = conn 165 | } 166 | 167 | return err 168 | } 169 | 170 | func (c *Connection) notifyChannels(conn *amqp.Connection) { 171 | c.channelsMutex.Lock() 172 | defer c.channelsMutex.Unlock() 173 | 174 | for _, v := range c.channels { 175 | v <- conn 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /consumer.go: -------------------------------------------------------------------------------- 1 | package pubsub 2 | 3 | import ( 4 | "fmt" 5 | "sync" 6 | "time" 7 | 8 | "github.com/prometheus/client_golang/prometheus" 9 | amqp "github.com/rabbitmq/amqp091-go" 10 | "github.com/samber/lo" 11 | "github.com/samber/mo" 12 | ) 13 | 14 | const ( 15 | // @TODO: Using a different exchange would be a breaking change. 16 | // deadLetterExchange = "internal.dlx" 17 | // retryExchange = "internal.retry" 18 | ) 19 | 20 | type ConsumerOptionsQueue struct { 21 | Name string 22 | 23 | // optional arguments 24 | Durable mo.Option[bool] // default true 25 | AutoDelete mo.Option[bool] // default false 26 | ExclusiveConsumer mo.Option[bool] // default false 27 | NoWait mo.Option[bool] // default false 28 | Args mo.Option[amqp.Table] // default nil 29 | } 30 | 31 | type ConsumerOptionsBinding struct { 32 | ExchangeName string 33 | RoutingKey string 34 | 35 | // optional arguments 36 | Args mo.Option[amqp.Table] // default nil 37 | } 38 | 39 | type ConsumerOptionsMessage struct { 40 | // optional arguments 41 | AutoAck mo.Option[bool] // default false 42 | PrefetchCount mo.Option[int] // default 0 43 | PrefetchSize mo.Option[int] // default 0 44 | } 45 | 46 | type ConsumerOptions struct { 47 | Queue ConsumerOptionsQueue 48 | Bindings []ConsumerOptionsBinding 49 | Message ConsumerOptionsMessage 50 | 51 | // optional arguments 52 | Metrics ConsumerOptionsMetrics 53 | EnableDeadLetter mo.Option[bool] // default false 54 | Defer mo.Option[time.Duration] // default no Defer 55 | ConsumeArgs mo.Option[amqp.Table] // default nil 56 | RetryStrategy mo.Option[RetryStrategy] // default no retry 57 | RetryConsistency mo.Option[RetryConsistency] // default eventually consistent 58 | } 59 | 60 | type QueueSetupExchangeOptions struct { 61 | name mo.Option[string] 62 | kind mo.Option[string] 63 | durable mo.Option[bool] 64 | autoDelete mo.Option[bool] 65 | internal mo.Option[bool] 66 | noWait mo.Option[bool] 67 | } 68 | 69 | type QueueSetupQueueOptions struct { 70 | name string 71 | durable bool 72 | autoDelete bool 73 | exclusive bool 74 | noWait bool 75 | args mo.Option[amqp.Table] 76 | } 77 | 78 | type QueueSetupOptions struct { 79 | Exchange QueueSetupExchangeOptions 80 | Queue QueueSetupQueueOptions 81 | } 82 | 83 | type Consumer struct { 84 | conn *Connection 85 | name string 86 | options ConsumerOptions 87 | 88 | delivery chan *amqp.Delivery 89 | closeOnce sync.Once 90 | done *rpc[struct{}, struct{}] 91 | 92 | mu sync.RWMutex 93 | bindingUpdates *rpc[lo.Tuple2[bool, ConsumerOptionsBinding], error] 94 | 95 | retryProducer *Producer 96 | 97 | metrics []*metric 98 | } 99 | 100 | func NewConsumer(conn *Connection, name string, opt ConsumerOptions) *Consumer { 101 | doneCh := make(chan struct{}) 102 | bindingUpdatesCh := make(chan<- lo.Tuple2[bool, ConsumerOptionsBinding], 10) 103 | 104 | c := Consumer{ 105 | conn: conn, 106 | name: name, 107 | options: opt, 108 | 109 | delivery: make(chan *amqp.Delivery), 110 | closeOnce: sync.Once{}, 111 | done: newRPC[struct{}, struct{}](doneCh), 112 | 113 | mu: sync.RWMutex{}, 114 | bindingUpdates: newRPC[lo.Tuple2[bool, ConsumerOptionsBinding], error](bindingUpdatesCh), 115 | 116 | retryProducer: nil, 117 | 118 | metrics: opt.Metrics.metrics(name), 119 | } 120 | 121 | if opt.RetryStrategy.IsPresent() { 122 | c.retryProducer = NewProducer( 123 | conn, 124 | name+".retry", 125 | ProducerOptions{ 126 | Exchange: ProducerOptionsExchange{}, 127 | }, 128 | ) 129 | } 130 | 131 | go c.lifecycle() 132 | 133 | return &c 134 | } 135 | 136 | func (svc *Consumer) Describe(ch chan<- *prometheus.Desc) { 137 | for _, metric := range svc.metrics { 138 | metric.Describe(ch) 139 | } 140 | } 141 | 142 | func (svc *Consumer) Collect(ch chan<- prometheus.Metric) { 143 | for _, metric := range svc.metrics { 144 | metric.Collect(ch) 145 | } 146 | } 147 | 148 | func (c *Consumer) lifecycle() { 149 | cancel, connectionListener := c.conn.ListenConnection() 150 | onConnect := make(chan struct{}, 42) 151 | onDisconnect := make(chan struct{}, 42) 152 | 153 | var conn *amqp.Connection 154 | var channel *amqp.Channel 155 | 156 | defer func() { 157 | safeCloseChan(onConnect) 158 | safeCloseChan(onDisconnect) 159 | }() 160 | 161 | for { 162 | select { 163 | case _conn := <-connectionListener: 164 | conn = _conn 165 | if conn != nil { 166 | onConnect <- struct{}{} 167 | } else { 168 | onDisconnect <- struct{}{} 169 | } 170 | 171 | case <-onConnect: 172 | channel = c.closeChannel(channel) 173 | 174 | if conn == nil || conn.IsClosed() { 175 | continue 176 | } 177 | 178 | _channel, onChannelClosed, err := c.setupConsumer(conn) 179 | if err != nil { 180 | logger(ScopeConsumer, c.name, "Could not start consumer", map[string]any{"error": err.Error()}) 181 | time.Sleep(1 * time.Second) // retry in 1 second 182 | onConnect <- struct{}{} 183 | } else { 184 | channel = _channel 185 | go func() { 186 | // ok && err==nil -> channel closed 187 | // ok && err!=nil -> channel error (message timeout, connection error, etc...) 188 | err, ok := <-onChannelClosed 189 | if ok && err != nil { 190 | logger(ScopeChannel, c.name, "Channel closed: "+err.Reason, map[string]any{"error": err.Error()}) 191 | onConnect <- struct{}{} 192 | } 193 | }() 194 | } 195 | 196 | case <-onDisconnect: 197 | channel = c.closeChannel(channel) 198 | 199 | case update := <-c.bindingUpdates.C: 200 | err := c.onBindingUpdate(channel, update.A) 201 | if err != nil { 202 | logger(ScopeConsumer, c.name, "Could not change binding", map[string]any{"error": err.Error()}) 203 | update.B(err) 204 | } else { 205 | update.B(nil) 206 | } 207 | 208 | case req := <-c.done.C: 209 | channel = c.closeChannel(channel) //nolint:ineffassign,staticcheck 210 | 211 | cancel() // first, remove from connection listeners 212 | safeCloseChan(c.bindingUpdates.C) // second, stop updating queue bindings 213 | drainChan(c.delivery) // third, flush channel -- we don't requeue message since amqp will do it for us 214 | safeCloseChan(c.delivery) // last, stop consuming messages 215 | 216 | // send response to rpc 217 | req.B(struct{}{}) 218 | return 219 | } 220 | } 221 | } 222 | 223 | func (c *Consumer) closeChannel(channel *amqp.Channel) *amqp.Channel { 224 | if channel != nil && !channel.IsClosed() { 225 | channel.Close() 226 | } 227 | 228 | // Just to be sure we won't read twice some messages. 229 | // Also, it offers some garantee on message order on reconnect. 230 | drainChan(c.delivery) 231 | 232 | return nil 233 | } 234 | 235 | func (c *Consumer) Close() error { 236 | c.closeOnce.Do(func() { 237 | _ = c.done.Send(struct{}{}) 238 | safeCloseChan(c.done.C) 239 | 240 | if c.retryProducer != nil { 241 | _ = c.retryProducer.Close() 242 | } 243 | }) 244 | 245 | return nil 246 | } 247 | 248 | func (c *Consumer) setupConsumer(conn *amqp.Connection) (*amqp.Channel, <-chan *amqp.Error, error) { 249 | // create a channel dedicated to this consumer 250 | channel, err := conn.Channel() 251 | if err != nil { 252 | return nil, nil, err 253 | } 254 | 255 | // create dead-letter queue if necessary 256 | queueArgs := c.options.Queue.Args.OrElse(nil) 257 | 258 | if c.options.EnableDeadLetter.OrElse(false) { 259 | deadLetterArgs, err2 := c.setupDeadLetter(channel) 260 | if err2 != nil { 261 | _ = channel.Close() 262 | return nil, nil, err2 263 | } 264 | 265 | queueArgs = lo.Assign(queueArgs, deadLetterArgs) 266 | } 267 | 268 | // create queue if not exist 269 | _, err = channel.QueueDeclare( 270 | c.options.Queue.Name, 271 | c.options.Queue.Durable.OrElse(true), 272 | c.options.Queue.AutoDelete.OrElse(false), 273 | c.options.Queue.ExclusiveConsumer.OrElse(false), 274 | c.options.Queue.NoWait.OrElse(false), 275 | queueArgs, 276 | ) 277 | if err != nil { 278 | _ = channel.Close() 279 | return nil, nil, err 280 | } 281 | 282 | err = channel.Qos( 283 | c.options.Message.PrefetchCount.OrElse(0), 284 | c.options.Message.PrefetchSize.OrElse(0), 285 | false, 286 | ) 287 | if err != nil { 288 | _ = channel.Close() 289 | return nil, nil, err 290 | } 291 | 292 | queueToBind := c.options.Queue.Name 293 | 294 | // create defer queue if necessary 295 | if c.options.Defer.IsPresent() { 296 | err = c.setupDefer(channel, c.options.Defer.MustGet()) 297 | if err != nil { 298 | _ = channel.Close() 299 | return nil, nil, err 300 | } 301 | 302 | queueToBind = c.options.Queue.Name + ".defer" 303 | } 304 | 305 | // binding exchange->queue 306 | c.mu.Lock() 307 | bindings := c.options.Bindings 308 | c.mu.Unlock() 309 | for _, b := range bindings { 310 | err = channel.QueueBind( 311 | queueToBind, 312 | b.RoutingKey, 313 | b.ExchangeName, 314 | false, 315 | b.Args.OrElse(nil), 316 | ) 317 | if err != nil { 318 | _ = channel.Close() 319 | return nil, nil, err 320 | } 321 | } 322 | 323 | // create retry queue if necessary 324 | if c.options.RetryStrategy.IsPresent() { 325 | err = c.setupRetry(channel) 326 | if err != nil { 327 | _ = channel.Close() 328 | return nil, nil, err 329 | } 330 | } 331 | 332 | err = c.onMessage(channel) 333 | if err != nil { 334 | _ = channel.Close() 335 | return nil, nil, err 336 | } 337 | 338 | return channel, channel.NotifyClose(make(chan *amqp.Error)), nil 339 | } 340 | 341 | func (c *Consumer) setupQueue(channel *amqp.Channel, opts QueueSetupOptions, bindQueueToDeadLetter bool) error { 342 | err := channel.ExchangeDeclare( 343 | opts.Exchange.name.OrElse("amq.direct"), 344 | opts.Exchange.kind.OrElse(amqp.ExchangeDirect), 345 | opts.Exchange.durable.OrElse(true), 346 | opts.Exchange.autoDelete.OrElse(false), 347 | opts.Exchange.internal.OrElse(false), 348 | opts.Exchange.noWait.OrElse(false), 349 | nil, 350 | ) 351 | if err != nil { 352 | return err 353 | } 354 | 355 | _, err = channel.QueueDeclare( 356 | opts.Queue.name, 357 | opts.Queue.durable, 358 | opts.Queue.autoDelete, 359 | opts.Queue.exclusive, 360 | opts.Queue.noWait, 361 | opts.Queue.args.OrElse(nil), 362 | ) 363 | if err != nil { 364 | return err 365 | } 366 | 367 | // binding exchange->queue 368 | err = channel.QueueBind( 369 | opts.Queue.name, 370 | opts.Queue.name, 371 | opts.Exchange.name.OrElse("amq.direct"), 372 | false, 373 | nil, 374 | ) 375 | if err != nil { 376 | return err 377 | } 378 | 379 | if bindQueueToDeadLetter { 380 | err = channel.QueueBind( 381 | c.options.Queue.Name, 382 | c.options.Queue.Name, 383 | opts.Exchange.name.OrElse("amq.direct"), 384 | false, 385 | nil, 386 | ) 387 | if err != nil { 388 | return err 389 | } 390 | } 391 | 392 | return nil 393 | } 394 | 395 | func (c *Consumer) setupDeadLetter(channel *amqp.Channel) (map[string]any, error) { 396 | deadLetterQueueName := c.options.Queue.Name + ".deadLetter" 397 | 398 | args := amqp.Table{ 399 | "x-dead-letter-exchange": "amq.direct", 400 | "x-dead-letter-routing-key": deadLetterQueueName, 401 | } 402 | 403 | opts := QueueSetupOptions{ 404 | Exchange: QueueSetupExchangeOptions{ 405 | durable: mo.Some(true), 406 | autoDelete: mo.Some(false), 407 | internal: mo.Some(false), // @TODO: should be `true` (breaking change) 408 | noWait: mo.Some(false), 409 | }, 410 | Queue: QueueSetupQueueOptions{ 411 | name: deadLetterQueueName, 412 | durable: true, 413 | autoDelete: false, 414 | exclusive: false, 415 | noWait: false, 416 | }, 417 | } 418 | 419 | return args, c.setupQueue(channel, opts, false) 420 | } 421 | 422 | func (c *Consumer) setupRetry(channel *amqp.Channel) error { 423 | opts := QueueSetupOptions{ 424 | Exchange: QueueSetupExchangeOptions{ 425 | durable: mo.Some(true), 426 | autoDelete: mo.Some(false), 427 | internal: mo.Some(false), 428 | noWait: mo.Some(false), 429 | }, 430 | Queue: QueueSetupQueueOptions{ 431 | name: c.options.Queue.Name + ".retry", 432 | durable: c.options.Queue.Durable.OrElse(true), 433 | autoDelete: c.options.Queue.AutoDelete.OrElse(false), 434 | exclusive: false, 435 | noWait: false, 436 | args: mo.Some(amqp.Table{ 437 | "x-dead-letter-exchange": "amq.direct", 438 | "x-dead-letter-routing-key": c.options.Queue.Name, 439 | }), 440 | }, 441 | } 442 | 443 | return c.setupQueue(channel, opts, true) 444 | } 445 | 446 | func (c *Consumer) setupDefer(channel *amqp.Channel, delay time.Duration) error { 447 | opts := QueueSetupOptions{ 448 | Exchange: QueueSetupExchangeOptions{}, 449 | Queue: QueueSetupQueueOptions{ 450 | name: c.options.Queue.Name + ".defer", 451 | durable: c.options.Queue.Durable.OrElse(true), 452 | autoDelete: c.options.Queue.AutoDelete.OrElse(false), 453 | exclusive: false, 454 | noWait: false, 455 | args: mo.Some(amqp.Table{ 456 | "x-dead-letter-exchange": "amq.direct", 457 | "x-dead-letter-routing-key": c.options.Queue.Name, 458 | "x-message-ttl": delay.Milliseconds(), 459 | }), 460 | }, 461 | } 462 | 463 | return c.setupQueue(channel, opts, true) 464 | } 465 | 466 | func (c *Consumer) onBindingUpdate(channel *amqp.Channel, update lo.Tuple2[bool, ConsumerOptionsBinding]) error { 467 | adding, binding := update.Unpack() 468 | 469 | queueToBind := c.options.Queue.Name 470 | 471 | if c.options.Defer.IsPresent() { 472 | queueToBind = c.options.Queue.Name + ".defer" 473 | } 474 | 475 | if channel == nil || channel.IsClosed() { 476 | return nil // binding will be added on reconnect 477 | } 478 | 479 | err := lo.TernaryF( 480 | adding, 481 | func() error { 482 | return channel.QueueBind( 483 | queueToBind, 484 | binding.RoutingKey, 485 | binding.ExchangeName, 486 | false, 487 | binding.Args.OrElse(nil), 488 | ) 489 | }, func() error { 490 | return channel.QueueUnbind( 491 | queueToBind, 492 | binding.RoutingKey, 493 | binding.ExchangeName, 494 | binding.Args.OrElse(nil), 495 | ) 496 | }, 497 | ) 498 | 499 | if err != nil { 500 | _ = channel.Close() 501 | return fmt.Errorf("failed to (un)bind queue '%s' to exchange '%s' using routing key '%s': %s", queueToBind, binding.ExchangeName, binding.RoutingKey, err.Error()) 502 | } 503 | 504 | return nil 505 | } 506 | 507 | /** 508 | * Message stream 509 | */ 510 | func (c *Consumer) onMessage(channel *amqp.Channel) error { 511 | delivery, err := channel.Consume( 512 | c.options.Queue.Name, 513 | c.name, 514 | c.options.Message.AutoAck.OrElse(false), 515 | c.options.Queue.ExclusiveConsumer.OrElse(false), 516 | false, 517 | false, 518 | c.options.ConsumeArgs.OrElse(nil), 519 | ) 520 | if err != nil { 521 | return err 522 | } 523 | 524 | go func() { 525 | for raw := range delivery { 526 | if c.options.RetryStrategy.IsPresent() { 527 | raw.Acknowledger = newRetryAcknowledger( 528 | c.retryProducer, 529 | c.options.Queue.Name+".retry", 530 | c.options.RetryStrategy.MustGet(), 531 | c.options.RetryConsistency.OrElse(EventuallyConsistentRetry), 532 | raw, 533 | ) 534 | } 535 | 536 | c.delivery <- lo.ToPtr(raw) 537 | } 538 | 539 | // It may reach this line on consumer timeout or channel closing. 540 | // We let the c.delivery channel consumable. 541 | }() 542 | 543 | return nil 544 | } 545 | 546 | /** 547 | * API 548 | */ 549 | 550 | func (c *Consumer) Consume() <-chan *amqp.Delivery { 551 | return c.delivery 552 | } 553 | 554 | func (c *Consumer) AddBinding(exchangeName string, routingKey string, args mo.Option[amqp.Table]) error { 555 | binding := ConsumerOptionsBinding{ 556 | ExchangeName: exchangeName, 557 | RoutingKey: routingKey, 558 | Args: args, 559 | } 560 | 561 | err := c.bindingUpdates.Send(lo.T2(true, binding)) 562 | if err != nil { 563 | return err 564 | } 565 | 566 | c.mu.Lock() 567 | c.options.Bindings = append(c.options.Bindings, binding) 568 | c.mu.Unlock() 569 | 570 | return nil 571 | } 572 | 573 | func (c *Consumer) RemoveBinding(exchangeName string, routingKey string, args mo.Option[amqp.Table]) error { 574 | binding := ConsumerOptionsBinding{ 575 | ExchangeName: exchangeName, 576 | RoutingKey: routingKey, 577 | Args: args, 578 | } 579 | 580 | err := c.bindingUpdates.Send(lo.T2(false, binding)) 581 | if err != nil { 582 | return err 583 | } 584 | 585 | c.mu.Lock() 586 | c.options.Bindings = lo.Filter(c.options.Bindings, func(item ConsumerOptionsBinding, _ int) bool { 587 | return item.ExchangeName != exchangeName && item.RoutingKey != routingKey 588 | }) 589 | c.mu.Unlock() 590 | 591 | return nil 592 | } 593 | -------------------------------------------------------------------------------- /consumer_metrics.go: -------------------------------------------------------------------------------- 1 | package pubsub 2 | 3 | import ( 4 | "github.com/prometheus/client_golang/prometheus" 5 | "github.com/samber/mo" 6 | ) 7 | 8 | func NewConsumerOptionsMetricsThresholdWarning(v float64) ConsumerOptionsMetricsThreshold { 9 | return ConsumerOptionsMetricsThreshold{ 10 | warning: mo.Some(mo.Left[float64, func() float64](v)), 11 | } 12 | } 13 | 14 | func NewConsumerOptionsMetricsThresholdError(v float64) ConsumerOptionsMetricsThreshold { 15 | return ConsumerOptionsMetricsThreshold{ 16 | eRror: mo.Some(mo.Left[float64, func() float64](v)), 17 | } 18 | } 19 | 20 | func NewConsumerOptionsMetricsThresholdWarningAndError(v1 float64, v2 float64) ConsumerOptionsMetricsThreshold { 21 | return ConsumerOptionsMetricsThreshold{ 22 | warning: mo.Some(mo.Left[float64, func() float64](v1)), 23 | eRror: mo.Some(mo.Left[float64, func() float64](v2)), 24 | } 25 | } 26 | 27 | func NewConsumerOptionsMetricsThresholdWarningFunc(f func() float64) ConsumerOptionsMetricsThreshold { 28 | return ConsumerOptionsMetricsThreshold{ 29 | warning: mo.Some(mo.Right[float64, func() float64](f)), 30 | } 31 | } 32 | 33 | func NewConsumerOptionsMetricsThresholdErrorFunc(f func() float64) ConsumerOptionsMetricsThreshold { 34 | return ConsumerOptionsMetricsThreshold{ 35 | eRror: mo.Some(mo.Right[float64, func() float64](f)), 36 | } 37 | } 38 | 39 | func NewConsumerOptionsMetricsThresholdWarningFuncAndErrorFunc(f1 func() float64, f2 func() float64) ConsumerOptionsMetricsThreshold { 40 | return ConsumerOptionsMetricsThreshold{ 41 | warning: mo.Some(mo.Right[float64, func() float64](f1)), 42 | eRror: mo.Some(mo.Right[float64, func() float64](f2)), 43 | } 44 | } 45 | 46 | type ConsumerOptionsMetricsThreshold struct { 47 | warning mo.Option[mo.Either[float64, func() float64]] 48 | eRror mo.Option[mo.Either[float64, func() float64]] 49 | } 50 | 51 | func (opt ConsumerOptionsMetricsThreshold) metrics(name string, description string, consumerName string) []*metric { 52 | metrics := []*metric{} 53 | 54 | if w, ok := opt.warning.Get(); ok { 55 | metrics = append(metrics, newMetric( 56 | name+"_warning", 57 | description, 58 | consumerName, 59 | w, 60 | )) 61 | } 62 | 63 | if e, ok := opt.eRror.Get(); ok { 64 | metrics = append(metrics, newMetric( 65 | name+"_error", 66 | description, 67 | consumerName, 68 | e, 69 | )) 70 | } 71 | 72 | return metrics 73 | } 74 | 75 | type ConsumerOptionsMetrics struct { 76 | QueueMessageBytesThreshold ConsumerOptionsMetricsThreshold 77 | QueueMessagesThreshold ConsumerOptionsMetricsThreshold 78 | 79 | DeadLetterQueueMessageBytesThreshold ConsumerOptionsMetricsThreshold 80 | DeadLetterQueueMessagesThreshold ConsumerOptionsMetricsThreshold 81 | DeadLetterQueueMessageRateThreshold ConsumerOptionsMetricsThreshold 82 | 83 | RetryQueueMessageBytesThreshold ConsumerOptionsMetricsThreshold 84 | RetryQueueMessagesThreshold ConsumerOptionsMetricsThreshold 85 | RetryQueueMessageRateThreshold ConsumerOptionsMetricsThreshold 86 | } 87 | 88 | func (opt ConsumerOptionsMetrics) metrics(consumerName string) []*metric { 89 | metrics := []*metric{} 90 | 91 | metrics = append(metrics, opt.QueueMessageBytesThreshold.metrics("queue_message_bytes_threshold", "", consumerName)...) 92 | metrics = append(metrics, opt.QueueMessagesThreshold.metrics("queue_messages_threshold", "", consumerName)...) 93 | 94 | metrics = append(metrics, opt.DeadLetterQueueMessageBytesThreshold.metrics("deadletter_queue_message_bytes_threshold", "", consumerName)...) 95 | metrics = append(metrics, opt.DeadLetterQueueMessagesThreshold.metrics("deadletter_queue_messages_threshold", "", consumerName)...) 96 | metrics = append(metrics, opt.DeadLetterQueueMessageRateThreshold.metrics("deadletter_queue_messages_rate_threshold", "", consumerName)...) 97 | 98 | metrics = append(metrics, opt.RetryQueueMessageBytesThreshold.metrics("retry_queue_message_bytes_threshold", "", consumerName)...) 99 | metrics = append(metrics, opt.RetryQueueMessagesThreshold.metrics("retry_queue_messages_threshold", "", consumerName)...) 100 | metrics = append(metrics, opt.RetryQueueMessageRateThreshold.metrics("retry_queue_messages_rate_threshold", "", consumerName)...) 101 | 102 | return metrics 103 | } 104 | 105 | func newMetric(name string, description string, consumer string, value mo.Either[float64, func() float64]) *metric { 106 | return &metric{ 107 | value: value, 108 | consumer: consumer, 109 | desc: prometheus.NewDesc( 110 | name, 111 | description, 112 | []string{"consumer"}, 113 | nil, 114 | ), 115 | } 116 | } 117 | 118 | type metric struct { 119 | value mo.Either[float64, func() float64] 120 | consumer string 121 | desc *prometheus.Desc 122 | } 123 | 124 | func (m *metric) Describe(ch chan<- *prometheus.Desc) { 125 | ch <- m.desc 126 | } 127 | 128 | func (m *metric) Collect(ch chan<- prometheus.Metric) { 129 | ch <- prometheus.MustNewConstMetric( 130 | m.desc, 131 | prometheus.GaugeValue, 132 | eitherToValue(m.value), 133 | m.consumer, 134 | ) 135 | } 136 | 137 | func eitherToValue(o mo.Either[float64, func() float64]) float64 { 138 | if v, ok := o.Left(); ok { 139 | return v 140 | } 141 | 142 | return o.MustRight()() 143 | } 144 | -------------------------------------------------------------------------------- /doc/defer.key: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samber/go-amqp-pubsub/583759a228b7bf9cd6170f12678745dd0f783f7c/doc/defer.key -------------------------------------------------------------------------------- /doc/defer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samber/go-amqp-pubsub/583759a228b7bf9cd6170f12678745dd0f783f7c/doc/defer.png -------------------------------------------------------------------------------- /doc/retry.key: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samber/go-amqp-pubsub/583759a228b7bf9cd6170f12678745dd0f783f7c/doc/retry.key -------------------------------------------------------------------------------- /doc/retry.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samber/go-amqp-pubsub/583759a228b7bf9cd6170f12678745dd0f783f7c/doc/retry.png -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | rabbitmq: 5 | image: rabbitmq:3-management 6 | ports: 7 | - 5672:5672 8 | - 15672:15672 9 | volumes: 10 | - ./examples/rabbitmq.conf:/etc/rabbitmq/rabbitmq.conf 11 | environment: 12 | - RABBITMQ_DEFAULT_USER=dev 13 | - RABBITMQ_DEFAULT_PASS=dev 14 | -------------------------------------------------------------------------------- /examples/consumer-single-active/go.mod: -------------------------------------------------------------------------------- 1 | module consumer 2 | 3 | go 1.19 4 | 5 | require ( 6 | github.com/rabbitmq/amqp091-go v1.8.1 7 | github.com/samber/go-amqp-pubsub v0.0.0-20210710222824-c4781d5ae30d 8 | github.com/samber/lo v1.35.0 9 | github.com/samber/mo v1.5.1 10 | github.com/sirupsen/logrus v1.9.0 11 | ) 12 | 13 | require ( 14 | github.com/google/uuid v1.3.0 // indirect 15 | golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17 // indirect 16 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 // indirect 17 | ) 18 | 19 | replace github.com/samber/go-amqp-pubsub => ../.. 20 | -------------------------------------------------------------------------------- /examples/consumer-single-active/go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 2 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 3 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= 5 | github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 6 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 7 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 8 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 9 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 10 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 11 | github.com/rabbitmq/amqp091-go v1.8.1 h1:RejT1SBUim5doqcL6s7iN6SBmsQqyTgXb1xMlH0h1hA= 12 | github.com/rabbitmq/amqp091-go v1.8.1/go.mod h1:+jPrT9iY2eLjRaMSRHUhc3z14E/l85kv/f+6luSD3pc= 13 | github.com/samber/lo v1.35.0 h1:GlT8CV1GE+v97Y7MLF1wXvX6mjoxZ+hi61tj/ZcQwY0= 14 | github.com/samber/lo v1.35.0/go.mod h1:HLeWcJRRyLKp3+/XBJvOrerCQn9mhdKMHyd7IRlgeQ8= 15 | github.com/samber/mo v1.5.1 h1:5dRSevAB33Q/OrYwTmtksHHxquuf2urnRSUTsdTFysY= 16 | github.com/samber/mo v1.5.1/go.mod h1:pDuQgWscOVGGoEz+NAeth/Xq+MPAcXxCeph1XIAm/DU= 17 | github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0= 18 | github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= 19 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 20 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 21 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 22 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 23 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 24 | github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= 25 | github.com/thoas/go-funk v0.9.1 h1:O549iLZqPpTUQ10ykd26sZhzD+rmR5pWhuElrhbC20M= 26 | go.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A= 27 | go.uber.org/goleak v1.2.1/go.mod h1:qlT2yGI9QafXHhZZLxlSuNsMw3FFLxBr+tBRlmO1xH4= 28 | golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17 h1:3MTrJm4PyNL9NBqvYDSj3DHl46qQakyfqfWo4jgfaEM= 29 | golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17/go.mod h1:lgLbSvA5ygNOMpwM/9anMpWVlVJ7Z+cHWq/eFuinpGE= 30 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 h1:0A+M6Uqn+Eje4kHMK80dtF3JCXC4ykBgQG4Fe06QRhQ= 31 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 32 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 33 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 34 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 35 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 36 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 37 | -------------------------------------------------------------------------------- /examples/consumer-single-active/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "time" 6 | 7 | amqp "github.com/rabbitmq/amqp091-go" 8 | pubsub "github.com/samber/go-amqp-pubsub" 9 | "github.com/samber/lo" 10 | "github.com/samber/mo" 11 | "github.com/sirupsen/logrus" 12 | ) 13 | 14 | var rabbitmqURI = flag.String("rabbitmq-uri", "amqp://dev:dev@localhost:5672", "RabbitMQ URI") 15 | 16 | const ( 17 | queueName string = "product.onEdit" 18 | 19 | routingKeyProductCreated string = "product.created" 20 | routingKeyProductUpdated string = "product.updated" 21 | routingKeyProductRemoved string = "product.removed" 22 | ) 23 | 24 | func main() { 25 | flag.Parse() 26 | 27 | if rabbitmqURI == nil { 28 | logrus.Fatal("missing --rabbitmiq-uri parameter") 29 | } 30 | 31 | conn, err := pubsub.NewConnection("example-connection-1", pubsub.ConnectionOptions{ 32 | URI: *rabbitmqURI, 33 | Config: amqp.Config{ 34 | Dial: amqp.DefaultDial(time.Second), 35 | Heartbeat: time.Second, 36 | }, 37 | LazyConnection: mo.Some(false), 38 | }) 39 | if err != nil { 40 | // We ignore error, since it will reconnect automatically when available. 41 | // panic(err) 42 | } 43 | 44 | consumer := pubsub.NewConsumer(conn, "example-consumer-1", pubsub.ConsumerOptions{ 45 | Queue: pubsub.ConsumerOptionsQueue{ 46 | Name: queueName, 47 | Args: mo.Some(amqp.Table{ 48 | "x-single-active-consumer": true, 49 | }), 50 | }, 51 | Bindings: []pubsub.ConsumerOptionsBinding{ 52 | // crud 53 | {ExchangeName: "product.event", RoutingKey: "product.created"}, 54 | {ExchangeName: "product.event", RoutingKey: "product.updated"}, 55 | }, 56 | Message: pubsub.ConsumerOptionsMessage{ 57 | PrefetchCount: mo.Some(1000), 58 | }, 59 | EnableDeadLetter: mo.Some(true), 60 | }) 61 | 62 | logrus.Info("***** Let's go! ***** ") 63 | 64 | consumeMessages(consumer) 65 | 66 | logrus.Info("***** Finished! ***** ") 67 | 68 | consumer.Close() 69 | conn.Close() 70 | 71 | logrus.Info("***** Closed! ***** ") 72 | } 73 | 74 | func consumeMessages(consumer *pubsub.Consumer) { 75 | // Feel free to kill RabbitMQ and restart it, to see what happens ;) 76 | // - docker-compose kill rabbitmq 77 | // - docker-compose up rabbitmq 78 | 79 | channel := consumer.Consume() 80 | 81 | i := 0 82 | for msg := range channel { 83 | lo.Try0(func() { // handle exceptions 84 | consumeMessage(i, msg) 85 | }) 86 | 87 | i++ 88 | } 89 | } 90 | 91 | func consumeMessage(index int, msg *amqp.Delivery) { 92 | logrus.Infof("Consumed message [ID=%d, EX=%s, RK=%s] %s", index, msg.Exchange, msg.RoutingKey, string(msg.Body)) 93 | 94 | // simulate timeout 95 | // n := rand.Intn(20) 96 | // time.Sleep(time.Duration(n) * time.Second) 97 | 98 | if index%10 == 0 { 99 | msg.Reject(false) 100 | } else { 101 | msg.Ack(false) 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /examples/consumer-with-delay/go.mod: -------------------------------------------------------------------------------- 1 | module consumer 2 | 3 | go 1.19 4 | 5 | require ( 6 | github.com/rabbitmq/amqp091-go v1.8.1 7 | github.com/samber/go-amqp-pubsub v0.0.0-20210710222824-c4781d5ae30d 8 | github.com/samber/lo v1.35.0 9 | github.com/samber/mo v1.5.1 10 | github.com/sirupsen/logrus v1.9.0 11 | ) 12 | 13 | require ( 14 | github.com/google/uuid v1.3.0 // indirect 15 | golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17 // indirect 16 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 // indirect 17 | ) 18 | 19 | replace github.com/samber/go-amqp-pubsub => ../.. 20 | -------------------------------------------------------------------------------- /examples/consumer-with-delay/go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 2 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 3 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= 5 | github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 6 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 7 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 8 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 9 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 10 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 11 | github.com/rabbitmq/amqp091-go v1.8.1 h1:RejT1SBUim5doqcL6s7iN6SBmsQqyTgXb1xMlH0h1hA= 12 | github.com/rabbitmq/amqp091-go v1.8.1/go.mod h1:+jPrT9iY2eLjRaMSRHUhc3z14E/l85kv/f+6luSD3pc= 13 | github.com/samber/lo v1.35.0 h1:GlT8CV1GE+v97Y7MLF1wXvX6mjoxZ+hi61tj/ZcQwY0= 14 | github.com/samber/lo v1.35.0/go.mod h1:HLeWcJRRyLKp3+/XBJvOrerCQn9mhdKMHyd7IRlgeQ8= 15 | github.com/samber/mo v1.5.1 h1:5dRSevAB33Q/OrYwTmtksHHxquuf2urnRSUTsdTFysY= 16 | github.com/samber/mo v1.5.1/go.mod h1:pDuQgWscOVGGoEz+NAeth/Xq+MPAcXxCeph1XIAm/DU= 17 | github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0= 18 | github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= 19 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 20 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 21 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 22 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 23 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 24 | github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= 25 | github.com/thoas/go-funk v0.9.1 h1:O549iLZqPpTUQ10ykd26sZhzD+rmR5pWhuElrhbC20M= 26 | go.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A= 27 | go.uber.org/goleak v1.2.1/go.mod h1:qlT2yGI9QafXHhZZLxlSuNsMw3FFLxBr+tBRlmO1xH4= 28 | golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17 h1:3MTrJm4PyNL9NBqvYDSj3DHl46qQakyfqfWo4jgfaEM= 29 | golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17/go.mod h1:lgLbSvA5ygNOMpwM/9anMpWVlVJ7Z+cHWq/eFuinpGE= 30 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 h1:0A+M6Uqn+Eje4kHMK80dtF3JCXC4ykBgQG4Fe06QRhQ= 31 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 32 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 33 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 34 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 35 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 36 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 37 | -------------------------------------------------------------------------------- /examples/consumer-with-delay/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "time" 6 | 7 | amqp "github.com/rabbitmq/amqp091-go" 8 | pubsub "github.com/samber/go-amqp-pubsub" 9 | "github.com/samber/lo" 10 | "github.com/samber/mo" 11 | "github.com/sirupsen/logrus" 12 | ) 13 | 14 | var rabbitmqURI = flag.String("rabbitmq-uri", "amqp://dev:dev@localhost:5672", "RabbitMQ URI") 15 | 16 | const ( 17 | queueName string = "product.onEdit" 18 | 19 | routingKeyProductCreated string = "product.created" 20 | routingKeyProductUpdated string = "product.updated" 21 | routingKeyProductRemoved string = "product.removed" 22 | ) 23 | 24 | func main() { 25 | flag.Parse() 26 | 27 | if rabbitmqURI == nil { 28 | logrus.Fatal("missing --rabbitmiq-uri parameter") 29 | } 30 | 31 | conn, err := pubsub.NewConnection("example-connection-1", pubsub.ConnectionOptions{ 32 | URI: *rabbitmqURI, 33 | Config: amqp.Config{ 34 | Dial: amqp.DefaultDial(time.Second), 35 | Heartbeat: time.Second, 36 | }, 37 | LazyConnection: mo.Some(false), 38 | }) 39 | if err != nil { 40 | // We ignore error, since it will reconnect automatically when available. 41 | // panic(err) 42 | } 43 | 44 | consumer := pubsub.NewConsumer(conn, "example-consumer-1", pubsub.ConsumerOptions{ 45 | Queue: pubsub.ConsumerOptionsQueue{ 46 | Name: queueName, 47 | }, 48 | Bindings: []pubsub.ConsumerOptionsBinding{ 49 | // crud 50 | {ExchangeName: "product.event", RoutingKey: "product.created"}, 51 | {ExchangeName: "product.event", RoutingKey: "product.updated"}, 52 | {ExchangeName: "product.event", RoutingKey: "product.removed"}, 53 | }, 54 | Message: pubsub.ConsumerOptionsMessage{ 55 | PrefetchCount: mo.Some(1000), 56 | }, 57 | EnableDeadLetter: mo.Some(true), 58 | Defer: mo.Some(5 * time.Second), 59 | }) 60 | 61 | logrus.Info("***** Let's go! ***** ") 62 | 63 | consumeMessages(consumer) 64 | 65 | logrus.Info("***** Finished! ***** ") 66 | 67 | consumer.Close() 68 | conn.Close() 69 | 70 | logrus.Info("***** Closed! ***** ") 71 | } 72 | 73 | func consumeMessages(consumer *pubsub.Consumer) { 74 | // Feel free to kill RabbitMQ and restart it, to see what happens ;) 75 | // - docker-compose kill rabbitmq 76 | // - docker-compose up rabbitmq 77 | 78 | channel := consumer.Consume() 79 | 80 | i := 0 81 | for msg := range channel { 82 | lo.Try0(func() { // handle exceptions 83 | consumeMessage(i, msg) 84 | }) 85 | 86 | i++ 87 | } 88 | } 89 | 90 | func consumeMessage(index int, msg *amqp.Delivery) { 91 | logrus.Infof("Consumed message [ID=%d, EX=%s, RK=%s, TIME=%s] %s", index, msg.Exchange, msg.RoutingKey, time.Now().Format("15:04:05.999"), string(msg.Body)) 92 | 93 | msg.Ack(false) 94 | } 95 | -------------------------------------------------------------------------------- /examples/consumer-with-dynamic-binding/go.mod: -------------------------------------------------------------------------------- 1 | module consumer 2 | 3 | go 1.19 4 | 5 | require ( 6 | github.com/rabbitmq/amqp091-go v1.8.1 7 | github.com/samber/go-amqp-pubsub v0.0.0-20210710222824-c4781d5ae30d 8 | github.com/samber/lo v1.35.0 9 | github.com/samber/mo v1.5.1 10 | github.com/sirupsen/logrus v1.9.0 11 | ) 12 | 13 | require ( 14 | github.com/google/uuid v1.3.0 // indirect 15 | golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17 // indirect 16 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 // indirect 17 | ) 18 | 19 | replace github.com/samber/go-amqp-pubsub => ../.. 20 | -------------------------------------------------------------------------------- /examples/consumer-with-dynamic-binding/go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 2 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 3 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= 5 | github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 6 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 7 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 8 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 9 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 10 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 11 | github.com/rabbitmq/amqp091-go v1.8.1 h1:RejT1SBUim5doqcL6s7iN6SBmsQqyTgXb1xMlH0h1hA= 12 | github.com/rabbitmq/amqp091-go v1.8.1/go.mod h1:+jPrT9iY2eLjRaMSRHUhc3z14E/l85kv/f+6luSD3pc= 13 | github.com/samber/lo v1.35.0 h1:GlT8CV1GE+v97Y7MLF1wXvX6mjoxZ+hi61tj/ZcQwY0= 14 | github.com/samber/lo v1.35.0/go.mod h1:HLeWcJRRyLKp3+/XBJvOrerCQn9mhdKMHyd7IRlgeQ8= 15 | github.com/samber/mo v1.5.1 h1:5dRSevAB33Q/OrYwTmtksHHxquuf2urnRSUTsdTFysY= 16 | github.com/samber/mo v1.5.1/go.mod h1:pDuQgWscOVGGoEz+NAeth/Xq+MPAcXxCeph1XIAm/DU= 17 | github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0= 18 | github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= 19 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 20 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 21 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 22 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 23 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 24 | github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= 25 | github.com/thoas/go-funk v0.9.1 h1:O549iLZqPpTUQ10ykd26sZhzD+rmR5pWhuElrhbC20M= 26 | go.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A= 27 | go.uber.org/goleak v1.2.1/go.mod h1:qlT2yGI9QafXHhZZLxlSuNsMw3FFLxBr+tBRlmO1xH4= 28 | golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17 h1:3MTrJm4PyNL9NBqvYDSj3DHl46qQakyfqfWo4jgfaEM= 29 | golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17/go.mod h1:lgLbSvA5ygNOMpwM/9anMpWVlVJ7Z+cHWq/eFuinpGE= 30 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 h1:0A+M6Uqn+Eje4kHMK80dtF3JCXC4ykBgQG4Fe06QRhQ= 31 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 32 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 33 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 34 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 35 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 36 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 37 | -------------------------------------------------------------------------------- /examples/consumer-with-dynamic-binding/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "time" 7 | 8 | amqp "github.com/rabbitmq/amqp091-go" 9 | pubsub "github.com/samber/go-amqp-pubsub" 10 | "github.com/samber/lo" 11 | "github.com/samber/mo" 12 | "github.com/sirupsen/logrus" 13 | ) 14 | 15 | var rabbitmqURI = flag.String("rabbitmq-uri", "amqp://dev:dev@localhost:5672", "RabbitMQ URI") 16 | 17 | const ( 18 | queueName string = "product.onEdit" 19 | 20 | routingKeyProductCreated string = "product.created" 21 | routingKeyProductUpdated string = "product.updated" 22 | routingKeyProductRemoved string = "product.removed" 23 | ) 24 | 25 | func main() { 26 | flag.Parse() 27 | 28 | if rabbitmqURI == nil { 29 | logrus.Fatal("missing --rabbitmiq-uri parameter") 30 | } 31 | 32 | conn, err := pubsub.NewConnection("example-connection-1", pubsub.ConnectionOptions{ 33 | URI: *rabbitmqURI, 34 | Config: amqp.Config{ 35 | Dial: amqp.DefaultDial(time.Second), 36 | Heartbeat: time.Second, 37 | }, 38 | LazyConnection: mo.Some(false), 39 | }) 40 | if err != nil { 41 | // We ignore error, since it will reconnect automatically when available. 42 | // panic(err) 43 | } 44 | 45 | consumer := pubsub.NewConsumer(conn, "example-consumer-1", pubsub.ConsumerOptions{ 46 | Queue: pubsub.ConsumerOptionsQueue{ 47 | Name: queueName, 48 | }, 49 | Bindings: []pubsub.ConsumerOptionsBinding{ 50 | // crud 51 | {ExchangeName: "product.event", RoutingKey: "product.created"}, 52 | {ExchangeName: "product.event", RoutingKey: "product.updated"}, 53 | }, 54 | Message: pubsub.ConsumerOptionsMessage{ 55 | PrefetchCount: mo.Some(1000), 56 | }, 57 | EnableDeadLetter: mo.Some(true), 58 | }) 59 | 60 | logrus.Info("***** Let's go! ***** ") 61 | 62 | go fuzzyConcurrentQueueBinding(consumer) 63 | consumeMessages(consumer) 64 | 65 | logrus.Info("***** Finished! ***** ") 66 | 67 | consumer.Close() 68 | conn.Close() 69 | 70 | logrus.Info("***** Closed! ***** ") 71 | } 72 | 73 | func consumeMessages(consumer *pubsub.Consumer) { 74 | // Feel free to kill RabbitMQ and restart it, to see what happens ;) 75 | // - docker-compose kill rabbitmq 76 | // - docker-compose up rabbitmq 77 | 78 | channel := consumer.Consume() 79 | 80 | i := 0 81 | for msg := range channel { 82 | lo.Try0(func() { // handle exceptions 83 | consumeMessage(i, msg) 84 | }) 85 | 86 | i++ 87 | } 88 | } 89 | 90 | func consumeMessage(index int, msg *amqp.Delivery) { 91 | logrus.Infof("Consumed message [ID=%d, EX=%s, RK=%s] %s", index, msg.Exchange, msg.RoutingKey, string(msg.Body)) 92 | 93 | if index%10 == 0 { 94 | msg.Reject(false) 95 | } else { 96 | msg.Ack(false) 97 | } 98 | } 99 | 100 | func fuzzyConcurrentQueueBinding(consumer *pubsub.Consumer) { 101 | for { 102 | go func() { 103 | exchangeName := "product.event" 104 | routingKey := "fuzzy." + lo.RandomString(5, lo.AlphanumericCharset) 105 | 106 | consumer.AddBinding(exchangeName, routingKey, mo.None[amqp.Table]()) 107 | time.Sleep(10 * time.Millisecond) 108 | consumer.RemoveBinding(exchangeName, routingKey, mo.None[amqp.Table]()) 109 | 110 | fmt.Printf("fuzzy: binding/unbinding exchange '%s' to queue '%s' using routing key '%s'\n", exchangeName, queueName, routingKey) 111 | }() 112 | 113 | time.Sleep(10 * time.Millisecond) 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /examples/consumer-with-graceful-shutdown/go.mod: -------------------------------------------------------------------------------- 1 | module consumer 2 | 3 | go 1.21 4 | 5 | toolchain go1.21.0 6 | 7 | require ( 8 | github.com/rabbitmq/amqp091-go v1.8.1 9 | github.com/samber/go-amqp-pubsub v0.0.0-20210710222824-c4781d5ae30d 10 | github.com/samber/lo v1.35.0 11 | github.com/samber/mo v1.5.1 12 | github.com/sirupsen/logrus v1.9.0 13 | ) 14 | 15 | require ( 16 | github.com/beorn7/perks v1.0.1 // indirect 17 | github.com/cespare/xxhash/v2 v2.2.0 // indirect 18 | github.com/golang/protobuf v1.5.3 // indirect 19 | github.com/google/uuid v1.3.0 // indirect 20 | github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect 21 | github.com/prometheus/client_golang v1.16.0 // indirect 22 | github.com/prometheus/client_model v0.3.0 // indirect 23 | github.com/prometheus/common v0.42.0 // indirect 24 | github.com/prometheus/procfs v0.10.1 // indirect 25 | golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17 // indirect 26 | golang.org/x/sys v0.8.0 // indirect 27 | google.golang.org/protobuf v1.33.0 // indirect 28 | ) 29 | 30 | replace github.com/samber/go-amqp-pubsub => ../.. 31 | -------------------------------------------------------------------------------- /examples/consumer-with-graceful-shutdown/go.sum: -------------------------------------------------------------------------------- 1 | github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= 2 | github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 3 | github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= 4 | github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 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/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 9 | github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= 10 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 11 | github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= 12 | github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= 13 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 14 | github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= 15 | github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 16 | github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= 17 | github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 18 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 19 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 20 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 21 | github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= 22 | github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= 23 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 24 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 25 | github.com/prometheus/client_golang v1.16.0 h1:yk/hx9hDbrGHovbci4BY+pRMfSuuat626eFsHb7tmT8= 26 | github.com/prometheus/client_golang v1.16.0/go.mod h1:Zsulrv/L9oM40tJ7T815tM89lFEugiJ9HzIqaAx4LKc= 27 | github.com/prometheus/client_model v0.3.0 h1:UBgGFHqYdG/TPFD1B1ogZywDqEkwp3fBMvqdiQ7Xew4= 28 | github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w= 29 | github.com/prometheus/common v0.42.0 h1:EKsfXEYo4JpWMHH5cg+KOUWeuJSov1Id8zGR8eeI1YM= 30 | github.com/prometheus/common v0.42.0/go.mod h1:xBwqVerjNdUDjgODMpudtOMwlOwf2SaTr1yjz4b7Zbc= 31 | github.com/prometheus/procfs v0.10.1 h1:kYK1Va/YMlutzCGazswoHKo//tZVlFpKYh+PymziUAg= 32 | github.com/prometheus/procfs v0.10.1/go.mod h1:nwNm2aOCAYw8uTR/9bWRREkZFxAUcWzPHWJq+XBB/FM= 33 | github.com/rabbitmq/amqp091-go v1.8.1 h1:RejT1SBUim5doqcL6s7iN6SBmsQqyTgXb1xMlH0h1hA= 34 | github.com/rabbitmq/amqp091-go v1.8.1/go.mod h1:+jPrT9iY2eLjRaMSRHUhc3z14E/l85kv/f+6luSD3pc= 35 | github.com/samber/lo v1.35.0 h1:GlT8CV1GE+v97Y7MLF1wXvX6mjoxZ+hi61tj/ZcQwY0= 36 | github.com/samber/lo v1.35.0/go.mod h1:HLeWcJRRyLKp3+/XBJvOrerCQn9mhdKMHyd7IRlgeQ8= 37 | github.com/samber/mo v1.5.1 h1:5dRSevAB33Q/OrYwTmtksHHxquuf2urnRSUTsdTFysY= 38 | github.com/samber/mo v1.5.1/go.mod h1:pDuQgWscOVGGoEz+NAeth/Xq+MPAcXxCeph1XIAm/DU= 39 | github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0= 40 | github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= 41 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 42 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 43 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 44 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 45 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 46 | github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= 47 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 48 | github.com/thoas/go-funk v0.9.1 h1:O549iLZqPpTUQ10ykd26sZhzD+rmR5pWhuElrhbC20M= 49 | github.com/thoas/go-funk v0.9.1/go.mod h1:+IWnUfUmFO1+WVYQWQtIJHeRRdaIyyYglZN7xzUPe4Q= 50 | go.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A= 51 | go.uber.org/goleak v1.2.1/go.mod h1:qlT2yGI9QafXHhZZLxlSuNsMw3FFLxBr+tBRlmO1xH4= 52 | golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17 h1:3MTrJm4PyNL9NBqvYDSj3DHl46qQakyfqfWo4jgfaEM= 53 | golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17/go.mod h1:lgLbSvA5ygNOMpwM/9anMpWVlVJ7Z+cHWq/eFuinpGE= 54 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 55 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 56 | golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU= 57 | golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 58 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 59 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 60 | google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 61 | google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= 62 | google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= 63 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 64 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 65 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 66 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 67 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 68 | -------------------------------------------------------------------------------- /examples/consumer-with-graceful-shutdown/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "os" 6 | "os/signal" 7 | "syscall" 8 | "time" 9 | 10 | amqp "github.com/rabbitmq/amqp091-go" 11 | pubsub "github.com/samber/go-amqp-pubsub" 12 | "github.com/samber/lo" 13 | "github.com/samber/mo" 14 | "github.com/sirupsen/logrus" 15 | ) 16 | 17 | var rabbitmqURI = flag.String("rabbitmq-uri", "amqp://dev:dev@localhost:5672", "RabbitMQ URI") 18 | 19 | const ( 20 | queueName string = "product.onEdit" 21 | 22 | routingKeyProductCreated string = "product.created" 23 | routingKeyProductUpdated string = "product.updated" 24 | routingKeyProductRemoved string = "product.removed" 25 | ) 26 | 27 | func main() { 28 | flag.Parse() 29 | 30 | if rabbitmqURI == nil { 31 | logrus.Fatal("missing --rabbitmiq-uri parameter") 32 | } 33 | 34 | conn, err := pubsub.NewConnection("example-connection-1", pubsub.ConnectionOptions{ 35 | URI: *rabbitmqURI, 36 | Config: amqp.Config{ 37 | Dial: amqp.DefaultDial(time.Second), 38 | Heartbeat: time.Second, 39 | }, 40 | LazyConnection: mo.Some(true), 41 | }) 42 | if err != nil { 43 | // We ignore error, since it will reconnect automatically when available. 44 | // panic(err) 45 | } 46 | 47 | consumer := pubsub.NewConsumer(conn, "example-consumer-1", pubsub.ConsumerOptions{ 48 | Queue: pubsub.ConsumerOptionsQueue{ 49 | Name: queueName, 50 | }, 51 | Bindings: []pubsub.ConsumerOptionsBinding{ 52 | // crud 53 | {ExchangeName: "product.event", RoutingKey: "product.created"}, 54 | {ExchangeName: "product.event", RoutingKey: "product.updated"}, 55 | }, 56 | Message: pubsub.ConsumerOptionsMessage{ 57 | PrefetchCount: mo.Some(0), 58 | }, 59 | EnableDeadLetter: mo.Some(true), 60 | }) 61 | 62 | // handle ctrl+c 63 | c := make(chan os.Signal) 64 | signal.Notify(c, os.Interrupt, syscall.SIGTERM) 65 | go func() { 66 | <-c 67 | logrus.Info("***** Handled signal ***** ") 68 | consumer.Close() 69 | conn.Close() 70 | }() 71 | 72 | logrus.Info("***** Let's go! ***** ") 73 | 74 | consumeMessages(consumer) 75 | 76 | logrus.Info("***** Finished! ***** ") 77 | 78 | // close again, just in case a segfault would be hidden in the lib 79 | consumer.Close() 80 | conn.Close() 81 | 82 | logrus.Info("***** Closed! ***** ") 83 | } 84 | 85 | func consumeMessages(consumer *pubsub.Consumer) { 86 | // Feel free to kill RabbitMQ and restart it, to see what happens ;) 87 | // - docker-compose kill rabbitmq 88 | // - docker-compose up rabbitmq 89 | 90 | channel := consumer.Consume() 91 | 92 | i := 0 93 | for msg := range channel { 94 | lo.Try0(func() { // handle exceptions 95 | consumeMessage(i, msg) 96 | }) 97 | 98 | i++ 99 | } 100 | } 101 | 102 | func consumeMessage(index int, msg *amqp.Delivery) { 103 | logrus.Infof("Consumed message [ID=%d, EX=%s, RK=%s] %s", index, msg.Exchange, msg.RoutingKey, string(msg.Body)) 104 | 105 | // simulate timeout 106 | // time.Sleep(60 * time.Second) 107 | 108 | if index%10 == 0 { 109 | msg.Reject(false) 110 | } else { 111 | msg.Ack(false) 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /examples/consumer-with-lazy-retry/go.mod: -------------------------------------------------------------------------------- 1 | module consumer 2 | 3 | go 1.19 4 | 5 | require ( 6 | github.com/rabbitmq/amqp091-go v1.8.1 7 | github.com/samber/go-amqp-pubsub v0.0.0-20210710222824-c4781d5ae30d 8 | github.com/samber/lo v1.35.0 9 | github.com/samber/mo v1.5.1 10 | github.com/sirupsen/logrus v1.9.0 11 | ) 12 | 13 | require ( 14 | github.com/google/uuid v1.3.0 // indirect 15 | golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17 // indirect 16 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 // indirect 17 | ) 18 | 19 | replace github.com/samber/go-amqp-pubsub => ../.. 20 | -------------------------------------------------------------------------------- /examples/consumer-with-lazy-retry/go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 2 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 3 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= 5 | github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 6 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 7 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 8 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 9 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 10 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 11 | github.com/rabbitmq/amqp091-go v1.8.1 h1:RejT1SBUim5doqcL6s7iN6SBmsQqyTgXb1xMlH0h1hA= 12 | github.com/rabbitmq/amqp091-go v1.8.1/go.mod h1:+jPrT9iY2eLjRaMSRHUhc3z14E/l85kv/f+6luSD3pc= 13 | github.com/samber/lo v1.35.0 h1:GlT8CV1GE+v97Y7MLF1wXvX6mjoxZ+hi61tj/ZcQwY0= 14 | github.com/samber/lo v1.35.0/go.mod h1:HLeWcJRRyLKp3+/XBJvOrerCQn9mhdKMHyd7IRlgeQ8= 15 | github.com/samber/mo v1.5.1 h1:5dRSevAB33Q/OrYwTmtksHHxquuf2urnRSUTsdTFysY= 16 | github.com/samber/mo v1.5.1/go.mod h1:pDuQgWscOVGGoEz+NAeth/Xq+MPAcXxCeph1XIAm/DU= 17 | github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0= 18 | github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= 19 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 20 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 21 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 22 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 23 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 24 | github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= 25 | github.com/thoas/go-funk v0.9.1 h1:O549iLZqPpTUQ10ykd26sZhzD+rmR5pWhuElrhbC20M= 26 | go.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A= 27 | go.uber.org/goleak v1.2.1/go.mod h1:qlT2yGI9QafXHhZZLxlSuNsMw3FFLxBr+tBRlmO1xH4= 28 | golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17 h1:3MTrJm4PyNL9NBqvYDSj3DHl46qQakyfqfWo4jgfaEM= 29 | golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17/go.mod h1:lgLbSvA5ygNOMpwM/9anMpWVlVJ7Z+cHWq/eFuinpGE= 30 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 h1:0A+M6Uqn+Eje4kHMK80dtF3JCXC4ykBgQG4Fe06QRhQ= 31 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 32 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 33 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 34 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 35 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 36 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 37 | -------------------------------------------------------------------------------- /examples/consumer-with-lazy-retry/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "time" 6 | 7 | amqp "github.com/rabbitmq/amqp091-go" 8 | pubsub "github.com/samber/go-amqp-pubsub" 9 | "github.com/samber/lo" 10 | "github.com/samber/mo" 11 | "github.com/sirupsen/logrus" 12 | ) 13 | 14 | var rabbitmqURI = flag.String("rabbitmq-uri", "amqp://dev:dev@localhost:5672", "RabbitMQ URI") 15 | 16 | const ( 17 | queueName string = "product.onEdit" 18 | 19 | routingKeyProductCreated string = "product.created" 20 | routingKeyProductUpdated string = "product.updated" 21 | routingKeyProductRemoved string = "product.removed" 22 | ) 23 | 24 | func main() { 25 | flag.Parse() 26 | 27 | if rabbitmqURI == nil { 28 | logrus.Fatal("missing --rabbitmiq-uri parameter") 29 | } 30 | 31 | conn, err := pubsub.NewConnection("example-connection-1", pubsub.ConnectionOptions{ 32 | URI: *rabbitmqURI, 33 | Config: amqp.Config{ 34 | Dial: amqp.DefaultDial(time.Second), 35 | Heartbeat: time.Second, 36 | }, 37 | LazyConnection: mo.Some(false), 38 | }) 39 | if err != nil { 40 | // We ignore error, since it will reconnect automatically when available. 41 | // panic(err) 42 | } 43 | 44 | consumer := pubsub.NewConsumer(conn, "example-consumer-1", pubsub.ConsumerOptions{ 45 | Queue: pubsub.ConsumerOptionsQueue{ 46 | Name: queueName, 47 | }, 48 | Bindings: []pubsub.ConsumerOptionsBinding{ 49 | // crud 50 | {ExchangeName: "product.event", RoutingKey: "product.created"}, 51 | {ExchangeName: "product.event", RoutingKey: "product.updated"}, 52 | {ExchangeName: "product.event", RoutingKey: "product.removed"}, 53 | }, 54 | Message: pubsub.ConsumerOptionsMessage{ 55 | PrefetchCount: mo.Some(1000), 56 | }, 57 | EnableDeadLetter: mo.Some(true), 58 | RetryStrategy: mo.Some(pubsub.NewLazyRetryStrategy(3)), // max 3 attempts 59 | RetryConsistency: mo.Some(pubsub.EventuallyConsistentRetry), 60 | }) 61 | 62 | logrus.Info("***** Let's go! ***** ") 63 | 64 | consumeMessages(consumer) 65 | 66 | logrus.Info("***** Finished! ***** ") 67 | 68 | consumer.Close() 69 | conn.Close() 70 | 71 | logrus.Info("***** Closed! ***** ") 72 | } 73 | 74 | func consumeMessages(consumer *pubsub.Consumer) { 75 | // Feel free to kill RabbitMQ and restart it, to see what happens ;) 76 | // - docker-compose kill rabbitmq 77 | // - docker-compose up rabbitmq 78 | 79 | channel := consumer.Consume() 80 | 81 | i := 0 82 | for msg := range channel { 83 | lo.Try0(func() { // handle exceptions 84 | consumeMessage(i, msg) 85 | }) 86 | 87 | i++ 88 | } 89 | } 90 | 91 | func consumeMessage(index int, msg *amqp.Delivery) { 92 | logrus.Infof("Consumed message [ID=%d, EX=%s, RK=%s, TIME=%s] %s", index, msg.Exchange, msg.RoutingKey, time.Now().Format("15:04:05.999"), string(msg.Body)) 93 | 94 | pubsub.RejectWithRetry(msg, 5*time.Second) 95 | //msg.Reject(false) 96 | } 97 | -------------------------------------------------------------------------------- /examples/consumer-with-metrics/go.mod: -------------------------------------------------------------------------------- 1 | module consumer 2 | 3 | go 1.19 4 | 5 | require ( 6 | github.com/prometheus/client_golang v1.16.0 7 | github.com/rabbitmq/amqp091-go v1.8.1 8 | github.com/samber/go-amqp-pubsub v0.0.0-20210710222824-c4781d5ae30d 9 | github.com/samber/lo v1.35.0 10 | github.com/samber/mo v1.5.1 11 | github.com/sirupsen/logrus v1.9.0 12 | ) 13 | 14 | require ( 15 | github.com/beorn7/perks v1.0.1 // indirect 16 | github.com/cespare/xxhash/v2 v2.2.0 // indirect 17 | github.com/golang/protobuf v1.5.3 // indirect 18 | github.com/google/uuid v1.3.0 // indirect 19 | github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect 20 | github.com/prometheus/client_model v0.3.0 // indirect 21 | github.com/prometheus/common v0.42.0 // indirect 22 | github.com/prometheus/procfs v0.10.1 // indirect 23 | golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17 // indirect 24 | golang.org/x/sys v0.8.0 // indirect 25 | google.golang.org/protobuf v1.33.0 // indirect 26 | ) 27 | 28 | replace github.com/samber/go-amqp-pubsub => ../.. 29 | -------------------------------------------------------------------------------- /examples/consumer-with-metrics/go.sum: -------------------------------------------------------------------------------- 1 | github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= 2 | github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 3 | github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= 4 | github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 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/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 9 | github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= 10 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 11 | github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= 12 | github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= 13 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 14 | github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= 15 | github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= 16 | github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 17 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 18 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 19 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 20 | github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= 21 | github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= 22 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 23 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 24 | github.com/prometheus/client_golang v1.16.0 h1:yk/hx9hDbrGHovbci4BY+pRMfSuuat626eFsHb7tmT8= 25 | github.com/prometheus/client_golang v1.16.0/go.mod h1:Zsulrv/L9oM40tJ7T815tM89lFEugiJ9HzIqaAx4LKc= 26 | github.com/prometheus/client_model v0.3.0 h1:UBgGFHqYdG/TPFD1B1ogZywDqEkwp3fBMvqdiQ7Xew4= 27 | github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w= 28 | github.com/prometheus/common v0.42.0 h1:EKsfXEYo4JpWMHH5cg+KOUWeuJSov1Id8zGR8eeI1YM= 29 | github.com/prometheus/common v0.42.0/go.mod h1:xBwqVerjNdUDjgODMpudtOMwlOwf2SaTr1yjz4b7Zbc= 30 | github.com/prometheus/procfs v0.10.1 h1:kYK1Va/YMlutzCGazswoHKo//tZVlFpKYh+PymziUAg= 31 | github.com/prometheus/procfs v0.10.1/go.mod h1:nwNm2aOCAYw8uTR/9bWRREkZFxAUcWzPHWJq+XBB/FM= 32 | github.com/rabbitmq/amqp091-go v1.8.1 h1:RejT1SBUim5doqcL6s7iN6SBmsQqyTgXb1xMlH0h1hA= 33 | github.com/rabbitmq/amqp091-go v1.8.1/go.mod h1:+jPrT9iY2eLjRaMSRHUhc3z14E/l85kv/f+6luSD3pc= 34 | github.com/samber/lo v1.35.0 h1:GlT8CV1GE+v97Y7MLF1wXvX6mjoxZ+hi61tj/ZcQwY0= 35 | github.com/samber/lo v1.35.0/go.mod h1:HLeWcJRRyLKp3+/XBJvOrerCQn9mhdKMHyd7IRlgeQ8= 36 | github.com/samber/mo v1.5.1 h1:5dRSevAB33Q/OrYwTmtksHHxquuf2urnRSUTsdTFysY= 37 | github.com/samber/mo v1.5.1/go.mod h1:pDuQgWscOVGGoEz+NAeth/Xq+MPAcXxCeph1XIAm/DU= 38 | github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0= 39 | github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= 40 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 41 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 42 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 43 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 44 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 45 | github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= 46 | github.com/thoas/go-funk v0.9.1 h1:O549iLZqPpTUQ10ykd26sZhzD+rmR5pWhuElrhbC20M= 47 | go.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A= 48 | go.uber.org/goleak v1.2.1/go.mod h1:qlT2yGI9QafXHhZZLxlSuNsMw3FFLxBr+tBRlmO1xH4= 49 | golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17 h1:3MTrJm4PyNL9NBqvYDSj3DHl46qQakyfqfWo4jgfaEM= 50 | golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17/go.mod h1:lgLbSvA5ygNOMpwM/9anMpWVlVJ7Z+cHWq/eFuinpGE= 51 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 52 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 53 | golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU= 54 | golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 55 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 56 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 57 | google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 58 | google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= 59 | google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= 60 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 61 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 62 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 63 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 64 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 65 | -------------------------------------------------------------------------------- /examples/consumer-with-metrics/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "net/http" 6 | "time" 7 | 8 | amqp "github.com/rabbitmq/amqp091-go" 9 | pubsub "github.com/samber/go-amqp-pubsub" 10 | "github.com/samber/lo" 11 | "github.com/samber/mo" 12 | "github.com/sirupsen/logrus" 13 | 14 | "github.com/prometheus/client_golang/prometheus" 15 | "github.com/prometheus/client_golang/prometheus/promhttp" 16 | ) 17 | 18 | var rabbitmqURI = flag.String("rabbitmq-uri", "amqp://dev:dev@localhost:5672", "RabbitMQ URI") 19 | 20 | const ( 21 | queueName string = "product.onEdit" 22 | 23 | routingKeyProductCreated string = "product.created" 24 | routingKeyProductUpdated string = "product.updated" 25 | routingKeyProductRemoved string = "product.removed" 26 | ) 27 | 28 | func main() { 29 | flag.Parse() 30 | 31 | if rabbitmqURI == nil { 32 | logrus.Fatal("missing --rabbitmiq-uri parameter") 33 | } 34 | 35 | conn, err := pubsub.NewConnection("example-connection-1", pubsub.ConnectionOptions{ 36 | URI: *rabbitmqURI, 37 | Config: amqp.Config{ 38 | Dial: amqp.DefaultDial(time.Second), 39 | Heartbeat: time.Second, 40 | }, 41 | LazyConnection: mo.Some(false), 42 | }) 43 | if err != nil { 44 | // We ignore error, since it will reconnect automatically when available. 45 | // panic(err) 46 | } 47 | 48 | consumer := pubsub.NewConsumer(conn, "example-consumer-1", pubsub.ConsumerOptions{ 49 | Queue: pubsub.ConsumerOptionsQueue{ 50 | Name: queueName, 51 | }, 52 | Bindings: []pubsub.ConsumerOptionsBinding{ 53 | // crud 54 | {ExchangeName: "product.event", RoutingKey: "product.created"}, 55 | {ExchangeName: "product.event", RoutingKey: "product.updated"}, 56 | }, 57 | Metrics: pubsub.ConsumerOptionsMetrics{ 58 | // triggers a warning when queue size is greater than 100_000 messages 59 | QueueMessagesThreshold: pubsub.NewConsumerOptionsMetricsThresholdWarning(100_000), 60 | // triggers an error when retry rate is greater than 10% except the weekend 61 | RetryQueueMessageRateThreshold: pubsub.NewConsumerOptionsMetricsThresholdErrorFunc(func() float64 { 62 | weekday := time.Now().Weekday() 63 | if weekday == time.Saturday || weekday == time.Sunday { 64 | return 0.8 65 | } 66 | 67 | return 0.1 68 | }), 69 | }, 70 | Message: pubsub.ConsumerOptionsMessage{ 71 | PrefetchCount: mo.Some(1000), 72 | }, 73 | EnableDeadLetter: mo.Some(true), 74 | }) 75 | 76 | // register consumer metrics 77 | prometheus.DefaultRegisterer.MustRegister(consumer) 78 | 79 | // expose metrics 80 | go func() { 81 | http.Handle("/metrics", promhttp.Handler()) 82 | http.ListenAndServe(":9000", nil) 83 | }() 84 | 85 | logrus.Info("***** Let's go! ***** ") 86 | 87 | consumeMessages(consumer) 88 | 89 | logrus.Info("***** Finished! ***** ") 90 | 91 | consumer.Close() 92 | conn.Close() 93 | 94 | logrus.Info("***** Closed! ***** ") 95 | } 96 | 97 | func consumeMessages(consumer *pubsub.Consumer) { 98 | // Feel free to kill RabbitMQ and restart it, to see what happens ;) 99 | // - docker-compose kill rabbitmq 100 | // - docker-compose up rabbitmq 101 | 102 | channel := consumer.Consume() 103 | 104 | i := 0 105 | for msg := range channel { 106 | lo.Try0(func() { // handle exceptions 107 | consumeMessage(i, msg) 108 | }) 109 | 110 | i++ 111 | } 112 | } 113 | 114 | func consumeMessage(index int, msg *amqp.Delivery) { 115 | logrus.Infof("Consumed message [ID=%d, EX=%s, RK=%s] %s", index, msg.Exchange, msg.RoutingKey, string(msg.Body)) 116 | 117 | // simulate timeout 118 | // n := rand.Intn(20) 119 | // time.Sleep(time.Duration(n) * time.Second) 120 | 121 | if index%10 == 0 { 122 | msg.Reject(false) 123 | } else { 124 | msg.Ack(false) 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /examples/consumer-with-pool-and-batch/go.mod: -------------------------------------------------------------------------------- 1 | module consumer 2 | 3 | go 1.19 4 | 5 | require ( 6 | github.com/rabbitmq/amqp091-go v1.8.1 7 | github.com/samber/go-amqp-pubsub v0.0.0-20210710222824-c4781d5ae30d 8 | github.com/samber/lo v1.35.0 9 | github.com/samber/mo v1.5.1 10 | github.com/sirupsen/logrus v1.9.0 11 | ) 12 | 13 | require ( 14 | github.com/google/uuid v1.3.0 // indirect 15 | golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17 // indirect 16 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 // indirect 17 | ) 18 | 19 | replace github.com/samber/go-amqp-pubsub => ../.. 20 | -------------------------------------------------------------------------------- /examples/consumer-with-pool-and-batch/go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 2 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 3 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= 5 | github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 6 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 7 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 8 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 9 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 10 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 11 | github.com/rabbitmq/amqp091-go v1.8.1 h1:RejT1SBUim5doqcL6s7iN6SBmsQqyTgXb1xMlH0h1hA= 12 | github.com/rabbitmq/amqp091-go v1.8.1/go.mod h1:+jPrT9iY2eLjRaMSRHUhc3z14E/l85kv/f+6luSD3pc= 13 | github.com/samber/lo v1.35.0 h1:GlT8CV1GE+v97Y7MLF1wXvX6mjoxZ+hi61tj/ZcQwY0= 14 | github.com/samber/lo v1.35.0/go.mod h1:HLeWcJRRyLKp3+/XBJvOrerCQn9mhdKMHyd7IRlgeQ8= 15 | github.com/samber/mo v1.5.1 h1:5dRSevAB33Q/OrYwTmtksHHxquuf2urnRSUTsdTFysY= 16 | github.com/samber/mo v1.5.1/go.mod h1:pDuQgWscOVGGoEz+NAeth/Xq+MPAcXxCeph1XIAm/DU= 17 | github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0= 18 | github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= 19 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 20 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 21 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 22 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 23 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 24 | github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= 25 | github.com/thoas/go-funk v0.9.1 h1:O549iLZqPpTUQ10ykd26sZhzD+rmR5pWhuElrhbC20M= 26 | go.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A= 27 | go.uber.org/goleak v1.2.1/go.mod h1:qlT2yGI9QafXHhZZLxlSuNsMw3FFLxBr+tBRlmO1xH4= 28 | golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17 h1:3MTrJm4PyNL9NBqvYDSj3DHl46qQakyfqfWo4jgfaEM= 29 | golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17/go.mod h1:lgLbSvA5ygNOMpwM/9anMpWVlVJ7Z+cHWq/eFuinpGE= 30 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 h1:0A+M6Uqn+Eje4kHMK80dtF3JCXC4ykBgQG4Fe06QRhQ= 31 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 32 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 33 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 34 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 35 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 36 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 37 | -------------------------------------------------------------------------------- /examples/consumer-with-pool-and-batch/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "strings" 7 | "sync" 8 | "time" 9 | 10 | amqp "github.com/rabbitmq/amqp091-go" 11 | pubsub "github.com/samber/go-amqp-pubsub" 12 | "github.com/samber/lo" 13 | "github.com/samber/mo" 14 | "github.com/sirupsen/logrus" 15 | ) 16 | 17 | var rabbitmqURI = flag.String("rabbitmq-uri", "amqp://dev:dev@localhost:5672", "RabbitMQ URI") 18 | 19 | const ( 20 | routingKeyProductCreated string = "product.created" 21 | routingKeyProductUpdated string = "product.updated" 22 | routingKeyProductRemoved string = "product.removed" 23 | ) 24 | 25 | func main() { 26 | flag.Parse() 27 | 28 | if rabbitmqURI == nil { 29 | logrus.Fatal("missing --rabbitmiq-uri parameter") 30 | } 31 | 32 | conn, err := pubsub.NewConnection("example-connection-1", pubsub.ConnectionOptions{ 33 | URI: *rabbitmqURI, 34 | Config: amqp.Config{ 35 | Dial: amqp.DefaultDial(time.Second), 36 | Heartbeat: time.Second, 37 | }, 38 | LazyConnection: mo.Some(false), 39 | }) 40 | if err != nil { 41 | // We ignore error, since it will reconnect automatically when available. 42 | // panic(err) 43 | } 44 | 45 | consumer := pubsub.NewConsumer(conn, "example-consumer-1", pubsub.ConsumerOptions{ 46 | Queue: pubsub.ConsumerOptionsQueue{ 47 | Name: "product.onEdit", 48 | }, 49 | Bindings: []pubsub.ConsumerOptionsBinding{ 50 | // crud 51 | {ExchangeName: "product.event", RoutingKey: "product.created"}, 52 | {ExchangeName: "product.event", RoutingKey: "product.updated"}, 53 | }, 54 | Message: pubsub.ConsumerOptionsMessage{ 55 | PrefetchCount: mo.Some(100), 56 | }, 57 | EnableDeadLetter: mo.Some(true), 58 | }) 59 | 60 | logrus.Info("***** Let's go! ***** ") 61 | 62 | consumeMessages(5, consumer) 63 | 64 | logrus.Info("***** Finished! ***** ") 65 | 66 | consumer.Close() 67 | conn.Close() 68 | 69 | logrus.Info("***** Closed! ***** ") 70 | } 71 | 72 | func consumeMessages(workers int, consumer *pubsub.Consumer) { 73 | // Feel free to kill RabbitMQ and restart it, to see what happens ;) 74 | // - docker-compose kill rabbitmq 75 | // - docker-compose up rabbitmq 76 | 77 | wg := new(sync.WaitGroup) 78 | wg.Add(workers) 79 | 80 | channel := consumer.Consume() 81 | channels := lo.ChannelDispatcher(channel, workers, 42, lo.DispatchingStrategyRoundRobin[*amqp.Delivery]) 82 | 83 | for i := range channels { 84 | go func(index int) { 85 | worker(index, channels[index]) 86 | 87 | wg.Done() 88 | }(i) 89 | } 90 | 91 | wg.Wait() 92 | } 93 | 94 | func worker(workerID int, channel <-chan *amqp.Delivery) { 95 | batchSize := 10 96 | batchTime := time.Second 97 | 98 | for { 99 | buffer, length, _, ok := lo.BufferWithTimeout(channel, batchSize, batchTime) 100 | if !ok { 101 | break 102 | } else if length == 0 { 103 | continue 104 | } 105 | 106 | lo.Try0(func() { // handle exceptions 107 | consumeMessage(workerID, buffer) 108 | }) 109 | } 110 | } 111 | 112 | func consumeMessage(workerID int, messages []*amqp.Delivery) { 113 | text := []string{fmt.Sprintf("WORKER %d - BATCH:", workerID)} 114 | 115 | for i, message := range messages { 116 | text = append(text, fmt.Sprintf("Consumed message [ID=%d, EX=%s, RK=%s] %s", workerID, message.Exchange, message.RoutingKey, string(message.Body))) 117 | 118 | if (workerID+i)%10 == 0 { 119 | message.Reject(false) 120 | } else { 121 | message.Ack(false) 122 | } 123 | } 124 | 125 | logrus.Info(strings.Join(text, "\n") + "\n\n") 126 | } 127 | -------------------------------------------------------------------------------- /examples/consumer-with-retry/go.mod: -------------------------------------------------------------------------------- 1 | module consumer 2 | 3 | go 1.19 4 | 5 | require ( 6 | github.com/rabbitmq/amqp091-go v1.8.1 7 | github.com/samber/go-amqp-pubsub v0.0.0-20210710222824-c4781d5ae30d 8 | github.com/samber/lo v1.35.0 9 | github.com/samber/mo v1.5.1 10 | github.com/sirupsen/logrus v1.9.0 11 | ) 12 | 13 | require ( 14 | github.com/google/uuid v1.3.0 // indirect 15 | golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17 // indirect 16 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 // indirect 17 | ) 18 | 19 | replace github.com/samber/go-amqp-pubsub => ../.. 20 | -------------------------------------------------------------------------------- /examples/consumer-with-retry/go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 2 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 3 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= 5 | github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 6 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 7 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 8 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 9 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 10 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 11 | github.com/rabbitmq/amqp091-go v1.8.1 h1:RejT1SBUim5doqcL6s7iN6SBmsQqyTgXb1xMlH0h1hA= 12 | github.com/rabbitmq/amqp091-go v1.8.1/go.mod h1:+jPrT9iY2eLjRaMSRHUhc3z14E/l85kv/f+6luSD3pc= 13 | github.com/samber/lo v1.35.0 h1:GlT8CV1GE+v97Y7MLF1wXvX6mjoxZ+hi61tj/ZcQwY0= 14 | github.com/samber/lo v1.35.0/go.mod h1:HLeWcJRRyLKp3+/XBJvOrerCQn9mhdKMHyd7IRlgeQ8= 15 | github.com/samber/mo v1.5.1 h1:5dRSevAB33Q/OrYwTmtksHHxquuf2urnRSUTsdTFysY= 16 | github.com/samber/mo v1.5.1/go.mod h1:pDuQgWscOVGGoEz+NAeth/Xq+MPAcXxCeph1XIAm/DU= 17 | github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0= 18 | github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= 19 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 20 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 21 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 22 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 23 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 24 | github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= 25 | github.com/thoas/go-funk v0.9.1 h1:O549iLZqPpTUQ10ykd26sZhzD+rmR5pWhuElrhbC20M= 26 | go.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A= 27 | go.uber.org/goleak v1.2.1/go.mod h1:qlT2yGI9QafXHhZZLxlSuNsMw3FFLxBr+tBRlmO1xH4= 28 | golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17 h1:3MTrJm4PyNL9NBqvYDSj3DHl46qQakyfqfWo4jgfaEM= 29 | golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17/go.mod h1:lgLbSvA5ygNOMpwM/9anMpWVlVJ7Z+cHWq/eFuinpGE= 30 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 h1:0A+M6Uqn+Eje4kHMK80dtF3JCXC4ykBgQG4Fe06QRhQ= 31 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 32 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 33 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 34 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 35 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 36 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 37 | -------------------------------------------------------------------------------- /examples/consumer-with-retry/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "time" 6 | 7 | amqp "github.com/rabbitmq/amqp091-go" 8 | pubsub "github.com/samber/go-amqp-pubsub" 9 | "github.com/samber/lo" 10 | "github.com/samber/mo" 11 | "github.com/sirupsen/logrus" 12 | ) 13 | 14 | var rabbitmqURI = flag.String("rabbitmq-uri", "amqp://dev:dev@localhost:5672", "RabbitMQ URI") 15 | 16 | const ( 17 | queueName string = "product.onEdit" 18 | 19 | routingKeyProductCreated string = "product.created" 20 | routingKeyProductUpdated string = "product.updated" 21 | routingKeyProductRemoved string = "product.removed" 22 | ) 23 | 24 | func main() { 25 | flag.Parse() 26 | 27 | if rabbitmqURI == nil { 28 | logrus.Fatal("missing --rabbitmiq-uri parameter") 29 | } 30 | 31 | conn, err := pubsub.NewConnection("example-connection-1", pubsub.ConnectionOptions{ 32 | URI: *rabbitmqURI, 33 | Config: amqp.Config{ 34 | Dial: amqp.DefaultDial(time.Second), 35 | Heartbeat: time.Second, 36 | }, 37 | LazyConnection: mo.Some(false), 38 | }) 39 | if err != nil { 40 | // We ignore error, since it will reconnect automatically when available. 41 | // panic(err) 42 | } 43 | 44 | consumer := pubsub.NewConsumer(conn, "example-consumer-1", pubsub.ConsumerOptions{ 45 | Queue: pubsub.ConsumerOptionsQueue{ 46 | Name: queueName, 47 | }, 48 | Bindings: []pubsub.ConsumerOptionsBinding{ 49 | // crud 50 | {ExchangeName: "product.event", RoutingKey: "product.created"}, 51 | {ExchangeName: "product.event", RoutingKey: "product.updated"}, 52 | {ExchangeName: "product.event", RoutingKey: "product.removed"}, 53 | }, 54 | Message: pubsub.ConsumerOptionsMessage{ 55 | PrefetchCount: mo.Some(1000), 56 | }, 57 | EnableDeadLetter: mo.Some(true), 58 | // RetryStrategy: mo.Some(pubsub.NewConstantRetryStrategy(3, 3*time.Second)), 59 | RetryStrategy: mo.Some(pubsub.NewExponentialRetryStrategy(3, 3*time.Second, 2)), 60 | RetryConsistency: mo.Some(pubsub.EventuallyConsistentRetry), 61 | }) 62 | 63 | logrus.Info("***** Let's go! ***** ") 64 | 65 | consumeMessages(consumer) 66 | 67 | logrus.Info("***** Finished! ***** ") 68 | 69 | consumer.Close() 70 | conn.Close() 71 | 72 | logrus.Info("***** Closed! ***** ") 73 | } 74 | 75 | func consumeMessages(consumer *pubsub.Consumer) { 76 | // Feel free to kill RabbitMQ and restart it, to see what happens ;) 77 | // - docker-compose kill rabbitmq 78 | // - docker-compose up rabbitmq 79 | 80 | channel := consumer.Consume() 81 | 82 | i := 0 83 | for msg := range channel { 84 | lo.Try0(func() { // handle exceptions 85 | consumeMessage(i, msg) 86 | }) 87 | 88 | i++ 89 | } 90 | } 91 | 92 | func consumeMessage(index int, msg *amqp.Delivery) { 93 | logrus.Infof("Consumed message [ID=%d, EX=%s, RK=%s, TIME=%s] %s", index, msg.Exchange, msg.RoutingKey, time.Now().Format("15:04:05.999"), string(msg.Body)) 94 | 95 | msg.Reject(false) 96 | } 97 | -------------------------------------------------------------------------------- /examples/consumer/go.mod: -------------------------------------------------------------------------------- 1 | module consumer 2 | 3 | go 1.19 4 | 5 | require ( 6 | github.com/rabbitmq/amqp091-go v1.8.1 7 | github.com/samber/go-amqp-pubsub v0.0.0-20210710222824-c4781d5ae30d 8 | github.com/samber/lo v1.35.0 9 | github.com/samber/mo v1.5.1 10 | github.com/sirupsen/logrus v1.9.0 11 | ) 12 | 13 | require ( 14 | github.com/beorn7/perks v1.0.1 // indirect 15 | github.com/cespare/xxhash/v2 v2.2.0 // indirect 16 | github.com/golang/protobuf v1.5.3 // indirect 17 | github.com/google/uuid v1.3.0 // indirect 18 | github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect 19 | github.com/prometheus/client_golang v1.16.0 // indirect 20 | github.com/prometheus/client_model v0.3.0 // indirect 21 | github.com/prometheus/common v0.42.0 // indirect 22 | github.com/prometheus/procfs v0.10.1 // indirect 23 | golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17 // indirect 24 | golang.org/x/sys v0.8.0 // indirect 25 | google.golang.org/protobuf v1.33.0 // indirect 26 | ) 27 | 28 | replace github.com/samber/go-amqp-pubsub => ../.. 29 | -------------------------------------------------------------------------------- /examples/consumer/go.sum: -------------------------------------------------------------------------------- 1 | github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= 2 | github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 3 | github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= 4 | github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 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/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 9 | github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= 10 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 11 | github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= 12 | github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= 13 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 14 | github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= 15 | github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= 16 | github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 17 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 18 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 19 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 20 | github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= 21 | github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= 22 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 23 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 24 | github.com/prometheus/client_golang v1.16.0 h1:yk/hx9hDbrGHovbci4BY+pRMfSuuat626eFsHb7tmT8= 25 | github.com/prometheus/client_golang v1.16.0/go.mod h1:Zsulrv/L9oM40tJ7T815tM89lFEugiJ9HzIqaAx4LKc= 26 | github.com/prometheus/client_model v0.3.0 h1:UBgGFHqYdG/TPFD1B1ogZywDqEkwp3fBMvqdiQ7Xew4= 27 | github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w= 28 | github.com/prometheus/common v0.42.0 h1:EKsfXEYo4JpWMHH5cg+KOUWeuJSov1Id8zGR8eeI1YM= 29 | github.com/prometheus/common v0.42.0/go.mod h1:xBwqVerjNdUDjgODMpudtOMwlOwf2SaTr1yjz4b7Zbc= 30 | github.com/prometheus/procfs v0.10.1 h1:kYK1Va/YMlutzCGazswoHKo//tZVlFpKYh+PymziUAg= 31 | github.com/prometheus/procfs v0.10.1/go.mod h1:nwNm2aOCAYw8uTR/9bWRREkZFxAUcWzPHWJq+XBB/FM= 32 | github.com/rabbitmq/amqp091-go v1.8.1 h1:RejT1SBUim5doqcL6s7iN6SBmsQqyTgXb1xMlH0h1hA= 33 | github.com/rabbitmq/amqp091-go v1.8.1/go.mod h1:+jPrT9iY2eLjRaMSRHUhc3z14E/l85kv/f+6luSD3pc= 34 | github.com/samber/lo v1.35.0 h1:GlT8CV1GE+v97Y7MLF1wXvX6mjoxZ+hi61tj/ZcQwY0= 35 | github.com/samber/lo v1.35.0/go.mod h1:HLeWcJRRyLKp3+/XBJvOrerCQn9mhdKMHyd7IRlgeQ8= 36 | github.com/samber/mo v1.5.1 h1:5dRSevAB33Q/OrYwTmtksHHxquuf2urnRSUTsdTFysY= 37 | github.com/samber/mo v1.5.1/go.mod h1:pDuQgWscOVGGoEz+NAeth/Xq+MPAcXxCeph1XIAm/DU= 38 | github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0= 39 | github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= 40 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 41 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 42 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 43 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 44 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 45 | github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= 46 | github.com/thoas/go-funk v0.9.1 h1:O549iLZqPpTUQ10ykd26sZhzD+rmR5pWhuElrhbC20M= 47 | go.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A= 48 | go.uber.org/goleak v1.2.1/go.mod h1:qlT2yGI9QafXHhZZLxlSuNsMw3FFLxBr+tBRlmO1xH4= 49 | golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17 h1:3MTrJm4PyNL9NBqvYDSj3DHl46qQakyfqfWo4jgfaEM= 50 | golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17/go.mod h1:lgLbSvA5ygNOMpwM/9anMpWVlVJ7Z+cHWq/eFuinpGE= 51 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 52 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 53 | golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU= 54 | golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 55 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 56 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 57 | google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 58 | google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= 59 | google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= 60 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 61 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 62 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 63 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 64 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 65 | -------------------------------------------------------------------------------- /examples/consumer/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "time" 6 | 7 | amqp "github.com/rabbitmq/amqp091-go" 8 | pubsub "github.com/samber/go-amqp-pubsub" 9 | "github.com/samber/lo" 10 | "github.com/samber/mo" 11 | "github.com/sirupsen/logrus" 12 | ) 13 | 14 | var rabbitmqURI = flag.String("rabbitmq-uri", "amqp://dev:dev@localhost:5672", "RabbitMQ URI") 15 | 16 | const ( 17 | queueName string = "product.onEdit" 18 | 19 | routingKeyProductCreated string = "product.created" 20 | routingKeyProductUpdated string = "product.updated" 21 | routingKeyProductRemoved string = "product.removed" 22 | ) 23 | 24 | func main() { 25 | flag.Parse() 26 | 27 | if rabbitmqURI == nil { 28 | logrus.Fatal("missing --rabbitmiq-uri parameter") 29 | } 30 | 31 | conn, err := pubsub.NewConnection("example-connection-1", pubsub.ConnectionOptions{ 32 | URI: *rabbitmqURI, 33 | Config: amqp.Config{ 34 | Dial: amqp.DefaultDial(time.Second), 35 | Heartbeat: time.Second, 36 | }, 37 | LazyConnection: mo.Some(true), 38 | }) 39 | if err != nil { 40 | // We ignore error, since it will reconnect automatically when available. 41 | // panic(err) 42 | } 43 | 44 | consumer := pubsub.NewConsumer(conn, "example-consumer-1", pubsub.ConsumerOptions{ 45 | Queue: pubsub.ConsumerOptionsQueue{ 46 | Name: queueName, 47 | }, 48 | Bindings: []pubsub.ConsumerOptionsBinding{ 49 | // crud 50 | {ExchangeName: "product.event", RoutingKey: "product.created"}, 51 | {ExchangeName: "product.event", RoutingKey: "product.updated"}, 52 | }, 53 | Message: pubsub.ConsumerOptionsMessage{ 54 | PrefetchCount: mo.Some(1000), 55 | }, 56 | EnableDeadLetter: mo.Some(true), 57 | }) 58 | 59 | logrus.Info("***** Let's go! ***** ") 60 | 61 | consumeMessages(consumer) 62 | 63 | logrus.Info("***** Finished! ***** ") 64 | 65 | consumer.Close() 66 | conn.Close() 67 | 68 | logrus.Info("***** Closed! ***** ") 69 | } 70 | 71 | func consumeMessages(consumer *pubsub.Consumer) { 72 | // Feel free to kill RabbitMQ and restart it, to see what happens ;) 73 | // - docker-compose kill rabbitmq 74 | // - docker-compose up rabbitmq 75 | 76 | channel := consumer.Consume() 77 | 78 | i := 0 79 | for msg := range channel { 80 | lo.Try0(func() { // handle exceptions 81 | consumeMessage(i, msg) 82 | }) 83 | 84 | i++ 85 | } 86 | } 87 | 88 | func consumeMessage(index int, msg *amqp.Delivery) { 89 | logrus.Infof("Consumed message [ID=%d, EX=%s, RK=%s] %s", index, msg.Exchange, msg.RoutingKey, string(msg.Body)) 90 | 91 | // simulate timeout 92 | // time.Sleep(100 * time.Second) 93 | 94 | if index%10 == 0 { 95 | msg.Reject(false) 96 | } else { 97 | msg.Ack(false) 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /examples/producer/go.mod: -------------------------------------------------------------------------------- 1 | module producer 2 | 3 | go 1.21 4 | 5 | toolchain go1.21.0 6 | 7 | require ( 8 | github.com/rabbitmq/amqp091-go v1.8.1 9 | github.com/samber/go-amqp-pubsub v0.0.0-20210710222824-c4781d5ae30d 10 | github.com/samber/mo v1.5.1 11 | github.com/sirupsen/logrus v1.9.3 12 | ) 13 | 14 | require ( 15 | github.com/beorn7/perks v1.0.1 // indirect 16 | github.com/cespare/xxhash/v2 v2.2.0 // indirect 17 | github.com/golang/protobuf v1.5.3 // indirect 18 | github.com/google/uuid v1.3.0 // indirect 19 | github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect 20 | github.com/prometheus/client_golang v1.16.0 // indirect 21 | github.com/prometheus/client_model v0.3.0 // indirect 22 | github.com/prometheus/common v0.42.0 // indirect 23 | github.com/prometheus/procfs v0.10.1 // indirect 24 | github.com/samber/lo v1.35.0 // indirect 25 | golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17 // indirect 26 | golang.org/x/sys v0.8.0 // indirect 27 | google.golang.org/protobuf v1.33.0 // indirect 28 | ) 29 | 30 | replace github.com/samber/go-amqp-pubsub => ../.. 31 | -------------------------------------------------------------------------------- /examples/producer/go.sum: -------------------------------------------------------------------------------- 1 | github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= 2 | github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 3 | github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= 4 | github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 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/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 9 | github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= 10 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 11 | github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= 12 | github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= 13 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 14 | github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= 15 | github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 16 | github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= 17 | github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 18 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 19 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 20 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 21 | github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= 22 | github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= 23 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 24 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 25 | github.com/prometheus/client_golang v1.16.0 h1:yk/hx9hDbrGHovbci4BY+pRMfSuuat626eFsHb7tmT8= 26 | github.com/prometheus/client_golang v1.16.0/go.mod h1:Zsulrv/L9oM40tJ7T815tM89lFEugiJ9HzIqaAx4LKc= 27 | github.com/prometheus/client_model v0.3.0 h1:UBgGFHqYdG/TPFD1B1ogZywDqEkwp3fBMvqdiQ7Xew4= 28 | github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w= 29 | github.com/prometheus/common v0.42.0 h1:EKsfXEYo4JpWMHH5cg+KOUWeuJSov1Id8zGR8eeI1YM= 30 | github.com/prometheus/common v0.42.0/go.mod h1:xBwqVerjNdUDjgODMpudtOMwlOwf2SaTr1yjz4b7Zbc= 31 | github.com/prometheus/procfs v0.10.1 h1:kYK1Va/YMlutzCGazswoHKo//tZVlFpKYh+PymziUAg= 32 | github.com/prometheus/procfs v0.10.1/go.mod h1:nwNm2aOCAYw8uTR/9bWRREkZFxAUcWzPHWJq+XBB/FM= 33 | github.com/rabbitmq/amqp091-go v1.8.1 h1:RejT1SBUim5doqcL6s7iN6SBmsQqyTgXb1xMlH0h1hA= 34 | github.com/rabbitmq/amqp091-go v1.8.1/go.mod h1:+jPrT9iY2eLjRaMSRHUhc3z14E/l85kv/f+6luSD3pc= 35 | github.com/samber/lo v1.35.0 h1:GlT8CV1GE+v97Y7MLF1wXvX6mjoxZ+hi61tj/ZcQwY0= 36 | github.com/samber/lo v1.35.0/go.mod h1:HLeWcJRRyLKp3+/XBJvOrerCQn9mhdKMHyd7IRlgeQ8= 37 | github.com/samber/mo v1.5.1 h1:5dRSevAB33Q/OrYwTmtksHHxquuf2urnRSUTsdTFysY= 38 | github.com/samber/mo v1.5.1/go.mod h1:pDuQgWscOVGGoEz+NAeth/Xq+MPAcXxCeph1XIAm/DU= 39 | github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= 40 | github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= 41 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 42 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 43 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 44 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 45 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 46 | github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= 47 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 48 | github.com/thoas/go-funk v0.9.1 h1:O549iLZqPpTUQ10ykd26sZhzD+rmR5pWhuElrhbC20M= 49 | github.com/thoas/go-funk v0.9.1/go.mod h1:+IWnUfUmFO1+WVYQWQtIJHeRRdaIyyYglZN7xzUPe4Q= 50 | go.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A= 51 | go.uber.org/goleak v1.2.1/go.mod h1:qlT2yGI9QafXHhZZLxlSuNsMw3FFLxBr+tBRlmO1xH4= 52 | golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17 h1:3MTrJm4PyNL9NBqvYDSj3DHl46qQakyfqfWo4jgfaEM= 53 | golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17/go.mod h1:lgLbSvA5ygNOMpwM/9anMpWVlVJ7Z+cHWq/eFuinpGE= 54 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 55 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 56 | golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU= 57 | golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 58 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 59 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 60 | google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 61 | google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= 62 | google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= 63 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 64 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 65 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 66 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 67 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 68 | -------------------------------------------------------------------------------- /examples/producer/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "flag" 6 | "time" 7 | 8 | amqp "github.com/rabbitmq/amqp091-go" 9 | pubsub "github.com/samber/go-amqp-pubsub" 10 | "github.com/samber/mo" 11 | "github.com/sirupsen/logrus" 12 | ) 13 | 14 | var rabbitmqURI = flag.String("rabbitmq-uri", "amqp://dev:dev@localhost:5672", "RabbitMQ URI") 15 | 16 | const ( 17 | routingKeyProductCreated string = "product.created" 18 | routingKeyProductUpdated string = "product.updated" 19 | routingKeyProductRemoved string = "product.removed" 20 | ) 21 | 22 | var productRk = []string{routingKeyProductCreated, routingKeyProductUpdated, routingKeyProductRemoved} 23 | 24 | func main() { 25 | flag.Parse() 26 | 27 | if rabbitmqURI == nil { 28 | logrus.Fatal("missing --rabbitmiq-uri parameter") 29 | } 30 | 31 | conn, err := pubsub.NewConnection("example-connection-1", pubsub.ConnectionOptions{ 32 | URI: *rabbitmqURI, 33 | Config: amqp.Config{ 34 | Dial: amqp.DefaultDial(time.Second), 35 | Heartbeat: time.Second, 36 | }, 37 | LazyConnection: mo.Some(true), 38 | }) 39 | if err != nil { 40 | // We ignore error, since it will reconnect automatically when available. 41 | // panic(err) 42 | } 43 | 44 | producer := pubsub.NewProducer(conn, "example-producer-1", pubsub.ProducerOptions{ 45 | Exchange: pubsub.ProducerOptionsExchange{ 46 | Name: mo.Some("product.event"), 47 | Kind: mo.Some(pubsub.ExchangeKindTopic), 48 | }, 49 | }) 50 | 51 | logrus.Info("***** Let's go! ***** ") 52 | 53 | publishMessages(producer) 54 | 55 | logrus.Info("***** Finished! ***** ") 56 | 57 | producer.Close() 58 | conn.Close() 59 | 60 | logrus.Info("***** Closed! ***** ") 61 | } 62 | 63 | func publishMessages(producer *pubsub.Producer) { 64 | // Feel free to kill RabbitMQ and restart it, to see what happens ;) 65 | // - docker-compose kill rabbitmq 66 | // - docker-compose up rabbitmq 67 | 68 | time.Sleep(1 * time.Second) 69 | for i := 0; i < 1000000; i++ { 70 | time.Sleep(100 * time.Microsecond) 71 | 72 | routingKey := productRk[i%len(productRk)] 73 | 74 | body, _ := json.Marshal(map[string]interface{}{ 75 | "id": i, 76 | "name": "tomatoes", 77 | }) 78 | 79 | err := producer.Publish(routingKey, false, false, amqp.Publishing{ 80 | ContentType: "application/json", 81 | DeliveryMode: amqp.Persistent, 82 | Body: body, 83 | }) 84 | if err != nil { 85 | logrus.Error(err) 86 | } else { 87 | logrus.Infof("Published message [RK=%s] %s", routingKey, string(body)) 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /examples/rabbitmq.conf: -------------------------------------------------------------------------------- 1 | 2 | # 10 secondes 3 | consumer_timeout = 10000 4 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/samber/go-amqp-pubsub 2 | 3 | go 1.20 4 | 5 | require ( 6 | github.com/google/uuid v1.6.0 7 | github.com/prometheus/client_golang v1.20.5 8 | github.com/rabbitmq/amqp091-go v1.10.0 9 | github.com/samber/lo v1.50.0 10 | github.com/samber/mo v1.14.0 11 | go.uber.org/goleak v1.3.0 12 | ) 13 | 14 | require ( 15 | github.com/beorn7/perks v1.0.1 // indirect 16 | github.com/cespare/xxhash/v2 v2.3.0 // indirect 17 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 18 | github.com/prometheus/client_model v0.6.1 // indirect 19 | github.com/prometheus/common v0.55.0 // indirect 20 | github.com/prometheus/procfs v0.15.1 // indirect 21 | golang.org/x/sys v0.22.0 // indirect 22 | golang.org/x/text v0.22.0 // indirect 23 | google.golang.org/protobuf v1.34.2 // indirect 24 | ) 25 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= 2 | github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 3 | github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= 4 | github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 5 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 6 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 7 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 8 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 9 | github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= 10 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= 11 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= 12 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 13 | github.com/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y= 14 | github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= 15 | github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= 16 | github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= 17 | github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc= 18 | github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8= 19 | github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= 20 | github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= 21 | github.com/rabbitmq/amqp091-go v1.10.0 h1:STpn5XsHlHGcecLmMFCtg7mqq0RnD+zFr4uzukfVhBw= 22 | github.com/rabbitmq/amqp091-go v1.10.0/go.mod h1:Hy4jKW5kQART1u+JkDTF9YYOQUHXqMuhrgxOEeS7G4o= 23 | github.com/samber/lo v1.50.0 h1:XrG0xOeHs+4FQ8gJR97zDz5uOFMW7OwFWiFVzqopKgY= 24 | github.com/samber/lo v1.50.0/go.mod h1:RjZyNk6WSnUFRKK6EyOhsRJMqft3G+pg7dCWHQCWvsc= 25 | github.com/samber/mo v1.14.0 h1:lRKVxkmlfN1m+i7kjycraJ78JdPcuxTm0pXOSh1+vl4= 26 | github.com/samber/mo v1.14.0/go.mod h1:BfkrCPuYzVG3ZljnZB783WIJIGk1mcZr9c9CPf8tAxs= 27 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 28 | go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= 29 | go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= 30 | golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= 31 | golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 32 | golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= 33 | golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= 34 | google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= 35 | google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= 36 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 37 | -------------------------------------------------------------------------------- /helper.go: -------------------------------------------------------------------------------- 1 | package pubsub 2 | 3 | import ( 4 | amqp "github.com/rabbitmq/amqp091-go" 5 | ) 6 | 7 | func GetMessageHeader[T any](msg *amqp.Delivery, key string) (value T, ok bool) { 8 | if msg == nil { 9 | return 10 | } 11 | 12 | if msg.Headers == nil { 13 | return 14 | } 15 | 16 | anyValue, ok := msg.Headers[key] 17 | if !ok { 18 | return 19 | } 20 | 21 | value, ok = anyValue.(T) 22 | return value, ok 23 | } 24 | -------------------------------------------------------------------------------- /lo.go: -------------------------------------------------------------------------------- 1 | package pubsub 2 | 3 | import ( 4 | "sync" 5 | 6 | "github.com/samber/lo" 7 | ) 8 | 9 | /** 10 | * See https://github.com/samber/lo/pull/270 11 | */ 12 | 13 | type rpc[T any, R any] struct { 14 | C chan lo.Tuple2[T, func(R)] 15 | } 16 | 17 | // NewRPC synchronizes goroutines for a bidirectionnal request-response communication. 18 | func newRPC[T any, R any](ch chan<- T) *rpc[T, R] { 19 | return &rpc[T, R]{ 20 | C: make(chan lo.Tuple2[T, func(R)]), 21 | } 22 | } 23 | 24 | // Send blocks until response is triggered. 25 | func (rpc *rpc[T, R]) Send(request T) R { 26 | done := make(chan R) 27 | defer close(done) 28 | 29 | once := sync.Once{} 30 | 31 | rpc.C <- lo.T2(request, func(response R) { 32 | once.Do(func() { 33 | done <- response 34 | }) 35 | }) 36 | 37 | return <-done 38 | } 39 | 40 | /** 41 | * See https://github.com/samber/lo/pull/268 42 | */ 43 | 44 | // SafeClose protects against double-close panic. 45 | // Returns true on first close, only. 46 | // May be equivalent to calling `sync.Once{}.Do(func() { close(ch) })`.` 47 | func safeCloseChan[T any](ch chan<- T) (justClosed bool) { 48 | defer func() { 49 | if recover() != nil { 50 | justClosed = false 51 | } 52 | }() 53 | 54 | close(ch) // may panic 55 | return true 56 | } 57 | 58 | /** 59 | * See 60 | */ 61 | func drainChan[T any](ch <-chan T) { 62 | for { 63 | select { 64 | case <-ch: 65 | default: 66 | return 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /logger.go: -------------------------------------------------------------------------------- 1 | package pubsub 2 | 3 | import "log" 4 | 5 | type Scope string 6 | 7 | const ( 8 | ScopeConnection Scope = "connection" 9 | ScopeChannel Scope = "channel" 10 | ScopeExchange Scope = "exchange" 11 | ScopeQueue Scope = "queue" 12 | ScopeConsumer Scope = "consumer" 13 | ScopeProducer Scope = "producer" 14 | ) 15 | 16 | var logger func(scope Scope, name string, msg string, attributes map[string]any) = DefaultLogger 17 | 18 | func SetLogger(cb func(scope Scope, name string, msg string, attributes map[string]any)) { 19 | logger = cb 20 | } 21 | 22 | func DefaultLogger(scope Scope, name string, msg string, attributes map[string]any) { 23 | // bearer:disable go_lang_logger_leak 24 | log.Printf("AMQP %s '%s': %s", scope, name, msg) 25 | 26 | // if attributes == nil { 27 | // attributes = map[string]any{} 28 | // } 29 | 30 | // attrs := lo.MapToSlice(attributes, func(key string, value any) any { 31 | // return slog.Any(key, value) 32 | // }) 33 | 34 | // msg = fmt.Sprintf("AMQP %s '%s': %s", scope, name, msg) 35 | // slog.Error(msg, attrs...) 36 | } 37 | -------------------------------------------------------------------------------- /main_test.go: -------------------------------------------------------------------------------- 1 | package pubsub 2 | 3 | import ( 4 | "testing" 5 | 6 | "go.uber.org/goleak" 7 | ) 8 | 9 | func TestMain(m *testing.M) { 10 | goleak.VerifyTestMain(m) 11 | } 12 | -------------------------------------------------------------------------------- /producer.go: -------------------------------------------------------------------------------- 1 | package pubsub 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "strings" 7 | "sync" 8 | "time" 9 | 10 | amqp "github.com/rabbitmq/amqp091-go" 11 | "github.com/samber/mo" 12 | ) 13 | 14 | type ProducerOptionsExchange struct { 15 | Name mo.Option[string] // default "amq.direct" 16 | Kind mo.Option[ExchangeKind] // default "direct" 17 | 18 | // optional arguments 19 | Durable mo.Option[bool] // default true 20 | AutoDelete mo.Option[bool] // default false 21 | Internal mo.Option[bool] // default false 22 | NoWait mo.Option[bool] // default false 23 | Args mo.Option[amqp.Table] // default nil 24 | } 25 | 26 | type ProducerOptions struct { 27 | Exchange ProducerOptionsExchange 28 | } 29 | 30 | type Producer struct { 31 | conn *Connection 32 | name string 33 | options ProducerOptions 34 | 35 | mu sync.RWMutex 36 | channel *amqp.Channel 37 | closeOnce sync.Once 38 | done *rpc[struct{}, struct{}] 39 | } 40 | 41 | func NewProducer(conn *Connection, name string, opt ProducerOptions) *Producer { 42 | doneCh := make(chan struct{}) 43 | 44 | p := &Producer{ 45 | conn: conn, 46 | name: name, 47 | options: opt, 48 | 49 | mu: sync.RWMutex{}, 50 | channel: nil, 51 | closeOnce: sync.Once{}, 52 | done: newRPC[struct{}, struct{}](doneCh), 53 | } 54 | 55 | go p.lifecycle() 56 | 57 | return p 58 | } 59 | 60 | func (p *Producer) lifecycle() { 61 | cancel, connectionListener := p.conn.ListenConnection() 62 | onConnect := make(chan struct{}, 42) 63 | onDisconnect := make(chan struct{}, 42) 64 | 65 | var conn *amqp.Connection 66 | 67 | for { 68 | select { 69 | case _conn := <-connectionListener: 70 | conn = _conn 71 | if conn != nil { 72 | onConnect <- struct{}{} 73 | } else { 74 | onDisconnect <- struct{}{} 75 | } 76 | 77 | case <-onConnect: 78 | p.closeChannel() 79 | 80 | if conn == nil || conn.IsClosed() { 81 | continue 82 | } 83 | 84 | _channel, onChannelClosed, err := p.setupProducer(conn) 85 | if err != nil { 86 | logger(ScopeProducer, p.name, "Could not start producer", map[string]any{"error": err.Error()}) 87 | time.Sleep(1 * time.Second) // retry in 1 second 88 | onConnect <- struct{}{} 89 | } else { 90 | p.mu.Lock() 91 | p.channel = _channel 92 | p.mu.Unlock() 93 | 94 | go func() { 95 | // ok && err==nil -> channel closed 96 | // ok && err!=nil -> channel error (connection error, etc...) 97 | err, ok := <-onChannelClosed 98 | if ok && err != nil { 99 | logger(ScopeChannel, p.name, "Channel closed: "+err.Reason, map[string]any{"error": err.Error()}) 100 | onConnect <- struct{}{} 101 | } 102 | }() 103 | } 104 | 105 | case <-onDisconnect: 106 | p.closeChannel() 107 | 108 | case req := <-p.done.C: 109 | cancel() 110 | req.B(struct{}{}) 111 | return 112 | } 113 | } 114 | } 115 | 116 | func (p *Producer) closeChannel() { 117 | p.mu.Lock() 118 | if p.channel != nil && !p.channel.IsClosed() { 119 | p.channel.Close() 120 | p.channel = nil 121 | } 122 | p.mu.Unlock() 123 | } 124 | 125 | func (p *Producer) Close() error { 126 | p.closeOnce.Do(func() { 127 | _ = p.done.Send(struct{}{}) 128 | safeCloseChan(p.done.C) 129 | }) 130 | 131 | return nil 132 | } 133 | 134 | func (p *Producer) setupProducer(conn *amqp.Connection) (*amqp.Channel, <-chan *amqp.Error, error) { 135 | // create a channel dedicated to this producer 136 | channel, err := conn.Channel() 137 | if err != nil { 138 | return nil, nil, err 139 | } 140 | 141 | // check if exchange is reserved and pre-declared 142 | if strings.HasPrefix(p.options.Exchange.Name.OrElse("amq.direct"), "amq.") { 143 | err = channel.ExchangeDeclarePassive( 144 | p.options.Exchange.Name.OrElse("amq.direct"), 145 | string(p.options.Exchange.Kind.OrElse(ExchangeKindDirect)), 146 | p.options.Exchange.Durable.OrElse(true), 147 | p.options.Exchange.AutoDelete.OrElse(false), 148 | p.options.Exchange.Internal.OrElse(false), 149 | p.options.Exchange.NoWait.OrElse(false), 150 | p.options.Exchange.Args.OrElse(nil), 151 | ) 152 | } else { 153 | // create exchange if not exist 154 | err = channel.ExchangeDeclare( 155 | p.options.Exchange.Name.OrElse("amq.direct"), 156 | string(p.options.Exchange.Kind.OrElse(ExchangeKindDirect)), 157 | p.options.Exchange.Durable.OrElse(true), 158 | p.options.Exchange.AutoDelete.OrElse(false), 159 | p.options.Exchange.Internal.OrElse(false), 160 | p.options.Exchange.NoWait.OrElse(false), 161 | p.options.Exchange.Args.OrElse(nil), 162 | ) 163 | } 164 | 165 | if err != nil { 166 | _ = channel.Close() 167 | return nil, nil, err 168 | } 169 | 170 | return channel, channel.NotifyClose(make(chan *amqp.Error)), nil 171 | } 172 | 173 | /** 174 | * API 175 | */ 176 | 177 | func (p *Producer) PublishWithContext(ctx context.Context, routingKey string, mandatory bool, immediate bool, msg amqp.Publishing) error { 178 | p.mu.Lock() 179 | defer p.mu.Unlock() 180 | 181 | if p.channel == nil { 182 | return fmt.Errorf("AMQP: channel '%s' not available", p.name) 183 | } 184 | 185 | return p.channel.PublishWithContext( 186 | ctx, 187 | p.options.Exchange.Name.OrElse("amq.direct"), 188 | routingKey, 189 | mandatory, 190 | immediate, 191 | msg, 192 | ) 193 | } 194 | 195 | func (p *Producer) PublishWithDeferredConfirmWithContext(ctx context.Context, routingKey string, mandatory bool, immediate bool, msg amqp.Publishing) (*amqp.DeferredConfirmation, error) { 196 | p.mu.RLock() 197 | defer p.mu.RUnlock() 198 | 199 | if p.channel == nil { 200 | return nil, fmt.Errorf("AMQP: channel '%s' not available", p.name) 201 | } 202 | 203 | return p.channel.PublishWithDeferredConfirmWithContext( 204 | ctx, 205 | p.options.Exchange.Name.OrElse("amq.direct"), 206 | routingKey, 207 | mandatory, 208 | immediate, 209 | msg, 210 | ) 211 | } 212 | 213 | func (p *Producer) Publish(routingKey string, mandatory bool, immediate bool, msg amqp.Publishing) error { 214 | return p.PublishWithContext( 215 | context.Background(), 216 | routingKey, 217 | mandatory, 218 | immediate, 219 | msg, 220 | ) 221 | } 222 | 223 | func (p *Producer) PublishWithDeferredConfirm(routingKey string, mandatory bool, immediate bool, msg amqp.Publishing) (*amqp.DeferredConfirmation, error) { 224 | return p.PublishWithDeferredConfirmWithContext( 225 | context.Background(), 226 | routingKey, 227 | mandatory, 228 | immediate, 229 | msg, 230 | ) 231 | } 232 | -------------------------------------------------------------------------------- /retry_acknowledgement.go: -------------------------------------------------------------------------------- 1 | package pubsub 2 | 3 | import ( 4 | "strconv" 5 | "time" 6 | 7 | amqp "github.com/rabbitmq/amqp091-go" 8 | "github.com/samber/lo" 9 | ) 10 | 11 | type RetryConsistency int 12 | 13 | const ( 14 | ConsistentRetry RetryConsistency = 0 // slow 15 | EventuallyConsistentRetry RetryConsistency = 1 // fast, at *least* once 16 | ) 17 | 18 | func GetAttempts(msg *amqp.Delivery) int { 19 | result := 0 20 | 21 | ok := lo.Try0(func() { 22 | header, ok := msg.Headers["x-retry-attempts"] 23 | if !ok { 24 | return 25 | } 26 | 27 | attempts, err := strconv.ParseInt(header.(string), 10, 32) 28 | if err != nil { 29 | return 30 | } 31 | 32 | result = int(attempts) 33 | }) 34 | if !ok { 35 | logger(ScopeConsumer, "", "could not parse x-retry-attempts header", nil) 36 | return 0 37 | } 38 | 39 | return result 40 | } 41 | 42 | func RejectWithRetry(msg *amqp.Delivery, ttl time.Duration) error { 43 | acknowledger, ok := msg.Acknowledger.(*retryAcknowledger) 44 | if !ok || ttl < 0 { 45 | return msg.Reject(false) 46 | } 47 | 48 | attempts := GetAttempts(msg) 49 | 50 | _, retryOk := acknowledger.retryer.NextBackOff(msg, attempts) 51 | if retryOk { 52 | return acknowledger.retry(msg.DeliveryTag, attempts, ttl) 53 | } 54 | 55 | return acknowledger.parent.Reject(msg.DeliveryTag, false) 56 | } 57 | 58 | type retryAcknowledger struct { 59 | retryProducer *Producer 60 | retryQueue string 61 | retryer RetryStrategy 62 | retryConsistency RetryConsistency 63 | msg amqp.Delivery 64 | parent amqp.Acknowledger 65 | } 66 | 67 | func newRetryAcknowledger(retryProducer *Producer, retryQueue string, retryer RetryStrategy, retryConsistency RetryConsistency, msg amqp.Delivery) amqp.Acknowledger { 68 | return &retryAcknowledger{ 69 | retryProducer: retryProducer, 70 | retryQueue: retryQueue, 71 | retryer: retryer, 72 | retryConsistency: retryConsistency, 73 | msg: msg, 74 | parent: msg.Acknowledger, 75 | } 76 | } 77 | 78 | func (a *retryAcknowledger) Ack(tag uint64, multiple bool) error { 79 | return a.parent.Ack(tag, multiple) 80 | } 81 | 82 | func (a *retryAcknowledger) Nack(tag uint64, multiple bool, requeue bool) error { 83 | if multiple { 84 | panic("multiple nack is not available with retry strategy") 85 | } 86 | 87 | if requeue { 88 | panic("requeue is not available with retry strategy") 89 | } 90 | 91 | attempts := GetAttempts(&a.msg) 92 | 93 | ttl, ok := a.retryer.NextBackOff(&a.msg, attempts) 94 | if ok { 95 | return a.retry(tag, attempts, ttl) 96 | } 97 | 98 | return a.parent.Nack(tag, false, requeue) 99 | } 100 | 101 | func (a *retryAcknowledger) Reject(tag uint64, requeue bool) error { 102 | if requeue { 103 | panic("requeue is not available with retry strategy") 104 | } 105 | 106 | attempts := GetAttempts(&a.msg) 107 | 108 | ttl, ok := a.retryer.NextBackOff(&a.msg, attempts) 109 | if ok && ttl >= 0 { 110 | return a.retry(tag, attempts, ttl) 111 | } 112 | 113 | return a.parent.Reject(tag, requeue) 114 | } 115 | 116 | func (a *retryAcknowledger) retry(tag uint64, attempts int, ttl time.Duration) error { 117 | headers := a.msg.Headers 118 | if headers == nil { 119 | headers = amqp.Table{} 120 | } 121 | 122 | headers["x-retry-attempts"] = strconv.FormatInt(int64(attempts+1), 10) 123 | 124 | if _, ok := headers["x-first-retry-exchange"]; !ok { 125 | headers["x-first-retry-exchange"] = a.msg.Exchange 126 | } 127 | 128 | if _, ok := headers["x-first-retry-routing-key"]; !ok { 129 | headers["x-first-retry-routing-key"] = a.msg.RoutingKey 130 | } 131 | 132 | msg := amqp.Publishing{ 133 | Headers: headers, 134 | ContentType: a.msg.ContentType, 135 | ContentEncoding: a.msg.ContentEncoding, 136 | DeliveryMode: a.msg.DeliveryMode, 137 | Priority: a.msg.Priority, 138 | CorrelationId: a.msg.CorrelationId, 139 | ReplyTo: a.msg.ReplyTo, 140 | Expiration: strconv.FormatInt(ttl.Milliseconds(), 10), 141 | MessageId: a.msg.MessageId, 142 | Timestamp: a.msg.Timestamp, 143 | Type: a.msg.Type, 144 | UserId: a.msg.UserId, 145 | AppId: a.msg.AppId, 146 | Body: a.msg.Body, 147 | } 148 | 149 | switch a.retryConsistency { 150 | case ConsistentRetry: 151 | err := a.retryProducer.channel.Tx() 152 | if err != nil { 153 | return err 154 | } 155 | 156 | err = a.retryProducer.Publish(a.retryQueue, true, false, msg) 157 | if err != nil { 158 | _ = a.retryProducer.channel.TxRollback() 159 | return err 160 | } 161 | 162 | err = a.parent.Ack(tag, false) 163 | if err != nil { 164 | _ = a.retryProducer.channel.TxRollback() 165 | return err 166 | } 167 | 168 | return a.retryProducer.channel.TxCommit() 169 | 170 | case EventuallyConsistentRetry: 171 | err := a.retryProducer.Publish(a.retryQueue, true, false, msg) 172 | if err != nil { 173 | return err 174 | } 175 | 176 | return a.parent.Ack(tag, false) 177 | 178 | default: 179 | panic("unsupported retry consistency") 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /retry_strategies.go: -------------------------------------------------------------------------------- 1 | package pubsub 2 | 3 | import ( 4 | "math" 5 | "time" 6 | 7 | amqp "github.com/rabbitmq/amqp091-go" 8 | ) 9 | 10 | type RetryStrategy interface { 11 | NextBackOff(*amqp.Delivery, int) (time.Duration, bool) 12 | } 13 | 14 | type ConstantRetryStrategy struct { 15 | maxRetry int 16 | interval time.Duration 17 | } 18 | 19 | func NewConstantRetryStrategy(maxRetry int, interval time.Duration) RetryStrategy { 20 | return &ConstantRetryStrategy{ 21 | maxRetry: maxRetry, 22 | interval: interval, 23 | } 24 | } 25 | 26 | func (rs *ConstantRetryStrategy) NextBackOff(msg *amqp.Delivery, attempts int) (time.Duration, bool) { 27 | if attempts >= rs.maxRetry { 28 | return 0, false 29 | } 30 | 31 | return rs.interval, true 32 | } 33 | 34 | type ExponentialRetryStrategy struct { 35 | maxRetry int 36 | initialInterval time.Duration 37 | intervalMultiplier float64 38 | } 39 | 40 | func NewExponentialRetryStrategy(maxRetry int, initialInterval time.Duration, intervalMultiplier float64) RetryStrategy { 41 | return &ExponentialRetryStrategy{ 42 | maxRetry: maxRetry, 43 | initialInterval: initialInterval, 44 | intervalMultiplier: intervalMultiplier, 45 | } 46 | } 47 | 48 | func (rs *ExponentialRetryStrategy) NextBackOff(msg *amqp.Delivery, attempts int) (time.Duration, bool) { 49 | if attempts >= rs.maxRetry { 50 | return 0, false 51 | } 52 | 53 | ns := float64(rs.initialInterval.Nanoseconds()) * math.Pow(rs.intervalMultiplier, float64(attempts)) 54 | return time.Duration(ns), true 55 | } 56 | 57 | type LazyRetryStrategy struct { 58 | maxRetry int 59 | } 60 | 61 | // ManualRetryStrategy is a retry strategy that will never automatically retry. 62 | // It will only retry if the message is rejected with a TTL. 63 | // This is useful if you want to retry the message manually with a custom TTL. 64 | // To do this, you should use the RejectWithRetry function. 65 | func NewLazyRetryStrategy(maxRetry int) RetryStrategy { 66 | return &LazyRetryStrategy{ 67 | maxRetry: maxRetry, 68 | } 69 | } 70 | 71 | func (rs *LazyRetryStrategy) NextBackOff(msg *amqp.Delivery, attempts int) (time.Duration, bool) { 72 | if attempts >= rs.maxRetry { 73 | return 0, false 74 | } 75 | return time.Duration(-1), true 76 | } 77 | -------------------------------------------------------------------------------- /types.go: -------------------------------------------------------------------------------- 1 | package pubsub 2 | 3 | type ExchangeKind string 4 | 5 | const ( 6 | ExchangeKindDirect ExchangeKind = "direct" 7 | ExchangeKindFanout ExchangeKind = "fanout" 8 | ExchangeKindTopic ExchangeKind = "topic" 9 | ExchangeKindHeaders ExchangeKind = "headers" 10 | ) 11 | --------------------------------------------------------------------------------