Compare commits

...

10 Commits

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

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

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

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

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

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

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

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

---------

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

View File

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

View File

@ -1,30 +1,28 @@
-----BEGIN ENCRYPTED PRIVATE KEY----- -----BEGIN PRIVATE KEY-----
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 eT5xfQPC5smWZlI0l0JK1bs=
orqH4SABIAbYTwNOb7k/wOc4EfucawBqMG4g+29qewD67+EXjB0GadqOXRoQyhRq -----END PRIVATE KEY-----
hd74uUK5gzJOqStqiowQ0A==
-----END ENCRYPTED PRIVATE KEY-----

Binary file not shown.

View File

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

View File

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

Binary file not shown.

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -7,13 +7,7 @@ set -o xtrace
script_dir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" 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'
@ -91,6 +85,8 @@ 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"
@ -163,8 +159,7 @@ 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,10 +17,14 @@ 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,3 +1,10 @@
# Get the currently used golang install path (in GOPATH/bin, unless GOBIN is set)
ifeq (,$(shell go env GOBIN))
GOBIN = $(shell go env GOPATH)/bin
else
GOBIN = $(shell go env GOBIN)
endif
all: test all: test
format: format:
@ -5,8 +12,21 @@ format:
vet: vet:
go vet ./pkg/rabbitmqamqp go vet ./pkg/rabbitmqamqp
go vet ./docs/examples/...
test: format vet STATICCHECK ?= $(GOBIN)/staticcheck
STATICCHECK_VERSION ?= latest
$(STATICCHECK):
go install honnef.co/go/tools/cmd/staticcheck@$(STATICCHECK_VERSION)
check: $(STATICCHECK)
$(STATICCHECK) ./pkg/rabbitmqamqp
$(STATICCHECK) ./docs/examples/...
test: format vet check
cd ./pkg/rabbitmqamqp && go run -mod=mod github.com/onsi/ginkgo/v2/ginkgo \ 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 \
@ -14,8 +34,8 @@ test: format vet
rabbitmq-server-start-arm: rabbitmq-server-start:
./.ci/ubuntu/gha-setup.sh start pull arm ./.ci/ubuntu/gha-setup.sh start pull
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. This library is meant to be used with RabbitMQ 4.0. (2025.06.26)
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 @@ Suitable for testing in pre-production environments.
## Documentation ## Documentation
- [Client Guide](https://www.rabbitmq.com/client-libraries/amqp-client-libraries) (work in progress for this client) - [Client Guide](https://www.rabbitmq.com/client-libraries/amqp-client-libraries)

View File

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

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

View File

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

View File

@ -15,7 +15,7 @@ func main() {
rmq.Info("Getting started with AMQP Go AMQP 1.0 Client") rmq.Info("Getting started with AMQP Go AMQP 1.0 Client")
/// Create a channel to receive state change notifications /// Create a channel to receive connection state change notifications
stateChanged := make(chan *rmq.StateChanged, 1) 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,10 +23,16 @@ func main() {
} }
}(stateChanged) }(stateChanged)
// rmq.NewEnvironment setups the environment. // rmq.NewClusterEnvironment 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([]string{"amqp://"}, nil) env := rmq.NewEnvironment("amqp://guest:guest@localhost:5672/", nil)
// in case you have multiple endpoints you can use the following:
//env := rmq.NewClusterEnvironment([]rmq.Endpoint{
// {Address: "amqp://server1", Options: &rmq.AmqpConnOptions{}},
// {Address: "amqp://server2", Options: &rmq.AmqpConnOptions{}},
//})
// Open a connection to the AMQP 1.0 server ( RabbitMQ >= 4.0) // Open a connection to the AMQP 1.0 server ( RabbitMQ >= 4.0)
amqpConnection, err := env.NewConnection(context.Background()) amqpConnection, err := env.NewConnection(context.Background())
@ -74,7 +80,6 @@ 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)
@ -89,16 +94,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("[NewConsumer]", "consumer closed. Context", err) rmq.Info("[Consumer]", "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("[NewConsumer]", "Error receiving message", err) rmq.Error("[Consumer]", "Error receiving message", err)
return return
} }
rmq.Info("[NewConsumer]", "Received message", rmq.Info("[Consumer]", "Received message",
fmt.Sprintf("%s", deliveryContext.Message().Data)) fmt.Sprintf("%s", deliveryContext.Message().Data))
err = deliveryContext.Accept(context.Background()) err = deliveryContext.Accept(context.Background())
@ -112,7 +117,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,
}, "getting-started-publisher") }, nil)
if err != nil { if err != nil {
rmq.Error("Error creating publisher", err) rmq.Error("Error creating publisher", err)
return return
@ -128,18 +133,15 @@ func main() {
} }
switch publishResult.Outcome.(type) { switch publishResult.Outcome.(type) {
case *rmq.StateAccepted: case *rmq.StateAccepted:
rmq.Info("[NewPublisher]", "Message accepted", publishResult.Message.Data[0]) rmq.Info("[Publisher]", "Message accepted", publishResult.Message.Data[0])
break
case *rmq.StateReleased: case *rmq.StateReleased:
rmq.Warn("[NewPublisher]", "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("[NewPublisher]", "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("[NewPublisher]", "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
@ -156,13 +158,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("[NewConsumer]", err) rmq.Error("[Consumer]", 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("[NewPublisher]", err) rmq.Error("[Publisher]", err)
return return
} }

View File

@ -17,8 +17,7 @@ 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"}
@ -31,7 +30,7 @@ func main() {
} }
// create a publisher without a target // create a publisher without a target
publisher, err := amqpConnection.NewPublisher(context.TODO(), nil, "stream-publisher") publisher, err := amqpConnection.NewPublisher(context.TODO(), nil, nil)
checkError(err) checkError(err)
// publish messages to the stream // publish messages to the stream
@ -55,7 +54,6 @@ 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,18 +16,19 @@ 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 { for isRunning {
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)
} }
}() }()
@ -41,12 +42,22 @@ 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(), []string{"amqp://"}, &rmq.AmqpConnOptions{ amqpConnection, err := rmq.Dial(context.Background(), "amqp://", &rmq.AmqpConnOptions{
SASLType: amqp.SASLTypeAnonymous(), SASLType: amqp.SASLTypeAnonymous(),
ContainerID: "reliable-amqp10-go", ContainerID: "reliable-amqp10-go",
RecoveryConfiguration: &rmq.RecoveryConfiguration{ RecoveryConfiguration: &rmq.RecoveryConfiguration{
@ -87,13 +98,13 @@ func main() {
// Consume messages from the queue // Consume messages from the queue
go func(ctx context.Context) { go func(ctx context.Context) {
for { for isRunning {
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 { if err != nil && isRunning {
// 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()
@ -107,7 +118,7 @@ func main() {
atomic.AddInt32(&received, 1) atomic.AddInt32(&received, 1)
err = deliveryContext.Accept(context.Background()) err = deliveryContext.Accept(context.Background())
if err != nil { if err != nil && isRunning {
// same here the delivery could not be accepted due to a network error // 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)
@ -118,18 +129,19 @@ func main() {
publisher, err := amqpConnection.NewPublisher(context.Background(), &rmq.QueueAddress{ publisher, err := amqpConnection.NewPublisher(context.Background(), &rmq.QueueAddress{
Queue: queueName, Queue: queueName,
}, "reliable-publisher") }, nil)
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
@ -147,13 +159,10 @@ 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
@ -163,7 +172,6 @@ func main() {
} }
}() }()
} }
wg.Wait()
println("press any key to close the connection") println("press any key to close the connection")

View File

@ -19,7 +19,8 @@ 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([]string{"amqp://"}, nil) env := rmq.NewEnvironment("amqp://guest:guest@localhost:5672/", nil)
amqpConnection, err := env.NewConnection(context.Background()) amqpConnection, err := env.NewConnection(context.Background())
checkError(err) checkError(err)
management := amqpConnection.Management() management := amqpConnection.Management()
@ -35,7 +36,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}, "stream-publisher") publisher, err := amqpConnection.NewPublisher(context.TODO(), &rmq.QueueAddress{Queue: queueStream}, nil)
checkError(err) checkError(err)
// publish messages to the stream // publish messages to the stream
@ -47,17 +48,14 @@ 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,9 +17,10 @@ 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([]string{"amqp://"}, nil) env := rmq.NewEnvironment("amqp://guest:guest@localhost:5672/", nil)
amqpConnection, err := env.NewConnection(context.Background()) amqpConnection, err := env.NewConnection(context.Background())
checkError(err) checkError(err)
management := amqpConnection.Management() management := amqpConnection.Management()
@ -35,7 +36,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}, "stream-publisher") publisher, err := amqpConnection.NewPublisher(context.TODO(), &rmq.QueueAddress{Queue: queueStream}, nil)
checkError(err) checkError(err)
filters := []string{"MyFilter1", "MyFilter2", "MyFilter3", "MyFilter4"} filters := []string{"MyFilter1", "MyFilter2", "MyFilter3", "MyFilter4"}
@ -50,17 +51,14 @@ 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
@ -76,7 +74,25 @@ 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
Filters: []string{"MyFilter1", "MyFilter2"}, StreamFilterOptions: &rmq.StreamFilterOptions{
Values: []string{"MyFilter1", "MyFilter2"},
// it is also possible to filter by application properties or message properties
// you can create filters like:
// msg.ApplicationProperties = map[string]interface{}{"key3": "value3"}
// during the publish you can do something like:
// msg.ApplicationProperties = map[string]interface{}{"key1": "value1"}
// publisher.Publish(context.Background(), msg)
//ApplicationProperties: nil,
// or here you can filter by message properties
// like:
// msg.Properties = &amqp.MessageProperties{Subject: "MySubject"}
// during the publish you can do something like:
// msg.Properties = &amqp.MessageProperties{Subject: "MySubject"}
// publisher.Publish(context.Background(), msg)
//Properties: nil,
// see amqp_consumer_stream_test.go for more examples
},
}) })
checkError(err) checkError(err)

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

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

View File

@ -11,7 +11,7 @@ func main() {
queueName := "getting-started-go-queue" queueName := "getting-started-go-queue"
routingKey := "routing-key" routingKey := "routing-key"
env := rmq.NewEnvironment([]string{"amqp://guest:guest@localhost:5672"}, nil) env := rmq.NewEnvironment("amqp://guest:guest@localhost:5672/", nil)
// Open a connection to the AMQP 1.0 server ( RabbitMQ >= 4.0) // 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,
}, "getting-started-publisher") }, nil)
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,9 +1,10 @@
module github.com/rabbitmq/rabbitmq-amqp-go-client module github.com/rabbitmq/rabbitmq-amqp-go-client
go 1.22.0 go 1.23.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
@ -14,9 +15,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.33.0 // indirect golang.org/x/net v0.38.0 // indirect
golang.org/x/sys v0.28.0 // indirect golang.org/x/sys v0.31.0 // indirect
golang.org/x/text v0.21.0 // indirect golang.org/x/text v0.23.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,6 +8,8 @@ github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-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=
@ -22,12 +24,12 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/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.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
golang.org/x/tools v0.28.0 h1:WuB6qZ4RPCQo5aP3WdKZS7i595EdWqWR8vqJTlwTVK8= golang.org/x/tools v0.28.0 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"
) )
// TargetAddress is an interface that represents an address that can be used to send messages to. // ITargetAddress is an interface that represents an address that can be used to send messages to.
// It can be either a Queue or an Exchange with a routing key. // It can be either a Queue or an Exchange with a routing key.
type TargetAddress interface { type ITargetAddress 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/next/amqp#address-v2 // see: https://www.rabbitmq.com/docs/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 "/" + exchanges + "/" + encodePathSegments(*exchange) + "/" + encodePathSegments(*key) + urlAppend, nil return fmt.Sprintf("/%s/%s/%s%s", exchanges, encodePathSegments(*exchange), encodePathSegments(*key), urlAppend), nil
} }
return "/" + exchanges + "/" + encodePathSegments(*exchange) + urlAppend, nil return fmt.Sprintf("/%s/%s%s", exchanges, encodePathSegments(*exchange), urlAppend), nil
} }
if queue == nil { if queue == nil {
@ -73,8 +73,7 @@ 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,59 +6,57 @@ import (
) )
var _ = Describe("address builder test ", func() { var _ = Describe("address builder test ", func() {
It("With exchange, queue and key should raise and error", func() { // Error cases
queue := "my_queue" Describe("Error cases", func() {
exchange := "my_exchange" DescribeTable("should return appropriate errors",
func(exchange, key, queue *string, expectedErr string) {
_, err := address(&exchange, nil, &queue, nil) _, err := address(exchange, key, queue, nil)
Expect(err).NotTo(BeNil()) Expect(err).To(HaveOccurred())
Expect(err.Error()).To(Equal("exchange and queue cannot be set together")) Expect(err.Error()).To(Equal(expectedErr))
},
Entry("when both exchange and queue are set",
stringPtr("my_exchange"), nil, stringPtr("my_queue"),
"exchange and queue cannot be set together"),
Entry("when neither exchange nor queue is set",
nil, nil, nil,
"exchange or queue must be set"),
)
}) })
It("Without exchange and queue should raise and error", func() { // Exchange-related cases
_, err := address(nil, nil, nil, nil) Describe("Exchange addresses", func() {
Expect(err).NotTo(BeNil()) DescribeTable("should generate correct exchange addresses",
Expect(err.Error()).To(Equal("exchange or queue must be set")) func(exchange, key *string, expected string) {
address, err := address(exchange, key, nil, nil)
Expect(err).NotTo(HaveOccurred())
Expect(address).To(Equal(expected))
},
Entry("with exchange and key",
stringPtr("my_exchange"), stringPtr("my_key"),
"/exchanges/my_exchange/my_key"),
Entry("with exchange only",
stringPtr("my_exchange"), nil,
"/exchanges/my_exchange"),
Entry("with special characters",
stringPtr("my_ exchange/()"), stringPtr("my_key "),
"/exchanges/my_%20exchange%2F%28%29/my_key%20"),
)
}) })
It("With exchange and key should return address", func() { // Queue-related cases
exchange := "my_exchange" Describe("Queue addresses", func() {
key := "my_key" It("should generate correct queue address with special characters", func() {
queue := "my_queue>"
address, err := address(nil, nil, &queue, nil)
Expect(err).NotTo(HaveOccurred())
Expect(address).To(Equal("/queues/my_queue%3E"))
})
address, err := address(&exchange, &key, nil, nil) It("should generate correct purge queue address", func() {
Expect(err).To(BeNil()) queue := "my_queue"
Expect(address).To(Equal("/exchanges/my_exchange/my_key")) address, err := purgeQueueAddress(&queue)
Expect(err).NotTo(HaveOccurred())
Expect(address).To(Equal("/queues/my_queue/messages"))
})
}) })
It("With exchange should return address", func() {
exchange := "my_exchange"
address, err := address(&exchange, nil, nil, nil)
Expect(err).To(BeNil())
Expect(address).To(Equal("/exchanges/my_exchange"))
})
It("With exchange and key with names to encode should return the encoded address", func() {
exchange := "my_ exchange/()"
key := "my_key "
address, err := address(&exchange, &key, nil, nil)
Expect(err).To(BeNil())
Expect(address).To(Equal("/exchanges/my_%20exchange%2F%28%29/my_key%20"))
})
It("With queue should return address", func() {
queue := "my_queue>"
address, err := address(nil, nil, &queue, nil)
Expect(err).To(BeNil())
Expect(address).To(Equal("/queues/my_queue%3E"))
})
It("With queue and urlParameters should return address", func() {
queue := "my_queue"
address, err := purgeQueueAddress(&queue)
Expect(err).To(BeNil())
Expect(address).To(Equal("/queues/my_queue/messages"))
})
}) })

View File

@ -58,8 +58,8 @@ func (b *AMQPBinding) Bind(ctx context.Context) (string, error) {
kv[destination] = b.destinationName kv[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})
bindingPathWithExchangeQueueKey := bindingPathWithExchangeQueueKey(b.toQueue, b.sourceName, b.destinationName, b.bindingKey) bindingPathWithExchangeQueueAndKey := bindingPathWithExchangeQueueKey(b.toQueue, b.sourceName, b.destinationName, b.bindingKey)
return bindingPathWithExchangeQueueKey, err return bindingPathWithExchangeQueueAndKey, 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(), []string{"amqp://"}, nil) conn, err := Dial(context.TODO(), "amqp://", nil)
Expect(err).To(BeNil()) Expect(err).To(BeNil())
connection = conn connection = conn
management = connection.Management() management = connection.Management()

