Compare commits

..

No commits in common. "171dca447c499d8353ea45a068bef76708571eb9" and "a0dbb5594dcea4f284357886abddd559158c55cd" have entirely different histories.

58 changed files with 764 additions and 2148 deletions

View File

@ -1,21 +1,21 @@
-----BEGIN CERTIFICATE----- -----BEGIN CERTIFICATE-----
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
-----END CERTIFICATE----- -----END CERTIFICATE-----

View File

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

Binary file not shown.

View File

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

View File

@ -1,28 +1,30 @@
-----BEGIN PRIVATE KEY----- -----BEGIN ENCRYPTED PRIVATE KEY-----
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
ValHWT50FbbgAY337QwDjO8= 5T42x6xJtS6qTRttwrpRE5KjBHgcq0m8LSQu6chKwWinFUfOAdcZODvVVqLA2e6K
-----END PRIVATE KEY----- plfcGb027WE4DHqTzW73nbnK+NwkP3lLORbro7KWDFdNKP3v/Rx0Uq7CPGVN994G
tH7wyGheZNTMtKDFGrAdOqse9/sKcTF3thaqYqXgDDZY
-----END ENCRYPTED PRIVATE KEY-----

Binary file not shown.

View File

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

View File

@ -1,28 +1,30 @@
-----BEGIN PRIVATE KEY----- -----BEGIN ENCRYPTED PRIVATE KEY-----
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
rRdolD9MXCQidIrqwsPyHprv tNKp4KrlpF3jGIaPODXZVE2aIrhI0njVlUjIQRs6OOMXleO6+xWQI/1fx/xn/oKm
-----END PRIVATE KEY----- TBUOtW3Y7AzyojbPiScvjmT+aoVwAZ3juHnUuxEuyUcI3WokkWPpllcaGd95sCUG
7iR90VPBJ/meYyQMYY1BGq4ngi5DvLGy6K/pS5CHPi0U
-----END ENCRYPTED PRIVATE KEY-----

View File

