Compare commits

...

10 Commits

Author SHA1 Message Date
Jesse Qu 171dca447c Added into our repositories 2025-06-26 12:08:34 +08:00
Gabriele Santomaggio ccbc8d1c16
Add ErrMaxReconnectAttemptsReached (#49)
* add error max ErrMaxReconnectAttemptsReached
* closes https://github.com/rabbitmq/rabbitmq-amqp-go-client/issues/48
---------
Signed-off-by: Gabriele Santomaggio <G.santomaggio@gmail.com>
2025-06-18 14:46:55 +02:00
dependabot[bot] 25962eccd1
Bump golang.org/x/net from 0.33.0 to 0.38.0 (#47)
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.33.0 to 0.38.0.
- [Commits](https://github.com/golang/net/compare/v0.33.0...v0.38.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-version: 0.38.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-21 06:33:13 +02:00
Gabriele Santomaggio ebc9a3435c
Set message durable as default (#46)
* set message durable as default
* related to https://github.com/rabbitmq/rabbitmq-server/pull/13918
---------

Signed-off-by: Gabriele Santomaggio <G.santomaggio@gmail.com>
2025-05-20 15:11:27 +02:00
dependabot[bot] ca3cc92d5c
Bump github.com/golang-jwt/jwt/v5 from 5.2.1 to 5.2.2 (#44)
Bumps [github.com/golang-jwt/jwt/v5](https://github.com/golang-jwt/jwt) from 5.2.1 to 5.2.2.
- [Release notes](https://github.com/golang-jwt/jwt/releases)
- [Changelog](https://github.com/golang-jwt/jwt/blob/main/VERSION_HISTORY.md)
- [Commits](https://github.com/golang-jwt/jwt/compare/v5.2.1...v5.2.2)

---
updated-dependencies:
- dependency-name: github.com/golang-jwt/jwt/v5
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-25 09:53:02 +01:00
Gabriele Santomaggio f52c7983ce
Broadcast to tmq queues example (#43)
* broadcast example
---------
Signed-off-by: Gabriele Santomaggio <G.santomaggio@gmail.com>
2025-03-25 09:52:42 +01:00
Gabriele Santomaggio d84c3d22de
use env
Signed-off-by: Gabriele Santomaggio <G.santomaggio@gmail.com>
2025-03-05 10:30:08 +01:00
Gabriele Santomaggio 6cd3f90025
Implement JWT (OAuth 2) (#39)
- add tls connection test
- Implement JWT (OAuth 2) closes https://github.com/rabbitmq/rabbitmq-amqp-go-client/issues/23
- refactor connection interfaces  to support only one endpoint  
- refactor Environment interfaces to support multiple endpoints 
- Validate the features for RabbitMQ 4.1 like OAuth refresh token 
---------

Signed-off-by: Gabriele Santomaggio <G.santomaggio@gmail.com>
2025-03-05 09:46:28 +01:00
Gabriele Santomaggio 24649319d8
Implement Filters (#38)
* Closes: Implement properties-filter and application-properties-filter #25
* Refactor the interfaces to be more coherent
---------

Signed-off-by: Gabriele Santomaggio <G.santomaggio@gmail.com>
2025-02-27 13:58:59 +01:00
Gabriele Santomaggio 8ffd1e6fc3
static check (#36)
* static check
* remove duplication
* add tests

---------

Signed-off-by: Gabriele Santomaggio <G.santomaggio@gmail.com>
2025-02-24 15:04:19 +01:00
58 changed files with 2153 additions and 769 deletions

View File

@ -1,21 +1,21 @@
-----BEGIN CERTIFICATE-----
MIIDhjCCAm6gAwIBAgIUJ2lTbiccSFtA9+8eGPQD5yGJ7w8wDQYJKoZIhvcNAQEL
BQAwTDE7MDkGA1UEAwwyVExTR2VuU2VsZlNpZ25lZHRSb290Q0EgMjAyMy0xMC0w
OFQwODoxNjowMy41OTA0NTQxDTALBgNVBAcMBCQkJCQwHhcNMjMxMDA4MTUxNjAz
WhcNMzMxMDA1MTUxNjAzWjBMMTswOQYDVQQDDDJUTFNHZW5TZWxmU2lnbmVkdFJv
b3RDQSAyMDIzLTEwLTA4VDA4OjE2OjAzLjU5MDQ1NDENMAsGA1UEBwwEJCQkJDCC
ASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANdiiGj37094gAHfVpbIQHfu
ccBVozpexrYjDCbjw4IyJJOajJRNGbYZwEt3Jt5NaDc+zyoBZpKaZWDEjOxbNYkd
MtIHyFW4V4ooA6pySR9pzMI91dXoCkzL9Ex23Zrj0KF70qBQuPTbF5bnAbMELFuv
quFnfMw2ALsFrWh2DOwnMlt1hbdj6Iapl2yRGhVSgsr72SK+67b+b7WH02VGDrfm
Y3qqx3xAI6woKSE2Ot14Csak/iR1xit68X5GhzvSdOos0Yo3I4v8mlFEO+kpKWB0
7y3Hb5AU/hqvSOwLRA+CV09bxN4N5rOfFHkPVuVMXQzX9mLCxzxroZn/sQzkrtMC
AwEAAaNgMF4wDwYDVR0TAQH/BAUwAwEB/zALBgNVHQ8EBAMCAQYwHQYDVR0OBBYE
FNSsn21DVr1XhhqmU+wMnLWFZc55MB8GA1UdIwQYMBaAFNSsn21DVr1XhhqmU+wM
nLWFZc55MA0GCSqGSIb3DQEBCwUAA4IBAQDRc1mAERvR1VPOaMevcB2pGaQfRLkN
fYgiO7rxd77FIQLieCbNNZ6z/fDRkBjgZ9xzAWl0vFJ0xZ0FSdUtAXEa9ES7r7eq
XOSW/5CRPeib4nMDeQvTSiCx5QqlIz4oUwW9bbpPcBQXM0IVZwo1Jbye/BGrjAmQ
Z3a5ph0f85Shjy2Yt9JB9BDCWOK8EU294CiKMUvdtQwSaQpl8GQfmvzWKAL4encu
ryEAPTDT9zuQi2bOCDY5QMwVNS6mDAsqbvMjOaHD/Cdzl26rgv+8QLVNDUvGfGtD
58bWugHyxCdnDToCtIEaJaoi7izKd0bILbuQXS7oKfryJpHwO+9U8ZjT
MIIDhDCCAmygAwIBAgIUMNeYbv9MMCXx9e/o+BO7JYbdHJowDQYJKoZIhvcNAQEL
BQAwSzE6MDgGA1UEAwwxVExTR2VuU2VsZlNpZ25lZFJvb3RDQSAyMDI1LTAyLTI3
VDE1OjQ0OjU4Ljg4MDUzMDENMAsGA1UEBwwEJCQkJDAeFw0yNTAyMjcxNDQ0NTha
Fw0zNTAyMjUxNDQ0NThaMEsxOjA4BgNVBAMMMVRMU0dlblNlbGZTaWduZWRSb290
Q0EgMjAyNS0wMi0yN1QxNTo0NDo1OC44ODA1MzAxDTALBgNVBAcMBCQkJCQwggEi
MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDqAwl11OZzzMDh1oaaA/IbnU39
VCDE3BsKKg3arhVGhYaSbYEtaJWhNbB12qkw2GEFeSl0mZCSorTJmQHmcUjcO0yH
zRIM5vzEscPOffUBfIxXiVehPyyNJa9P2IRE65i3d7mcmR62dG6EWtj1tW0VLKGc
d3STpmGoA9b8tuJZq9vt6ivDTv7OECCLmDR2IHKoAXKZQmsDgI1Dy0UuLCEWIzOq
r8WAq1at28AAiDL9Rh0bxyQ8oREx86zjLOXOsJ8CNNWFRheAFh65hWWMpM4SavEV
pW0kE9qCfopBGl4xpbBLE/gzkhMkUZVwri6cGakzvA+dmdCsS5D3t2ZHWIkzAgMB
AAGjYDBeMA8GA1UdEwEB/wQFMAMBAf8wCwYDVR0PBAQDAgEGMB0GA1UdDgQWBBQy
XdLfduEv+NHMZvy4ASFH6BIZWjAfBgNVHSMEGDAWgBQyXdLfduEv+NHMZvy4ASFH
6BIZWjANBgkqhkiG9w0BAQsFAAOCAQEAF0gI9aTScyOoImqvHQZXdfZlgphT8E/o
ks2DDY4ZC1KAIYxRj2y+M9zmrQqSbfhSSuEZ8IKaFKMiBPALBlEVrVJUGAoUAjrU
C9zxSg2TOjJqO2lJD3mMJ0u36cmv0sIPhlm0DRnxWg+1eKmAfEn/DPSj6V8xwHqH
7tpPEea19RgKwBCSOnVUmOwDnIEzCy9H/A8U4P2XzFFEIWSeGWlDHFRy8j4P5YyH
0TRpwR4JGh5t/5+bo4hfdHxSeY5wsWk2k+lfNszfau8qEDdFQVASXmJ6iTelSbqv
pSkMsWk9u8z7ENA+w3Qzhwg1OzOluDl8EVAJziSDfGZpWqeVWK1E4A==
-----END CERTIFICATE-----

View File

@ -1,30 +1,28 @@
-----BEGIN ENCRYPTED PRIVATE KEY-----
MIIFHDBOBgkqhkiG9w0BBQ0wQTApBgkqhkiG9w0BBQwwHAQIm6kLjkvzznECAggA
MAwGCCqGSIb3DQIJBQAwFAYIKoZIhvcNAwcECGjVddOBQ1QOBIIEyOAafHUtExxT
tY2ONQkUkXZG4/fIDvuNwt8IIkNUIGVp9WEDd4Mh5Ofa52uUKmlhj+FyRZ6u2mGT
VHU65e4kBYB10n0oybRPvRU1tFxgr8qI0T7Fqnx7WJAP3m0Bo/tWfqE0GHRrspZV
gABLVTOFvHE8oOsEh/ndMe+Y2qGaLsl+MF3jkfYAxSK2QwEK9HDa16Xsit7hqVbz
JUyvBmQVfTZzanIall+EpUntv/vlILKIlAFOZUXIZ/iL8LTQCmpycfGLknr4/9KP
gCYZmWFS18X9KVAwgV2kSdUebWH9phDosSw6fZh843l1SQvjG65PgrnWYb6Fw7B4
s3Nk6bXjHYtvLT19EUrQOdeOegynaQQBs5WIcp9LbKT3LJVQpaVGV9thi+LPz1Bu
Lep583ayXTecA7Dbfa6S9R97TgRoMdDWaz1kTBReQTUhrL5736A38gpwJeBZDqel
39sRULCKARz2ZX0YpeZCmfVhVVSguO5gCfACsqHoOiTxYOA97GR128BcpEVJ1lst
sZZNwT3m6xIcXbS37EImhUMGiQ5fyGZ+8FIozTL9xNopIR97b3ceA9CoLc7EVcFC
RxHvh1HwtpyBDyopJp2wYu31nqcSDsJh+lmjo5R7bqvDDmflfkfu1G45JkXKr3Vz
M89S/y6Uo8W/EYT2MPYTsqcobtjx6oM1RYkVuYTR6cyUgQkHGtptkzGKxYE8dYwQ
4EIm87czYvCW0Mrp6yy0NGKzqBb+19Kuqc0HO+YezEQ8RjOVb8+D+cuCp2ZSItJ/
S9m7BDTOzTS2lBotrFVkSbzaQafAmxQiaSP7gd0M9dnC0AOB2ILbyRAyIDQ0Y2dm
kMbiewQwNFiY9moRtgzHuHRfFZu4w996Q20cYZyMbxDfY17QoZQzfKWQH1BD7nq5
G4RFpInt6q4q0F94nQWCif195VZF64+8ETMteJqtBFhUSQbq7PzKdpuf8NFxczLt
MDEWg2l6qNLP+zswulcVbFcC/HxAu4UtYf2m5MAtaurXZZ/+xPW5c/0caWMycQ7g
fbkYvC4j0OT7aqqMd1SYzEx7l75Vqn1sr2BsXZFoaqK2c/1LIb6U1kAhyhDQ46rV
0v6q4GUk4fdnE4N+9MXWBvlKSnqEVYlE54IuSUrYRuuBhO4LQpPMOAafQPR6QCTI
ikqWVmLAj50n7uba0Ao9lRKR7bFpdOQob/nYMTKT6YQaohYhbCv4zIK9fDgWWiXE
a2ecIP3KiZzw7oLMKXLcDt1RkkzE1FQxLfOeZ5EP4RwBGPDvR88ELO+lGQQt3VnS
FIZoXBUFUf7bEUzTwM4240zkjDYQPxD1j769Zq/JZfKyOEXXOJT8xHiwMg3ARWuE
hGlNKApbJGMn9myC61KaGMyCKRvMVxI25w3LfI4OAWt+67BB5OuAG11nmn9Kja73
bhMFDIMZ8kE0p9IWfpiUJlDB9odGEc4z3Jl5CqBVDkMCDxq9BQDM0hSDk+ov8FO9
g03PqMxvsxd2c56vkMtNY4hSGkYfN0RsM3vTXXLtPwRwRZURCmKK76BmsT4oBd+W
orqH4SABIAbYTwNOb7k/wOc4EfucawBqMG4g+29qewD67+EXjB0GadqOXRoQyhRq
hd74uUK5gzJOqStqiowQ0A==
-----END ENCRYPTED PRIVATE KEY-----
-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDqAwl11OZzzMDh
1oaaA/IbnU39VCDE3BsKKg3arhVGhYaSbYEtaJWhNbB12qkw2GEFeSl0mZCSorTJ
mQHmcUjcO0yHzRIM5vzEscPOffUBfIxXiVehPyyNJa9P2IRE65i3d7mcmR62dG6E
Wtj1tW0VLKGcd3STpmGoA9b8tuJZq9vt6ivDTv7OECCLmDR2IHKoAXKZQmsDgI1D
y0UuLCEWIzOqr8WAq1at28AAiDL9Rh0bxyQ8oREx86zjLOXOsJ8CNNWFRheAFh65
hWWMpM4SavEVpW0kE9qCfopBGl4xpbBLE/gzkhMkUZVwri6cGakzvA+dmdCsS5D3
t2ZHWIkzAgMBAAECggEAZMk+D9PMFV/ASwQcIMVGRwJvDoZnPqIVu0D1ipOjciYc
GYC0PBxpJW98OqYcbH8k+jh+1Es3axBMkO8nVFrCKKgZg/ucpJXvk7+EN7EkDqnX
v/PVHAubYocyhE8aWJynv4z/EiUYhziKSNLf0qN7Ab2hNUR1nwnv0W8l7t3NixSY
4sIuGhm2QbqfHKvvG//GWOmvRIYLdJPZ69tJR8sOidIpNY2LI4tNXlC2fdPI+RaT
pOJcULSi+AZItyxHwELDR3u5xuWJ3KMcrBRiMees//dhg8Sga0tmBIW1vN33eKtW
wOkq48hBGUi8sfrRfVSiJBquZFURYrzC1J2EZQOcwQKBgQD3DuZl4NQiGaL3afY6
fp2hstVjRm8Xdy2AdCPXx5w+R8CJUpLDpHfYavT7pFUx7W1ERfbsqHujMbb2Zaq9
FyYdXvIpcqFjCJl16dLaDmzvDn98v9mWEB9I+NeqXSBVbbPhCFSuKElpx5OrrxeQ
CUMfofoTtQlHvSYUsz0vm/aiMQKBgQDye0HNNd41NiaXVYUQenNbwOBO+POUBxj2
pccNTpQulZEXug8IomfDLQ+cA3Dsqlf16tran4wVPUO53n2By07nof/tHV6y0IF9
oQPznCrbaKl35e4MlvDf0+FfGKNFWExwVeKJJGDVUWgBa+cXFvtUJHYDGb7Bo6+C
5NyqHw6EowKBgCyS056t4ZgFaBGbXIFRNr9ltHokywZAykTSr2TO7rGN4H7mFvSV
R8oUAf8ktvo7C+u1c8de3m+jGI976EIVWxsRdj9kHxnvA0Dy3sfYsm6u/vFS677X
Sc2wl7h09NB06m8/QYfqXNRo3YusG2QxR5r9blD/6Jy405YIgJGGYgkBAoGAGaAp
DhTZTOpSHcAt9dXbByFVE0OACm7NlpNie+eIBXxM/yLsn876BEho0+YRMxG1hgmx
41TlKwF0fNokjWj9B8G5GEf4UBF0/d/cWQxyAwoGjuM/yxjQj/cGZFRoPNXeDikl
bbTofuLBiRTsMSZ+nR/VUPKRlElGLSEeqOPrVt0CgYEAugfHj4AqmDfL1F3rbw+D
XwiyvRqwLBYzxZ0t8Hbm0uht0MKjTgJ/G2fkC7Y8aJgEn8jstfoVSh9sXUKgUnCA
MAuil2220ctEh04bp/na1z/9igJWGiJNbRVXjF+BRAbSJ25RY8BX5dDydcWbqzyr
eT5xfQPC5smWZlI0l0JK1bs=
-----END PRIVATE KEY-----

Binary file not shown.

View File

@ -1,22 +1,22 @@
-----BEGIN CERTIFICATE-----
MIIDvDCCAqSgAwIBAgIBAjANBgkqhkiG9w0BAQsFADBMMTswOQYDVQQDDDJUTFNH
ZW5TZWxmU2lnbmVkdFJvb3RDQSAyMDIzLTEwLTA4VDA4OjE2OjAzLjU5MDQ1NDEN
MAsGA1UEBwwEJCQkJDAeFw0yMzEwMDgxNTE2MDNaFw0zMzEwMDUxNTE2MDNaMCUx
EjAQBgNVBAMMCWxvY2FsaG9zdDEPMA0GA1UECgwGY2xpZW50MIIBIjANBgkqhkiG
9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxoOGcKsURRZG0D89J8rGcolZVqX56rDgA0Ma
cn4AosMQTZ86XAq+Ygn6QVcFV3NjuHxb29vsZfjSYbBpgQNLfpXN9EfeswVvaJND
wblKdRo10RTPslFewI4Aac88GXva+3DBMCwv3viI2S69apcuZgGw0+EKDh+JmbcM
sdH81hZhYjmrS529qSOIji8vJYFTCQPMbGN17elnA7pZaHEmPKj5mzm0veSBvCwU
OZORr4eFE7Nct5RmhLm8DWT0EBRUWT8D6/b6+0ln32Yv30YNpKrua5wkn+kxsvKJ
tQRRKYRyfegSj6mo6L4za1ZvwV/JMN5mDLQUajvtOCsD4NpKcQIDAQABo4HPMIHM
MAkGA1UdEwQCMAAwCwYDVR0PBAQDAgWgMBMGA1UdJQQMMAoGCCsGAQUFBwMCMCoG
A1UdEQQjMCGCCWxvY2FsaG9zdIIJUFJPS09GSUVWgglsb2NhbGhvc3QwMQYDVR0f
MIIDvDCCAqSgAwIBAgIBAjANBgkqhkiG9w0BAQsFADBLMTowOAYDVQQDDDFUTFNH
ZW5TZWxmU2lnbmVkUm9vdENBIDIwMjUtMDItMjdUMTU6NDQ6NTguODgwNTMwMQ0w
CwYDVQQHDAQkJCQkMB4XDTI1MDIyNzE0NDQ1OVoXDTM1MDIyNTE0NDQ1OVowJTES
MBAGA1UEAwwJbG9jYWxob3N0MQ8wDQYDVQQKDAZjbGllbnQwggEiMA0GCSqGSIb3
DQEBAQUAA4IBDwAwggEKAoIBAQC7L/xjD4iHTCf2IfXd/fayxkX0+dI+Z2y+latM
UFvn4GpDIz0Acfqjp3/NhShbWoHqOhR/w5l20J9Ljt2RmecpybK717Flst8Q0g0C
xm3GaN7fVLAxoWAIbzU7cAZMv0SRuu2RIo2HTt5i2xBljA5Bf6wMZqMFxvnNWNGt
TIWVUzCjeqWqPUi84XdHu0GWyQ11rIjCnw5zY3D8EFc+HoTgI33y81EABps7ybmH
BdUtMsAFEXgk3lJplaLeIvlM/HzBk+ffkqpcwC6kTnoR7Nww8a2aE6wHq91Hj+R7
mmAo8Hpx0grott/pmwWOd2Ld1w3gxC3I7D6yqjfT4Rjc6FyxAgMBAAGjgdAwgc0w
CQYDVR0TBAIwADALBgNVHQ8EBAMCBaAwEwYDVR0lBAwwCgYIKwYBBQUHAwIwKwYD
VR0RBCQwIoIJbG9jYWxob3N0ggpGMjNOMDQ5MlhUgglsb2NhbGhvc3QwMQYDVR0f
BCowKDAmoCSgIoYgaHR0cDovL2NybC1zZXJ2ZXI6ODAwMC9iYXNpYy5jcmwwHQYD
VR0OBBYEFLPquWS+kT4+JE+cssrriRkL9UADMB8GA1UdIwQYMBaAFNSsn21DVr1X
hhqmU+wMnLWFZc55MA0GCSqGSIb3DQEBCwUAA4IBAQC1Pz8SahCsQyiyuu6dz391
KENabMpCwb/2wxljN5lfkOvvUrVmupld8/5nIdN2drL9jCrfbBz5ZRz+9Ryb8yrc
sioH8Y9RNU5Gc3UJo7aAoMx4sIib6uJ+UO4fVlVvD4cN2h2sLHxtkI173Oo7lnMf
4c+75iyZYdkEDXaOk+UbR8dncCj84y1Sbt0FYfCMT688O4HYkIGA3xGmqyX7PYV/
CP8CNKwJEuZpQRaGdClkmAmoEPyuFW9ec+A9gOrgCpuFJBI4MRcicC5Q+qmx+LTM
pZ2louMnnlTRoj3tL4aDgfdwV0YGxyIjIzuYLy6QCF8MZ/TLwPK0C3oXXuYmCLBO
VR0OBBYEFLmThoy0pKufr0QWZRwg1FJGdcFRMB8GA1UdIwQYMBaAFDJd0t924S/4
0cxm/LgBIUfoEhlaMA0GCSqGSIb3DQEBCwUAA4IBAQCk4Ytqqtymc8h0M2HiIyhK
p2Dkf7GZRjBPvC6ULIxMEixslcDCkVTkLaYKRJL7xv37RNfc6kgi9K1IjPfDUtEm
IDm56hRhIvLkH/BsUbhhJsZnYBN1GbqmFNtNP7Zj2Yt6uAwFkFB6gnK7RflSwVaG
EYZhs8QEmZ1VhGymJorp5HGI6EcVkOhG3pScp5yaAqM2cKy7CLnZJfpCzQ12LZ7/
2UEKRtfILvN8kWaWOaGCM7t3Z2i6bfEh/1WZBmZnyK+zDBxv/YDp2iave/i7r/dY
tOZA1KB2OMWZY4pHmiEior05yf0o7xNctPdwy3+IvRYAH6FJhMA29XoizPW8Cvtk
-----END CERTIFICATE-----

View File

@ -1,30 +1,28 @@
-----BEGIN ENCRYPTED PRIVATE KEY-----
MIIFLTBXBgkqhkiG9w0BBQ0wSjApBgkqhkiG9w0BBQwwHAQIqKZZASlLYRICAggA
MAwGCCqGSIb3DQIJBQAwHQYJYIZIAWUDBAEqBBCQpWBZXmYQn0c6PZ4CnLrQBIIE
0HwXxx0lDzPbw53k/ak73G4CwBilSpaIM5x7jNwwD7UhiR4Qo9JiYLRy2zn0RQZJ
wK/Hhta3SKecTHqgMwPHk8s4Bu6EhSIm3/x2OhAtk2lLeubZkjgEKCfQbu4tVpeH
jOw66Pxz52fhdJ7GzaTnWjjTYmEPxNpkRiUAe0v+lOD09OQvQIFVEDyqSATzRUjd
GTvQs8H5N/XJR7xTuPRQekauY5gIcneE4oynGF5a9L870XfLh/H62f+pD19rvESh
qqdCxklxwAfHGHni2p1UKgNPxJHzSMH9dGCAGT1fxLg0RtXfBMdl3gzPnwbZ1PmB
tjVxCqtw7XAirdlBX79+dhZ58HCN+j7pkL9LWwRap1klN7Y+Iwf0XhK6imSY7Ex4
4odxin7kF1yW65PTYKyS7cRuFip+k2YShXApN5PrF5SqNEFVt0A9RG7h+GF7EXSD
QS0ecqwhnzuHGHSpBjvsEw9z3FWBL1tFC1i2cF7m3yTHDLVQoevkUY43Fmh8S/CZ
gthQ9P58A3dIDSJM0vcGhHqJBLbxOF7rSqwIuihZJhBfqclw1V4fKk9VuRzp4MHf
NrZEuCr8CTrcYnl2n6Z/MaJ33XRg8uwwy+O5RGF1I1GAmH2KdKORUtrYHlOdTd4K
2NXEgy2mgDQYPbl/1tk8bH6hroIY9Qofpzi7MTZ++32AY3ggf4GnqAk4eAP5R3Ey
PUYFtWaGftaOQCR5Ovocdn41YitUJxAPh6hE5HqVicO2rEfx13uzug9usdg6256i
GgKSTg4jqBiEw0oJhb9TVYNY44koh9yMRM/sfidqarNKWU7bWDVKhl3hGaNhj+oX
v6ZC8rH6m/zHRtbn7tAw/q+EtTHmLo2AaUf13V4Ii6VrEXMRSlv/AyipYmOIwgV2
EZriwyhsT2RaVesAgKExHbnP6dzX2P2IGTMNISZDNlATMT01BfWG/loPe+6DbxzW
aHv6Y0FknGeHGLDwiZMv/hyn8a4KOvIl35YZBJqZ8UxTirs9mLRd4Us5CdXAHQlL
5skAzf5FSrVbQvUbvKIrO+ULGB5mDATHR/tgOWVaP656tiRMrtFW8XGNxaPjyDPt
xhA3fVOc68f1UzTqoGpsZtUUMQxkndW3Tg50V4ssw4F9D4Grce9XXgfBdEFz/Gfc
gSR4SYKelS5udrMvqKxUs+zobx8TH2CqzwDDcC0kxqC9VCMnHqaD3wSMbN/RBoYT
lkD4DRmDFTwlsQd80i2j6K0eDFo7uvROWM72gAOb/wmssZBaSF3g0E5CrNSxApkz
+VhgqfGBYDrFZijMHiCw+XB8kFFBrlXlcBOUHu1trIi7nwcmN1JvnXL0dVOgfSGE
VNAmVz/sHdeAJacf05tehkAFTubdiZ24M/mM+VbKiJ2dajYRSoePPc8P65urlv6E
rszIC9NhfwBy0TDgXNC/GV3y8mC7rp6kzbyzEb2H2M5ltyEKOIyjvRvtks2/opSX
5T42x6xJtS6qTRttwrpRE5KjBHgcq0m8LSQu6chKwWinFUfOAdcZODvVVqLA2e6K
plfcGb027WE4DHqTzW73nbnK+NwkP3lLORbro7KWDFdNKP3v/Rx0Uq7CPGVN994G
tH7wyGheZNTMtKDFGrAdOqse9/sKcTF3thaqYqXgDDZY
-----END ENCRYPTED PRIVATE KEY-----
-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC7L/xjD4iHTCf2
IfXd/fayxkX0+dI+Z2y+latMUFvn4GpDIz0Acfqjp3/NhShbWoHqOhR/w5l20J9L
jt2RmecpybK717Flst8Q0g0Cxm3GaN7fVLAxoWAIbzU7cAZMv0SRuu2RIo2HTt5i
2xBljA5Bf6wMZqMFxvnNWNGtTIWVUzCjeqWqPUi84XdHu0GWyQ11rIjCnw5zY3D8
EFc+HoTgI33y81EABps7ybmHBdUtMsAFEXgk3lJplaLeIvlM/HzBk+ffkqpcwC6k
TnoR7Nww8a2aE6wHq91Hj+R7mmAo8Hpx0grott/pmwWOd2Ld1w3gxC3I7D6yqjfT
4Rjc6FyxAgMBAAECggEAAvFuHrGbFC4rRQMDg7NvRPV57/Awm84SUeHLtnC0rTiO
+ydrRAicEqV6zISu3dbWYD7RsoY6XA72KODiFMdjxiQCH9LJmtTSjd1mRSL7uAj5
6MQtsVi9SDdVZy3rpMUaGHolAOlB6pIfzClFFfpQgWZMPSACVAAs+SClH/wqGoRU
LBt8uAwwx9QWqId8nuP4OorAFQYqmmzFb6Q42CanSNObaWcutmIuziOv/P9hRKcx
WbUei3Q5+DjU4ScFGXmyKzP4DxYMJM42jqocZdggHk+eL2yjdR5TmUpnR5WDcjtM
pf5lXzbPsCaFpVsQgHTZgA5OWcaztEtObnINrryb5QKBgQDtOpp5K0kO6HafC9CI
sPDmYyEtfg+IIv9rdEzHmpuj3uyeFBxO3fNE51m1pxGr2DmfekeWzG17lIFIrOkf
t9SAMVPiI5m5mKsPHt3bRE3CjJNjHTvj3tQyF7xDEsJdYyTvEGB5tTKl+t338Wq8
1H2B0szEr8dEbt1Hf2WFz/lA1QKBgQDJ/7lQnXPC50+U0IhdFk9K/YPE1FOo3ck8
EIi0S4A0F97N5bCoQ1n+PSCLMGDp3f/QQfX6dh/dnX+SXXlOVNbquKuy5/uIj1Lq
glA0Jj9wKFDVpZG6wziB79TQWQP6TltsQJ4NGwpuPUbyxUQBBg2t3cZy9BJpTJjQ
LUYbSnm6bQKBgQDBi6eOJjeT9ysYdc4sR5gzjzr5X7kSS+Nx6s/dphFHcFBCZIv3
+HNKiyoQ3362YlIY/+26ZY0JX07fWVtVqmiwMg6LGJqJ5rnhO0CsbRy4FnMFUUuU
jS84s07AtmRnRsVSWl0rzx7Edll0ub1o1ECVk8PG0NbVyVG1zIWq19Q3BQKBgEOa
8LzIVawPmpTlzh3Jj7Q7cNR5c556zBTsO7SL6FaG/qzOiPdnw0DR2Ih9IpJjGHDt
ApRW4IddZQrpeeX7gwp/0AdKmOa1gTy3bHxnqKey9orqpQFqwQjL6d/pSumFPBfY
8IzWVgFbRNmPqBjnm8BrDzX99gOD/Uj/Pg14OZFpAoGAfDfDCsKbW6TJnvbIFkw3
/V4v6WAwaUS9mECtRb00yzNtn7YbEveQ2/pKPLPZ5z8Vz9pMpuzXfoqFYUEJNsz/
F2qNaYrvREsDbsLVqFdTTyNHPicilcM8bfmaspI72Tb/YkJNH0/cVhF6H8/J02rr
ValHWT50FbbgAY337QwDjO8=
-----END PRIVATE KEY-----

Binary file not shown.

View File

@ -1,22 +1,22 @@
-----BEGIN CERTIFICATE-----
MIIDvDCCAqSgAwIBAgIBATANBgkqhkiG9w0BAQsFADBMMTswOQYDVQQDDDJUTFNH
ZW5TZWxmU2lnbmVkdFJvb3RDQSAyMDIzLTEwLTA4VDA4OjE2OjAzLjU5MDQ1NDEN
MAsGA1UEBwwEJCQkJDAeFw0yMzEwMDgxNTE2MDNaFw0zMzEwMDUxNTE2MDNaMCUx
EjAQBgNVBAMMCWxvY2FsaG9zdDEPMA0GA1UECgwGc2VydmVyMIIBIjANBgkqhkiG
9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2dxp0wR++oE89W/mhEL7/XfJfo8iDbKKciUP
PyIgBvggv625HifmEJG+epl77KinbCuZdc0DX/2FKH6HPM/tC6VcWB2cZRSHpBSM
aieRV4yiaUFTqlOgQalJyRczRtv35QPdaIcDOX4lOw887sn6sJuZY5FtAyDr3opA
gZWLR+6fqi0YWqp5wqaz3hMzTGEEuu/ZKSqMWURRvp+Voz13auiShvhRb9hsdRp0
zf12Y9wGhWjOg7G6v1r/BP6/Nr1gWrgNUhuomSFC1FCRdCr1VrLpUfG3VNloVEOG
mbWYfo+cDN6fV+PDlVB5UQp9YciFfpGXBzSXgNcsk8fEXpg8IQIDAQABo4HPMIHM
MAkGA1UdEwQCMAAwCwYDVR0PBAQDAgWgMBMGA1UdJQQMMAoGCCsGAQUFBwMBMCoG
A1UdEQQjMCGCCWxvY2FsaG9zdIIJUFJPS09GSUVWgglsb2NhbGhvc3QwHQYDVR0O
BBYEFPezEEGf7j3HedbaRCh4/FHT2VXrMB8GA1UdIwQYMBaAFNSsn21DVr1Xhhqm
U+wMnLWFZc55MDEGA1UdHwQqMCgwJqAkoCKGIGh0dHA6Ly9jcmwtc2VydmVyOjgw
MDAvYmFzaWMuY3JsMA0GCSqGSIb3DQEBCwUAA4IBAQBLeagmroj4FFOXgUqDQo7i
kGCBZuCmn6GnCYdwEHtMoysGZ3vNFsB1BCug4fTuL7OU1l+Xw8iVnIvnGBpKypmt
b7h9dN6urty0ewCS4WO8BTZUIdc1RJMo9N+nEMTja+5cqXHtO/VQnO2eqeALWJUU
IDPycb6HcTkHGFX0QDwxsPuMFL3p5HGr6U0llLF0J5FedxUA/YLLVCStofrWvBGT
PKngh7S6ntaIUnTvwyzY2kPJ+byqRDNrL5jdavw1U8cGh1vi3k9mf1Uloi0mnAMT
kqOPzbQmHIQjxIOwqp2xkObXgqz1b0KNDfRDTwp90wzVxOCF5JJBCAIjPyLuncDv
MIIDvDCCAqSgAwIBAgIBATANBgkqhkiG9w0BAQsFADBLMTowOAYDVQQDDDFUTFNH
ZW5TZWxmU2lnbmVkUm9vdENBIDIwMjUtMDItMjdUMTU6NDQ6NTguODgwNTMwMQ0w
CwYDVQQHDAQkJCQkMB4XDTI1MDIyNzE0NDQ1OVoXDTM1MDIyNTE0NDQ1OVowJTES
MBAGA1UEAwwJbG9jYWxob3N0MQ8wDQYDVQQKDAZzZXJ2ZXIwggEiMA0GCSqGSIb3
DQEBAQUAA4IBDwAwggEKAoIBAQCn1MRZTV3ATEvS8jFXhci/HGup4acSa1AduNak
8fpGHSFFmrywY6cl00rmPa95nfGloqbkRydqOwMn1Pv3XfHc3UeaiBgU+FNRj9u6
NOwJ0zR3QkqLxvQqbjrvxMN/IaZ2WL0Zem+j8YIY9yHytjkLEX2AH9AZLwHpdBLI
vSVeS3BNF/gKpXYExGNNfG47/Lo0fIgwboN069pHY/Ff80SAzUkzRcOxDplJoMWp
wym15ssmAnGzAzTrMhKIJ7rUyaE0ZNAIcid7KQ1VzB+yMpeYz5pdbx0G4U/DuVXf
j8FnwlGwGAw05CckDjZcgrWNgLz1kqEcMV/UEFlbQuEzl5kTAgMBAAGjgdAwgc0w
CQYDVR0TBAIwADALBgNVHQ8EBAMCBaAwEwYDVR0lBAwwCgYIKwYBBQUHAwEwKwYD
VR0RBCQwIoIJbG9jYWxob3N0ggpGMjNOMDQ5MlhUgglsb2NhbGhvc3QwHQYDVR0O
BBYEFGv69aUODEtJA5QWU4KalMtGvuGYMB8GA1UdIwQYMBaAFDJd0t924S/40cxm
/LgBIUfoEhlaMDEGA1UdHwQqMCgwJqAkoCKGIGh0dHA6Ly9jcmwtc2VydmVyOjgw
MDAvYmFzaWMuY3JsMA0GCSqGSIb3DQEBCwUAA4IBAQBQxX+IwLmt9emhC/of3riN
wQaLXGYKKMHcsimGkBsQbitWlwWtBZwR2F9aOlvcOAlFbQ2Enldbdpkens1YwR4k
Fsx2VdOnumSYbq6DKZg0mMrg3AqufYLBGVPSGNksQ6qERZVD5NGATLh0kA9R3q0h
eGKJbHyrdI6fkSELkmBGbuetjmGIfmYh+OjYZhqvU5mutjdOfY9k1t08eRvdNiIB
4HxFVEk/S0opA98LkjY0wjPSAMZAWPNxHD5vHoaI6VwYnxLadD1NcasfEpae6uLW
t7CT+v6rtfBXvczfdd9rmhCmcHR5ckrL/wbpnvgkloQqxclw5IpDt/JkPyGghWx3
-----END CERTIFICATE-----

View File

@ -1,30 +1,28 @@
-----BEGIN ENCRYPTED PRIVATE KEY-----
MIIFLTBXBgkqhkiG9w0BBQ0wSjApBgkqhkiG9w0BBQwwHAQILovSnFfKBhECAggA
MAwGCCqGSIb3DQIJBQAwHQYJYIZIAWUDBAEqBBBmLCdyyKqcbbjoi1/8A+rxBIIE
0Mmi72DP32seewlELsG4gVkOH6Gwvs5iAqHYap1yOps3mfI1TtuMhDEZDH2Sj+MB
J1E35WEzJGGxTVhvK/J+R/1fUfd44Acgl1Ks1IINJyre4+vYfDUyWB5O2lS+9mr7
L6q7kfAbBB2OuAEuGL5GMlTRetyASXbspWbi0M+vA9R+NemYbRzFpozP/fedFpQY
6r/QnogSwuRcE1VMghUjZwzZWyG2HFMFp5emiAHRVi+SxLpIIv6wwV8SB4jDMO46
CsyxLjkjhd2GmkMRpmIxXw7eXbWa/bnf/KhJG7gSDBgmGuoBJ4cDnQc2jFN8UqXW
IG3+K6PIeGTT/t4aC6YSq+kb8R3rTfVbPdq51Uo55uMatpJg8AatsysL900nNfuz
MejikInTz4+m6jY5kzEm+fToRNHXhcmnQeD6SYc8PNi/4QfxiMcHcI91GRNQ2nFI
Xd5a1CG4f78WGUmK9PylxBdh+1nx9yQyrZKWcShuLkOQk4UAL0w31B70/l9jVoiN
gcN4w18TUfYLIg8Ab6lL6wXipBrr1AjB/Dn2oCpMTiMolyWcsPAHDtxvrsgbsXRr
vxd/vNo+RpSsvjq2wnXhxe+qC/uHBzJeyfx0m+rs6vBKPZvS7uTBfYGG+RhVJvb5
W2RRfprvTzgBbbKBCTJ5ry4SMZX7ci008f7oVqKLAlsApA58dDgZ+ORF4TxtdSkJ
u3r2htUBvC+mzYMYU4D+sYQ7S9qqVhKe7hvNzLW5UhkEhH57SQ1dIcstTsTYUDC7
1o/zOkpVxByudKEGwgEtyYM+DD/YoGLGB/4qPULnHFOBwxWdK6Ov9I0ezuhe/nOA
ERe3ixLklwHRI5sM/gt57A7MiMPhFHDpqt/xO/m/uCX2VRDW/IAKXpIfxuuxDcIz
MLLxJhYCrGRHMStmBAPy3zmmhpn+wHTkwVbEVRMsh+o8M2vPelrysUtUlarRBQI+
l5tY/UCgX0bGUvHKIp5z8GuRu/CTpjtpsyuNwtpq2TrgnmyiznyfFl4oknvEcfmF
BLUd23ZrTyn1ha8cnKXY9JSHgS2cxdU0QnkPT1BEypptf30nQ1lLqiUg9GLR+xC5
EeHn/80gL/MrpVnWdEznJdWMzau39kqf3ajNQlUb/SX5YQaeUKYrWoLHI+UNhUG2
5fr2vcBgk0gt7k5ZDpWejhEu0BDTf3xrE9dU2jj6hOw6E+Q5bI59QvnLYqCvqBmE
asDMBafo+/Px8xnXazFr5b5FyNqeXzBRPgRw5wFmK5YdFXU0fIpuF9IJb1TwLITp
Hk+Hn760AsT3ALzHgRzC2e6bUUO6F/iw/6s6awwRbEPpLYTHwb9Mv7efeVsGTYiM
Fi0OHapnzzbb4ErVL+92mkOT8flDoLhbKHJCRbOvu4C9awRs5aVbkEsygV67tLwu
SIgUMpdxOMYYquyCJ+WUbyv5VSyvhnUIj7u2kdH+zyAendAi4Rgx/5e4PcD62c+X
tNKp4KrlpF3jGIaPODXZVE2aIrhI0njVlUjIQRs6OOMXleO6+xWQI/1fx/xn/oKm
TBUOtW3Y7AzyojbPiScvjmT+aoVwAZ3juHnUuxEuyUcI3WokkWPpllcaGd95sCUG
7iR90VPBJ/meYyQMYY1BGq4ngi5DvLGy6K/pS5CHPi0U
-----END ENCRYPTED PRIVATE KEY-----
-----BEGIN PRIVATE KEY-----
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCn1MRZTV3ATEvS
8jFXhci/HGup4acSa1AduNak8fpGHSFFmrywY6cl00rmPa95nfGloqbkRydqOwMn
1Pv3XfHc3UeaiBgU+FNRj9u6NOwJ0zR3QkqLxvQqbjrvxMN/IaZ2WL0Zem+j8YIY
9yHytjkLEX2AH9AZLwHpdBLIvSVeS3BNF/gKpXYExGNNfG47/Lo0fIgwboN069pH
Y/Ff80SAzUkzRcOxDplJoMWpwym15ssmAnGzAzTrMhKIJ7rUyaE0ZNAIcid7KQ1V
zB+yMpeYz5pdbx0G4U/DuVXfj8FnwlGwGAw05CckDjZcgrWNgLz1kqEcMV/UEFlb
QuEzl5kTAgMBAAECggEAEaH/jRhdSLZbYwrSF011hWqxfxQ3ru46aR0B5CuOLW6j
D8KNn4Sgy48S9/S0KnVnLY1UtngpUnZnwvgUDu2+WwOeocQ5r35VlqSkI8Cqqe+Y
PA1pcp0RCyIwq/9CwOkiqZ1yJKqh7xoRHplcZjkx7hFE28C75uFy9Hme/ZstwWXF
E+6Puia3YcE1CAYiIzrdKDGL+uIVjMfXQue3JybST9CzSPk2mgTq4tGLDON82V3u
RC80YmhSrzgi9/CPBQwE2YtD3zO0RTqTE1s2efP1ApfWZDL09rBWj6P4lplFnjzk
IAW35SbP8zEtnFuMLEui2cSr0ewAPks5x5HflitxhQKBgQDSP6R2iDa1XsxsMCO+
hAgvIKelzrI1vdOs5OmpQQonL6t0xfFbesAEKxhoRQe73nvgQechLrbTjcAMDemj
F98TC39f3TGMVi2XQMaAkJMt+3NGtYj7OTrfwIB7sFZXg9guO1EG4hEsQbP0P11S
aFEoRp+/0dRVDc5PvHz70sNz1wKBgQDMWiqENzhuk1Ha2XzRpmgLvDqZ5x1rS+2p
LvwcVAEFuK1EoOqcGy8KBYz2HHQg3dbdDlM/ptSaV1YFqB8DGW+pfAHFqiS47YQf
QHSgoYXfHm9rSDkS2gnPLK0V8rN8Ft5umWx5FkT8x7ormaxUVcSg2Dxhe3J4UCh+
EwJhoXudJQKBgQCpceVIKkt9LOOvpbSJDLvTz4uNk+IIce6w/uRaJjLalg6m1AjK
40jxkxHepxOuk4ZenH58PbvXD/zhOi075jdAkBmd1xTht2qS5f+VCe+0NV0YdaHq
ZptOTUS/asSLT5Tg3alV1MhmVKWFibPagHw364NAAwoPaksF9DD+e0ROjQKBgH4q
VQGYTkEGt4zUphmSEb7dEZkfdaxfDnZbyc97lb4AjQlICFEk/1/CmYsBejkofZWx
WHh9+djofvWzHKJ/O895/mYZa9641c+trdPWpZ5hXgzwZDxdXZ0JSju4wlOkkuPZ
2XzQ4ProHOr6T8kpwuJDXtQYsU3Sv41HEztPxc/5AoGBAIbELsuD0WsEEmQu10hp
fCzzUan8tS5cFYEBXYI6XoUBTHv/TM5Mx+hpEqZFYOALVmYyLEC6eFKYFFZVDZ6t
Up6L91J0AhsInnON3RotUekx4l77woMufYmOSuARqU/+UjkXUfrCXnjK/064HNEO
rRdolD9MXCQidIrqwsPyHprv
-----END PRIVATE KEY-----

View File

@ -0,0 +1,13 @@
[
{rabbitmq_auth_backend_oauth2, [{key_config,
[{signing_keys,
#{<<"token-key">> =>
{map,
#{<<"alg">> => <<"HS256">>,
<<"k">> => <<"abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGH">>,
<<"kid">> => <<"token-key">>,
<<"kty">> => <<"oct">>,
<<"use">> => <<"sig">>,
<<"value">> => <<"token-key">>}}}}]},
{resource_server_id,<<"rabbitmq">>}]}
].

View File

@ -0,0 +1 @@
{"rabbit_version":"4.1.0-beta.4","rabbitmq_version":"4.1.0-beta.4","product_name":"RabbitMQ","product_version":"4.1.0-beta.4","rabbitmq_definition_format":"cluster","original_cluster_name":"rabbit@rabbitmq-amqp-go-client-rabbitmq","explanation":"Definitions of cluster 'rabbit@rabbitmq-amqp-go-client-rabbitmq'","users":[{"name":"guest","password_hash":"5AXVjnnJAKWzGy8L/t9vhOi5iZ4j2wwUA9aI0QoOgBYPXmGS","hashing_algorithm":"rabbit_password_hashing_sha256","tags":["administrator"],"limits":{}},{"name":"user_1","password_hash":"k91LVmfv+JsXCihK+BiwURDo2otPX4wRtX4vErArkhRq/kkJ","hashing_algorithm":"rabbit_password_hashing_sha256","tags":["administrator"],"limits":{}},{"name":"O=client,CN=localhost","password_hash":"n3z/QaCVGTgelie+hmxw7//jYQmtERIVOQj+tw47AoPVAsCh","hashing_algorithm":"rabbit_password_hashing_sha256","tags":["administrator"],"limits":{}}],"vhosts":[{"name":"vhost_user_1","description":"","metadata":{"description":"","tags":[],"default_queue_type":"classic"},"tags":[],"default_queue_type":"classic"},{"name":"/","description":"Default virtual host","metadata":{"description":"Default virtual host","tags":[],"default_queue_type":"classic"},"tags":[],"default_queue_type":"classic"},{"name":"tls","description":"","metadata":{"description":"","tags":[],"default_queue_type":"classic"},"tags":[],"default_queue_type":"classic"}],"permissions":[{"user":"O=client,CN=localhost","vhost":"/","configure":".*","write":".*","read":".*"},{"user":"guest","vhost":"/","configure":".*","write":".*","read":".*"},{"user":"O=client,CN=localhost","vhost":"tls","configure":".*","write":".*","read":".*"},{"user":"guest","vhost":"vhost_user_1","configure":".*","write":".*","read":".*"},{"user":"guest","vhost":"tls","configure":".*","write":".*","read":".*"},{"user":"user_1","vhost":"vhost_user_1","configure":".*","write":".*","read":".*"}],"topic_permissions":[],"parameters":[],"global_parameters":[{"name":"cluster_tags","value":[]},{"name":"internal_cluster_id","value":"rabbitmq-cluster-id-A5bx3jtkxi8ukG64KRkw8g"}],"policies":[],"queues":[],"exchanges":[],"bindings":[]}

View File

@ -1 +1 @@
[rabbitmq_auth_mechanism_ssl,rabbitmq_management,rabbitmq_stream,rabbitmq_stream_management,rabbitmq_top].
[rabbitmq_auth_mechanism_ssl,rabbitmq_management,rabbitmq_stream,rabbitmq_stream_management,rabbitmq_top,rabbitmq_auth_backend_oauth2].

View File

@ -7,13 +7,7 @@ set -o xtrace
script_dir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
readonly script_dir
echo "[INFO] script_dir: '$script_dir'"
if [[ $3 == 'arm' ]]
then
readonly rabbitmq_image="${RABBITMQ_IMAGE:-pivotalrabbitmq/rabbitmq-arm64:main}"
else
readonly rabbitmq_image="${RABBITMQ_IMAGE:-pivotalrabbitmq/rabbitmq:main}"
fi
readonly rabbitmq_image=rabbitmq:4.1.0-beta.4-management-alpine
readonly docker_name_prefix='rabbitmq-amqp-go-client'
@ -91,6 +85,8 @@ function start_rabbitmq
--network "$docker_network_name" \
--volume "$GITHUB_WORKSPACE/.ci/ubuntu/enabled_plugins:/etc/rabbitmq/enabled_plugins" \
--volume "$GITHUB_WORKSPACE/.ci/ubuntu/rabbitmq.conf:/etc/rabbitmq/rabbitmq.conf:ro" \
--volume "$GITHUB_WORKSPACE/.ci/ubuntu/definitions.json:/etc/rabbitmq/definitions.json:ro" \
--volume "$GITHUB_WORKSPACE/.ci/ubuntu/advanced.config:/etc/rabbitmq/advanced.config:ro" \
--volume "$GITHUB_WORKSPACE/.ci/certs:/etc/rabbitmq/certs:ro" \
--volume "$GITHUB_WORKSPACE/.ci/ubuntu/log:/var/log/rabbitmq" \
"$rabbitmq_image"
@ -163,8 +159,7 @@ function install_ca_certificate
openssl s_client -connect localhost:5671 \
-CAfile "$GITHUB_WORKSPACE/.ci/certs/ca_certificate.pem" \
-cert "$GITHUB_WORKSPACE/.ci/certs/client_localhost_certificate.pem" \
-key "$GITHUB_WORKSPACE/.ci/certs/client_localhost_key.pem" \
-pass pass:grapefruit < /dev/null
-key "$GITHUB_WORKSPACE/.ci/certs/client_localhost_key.pem"
}
docker network create "$docker_network_name" || echo "[INFO] network '$docker_network_name' is already created"

View File

@ -17,10 +17,14 @@ ssl_options.cacertfile = /etc/rabbitmq/certs/ca_certificate.pem
ssl_options.certfile = /etc/rabbitmq/certs/server_localhost_certificate.pem
ssl_options.keyfile = /etc/rabbitmq/certs/server_localhost_key.pem
ssl_options.verify = verify_peer
ssl_options.password = grapefruit
ssl_options.depth = 1
ssl_options.fail_if_no_peer_cert = false
auth_mechanisms.1 = PLAIN
auth_mechanisms.2 = ANONYMOUS
auth_mechanisms.3 = EXTERNAL
auth_backends.1 = internal
auth_backends.2 = rabbit_auth_backend_oauth2
load_definitions = /etc/rabbitmq/definitions.json

View File

@ -1,3 +1,10 @@
# Get the currently used golang install path (in GOPATH/bin, unless GOBIN is set)
ifeq (,$(shell go env GOBIN))
GOBIN = $(shell go env GOPATH)/bin
else
GOBIN = $(shell go env GOBIN)
endif
all: test
format:
@ -5,8 +12,21 @@ format:
vet:
go vet ./pkg/rabbitmqamqp
go vet ./docs/examples/...
test: format vet
STATICCHECK ?= $(GOBIN)/staticcheck
STATICCHECK_VERSION ?= latest
$(STATICCHECK):
go install honnef.co/go/tools/cmd/staticcheck@$(STATICCHECK_VERSION)
check: $(STATICCHECK)
$(STATICCHECK) ./pkg/rabbitmqamqp
$(STATICCHECK) ./docs/examples/...
test: format vet check
cd ./pkg/rabbitmqamqp && go run -mod=mod github.com/onsi/ginkgo/v2/ginkgo \
--randomize-all --randomize-suites \
--cover --coverprofile=coverage.txt --covermode=atomic \
@ -14,8 +34,8 @@ test: format vet
rabbitmq-server-start-arm:
./.ci/ubuntu/gha-setup.sh start pull arm
rabbitmq-server-start:
./.ci/ubuntu/gha-setup.sh start pull
rabbitmq-server-stop:
./.ci/ubuntu/gha-setup.sh stop

View File

@ -1,13 +1,13 @@
# RabbitMQ AMQP 1.0 Golang Client
This library is meant to be used with RabbitMQ 4.0.
Suitable for testing in pre-production environments.
This library is meant to be used with RabbitMQ 4.0. (2025.06.26)
## Getting Started
- [Getting Started](docs/examples/getting_started)
- [Examples](docs/examples)
Inside the `docs/examples` directory you will find several examples to get you started.</br>
Also advanced examples like how to use streams, how to handle reconnections, and how to use TLS.
- Getting started Video tutorial: </br>
[![Getting Started](https://img.youtube.com/vi/iR1JUFh3udI/0.jpg)](https://youtu.be/iR1JUFh3udI)
@ -15,7 +15,7 @@ Suitable for testing in pre-production environments.
## Documentation
- [Client Guide](https://www.rabbitmq.com/client-libraries/amqp-client-libraries) (work in progress for this client)
- [Client Guide](https://www.rabbitmq.com/client-libraries/amqp-client-libraries)

View File

@ -6,4 +6,7 @@
- [Streams](streams) - An example of how to use [RabbitMQ Streams](https://www.rabbitmq.com/docs/streams) with AMQP 1.0
- [Stream Filtering](streams_filtering) - An example of how to use streams [Filter Expressions](https://www.rabbitmq.com/blog/2024/12/13/amqp-filter-expressions)
- [Publisher per message target](publisher_msg_targets) - An example of how to use a single publisher to send messages in different queues with the address to the message target in the message properties.
- [Video](video) - From the YouTube tutorial [AMQP 1.0 with Golang](https://youtu.be/iR1JUFh3udI)
- [Video](video) - From the YouTube tutorial [AMQP 1.0 with Golang](https://youtu.be/iR1JUFh3udI)
- [TLS](tls) - An example of how to use TLS with the AMQP 1.0 client.
- [Advanced Settings](advanced_settings) - An example of how to use the advanced connection settings of the AMQP 1.0 client.
- [Broadcast](broadcast) - An example of how to use fanout to broadcast messages to multiple auto-deleted queues.

View File

@ -0,0 +1,76 @@
package main
import (
"context"
"fmt"
"github.com/Azure/go-amqp"
rmq "github.com/rabbitmq/rabbitmq-amqp-go-client/pkg/rabbitmqamqp"
"time"
)
func main() {
rmq.Info("Golang AMQP 1.0 Advanced connection settings")
// rmq.NewClusterEnvironment setups the environment.
// define multiple endpoints with different connection settings
// the connection will be created based on the strategy Sequential
env := rmq.NewClusterEnvironmentWithStrategy([]rmq.Endpoint{
//this is correct
{Address: "amqp://localhost:5672", Options: &rmq.AmqpConnOptions{
ContainerID: "My connection one ",
SASLType: amqp.SASLTypeAnonymous(),
RecoveryConfiguration: &rmq.RecoveryConfiguration{
ActiveRecovery: false,
},
OAuth2Options: nil,
Id: "my first id",
}},
// this is correct
{Address: "amqp://localhost:5672", Options: &rmq.AmqpConnOptions{
ContainerID: "My connection two",
SASLType: amqp.SASLTypePlain("guest", "guest"),
RecoveryConfiguration: &rmq.RecoveryConfiguration{
ActiveRecovery: true,
BackOffReconnectInterval: 2 * time.Second,
MaxReconnectAttempts: 5,
},
OAuth2Options: nil,
Id: "my second id",
}},
//this end point is incorrect, so won't be used
//so another endpoint will be used
{Address: "amqp://wrong:5672", Options: &rmq.AmqpConnOptions{
ContainerID: "My connection wrong",
SASLType: amqp.SASLTypePlain("guest", "guest"),
RecoveryConfiguration: &rmq.RecoveryConfiguration{
ActiveRecovery: true,
BackOffReconnectInterval: 2 * time.Second,
MaxReconnectAttempts: 5,
},
OAuth2Options: nil,
Id: "my wrong id",
}},
}, rmq.StrategyRandom)
for i := 0; i < 5; i++ {
connection, err := env.NewConnection(context.Background())
if err != nil {
rmq.Error("Error opening connection", err)
return
}
rmq.Info("Connection opened", "Container ID", connection.Id())
time.Sleep(200 * time.Millisecond)
}
// Here you should see the connection opened for the first two endpoints
// with the containers ID "My connection one" and with the containers ID "My connection two"
// press any key to exit
fmt.Println("Press any key to exit")
var input string
_, _ = fmt.Scanln(&input)
}

View File

@ -0,0 +1,110 @@
package main
import (
"context"
"fmt"
rmq "github.com/rabbitmq/rabbitmq-amqp-go-client/pkg/rabbitmqamqp"
"time"
)
func main() {
env := rmq.NewEnvironment("amqp://guest:guest@localhost:5672/", nil)
// Open a connection to the AMQP 1.0 server ( RabbitMQ >= 4.0)
amqpConnection, err := env.NewConnection(context.Background())
if err != nil {
rmq.Error("Error opening connection", err)
return
}
const broadcastExchange = "broadcast"
// Create the management interface for the connection
// so we can declare exchanges, queues, and bindings
management := amqpConnection.Management()
_, err = management.DeclareExchange(context.Background(), &rmq.FanOutExchangeSpecification{
Name: broadcastExchange,
})
if err != nil {
rmq.Error("Error declaring exchange", err)
return
}
for i := 0; i < 5; i++ {
// create temp queues
q, err := management.DeclareQueue(context.Background(), &rmq.AutoGeneratedQueueSpecification{
IsAutoDelete: true,
IsExclusive: true,
})
if err != nil {
rmq.Error("Error DeclareQueue", err)
return
}
_, err = management.Bind(context.TODO(), &rmq.ExchangeToQueueBindingSpecification{
SourceExchange: broadcastExchange,
DestinationQueue: q.Name(),
})
if err != nil {
rmq.Error("Error binding", err)
return
}
go func(idx int) {
consumer, err := amqpConnection.NewConsumer(context.Background(), q.Name(), nil)
if err != nil {
rmq.Error("Error creating consumer", err)
return
}
for {
dcx, err1 := consumer.Receive(context.Background())
if err1 != nil {
rmq.Error("Error receiving message", err)
return
}
rmq.Info("[Consumer]", "index", idx, "msg", fmt.Sprintf("%s", dcx.Message().Data), "[queue]", q.Name())
err1 = dcx.Accept(context.Background())
if err1 != nil {
rmq.Error("Error accepting message", err)
return
}
}
}(i)
}
publisher, err := amqpConnection.NewPublisher(context.Background(), &rmq.ExchangeAddress{
Exchange: broadcastExchange,
}, nil)
if err != nil {
rmq.Error("Error creating publisher", err)
return
}
for i := 0; i < 10_000; i++ {
publishResult, err := publisher.Publish(context.Background(),
rmq.NewMessage([]byte("Hello AMQP 1.0 - id:"+fmt.Sprintf("%d", i))))
if err != nil {
rmq.Error("Error publishing message", err)
return
}
switch publishResult.Outcome.(type) {
// publish result
case *rmq.StateAccepted:
rmq.Info("[Publisher] Message accepted", "message", publishResult.Message.GetData())
default:
rmq.Error("[Publisher] Message not accepted", "outcome", publishResult.Outcome)
}
time.Sleep(1 * time.Second)
}
// press any key to close the connection
var input string
_, _ = fmt.Scanln(&input)
}

View File

@ -15,7 +15,7 @@ func main() {
rmq.Info("Getting started with AMQP Go AMQP 1.0 Client")
/// Create a channel to receive state change notifications
/// Create a channel to receive connection state change notifications
stateChanged := make(chan *rmq.StateChanged, 1)
go func(ch chan *rmq.StateChanged) {
for statusChanged := range ch {
@ -23,10 +23,16 @@ func main() {
}
}(stateChanged)
// rmq.NewEnvironment setups the environment.
// rmq.NewClusterEnvironment setups the environment.
// The environment is used to create connections
// given the same parameters
env := rmq.NewEnvironment([]string{"amqp://"}, nil)
env := rmq.NewEnvironment("amqp://guest:guest@localhost:5672/", nil)
// in case you have multiple endpoints you can use the following:
//env := rmq.NewClusterEnvironment([]rmq.Endpoint{
// {Address: "amqp://server1", Options: &rmq.AmqpConnOptions{}},
// {Address: "amqp://server2", Options: &rmq.AmqpConnOptions{}},
//})
// Open a connection to the AMQP 1.0 server ( RabbitMQ >= 4.0)
amqpConnection, err := env.NewConnection(context.Background())
@ -74,7 +80,6 @@ func main() {
// Create a consumer to receive messages from the queue
// you need to build the address of the queue, but you can use the helper function
consumer, err := amqpConnection.NewConsumer(context.Background(), queueName, nil)
if err != nil {
rmq.Error("Error creating consumer", err)
@ -89,16 +94,16 @@ func main() {
deliveryContext, err := consumer.Receive(ctx)
if errors.Is(err, context.Canceled) {
// The consumer was closed correctly
rmq.Info("[NewConsumer]", "consumer closed. Context", err)
rmq.Info("[Consumer]", "consumer closed. Context", err)
return
}
if err != nil {
// An error occurred receiving the message
rmq.Error("[NewConsumer]", "Error receiving message", err)
rmq.Error("[Consumer]", "Error receiving message", err)
return
}
rmq.Info("[NewConsumer]", "Received message",
rmq.Info("[Consumer]", "Received message",
fmt.Sprintf("%s", deliveryContext.Message().Data))
err = deliveryContext.Accept(context.Background())
@ -112,7 +117,7 @@ func main() {
publisher, err := amqpConnection.NewPublisher(context.Background(), &rmq.ExchangeAddress{
Exchange: exchangeName,
Key: routingKey,
}, "getting-started-publisher")
}, nil)
if err != nil {
rmq.Error("Error creating publisher", err)
return
@ -128,18 +133,15 @@ func main() {
}
switch publishResult.Outcome.(type) {
case *rmq.StateAccepted:
rmq.Info("[NewPublisher]", "Message accepted", publishResult.Message.Data[0])
break
rmq.Info("[Publisher]", "Message accepted", publishResult.Message.Data[0])
case *rmq.StateReleased:
rmq.Warn("[NewPublisher]", "Message was not routed", publishResult.Message.Data[0])
break
rmq.Warn("[Publisher]", "Message was not routed", publishResult.Message.Data[0])
case *rmq.StateRejected:
rmq.Warn("[NewPublisher]", "Message rejected", publishResult.Message.Data[0])
rmq.Warn("[Publisher]", "Message rejected", publishResult.Message.Data[0])
stateType := publishResult.Outcome.(*rmq.StateRejected)
if stateType.Error != nil {
rmq.Warn("[NewPublisher]", "Message rejected with error: %v", stateType.Error)
rmq.Warn("[Publisher]", "Message rejected with error: %v", stateType.Error)
}
break
default:
// these status are not supported. Leave it for AMQP 1.0 compatibility
// see: https://www.rabbitmq.com/docs/next/amqp#outcomes
@ -156,13 +158,13 @@ func main() {
//Close the consumer
err = consumer.Close(context.Background())
if err != nil {
rmq.Error("[NewConsumer]", err)
rmq.Error("[Consumer]", err)
return
}
// Close the publisher
err = publisher.Close(context.Background())
if err != nil {
rmq.Error("[NewPublisher]", err)
rmq.Error("[Publisher]", err)
return
}

View File

@ -17,8 +17,7 @@ func checkError(err error) {
func main() {
rmq.Info("Define the publisher message targets")
env := rmq.NewEnvironment([]string{"amqp://"}, nil)
env := rmq.NewEnvironment("amqp://guest:guest@localhost:5672/", nil)
amqpConnection, err := env.NewConnection(context.Background())
checkError(err)
queues := []string{"queue1", "queue2", "queue3"}
@ -31,7 +30,7 @@ func main() {
}
// create a publisher without a target
publisher, err := amqpConnection.NewPublisher(context.TODO(), nil, "stream-publisher")
publisher, err := amqpConnection.NewPublisher(context.TODO(), nil, nil)
checkError(err)
// publish messages to the stream
@ -55,7 +54,6 @@ func main() {
switch publishResult.Outcome.(type) {
case *amqp.StateAccepted:
rmq.Info("[Publisher]", "Message accepted", publishResult.Message.Data[0])
break
default:
rmq.Warn("[Publisher]", "Message not accepted", publishResult.Message.Data[0])
}

View File

@ -16,18 +16,19 @@ func main() {
var stateAccepted int32
var stateReleased int32
var stateRejected int32
var isRunning bool
var received int32
var failed int32
startTime := time.Now()
isRunning = true
go func() {
for {
for isRunning {
time.Sleep(5 * time.Second)
total := stateAccepted + stateReleased + stateRejected
messagesPerSecond := float64(total) / time.Since(startTime).Seconds()
rmq.Info("[Stats]", "sent", total, "received", received, "failed", failed, "messagesPerSecond", messagesPerSecond)
}
}()
@ -41,12 +42,22 @@ func main() {
switch statusChanged.To.(type) {
case *rmq.StateOpen:
signalBlock.Broadcast()
case *rmq.StateReconnecting:
rmq.Info("[connection]", "Reconnecting to the AMQP 1.0 server")
case *rmq.StateClosed:
StateClosed := statusChanged.To.(*rmq.StateClosed)
if errors.Is(StateClosed.GetError(), rmq.ErrMaxReconnectAttemptsReached) {
rmq.Error("[connection]", "Max reconnect attempts reached. Closing connection", StateClosed.GetError())
signalBlock.Broadcast()
isRunning = false
}
}
}
}(stateChanged)
// Open a connection to the AMQP 1.0 server
amqpConnection, err := rmq.Dial(context.Background(), []string{"amqp://"}, &rmq.AmqpConnOptions{
amqpConnection, err := rmq.Dial(context.Background(), "amqp://", &rmq.AmqpConnOptions{
SASLType: amqp.SASLTypeAnonymous(),
ContainerID: "reliable-amqp10-go",
RecoveryConfiguration: &rmq.RecoveryConfiguration{
@ -87,13 +98,13 @@ func main() {
// Consume messages from the queue
go func(ctx context.Context) {
for {
for isRunning {
deliveryContext, err := consumer.Receive(ctx)
if errors.Is(err, context.Canceled) {
// The consumer was closed correctly
return
}
if err != nil {
if err != nil && isRunning {
// An error occurred receiving the message
// here the consumer could be disconnected from the server due to a network error
signalBlock.L.Lock()
@ -107,7 +118,7 @@ func main() {
atomic.AddInt32(&received, 1)
err = deliveryContext.Accept(context.Background())
if err != nil {
if err != nil && isRunning {
// same here the delivery could not be accepted due to a network error
// we wait for 2_500 ms and try again
time.Sleep(2500 * time.Millisecond)
@ -118,18 +129,19 @@ func main() {
publisher, err := amqpConnection.NewPublisher(context.Background(), &rmq.QueueAddress{
Queue: queueName,
}, "reliable-publisher")
}, nil)
if err != nil {
rmq.Error("Error creating publisher", err)
return
}
wg := &sync.WaitGroup{}
for i := 0; i < 1; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for i := 0; i < 500_000; i++ {
if !isRunning {
rmq.Info("[Publisher]", "Publisher is stopped simulation not running, queue", queueName)
return
}
publishResult, err := publisher.Publish(context.Background(), rmq.NewMessage([]byte("Hello, World!"+fmt.Sprintf("%d", i))))
if err != nil {
// here you need to deal with the error. You can store the message in a local in memory/persistent storage
@ -147,13 +159,10 @@ func main() {
switch publishResult.Outcome.(type) {
case *rmq.StateAccepted:
atomic.AddInt32(&stateAccepted, 1)
break
case *rmq.StateReleased:
atomic.AddInt32(&stateReleased, 1)
break
case *rmq.StateRejected:
atomic.AddInt32(&stateRejected, 1)
break
default:
// these status are not supported. Leave it for AMQP 1.0 compatibility
// see: https://www.rabbitmq.com/docs/next/amqp#outcomes
@ -163,7 +172,6 @@ func main() {
}
}()
}
wg.Wait()
println("press any key to close the connection")

View File

@ -19,7 +19,8 @@ func main() {
rmq.Info("Golang AMQP 1.0 Streams example")
queueStream := "stream-go-queue-" + time.Now().String()
env := rmq.NewEnvironment([]string{"amqp://"}, nil)
env := rmq.NewEnvironment("amqp://guest:guest@localhost:5672/", nil)
amqpConnection, err := env.NewConnection(context.Background())
checkError(err)
management := amqpConnection.Management()
@ -35,7 +36,7 @@ func main() {
// create a stream publisher. In this case we use the QueueAddress to make the example
// simple. So we use the default exchange here.
publisher, err := amqpConnection.NewPublisher(context.TODO(), &rmq.QueueAddress{Queue: queueStream}, "stream-publisher")
publisher, err := amqpConnection.NewPublisher(context.TODO(), &rmq.QueueAddress{Queue: queueStream}, nil)
checkError(err)
// publish messages to the stream
@ -47,17 +48,14 @@ func main() {
switch publishResult.Outcome.(type) {
case *rmq.StateAccepted:
rmq.Info("[Publisher]", "Message accepted", publishResult.Message.Data[0])
break
case *rmq.StateReleased:
rmq.Warn("[Publisher]", "Message was not routed", publishResult.Message.Data[0])
break
case *rmq.StateRejected:
rmq.Warn("[Publisher]", "Message rejected", publishResult.Message.Data[0])
stateType := publishResult.Outcome.(*rmq.StateRejected)
if stateType.Error != nil {
rmq.Warn("[Publisher]", "Message rejected with error: %v", stateType.Error)
}
break
default:
// these status are not supported. Leave it for AMQP 1.0 compatibility
// see: https://www.rabbitmq.com/docs/next/amqp#outcomes

View File

@ -17,9 +17,10 @@ func checkError(err error) {
func main() {
// see also: https://www.rabbitmq.com/blog/2024/12/13/amqp-filter-expressions
rmq.Info("Golang AMQP 1.0 Streams example with filtering")
queueStream := "stream-go-queue-filtering-" + time.Now().String()
env := rmq.NewEnvironment([]string{"amqp://"}, nil)
env := rmq.NewEnvironment("amqp://guest:guest@localhost:5672/", nil)
amqpConnection, err := env.NewConnection(context.Background())
checkError(err)
management := amqpConnection.Management()
@ -35,7 +36,7 @@ func main() {
// create a stream publisher. In this case we use the QueueAddress to make the example
// simple. So we use the default exchange here.
publisher, err := amqpConnection.NewPublisher(context.TODO(), &rmq.QueueAddress{Queue: queueStream}, "stream-publisher")
publisher, err := amqpConnection.NewPublisher(context.TODO(), &rmq.QueueAddress{Queue: queueStream}, nil)
checkError(err)
filters := []string{"MyFilter1", "MyFilter2", "MyFilter3", "MyFilter4"}
@ -50,17 +51,14 @@ func main() {
switch publishResult.Outcome.(type) {
case *rmq.StateAccepted:
rmq.Info("[Publisher]", "Message accepted", publishResult.Message.Data[0])
break
case *rmq.StateReleased:
rmq.Warn("[Publisher]", "Message was not routed", publishResult.Message.Data[0])
break
case *rmq.StateRejected:
rmq.Warn("[Publisher]", "Message rejected", publishResult.Message.Data[0])
stateType := publishResult.Outcome.(*rmq.StateRejected)
if stateType.Error != nil {
rmq.Warn("[Publisher]", "Message rejected with error: %v", stateType.Error)
}
break
default:
// these status are not supported. Leave it for AMQP 1.0 compatibility
// see: https://www.rabbitmq.com/docs/next/amqp#outcomes
@ -76,7 +74,25 @@ func main() {
// add a filter to the consumer, in this case we use only the filter values
// MyFilter1 and MyFilter2. So all other messages won't be received
Filters: []string{"MyFilter1", "MyFilter2"},
StreamFilterOptions: &rmq.StreamFilterOptions{
Values: []string{"MyFilter1", "MyFilter2"},
// it is also possible to filter by application properties or message properties
// you can create filters like:
// msg.ApplicationProperties = map[string]interface{}{"key3": "value3"}
// during the publish you can do something like:
// msg.ApplicationProperties = map[string]interface{}{"key1": "value1"}
// publisher.Publish(context.Background(), msg)
//ApplicationProperties: nil,
// or here you can filter by message properties
// like:
// msg.Properties = &amqp.MessageProperties{Subject: "MySubject"}
// during the publish you can do something like:
// msg.Properties = &amqp.MessageProperties{Subject: "MySubject"}
// publisher.Publish(context.Background(), msg)
//Properties: nil,
// see amqp_consumer_stream_test.go for more examples
},
})
checkError(err)

55
docs/examples/tls/tls.go Normal file
View File

@ -0,0 +1,55 @@
package main
import (
"context"
"crypto/tls"
"crypto/x509"
"github.com/Azure/go-amqp"
rmq "github.com/rabbitmq/rabbitmq-amqp-go-client/pkg/rabbitmqamqp"
"os"
)
func check(err error) {
if err != nil {
panic(err)
}
}
func main() {
// to run the example you can use the certificates from the rabbitmq-amqp-go-client
// inside the directory .ci/certs
caCert, err := os.ReadFile("/path/ca_certificate.pem")
check(err)
// Create a CA certificate pool and add the CA certificate to it
caCertPool := x509.NewCertPool()
caCertPool.AppendCertsFromPEM(caCert)
// Load client cert
clientCert, err := tls.LoadX509KeyPair("/path//client_localhost_certificate.pem",
"/path//client_localhost_key.pem")
check(err)
// Create a TLS configuration
tlsConfig := &tls.Config{
Certificates: []tls.Certificate{clientCert},
RootCAs: caCertPool,
InsecureSkipVerify: false,
ServerName: "localhost", // the server name should match the name on the certificate
}
env := rmq.NewClusterEnvironment([]rmq.Endpoint{
{Address: "amqps://localhost:5671", Options: &rmq.AmqpConnOptions{
SASLType: amqp.SASLTypeAnonymous(),
TLSConfig: tlsConfig,
}},
})
connection, err := env.NewConnection(context.Background())
check(err)
// Close the connection
err = connection.Close(context.Background())
check(err)
}

View File

@ -11,7 +11,7 @@ func main() {
queueName := "getting-started-go-queue"
routingKey := "routing-key"
env := rmq.NewEnvironment([]string{"amqp://guest:guest@localhost:5672"}, nil)
env := rmq.NewEnvironment("amqp://guest:guest@localhost:5672/", nil)
// Open a connection to the AMQP 1.0 server ( RabbitMQ >= 4.0)
amqpConnection, err := env.NewConnection(context.Background())
@ -60,7 +60,7 @@ func main() {
publisher, err := amqpConnection.NewPublisher(context.Background(), &rmq.ExchangeAddress{
Exchange: exchangeName,
Key: routingKey,
}, "getting-started-publisher")
}, nil)
if err != nil {
rmq.Error("Error creating publisher", err)
return

9
go.mod
View File

@ -1,9 +1,10 @@
module github.com/rabbitmq/rabbitmq-amqp-go-client
go 1.22.0
go 1.23.0
require (
github.com/Azure/go-amqp v1.4.0
github.com/golang-jwt/jwt/v5 v5.2.2
github.com/google/uuid v1.6.0
github.com/onsi/ginkgo/v2 v2.22.1
github.com/onsi/gomega v1.36.2
@ -14,9 +15,9 @@ require (
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
github.com/google/go-cmp v0.6.0 // indirect
github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad // indirect
golang.org/x/net v0.33.0 // indirect
golang.org/x/sys v0.28.0 // indirect
golang.org/x/text v0.21.0 // indirect
golang.org/x/net v0.38.0 // indirect
golang.org/x/sys v0.31.0 // indirect
golang.org/x/text v0.23.0 // indirect
golang.org/x/tools v0.28.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

14
go.sum
View File

@ -8,6 +8,8 @@ github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad h1:a6HEuzUHeKH6hwfN/ZoQgRgVIWFJljSWa/zetS2WTvg=
@ -22,12 +24,12 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
golang.org/x/tools v0.28.0 h1:WuB6qZ4RPCQo5aP3WdKZS7i595EdWqWR8vqJTlwTVK8=
golang.org/x/tools v0.28.0/go.mod h1:dcIOrVd3mfQKTgrDVQHqCPMWy6lnhfhtX3hLXYVLfRw=
google.golang.org/protobuf v1.36.1 h1:yBPeRvTftaleIgM3PZ/WBIZ7XM/eEYAaEyCwvyjq/gk=

View File

@ -6,9 +6,9 @@ import (
"strings"
)
// TargetAddress is an interface that represents an address that can be used to send messages to.
// ITargetAddress is an interface that represents an address that can be used to send messages to.
// It can be either a Queue or an Exchange with a routing key.
type TargetAddress interface {
type ITargetAddress interface {
toAddress() (string, error)
}
@ -45,7 +45,7 @@ func (eas *ExchangeAddress) toAddress() (string, error) {
}
// address Creates the address for the exchange or queue following the RabbitMQ conventions.
// see: https://www.rabbitmq.com/docs/next/amqp#address-v2
// see: https://www.rabbitmq.com/docs/amqp#address-v2
func address(exchange, key, queue *string, urlParameters *string) (string, error) {
if exchange == nil && queue == nil {
return "", errors.New("exchange or queue must be set")
@ -61,9 +61,9 @@ func address(exchange, key, queue *string, urlParameters *string) (string, error
if !isStringNilOrEmpty(exchange) {
if !isStringNilOrEmpty(key) {
return "/" + exchanges + "/" + encodePathSegments(*exchange) + "/" + encodePathSegments(*key) + urlAppend, nil
return fmt.Sprintf("/%s/%s/%s%s", exchanges, encodePathSegments(*exchange), encodePathSegments(*key), urlAppend), nil
}
return "/" + exchanges + "/" + encodePathSegments(*exchange) + urlAppend, nil
return fmt.Sprintf("/%s/%s%s", exchanges, encodePathSegments(*exchange), urlAppend), nil
}
if queue == nil {
@ -73,8 +73,7 @@ func address(exchange, key, queue *string, urlParameters *string) (string, error
if isStringNilOrEmpty(queue) {
return "", errors.New("queue must be set")
}
return "/" + queues + "/" + encodePathSegments(*queue) + urlAppend, nil
return fmt.Sprintf("/%s/%s%s", queues, encodePathSegments(*queue), urlAppend), nil
}
// exchangeAddress Creates the address for the exchange

View File

@ -6,59 +6,57 @@ import (
)
var _ = Describe("address builder test ", func() {
It("With exchange, queue and key should raise and error", func() {
queue := "my_queue"
exchange := "my_exchange"
_, err := address(&exchange, nil, &queue, nil)
Expect(err).NotTo(BeNil())
Expect(err.Error()).To(Equal("exchange and queue cannot be set together"))
// Error cases
Describe("Error cases", func() {
DescribeTable("should return appropriate errors",
func(exchange, key, queue *string, expectedErr string) {
_, err := address(exchange, key, queue, nil)
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(Equal(expectedErr))
},
Entry("when both exchange and queue are set",
stringPtr("my_exchange"), nil, stringPtr("my_queue"),
"exchange and queue cannot be set together"),
Entry("when neither exchange nor queue is set",
nil, nil, nil,
"exchange or queue must be set"),
)
})
It("Without exchange and queue should raise and error", func() {
_, err := address(nil, nil, nil, nil)
Expect(err).NotTo(BeNil())
Expect(err.Error()).To(Equal("exchange or queue must be set"))
// Exchange-related cases
Describe("Exchange addresses", func() {
DescribeTable("should generate correct exchange addresses",
func(exchange, key *string, expected string) {
address, err := address(exchange, key, nil, nil)
Expect(err).NotTo(HaveOccurred())
Expect(address).To(Equal(expected))
},
Entry("with exchange and key",
stringPtr("my_exchange"), stringPtr("my_key"),
"/exchanges/my_exchange/my_key"),
Entry("with exchange only",
stringPtr("my_exchange"), nil,
"/exchanges/my_exchange"),
Entry("with special characters",
stringPtr("my_ exchange/()"), stringPtr("my_key "),
"/exchanges/my_%20exchange%2F%28%29/my_key%20"),
)
})
It("With exchange and key should return address", func() {
exchange := "my_exchange"
key := "my_key"
// Queue-related cases
Describe("Queue addresses", func() {
It("should generate correct queue address with special characters", func() {
queue := "my_queue>"
address, err := address(nil, nil, &queue, nil)
Expect(err).NotTo(HaveOccurred())
Expect(address).To(Equal("/queues/my_queue%3E"))
})
address, err := address(&exchange, &key, nil, nil)
Expect(err).To(BeNil())
Expect(address).To(Equal("/exchanges/my_exchange/my_key"))
It("should generate correct purge queue address", func() {
queue := "my_queue"
address, err := purgeQueueAddress(&queue)
Expect(err).NotTo(HaveOccurred())
Expect(address).To(Equal("/queues/my_queue/messages"))
})
})
It("With exchange should return address", func() {
exchange := "my_exchange"
address, err := address(&exchange, nil, nil, nil)
Expect(err).To(BeNil())
Expect(address).To(Equal("/exchanges/my_exchange"))
})
It("With exchange and key with names to encode should return the encoded address", func() {
exchange := "my_ exchange/()"
key := "my_key "
address, err := address(&exchange, &key, nil, nil)
Expect(err).To(BeNil())
Expect(address).To(Equal("/exchanges/my_%20exchange%2F%28%29/my_key%20"))
})
It("With queue should return address", func() {
queue := "my_queue>"
address, err := address(nil, nil, &queue, nil)
Expect(err).To(BeNil())
Expect(address).To(Equal("/queues/my_queue%3E"))
})
It("With queue and urlParameters should return address", func() {
queue := "my_queue"
address, err := purgeQueueAddress(&queue)
Expect(err).To(BeNil())
Expect(address).To(Equal("/queues/my_queue/messages"))
})
})

View File

@ -58,8 +58,8 @@ func (b *AMQPBinding) Bind(ctx context.Context) (string, error) {
kv[destination] = b.destinationName
kv["arguments"] = make(map[string]any)
_, err := b.management.Request(ctx, kv, path, commandPost, []int{responseCode204})
bindingPathWithExchangeQueueKey := bindingPathWithExchangeQueueKey(b.toQueue, b.sourceName, b.destinationName, b.bindingKey)
return bindingPathWithExchangeQueueKey, err
bindingPathWithExchangeQueueAndKey := bindingPathWithExchangeQueueKey(b.toQueue, b.sourceName, b.destinationName, b.bindingKey)
return bindingPathWithExchangeQueueAndKey, err
}
// Unbind removes a binding between an exchange and a queue or exchange

View File

@ -10,7 +10,7 @@ var _ = Describe("AMQP Bindings test ", func() {
var connection *AmqpConnection
var management *AmqpManagement
BeforeEach(func() {
conn, err := Dial(context.TODO(), []string{"amqp://"}, nil)
conn, err := Dial(context.TODO(), "amqp://", nil)
Expect(err).To(BeNil())
connection = conn
management = connection.Management()

View File

@ -12,18 +12,39 @@ import (
"time"
)
//func (c *ConnUrlHelper) UseSsl(value bool) {
// c.UseSsl = value
// if value {
// c.Scheme = "amqps"
// } else {
// c.Scheme = "amqp"
// }
//}
type AmqpAddress struct {
// the address of the AMQP server
// it is in the form of amqp://<host>:<port>
// or amqps://<host>:<port>
// the port is optional
// the default port is 5672
// the default protocol is amqp
// the default host is localhost
// the default virtual host is "/"
// the default user is guest
// the default password is guest
// the default SASL type is SASLTypeAnonymous
Address string
// Options: Additional options for the connection
Options *AmqpConnOptions
}
type OAuth2Options struct {
Token string
}
func (o OAuth2Options) Clone() *OAuth2Options {
cloned := &OAuth2Options{
Token: o.Token,
}
return cloned
}
type AmqpConnOptions struct {
// wrapper for amqp.ConnOptions
ContainerID string
// wrapper for amqp.ConnOptions
HostName string
// wrapper for amqp.ConnOptions
@ -51,8 +72,40 @@ type AmqpConnOptions struct {
// when the connection is closed unexpectedly.
RecoveryConfiguration *RecoveryConfiguration
// copy the addresses for reconnection
addresses []string
// The OAuth2Options is used to configure the connection with OAuth2 token.
OAuth2Options *OAuth2Options
// Local connection identifier (not sent to the server)
// if not provided, a random UUID is generated
Id string
}
func (a *AmqpConnOptions) isOAuth2() bool {
return a.OAuth2Options != nil
}
func (a *AmqpConnOptions) Clone() *AmqpConnOptions {
cloned := &AmqpConnOptions{
ContainerID: a.ContainerID,
IdleTimeout: a.IdleTimeout,
MaxFrameSize: a.MaxFrameSize,
MaxSessions: a.MaxSessions,
Properties: a.Properties,
SASLType: a.SASLType,
TLSConfig: a.TLSConfig,
WriteTimeout: a.WriteTimeout,
Id: a.Id,
}
if a.OAuth2Options != nil {
cloned.OAuth2Options = a.OAuth2Options.Clone()
}
if a.RecoveryConfiguration != nil {
cloned.RecoveryConfiguration = a.RecoveryConfiguration.Clone()
}
return cloned
}
type AmqpConnection struct {
@ -60,10 +113,10 @@ type AmqpConnection struct {
featuresAvailable *featuresAvailable
azureConnection *amqp.Conn
id string
management *AmqpManagement
lifeCycle *LifeCycle
amqpConnOptions *AmqpConnOptions
address string
session *amqp.Session
refMap *sync.Map
entitiesTracker *entitiesTracker
@ -75,9 +128,10 @@ func (a *AmqpConnection) Properties() map[string]any {
}
// NewPublisher creates a new Publisher that sends messages to the provided destination.
// The destination is a TargetAddress that can be a Queue or an Exchange with a routing key.
// The destination is a ITargetAddress that can be a Queue or an Exchange with a routing key.
// options is an IPublisherOptions that can be used to configure the publisher.
// See QueueAddress and ExchangeAddress for more information.
func (a *AmqpConnection) NewPublisher(ctx context.Context, destination TargetAddress, linkName string) (*Publisher, error) {
func (a *AmqpConnection) NewPublisher(ctx context.Context, destination ITargetAddress, options IPublisherOptions) (*Publisher, error) {
destinationAdd := ""
err := error(nil)
if destination != nil {
@ -91,14 +145,20 @@ func (a *AmqpConnection) NewPublisher(ctx context.Context, destination TargetAdd
}
}
return newPublisher(ctx, a, destinationAdd, linkName)
return newPublisher(ctx, a, destinationAdd, options)
}
// NewConsumer creates a new Consumer that listens to the provided destination. Destination is a QueueAddress.
func (a *AmqpConnection) NewConsumer(ctx context.Context, queueName string, options ConsumerOptions) (*Consumer, error) {
// NewConsumer creates a new Consumer that listens to the provided Queue
func (a *AmqpConnection) NewConsumer(ctx context.Context, queueName string, options IConsumerOptions) (*Consumer, error) {
destination := &QueueAddress{
Queue: queueName,
}
if options != nil {
err := options.validate(a.featuresAvailable)
if err != nil {
return nil, err
}
}
destinationAdd, err := destination.toAddress()
if err != nil {
@ -110,16 +170,53 @@ func (a *AmqpConnection) NewConsumer(ctx context.Context, queueName string, opti
// Dial connect to the AMQP 1.0 server using the provided connectionSettings
// Returns a pointer to the new AmqpConnection if successful else an error.
// addresses is a list of addresses to connect to. It picks one randomly.
// It is enough that one of the addresses is reachable.
func Dial(ctx context.Context, addresses []string, connOptions *AmqpConnOptions, args ...string) (*AmqpConnection, error) {
func Dial(ctx context.Context, address string, connOptions *AmqpConnOptions) (*AmqpConnection, error) {
connOptions, err := validateOptions(connOptions)
if err != nil {
return nil, err
}
// create the connection
conn := &AmqpConnection{
management: newAmqpManagement(),
lifeCycle: NewLifeCycle(),
amqpConnOptions: connOptions,
entitiesTracker: newEntitiesTracker(),
featuresAvailable: newFeaturesAvailable(),
}
err = conn.open(ctx, address, connOptions)
if err != nil {
return nil, err
}
conn.amqpConnOptions = connOptions
conn.address = address
conn.lifeCycle.SetState(&StateOpen{})
return conn, nil
}
func validateOptions(connOptions *AmqpConnOptions) (*AmqpConnOptions, error) {
if connOptions == nil {
connOptions = &AmqpConnOptions{
// RabbitMQ requires SASL security layer
// to be enabled for AMQP 1.0 connections.
// So this is mandatory and default in case not defined.
SASLType: amqp.SASLTypeAnonymous(),
connOptions = &AmqpConnOptions{}
}
if connOptions.SASLType == nil {
// RabbitMQ requires SASL security layer
// to be enabled for AMQP 1.0 connections.
// So this is mandatory and default in case not defined.
connOptions.SASLType = amqp.SASLTypeAnonymous()
}
if connOptions.Id == "" {
connOptions.Id = uuid.New().String()
}
// In case of OAuth2 token, the SASLType should be set to SASLTypePlain
if connOptions.isOAuth2() {
if connOptions.OAuth2Options.Token == "" {
return nil, fmt.Errorf("OAuth2 token is empty")
}
connOptions.SASLType = amqp.SASLTypePlain("", connOptions.OAuth2Options.Token)
}
if connOptions.RecoveryConfiguration == nil {
@ -134,37 +231,28 @@ func Dial(ctx context.Context, addresses []string, connOptions *AmqpConnOptions,
return nil, fmt.Errorf("BackOffReconnectInterval should be greater than 1 second")
}
// create the connection
conn := &AmqpConnection{
management: NewAmqpManagement(),
lifeCycle: NewLifeCycle(),
amqpConnOptions: connOptions,
entitiesTracker: newEntitiesTracker(),
featuresAvailable: newFeaturesAvailable(),
}
tmp := make([]string, len(addresses))
copy(tmp, addresses)
err := conn.open(ctx, addresses, connOptions, args...)
if err != nil {
return nil, err
}
conn.amqpConnOptions = connOptions
conn.amqpConnOptions.addresses = addresses
conn.lifeCycle.SetState(&StateOpen{})
return conn, nil
return connOptions, nil
}
// Open opens a connection to the AMQP 1.0 server.
// using the provided connectionSettings and the AMQPLite library.
// Setups the connection and the management interface.
func (a *AmqpConnection) open(ctx context.Context, addresses []string, connOptions *AmqpConnOptions, args ...string) error {
func (a *AmqpConnection) open(ctx context.Context, address string, connOptions *AmqpConnOptions) error {
// random pick and extract one address to use for connection
var azureConnection *amqp.Conn
//connOptions.hostName is the way to set the virtual host
// so we need to pre-parse the URI to get the virtual host
// the PARSE is copied from go-amqp091 library
// the URI will be parsed is parsed again in the amqp lite library
uri, err := ParseURI(address)
if err != nil {
return err
}
amqpLiteConnOptions := &amqp.ConnOptions{
ContainerID: connOptions.ContainerID,
HostName: connOptions.HostName,
HostName: fmt.Sprintf("vhost:%s", uri.Vhost),
IdleTimeout: connOptions.IdleTimeout,
MaxFrameSize: connOptions.MaxFrameSize,
MaxSessions: connOptions.MaxSessions,
@ -173,155 +261,134 @@ func (a *AmqpConnection) open(ctx context.Context, addresses []string, connOptio
TLSConfig: connOptions.TLSConfig,
WriteTimeout: connOptions.WriteTimeout,
}
tmp := make([]string, len(addresses))
copy(tmp, addresses)
// random pick and extract one address to use for connection
var azureConnection *amqp.Conn
for len(tmp) > 0 {
idx := random(len(tmp))
addr := tmp[idx]
//connOptions.HostName is the way to set the virtual host
// so we need to pre-parse the URI to get the virtual host
// the PARSE is copied from go-amqp091 library
// the URI will be parsed is parsed again in the amqp lite library
uri, err := ParseURI(addr)
if err != nil {
return err
}
connOptions.HostName = fmt.Sprintf("vhost:%s", uri.Vhost)
// remove the index from the tmp list
tmp = append(tmp[:idx], tmp[idx+1:]...)
azureConnection, err = amqp.Dial(ctx, addr, amqpLiteConnOptions)
if err != nil {
Error("Failed to open connection", ExtractWithoutPassword(addr), err)
continue
}
a.properties = azureConnection.Properties()
err = a.featuresAvailable.ParseProperties(a.properties)
if err != nil {
Warn("Validate properties Error.", ExtractWithoutPassword(addr), err)
}
if !a.featuresAvailable.is4OrMore {
Warn("The server version is less than 4.0.0", ExtractWithoutPassword(addr))
}
if !a.featuresAvailable.isRabbitMQ {
Warn("The server is not RabbitMQ", ExtractWithoutPassword(addr))
}
Debug("Connected to", ExtractWithoutPassword(addr))
break
azureConnection, err = amqp.Dial(ctx, address, amqpLiteConnOptions)
if err != nil {
Error("Failed to open connection", ExtractWithoutPassword(address), err, "ID", connOptions.Id)
return fmt.Errorf("failed to open connection: %w", err)
}
if azureConnection == nil {
return fmt.Errorf("failed to connect to any of the provided addresses")
a.properties = azureConnection.Properties()
err = a.featuresAvailable.ParseProperties(a.properties)
if err != nil {
Warn("Validate properties Error.", ExtractWithoutPassword(address), err)
}
if len(args) > 0 {
a.id = args[0]
} else {
a.id = uuid.New().String()
if !a.featuresAvailable.is4OrMore {
Warn("The server version is less than 4.0.0", ExtractWithoutPassword(address), "ID", connOptions.Id)
}
if !a.featuresAvailable.isRabbitMQ {
Warn("The server is not RabbitMQ", ExtractWithoutPassword(address))
}
Debug("Connected to", ExtractWithoutPassword(address), "ID", connOptions.Id)
a.azureConnection = azureConnection
var err error
a.session, err = a.azureConnection.NewSession(ctx, nil)
if err != nil {
return fmt.Errorf("failed to open session, for the connection id:%s, error: %w", a.Id(), err)
}
go func() {
select {
case <-azureConnection.Done():
{
a.lifeCycle.SetState(&StateClosed{error: azureConnection.Err()})
if azureConnection.Err() != nil {
Error("connection closed unexpectedly", "error", azureConnection.Err())
a.maybeReconnect()
<-azureConnection.Done()
{
a.lifeCycle.SetState(&StateClosed{error: azureConnection.Err()})
if azureConnection.Err() != nil {
Error("connection closed unexpectedly", "error", azureConnection.Err(), "ID", a.Id())
a.maybeReconnect()
return
}
Debug("connection closed successfully")
return
}
Debug("connection closed successfully", "ID", a.Id())
}
}()
if err != nil {
return err
}
err = a.management.Open(ctx, a)
if err != nil {
// TODO close connection?
return err
}
Debug("Management interface opened", "ID", a.Id())
return nil
}
func (a *AmqpConnection) maybeReconnect() {
if !a.amqpConnOptions.RecoveryConfiguration.ActiveRecovery {
Info("Recovery is disabled, closing connection")
Info("Recovery is disabled, closing connection", "ID", a.Id())
return
}
a.lifeCycle.SetState(&StateReconnecting{})
numberOfAttempts := 1
waitTime := a.amqpConnOptions.RecoveryConfiguration.BackOffReconnectInterval
reconnected := false
for numberOfAttempts <= a.amqpConnOptions.RecoveryConfiguration.MaxReconnectAttempts {
// Add exponential backoff with jitter
baseDelay := a.amqpConnOptions.RecoveryConfiguration.BackOffReconnectInterval
maxDelay := 1 * time.Minute
for attempt := 1; attempt <= a.amqpConnOptions.RecoveryConfiguration.MaxReconnectAttempts; attempt++ {
///wait for before reconnecting
// add some random milliseconds to the wait time to avoid thundering herd
// the random time is between 0 and 500 milliseconds
waitTime = waitTime + time.Duration(rand.Intn(500))*time.Millisecond
// Calculate delay with exponential backoff and jitter
jitter := time.Duration(rand.Intn(500)) * time.Millisecond
delay := baseDelay + jitter
if delay > maxDelay {
delay = maxDelay
}
Info("Waiting before reconnecting", "in", waitTime, "attempt", numberOfAttempts)
time.Sleep(waitTime)
Info("Attempting reconnection", "attempt", attempt, "delay", delay, "ID", a.Id())
time.Sleep(delay)
// context with timeout
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
// try to createSender
err := a.open(ctx, a.amqpConnOptions.addresses, a.amqpConnOptions)
err := a.open(ctx, a.address, a.amqpConnOptions)
cancel()
if err != nil {
numberOfAttempts++
waitTime = waitTime * 2
Error("Failed to connection. ", "id", a.Id(), "error", err)
} else {
reconnected = true
break
if err == nil {
a.restartEntities()
a.lifeCycle.SetState(&StateOpen{})
return
}
baseDelay *= 2
Error("Reconnection attempt failed", "attempt", attempt, "error", err, "ID", a.Id())
}
if reconnected {
var fails int32
Info("Reconnected successfully, restarting publishers and consumers")
a.entitiesTracker.publishers.Range(func(key, value any) bool {
publisher := value.(*Publisher)
// try to createSender
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
err := publisher.createSender(ctx)
if err != nil {
atomic.AddInt32(&fails, 1)
Error("Failed to createSender publisher", "ID", publisher.Id(), "error", err)
}
cancel()
return true
})
Info("Restarted publishers", "number of fails", fails)
fails = 0
a.entitiesTracker.consumers.Range(func(key, value any) bool {
consumer := value.(*Consumer)
// try to createSender
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
err := consumer.createReceiver(ctx)
if err != nil {
atomic.AddInt32(&fails, 1)
Error("Failed to createReceiver consumer", "ID", consumer.Id(), "error", err)
}
cancel()
return true
})
Info("Restarted consumers", "number of fails", fails)
// If we reach here, all attempts failed
Error("All reconnection attempts failed, closing connection", "ID", a.Id())
a.lifeCycle.SetState(&StateClosed{error: ErrMaxReconnectAttemptsReached})
a.lifeCycle.SetState(&StateOpen{})
}
}
// restartEntities attempts to restart all publishers and consumers after a reconnection
func (a *AmqpConnection) restartEntities() {
var publisherFails, consumerFails int32
// Restart publishers
a.entitiesTracker.publishers.Range(func(key, value any) bool {
publisher := value.(*Publisher)
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
if err := publisher.createSender(ctx); err != nil {
atomic.AddInt32(&publisherFails, 1)
Error("Failed to restart publisher", "ID", publisher.Id(), "error", err, "ID", a.Id())
}
return true
})
// Restart consumers
a.entitiesTracker.consumers.Range(func(key, value any) bool {
consumer := value.(*Consumer)
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
if err := consumer.createReceiver(ctx); err != nil {
atomic.AddInt32(&consumerFails, 1)
Error("Failed to restart consumer", "ID", consumer.Id(), "error", err, "ID", a.Id())
}
return true
})
Info("Entity restart complete",
"publisherFails", publisherFails,
"consumerFails", consumerFails)
}
func (a *AmqpConnection) close() {
@ -343,7 +410,7 @@ func (a *AmqpConnection) Close(ctx context.Context) error {
err := a.management.Close(ctx)
if err != nil {
Error("Failed to close management", "error:", err)
Error("Failed to close management", "error:", err, "ID", a.Id())
}
err = a.azureConnection.Close()
a.close()
@ -353,15 +420,15 @@ func (a *AmqpConnection) Close(ctx context.Context) error {
// NotifyStatusChange registers a channel to receive getState change notifications
// from the connection.
func (a *AmqpConnection) NotifyStatusChange(channel chan *StateChanged) {
a.lifeCycle.chStatusChanged = channel
a.lifeCycle.notifyStatusChange(channel)
}
func (a *AmqpConnection) State() LifeCycleState {
func (a *AmqpConnection) State() ILifeCycleState {
return a.lifeCycle.State()
}
func (a *AmqpConnection) Id() string {
return a.id
return a.amqpConnOptions.Id
}
// *** management section ***
@ -372,4 +439,24 @@ func (a *AmqpConnection) Management() *AmqpManagement {
return a.management
}
func (a *AmqpConnection) RefreshToken(background context.Context, token string) error {
if !a.amqpConnOptions.isOAuth2() {
return fmt.Errorf("the connection is not configured to use OAuth2 token")
}
if a.amqpConnOptions.isOAuth2() && !a.featuresAvailable.is41OrMore {
return fmt.Errorf("the server does not support OAuth2 token, you need to upgrade to RabbitMQ 4.1 or later")
}
err := a.Management().refreshToken(background, token)
if err != nil {
return err
}
// update the SASLType in case of reconnect after token refresh
// it should use the new token
a.amqpConnOptions.SASLType = amqp.SASLTypePlain("", token)
return nil
}
//*** end management section ***

View File

@ -1,10 +1,14 @@
package rabbitmqamqp
import (
"errors"
"sync"
"time"
)
// ErrMaxReconnectAttemptsReached typed error when the MaxReconnectAttempts is reached
var ErrMaxReconnectAttemptsReached = errors.New("max reconnect attempts reached, connection will not be recovered")
type RecoveryConfiguration struct {
/*
ActiveRecovery Define if the recovery is activated.
@ -29,6 +33,17 @@ type RecoveryConfiguration struct {
MaxReconnectAttempts int
}
func (c *RecoveryConfiguration) Clone() *RecoveryConfiguration {
cloned := &RecoveryConfiguration{
ActiveRecovery: c.ActiveRecovery,
BackOffReconnectInterval: c.BackOffReconnectInterval,
MaxReconnectAttempts: c.MaxReconnectAttempts,
}
return cloned
}
func NewRecoveryConfiguration() *RecoveryConfiguration {
return &RecoveryConfiguration{
ActiveRecovery: true,
@ -49,35 +64,19 @@ func newEntitiesTracker() *entitiesTracker {
}
}
func (e *entitiesTracker) storeOrReplaceProducer(entity entityIdentifier) {
func (e *entitiesTracker) storeOrReplaceProducer(entity iEntityIdentifier) {
e.publishers.Store(entity.Id(), entity)
}
func (e *entitiesTracker) getProducer(id string) (*Publisher, bool) {
producer, ok := e.publishers.Load(id)
if !ok {
return nil, false
}
return producer.(*Publisher), true
}
func (e *entitiesTracker) removeProducer(entity entityIdentifier) {
func (e *entitiesTracker) removeProducer(entity iEntityIdentifier) {
e.publishers.Delete(entity.Id())
}
func (e *entitiesTracker) storeOrReplaceConsumer(entity entityIdentifier) {
func (e *entitiesTracker) storeOrReplaceConsumer(entity iEntityIdentifier) {
e.consumers.Store(entity.Id(), entity)
}
func (e *entitiesTracker) getConsumer(id string) (*Consumer, bool) {
consumer, ok := e.consumers.Load(id)
if !ok {
return nil, false
}
return consumer.(*Consumer), true
}
func (e *entitiesTracker) removeConsumer(entity entityIdentifier) {
func (e *entitiesTracker) removeConsumer(entity iEntityIdentifier) {
e.consumers.Delete(entity.Id())
}

View File

@ -2,11 +2,12 @@ package rabbitmqamqp
import (
"context"
"time"
"github.com/Azure/go-amqp"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
testhelper "github.com/rabbitmq/rabbitmq-amqp-go-client/pkg/test-helper"
"time"
)
var _ = Describe("Recovery connection test", func() {
@ -22,7 +23,7 @@ var _ = Describe("Recovery connection test", func() {
*/
name := "connection should reconnect producers and consumers if dropped by via REST API"
connection, err := Dial(context.Background(), []string{"amqp://"}, &AmqpConnOptions{
env := NewEnvironment("amqp://", &AmqpConnOptions{
SASLType: amqp.SASLTypeAnonymous(),
ContainerID: name,
// reduced the reconnect interval to speed up the test
@ -31,7 +32,10 @@ var _ = Describe("Recovery connection test", func() {
BackOffReconnectInterval: 2 * time.Second,
MaxReconnectAttempts: 5,
},
Id: "reconnect producers and consumers",
})
connection, err := env.NewConnection(context.Background())
Expect(err).To(BeNil())
ch := make(chan *StateChanged, 1)
connection.NotifyStatusChange(ch)
@ -45,10 +49,11 @@ var _ = Describe("Recovery connection test", func() {
consumer, err := connection.NewConsumer(context.Background(),
qName, nil)
Expect(err).To(BeNil())
publisher, err := connection.NewPublisher(context.Background(), &QueueAddress{
Queue: qName,
}, "test")
}, nil)
Expect(err).To(BeNil())
Expect(publisher).NotTo(BeNil())
@ -113,11 +118,28 @@ var _ = Describe("Recovery connection test", func() {
// from reconnecting to open
// from open to closed (without error)
Expect(err).To(BeNil())
Expect(consumer.Close(context.Background())).NotTo(BeNil())
Expect(publisher.Close(context.Background())).NotTo(BeNil())
entLen := 0
connection.entitiesTracker.consumers.Range(func(key, value interface{}) bool {
entLen++
return true
})
Expect(entLen).To(Equal(0))
entLen = 0
connection.entitiesTracker.publishers.Range(func(key, value interface{}) bool {
entLen++
return true
})
Expect(entLen).To(Equal(0))
})
It("connection should not reconnect producers and consumers if the auto-recovery is disabled", func() {
name := "connection should reconnect producers and consumers if dropped by via REST API"
connection, err := Dial(context.Background(), []string{"amqp://"}, &AmqpConnOptions{
connection, err := Dial(context.Background(), "amqp://", &AmqpConnOptions{
SASLType: amqp.SASLTypeAnonymous(),
ContainerID: name,
// reduced the reconnect interval to speed up the test
@ -155,7 +177,7 @@ var _ = Describe("Recovery connection test", func() {
It("validate the Recovery connection parameters", func() {
_, err := Dial(context.Background(), []string{"amqp://"}, &AmqpConnOptions{
_, err := Dial(context.Background(), "amqp://", &AmqpConnOptions{
SASLType: amqp.SASLTypeAnonymous(),
// reduced the reconnect interval to speed up the test
RecoveryConfiguration: &RecoveryConfiguration{
@ -167,7 +189,7 @@ var _ = Describe("Recovery connection test", func() {
Expect(err).NotTo(BeNil())
Expect(err.Error()).To(ContainSubstring("BackOffReconnectInterval should be greater than"))
_, err = Dial(context.Background(), []string{"amqp://"}, &AmqpConnOptions{
_, err = Dial(context.Background(), "amqp://", &AmqpConnOptions{
SASLType: amqp.SASLTypeAnonymous(),
RecoveryConfiguration: &RecoveryConfiguration{
ActiveRecovery: true,

View File

@ -2,25 +2,29 @@ package rabbitmqamqp
import (
"context"
"crypto/tls"
"crypto/x509"
"fmt"
"github.com/Azure/go-amqp"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"os"
"sync"
"time"
)
var _ = Describe("AMQP connection Test", func() {
It("AMQP SASLTypeAnonymous connection should succeed", func() {
connection, err := Dial(context.Background(), []string{"amqp://"}, &AmqpConnOptions{
connection, err := Dial(context.Background(), "amqp://", &AmqpConnOptions{
SASLType: amqp.SASLTypeAnonymous()})
Expect(err).To(BeNil())
err = connection.Close(context.Background())
Expect(err).To(BeNil())
})
//
It("AMQP SASLTypePlain connection should succeed", func() {
connection, err := Dial(context.Background(), []string{"amqp://"}, &AmqpConnOptions{
connection, err := Dial(context.Background(), "amqp://", &AmqpConnOptions{
SASLType: amqp.SASLTypePlain("guest", "guest")})
Expect(err).To(BeNil())
@ -29,38 +33,17 @@ var _ = Describe("AMQP connection Test", func() {
err = connection.Close(context.Background())
Expect(err).To(BeNil())
})
It("AMQP connection connect to the one correct uri and fails the others", func() {
conn, err := Dial(context.Background(), []string{"amqp://localhost:1234", "amqp://nohost:555", "amqp://"}, nil)
Expect(err).To(BeNil())
Expect(conn.Close(context.Background()))
})
It("AMQP connection should fail due of wrong Port", func() {
_, err := Dial(context.Background(), []string{"amqp://localhost:1234"}, nil)
Expect(err).NotTo(BeNil())
})
It("AMQP connection should fail due of wrong Host", func() {
_, err := Dial(context.Background(), []string{"amqp://wrong_host:5672"}, nil)
Expect(err).NotTo(BeNil())
})
It("AMQP connection should fails with all the wrong uris", func() {
_, err := Dial(context.Background(), []string{"amqp://localhost:1234", "amqp://nohost:555", "amqp://nono"}, nil)
Expect(err).NotTo(BeNil())
})
//
It("AMQP connection should fail due to context cancellation", func() {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Millisecond)
cancel()
_, err := Dial(ctx, []string{"amqp://"}, nil)
_, err := Dial(ctx, "amqp://", nil)
Expect(err).NotTo(BeNil())
})
//
It("AMQP connection should receive events", func() {
ch := make(chan *StateChanged, 1)
connection, err := Dial(context.Background(), []string{"amqp://"}, nil)
connection, err := Dial(context.Background(), "amqp://", nil)
Expect(err).To(BeNil())
connection.NotifyStatusChange(ch)
err = connection.Close(context.Background())
@ -72,19 +55,113 @@ var _ = Describe("AMQP connection Test", func() {
Expect(recv.To).To(Equal(&StateClosed{}))
})
//It("AMQP TLS connection should success with SASLTypeAnonymous ", func() {
// amqpConnection := NewAmqpConnection()
// Expect(amqpConnection).NotTo(BeNil())
// Expect(amqpConnection).To(BeAssignableToTypeOf(&AmqpConnection{}))
//
// connectionSettings := NewConnUrlHelper().
// UseSsl(true).Port(5671).TlsConfig(&tls.Config{
// //ServerName: "localhost",
// InsecureSkipVerify: true,
// })
// Expect(connectionSettings).NotTo(BeNil())
// Expect(connectionSettings).To(BeAssignableToTypeOf(&ConnUrlHelper{}))
// err := amqpConnection.Open(context.Background(), connectionSettings)
// Expect(err).To(BeNil())
//})
It("Entity tracker should be aligned with consumers and publishers ", func() {
connection, err := Dial(context.Background(), "amqp://", &AmqpConnOptions{
SASLType: amqp.SASLTypeAnonymous()})
Expect(err).To(BeNil())
Expect(connection).NotTo(BeNil())
queueName := generateNameWithDateTime("Entity tracker should be aligned with consumers and publishers")
_, err = connection.Management().DeclareQueue(context.Background(), &QuorumQueueSpecification{
Name: queueName,
})
Expect(err).To(BeNil())
publisher, err := connection.NewPublisher(context.Background(), &QueueAddress{Queue: queueName},
&PublisherOptions{
Id: "my_id",
SenderLinkName: "my_sender_link",
})
Expect(err).To(BeNil())
Expect(publisher).NotTo(BeNil())
consumer, err := connection.NewConsumer(context.Background(), queueName, nil)
Expect(err).To(BeNil())
Expect(consumer).NotTo(BeNil())
// check the entity tracker
Expect(connection.entitiesTracker).NotTo(BeNil())
entLen := 0
connection.entitiesTracker.consumers.Range(func(key, value interface{}) bool {
entLen++
return true
})
Expect(entLen).To(Equal(1))
entLen = 0
connection.entitiesTracker.publishers.Range(func(key, value interface{}) bool {
entLen++
return true
})
Expect(entLen).To(Equal(1))
Expect(consumer.Close(context.Background())).To(BeNil())
Expect(publisher.Close(context.Background())).To(BeNil())
entLen = 0
connection.entitiesTracker.consumers.Range(func(key, value interface{}) bool {
entLen++
return true
})
Expect(entLen).To(Equal(0))
entLen = 0
connection.entitiesTracker.publishers.Range(func(key, value interface{}) bool {
entLen++
return true
})
Expect(entLen).To(Equal(0))
err = connection.Management().DeleteQueue(context.Background(), queueName)
Expect(err).To(BeNil())
Expect(connection.Close(context.Background())).To(BeNil())
})
Describe("AMQP TLS connection should succeed with in different vhosts with Anonymous and External.", func() {
wg := &sync.WaitGroup{}
wg.Add(4)
DescribeTable("TLS connection should success in different vhosts ", func(virtualHost string, sasl amqp.SASLType) {
// Load CA cert
caCert, err := os.ReadFile("../../.ci/certs/ca_certificate.pem")
Expect(err).To(BeNil())
// Create a CA certificate pool and add the CA certificate to it
caCertPool := x509.NewCertPool()
caCertPool.AppendCertsFromPEM(caCert)
// Load client cert
clientCert, err := tls.LoadX509KeyPair("../../.ci/certs/client_localhost_certificate.pem",
"../../.ci/certs/client_localhost_key.pem")
Expect(err).To(BeNil())
// Create a TLS configuration
tlsConfig := &tls.Config{
Certificates: []tls.Certificate{clientCert},
RootCAs: caCertPool,
InsecureSkipVerify: false,
ServerName: "localhost",
}
// Dial the AMQP server with TLS configuration
connection, err := Dial(context.Background(), fmt.Sprintf("amqps://localhost:5671/%s", virtualHost), &AmqpConnOptions{
SASLType: sasl,
TLSConfig: tlsConfig,
})
Expect(err).To(BeNil())
Expect(connection).NotTo(BeNil())
// Close the connection
err = connection.Close(context.Background())
Expect(err).To(BeNil())
wg.Done()
},
Entry("with virtual host. External", "%2F", amqp.SASLTypeExternal("")),
Entry("with a not default virtual host. External", "tls", amqp.SASLTypeExternal("")),
Entry("with virtual host. Anonymous", "%2F", amqp.SASLTypeAnonymous()),
Entry("with a not default virtual host. Anonymous", "tls", amqp.SASLTypeAnonymous()),
)
go func() {
wg.Wait()
}()
})
})

View File

@ -25,17 +25,23 @@ func (dc *DeliveryContext) Discard(ctx context.Context, e *amqp.Error) error {
return dc.receiver.RejectMessage(ctx, dc.message, e)
}
func (dc *DeliveryContext) DiscardWithAnnotations(ctx context.Context, annotations amqp.Annotations) error {
// copyAnnotations helper function to copy annotations
func copyAnnotations(annotations amqp.Annotations) (amqp.Annotations, error) {
if err := validateMessageAnnotations(annotations); err != nil {
return nil, err
}
destination := make(amqp.Annotations)
for key, value := range annotations {
destination[key] = value
}
return destination, nil
}
func (dc *DeliveryContext) DiscardWithAnnotations(ctx context.Context, annotations amqp.Annotations) error {
destination, err := copyAnnotations(annotations)
if err != nil {
return err
}
// copy the rabbitmq annotations to amqp annotations
destination := make(amqp.Annotations)
for keyA, value := range annotations {
destination[keyA] = value
}
return dc.receiver.ModifyMessage(ctx, dc.message, &amqp.ModifyMessageOptions{
DeliveryFailed: true,
UndeliverableHere: true,
@ -48,15 +54,10 @@ func (dc *DeliveryContext) Requeue(ctx context.Context) error {
}
func (dc *DeliveryContext) RequeueWithAnnotations(ctx context.Context, annotations amqp.Annotations) error {
if err := validateMessageAnnotations(annotations); err != nil {
destination, err := copyAnnotations(annotations)
if err != nil {
return err
}
// copy the rabbitmq annotations to amqp annotations
destination := make(amqp.Annotations)
for key, value := range annotations {
destination[key] = value
}
return dc.receiver.ModifyMessage(ctx, dc.message, &amqp.ModifyMessageOptions{
DeliveryFailed: false,
UndeliverableHere: false,
@ -67,7 +68,7 @@ func (dc *DeliveryContext) RequeueWithAnnotations(ctx context.Context, annotatio
type Consumer struct {
receiver atomic.Pointer[amqp.Receiver]
connection *AmqpConnection
options ConsumerOptions
options IConsumerOptions
destinationAdd string
id string
@ -84,10 +85,10 @@ func (c *Consumer) Id() string {
return c.id
}
func newConsumer(ctx context.Context, connection *AmqpConnection, destinationAdd string, options ConsumerOptions, args ...string) (*Consumer, error) {
func newConsumer(ctx context.Context, connection *AmqpConnection, destinationAdd string, options IConsumerOptions) (*Consumer, error) {
id := fmt.Sprintf("consumer-%s", uuid.New().String())
if len(args) > 0 {
id = args[0]
if options != nil && options.id() != "" {
id = options.id()
}
r := &Consumer{connection: connection, options: options,
@ -144,5 +145,6 @@ func (c *Consumer) Receive(ctx context.Context) (*DeliveryContext, error) {
}
func (c *Consumer) Close(ctx context.Context) error {
c.connection.entitiesTracker.removeConsumer(c)
return c.receiver.Load().Close(ctx)
}

View File

@ -7,31 +7,10 @@ import (
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
testhelper "github.com/rabbitmq/rabbitmq-amqp-go-client/pkg/test-helper"
"strconv"
"sync"
"time"
)
func publishMessagesWithStreamTag(queueName string, filterValue string, count int) {
conn, err := Dial(context.TODO(), []string{"amqp://guest:guest@localhost"}, nil)
Expect(err).To(BeNil())
publisher, err := conn.NewPublisher(context.TODO(), &QueueAddress{Queue: queueName}, "producer_filter_stream")
Expect(err).To(BeNil())
Expect(publisher).NotTo(BeNil())
for i := 0; i < count; i++ {
body := filterValue + " #" + strconv.Itoa(i)
msg := NewMessageWithFilter([]byte(body), filterValue)
publishResult, err := publisher.Publish(context.TODO(), msg)
Expect(err).To(BeNil())
Expect(publishResult).NotTo(BeNil())
Expect(publishResult.Outcome).To(Equal(&amqp.StateAccepted{}))
}
err = conn.Close(context.TODO())
Expect(err).To(BeNil())
}
var _ = Describe("Consumer stream test", func() {
It("start consuming with different offset types", func() {
@ -49,7 +28,7 @@ var _ = Describe("Consumer stream test", func() {
*/
qName := generateName("start consuming with different offset types")
connection, err := Dial(context.Background(), []string{"amqp://"}, nil)
connection, err := Dial(context.Background(), "amqp://", nil)
Expect(err).To(BeNil())
queueInfo, err := connection.Management().DeclareQueue(context.Background(), &StreamQueueSpecification{
Name: qName,
@ -73,7 +52,7 @@ var _ = Describe("Consumer stream test", func() {
dc, err := consumerOffsetValue.Receive(context.Background())
Expect(err).To(BeNil())
Expect(fmt.Sprintf("%s", dc.Message().GetData())).To(Equal(fmt.Sprintf("Message #%d", i+5)))
Expect(string(dc.Message().GetData())).To(Equal(fmt.Sprintf("Message #%d", i+5)))
Expect(dc.Accept(context.Background())).To(BeNil())
}
@ -88,7 +67,7 @@ var _ = Describe("Consumer stream test", func() {
dc, err := consumerFirst.Receive(context.Background())
Expect(err).To(BeNil())
Expect(dc.Message()).NotTo(BeNil())
Expect(fmt.Sprintf("%s", dc.Message().GetData())).To(Equal(fmt.Sprintf("Message #%d", i)))
Expect(string(dc.Message().GetData())).To(Equal(fmt.Sprintf("Message #%d", i)))
Expect(dc.Accept(context.Background())).To(BeNil())
}
@ -107,7 +86,7 @@ var _ = Describe("Consumer stream test", func() {
dc, err := consumerLast.Receive(context.Background())
Expect(err).To(BeNil())
Expect(dc.Message()).NotTo(BeNil())
Expect(fmt.Sprintf("%s", dc.Message().GetData())).NotTo(Equal(fmt.Sprintf("Message #%d", 0)))
Expect(string(dc.Message().GetData())).NotTo(Equal(fmt.Sprintf("Message #%d", 0)))
Expect(dc.Accept(context.Background())).To(BeNil())
consumerNext, err := connection.NewConsumer(context.Background(), qName, &StreamConsumerOptions{
@ -125,7 +104,7 @@ var _ = Describe("Consumer stream test", func() {
dc, err = consumerNext.Receive(context.Background())
Expect(err).To(BeNil())
Expect(dc.Message()).NotTo(BeNil())
Expect(fmt.Sprintf("%s", dc.Message().GetData())).To(Equal("the next message"))
Expect(string(dc.Message().GetData())).To(Equal("the next message"))
Expect(dc.Accept(context.Background())).To(BeNil())
signal <- struct{}{}
}()
@ -146,7 +125,7 @@ var _ = Describe("Consumer stream test", func() {
*/
qName := generateName("consumer should restart form the last offset in case of disconnection")
connection, err := Dial(context.Background(), []string{"amqp://"}, &AmqpConnOptions{
connection, err := Dial(context.Background(), "amqp://", &AmqpConnOptions{
SASLType: amqp.SASLTypeAnonymous(),
ContainerID: qName,
RecoveryConfiguration: &RecoveryConfiguration{
@ -179,7 +158,7 @@ var _ = Describe("Consumer stream test", func() {
dc, err := consumer.Receive(context.Background())
Expect(err).To(BeNil())
Expect(dc.Message()).NotTo(BeNil())
Expect(fmt.Sprintf("%s", dc.Message().GetData())).To(Equal(fmt.Sprintf("Message #%d", i)))
Expect(string(dc.Message().GetData())).To(Equal(fmt.Sprintf("Message #%d", i)))
Expect(dc.Accept(context.Background())).To(BeNil())
}
@ -200,7 +179,7 @@ var _ = Describe("Consumer stream test", func() {
dc, err := consumer.Receive(context.Background())
Expect(err).To(BeNil())
Expect(dc.Message()).NotTo(BeNil())
Expect(fmt.Sprintf("%s", dc.Message().GetData())).To(Equal(fmt.Sprintf("Message #%d", i)))
Expect(string(dc.Message().GetData())).To(Equal(fmt.Sprintf("Message #%d", i)))
}
Expect(consumer.Close(context.Background())).To(BeNil())
@ -209,7 +188,7 @@ var _ = Describe("Consumer stream test", func() {
})
It("consumer should filter messages based on x-stream-filter", func() {
qName := generateName("consumer should filter messages based on x-stream-filter")
connection, err := Dial(context.Background(), []string{"amqp://"}, nil)
connection, err := Dial(context.Background(), "amqp://", nil)
Expect(err).To(BeNil())
queueInfo, err := connection.Management().DeclareQueue(context.Background(), &StreamQueueSpecification{
Name: qName,
@ -217,15 +196,34 @@ var _ = Describe("Consumer stream test", func() {
Expect(err).To(BeNil())
Expect(queueInfo).NotTo(BeNil())
Expect(queueInfo.name).To(Equal(qName))
publishMessagesWithStreamTag(qName, "banana", 10)
publishMessagesWithStreamTag(qName, "apple", 10)
publishMessagesWithStreamTag(qName, "", 10)
publishMessagesWithMessageLogic(qName, "banana", 10, func(msg *amqp.Message) {
msg.Annotations = amqp.Annotations{
// here we set the filter value taken from the filters array
StreamFilterValue: "banana",
}
})
publishMessagesWithMessageLogic(qName, "apple", 10, func(msg *amqp.Message) {
msg.Annotations = amqp.Annotations{
// here we set the filter value taken from the filters array
StreamFilterValue: "apple",
}
})
publishMessagesWithMessageLogic(qName, "", 10, func(msg *amqp.Message) {
msg.Annotations = amqp.Annotations{
// here we set the filter value taken from the filters array
StreamFilterValue: "",
}
})
consumerBanana, err := connection.NewConsumer(context.Background(), qName, &StreamConsumerOptions{
ReceiverLinkName: "consumer banana should filter messages based on x-stream-filter",
InitialCredits: 200,
Offset: &OffsetFirst{},
Filters: []string{"banana"},
StreamFilterOptions: &StreamFilterOptions{
Values: []string{"banana"},
},
})
Expect(err).To(BeNil())
@ -235,16 +233,18 @@ var _ = Describe("Consumer stream test", func() {
dc, err := consumerBanana.Receive(context.Background())
Expect(err).To(BeNil())
Expect(dc.Message()).NotTo(BeNil())
Expect(fmt.Sprintf("%s", dc.Message().GetData())).To(Equal(fmt.Sprintf("banana #%d", i)))
Expect(string(dc.Message().GetData())).To(Equal(fmt.Sprintf("Message_id:%d_label:%s", i, "banana")))
Expect(dc.Accept(context.Background())).To(BeNil())
}
consumerApple, err := connection.NewConsumer(context.Background(), qName, &StreamConsumerOptions{
ReceiverLinkName: "consumer apple should filter messages based on x-stream-filter",
InitialCredits: 200,
Offset: &OffsetFirst{},
Filters: []string{"apple"},
FilterMatchUnfiltered: true,
ReceiverLinkName: "consumer apple should filter messages based on x-stream-filter",
InitialCredits: 200,
Offset: &OffsetFirst{},
StreamFilterOptions: &StreamFilterOptions{
Values: []string{"apple"},
MatchUnfiltered: true,
},
})
Expect(err).To(BeNil())
@ -254,7 +254,8 @@ var _ = Describe("Consumer stream test", func() {
dc, err := consumerApple.Receive(context.Background())
Expect(err).To(BeNil())
Expect(dc.Message()).NotTo(BeNil())
Expect(fmt.Sprintf("%s", dc.Message().GetData())).To(Equal(fmt.Sprintf("apple #%d", i)))
Expect(string(dc.Message().GetData())).To(Equal(fmt.Sprintf("Message_id:%d_label:%s", i, "apple")))
Expect(dc.Accept(context.Background())).To(BeNil())
}
@ -262,7 +263,9 @@ var _ = Describe("Consumer stream test", func() {
ReceiverLinkName: "consumer apple and banana should filter messages based on x-stream-filter",
InitialCredits: 200,
Offset: &OffsetFirst{},
Filters: []string{"apple", "banana"},
StreamFilterOptions: &StreamFilterOptions{
Values: []string{"apple", "banana"},
},
})
Expect(err).To(BeNil())
@ -273,19 +276,23 @@ var _ = Describe("Consumer stream test", func() {
Expect(err).To(BeNil())
Expect(dc.Message()).NotTo(BeNil())
if i < 10 {
Expect(fmt.Sprintf("%s", dc.Message().GetData())).To(Equal(fmt.Sprintf("banana #%d", i)))
Expect(string(dc.Message().GetData())).To(Equal(fmt.Sprintf("Message_id:%d_label:%s", i, "banana")))
} else {
Expect(fmt.Sprintf("%s", dc.Message().GetData())).To(Equal(fmt.Sprintf("apple #%d", i-10)))
Expect(string(dc.Message().GetData())).To(Equal(fmt.Sprintf("Message_id:%d_label:%s", i-10, "apple")))
}
Expect(dc.Accept(context.Background())).To(BeNil())
}
consumerAppleMatchUnfiltered, err := connection.NewConsumer(context.Background(), qName, &StreamConsumerOptions{
ReceiverLinkName: "consumer apple should filter messages based on x-stream-filter and FilterMatchUnfiltered true",
InitialCredits: 200,
Offset: &OffsetFirst{},
Filters: []string{"apple"},
FilterMatchUnfiltered: true,
ReceiverLinkName: "consumer apple should filter messages based on x-stream-filter and MatchUnfiltered true",
InitialCredits: 200,
Offset: &OffsetFirst{},
StreamFilterOptions: &StreamFilterOptions{
Values: []string{"apple"},
MatchUnfiltered: true,
},
})
Expect(err).To(BeNil())
@ -296,10 +303,9 @@ var _ = Describe("Consumer stream test", func() {
Expect(err).To(BeNil())
Expect(dc.Message()).NotTo(BeNil())
if i < 10 {
Expect(fmt.Sprintf("%s", dc.Message().GetData())).To(Equal(fmt.Sprintf("apple #%d", i)))
Expect(string(dc.Message().GetData())).To(Equal(fmt.Sprintf("Message_id:%d_label:%s", i, "apple")))
} else {
Expect(fmt.Sprintf("%s", dc.Message().GetData())).To(Equal(fmt.Sprintf(" #%d", i-10)))
Expect(string(dc.Message().GetData())).To(Equal(fmt.Sprintf("Message_id:%d_label:%s", i-10, "")))
}
Expect(dc.Accept(context.Background())).To(BeNil())
}
@ -312,4 +318,263 @@ var _ = Describe("Consumer stream test", func() {
Expect(connection.Close(context.Background())).To(BeNil())
})
Describe("consumer should filter messages based on application properties", func() {
qName := generateName("consumer should filter messages based on application properties")
connection, err := Dial(context.Background(), "amqp://", nil)
Expect(err).To(BeNil())
queueInfo, err := connection.Management().DeclareQueue(context.Background(), &StreamQueueSpecification{
Name: qName,
})
Expect(err).To(BeNil())
Expect(queueInfo).NotTo(BeNil())
publishMessagesWithMessageLogic(qName, "ignoredKey", 7, func(msg *amqp.Message) {
msg.ApplicationProperties = map[string]interface{}{"ignoredKey": "ignoredValue"}
})
publishMessagesWithMessageLogic(qName, "key1", 10, func(msg *amqp.Message) {
msg.ApplicationProperties = map[string]interface{}{"key1": "value1", "constFilterKey": "constFilterValue"}
})
publishMessagesWithMessageLogic(qName, "key2", 10, func(msg *amqp.Message) {
msg.ApplicationProperties = map[string]interface{}{"key2": "value2", "constFilterKey": "constFilterValue"}
})
publishMessagesWithMessageLogic(qName, "key3", 10, func(msg *amqp.Message) {
msg.ApplicationProperties = map[string]interface{}{"key3": "value3", "constFilterKey": "constFilterValue"}
})
var wg sync.WaitGroup
wg.Add(3)
DescribeTable("consumer should filter messages based on application properties", func(key string, value any, label string) {
consumer, err := connection.NewConsumer(context.Background(), qName, &StreamConsumerOptions{
InitialCredits: 200,
Offset: &OffsetFirst{},
StreamFilterOptions: &StreamFilterOptions{
ApplicationProperties: map[string]any{
key: value,
// this is a constant filter append during the publishMessagesWithApplicationProperties
// to test the multiple filters
"constFilterKey": "constFilterValue",
},
},
})
Expect(err).To(BeNil())
Expect(consumer).NotTo(BeNil())
Expect(consumer).To(BeAssignableToTypeOf(&Consumer{}))
for i := 0; i < 10; i++ {
dc, err := consumer.Receive(context.Background())
Expect(err).To(BeNil())
Expect(dc.Message()).NotTo(BeNil())
Expect(string(dc.Message().GetData())).To(Equal(fmt.Sprintf("Message_id:%d_label:%s", i, label)))
Expect(dc.message.ApplicationProperties).To(HaveKeyWithValue(key, value))
Expect(dc.Accept(context.Background())).To(BeNil())
}
Expect(consumer.Close(context.Background())).To(BeNil())
wg.Done()
},
Entry("key1 value1", "key1", "value1", "key1"),
Entry("key2 value2", "key2", "value2", "key2"),
Entry("key3 value3", "key3", "value3", "key3"),
)
go func() {
wg.Wait()
Expect(connection.Management().DeleteQueue(context.Background(), qName)).To(BeNil())
Expect(connection.Close(context.Background())).To(BeNil())
}()
})
Describe("consumer should filter messages based on properties", func() {
/*
Test the consumer should filter messages based on properties
*/
qName := generateName("consumer should filter messages based on properties")
qName += time.Now().String()
connection, err := Dial(context.Background(), "amqp://", nil)
Expect(err).To(BeNil())
queueInfo, err := connection.Management().DeclareQueue(context.Background(), &StreamQueueSpecification{
Name: qName,
})
Expect(err).To(BeNil())
Expect(queueInfo).NotTo(BeNil())
publishMessagesWithMessageLogic(qName, "MessageID", 10, func(msg *amqp.Message) {
msg.Properties = &amqp.MessageProperties{MessageID: "MessageID"}
})
publishMessagesWithMessageLogic(qName, "Subject", 10, func(msg *amqp.Message) {
msg.Properties = &amqp.MessageProperties{Subject: stringPtr("Subject")}
})
publishMessagesWithMessageLogic(qName, "ReplyTo", 10, func(msg *amqp.Message) {
msg.Properties = &amqp.MessageProperties{ReplyTo: stringPtr("ReplyTo")}
})
publishMessagesWithMessageLogic(qName, "ContentType", 10, func(msg *amqp.Message) {
msg.Properties = &amqp.MessageProperties{ContentType: stringPtr("ContentType")}
})
publishMessagesWithMessageLogic(qName, "ContentEncoding", 10, func(msg *amqp.Message) {
msg.Properties = &amqp.MessageProperties{ContentEncoding: stringPtr("ContentEncoding")}
})
publishMessagesWithMessageLogic(qName, "GroupID", 10, func(msg *amqp.Message) {
msg.Properties = &amqp.MessageProperties{GroupID: stringPtr("GroupID")}
})
publishMessagesWithMessageLogic(qName, "ReplyToGroupID", 10, func(msg *amqp.Message) {
msg.Properties = &amqp.MessageProperties{ReplyToGroupID: stringPtr("ReplyToGroupID")}
})
// GroupSequence
publishMessagesWithMessageLogic(qName, "GroupSequence", 10, func(msg *amqp.Message) {
msg.Properties = &amqp.MessageProperties{GroupSequence: uint32Ptr(137)}
})
// ReplyToGroupID
publishMessagesWithMessageLogic(qName, "ReplyToGroupID", 10, func(msg *amqp.Message) {
msg.Properties = &amqp.MessageProperties{ReplyToGroupID: stringPtr("ReplyToGroupID")}
})
// CreationTime
publishMessagesWithMessageLogic(qName, "CreationTime", 10, func(msg *amqp.Message) {
msg.Properties = &amqp.MessageProperties{CreationTime: timePtr(createDateTime())}
})
// AbsoluteExpiryTime
publishMessagesWithMessageLogic(qName, "AbsoluteExpiryTime", 10, func(msg *amqp.Message) {
msg.Properties = &amqp.MessageProperties{AbsoluteExpiryTime: timePtr(createDateTime())}
})
// CorrelationID
publishMessagesWithMessageLogic(qName, "CorrelationID", 10, func(msg *amqp.Message) {
msg.Properties = &amqp.MessageProperties{CorrelationID: "CorrelationID"}
})
var wg sync.WaitGroup
wg.Add(12)
DescribeTable("consumer should filter messages based on properties", func(properties *amqp.MessageProperties, label string) {
consumer, err := connection.NewConsumer(context.Background(), qName, &StreamConsumerOptions{
InitialCredits: 200,
Offset: &OffsetFirst{},
StreamFilterOptions: &StreamFilterOptions{
Properties: properties,
},
})
Expect(err).To(BeNil())
Expect(consumer).NotTo(BeNil())
Expect(consumer).To(BeAssignableToTypeOf(&Consumer{}))
for i := 0; i < 10; i++ {
dc, err := consumer.Receive(context.Background())
Expect(err).To(BeNil())
Expect(dc.Message()).NotTo(BeNil())
Expect(string(dc.Message().GetData())).To(Equal(fmt.Sprintf("Message_id:%d_label:%s", i, label)))
// we test one by one because of the date time fields
// It is not possible to compare the whole structure due of the time
// It is not perfect but it is enough for the test
if dc.message.Properties.MessageID != nil {
Expect(dc.message.Properties.MessageID).To(Equal(properties.MessageID))
}
if dc.message.Properties.Subject != nil {
Expect(dc.message.Properties.Subject).To(Equal(properties.Subject))
}
if dc.message.Properties.ReplyTo != nil {
Expect(dc.message.Properties.ReplyTo).To(Equal(properties.ReplyTo))
}
if dc.message.Properties.ContentType != nil {
Expect(dc.message.Properties.ContentType).To(Equal(properties.ContentType))
}
if dc.message.Properties.ContentEncoding != nil {
Expect(dc.message.Properties.ContentEncoding).To(Equal(properties.ContentEncoding))
}
if dc.message.Properties.GroupID != nil {
Expect(dc.message.Properties.GroupID).To(Equal(properties.GroupID))
}
if dc.message.Properties.ReplyToGroupID != nil {
Expect(dc.message.Properties.ReplyToGroupID).To(Equal(properties.ReplyToGroupID))
}
if dc.message.Properties.GroupSequence != nil {
Expect(dc.message.Properties.GroupSequence).To(Equal(properties.GroupSequence))
}
if dc.message.Properties.ReplyToGroupID != nil {
Expect(dc.message.Properties.ReplyToGroupID).To(Equal(properties.ReplyToGroupID))
}
// here we compare only the year, month and day
// it is not perfect but it is enough for the test
if dc.message.Properties.CreationTime != nil {
Expect(dc.message.Properties.CreationTime.Year()).To(Equal(properties.CreationTime.Year()))
Expect(dc.message.Properties.CreationTime.Month()).To(Equal(properties.CreationTime.Month()))
Expect(dc.message.Properties.CreationTime.Day()).To(Equal(properties.CreationTime.Day()))
}
if dc.message.Properties.AbsoluteExpiryTime != nil {
Expect(dc.message.Properties.AbsoluteExpiryTime.Year()).To(Equal(properties.AbsoluteExpiryTime.Year()))
Expect(dc.message.Properties.AbsoluteExpiryTime.Month()).To(Equal(properties.AbsoluteExpiryTime.Month()))
Expect(dc.message.Properties.AbsoluteExpiryTime.Day()).To(Equal(properties.AbsoluteExpiryTime.Day()))
}
if dc.message.Properties.CorrelationID != nil {
Expect(dc.message.Properties.CorrelationID).To(Equal(properties.CorrelationID))
}
Expect(dc.Accept(context.Background())).To(BeNil())
}
Expect(consumer.Close(context.Background())).To(BeNil())
wg.Done()
},
Entry("MessageID", &amqp.MessageProperties{MessageID: "MessageID"}, "MessageID"),
Entry("Subject", &amqp.MessageProperties{Subject: stringPtr("Subject")}, "Subject"),
Entry("ReplyTo", &amqp.MessageProperties{ReplyTo: stringPtr("ReplyTo")}, "ReplyTo"),
Entry("ContentType", &amqp.MessageProperties{ContentType: stringPtr("ContentType")}, "ContentType"),
Entry("ContentEncoding", &amqp.MessageProperties{ContentEncoding: stringPtr("ContentEncoding")}, "ContentEncoding"),
Entry("GroupID", &amqp.MessageProperties{GroupID: stringPtr("GroupID")}, "GroupID"),
Entry("ReplyToGroupID", &amqp.MessageProperties{ReplyToGroupID: stringPtr("ReplyToGroupID")}, "ReplyToGroupID"),
Entry("GroupSequence", &amqp.MessageProperties{GroupSequence: uint32Ptr(137)}, "GroupSequence"),
Entry("ReplyToGroupID", &amqp.MessageProperties{ReplyToGroupID: stringPtr("ReplyToGroupID")}, "ReplyToGroupID"),
Entry("CreationTime", &amqp.MessageProperties{CreationTime: timePtr(createDateTime())}, "CreationTime"),
Entry("AbsoluteExpiryTime", &amqp.MessageProperties{AbsoluteExpiryTime: timePtr(createDateTime())}, "AbsoluteExpiryTime"),
Entry("CorrelationID", &amqp.MessageProperties{CorrelationID: "CorrelationID"}, "CorrelationID"),
)
go func() {
wg.Wait()
Expect(connection.Management().DeleteQueue(context.Background(), qName)).To(BeNil())
Expect(connection.Close(context.Background())).To(BeNil())
}()
})
})
type msgLogic = func(*amqp.Message)
func publishMessagesWithMessageLogic(queue string, label string, count int, logic msgLogic) {
conn, err := Dial(context.TODO(), "amqp://guest:guest@localhost", nil)
Expect(err).To(BeNil())
publisher, err := conn.NewPublisher(context.TODO(), &QueueAddress{Queue: queue},
nil)
Expect(err).To(BeNil())
Expect(publisher).NotTo(BeNil())
for i := 0; i < count; i++ {
body := fmt.Sprintf("Message_id:%d_label:%s", i, label)
msg := NewMessage([]byte(body))
logic(msg)
publishResult, err := publisher.Publish(context.TODO(), msg)
Expect(err).To(BeNil())
Expect(publishResult).NotTo(BeNil())
Expect(publishResult.Outcome).To(Equal(&amqp.StateAccepted{}))
}
err = conn.Close(context.TODO())
Expect(err).To(BeNil())
}

View File

@ -13,7 +13,7 @@ var _ = Describe("NewConsumer tests", func() {
It("AMQP NewConsumer should fail due to context cancellation", func() {
qName := generateNameWithDateTime("AMQP NewConsumer should fail due to context cancellation")
connection, err := Dial(context.Background(), []string{"amqp://"}, nil)
connection, err := Dial(context.Background(), "amqp://", nil)
Expect(err).To(BeNil())
queue, err := connection.Management().DeclareQueue(context.Background(), &QuorumQueueSpecification{
@ -33,7 +33,7 @@ var _ = Describe("NewConsumer tests", func() {
It("AMQP NewConsumer should ack and empty the queue", func() {
qName := generateNameWithDateTime("AMQP NewConsumer should ack and empty the queue")
connection, err := Dial(context.Background(), []string{"amqp://"}, nil)
connection, err := Dial(context.Background(), "amqp://", nil)
Expect(err).To(BeNil())
queue, err := connection.Management().DeclareQueue(context.Background(), &QuorumQueueSpecification{
Name: qName,
@ -62,7 +62,7 @@ var _ = Describe("NewConsumer tests", func() {
It("AMQP NewConsumer should requeue the message to the queue", func() {
qName := generateNameWithDateTime("AMQP NewConsumer should requeue the message to the queue")
connection, err := Dial(context.Background(), []string{"amqp://"}, nil)
connection, err := Dial(context.Background(), "amqp://", nil)
Expect(err).To(BeNil())
queue, err := connection.Management().DeclareQueue(context.Background(), &QuorumQueueSpecification{
Name: qName,
@ -81,6 +81,7 @@ var _ = Describe("NewConsumer tests", func() {
Expect(consumer.Close(context.Background())).To(BeNil())
Expect(err).To(BeNil())
nMessages, err := connection.Management().PurgeQueue(context.Background(), qName)
Expect(err).To(BeNil())
Expect(nMessages).To(Equal(1))
Expect(connection.Management().DeleteQueue(context.Background(), qName)).To(BeNil())
Expect(connection.Close(context.Background())).To(BeNil())
@ -89,7 +90,7 @@ var _ = Describe("NewConsumer tests", func() {
It("AMQP NewConsumer should requeue the message to the queue with annotations", func() {
qName := generateNameWithDateTime("AMQP NewConsumer should requeue the message to the queue with annotations")
connection, err := Dial(context.Background(), []string{"amqp://"}, nil)
connection, err := Dial(context.Background(), "amqp://", nil)
Expect(err).To(BeNil())
queue, err := connection.Management().DeclareQueue(context.Background(), &QuorumQueueSpecification{
Name: qName,
@ -116,6 +117,7 @@ var _ = Describe("NewConsumer tests", func() {
Expect(consumer.Close(context.Background())).To(BeNil())
Expect(err).To(BeNil())
nMessages, err := connection.Management().PurgeQueue(context.Background(), qName)
Expect(err).To(BeNil())
Expect(nMessages).To(Equal(1))
Expect(connection.Management().DeleteQueue(context.Background(), qName)).To(BeNil())
Expect(connection.Close(context.Background())).To(BeNil())
@ -124,7 +126,7 @@ var _ = Describe("NewConsumer tests", func() {
It("AMQP NewConsumer should discard the message to the queue with and without annotations", func() {
// TODO: Implement this test with a dead letter queue to test the discard feature
qName := generateNameWithDateTime("AMQP NewConsumer should discard the message to the queue with and without annotations")
connection, err := Dial(context.Background(), []string{"amqp://"}, nil)
connection, err := Dial(context.Background(), "amqp://", nil)
Expect(err).To(BeNil())
queue, err := connection.Management().DeclareQueue(context.Background(), &QuorumQueueSpecification{
Name: qName,
@ -153,6 +155,7 @@ var _ = Describe("NewConsumer tests", func() {
Info: nil,
})).To(BeNil())
nMessages, err := connection.Management().PurgeQueue(context.Background(), qName)
Expect(err).To(BeNil())
Expect(nMessages).To(Equal(0))
Expect(consumer.Close(context.Background())).To(BeNil())
Expect(connection.Management().DeleteQueue(context.Background(), qName)).To(BeNil())

View File

@ -3,47 +3,103 @@ package rabbitmqamqp
import (
"context"
"fmt"
"github.com/Azure/go-amqp"
"sync"
"sync/atomic"
)
type Environment struct {
connections sync.Map
addresses []string
connOptions *AmqpConnOptions
type TEndPointStrategy int
const (
StrategyRandom TEndPointStrategy = iota
StrategySequential TEndPointStrategy = iota
)
type Endpoint struct {
Address string
Options *AmqpConnOptions
}
func NewEnvironment(addresses []string, connOptions *AmqpConnOptions) *Environment {
func DefaultEndpoints() []Endpoint {
ep := Endpoint{
Address: "amqp://",
Options: &AmqpConnOptions{
SASLType: amqp.SASLTypeAnonymous(),
},
}
return []Endpoint{ep}
}
type Environment struct {
connections sync.Map
endPoints []Endpoint
EndPointStrategy TEndPointStrategy
nextConnectionId int32
}
func NewEnvironment(address string, options *AmqpConnOptions) *Environment {
return NewClusterEnvironmentWithStrategy([]Endpoint{{Address: address, Options: options}}, StrategyRandom)
}
func NewClusterEnvironment(endPoints []Endpoint) *Environment {
return NewClusterEnvironmentWithStrategy(endPoints, StrategyRandom)
}
func NewClusterEnvironmentWithStrategy(endPoints []Endpoint, strategy TEndPointStrategy) *Environment {
return &Environment{
connections: sync.Map{},
addresses: addresses,
connOptions: connOptions,
connections: sync.Map{},
endPoints: endPoints,
EndPointStrategy: strategy,
nextConnectionId: 0,
}
}
// NewConnection get a new connection from the environment.
// If the connection id is provided, it will be used as the connection id.
// If the connection id is not provided, a new connection id will be generated.
// The connection id is unique in the environment.
// It picks an endpoint from the list of endpoints, based on EndPointStrategy, and tries to open a connection.
// It fails if all the endpoints are not reachable.
// The Environment will keep track of the connection and close it when the environment is closed.
func (e *Environment) NewConnection(ctx context.Context, args ...string) (*AmqpConnection, error) {
if len(args) > 0 && len(args[0]) > 0 {
// check if connection already exists
if _, ok := e.connections.Load(args[0]); ok {
return nil, fmt.Errorf("connection with id %s already exists", args[0])
}
}
func (e *Environment) NewConnection(ctx context.Context) (*AmqpConnection, error) {
connection, err := Dial(ctx, e.addresses, e.connOptions, args...)
if err != nil {
return nil, err
tmp := make([]Endpoint, len(e.endPoints))
copy(tmp, e.endPoints)
lastError := error(nil)
for len(tmp) > 0 {
idx := 0
switch e.EndPointStrategy {
case StrategyRandom:
idx = random(len(tmp))
case StrategySequential:
idx = 0
}
addr := tmp[idx]
// remove the index from the tmp list
tmp = append(tmp[:idx], tmp[idx+1:]...)
var cloned *AmqpConnOptions
if addr.Options != nil {
cloned = addr.Options.Clone()
}
connection, err := Dial(ctx, addr.Address, cloned)
if err != nil {
Error("Failed to open connection", ExtractWithoutPassword(addr.Address), err)
lastError = err
continue
}
// here we use it to make each connection unique
atomic.AddInt32(&e.nextConnectionId, 1)
connection.amqpConnOptions.Id = fmt.Sprintf("%s_%d", connection.amqpConnOptions.Id, e.nextConnectionId)
e.connections.Store(connection.Id(), connection)
connection.refMap = &e.connections
return connection, nil
}
e.connections.Store(connection.Id(), connection)
connection.refMap = &e.connections
return connection, nil
return nil, fmt.Errorf("fail to open connection. Last error: %w", lastError)
}
// Connections gets the active connections in the environment
func (e *Environment) Connections() []*AmqpConnection {
connections := make([]*AmqpConnection, 0)
e.connections.Range(func(key, value interface{}) bool {

View File

@ -2,13 +2,14 @@ package rabbitmqamqp
import (
"context"
"github.com/Azure/go-amqp"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
var _ = Describe("AMQP Environment Test", func() {
It("AMQP Environment connection should succeed", func() {
env := NewEnvironment([]string{"amqp://"}, nil)
env := NewClusterEnvironment([]Endpoint{{Address: "amqp://"}})
Expect(env).NotTo(BeNil())
Expect(env.Connections()).NotTo(BeNil())
Expect(len(env.Connections())).To(Equal(0))
@ -22,7 +23,7 @@ var _ = Describe("AMQP Environment Test", func() {
})
It("AMQP Environment CloseConnections should remove all the elements form the list", func() {
env := NewEnvironment([]string{"amqp://"}, nil)
env := NewClusterEnvironment([]Endpoint{{Address: "amqp://"}})
Expect(env).NotTo(BeNil())
Expect(env.Connections()).NotTo(BeNil())
Expect(len(env.Connections())).To(Equal(0))
@ -36,22 +37,83 @@ var _ = Describe("AMQP Environment Test", func() {
Expect(len(env.Connections())).To(Equal(0))
})
It("AMQP Environment connection ID should be unique", func() {
env := NewEnvironment([]string{"amqp://"}, nil)
It("Get new connection should connect to the one correct uri and fails the others", func() {
env := NewClusterEnvironment([]Endpoint{{Address: "amqp://localhost:1234"}, {Address: "amqp://nohost:555"}, {Address: "amqp://"}})
conn, err := env.NewConnection(context.Background())
Expect(err).To(BeNil())
Expect(conn.Close(context.Background()))
})
It("Get new connection should fail due of wrong Port", func() {
env := NewClusterEnvironment([]Endpoint{{Address: "amqp://localhost:1234"}})
_, err := env.NewConnection(context.Background())
Expect(err).NotTo(BeNil())
})
It("AMQP connection should fail due of wrong Host", func() {
env := NewClusterEnvironment([]Endpoint{{Address: "amqp://wrong_host:5672"}})
_, err := env.NewConnection(context.Background())
Expect(err).NotTo(BeNil())
})
It("AMQP connection should fails with all the wrong uris", func() {
env := NewClusterEnvironment([]Endpoint{{Address: "amqp://localhost:1234"}, {Address: "amqp://nohost:555"}, {Address: "amqp://nono"}})
_, err := env.NewConnection(context.Background())
Expect(err).NotTo(BeNil())
})
It("AMQP connection should success in different vhosts", func() {
// user_1 and vhost_user_1 are preloaded in the rabbitmq server during the startup
env := NewEnvironment("amqp://user_1:user_1@localhost:5672/vhost_user_1", nil)
Expect(env).NotTo(BeNil())
Expect(env.Connections()).NotTo(BeNil())
Expect(len(env.Connections())).To(Equal(0))
connection, err := env.NewConnection(context.Background(), "myConnectionId")
conn, err := env.NewConnection(context.Background())
Expect(err).To(BeNil())
Expect(connection).NotTo(BeNil())
Expect(len(env.Connections())).To(Equal(1))
connectionShouldBeNil, err := env.NewConnection(context.Background(), "myConnectionId")
Expect(err).NotTo(BeNil())
Expect(err.Error()).To(ContainSubstring("connection with id myConnectionId already exists"))
Expect(connectionShouldBeNil).To(BeNil())
Expect(len(env.Connections())).To(Equal(1))
Expect(connection.Close(context.Background())).To(BeNil())
Expect(len(env.Connections())).To(Equal(0))
Expect(conn.Close(context.Background()))
})
It("AMQP connection should fail with user_1 does not have the grant for /", func() {
// user_1 is preloaded in the rabbitmq server during the startup
env := NewEnvironment("amqp://user_1:user_1@localhost:5672/", nil)
Expect(env).NotTo(BeNil())
_, err := env.NewConnection(context.Background())
Expect(err).NotTo(BeNil())
})
Describe("Environment strategy", func() {
DescribeTable("Environment with strategy should success", func(strategy TEndPointStrategy) {
env := NewClusterEnvironmentWithStrategy([]Endpoint{{Address: "amqp://", Options: &AmqpConnOptions{Id: "my"}}, {Address: "amqp://nohost:555"}, {Address: "amqp://nono"}}, StrategyRandom)
Expect(env).NotTo(BeNil())
Expect(env.Connections()).NotTo(BeNil())
Expect(len(env.Connections())).To(Equal(0))
conn, err := env.NewConnection(context.Background())
Expect(err).To(BeNil())
Expect(conn.Id()).To(Equal("my_1"))
Expect(conn.Close(context.Background()))
},
Entry("StrategyRandom", StrategyRandom),
Entry("StrategySequential", StrategySequential),
)
})
Describe("Environment should success even partial options", func() {
DescribeTable("Environment should success even partial options", func(options *AmqpConnOptions) {
env := NewClusterEnvironment([]Endpoint{{Address: "amqp://", Options: options}})
Expect(env).NotTo(BeNil())
Expect(env.Connections()).NotTo(BeNil())
Expect(len(env.Connections())).To(Equal(0))
conn, err := env.NewConnection(context.Background())
Expect(err).To(BeNil())
Expect(conn.Close(context.Background()))
},
Entry("Partial options", &AmqpConnOptions{Id: "my"}),
Entry("Partial options", &AmqpConnOptions{SASLType: amqp.SASLTypeAnonymous()}),
Entry("Partial options", &AmqpConnOptions{ContainerID: "cid_my"}),
)
})
})

View File

@ -11,7 +11,7 @@ var _ = Describe("AMQP Exchange test ", func() {
var connection *AmqpConnection
var management *AmqpManagement
BeforeEach(func() {
conn, err := Dial(context.TODO(), []string{"amqp://"}, nil)
conn, err := Dial(context.TODO(), "amqp://", nil)
connection = conn
Expect(err).To(BeNil())
management = connection.Management()

View File

@ -4,10 +4,11 @@ import (
"context"
"errors"
"fmt"
"github.com/Azure/go-amqp"
"github.com/google/uuid"
"strconv"
"time"
"github.com/Azure/go-amqp"
"github.com/google/uuid"
)
var ErrPreconditionFailed = errors.New("precondition Failed")
@ -22,10 +23,9 @@ type AmqpManagement struct {
sender *amqp.Sender
receiver *amqp.Receiver
lifeCycle *LifeCycle
cancel context.CancelFunc
}
func NewAmqpManagement() *AmqpManagement {
func newAmqpManagement() *AmqpManagement {
return &AmqpManagement{
lifeCycle: NewLifeCycle(),
}
@ -173,9 +173,9 @@ func (a *AmqpManagement) request(ctx context.Context, id string, body any, path
return make(map[string]any), nil
}
func (a *AmqpManagement) DeclareQueue(ctx context.Context, specification QueueSpecification) (*AmqpQueueInfo, error) {
func (a *AmqpManagement) DeclareQueue(ctx context.Context, specification IQueueSpecification) (*AmqpQueueInfo, error) {
if specification == nil {
return nil, fmt.Errorf("queue specification cannot be nil. You need to provide a valid QueueSpecification")
return nil, fmt.Errorf("queue specification cannot be nil. You need to provide a valid IQueueSpecification")
}
amqpQueue := newAmqpQueue(a, specification.name())
@ -192,9 +192,9 @@ func (a *AmqpManagement) DeleteQueue(ctx context.Context, name string) error {
return q.Delete(ctx)
}
func (a *AmqpManagement) DeclareExchange(ctx context.Context, exchangeSpecification ExchangeSpecification) (*AmqpExchangeInfo, error) {
func (a *AmqpManagement) DeclareExchange(ctx context.Context, exchangeSpecification IExchangeSpecification) (*AmqpExchangeInfo, error) {
if exchangeSpecification == nil {
return nil, errors.New("exchange specification cannot be nil. You need to provide a valid ExchangeSpecification")
return nil, errors.New("exchange specification cannot be nil. You need to provide a valid IExchangeSpecification")
}
exchange := newAmqpExchange(a, exchangeSpecification.name())
@ -208,9 +208,9 @@ func (a *AmqpManagement) DeleteExchange(ctx context.Context, name string) error
return e.Delete(ctx)
}
func (a *AmqpManagement) Bind(ctx context.Context, bindingSpecification BindingSpecification) (string, error) {
func (a *AmqpManagement) Bind(ctx context.Context, bindingSpecification IBindingSpecification) (string, error) {
if bindingSpecification == nil {
return "", fmt.Errorf("binding specification cannot be nil. You need to provide a valid BindingSpecification")
return "", fmt.Errorf("binding specification cannot be nil. You need to provide a valid IBindingSpecification")
}
bind := newAMQPBinding(a)
@ -220,9 +220,9 @@ func (a *AmqpManagement) Bind(ctx context.Context, bindingSpecification BindingS
return bind.Bind(ctx)
}
func (a *AmqpManagement) Unbind(ctx context.Context, bindingPath string) error {
func (a *AmqpManagement) Unbind(ctx context.Context, path string) error {
bind := newAMQPBinding(a)
return bind.Unbind(ctx, bindingPath)
return bind.Unbind(ctx, path)
}
func (a *AmqpManagement) QueueInfo(ctx context.Context, queueName string) (*AmqpQueueInfo, error) {
path, err := queueAddress(&queueName)
@ -236,15 +236,22 @@ func (a *AmqpManagement) QueueInfo(ctx context.Context, queueName string) (*Amqp
return newAmqpQueueInfo(result), nil
}
func (a *AmqpManagement) PurgeQueue(ctx context.Context, queueName string) (int, error) {
purge := newAmqpQueue(a, queueName)
// PurgeQueue purges the queue
// returns the number of messages purged
func (a *AmqpManagement) PurgeQueue(ctx context.Context, name string) (int, error) {
purge := newAmqpQueue(a, name)
return purge.Purge(ctx)
}
func (a *AmqpManagement) refreshToken(ctx context.Context, token string) error {
_, err := a.Request(ctx, []byte(token), authTokens, commandPut, []int{responseCode204})
return err
}
func (a *AmqpManagement) NotifyStatusChange(channel chan *StateChanged) {
a.lifeCycle.chStatusChanged = channel
}
func (a *AmqpManagement) State() LifeCycleState {
func (a *AmqpManagement) State() ILifeCycleState {
return a.lifeCycle.State()
}

View File

@ -11,7 +11,7 @@ import (
var _ = Describe("Management tests", func() {
It("AMQP Management should fail due to context cancellation", func() {
connection, err := Dial(context.Background(), []string{"amqp://"}, nil)
connection, err := Dial(context.Background(), "amqp://", nil)
Expect(err).To(BeNil())
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Millisecond)
@ -23,7 +23,7 @@ var _ = Describe("Management tests", func() {
It("AMQP Management should receive events", func() {
ch := make(chan *StateChanged, 2)
connection, err := Dial(context.Background(), []string{"amqp://"}, &AmqpConnOptions{
connection, err := Dial(context.Background(), "amqp://", &AmqpConnOptions{
SASLType: amqp.SASLTypeAnonymous(),
RecoveryConfiguration: &RecoveryConfiguration{
ActiveRecovery: false,
@ -43,27 +43,31 @@ var _ = Describe("Management tests", func() {
It("Request", func() {
connection, err := Dial(context.Background(), []string{"amqp://"}, nil)
connection, err := Dial(context.Background(), "amqp://", nil)
Expect(err).To(BeNil())
management := connection.Management()
kv := make(map[string]any)
kv["durable"] = true
kv["auto_delete"] = false
kv["auto_delete"] = true
_queueArguments := make(map[string]any)
_queueArguments["x-queue-type"] = "quorum"
_queueArguments["x-queue-type"] = "classic"
kv["arguments"] = _queueArguments
path := "/queues/test"
result, err := management.Request(context.Background(), kv, path, "PUT", []int{200})
Expect(err).To(BeNil())
Expect(result).NotTo(BeNil())
result, err = management.Request(context.Background(), amqp.Null{}, path, "DELETE", []int{responseCode200})
Expect(err).To(BeNil())
Expect(result).NotTo(BeNil())
Expect(management.Close(context.Background())).To(BeNil())
Expect(connection.Close(context.Background())).To(BeNil())
})
It("GET on non-existing queue returns ErrDoesNotExist", func() {
connection, err := Dial(context.Background(), []string{"amqp://"}, nil)
connection, err := Dial(context.Background(), "amqp://", nil)
Expect(err).To(BeNil())
management := connection.Management()

View File

@ -26,13 +26,13 @@ func (m *Publisher) Id() string {
return m.id
}
func newPublisher(ctx context.Context, connection *AmqpConnection, destinationAdd string, linkName string, args ...string) (*Publisher, error) {
func newPublisher(ctx context.Context, connection *AmqpConnection, destinationAdd string, options IPublisherOptions) (*Publisher, error) {
id := fmt.Sprintf("publisher-%s", uuid.New().String())
if len(args) > 0 {
id = args[0]
if options != nil && options.id() != "" {
id = options.id()
}
r := &Publisher{connection: connection, linkName: linkName, destinationAdd: destinationAdd, id: id}
r := &Publisher{connection: connection, linkName: getLinkName(options), destinationAdd: destinationAdd, id: id}
connection.entitiesTracker.storeOrReplaceProducer(r)
err := r.createSender(ctx)
if err != nil {
@ -62,7 +62,7 @@ RabbitMQ supports the following DeliveryState types:
- StateRejected
See: https://www.rabbitmq.com/docs/next/amqp#outcomes for more information.
Note: If the destination address is not defined during the creation, the message must have a TO property set.
If the destination address is not defined during the creation, the message must have a TO property set.
You can use the helper "MessagePropertyToAddress" to create the destination address.
See the examples:
Create a new publisher that sends messages to a specific destination address:
@ -84,6 +84,16 @@ Create a new publisher that sends messages based on message destination address:
..:= MessagePropertyToAddress(msg, &QueueAddress{Queue: "myQueueName"})
..:= publisher.Publish(context.Background(), msg)
</code>
The message is persistent by default by setting the Header.Durable to true when Header is nil.
You can set the message to be non-persistent by setting the Header.Durable to false.
Note:
When you use the `Header` is up to you to set the message properties,
You need set the `Header.Durable` to true or false.
<code>
</code>
*/
func (m *Publisher) Publish(ctx context.Context, message *amqp.Message) (*PublishResult, error) {
@ -97,6 +107,14 @@ func (m *Publisher) Publish(ctx context.Context, message *amqp.Message) (*Publis
return nil, err
}
}
// set the default persistence to the message
if message.Header == nil {
message.Header = &amqp.MessageHeader{
Durable: true,
}
}
r, err := m.sender.Load().SendWithReceipt(ctx, message, nil)
if err != nil {
return nil, err

View File

@ -10,7 +10,7 @@ import (
var _ = Describe("AMQP publisher ", func() {
It("Send a message to a queue with a Message Target NewPublisher", func() {
qName := generateNameWithDateTime("Send a message to a queue with a Message Target NewPublisher")
connection, err := Dial(context.Background(), []string{"amqp://"}, nil)
connection, err := Dial(context.Background(), "amqp://", nil)
Expect(err).To(BeNil())
Expect(connection).NotTo(BeNil())
queueInfo, err := connection.Management().DeclareQueue(context.Background(), &QuorumQueueSpecification{
@ -18,7 +18,7 @@ var _ = Describe("AMQP publisher ", func() {
})
Expect(err).To(BeNil())
Expect(queueInfo).NotTo(BeNil())
publisher, err := connection.NewPublisher(context.Background(), &QueueAddress{Queue: qName}, "test")
publisher, err := connection.NewPublisher(context.Background(), &QueueAddress{Queue: qName}, nil)
Expect(err).To(BeNil())
Expect(publisher).NotTo(BeNil())
Expect(publisher).To(BeAssignableToTypeOf(&Publisher{}))
@ -36,11 +36,11 @@ var _ = Describe("AMQP publisher ", func() {
})
It("NewPublisher should fail to a not existing exchange", func() {
connection, err := Dial(context.Background(), []string{"amqp://"}, nil)
connection, err := Dial(context.Background(), "amqp://", nil)
Expect(err).To(BeNil())
Expect(connection).NotTo(BeNil())
exchangeName := "Nope"
publisher, err := connection.NewPublisher(context.Background(), &ExchangeAddress{Exchange: exchangeName}, "test")
publisher, err := connection.NewPublisher(context.Background(), &ExchangeAddress{Exchange: exchangeName}, nil)
Expect(err).NotTo(BeNil())
Expect(publisher).To(BeNil())
Expect(connection.Close(context.Background())).To(BeNil())
@ -48,7 +48,7 @@ var _ = Describe("AMQP publisher ", func() {
It("publishResult should released to a not existing routing key", func() {
eName := generateNameWithDateTime("publishResult should released to a not existing routing key")
connection, err := Dial(context.Background(), []string{"amqp://"}, nil)
connection, err := Dial(context.Background(), "amqp://", nil)
Expect(err).To(BeNil())
Expect(connection).NotTo(BeNil())
exchange, err := connection.Management().DeclareExchange(context.Background(), &TopicExchangeSpecification{
@ -62,7 +62,7 @@ var _ = Describe("AMQP publisher ", func() {
publisher, err := connection.NewPublisher(context.Background(), &ExchangeAddress{
Exchange: eName,
Key: routingKeyNope,
}, "test")
}, nil)
Expect(err).To(BeNil())
Expect(publisher).NotTo(BeNil())
publishResult, err := publisher.Publish(context.Background(), NewMessage([]byte("hello")))
@ -75,14 +75,14 @@ var _ = Describe("AMQP publisher ", func() {
It("Send a message to a deleted queue should fail", func() {
qName := generateNameWithDateTime("Send a message to a deleted queue should fail")
connection, err := Dial(context.Background(), []string{"amqp://"}, nil)
connection, err := Dial(context.Background(), "amqp://", nil)
Expect(err).To(BeNil())
Expect(connection).NotTo(BeNil())
_, err = connection.Management().DeclareQueue(context.Background(), &QuorumQueueSpecification{
Name: qName,
})
Expect(err).To(BeNil())
publisher, err := connection.NewPublisher(context.Background(), &QueueAddress{Queue: qName}, "test")
publisher, err := connection.NewPublisher(context.Background(), &QueueAddress{Queue: qName}, nil)
Expect(err).To(BeNil())
Expect(publisher).NotTo(BeNil())
publishResult, err := publisher.Publish(context.Background(), NewMessage([]byte("hello")))
@ -91,15 +91,16 @@ var _ = Describe("AMQP publisher ", func() {
err = connection.management.DeleteQueue(context.Background(), qName)
Expect(err).To(BeNil())
publishResult, err = publisher.Publish(context.Background(), NewMessage([]byte("hello")))
Expect(publishResult).To(BeNil())
Expect(err).NotTo(BeNil())
Expect(connection.Close(context.Background()))
})
It("Multi Targets NewPublisher should fail with StateReleased when the destination does not exist", func() {
connection, err := Dial(context.Background(), []string{"amqp://"}, nil)
connection, err := Dial(context.Background(), "amqp://", nil)
Expect(err).To(BeNil())
Expect(connection).NotTo(BeNil())
publisher, err := connection.NewPublisher(context.Background(), nil, "test")
publisher, err := connection.NewPublisher(context.Background(), nil, nil)
Expect(err).To(BeNil())
Expect(publisher).NotTo(BeNil())
qName := generateNameWithDateTime("Targets NewPublisher should fail when the destination does not exist")
@ -120,11 +121,11 @@ var _ = Describe("AMQP publisher ", func() {
})
It("Multi Targets NewPublisher should success with StateReceived when the destination exists", func() {
connection, err := Dial(context.Background(), []string{"amqp://"}, nil)
connection, err := Dial(context.Background(), "amqp://", nil)
Expect(err).To(BeNil())
Expect(connection).NotTo(BeNil())
Expect(err).To(BeNil())
publisher, err := connection.NewPublisher(context.Background(), nil, "test")
publisher, err := connection.NewPublisher(context.Background(), nil, nil)
Expect(err).To(BeNil())
Expect(publisher).NotTo(BeNil())
name := generateNameWithDateTime("Targets NewPublisher should success with StateReceived when the destination exists")
@ -146,6 +147,8 @@ var _ = Describe("AMQP publisher ", func() {
Name: name,
IsAutoDelete: false,
})
Expect(err).To(BeNil())
msg = NewMessage([]byte("hello"))
Expect(MessagePropertyToAddress(msg, &ExchangeAddress{Exchange: name})).To(BeNil())
// the status should be StateReleased since the exchange does not have any binding
@ -174,10 +177,10 @@ var _ = Describe("AMQP publisher ", func() {
})
It("Multi Targets NewPublisher should fail it TO is not set or not valid", func() {
connection, err := Dial(context.Background(), []string{"amqp://"}, nil)
connection, err := Dial(context.Background(), "amqp://", nil)
Expect(err).To(BeNil())
Expect(connection).NotTo(BeNil())
publisher, err := connection.NewPublisher(context.Background(), nil, "test")
publisher, err := connection.NewPublisher(context.Background(), nil, nil)
Expect(err).To(BeNil())
Expect(publisher).NotTo(BeNil())
msg := NewMessage([]byte("hello"))
@ -200,4 +203,65 @@ var _ = Describe("AMQP publisher ", func() {
Expect(connection.Close(context.Background())).To(BeNil())
})
It("Message should durable by default", func() {
// https://github.com/rabbitmq/rabbitmq-server/pull/13918
// Here we test the default behavior of the message durability
// The lib should set the Header.Durable to true by default
// when the Header is set by the user
// it is up to the user to set the Header.Durable to true or false
connection, err := Dial(context.Background(), "amqp://", nil)
Expect(err).To(BeNil())
Expect(connection).NotTo(BeNil())
name := generateNameWithDateTime("Message should durable by default")
_, err = connection.Management().DeclareQueue(context.Background(), &QuorumQueueSpecification{
Name: name,
})
Expect(err).To(BeNil())
publisher, err := connection.NewPublisher(context.Background(), &QueueAddress{Queue: name}, nil)
Expect(err).To(BeNil())
Expect(publisher).NotTo(BeNil())
msg := NewMessage([]byte("hello"))
Expect(msg.Header).To(BeNil())
publishResult, err := publisher.Publish(context.Background(), msg)
Expect(err).To(BeNil())
Expect(publishResult).NotTo(BeNil())
Expect(publishResult.Outcome).To(Equal(&StateAccepted{}))
Expect(msg.Header).NotTo(BeNil())
Expect(msg.Header.Durable).To(BeTrue())
consumer, err := connection.NewConsumer(context.Background(), name, nil)
Expect(err).To(BeNil())
Expect(consumer).NotTo(BeNil())
dc, err := consumer.Receive(context.Background())
Expect(err).To(BeNil())
Expect(dc).NotTo(BeNil())
Expect(dc.Message().Header).NotTo(BeNil())
Expect(dc.Message().Header.Durable).To(BeTrue())
Expect(dc.Accept(context.Background())).To(BeNil())
msgNotPersistent := NewMessage([]byte("hello"))
msgNotPersistent.Header = &amqp.MessageHeader{
Durable: false,
}
publishResult, err = publisher.Publish(context.Background(), msgNotPersistent)
Expect(err).To(BeNil())
Expect(publishResult).NotTo(BeNil())
Expect(publishResult.Outcome).To(Equal(&StateAccepted{}))
Expect(msgNotPersistent.Header).NotTo(BeNil())
Expect(msgNotPersistent.Header.Durable).To(BeFalse())
dc, err = consumer.Receive(context.Background())
Expect(err).To(BeNil())
Expect(dc).NotTo(BeNil())
Expect(dc.Message().Header).NotTo(BeNil())
Expect(dc.Message().Header.Durable).To(BeFalse())
Expect(dc.Accept(context.Background())).To(BeNil())
Expect(publisher.Close(context.Background())).To(BeNil())
Expect(connection.Management().DeleteQueue(context.Background(), name)).To(BeNil())
Expect(connection.Close(context.Background())).To(BeNil())
})
})

View File

@ -8,14 +8,16 @@ import (
)
type AmqpQueueInfo struct {
name string
isDurable bool
isAutoDelete bool
isExclusive bool
leader string
members []string
arguments map[string]any
queueType TQueueType
name string
isDurable bool
isAutoDelete bool
isExclusive bool
leader string
members []string
arguments map[string]any
queueType TQueueType
consumerCount uint32
messageCount uint64
}
func (a *AmqpQueueInfo) Leader() string {
@ -27,15 +29,22 @@ func (a *AmqpQueueInfo) Members() []string {
}
func newAmqpQueueInfo(response map[string]any) *AmqpQueueInfo {
leader := ""
if response["leader"] != nil {
leader = response["leader"].(string)
}
return &AmqpQueueInfo{
name: response["name"].(string),
isDurable: response["durable"].(bool),
isAutoDelete: response["auto_delete"].(bool),
isExclusive: response["exclusive"].(bool),
queueType: TQueueType(response["type"].(string)),
leader: response["leader"].(string),
members: response["replicas"].([]string),
arguments: response["arguments"].(map[string]any),
name: response["name"].(string),
isDurable: response["durable"].(bool),
isAutoDelete: response["auto_delete"].(bool),
isExclusive: response["exclusive"].(bool),
queueType: TQueueType(response["type"].(string)),
leader: leader,
members: response["replicas"].([]string),
arguments: response["arguments"].(map[string]any),
consumerCount: response["consumer_count"].(uint32),
messageCount: response["message_count"].(uint64),
}
}
@ -63,6 +72,14 @@ func (a *AmqpQueueInfo) Arguments() map[string]any {
return a.arguments
}
func (a *AmqpQueueInfo) ConsumerCount() uint32 {
return a.consumerCount
}
func (a *AmqpQueueInfo) MessageCount() uint64 {
return a.messageCount
}
type AmqpQueue struct {
management *AmqpManagement
arguments map[string]any

View File

@ -11,7 +11,7 @@ var _ = Describe("AMQP Queue test ", func() {
var connection *AmqpConnection
var management *AmqpManagement
BeforeEach(func() {
conn, err := Dial(context.TODO(), []string{"amqp://"}, nil)
conn, err := Dial(context.TODO(), "amqp://", nil)
Expect(err).To(BeNil())
connection = conn
management = connection.Management()
@ -37,6 +37,7 @@ var _ = Describe("AMQP Queue test ", func() {
// validate GET (query queue info)
queueInfoReceived, err := management.QueueInfo(context.TODO(), queueName)
Expect(err).To(BeNil())
Expect(queueInfoReceived).To(Equal(queueInfo))
err = management.DeleteQueue(context.TODO(), queueName)
@ -68,6 +69,8 @@ var _ = Describe("AMQP Queue test ", func() {
Expect(queueInfo.IsAutoDelete()).To(BeTrue())
Expect(queueInfo.IsExclusive()).To(BeTrue())
Expect(queueInfo.Type()).To(Equal(Classic))
Expect(queueInfo.messageCount).To(BeZero())
Expect(queueInfo.consumerCount).To(BeZero())
Expect(queueInfo.Leader()).To(ContainSubstring("rabbit"))
Expect(len(queueInfo.Members())).To(BeNumerically(">", 0))
@ -84,6 +87,7 @@ var _ = Describe("AMQP Queue test ", func() {
// validate GET (query queue info)
queueInfoReceived, err := management.QueueInfo(context.TODO(), queueName)
Expect(err).To(BeNil())
Expect(queueInfoReceived).To(Equal(queueInfo))
err = management.DeleteQueue(context.TODO(), queueName)
@ -108,6 +112,7 @@ var _ = Describe("AMQP Queue test ", func() {
Expect(queueInfo.Type()).To(Equal(Quorum))
// validate GET (query queue info)
queueInfoReceived, err := management.QueueInfo(context.TODO(), queueName)
Expect(err).To(BeNil())
Expect(queueInfoReceived).To(Equal(queueInfo))
err = management.DeleteQueue(context.TODO(), queueName)
@ -134,6 +139,7 @@ var _ = Describe("AMQP Queue test ", func() {
Expect(queueInfo.Type()).To(Equal(Classic))
// validate GET (query queue info)
queueInfoReceived, err := management.QueueInfo(context.TODO(), queueName)
Expect(err).To(BeNil())
Expect(queueInfoReceived).To(Equal(queueInfo))
err = management.DeleteQueue(context.TODO(), queueName)
@ -238,10 +244,10 @@ var _ = Describe("AMQP Queue test ", func() {
})
func publishMessages(queueName string, count int, args ...string) {
conn, err := Dial(context.TODO(), []string{"amqp://guest:guest@localhost"}, nil)
conn, err := Dial(context.TODO(), "amqp://guest:guest@localhost", nil)
Expect(err).To(BeNil())
publisher, err := conn.NewPublisher(context.TODO(), &QueueAddress{Queue: queueName}, "test")
publisher, err := conn.NewPublisher(context.TODO(), &QueueAddress{Queue: queueName}, nil)
Expect(err).To(BeNil())
Expect(publisher).NotTo(BeNil())

View File

@ -1,6 +1,7 @@
package rabbitmqamqp
import (
"fmt"
"github.com/Azure/go-amqp"
"github.com/google/uuid"
)
@ -13,20 +14,20 @@ type StateRejected = amqp.StateRejected
type StateReleased = amqp.StateReleased
type StateModified = amqp.StateModified
type linkerName interface {
type iLinkerName interface {
linkName() string
}
func getLinkName(l linkerName) string {
func getLinkName(l iLinkerName) string {
if l == nil || l.linkName() == "" {
return uuid.New().String()
}
return l.linkName()
}
/// ConsumerOptions interface for the AMQP and Stream consumer///
/// IConsumerOptions interface for the AMQP and Stream consumer///
type ConsumerOptions interface {
type IConsumerOptions interface {
// linkName returns the name of the link
// if not set it will return a random UUID
linkName() string
@ -37,16 +38,22 @@ type ConsumerOptions interface {
// linkFilters returns the link filters for the link.
// It is mostly used for the stream consumers.
linkFilters() []amqp.LinkFilter
// id returns the id of the consumer
id() string
// validate the consumer options based on the available features
validate(available *featuresAvailable) error
}
func getInitialCredits(co ConsumerOptions) int32 {
func getInitialCredits(co IConsumerOptions) int32 {
if co == nil || co.initialCredits() == 0 {
return 256
}
return co.initialCredits()
}
func getLinkFilters(co ConsumerOptions) []amqp.LinkFilter {
func getLinkFilters(co IConsumerOptions) []amqp.LinkFilter {
if co == nil {
return nil
}
@ -69,32 +76,54 @@ func (mo *managementOptions) linkFilters() []amqp.LinkFilter {
return nil
}
type AMQPConsumerOptions struct {
//ReceiverLinkName: see the ConsumerOptions interface
ReceiverLinkName string
//InitialCredits: see the ConsumerOptions interface
InitialCredits int32
func (mo *managementOptions) id() string {
return "management"
}
func (aco *AMQPConsumerOptions) linkName() string {
return aco.ReceiverLinkName
}
func (aco *AMQPConsumerOptions) initialCredits() int32 {
return aco.InitialCredits
}
func (aco *AMQPConsumerOptions) linkFilters() []amqp.LinkFilter {
func (mo *managementOptions) validate(available *featuresAvailable) error {
return nil
}
type OffsetSpecification interface {
// ConsumerOptions represents the options for quorum and classic queues
type ConsumerOptions struct {
//ReceiverLinkName: see the IConsumerOptions interface
ReceiverLinkName string
//InitialCredits: see the IConsumerOptions interface
InitialCredits int32
// The id of the consumer
Id string
}
func (aco *ConsumerOptions) linkName() string {
return aco.ReceiverLinkName
}
func (aco *ConsumerOptions) initialCredits() int32 {
return aco.InitialCredits
}
func (aco *ConsumerOptions) linkFilters() []amqp.LinkFilter {
return nil
}
func (aco *ConsumerOptions) id() string {
return aco.Id
}
func (aco *ConsumerOptions) validate(available *featuresAvailable) error {
return nil
}
type IOffsetSpecification interface {
toLinkFilter() amqp.LinkFilter
}
const rmqStreamFilter = "rabbitmq:stream-filter"
const rmqStreamOffsetSpec = "rabbitmq:stream-offset-spec"
const rmqStreamMatchUnfiltered = "rabbitmq:stream-match-unfiltered"
const amqpApplicationPropertiesFilter = "amqp:application-properties-filter"
const amqpPropertiesFilter = "amqp:properties-filter"
const offsetFirst = "first"
const offsetNext = "next"
const offsetLast = "last"
@ -128,23 +157,37 @@ func (on *OffsetNext) toLinkFilter() amqp.LinkFilter {
return amqp.NewLinkFilter(rmqStreamOffsetSpec, 0, offsetNext)
}
// StreamFilterOptions represents the options that can be used to filter the stream data.
// It is used in the StreamConsumerOptions.
// See: https://www.rabbitmq.com/blog/2024/12/13/amqp-filter-expressions/
type StreamFilterOptions struct {
// Filter values.
Values []string
//
MatchUnfiltered bool
// Filter the data based on Application Property
ApplicationProperties map[string]any
// Filter the data based on Message Properties
Properties *amqp.MessageProperties
}
/*
StreamConsumerOptions represents the options that can be used to create a stream consumer.
StreamConsumerOptions represents the options for stream queues
It is mandatory in case of creating a stream consumer.
*/
type StreamConsumerOptions struct {
//ReceiverLinkName: see the ConsumerOptions interface
//ReceiverLinkName: see the IConsumerOptions interface
ReceiverLinkName string
//InitialCredits: see the ConsumerOptions interface
//InitialCredits: see the IConsumerOptions interface
InitialCredits int32
// The offset specification for the stream consumer
// see the interface implementations
Offset OffsetSpecification
// Filter values.
// See: https://www.rabbitmq.com/blog/2024/12/13/amqp-filter-expressions for more details
Filters []string
//
FilterMatchUnfiltered bool
Offset IOffsetSpecification
StreamFilterOptions *StreamFilterOptions
Id string
}
func (sco *StreamConsumerOptions) linkName() string {
@ -157,29 +200,117 @@ func (sco *StreamConsumerOptions) initialCredits() int32 {
func (sco *StreamConsumerOptions) linkFilters() []amqp.LinkFilter {
var filters []amqp.LinkFilter
filters = append(filters, sco.Offset.toLinkFilter())
if sco.Filters != nil {
l := []any{}
for _, f := range sco.Filters {
if sco.StreamFilterOptions != nil && sco.StreamFilterOptions.Values != nil {
var l []any
for _, f := range sco.StreamFilterOptions.Values {
l = append(l, f)
}
filters = append(filters, amqp.NewLinkFilter(rmqStreamFilter, 0, l))
filters = append(filters, amqp.NewLinkFilter(rmqStreamMatchUnfiltered, 0, sco.FilterMatchUnfiltered))
filters = append(filters, amqp.NewLinkFilter(rmqStreamMatchUnfiltered, 0, sco.StreamFilterOptions.MatchUnfiltered))
}
if sco.StreamFilterOptions != nil && sco.StreamFilterOptions.ApplicationProperties != nil {
l := map[string]any{}
for k, v := range sco.StreamFilterOptions.ApplicationProperties {
l[k] = v
}
filters = append(filters, amqp.NewLinkFilter(amqpApplicationPropertiesFilter, 0, l))
}
if sco.StreamFilterOptions != nil && sco.StreamFilterOptions.Properties != nil {
l := map[amqp.Symbol]any{}
if sco.StreamFilterOptions.Properties.ContentType != nil {
l["content-type"] = amqp.Symbol(*sco.StreamFilterOptions.Properties.ContentType)
}
if sco.StreamFilterOptions.Properties.ContentEncoding != nil {
l["content-encoding"] = amqp.Symbol(*sco.StreamFilterOptions.Properties.ContentEncoding)
}
if sco.StreamFilterOptions.Properties.CorrelationID != nil {
l["correlation-id"] = sco.StreamFilterOptions.Properties.CorrelationID
}
if sco.StreamFilterOptions.Properties.MessageID != nil {
l["message-id"] = sco.StreamFilterOptions.Properties.MessageID
}
if sco.StreamFilterOptions.Properties.Subject != nil {
l["subject"] = *sco.StreamFilterOptions.Properties.Subject
}
if sco.StreamFilterOptions.Properties.ReplyTo != nil {
l["reply-to"] = *sco.StreamFilterOptions.Properties.ReplyTo
}
if sco.StreamFilterOptions.Properties.To != nil {
l["to"] = *sco.StreamFilterOptions.Properties.To
}
if sco.StreamFilterOptions.Properties.GroupID != nil {
l["group-id"] = *sco.StreamFilterOptions.Properties.GroupID
}
if sco.StreamFilterOptions.Properties.UserID != nil {
l["user-id"] = sco.StreamFilterOptions.Properties.UserID
}
if sco.StreamFilterOptions.Properties.AbsoluteExpiryTime != nil {
l["absolute-expiry-time"] = sco.StreamFilterOptions.Properties.AbsoluteExpiryTime
}
if sco.StreamFilterOptions.Properties.CreationTime != nil {
l["creation-time"] = sco.StreamFilterOptions.Properties.CreationTime
}
if sco.StreamFilterOptions.Properties.GroupSequence != nil {
l["group-sequence"] = *sco.StreamFilterOptions.Properties.GroupSequence
}
if sco.StreamFilterOptions.Properties.ReplyToGroupID != nil {
l["reply-to-group-id"] = *sco.StreamFilterOptions.Properties.ReplyToGroupID
}
if len(l) > 0 {
filters = append(filters, amqp.NewLinkFilter(amqpPropertiesFilter, 0, l))
}
}
return filters
}
///// ProducerOptions /////
type ProducerOptions interface {
linkName() string
func (sco *StreamConsumerOptions) id() string {
return sco.Id
}
type AMQPProducerOptions struct {
func (sco *StreamConsumerOptions) validate(available *featuresAvailable) error {
if sco.StreamFilterOptions != nil && sco.StreamFilterOptions.Properties != nil {
if !available.is41OrMore {
return fmt.Errorf("stream consumer with properties filter is not supported. You need RabbitMQ 4.1 or later")
}
}
return nil
}
///// PublisherOptions /////
type IPublisherOptions interface {
linkName() string
id() string
}
type PublisherOptions struct {
Id string
SenderLinkName string
}
func (apo *AMQPProducerOptions) linkName() string {
func (apo *PublisherOptions) linkName() string {
return apo.SenderLinkName
}
func (apo *PublisherOptions) id() string {
return apo.Id
}

View File

@ -40,7 +40,7 @@ func createSenderLinkOptions(address string, linkName string, deliveryMode int)
// receiverLinkOptions returns the options for a receiver link
// with the given address and link name.
// That should be the same for all the links.
func createReceiverLinkOptions(address string, options ConsumerOptions, deliveryMode int) *amqp.ReceiverOptions {
func createReceiverLinkOptions(address string, options IConsumerOptions, deliveryMode int) *amqp.ReceiverOptions {
prop := make(map[string]any)
prop["paired"] = true
receiverSettleMode := amqp.SenderSettleModeSettled.Ptr()
@ -74,7 +74,7 @@ func random(max int) int {
}
func validateMessageAnnotations(annotations amqp.Annotations) error {
for k, _ := range annotations {
for k := range annotations {
switch tp := k.(type) {
case string:
if err := validateMessageAnnotationKey(tp); err != nil {

View File

@ -29,6 +29,7 @@ const (
key = "key"
queues = "queues"
bindings = "bindings"
authTokens = "/auth/tokens"
)
func validatePositive(label string, value int64) error {

View File

@ -34,15 +34,15 @@ var _ = Describe("Converters", func() {
})
It("Converts from string logError", func() {
v, err := CapacityFrom("10LL")
_, err := CapacityFrom("10LL")
Expect(fmt.Sprintf("%s", err)).
To(ContainSubstring("Invalid unit size format"))
v, err = CapacityFrom("aGB")
_, err = CapacityFrom("aGB")
Expect(fmt.Sprintf("%s", err)).
To(ContainSubstring("Invalid number format"))
v, err = CapacityFrom("")
v, err := CapacityFrom("")
Expect(v).To(Equal(int64(0)))
Expect(err).To(BeNil())

View File

@ -1,6 +1,6 @@
package rabbitmqamqp
type entityIdentifier interface {
type iEntityIdentifier interface {
Id() string
}
@ -21,9 +21,9 @@ func (e QueueType) String() string {
}
/*
QueueSpecification represents the specification of a queue
IQueueSpecification represents the specification of a queue
*/
type QueueSpecification interface {
type IQueueSpecification interface {
name() string
isAutoDelete() bool
isExclusive() bool
@ -31,7 +31,7 @@ type QueueSpecification interface {
buildArguments() map[string]any
}
type OverflowStrategy interface {
type IOverflowStrategy interface {
overflowStrategy() string
}
@ -56,7 +56,7 @@ func (r *RejectPublishDlxOverflowStrategy) overflowStrategy() string {
return "reject-publish-dlx"
}
type LeaderLocator interface {
type ILeaderLocator interface {
leaderLocator() string
}
@ -79,18 +79,19 @@ QuorumQueueSpecification represents the specification of the quorum queue
*/
type QuorumQueueSpecification struct {
Name string
AutoExpire int64
MessageTTL int64
OverflowStrategy OverflowStrategy
SingleActiveConsumer bool
DeadLetterExchange string
DeadLetterRoutingKey string
MaxLength int64
MaxLengthBytes int64
DeliveryLimit int64
TargetClusterSize int64
LeaderLocator LeaderLocator
Name string
AutoExpire int64
MessageTTL int64
OverflowStrategy IOverflowStrategy
SingleActiveConsumer bool
DeadLetterExchange string
DeadLetterRoutingKey string
MaxLength int64
MaxLengthBytes int64
DeliveryLimit int64
TargetClusterSize int64
LeaderLocator ILeaderLocator
QuorumInitialGroupSize int
}
func (q *QuorumQueueSpecification) name() string {
@ -155,6 +156,10 @@ func (q *QuorumQueueSpecification) buildArguments() map[string]any {
result["x-queue-leader-locator"] = q.LeaderLocator.leaderLocator()
}
if q.QuorumInitialGroupSize != 0 {
result["x-quorum-initial-group-size"] = q.QuorumInitialGroupSize
}
result["x-queue-type"] = q.queueType().String()
return result
}
@ -168,14 +173,14 @@ type ClassicQueueSpecification struct {
IsExclusive bool
AutoExpire int64
MessageTTL int64
OverflowStrategy OverflowStrategy
OverflowStrategy IOverflowStrategy
SingleActiveConsumer bool
DeadLetterExchange string
DeadLetterRoutingKey string
MaxLength int64
MaxLengthBytes int64
MaxPriority int64
LeaderLocator LeaderLocator
LeaderLocator ILeaderLocator
}
func (q *ClassicQueueSpecification) name() string {
@ -344,8 +349,8 @@ func (e ExchangeType) String() string {
return string(e.Type)
}
// ExchangeSpecification represents the specification of an exchange
type ExchangeSpecification interface {
// IExchangeSpecification represents the specification of an exchange
type IExchangeSpecification interface {
name() string
isAutoDelete() bool
exchangeType() ExchangeType
@ -465,7 +470,7 @@ func (c *CustomExchangeSpecification) arguments() map[string]any {
// / **** Binding ****
type BindingSpecification interface {
type IBindingSpecification interface {
sourceExchange() string
destination() string
bindingKey() string

View File

@ -2,6 +2,7 @@ package rabbitmqamqp
import (
"fmt"
"github.com/Azure/go-amqp"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
@ -108,6 +109,24 @@ var _ = Describe("Available Features", func() {
Expect(availableFeatures.is4OrMore).To(BeTrue())
Expect(availableFeatures.is41OrMore).To(BeTrue())
Expect(availableFeatures.isRabbitMQ).To(BeTrue())
})
It("StreamConsumerOptions validate for RabbitMQ 4.1", func() {
Expect((&StreamConsumerOptions{
StreamFilterOptions: &StreamFilterOptions{
Properties: &amqp.MessageProperties{
MessageID: "123",
},
},
}).validate(&featuresAvailable{is41OrMore: false})).To(MatchError("stream consumer with properties filter is not supported. You need RabbitMQ 4.1 or later"))
Expect((&StreamConsumerOptions{
StreamFilterOptions: &StreamFilterOptions{
Properties: &amqp.MessageProperties{
MessageID: "123",
},
},
}).validate(&featuresAvailable{is41OrMore: true})).To(BeNil())
})
})

View File

@ -5,7 +5,7 @@ import (
"sync"
)
type LifeCycleState interface {
type ILifeCycleState interface {
getState() int
}
@ -49,7 +49,7 @@ const (
closed = iota
)
func statusToString(status LifeCycleState) string {
func statusToString(status ILifeCycleState) string {
switch status.getState() {
case open:
return "open"
@ -65,8 +65,8 @@ func statusToString(status LifeCycleState) string {
}
type StateChanged struct {
From LifeCycleState
To LifeCycleState
From ILifeCycleState
To ILifeCycleState
}
func (s StateChanged) String() string {
@ -77,6 +77,9 @@ func (s StateChanged) String() string {
switch s.To.(type) {
case *StateClosed:
if s.To.(*StateClosed).error == nil {
return fmt.Sprintf("From: %s, To: %s", statusToString(s.From), statusToString(s.To))
}
return fmt.Sprintf("From: %s, To: %s, Error: %s", statusToString(s.From), statusToString(s.To), s.To.(*StateClosed).error)
}
@ -85,7 +88,7 @@ func (s StateChanged) String() string {
}
type LifeCycle struct {
state LifeCycleState
state ILifeCycleState
chStatusChanged chan *StateChanged
mutex *sync.Mutex
}
@ -97,13 +100,13 @@ func NewLifeCycle() *LifeCycle {
}
}
func (l *LifeCycle) State() LifeCycleState {
func (l *LifeCycle) State() ILifeCycleState {
l.mutex.Lock()
defer l.mutex.Unlock()
return l.state
}
func (l *LifeCycle) SetState(value LifeCycleState) {
func (l *LifeCycle) SetState(value ILifeCycleState) {
l.mutex.Lock()
defer l.mutex.Unlock()
if l.state == value {
@ -122,3 +125,9 @@ func (l *LifeCycle) SetState(value LifeCycleState) {
To: value,
}
}
func (l *LifeCycle) notifyStatusChange(channel chan *StateChanged) {
l.mutex.Lock()
defer l.mutex.Unlock()
l.chStatusChanged = channel
}

View File

@ -8,7 +8,7 @@ import (
// MessagePropertyToAddress sets the To property of the message to the address of the target.
// The target must be a QueueAddress or an ExchangeAddress.
// Note: The field msgRef.Properties.To will be overwritten if it is already set.
func MessagePropertyToAddress(msgRef *amqp.Message, target TargetAddress) error {
func MessagePropertyToAddress(msgRef *amqp.Message, target ITargetAddress) error {
if target == nil {
return errors.New("target cannot be nil")
}
@ -33,7 +33,7 @@ func NewMessage(body []byte) *amqp.Message {
// NewMessageWithAddress creates a new AMQP 1.0 new message with the given payload and sets the To property to the address of the target.
// The target must be a QueueAddress or an ExchangeAddress.
// This function is a helper that combines NewMessage and MessagePropertyToAddress.
func NewMessageWithAddress(body []byte, target TargetAddress) (*amqp.Message, error) {
func NewMessageWithAddress(body []byte, target ITargetAddress) (*amqp.Message, error) {
message := amqp.NewMessage(body)
err := MessagePropertyToAddress(message, target)
if err != nil {

View File

@ -0,0 +1,207 @@
package rabbitmqamqp
// test the OAuth2 connection
import (
"context"
"encoding/base64"
"github.com/golang-jwt/jwt/v5"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
testhelper "github.com/rabbitmq/rabbitmq-amqp-go-client/pkg/test-helper"
"math/rand"
"time"
)
const Base64Key = "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGH"
// const HmacKey KEY = new HmacKey(Base64.getDecoder().decode(Base64Key));
const AUDIENCE = "rabbitmq"
// Helper function to generate random string
func randomString(length int) string {
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
result := make([]byte, length)
for i := range result {
result[i] = charset[rand.Intn(len(charset))]
}
return string(result)
}
var _ = Describe("OAuth2 Tests", func() {
It("OAuth2 Connection should success", func() {
tokenString := token(time.Now().Add(time.Duration(2500) * time.Millisecond))
Expect(tokenString).NotTo(BeEmpty())
conn, err := Dial(context.TODO(), "amqp://localhost:5672",
&AmqpConnOptions{
ContainerID: "oAuth2Test",
OAuth2Options: &OAuth2Options{
Token: tokenString,
},
})
Expect(err).To(BeNil())
Expect(conn).NotTo(BeNil())
qName := generateName("OAuth2 Connection should success")
_, err = conn.Management().DeclareQueue(context.Background(), &QuorumQueueSpecification{
Name: qName,
})
Expect(err).To(BeNil())
Expect(conn.Management().DeleteQueue(context.Background(), qName)).To(BeNil())
Expect(conn.Close(context.Background())).To(BeNil())
})
It("OAuth2 Connection should disconnect after the timeout", func() {
tokenString := token(time.Now().Add(time.Duration(1_000) * time.Millisecond))
Expect(tokenString).NotTo(BeEmpty())
conn, err := Dial(context.TODO(), "amqp://localhost:5672",
&AmqpConnOptions{
ContainerID: "oAuth2TestTimeout",
OAuth2Options: &OAuth2Options{
Token: tokenString,
},
RecoveryConfiguration: &RecoveryConfiguration{
ActiveRecovery: false,
},
})
Expect(err).To(BeNil())
Expect(conn).NotTo(BeNil())
ch := make(chan *StateChanged, 1)
go func() {
defer GinkgoRecover()
for statusChanged := range ch {
x := statusChanged.To.(*StateClosed)
Expect(x.GetError()).NotTo(BeNil())
Expect(x.GetError().Error()).To(ContainSubstring("credential expired"))
}
}()
conn.NotifyStatusChange(ch)
time.Sleep(1 * time.Second)
})
It("OAuth2 Connection should be alive after token refresh", func() {
tokenString := token(time.Now().Add(time.Duration(1) * time.Second))
Expect(tokenString).NotTo(BeEmpty())
conn, err := Dial(context.TODO(), "amqp://localhost:5672",
&AmqpConnOptions{
ContainerID: "oAuth2Test",
OAuth2Options: &OAuth2Options{
Token: tokenString,
},
RecoveryConfiguration: &RecoveryConfiguration{
ActiveRecovery: false,
},
})
Expect(err).To(BeNil())
Expect(conn).NotTo(BeNil())
time.Sleep(100 * time.Millisecond)
err = conn.RefreshToken(context.Background(), token(time.Now().Add(time.Duration(2500)*time.Millisecond)))
time.Sleep(1 * time.Second)
Expect(err).To(BeNil())
Expect(conn.Close(context.Background())).To(BeNil())
})
// this test is a bit flaky, it may fail if the connection is not closed in time
// that should mark as flakes
It("OAuth2 Connection should use the new token to reconnect", func() {
name := "oAuth2TestReconnect_" + time.Now().String()
startToken := token(time.Now().Add(time.Duration(1) * time.Second))
connection, err := Dial(context.Background(), "amqp://", &AmqpConnOptions{
OAuth2Options: &OAuth2Options{
Token: startToken,
},
ContainerID: name,
// reduced the reconnect interval to speed up the test
RecoveryConfiguration: &RecoveryConfiguration{
ActiveRecovery: true,
BackOffReconnectInterval: 1100 * time.Millisecond,
MaxReconnectAttempts: 5,
},
})
Expect(err).To(BeNil())
Expect(connection).NotTo(BeNil())
ch := make(chan *StateChanged, 1)
connection.NotifyStatusChange(ch)
newToken := token(time.Now().Add(time.Duration(10) * time.Second))
Expect(connection.RefreshToken(context.Background(), newToken)).To(BeNil())
time.Sleep(1 * time.Second)
// here the token used during the connection (startToken) is expired
// the new token should be used to reconnect.
// The test is to validate that the client uses the new token to reconnect
// The RefreshToken requests a new token and updates the connection with the new token
Eventually(func() bool {
err := testhelper.DropConnectionContainerID(name)
return err == nil
}).WithTimeout(5 * time.Second).WithPolling(400 * time.Millisecond).Should(BeTrue())
st1 := <-ch
Expect(st1.From).To(Equal(&StateOpen{}))
Expect(st1.To).To(BeAssignableToTypeOf(&StateClosed{}))
time.Sleep(1 * time.Second)
// the connection should not be reconnected
Eventually(func() bool {
conn, err := testhelper.GetConnectionByContainerID(name)
return err == nil && conn != nil
}).WithTimeout(5 * time.Second).WithPolling(400 * time.Millisecond).Should(BeTrue())
Expect(connection.Close(context.Background())).To(BeNil())
})
It("Setting OAuth2 on the Environment should work", func() {
env := NewClusterEnvironment([]Endpoint{
{Address: "amqp://", Options: &AmqpConnOptions{
OAuth2Options: &OAuth2Options{
Token: token(time.Now().Add(time.Duration(10) * time.Second)),
},
},
}})
Expect(env).NotTo(BeNil())
Expect(env.Connections()).NotTo(BeNil())
Expect(len(env.Connections())).To(Equal(0))
connection, err := env.NewConnection(context.Background())
Expect(err).To(BeNil())
Expect(connection).NotTo(BeNil())
Expect(len(env.Connections())).To(Equal(1))
Expect(connection.Close(context.Background())).To(BeNil())
Expect(len(env.Connections())).To(Equal(0))
})
It("Can't use refresh token if not OAuth2 is enabled ", func() {
connection, err := Dial(context.Background(), "amqp://", nil)
Expect(err).To(BeNil())
Expect(connection).NotTo(BeNil())
err = connection.RefreshToken(context.Background(), token(time.Now().Add(time.Duration(10)*time.Second)))
Expect(err).NotTo(BeNil())
Expect(err.Error()).To(ContainSubstring("is not configured to use OAuth2 token"))
Expect(connection.Close(context.Background())).To(BeNil())
})
})
func token(duration time.Time) string {
decodedKey, _ := base64.StdEncoding.DecodeString(Base64Key)
claims := jwt.MapClaims{
"iss": "unit_test",
"aud": AUDIENCE,
"exp": jwt.NewNumericDate(duration),
"scope": []string{"rabbitmq.configure:*/*", "rabbitmq.write:*/*", "rabbitmq.read:*/*"},
"random": randomString(6),
}
// Create a new token object, specifying signing method and the claims
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
token.Header["kid"] = "token-key"
// Sign and get the complete encoded token as a string using the secret
tokenString, err := token.SignedString(decodedKey)
Expect(err).To(BeNil())
return tokenString
}

View File

@ -8,5 +8,30 @@ import (
func generateNameWithDateTime(name string) string {
return fmt.Sprintf("%s_%s", name, strconv.FormatInt(time.Now().Unix(), 10))
}
// Helper function to create string pointers
func stringPtr(s string) *string {
return &s
}
func uint32Ptr(i uint32) *uint32 {
return &i
}
// create a static date time string for testing
func createDateTime() time.Time {
layout := time.RFC3339
value := "2006-01-02T15:04:05Z"
t, err := time.Parse(layout, value)
if err != nil {
panic(err)
}
return t
}
// convert time to pointer
func timePtr(t time.Time) *time.Time {
return &t
}

View File

@ -62,7 +62,6 @@ func DropConnectionContainerID(Id string) error {
if err != nil {
return err
}
return nil
}
@ -74,6 +73,7 @@ func DropConnection(name string, port string) error {
return nil
}
func httpGet(url, username, password string) (string, error) {
return baseCall(url, username, password, "GET")
}
@ -106,6 +106,11 @@ func baseCall(url, username, password string, method string) (string, error) {
return string(bodyBytes), nil
}
if resp.StatusCode == 201 {
// Created! it is ok
return "", nil
}
if resp.StatusCode == 204 { // No Content
return "", nil
}