View File

@ -12,18 +12,39 @@ import (
"time" "time"
) )
//func (c *ConnUrlHelper) UseSsl(value bool) { type AmqpAddress struct {
// c.UseSsl = value // the address of the AMQP server
// if value { // it is in the form of amqp://<host>:<port>
// c.Scheme = "amqps" // or amqps://<host>:<port>
// } else { // the port is optional
// c.Scheme = "amqp" // the default port is 5672
// } // the default protocol is amqp
//} // the default host is localhost
// the default virtual host is "/"
// the default user is guest
// the default password is guest
// the default SASL type is SASLTypeAnonymous
Address string
// Options: Additional options for the connection
Options *AmqpConnOptions
}
type OAuth2Options struct {
Token string
}
func (o OAuth2Options) Clone() *OAuth2Options {
cloned := &OAuth2Options{
Token: o.Token,
}
return cloned
}
type AmqpConnOptions struct { 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
@ -51,8 +72,40 @@ type AmqpConnOptions struct {
// when the connection is closed unexpectedly. // when the connection is closed unexpectedly.
RecoveryConfiguration *RecoveryConfiguration RecoveryConfiguration *RecoveryConfiguration
// copy the addresses for reconnection // The OAuth2Options is used to configure the connection with OAuth2 token.
addresses []string OAuth2Options *OAuth2Options
// Local connection identifier (not sent to the server)
// if not provided, a random UUID is generated
Id string
}
func (a *AmqpConnOptions) isOAuth2() bool {
return a.OAuth2Options != nil
}
func (a *AmqpConnOptions) Clone() *AmqpConnOptions {
cloned := &AmqpConnOptions{
ContainerID: a.ContainerID,
IdleTimeout: a.IdleTimeout,
MaxFrameSize: a.MaxFrameSize,
MaxSessions: a.MaxSessions,
Properties: a.Properties,
SASLType: a.SASLType,
TLSConfig: a.TLSConfig,
WriteTimeout: a.WriteTimeout,
Id: a.Id,
}
if a.OAuth2Options != nil {
cloned.OAuth2Options = a.OAuth2Options.Clone()
}
if a.RecoveryConfiguration != nil {
cloned.RecoveryConfiguration = a.RecoveryConfiguration.Clone()
}
return cloned
} }
type AmqpConnection struct { type AmqpConnection struct {
@ -60,10 +113,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
@ -75,9 +128,10 @@ 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 TargetAddress that can be a Queue or an Exchange with a routing key. // The destination is a ITargetAddress that can be a Queue or an Exchange with a routing key.
// options is an IPublisherOptions that can be used to configure the publisher.
// See QueueAddress and ExchangeAddress for more information. // See QueueAddress and ExchangeAddress for more information.
func (a *AmqpConnection) NewPublisher(ctx context.Context, destination TargetAddress, linkName string) (*Publisher, error) { func (a *AmqpConnection) NewPublisher(ctx context.Context, destination ITargetAddress, options IPublisherOptions) (*Publisher, error) {
destinationAdd := "" destinationAdd := ""
err := error(nil) err := error(nil)
if destination != nil { if destination != nil {
@ -91,14 +145,20 @@ func (a *AmqpConnection) NewPublisher(ctx context.Context, destination TargetAdd
} }
} }
return newPublisher(ctx, a, destinationAdd, linkName) return newPublisher(ctx, a, destinationAdd, options)
} }
// NewConsumer creates a new Consumer that listens to the provided destination. Destination is a QueueAddress. // NewConsumer creates a new Consumer that listens to the provided Queue
func (a *AmqpConnection) NewConsumer(ctx context.Context, queueName string, options ConsumerOptions) (*Consumer, error) { func (a *AmqpConnection) NewConsumer(ctx context.Context, queueName string, options IConsumerOptions) (*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 {
@ -110,16 +170,53 @@ func (a *AmqpConnection) NewConsumer(ctx context.Context, queueName string, opti
// Dial connect to the AMQP 1.0 server using the provided connectionSettings // 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.
// addresses is a list of addresses to connect to. It picks one randomly. func Dial(ctx context.Context, address string, connOptions *AmqpConnOptions) (*AmqpConnection, error) {
// It is enough that one of the addresses is reachable. connOptions, err := validateOptions(connOptions)
func Dial(ctx context.Context, addresses []string, connOptions *AmqpConnOptions, args ...string) (*AmqpConnection, error) { if err != nil {
return nil, err
}
// create the connection
conn := &AmqpConnection{
management: newAmqpManagement(),
lifeCycle: NewLifeCycle(),
amqpConnOptions: connOptions,
entitiesTracker: newEntitiesTracker(),
featuresAvailable: newFeaturesAvailable(),
}
err = conn.open(ctx, address, connOptions)
if err != nil {
return nil, err
}
conn.amqpConnOptions = connOptions
conn.address = address
conn.lifeCycle.SetState(&StateOpen{})
return conn, nil
}
func validateOptions(connOptions *AmqpConnOptions) (*AmqpConnOptions, error) {
if connOptions == nil { if connOptions == nil {
connOptions = &AmqpConnOptions{ connOptions = &AmqpConnOptions{}
// RabbitMQ requires SASL security layer }
// to be enabled for AMQP 1.0 connections. if connOptions.SASLType == nil {
// So this is mandatory and default in case not defined. // RabbitMQ requires SASL security layer
SASLType: amqp.SASLTypeAnonymous(), // to be enabled for AMQP 1.0 connections.
// So this is mandatory and default in case not defined.
connOptions.SASLType = amqp.SASLTypeAnonymous()
}
if connOptions.Id == "" {
connOptions.Id = uuid.New().String()
}
// In case of OAuth2 token, the SASLType should be set to SASLTypePlain
if connOptions.isOAuth2() {
if connOptions.OAuth2Options.Token == "" {
return nil, fmt.Errorf("OAuth2 token is empty")
} }
connOptions.SASLType = amqp.SASLTypePlain("", connOptions.OAuth2Options.Token)
} }
if connOptions.RecoveryConfiguration == nil { if connOptions.RecoveryConfiguration == nil {
@ -134,37 +231,28 @@ func Dial(ctx context.Context, addresses []string, connOptions *AmqpConnOptions,
return nil, fmt.Errorf("BackOffReconnectInterval should be greater than 1 second") return nil, fmt.Errorf("BackOffReconnectInterval should be greater than 1 second")
} }
// create the connection return connOptions, nil
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, addresses []string, connOptions *AmqpConnOptions, args ...string) error { func (a *AmqpConnection) open(ctx context.Context, address string, connOptions *AmqpConnOptions) error {
// random pick and extract one address to use for connection
var azureConnection *amqp.Conn
//connOptions.hostName is the way to set the virtual host
// so we need to pre-parse the URI to get the virtual host
// the PARSE is copied from go-amqp091 library
// the URI will be parsed is parsed again in the amqp lite library
uri, err := ParseURI(address)
if err != nil {
return err
}
amqpLiteConnOptions := &amqp.ConnOptions{ amqpLiteConnOptions := &amqp.ConnOptions{
ContainerID: connOptions.ContainerID, ContainerID: connOptions.ContainerID,
HostName: connOptions.HostName, HostName: fmt.Sprintf("vhost:%s", uri.Vhost),
IdleTimeout: connOptions.IdleTimeout, IdleTimeout: connOptions.IdleTimeout,
MaxFrameSize: connOptions.MaxFrameSize, MaxFrameSize: connOptions.MaxFrameSize,
MaxSessions: connOptions.MaxSessions, MaxSessions: connOptions.MaxSessions,
@ -173,155 +261,134 @@ func (a *AmqpConnection) open(ctx context.Context, addresses []string, connOptio
TLSConfig: connOptions.TLSConfig, TLSConfig: connOptions.TLSConfig,
WriteTimeout: connOptions.WriteTimeout, WriteTimeout: connOptions.WriteTimeout,
} }
tmp := make([]string, len(addresses)) azureConnection, err = amqp.Dial(ctx, address, amqpLiteConnOptions)
copy(tmp, addresses) if err != nil {
Error("Failed to open connection", ExtractWithoutPassword(address), err, "ID", connOptions.Id)
// random pick and extract one address to use for connection return fmt.Errorf("failed to open connection: %w", err)
var azureConnection *amqp.Conn
for len(tmp) > 0 {
idx := random(len(tmp))
addr := tmp[idx]
//connOptions.HostName is the way to set the virtual host
// so we need to pre-parse the URI to get the virtual host
// the PARSE is copied from go-amqp091 library
// the URI will be parsed is parsed again in the amqp lite library
uri, err := ParseURI(addr)
if err != nil {
return err
}
connOptions.HostName = fmt.Sprintf("vhost:%s", uri.Vhost)
// remove the index from the tmp list
tmp = append(tmp[:idx], tmp[idx+1:]...)
azureConnection, err = amqp.Dial(ctx, addr, amqpLiteConnOptions)
if err != nil {
Error("Failed to open connection", ExtractWithoutPassword(addr), err)
continue
}
a.properties = azureConnection.Properties()
err = a.featuresAvailable.ParseProperties(a.properties)
if err != nil {
Warn("Validate properties Error.", ExtractWithoutPassword(addr), err)
}
if !a.featuresAvailable.is4OrMore {
Warn("The server version is less than 4.0.0", ExtractWithoutPassword(addr))
}
if !a.featuresAvailable.isRabbitMQ {
Warn("The server is not RabbitMQ", ExtractWithoutPassword(addr))
}
Debug("Connected to", ExtractWithoutPassword(addr))
break
} }
if azureConnection == nil { a.properties = azureConnection.Properties()
return fmt.Errorf("failed to connect to any of the provided addresses") err = a.featuresAvailable.ParseProperties(a.properties)
if err != nil {
Warn("Validate properties Error.", ExtractWithoutPassword(address), err)
} }
if len(args) > 0 { if !a.featuresAvailable.is4OrMore {
a.id = args[0] Warn("The server version is less than 4.0.0", ExtractWithoutPassword(address), "ID", connOptions.Id)
} else {
a.id = uuid.New().String()
} }
if !a.featuresAvailable.isRabbitMQ {
Warn("The server is not RabbitMQ", ExtractWithoutPassword(address))
}
Debug("Connected to", ExtractWithoutPassword(address), "ID", connOptions.Id)
a.azureConnection = azureConnection a.azureConnection = azureConnection
var err error
a.session, err = a.azureConnection.NewSession(ctx, nil) a.session, err = a.azureConnection.NewSession(ctx, nil)
if err != nil {
return fmt.Errorf("failed to open session, for the connection id:%s, error: %w", a.Id(), err)
}
go func() { go func() {
select { <-azureConnection.Done()
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")
} }
Debug("connection closed successfully", "ID", a.Id())
} }
}() }()
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") Info("Recovery is disabled, closing connection", "ID", a.Id())
return return
} }
a.lifeCycle.SetState(&StateReconnecting{}) a.lifeCycle.SetState(&StateReconnecting{})
numberOfAttempts := 1 // Add exponential backoff with jitter
waitTime := a.amqpConnOptions.RecoveryConfiguration.BackOffReconnectInterval baseDelay := a.amqpConnOptions.RecoveryConfiguration.BackOffReconnectInterval
reconnected := false maxDelay := 1 * time.Minute
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
waitTime = waitTime + time.Duration(rand.Intn(500))*time.Millisecond // Calculate delay with exponential backoff and jitter
jitter := time.Duration(rand.Intn(500)) * time.Millisecond
delay := baseDelay + jitter
if delay > maxDelay {
delay = maxDelay
}
Info("Waiting before reconnecting", "in", waitTime, "attempt", numberOfAttempts) Info("Attempting reconnection", "attempt", attempt, "delay", delay, "ID", a.Id())
time.Sleep(waitTime) time.Sleep(delay)
// 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.amqpConnOptions.addresses, a.amqpConnOptions) err := a.open(ctx, a.address, a.amqpConnOptions)
cancel() cancel()
if err != nil { if err == nil {
numberOfAttempts++ a.restartEntities()
waitTime = waitTime * 2 a.lifeCycle.SetState(&StateOpen{})
Error("Failed to connection. ", "id", a.Id(), "error", err) return
} else {
reconnected = true
break
} }
baseDelay *= 2
Error("Reconnection attempt failed", "attempt", attempt, "error", err, "ID", a.Id())
} }
if reconnected { // If we reach here, all attempts failed
var fails int32 Error("All reconnection attempts failed, closing connection", "ID", a.Id())
Info("Reconnected successfully, restarting publishers and consumers") a.lifeCycle.SetState(&StateClosed{error: ErrMaxReconnectAttemptsReached})
a.entitiesTracker.publishers.Range(func(key, value any) bool {
publisher := value.(*Publisher)
// try to createSender
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
err := publisher.createSender(ctx)
if err != nil {
atomic.AddInt32(&fails, 1)
Error("Failed to createSender publisher", "ID", publisher.Id(), "error", err)
}
cancel()
return true
})
Info("Restarted publishers", "number of fails", fails)
fails = 0
a.entitiesTracker.consumers.Range(func(key, value any) bool {
consumer := value.(*Consumer)
// try to createSender
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
err := consumer.createReceiver(ctx)
if err != nil {
atomic.AddInt32(&fails, 1)
Error("Failed to createReceiver consumer", "ID", consumer.Id(), "error", err)
}
cancel()
return true
})
Info("Restarted consumers", "number of fails", fails)
a.lifeCycle.SetState(&StateOpen{}) }
}
// restartEntities attempts to restart all publishers and consumers after a reconnection
func (a *AmqpConnection) restartEntities() {
var publisherFails, consumerFails int32
// Restart publishers
a.entitiesTracker.publishers.Range(func(key, value any) bool {
publisher := value.(*Publisher)
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
if err := publisher.createSender(ctx); err != nil {
atomic.AddInt32(&publisherFails, 1)
Error("Failed to restart publisher", "ID", publisher.Id(), "error", err, "ID", a.Id())
}
return true
})
// Restart consumers
a.entitiesTracker.consumers.Range(func(key, value any) bool {
consumer := value.(*Consumer)
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
if err := consumer.createReceiver(ctx); err != nil {
atomic.AddInt32(&consumerFails, 1)
Error("Failed to restart consumer", "ID", consumer.Id(), "error", err, "ID", a.Id())
}
return true
})
Info("Entity restart complete",
"publisherFails", publisherFails,
"consumerFails", consumerFails)
} }
func (a *AmqpConnection) close() { func (a *AmqpConnection) close() {
@ -343,7 +410,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) Error("Failed to close management", "error:", err, "ID", a.Id())
} }
err = a.azureConnection.Close() err = a.azureConnection.Close()
a.close() a.close()
@ -353,15 +420,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.chStatusChanged = channel a.lifeCycle.notifyStatusChange(channel)
} }
func (a *AmqpConnection) State() LifeCycleState { func (a *AmqpConnection) State() ILifeCycleState {
return a.lifeCycle.State() return a.lifeCycle.State()
} }
func (a *AmqpConnection) Id() string { func (a *AmqpConnection) Id() string {
return a.id return a.amqpConnOptions.Id
} }
// *** management section *** // *** management section ***
@ -372,4 +439,24 @@ 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,10 +1,14 @@
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.
@ -29,6 +33,17 @@ 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,
@ -49,35 +64,19 @@ func newEntitiesTracker() *entitiesTracker {
} }
} }
func (e *entitiesTracker) storeOrReplaceProducer(entity entityIdentifier) { func (e *entitiesTracker) storeOrReplaceProducer(entity iEntityIdentifier) {
e.publishers.Store(entity.Id(), entity) e.publishers.Store(entity.Id(), entity)
} }
func (e *entitiesTracker) getProducer(id string) (*Publisher, bool) { func (e *entitiesTracker) removeProducer(entity iEntityIdentifier) {
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 entityIdentifier) { func (e *entitiesTracker) storeOrReplaceConsumer(entity iEntityIdentifier) {
e.consumers.Store(entity.Id(), entity) e.consumers.Store(entity.Id(), entity)
} }
func (e *entitiesTracker) getConsumer(id string) (*Consumer, bool) { func (e *entitiesTracker) removeConsumer(entity iEntityIdentifier) {
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,11 +2,12 @@ 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() {
@ -22,7 +23,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"
connection, err := Dial(context.Background(), []string{"amqp://"}, &AmqpConnOptions{ env := NewEnvironment("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
@ -31,7 +32,10 @@ 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)
@ -45,10 +49,11 @@ 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,
}, "test") }, nil)
Expect(err).To(BeNil()) Expect(err).To(BeNil())
Expect(publisher).NotTo(BeNil()) Expect(publisher).NotTo(BeNil())
@ -113,11 +118,28 @@ 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(), []string{"amqp://"}, &AmqpConnOptions{ connection, err := Dial(context.Background(), "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
@ -155,7 +177,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(), []string{"amqp://"}, &AmqpConnOptions{ _, err := Dial(context.Background(), "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{
@ -167,7 +189,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(), []string{"amqp://"}, &AmqpConnOptions{ _, err = Dial(context.Background(), "amqp://", &AmqpConnOptions{
SASLType: amqp.SASLTypeAnonymous(), SASLType: amqp.SASLTypeAnonymous(),
RecoveryConfiguration: &RecoveryConfiguration{ RecoveryConfiguration: &RecoveryConfiguration{
ActiveRecovery: true, ActiveRecovery: true,

View File

@ -2,25 +2,29 @@ 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(), []string{"amqp://"}, &AmqpConnOptions{ connection, err := Dial(context.Background(), "amqp://", &AmqpConnOptions{
SASLType: amqp.SASLTypePlain("guest", "guest")}) SASLType: amqp.SASLTypePlain("guest", "guest")})
Expect(err).To(BeNil()) Expect(err).To(BeNil())
@ -29,38 +33,17 @@ 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, []string{"amqp://"}, nil) _, err := Dial(ctx, "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(), []string{"amqp://"}, nil) connection, err := Dial(context.Background(), "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())
@ -72,19 +55,113 @@ var _ = Describe("AMQP connection Test", func() {
Expect(recv.To).To(Equal(&StateClosed{})) Expect(recv.To).To(Equal(&StateClosed{}))
}) })
//It("AMQP TLS connection should success with SASLTypeAnonymous ", func() { It("Entity tracker should be aligned with consumers and publishers ", func() {
// amqpConnection := NewAmqpConnection() connection, err := Dial(context.Background(), "amqp://", &AmqpConnOptions{
// Expect(amqpConnection).NotTo(BeNil()) SASLType: amqp.SASLTypeAnonymous()})
// Expect(amqpConnection).To(BeAssignableToTypeOf(&AmqpConnection{})) Expect(err).To(BeNil())
// Expect(connection).NotTo(BeNil())
// connectionSettings := NewConnUrlHelper().
// UseSsl(true).Port(5671).TlsConfig(&tls.Config{ queueName := generateNameWithDateTime("Entity tracker should be aligned with consumers and publishers")
// //ServerName: "localhost",
// InsecureSkipVerify: true, _, err = connection.Management().DeclareQueue(context.Background(), &QuorumQueueSpecification{
// }) Name: queueName,
// Expect(connectionSettings).NotTo(BeNil()) })
// Expect(connectionSettings).To(BeAssignableToTypeOf(&ConnUrlHelper{}))
// err := amqpConnection.Open(context.Background(), connectionSettings) Expect(err).To(BeNil())
// Expect(err).To(BeNil()) publisher, err := connection.NewPublisher(context.Background(), &QueueAddress{Queue: queueName},
//}) &PublisherOptions{
Id: "my_id",
SenderLinkName: "my_sender_link",
})
Expect(err).To(BeNil())
Expect(publisher).NotTo(BeNil())
consumer, err := connection.NewConsumer(context.Background(), queueName, nil)
Expect(err).To(BeNil())
Expect(consumer).NotTo(BeNil())
// check the entity tracker
Expect(connection.entitiesTracker).NotTo(BeNil())
entLen := 0
connection.entitiesTracker.consumers.Range(func(key, value interface{}) bool {
entLen++
return true
})
Expect(entLen).To(Equal(1))
entLen = 0
connection.entitiesTracker.publishers.Range(func(key, value interface{}) bool {
entLen++
return true
})
Expect(entLen).To(Equal(1))
Expect(consumer.Close(context.Background())).To(BeNil())
Expect(publisher.Close(context.Background())).To(BeNil())
entLen = 0
connection.entitiesTracker.consumers.Range(func(key, value interface{}) bool {
entLen++
return true
})
Expect(entLen).To(Equal(0))
entLen = 0
connection.entitiesTracker.publishers.Range(func(key, value interface{}) bool {
entLen++
return true
})
Expect(entLen).To(Equal(0))
err = connection.Management().DeleteQueue(context.Background(), queueName)
Expect(err).To(BeNil())
Expect(connection.Close(context.Background())).To(BeNil())
})
Describe("AMQP TLS connection should succeed with in different vhosts with Anonymous and External.", func() {
wg := &sync.WaitGroup{}
wg.Add(4)
DescribeTable("TLS connection should success in different vhosts ", func(virtualHost string, sasl amqp.SASLType) {
// Load CA cert
caCert, err := os.ReadFile("../../.ci/certs/ca_certificate.pem")
Expect(err).To(BeNil())
// Create a CA certificate pool and add the CA certificate to it
caCertPool := x509.NewCertPool()
caCertPool.AppendCertsFromPEM(caCert)
// Load client cert
clientCert, err := tls.LoadX509KeyPair("../../.ci/certs/client_localhost_certificate.pem",
"../../.ci/certs/client_localhost_key.pem")
Expect(err).To(BeNil())
// Create a TLS configuration
tlsConfig := &tls.Config{
Certificates: []tls.Certificate{clientCert},
RootCAs: caCertPool,
InsecureSkipVerify: false,
ServerName: "localhost",
}
// Dial the AMQP server with TLS configuration
connection, err := Dial(context.Background(), fmt.Sprintf("amqps://localhost:5671/%s", virtualHost), &AmqpConnOptions{
SASLType: sasl,
TLSConfig: tlsConfig,
})
Expect(err).To(BeNil())
Expect(connection).NotTo(BeNil())
// Close the connection
err = connection.Close(context.Background())
Expect(err).To(BeNil())
wg.Done()
},
Entry("with virtual host. External", "%2F", amqp.SASLTypeExternal("")),
Entry("with a not default virtual host. External", "tls", amqp.SASLTypeExternal("")),
Entry("with virtual host. Anonymous", "%2F", amqp.SASLTypeAnonymous()),
Entry("with a not default virtual host. Anonymous", "tls", amqp.SASLTypeAnonymous()),
)
go func() {
wg.Wait()
}()
})
}) })