@ -1,13 +0,0 @@
[
{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

@ -1 +0,0 @@
{"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_backend_oauth2]. [rabbitmq_auth_mechanism_ssl,rabbitmq_management,rabbitmq_stream,rabbitmq_stream_management,rabbitmq_top].

View File

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

View File

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

View File

@ -1,10 +1,3 @@
# 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 all: test
format: format:
@ -12,21 +5,8 @@ format:
vet: vet:
go vet ./pkg/rabbitmqamqp go vet ./pkg/rabbitmqamqp
go vet ./docs/examples/...
STATICCHECK ?= $(GOBIN)/staticcheck test: format vet
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 \ cd ./pkg/rabbitmqamqp && go run -mod=mod github.com/onsi/ginkgo/v2/ginkgo \
--randomize-all --randomize-suites \ --randomize-all --randomize-suites \
--cover --coverprofile=coverage.txt --covermode=atomic \ --cover --coverprofile=coverage.txt --covermode=atomic \
@ -34,8 +14,8 @@ test: format vet check
rabbitmq-server-start: rabbitmq-server-start-arm:
./.ci/ubuntu/gha-setup.sh start pull ./.ci/ubuntu/gha-setup.sh start pull arm
rabbitmq-server-stop: rabbitmq-server-stop:
./.ci/ubuntu/gha-setup.sh stop ./.ci/ubuntu/gha-setup.sh stop

View File

@ -1,13 +1,13 @@
# RabbitMQ AMQP 1.0 Golang Client # RabbitMQ AMQP 1.0 Golang Client
This library is meant to be used with RabbitMQ 4.0. (2025.06.26) This library is meant to be used with RabbitMQ 4.0.
Suitable for testing in pre-production environments.
## Getting Started ## Getting Started
- [Getting Started](docs/examples/getting_started) - [Getting Started](docs/examples/getting_started)
- [Examples](docs/examples) - [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 Video tutorial: </br>
[![Getting Started](https://img.youtube.com/vi/iR1JUFh3udI/0.jpg)](https://youtu.be/iR1JUFh3udI) [![Getting Started](https://img.youtube.com/vi/iR1JUFh3udI/0.jpg)](https://youtu.be/iR1JUFh3udI)
@ -15,7 +15,7 @@ This library is meant to be used with RabbitMQ 4.0. (2025.06.26)
## Documentation ## Documentation
- [Client Guide](https://www.rabbitmq.com/client-libraries/amqp-client-libraries) - [Client Guide](https://www.rabbitmq.com/client-libraries/amqp-client-libraries) (work in progress for this client)

View File

@ -7,6 +7,3 @@
- [Stream Filtering](streams_filtering) - An example of how to use streams [Filter Expressions](https://www.rabbitmq.com/blog/2024/12/13/amqp-filter-expressions) - [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. - [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

@ -1,76 +0,0 @@
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

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

View File

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

View File

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

View File

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

View File

@ -17,10 +17,9 @@ func checkError(err error) {
func main() { 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") rmq.Info("Golang AMQP 1.0 Streams example with filtering")
queueStream := "stream-go-queue-filtering-" + time.Now().String() queueStream := "stream-go-queue-filtering-" + time.Now().String()
env := rmq.NewEnvironment("amqp://guest:guest@localhost:5672/", nil) env := rmq.NewEnvironment([]string{"amqp://"}, nil)
amqpConnection, err := env.NewConnection(context.Background()) amqpConnection, err := env.NewConnection(context.Background())
checkError(err) checkError(err)
management := amqpConnection.Management() management := amqpConnection.Management()
@ -36,7 +35,7 @@ func main() {
// create a stream publisher. In this case we use the QueueAddress to make the example // create a stream publisher. In this case we use the QueueAddress to make the example
// simple. So we use the default exchange here. // simple. So we use the default exchange here.
publisher, err := amqpConnection.NewPublisher(context.TODO(), &rmq.QueueAddress{Queue: queueStream}, nil) publisher, err := amqpConnection.NewPublisher(context.TODO(), &rmq.QueueAddress{Queue: queueStream}, "stream-publisher")
checkError(err) checkError(err)
filters := []string{"MyFilter1", "MyFilter2", "MyFilter3", "MyFilter4"} filters := []string{"MyFilter1", "MyFilter2", "MyFilter3", "MyFilter4"}
@ -51,14 +50,17 @@ func main() {
switch publishResult.Outcome.(type) { switch publishResult.Outcome.(type) {
case *rmq.StateAccepted: case *rmq.StateAccepted:
rmq.Info("[Publisher]", "Message accepted", publishResult.Message.Data[0]) rmq.Info("[Publisher]", "Message accepted", publishResult.Message.Data[0])
break
case *rmq.StateReleased: case *rmq.StateReleased:
rmq.Warn("[Publisher]", "Message was not routed", publishResult.Message.Data[0]) rmq.Warn("[Publisher]", "Message was not routed", publishResult.Message.Data[0])
break
case *rmq.StateRejected: case *rmq.StateRejected:
rmq.Warn("[Publisher]", "Message rejected", publishResult.Message.Data[0]) rmq.Warn("[Publisher]", "Message rejected", publishResult.Message.Data[0])
stateType := publishResult.Outcome.(*rmq.StateRejected) stateType := publishResult.Outcome.(*rmq.StateRejected)
if stateType.Error != nil { if stateType.Error != nil {
rmq.Warn("[Publisher]", "Message rejected with error: %v", stateType.Error) rmq.Warn("[Publisher]", "Message rejected with error: %v", stateType.Error)
} }
break
default: default:
// these status are not supported. Leave it for AMQP 1.0 compatibility // these status are not supported. Leave it for AMQP 1.0 compatibility
// see: https://www.rabbitmq.com/docs/next/amqp#outcomes // see: https://www.rabbitmq.com/docs/next/amqp#outcomes
@ -74,25 +76,7 @@ func main() {
// add a filter to the consumer, in this case we use only the filter values // 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 // MyFilter1 and MyFilter2. So all other messages won't be received
StreamFilterOptions: &rmq.StreamFilterOptions{ Filters: []string{"MyFilter1", "MyFilter2"},
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) checkError(err)

View File

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

9
go.mod
View File

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

14
go.sum
View File

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

View File

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

View File

@ -6,57 +6,59 @@ import (
) )
var _ = Describe("address builder test ", func() { var _ = Describe("address builder test ", func() {
// Error cases It("With exchange, queue and key should raise and error", func() {
Describe("Error cases", func() { queue := "my_queue"
DescribeTable("should return appropriate errors", exchange := "my_exchange"
func(exchange, key, queue *string, expectedErr string) {
_, err := address(exchange, key, queue, nil) _, err := address(&exchange, nil, &queue, nil)
Expect(err).To(HaveOccurred()) Expect(err).NotTo(BeNil())
Expect(err.Error()).To(Equal(expectedErr)) Expect(err.Error()).To(Equal("exchange and queue cannot be set together"))
},
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"),
)
}) })
// Exchange-related cases It("Without exchange and queue should raise and error", func() {
Describe("Exchange addresses", func() { _, err := address(nil, nil, nil, nil)
DescribeTable("should generate correct exchange addresses", Expect(err).NotTo(BeNil())
func(exchange, key *string, expected string) { Expect(err.Error()).To(Equal("exchange or queue must be set"))
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"),
)
}) })
// Queue-related cases It("With exchange and key should return address", func() {
Describe("Queue addresses", func() { exchange := "my_exchange"
It("should generate correct queue address with special characters", func() { key := "my_key"
address, err := address(&exchange, &key, nil, nil)
Expect(err).To(BeNil())
Expect(address).To(Equal("/exchanges/my_exchange/my_key"))
})
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>" queue := "my_queue>"
address, err := address(nil, nil, &queue, nil) address, err := address(nil, nil, &queue, nil)
Expect(err).NotTo(HaveOccurred()) Expect(err).To(BeNil())
Expect(address).To(Equal("/queues/my_queue%3E")) Expect(address).To(Equal("/queues/my_queue%3E"))
}) })
It("should generate correct purge queue address", func() { It("With queue and urlParameters should return address", func() {
queue := "my_queue" queue := "my_queue"
address, err := purgeQueueAddress(&queue) address, err := purgeQueueAddress(&queue)
Expect(err).NotTo(HaveOccurred()) Expect(err).To(BeNil())
Expect(address).To(Equal("/queues/my_queue/messages")) 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[destination] = b.destinationName
kv["arguments"] = make(map[string]any) kv["arguments"] = make(map[string]any)
_, err := b.management.Request(ctx, kv, path, commandPost, []int{responseCode204}) _, err := b.management.Request(ctx, kv, path, commandPost, []int{responseCode204})
bindingPathWithExchangeQueueAndKey := bindingPathWithExchangeQueueKey(b.toQueue, b.sourceName, b.destinationName, b.bindingKey) bindingPathWithExchangeQueueKey := bindingPathWithExchangeQueueKey(b.toQueue, b.sourceName, b.destinationName, b.bindingKey)
return bindingPathWithExchangeQueueAndKey, err return bindingPathWithExchangeQueueKey, err
} }
// Unbind removes a binding between an exchange and a queue or exchange // 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 connection *AmqpConnection
var management *AmqpManagement var management *AmqpManagement
BeforeEach(func() { BeforeEach(func() {
conn, err := Dial(context.TODO(), "amqp://", nil) conn, err := Dial(context.TODO(), []string{"amqp://"}, nil)
Expect(err).To(BeNil()) Expect(err).To(BeNil())
connection = conn connection = conn
management = connection.Management() management = connection.Management()

View File

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

View File

@ -1,14 +1,10 @@
package rabbitmqamqp package rabbitmqamqp
import ( import (
"errors"
"sync" "sync"
"time" "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 { type RecoveryConfiguration struct {
/* /*
ActiveRecovery Define if the recovery is activated. ActiveRecovery Define if the recovery is activated.
@ -33,17 +29,6 @@ type RecoveryConfiguration struct {
MaxReconnectAttempts int MaxReconnectAttempts int
} }
func (c *RecoveryConfiguration) Clone() *RecoveryConfiguration {
cloned := &RecoveryConfiguration{
ActiveRecovery: c.ActiveRecovery,
BackOffReconnectInterval: c.BackOffReconnectInterval,
MaxReconnectAttempts: c.MaxReconnectAttempts,
}
return cloned
}
func NewRecoveryConfiguration() *RecoveryConfiguration { func NewRecoveryConfiguration() *RecoveryConfiguration {
return &RecoveryConfiguration{ return &RecoveryConfiguration{
ActiveRecovery: true, ActiveRecovery: true,
@ -64,19 +49,35 @@ func newEntitiesTracker() *entitiesTracker {
} }
} }
func (e *entitiesTracker) storeOrReplaceProducer(entity iEntityIdentifier) { func (e *entitiesTracker) storeOrReplaceProducer(entity entityIdentifier) {
e.publishers.Store(entity.Id(), entity) e.publishers.Store(entity.Id(), entity)
} }
func (e *entitiesTracker) removeProducer(entity iEntityIdentifier) { 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) {
e.publishers.Delete(entity.Id()) e.publishers.Delete(entity.Id())
} }
func (e *entitiesTracker) storeOrReplaceConsumer(entity iEntityIdentifier) { func (e *entitiesTracker) storeOrReplaceConsumer(entity entityIdentifier) {
e.consumers.Store(entity.Id(), entity) e.consumers.Store(entity.Id(), entity)
} }
func (e *entitiesTracker) removeConsumer(entity iEntityIdentifier) { 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) {
e.consumers.Delete(entity.Id()) e.consumers.Delete(entity.Id())
} }

View File

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

View File

@ -2,29 +2,25 @@ package rabbitmqamqp
import ( import (
"context" "context"
"crypto/tls"
"crypto/x509"
"fmt"
"github.com/Azure/go-amqp" "github.com/Azure/go-amqp"
. "github.com/onsi/ginkgo/v2" . "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega" . "github.com/onsi/gomega"
"os"
"sync"
"time" "time"
) )
var _ = Describe("AMQP connection Test", func() { var _ = Describe("AMQP connection Test", func() {
It("AMQP SASLTypeAnonymous connection should succeed", func() { It("AMQP SASLTypeAnonymous connection should succeed", func() {
connection, err := Dial(context.Background(), "amqp://", &AmqpConnOptions{
connection, err := Dial(context.Background(), []string{"amqp://"}, &AmqpConnOptions{
SASLType: amqp.SASLTypeAnonymous()}) SASLType: amqp.SASLTypeAnonymous()})
Expect(err).To(BeNil()) Expect(err).To(BeNil())
err = connection.Close(context.Background()) err = connection.Close(context.Background())
Expect(err).To(BeNil()) Expect(err).To(BeNil())
}) })
//
It("AMQP SASLTypePlain connection should succeed", func() { It("AMQP SASLTypePlain connection should succeed", func() {
connection, err := Dial(context.Background(), "amqp://", &AmqpConnOptions{ connection, err := Dial(context.Background(), []string{"amqp://"}, &AmqpConnOptions{
SASLType: amqp.SASLTypePlain("guest", "guest")}) SASLType: amqp.SASLTypePlain("guest", "guest")})
Expect(err).To(BeNil()) Expect(err).To(BeNil())
@ -33,17 +29,38 @@ var _ = Describe("AMQP connection Test", func() {
err = connection.Close(context.Background()) err = connection.Close(context.Background())
Expect(err).To(BeNil()) 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() { It("AMQP connection should fail due to context cancellation", func() {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Millisecond) ctx, cancel := context.WithTimeout(context.Background(), 10*time.Millisecond)
cancel() cancel()
_, err := Dial(ctx, "amqp://", nil) _, err := Dial(ctx, []string{"amqp://"}, nil)
Expect(err).NotTo(BeNil()) Expect(err).NotTo(BeNil())
}) })
//
It("AMQP connection should receive events", func() { It("AMQP connection should receive events", func() {
ch := make(chan *StateChanged, 1) ch := make(chan *StateChanged, 1)
connection, err := Dial(context.Background(), "amqp://", nil) connection, err := Dial(context.Background(), []string{"amqp://"}, nil)
Expect(err).To(BeNil()) Expect(err).To(BeNil())
connection.NotifyStatusChange(ch) connection.NotifyStatusChange(ch)
err = connection.Close(context.Background()) err = connection.Close(context.Background())
@ -55,113 +72,19 @@ var _ = Describe("AMQP connection Test", func() {
Expect(recv.To).To(Equal(&StateClosed{})) Expect(recv.To).To(Equal(&StateClosed{}))
}) })
It("Entity tracker should be aligned with consumers and publishers ", func() { //It("AMQP TLS connection should success with SASLTypeAnonymous ", func() {
connection, err := Dial(context.Background(), "amqp://", &AmqpConnOptions{ // amqpConnection := NewAmqpConnection()
SASLType: amqp.SASLTypeAnonymous()}) // Expect(amqpConnection).NotTo(BeNil())
Expect(err).To(BeNil()) // Expect(amqpConnection).To(BeAssignableToTypeOf(&AmqpConnection{}))
Expect(connection).NotTo(BeNil()) //
// connectionSettings := NewConnUrlHelper().
queueName := generateNameWithDateTime("Entity tracker should be aligned with consumers and publishers") // UseSsl(true).Port(5671).TlsConfig(&tls.Config{
// //ServerName: "localhost",
_, err = connection.Management().DeclareQueue(context.Background(), &QuorumQueueSpecification{ // InsecureSkipVerify: true,
Name: queueName, // })
}) // Expect(connectionSettings).NotTo(BeNil())
// Expect(connectionSettings).To(BeAssignableToTypeOf(&ConnUrlHelper{}))
Expect(err).To(BeNil()) // err := amqpConnection.Open(context.Background(), connectionSettings)
publisher, err := connection.NewPublisher(context.Background(), &QueueAddress{Queue: queueName}, // Expect(err).To(BeNil())
&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,23 +25,17 @@ func (dc *DeliveryContext) Discard(ctx context.Context, e *amqp.Error) error {
return dc.receiver.RejectMessage(ctx, dc.message, e) return dc.receiver.RejectMessage(ctx, dc.message, e)
} }
// 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 { func (dc *DeliveryContext) DiscardWithAnnotations(ctx context.Context, annotations amqp.Annotations) error {
destination, err := copyAnnotations(annotations) if err := validateMessageAnnotations(annotations); err != nil {
if err != nil {
return err 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{ return dc.receiver.ModifyMessage(ctx, dc.message, &amqp.ModifyMessageOptions{
DeliveryFailed: true, DeliveryFailed: true,
UndeliverableHere: true, UndeliverableHere: true,
@ -54,10 +48,15 @@ func (dc *DeliveryContext) Requeue(ctx context.Context) error {
} }
func (dc *DeliveryContext) RequeueWithAnnotations(ctx context.Context, annotations amqp.Annotations) error { func (dc *DeliveryContext) RequeueWithAnnotations(ctx context.Context, annotations amqp.Annotations) error {
destination, err := copyAnnotations(annotations) if err := validateMessageAnnotations(annotations); err != nil {
if err != nil {
return err 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{ return dc.receiver.ModifyMessage(ctx, dc.message, &amqp.ModifyMessageOptions{
DeliveryFailed: false, DeliveryFailed: false,
UndeliverableHere: false, UndeliverableHere: false,
@ -68,7 +67,7 @@ func (dc *DeliveryContext) RequeueWithAnnotations(ctx context.Context, annotatio
type Consumer struct { type Consumer struct {
receiver atomic.Pointer[amqp.Receiver] receiver atomic.Pointer[amqp.Receiver]
connection *AmqpConnection connection *AmqpConnection
options IConsumerOptions options ConsumerOptions
destinationAdd string destinationAdd string
id string id string
@ -85,10 +84,10 @@ func (c *Consumer) Id() string {
return c.id return c.id
} }
func newConsumer(ctx context.Context, connection *AmqpConnection, destinationAdd string, options IConsumerOptions) (*Consumer, error) { func newConsumer(ctx context.Context, connection *AmqpConnection, destinationAdd string, options ConsumerOptions, args ...string) (*Consumer, error) {
id := fmt.Sprintf("consumer-%s", uuid.New().String()) id := fmt.Sprintf("consumer-%s", uuid.New().String())
if options != nil && options.id() != "" { if len(args) > 0 {
id = options.id() id = args[0]
} }
r := &Consumer{connection: connection, options: options, r := &Consumer{connection: connection, options: options,
@ -145,6 +144,5 @@ func (c *Consumer) Receive(ctx context.Context) (*DeliveryContext, error) {
} }
func (c *Consumer) Close(ctx context.Context) error { func (c *Consumer) Close(ctx context.Context) error {
c.connection.entitiesTracker.removeConsumer(c)
return c.receiver.Load().Close(ctx) return c.receiver.Load().Close(ctx)
} }

View File

@ -7,10 +7,31 @@ import (
. "github.com/onsi/ginkgo/v2" . "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega" . "github.com/onsi/gomega"
testhelper "github.com/rabbitmq/rabbitmq-amqp-go-client/pkg/test-helper" testhelper "github.com/rabbitmq/rabbitmq-amqp-go-client/pkg/test-helper"
"sync" "strconv"
"time" "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() { var _ = Describe("Consumer stream test", func() {
It("start consuming with different offset types", func() { It("start consuming with different offset types", func() {
@ -28,7 +49,7 @@ var _ = Describe("Consumer stream test", func() {
*/ */
qName := generateName("start consuming with different offset types") qName := generateName("start consuming with different offset types")
connection, err := Dial(context.Background(), "amqp://", nil) connection, err := Dial(context.Background(), []string{"amqp://"}, nil)
Expect(err).To(BeNil()) Expect(err).To(BeNil())
queueInfo, err := connection.Management().DeclareQueue(context.Background(), &StreamQueueSpecification{ queueInfo, err := connection.Management().DeclareQueue(context.Background(), &StreamQueueSpecification{
Name: qName, Name: qName,
@ -52,7 +73,7 @@ var _ = Describe("Consumer stream test", func() {
dc, err := consumerOffsetValue.Receive(context.Background()) dc, err := consumerOffsetValue.Receive(context.Background())
Expect(err).To(BeNil()) Expect(err).To(BeNil())
Expect(string(dc.Message().GetData())).To(Equal(fmt.Sprintf("Message #%d", i+5))) Expect(fmt.Sprintf("%s", dc.Message().GetData())).To(Equal(fmt.Sprintf("Message #%d", i+5)))
Expect(dc.Accept(context.Background())).To(BeNil()) Expect(dc.Accept(context.Background())).To(BeNil())
} }
@ -67,7 +88,7 @@ var _ = Describe("Consumer stream test", func() {
dc, err := consumerFirst.Receive(context.Background()) dc, err := consumerFirst.Receive(context.Background())
Expect(err).To(BeNil()) Expect(err).To(BeNil())
Expect(dc.Message()).NotTo(BeNil()) Expect(dc.Message()).NotTo(BeNil())
Expect(string(dc.Message().GetData())).To(Equal(fmt.Sprintf("Message #%d", i))) Expect(fmt.Sprintf("%s", dc.Message().GetData())).To(Equal(fmt.Sprintf("Message #%d", i)))
Expect(dc.Accept(context.Background())).To(BeNil()) Expect(dc.Accept(context.Background())).To(BeNil())
} }
@ -86,7 +107,7 @@ var _ = Describe("Consumer stream test", func() {
dc, err := consumerLast.Receive(context.Background()) dc, err := consumerLast.Receive(context.Background())
Expect(err).To(BeNil()) Expect(err).To(BeNil())
Expect(dc.Message()).NotTo(BeNil()) Expect(dc.Message()).NotTo(BeNil())
Expect(string(dc.Message().GetData())).NotTo(Equal(fmt.Sprintf("Message #%d", 0))) Expect(fmt.Sprintf("%s", dc.Message().GetData())).NotTo(Equal(fmt.Sprintf("Message #%d", 0)))
Expect(dc.Accept(context.Background())).To(BeNil()) Expect(dc.Accept(context.Background())).To(BeNil())
consumerNext, err := connection.NewConsumer(context.Background(), qName, &StreamConsumerOptions{ consumerNext, err := connection.NewConsumer(context.Background(), qName, &StreamConsumerOptions{
@ -104,7 +125,7 @@ var _ = Describe("Consumer stream test", func() {
dc, err = consumerNext.Receive(context.Background()) dc, err = consumerNext.Receive(context.Background())
Expect(err).To(BeNil()) Expect(err).To(BeNil())
Expect(dc.Message()).NotTo(BeNil()) Expect(dc.Message()).NotTo(BeNil())
Expect(string(dc.Message().GetData())).To(Equal("the next message")) Expect(fmt.Sprintf("%s", dc.Message().GetData())).To(Equal("the next message"))
Expect(dc.Accept(context.Background())).To(BeNil()) Expect(dc.Accept(context.Background())).To(BeNil())
signal <- struct{}{} signal <- struct{}{}
}() }()
@ -125,7 +146,7 @@ var _ = Describe("Consumer stream test", func() {
*/ */
qName := generateName("consumer should restart form the last offset in case of disconnection") qName := generateName("consumer should restart form the last offset in case of disconnection")
connection, err := Dial(context.Background(), "amqp://", &AmqpConnOptions{ connection, err := Dial(context.Background(), []string{"amqp://"}, &AmqpConnOptions{
SASLType: amqp.SASLTypeAnonymous(), SASLType: amqp.SASLTypeAnonymous(),
ContainerID: qName, ContainerID: qName,
RecoveryConfiguration: &RecoveryConfiguration{ RecoveryConfiguration: &RecoveryConfiguration{
@ -158,7 +179,7 @@ var _ = Describe("Consumer stream test", func() {
dc, err := consumer.Receive(context.Background()) dc, err := consumer.Receive(context.Background())
Expect(err).To(BeNil()) Expect(err).To(BeNil())
Expect(dc.Message()).NotTo(BeNil()) Expect(dc.Message()).NotTo(BeNil())
Expect(string(dc.Message().GetData())).To(Equal(fmt.Sprintf("Message #%d", i))) Expect(fmt.Sprintf("%s", dc.Message().GetData())).To(Equal(fmt.Sprintf("Message #%d", i)))
Expect(dc.Accept(context.Background())).To(BeNil()) Expect(dc.Accept(context.Background())).To(BeNil())
} }
@ -179,7 +200,7 @@ var _ = Describe("Consumer stream test", func() {
dc, err := consumer.Receive(context.Background()) dc, err := consumer.Receive(context.Background())
Expect(err).To(BeNil()) Expect(err).To(BeNil())
Expect(dc.Message()).NotTo(BeNil()) Expect(dc.Message()).NotTo(BeNil())
Expect(string(dc.Message().GetData())).To(Equal(fmt.Sprintf("Message #%d", i))) Expect(fmt.Sprintf("%s", dc.Message().GetData())).To(Equal(fmt.Sprintf("Message #%d", i)))
} }
Expect(consumer.Close(context.Background())).To(BeNil()) Expect(consumer.Close(context.Background())).To(BeNil())
@ -188,7 +209,7 @@ var _ = Describe("Consumer stream test", func() {
}) })
It("consumer should filter messages based on x-stream-filter", func() { It("consumer should filter messages based on x-stream-filter", func() {
qName := generateName("consumer should filter messages based on x-stream-filter") qName := generateName("consumer should filter messages based on x-stream-filter")
connection, err := Dial(context.Background(), "amqp://", nil) connection, err := Dial(context.Background(), []string{"amqp://"}, nil)
Expect(err).To(BeNil()) Expect(err).To(BeNil())
queueInfo, err := connection.Management().DeclareQueue(context.Background(), &StreamQueueSpecification{ queueInfo, err := connection.Management().DeclareQueue(context.Background(), &StreamQueueSpecification{
Name: qName, Name: qName,
@ -196,34 +217,15 @@ var _ = Describe("Consumer stream test", func() {
Expect(err).To(BeNil()) Expect(err).To(BeNil())
Expect(queueInfo).NotTo(BeNil()) Expect(queueInfo).NotTo(BeNil())
Expect(queueInfo.name).To(Equal(qName)) Expect(queueInfo.name).To(Equal(qName))
publishMessagesWithMessageLogic(qName, "banana", 10, func(msg *amqp.Message) { publishMessagesWithStreamTag(qName, "banana", 10)
msg.Annotations = amqp.Annotations{ publishMessagesWithStreamTag(qName, "apple", 10)
// here we set the filter value taken from the filters array publishMessagesWithStreamTag(qName, "", 10)
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{ consumerBanana, err := connection.NewConsumer(context.Background(), qName, &StreamConsumerOptions{
ReceiverLinkName: "consumer banana should filter messages based on x-stream-filter", ReceiverLinkName: "consumer banana should filter messages based on x-stream-filter",
InitialCredits: 200, InitialCredits: 200,
Offset: &OffsetFirst{}, Offset: &OffsetFirst{},
StreamFilterOptions: &StreamFilterOptions{ Filters: []string{"banana"},
Values: []string{"banana"},
},
}) })
Expect(err).To(BeNil()) Expect(err).To(BeNil())
@ -233,7 +235,7 @@ var _ = Describe("Consumer stream test", func() {
dc, err := consumerBanana.Receive(context.Background()) dc, err := consumerBanana.Receive(context.Background())
Expect(err).To(BeNil()) Expect(err).To(BeNil())
Expect(dc.Message()).NotTo(BeNil()) Expect(dc.Message()).NotTo(BeNil())
Expect(string(dc.Message().GetData())).To(Equal(fmt.Sprintf("Message_id:%d_label:%s", i, "banana"))) Expect(fmt.Sprintf("%s", dc.Message().GetData())).To(Equal(fmt.Sprintf("banana #%d", i)))
Expect(dc.Accept(context.Background())).To(BeNil()) Expect(dc.Accept(context.Background())).To(BeNil())
} }
@ -241,10 +243,8 @@ var _ = Describe("Consumer stream test", func() {
ReceiverLinkName: "consumer apple should filter messages based on x-stream-filter", ReceiverLinkName: "consumer apple should filter messages based on x-stream-filter",
InitialCredits: 200, InitialCredits: 200,
Offset: &OffsetFirst{}, Offset: &OffsetFirst{},
StreamFilterOptions: &StreamFilterOptions{ Filters: []string{"apple"},
Values: []string{"apple"}, FilterMatchUnfiltered: true,
MatchUnfiltered: true,
},
}) })
Expect(err).To(BeNil()) Expect(err).To(BeNil())
@ -254,8 +254,7 @@ var _ = Describe("Consumer stream test", func() {
dc, err := consumerApple.Receive(context.Background()) dc, err := consumerApple.Receive(context.Background())
Expect(err).To(BeNil()) Expect(err).To(BeNil())
Expect(dc.Message()).NotTo(BeNil()) Expect(dc.Message()).NotTo(BeNil())
Expect(string(dc.Message().GetData())).To(Equal(fmt.Sprintf("Message_id:%d_label:%s", i, "apple"))) Expect(fmt.Sprintf("%s", dc.Message().GetData())).To(Equal(fmt.Sprintf("apple #%d", i)))
Expect(dc.Accept(context.Background())).To(BeNil()) Expect(dc.Accept(context.Background())).To(BeNil())
} }
@ -263,9 +262,7 @@ var _ = Describe("Consumer stream test", func() {
ReceiverLinkName: "consumer apple and banana should filter messages based on x-stream-filter", ReceiverLinkName: "consumer apple and banana should filter messages based on x-stream-filter",
InitialCredits: 200, InitialCredits: 200,
Offset: &OffsetFirst{}, Offset: &OffsetFirst{},
StreamFilterOptions: &StreamFilterOptions{ Filters: []string{"apple", "banana"},
Values: []string{"apple", "banana"},
},
}) })
Expect(err).To(BeNil()) Expect(err).To(BeNil())
@ -276,23 +273,19 @@ var _ = Describe("Consumer stream test", func() {
Expect(err).To(BeNil()) Expect(err).To(BeNil())
Expect(dc.Message()).NotTo(BeNil()) Expect(dc.Message()).NotTo(BeNil())
if i < 10 { if i < 10 {
Expect(string(dc.Message().GetData())).To(Equal(fmt.Sprintf("Message_id:%d_label:%s", i, "banana"))) Expect(fmt.Sprintf("%s", dc.Message().GetData())).To(Equal(fmt.Sprintf("banana #%d", i)))
} else { } 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()) Expect(dc.Accept(context.Background())).To(BeNil())
} }
consumerAppleMatchUnfiltered, err := connection.NewConsumer(context.Background(), qName, &StreamConsumerOptions{ consumerAppleMatchUnfiltered, err := connection.NewConsumer(context.Background(), qName, &StreamConsumerOptions{
ReceiverLinkName: "consumer apple should filter messages based on x-stream-filter and MatchUnfiltered true", ReceiverLinkName: "consumer apple should filter messages based on x-stream-filter and FilterMatchUnfiltered true",
InitialCredits: 200, InitialCredits: 200,
Offset: &OffsetFirst{}, Offset: &OffsetFirst{},
StreamFilterOptions: &StreamFilterOptions{ Filters: []string{"apple"},
Values: []string{"apple"}, FilterMatchUnfiltered: true,
MatchUnfiltered: true,
},
}) })
Expect(err).To(BeNil()) Expect(err).To(BeNil())
@ -303,9 +296,10 @@ var _ = Describe("Consumer stream test", func() {
Expect(err).To(BeNil()) Expect(err).To(BeNil())
Expect(dc.Message()).NotTo(BeNil()) Expect(dc.Message()).NotTo(BeNil())
if i < 10 { if i < 10 {
Expect(string(dc.Message().GetData())).To(Equal(fmt.Sprintf("Message_id:%d_label:%s", i, "apple"))) Expect(fmt.Sprintf("%s", dc.Message().GetData())).To(Equal(fmt.Sprintf("apple #%d", i)))
} else { } else {
Expect(string(dc.Message().GetData())).To(Equal(fmt.Sprintf("Message_id:%d_label:%s", i-10, ""))) Expect(fmt.Sprintf("%s", dc.Message().GetData())).To(Equal(fmt.Sprintf(" #%d", i-10)))
} }
Expect(dc.Accept(context.Background())).To(BeNil()) Expect(dc.Accept(context.Background())).To(BeNil())
} }
@ -318,263 +312,4 @@ var _ = Describe("Consumer stream test", func() {
Expect(connection.Close(context.Background())).To(BeNil()) 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() { It("AMQP NewConsumer should fail due to context cancellation", func() {
qName := generateNameWithDateTime("AMQP NewConsumer should fail due to context cancellation") qName := generateNameWithDateTime("AMQP NewConsumer should fail due to context cancellation")
connection, err := Dial(context.Background(), "amqp://", nil) connection, err := Dial(context.Background(), []string{"amqp://"}, nil)
Expect(err).To(BeNil()) Expect(err).To(BeNil())
queue, err := connection.Management().DeclareQueue(context.Background(), &QuorumQueueSpecification{ 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() { It("AMQP NewConsumer should ack and empty the queue", func() {
qName := generateNameWithDateTime("AMQP NewConsumer should ack and empty the queue") qName := generateNameWithDateTime("AMQP NewConsumer should ack and empty the queue")
connection, err := Dial(context.Background(), "amqp://", nil) connection, err := Dial(context.Background(), []string{"amqp://"}, nil)
Expect(err).To(BeNil()) Expect(err).To(BeNil())
queue, err := connection.Management().DeclareQueue(context.Background(), &QuorumQueueSpecification{ queue, err := connection.Management().DeclareQueue(context.Background(), &QuorumQueueSpecification{
Name: qName, Name: qName,
@ -62,7 +62,7 @@ var _ = Describe("NewConsumer tests", func() {
It("AMQP NewConsumer should requeue the message to the queue", func() { It("AMQP NewConsumer should requeue the message to the queue", func() {
qName := generateNameWithDateTime("AMQP NewConsumer should requeue the message to the queue") qName := generateNameWithDateTime("AMQP NewConsumer should requeue the message to the queue")
connection, err := Dial(context.Background(), "amqp://", nil) connection, err := Dial(context.Background(), []string{"amqp://"}, nil)
Expect(err).To(BeNil()) Expect(err).To(BeNil())
queue, err := connection.Management().DeclareQueue(context.Background(), &QuorumQueueSpecification{ queue, err := connection.Management().DeclareQueue(context.Background(), &QuorumQueueSpecification{
Name: qName, Name: qName,
@ -81,7 +81,6 @@ var _ = Describe("NewConsumer tests", func() {
Expect(consumer.Close(context.Background())).To(BeNil()) Expect(consumer.Close(context.Background())).To(BeNil())
Expect(err).To(BeNil()) Expect(err).To(BeNil())
nMessages, err := connection.Management().PurgeQueue(context.Background(), qName) nMessages, err := connection.Management().PurgeQueue(context.Background(), qName)
Expect(err).To(BeNil())
Expect(nMessages).To(Equal(1)) Expect(nMessages).To(Equal(1))
Expect(connection.Management().DeleteQueue(context.Background(), qName)).To(BeNil()) Expect(connection.Management().DeleteQueue(context.Background(), qName)).To(BeNil())
Expect(connection.Close(context.Background())).To(BeNil()) Expect(connection.Close(context.Background())).To(BeNil())
@ -90,7 +89,7 @@ var _ = Describe("NewConsumer tests", func() {
It("AMQP NewConsumer should requeue the message to the queue with annotations", 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") qName := generateNameWithDateTime("AMQP NewConsumer should requeue the message to the queue with annotations")
connection, err := Dial(context.Background(), "amqp://", nil) connection, err := Dial(context.Background(), []string{"amqp://"}, nil)
Expect(err).To(BeNil()) Expect(err).To(BeNil())
queue, err := connection.Management().DeclareQueue(context.Background(), &QuorumQueueSpecification{ queue, err := connection.Management().DeclareQueue(context.Background(), &QuorumQueueSpecification{
Name: qName, Name: qName,
@ -117,7 +116,6 @@ var _ = Describe("NewConsumer tests", func() {
Expect(consumer.Close(context.Background())).To(BeNil()) Expect(consumer.Close(context.Background())).To(BeNil())
Expect(err).To(BeNil()) Expect(err).To(BeNil())
nMessages, err := connection.Management().PurgeQueue(context.Background(), qName) nMessages, err := connection.Management().PurgeQueue(context.Background(), qName)
Expect(err).To(BeNil())
Expect(nMessages).To(Equal(1)) Expect(nMessages).To(Equal(1))
Expect(connection.Management().DeleteQueue(context.Background(), qName)).To(BeNil()) Expect(connection.Management().DeleteQueue(context.Background(), qName)).To(BeNil())
Expect(connection.Close(context.Background())).To(BeNil()) Expect(connection.Close(context.Background())).To(BeNil())
@ -126,7 +124,7 @@ var _ = Describe("NewConsumer tests", func() {
It("AMQP NewConsumer should discard the message to the queue with and without annotations", 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 // 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") qName := generateNameWithDateTime("AMQP NewConsumer should discard the message to the queue with and without annotations")
connection, err := Dial(context.Background(), "amqp://", nil) connection, err := Dial(context.Background(), []string{"amqp://"}, nil)
Expect(err).To(BeNil()) Expect(err).To(BeNil())
queue, err := connection.Management().DeclareQueue(context.Background(), &QuorumQueueSpecification{ queue, err := connection.Management().DeclareQueue(context.Background(), &QuorumQueueSpecification{
Name: qName, Name: qName,
@ -155,7 +153,6 @@ var _ = Describe("NewConsumer tests", func() {
Info: nil, Info: nil,
})).To(BeNil()) })).To(BeNil())
nMessages, err := connection.Management().PurgeQueue(context.Background(), qName) nMessages, err := connection.Management().PurgeQueue(context.Background(), qName)
Expect(err).To(BeNil())
Expect(nMessages).To(Equal(0)) Expect(nMessages).To(Equal(0))
Expect(consumer.Close(context.Background())).To(BeNil()) Expect(consumer.Close(context.Background())).To(BeNil())
Expect(connection.Management().DeleteQueue(context.Background(), qName)).To(BeNil()) Expect(connection.Management().DeleteQueue(context.Background(), qName)).To(BeNil())

View File

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

View File

@ -2,14 +2,13 @@ package rabbitmqamqp
import ( import (
"context" "context"
"github.com/Azure/go-amqp"
. "github.com/onsi/ginkgo/v2" . "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega" . "github.com/onsi/gomega"
) )
var _ = Describe("AMQP Environment Test", func() { var _ = Describe("AMQP Environment Test", func() {
It("AMQP Environment connection should succeed", func() { It("AMQP Environment connection should succeed", func() {
env := NewClusterEnvironment([]Endpoint{{Address: "amqp://"}}) env := NewEnvironment([]string{"amqp://"}, nil)
Expect(env).NotTo(BeNil()) Expect(env).NotTo(BeNil())
Expect(env.Connections()).NotTo(BeNil()) Expect(env.Connections()).NotTo(BeNil())
Expect(len(env.Connections())).To(Equal(0)) Expect(len(env.Connections())).To(Equal(0))
@ -23,7 +22,7 @@ var _ = Describe("AMQP Environment Test", func() {
}) })
It("AMQP Environment CloseConnections should remove all the elements form the list", func() { It("AMQP Environment CloseConnections should remove all the elements form the list", func() {
env := NewClusterEnvironment([]Endpoint{{Address: "amqp://"}}) env := NewEnvironment([]string{"amqp://"}, nil)
Expect(env).NotTo(BeNil()) Expect(env).NotTo(BeNil())
Expect(env.Connections()).NotTo(BeNil()) Expect(env.Connections()).NotTo(BeNil())
Expect(len(env.Connections())).To(Equal(0)) Expect(len(env.Connections())).To(Equal(0))
@ -37,83 +36,22 @@ var _ = Describe("AMQP Environment Test", func() {
Expect(len(env.Connections())).To(Equal(0)) Expect(len(env.Connections())).To(Equal(0))
}) })
It("Get new connection should connect to the one correct uri and fails the others", func() { It("AMQP Environment connection ID should be unique", func() {
env := NewEnvironment([]string{"amqp://"}, nil)
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).NotTo(BeNil())
Expect(env.Connections()).NotTo(BeNil()) Expect(env.Connections()).NotTo(BeNil())
Expect(len(env.Connections())).To(Equal(0)) Expect(len(env.Connections())).To(Equal(0))
conn, err := env.NewConnection(context.Background()) connection, err := env.NewConnection(context.Background(), "myConnectionId")
Expect(err).To(BeNil()) Expect(err).To(BeNil())
Expect(conn.Close(context.Background())) Expect(connection).NotTo(BeNil())
}) Expect(len(env.Connections())).To(Equal(1))
connectionShouldBeNil, err := env.NewConnection(context.Background(), "myConnectionId")
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()) Expect(err).NotTo(BeNil())
}) Expect(err.Error()).To(ContainSubstring("connection with id myConnectionId already exists"))
Expect(connectionShouldBeNil).To(BeNil())
Describe("Environment strategy", func() { Expect(len(env.Connections())).To(Equal(1))
DescribeTable("Environment with strategy should success", func(strategy TEndPointStrategy) { Expect(connection.Close(context.Background())).To(BeNil())
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)) 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 connection *AmqpConnection
var management *AmqpManagement var management *AmqpManagement
BeforeEach(func() { BeforeEach(func() {
conn, err := Dial(context.TODO(), "amqp://", nil) conn, err := Dial(context.TODO(), []string{"amqp://"}, nil)
connection = conn connection = conn
Expect(err).To(BeNil()) Expect(err).To(BeNil())
management = connection.Management() management = connection.Management()

View File

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

View File

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

View File

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

View File

@ -10,7 +10,7 @@ import (
var _ = Describe("AMQP publisher ", func() { var _ = Describe("AMQP publisher ", func() {
It("Send a message to a queue with a Message Target NewPublisher", 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") qName := generateNameWithDateTime("Send a message to a queue with a Message Target NewPublisher")
connection, err := Dial(context.Background(), "amqp://", nil) connection, err := Dial(context.Background(), []string{"amqp://"}, nil)
Expect(err).To(BeNil()) Expect(err).To(BeNil())
Expect(connection).NotTo(BeNil()) Expect(connection).NotTo(BeNil())
queueInfo, err := connection.Management().DeclareQueue(context.Background(), &QuorumQueueSpecification{ queueInfo, err := connection.Management().DeclareQueue(context.Background(), &QuorumQueueSpecification{
@ -18,7 +18,7 @@ var _ = Describe("AMQP publisher ", func() {
}) })
Expect(err).To(BeNil()) Expect(err).To(BeNil())
Expect(queueInfo).NotTo(BeNil()) Expect(queueInfo).NotTo(BeNil())
publisher, err := connection.NewPublisher(context.Background(), &QueueAddress{Queue: qName}, nil) publisher, err := connection.NewPublisher(context.Background(), &QueueAddress{Queue: qName}, "test")
Expect(err).To(BeNil()) Expect(err).To(BeNil())
Expect(publisher).NotTo(BeNil()) Expect(publisher).NotTo(BeNil())
Expect(publisher).To(BeAssignableToTypeOf(&Publisher{})) Expect(publisher).To(BeAssignableToTypeOf(&Publisher{}))
@ -36,11 +36,11 @@ var _ = Describe("AMQP publisher ", func() {
}) })
It("NewPublisher should fail to a not existing exchange", func() { It("NewPublisher should fail to a not existing exchange", func() {
connection, err := Dial(context.Background(), "amqp://", nil) connection, err := Dial(context.Background(), []string{"amqp://"}, nil)
Expect(err).To(BeNil()) Expect(err).To(BeNil())
Expect(connection).NotTo(BeNil()) Expect(connection).NotTo(BeNil())
exchangeName := "Nope" exchangeName := "Nope"
publisher, err := connection.NewPublisher(context.Background(), &ExchangeAddress{Exchange: exchangeName}, nil) publisher, err := connection.NewPublisher(context.Background(), &ExchangeAddress{Exchange: exchangeName}, "test")
Expect(err).NotTo(BeNil()) Expect(err).NotTo(BeNil())
Expect(publisher).To(BeNil()) Expect(publisher).To(BeNil())
Expect(connection.Close(context.Background())).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() { It("publishResult should released to a not existing routing key", func() {
eName := generateNameWithDateTime("publishResult should released to a not existing routing key") eName := generateNameWithDateTime("publishResult should released to a not existing routing key")
connection, err := Dial(context.Background(), "amqp://", nil) connection, err := Dial(context.Background(), []string{"amqp://"}, nil)
Expect(err).To(BeNil()) Expect(err).To(BeNil())
Expect(connection).NotTo(BeNil()) Expect(connection).NotTo(BeNil())
exchange, err := connection.Management().DeclareExchange(context.Background(), &TopicExchangeSpecification{ exchange, err := connection.Management().DeclareExchange(context.Background(), &TopicExchangeSpecification{
@ -62,7 +62,7 @@ var _ = Describe("AMQP publisher ", func() {
publisher, err := connection.NewPublisher(context.Background(), &ExchangeAddress{ publisher, err := connection.NewPublisher(context.Background(), &ExchangeAddress{
Exchange: eName, Exchange: eName,
Key: routingKeyNope, Key: routingKeyNope,
}, nil) }, "test")
Expect(err).To(BeNil()) Expect(err).To(BeNil())
Expect(publisher).NotTo(BeNil()) Expect(publisher).NotTo(BeNil())
publishResult, err := publisher.Publish(context.Background(), NewMessage([]byte("hello"))) 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() { It("Send a message to a deleted queue should fail", func() {
qName := generateNameWithDateTime("Send a message to a deleted queue should fail") qName := generateNameWithDateTime("Send a message to a deleted queue should fail")
connection, err := Dial(context.Background(), "amqp://", nil) connection, err := Dial(context.Background(), []string{"amqp://"}, nil)
Expect(err).To(BeNil()) Expect(err).To(BeNil())
Expect(connection).NotTo(BeNil()) Expect(connection).NotTo(BeNil())
_, err = connection.Management().DeclareQueue(context.Background(), &QuorumQueueSpecification{ _, err = connection.Management().DeclareQueue(context.Background(), &QuorumQueueSpecification{
Name: qName, Name: qName,
}) })
Expect(err).To(BeNil()) Expect(err).To(BeNil())
publisher, err := connection.NewPublisher(context.Background(), &QueueAddress{Queue: qName}, nil) publisher, err := connection.NewPublisher(context.Background(), &QueueAddress{Queue: qName}, "test")
Expect(err).To(BeNil()) Expect(err).To(BeNil())
Expect(publisher).NotTo(BeNil()) Expect(publisher).NotTo(BeNil())
publishResult, err := publisher.Publish(context.Background(), NewMessage([]byte("hello"))) publishResult, err := publisher.Publish(context.Background(), NewMessage([]byte("hello")))
@ -91,16 +91,15 @@ var _ = Describe("AMQP publisher ", func() {
err = connection.management.DeleteQueue(context.Background(), qName) err = connection.management.DeleteQueue(context.Background(), qName)
Expect(err).To(BeNil()) Expect(err).To(BeNil())
publishResult, err = publisher.Publish(context.Background(), NewMessage([]byte("hello"))) publishResult, err = publisher.Publish(context.Background(), NewMessage([]byte("hello")))
Expect(publishResult).To(BeNil())
Expect(err).NotTo(BeNil()) Expect(err).NotTo(BeNil())
Expect(connection.Close(context.Background())) Expect(connection.Close(context.Background()))
}) })
It("Multi Targets NewPublisher should fail with StateReleased when the destination does not exist", func() { It("Multi Targets NewPublisher should fail with StateReleased when the destination does not exist", func() {
connection, err := Dial(context.Background(), "amqp://", nil) connection, err := Dial(context.Background(), []string{"amqp://"}, nil)
Expect(err).To(BeNil()) Expect(err).To(BeNil())
Expect(connection).NotTo(BeNil()) Expect(connection).NotTo(BeNil())
publisher, err := connection.NewPublisher(context.Background(), nil, nil) publisher, err := connection.NewPublisher(context.Background(), nil, "test")
Expect(err).To(BeNil()) Expect(err).To(BeNil())
Expect(publisher).NotTo(BeNil()) Expect(publisher).NotTo(BeNil())
qName := generateNameWithDateTime("Targets NewPublisher should fail when the destination does not exist") qName := generateNameWithDateTime("Targets NewPublisher should fail when the destination does not exist")
@ -121,11 +120,11 @@ var _ = Describe("AMQP publisher ", func() {
}) })
It("Multi Targets NewPublisher should success with StateReceived when the destination exists", func() { It("Multi Targets NewPublisher should success with StateReceived when the destination exists", func() {
connection, err := Dial(context.Background(), "amqp://", nil) connection, err := Dial(context.Background(), []string{"amqp://"}, nil)
Expect(err).To(BeNil()) Expect(err).To(BeNil())
Expect(connection).NotTo(BeNil()) Expect(connection).NotTo(BeNil())
Expect(err).To(BeNil()) Expect(err).To(BeNil())
publisher, err := connection.NewPublisher(context.Background(), nil, nil) publisher, err := connection.NewPublisher(context.Background(), nil, "test")
Expect(err).To(BeNil()) Expect(err).To(BeNil())
Expect(publisher).NotTo(BeNil()) Expect(publisher).NotTo(BeNil())
name := generateNameWithDateTime("Targets NewPublisher should success with StateReceived when the destination exists") name := generateNameWithDateTime("Targets NewPublisher should success with StateReceived when the destination exists")
@ -147,8 +146,6 @@ var _ = Describe("AMQP publisher ", func() {
Name: name, Name: name,
IsAutoDelete: false, IsAutoDelete: false,
}) })
Expect(err).To(BeNil())
msg = NewMessage([]byte("hello")) msg = NewMessage([]byte("hello"))
Expect(MessagePropertyToAddress(msg, &ExchangeAddress{Exchange: name})).To(BeNil()) Expect(MessagePropertyToAddress(msg, &ExchangeAddress{Exchange: name})).To(BeNil())
// the status should be StateReleased since the exchange does not have any binding // the status should be StateReleased since the exchange does not have any binding
@ -177,10 +174,10 @@ var _ = Describe("AMQP publisher ", func() {
}) })
It("Multi Targets NewPublisher should fail it TO is not set or not valid", func() { It("Multi Targets NewPublisher should fail it TO is not set or not valid", func() {
connection, err := Dial(context.Background(), "amqp://", nil) connection, err := Dial(context.Background(), []string{"amqp://"}, nil)
Expect(err).To(BeNil()) Expect(err).To(BeNil())
Expect(connection).NotTo(BeNil()) Expect(connection).NotTo(BeNil())
publisher, err := connection.NewPublisher(context.Background(), nil, nil) publisher, err := connection.NewPublisher(context.Background(), nil, "test")
Expect(err).To(BeNil()) Expect(err).To(BeNil())
Expect(publisher).NotTo(BeNil()) Expect(publisher).NotTo(BeNil())
msg := NewMessage([]byte("hello")) msg := NewMessage([]byte("hello"))
@ -203,65 +200,4 @@ var _ = Describe("AMQP publisher ", func() {
Expect(connection.Close(context.Background())).To(BeNil()) 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

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

View File

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

View File

@ -1,7 +1,6 @@
package rabbitmqamqp package rabbitmqamqp
import ( import (
"fmt"
"github.com/Azure/go-amqp" "github.com/Azure/go-amqp"
"github.com/google/uuid" "github.com/google/uuid"
) )
@ -14,20 +13,20 @@ type StateRejected = amqp.StateRejected
type StateReleased = amqp.StateReleased type StateReleased = amqp.StateReleased
type StateModified = amqp.StateModified type StateModified = amqp.StateModified
type iLinkerName interface { type linkerName interface {
linkName() string linkName() string
} }
func getLinkName(l iLinkerName) string { func getLinkName(l linkerName) string {
if l == nil || l.linkName() == "" { if l == nil || l.linkName() == "" {
return uuid.New().String() return uuid.New().String()
} }
return l.linkName() return l.linkName()
} }
/// IConsumerOptions interface for the AMQP and Stream consumer/// /// ConsumerOptions interface for the AMQP and Stream consumer///
type IConsumerOptions interface { type ConsumerOptions interface {
// linkName returns the name of the link // linkName returns the name of the link
// if not set it will return a random UUID // if not set it will return a random UUID
linkName() string linkName() string
@ -38,22 +37,16 @@ type IConsumerOptions interface {
// linkFilters returns the link filters for the link. // linkFilters returns the link filters for the link.
// It is mostly used for the stream consumers. // It is mostly used for the stream consumers.
linkFilters() []amqp.LinkFilter 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 IConsumerOptions) int32 { func getInitialCredits(co ConsumerOptions) int32 {
if co == nil || co.initialCredits() == 0 { if co == nil || co.initialCredits() == 0 {
return 256 return 256
} }
return co.initialCredits() return co.initialCredits()
} }
func getLinkFilters(co IConsumerOptions) []amqp.LinkFilter { func getLinkFilters(co ConsumerOptions) []amqp.LinkFilter {
if co == nil { if co == nil {
return nil return nil
} }
@ -76,54 +69,32 @@ func (mo *managementOptions) linkFilters() []amqp.LinkFilter {
return nil return nil
} }
func (mo *managementOptions) id() string { type AMQPConsumerOptions struct {
return "management" //ReceiverLinkName: see the ConsumerOptions interface
}
func (mo *managementOptions) validate(available *featuresAvailable) error {
return nil
}
// ConsumerOptions represents the options for quorum and classic queues
type ConsumerOptions struct {
//ReceiverLinkName: see the IConsumerOptions interface
ReceiverLinkName string ReceiverLinkName string
//InitialCredits: see the IConsumerOptions interface //InitialCredits: see the ConsumerOptions interface
InitialCredits int32 InitialCredits int32
// The id of the consumer
Id string
} }
func (aco *ConsumerOptions) linkName() string { func (aco *AMQPConsumerOptions) linkName() string {
return aco.ReceiverLinkName return aco.ReceiverLinkName
} }
func (aco *ConsumerOptions) initialCredits() int32 { func (aco *AMQPConsumerOptions) initialCredits() int32 {
return aco.InitialCredits return aco.InitialCredits
} }
func (aco *ConsumerOptions) linkFilters() []amqp.LinkFilter { func (aco *AMQPConsumerOptions) linkFilters() []amqp.LinkFilter {
return nil return nil
} }
func (aco *ConsumerOptions) id() string { type OffsetSpecification interface {
return aco.Id
}
func (aco *ConsumerOptions) validate(available *featuresAvailable) error {
return nil
}
type IOffsetSpecification interface {
toLinkFilter() amqp.LinkFilter toLinkFilter() amqp.LinkFilter
} }
const rmqStreamFilter = "rabbitmq:stream-filter" const rmqStreamFilter = "rabbitmq:stream-filter"
const rmqStreamOffsetSpec = "rabbitmq:stream-offset-spec" const rmqStreamOffsetSpec = "rabbitmq:stream-offset-spec"
const rmqStreamMatchUnfiltered = "rabbitmq:stream-match-unfiltered" const rmqStreamMatchUnfiltered = "rabbitmq:stream-match-unfiltered"
const amqpApplicationPropertiesFilter = "amqp:application-properties-filter"
const amqpPropertiesFilter = "amqp:properties-filter"
const offsetFirst = "first" const offsetFirst = "first"
const offsetNext = "next" const offsetNext = "next"
const offsetLast = "last" const offsetLast = "last"
@ -157,37 +128,23 @@ func (on *OffsetNext) toLinkFilter() amqp.LinkFilter {
return amqp.NewLinkFilter(rmqStreamOffsetSpec, 0, offsetNext) 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 for stream queues StreamConsumerOptions represents the options that can be used to create a stream consumer.
It is mandatory in case of creating a stream consumer. It is mandatory in case of creating a stream consumer.
*/ */
type StreamConsumerOptions struct { type StreamConsumerOptions struct {
//ReceiverLinkName: see the IConsumerOptions interface //ReceiverLinkName: see the ConsumerOptions interface
ReceiverLinkName string ReceiverLinkName string
//InitialCredits: see the IConsumerOptions interface //InitialCredits: see the ConsumerOptions interface
InitialCredits int32 InitialCredits int32
// The offset specification for the stream consumer // The offset specification for the stream consumer
// see the interface implementations // see the interface implementations
Offset IOffsetSpecification Offset OffsetSpecification
StreamFilterOptions *StreamFilterOptions // Filter values.
// See: https://www.rabbitmq.com/blog/2024/12/13/amqp-filter-expressions for more details
Id string Filters []string
//
FilterMatchUnfiltered bool
} }
func (sco *StreamConsumerOptions) linkName() string { func (sco *StreamConsumerOptions) linkName() string {
@ -200,117 +157,29 @@ func (sco *StreamConsumerOptions) initialCredits() int32 {
func (sco *StreamConsumerOptions) linkFilters() []amqp.LinkFilter { func (sco *StreamConsumerOptions) linkFilters() []amqp.LinkFilter {
var filters []amqp.LinkFilter var filters []amqp.LinkFilter
filters = append(filters, sco.Offset.toLinkFilter()) filters = append(filters, sco.Offset.toLinkFilter())
if sco.StreamFilterOptions != nil && sco.StreamFilterOptions.Values != nil { if sco.Filters != nil {
var l []any l := []any{}
for _, f := range sco.StreamFilterOptions.Values { for _, f := range sco.Filters {
l = append(l, f) l = append(l, f)
} }
filters = append(filters, amqp.NewLinkFilter(rmqStreamFilter, 0, l)) filters = append(filters, amqp.NewLinkFilter(rmqStreamFilter, 0, l))
filters = append(filters, amqp.NewLinkFilter(rmqStreamMatchUnfiltered, 0, sco.StreamFilterOptions.MatchUnfiltered)) filters = append(filters, amqp.NewLinkFilter(rmqStreamMatchUnfiltered, 0, sco.FilterMatchUnfiltered))
} }
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 return filters
} }
func (sco *StreamConsumerOptions) id() string { ///// ProducerOptions /////
return sco.Id
}
func (sco *StreamConsumerOptions) validate(available *featuresAvailable) error { type ProducerOptions interface {
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 linkName() string
id() string
} }
type PublisherOptions struct { type AMQPProducerOptions struct {
Id string
SenderLinkName string SenderLinkName string
} }
func (apo *PublisherOptions) linkName() string { func (apo *AMQPProducerOptions) linkName() string {
return apo.SenderLinkName 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 // receiverLinkOptions returns the options for a receiver link
// with the given address and link name. // with the given address and link name.
// That should be the same for all the links. // That should be the same for all the links.
func createReceiverLinkOptions(address string, options IConsumerOptions, deliveryMode int) *amqp.ReceiverOptions { func createReceiverLinkOptions(address string, options ConsumerOptions, deliveryMode int) *amqp.ReceiverOptions {
prop := make(map[string]any) prop := make(map[string]any)
prop["paired"] = true prop["paired"] = true
receiverSettleMode := amqp.SenderSettleModeSettled.Ptr() receiverSettleMode := amqp.SenderSettleModeSettled.Ptr()
@ -74,7 +74,7 @@ func random(max int) int {
} }
func validateMessageAnnotations(annotations amqp.Annotations) error { func validateMessageAnnotations(annotations amqp.Annotations) error {
for k := range annotations { for k, _ := range annotations {
switch tp := k.(type) { switch tp := k.(type) {
case string: case string:
if err := validateMessageAnnotationKey(tp); err != nil { if err := validateMessageAnnotationKey(tp); err != nil {

View File

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

View File

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

View File

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

View File

@ -2,7 +2,6 @@ package rabbitmqamqp
import ( import (
"fmt" "fmt"
"github.com/Azure/go-amqp"
. "github.com/onsi/ginkgo/v2" . "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega" . "github.com/onsi/gomega"
) )
@ -109,24 +108,6 @@ var _ = Describe("Available Features", func() {
Expect(availableFeatures.is4OrMore).To(BeTrue()) Expect(availableFeatures.is4OrMore).To(BeTrue())
Expect(availableFeatures.is41OrMore).To(BeTrue()) Expect(availableFeatures.is41OrMore).To(BeTrue())
Expect(availableFeatures.isRabbitMQ).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" "sync"
) )
type ILifeCycleState interface { type LifeCycleState interface {
getState() int getState() int
} }
@ -49,7 +49,7 @@ const (
closed = iota closed = iota
) )
func statusToString(status ILifeCycleState) string { func statusToString(status LifeCycleState) string {
switch status.getState() { switch status.getState() {
case open: case open:
return "open" return "open"
@ -65,8 +65,8 @@ func statusToString(status ILifeCycleState) string {
} }
type StateChanged struct { type StateChanged struct {
From ILifeCycleState From LifeCycleState
To ILifeCycleState To LifeCycleState
} }
func (s StateChanged) String() string { func (s StateChanged) String() string {
@ -77,9 +77,6 @@ func (s StateChanged) String() string {
switch s.To.(type) { switch s.To.(type) {
case *StateClosed: 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) return fmt.Sprintf("From: %s, To: %s, Error: %s", statusToString(s.From), statusToString(s.To), s.To.(*StateClosed).error)
} }
@ -88,7 +85,7 @@ func (s StateChanged) String() string {
} }
type LifeCycle struct { type LifeCycle struct {
state ILifeCycleState state LifeCycleState
chStatusChanged chan *StateChanged chStatusChanged chan *StateChanged
mutex *sync.Mutex mutex *sync.Mutex
} }
@ -100,13 +97,13 @@ func NewLifeCycle() *LifeCycle {
} }
} }
func (l *LifeCycle) State() ILifeCycleState { func (l *LifeCycle) State() LifeCycleState {
l.mutex.Lock() l.mutex.Lock()
defer l.mutex.Unlock() defer l.mutex.Unlock()
return l.state return l.state
} }
func (l *LifeCycle) SetState(value ILifeCycleState) { func (l *LifeCycle) SetState(value LifeCycleState) {
l.mutex.Lock() l.mutex.Lock()
defer l.mutex.Unlock() defer l.mutex.Unlock()
if l.state == value { if l.state == value {
@ -125,9 +122,3 @@ func (l *LifeCycle) SetState(value ILifeCycleState) {
To: value, 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. // MessagePropertyToAddress sets the To property of the message to the address of the target.
// The target must be a QueueAddress or an ExchangeAddress. // The target must be a QueueAddress or an ExchangeAddress.
// Note: The field msgRef.Properties.To will be overwritten if it is already set. // Note: The field msgRef.Properties.To will be overwritten if it is already set.
func MessagePropertyToAddress(msgRef *amqp.Message, target ITargetAddress) error { func MessagePropertyToAddress(msgRef *amqp.Message, target TargetAddress) error {
if target == nil { if target == nil {
return errors.New("target cannot be 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. // 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. // The target must be a QueueAddress or an ExchangeAddress.
// This function is a helper that combines NewMessage and MessagePropertyToAddress. // This function is a helper that combines NewMessage and MessagePropertyToAddress.
func NewMessageWithAddress(body []byte, target ITargetAddress) (*amqp.Message, error) { func NewMessageWithAddress(body []byte, target TargetAddress) (*amqp.Message, error) {
message := amqp.NewMessage(body) message := amqp.NewMessage(body)
err := MessagePropertyToAddress(message, target) err := MessagePropertyToAddress(message, target)
if err != nil { if err != nil {

View File

@ -1,207 +0,0 @@
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,30 +8,5 @@ import (
func generateNameWithDateTime(name string) string { func generateNameWithDateTime(name string) string {
return fmt.Sprintf("%s_%s", name, strconv.FormatInt(time.Now().Unix(), 10)) 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,6 +62,7 @@ func DropConnectionContainerID(Id string) error {
if err != nil { if err != nil {
return err return err
} }
return nil return nil
} }
@ -73,7 +74,6 @@ func DropConnection(name string, port string) error {
return nil return nil
} }
func httpGet(url, username, password string) (string, error) { func httpGet(url, username, password string) (string, error) {
return baseCall(url, username, password, "GET") return baseCall(url, username, password, "GET")
} }
@ -106,11 +106,6 @@ func baseCall(url, username, password string, method string) (string, error) {
return string(bodyBytes), nil return string(bodyBytes), nil
} }
if resp.StatusCode == 201 {
// Created! it is ok
return "", nil
}
if resp.StatusCode == 204 { // No Content if resp.StatusCode == 204 { // No Content
return "", nil return "", nil
} }