├── .github
└── workflows
│ └── ci.yml
├── .golangci.yml
├── CHANGELOG.md
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── TODO.md
├── assets
├── always-on-mechanism.png
├── consumer-safeguard.png
├── definitions.example.json
├── gorabbit-logo-lg.jpg
├── gorabbit-logo-md.jpg
├── gorabbit-logo-sm.jpg
└── publishing-safeguard.png
├── channel.go
├── client.go
├── client_options.go
├── connection.go
├── connection_manager.go
├── constants.go
├── consumer.go
├── consumer_test.go
├── go.mod
├── go.sum
├── logger.go
├── manager.go
├── manager_options.go
├── manager_test.go
├── marshalling.go
├── marshalling_test.go
├── model.go
├── ttl_map.go
└── utils.go
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on: [push, pull_request]
4 |
5 | jobs:
6 | test-library:
7 | runs-on: ubuntu-latest
8 | strategy:
9 | matrix:
10 | go-version: [ '1.20', '1.21', '1.22' ]
11 |
12 | steps:
13 | - uses: actions/checkout@v4
14 |
15 | - name: Setup Go
16 | uses: actions/setup-go@v4
17 | with:
18 | go-version: ${{ matrix.go-version }}
19 |
20 | - name: Install dependencies
21 | run: go get .
22 |
23 | - name: Go formatting analysis
24 | run: |
25 | if [ -n "$(gofmt -l .)" ]; then
26 | gofmt -d .
27 | exit 1
28 | fi
29 |
30 | - name: Go code quality analysis
31 | run: go vet ./...
32 |
33 | - name: Go unit testing
34 | run: |
35 | go test -race $(go list ./... | grep -v /vendor/) -v -coverprofile=coverage.out
36 | go tool cover -func=coverage.out
37 |
38 | - name: Upload coverage results
39 | uses: actions/upload-artifact@v3
40 | with:
41 | name: coverage
42 | path: coverage.out
43 |
44 | test-lint:
45 | runs-on: ubuntu-latest
46 | strategy:
47 | matrix:
48 | go-version: [ '1.20', '1.21', '1.22' ]
49 |
50 | steps:
51 | - uses: actions/checkout@v4
52 |
53 | - name: Setup Go
54 | uses: actions/setup-go@v4
55 | with:
56 | go-version: ${{ matrix.go-version }}
57 |
58 | - name: Install golangci-lint
59 | run: |
60 | curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s v1.57.2
61 |
62 | - name: Run golangci-lint
63 | run: ./bin/golangci-lint run -v
64 |
--------------------------------------------------------------------------------
/.golangci.yml:
--------------------------------------------------------------------------------
1 | # This code is licensed under the terms of the MIT license https://opensource.org/license/mit
2 | # Copyright (c) 2021 Marat Reymers
3 |
4 | ## Golden config for golangci-lint v1.57.2
5 | #
6 | # This is the best config for golangci-lint based on my experience and opinion.
7 | # It is very strict, but not extremely strict.
8 | # Feel free to adapt and change it for your needs.
9 |
10 | run:
11 | # Timeout for analysis, e.g. 30s, 5m.
12 | # Default: 1m
13 | timeout: 3m
14 |
15 |
16 | # This file contains only configs which differ from defaults.
17 | # All possible options can be found here https://github.com/golangci/golangci-lint/blob/master/.golangci.reference.yml
18 | linters-settings:
19 | cyclop:
20 | # The maximal code complexity to report.
21 | # Default: 10
22 | max-complexity: 30
23 | # The maximal average package complexity.
24 | # If it's higher than 0.0 (float) the check is enabled
25 | # Default: 0.0
26 | package-average: 10.0
27 |
28 | errcheck:
29 | # Report about not checking of errors in type assertions: `a := b.(MyStruct)`.
30 | # Such cases aren't reported by default.
31 | # Default: false
32 | check-type-assertions: true
33 |
34 | exhaustive:
35 | # Program elements to check for exhaustiveness.
36 | # Default: [ switch ]
37 | check:
38 | - switch
39 | - map
40 |
41 | exhaustruct:
42 | # List of regular expressions to exclude struct packages and their names from checks.
43 | # Regular expressions must match complete canonical struct package/name/structname.
44 | # Default: []
45 | exclude:
46 | # std libs
47 | - "^net/http.Client$"
48 | - "^net/http.Cookie$"
49 | - "^net/http.Request$"
50 | - "^net/http.Response$"
51 | - "^net/http.Server$"
52 | - "^net/http.Transport$"
53 | - "^net/url.URL$"
54 | - "^os/exec.Cmd$"
55 | - "^reflect.StructField$"
56 | # public libs
57 | - "^github.com/Shopify/sarama.Config$"
58 | - "^github.com/Shopify/sarama.ProducerMessage$"
59 | - "^github.com/mitchellh/mapstructure.DecoderConfig$"
60 | - "^github.com/prometheus/client_golang/.+Opts$"
61 | - "^github.com/spf13/cobra.Command$"
62 | - "^github.com/spf13/cobra.CompletionOptions$"
63 | - "^github.com/stretchr/testify/mock.Mock$"
64 | - "^github.com/testcontainers/testcontainers-go.+Request$"
65 | - "^github.com/testcontainers/testcontainers-go.FromDockerfile$"
66 | - "^golang.org/x/tools/go/analysis.Analyzer$"
67 | - "^google.golang.org/protobuf/.+Options$"
68 | - "^gopkg.in/yaml.v3.Node$"
69 |
70 | funlen:
71 | # Checks the number of lines in a function.
72 | # If lower than 0, disable the check.
73 | # Default: 60
74 | lines: 120
75 | # Checks the number of statements in a function.
76 | # If lower than 0, disable the check.
77 | # Default: 40
78 | statements: 50
79 | # Ignore comments when counting lines.
80 | # Default false
81 | ignore-comments: true
82 |
83 | gocognit:
84 | # Minimal code complexity to report.
85 | # Default: 30 (but we recommend 10-20)
86 | min-complexity: 20
87 |
88 | gocritic:
89 | # Settings passed to gocritic.
90 | # The settings key is the name of a supported gocritic checker.
91 | # The list of supported checkers can be find in https://go-critic.github.io/overview.
92 | settings:
93 | captLocal:
94 | # Whether to restrict checker to params only.
95 | # Default: true
96 | paramsOnly: false
97 | underef:
98 | # Whether to skip (*x).method() calls where x is a pointer receiver.
99 | # Default: true
100 | skipRecvDeref: false
101 |
102 | gomnd:
103 | # List of function patterns to exclude from analysis.
104 | # Values always ignored: `time.Date`,
105 | # `strconv.FormatInt`, `strconv.FormatUint`, `strconv.FormatFloat`,
106 | # `strconv.ParseInt`, `strconv.ParseUint`, `strconv.ParseFloat`.
107 | # Default: []
108 | ignored-functions:
109 | - flag.Arg
110 | - flag.Duration.*
111 | - flag.Float.*
112 | - flag.Int.*
113 | - flag.Uint.*
114 | - os.Chmod
115 | - os.Mkdir.*
116 | - os.OpenFile
117 | - os.WriteFile
118 | - prometheus.ExponentialBuckets.*
119 | - prometheus.LinearBuckets
120 |
121 | gomodguard:
122 | blocked:
123 | # List of blocked modules.
124 | # Default: []
125 | modules:
126 | - github.com/golang/protobuf:
127 | recommendations:
128 | - google.golang.org/protobuf
129 | reason: "see https://developers.google.com/protocol-buffers/docs/reference/go/faq#modules"
130 | - github.com/satori/go.uuid:
131 | recommendations:
132 | - github.com/google/uuid
133 | reason: "satori's package is not maintained"
134 | - github.com/gofrs/uuid:
135 | recommendations:
136 | - github.com/gofrs/uuid/v5
137 | reason: "gofrs' package was not go module before v5"
138 |
139 | gosec:
140 | excludes:
141 | - G404 # This error is triggered when we use math/rand instead of crypto/rand (Too strict)
142 |
143 | govet:
144 | # Enable all analyzers.
145 | # Default: false
146 | enable-all: true
147 | # Disable analyzers by name.
148 | # Run `go tool vet help` to see all analyzers.
149 | # Default: []
150 | disable:
151 | - fieldalignment # too strict
152 | # Settings per analyzer.
153 | settings:
154 | shadow:
155 | # Whether to be strict about shadowing; can be noisy.
156 | # Default: false
157 | strict: true
158 |
159 | inamedparam:
160 | # Skips check for interface methods with only a single parameter.
161 | # Default: false
162 | skip-single-param: true
163 |
164 | lll:
165 | # Make an issue if line has more characters than this setting.
166 | # Default: 120
167 | line-length: 160
168 | tab-width: 1
169 |
170 | nakedret:
171 | # Make an issue if func has more lines of code than this setting, and it has naked returns.
172 | # Default: 30
173 | max-func-lines: 0
174 |
175 | nolintlint:
176 | # Exclude following linters from requiring an explanation.
177 | # Default: []
178 | allow-no-explanation: [ funlen, gocognit, lll ]
179 | # Enable to require an explanation of nonzero length after each nolint directive.
180 | # Default: false
181 | require-explanation: true
182 | # Enable to require nolint directives to mention the specific linter being suppressed.
183 | # Default: false
184 | require-specific: true
185 |
186 | perfsprint:
187 | # Optimizes into strings concatenation.
188 | # Default: true
189 | strconcat: false
190 |
191 | rowserrcheck:
192 | # database/sql is always checked
193 | # Default: []
194 | packages:
195 | - github.com/jmoiron/sqlx
196 |
197 | tenv:
198 | # The option `all` will run against whole test files (`_test.go`) regardless of method/function signatures.
199 | # Otherwise, only methods that take `*testing.T`, `*testing.B`, and `testing.TB` as arguments are checked.
200 | # Default: false
201 | all: true
202 |
203 |
204 | linters:
205 | disable-all: true
206 | enable:
207 | ## enabled by default
208 | - errcheck # checking for unchecked errors, these unchecked errors can be critical bugs in some cases
209 | - gosimple # specializes in simplifying a code
210 | - govet # reports suspicious constructs, such as Printf calls whose arguments do not align with the format string
211 | - ineffassign # detects when assignments to existing variables are not used
212 | - staticcheck # is a go vet on steroids, applying a ton of static analysis checks
213 | - typecheck # like the front-end of a Go compiler, parses and type-checks Go code
214 | - unused # checks for unused constants, variables, functions and types
215 | ## disabled by default
216 | - asasalint # checks for pass []any as any in variadic func(...any)
217 | - asciicheck # checks that your code does not contain non-ASCII identifiers
218 | - bidichk # checks for dangerous unicode character sequences
219 | - bodyclose # checks whether HTTP response body is closed successfully
220 | - cyclop # checks function and package cyclomatic complexity
221 | - dupl # tool for code clone detection
222 | - durationcheck # checks for two durations multiplied together
223 | - errname # checks that sentinel errors are prefixed with the Err and error types are suffixed with the Error
224 | - errorlint # finds code that will cause problems with the error wrapping scheme introduced in Go 1.13
225 | - execinquery # checks query string in Query function which reads your Go src files and warning it finds
226 | - exhaustive # checks exhaustiveness of enum switch statements
227 | - exportloopref # checks for pointers to enclosing loop variables
228 | - forbidigo # forbids identifiers
229 | - funlen # tool for detection of long functions
230 | - gocheckcompilerdirectives # validates go compiler directive comments (//go:)
231 | - gochecknoinits # checks that no init functions are present in Go code
232 | - gochecksumtype # checks exhaustiveness on Go "sum types"
233 | - gocognit # computes and checks the cognitive complexity of functions
234 | - goconst # finds repeated strings that could be replaced by a constant
235 | - gocritic # provides diagnostics that check for bugs, performance and style issues
236 | - gocyclo # computes and checks the cyclomatic complexity of functions
237 | - godot # checks if comments end in a period
238 | - goimports # in addition to fixing imports, goimports also formats your code in the same style as gofmt
239 | - gomnd # detects magic numbers
240 | - gomoddirectives # manages the use of 'replace', 'retract', and 'excludes' directives in go.mod
241 | - gomodguard # allow and block lists linter for direct Go module dependencies. This is different from depguard where there are different block types for example version constraints and module recommendations
242 | - goprintffuncname # checks that printf-like functions are named with f at the end
243 | - gosec # inspects source code for security problems
244 | - lll # reports long lines
245 | - loggercheck # checks key value pairs for common logger libraries (kitlog,klog,logr,zap)
246 | - makezero # finds slice declarations with non-zero initial length
247 | - mirror # reports wrong mirror patterns of bytes/strings usage
248 | - musttag # enforces field tags in (un)marshaled structs
249 | - nakedret # finds naked returns in functions greater than a specified function length
250 | - nestif # reports deeply nested if statements
251 | - nilerr # finds the code that returns nil even if it checks that the error is not nil
252 | - nilnil # checks that there is no simultaneous return of nil error and an invalid value
253 | - noctx # finds sending http request without context.Context
254 | - nolintlint # reports ill-formed or insufficient nolint directives
255 | - nonamedreturns # reports all named returns
256 | - perfsprint # checks that fmt.Sprintf can be replaced with a faster alternative
257 | - prealloc # [premature optimization, but can be used in some cases] finds slice declarations that could potentially be preallocated
258 | - predeclared # finds code that shadows one of Go's predeclared identifiers
259 | - promlinter # checks Prometheus metrics naming via promlint
260 | - protogetter # reports direct reads from proto message fields when getters should be used
261 | - reassign # checks that package variables are not reassigned
262 | - revive # fast, configurable, extensible, flexible, and beautiful linter for Go, drop-in replacement of golint
263 | - rowserrcheck # checks whether Err of rows is checked successfully
264 | - sloglint # ensure consistent code style when using log/slog
265 | - spancheck # checks for mistakes with OpenTelemetry/Census spans
266 | - sqlclosecheck # checks that sql.Rows and sql.Stmt are closed
267 | - stylecheck # is a replacement for golint
268 | - tagalign # checks that struct tags are well aligned
269 | - tenv # detects using os.Setenv instead of t.Setenv since Go1.17
270 | - testableexamples # checks if examples are testable (have an expected output)
271 | - testifylint # checks usage of github.com/stretchr/testify
272 | - testpackage # makes you use a separate _test package
273 | - tparallel # detects inappropriate usage of t.Parallel() method in your Go test codes
274 | - unconvert # removes unnecessary type conversions
275 | - unparam # reports unused function parameters
276 | - usestdlibvars # detects the possibility to use variables/constants from the Go standard library
277 | - wastedassign # finds wasted assignment statements
278 | - whitespace # detects leading and trailing whitespace
279 |
280 | ## you may want to enable
281 | #- decorder # checks declaration order and count of types, constants, variables and functions
282 | #- exhaustruct # [highly recommend to enable] checks if all structure fields are initialized
283 | #- gci # controls golang package import order and makes it always deterministic
284 | #- ginkgolinter # [if you use ginkgo/gomega] enforces standards of using ginkgo and gomega
285 | #- godox # detects FIXME, TODO and other comment keywords
286 | #- gochecknoglobals # checks that no global variables exist
287 | #- goheader # checks is file header matches to pattern
288 | #- inamedparam # [great idea, but too strict, need to ignore a lot of cases by default] reports interfaces with unnamed method parameters
289 | #- interfacebloat # checks the number of methods inside an interface
290 | #- intrange # finds places where for loops could make use of an integer range
291 | #- ireturn # accept interfaces, return concrete types
292 | #- varnamelen # [great idea, but too many false positives] checks that the length of a variable's name matches its scope
293 | #- wrapcheck # checks that errors returned from external packages are wrapped
294 | #- zerologlint # detects the wrong usage of zerolog that a user forgets to dispatch zerolog.Event
295 |
296 | ## disabled
297 | #- containedctx # detects struct contained context.Context field
298 | #- contextcheck # [too many false positives] checks the function whether use a non-inherited context
299 | #- copyloopvar # detects places where loop variables are copied
300 | #- depguard # [replaced by gomodguard] checks if package imports are in a list of acceptable packages
301 | #- dogsled # checks assignments with too many blank identifiers (e.g. x, _, _, _, := f())
302 | #- dupword # [useless without config] checks for duplicate words in the source code
303 | #- errchkjson # [don't see profit + I'm against of omitting errors like in the first example https://github.com/breml/errchkjson] checks types passed to the json encoding functions. Reports unsupported types and optionally reports occasions, where the check for the returned error can be omitted
304 | #- forcetypeassert # [replaced by errcheck] finds forced type assertions
305 | #- goerr113 # [too strict] checks the errors handling expressions
306 | #- gofmt # [replaced by goimports] checks whether code was gofmt-ed
307 | #- gofumpt # [replaced by goimports, gofumports is not available yet] checks whether code was gofumpt-ed
308 | #- gosmopolitan # reports certain i18n/l10n anti-patterns in your Go codebase
309 | #- grouper # analyzes expression groups
310 | #- importas # enforces consistent import aliases
311 | #- maintidx # measures the maintainability index of each function
312 | #- misspell # [useless] finds commonly misspelled English words in comments
313 | #- nlreturn # [too strict and mostly code is not more readable] checks for a new line before return and branch statements to increase code clarity
314 | #- nosprintfhostport # checks for misuse of Sprintf to construct a host with port in a URL
315 | #- paralleltest # [too many false positives] detects missing usage of t.Parallel() method in your Go test
316 | #- tagliatelle # checks the struct tags
317 | #- thelper # detects golang test helpers without t.Helper() call and checks the consistency of test helpers
318 | #- wsl # [too strict and mostly code is not more readable] whitespace linter forces you to use empty lines
319 |
320 |
321 | issues:
322 | # Maximum count of issues with the same text.
323 | # Set to 0 to disable.
324 | # Default: 3
325 | max-same-issues: 50
326 |
327 | exclude-rules:
328 | - source: "(noinspection|TODO)"
329 | linters: [ godot ]
330 | - source: "//noinspection"
331 | linters: [ gocritic ]
332 | - path: "_test\\.go"
333 | linters:
334 | - bodyclose
335 | - dupl
336 | - funlen
337 | - goconst
338 | - gosec
339 | - noctx
340 | - wrapcheck
341 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # 1.1.2
2 |
3 | - Allow sending messages with a TTL via `PublishWithOptions` by adding a new `TTL` property in `PublishingOptions` ([PR](https://github.com/KardinalAI/gorabbit/pull/19)).
4 |
5 | # 1.1.1
6 |
7 | - Minor fix for correct usage of the `ConnectionName` parameter, and the possibility to declare it via environment variables ([PR](https://github.com/KardinalAI/gorabbit/pull/18)).
8 |
9 | # 1.1.0
10 |
11 | - Allow setting a connection name ([PR](https://github.com/KardinalAI/gorabbit/pull/8))
12 | - Allow configuring a custom marshaller ([PR](https://github.com/KardinalAI/gorabbit/pull/10))
13 | - Allow setting autoDelete on queues ([PR](https://github.com/KardinalAI/gorabbit/pull/11))
14 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | We as members, contributors, and leaders pledge to make participation in our
6 | community a harassment-free experience for everyone, regardless of age, body
7 | size, visible or invisible disability, ethnicity, sex characteristics, gender
8 | identity and expression, level of experience, education, socio-economic status,
9 | nationality, personal appearance, race, religion, or sexual identity
10 | and orientation.
11 |
12 | We pledge to act and interact in ways that contribute to an open, welcoming,
13 | diverse, inclusive, and healthy community.
14 |
15 | ## Our Standards
16 |
17 | Examples of behavior that contributes to a positive environment for our
18 | community include:
19 |
20 | * Demonstrating empathy and kindness toward other people
21 | * Being respectful of differing opinions, viewpoints, and experiences
22 | * Giving and gracefully accepting constructive feedback
23 | * Accepting responsibility and apologizing to those affected by our mistakes,
24 | and learning from the experience
25 | * Focusing on what is best not just for us as individuals, but for the
26 | overall community
27 |
28 | Examples of unacceptable behavior include:
29 |
30 | * The use of sexualized language or imagery, and sexual attention or
31 | advances of any kind
32 | * Trolling, insulting or derogatory comments, and personal or political attacks
33 | * Public or private harassment
34 | * Publishing others' private information, such as a physical or email
35 | address, without their explicit permission
36 | * Other conduct which could reasonably be considered inappropriate in a
37 | professional setting
38 |
39 | ## Enforcement Responsibilities
40 |
41 | Community leaders are responsible for clarifying and enforcing our standards of
42 | acceptable behavior and will take appropriate and fair corrective action in
43 | response to any behavior that they deem inappropriate, threatening, offensive,
44 | or harmful.
45 |
46 | Community leaders have the right and responsibility to remove, edit, or reject
47 | comments, commits, code, wiki edits, issues, and other contributions that are
48 | not aligned to this Code of Conduct, and will communicate reasons for moderation
49 | decisions when appropriate.
50 |
51 | ## Scope
52 |
53 | This Code of Conduct applies within all community spaces, and also applies when
54 | an individual is officially representing the community in public spaces.
55 | Examples of representing our community include using an official email address,
56 | posting via an official social media account, or acting as an appointed
57 | representative at an online or offline event.
58 |
59 | ## Enforcement
60 |
61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
62 | reported to the community leaders responsible for enforcement at
63 | [INSERT CONTACT METHOD].
64 | All complaints will be reviewed and investigated promptly and fairly.
65 |
66 | All community leaders are obligated to respect the privacy and security of the
67 | reporter of any incident.
68 |
69 | ## Enforcement Guidelines
70 |
71 | Community leaders will follow these Community Impact Guidelines in determining
72 | the consequences for any action they deem in violation of this Code of Conduct:
73 |
74 | ### 1. Correction
75 |
76 | **Community Impact**: Use of inappropriate language or other behavior deemed
77 | unprofessional or unwelcome in the community.
78 |
79 | **Consequence**: A private, written warning from community leaders, providing
80 | clarity around the nature of the violation and an explanation of why the
81 | behavior was inappropriate. A public apology may be requested.
82 |
83 | ### 2. Warning
84 |
85 | **Community Impact**: A violation through a single incident or series
86 | of actions.
87 |
88 | **Consequence**: A warning with consequences for continued behavior. No
89 | interaction with the people involved, including unsolicited interaction with
90 | those enforcing the Code of Conduct, for a specified period of time. This
91 | includes avoiding interactions in community spaces as well as external channels
92 | like social media. Violating these terms may lead to a temporary or
93 | permanent ban.
94 |
95 | ### 3. Temporary Ban
96 |
97 | **Community Impact**: A serious violation of community standards, including
98 | sustained inappropriate behavior.
99 |
100 | **Consequence**: A temporary ban from any sort of interaction or public
101 | communication with the community for a specified period of time. No public or
102 | private interaction with the people involved, including unsolicited interaction
103 | with those enforcing the Code of Conduct, is allowed during this period.
104 | Violating these terms may lead to a permanent ban.
105 |
106 | ### 4. Permanent Ban
107 |
108 | **Community Impact**: Demonstrating a pattern of violation of community
109 | standards, including sustained inappropriate behavior, harassment of an
110 | individual, or aggression toward or disparagement of classes of individuals.
111 |
112 | **Consequence**: A permanent ban from any sort of public interaction within
113 | the community.
114 |
115 | ## Attribution
116 |
117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage],
118 | version 2.0, available at
119 | [https://www.contributor-covenant.org/version/2/0/code_of_conduct.html][v2.0].
120 |
121 | Community Impact Guidelines were inspired by
122 | [Mozilla's code of conduct enforcement ladder][Mozilla CoC].
123 |
124 | For answers to common questions about this code of conduct, see the FAQ at
125 | [https://www.contributor-covenant.org/faq][FAQ]. Translations are available
126 | at [https://www.contributor-covenant.org/translations][translations].
127 |
128 | [homepage]: https://www.contributor-covenant.org
129 |
130 | [v2.0]: https://www.contributor-covenant.org/version/2/0/code_of_conduct.html
131 |
132 | [Mozilla CoC]: https://github.com/mozilla/diversity
133 |
134 | [FAQ]: https://www.contributor-covenant.org/faq
135 |
136 | [translations]: https://www.contributor-covenant.org/translations
137 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing to Gorabbit
2 |
3 | Thank you for your interest in contributing to Gorabbit! We welcome contributions from the community to make our project
4 | better.
5 |
6 | ## Getting Started
7 |
8 | Before you start contributing, please make sure you have read the [README](./README.md) to understand the project and
9 | its goals.
10 |
11 | ## How Can I Contribute?
12 |
13 | ### Reporting Bugs
14 |
15 | If you come across a bug or unexpected behavior, please help us by submitting a detailed bug report. Use
16 | the [GitHub Issues](https://github.com/KardinalAI/gorabbit/issues) to report bugs, and make sure to include the
17 | following information:
18 |
19 | - A clear and descriptive title.
20 | - A detailed description of the issue, including steps to reproduce.
21 | - Information about your environment: Go version, operating system, etc.
22 |
23 | ### Suggesting Enhancements
24 |
25 | If you have ideas for enhancements or new features, we would love to hear them! Create an issue
26 | on [GitHub](https://github.com/KardinalAI/gorabbit/issues) with a clear description of your proposal, and we can discuss
27 | it together.
28 |
29 | ### Pull Requests
30 |
31 | We welcome contributions in the form of pull requests. If you want to contribute code, please follow these guidelines:
32 |
33 | 1. Fork the repository and create your branch from `v1`.
34 | 2. Make sure your code adheres to the [Go coding standards](https://golang.org/doc/effective_go).
35 | 3. Make sure your code respects all [linting rules](./.golangci.yml) using [golangci-lint](https://golangci-lint.run/)
36 | version 1.57.2.
37 | 4. Write clear commit messages and include documentation if necessary.
38 | 5. Make sure your code passes the existing tests.
39 | 6. Open a pull request, providing a clear description of your changes.
40 |
41 | ## Code of Conduct
42 |
43 | Please note that this project has a [Code of Conduct](./CODE_OF_CONDUCT.md). By participating in this project, you are
44 | expected to uphold this code. Please report any unacceptable behavior to an administrator.
45 |
46 | ## License
47 |
48 | By contributing to Gorabbit, you agree that your contributions will be licensed under the [LICENSE](./LICENSE) file
49 | associated with this project.
50 |
51 | Thank you for your contributions!
52 |
53 | KardinalAI
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 KardinalAI
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Gorabbit
2 |
3 |
4 |
5 |
6 |
7 | Gorabbit is a wrapper that provides high level and robust RabbitMQ operations through a client or a manager.
8 |
9 | This wrapper depends on the official [Go RabbitMQ plugin](https://github.com/rabbitmq/amqp091-go).
10 |
11 | * [Installation](#installation)
12 | * [Go Module](#go-module)
13 | * [Environment Variables](#environment-variables)
14 | * [Always On Mechanism](#always-on-mechanism)
15 | * [Client](#client)
16 | * [Initialization](#client-initialization)
17 | * [Options](#client-options)
18 | * [Default Options](#client-with-default-options)
19 | * [Custom Options](#client-with-custom-options)
20 | * [Builder](#client-options-using-the-builder)
21 | * [Struct](#client-options-using-struct-initialization)
22 | * [Disconnection](#client-disconnection)
23 | * [Publishing](#publishing)
24 | * [Consuming](#consuming)
25 | * [Ready and Health Checks](#ready-and-health-checks)
26 | * [Manager](#manager)
27 | * [Initialization](#manager-initialization)
28 | * [Options](#manager-options)
29 | * [Default Options](#manager-with-default-options)
30 | * [Custom Options](#manager-with-custom-options)
31 | * [Builder](#manager-options-using-the-builder)
32 | * [Struct](#manager-options-using-struct-initialization)
33 | * [Disconnection](#manager-disconnection)
34 | * [Operations](#manager-operations)
35 | * [Exchange Creation](#exchange-creation)
36 | * [Queue Creation](#queue-creation)
37 | * [Binding Creation](#binding-creation)
38 | * [Message Count](#queue-messages-count)
39 | * [Push Message](#push-message)
40 | * [Pop Message](#pop-message)
41 | * [Purge Queue](#purge-queue)
42 | * [Delete Queue](#delete-queue)
43 | * [Delete Exchange](#delete-exchange)
44 | * [Setup From Definitions](#setup-from-schema-definition-file)
45 |
46 | ## Installation
47 |
48 | ### Go module
49 |
50 | ```bash
51 | go get github.com/KardinalAI/gorabbit
52 | ```
53 |
54 | ### Environment variables
55 |
56 | The client's and manager's `Mode` can also be set via an environment variable that will **override** the manually
57 | entered value.
58 |
59 | ```dotenv
60 | GORABBIT_MODE: debug # possible values: release or debug
61 | ```
62 |
63 | The client and manager can also be completely disabled via the following environment variable:
64 |
65 | ```dotenv
66 | GORABBIT_DISABLED: true # possible values: true, false, 1, or 0
67 | ```
68 |
69 | ## Always-on mechanism
70 |
71 | Here is a visual representation of the always-on mechanism of a connection and channel when the `KeepAlive` flag is set
72 | to true.
73 |
74 | 
75 |
76 | ## Client
77 |
78 | The gorabbit client offers 2 main functionalities:
79 |
80 | * Publishing
81 | * Consuming
82 |
83 | Additionally, the client also provides a ready check and a health check.
84 |
85 | ### Client initialization
86 |
87 | A client can be initialized via the constructor `NewClient`. This constructor takes `ClientOptions` as an optional
88 | parameter.
89 |
90 | ### Client options
91 |
92 | | Property | Description | Default Value |
93 | |---------------------|---------------------------------------------------------------|---------------|
94 | | Host | The hostname of the RabbitMQ server | 127.0.0.1 |
95 | | Port | The port of the RabbitMQ server | 5672 |
96 | | Username | The plain authentication username | guest |
97 | | Password | The plain authentication password | guest |
98 | | Vhost | The specific vhost to use when connection to CloudAMQP | |
99 | | UseTLS | The flag that activates the use of TLS (amqps) | false |
100 | | ConnectionName | The desired connection name | Gorabbit |
101 | | KeepAlive | The flag that activates retry and re-connect mechanisms | true |
102 | | RetryDelay | The delay between each retry and re-connection | 3 seconds |
103 | | MaxRetry | The max number of message retry if it failed to process | 5 |
104 | | PublishingCacheTTL | The time to live for a failed publish when set in cache | 60 seconds |
105 | | PublishingCacheSize | The max number of failed publish to add into cache | 128 |
106 | | Mode | The mode defines whether logs are shown or not | Release |
107 | | Marshaller | The content type used for messages and how they're marshalled | JSON |
108 |
109 | ### Client with default options
110 |
111 | Passing `nil` options will trigger the client to use default values (host, port, credentials, etc...)
112 | via `DefaultClientOptions()`.
113 |
114 | ```go
115 | client := gorabbit.NewClient(nil)
116 | ```
117 |
118 | You can also explicitly pass `DefaultClientOptions()` for a cleaner initialization.
119 |
120 | ```go
121 | client := gorabbit.NewClient(gorabbit.DefaultClientOptions())
122 | ```
123 |
124 | Finally, passing a `NewClientOptions()` method also initializes default values if not overwritten.
125 |
126 | ```go
127 | client := gorabbit.NewClient(gorabbit.NewClientOptions())
128 | ```
129 |
130 | ### Client with options from environment variables
131 |
132 | You can instantiate a client from environment variables, without the need of manually specifying options in the code.
133 |
134 | ```go
135 | client := gorabbit.NewClientFromEnv()
136 | ```
137 |
138 | Here are the following supported environment variables:
139 |
140 | * `RABBITMQ_HOST`: Defines the host,
141 | * `RABBITMQ_PORT`: Defines the port,
142 | * `RABBITMQ_USERNAME`: Defines the username,
143 | * `RABBITMQ_PASSWORD`: Defines the password,
144 | * `RABBITMQ_VHOST`: Defines the vhost,
145 | * `RABBITMQ_USE_TLS`: Defines whether to use TLS or no.
146 | * `RABBITMQ_CONNECTION_NAME`: Defines the desired connection name.
147 |
148 | **Note that environment variables are all optional, so missing keys will be replaced by their corresponding default.**
149 |
150 | ### Client with custom options
151 |
152 | We can input custom values for a specific property, either via the built-in builder or via direct struct initialization.
153 |
154 | #### Client options using the builder
155 |
156 | `NewClientOptions()` and `DefaultClientOptions()` both return an instance of `*ClientOptions` that can act as a builder.
157 |
158 | ```go
159 | options := gorabbit.NewClientOptions().
160 | SetMode(gorabbit.Debug).
161 | SetCredentials("root", "password").
162 | SetRetryDelay(5 * time.Second)
163 |
164 | client := gorabbit.NewClient(options)
165 | ```
166 |
167 | > :information_source: There is a setter method for each property.
168 |
169 | #### Client options using struct initialization
170 |
171 | `ClientOptions` is an exported type, so it can be used directly.
172 |
173 | ```go
174 | options := gorabbit.ClientOptions {
175 | Host: "localhost",
176 | Port: 5673,
177 | Username: "root",
178 | Password: "password",
179 | ...
180 | }
181 |
182 | client := gorabbit.NewClient(&options)
183 | ```
184 |
185 | > :warning: Direct initialization via the struct **does not use default values on missing properties**, so be sure to
186 | > fill in every property available.
187 |
188 | ### Client disconnection
189 |
190 | When a client is initialized, to prevent a leak, always disconnect it when no longer needed.
191 |
192 | ```go
193 | client := gorabbit.NewClient(gorabbit.DefaultClientOptions())
194 | defer client.Disconnect()
195 | ```
196 |
197 | ### Publishing
198 |
199 | To send a message, the client offers two simple methods: `Publish` and `PublishWithOptions`. The required arguments for
200 | publishing are:
201 |
202 | * Exchange (which exchange the message should be sent to)
203 | * Routing Key
204 | * Payload (`interface{}`, the object will be marshalled internally)
205 |
206 | Example of sending a simple string
207 |
208 | ```go
209 | err := client.Publish("events_exchange", "event.foo.bar.created", "foo string")
210 | ```
211 |
212 | Example of sending an object
213 |
214 | ```go
215 | type foo struct {
216 | Action string
217 | }
218 |
219 | err := client.Publish("events_exchange", "event.foo.bar.created", foo{Action: "bar"})
220 | ```
221 |
222 | Optionally, you can set the message's `Priority`, `DeliveryMode` and `Expiration` via the `PublishWithOptions` method.
223 |
224 | ```go
225 | options := gorabbit.SendOptions().
226 | SetPriority(gorabbit.PriorityMedium).
227 | SetDeliveryMode(gorabbit.Persistent).
228 | SetTTL(5*time.Second)
229 |
230 | err := client.PublishWithOptions("events_exchange", "event.foo.bar.created", "foo string", options)
231 | ```
232 |
233 | > :information_source: If the `KeepAlive` flag is set to true when initializing the client, failed publishing will be
234 | > cached once
235 | > and re-published as soon as the channel is back up.
236 | >
237 | > 
238 |
239 | ### Consuming
240 |
241 | To consume messages, gorabbit offers a very simple asynchronous consumer method `Consume` that takes a `MessageConsumer`
242 | as argument. Error handling, acknowledgement, negative acknowledgement and rejection are all done internally by the
243 | consumer.
244 |
245 | ```go
246 | err := client.RegisterConsumer(gorabbit.MessageConsumer{
247 | Queue: "events_queue",
248 | Name: "toto_consumer",
249 | PrefetchSize: 0,
250 | PrefetchCount: 10,
251 | AutoAck: false,
252 | ConcurrentProcess: false,
253 | Handlers: gorabbit.MQTTMessageHandlers{
254 | "event.foo.bar.created": func (payload []byte) error {
255 | fmt.Println(string(payload))
256 |
257 | return nil
258 | },
259 | },
260 | })
261 | ```
262 |
263 | * Queue: The queue to consume messages from
264 | * Name: Unique identifier for the consumer
265 | * PrefetchSize: The maximum size of messages that can be processed at the same time
266 | * PrefetchCount: The maximum number of messages that can be processed at the same time
267 | * AutoAck: Automatic acknowledgement of messages upon reception
268 | * ConcurrentProcess: Asynchronous handling of deliveries
269 | * Handlers: A list of handlers for specified routes
270 |
271 | **NB:** [RabbitMQ Wildcards](https://www.cloudamqp.com/blog/rabbitmq-topic-exchange-explained.html) are also supported.
272 | If multiple routing keys have the same handler, a wildcard can be used, for example:
273 | `event.foo.bar.*` or `event.foo.#`.
274 |
275 | > :information_source: If the `KeepAlive` flag is set to true when initializing the client, consumers will
276 | > auto-reconnect after a connection loss.
277 | > This mechanism is indefinite and therefore, consuming from a non-existent queue will trigger an error repeatedly but
278 | > will not affect
279 | > other consumptions. This is because each consumer has its **own channel**.
280 | >
281 | > 
282 |
283 | ### Ready and Health checks
284 |
285 | The client offers `IsReady()` and `IsHealthy()` checks that can be used for monitoring.
286 |
287 | **Ready:** Verifies that connections are opened and ready to launch new operations.
288 |
289 | **Healthy:** Verifies that both connections and channels are opened, ready and ongoing operations are working
290 | (Consumers are consuming).
291 |
292 | ## Manager
293 |
294 | The gorabbit manager offers multiple management operations:
295 |
296 | * Exchange, queue and bindings creation
297 | * Exchange and queue deletion
298 | * Queue evaluation: Exists, number of messages
299 | * Queue operations: Pop message, push message, purge
300 |
301 | > :warning: A manager should only be used for either testing RabbitMQ functionalities or setting up a RabbitMQ server.
302 | > The manager does not provide robust mechanisms of retry and reconnection like the client.
303 |
304 | ### Manager initialization
305 |
306 | A manager can be initialized via the constructor `NewManager`. This constructor takes `ManagerOptions` as an optional
307 | parameter.
308 |
309 | ### Manager options
310 |
311 | | Property | Description | Default Value |
312 | |------------|---------------------------------------------------------------|---------------|
313 | | Host | The hostname of the RabbitMQ server | 127.0.0.1 |
314 | | Port | The port of the RabbitMQ server | 5672 |
315 | | Username | The plain authentication username | guest |
316 | | Password | The plain authentication password | guest |
317 | | Vhost | The specific vhost to use when connection to CloudAMQP | |
318 | | UseTLS | The flag that activates the use of TLS (amqps) | false |
319 | | Mode | The mode defines whether logs are shown or not | Release |
320 | | Marshaller | The content type used for messages and how they're marshalled | JSON |
321 |
322 | ### Manager with default options
323 |
324 | Passing `nil` options will trigger the manager to use default values (host, port, credentials, etc...)
325 | via `DefaultManagerOptions()`.
326 |
327 | ```go
328 | manager := gorabbit.NewManager(nil)
329 | ```
330 |
331 | You can also explicitly pass `DefaultManagerOptions()` for a cleaner initialization.
332 |
333 | ```go
334 | manager := gorabbit.NewManager(gorabbit.DefaultManagerOptions())
335 | ```
336 |
337 | Finally, passing a `NewManagerOptions()` method also initializes default values if not overwritten.
338 |
339 | ```go
340 | manager := gorabbit.NewManager(gorabbit.NewManagerOptions())
341 | ```
342 |
343 | ### Manager with options from environment variables
344 |
345 | You can instantiate a manager from environment variables, without the need of manually specifying options in the code.
346 |
347 | ```go
348 | manager := gorabbit.NewManagerFromEnv()
349 | ```
350 |
351 | Here are the following supported environment variables:
352 |
353 | * `RABBITMQ_HOST`: Defines the host,
354 | * `RABBITMQ_PORT`: Defines the port,
355 | * `RABBITMQ_USERNAME`: Defines the username,
356 | * `RABBITMQ_PASSWORD`: Defines the password,
357 | * `RABBITMQ_VHOST`: Defines the vhost,
358 | * `RABBITMQ_USE_TLS`: Defines whether to use TLS or no.
359 |
360 | **Note that environment variables are all optional, so missing keys will be replaced by their corresponding default.**
361 |
362 | ### Manager with custom options
363 |
364 | We can input custom values for a specific property, either via the built-in builder or via direct struct initialization.
365 |
366 | #### Manager options using the builder
367 |
368 | `NewManagerOptions()` and `DefaultManagerOptions()` both return an instance of `*ManagerOptions` that can act as a
369 | builder.
370 |
371 | ```go
372 | options := gorabbit.NewManagerOptions().
373 | SetMode(gorabbit.Debug).
374 | SetCredentials("root", "password")
375 |
376 | manager := gorabbit.NewManager(options)
377 | ```
378 |
379 | > :information_source: There is a setter method for each property.
380 |
381 | #### Manager options using struct initialization
382 |
383 | `ManagerOptions` is an exported type, so it can be used directly.
384 |
385 | ```go
386 | options := gorabbit.ManagerOptions {
387 | Host: "localhost",
388 | Port: 5673,
389 | Username: "root",
390 | Password: "password",
391 | Mode: gorabbit.Debug,
392 | }
393 |
394 | manager := gorabbit.NewManager(&options)
395 | ```
396 |
397 | > :warning: Direct initialization via the struct **does not use default values on missing properties**, so be sure to
398 | > fill in every property available.
399 |
400 | ### Manager disconnection
401 |
402 | When a manager is initialized, to prevent a leak, always disconnect it when no longer needed.
403 |
404 | ```go
405 | manager := gorabbit.NewManager(gorabbit.DefaultManagerOptions())
406 | defer manager.Disconnect()
407 | ```
408 |
409 | ### Manager operations
410 |
411 | The manager offers all necessary operations to manager a RabbitMQ server.
412 |
413 | #### Exchange creation
414 |
415 | Creates an exchange with optional arguments.
416 |
417 | ```go
418 | err := manager.CreateExchange(gorabbit.ExchangeConfig{
419 | Name: "events_exchange",
420 | Type: gorabbit.ExchangeTypeTopic,
421 | Persisted: false,
422 | Args: nil,
423 | })
424 | ```
425 |
426 | #### Queue creation
427 |
428 | Creates a queue with optional arguments and bindings if declared.
429 |
430 | ```go
431 | err := manager.CreateQueue(gorabbit.QueueConfig{
432 | Name: "events_queue",
433 | Durable: false,
434 | Exclusive: false,
435 | Args: nil,
436 | Bindings: &[]gorabbit.BindingConfig{
437 | {
438 | RoutingKey: "event.foo.bar.created",
439 | Exchange: "events_exchange",
440 | },
441 | },
442 | })
443 | ```
444 |
445 | #### Binding creation
446 |
447 | Binds a queue to an exchange via a given routing key.
448 |
449 | ```go
450 | err := manager.BindExchangeToQueueViaRoutingKey("events_exchange", "events_queue", "event.foo.bar.created")
451 | ```
452 |
453 | #### Queue messages count
454 |
455 | Returns the number of messages in a queue, or an error if the queue does not exist. This method can also evaluate the
456 | existence of a queue.
457 |
458 | ```go
459 | messageCount, err := manager.GetNumberOfMessages("events_queue")
460 | ```
461 |
462 | #### Push message
463 |
464 | Pushes a single message to a given exchange.
465 |
466 | ```go
467 | err := manager.PushMessageToExchange("events_exchange", "event.foo.bar.created", "single_message_payload")
468 | ```
469 |
470 | #### Pop message
471 |
472 | Retrieves a single message from a given queue and auto acknowledges it if `autoAck` is set to true.
473 |
474 | ```go
475 | message, err := manager.PopMessageFromQueue("events_queue", true)
476 | ```
477 |
478 | #### Purge queue
479 |
480 | Deletes all messages from a given queue.
481 |
482 | ```go
483 | err := manager.PurgeQueue("events_queue")
484 | ```
485 |
486 | #### Delete queue
487 |
488 | Deletes a given queue.
489 |
490 | ```go
491 | err := manager.DeleteQueue("events_queue")
492 | ```
493 |
494 | #### Delete exchange
495 |
496 | Deletes a given exchange.
497 |
498 | ```go
499 | err := manager.DeleteExchange("events_exchange")
500 | ```
501 |
502 | #### Setup from schema definition file
503 |
504 | You can setup exchanges, queues and bindings automatically by referencing a
505 | [RabbitMQ Schema Definition](assets/definitions.example.json) JSON file.
506 |
507 | ```go
508 | err := manager.SetupFromDefinitions("/path/to/definitions.json")
509 | ```
510 |
511 | > :warning: The standard RabbitMQ definitions file contains configurations for
512 | > `users`, `vhosts` and `permissions`. Those configurations are not taken into consideration
513 | > in the `SetupFromDefinitions` method.
514 |
515 | ## Launch Local RabbitMQ Server
516 |
517 | To run a local rabbitMQ server quickly with a docker container, simply run the following command:
518 |
519 | ```bash
520 | docker run -it --rm --name rabbitmq -p 5672:5672 -p 15672:15672 rabbitmq:3-management
521 | ```
522 |
523 | It will launch a local RabbitMQ server mapped on port 5672, and the management dashboard will be mapped on
524 | port 15672 accessible on localhost:15672 with a username "guest" and password "guest".
525 |
526 | ## License
527 | **Gorabbit** is licensed under the [MIT](LICENSE).
--------------------------------------------------------------------------------
/TODO.md:
--------------------------------------------------------------------------------
1 | # TODO
2 |
3 | Must have or Nice to have features.
4 |
5 | ## Must Haves
6 |
7 | - [ ] Unregister consumer via a `UnregisterConsumer`
8 | - [x] ~~Send messages with a TTL~~
9 | - [ ] Send messages with a definable header
10 |
11 | ## Nice to have
12 |
13 | - [ ] Example projects for consumers and producers
14 |
15 | ## To Revisit/Rethink
16 |
17 | - [ ] Review the `publishingCache` in `channel.go`
18 | - [ ] Review the logger
19 | - [ ] Review the linter and redefine some rules if they are too strict
20 |
21 | ## To Fix
22 |
23 | - [ ] Concurrent consumption throwing errors when `PrefetchSize` is set
24 | - [ ] Consumer wildcard validator does not match all possibilities
--------------------------------------------------------------------------------
/assets/always-on-mechanism.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KardinalAI/gorabbit/9796c574c1eedefd10ba1ca988c3096f387f3e79/assets/always-on-mechanism.png
--------------------------------------------------------------------------------
/assets/consumer-safeguard.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KardinalAI/gorabbit/9796c574c1eedefd10ba1ca988c3096f387f3e79/assets/consumer-safeguard.png
--------------------------------------------------------------------------------
/assets/definitions.example.json:
--------------------------------------------------------------------------------
1 | {
2 | "exchanges": [
3 | {
4 | "name": "foo_exchange",
5 | "vhost": "/",
6 | "type": "topic",
7 | "durable": true,
8 | "auto_delete": false,
9 | "internal": false,
10 | "arguments": {}
11 | },
12 | {
13 | "name": "bar_exchange",
14 | "vhost": "/",
15 | "type": "topic",
16 | "durable": true,
17 | "auto_delete": false,
18 | "internal": false,
19 | "arguments": {}
20 | }
21 | ],
22 | "queues": [
23 | {
24 | "name": "foo",
25 | "vhost": "/",
26 | "durable": true,
27 | "auto_delete": false,
28 | "arguments": {}
29 | },
30 | {
31 | "name": "bar",
32 | "vhost": "/",
33 | "durable": true,
34 | "auto_delete": false,
35 | "arguments": {}
36 | }
37 | ],
38 | "bindings": [
39 | {
40 | "source": "foo_exchange",
41 | "vhost": "/",
42 | "destination": "foo",
43 | "destination_type": "queue",
44 | "routing_key": "event.foo.#",
45 | "arguments": {}
46 | },
47 | {
48 | "source": "bar_exchange",
49 | "vhost": "/",
50 | "destination": "bar",
51 | "destination_type": "queue",
52 | "routing_key": "event.bar.#",
53 | "arguments": {}
54 | }
55 | ]
56 | }
57 |
--------------------------------------------------------------------------------
/assets/gorabbit-logo-lg.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KardinalAI/gorabbit/9796c574c1eedefd10ba1ca988c3096f387f3e79/assets/gorabbit-logo-lg.jpg
--------------------------------------------------------------------------------
/assets/gorabbit-logo-md.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KardinalAI/gorabbit/9796c574c1eedefd10ba1ca988c3096f387f3e79/assets/gorabbit-logo-md.jpg
--------------------------------------------------------------------------------
/assets/gorabbit-logo-sm.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KardinalAI/gorabbit/9796c574c1eedefd10ba1ca988c3096f387f3e79/assets/gorabbit-logo-sm.jpg
--------------------------------------------------------------------------------
/assets/publishing-safeguard.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KardinalAI/gorabbit/9796c574c1eedefd10ba1ca988c3096f387f3e79/assets/publishing-safeguard.png
--------------------------------------------------------------------------------
/channel.go:
--------------------------------------------------------------------------------
1 | package gorabbit
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "time"
7 |
8 | "github.com/google/uuid"
9 |
10 | amqp "github.com/rabbitmq/amqp091-go"
11 | )
12 |
13 | // amqpChannels is a simple wrapper of an amqpChannel slice.
14 | type amqpChannels []*amqpChannel
15 |
16 | // publishingChannel loops through all channels and returns the first available publisher channel if it exists.
17 | func (a amqpChannels) publishingChannel() *amqpChannel {
18 | for _, channel := range a {
19 | if channel != nil && channel.connectionType == connectionTypePublisher {
20 | return channel
21 | }
22 | }
23 |
24 | return nil
25 | }
26 |
27 | // updateParentConnection updates every channel's parent connection.
28 | func (a amqpChannels) updateParentConnection(conn *amqp.Connection) {
29 | for _, channel := range a {
30 | channel.connection = conn
31 | }
32 | }
33 |
34 | // amqpChannel holds information about the management of the native amqp.Channel.
35 | type amqpChannel struct {
36 | // ctx is the parent context and acts as a safeguard.
37 | ctx context.Context
38 |
39 | // connection is the native amqp.Connection.
40 | connection *amqp.Connection
41 |
42 | // channel is the native amqp.Channel.
43 | channel *amqp.Channel
44 |
45 | // keepAlive is the flag that will define whether active guards and re-connections are enabled or not.
46 | keepAlive bool
47 |
48 | // retryDelay defines the delay to wait before re-connecting if the channel was closed and the keepAlive flag is set to true.
49 | retryDelay time.Duration
50 |
51 | // consumer is the MessageConsumer that holds all necessary information for the consumption of messages.
52 | consumer *MessageConsumer
53 |
54 | // consumptionCtx holds the consumption context.
55 | consumptionCtx context.Context
56 |
57 | // consumptionCancel is the cancel function of the consumptionCtx.
58 | consumptionCancel context.CancelFunc
59 |
60 | // consumptionHealth manages the status of all active consumptions.
61 | consumptionHealth consumptionHealth
62 |
63 | // publishingCache manages the caching of unpublished messages due to a connection error.
64 | publishingCache *ttlMap[string, mqttPublishing]
65 |
66 | // maxRetry defines the retry header for each message.
67 | maxRetry uint
68 |
69 | // closed is an inner property that switches to true if the channel was explicitly closed.
70 | closed bool
71 |
72 | // logger logs events.
73 | logger logger
74 |
75 | // releaseLogger forces logs not matter the mode. It is used to log important things.
76 | releaseLogger logger
77 |
78 | // connectionType defines the connectionType.
79 | connectionType connectionType
80 |
81 | // marshaller defines the marshalling method used to encode messages.
82 | marshaller Marshaller
83 | }
84 |
85 | // newConsumerChannel instantiates a new consumerChannel and amqpChannel for method inheritance.
86 | // - ctx is the parent context.
87 | // - connection is the parent amqp.Connection.
88 | // - keepAlive will keep the channel alive if true.
89 | // - retryDelay defines the delay between each retry, if the keepAlive flag is set to true.
90 | // - consumer is the MessageConsumer that will hold consumption information.
91 | // - maxRetry is the retry header for each message.
92 | // - logger is the parent logger.
93 | // - marshaller is the Marshaller used for encoding messages.
94 | func newConsumerChannel(
95 | ctx context.Context,
96 | connection *amqp.Connection,
97 | keepAlive bool,
98 | retryDelay time.Duration,
99 | consumer *MessageConsumer,
100 | logger logger,
101 | marshaller Marshaller,
102 | ) *amqpChannel {
103 | channel := &amqpChannel{
104 | ctx: ctx,
105 | connection: connection,
106 | keepAlive: keepAlive,
107 | retryDelay: retryDelay,
108 | logger: inheritLogger(logger, map[string]interface{}{
109 | "context": "channel",
110 | "type": connectionTypeConsumer,
111 | "consumer": consumer.Name,
112 | "queue": consumer.Queue,
113 | }),
114 | releaseLogger: &stdLogger{
115 | logger: newLogrus(),
116 | identifier: libraryName,
117 | logFields: map[string]interface{}{
118 | "context": "channel",
119 | "type": connectionTypeConsumer,
120 | "consumer": consumer.Name,
121 | "queue": consumer.Queue,
122 | },
123 | },
124 | connectionType: connectionTypeConsumer,
125 | consumptionHealth: make(consumptionHealth),
126 | consumer: consumer,
127 | marshaller: marshaller,
128 | }
129 |
130 | // We open an initial channel.
131 | err := channel.open()
132 |
133 | // If the channel failed to open and the keepAlive flag is set to true, we want to retry until success.
134 | if err != nil && keepAlive {
135 | go channel.retry()
136 | }
137 |
138 | return channel
139 | }
140 |
141 | // newPublishingChannel instantiates a new publishingChannel and amqpChannel for method inheritance.
142 | // - ctx is the parent context.
143 | // - connection is the parent amqp.Connection.
144 | // - keepAlive will keep the channel alive if true.
145 | // - retryDelay defines the delay between each retry, if the keepAlive flag is set to true.
146 | // - maxRetry defines the maximum number of times a message can be retried if its consumption failed.
147 | // - publishingCacheSize is the maximum cache size of failed publishing.
148 | // - publishingCacheTTL defines the time to live for each failed publishing that was put in cache.
149 | // - logger is the parent logger.
150 | // - marshaller is the Marshaller used for encoding messages.
151 | func newPublishingChannel(
152 | ctx context.Context,
153 | connection *amqp.Connection,
154 | keepAlive bool,
155 | retryDelay time.Duration,
156 | maxRetry uint,
157 | publishingCacheSize uint64,
158 | publishingCacheTTL time.Duration,
159 | logger logger,
160 | marshaller Marshaller,
161 | ) *amqpChannel {
162 | channel := &amqpChannel{
163 | ctx: ctx,
164 | connection: connection,
165 | keepAlive: keepAlive,
166 | retryDelay: retryDelay,
167 | logger: inheritLogger(logger, map[string]interface{}{
168 | "context": "channel",
169 | "type": connectionTypePublisher,
170 | }),
171 | releaseLogger: &stdLogger{
172 | logger: newLogrus(),
173 | identifier: libraryName,
174 | logFields: map[string]interface{}{
175 | "context": "channel",
176 | "type": connectionTypePublisher,
177 | },
178 | },
179 | connectionType: connectionTypePublisher,
180 | publishingCache: newTTLMap[string, mqttPublishing](publishingCacheSize, publishingCacheTTL),
181 | maxRetry: maxRetry,
182 | marshaller: marshaller,
183 | }
184 |
185 | // We open an initial channel.
186 | err := channel.open()
187 |
188 | // If the channel failed to open and the keepAlive flag is set to true, we want to retry until success.
189 | if err != nil && keepAlive {
190 | go channel.retry()
191 | }
192 |
193 | return channel
194 | }
195 |
196 | // open opens a new amqp.Channel from the parent connection.
197 | func (c *amqpChannel) open() error {
198 | // If the channel is nil or closed we return an error.
199 | if c.connection == nil || c.connection.IsClosed() {
200 | err := errConnectionClosed
201 |
202 | c.logger.Error(err, "Could not open channel")
203 |
204 | return err
205 | }
206 |
207 | // We request a channel from the parent connection.
208 | channel, err := c.connection.Channel()
209 | if err != nil {
210 | c.logger.Error(err, "Could not open channel")
211 |
212 | return err
213 | }
214 |
215 | c.channel = channel
216 |
217 | c.logger.Info("Channel opened")
218 |
219 | c.onChannelOpened()
220 |
221 | // If the keepAlive flag is set to true, we activate a new guard.
222 | if c.keepAlive {
223 | go c.guard()
224 | }
225 |
226 | return nil
227 | }
228 |
229 | // reconnect will indefinitely call the open method until a connection is successfully established or the context is canceled.
230 | func (c *amqpChannel) retry() {
231 | c.logger.Debug("Retry launched")
232 |
233 | for {
234 | select {
235 | case <-c.ctx.Done():
236 | c.logger.Debug("Retry stopped by the context")
237 |
238 | // If the context was canceled, we break out of the method.
239 | return
240 | default:
241 | // Wait for the retryDelay.
242 | time.Sleep(c.retryDelay)
243 |
244 | // If there is no channel or the current channel is closed, we open a new channel.
245 | if !c.ready() {
246 | err := c.open()
247 | // If the operation succeeds, we break the loop.
248 | if err == nil {
249 | c.logger.Debug("Retry successful")
250 |
251 | return
252 | }
253 |
254 | c.logger.Error(err, "Could not open new channel during retry")
255 | } else {
256 | // If the channel exists and is active, we break out.
257 | return
258 | }
259 | }
260 | }
261 | }
262 |
263 | // guard is a channel safeguard that listens to channel close events and re-launches the channel.
264 | func (c *amqpChannel) guard() {
265 | c.logger.Debug("Guard launched")
266 |
267 | for {
268 | select {
269 | case <-c.ctx.Done():
270 | c.logger.Debug("Guard stopped by the context")
271 |
272 | // If the context was canceled, we break out of the method.
273 | return
274 | case err, ok := <-c.channel.NotifyClose(make(chan *amqp.Error)):
275 | if !ok {
276 | return
277 | }
278 |
279 | if err != nil {
280 | c.logger.Warn("Channel lost", logField{Key: "reason", Value: err.Reason}, logField{Key: "code", Value: err.Code})
281 | }
282 |
283 | // If the channel was explicitly closed, we do not want to retry.
284 | if c.closed {
285 | return
286 | }
287 |
288 | c.onChannelClosed()
289 |
290 | go c.retry()
291 |
292 | return
293 | }
294 | }
295 | }
296 |
297 | // close the channel only if it is ready.
298 | func (c *amqpChannel) close() error {
299 | if c.ready() {
300 | err := c.channel.Close()
301 | if err != nil {
302 | c.logger.Error(err, "Could not close channel")
303 |
304 | return err
305 | }
306 | }
307 |
308 | c.closed = true
309 |
310 | return nil
311 | }
312 |
313 | // ready returns true if the channel exists and is not closed.
314 | func (c *amqpChannel) ready() bool {
315 | return c.channel != nil && !c.channel.IsClosed()
316 | }
317 |
318 | // healthy returns true if the channel exists and is not closed.
319 | func (c *amqpChannel) healthy() bool {
320 | if c.connectionType == connectionTypeConsumer {
321 | return c.ready() && c.consumptionHealth.IsHealthy()
322 | }
323 |
324 | return c.ready()
325 | }
326 |
327 | // onChannelOpened is called when a channel is successfully opened.
328 | func (c *amqpChannel) onChannelOpened() {
329 | if c.connectionType == connectionTypeConsumer {
330 | // We re-instantiate the consumptionContext and consumptionCancel.
331 | c.consumptionCtx, c.consumptionCancel = context.WithCancel(c.ctx)
332 |
333 | // This is just a safeguard.
334 | if c.consumer != nil {
335 | c.logger.Info("Launching consumer", logField{Key: "event", Value: "onChannelOpened"})
336 |
337 | // If the consumer is present we want to start consuming.
338 | go c.consume()
339 | }
340 | } else {
341 | // If the publishing cache is empty, nothing to do here.
342 | if c.publishingCache == nil || c.publishingCache.Len() == 0 {
343 | return
344 | }
345 |
346 | c.logger.Info("Emptying publishing cache", logField{Key: "event", Value: "onChannelOpened"})
347 |
348 | // For each cached unsuccessful message, we try publishing it again.
349 | c.publishingCache.ForEach(func(key string, msg mqttPublishing) {
350 | _ = c.channel.PublishWithContext(c.ctx, msg.Exchange, msg.RoutingKey, msg.Mandatory, msg.Immediate, msg.Msg)
351 |
352 | c.publishingCache.Delete(key)
353 | })
354 | }
355 | }
356 |
357 | // onChannelClosed is called when a channel is closed.
358 | func (c *amqpChannel) onChannelClosed() {
359 | if c.connectionType == connectionTypeConsumer {
360 | c.logger.Info("Canceling consumptions", logField{Key: "event", Value: "onChannelClosed"})
361 |
362 | // We cancel the consumptionCtx.
363 | c.consumptionCancel()
364 | }
365 | }
366 |
367 | // getID returns a unique identifier for the channel.
368 | func (c *amqpChannel) getID() string {
369 | if c.consumer == nil {
370 | return fmt.Sprintf("publisher_%s", uuid.NewString())
371 | }
372 |
373 | return fmt.Sprintf("%s_%s", c.consumer.Name, uuid.NewString())
374 | }
375 |
376 | // consume handles the consumption mechanism.
377 | func (c *amqpChannel) consume() {
378 | // TODO(Alex): Check if this can actually happen
379 | // If the channel is not ready, we cannot consume.
380 | if !c.ready() {
381 | c.logger.Warn("Channel not ready, cannot launch consumer")
382 |
383 | return
384 | }
385 |
386 | // TODO(Alex): Double check why setting a prefetch size greater than 0 causes an error
387 | // Set the QOS, which defines how many messages can be processed at the same time.
388 | err := c.channel.Qos(c.consumer.PrefetchCount, c.consumer.PrefetchSize, false)
389 | if err != nil {
390 | c.logger.Error(err, "Could not define QOS for consumer")
391 |
392 | return
393 | }
394 |
395 | deliveries, err := c.channel.Consume(c.consumer.Queue, c.getID(), c.consumer.AutoAck, false, false, false, nil)
396 |
397 | c.consumptionHealth.AddSubscription(c.consumer.Queue, err)
398 |
399 | if err != nil {
400 | c.logger.Error(err, "Could not consume messages")
401 |
402 | // If the queue does not exist yet, we want to force a release log with a warning for better visibility.
403 | if isErrorNotFound(err) {
404 | c.releaseLogger.Warn("Queue does not exist", logField{Key: "queue", Value: c.consumer.Queue})
405 | }
406 |
407 | return
408 | }
409 |
410 | for {
411 | select {
412 | case <-c.consumptionCtx.Done():
413 | return
414 | case delivery := <-deliveries:
415 | // When a queue is deleted midway, a delivery with no tag or ID is received.
416 | if delivery.DeliveryTag == 0 && delivery.MessageId == "" {
417 | c.logger.Warn("Queue has been deleted, stopping consumer")
418 |
419 | return
420 | }
421 |
422 | // We copy the delivery for the concurrent process of it (otherwise we may process the wrong delivery
423 | // if a new one is consumed while the previous is still being processed).
424 | loopDelivery := delivery
425 |
426 | if c.consumer.ConcurrentProcess {
427 | // We process the message asynchronously if the concurrency is set to true.
428 | go c.processDelivery(&loopDelivery)
429 | } else {
430 | // Otherwise, we process the message synchronously.
431 | c.processDelivery(&loopDelivery)
432 | }
433 | }
434 | }
435 | }
436 |
437 | // processDelivery is the logic that defines what to do with a processed delivery and its error.
438 | func (c *amqpChannel) processDelivery(delivery *amqp.Delivery) {
439 | handler := c.consumer.Handlers.FindFunc(delivery.RoutingKey)
440 |
441 | // If the handler doesn't exist for the received delivery, we negative acknowledge it without requeue.
442 | if handler == nil {
443 | c.logger.Debug("No handler found", logField{Key: "routingKey", Value: delivery.RoutingKey})
444 |
445 | // If the consumer is not set to auto acknowledge the delivery, we negative acknowledge it without requeue.
446 | if !c.consumer.AutoAck {
447 | _ = delivery.Nack(false, false)
448 | }
449 |
450 | return
451 | }
452 |
453 | err := handler(delivery.Body)
454 |
455 | // If the consumer has the autoAck flag activated, we want to retry the delivery in case of an error.
456 | if c.consumer.AutoAck {
457 | if err != nil {
458 | go c.retryDelivery(delivery, true)
459 | }
460 |
461 | return
462 | }
463 |
464 | // If there is no error, we can simply acknowledge the delivery.
465 | if err == nil {
466 | c.logger.Debug("Delivery successfully processed", logField{Key: "messageID", Value: delivery.MessageId})
467 |
468 | _ = delivery.Ack(false)
469 |
470 | return
471 | }
472 |
473 | // Otherwise we retry the delivery.
474 | go c.retryDelivery(delivery, false)
475 | }
476 |
477 | // retryDelivery processes a delivery retry based on its redelivery header.
478 | //
479 | //nolint:gocognit // We can allow the current complexity for now but we should revisit it later.
480 | func (c *amqpChannel) retryDelivery(delivery *amqp.Delivery, alreadyAcknowledged bool) {
481 | c.logger.Debug("Delivery retry launched")
482 |
483 | for {
484 | select {
485 | case <-c.consumptionCtx.Done():
486 | c.logger.Debug("Delivery retry stopped by the consumption context")
487 |
488 | return
489 | default:
490 | // We wait for the retry delay before retrying a message.
491 | time.Sleep(c.retryDelay)
492 |
493 | // We first extract the xDeathCountHeader.
494 | maxRetryHeader, exists := delivery.Headers[xDeathCountHeader]
495 |
496 | // If the header doesn't exist.
497 | if !exists {
498 | c.logger.Debug("Delivery retry invalid")
499 |
500 | // We negative acknowledge the delivery without requeue if the autoAck flag is set to false.
501 | if !alreadyAcknowledged {
502 | _ = delivery.Nack(false, false)
503 | }
504 |
505 | return
506 | }
507 |
508 | // We then cast the value as an int32.
509 | retriesCount, ok := maxRetryHeader.(int32)
510 |
511 | // If the casting fails,we negative acknowledge the delivery without requeue if the autoAck flag is set to false.
512 | if !ok {
513 | c.logger.Debug("Delivery retry invalid")
514 |
515 | if !alreadyAcknowledged {
516 | _ = delivery.Nack(false, false)
517 | }
518 |
519 | return
520 | }
521 |
522 | // If the retries count is still greater than 0, we re-publish the delivery with a decremented xDeathCountHeader.
523 | if retriesCount > 0 {
524 | c.logger.Debug("Retrying delivery", logField{Key: "retriesLeft", Value: retriesCount - 1})
525 |
526 | // We first negative acknowledge the existing delivery to remove it from queue if the autoAck flag is set to false.
527 | if !alreadyAcknowledged {
528 | _ = delivery.Nack(false, false)
529 | }
530 |
531 | // We create a new publishing which is a copy of the old one but with a decremented xDeathCountHeader.
532 | newPublishing := amqp.Publishing{
533 | ContentType: c.marshaller.ContentType(),
534 | Body: delivery.Body,
535 | Type: delivery.RoutingKey,
536 | Priority: delivery.Priority,
537 | DeliveryMode: delivery.DeliveryMode,
538 | MessageId: delivery.MessageId,
539 | Timestamp: delivery.Timestamp,
540 | Headers: map[string]interface{}{
541 | xDeathCountHeader: int(retriesCount - 1),
542 | },
543 | }
544 |
545 | // We work on a best-effort basis. We try to re-publish the delivery, but we do nothing if it fails.
546 | _ = c.channel.PublishWithContext(c.ctx, delivery.Exchange, delivery.RoutingKey, false, false, newPublishing)
547 |
548 | return
549 | }
550 |
551 | c.logger.Debug("Cannot retry delivery, max retries reached")
552 |
553 | // Otherwise, we negative acknowledge the delivery without requeue if the autoAck flag is set to false.
554 | if !alreadyAcknowledged {
555 | _ = delivery.Nack(false, false)
556 | }
557 |
558 | return
559 | }
560 | }
561 | }
562 |
563 | // publish will publish a message with the given configuration.
564 | func (c *amqpChannel) publish(exchange string, routingKey string, payload []byte, options *PublishingOptions) error {
565 | publishing := &amqp.Publishing{
566 | ContentType: c.marshaller.ContentType(),
567 | Body: payload,
568 | Type: routingKey,
569 | Priority: PriorityMedium.Uint8(),
570 | DeliveryMode: Persistent.Uint8(),
571 | MessageId: uuid.NewString(),
572 | Timestamp: time.Now(),
573 | Headers: map[string]interface{}{
574 | xDeathCountHeader: int(c.maxRetry),
575 | },
576 | }
577 |
578 | // If options are declared, we add the option.
579 | if options != nil {
580 | publishing.Priority = options.priority()
581 | publishing.DeliveryMode = options.mode()
582 | publishing.Expiration = options.ttl()
583 | }
584 |
585 | // If the channel is not ready, we cannot publish, but we send the message to cache if the keepAlive flag is set to true.
586 | if !c.ready() {
587 | err := errChannelClosed
588 |
589 | if c.keepAlive {
590 | c.logger.Error(err, "Could not publish message, sending to cache")
591 |
592 | msg := mqttPublishing{
593 | Exchange: exchange,
594 | RoutingKey: routingKey,
595 | Mandatory: false,
596 | Immediate: false,
597 | Msg: *publishing,
598 | }
599 |
600 | c.publishingCache.Put(msg.HashCode(), msg)
601 | } else {
602 | c.logger.Error(err, "Could not publish message")
603 | }
604 |
605 | return err
606 | }
607 |
608 | err := c.channel.PublishWithContext(c.ctx, exchange, routingKey, false, false, *publishing)
609 |
610 | // If the message could not be sent we return an error without caching it.
611 | if err != nil {
612 | c.logger.Error(err, "Could not publish message")
613 |
614 | // If the exchange does not exist yet, we want to force a release log with a warning for better visibility.
615 | if isErrorNotFound(err) {
616 | c.releaseLogger.Warn(
617 | "The MQTT message was not sent, exchange does not exist",
618 | logField{Key: "exchange", Value: exchange},
619 | logField{Key: "routingKey", Value: routingKey},
620 | )
621 | }
622 |
623 | return err
624 | }
625 |
626 | c.logger.Debug("Message successfully sent", logField{Key: "messageID", Value: publishing.MessageId})
627 |
628 | return nil
629 | }
630 |
--------------------------------------------------------------------------------
/client.go:
--------------------------------------------------------------------------------
1 | package gorabbit
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "os"
7 | )
8 |
9 | // MQTTClient is a simple MQTT interface that offers basic client operations such as:
10 | // - Publishing
11 | // - Consuming
12 | // - Disconnecting
13 | // - Ready and health checks
14 | type MQTTClient interface {
15 | // Disconnect launches the disconnection process.
16 | // This operation disables to client permanently.
17 | Disconnect() error
18 |
19 | // Publish will send the desired payload through the selected channel.
20 | // - exchange is the name of the exchange targeted for event publishing.
21 | // - routingKey is the route that the exchange will use to forward the message.
22 | // - payload is the object you want to send as a byte array.
23 | // Returns an error if the connection to the RabbitMQ server is down.
24 | Publish(exchange, routingKey string, payload interface{}) error
25 |
26 | // PublishWithOptions will send the desired payload through the selected channel.
27 | // - exchange is the name of the exchange targeted for event publishing.
28 | // - routingKey is the route that the exchange will use to forward the message.
29 | // - payload is the object you want to send as a byte array.
30 | // Optionally you can add publishingOptions for extra customization.
31 | // Returns an error if the connection to the RabbitMQ server is down.
32 | PublishWithOptions(exchange, routingKey string, payload interface{}, options *PublishingOptions) error
33 |
34 | // RegisterConsumer will register a MessageConsumer for internal queue subscription and message processing.
35 | // The MessageConsumer will hold a list of MQTTMessageHandlers to internalize message processing.
36 | // Based on the return of error of each handler, the process of acknowledgment, rejection and retry of messages is
37 | // fully handled internally.
38 | // Furthermore, connection lost and channel errors are also internally handled by the connectionManager that will keep consumers
39 | // alive if and when necessary.
40 | RegisterConsumer(consumer MessageConsumer) error
41 |
42 | // IsReady returns true if the client is fully operational and connected to the RabbitMQ.
43 | IsReady() bool
44 |
45 | // IsHealthy returns true if the client is ready (IsReady) and all channels are operating successfully.
46 | IsHealthy() bool
47 |
48 | // GetHost returns the host used to initialize the client.
49 | GetHost() string
50 |
51 | // GetPort returns the port used to initialize the client.
52 | GetPort() uint
53 |
54 | // GetUsername returns the username used to initialize the client.
55 | GetUsername() string
56 |
57 | // GetVhost returns the vhost used to initialize the client.
58 | GetVhost() string
59 |
60 | // IsDisabled returns whether the client is disabled or not.
61 | IsDisabled() bool
62 | }
63 |
64 | type mqttClient struct {
65 | // Host is the RabbitMQ server host name.
66 | Host string
67 |
68 | // Port is the RabbitMQ server port number.
69 | Port uint
70 |
71 | // Username is the RabbitMQ server allowed username.
72 | Username string
73 |
74 | // Password is the RabbitMQ server allowed password.
75 | Password string
76 |
77 | // Vhost is used for CloudAMQP connections to set the specific vhost.
78 | Vhost string
79 |
80 | // logger defines the logger used, depending on the mode set.
81 | logger logger
82 |
83 | // disabled completely disables the client if true.
84 | disabled bool
85 |
86 | // connectionManager manages the connection and channel logic and high-level logic
87 | // such as keep alive mechanism and health check.
88 | connectionManager *connectionManager
89 |
90 | // ctx holds the global context used for the client.
91 | ctx context.Context
92 |
93 | // cancel is the cancelFunc for the ctx.
94 | cancel context.CancelFunc
95 | }
96 |
97 | // NewClient will instantiate a new MQTTClient.
98 | // If options is set to nil, the DefaultClientOptions will be used.
99 | func NewClient(options *ClientOptions) MQTTClient {
100 | // If no options is passed, we use the DefaultClientOptions.
101 | if options == nil {
102 | options = DefaultClientOptions()
103 | }
104 |
105 | return newClientFromOptions(options)
106 | }
107 |
108 | // NewClientFromEnv will instantiate a new MQTTClient from environment variables.
109 | func NewClientFromEnv() MQTTClient {
110 | options := NewClientOptionsFromEnv()
111 |
112 | return newClientFromOptions(options)
113 | }
114 |
115 | func newClientFromOptions(options *ClientOptions) MQTTClient {
116 | client := &mqttClient{
117 | Host: options.Host,
118 | Port: options.Port,
119 | Username: options.Username,
120 | Password: options.Password,
121 | Vhost: options.Vhost,
122 | logger: &noLogger{},
123 | }
124 |
125 | // We check if the disabled flag is present, which will completely disable the MQTTClient.
126 | if disabledOverride := os.Getenv("GORABBIT_DISABLED"); disabledOverride != "" {
127 | switch disabledOverride {
128 | case "1", "true":
129 | client.disabled = true
130 |
131 | return client
132 | }
133 | }
134 |
135 | // We check if the mode was overwritten with the environment variable "GORABBIT_MODE".
136 | if modeOverride := os.Getenv("GORABBIT_MODE"); isValidMode(modeOverride) {
137 | // We override the mode only if it is valid
138 | options.Mode = modeOverride
139 | }
140 |
141 | if options.Mode == Debug {
142 | // If the mode is Debug, we want to actually log important events.
143 | client.logger = newStdLogger()
144 | }
145 |
146 | client.ctx, client.cancel = context.WithCancel(context.Background())
147 |
148 | protocol := defaultProtocol
149 |
150 | if options.UseTLS {
151 | protocol = securedProtocol
152 | }
153 |
154 | if options.Marshaller == nil {
155 | options.Marshaller = defaultMarshaller
156 | }
157 |
158 | dialURL := fmt.Sprintf("%s://%s:%s@%s:%d/%s", protocol, client.Username, client.Password, client.Host, client.Port, client.Vhost)
159 |
160 | client.connectionManager = newConnectionManager(
161 | client.ctx,
162 | dialURL,
163 | options.ConnectionName,
164 | options.KeepAlive,
165 | options.RetryDelay,
166 | options.MaxRetry,
167 | options.PublishingCacheSize,
168 | options.PublishingCacheTTL,
169 | client.logger,
170 | options.Marshaller,
171 | )
172 |
173 | return client
174 | }
175 |
176 | func (client *mqttClient) Publish(exchange string, routingKey string, payload interface{}) error {
177 | return client.PublishWithOptions(exchange, routingKey, payload, nil)
178 | }
179 |
180 | func (client *mqttClient) PublishWithOptions(exchange string, routingKey string, payload interface{}, options *PublishingOptions) error {
181 | // client is disabled, so we do nothing and return no error.
182 | if client.disabled {
183 | return nil
184 | }
185 |
186 | return client.connectionManager.publish(exchange, routingKey, payload, options)
187 | }
188 |
189 | func (client *mqttClient) RegisterConsumer(consumer MessageConsumer) error {
190 | // client is disabled, so we do nothing and return no error.
191 | if client.disabled {
192 | return nil
193 | }
194 |
195 | return client.connectionManager.registerConsumer(consumer)
196 | }
197 |
198 | func (client *mqttClient) Disconnect() error {
199 | // client is disabled, so we do nothing and return no error.
200 | if client.disabled {
201 | return nil
202 | }
203 |
204 | err := client.connectionManager.close()
205 |
206 | if err != nil {
207 | return err
208 | }
209 |
210 | // cancel the context to stop all reconnection goroutines.
211 | client.cancel()
212 |
213 | // disable the client to avoid trying to launch new operations.
214 | client.disabled = true
215 |
216 | return nil
217 | }
218 |
219 | func (client *mqttClient) IsReady() bool {
220 | // client is disabled, so we do nothing and return true.
221 | if client.disabled {
222 | return true
223 | }
224 |
225 | return client.connectionManager.isReady()
226 | }
227 |
228 | func (client *mqttClient) IsHealthy() bool {
229 | // client is disabled, so we do nothing and return true.
230 | if client.disabled {
231 | return true
232 | }
233 |
234 | return client.connectionManager.isHealthy()
235 | }
236 |
237 | func (client *mqttClient) GetHost() string {
238 | return client.Host
239 | }
240 |
241 | func (client *mqttClient) GetPort() uint {
242 | return client.Port
243 | }
244 |
245 | func (client *mqttClient) GetUsername() string {
246 | return client.Username
247 | }
248 |
249 | func (client *mqttClient) GetVhost() string {
250 | return client.Vhost
251 | }
252 |
253 | func (client *mqttClient) IsDisabled() bool {
254 | return client.disabled
255 | }
256 |
--------------------------------------------------------------------------------
/client_options.go:
--------------------------------------------------------------------------------
1 | package gorabbit
2 |
3 | import (
4 | "time"
5 |
6 | "github.com/Netflix/go-env"
7 | )
8 |
9 | // ClientOptions holds all necessary properties to launch a successful connection with an MQTTClient.
10 | type ClientOptions struct {
11 | // Host is the RabbitMQ server host name.
12 | Host string
13 |
14 | // Port is the RabbitMQ server port number.
15 | Port uint
16 |
17 | // Username is the RabbitMQ server allowed username.
18 | Username string
19 |
20 | // Password is the RabbitMQ server allowed password.
21 | Password string
22 |
23 | // Vhost is used for CloudAMQP connections to set the specific vhost.
24 | Vhost string
25 |
26 | // UseTLS defines whether we use amqp or amqps protocol.
27 | UseTLS bool
28 |
29 | // ConnectionName is the client connection name passed on to the RabbitMQ server.
30 | ConnectionName string
31 |
32 | // KeepAlive will determine whether the re-connection and retry mechanisms should be triggered.
33 | KeepAlive bool
34 |
35 | // RetryDelay will define the delay for the re-connection and retry mechanism.
36 | RetryDelay time.Duration
37 |
38 | // MaxRetry will define the number of retries when an amqpMessage could not be processed.
39 | MaxRetry uint
40 |
41 | // PublishingCacheTTL defines the time to live for each publishing cache item.
42 | PublishingCacheTTL time.Duration
43 |
44 | // PublishingCacheSize defines the max length of the publishing cache.
45 | PublishingCacheSize uint64
46 |
47 | // Mode will specify whether logs are enabled or not.
48 | Mode string
49 |
50 | // Marshaller defines the content type used for messages and how they're marshalled (default: JSON).
51 | Marshaller Marshaller
52 | }
53 |
54 | // DefaultClientOptions will return a ClientOptions with default values.
55 | func DefaultClientOptions() *ClientOptions {
56 | return &ClientOptions{
57 | Host: defaultHost,
58 | Port: defaultPort,
59 | Username: defaultUsername,
60 | Password: defaultPassword,
61 | Vhost: defaultVhost,
62 | UseTLS: defaultUseTLS,
63 | KeepAlive: defaultKeepAlive,
64 | RetryDelay: defaultRetryDelay,
65 | MaxRetry: defaultMaxRetry,
66 | PublishingCacheTTL: defaultPublishingCacheTTL,
67 | PublishingCacheSize: defaultPublishingCacheSize,
68 | Mode: defaultMode,
69 | Marshaller: defaultMarshaller,
70 | }
71 | }
72 |
73 | // NewClientOptions is the exported builder for a ClientOptions and will offer setter methods for an easy construction.
74 | // Any non-assigned field will be set to default through DefaultClientOptions.
75 | func NewClientOptions() *ClientOptions {
76 | return DefaultClientOptions()
77 | }
78 |
79 | // NewClientOptionsFromEnv will generate a ClientOptions from environment variables. Empty values will be taken as default
80 | // through the DefaultClientOptions.
81 | func NewClientOptionsFromEnv() *ClientOptions {
82 | defaultOpts := DefaultClientOptions()
83 |
84 | fromEnv := new(RabbitMQEnvs)
85 |
86 | _, err := env.UnmarshalFromEnviron(fromEnv)
87 | if err != nil {
88 | return defaultOpts
89 | }
90 |
91 | if fromEnv.Host != "" {
92 | defaultOpts.Host = fromEnv.Host
93 | }
94 |
95 | if fromEnv.Port > 0 {
96 | defaultOpts.Port = fromEnv.Port
97 | }
98 |
99 | if fromEnv.Username != "" {
100 | defaultOpts.Username = fromEnv.Username
101 | }
102 |
103 | if fromEnv.Password != "" {
104 | defaultOpts.Password = fromEnv.Password
105 | }
106 |
107 | if fromEnv.Vhost != "" {
108 | defaultOpts.Vhost = fromEnv.Vhost
109 | }
110 |
111 | defaultOpts.UseTLS = fromEnv.UseTLS
112 | defaultOpts.ConnectionName = fromEnv.ConnectionName
113 |
114 | return defaultOpts
115 | }
116 |
117 | // SetHost will assign the Host.
118 | func (c *ClientOptions) SetHost(host string) *ClientOptions {
119 | c.Host = host
120 |
121 | return c
122 | }
123 |
124 | // SetPort will assign the Port.
125 | func (c *ClientOptions) SetPort(port uint) *ClientOptions {
126 | c.Port = port
127 |
128 | return c
129 | }
130 |
131 | // SetCredentials will assign the Username and Password.
132 | func (c *ClientOptions) SetCredentials(username, password string) *ClientOptions {
133 | c.Username = username
134 | c.Password = password
135 |
136 | return c
137 | }
138 |
139 | // SetVhost will assign the Vhost.
140 | func (c *ClientOptions) SetVhost(vhost string) *ClientOptions {
141 | c.Vhost = vhost
142 |
143 | return c
144 | }
145 |
146 | // SetUseTLS will assign the UseTLS status.
147 | func (c *ClientOptions) SetUseTLS(use bool) *ClientOptions {
148 | c.UseTLS = use
149 |
150 | return c
151 | }
152 |
153 | // SetConnectionName will assign the ConnectionName.
154 | func (c *ClientOptions) SetConnectionName(connectionName string) *ClientOptions {
155 | c.ConnectionName = connectionName
156 |
157 | return c
158 | }
159 |
160 | // SetKeepAlive will assign the KeepAlive status.
161 | func (c *ClientOptions) SetKeepAlive(keepAlive bool) *ClientOptions {
162 | c.KeepAlive = keepAlive
163 |
164 | return c
165 | }
166 |
167 | // SetRetryDelay will assign the retry delay.
168 | func (c *ClientOptions) SetRetryDelay(delay time.Duration) *ClientOptions {
169 | c.RetryDelay = delay
170 |
171 | return c
172 | }
173 |
174 | // SetMaxRetry will assign the max retry count.
175 | func (c *ClientOptions) SetMaxRetry(retry uint) *ClientOptions {
176 | c.MaxRetry = retry
177 |
178 | return c
179 | }
180 |
181 | // SetPublishingCacheTTL will assign the publishing cache item TTL.
182 | func (c *ClientOptions) SetPublishingCacheTTL(ttl time.Duration) *ClientOptions {
183 | c.PublishingCacheTTL = ttl
184 |
185 | return c
186 | }
187 |
188 | // SetPublishingCacheSize will assign the publishing cache max length.
189 | func (c *ClientOptions) SetPublishingCacheSize(size uint64) *ClientOptions {
190 | c.PublishingCacheSize = size
191 |
192 | return c
193 | }
194 |
195 | // SetMode will assign the Mode if valid.
196 | func (c *ClientOptions) SetMode(mode string) *ClientOptions {
197 | if isValidMode(mode) {
198 | c.Mode = mode
199 | }
200 |
201 | return c
202 | }
203 |
204 | // SetMarshaller will assign the Marshaller.
205 | func (c *ClientOptions) SetMarshaller(marshaller Marshaller) *ClientOptions {
206 | c.Marshaller = marshaller
207 |
208 | return c
209 | }
210 |
--------------------------------------------------------------------------------
/connection.go:
--------------------------------------------------------------------------------
1 | package gorabbit
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "net/url"
7 | "time"
8 |
9 | "github.com/google/uuid"
10 | amqp "github.com/rabbitmq/amqp091-go"
11 | )
12 |
13 | // amqpConnection holds information about the management of the native amqp.Connection.
14 | type amqpConnection struct {
15 | // ctx is the parent context and acts as a safeguard.
16 | ctx context.Context
17 |
18 | // connection is the native amqp.Connection.
19 | connection *amqp.Connection
20 |
21 | // uri represents the connection string to the RabbitMQ server.
22 | uri string
23 |
24 | // connectionName is the client connection name passed on to the RabbitMQ server.
25 | connectionName string
26 |
27 | // keepAlive is the flag that will define whether active guards and re-connections are enabled or not.
28 | keepAlive bool
29 |
30 | // retryDelay defines the delay to wait before re-connecting if we lose connection and the keepAlive flag is set to true.
31 | retryDelay time.Duration
32 |
33 | // closed is an inner property that switches to true if the connection was explicitly closed.
34 | closed bool
35 |
36 | // channels holds a list of active amqpChannel
37 | channels amqpChannels
38 |
39 | // maxRetry defines the number of retries when publishing a message.
40 | maxRetry uint
41 |
42 | // publishingCacheSize defines the maximum length of cached failed publishing.
43 | publishingCacheSize uint64
44 |
45 | // publishingCacheTTL defines the time to live for a cached failed publishing.
46 | publishingCacheTTL time.Duration
47 |
48 | // logger logs events.
49 | logger logger
50 |
51 | // connectionType defines the connectionType.
52 | connectionType connectionType
53 |
54 | // marshaller defines the marshalling method used to encode messages.
55 | marshaller Marshaller
56 | }
57 |
58 | // newConsumerConnection initializes a new consumer amqpConnection with given arguments.
59 | // - ctx is the parent context.
60 | // - uri is the connection string.
61 | // - connectionName is the connection name.
62 | // - keepAlive will keep the connection alive if true.
63 | // - retryDelay defines the delay between each re-connection, if the keepAlive flag is set to true.
64 | // - logger is the parent logger.
65 | // - marshaller is the Marshaller used for encoding messages.
66 | func newConsumerConnection(
67 | ctx context.Context,
68 | uri string,
69 | connectionName string,
70 | keepAlive bool,
71 | retryDelay time.Duration,
72 | logger logger,
73 | marshaller Marshaller,
74 | ) *amqpConnection {
75 | connectionName = fmt.Sprintf("%s-consumer-%s", connectionName, uuid.NewString())
76 |
77 | return newConnection(ctx, uri, connectionName, keepAlive, retryDelay, logger, connectionTypeConsumer, marshaller)
78 | }
79 |
80 | // newPublishingConnection initializes a new publisher amqpConnection with given arguments.
81 | // - ctx is the parent context.
82 | // - uri is the connection string.
83 | // - connectionName is the connection name.
84 | // - keepAlive will keep the connection alive if true.
85 | // - retryDelay defines the delay between each re-connection, if the keepAlive flag is set to true.
86 | // - maxRetry defines the publishing max retry header.
87 | // - publishingCacheSize defines the maximum length of failed publishing cache.
88 | // - publishingCacheTTL defines the time to live for failed publishing in cache.
89 | // - logger is the parent logger.
90 | // - marshaller is the Marshaller used for encoding messages.
91 | func newPublishingConnection(
92 | ctx context.Context,
93 | uri string,
94 | connectionName string,
95 | keepAlive bool,
96 | retryDelay time.Duration,
97 | maxRetry uint,
98 | publishingCacheSize uint64,
99 | publishingCacheTTL time.Duration,
100 | logger logger,
101 | marshaller Marshaller,
102 | ) *amqpConnection {
103 | connectionName = fmt.Sprintf("%s-publisher-%s", connectionName, uuid.NewString())
104 |
105 | conn := newConnection(ctx, uri, connectionName, keepAlive, retryDelay, logger, connectionTypePublisher, marshaller)
106 |
107 | conn.maxRetry = maxRetry
108 | conn.publishingCacheSize = publishingCacheSize
109 | conn.publishingCacheTTL = publishingCacheTTL
110 |
111 | return conn
112 | }
113 |
114 | // newConnection initializes a new amqpConnection with given arguments.
115 | // - ctx is the parent context.
116 | // - uri is the connection string.
117 | // - connectionName is the connection name.
118 | // - keepAlive will keep the connection alive if true.
119 | // - retryDelay defines the delay between each re-connection, if the keepAlive flag is set to true.
120 | // - logger is the parent logger.
121 | // - marshaller is the Marshaller used for encoding messages.
122 | func newConnection(
123 | ctx context.Context,
124 | uri string,
125 | connectionName string,
126 | keepAlive bool,
127 | retryDelay time.Duration,
128 | logger logger,
129 | connectionType connectionType,
130 | marshaller Marshaller,
131 | ) *amqpConnection {
132 | conn := &amqpConnection{
133 | ctx: ctx,
134 | uri: uri,
135 | connectionName: connectionName,
136 | keepAlive: keepAlive,
137 | retryDelay: retryDelay,
138 | channels: make(amqpChannels, 0),
139 | logger: inheritLogger(logger, map[string]interface{}{
140 | "context": "connection",
141 | "type": connectionType,
142 | }),
143 | connectionType: connectionType,
144 | marshaller: marshaller,
145 | }
146 |
147 | conn.logger.Debug("Initializing new amqp connection", logField{Key: "uri", Value: conn.uriForLog()})
148 |
149 | // We open an initial connection.
150 | err := conn.open()
151 |
152 | // If the connection failed and the keepAlive flag is set to true, we want to re-connect until success.
153 | if err != nil && keepAlive {
154 | go conn.reconnect()
155 | }
156 |
157 | return conn
158 | }
159 |
160 | // open opens a new amqp.Connection with the help of a defined uri.
161 | func (a *amqpConnection) open() error {
162 | // If the uri is empty, we return an error.
163 | if a.uri == "" {
164 | return errEmptyURI
165 | }
166 |
167 | a.logger.Debug("Connecting to RabbitMQ server", logField{Key: "uri", Value: a.uriForLog()})
168 |
169 | props := amqp.NewConnectionProperties()
170 | if a.connectionName != "" {
171 | props.SetClientConnectionName(a.connectionName)
172 | }
173 |
174 | // We request a connection from the RabbitMQ server.
175 | conn, err := amqp.DialConfig(a.uri, amqp.Config{
176 | Heartbeat: defaultHeartbeat,
177 | Locale: defaultLocale,
178 | Properties: props,
179 | })
180 | if err != nil {
181 | a.logger.Error(err, "Connection failed")
182 |
183 | return err
184 | }
185 |
186 | a.logger.Info("Connection successful", logField{Key: "uri", Value: a.uriForLog()})
187 |
188 | a.connection = conn
189 |
190 | a.channels.updateParentConnection(a.connection)
191 |
192 | // If the keepAlive flag is set to true, we activate a new guard.
193 | if a.keepAlive {
194 | go a.guard()
195 | }
196 |
197 | return nil
198 | }
199 |
200 | // reconnect will indefinitely call the open method until a connection is successfully established or the context is canceled.
201 | func (a *amqpConnection) reconnect() {
202 | a.logger.Debug("Re-connection launched")
203 |
204 | for {
205 | select {
206 | case <-a.ctx.Done():
207 | a.logger.Debug("Re-connection stopped by the context")
208 |
209 | // If the context was canceled, we break out of the method.
210 | return
211 | default:
212 | // Wait for the retryDelay.
213 | time.Sleep(a.retryDelay)
214 |
215 | // If there is no connection or the current connection is closed, we open a new connection.
216 | if !a.ready() {
217 | err := a.open()
218 | // If the operation succeeds, we break the loop.
219 | if err == nil {
220 | a.logger.Debug("Re-connection successful")
221 |
222 | return
223 | }
224 |
225 | a.logger.Error(err, "Could not open new connection during re-connection")
226 | } else {
227 | // If the connection exists and is active, we break out.
228 | return
229 | }
230 | }
231 | }
232 | }
233 |
234 | // guard is a connection safeguard that listens to connection close events and re-launches the connection.
235 | func (a *amqpConnection) guard() {
236 | a.logger.Debug("Guard launched")
237 |
238 | for {
239 | select {
240 | case <-a.ctx.Done():
241 | a.logger.Debug("Guard stopped by the context")
242 |
243 | // If the context was canceled, we break out of the method.
244 | return
245 | case err, ok := <-a.connection.NotifyClose(make(chan *amqp.Error)):
246 | if !ok {
247 | return
248 | }
249 |
250 | if err != nil {
251 | a.logger.Warn("Connection lost", logField{Key: "reason", Value: err.Reason}, logField{Key: "code", Value: err.Code})
252 | }
253 |
254 | // If the connection was explicitly closed, we do not want to re-connect.
255 | if a.closed {
256 | return
257 | }
258 |
259 | go a.reconnect()
260 |
261 | return
262 | }
263 | }
264 | }
265 |
266 | // close the connection only if it is ready.
267 | func (a *amqpConnection) close() error {
268 | if a.ready() {
269 | for _, channel := range a.channels {
270 | err := channel.close()
271 | if err != nil {
272 | return err
273 | }
274 | }
275 |
276 | err := a.connection.Close()
277 | if err != nil {
278 | a.logger.Error(err, "Could not close connection")
279 |
280 | return err
281 | }
282 | }
283 |
284 | a.closed = true
285 |
286 | a.logger.Info("Connection closed")
287 |
288 | return nil
289 | }
290 |
291 | // ready returns true if the connection exists and is not closed.
292 | func (a *amqpConnection) ready() bool {
293 | return a.connection != nil && !a.connection.IsClosed()
294 | }
295 |
296 | // healthy returns true if the connection exists, is not closed and all child channels are healthy.
297 | func (a *amqpConnection) healthy() bool {
298 | // If the connection is not ready, return false.
299 | if !a.ready() {
300 | return false
301 | }
302 |
303 | // Verify that all connection channels are ready too.
304 | for _, channel := range a.channels {
305 | if !channel.healthy() {
306 | return false
307 | }
308 | }
309 |
310 | return true
311 | }
312 |
313 | // registerConsumer opens a new consumerChannel and registers the MessageConsumer.
314 | func (a *amqpConnection) registerConsumer(consumer MessageConsumer) error {
315 | for _, channel := range a.channels {
316 | if channel.consumer != nil && channel.consumer.Queue == consumer.Queue {
317 | err := errConsumerAlreadyExists
318 |
319 | a.logger.Error(err, "Could not register consumer", logField{Key: "consumer", Value: consumer.Name})
320 |
321 | return err
322 | }
323 | }
324 |
325 | if err := consumer.Handlers.Validate(); err != nil {
326 | return err
327 | }
328 |
329 | channel := newConsumerChannel(a.ctx, a.connection, a.keepAlive, a.retryDelay, &consumer, a.logger, a.marshaller)
330 |
331 | a.channels = append(a.channels, channel)
332 |
333 | a.logger.Info("Consumer registered", logField{Key: "consumer", Value: consumer.Name})
334 |
335 | return nil
336 | }
337 |
338 | func (a *amqpConnection) publish(exchange, routingKey string, payload []byte, options *PublishingOptions) error {
339 | publishingChannel := a.channels.publishingChannel()
340 | if publishingChannel == nil {
341 | publishingChannel = newPublishingChannel(
342 | a.ctx, a.connection, a.keepAlive, a.retryDelay, a.maxRetry,
343 | a.publishingCacheSize, a.publishingCacheTTL, a.logger, a.marshaller,
344 | )
345 |
346 | a.channels = append(a.channels, publishingChannel)
347 | }
348 |
349 | return publishingChannel.publish(exchange, routingKey, payload, options)
350 | }
351 |
352 | // uriForLog returns the uri with the password hidden for security measures.
353 | func (a *amqpConnection) uriForLog() string {
354 | if a.uri == "" {
355 | return a.uri
356 | }
357 |
358 | parsedURL, err := url.Parse(a.uri)
359 | if err != nil {
360 | return ""
361 | }
362 |
363 | hiddenPassword := "xxxx"
364 |
365 | if parsedURL.User != nil {
366 | parsedURL.User = url.UserPassword(parsedURL.User.Username(), hiddenPassword)
367 | }
368 |
369 | return parsedURL.String()
370 | }
371 |
--------------------------------------------------------------------------------
/connection_manager.go:
--------------------------------------------------------------------------------
1 | package gorabbit
2 |
3 | import (
4 | "context"
5 | "time"
6 | )
7 |
8 | type connectionManager struct {
9 | // consumerConnection holds the independent consuming connection.
10 | consumerConnection *amqpConnection
11 |
12 | // publisherConnection holds the independent publishing connection.
13 | publisherConnection *amqpConnection
14 |
15 | // marshaller holds the marshaller used to encode messages.
16 | marshaller Marshaller
17 | }
18 |
19 | // newConnectionManager instantiates a new connectionManager with given arguments.
20 | func newConnectionManager(
21 | ctx context.Context,
22 | uri string,
23 | connectionName string,
24 | keepAlive bool,
25 | retryDelay time.Duration,
26 | maxRetry uint,
27 | publishingCacheSize uint64,
28 | publishingCacheTTL time.Duration,
29 | logger logger,
30 | marshaller Marshaller,
31 | ) *connectionManager {
32 | if connectionName == "" {
33 | connectionName = libraryName
34 | }
35 |
36 | c := &connectionManager{
37 | consumerConnection: newConsumerConnection(
38 | ctx, uri, connectionName, keepAlive, retryDelay, logger, marshaller,
39 | ),
40 | publisherConnection: newPublishingConnection(
41 | ctx, uri, connectionName, keepAlive, retryDelay, maxRetry,
42 | publishingCacheSize, publishingCacheTTL, logger, marshaller,
43 | ),
44 | marshaller: marshaller,
45 | }
46 |
47 | return c
48 | }
49 |
50 | // close offers the basic connection and channel close() mechanism but with extra higher level checks.
51 | func (c *connectionManager) close() error {
52 | if err := c.publisherConnection.close(); err != nil {
53 | return err
54 | }
55 |
56 | return c.consumerConnection.close()
57 | }
58 |
59 | // isReady returns true if both consumerConnection and publishingConnection are ready.
60 | func (c *connectionManager) isReady() bool {
61 | if c.publisherConnection == nil || c.consumerConnection == nil {
62 | return false
63 | }
64 |
65 | return c.publisherConnection.ready() && c.consumerConnection.ready()
66 | }
67 |
68 | // isHealthy returns true if both consumerConnection and publishingConnection are healthy.
69 | func (c *connectionManager) isHealthy() bool {
70 | if c.publisherConnection == nil || c.consumerConnection == nil {
71 | return false
72 | }
73 |
74 | return c.publisherConnection.healthy() && c.consumerConnection.healthy()
75 | }
76 |
77 | // registerConsumer registers a new MessageConsumer.
78 | func (c *connectionManager) registerConsumer(consumer MessageConsumer) error {
79 | if c.consumerConnection == nil {
80 | return errConsumerConnectionNotInitialized
81 | }
82 |
83 | return c.consumerConnection.registerConsumer(consumer)
84 | }
85 |
86 | func (c *connectionManager) publish(exchange, routingKey string, payload interface{}, options *PublishingOptions) error {
87 | if c.publisherConnection == nil {
88 | return errPublisherConnectionNotInitialized
89 | }
90 |
91 | payloadBytes, err := c.marshaller.Marshal(payload)
92 | if err != nil {
93 | return err
94 | }
95 |
96 | return c.publisherConnection.publish(exchange, routingKey, payloadBytes, options)
97 | }
98 |
--------------------------------------------------------------------------------
/constants.go:
--------------------------------------------------------------------------------
1 | package gorabbit
2 |
3 | import (
4 | "errors"
5 | "time"
6 | )
7 |
8 | // Library name.
9 | const libraryName = "Gorabbit"
10 |
11 | // Connection protocols.
12 | const (
13 | defaultProtocol = "amqp"
14 | securedProtocol = "amqps"
15 | )
16 |
17 | // Default values for the ClientOptions and ManagerOptions.
18 | const (
19 | defaultHost = "127.0.0.1"
20 | defaultPort = 5672
21 | defaultUsername = "guest"
22 | defaultPassword = "guest"
23 | defaultVhost = ""
24 | defaultUseTLS = false
25 | defaultKeepAlive = true
26 | defaultRetryDelay = 3 * time.Second
27 | defaultMaxRetry = 5
28 | defaultPublishingCacheTTL = 60 * time.Second
29 | defaultPublishingCacheSize = 128
30 | defaultMode = Release
31 | )
32 |
33 | var defaultMarshaller = NewJSONMarshaller()
34 |
35 | // Default values for the amqp Config.
36 | const (
37 | defaultHeartbeat = 10 * time.Second
38 | defaultLocale = "en_US"
39 | )
40 |
41 | const (
42 | xDeathCountHeader = "x-death-count"
43 | )
44 |
45 | // Connection Types.
46 |
47 | type connectionType string
48 |
49 | const (
50 | connectionTypeConsumer connectionType = "consumer"
51 | connectionTypePublisher connectionType = "publisher"
52 | )
53 |
54 | // Exchange Types
55 |
56 | type ExchangeType string
57 |
58 | const (
59 | ExchangeTypeTopic ExchangeType = "topic"
60 | ExchangeTypeDirect ExchangeType = "direct"
61 | ExchangeTypeFanout ExchangeType = "fanout"
62 | ExchangeTypeHeaders ExchangeType = "headers"
63 | )
64 |
65 | func (e ExchangeType) String() string {
66 | return string(e)
67 | }
68 |
69 | // Priority Levels.
70 |
71 | type MessagePriority uint8
72 |
73 | const (
74 | PriorityLowest MessagePriority = 1
75 | PriorityVeryLow MessagePriority = 2
76 | PriorityLow MessagePriority = 3
77 | PriorityMedium MessagePriority = 4
78 | PriorityHigh MessagePriority = 5
79 | PriorityHighest MessagePriority = 6
80 | )
81 |
82 | func (m MessagePriority) Uint8() uint8 {
83 | return uint8(m)
84 | }
85 |
86 | // Delivery Modes.
87 |
88 | type DeliveryMode uint8
89 |
90 | const (
91 | Transient DeliveryMode = 1
92 | Persistent DeliveryMode = 2
93 | )
94 |
95 | func (d DeliveryMode) Uint8() uint8 {
96 | return uint8(d)
97 | }
98 |
99 | // Logging Modes.
100 | const (
101 | Release = "release"
102 | Debug = "debug"
103 | )
104 |
105 | func isValidMode(mode string) bool {
106 | return mode == Release || mode == Debug
107 | }
108 |
109 | // Errors.
110 | var (
111 | errEmptyURI = errors.New("amqp uri is empty")
112 | errChannelClosed = errors.New("channel is closed")
113 | errConnectionClosed = errors.New("connection is closed")
114 | errConsumerAlreadyExists = errors.New("consumer already exists")
115 | errConsumerConnectionNotInitialized = errors.New("consumerConnection is not initialized")
116 | errPublisherConnectionNotInitialized = errors.New("publisherConnection is not initialized")
117 | errEmptyQueue = errors.New("queue is empty")
118 | )
119 |
--------------------------------------------------------------------------------
/consumer.go:
--------------------------------------------------------------------------------
1 | package gorabbit
2 |
3 | import (
4 | "errors"
5 | "fmt"
6 | "strings"
7 | )
8 |
9 | // MQTTMessageHandlers is a wrapper that holds a map[string]MQTTMessageHandlerFunc.
10 | type MQTTMessageHandlers map[string]MQTTMessageHandlerFunc
11 |
12 | // MQTTMessageHandlerFunc is the function that will be called when a delivery is received.
13 | type MQTTMessageHandlerFunc func(payload []byte) error
14 |
15 | // Validate verifies that all routing keys in the handlers are properly formatted and allowed.
16 | //
17 | //nolint:gocognit // We can allow the current complexity for now but we should revisit it later.
18 | func (mh MQTTMessageHandlers) Validate() error {
19 | for k := range mh {
20 | // A routing key cannot be empty.
21 | if len(k) == 0 {
22 | return errors.New("a routing key cannot be empty")
23 | }
24 |
25 | // A routing key cannot be equal to the wildcard '#'.
26 | if len(k) == 1 && k == "#" {
27 | return errors.New("a routing key cannot be the wildcard '#'")
28 | }
29 |
30 | // A routing key cannot contain spaces.
31 | if strings.Contains(k, " ") {
32 | return errors.New("a routing key cannot contain spaces")
33 | }
34 |
35 | // If a routing key is not just made up of one word.
36 | if strings.Contains(k, ".") {
37 | // We need to make sure that we do not find an empty word or a '%' in the middle of the key.
38 | split := strings.Split(k, ".")
39 |
40 | for i, v := range split {
41 | // We cannot have empty strings.
42 | if v == "" {
43 | return fmt.Errorf("the routing key '%s' is not properly formatted", k)
44 | }
45 |
46 | // The wildcard '#' is not allowed in the middle.
47 | if v == "#" && i > 0 && i < len(split)-1 {
48 | return fmt.Errorf("the wildcard '#' in the routing key '%s' is not allowed", k)
49 | }
50 | }
51 | }
52 | }
53 |
54 | return nil
55 | }
56 |
57 | // matchesPrefixWildcard verifies that everything that comes after the '#' wildcard matches.
58 | func (mh MQTTMessageHandlers) matchesPrefixWildcard(storedWords, words []string) bool {
59 | // compareIndex starts after the wildcard in the storedWords array.
60 | compareIndex := 1
61 |
62 | // we initialize the wordIdx at -1.
63 | wordIdx := -1
64 |
65 | // Here we are searching for the first occurrence of the first word after the '#' wildcard
66 | // of the storedWords in the words.
67 | for i, w := range words {
68 | if w == storedWords[compareIndex] {
69 | // We can now start comparing at 'i'.
70 | wordIdx = i
71 |
72 | break
73 | }
74 | }
75 |
76 | // If we did not find the first word, then surely the key does not match.
77 | if wordIdx == -1 {
78 | return false
79 | }
80 |
81 | // If the length of storedWords is not the same as the length of words after the wildcard,
82 | // then surely the key does not match.
83 | if len(storedWords)-compareIndex != len(words)-wordIdx {
84 | return false
85 | }
86 |
87 | // Now we can compare, word by word if the routing keys matches.
88 | for i := wordIdx; i < len(words); i++ {
89 | // Be careful, if we find '*' then it should match no matter what.
90 | if storedWords[compareIndex] != words[i] && storedWords[compareIndex] != "*" {
91 | return false
92 | }
93 |
94 | // We move right in the storedWords.
95 | compareIndex++
96 | }
97 |
98 | return true
99 | }
100 |
101 | // matchesSuffixWildcard verifies that everything that comes before the '#' wildcard matches.
102 | func (mh MQTTMessageHandlers) matchesSuffixWildcard(storedWords, words []string) bool {
103 | backCount := 2
104 |
105 | // compareIndex starts before the wildcard in the storedWords array.
106 | compareIndex := len(storedWords) - backCount
107 |
108 | // we initialize the wordIdx at -1.
109 | wordIdx := -1
110 |
111 | // Here we are searching for the first occurrence of the first word before the '#' wildcard
112 | // of the storedWords in the words.
113 | for i, w := range words {
114 | if w == storedWords[compareIndex] {
115 | wordIdx = i
116 |
117 | break
118 | }
119 | }
120 |
121 | // If we did not find the first word, then surely the key does not match.
122 | if wordIdx == -1 {
123 | return false
124 | }
125 |
126 | // If the indexes are not the same then surely the key does not match.
127 | if compareIndex != wordIdx {
128 | return false
129 | }
130 |
131 | // Now we can compare, word by word, going backwards if the routing keys matches.
132 | for i := wordIdx; i > -1; i-- {
133 | // Be careful, if we find '*' then it should match no matter what.
134 | if storedWords[compareIndex] != words[i] && storedWords[compareIndex] != "*" {
135 | return false
136 | }
137 |
138 | // We move left in the storedWords.
139 | compareIndex--
140 | }
141 |
142 | return true
143 | }
144 |
145 | // matchesSuffixWildcard verifies that 2 keys match word by word.
146 | func (mh MQTTMessageHandlers) matchesKey(storedWords, words []string) bool {
147 | // If the lengths are not the same then surely the key does not match.
148 | if len(storedWords) != len(words) {
149 | return false
150 | }
151 |
152 | // Now we can compare, word by word if the routing keys matches.
153 | for i, word := range words {
154 | // Be careful, if we find '*' then it should match no matter what.
155 | if storedWords[i] != word && storedWords[i] != "*" {
156 | return false
157 | }
158 | }
159 |
160 | return true
161 | }
162 |
163 | func (mh MQTTMessageHandlers) FindFunc(routingKey string) MQTTMessageHandlerFunc {
164 | // We first check for a direct match
165 | if fn, found := mh[routingKey]; found {
166 | return fn
167 | }
168 |
169 | // Split the routing key into individual words.
170 | words := strings.Split(routingKey, ".")
171 |
172 | // Check if any of the registered keys match the routing key.
173 | for key, fn := range mh {
174 | // Split the registered key into individual words.
175 | storedWords := strings.Split(key, ".")
176 |
177 | //nolint: gocritic,nestif // We need this if-else block
178 | if storedWords[0] == "#" {
179 | if !mh.matchesPrefixWildcard(storedWords, words) {
180 | continue
181 | }
182 | } else if storedWords[len(storedWords)-1] == "#" {
183 | if !mh.matchesSuffixWildcard(storedWords, words) {
184 | continue
185 | }
186 | } else {
187 | if !mh.matchesKey(storedWords, words) {
188 | continue
189 | }
190 | }
191 |
192 | return fn
193 | }
194 |
195 | // No matching keys were found.
196 | return nil
197 | }
198 |
199 | // MessageConsumer holds all the information needed to consume messages.
200 | type MessageConsumer struct {
201 | // Queue defines the queue from which we want to consume messages.
202 | Queue string
203 |
204 | // Name is a unique identifier of the consumer. Should be as explicit as possible.
205 | Name string
206 |
207 | // PrefetchSize defines the max size of messages that are allowed to be processed at the same time.
208 | // This property is dropped if AutoAck is set to true.
209 | PrefetchSize int
210 |
211 | // PrefetchCount defines the max number of messages that are allowed to be processed at the same time.
212 | // This property is dropped if AutoAck is set to true.
213 | PrefetchCount int
214 |
215 | // AutoAck defines whether a message is directly acknowledged or not when being consumed.
216 | AutoAck bool
217 |
218 | // ConcurrentProcess will make MQTTMessageHandlers run concurrently for faster consumption, if set to true.
219 | ConcurrentProcess bool
220 |
221 | // Handlers is the list of defined handlers.
222 | Handlers MQTTMessageHandlers
223 | }
224 |
225 | // HashCode returns a unique identifier for the defined consumer.
226 | func (c MessageConsumer) HashCode() string {
227 | return fmt.Sprintf("%s-%s", c.Queue, c.Name)
228 | }
229 |
--------------------------------------------------------------------------------
/consumer_test.go:
--------------------------------------------------------------------------------
1 | package gorabbit_test
2 |
3 | import (
4 | "errors"
5 | "testing"
6 |
7 | "github.com/stretchr/testify/assert"
8 |
9 | "github.com/KardinalAI/gorabbit"
10 | )
11 |
12 | func TestMQTTMessageHandlers_Validate(t *testing.T) {
13 | tests := []struct {
14 | handlers gorabbit.MQTTMessageHandlers
15 | expectedError error
16 | }{
17 | {
18 | handlers: gorabbit.MQTTMessageHandlers{
19 | "event.user.#": func(_ []byte) error { return nil },
20 | "event.email.*.generated": func(_ []byte) error { return nil },
21 | "event.*.space.boom": func(_ []byte) error { return nil },
22 | "*.toto.order.passed": func(_ []byte) error { return nil },
23 | "#.toto": func(_ []byte) error { return nil },
24 | },
25 | expectedError: nil,
26 | },
27 | {
28 | handlers: gorabbit.MQTTMessageHandlers{
29 | "": func(_ []byte) error { return nil },
30 | },
31 | expectedError: errors.New("a routing key cannot be empty"),
32 | },
33 | {
34 | handlers: gorabbit.MQTTMessageHandlers{
35 | " ": func(_ []byte) error { return nil },
36 | },
37 | expectedError: errors.New("a routing key cannot contain spaces"),
38 | },
39 | {
40 | handlers: gorabbit.MQTTMessageHandlers{
41 | "#": func(_ []byte) error { return nil },
42 | },
43 | expectedError: errors.New("a routing key cannot be the wildcard '#'"),
44 | },
45 | {
46 | handlers: gorabbit.MQTTMessageHandlers{
47 | "toto.#.titi": func(_ []byte) error { return nil },
48 | },
49 | expectedError: errors.New("the wildcard '#' in the routing key 'toto.#.titi' is not allowed"),
50 | },
51 | {
52 | handlers: gorabbit.MQTTMessageHandlers{
53 | "toto titi": func(_ []byte) error { return nil },
54 | },
55 | expectedError: errors.New("a routing key cannot contain spaces"),
56 | },
57 | {
58 | handlers: gorabbit.MQTTMessageHandlers{
59 | "toto..titi": func(_ []byte) error { return nil },
60 | },
61 | expectedError: errors.New("the routing key 'toto..titi' is not properly formatted"),
62 | },
63 | {
64 | handlers: gorabbit.MQTTMessageHandlers{
65 | ".toto.titi": func(_ []byte) error { return nil },
66 | },
67 | expectedError: errors.New("the routing key '.toto.titi' is not properly formatted"),
68 | },
69 | {
70 | handlers: gorabbit.MQTTMessageHandlers{
71 | "toto.titi.": func(_ []byte) error { return nil },
72 | },
73 | expectedError: errors.New("the routing key 'toto.titi.' is not properly formatted"),
74 | },
75 | }
76 |
77 | for _, test := range tests {
78 | err := test.handlers.Validate()
79 |
80 | assert.Equal(t, test.expectedError, err)
81 | }
82 | }
83 |
84 | func TestMQTTMessageHandlers_FindFunc(t *testing.T) {
85 | handlers := gorabbit.MQTTMessageHandlers{
86 | "event.user.#": func(_ []byte) error { return nil },
87 | "event.email.*.generated": func(_ []byte) error { return nil },
88 | "event.*.space.boom": func(_ []byte) error { return nil },
89 | "*.toto.order.passed": func(_ []byte) error { return nil },
90 | "#.toto": func(_ []byte) error { return nil },
91 | }
92 |
93 | tests := []struct {
94 | input string
95 | shouldMatch bool
96 | }{
97 | {
98 | input: "event.user.plan.generated",
99 | shouldMatch: true,
100 | },
101 | {
102 | input: "event.user.password.generated.before.awakening.the.titan",
103 | shouldMatch: true,
104 | },
105 | {
106 | input: "event.email.subject.generated",
107 | shouldMatch: true,
108 | },
109 | {
110 | input: "event.email.toto.generated",
111 | shouldMatch: true,
112 | },
113 | {
114 | input: "event.email.titi.generated",
115 | shouldMatch: true,
116 | },
117 | {
118 | input: "event.email.order.created",
119 | shouldMatch: false,
120 | },
121 | {
122 | input: "event.toto.space.boom",
123 | shouldMatch: true,
124 | },
125 | {
126 | input: "event.toto.space.not_boom",
127 | shouldMatch: false,
128 | },
129 | {
130 | input: "command.toto.order.passed",
131 | shouldMatch: true,
132 | },
133 | {
134 | input: "command.toto.order.passed.please",
135 | shouldMatch: false,
136 | },
137 | {
138 | input: "event.toto",
139 | shouldMatch: true,
140 | },
141 | {
142 | input: "event.space.space.toto",
143 | shouldMatch: true,
144 | },
145 | {
146 | input: "event.toto.space",
147 | shouldMatch: false,
148 | },
149 | }
150 |
151 | for _, test := range tests {
152 | fn := handlers.FindFunc(test.input)
153 |
154 | if test.shouldMatch {
155 | assert.NotNil(t, fn)
156 | } else {
157 | assert.Nil(t, fn)
158 | }
159 | }
160 | }
161 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/KardinalAI/gorabbit
2 |
3 | go 1.21
4 |
5 | require (
6 | github.com/Netflix/go-env v0.0.0-20220526054621-78278af1949d
7 | github.com/google/uuid v1.6.0
8 | github.com/rabbitmq/amqp091-go v1.10.0
9 | github.com/sirupsen/logrus v1.9.3
10 | github.com/stretchr/testify v1.9.0
11 | github.com/testcontainers/testcontainers-go v0.30.0
12 | github.com/testcontainers/testcontainers-go/modules/rabbitmq v0.30.0
13 | )
14 |
15 | require (
16 | dario.cat/mergo v1.0.0 // indirect
17 | github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
18 | github.com/Microsoft/go-winio v0.6.1 // indirect
19 | github.com/Microsoft/hcsshim v0.11.4 // indirect
20 | github.com/cenkalti/backoff/v4 v4.2.1 // indirect
21 | github.com/containerd/containerd v1.7.12 // indirect
22 | github.com/containerd/log v0.1.0 // indirect
23 | github.com/cpuguy83/dockercfg v0.3.1 // indirect
24 | github.com/davecgh/go-spew v1.1.1 // indirect
25 | github.com/distribution/reference v0.5.0 // indirect
26 | github.com/docker/docker v25.0.5+incompatible // indirect
27 | github.com/docker/go-connections v0.5.0 // indirect
28 | github.com/docker/go-units v0.5.0 // indirect
29 | github.com/felixge/httpsnoop v1.0.4 // indirect
30 | github.com/go-logr/logr v1.4.1 // indirect
31 | github.com/go-logr/stdr v1.2.2 // indirect
32 | github.com/go-ole/go-ole v1.2.6 // indirect
33 | github.com/gogo/protobuf v1.3.2 // indirect
34 | github.com/golang/protobuf v1.5.3 // indirect
35 | github.com/klauspost/compress v1.16.0 // indirect
36 | github.com/kr/text v0.2.0 // indirect
37 | github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
38 | github.com/magiconair/properties v1.8.7 // indirect
39 | github.com/moby/patternmatcher v0.6.0 // indirect
40 | github.com/moby/sys/sequential v0.5.0 // indirect
41 | github.com/moby/sys/user v0.1.0 // indirect
42 | github.com/moby/term v0.5.0 // indirect
43 | github.com/morikuni/aec v1.0.0 // indirect
44 | github.com/opencontainers/go-digest v1.0.0 // indirect
45 | github.com/opencontainers/image-spec v1.1.0 // indirect
46 | github.com/pkg/errors v0.9.1 // indirect
47 | github.com/pmezard/go-difflib v1.0.0 // indirect
48 | github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
49 | github.com/shirou/gopsutil/v3 v3.23.12 // indirect
50 | github.com/shoenig/go-m1cpu v0.1.6 // indirect
51 | github.com/tklauser/go-sysconf v0.3.12 // indirect
52 | github.com/tklauser/numcpus v0.6.1 // indirect
53 | github.com/yusufpapurcu/wmi v1.2.3 // indirect
54 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect
55 | go.opentelemetry.io/otel v1.24.0 // indirect
56 | go.opentelemetry.io/otel/metric v1.24.0 // indirect
57 | go.opentelemetry.io/otel/trace v1.24.0 // indirect
58 | golang.org/x/exp v0.0.0-20230510235704-dd950f8aeaea // indirect
59 | golang.org/x/mod v0.16.0 // indirect
60 | golang.org/x/sys v0.19.0 // indirect
61 | golang.org/x/tools v0.13.0 // indirect
62 | google.golang.org/genproto/googleapis/rpc v0.0.0-20230711160842-782d3b101e98 // indirect
63 | google.golang.org/grpc v1.58.3 // indirect
64 | google.golang.org/protobuf v1.33.0 // indirect
65 | gopkg.in/yaml.v3 v3.0.1 // indirect
66 | )
67 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk=
2 | dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
3 | github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 h1:bvDV9vkmnHYOMsOr4WLk+Vo07yKIzd94sVoIqshQ4bU=
4 | github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8=
5 | github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8=
6 | github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
7 | github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow=
8 | github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM=
9 | github.com/Microsoft/hcsshim v0.11.4 h1:68vKo2VN8DE9AdN4tnkWnmdhqdbpUFM8OF3Airm7fz8=
10 | github.com/Microsoft/hcsshim v0.11.4/go.mod h1:smjE4dvqPX9Zldna+t5FG3rnoHhaB7QYxPRqGcpAD9w=
11 | github.com/Netflix/go-env v0.0.0-20220526054621-78278af1949d h1:wvStE9wLpws31NiWUx+38wny1msZ/tm+eL5xmm4Y7So=
12 | github.com/Netflix/go-env v0.0.0-20220526054621-78278af1949d/go.mod h1:9XMFaCeRyW7fC9XJOWQ+NdAv8VLG7ys7l3x4ozEGLUQ=
13 | github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM=
14 | github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
15 | github.com/containerd/containerd v1.7.12 h1:+KQsnv4VnzyxWcfO9mlxxELaoztsDEjOuCMPAuPqgU0=
16 | github.com/containerd/containerd v1.7.12/go.mod h1:/5OMpE1p0ylxtEUGY8kuCYkDRzJm9NO1TFMWjUpdevk=
17 | github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
18 | github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
19 | github.com/cpuguy83/dockercfg v0.3.1 h1:/FpZ+JaygUR/lZP2NlFI2DVfrOEMAIKP5wWEJdoYe9E=
20 | github.com/cpuguy83/dockercfg v0.3.1/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc=
21 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
22 | github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY=
23 | github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
24 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
25 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
26 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
27 | github.com/distribution/reference v0.5.0 h1:/FUIFXtfc/x2gpa5/VGfiGLuOIdYa1t65IKK2OFGvA0=
28 | github.com/distribution/reference v0.5.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
29 | github.com/docker/docker v25.0.5+incompatible h1:UmQydMduGkrD5nQde1mecF/YnSbTOaPeFIeP5C4W+DE=
30 | github.com/docker/docker v25.0.5+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
31 | github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
32 | github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
33 | github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
34 | github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
35 | github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
36 | github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
37 | github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
38 | github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ=
39 | github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
40 | github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
41 | github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
42 | github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
43 | github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
44 | github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
45 | github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
46 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
47 | github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
48 | github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
49 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
50 | github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
51 | github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
52 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
53 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
54 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
55 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
56 | github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 h1:YBftPWNWd4WwGqtY2yeZL2ef8rHAxPBD8KFhJpmcqms=
57 | github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0/go.mod h1:YN5jB8ie0yfIUg6VvR9Kz84aCaG7AsGZnLjhHbUqwPg=
58 | github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
59 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
60 | github.com/klauspost/compress v1.16.0 h1:iULayQNOReoYUe+1qtKOqw9CwJv3aNQu8ivo7lw1HU4=
61 | github.com/klauspost/compress v1.16.0/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
62 | github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
63 | github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
64 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
65 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
66 | github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4=
67 | github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
68 | github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
69 | github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
70 | github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk=
71 | github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc=
72 | github.com/moby/sys/sequential v0.5.0 h1:OPvI35Lzn9K04PBbCLW0g4LcFAJgHsvXsRyewg5lXtc=
73 | github.com/moby/sys/sequential v0.5.0/go.mod h1:tH2cOOs5V9MlPiXcQzRC+eEyab644PWKGRYaaV5ZZlo=
74 | github.com/moby/sys/user v0.1.0 h1:WmZ93f5Ux6het5iituh9x2zAG7NFY9Aqi49jjE1PaQg=
75 | github.com/moby/sys/user v0.1.0/go.mod h1:fKJhFOnsCN6xZ5gSfbM6zaHGgDJMrqt9/reuj4T7MmU=
76 | github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0=
77 | github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y=
78 | github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
79 | github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
80 | github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
81 | github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
82 | github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug=
83 | github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM=
84 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
85 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
86 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
87 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
88 | github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw=
89 | github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
90 | github.com/rabbitmq/amqp091-go v1.10.0 h1:STpn5XsHlHGcecLmMFCtg7mqq0RnD+zFr4uzukfVhBw=
91 | github.com/rabbitmq/amqp091-go v1.10.0/go.mod h1:Hy4jKW5kQART1u+JkDTF9YYOQUHXqMuhrgxOEeS7G4o=
92 | github.com/rogpeppe/go-internal v1.8.1 h1:geMPLpDpQOgVyCg5z5GoRwLHepNdb71NXb67XFkP+Eg=
93 | github.com/rogpeppe/go-internal v1.8.1/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o=
94 | github.com/shirou/gopsutil/v3 v3.23.12 h1:z90NtUkp3bMtmICZKpC4+WaknU1eXtp5vtbQ11DgpE4=
95 | github.com/shirou/gopsutil/v3 v3.23.12/go.mod h1:1FrWgea594Jp7qmjHUUPlJDTPgcsb9mGnXDxavtikzM=
96 | github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM=
97 | github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ=
98 | github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU=
99 | github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k=
100 | github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
101 | github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
102 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
103 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
104 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
105 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
106 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
107 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
108 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
109 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
110 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
111 | github.com/testcontainers/testcontainers-go v0.30.0 h1:jmn/XS22q4YRrcMwWg0pAwlClzs/abopbsBzrepyc4E=
112 | github.com/testcontainers/testcontainers-go v0.30.0/go.mod h1:K+kHNGiM5zjklKjgTtcrEetF3uhWbMUyqAQoyoh8Pf0=
113 | github.com/testcontainers/testcontainers-go/modules/rabbitmq v0.30.0 h1:FtkqA628qBpEmPj+yTaCgWzpR4ERT1A4oad8nvhDYgQ=
114 | github.com/testcontainers/testcontainers-go/modules/rabbitmq v0.30.0/go.mod h1:JC5EnBLOGC5eEL0Vcf3bwvUAKixuoZsJ4K3g0ioZ+WU=
115 | github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU=
116 | github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI=
117 | github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk=
118 | github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY=
119 | github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
120 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
121 | github.com/yusufpapurcu/wmi v1.2.3 h1:E1ctvB7uKFMOJw3fdOW32DwGE9I7t++CRUEMKvFoFiw=
122 | github.com/yusufpapurcu/wmi v1.2.3/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
123 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 h1:jq9TW8u3so/bN+JPT166wjOI6/vQPF6Xe7nMNIltagk=
124 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw=
125 | go.opentelemetry.io/otel v1.24.0 h1:0LAOdjNmQeSTzGBzduGe/rU4tZhMwL5rWgtp9Ku5Jfo=
126 | go.opentelemetry.io/otel v1.24.0/go.mod h1:W7b9Ozg4nkF5tWI5zsXkaKKDjdVjpD4oAt9Qi/MArHo=
127 | go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0 h1:Mne5On7VWdx7omSrSSZvM4Kw7cS7NQkOOmLcgscI51U=
128 | go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0/go.mod h1:IPtUMKL4O3tH5y+iXVyAXqpAwMuzC1IrxVS81rummfE=
129 | go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0 h1:IeMeyr1aBvBiPVYihXIaeIZba6b8E1bYp7lbdxK8CQg=
130 | go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0/go.mod h1:oVdCUtjq9MK9BlS7TtucsQwUcXcymNiEDjgDD2jMtZU=
131 | go.opentelemetry.io/otel/metric v1.24.0 h1:6EhoGWWK28x1fbpA4tYTOWBkPefTDQnb8WSGXlc88kI=
132 | go.opentelemetry.io/otel/metric v1.24.0/go.mod h1:VYhLe1rFfxuTXLgj4CBiyz+9WYBA8pNGJgDcSFRKBco=
133 | go.opentelemetry.io/otel/sdk v1.19.0 h1:6USY6zH+L8uMH8L3t1enZPR3WFEmSTADlqldyHtJi3o=
134 | go.opentelemetry.io/otel/sdk v1.19.0/go.mod h1:NedEbbS4w3C6zElbLdPJKOpJQOrGUJ+GfzpjUvI0v1A=
135 | go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y1YELI=
136 | go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU=
137 | go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I=
138 | go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM=
139 | go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
140 | go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
141 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
142 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
143 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
144 | golang.org/x/exp v0.0.0-20230510235704-dd950f8aeaea h1:vLCWI/yYrdEHyN2JzIzPO3aaQJHQdp89IZBA/+azVC4=
145 | golang.org/x/exp v0.0.0-20230510235704-dd950f8aeaea/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w=
146 | golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
147 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
148 | golang.org/x/mod v0.16.0 h1:QX4fJ0Rr5cPQCF7O9lh9Se4pmwfwskqZfq5moyldzic=
149 | golang.org/x/mod v0.16.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
150 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
151 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
152 | golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
153 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
154 | golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
155 | golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
156 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
157 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
158 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
159 | golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E=
160 | golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
161 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
162 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
163 | golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
164 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
165 | golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
166 | golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
167 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
168 | golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
169 | golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
170 | golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
171 | golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o=
172 | golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
173 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
174 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
175 | golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
176 | golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
177 | golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
178 | golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
179 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
180 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
181 | golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
182 | golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
183 | golang.org/x/tools v0.13.0 h1:Iey4qkscZuv0VvIt8E0neZjtPVQFSc870HQ448QgEmQ=
184 | golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
185 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
186 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
187 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
188 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
189 | google.golang.org/genproto v0.0.0-20230711160842-782d3b101e98 h1:Z0hjGZePRE0ZBWotvtrwxFNrNE9CUAGtplaDK5NNI/g=
190 | google.golang.org/genproto/googleapis/api v0.0.0-20230711160842-782d3b101e98 h1:FmF5cCW94Ij59cfpoLiwTgodWmm60eEV0CjlsVg2fuw=
191 | google.golang.org/genproto/googleapis/api v0.0.0-20230711160842-782d3b101e98/go.mod h1:rsr7RhLuwsDKL7RmgDDCUc6yaGr1iqceVb5Wv6f6YvQ=
192 | google.golang.org/genproto/googleapis/rpc v0.0.0-20230711160842-782d3b101e98 h1:bVf09lpb+OJbByTj913DRJioFFAjf/ZGxEz7MajTp2U=
193 | google.golang.org/genproto/googleapis/rpc v0.0.0-20230711160842-782d3b101e98/go.mod h1:TUfxEVdsvPg18p6AslUXFoLdpED4oBnGwyqk3dV1XzM=
194 | google.golang.org/grpc v1.58.3 h1:BjnpXut1btbtgN/6sp+brB2Kbm2LjNXnidYujAVbSoQ=
195 | google.golang.org/grpc v1.58.3/go.mod h1:tgX3ZQDlNJGU96V6yHh1T/JeoBQ2TXdr43YbYSsCJk0=
196 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
197 | google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
198 | google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
199 | google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
200 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
201 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
202 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
203 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
204 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
205 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
206 | gotest.tools/v3 v3.5.0 h1:Ljk6PdHdOhAb5aDMWXjDLMMhph+BpztA4v1QdqEW2eY=
207 | gotest.tools/v3 v3.5.0/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU=
208 |
--------------------------------------------------------------------------------
/logger.go:
--------------------------------------------------------------------------------
1 | package gorabbit
2 |
3 | import (
4 | "os"
5 |
6 | "github.com/sirupsen/logrus"
7 | )
8 |
9 | type logField struct {
10 | Key string
11 | Value interface{}
12 | }
13 |
14 | // logger is the interface that defines log methods.
15 | type logger interface {
16 | Error(error, string, ...logField)
17 |
18 | Warn(string, ...logField)
19 |
20 | Info(string, ...logField)
21 |
22 | Debug(string, ...logField)
23 | }
24 |
25 | // stdLogger logs to stdout using logrus (https://github.com/sirupsen/logrus).
26 | type stdLogger struct {
27 | logger *logrus.Logger
28 | identifier string
29 | logFields map[string]interface{}
30 | }
31 |
32 | func newStdLogger() logger {
33 | return &stdLogger{
34 | logger: newLogrus(),
35 | identifier: libraryName,
36 | logFields: nil,
37 | }
38 | }
39 |
40 | func (l stdLogger) getExtraFields(fields []logField) map[string]interface{} {
41 | extraFields := make(map[string]interface{})
42 |
43 | for k, field := range l.logFields {
44 | extraFields[k] = field
45 | }
46 |
47 | for _, extraField := range fields {
48 | extraFields[extraField.Key] = extraField.Value
49 | }
50 |
51 | return extraFields
52 | }
53 |
54 | func (l stdLogger) Error(err error, s string, fields ...logField) {
55 | log := l.logger.WithField("library", l.identifier)
56 |
57 | extraFields := l.getExtraFields(fields)
58 |
59 | log.WithFields(extraFields).WithError(err).Error(s)
60 | }
61 |
62 | func (l stdLogger) Warn(s string, fields ...logField) {
63 | log := l.logger.WithField("library", l.identifier)
64 |
65 | extraFields := l.getExtraFields(fields)
66 |
67 | log.WithFields(extraFields).Warn(s)
68 | }
69 |
70 | func (l stdLogger) Info(s string, fields ...logField) {
71 | log := l.logger.WithField("library", l.identifier)
72 |
73 | extraFields := l.getExtraFields(fields)
74 |
75 | log.WithFields(extraFields).Info(s)
76 | }
77 |
78 | func (l stdLogger) Debug(s string, fields ...logField) {
79 | log := l.logger.WithField("library", l.identifier)
80 |
81 | extraFields := l.getExtraFields(fields)
82 |
83 | log.WithFields(extraFields).Debug(s)
84 | }
85 |
86 | // noLogger does not log at all, this is the default.
87 | type noLogger struct{}
88 |
89 | func (l noLogger) Error(_ error, _ string, _ ...logField) {}
90 |
91 | func (l noLogger) Warn(_ string, _ ...logField) {}
92 |
93 | func (l noLogger) Info(_ string, _ ...logField) {}
94 |
95 | func (l noLogger) Debug(_ string, _ ...logField) {}
96 |
97 | func newLogrus() *logrus.Logger {
98 | log := &logrus.Logger{
99 | Out: os.Stdout,
100 | Formatter: &logrus.JSONFormatter{
101 | DisableTimestamp: true,
102 | },
103 | Level: logrus.DebugLevel,
104 | }
105 |
106 | logLevel := os.Getenv("LOG_LEVEL")
107 | if logLevel != "" {
108 | lvl, err := logrus.ParseLevel(logLevel)
109 | if err == nil {
110 | log.Level = lvl
111 | }
112 | }
113 |
114 | return log
115 | }
116 |
117 | func inheritLogger(parent logger, logFields map[string]interface{}) logger {
118 | switch v := parent.(type) {
119 | case *stdLogger:
120 | return &stdLogger{
121 | logger: v.logger,
122 | identifier: libraryName,
123 | logFields: logFields,
124 | }
125 | default:
126 | return parent
127 | }
128 | }
129 |
--------------------------------------------------------------------------------
/manager.go:
--------------------------------------------------------------------------------
1 | package gorabbit
2 |
3 | import (
4 | "context"
5 | "encoding/json"
6 | "fmt"
7 | "os"
8 | "time"
9 |
10 | "github.com/google/uuid"
11 | amqp "github.com/rabbitmq/amqp091-go"
12 | )
13 |
14 | // MQTTManager is a simple MQTT interface that offers basic management operations such as:
15 | // - Creation of queue, exchange and bindings
16 | // - Deletion of queues and exchanges
17 | // - Purge of queues
18 | // - Queue evaluation (existence and number of messages)
19 | type MQTTManager interface {
20 | // Disconnect launches the disconnection process.
21 | // This operation disables to manager permanently.
22 | Disconnect() error
23 |
24 | // CreateQueue will create a new queue from QueueConfig.
25 | CreateQueue(config QueueConfig) error
26 |
27 | // CreateExchange will create a new exchange from ExchangeConfig.
28 | CreateExchange(config ExchangeConfig) error
29 |
30 | // BindExchangeToQueueViaRoutingKey will bind an exchange to a queue via a given routingKey.
31 | // Returns an error if the connection to the RabbitMQ server is down or if the exchange or queue does not exist.
32 | BindExchangeToQueueViaRoutingKey(exchange, queue, routingKey string) error
33 |
34 | // GetNumberOfMessages retrieves the number of messages currently sitting in a given queue.
35 | // Returns an error if the connection to the RabbitMQ server is down or the queue does not exist.
36 | GetNumberOfMessages(queue string) (int, error)
37 |
38 | // PushMessageToExchange pushes a message to a given exchange with a given routing key.
39 | // Returns an error if the connection to the RabbitMQ server is down or the exchange does not exist.
40 | PushMessageToExchange(exchange, routingKey string, payload interface{}) error
41 |
42 | // PopMessageFromQueue retrieves the first message of a queue. The message can then be auto-acknowledged or not.
43 | // Returns an error if the connection to the RabbitMQ server is down or the queue does not exist or is empty.
44 | PopMessageFromQueue(queue string, autoAck bool) (*amqp.Delivery, error)
45 |
46 | // PurgeQueue will empty a queue of all its current messages.
47 | // Returns an error if the connection to the RabbitMQ server is down or the queue does not exist.
48 | PurgeQueue(queue string) error
49 |
50 | // DeleteQueue permanently deletes an existing queue.
51 | // Returns an error if the connection to the RabbitMQ server is down or the queue does not exist.
52 | DeleteQueue(queue string) error
53 |
54 | // DeleteExchange permanently deletes an existing exchange.
55 | // Returns an error if the connection to the RabbitMQ server is down or the exchange does not exist.
56 | DeleteExchange(exchange string) error
57 |
58 | // SetupFromDefinitions loads a definitions.json file and automatically sets up exchanges, queues and bindings.
59 | SetupFromDefinitions(path string) error
60 |
61 | // GetHost returns the host used to initialize the manager.
62 | GetHost() string
63 |
64 | // GetPort returns the port used to initialize the manager.
65 | GetPort() uint
66 |
67 | // GetUsername returns the username used to initialize the manager.
68 | GetUsername() string
69 |
70 | // GetVhost returns the vhost used to initialize the manager.
71 | GetVhost() string
72 |
73 | // IsDisabled returns whether the manager is disabled or not.
74 | IsDisabled() bool
75 | }
76 |
77 | type mqttManager struct {
78 | // Host is the RabbitMQ server host name.
79 | Host string
80 |
81 | // Port is the RabbitMQ server port number.
82 | Port uint
83 |
84 | // Username is the RabbitMQ server allowed username.
85 | Username string
86 |
87 | // Password is the RabbitMQ server allowed password.
88 | Password string
89 |
90 | // Vhost is used for CloudAMQP connections to set the specific vhost.
91 | Vhost string
92 |
93 | // logger defines the logger used, depending on the mode set.
94 | logger logger
95 |
96 | // disabled completely disables the manager if true.
97 | disabled bool
98 |
99 | // connection holds the single connection to the RabbitMQ server.
100 | connection *amqp.Connection
101 |
102 | // channel holds the single channel from the connection.
103 | channel *amqp.Channel
104 |
105 | // marshaller holds the marshaller used to encode messages.
106 | marshaller Marshaller
107 | }
108 |
109 | // NewManager will instantiate a new MQTTManager.
110 | // If options is set to nil, the DefaultManagerOptions will be used.
111 | func NewManager(options *ManagerOptions) (MQTTManager, error) {
112 | // If no options is passed, we use the DefaultManagerOptions.
113 | if options == nil {
114 | options = DefaultManagerOptions()
115 | }
116 |
117 | return newManagerFromOptions(options)
118 | }
119 |
120 | // NewManagerFromEnv will instantiate a new MQTTManager from environment variables.
121 | func NewManagerFromEnv() (MQTTManager, error) {
122 | options := NewManagerOptionsFromEnv()
123 |
124 | return newManagerFromOptions(options)
125 | }
126 |
127 | func newManagerFromOptions(options *ManagerOptions) (MQTTManager, error) {
128 | manager := &mqttManager{
129 | Host: options.Host,
130 | Port: options.Port,
131 | Username: options.Username,
132 | Password: options.Password,
133 | Vhost: options.Vhost,
134 | logger: &noLogger{},
135 | }
136 |
137 | // We check if the disabled flag is present, which will completely disable the MQTTManager.
138 | if disabledOverride := os.Getenv("GORABBIT_DISABLED"); disabledOverride != "" {
139 | switch disabledOverride {
140 | case "1", "true":
141 | manager.disabled = true
142 |
143 | return manager, nil
144 | }
145 | }
146 |
147 | // We check if the mode was overwritten with the environment variable "GORABBIT_MODE".
148 | if modeOverride := os.Getenv("GORABBIT_MODE"); isValidMode(modeOverride) {
149 | // We override the mode only if it is valid
150 | options.Mode = modeOverride
151 | }
152 |
153 | if options.Mode == Debug {
154 | // If the mode is Debug, we want to actually log important events.
155 | manager.logger = newStdLogger()
156 | }
157 |
158 | protocol := defaultProtocol
159 |
160 | if options.UseTLS {
161 | protocol = securedProtocol
162 | }
163 |
164 | if options.Marshaller == nil {
165 | options.Marshaller = defaultMarshaller
166 | }
167 | manager.marshaller = options.Marshaller
168 |
169 | dialURL := fmt.Sprintf("%s://%s:%s@%s:%d/%s", protocol, manager.Username, manager.Password, manager.Host, manager.Port, manager.Vhost)
170 |
171 | var err error
172 |
173 | manager.connection, err = amqp.Dial(dialURL)
174 | if err != nil {
175 | return manager, err
176 | }
177 |
178 | manager.channel, err = manager.connection.Channel()
179 | if err != nil {
180 | return manager, err
181 | }
182 |
183 | return manager, nil
184 | }
185 |
186 | func (manager *mqttManager) Disconnect() error {
187 | // Manager is disabled, so we do nothing and return no error.
188 | if manager.disabled {
189 | return nil
190 | }
191 |
192 | // We close the manager's channel only if it is opened.
193 | if manager.channel != nil && !manager.channel.IsClosed() {
194 | err := manager.channel.Close()
195 | if err != nil {
196 | return err
197 | }
198 | }
199 |
200 | // We close the manager's connection only if it is opened.
201 | if manager.connection != nil && !manager.connection.IsClosed() {
202 | return manager.connection.Close()
203 | }
204 |
205 | return nil
206 | }
207 |
208 | func (manager *mqttManager) CreateQueue(config QueueConfig) error {
209 | // Manager is disabled, so we do nothing and return no error.
210 | if manager.disabled {
211 | return nil
212 | }
213 |
214 | // If the manager is not ready, we return its error.
215 | if ready, err := manager.ready(); !ready {
216 | return err
217 | }
218 |
219 | // We declare the queue via the channel.
220 | _, err := manager.channel.QueueDeclare(
221 | config.Name, // name
222 | config.Durable, // durable
223 | config.AutoDelete, // delete when unused
224 | config.Exclusive, // exclusive
225 | false, // no-wait
226 | config.Args,
227 | )
228 |
229 | if err != nil {
230 | return err
231 | }
232 |
233 | // If bindings are also declared, we create the bindings too.
234 | if config.Bindings != nil {
235 | for _, binding := range config.Bindings {
236 | err = manager.BindExchangeToQueueViaRoutingKey(binding.Exchange, config.Name, binding.RoutingKey)
237 |
238 | if err != nil {
239 | return err
240 | }
241 | }
242 | }
243 |
244 | return nil
245 | }
246 |
247 | func (manager *mqttManager) CreateExchange(config ExchangeConfig) error {
248 | // Manager is disabled, so we do nothing and return no error.
249 | if manager.disabled {
250 | return nil
251 | }
252 |
253 | // If the manager is not ready, we return its error.
254 | if ready, err := manager.ready(); !ready {
255 | return err
256 | }
257 |
258 | // We declare the exchange via the channel.
259 | return manager.channel.ExchangeDeclare(
260 | config.Name, // name
261 | config.Type.String(), // type
262 | config.Persisted, // durable
263 | !config.Persisted, // auto-deleted
264 | false, // internal
265 | false, // no-wait
266 | config.Args, // arguments
267 | )
268 | }
269 |
270 | func (manager *mqttManager) BindExchangeToQueueViaRoutingKey(exchange, queue, routingKey string) error {
271 | // Manager is disabled, so we do nothing and return no error.
272 | if manager.disabled {
273 | return nil
274 | }
275 |
276 | // If the manager is not ready, we return its error.
277 | if ready, err := manager.ready(); !ready {
278 | return err
279 | }
280 |
281 | // We bind the queue to a given exchange and routing key via the channel.
282 | return manager.channel.QueueBind(
283 | queue,
284 | routingKey,
285 | exchange,
286 | false,
287 | nil,
288 | )
289 | }
290 |
291 | func (manager *mqttManager) GetNumberOfMessages(queue string) (int, error) {
292 | // Manager is disabled, so we do nothing and return no error.
293 | if manager.disabled {
294 | return -1, nil
295 | }
296 |
297 | // If the manager is not ready, we return its error.
298 | if ready, err := manager.ready(); !ready {
299 | return -1, err
300 | }
301 |
302 | // We passively declare the queue via the channel, this will return the existing queue or an error if it doesn't exist.
303 | q, err := manager.channel.QueueDeclarePassive(
304 | queue,
305 | false,
306 | false,
307 | false,
308 | false,
309 | nil,
310 | )
311 |
312 | if err != nil {
313 | return -1, err
314 | }
315 |
316 | return q.Messages, nil
317 | }
318 |
319 | func (manager *mqttManager) PushMessageToExchange(exchange, routingKey string, payload interface{}) error {
320 | // Manager is disabled, so we do nothing and return no error.
321 | if manager.disabled {
322 | return nil
323 | }
324 |
325 | // If the manager is not ready, we return its error.
326 | if ready, err := manager.ready(); !ready {
327 | return err
328 | }
329 |
330 | // We convert the payload to a []byte.
331 | payloadBytes, err := manager.marshaller.Marshal(payload)
332 | if err != nil {
333 | return err
334 | }
335 |
336 | // We build the amqp.Publishing object.
337 | publishing := amqp.Publishing{
338 | ContentType: manager.marshaller.ContentType(),
339 | Body: payloadBytes,
340 | Type: routingKey,
341 | Priority: PriorityMedium.Uint8(),
342 | DeliveryMode: Transient.Uint8(),
343 | MessageId: uuid.NewString(),
344 | Timestamp: time.Now(),
345 | }
346 |
347 | // We push the message via the channel.
348 | return manager.channel.PublishWithContext(context.TODO(), exchange, routingKey, false, false, publishing)
349 | }
350 |
351 | func (manager *mqttManager) PopMessageFromQueue(queue string, autoAck bool) (*amqp.Delivery, error) {
352 | // Manager is disabled, so we do nothing and return no error.
353 | if manager.disabled {
354 | //nolint: nilnil // We must return
355 | return nil, nil
356 | }
357 |
358 | // If the manager is not ready, we return its error.
359 | if ready, err := manager.ready(); !ready {
360 | return nil, err
361 | }
362 |
363 | // We get the message via the channel.
364 | m, ok, err := manager.channel.Get(queue, autoAck)
365 |
366 | if err != nil {
367 | return nil, err
368 | }
369 |
370 | // If the queue is empty.
371 | if !ok {
372 | return nil, errEmptyQueue
373 | }
374 |
375 | return &m, nil
376 | }
377 |
378 | func (manager *mqttManager) PurgeQueue(queue string) error {
379 | // Manager is disabled, so we do nothing and return no error.
380 | if manager.disabled {
381 | return nil
382 | }
383 |
384 | // If the manager is not ready, we return its error.
385 | if ready, err := manager.ready(); !ready {
386 | return err
387 | }
388 |
389 | // We purge the queue via the channel.
390 | _, err := manager.channel.QueuePurge(queue, false)
391 |
392 | if err != nil {
393 | return err
394 | }
395 |
396 | return nil
397 | }
398 |
399 | func (manager *mqttManager) DeleteQueue(queue string) error {
400 | // Manager is disabled, so we do nothing and return no error.
401 | if manager.disabled {
402 | return nil
403 | }
404 |
405 | // If the manager is not ready, we return its error.
406 | if ready, err := manager.ready(); !ready {
407 | return err
408 | }
409 |
410 | // We delete the queue via the channel.
411 | _, err := manager.channel.QueueDelete(queue, false, false, false)
412 |
413 | if err != nil {
414 | return err
415 | }
416 |
417 | return nil
418 | }
419 |
420 | func (manager *mqttManager) DeleteExchange(exchange string) error {
421 | // Manager is disabled, so we do nothing and return no error.
422 | if manager.disabled {
423 | return nil
424 | }
425 |
426 | // If the manager is not ready, we return its error.
427 | if ready, err := manager.ready(); !ready {
428 | return err
429 | }
430 |
431 | // We delete the exchange via the channel.
432 | return manager.channel.ExchangeDelete(exchange, false, false)
433 | }
434 |
435 | func (manager *mqttManager) SetupFromDefinitions(path string) error {
436 | // Manager is disabled, so we do nothing and return no error.
437 | if manager.disabled {
438 | return nil
439 | }
440 |
441 | // If the manager is not ready, we return its error.
442 | if ready, err := manager.ready(); !ready {
443 | return err
444 | }
445 |
446 | // We read the definitions.json file.
447 | definitions, err := os.ReadFile(path)
448 | if err != nil {
449 | return err
450 | }
451 |
452 | def := new(SchemaDefinitions)
453 |
454 | // We parse the definitions.json file into the corresponding struct.
455 | err = json.Unmarshal(definitions, def)
456 | if err != nil {
457 | return err
458 | }
459 |
460 | for _, queue := range def.Queues {
461 | // We create the queue.
462 | err = manager.CreateQueue(QueueConfig{
463 | Name: queue.Name,
464 | Durable: queue.Durable,
465 | Exclusive: false,
466 | })
467 |
468 | if err != nil {
469 | return err
470 | }
471 | }
472 |
473 | for _, exchange := range def.Exchanges {
474 | // We create the exchange.
475 | err = manager.CreateExchange(ExchangeConfig{
476 | Name: exchange.Name,
477 | Type: ExchangeType(exchange.Type),
478 | Persisted: exchange.Durable,
479 | })
480 |
481 | if err != nil {
482 | return err
483 | }
484 | }
485 |
486 | for _, binding := range def.Bindings {
487 | // We bind the given exchange to the given queue via the given routing key.
488 | err = manager.BindExchangeToQueueViaRoutingKey(binding.Source, binding.Destination, binding.RoutingKey)
489 |
490 | if err != nil {
491 | return err
492 | }
493 | }
494 |
495 | return nil
496 | }
497 |
498 | func (manager *mqttManager) checkChannel() error {
499 | var err error
500 |
501 | // If the connection is nil or closed, we must request a new channel.
502 | if manager.channel == nil || manager.channel.IsClosed() {
503 | manager.channel, err = manager.connection.Channel()
504 | }
505 |
506 | return err
507 | }
508 |
509 | func (manager *mqttManager) ready() (bool, error) {
510 | // Manager is disabled, so we do nothing and return no error.
511 | if manager.disabled {
512 | return true, nil
513 | }
514 |
515 | // If the connection is nil or closed, we return an error because the manager is not ready.
516 | if manager.connection == nil || manager.connection.IsClosed() {
517 | return false, errConnectionClosed
518 | }
519 |
520 | // We check the channel as it might have been closed, and we need to request a new one.
521 | if err := manager.checkChannel(); err != nil {
522 | return false, err
523 | }
524 |
525 | // If the channel is still nil or closed, we return an error because the manager is not ready.
526 | if manager.channel == nil || manager.channel.IsClosed() {
527 | return false, errChannelClosed
528 | }
529 |
530 | return true, nil
531 | }
532 |
533 | func (manager *mqttManager) GetHost() string {
534 | return manager.Host
535 | }
536 |
537 | func (manager *mqttManager) GetPort() uint {
538 | return manager.Port
539 | }
540 |
541 | func (manager *mqttManager) GetUsername() string {
542 | return manager.Username
543 | }
544 |
545 | func (manager *mqttManager) GetVhost() string {
546 | return manager.Vhost
547 | }
548 |
549 | func (manager *mqttManager) IsDisabled() bool {
550 | return manager.disabled
551 | }
552 |
--------------------------------------------------------------------------------
/manager_options.go:
--------------------------------------------------------------------------------
1 | package gorabbit
2 |
3 | import "github.com/Netflix/go-env"
4 |
5 | // ManagerOptions holds all necessary properties to launch a successful connection with an MQTTManager.
6 | type ManagerOptions struct {
7 | // Host is the RabbitMQ server host name.
8 | Host string
9 |
10 | // Port is the RabbitMQ server port number.
11 | Port uint
12 |
13 | // Username is the RabbitMQ server allowed username.
14 | Username string
15 |
16 | // Password is the RabbitMQ server allowed password.
17 | Password string
18 |
19 | // Vhost is used for CloudAMQP connections to set the specific vhost.
20 | Vhost string
21 |
22 | // UseTLS defines whether we use amqp or amqps protocol.
23 | UseTLS bool
24 |
25 | // Mode will specify whether logs are enabled or not.
26 | Mode string
27 |
28 | // Marshaller defines the content type used for messages and how they're marshalled (default: JSON).
29 | Marshaller Marshaller
30 | }
31 |
32 | // DefaultManagerOptions will return a ManagerOptions with default values.
33 | func DefaultManagerOptions() *ManagerOptions {
34 | return &ManagerOptions{
35 | Host: defaultHost,
36 | Port: defaultPort,
37 | Username: defaultUsername,
38 | Password: defaultPassword,
39 | Vhost: defaultVhost,
40 | UseTLS: defaultUseTLS,
41 | Mode: defaultMode,
42 | Marshaller: defaultMarshaller,
43 | }
44 | }
45 |
46 | // NewManagerOptions is the exported builder for a ManagerOptions and will offer setter methods for an easy construction.
47 | // Any non-assigned field will be set to default through DefaultManagerOptions.
48 | func NewManagerOptions() *ManagerOptions {
49 | return DefaultManagerOptions()
50 | }
51 |
52 | // NewManagerOptionsFromEnv will generate a ManagerOptions from environment variables. Empty values will be taken as default
53 | // through the DefaultManagerOptions.
54 | func NewManagerOptionsFromEnv() *ManagerOptions {
55 | defaultOpts := DefaultManagerOptions()
56 |
57 | fromEnv := new(RabbitMQEnvs)
58 |
59 | _, err := env.UnmarshalFromEnviron(fromEnv)
60 | if err != nil {
61 | return defaultOpts
62 | }
63 |
64 | if fromEnv.Host != "" {
65 | defaultOpts.Host = fromEnv.Host
66 | }
67 |
68 | if fromEnv.Port > 0 {
69 | defaultOpts.Port = fromEnv.Port
70 | }
71 |
72 | if fromEnv.Username != "" {
73 | defaultOpts.Username = fromEnv.Username
74 | }
75 |
76 | if fromEnv.Password != "" {
77 | defaultOpts.Password = fromEnv.Password
78 | }
79 |
80 | if fromEnv.Vhost != "" {
81 | defaultOpts.Vhost = fromEnv.Vhost
82 | }
83 |
84 | defaultOpts.UseTLS = fromEnv.UseTLS
85 |
86 | return defaultOpts
87 | }
88 |
89 | // SetHost will assign the host.
90 | func (m *ManagerOptions) SetHost(host string) *ManagerOptions {
91 | m.Host = host
92 |
93 | return m
94 | }
95 |
96 | // SetPort will assign the port.
97 | func (m *ManagerOptions) SetPort(port uint) *ManagerOptions {
98 | m.Port = port
99 |
100 | return m
101 | }
102 |
103 | // SetCredentials will assign the username and password.
104 | func (m *ManagerOptions) SetCredentials(username, password string) *ManagerOptions {
105 | m.Username = username
106 | m.Password = password
107 |
108 | return m
109 | }
110 |
111 | // SetVhost will assign the Vhost.
112 | func (m *ManagerOptions) SetVhost(vhost string) *ManagerOptions {
113 | m.Vhost = vhost
114 |
115 | return m
116 | }
117 |
118 | // SetUseTLS will assign the UseTLS status.
119 | func (m *ManagerOptions) SetUseTLS(use bool) *ManagerOptions {
120 | m.UseTLS = use
121 |
122 | return m
123 | }
124 |
125 | // SetMode will assign the mode if valid.
126 | func (m *ManagerOptions) SetMode(mode string) *ManagerOptions {
127 | if isValidMode(mode) {
128 | m.Mode = mode
129 | }
130 |
131 | return m
132 | }
133 |
134 | // SetMarshaller will assign the Marshaller.
135 | func (m *ManagerOptions) SetMarshaller(marshaller Marshaller) *ManagerOptions {
136 | m.Marshaller = marshaller
137 |
138 | return m
139 | }
140 |
--------------------------------------------------------------------------------
/manager_test.go:
--------------------------------------------------------------------------------
1 | package gorabbit_test
2 |
3 | import (
4 | "context"
5 | "log"
6 | "strconv"
7 | "strings"
8 | "testing"
9 | "time"
10 |
11 | "github.com/KardinalAI/gorabbit"
12 | amqp "github.com/rabbitmq/amqp091-go"
13 | "github.com/stretchr/testify/assert"
14 | "github.com/stretchr/testify/require"
15 | "github.com/stretchr/testify/suite"
16 | "github.com/testcontainers/testcontainers-go"
17 | "github.com/testcontainers/testcontainers-go/modules/rabbitmq"
18 | )
19 |
20 | type RabbitMQContainer struct {
21 | *rabbitmq.RabbitMQContainer
22 | ContainerHost string
23 | ContainerPort uint
24 | Username string
25 | Password string
26 | }
27 |
28 | func CreateRabbitMQContainer(ctx context.Context) (*RabbitMQContainer, error) {
29 | rContainer, err := rabbitmq.RunContainer(ctx,
30 | testcontainers.WithImage("rabbitmq:3.12.11-management-alpine"),
31 | rabbitmq.WithAdminUsername("guest"),
32 | rabbitmq.WithAdminPassword("guest"),
33 | )
34 |
35 | if err != nil {
36 | return nil, err
37 | }
38 |
39 | ep, err := rContainer.AmqpURL(ctx)
40 | if err != nil {
41 | return nil, err
42 | }
43 |
44 | uri, err := amqp.ParseURI(ep)
45 | if err != nil {
46 | return nil, err
47 | }
48 |
49 | rabbitMQContainer := &RabbitMQContainer{
50 | RabbitMQContainer: rContainer,
51 | ContainerHost: uri.Host,
52 | ContainerPort: uint(uri.Port),
53 | Username: uri.Username,
54 | Password: uri.Password,
55 | }
56 |
57 | return rabbitMQContainer, nil
58 | }
59 |
60 | type ManagerTestSuite struct {
61 | suite.Suite
62 | rabbitMQContainer *RabbitMQContainer
63 | ctx context.Context
64 | }
65 |
66 | func (suite *ManagerTestSuite) SetupSuite() {
67 | suite.ctx = context.Background()
68 |
69 | rContainer, err := CreateRabbitMQContainer(suite.ctx)
70 | if err != nil {
71 | log.Fatal(err)
72 | }
73 |
74 | suite.rabbitMQContainer = rContainer
75 | }
76 |
77 | func (suite *ManagerTestSuite) TearDownSuite() {
78 | if err := suite.rabbitMQContainer.Terminate(suite.ctx); err != nil {
79 | log.Fatalf("error terminating RabbitMQ container: %s", err)
80 | }
81 | }
82 |
83 | func (suite *ManagerTestSuite) TestNewManager() {
84 | t := suite.T()
85 |
86 | t.Run("Instantiating new manager with correct parameters", func(t *testing.T) {
87 | managerOpts := gorabbit.NewManagerOptions().
88 | SetHost(suite.rabbitMQContainer.ContainerHost).
89 | SetPort(suite.rabbitMQContainer.ContainerPort).
90 | SetCredentials(suite.rabbitMQContainer.Username, suite.rabbitMQContainer.Password)
91 |
92 | manager, err := gorabbit.NewManager(managerOpts)
93 |
94 | require.NoError(t, err)
95 | assert.NotNil(t, manager)
96 |
97 | assert.NotEmpty(t, manager.GetHost())
98 | assert.NotZero(t, manager.GetPort())
99 | assert.NotEmpty(t, manager.GetUsername())
100 |
101 | require.NoError(t, manager.Disconnect())
102 | })
103 |
104 | t.Run("Instantiating new manager with incorrect credentials", func(t *testing.T) {
105 | managerOpts := gorabbit.NewManagerOptions().
106 | SetHost(suite.rabbitMQContainer.ContainerHost).
107 | SetPort(suite.rabbitMQContainer.ContainerPort).
108 | SetCredentials("bad", "password")
109 |
110 | manager, err := gorabbit.NewManager(managerOpts)
111 |
112 | require.Error(t, err)
113 | assert.Equal(t, "Exception (403) Reason: \"username or password not allowed\"", err.Error())
114 | assert.NotNil(t, manager)
115 |
116 | // Running any operation at that point should fail, except for disconnecting
117 | require.NoError(t, manager.Disconnect())
118 |
119 | err = manager.CreateQueue(gorabbit.QueueConfig{})
120 |
121 | require.Error(t, err)
122 | assert.Equal(t, "connection is closed", err.Error())
123 | })
124 |
125 | t.Run("Instantiating new manager with incorrect host", func(t *testing.T) {
126 | managerOpts := gorabbit.NewManagerOptions().
127 | SetHost("incorrect_host").
128 | SetPort(suite.rabbitMQContainer.ContainerPort).
129 | SetCredentials(suite.rabbitMQContainer.Username, suite.rabbitMQContainer.Password)
130 |
131 | manager, err := gorabbit.NewManager(managerOpts)
132 |
133 | require.Error(t, err)
134 |
135 | hasDialPrefix := strings.HasPrefix(err.Error(), "dial tcp: lookup incorrect_host on")
136 | assert.True(t, hasDialPrefix)
137 |
138 | hasDialSuffix := strings.HasSuffix(err.Error(), "server misbehaving")
139 | assert.True(t, hasDialSuffix)
140 |
141 | assert.NotNil(t, manager)
142 |
143 | // Running any operation at that point should fail, except for disconnecting
144 | require.NoError(t, manager.Disconnect())
145 |
146 | err = manager.CreateQueue(gorabbit.QueueConfig{})
147 |
148 | require.Error(t, err)
149 | assert.Equal(t, "connection is closed", err.Error())
150 | })
151 |
152 | t.Run("Instantiating new manager with incorrect port", func(t *testing.T) {
153 | managerOpts := gorabbit.NewManagerOptions().
154 | SetHost(suite.rabbitMQContainer.ContainerHost).
155 | SetPort(uint(123)).
156 | SetCredentials(suite.rabbitMQContainer.Username, suite.rabbitMQContainer.Password)
157 |
158 | manager, err := gorabbit.NewManager(managerOpts)
159 |
160 | require.Error(t, err)
161 |
162 | hasDialPrefix := strings.HasPrefix(err.Error(), "dial tcp")
163 | assert.True(t, hasDialPrefix)
164 |
165 | hasDialSuffix := strings.HasSuffix(err.Error(), "connection refused")
166 | assert.True(t, hasDialSuffix)
167 |
168 | assert.NotNil(t, manager)
169 |
170 | // Running any operation at that point should fail, except for disconnecting
171 | require.NoError(t, manager.Disconnect())
172 |
173 | err = manager.CreateQueue(gorabbit.QueueConfig{})
174 |
175 | require.Error(t, err)
176 | assert.Equal(t, "connection is closed", err.Error())
177 | })
178 | }
179 |
180 | func (suite *ManagerTestSuite) TestNewFromEnv() {
181 | t := suite.T()
182 |
183 | t.Setenv("RABBITMQ_HOST", suite.rabbitMQContainer.ContainerHost)
184 | t.Setenv("RABBITMQ_PORT", strconv.Itoa(int(suite.rabbitMQContainer.ContainerPort)))
185 | t.Setenv("RABBITMQ_USERNAME", suite.rabbitMQContainer.Username)
186 | t.Setenv("RABBITMQ_PASSWORD", suite.rabbitMQContainer.Password)
187 |
188 | manager, err := gorabbit.NewManagerFromEnv()
189 |
190 | require.NoError(t, err)
191 | assert.NotNil(t, manager)
192 |
193 | require.NoError(t, manager.Disconnect())
194 |
195 | t.Run("Instantiating a manager with disabled flag", func(t *testing.T) {
196 | t.Setenv("GORABBIT_DISABLED", "true")
197 |
198 | manager, err = gorabbit.NewManagerFromEnv()
199 |
200 | require.NoError(t, err)
201 | assert.NotNil(t, manager)
202 |
203 | assert.True(t, manager.IsDisabled())
204 | })
205 | }
206 |
207 | func (suite *ManagerTestSuite) TestCreateQueue() {
208 | t := suite.T()
209 |
210 | t.Setenv("RABBITMQ_HOST", suite.rabbitMQContainer.ContainerHost)
211 | t.Setenv("RABBITMQ_PORT", strconv.Itoa(int(suite.rabbitMQContainer.ContainerPort)))
212 | t.Setenv("RABBITMQ_USERNAME", suite.rabbitMQContainer.Username)
213 | t.Setenv("RABBITMQ_PASSWORD", suite.rabbitMQContainer.Password)
214 |
215 | manager, err := gorabbit.NewManagerFromEnv()
216 |
217 | require.NoError(t, err)
218 | assert.NotNil(t, manager)
219 |
220 | t.Run("Creating queue with valid parameters", func(t *testing.T) {
221 | queueConfig := gorabbit.QueueConfig{
222 | Name: "test_queue",
223 | Durable: true,
224 | Exclusive: false,
225 | }
226 |
227 | err = manager.CreateQueue(queueConfig)
228 |
229 | require.NoError(t, err)
230 |
231 | err = manager.DeleteQueue(queueConfig.Name)
232 |
233 | require.NoError(t, err)
234 | })
235 |
236 | t.Run("Creating queue with empty name should work", func(t *testing.T) {
237 | queueConfig := gorabbit.QueueConfig{}
238 |
239 | err = manager.CreateQueue(queueConfig)
240 |
241 | require.NoError(t, err)
242 |
243 | err = manager.DeleteQueue(queueConfig.Name)
244 |
245 | require.NoError(t, err)
246 | })
247 |
248 | t.Run("Creating queue with bindings but non-existent exchange", func(t *testing.T) {
249 | queueConfig := gorabbit.QueueConfig{
250 | Name: "test_queue",
251 | Durable: true,
252 | Exclusive: false,
253 | Bindings: []gorabbit.BindingConfig{
254 | {
255 | RoutingKey: "routing_key",
256 | Exchange: "test_exchange",
257 | },
258 | },
259 | }
260 |
261 | err = manager.CreateQueue(queueConfig)
262 |
263 | require.Error(t, err)
264 | assert.Equal(t, "Exception (404) Reason: \"NOT_FOUND - no exchange 'test_exchange' in vhost '/'\"", err.Error())
265 | })
266 |
267 | t.Run("Creating queue with bindings and existing exchange", func(t *testing.T) {
268 | exchangeConfig := gorabbit.ExchangeConfig{
269 | Name: "test_exchange_with_bindings",
270 | Type: "topic",
271 | }
272 |
273 | err = manager.CreateExchange(exchangeConfig)
274 |
275 | require.NoError(t, err)
276 |
277 | queueConfig := gorabbit.QueueConfig{
278 | Name: "test_queue",
279 | Durable: true,
280 | Exclusive: false,
281 | Bindings: []gorabbit.BindingConfig{
282 | {
283 | RoutingKey: "routing_key",
284 | Exchange: "test_exchange_with_bindings",
285 | },
286 | },
287 | }
288 |
289 | err = manager.CreateQueue(queueConfig)
290 |
291 | require.NoError(t, err)
292 |
293 | err = manager.DeleteExchange(exchangeConfig.Name)
294 |
295 | require.NoError(t, err)
296 |
297 | err = manager.DeleteQueue(queueConfig.Name)
298 |
299 | require.NoError(t, err)
300 | })
301 |
302 | require.NoError(t, manager.Disconnect())
303 | }
304 |
305 | func (suite *ManagerTestSuite) TestCreateExchange() {
306 | t := suite.T()
307 |
308 | t.Setenv("RABBITMQ_HOST", suite.rabbitMQContainer.ContainerHost)
309 | t.Setenv("RABBITMQ_PORT", strconv.Itoa(int(suite.rabbitMQContainer.ContainerPort)))
310 | t.Setenv("RABBITMQ_USERNAME", suite.rabbitMQContainer.Username)
311 | t.Setenv("RABBITMQ_PASSWORD", suite.rabbitMQContainer.Password)
312 |
313 | manager, err := gorabbit.NewManagerFromEnv()
314 |
315 | require.NoError(t, err)
316 | assert.NotNil(t, manager)
317 |
318 | t.Run("Creating exchange with valid parameters", func(t *testing.T) {
319 | exchangeConfig := gorabbit.ExchangeConfig{
320 | Name: "test_exchange",
321 | Type: gorabbit.ExchangeTypeTopic,
322 | }
323 |
324 | err = manager.CreateExchange(exchangeConfig)
325 |
326 | require.NoError(t, err)
327 |
328 | err = manager.DeleteExchange(exchangeConfig.Name)
329 |
330 | require.NoError(t, err)
331 | })
332 |
333 | t.Run("Creating exchange with empty parameters", func(t *testing.T) {
334 | exchangeConfig := gorabbit.ExchangeConfig{}
335 |
336 | err = manager.CreateExchange(exchangeConfig)
337 |
338 | require.Error(t, err)
339 | assert.Equal(t, "Exception (503) Reason: \"COMMAND_INVALID - invalid exchange type ''\"", err.Error())
340 |
341 | // By now the manager's connection should be closed
342 | err = manager.CreateExchange(exchangeConfig)
343 |
344 | require.Error(t, err)
345 | assert.Equal(t, "connection is closed", err.Error())
346 |
347 | manager, err = gorabbit.NewManagerFromEnv()
348 |
349 | require.NoError(t, err)
350 | assert.NotNil(t, manager)
351 | })
352 |
353 | t.Run("Creating exchange with empty name", func(t *testing.T) {
354 | exchangeConfig := gorabbit.ExchangeConfig{
355 | Name: "",
356 | Type: gorabbit.ExchangeTypeTopic,
357 | }
358 |
359 | err = manager.CreateExchange(exchangeConfig)
360 |
361 | require.Error(t, err)
362 | assert.Equal(t, "Exception (403) Reason: \"ACCESS_REFUSED - operation not permitted on the default exchange\"", err.Error())
363 |
364 | manager, err = gorabbit.NewManagerFromEnv()
365 |
366 | require.NoError(t, err)
367 | assert.NotNil(t, manager)
368 | })
369 |
370 | t.Run("Creating all type of exchanges", func(t *testing.T) {
371 | topicExchange := gorabbit.ExchangeConfig{
372 | Name: "topic_exchange",
373 | Type: gorabbit.ExchangeTypeTopic,
374 | }
375 |
376 | err = manager.CreateExchange(topicExchange)
377 |
378 | require.NoError(t, err)
379 |
380 | directExchange := gorabbit.ExchangeConfig{
381 | Name: "direct_exchange",
382 | Type: gorabbit.ExchangeTypeDirect,
383 | }
384 |
385 | err = manager.CreateExchange(directExchange)
386 |
387 | require.NoError(t, err)
388 |
389 | fanoutExchange := gorabbit.ExchangeConfig{
390 | Name: "fanout_exchange",
391 | Type: gorabbit.ExchangeTypeFanout,
392 | }
393 |
394 | err = manager.CreateExchange(fanoutExchange)
395 |
396 | require.NoError(t, err)
397 |
398 | headersExchange := gorabbit.ExchangeConfig{
399 | Name: "headers_exchange",
400 | Type: gorabbit.ExchangeTypeHeaders,
401 | }
402 |
403 | err = manager.CreateExchange(headersExchange)
404 |
405 | require.NoError(t, err)
406 |
407 | require.NoError(t, manager.DeleteExchange(topicExchange.Name))
408 | require.NoError(t, manager.DeleteExchange(directExchange.Name))
409 | require.NoError(t, manager.DeleteExchange(fanoutExchange.Name))
410 | require.NoError(t, manager.DeleteExchange(headersExchange.Name))
411 | })
412 |
413 | require.NoError(t, manager.Disconnect())
414 | }
415 |
416 | func (suite *ManagerTestSuite) TestBindExchangeToQueueViaRoutingKey() {
417 | t := suite.T()
418 |
419 | t.Setenv("RABBITMQ_HOST", suite.rabbitMQContainer.ContainerHost)
420 | t.Setenv("RABBITMQ_PORT", strconv.Itoa(int(suite.rabbitMQContainer.ContainerPort)))
421 | t.Setenv("RABBITMQ_USERNAME", suite.rabbitMQContainer.Username)
422 | t.Setenv("RABBITMQ_PASSWORD", suite.rabbitMQContainer.Password)
423 |
424 | manager, err := gorabbit.NewManagerFromEnv()
425 |
426 | require.NoError(t, err)
427 | assert.NotNil(t, manager)
428 |
429 | queueConfig := gorabbit.QueueConfig{
430 | Name: "test_queue",
431 | Durable: true,
432 | Exclusive: false,
433 | }
434 |
435 | err = manager.CreateQueue(queueConfig)
436 |
437 | require.NoError(t, err)
438 |
439 | exchangeConfig := gorabbit.ExchangeConfig{
440 | Name: "test_exchange",
441 | Type: "topic",
442 | }
443 |
444 | err = manager.CreateExchange(exchangeConfig)
445 |
446 | require.NoError(t, err)
447 |
448 | t.Run("Binding existing exchange to existing queue via routing key", func(t *testing.T) {
449 | err = manager.BindExchangeToQueueViaRoutingKey(exchangeConfig.Name, queueConfig.Name, "routing_key")
450 |
451 | require.NoError(t, err)
452 | })
453 |
454 | t.Run("Binding non-existing exchange to existing queue via routing key", func(t *testing.T) {
455 | err = manager.BindExchangeToQueueViaRoutingKey("non_existing_exchange", queueConfig.Name, "routing_key")
456 |
457 | require.Error(t, err)
458 | assert.Equal(t, "Exception (404) Reason: \"NOT_FOUND - no exchange 'non_existing_exchange' in vhost '/'\"", err.Error())
459 | })
460 |
461 | t.Run("Binding existing exchange to non-existing queue via routing key", func(t *testing.T) {
462 | err = manager.BindExchangeToQueueViaRoutingKey(exchangeConfig.Name, "non_existing_queue", "routing_key")
463 |
464 | require.Error(t, err)
465 | assert.Equal(t, "Exception (404) Reason: \"NOT_FOUND - no queue 'non_existing_queue' in vhost '/'\"", err.Error())
466 | })
467 |
468 | require.NoError(t, manager.DeleteQueue(exchangeConfig.Name))
469 | require.NoError(t, manager.DeleteQueue(queueConfig.Name))
470 |
471 | require.NoError(t, manager.Disconnect())
472 | }
473 |
474 | func (suite *ManagerTestSuite) TestGetNumberOfMessages() {
475 | t := suite.T()
476 |
477 | t.Setenv("RABBITMQ_HOST", suite.rabbitMQContainer.ContainerHost)
478 | t.Setenv("RABBITMQ_PORT", strconv.Itoa(int(suite.rabbitMQContainer.ContainerPort)))
479 | t.Setenv("RABBITMQ_USERNAME", suite.rabbitMQContainer.Username)
480 | t.Setenv("RABBITMQ_PASSWORD", suite.rabbitMQContainer.Password)
481 |
482 | manager, err := gorabbit.NewManagerFromEnv()
483 |
484 | require.NoError(t, err)
485 | assert.NotNil(t, manager)
486 |
487 | t.Run("Getting the number of messages from existing queue", func(t *testing.T) {
488 | queueConfig := gorabbit.QueueConfig{
489 | Name: "test_queue",
490 | Durable: true,
491 | Exclusive: false,
492 | }
493 |
494 | err = manager.CreateQueue(queueConfig)
495 |
496 | require.NoError(t, err)
497 |
498 | exchangeConfig := gorabbit.ExchangeConfig{
499 | Name: "test_exchange",
500 | Type: "topic",
501 | }
502 |
503 | err = manager.CreateExchange(exchangeConfig)
504 |
505 | require.NoError(t, err)
506 |
507 | err = manager.BindExchangeToQueueViaRoutingKey(exchangeConfig.Name, queueConfig.Name, "routing_key")
508 |
509 | require.NoError(t, err)
510 |
511 | count, countErr := manager.GetNumberOfMessages(queueConfig.Name)
512 |
513 | require.NoError(t, countErr)
514 | assert.Zero(t, count)
515 |
516 | require.NoError(t, manager.DeleteExchange(exchangeConfig.Name))
517 | require.NoError(t, manager.DeleteQueue(queueConfig.Name))
518 | })
519 |
520 | t.Run("Getting the number of messages from non-existing queue", func(t *testing.T) {
521 | count, countErr := manager.GetNumberOfMessages("non_existing_queue")
522 |
523 | require.Error(t, countErr)
524 | assert.Equal(t, "Exception (404) Reason: \"NOT_FOUND - no queue 'non_existing_queue' in vhost '/'\"", countErr.Error())
525 | assert.Equal(t, -1, count)
526 | })
527 |
528 | require.NoError(t, manager.Disconnect())
529 | }
530 |
531 | func (suite *ManagerTestSuite) TestPushMessageToExchange() {
532 | t := suite.T()
533 |
534 | t.Setenv("RABBITMQ_HOST", suite.rabbitMQContainer.ContainerHost)
535 | t.Setenv("RABBITMQ_PORT", strconv.Itoa(int(suite.rabbitMQContainer.ContainerPort)))
536 | t.Setenv("RABBITMQ_USERNAME", suite.rabbitMQContainer.Username)
537 | t.Setenv("RABBITMQ_PASSWORD", suite.rabbitMQContainer.Password)
538 |
539 | manager, err := gorabbit.NewManagerFromEnv()
540 |
541 | require.NoError(t, err)
542 | assert.NotNil(t, manager)
543 |
544 | t.Run("Push message to exchange", func(t *testing.T) {
545 | queueConfig := gorabbit.QueueConfig{
546 | Name: "test_queue",
547 | Durable: true,
548 | Exclusive: false,
549 | }
550 |
551 | err = manager.CreateQueue(queueConfig)
552 |
553 | require.NoError(t, err)
554 |
555 | exchangeConfig := gorabbit.ExchangeConfig{
556 | Name: "test_exchange",
557 | Type: "topic",
558 | }
559 |
560 | err = manager.CreateExchange(exchangeConfig)
561 |
562 | require.NoError(t, err)
563 |
564 | err = manager.BindExchangeToQueueViaRoutingKey(exchangeConfig.Name, queueConfig.Name, "routing_key")
565 |
566 | require.NoError(t, err)
567 |
568 | err = manager.PushMessageToExchange(exchangeConfig.Name, "routing_key", "Some message")
569 |
570 | // Small sleep for allowing message to be sent.
571 | time.Sleep(50 * time.Millisecond)
572 |
573 | require.NoError(t, err)
574 |
575 | count, countErr := manager.GetNumberOfMessages(queueConfig.Name)
576 |
577 | require.NoError(t, countErr)
578 | assert.Equal(t, 1, count)
579 |
580 | require.NoError(t, manager.PurgeQueue(queueConfig.Name))
581 |
582 | count, countErr = manager.GetNumberOfMessages(queueConfig.Name)
583 |
584 | require.NoError(t, countErr)
585 | assert.Zero(t, count)
586 |
587 | require.NoError(t, manager.DeleteExchange(exchangeConfig.Name))
588 | require.NoError(t, manager.DeleteQueue(queueConfig.Name))
589 | })
590 |
591 | t.Run("Pushing message to non-existing exchange should still work", func(t *testing.T) {
592 | err = manager.PushMessageToExchange("non_existing_exchange", "routing_key", "Some message")
593 |
594 | // Small sleep for allowing message to be sent.
595 | time.Sleep(50 * time.Millisecond)
596 |
597 | require.NoError(t, err)
598 | })
599 |
600 | require.NoError(t, manager.Disconnect())
601 | }
602 |
603 | func (suite *ManagerTestSuite) TestPopMessageFromQueue() {
604 | t := suite.T()
605 |
606 | t.Setenv("RABBITMQ_HOST", suite.rabbitMQContainer.ContainerHost)
607 | t.Setenv("RABBITMQ_PORT", strconv.Itoa(int(suite.rabbitMQContainer.ContainerPort)))
608 | t.Setenv("RABBITMQ_USERNAME", suite.rabbitMQContainer.Username)
609 | t.Setenv("RABBITMQ_PASSWORD", suite.rabbitMQContainer.Password)
610 |
611 | manager, err := gorabbit.NewManagerFromEnv()
612 |
613 | require.NoError(t, err)
614 | assert.NotNil(t, manager)
615 |
616 | t.Run("Push message to exchange and consume it", func(t *testing.T) {
617 | queueConfig := gorabbit.QueueConfig{
618 | Name: "test_queue",
619 | Durable: true,
620 | Exclusive: false,
621 | }
622 |
623 | err = manager.CreateQueue(queueConfig)
624 |
625 | require.NoError(t, err)
626 |
627 | exchangeConfig := gorabbit.ExchangeConfig{
628 | Name: "test_exchange",
629 | Type: "topic",
630 | }
631 |
632 | err = manager.CreateExchange(exchangeConfig)
633 |
634 | require.NoError(t, err)
635 |
636 | err = manager.BindExchangeToQueueViaRoutingKey(exchangeConfig.Name, queueConfig.Name, "routing_key")
637 |
638 | require.NoError(t, err)
639 |
640 | err = manager.PushMessageToExchange(exchangeConfig.Name, "routing_key", "Some message")
641 |
642 | // Small sleep for allowing message to be sent.
643 | time.Sleep(50 * time.Millisecond)
644 |
645 | require.NoError(t, err)
646 |
647 | count, countErr := manager.GetNumberOfMessages(queueConfig.Name)
648 |
649 | require.NoError(t, countErr)
650 | assert.Equal(t, 1, count)
651 |
652 | delivery, popErr := manager.PopMessageFromQueue(queueConfig.Name, true)
653 |
654 | require.NoError(t, popErr)
655 | assert.Equal(t, "\"Some message\"", string(delivery.Body))
656 |
657 | require.NoError(t, manager.PurgeQueue(queueConfig.Name))
658 |
659 | count, countErr = manager.GetNumberOfMessages(queueConfig.Name)
660 |
661 | require.NoError(t, countErr)
662 | assert.Zero(t, count)
663 |
664 | require.NoError(t, manager.DeleteExchange(exchangeConfig.Name))
665 | require.NoError(t, manager.DeleteQueue(queueConfig.Name))
666 | })
667 |
668 | t.Run("Popping message from non-existing queue", func(t *testing.T) {
669 | delivery, popErr := manager.PopMessageFromQueue("non_existing_queue", true)
670 |
671 | require.Error(t, popErr)
672 | assert.Nil(t, delivery)
673 | assert.Equal(t, "Exception (404) Reason: \"NOT_FOUND - no queue 'non_existing_queue' in vhost '/'\"", popErr.Error())
674 | })
675 |
676 | t.Run("Popping message from existent empty queue", func(t *testing.T) {
677 | queueConfig := gorabbit.QueueConfig{
678 | Name: "test_queue",
679 | Durable: true,
680 | Exclusive: false,
681 | }
682 |
683 | err = manager.CreateQueue(queueConfig)
684 |
685 | require.NoError(t, err)
686 |
687 | delivery, popErr := manager.PopMessageFromQueue(queueConfig.Name, true)
688 |
689 | require.Error(t, popErr)
690 | require.Equal(t, "queue is empty", popErr.Error())
691 | assert.Nil(t, delivery)
692 |
693 | require.NoError(t, manager.DeleteQueue(queueConfig.Name))
694 | })
695 |
696 | require.NoError(t, manager.Disconnect())
697 | }
698 |
699 | func (suite *ManagerTestSuite) TestSetupFromDefinitions() {
700 | t := suite.T()
701 |
702 | t.Setenv("RABBITMQ_HOST", suite.rabbitMQContainer.ContainerHost)
703 | t.Setenv("RABBITMQ_PORT", strconv.Itoa(int(suite.rabbitMQContainer.ContainerPort)))
704 | t.Setenv("RABBITMQ_USERNAME", suite.rabbitMQContainer.Username)
705 | t.Setenv("RABBITMQ_PASSWORD", suite.rabbitMQContainer.Password)
706 |
707 | manager, err := gorabbit.NewManagerFromEnv()
708 |
709 | require.NoError(t, err)
710 | assert.NotNil(t, manager)
711 |
712 | t.Run("Setting up from definitions with wrong path", func(t *testing.T) {
713 | err = manager.SetupFromDefinitions("wrong-path.json")
714 |
715 | require.Error(t, err)
716 | assert.Equal(t, "open wrong-path.json: no such file or directory", err.Error())
717 | })
718 |
719 | t.Run("Setting up from definitions with right path", func(t *testing.T) {
720 | err = manager.SetupFromDefinitions("assets/definitions.example.json")
721 |
722 | require.NoError(t, err)
723 | })
724 |
725 | require.NoError(t, manager.Disconnect())
726 | }
727 |
728 | func TestManagerTestSuite(t *testing.T) {
729 | suite.Run(t, new(ManagerTestSuite))
730 | }
731 |
--------------------------------------------------------------------------------
/marshalling.go:
--------------------------------------------------------------------------------
1 | package gorabbit
2 |
3 | import (
4 | "encoding/json"
5 | "fmt"
6 | )
7 |
8 | type Marshaller interface {
9 | ContentType() string
10 | Marshal(data any) ([]byte, error)
11 | }
12 |
13 | type marshaller struct {
14 | contentType string
15 | marshal func(data any) ([]byte, error)
16 | }
17 |
18 | func (m *marshaller) ContentType() string {
19 | return m.contentType
20 | }
21 |
22 | func (m *marshaller) Marshal(data any) ([]byte, error) {
23 | return m.marshal(data)
24 | }
25 |
26 | func NewJSONMarshaller() Marshaller {
27 | return &marshaller{
28 | contentType: "application/json",
29 | marshal: json.Marshal,
30 | }
31 | }
32 |
33 | func NewTextMarshaller() Marshaller {
34 | return &marshaller{
35 | contentType: "text/plain",
36 | marshal: func(data any) ([]byte, error) {
37 | switch s := data.(type) {
38 | case string:
39 | return []byte(s), nil
40 | case []byte:
41 | return s, nil
42 | default:
43 | return nil, fmt.Errorf("cannot marshal %T as text", data)
44 | }
45 | },
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/marshalling_test.go:
--------------------------------------------------------------------------------
1 | package gorabbit_test
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/KardinalAI/gorabbit"
7 | "github.com/stretchr/testify/assert"
8 | "github.com/stretchr/testify/require"
9 | )
10 |
11 | func TestJSONMarshaller(t *testing.T) {
12 | m := gorabbit.NewJSONMarshaller()
13 | assert.NotNil(t, m)
14 |
15 | assert.Equal(t, "application/json", m.ContentType())
16 |
17 | data, err := m.Marshal("test")
18 | require.NoError(t, err)
19 | assert.Equal(t, []byte(`"test"`), data)
20 | }
21 |
22 | func TestTextMarshaller(t *testing.T) {
23 | m := gorabbit.NewTextMarshaller()
24 | assert.NotNil(t, m)
25 |
26 | assert.Equal(t, "text/plain", m.ContentType())
27 |
28 | data, err := m.Marshal("test")
29 | require.NoError(t, err)
30 | assert.Equal(t, []byte(`test`), data)
31 | }
32 |
--------------------------------------------------------------------------------
/model.go:
--------------------------------------------------------------------------------
1 | package gorabbit
2 |
3 | import (
4 | "strconv"
5 | "time"
6 |
7 | amqp "github.com/rabbitmq/amqp091-go"
8 | )
9 |
10 | type SchemaDefinitions struct {
11 | Exchanges []struct {
12 | Name string `json:"name"`
13 | Vhost string `json:"vhost"`
14 | Type string `json:"type"`
15 | Durable bool `json:"durable"`
16 | AutoDelete bool `json:"auto_delete"`
17 | Internal bool `json:"internal"`
18 | Arguments struct {
19 | } `json:"arguments"`
20 | } `json:"exchanges"`
21 | Queues []struct {
22 | Name string `json:"name"`
23 | Vhost string `json:"vhost"`
24 | Durable bool `json:"durable"`
25 | AutoDelete bool `json:"auto_delete"`
26 | Arguments struct {
27 | } `json:"arguments"`
28 | } `json:"queues"`
29 | Bindings []struct {
30 | Source string `json:"source"`
31 | Vhost string `json:"vhost"`
32 | Destination string `json:"destination"`
33 | DestinationType string `json:"destination_type"`
34 | RoutingKey string `json:"routing_key"`
35 | Arguments struct {
36 | } `json:"arguments"`
37 | } `json:"bindings"`
38 | }
39 |
40 | type ExchangeConfig struct {
41 | Name string `yaml:"name"`
42 | Type ExchangeType `yaml:"type"`
43 | Persisted bool `yaml:"persisted"`
44 | Args map[string]interface{} `yaml:"args"`
45 | }
46 |
47 | type QueueConfig struct {
48 | Name string `yaml:"name"`
49 | Durable bool `yaml:"durable"`
50 | Exclusive bool `yaml:"exclusive"`
51 | AutoDelete bool `yaml:"autoDelete"`
52 | Args map[string]interface{} `yaml:"args"`
53 | Bindings []BindingConfig `yaml:"bindings"`
54 | }
55 |
56 | type BindingConfig struct {
57 | RoutingKey string `yaml:"routing_key"`
58 | Exchange string `yaml:"exchange"`
59 | }
60 |
61 | type PublishingOptions struct {
62 | MessagePriority *MessagePriority
63 | DeliveryMode *DeliveryMode
64 | TTL *time.Duration
65 | }
66 |
67 | func SendOptions() *PublishingOptions {
68 | return &PublishingOptions{}
69 | }
70 |
71 | func (m *PublishingOptions) priority() uint8 {
72 | if m.MessagePriority == nil {
73 | return PriorityMedium.Uint8()
74 | }
75 |
76 | return m.MessagePriority.Uint8()
77 | }
78 |
79 | func (m *PublishingOptions) mode() uint8 {
80 | if m.DeliveryMode == nil {
81 | return Persistent.Uint8()
82 | }
83 |
84 | return m.DeliveryMode.Uint8()
85 | }
86 |
87 | func (m *PublishingOptions) ttl() string {
88 | if m.TTL == nil {
89 | return ""
90 | }
91 |
92 | return strconv.FormatInt(m.TTL.Milliseconds(), 10)
93 | }
94 |
95 | func (m *PublishingOptions) SetPriority(priority MessagePriority) *PublishingOptions {
96 | m.MessagePriority = &priority
97 |
98 | return m
99 | }
100 |
101 | func (m *PublishingOptions) SetMode(mode DeliveryMode) *PublishingOptions {
102 | m.DeliveryMode = &mode
103 |
104 | return m
105 | }
106 |
107 | func (m *PublishingOptions) SetTTL(ttl time.Duration) *PublishingOptions {
108 | m.TTL = &ttl
109 |
110 | return m
111 | }
112 |
113 | type consumptionHealth map[string]bool
114 |
115 | func (s consumptionHealth) IsHealthy() bool {
116 | for _, v := range s {
117 | if !v {
118 | return false
119 | }
120 | }
121 |
122 | return true
123 | }
124 |
125 | func (s consumptionHealth) AddSubscription(queue string, err error) {
126 | s[queue] = err == nil
127 | }
128 |
129 | type mqttPublishing struct {
130 | Exchange string
131 | RoutingKey string
132 | Mandatory bool
133 | Immediate bool
134 | Msg amqp.Publishing
135 | }
136 |
137 | func (m mqttPublishing) HashCode() string {
138 | return m.Msg.MessageId
139 | }
140 |
141 | type RabbitMQEnvs struct {
142 | Host string `env:"RABBITMQ_HOST"`
143 | Port uint `env:"RABBITMQ_PORT"`
144 | Username string `env:"RABBITMQ_USERNAME"`
145 | Password string `env:"RABBITMQ_PASSWORD"`
146 | Vhost string `env:"RABBITMQ_VHOST"`
147 | UseTLS bool `env:"RABBITMQ_USE_TLS"`
148 | ConnectionName string `env:"RABBITMQ_CONNECTION_NAME"`
149 | }
150 |
--------------------------------------------------------------------------------
/ttl_map.go:
--------------------------------------------------------------------------------
1 | package gorabbit
2 |
3 | import (
4 | "sync"
5 | "time"
6 | )
7 |
8 | type ttlMapValue[V any] struct {
9 | value V
10 | createdAt time.Time
11 | }
12 |
13 | type ttlMap[K comparable, V any] struct {
14 | m map[K]ttlMapValue[V]
15 | l sync.Mutex
16 | }
17 |
18 | func newTTLMap[K comparable, V any](ln uint64, maxTTL time.Duration) *ttlMap[K, V] {
19 | m := &ttlMap[K, V]{m: make(map[K]ttlMapValue[V], ln)}
20 |
21 | go func() {
22 | const tickFraction = 3
23 |
24 | for now := range time.Tick(maxTTL / tickFraction) {
25 | m.l.Lock()
26 | for k := range m.m {
27 | issueDate := m.m[k].createdAt
28 | if now.Sub(issueDate) >= maxTTL {
29 | delete(m.m, k)
30 | }
31 | }
32 | m.l.Unlock()
33 | }
34 | }()
35 |
36 | return m
37 | }
38 |
39 | func (m *ttlMap[K, V]) Len() int {
40 | return len(m.m)
41 | }
42 |
43 | func (m *ttlMap[K, V]) Put(k K, v V) {
44 | m.l.Lock()
45 |
46 | defer m.l.Unlock()
47 |
48 | if _, ok := m.m[k]; !ok {
49 | m.m[k] = ttlMapValue[V]{value: v, createdAt: time.Now()}
50 | }
51 | }
52 |
53 | func (m *ttlMap[K, V]) Get(k K) (V, bool) {
54 | m.l.Lock()
55 |
56 | defer m.l.Unlock()
57 |
58 | v, found := m.m[k]
59 |
60 | innerVal := v.value
61 |
62 | return innerVal, found
63 | }
64 |
65 | func (m *ttlMap[K, V]) ForEach(process func(k K, v V)) {
66 | for key, value := range m.m {
67 | innerVal := value.value
68 |
69 | process(key, innerVal)
70 | }
71 | }
72 |
73 | func (m *ttlMap[K, V]) Delete(k K) {
74 | m.l.Lock()
75 |
76 | defer m.l.Unlock()
77 |
78 | delete(m.m, k)
79 | }
80 |
--------------------------------------------------------------------------------
/utils.go:
--------------------------------------------------------------------------------
1 | package gorabbit
2 |
3 | import (
4 | "errors"
5 |
6 | amqp "github.com/rabbitmq/amqp091-go"
7 | )
8 |
9 | // Error Utils.
10 | const (
11 | codeNotFound = 404
12 | )
13 |
14 | // isErrorNotFound checks if the error returned by a connection or channel has the 404 code.
15 | func isErrorNotFound(err error) bool {
16 | var amqpError *amqp.Error
17 |
18 | errors.As(err, &amqpError)
19 |
20 | if amqpError == nil {
21 | return false
22 | }
23 |
24 | return amqpError.Code == codeNotFound
25 | }
26 |
--------------------------------------------------------------------------------