View File

@ -25,17 +25,23 @@ func (dc *DeliveryContext) Discard(ctx context.Context, e *amqp.Error) error {
return dc.receiver.RejectMessage(ctx, dc.message, e) return dc.receiver.RejectMessage(ctx, dc.message, e)
} }
func (dc *DeliveryContext) DiscardWithAnnotations(ctx context.Context, annotations amqp.Annotations) error { // copyAnnotations helper function to copy annotations
func copyAnnotations(annotations amqp.Annotations) (amqp.Annotations, error) {
if err := validateMessageAnnotations(annotations); err != nil { if err := validateMessageAnnotations(annotations); err != nil {
return nil, err
}
destination := make(amqp.Annotations)
for key, value := range annotations {
destination[key] = value
}
return destination, nil
}
func (dc *DeliveryContext) DiscardWithAnnotations(ctx context.Context, annotations amqp.Annotations) error {
destination, err := copyAnnotations(annotations)
if err != nil {
return err 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,
@ -48,15 +54,10 @@ 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 {
if err := validateMessageAnnotations(annotations); err != nil { destination, err := copyAnnotations(annotations)
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,
@ -67,7 +68,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 ConsumerOptions options IConsumerOptions
destinationAdd string destinationAdd string
id string id string
@ -84,10 +85,10 @@ func (c *Consumer) Id() string {
return c.id return c.id
} }
func newConsumer(ctx context.Context, connection *AmqpConnection, destinationAdd string, options ConsumerOptions, args ...string) (*Consumer, error) { func newConsumer(ctx context.Context, connection *AmqpConnection, destinationAdd string, options IConsumerOptions) (*Consumer, error) {
id := fmt.Sprintf("consumer-%s", uuid.New().String()) id := fmt.Sprintf("consumer-%s", uuid.New().String())
if len(args) > 0 { if options != nil && options.id() != "" {
id = args[0] id = options.id()
} }
r := &Consumer{connection: connection, options: options, r := &Consumer{connection: connection, options: options,
@ -144,5 +145,6 @@ func (c *Consumer) Receive(ctx context.Context) (*DeliveryContext, error) {
} }
func (c *Consumer) Close(ctx context.Context) error { 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,31 +7,10 @@ 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"
"strconv" "sync"
"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() {
@ -49,7 +28,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(), []string{"amqp://"}, nil) connection, err := Dial(context.Background(), "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,
@ -73,7 +52,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(fmt.Sprintf("%s", dc.Message().GetData())).To(Equal(fmt.Sprintf("Message #%d", i+5))) Expect(string(dc.Message().GetData())).To(Equal(fmt.Sprintf("Message #%d", i+5)))
Expect(dc.Accept(context.Background())).To(BeNil()) Expect(dc.Accept(context.Background())).To(BeNil())
} }
@ -88,7 +67,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(fmt.Sprintf("%s", dc.Message().GetData())).To(Equal(fmt.Sprintf("Message #%d", i))) Expect(string(dc.Message().GetData())).To(Equal(fmt.Sprintf("Message #%d", i)))
Expect(dc.Accept(context.Background())).To(BeNil()) Expect(dc.Accept(context.Background())).To(BeNil())
} }
@ -107,7 +86,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(fmt.Sprintf("%s", dc.Message().GetData())).NotTo(Equal(fmt.Sprintf("Message #%d", 0))) Expect(string(dc.Message().GetData())).NotTo(Equal(fmt.Sprintf("Message #%d", 0)))
Expect(dc.Accept(context.Background())).To(BeNil()) Expect(dc.Accept(context.Background())).To(BeNil())
consumerNext, err := connection.NewConsumer(context.Background(), qName, &StreamConsumerOptions{ consumerNext, err := connection.NewConsumer(context.Background(), qName, &StreamConsumerOptions{
@ -125,7 +104,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(fmt.Sprintf("%s", dc.Message().GetData())).To(Equal("the next message")) Expect(string(dc.Message().GetData())).To(Equal("the next message"))
Expect(dc.Accept(context.Background())).To(BeNil()) Expect(dc.Accept(context.Background())).To(BeNil())
signal <- struct{}{} signal <- struct{}{}
}() }()
@ -146,7 +125,7 @@ var _ = Describe("Consumer stream test", func() {
*/ */
qName := generateName("consumer should restart form the last offset in case of disconnection") qName := generateName("consumer should restart form the last offset in case of disconnection")
connection, err := Dial(context.Background(), []string{"amqp://"}, &AmqpConnOptions{ connection, err := Dial(context.Background(), "amqp://", &AmqpConnOptions{
SASLType: amqp.SASLTypeAnonymous(), SASLType: amqp.SASLTypeAnonymous(),
ContainerID: qName, ContainerID: qName,
RecoveryConfiguration: &RecoveryConfiguration{ RecoveryConfiguration: &RecoveryConfiguration{
@ -179,7 +158,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(fmt.Sprintf("%s", dc.Message().GetData())).To(Equal(fmt.Sprintf("Message #%d", i))) Expect(string(dc.Message().GetData())).To(Equal(fmt.Sprintf("Message #%d", i)))
Expect(dc.Accept(context.Background())).To(BeNil()) Expect(dc.Accept(context.Background())).To(BeNil())
} }
@ -200,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(fmt.Sprintf("%s", dc.Message().GetData())).To(Equal(fmt.Sprintf("Message #%d", i))) Expect(string(dc.Message().GetData())).To(Equal(fmt.Sprintf("Message #%d", i)))
} }
Expect(consumer.Close(context.Background())).To(BeNil()) Expect(consumer.Close(context.Background())).To(BeNil())
@ -209,7 +188,7 @@ var _ = Describe("Consumer stream test", func() {
}) })
It("consumer should filter messages based on x-stream-filter", func() { 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(), []string{"amqp://"}, nil) connection, err := Dial(context.Background(), "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,
@ -217,15 +196,34 @@ 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))
publishMessagesWithStreamTag(qName, "banana", 10) publishMessagesWithMessageLogic(qName, "banana", 10, func(msg *amqp.Message) {
publishMessagesWithStreamTag(qName, "apple", 10) msg.Annotations = amqp.Annotations{
publishMessagesWithStreamTag(qName, "", 10) // here we set the filter value taken from the filters array
StreamFilterValue: "banana",
}
})
publishMessagesWithMessageLogic(qName, "apple", 10, func(msg *amqp.Message) {
msg.Annotations = amqp.Annotations{
// here we set the filter value taken from the filters array
StreamFilterValue: "apple",
}
})
publishMessagesWithMessageLogic(qName, "", 10, func(msg *amqp.Message) {
msg.Annotations = amqp.Annotations{
// here we set the filter value taken from the filters array
StreamFilterValue: "",
}
})
consumerBanana, err := connection.NewConsumer(context.Background(), qName, &StreamConsumerOptions{ 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{},
Filters: []string{"banana"}, StreamFilterOptions: &StreamFilterOptions{
Values: []string{"banana"},
},
}) })
Expect(err).To(BeNil()) Expect(err).To(BeNil())
@ -235,16 +233,18 @@ 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(fmt.Sprintf("%s", dc.Message().GetData())).To(Equal(fmt.Sprintf("banana #%d", i))) Expect(string(dc.Message().GetData())).To(Equal(fmt.Sprintf("Message_id:%d_label:%s", i, "banana")))
Expect(dc.Accept(context.Background())).To(BeNil()) Expect(dc.Accept(context.Background())).To(BeNil())
} }
consumerApple, err := connection.NewConsumer(context.Background(), qName, &StreamConsumerOptions{ consumerApple, err := connection.NewConsumer(context.Background(), qName, &StreamConsumerOptions{
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{},
Filters: []string{"apple"}, StreamFilterOptions: &StreamFilterOptions{
FilterMatchUnfiltered: true, Values: []string{"apple"},
MatchUnfiltered: true,
},
}) })
Expect(err).To(BeNil()) Expect(err).To(BeNil())
@ -254,7 +254,8 @@ 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(fmt.Sprintf("%s", dc.Message().GetData())).To(Equal(fmt.Sprintf("apple #%d", i))) Expect(string(dc.Message().GetData())).To(Equal(fmt.Sprintf("Message_id:%d_label:%s", i, "apple")))
Expect(dc.Accept(context.Background())).To(BeNil()) Expect(dc.Accept(context.Background())).To(BeNil())
} }
@ -262,7 +263,9 @@ var _ = Describe("Consumer stream test", func() {
ReceiverLinkName: "consumer apple and banana should filter messages based on x-stream-filter", ReceiverLinkName: "consumer apple and banana should filter messages based on x-stream-filter",
InitialCredits: 200, InitialCredits: 200,
Offset: &OffsetFirst{}, Offset: &OffsetFirst{},
Filters: []string{"apple", "banana"}, StreamFilterOptions: &StreamFilterOptions{
Values: []string{"apple", "banana"},
},
}) })
Expect(err).To(BeNil()) Expect(err).To(BeNil())
@ -273,19 +276,23 @@ 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(fmt.Sprintf("%s", dc.Message().GetData())).To(Equal(fmt.Sprintf("banana #%d", i))) Expect(string(dc.Message().GetData())).To(Equal(fmt.Sprintf("Message_id:%d_label:%s", i, "banana")))
} else { } 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 FilterMatchUnfiltered true", ReceiverLinkName: "consumer apple should filter messages based on x-stream-filter and MatchUnfiltered true",
InitialCredits: 200, InitialCredits: 200,
Offset: &OffsetFirst{}, Offset: &OffsetFirst{},
Filters: []string{"apple"}, StreamFilterOptions: &StreamFilterOptions{
FilterMatchUnfiltered: true, Values: []string{"apple"},
MatchUnfiltered: true,
},
}) })
Expect(err).To(BeNil()) Expect(err).To(BeNil())
@ -296,10 +303,9 @@ 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(fmt.Sprintf("%s", dc.Message().GetData())).To(Equal(fmt.Sprintf("apple #%d", i))) Expect(string(dc.Message().GetData())).To(Equal(fmt.Sprintf("Message_id:%d_label:%s", i, "apple")))
} else { } else {
Expect(fmt.Sprintf("%s", dc.Message().GetData())).To(Equal(fmt.Sprintf(" #%d", i-10))) Expect(string(dc.Message().GetData())).To(Equal(fmt.Sprintf("Message_id:%d_label:%s", i-10, "")))
} }
Expect(dc.Accept(context.Background())).To(BeNil()) Expect(dc.Accept(context.Background())).To(BeNil())
} }
@ -312,4 +318,263 @@ 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(), []string{"amqp://"}, nil) connection, err := Dial(context.Background(), "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(), []string{"amqp://"}, nil) connection, err := Dial(context.Background(), "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(), []string{"amqp://"}, nil) connection, err := Dial(context.Background(), "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,6 +81,7 @@ 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())
@ -89,7 +90,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(), []string{"amqp://"}, nil) connection, err := Dial(context.Background(), "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,
@ -116,6 +117,7 @@ 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())
@ -124,7 +126,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(), []string{"amqp://"}, nil) connection, err := Dial(context.Background(), "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,
@ -153,6 +155,7 @@ 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,47 +3,103 @@ package rabbitmqamqp
import ( import (
"context" "context"
"fmt" "fmt"
"github.com/Azure/go-amqp"
"sync" "sync"
"sync/atomic"
) )
type Environment struct { type TEndPointStrategy int
connections sync.Map
addresses []string const (
connOptions *AmqpConnOptions StrategyRandom TEndPointStrategy = iota
StrategySequential TEndPointStrategy = iota
)
type Endpoint struct {
Address string
Options *AmqpConnOptions
} }
func NewEnvironment(addresses []string, connOptions *AmqpConnOptions) *Environment { func DefaultEndpoints() []Endpoint {
ep := Endpoint{
Address: "amqp://",
Options: &AmqpConnOptions{
SASLType: amqp.SASLTypeAnonymous(),
},
}
return []Endpoint{ep}
}
type Environment struct {
connections sync.Map
endPoints []Endpoint
EndPointStrategy TEndPointStrategy
nextConnectionId int32
}
func NewEnvironment(address string, options *AmqpConnOptions) *Environment {
return NewClusterEnvironmentWithStrategy([]Endpoint{{Address: address, Options: options}}, StrategyRandom)
}
func NewClusterEnvironment(endPoints []Endpoint) *Environment {
return NewClusterEnvironmentWithStrategy(endPoints, StrategyRandom)
}
func NewClusterEnvironmentWithStrategy(endPoints []Endpoint, strategy TEndPointStrategy) *Environment {
return &Environment{ return &Environment{
connections: sync.Map{}, connections: sync.Map{},
addresses: addresses, endPoints: endPoints,
connOptions: connOptions, EndPointStrategy: strategy,
nextConnectionId: 0,
} }
} }
// NewConnection get a new connection from the environment. // NewConnection get a new connection from the environment.
// If the connection id is provided, it will be used as the connection id. // It picks an endpoint from the list of endpoints, based on EndPointStrategy, and tries to open a connection.
// If the connection id is not provided, a new connection id will be generated. // It fails if all the endpoints are not reachable.
// 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, args ...string) (*AmqpConnection, error) { func (e *Environment) NewConnection(ctx context.Context) (*AmqpConnection, error) {
if len(args) > 0 && len(args[0]) > 0 {
// check if connection already exists
if _, ok := e.connections.Load(args[0]); ok {
return nil, fmt.Errorf("connection with id %s already exists", args[0])
}
}
connection, err := Dial(ctx, e.addresses, e.connOptions, args...) tmp := make([]Endpoint, len(e.endPoints))
if err != nil { copy(tmp, e.endPoints)
return nil, err lastError := error(nil)
for len(tmp) > 0 {
idx := 0
switch e.EndPointStrategy {
case StrategyRandom:
idx = random(len(tmp))
case StrategySequential:
idx = 0
}
addr := tmp[idx]
// remove the index from the tmp list
tmp = append(tmp[:idx], tmp[idx+1:]...)
var cloned *AmqpConnOptions
if addr.Options != nil {
cloned = addr.Options.Clone()
}
connection, err := Dial(ctx, addr.Address, cloned)
if err != nil {
Error("Failed to open connection", ExtractWithoutPassword(addr.Address), err)
lastError = err
continue
}
// here we use it to make each connection unique
atomic.AddInt32(&e.nextConnectionId, 1)
connection.amqpConnOptions.Id = fmt.Sprintf("%s_%d", connection.amqpConnOptions.Id, e.nextConnectionId)
e.connections.Store(connection.Id(), connection)
connection.refMap = &e.connections
return connection, nil
} }
e.connections.Store(connection.Id(), connection) return nil, fmt.Errorf("fail to open connection. Last error: %w", lastError)
connection.refMap = &e.connections
return connection, nil
} }
// 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,13 +2,14 @@ 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 := NewEnvironment([]string{"amqp://"}, nil) env := NewClusterEnvironment([]Endpoint{{Address: "amqp://"}})
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))
@ -22,7 +23,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 := NewEnvironment([]string{"amqp://"}, nil) env := NewClusterEnvironment([]Endpoint{{Address: "amqp://"}})
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))
@ -36,22 +37,83 @@ var _ = Describe("AMQP Environment Test", func() {
Expect(len(env.Connections())).To(Equal(0)) Expect(len(env.Connections())).To(Equal(0))
}) })
It("AMQP Environment connection ID should be unique", func() { It("Get new connection should connect to the one correct uri and fails the others", 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))
connection, err := env.NewConnection(context.Background(), "myConnectionId") conn, err := env.NewConnection(context.Background())
Expect(err).To(BeNil()) Expect(err).To(BeNil())
Expect(connection).NotTo(BeNil()) Expect(conn.Close(context.Background()))
Expect(len(env.Connections())).To(Equal(1))
connectionShouldBeNil, err := env.NewConnection(context.Background(), "myConnectionId")
Expect(err).NotTo(BeNil())
Expect(err.Error()).To(ContainSubstring("connection with id myConnectionId already exists"))
Expect(connectionShouldBeNil).To(BeNil())
Expect(len(env.Connections())).To(Equal(1))
Expect(connection.Close(context.Background())).To(BeNil())
Expect(len(env.Connections())).To(Equal(0))
}) })
It("AMQP connection should fail with user_1 does not have the grant for /", func() {
// user_1 is preloaded in the rabbitmq server during the startup
env := NewEnvironment("amqp://user_1:user_1@localhost:5672/", nil)
Expect(env).NotTo(BeNil())
_, err := env.NewConnection(context.Background())
Expect(err).NotTo(BeNil())
})
Describe("Environment strategy", func() {
DescribeTable("Environment with strategy should success", func(strategy TEndPointStrategy) {
env := NewClusterEnvironmentWithStrategy([]Endpoint{{Address: "amqp://", Options: &AmqpConnOptions{Id: "my"}}, {Address: "amqp://nohost:555"}, {Address: "amqp://nono"}}, StrategyRandom)
Expect(env).NotTo(BeNil())
Expect(env.Connections()).NotTo(BeNil())
Expect(len(env.Connections())).To(Equal(0))
conn, err := env.NewConnection(context.Background())
Expect(err).To(BeNil())
Expect(conn.Id()).To(Equal("my_1"))
Expect(conn.Close(context.Background()))
},
Entry("StrategyRandom", StrategyRandom),
Entry("StrategySequential", StrategySequential),
)
})
Describe("Environment should success even partial options", func() {
DescribeTable("Environment should success even partial options", func(options *AmqpConnOptions) {
env := NewClusterEnvironment([]Endpoint{{Address: "amqp://", Options: options}})
Expect(env).NotTo(BeNil())
Expect(env.Connections()).NotTo(BeNil())
Expect(len(env.Connections())).To(Equal(0))
conn, err := env.NewConnection(context.Background())
Expect(err).To(BeNil())
Expect(conn.Close(context.Background()))
},
Entry("Partial options", &AmqpConnOptions{Id: "my"}),
Entry("Partial options", &AmqpConnOptions{SASLType: amqp.SASLTypeAnonymous()}),
Entry("Partial options", &AmqpConnOptions{ContainerID: "cid_my"}),
)
})
}) })

View File

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

View File

@ -4,10 +4,11 @@ import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
"github.com/Azure/go-amqp"
"github.com/google/uuid"
"strconv" "strconv"
"time" "time"
"github.com/Azure/go-amqp"
"github.com/google/uuid"
) )
var ErrPreconditionFailed = errors.New("precondition Failed") var ErrPreconditionFailed = errors.New("precondition Failed")
@ -22,10 +23,9 @@ 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 QueueSpecification) (*AmqpQueueInfo, error) { func (a *AmqpManagement) DeclareQueue(ctx context.Context, specification IQueueSpecification) (*AmqpQueueInfo, error) {
if specification == nil { if specification == nil {
return nil, fmt.Errorf("queue specification cannot be nil. You need to provide a valid QueueSpecification") return nil, fmt.Errorf("queue specification cannot be nil. You need to provide a valid IQueueSpecification")
} }
amqpQueue := newAmqpQueue(a, specification.name()) 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 ExchangeSpecification) (*AmqpExchangeInfo, error) { func (a *AmqpManagement) DeclareExchange(ctx context.Context, exchangeSpecification IExchangeSpecification) (*AmqpExchangeInfo, error) {
if exchangeSpecification == nil { if exchangeSpecification == nil {
return nil, errors.New("exchange specification cannot be nil. You need to provide a valid ExchangeSpecification") return nil, errors.New("exchange specification cannot be nil. You need to provide a valid IExchangeSpecification")
} }
exchange := newAmqpExchange(a, exchangeSpecification.name()) 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 BindingSpecification) (string, error) { func (a *AmqpManagement) Bind(ctx context.Context, bindingSpecification IBindingSpecification) (string, error) {
if bindingSpecification == nil { if bindingSpecification == nil {
return "", fmt.Errorf("binding specification cannot be nil. You need to provide a valid BindingSpecification") return "", fmt.Errorf("binding specification cannot be nil. You need to provide a valid IBindingSpecification")
} }
bind := newAMQPBinding(a) bind := newAMQPBinding(a)
@ -220,9 +220,9 @@ func (a *AmqpManagement) Bind(ctx context.Context, bindingSpecification BindingS
return bind.Bind(ctx) return bind.Bind(ctx)
} }
func (a *AmqpManagement) Unbind(ctx context.Context, bindingPath string) error { func (a *AmqpManagement) Unbind(ctx context.Context, path string) error {
bind := newAMQPBinding(a) bind := newAMQPBinding(a)
return bind.Unbind(ctx, bindingPath) return bind.Unbind(ctx, path)
} }
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,15 +236,22 @@ func (a *AmqpManagement) QueueInfo(ctx context.Context, queueName string) (*Amqp
return newAmqpQueueInfo(result), nil return newAmqpQueueInfo(result), nil
} }
func (a *AmqpManagement) PurgeQueue(ctx context.Context, queueName string) (int, error) { // PurgeQueue purges the queue
purge := newAmqpQueue(a, queueName) // returns the number of messages purged
func (a *AmqpManagement) PurgeQueue(ctx context.Context, name string) (int, error) {
purge := newAmqpQueue(a, name)
return purge.Purge(ctx) 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() LifeCycleState { func (a *AmqpManagement) State() ILifeCycleState {
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(), []string{"amqp://"}, nil) connection, err := Dial(context.Background(), "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(), []string{"amqp://"}, &AmqpConnOptions{ connection, err := Dial(context.Background(), "amqp://", &AmqpConnOptions{
SASLType: amqp.SASLTypeAnonymous(), SASLType: amqp.SASLTypeAnonymous(),
RecoveryConfiguration: &RecoveryConfiguration{ RecoveryConfiguration: &RecoveryConfiguration{
ActiveRecovery: false, ActiveRecovery: false,
@ -43,27 +43,31 @@ var _ = Describe("Management tests", func() {
It("Request", func() { It("Request", func() {
connection, err := Dial(context.Background(), []string{"amqp://"}, nil) connection, err := Dial(context.Background(), "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"] = false kv["auto_delete"] = true
_queueArguments := make(map[string]any) _queueArguments := make(map[string]any)
_queueArguments["x-queue-type"] = "quorum" _queueArguments["x-queue-type"] = "classic"
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(), []string{"amqp://"}, nil) connection, err := Dial(context.Background(), "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, linkName string, args ...string) (*Publisher, error) { func newPublisher(ctx context.Context, connection *AmqpConnection, destinationAdd string, options IPublisherOptions) (*Publisher, error) {
id := fmt.Sprintf("publisher-%s", uuid.New().String()) id := fmt.Sprintf("publisher-%s", uuid.New().String())
if len(args) > 0 { if options != nil && options.id() != "" {
id = args[0] id = options.id()
} }
r := &Publisher{connection: connection, linkName: linkName, destinationAdd: destinationAdd, id: id} r := &Publisher{connection: connection, linkName: getLinkName(options), destinationAdd: destinationAdd, id: id}
connection.entitiesTracker.storeOrReplaceProducer(r) 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.
Note: If the destination address is not defined during the creation, the message must have a TO property set. If the destination address is not defined during the creation, the message must have a TO property set.
You can use the helper "MessagePropertyToAddress" to create the destination address. 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,6 +84,16 @@ 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) {
@ -97,6 +107,14 @@ 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(), []string{"amqp://"}, nil) connection, err := Dial(context.Background(), "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}, "test") publisher, err := connection.NewPublisher(context.Background(), &QueueAddress{Queue: qName}, nil)
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(), []string{"amqp://"}, nil) connection, err := Dial(context.Background(), "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}, "test") publisher, err := connection.NewPublisher(context.Background(), &ExchangeAddress{Exchange: exchangeName}, nil)
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(), []string{"amqp://"}, nil) connection, err := Dial(context.Background(), "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,
}, "test") }, nil)
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(), []string{"amqp://"}, nil) connection, err := Dial(context.Background(), "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}, "test") publisher, err := connection.NewPublisher(context.Background(), &QueueAddress{Queue: qName}, nil)
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,15 +91,16 @@ 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(), []string{"amqp://"}, nil) connection, err := Dial(context.Background(), "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, "test") publisher, err := connection.NewPublisher(context.Background(), nil, nil)
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")
@ -120,11 +121,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(), []string{"amqp://"}, nil) connection, err := Dial(context.Background(), "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, "test") publisher, err := connection.NewPublisher(context.Background(), nil, nil)
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")
@ -146,6 +147,8 @@ 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
@ -174,10 +177,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(), []string{"amqp://"}, nil) connection, err := Dial(context.Background(), "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, "test") publisher, err := connection.NewPublisher(context.Background(), nil, nil)
Expect(err).To(BeNil()) Expect(err).To(BeNil())
Expect(publisher).NotTo(BeNil()) Expect(publisher).NotTo(BeNil())
msg := NewMessage([]byte("hello")) msg := NewMessage([]byte("hello"))
@ -200,4 +203,65 @@ 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

@ -8,14 +8,16 @@ import (
) )
type AmqpQueueInfo struct { type AmqpQueueInfo struct {
name string name string
isDurable bool isDurable bool
isAutoDelete bool isAutoDelete bool
isExclusive bool isExclusive bool
leader string leader string
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 {
@ -27,15 +29,22 @@ 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: response["leader"].(string), leader: leader,
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),
} }
} }
@ -63,6 +72,14 @@ 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(), []string{"amqp://"}, nil) conn, err := Dial(context.TODO(), "amqp://", nil)
Expect(err).To(BeNil()) Expect(err).To(BeNil())
connection = conn connection = conn
management = connection.Management() management = connection.Management()
@ -37,6 +37,7 @@ 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)
@ -68,6 +69,8 @@ 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))
@ -84,6 +87,7 @@ 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)
@ -108,6 +112,7 @@ 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)
@ -134,6 +139,7 @@ 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)
@ -238,10 +244,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(), []string{"amqp://guest:guest@localhost"}, nil) conn, err := Dial(context.TODO(), "amqp://guest:guest@localhost", nil)
Expect(err).To(BeNil()) Expect(err).To(BeNil())
publisher, err := conn.NewPublisher(context.TODO(), &QueueAddress{Queue: queueName}, "test") publisher, err := conn.NewPublisher(context.TODO(), &QueueAddress{Queue: queueName}, nil)
Expect(err).To(BeNil()) Expect(err).To(BeNil())
Expect(publisher).NotTo(BeNil()) Expect(publisher).NotTo(BeNil())

View File

@ -1,6 +1,7 @@
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"
) )
@ -13,20 +14,20 @@ type StateRejected = amqp.StateRejected
type StateReleased = amqp.StateReleased type StateReleased = amqp.StateReleased
type StateModified = amqp.StateModified type StateModified = amqp.StateModified
type linkerName interface { type iLinkerName interface {
linkName() string linkName() string
} }
func getLinkName(l linkerName) string { func getLinkName(l iLinkerName) 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()
} }
/// ConsumerOptions interface for the AMQP and Stream consumer/// /// IConsumerOptions interface for the AMQP and Stream consumer///
type ConsumerOptions interface { type IConsumerOptions interface {
// linkName returns the name of the link // 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
@ -37,16 +38,22 @@ type ConsumerOptions 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 ConsumerOptions) int32 { func getInitialCredits(co IConsumerOptions) 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 ConsumerOptions) []amqp.LinkFilter { func getLinkFilters(co IConsumerOptions) []amqp.LinkFilter {
if co == nil { if co == nil {
return nil return nil
} }
@ -69,32 +76,54 @@ func (mo *managementOptions) linkFilters() []amqp.LinkFilter {
return nil return nil
} }
type AMQPConsumerOptions struct { func (mo *managementOptions) id() string {
//ReceiverLinkName: see the ConsumerOptions interface return "management"
ReceiverLinkName string
//InitialCredits: see the ConsumerOptions interface
InitialCredits int32
} }
func (aco *AMQPConsumerOptions) linkName() string { func (mo *managementOptions) validate(available *featuresAvailable) error {
return aco.ReceiverLinkName
}
func (aco *AMQPConsumerOptions) initialCredits() int32 {
return aco.InitialCredits
}
func (aco *AMQPConsumerOptions) linkFilters() []amqp.LinkFilter {
return nil return nil
} }
type OffsetSpecification interface { // ConsumerOptions represents the options for quorum and classic queues
type ConsumerOptions struct {
//ReceiverLinkName: see the IConsumerOptions interface
ReceiverLinkName string
//InitialCredits: see the IConsumerOptions interface
InitialCredits int32
// The id of the consumer
Id string
}
func (aco *ConsumerOptions) linkName() string {
return aco.ReceiverLinkName
}
func (aco *ConsumerOptions) initialCredits() int32 {
return aco.InitialCredits
}
func (aco *ConsumerOptions) linkFilters() []amqp.LinkFilter {
return nil
}
func (aco *ConsumerOptions) id() string {
return aco.Id
}
func (aco *ConsumerOptions) validate(available *featuresAvailable) error {
return nil
}
type IOffsetSpecification interface {
toLinkFilter() amqp.LinkFilter 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"
@ -128,23 +157,37 @@ 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 that can be used to create a stream consumer. StreamConsumerOptions represents the options for stream queues
It is mandatory in case of creating a stream consumer. It is mandatory in case of creating a stream consumer.
*/ */
type StreamConsumerOptions struct { type StreamConsumerOptions struct {
//ReceiverLinkName: see the ConsumerOptions interface //ReceiverLinkName: see the IConsumerOptions interface
ReceiverLinkName string ReceiverLinkName string
//InitialCredits: see the ConsumerOptions interface //InitialCredits: see the IConsumerOptions 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 OffsetSpecification Offset IOffsetSpecification
// Filter values. StreamFilterOptions *StreamFilterOptions
// See: https://www.rabbitmq.com/blog/2024/12/13/amqp-filter-expressions for more details
Filters []string Id string
//
FilterMatchUnfiltered bool
} }
func (sco *StreamConsumerOptions) linkName() string { func (sco *StreamConsumerOptions) linkName() string {
@ -157,29 +200,117 @@ 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.Filters != nil { if sco.StreamFilterOptions != nil && sco.StreamFilterOptions.Values != nil {
l := []any{} var l []any
for _, f := range sco.Filters { for _, f := range sco.StreamFilterOptions.Values {
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.FilterMatchUnfiltered)) filters = append(filters, amqp.NewLinkFilter(rmqStreamMatchUnfiltered, 0, sco.StreamFilterOptions.MatchUnfiltered))
} }
if sco.StreamFilterOptions != nil && sco.StreamFilterOptions.ApplicationProperties != nil {
l := map[string]any{}
for k, v := range sco.StreamFilterOptions.ApplicationProperties {
l[k] = v
}
filters = append(filters, amqp.NewLinkFilter(amqpApplicationPropertiesFilter, 0, l))
}
if sco.StreamFilterOptions != nil && sco.StreamFilterOptions.Properties != nil {
l := map[amqp.Symbol]any{}
if sco.StreamFilterOptions.Properties.ContentType != nil {
l["content-type"] = amqp.Symbol(*sco.StreamFilterOptions.Properties.ContentType)
}
if sco.StreamFilterOptions.Properties.ContentEncoding != nil {
l["content-encoding"] = amqp.Symbol(*sco.StreamFilterOptions.Properties.ContentEncoding)
}
if sco.StreamFilterOptions.Properties.CorrelationID != nil {
l["correlation-id"] = sco.StreamFilterOptions.Properties.CorrelationID
}
if sco.StreamFilterOptions.Properties.MessageID != nil {
l["message-id"] = sco.StreamFilterOptions.Properties.MessageID
}
if sco.StreamFilterOptions.Properties.Subject != nil {
l["subject"] = *sco.StreamFilterOptions.Properties.Subject
}
if sco.StreamFilterOptions.Properties.ReplyTo != nil {
l["reply-to"] = *sco.StreamFilterOptions.Properties.ReplyTo
}
if sco.StreamFilterOptions.Properties.To != nil {
l["to"] = *sco.StreamFilterOptions.Properties.To
}
if sco.StreamFilterOptions.Properties.GroupID != nil {
l["group-id"] = *sco.StreamFilterOptions.Properties.GroupID
}
if sco.StreamFilterOptions.Properties.UserID != nil {
l["user-id"] = sco.StreamFilterOptions.Properties.UserID
}
if sco.StreamFilterOptions.Properties.AbsoluteExpiryTime != nil {
l["absolute-expiry-time"] = sco.StreamFilterOptions.Properties.AbsoluteExpiryTime
}
if sco.StreamFilterOptions.Properties.CreationTime != nil {
l["creation-time"] = sco.StreamFilterOptions.Properties.CreationTime
}
if sco.StreamFilterOptions.Properties.GroupSequence != nil {
l["group-sequence"] = *sco.StreamFilterOptions.Properties.GroupSequence
}
if sco.StreamFilterOptions.Properties.ReplyToGroupID != nil {
l["reply-to-group-id"] = *sco.StreamFilterOptions.Properties.ReplyToGroupID
}
if len(l) > 0 {
filters = append(filters, amqp.NewLinkFilter(amqpPropertiesFilter, 0, l))
}
}
return filters return filters
} }
///// ProducerOptions ///// func (sco *StreamConsumerOptions) id() string {
return sco.Id
type ProducerOptions interface {
linkName() string
} }
type AMQPProducerOptions struct { func (sco *StreamConsumerOptions) validate(available *featuresAvailable) error {
if sco.StreamFilterOptions != nil && sco.StreamFilterOptions.Properties != nil {
if !available.is41OrMore {
return fmt.Errorf("stream consumer with properties filter is not supported. You need RabbitMQ 4.1 or later")
}
}
return nil
}
///// PublisherOptions /////
type IPublisherOptions interface {
linkName() string
id() string
}
type PublisherOptions struct {
Id string
SenderLinkName string SenderLinkName string
} }
func (apo *AMQPProducerOptions) linkName() string { func (apo *PublisherOptions) 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 ConsumerOptions, deliveryMode int) *amqp.ReceiverOptions { func createReceiverLinkOptions(address string, options IConsumerOptions, 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,6 +29,7 @@ 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() {
v, err := CapacityFrom("10LL") _, 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"))
v, err = CapacityFrom("aGB") _, 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 entityIdentifier interface { type iEntityIdentifier interface {
Id() string Id() string
} }
@ -21,9 +21,9 @@ func (e QueueType) String() string {
} }
/* /*
QueueSpecification represents the specification of a queue IQueueSpecification represents the specification of a queue
*/ */
type QueueSpecification interface { type IQueueSpecification interface {
name() string name() string
isAutoDelete() bool isAutoDelete() bool
isExclusive() bool isExclusive() bool
@ -31,7 +31,7 @@ type QueueSpecification interface {
buildArguments() map[string]any buildArguments() map[string]any
} }
type OverflowStrategy interface { type IOverflowStrategy interface {
overflowStrategy() string overflowStrategy() string
} }
@ -56,7 +56,7 @@ func (r *RejectPublishDlxOverflowStrategy) overflowStrategy() string {
return "reject-publish-dlx" return "reject-publish-dlx"
} }
type LeaderLocator interface { type ILeaderLocator interface {
leaderLocator() string leaderLocator() string
} }
@ -79,18 +79,19 @@ QuorumQueueSpecification represents the specification of the quorum queue
*/ */
type QuorumQueueSpecification struct { type QuorumQueueSpecification struct {
Name string Name string
AutoExpire int64 AutoExpire int64
MessageTTL int64 MessageTTL int64
OverflowStrategy OverflowStrategy OverflowStrategy IOverflowStrategy
SingleActiveConsumer bool SingleActiveConsumer bool
DeadLetterExchange string DeadLetterExchange string
DeadLetterRoutingKey string DeadLetterRoutingKey string
MaxLength int64 MaxLength int64
MaxLengthBytes int64 MaxLengthBytes int64
DeliveryLimit int64 DeliveryLimit int64
TargetClusterSize int64 TargetClusterSize int64
LeaderLocator LeaderLocator LeaderLocator ILeaderLocator
QuorumInitialGroupSize int
} }
func (q *QuorumQueueSpecification) name() string { func (q *QuorumQueueSpecification) name() string {
@ -155,6 +156,10 @@ 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
} }
@ -168,14 +173,14 @@ type ClassicQueueSpecification struct {
IsExclusive bool IsExclusive bool
AutoExpire int64 AutoExpire int64
MessageTTL int64 MessageTTL int64
OverflowStrategy OverflowStrategy OverflowStrategy IOverflowStrategy
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 LeaderLocator LeaderLocator ILeaderLocator
} }
func (q *ClassicQueueSpecification) name() string { func (q *ClassicQueueSpecification) name() string {
@ -344,8 +349,8 @@ func (e ExchangeType) String() string {
return string(e.Type) return string(e.Type)
} }
// ExchangeSpecification represents the specification of an exchange // IExchangeSpecification represents the specification of an exchange
type ExchangeSpecification interface { type IExchangeSpecification interface {
name() string name() string
isAutoDelete() bool isAutoDelete() bool
exchangeType() ExchangeType exchangeType() ExchangeType
@ -465,7 +470,7 @@ func (c *CustomExchangeSpecification) arguments() map[string]any {
// / **** Binding **** // / **** Binding ****
type BindingSpecification interface { type IBindingSpecification interface {
sourceExchange() string sourceExchange() string
destination() string destination() string
bindingKey() string bindingKey() string

View File

@ -2,6 +2,7 @@ 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"
) )
@ -108,6 +109,24 @@ 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 LifeCycleState interface { type ILifeCycleState interface {
getState() int getState() int
} }
@ -49,7 +49,7 @@ const (
closed = iota closed = iota
) )
func statusToString(status LifeCycleState) string { func statusToString(status ILifeCycleState) string {
switch status.getState() { switch status.getState() {
case open: case open:
return "open" return "open"
@ -65,8 +65,8 @@ func statusToString(status LifeCycleState) string {
} }
type StateChanged struct { type StateChanged struct {
From LifeCycleState From ILifeCycleState
To LifeCycleState To ILifeCycleState
} }
func (s StateChanged) String() string { func (s StateChanged) String() string {
@ -77,6 +77,9 @@ 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)
} }
@ -85,7 +88,7 @@ func (s StateChanged) String() string {
} }
type LifeCycle struct { type LifeCycle struct {
state LifeCycleState state ILifeCycleState
chStatusChanged chan *StateChanged chStatusChanged chan *StateChanged
mutex *sync.Mutex mutex *sync.Mutex
} }
@ -97,13 +100,13 @@ func NewLifeCycle() *LifeCycle {
} }
} }
func (l *LifeCycle) State() LifeCycleState { func (l *LifeCycle) State() ILifeCycleState {
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 LifeCycleState) { func (l *LifeCycle) SetState(value ILifeCycleState) {
l.mutex.Lock() l.mutex.Lock()
defer l.mutex.Unlock() defer l.mutex.Unlock()
if l.state == value { if l.state == value {
@ -122,3 +125,9 @@ func (l *LifeCycle) SetState(value LifeCycleState) {
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 TargetAddress) error { func MessagePropertyToAddress(msgRef *amqp.Message, target ITargetAddress) 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 TargetAddress) (*amqp.Message, error) { func NewMessageWithAddress(body []byte, target ITargetAddress) (*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

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

View File

@ -8,5 +8,30 @@ import (
func generateNameWithDateTime(name string) string { 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,7 +62,6 @@ func DropConnectionContainerID(Id string) error {
if err != nil { if err != nil {
return err return err
} }
return nil return nil
} }
@ -74,6 +73,7 @@ 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,6 +106,11 @@ 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
} }