From 28e1e459b2a927d72155c19f692f197fce46df30 Mon Sep 17 00:00:00 2001 From: Luke Bakken Date: Thu, 11 May 2023 10:04:20 -0700 Subject: [PATCH] Implement async for simple AMQP methods Related to: * #1345 * #1308 * #970 * #843 Separate out Unit, Integration and Parallel Integration tests * Creates dedicated test projects for parallel test execution (AsyncIntegration / Integration.csproj) and sequential (SequentialIntegration.csproj). * Ensures that the ThreadPool is set with enough threads. * Ensures that all test connections have their client provided name set. * Fix SequentialTests that require a unique connection name. * Shorten up test names used for connection client provided names * Ensure all async tests are in the AsyncIntegration project. * Convert MassPublish to async/await with multiple publishing connections. * Add MaxParallelThreads in a csproj comment in case we want to try that out * Wait longer when IsRunningInCI * Introduce the RentedMemory struct to encapsulate a rented byte array and its associated ReadOnlyMemory. * Add CreateChannelAsync and modify AsyncIntegration tests to use it. * Add CreateConnectionAsync * Use CreateConnectionAsync in AsyncIntegration --- .ci/oauth2/setup.sh | 10 +- .ci/oauth2/test.sh | 2 +- .ci/ubuntu/rabbitmq.conf | 5 +- .ci/windows/gha-run-tests.ps1 | 39 - .ci/windows/gha-setup.ps1 | 14 +- .ci/windows/rabbitmq.conf.in | 5 +- .git-blame-ignore-revs | 5 + .github/workflows/build-test.yaml | 191 ++- .gitignore | 6 +- Build.csproj | 12 +- RUNNING_TESTS.md | 6 +- RabbitMQDotNetClient.sln | 49 +- build.ps1 | 2 +- .../AsyncBasicConsumerFake.cs | 5 +- .../ConsumerDispatching/ConsumerDispatcher.cs | 19 +- .../WireFormatting/MethodFraming.cs | 10 +- .../RabbitMQ.Client/RabbitMQ.Client.csproj | 12 + .../RabbitMQ.Client/client/ClientArrayPool.cs | 107 ++ .../RabbitMQ.Client/client/RentedMemory.cs | 97 ++ .../client/api/AmqpTimestamp.cs | 3 - .../client/api/AsyncDefaultBasicConsumer.cs | 5 +- .../client/api/BasicGetResult.cs | 50 +- .../client/api/ConnectionFactory.cs | 166 ++- .../client/api/DefaultBasicConsumer.cs | 2 +- .../client/api/IAsyncBasicConsumer.cs | 3 +- .../client/api/IBasicConsumer.cs | 2 +- .../RabbitMQ.Client/client/api/IChannel.cs | 255 +++- .../client/api/IChannelExtensions.cs | 23 +- .../RabbitMQ.Client/client/api/IConnection.cs | 6 + .../client/api/IConnectionFactory.cs | 73 +- .../events/AsyncEventingBasicConsumer.cs | 4 +- .../client/events/BasicDeliverEventArgs.cs | 4 +- .../client/events/BasicReturnEventArgs.cs | 23 +- .../client/events/EventingBasicConsumer.cs | 3 +- .../RabbitMQ.Client/client/framing/Channel.cs | 50 +- .../client/impl/AsyncRpcContinuations.cs | 479 ++++++++ .../client/impl/AutorecoveringChannel.cs | 172 ++- .../impl/AutorecoveringConnection.Recovery.cs | 3 + .../client/impl/AutorecoveringConnection.cs | 39 +- .../client/impl/ChannelBase.cs | 1084 ++++++++++++----- .../client/impl/CommandAssembler.cs | 46 +- .../client/impl/Connection.Commands.cs | 8 - .../client/impl/Connection.Receive.cs | 2 + .../RabbitMQ.Client/client/impl/Connection.cs | 61 +- .../AsyncConsumerDispatcher.cs | 44 +- .../ConsumerDispatching/ConsumerDispatcher.cs | 55 +- .../ConsumerDispatcherChannelBase.cs | 14 +- .../ConsumerDispatching/FallbackConsumer.cs | 8 +- .../IConsumerDispatcher.cs | 4 +- projects/RabbitMQ.Client/client/impl/Frame.cs | 32 +- .../client/impl/IFrameHandler.cs | 2 +- .../RabbitMQ.Client/client/impl/ISession.cs | 8 +- .../client/impl/IncomingCommand.cs | 90 +- .../client/impl/RecoveryAwareChannel.cs | 40 + ...RpcContinuation.cs => RpcContinuations.cs} | 62 +- .../client/impl/SocketFrameHandler.cs | 49 +- .../CreateChannel/CreateChannel.csproj | 2 +- .../Applications}/CreateChannel/Program.cs | 0 .../MassPublish/MassPublish.csproj | 3 +- .../Test/Applications/MassPublish/Program.cs | 178 +++ .../AsyncIntegration/AsyncIntegration.csproj | 54 + .../AsyncIntegrationFixture.cs | 91 ++ .../AsyncIntegration/TestAsyncConsumer.cs | 469 +++++++ .../TestAsyncConsumerExceptions.cs | 88 +- .../AsyncIntegration/TestBasicGetAsync.cs | 62 + .../AsyncIntegration/TestBasicPublishAsync.cs | 74 ++ ...ncurrentAccessWithSharedConnectionAsync.cs | 161 +++ .../TestConfirmSelectAsync.cs | 69 ++ .../TestExchangeDeclareAsync.cs | 103 ++ .../AsyncIntegration/TestExtensionsAsync.cs} | 48 +- .../TestFloodPublishingAsync.cs | 213 ++++ .../TestMessageCountAsync.cs} | 13 +- .../TestPassiveDeclareAsync.cs | 67 + .../TestPublishSharedChannelAsync.cs} | 90 +- .../TestPublisherConfirmsAsync.cs} | 77 +- .../AsyncIntegration/TestQueueDeclareAsync.cs | 147 +++ projects/Test/Common/Common.csproj | 38 + projects/Test/Common/IntegrationFixture.cs | 365 ++++++ projects/{Unit => Test/Common}/RabbitMQCtl.cs | 243 ++-- .../Common/TimingFixture.cs} | 31 +- projects/Test/Integration/Integration.csproj | 51 + projects/{Unit => Test/Integration}/SslEnv.cs | 17 +- .../{Unit => Test/Integration}/TestAuth.cs | 25 +- .../Integration}/TestBasicGet.cs | 41 +- projects/Test/Integration/TestBasicPublish.cs | 294 +++++ .../Integration}/TestChannelAllocation.cs | 76 +- .../Integration}/TestChannelShutdown.cs | 5 +- .../Integration}/TestChannelSoftErrors.cs | 3 +- ...estConcurrentAccessWithSharedConnection.cs | 165 +++ .../Integration}/TestConfirmSelect.cs | 3 +- .../Integration}/TestConnectionFactory.cs | 182 +-- ...estConnectionFactoryContinuationTimeout.cs | 20 +- .../Integration}/TestConnectionShutdown.cs | 28 +- projects/Test/Integration/TestConsumer.cs | 142 +++ .../Integration}/TestConsumerCancelNotify.cs | 3 +- .../Integration}/TestConsumerCount.cs | 3 +- .../Integration}/TestConsumerExceptions.cs | 5 +- .../TestConsumerOperationDispatch.cs | 60 +- .../Integration}/TestEventingConsumer.cs | 7 +- .../Integration}/TestExceptionMessages.cs | 2 +- .../Integration}/TestExchangeDeclare.cs | 50 +- .../Integration}/TestHeartbeats.cs | 112 +- .../Integration}/TestInitialConnection.cs | 7 +- .../Integration}/TestInvalidAck.cs | 3 +- .../Integration}/TestMainLoop.cs | 22 +- .../{Unit => Test/Integration}/TestNowait.cs | 3 +- .../Integration}/TestPassiveDeclare.cs | 2 +- .../Integration}/TestQueueDeclare.cs | 70 +- .../{Unit => Test/Integration}/TestSsl.cs | 65 +- .../Integration}/TestUpdateSecret.cs | 3 +- .../OAuth2}/APIApproval.Approve.verified.txt | 0 .../OAuth2}/APIApproval.cs | 0 .../OAuth2/OAuth2.csproj} | 13 +- .../{OAuth2Test => Test/OAuth2}/README.md | 0 .../OAuth2}/RequestFormMatcher.cs | 0 .../{OAuth2Test => Test/OAuth2}/TestOAuth2.cs | 5 +- .../OAuth2}/TestOAuth2Client.cs | 0 .../TestOAuth2ClientCredentialsProvider.cs | 0 .../OAuth2}/enabled_plugins | 0 .../OAuth2}/keycloak/import/test-realm.json | 0 .../OAuth2}/keycloak/rabbitmq.conf | 0 .../keycloak/signing-key/signing-key.pem | 0 .../OAuth2}/uaa/log4j2.properties | 0 .../OAuth2}/uaa/rabbitmq.conf | 0 .../OAuth2}/uaa/signing-key/signing-key.pem | 0 .../{OAuth2Test => Test/OAuth2}/uaa/uaa.yml | 0 .../SequentialIntegration.csproj | 50 + .../SequentialIntegrationFixture.cs | 89 ++ .../TestConnectionBlocked.cs | 6 +- .../TestConnectionRecovery.cs | 656 +--------- .../TestConnectionRecoveryBase.cs | 389 ++++++ .../TestConnectionRecoveryWithoutSetup.cs | 333 +++++ .../Unit/APIApproval.Approve.verified.txt | 92 +- projects/{ => Test}/Unit/APIApproval.cs | 2 +- .../{Unit/Helper => Test/Unit}/DebugUtil.cs | 4 +- .../Unit/TestAmqpTcpEndpointParsing.cs | 3 +- projects/{ => Test}/Unit/TestAmqpUri.cs | 3 +- .../{ => Test}/Unit/TestBasicProperties.cs | 47 +- projects/{ => Test}/Unit/TestBlockingCell.cs | 2 +- .../{ => Test}/Unit/TestContentHeaderCodec.cs | 3 +- .../Unit/TestFieldTableFormatting.cs | 4 +- .../Unit/TestFieldTableFormattingGeneric.cs | 4 +- .../{ => Test}/Unit/TestFrameFormatting.cs | 33 +- .../Unit/TestIEndpointResolverExtensions.cs | 3 +- projects/{ => Test}/Unit/TestIntAllocator.cs | 2 +- .../Unit/TestMethodArgumentCodec.cs | 3 +- .../Unit/TestNetworkByteOrderSerialization.cs | 3 +- .../{ => Test}/Unit/TestPublicationAddress.cs | 3 +- .../Unit/TestRpcContinuationQueue.cs | 2 +- .../{ => Test}/Unit/TestTcpClientAdapter.cs | 2 +- .../Unit/TestTimerBasedCredentialRefresher.cs | 3 +- .../Unit/TimingFixture.cs} | 31 +- projects/{ => Test}/Unit/Unit.csproj | 11 +- .../{ => Test}/Unit/WireFormattingFixture.cs | 2 +- .../TestApplications/MassPublish/Program.cs | 85 -- projects/Unit/Fixtures.cs | 513 -------- projects/Unit/TestAsyncConsumer.cs | 358 ------ projects/Unit/TestBasicPublish.cs | 256 ---- ...estConcurrentAccessWithSharedConnection.cs | 223 ---- projects/Unit/TestConsumer.cs | 96 -- projects/Unit/TestFloodPublishing.cs | 166 --- projects/Unit/TestRecoverAfterCancel.cs | 109 -- 162 files changed, 7294 insertions(+), 4040 deletions(-) delete mode 100644 .ci/windows/gha-run-tests.ps1 create mode 100644 .git-blame-ignore-revs create mode 100644 projects/RabbitMQ.Client/client/ClientArrayPool.cs create mode 100644 projects/RabbitMQ.Client/client/RentedMemory.cs create mode 100644 projects/RabbitMQ.Client/client/impl/AsyncRpcContinuations.cs rename projects/RabbitMQ.Client/client/impl/{SimpleBlockingRpcContinuation.cs => RpcContinuations.cs} (62%) rename projects/{TestApplications => Test/Applications}/CreateChannel/CreateChannel.csproj (83%) rename projects/{TestApplications => Test/Applications}/CreateChannel/Program.cs (100%) rename projects/{TestApplications => Test/Applications}/MassPublish/MassPublish.csproj (77%) create mode 100644 projects/Test/Applications/MassPublish/Program.cs create mode 100644 projects/Test/AsyncIntegration/AsyncIntegration.csproj create mode 100644 projects/Test/AsyncIntegration/AsyncIntegrationFixture.cs create mode 100644 projects/Test/AsyncIntegration/TestAsyncConsumer.cs rename projects/{Unit => Test/AsyncIntegration}/TestAsyncConsumerExceptions.cs (68%) create mode 100644 projects/Test/AsyncIntegration/TestBasicGetAsync.cs create mode 100644 projects/Test/AsyncIntegration/TestBasicPublishAsync.cs create mode 100644 projects/Test/AsyncIntegration/TestConcurrentAccessWithSharedConnectionAsync.cs create mode 100644 projects/Test/AsyncIntegration/TestConfirmSelectAsync.cs create mode 100644 projects/Test/AsyncIntegration/TestExchangeDeclareAsync.cs rename projects/{Unit/TestExtensions.cs => Test/AsyncIntegration/TestExtensionsAsync.cs} (52%) create mode 100644 projects/Test/AsyncIntegration/TestFloodPublishingAsync.cs rename projects/{Unit/TestMessageCount.cs => Test/AsyncIntegration/TestMessageCountAsync.cs} (79%) create mode 100644 projects/Test/AsyncIntegration/TestPassiveDeclareAsync.cs rename projects/{Unit/TestPublishSharedChannel.cs => Test/AsyncIntegration/TestPublishSharedChannelAsync.cs} (54%) rename projects/{Unit/TestPublisherConfirms.cs => Test/AsyncIntegration/TestPublisherConfirmsAsync.cs} (62%) create mode 100644 projects/Test/AsyncIntegration/TestQueueDeclareAsync.cs create mode 100644 projects/Test/Common/Common.csproj create mode 100644 projects/Test/Common/IntegrationFixture.cs rename projects/{Unit => Test/Common}/RabbitMQCtl.cs (67%) rename projects/{RabbitMQ.Client/client/framing/BasicRecoverAsync.cs => Test/Common/TimingFixture.cs} (67%) create mode 100644 projects/Test/Integration/Integration.csproj rename projects/{Unit => Test/Integration}/SslEnv.cs (85%) rename projects/{Unit => Test/Integration}/TestAuth.cs (78%) rename projects/{Unit => Test/Integration}/TestBasicGet.cs (68%) create mode 100644 projects/Test/Integration/TestBasicPublish.cs rename projects/{Unit => Test/Integration}/TestChannelAllocation.cs (66%) rename projects/{Unit => Test/Integration}/TestChannelShutdown.cs (94%) rename projects/{Unit => Test/Integration}/TestChannelSoftErrors.cs (98%) create mode 100644 projects/Test/Integration/TestConcurrentAccessWithSharedConnection.cs rename projects/{Unit => Test/Integration}/TestConfirmSelect.cs (97%) rename projects/{Unit => Test/Integration}/TestConnectionFactory.cs (66%) rename projects/{Unit => Test/Integration}/TestConnectionFactoryContinuationTimeout.cs (78%) rename projects/{Unit => Test/Integration}/TestConnectionShutdown.cs (84%) create mode 100644 projects/Test/Integration/TestConsumer.cs rename projects/{Unit => Test/Integration}/TestConsumerCancelNotify.cs (99%) rename projects/{Unit => Test/Integration}/TestConsumerCount.cs (97%) rename projects/{Unit => Test/Integration}/TestConsumerExceptions.cs (98%) rename projects/{Unit => Test/Integration}/TestConsumerOperationDispatch.cs (87%) rename projects/{Unit => Test/Integration}/TestEventingConsumer.cs (96%) rename projects/{Unit => Test/Integration}/TestExceptionMessages.cs (98%) rename projects/{Unit => Test/Integration}/TestExchangeDeclare.cs (60%) rename projects/{Unit => Test/Integration}/TestHeartbeats.cs (62%) rename projects/{Unit => Test/Integration}/TestInitialConnection.cs (88%) rename projects/{Unit => Test/Integration}/TestInvalidAck.cs (97%) rename projects/{Unit => Test/Integration}/TestMainLoop.cs (80%) rename projects/{Unit => Test/Integration}/TestNowait.cs (98%) rename projects/{Unit => Test/Integration}/TestPassiveDeclare.cs (98%) rename projects/{Unit => Test/Integration}/TestQueueDeclare.cs (63%) rename projects/{Unit => Test/Integration}/TestSsl.cs (70%) rename projects/{Unit => Test/Integration}/TestUpdateSecret.cs (95%) rename projects/{OAuth2Test => Test/OAuth2}/APIApproval.Approve.verified.txt (100%) rename projects/{OAuth2Test => Test/OAuth2}/APIApproval.cs (100%) rename projects/{OAuth2Test/OAuth2Test.csproj => Test/OAuth2/OAuth2.csproj} (75%) rename projects/{OAuth2Test => Test/OAuth2}/README.md (100%) rename projects/{OAuth2Test => Test/OAuth2}/RequestFormMatcher.cs (100%) rename projects/{OAuth2Test => Test/OAuth2}/TestOAuth2.cs (97%) rename projects/{OAuth2Test => Test/OAuth2}/TestOAuth2Client.cs (100%) rename projects/{OAuth2Test => Test/OAuth2}/TestOAuth2ClientCredentialsProvider.cs (100%) rename projects/{OAuth2Test => Test/OAuth2}/enabled_plugins (100%) rename projects/{OAuth2Test => Test/OAuth2}/keycloak/import/test-realm.json (100%) rename projects/{OAuth2Test => Test/OAuth2}/keycloak/rabbitmq.conf (100%) rename projects/{OAuth2Test => Test/OAuth2}/keycloak/signing-key/signing-key.pem (100%) rename projects/{OAuth2Test => Test/OAuth2}/uaa/log4j2.properties (100%) rename projects/{OAuth2Test => Test/OAuth2}/uaa/rabbitmq.conf (100%) rename projects/{OAuth2Test => Test/OAuth2}/uaa/signing-key/signing-key.pem (100%) rename projects/{OAuth2Test => Test/OAuth2}/uaa/uaa.yml (100%) create mode 100644 projects/Test/SequentialIntegration/SequentialIntegration.csproj create mode 100644 projects/Test/SequentialIntegration/SequentialIntegrationFixture.cs rename projects/{Unit => Test/SequentialIntegration}/TestConnectionBlocked.cs (96%) rename projects/{Unit => Test/SequentialIntegration}/TestConnectionRecovery.cs (67%) create mode 100644 projects/Test/SequentialIntegration/TestConnectionRecoveryBase.cs create mode 100644 projects/Test/SequentialIntegration/TestConnectionRecoveryWithoutSetup.cs rename projects/{ => Test}/Unit/APIApproval.Approve.verified.txt (88%) rename projects/{ => Test}/Unit/APIApproval.cs (98%) rename projects/{Unit/Helper => Test/Unit}/DebugUtil.cs (98%) rename projects/{ => Test}/Unit/TestAmqpTcpEndpointParsing.cs (99%) rename projects/{ => Test}/Unit/TestAmqpUri.cs (99%) rename projects/{ => Test}/Unit/TestBasicProperties.cs (81%) rename projects/{ => Test}/Unit/TestBlockingCell.cs (99%) rename projects/{ => Test}/Unit/TestContentHeaderCodec.cs (99%) rename projects/{ => Test}/Unit/TestFieldTableFormatting.cs (99%) rename projects/{ => Test}/Unit/TestFieldTableFormattingGeneric.cs (99%) rename projects/{ => Test}/Unit/TestFrameFormatting.cs (85%) rename projects/{ => Test}/Unit/TestIEndpointResolverExtensions.cs (98%) rename projects/{ => Test}/Unit/TestIntAllocator.cs (98%) rename projects/{ => Test}/Unit/TestMethodArgumentCodec.cs (99%) rename projects/{ => Test}/Unit/TestNetworkByteOrderSerialization.cs (99%) rename projects/{ => Test}/Unit/TestPublicationAddress.cs (98%) rename projects/{ => Test}/Unit/TestRpcContinuationQueue.cs (98%) rename projects/{ => Test}/Unit/TestTcpClientAdapter.cs (98%) rename projects/{ => Test}/Unit/TestTimerBasedCredentialRefresher.cs (99%) rename projects/{RabbitMQ.Client/client/framing/BasicRecover.cs => Test/Unit/TimingFixture.cs} (67%) rename projects/{ => Test}/Unit/Unit.csproj (79%) rename projects/{ => Test}/Unit/WireFormattingFixture.cs (98%) delete mode 100644 projects/TestApplications/MassPublish/Program.cs delete mode 100644 projects/Unit/Fixtures.cs delete mode 100644 projects/Unit/TestAsyncConsumer.cs delete mode 100644 projects/Unit/TestBasicPublish.cs delete mode 100644 projects/Unit/TestConcurrentAccessWithSharedConnection.cs delete mode 100644 projects/Unit/TestConsumer.cs delete mode 100644 projects/Unit/TestFloodPublishing.cs delete mode 100644 projects/Unit/TestRecoverAfterCancel.cs diff --git a/.ci/oauth2/setup.sh b/.ci/oauth2/setup.sh index 49af95fb16..8c11cee739 100755 --- a/.ci/oauth2/setup.sh +++ b/.ci/oauth2/setup.sh @@ -46,9 +46,9 @@ function start_rabbitmq --network "$docker_network" \ --publish 5672:5672 \ --publish 15672:15672 \ - --volume "$GITHUB_WORKSPACE/projects/OAuth2Test/enabled_plugins:/etc/rabbitmq/enabled_plugins" \ - --volume "$GITHUB_WORKSPACE/projects/OAuth2Test/$mode/rabbitmq.conf:/etc/rabbitmq/rabbitmq.conf:ro" \ - --volume "$GITHUB_WORKSPACE/projects/OAuth2Test/$mode/signing-key/signing-key.pem:/etc/rabbitmq/signing-key.pem:ro" \ + --volume "$GITHUB_WORKSPACE/projects/Test/OAuth2/enabled_plugins:/etc/rabbitmq/enabled_plugins" \ + --volume "$GITHUB_WORKSPACE/projects/Test/OAuth2/$mode/rabbitmq.conf:/etc/rabbitmq/rabbitmq.conf:ro" \ + --volume "$GITHUB_WORKSPACE/projects/Test/OAuth2/$mode/signing-key/signing-key.pem:/etc/rabbitmq/signing-key.pem:ro" \ rabbitmq:3-management } @@ -90,7 +90,7 @@ function start_oauth_service --publish 8080:8080 \ --env 'UAA_CONFIG_PATH=/uaa' \ --env 'JAVA_OPTS=-Djava.security.egd=file:/dev/./urandom' \ - --volume "$GITHUB_WORKSPACE/projects/OAuth2Test/uaa:/uaa" \ + --volume "$GITHUB_WORKSPACE/projects/Test/OAuth2/uaa:/uaa" \ "cloudfoundry/uaa:$uaa_image_version" else readonly keycloak_docker_name="$docker_name_prefix-keycloak" @@ -101,7 +101,7 @@ function start_oauth_service --env 'KEYCLOAK_ADMIN=admin' \ --env 'KEYCLOAK_ADMIN_PASSWORD=admin' \ --env KC_HEALTH_ENABLED=true \ - --volume "$GITHUB_WORKSPACE/projects/OAuth2Test/keycloak/import:/opt/keycloak/data/import" \ + --volume "$GITHUB_WORKSPACE/projects/Test/OAuth2/keycloak/import:/opt/keycloak/data/import" \ "quay.io/keycloak/keycloak:$keycloak_image_version" start-dev --metrics-enabled=true --import-realm fi } diff --git a/.ci/oauth2/test.sh b/.ci/oauth2/test.sh index e99f5449fa..eda1f159b6 100755 --- a/.ci/oauth2/test.sh +++ b/.ci/oauth2/test.sh @@ -12,4 +12,4 @@ source "$script_dir/common.sh" export OAUTH2_MODE="$mode" -dotnet test --environment OAUTH2_MODE="$mode" "$GITHUB_WORKSPACE/projects/OAuth2Test/OAuth2Test.csproj" --logger "console;verbosity=detailed" --framework "net6.0" +dotnet test --environment OAUTH2_MODE="$mode" "$GITHUB_WORKSPACE/projects/Test/OAuth2/OAuth2.csproj" --logger "console;verbosity=detailed" diff --git a/.ci/ubuntu/rabbitmq.conf b/.ci/ubuntu/rabbitmq.conf index ba2a758c03..d03fa510cd 100644 --- a/.ci/ubuntu/rabbitmq.conf +++ b/.ci/ubuntu/rabbitmq.conf @@ -1,6 +1,9 @@ log.console = false +log.exchange = false log.file = /var/log/rabbitmq/rabbitmq.log -log.file.level = debug +log.file.level = info +# log.connection.level = warning +# log.channel.level = warning listeners.tcp.default = 5672 listeners.ssl.default = 5671 reverse_dns_lookups = false diff --git a/.ci/windows/gha-run-tests.ps1 b/.ci/windows/gha-run-tests.ps1 deleted file mode 100644 index d75f977e57..0000000000 --- a/.ci/windows/gha-run-tests.ps1 +++ /dev/null @@ -1,39 +0,0 @@ -$ProgressPreference = 'Continue' -$ErrorActionPreference = 'Stop' -Set-StrictMode -Version 2.0 - -$erlang_reg_path = 'HKLM:\SOFTWARE\Ericsson\Erlang' -if (Test-Path 'HKLM:\SOFTWARE\WOW6432Node\') -{ - $erlang_reg_path = 'HKLM:\SOFTWARE\WOW6432Node\Ericsson\Erlang' -} -$erlang_erts_version = Get-ChildItem -Path $erlang_reg_path -Name -$erlang_home = (Get-ItemProperty -LiteralPath $erlang_reg_path\$erlang_erts_version).'(default)' - -Write-Host "[INFO] Setting ERLANG_HOME to '$erlang_home'..." -$env:ERLANG_HOME = $erlang_home -[Environment]::SetEnvironmentVariable('ERLANG_HOME', $erlang_home, 'Machine') - -$rabbitmq_base_path = (Get-ItemProperty -Name Install_Dir -Path 'HKLM:\SOFTWARE\WOW6432Node\VMware, Inc.\RabbitMQ Server').Install_Dir -$regPath = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\RabbitMQ' -if (Test-Path 'HKLM:\SOFTWARE\WOW6432Node\') -{ - $regPath = 'HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\RabbitMQ' -} -$rabbitmq_version = (Get-ItemProperty $regPath "DisplayVersion").DisplayVersion -$rabbitmqctl_path = Resolve-Path -LiteralPath (Join-Path -Path $rabbitmq_base_path -ChildPath "rabbitmq_server-$rabbitmq_version" | Join-Path -ChildPath 'sbin' | Join-Path -ChildPath 'rabbitmqctl.bat') - -Write-Host "[INFO] Setting RABBITMQ_RABBITMQCTL_PATH to '$rabbitmqctl_path'..." -$env:RABBITMQ_RABBITMQCTL_PATH = $rabbitmqctl_path -[Environment]::SetEnvironmentVariable('RABBITMQ_RABBITMQCTL_PATH', $rabbitmqctl_path, 'Machine') - -New-Variable -Name ci_dir -Option Constant -Value (Join-Path -Path $env:GITHUB_WORKSPACE -ChildPath '.ci') -New-Variable -Name certs_dir -Option Constant -Value (Join-Path -Path $ci_dir -ChildPath 'certs') - -$csproj_file = Resolve-Path -LiteralPath (Join-Path -Path $env:GITHUB_WORKSPACE -ChildPath 'projects' | Join-Path -ChildPath 'Unit' | Join-Path -ChildPath 'Unit.csproj') - -dotnet test --environment "RABBITMQ_RABBITMQCTL_PATH=$rabbitmqctl_path" ` - --environment 'RABBITMQ_LONG_RUNNING_TESTS=true' ` - --environment 'PASSWORD=grapefruit' ` - --environment "SSL_CERTS_DIR=$certs_dir" ` - $csproj_file --no-restore --no-build --logger "console;verbosity=detailed" diff --git a/.ci/windows/gha-setup.ps1 b/.ci/windows/gha-setup.ps1 index 605b851250..2f1cf929c2 100644 --- a/.ci/windows/gha-setup.ps1 +++ b/.ci/windows/gha-setup.ps1 @@ -65,6 +65,7 @@ $erlang_home = (Get-ItemProperty -LiteralPath $erlang_reg_path\$erlang_erts_vers Write-Host "[INFO] Setting ERLANG_HOME to '$erlang_home'..." $env:ERLANG_HOME = $erlang_home [Environment]::SetEnvironmentVariable('ERLANG_HOME', $erlang_home, 'Machine') +Add-Content -Verbose -LiteralPath $env:GITHUB_ENV -Value "ERLANG_HOME=$erlang_home" Write-Host "[INFO] Setting RABBITMQ_SERVER_ADDITIONAL_ERL_ARGS..." $env:RABBITMQ_SERVER_ADDITIONAL_ERL_ARGS = '-rabbitmq_stream advertised_host localhost' @@ -189,6 +190,17 @@ Write-Host '[INFO] Enabling plugins...' & $rabbitmq_plugins_path enable rabbitmq_management rabbitmq_stream rabbitmq_stream_management rabbitmq_amqp1_0 echo Q | openssl s_client -connect localhost:5671 -CAfile "$certs_dir/ca_certificate.pem" -cert "$certs_dir/client_localhost_certificate.pem" -key "$certs_dir/client_localhost_key.pem" -pass pass:grapefruit -if ($LASTEXITCODE -ne 0) { +if ($LASTEXITCODE -ne 0) +{ throw "[ERROR] 'openssl s_client' returned error: $LASTEXITCODE" } + + +$rabbitmqctl_path = Resolve-Path -LiteralPath ` + (Join-Path -Path $rabbitmq_base_path -ChildPath "rabbitmq_server-$rabbitmq_version" | Join-Path -ChildPath 'sbin' | Join-Path -ChildPath 'rabbitmqctl.bat') + +Write-Host "[INFO] Setting RABBITMQ_RABBITMQCTL_PATH to '$rabbitmqctl_path'..." +$env:RABBITMQ_RABBITMQCTL_PATH = $rabbitmqctl_path +[Environment]::SetEnvironmentVariable('RABBITMQ_RABBITMQCTL_PATH', $rabbitmqctl_path, 'Machine') +Add-Content -Verbose -LiteralPath $env:GITHUB_OUTPUT -Value "path=$rabbitmqctl_path" +Add-Content -Verbose -LiteralPath $env:GITHUB_ENV -Value "RABBITMQ_RABBITMQCTL_PATH=$rabbitmqctl_path" diff --git a/.ci/windows/rabbitmq.conf.in b/.ci/windows/rabbitmq.conf.in index 0923dbe6c7..dfcb44c51e 100644 --- a/.ci/windows/rabbitmq.conf.in +++ b/.ci/windows/rabbitmq.conf.in @@ -1,5 +1,8 @@ log.console = false -log.file.level = debug +log.exchange = false +log.file.level = info +# log.connection.level = warning +# log.channel.level = warning listeners.tcp.default = 5672 listeners.ssl.default = 5671 reverse_dns_lookups = false diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs new file mode 100644 index 0000000000..a9209d0db8 --- /dev/null +++ b/.git-blame-ignore-revs @@ -0,0 +1,5 @@ +# https://github.com/rabbitmq/rabbitmq-dotnet-client/commit/1713f50eb2dc52a97184f3857f70841dd55b5bef +1713f50eb2dc52a97184f3857f70841dd55b5bef +67c02d79d3ae48fea7de93c758dce91a51d14988 +# Revert the above +6b1a06bd429f395891a3230cad92e674dcbbb0d2 diff --git a/.github/workflows/build-test.yaml b/.github/workflows/build-test.yaml index a7e4d6aa5b..38b1c51317 100644 --- a/.github/workflows/build-test.yaml +++ b/.github/workflows/build-test.yaml @@ -5,7 +5,7 @@ on: jobs: build-win32: - name: build/test on windows-latest + name: build, unit test on windows-latest runs-on: windows-latest # https://github.com/NuGet/Home/issues/11548 env: @@ -15,13 +15,6 @@ jobs: uses: actions/checkout@v4 with: submodules: true - - name: Cache installers - uses: actions/cache@v3 - with: - # Note: the cache path is relative to the workspace directory - # https://docs.github.com/en/actions/using-workflows/caching-dependencies-to-speed-up-workflows#using-the-cache-action - path: ~/installers - key: ${{ runner.os }}-v0-${{ hashFiles('.ci/versions.json') }} - name: Cache NuGet packages uses: actions/cache@v3 with: @@ -31,24 +24,99 @@ jobs: key: ${{ runner.os }}-v1-nuget-${{ hashFiles('**/*.csproj') }} restore-keys: | ${{ runner.os }}-v1-nuget- - - name: Install and Start RabbitMQ - run: .\.ci\windows\gha-setup.ps1 - - name: List NuGet sources - run: dotnet nuget locals all --list - name: Build (Debug) run: dotnet build ${{ github.workspace }}\Build.csproj - name: Verify - run: dotnet format ${{ github.workspace }}\RabbitMQDotNetClient.sln --verify-no-changes --verbosity=diagnostic - - name: Test - run: .\.ci\windows\gha-run-tests.ps1 + run: dotnet format ${{ github.workspace }}\RabbitMQDotNetClient.sln --no-restore --verify-no-changes --verbosity=diagnostic + - name: APIApproval Test + run: dotnet test "${{ github.workspace }}\projects\Test\Unit\Unit.csproj" --no-restore --no-build --logger 'console;verbosity=detailed' --filter='FullyQualifiedName=Test.Unit.APIApproval.Approve' + - name: Unit Tests + run: dotnet test "${{ github.workspace }}\projects\Test\Unit\Unit.csproj" --no-restore --no-build --logger 'console;verbosity=detailed' + - name: Upload Build (Debug) + uses: actions/upload-artifact@v3 + with: + name: rabbitmq-dotnet-client-build-win32 + path: | + projects/Test/Unit/bin + projects/Test/AsyncIntegration/bin + projects/Test/Integration/bin + projects/Test/SequentialIntegration/bin + projects/RabbitMQ.*/bin + integration-win32: + name: integration test on windows-latest + needs: build-win32 + runs-on: windows-latest + # https://github.com/NuGet/Home/issues/11548 + env: + NUGET_CERT_REVOCATION_MODE: offline + steps: + - name: Clone repository + uses: actions/checkout@v4 + with: + submodules: true + - name: Cache installers + uses: actions/cache@v3 + with: + # Note: the cache path is relative to the workspace directory + # https://docs.github.com/en/actions/using-workflows/caching-dependencies-to-speed-up-workflows#using-the-cache-action + path: ~/installers + key: ${{ runner.os }}-v0-${{ hashFiles('.ci/versions.json') }} + - name: Download Build (Debug) + uses: actions/download-artifact@v3 + with: + name: rabbitmq-dotnet-client-build-win32 + path: projects + - name: Install and Start RabbitMQ + id: install-start-rabbitmq + run: .\.ci\windows\gha-setup.ps1 + - name: Async Integration Tests + run: dotnet test --environment "RABBITMQ_RABBITMQCTL_PATH=${{ steps.install-start-rabbitmq.outputs.path }}" --environment 'RABBITMQ_LONG_RUNNING_TESTS=false' "${{ github.workspace }}\projects\Test\AsyncIntegration\AsyncIntegration.csproj" --no-restore --no-build --logger 'console;verbosity=detailed' + - name: Integration Tests + run: dotnet test --environment "RABBITMQ_RABBITMQCTL_PATH=${{ steps.install-start-rabbitmq.outputs.path }}" --environment 'RABBITMQ_LONG_RUNNING_TESTS=false' --environment 'PASSWORD=grapefruit' --environment SSL_CERTS_DIR="${{ github.workspace }}\.ci\certs" "${{ github.workspace }}\projects\Test\Integration\Integration.csproj" --no-restore --no-build --logger 'console;verbosity=detailed' - name: Maybe upload RabbitMQ logs if: failure() uses: actions/upload-artifact@v3 with: - name: rabbitmq-logs + name: rabbitmq-logs-integration-win32 path: ~/AppData/Roaming/RabbitMQ/log/ - build: - name: build/test on ubuntu-latest + sequential-integration-win32: + name: sequential integration test on windows-latest + needs: build-win32 + runs-on: windows-latest + # https://github.com/NuGet/Home/issues/11548 + env: + NUGET_CERT_REVOCATION_MODE: offline + steps: + - name: Clone repository + uses: actions/checkout@v4 + with: + submodules: true + - name: Cache installers + uses: actions/cache@v3 + with: + # Note: the cache path is relative to the workspace directory + # https://docs.github.com/en/actions/using-workflows/caching-dependencies-to-speed-up-workflows#using-the-cache-action + path: ~/installers + key: ${{ runner.os }}-v0-${{ hashFiles('.ci/versions.json') }} + - name: Download Build (Debug) + uses: actions/download-artifact@v3 + with: + name: rabbitmq-dotnet-client-build-win32 + path: projects + - name: Install and Start RabbitMQ + id: install-start-rabbitmq + run: .\.ci\windows\gha-setup.ps1 + - name: Sequential Integration Tests + run: dotnet test --environment "RABBITMQ_RABBITMQCTL_PATH=${{ steps.install-start-rabbitmq.outputs.path }}" "${{ github.workspace }}\projects\Test\SequentialIntegration\SequentialIntegration.csproj" --no-restore --no-build --logger 'console;verbosity=detailed' + - name: Maybe upload RabbitMQ logs + if: failure() + uses: actions/upload-artifact@v3 + with: + name: rabbitmq-logs-sequential-integration-win32 + path: ~/AppData/Roaming/RabbitMQ/log/ + + build-ubuntu: + name: build, unit test on ubuntu-latest runs-on: ubuntu-latest steps: - name: Clone repository @@ -68,26 +136,93 @@ jobs: key: ${{ runner.os }}-v1-nuget-${{ hashFiles('**/*.csproj') }} restore-keys: | ${{ runner.os }}-v1-nuget- - - name: Start RabbitMQ - id: start-rabbitmq - run: ${{ github.workspace }}/.ci/ubuntu/gha-setup.sh - - name: List NuGet sources - run: dotnet nuget locals all --list - name: Build (Debug) run: dotnet build ${{ github.workspace }}/Build.csproj - name: Verify - run: dotnet format ${{ github.workspace }}/RabbitMQDotNetClient.sln --verify-no-changes --verbosity=diagnostic - - name: Test + run: dotnet format ${{ github.workspace }}/RabbitMQDotNetClient.sln --no-restore --verify-no-changes --verbosity=diagnostic + - name: APIApproval Test + run: dotnet test "${{ github.workspace }}/projects/Test/Unit/Unit.csproj" --no-restore --no-build --logger 'console;verbosity=detailed' --filter='FullyQualifiedName=Test.Unit.APIApproval.Approve' + - name: Unit Tests + run: dotnet test "${{ github.workspace }}/projects/Test/Unit/Unit.csproj" --no-restore --no-build --verbosity=diagnostic --logger 'console;verbosity=detailed' + - name: Upload Build (Debug) + uses: actions/upload-artifact@v3 + with: + name: rabbitmq-dotnet-client-build-ubuntu + path: | + projects/Test/Unit/bin + projects/Test/AsyncIntegration/bin + projects/Test/Integration/bin + projects/Test/SequentialIntegration/bin + projects/RabbitMQ.*/bin + integration-ubuntu: + name: integration test on ubuntu-latest + needs: build-ubuntu + runs-on: ubuntu-latest + steps: + - name: Clone repository + uses: actions/checkout@v4 + with: + submodules: true + - name: Setup .NET + uses: actions/setup-dotnet@v3 + with: + dotnet-version: 6.x + - name: Download Build (Debug) + uses: actions/download-artifact@v3 + with: + name: rabbitmq-dotnet-client-build-ubuntu + path: projects + - name: Start RabbitMQ + id: start-rabbitmq + run: ${{ github.workspace }}/.ci/ubuntu/gha-setup.sh + - name: Async Integration Tests run: | dotnet test \ --environment "RABBITMQ_RABBITMQCTL_PATH=DOCKER:${{ steps.start-rabbitmq.outputs.id }}" \ - --environment 'RABBITMQ_LONG_RUNNING_TESTS=true' \ + "${{ github.workspace }}/projects/Test/AsyncIntegration/AsyncIntegration.csproj" --no-restore --no-build --logger 'console;verbosity=detailed' + - name: Integration Tests + run: | + dotnet test \ + --environment "RABBITMQ_RABBITMQCTL_PATH=DOCKER:${{ steps.start-rabbitmq.outputs.id }}" \ + --environment 'RABBITMQ_LONG_RUNNING_TESTS=false' \ --environment 'PASSWORD=grapefruit' \ --environment SSL_CERTS_DIR="${{ github.workspace }}/.ci/certs" \ - "${{ github.workspace }}/projects/Unit/Unit.csproj" --no-restore --no-build --logger 'console;verbosity=detailed' --framework 'net6.0' + "${{ github.workspace }}/projects/Test/Integration/Integration.csproj" --no-restore --no-build --logger 'console;verbosity=detailed' + - name: Maybe upload RabbitMQ logs + if: failure() + uses: actions/upload-artifact@v3 + with: + name: rabbitmq-logs-integration-ubuntu + path: ${{ github.workspace }}/.ci/ubuntu/log/ + sequential-integration-ubuntu: + name: sequential integration test on ubuntu-latest + needs: build-ubuntu + runs-on: ubuntu-latest + steps: + - name: Clone repository + uses: actions/checkout@v4 + with: + submodules: true + - name: Setup .NET + uses: actions/setup-dotnet@v3 + with: + dotnet-version: 6.x + - name: Download Build (Debug) + uses: actions/download-artifact@v3 + with: + name: rabbitmq-dotnet-client-build-ubuntu + path: projects + - name: Start RabbitMQ + id: start-rabbitmq + run: ${{ github.workspace }}/.ci/ubuntu/gha-setup.sh + - name: Sequential Integration Tests + run: | + dotnet test \ + --environment "RABBITMQ_RABBITMQCTL_PATH=DOCKER:${{ steps.start-rabbitmq.outputs.id }}" \ + "${{ github.workspace }}/projects/Test/SequentialIntegration/SequentialIntegration.csproj" --no-restore --no-build --logger 'console;verbosity=detailed' - name: Maybe upload RabbitMQ logs if: failure() uses: actions/upload-artifact@v3 with: - name: rabbitmq-logs + name: rabbitmq-logs-sequential-integration-ubuntu path: ${{ github.workspace }}/.ci/ubuntu/log/ diff --git a/.gitignore b/.gitignore index 19a14ef6f4..ff820f3350 100644 --- a/.gitignore +++ b/.gitignore @@ -52,8 +52,8 @@ build/ BenchmarkDotNet.Artifacts/* -projects/Unit/APIApproval.Approve.received.txt -projects/Unit/APIApproval.Approve.*.received.txt +projects/Test/Unit/APIApproval.Approve.received.txt +projects/Test/Unit/APIApproval.Approve.*.received.txt # Visual Studio 2015 cache/options directory .vs/ @@ -115,7 +115,7 @@ UpgradeLog*.htm # Unit tests -projects/Unit*/TestResult.xml +projects/Test/Unit*/TestResult.xml # Development scripts diff --git a/Build.csproj b/Build.csproj index b4c88b63a2..c59632d35f 100644 --- a/Build.csproj +++ b/Build.csproj @@ -9,10 +9,14 @@ - - - - + + + + + + + + diff --git a/RUNNING_TESTS.md b/RUNNING_TESTS.md index a25a008221..f8b398ede4 100644 --- a/RUNNING_TESTS.md +++ b/RUNNING_TESTS.md @@ -71,7 +71,7 @@ in this example, it should be `./rabbitmq-server/deps/rabbit/sbin/rabbitmqctl`. It is possible to override the location using `RABBITMQ_RABBITMQCTL_PATH`: ``` -RABBITMQ_RABBITMQCTL_PATH=/path/to/rabbitmqctl dotnet test projects/Unit +RABBITMQ_RABBITMQCTL_PATH=/path/to/rabbitmqctl dotnet test projects/Test/Unit.csproj ``` ### Option Three: Using a Docker Container @@ -110,9 +110,9 @@ Running individual tests and fixtures on Windows is trivial using the Visual Stu To run a specific tests fixture on MacOS or Linux, use the NUnit filter expressions to select the tests to be run: ``` shell -dotnet test projects/Unit --filter "Name~TestAmqpUriParseFail" +dotnet test projects/Test/Unit.csproj --filter "Name~TestAmqpUriParseFail" -dotnet test projects/Unit --filter "FullyQualifiedName~RabbitMQ.Client.Unit.TestHeartbeats" +dotnet test projects/Test/Unit.csproj --filter "FullyQualifiedName~RabbitMQ.Client.Unit.TestHeartbeats" ``` ## Running Tests for a Specific .NET Target diff --git a/RabbitMQDotNetClient.sln b/RabbitMQDotNetClient.sln index 4d22b5e523..efdd0fcf97 100644 --- a/RabbitMQDotNetClient.sln +++ b/RabbitMQDotNetClient.sln @@ -9,19 +9,35 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RabbitMQ.Client", "projects\RabbitMQ.Client\RabbitMQ.Client.csproj", "{8C554257-5ECC-45DB-873D-560BFBB74EC8}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Unit", "projects\Unit\Unit.csproj", "{B8FAC024-CC03-4067-9FFC-02846FB8AE48}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Unit", "projects\Test\Unit\Unit.csproj", "{B8FAC024-CC03-4067-9FFC-02846FB8AE48}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Benchmarks", "projects\Benchmarks\Benchmarks.csproj", "{38D72C9A-68E9-4653-B0CE-C7BA9FFD91D0}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MassPublish", "projects\TestApplications\MassPublish\MassPublish.csproj", "{0E3C4FBE-9976-40A3-9F57-DC0D9B7A39A6}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MassPublish", "projects\Test\Applications\MassPublish\MassPublish.csproj", "{0E3C4FBE-9976-40A3-9F57-DC0D9B7A39A6}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "TestApplications", "TestApplications", "{D21B282C-49E6-4A30-887B-9626D94B8D69}" +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Applications", "Applications", "{D21B282C-49E6-4A30-887B-9626D94B8D69}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CreateChannel", "projects\TestApplications\CreateChannel\CreateChannel.csproj", "{4A589408-F3A3-40E1-A6DF-F5E620F7CA31}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CreateChannel", "projects\Test\Applications\CreateChannel\CreateChannel.csproj", "{4A589408-F3A3-40E1-A6DF-F5E620F7CA31}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RabbitMQ.Client.OAuth2", "projects\RabbitMQ.Client.OAuth2\RabbitMQ.Client.OAuth2.csproj", "{794C7B31-0E9A-44A4-B285-0F3CAF6209F1}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OAuth2Test", "projects\OAuth2Test\OAuth2Test.csproj", "{897D13F0-AF06-444A-9072-CF7E809A4A2C}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OAuth2", "projects\Test\OAuth2\OAuth2.csproj", "{897D13F0-AF06-444A-9072-CF7E809A4A2C}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Test", "Test", "{EFD4BED5-13A5-4D9C-AADF-CAB7E1573704}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Integration", "projects\Test\Integration\Integration.csproj", "{B01347D8-C327-471B-A1FE-7B86F7684A27}" + ProjectSection(ProjectDependencies) = postProject + {C11F25F4-7EA1-4874-9E25-DEB42E3A7C67} = {C11F25F4-7EA1-4874-9E25-DEB42E3A7C67} + EndProjectSection +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SequentialIntegration", "projects\Test\SequentialIntegration\SequentialIntegration.csproj", "{F25725D7-2978-45F4-B90F-25D6F8B71C9E}" + ProjectSection(ProjectDependencies) = postProject + {C11F25F4-7EA1-4874-9E25-DEB42E3A7C67} = {C11F25F4-7EA1-4874-9E25-DEB42E3A7C67} + EndProjectSection +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Common", "projects\Test\Common\Common.csproj", "{C11F25F4-7EA1-4874-9E25-DEB42E3A7C67}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AsyncIntegration", "projects\Test\AsyncIntegration\AsyncIntegration.csproj", "{D98F96C5-F7FB-45FC-92A0-9133850FB432}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -57,13 +73,36 @@ Global {897D13F0-AF06-444A-9072-CF7E809A4A2C}.Debug|Any CPU.Build.0 = Debug|Any CPU {897D13F0-AF06-444A-9072-CF7E809A4A2C}.Release|Any CPU.ActiveCfg = Release|Any CPU {897D13F0-AF06-444A-9072-CF7E809A4A2C}.Release|Any CPU.Build.0 = Release|Any CPU + {B01347D8-C327-471B-A1FE-7B86F7684A27}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B01347D8-C327-471B-A1FE-7B86F7684A27}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B01347D8-C327-471B-A1FE-7B86F7684A27}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B01347D8-C327-471B-A1FE-7B86F7684A27}.Release|Any CPU.Build.0 = Release|Any CPU + {F25725D7-2978-45F4-B90F-25D6F8B71C9E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F25725D7-2978-45F4-B90F-25D6F8B71C9E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F25725D7-2978-45F4-B90F-25D6F8B71C9E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F25725D7-2978-45F4-B90F-25D6F8B71C9E}.Release|Any CPU.Build.0 = Release|Any CPU + {C11F25F4-7EA1-4874-9E25-DEB42E3A7C67}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C11F25F4-7EA1-4874-9E25-DEB42E3A7C67}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C11F25F4-7EA1-4874-9E25-DEB42E3A7C67}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C11F25F4-7EA1-4874-9E25-DEB42E3A7C67}.Release|Any CPU.Build.0 = Release|Any CPU + {D98F96C5-F7FB-45FC-92A0-9133850FB432}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D98F96C5-F7FB-45FC-92A0-9133850FB432}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D98F96C5-F7FB-45FC-92A0-9133850FB432}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D98F96C5-F7FB-45FC-92A0-9133850FB432}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection GlobalSection(NestedProjects) = preSolution + {B8FAC024-CC03-4067-9FFC-02846FB8AE48} = {EFD4BED5-13A5-4D9C-AADF-CAB7E1573704} {0E3C4FBE-9976-40A3-9F57-DC0D9B7A39A6} = {D21B282C-49E6-4A30-887B-9626D94B8D69} + {D21B282C-49E6-4A30-887B-9626D94B8D69} = {EFD4BED5-13A5-4D9C-AADF-CAB7E1573704} {4A589408-F3A3-40E1-A6DF-F5E620F7CA31} = {D21B282C-49E6-4A30-887B-9626D94B8D69} + {897D13F0-AF06-444A-9072-CF7E809A4A2C} = {EFD4BED5-13A5-4D9C-AADF-CAB7E1573704} + {B01347D8-C327-471B-A1FE-7B86F7684A27} = {EFD4BED5-13A5-4D9C-AADF-CAB7E1573704} + {F25725D7-2978-45F4-B90F-25D6F8B71C9E} = {EFD4BED5-13A5-4D9C-AADF-CAB7E1573704} + {C11F25F4-7EA1-4874-9E25-DEB42E3A7C67} = {EFD4BED5-13A5-4D9C-AADF-CAB7E1573704} + {D98F96C5-F7FB-45FC-92A0-9133850FB432} = {EFD4BED5-13A5-4D9C-AADF-CAB7E1573704} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {3C6A0C44-FA63-4101-BBF9-2598641167D1} diff --git a/build.ps1 b/build.ps1 index 9b630e8011..caae1b570a 100644 --- a/build.ps1 +++ b/build.ps1 @@ -14,7 +14,7 @@ Write-Host "Done building." -ForegroundColor "Green" if ($RunTests) { - $unit_csproj_file = Resolve-Path -LiteralPath (Join-Path -Path $PSScriptRoot -ChildPath 'projects' | Join-Path -ChildPath 'Unit' | Join-Path -ChildPath 'Unit.csproj') + $unit_csproj_file = Resolve-Path -LiteralPath (Join-Path -Path $PSScriptRoot -ChildPath 'projects' | Join-Path -ChildPath 'Test' | Join-Path -ChildPath 'Unit' | Join-Path -ChildPath 'Unit.csproj') Write-Host "Running Unit / Integration tests from '$unit_csproj_file' (all frameworks)" -ForegroundColor "Magenta" dotnet test $unit_csproj_file --no-restore --no-build --logger "console;verbosity=detailed" if ($LastExitCode -ne 0) { diff --git a/projects/Benchmarks/ConsumerDispatching/AsyncBasicConsumerFake.cs b/projects/Benchmarks/ConsumerDispatching/AsyncBasicConsumerFake.cs index 573db2a277..0fbf03c05a 100644 --- a/projects/Benchmarks/ConsumerDispatching/AsyncBasicConsumerFake.cs +++ b/projects/Benchmarks/ConsumerDispatching/AsyncBasicConsumerFake.cs @@ -18,7 +18,8 @@ public AsyncBasicConsumerFake(ManualResetEventSlim autoResetEvent) _autoResetEvent = autoResetEvent; } - public Task HandleBasicDeliver(string consumerTag, ulong deliveryTag, bool redelivered, string exchange, string routingKey, in ReadOnlyBasicProperties properties, ReadOnlyMemory body) + public Task HandleBasicDeliver(string consumerTag, ulong deliveryTag, bool redelivered, string exchange, string routingKey, + in ReadOnlyBasicProperties properties, RentedMemory body) { if (Interlocked.Increment(ref _current) == Count) { @@ -29,7 +30,7 @@ public Task HandleBasicDeliver(string consumerTag, ulong deliveryTag, bool redel } void IBasicConsumer.HandleBasicDeliver(string consumerTag, ulong deliveryTag, bool redelivered, string exchange, string routingKey, - in ReadOnlyBasicProperties properties, ReadOnlyMemory body) + in ReadOnlyBasicProperties properties, RentedMemory body) { if (Interlocked.Increment(ref _current) == Count) { diff --git a/projects/Benchmarks/ConsumerDispatching/ConsumerDispatcher.cs b/projects/Benchmarks/ConsumerDispatching/ConsumerDispatcher.cs index b751091c4d..2a6d272a22 100644 --- a/projects/Benchmarks/ConsumerDispatching/ConsumerDispatcher.cs +++ b/projects/Benchmarks/ConsumerDispatching/ConsumerDispatcher.cs @@ -1,4 +1,5 @@ -using System.Threading; +using System; +using System.Threading; using BenchmarkDotNet.Attributes; using RabbitMQ.Client; using RabbitMQ.Client.ConsumerDispatching; @@ -18,7 +19,15 @@ public class ConsumerDispatcherBase protected readonly string _exchange = "Exchange"; protected readonly string _routingKey = "RoutingKey"; protected readonly ReadOnlyBasicProperties _properties = new ReadOnlyBasicProperties(); - protected readonly byte[] _body = new byte[512]; + protected readonly RentedMemory _body; + + public ConsumerDispatcherBase() + { + var r = new Random(); + byte[] body = new byte[512]; + r.NextBytes(body); + _body = new RentedMemory(body); + } } public class BasicDeliverConsumerDispatching : ConsumerDispatcherBase @@ -36,12 +45,13 @@ public void SetUpAsyncConsumer() _dispatcher = new AsyncConsumerDispatcher(null, Concurrency); _dispatcher.HandleBasicConsumeOk(_consumer, _consumerTag); } + [Benchmark] public void AsyncConsumerDispatcher() { for (int i = 0; i < Count; i++) { - _dispatcher.HandleBasicDeliver(_consumerTag, _deliveryTag, false, _exchange, _routingKey, _properties, _body, _body); + _dispatcher.HandleBasicDeliver(_consumerTag, _deliveryTag, false, _exchange, _routingKey, _properties, _body); } _autoResetEvent.Wait(); _autoResetEvent.Reset(); @@ -54,12 +64,13 @@ public void SetUpConsumer() _dispatcher = new ConsumerDispatcher(null, Concurrency); _dispatcher.HandleBasicConsumeOk(_consumer, _consumerTag); } + [Benchmark] public void ConsumerDispatcher() { for (int i = 0; i < Count; i++) { - _dispatcher.HandleBasicDeliver(_consumerTag, _deliveryTag, false, _exchange, _routingKey, _properties, _body, _body); + _dispatcher.HandleBasicDeliver(_consumerTag, _deliveryTag, false, _exchange, _routingKey, _properties, _body); } _autoResetEvent.Wait(); _autoResetEvent.Reset(); diff --git a/projects/Benchmarks/WireFormatting/MethodFraming.cs b/projects/Benchmarks/WireFormatting/MethodFraming.cs index e2f032341e..7e66796e5f 100644 --- a/projects/Benchmarks/WireFormatting/MethodFraming.cs +++ b/projects/Benchmarks/WireFormatting/MethodFraming.cs @@ -19,7 +19,7 @@ public class MethodFramingBasicAck public ushort Channel { get; set; } [Benchmark] - public ReadOnlyMemory BasicAckWrite() => Framing.SerializeToFrames(ref _basicAck, Channel); + internal RentedMemory BasicAckWrite() => Framing.SerializeToFrames(ref _basicAck, Channel); } [Config(typeof(Config))] @@ -41,13 +41,13 @@ public class MethodFramingBasicPublish public int FrameMax { get; set; } [Benchmark] - public ReadOnlyMemory BasicPublishWriteNonEmpty() => Framing.SerializeToFrames(ref _basicPublish, ref _properties, _body, Channel, FrameMax); + internal RentedMemory BasicPublishWriteNonEmpty() => Framing.SerializeToFrames(ref _basicPublish, ref _properties, _body, Channel, FrameMax); [Benchmark] - public ReadOnlyMemory BasicPublishWrite() => Framing.SerializeToFrames(ref _basicPublish, ref _propertiesEmpty, _bodyEmpty, Channel, FrameMax); + internal RentedMemory BasicPublishWrite() => Framing.SerializeToFrames(ref _basicPublish, ref _propertiesEmpty, _bodyEmpty, Channel, FrameMax); [Benchmark] - public ReadOnlyMemory BasicPublishMemoryWrite() => Framing.SerializeToFrames(ref _basicPublishMemory, ref _propertiesEmpty, _bodyEmpty, Channel, FrameMax); + internal RentedMemory BasicPublishMemoryWrite() => Framing.SerializeToFrames(ref _basicPublishMemory, ref _propertiesEmpty, _bodyEmpty, Channel, FrameMax); } [Config(typeof(Config))] @@ -60,6 +60,6 @@ public class MethodFramingChannelClose public ushort Channel { get; set; } [Benchmark] - public ReadOnlyMemory ChannelCloseWrite() => Framing.SerializeToFrames(ref _channelClose, Channel); + internal RentedMemory ChannelCloseWrite() => Framing.SerializeToFrames(ref _channelClose, Channel); } } diff --git a/projects/RabbitMQ.Client/RabbitMQ.Client.csproj b/projects/RabbitMQ.Client/RabbitMQ.Client.csproj index 68ebb42a02..5517d2dca2 100644 --- a/projects/RabbitMQ.Client/RabbitMQ.Client.csproj +++ b/projects/RabbitMQ.Client/RabbitMQ.Client.csproj @@ -45,9 +45,21 @@ + + <_Parameter1>Common, PublicKey=00240000048000009400000006020000002400005253413100040000010001008d20ec856aeeb8c3153a77faa2d80e6e43b5db93224a20cc7ae384f65f142e89730e2ff0fcc5d578bbe96fa98a7196c77329efdee4579b3814c0789e5a39b51df6edd75b602a33ceabdfcf19a3feb832f31d8254168cd7ba5700dfbca301fbf8db614ba41ba18474de0a5f4c2d51c995bc3636c641c8cbe76f45717bfcb943b5 + <_Parameter1>Unit, PublicKey=00240000048000009400000006020000002400005253413100040000010001008d20ec856aeeb8c3153a77faa2d80e6e43b5db93224a20cc7ae384f65f142e89730e2ff0fcc5d578bbe96fa98a7196c77329efdee4579b3814c0789e5a39b51df6edd75b602a33ceabdfcf19a3feb832f31d8254168cd7ba5700dfbca301fbf8db614ba41ba18474de0a5f4c2d51c995bc3636c641c8cbe76f45717bfcb943b5 + + <_Parameter1>AsyncIntegration, PublicKey=00240000048000009400000006020000002400005253413100040000010001008d20ec856aeeb8c3153a77faa2d80e6e43b5db93224a20cc7ae384f65f142e89730e2ff0fcc5d578bbe96fa98a7196c77329efdee4579b3814c0789e5a39b51df6edd75b602a33ceabdfcf19a3feb832f31d8254168cd7ba5700dfbca301fbf8db614ba41ba18474de0a5f4c2d51c995bc3636c641c8cbe76f45717bfcb943b5 + + + <_Parameter1>Integration, PublicKey=00240000048000009400000006020000002400005253413100040000010001008d20ec856aeeb8c3153a77faa2d80e6e43b5db93224a20cc7ae384f65f142e89730e2ff0fcc5d578bbe96fa98a7196c77329efdee4579b3814c0789e5a39b51df6edd75b602a33ceabdfcf19a3feb832f31d8254168cd7ba5700dfbca301fbf8db614ba41ba18474de0a5f4c2d51c995bc3636c641c8cbe76f45717bfcb943b5 + + + <_Parameter1>SequentialIntegration, PublicKey=00240000048000009400000006020000002400005253413100040000010001008d20ec856aeeb8c3153a77faa2d80e6e43b5db93224a20cc7ae384f65f142e89730e2ff0fcc5d578bbe96fa98a7196c77329efdee4579b3814c0789e5a39b51df6edd75b602a33ceabdfcf19a3feb832f31d8254168cd7ba5700dfbca301fbf8db614ba41ba18474de0a5f4c2d51c995bc3636c641c8cbe76f45717bfcb943b5 + <_Parameter1>Benchmarks, PublicKey=00240000048000009400000006020000002400005253413100040000010001008d20ec856aeeb8c3153a77faa2d80e6e43b5db93224a20cc7ae384f65f142e89730e2ff0fcc5d578bbe96fa98a7196c77329efdee4579b3814c0789e5a39b51df6edd75b602a33ceabdfcf19a3feb832f31d8254168cd7ba5700dfbca301fbf8db614ba41ba18474de0a5f4c2d51c995bc3636c641c8cbe76f45717bfcb943b5 diff --git a/projects/RabbitMQ.Client/client/ClientArrayPool.cs b/projects/RabbitMQ.Client/client/ClientArrayPool.cs new file mode 100644 index 0000000000..d84e3c8cb5 --- /dev/null +++ b/projects/RabbitMQ.Client/client/ClientArrayPool.cs @@ -0,0 +1,107 @@ +// This source code is dual-licensed under the Apache License, version +// 2.0, and the Mozilla Public License, version 2.0. +// +// The APL v2.0: +// +//--------------------------------------------------------------------------- +// Copyright (c) 2007-2020 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//--------------------------------------------------------------------------- +// +// The MPL v2.0: +// +//--------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// +// Copyright (c) 2007-2020 VMware, Inc. All rights reserved. +//--------------------------------------------------------------------------- + +using System; +using System.Buffers; +using System.Collections.Concurrent; +using System.Runtime.CompilerServices; + +namespace RabbitMQ.Client +{ + internal static class ClientArrayPool + { + private static readonly ConcurrentDictionary _checkouts; + + private static readonly bool s_useArrayPool = true; + private static readonly bool s_trackCheckouts = false; + + static ClientArrayPool() + { + if (false == bool.TryParse(Environment.GetEnvironmentVariable("RABBITMQ_CLIENT_USE_ARRAY_POOL"), out s_useArrayPool)) + { + s_useArrayPool = true; + } + + if (s_trackCheckouts) + { + _checkouts = new ConcurrentDictionary(); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static byte[] Rent(int minimumLength) + { + byte[] rv; + + if (s_useArrayPool) + { + rv = ArrayPool.Shared.Rent(minimumLength); + } + else + { + rv = new byte[minimumLength]; + } + + if (s_trackCheckouts) + { + if (_checkouts.ContainsKey(rv)) + { + throw new InvalidOperationException("ARRAY ALREADY RENTED"); + } + else + { + _checkouts[rv] = true; + } + } + + return rv; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static void Return(byte[] array) + { + if (array != null && array.Length > 0) + { + if (s_trackCheckouts && array.Length > 0) + { + if (false == _checkouts.TryRemove(array, out _)) + { + throw new InvalidOperationException("ARRAY NOT RENTED"); + } + } + if (s_useArrayPool) + { + ArrayPool.Shared.Return(array, clearArray: true); + } + } + } + } +} diff --git a/projects/RabbitMQ.Client/client/RentedMemory.cs b/projects/RabbitMQ.Client/client/RentedMemory.cs new file mode 100644 index 0000000000..9f65570750 --- /dev/null +++ b/projects/RabbitMQ.Client/client/RentedMemory.cs @@ -0,0 +1,97 @@ +// This source code is dual-licensed under the Apache License, version +// 2.0, and the Mozilla Public License, version 2.0. +// +// The APL v2.0: +// +//--------------------------------------------------------------------------- +// Copyright (c) 2007-2020 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//--------------------------------------------------------------------------- +// +// The MPL v2.0: +// +//--------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// +// Copyright (c) 2007-2020 VMware, Inc. All rights reserved. +//--------------------------------------------------------------------------- + +using System; + +namespace RabbitMQ.Client +{ + public struct RentedMemory : IDisposable + { + private readonly int _size; + private readonly ReadOnlyMemory _memory; + private readonly byte[] _rentedArray; + private bool _disposedValue; + + internal RentedMemory(byte[] rentedArray) + : this(rentedArray.Length, new ReadOnlyMemory(rentedArray), rentedArray) + { + } + + internal RentedMemory(ReadOnlyMemory memory, byte[] rentedArray) + : this(memory.Length, memory, rentedArray) + { + } + + internal RentedMemory(int size, ReadOnlyMemory memory, byte[] rentedArray) + { + _size = size; + _memory = memory; + _rentedArray = rentedArray; + } + + public int Size => _size; + + public ReadOnlyMemory Memory => _memory; + + public ReadOnlySpan Span => _memory.Span; + + public static implicit operator byte[](RentedMemory m) + { + return m._memory.ToArray(); + } + + internal byte[] RentedArray => _rentedArray; + + internal RentedMemory Copy() + { + return new RentedMemory(_size, new ReadOnlyMemory(_memory.ToArray()), null); + } + + private void Dispose(bool disposing) + { + if (!_disposedValue) + { + if (disposing && _rentedArray != null) + { + ClientArrayPool.Return(_rentedArray); + } + + _disposedValue = true; + } + } + + public void Dispose() + { + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + } +} diff --git a/projects/RabbitMQ.Client/client/api/AmqpTimestamp.cs b/projects/RabbitMQ.Client/client/api/AmqpTimestamp.cs index 2465fd167b..b735d14be3 100644 --- a/projects/RabbitMQ.Client/client/api/AmqpTimestamp.cs +++ b/projects/RabbitMQ.Client/client/api/AmqpTimestamp.cs @@ -33,9 +33,6 @@ namespace RabbitMQ.Client { - // time representations in mainstream languages: the horror, the horror - // see in particular the difference between .NET 1.x and .NET 2.0's versions of DateTime - /// /// Structure holding an AMQP timestamp, a posix 64-bit time_t. /// diff --git a/projects/RabbitMQ.Client/client/api/AsyncDefaultBasicConsumer.cs b/projects/RabbitMQ.Client/client/api/AsyncDefaultBasicConsumer.cs index a0c0af161a..336b3c5d55 100644 --- a/projects/RabbitMQ.Client/client/api/AsyncDefaultBasicConsumer.cs +++ b/projects/RabbitMQ.Client/client/api/AsyncDefaultBasicConsumer.cs @@ -113,7 +113,7 @@ public virtual Task HandleBasicDeliver(string consumerTag, string exchange, string routingKey, in ReadOnlyBasicProperties properties, - ReadOnlyMemory body) + RentedMemory body) { // Nothing to do here. return Task.CompletedTask; @@ -165,7 +165,8 @@ void IBasicConsumer.HandleBasicConsumeOk(string consumerTag) throw new InvalidOperationException("Should never be called. Enable 'DispatchConsumersAsync'."); } - void IBasicConsumer.HandleBasicDeliver(string consumerTag, ulong deliveryTag, bool redelivered, string exchange, string routingKey, in ReadOnlyBasicProperties properties, ReadOnlyMemory body) + void IBasicConsumer.HandleBasicDeliver(string consumerTag, ulong deliveryTag, bool redelivered, string exchange, string routingKey, + in ReadOnlyBasicProperties properties, RentedMemory body) { throw new InvalidOperationException("Should never be called. Enable 'DispatchConsumersAsync'."); } diff --git a/projects/RabbitMQ.Client/client/api/BasicGetResult.cs b/projects/RabbitMQ.Client/client/api/BasicGetResult.cs index 4e9a9d1cc9..fbdff2f615 100644 --- a/projects/RabbitMQ.Client/client/api/BasicGetResult.cs +++ b/projects/RabbitMQ.Client/client/api/BasicGetResult.cs @@ -29,19 +29,14 @@ // Copyright (c) 2007-2020 VMware, Inc. All rights reserved. //--------------------------------------------------------------------------- -using System; -using System.Buffers; - namespace RabbitMQ.Client { /// Represents Basic.GetOk responses from the server. /// /// Basic.Get either returns an instance of this class, or null if a Basic.GetEmpty was received. /// - public sealed class BasicGetResult : IDisposable + public sealed class BasicGetResult { - private readonly byte[] _rentedArray; - /// /// Sets the new instance's properties from the arguments passed in. /// @@ -53,7 +48,7 @@ public sealed class BasicGetResult : IDisposable /// The Basic-class content header properties for the message. /// The body public BasicGetResult(ulong deliveryTag, bool redelivered, string exchange, string routingKey, - uint messageCount, in ReadOnlyBasicProperties basicProperties, ReadOnlyMemory body) + uint messageCount, in ReadOnlyBasicProperties basicProperties, RentedMemory body) { DeliveryTag = deliveryTag; Redelivered = redelivered; @@ -61,32 +56,14 @@ public BasicGetResult(ulong deliveryTag, bool redelivered, string exchange, stri RoutingKey = routingKey; MessageCount = messageCount; BasicProperties = basicProperties; - Body = body; + /* + * Note: copying the body here is critical. The only other option to ensure the rented memory + * is returned correctly is to make this class implement IDisposable, but then it would be on the + * user to dispose of it. Basic.Get is discouraged, anyway, so a copy here is preferable. + */ + Body = body.Copy(); } - /// - /// Sets the new instance's properties from the arguments passed in. - /// - /// Delivery tag for the message. - /// Redelivered flag for the message - /// The exchange this message was published to. - /// Routing key with which the message was published. - /// The number of messages pending on the queue, excluding the message being delivered. - /// The Basic-class content header properties for the message. - /// The body - /// The rented array which body is part of. - public BasicGetResult(ulong deliveryTag, bool redelivered, string exchange, string routingKey, - uint messageCount, in ReadOnlyBasicProperties basicProperties, ReadOnlyMemory body, byte[] rentedArray) - { - DeliveryTag = deliveryTag; - Redelivered = redelivered; - Exchange = exchange; - RoutingKey = routingKey; - MessageCount = messageCount; - BasicProperties = basicProperties; - Body = body; - _rentedArray = rentedArray; - } /// /// Retrieves the Basic-class content header properties for this message. @@ -96,7 +73,7 @@ public BasicGetResult(ulong deliveryTag, bool redelivered, string exchange, stri /// /// Retrieves the body of this message. /// - public ReadOnlyMemory Body { get; } + public RentedMemory Body { get; } /// /// Retrieve the delivery tag for this message. See also . @@ -126,14 +103,5 @@ public BasicGetResult(ulong deliveryTag, bool redelivered, string exchange, stri /// Retrieve the routing key with which this message was published. /// public string RoutingKey { get; } - - /// - public void Dispose() - { - if (_rentedArray != null) - { - ArrayPool.Shared.Return(_rentedArray); - } - } } } diff --git a/projects/RabbitMQ.Client/client/api/ConnectionFactory.cs b/projects/RabbitMQ.Client/client/api/ConnectionFactory.cs index 82d7ff6d24..b0042f50ba 100644 --- a/projects/RabbitMQ.Client/client/api/ConnectionFactory.cs +++ b/projects/RabbitMQ.Client/client/api/ConnectionFactory.cs @@ -36,6 +36,7 @@ using System.Reflection; using System.Security.Authentication; using System.Text; +using System.Threading.Tasks; using RabbitMQ.Client.Exceptions; using RabbitMQ.Client.Framing.Impl; using RabbitMQ.Client.Impl; @@ -410,6 +411,19 @@ public IConnection CreateConnection() return CreateConnection(ClientProvidedName); } + /// + /// Asynchronously reate a connection to one of the endpoints provided by the IEndpointResolver + /// returned by the EndpointResolverFactory. By default the configured + /// hostname and port are used. + /// + /// + /// When the configured hostname was not reachable. + /// + public ValueTask CreateConnectionAsync() + { + return CreateConnectionAsync(ClientProvidedName); + } + /// /// Create a connection to one of the endpoints provided by the IEndpointResolver /// returned by the EndpointResolverFactory. By default the configured @@ -429,6 +443,25 @@ public IConnection CreateConnection(string clientProvidedName) return CreateConnection(EndpointResolverFactory(LocalEndpoints()), clientProvidedName); } + /// + /// Asynchronously create a connection to one of the endpoints provided by the IEndpointResolver + /// returned by the EndpointResolverFactory. By default the configured + /// hostname and port are used. + /// + /// + /// Application-specific connection name, will be displayed in the management UI + /// if RabbitMQ server supports it. This value doesn't have to be unique and cannot + /// be used as a connection identifier, e.g. in HTTP API requests. + /// This value is supposed to be human-readable. + /// + /// + /// When the configured hostname was not reachable. + /// + public ValueTask CreateConnectionAsync(string clientProvidedName) + { + return CreateConnectionAsync(EndpointResolverFactory(LocalEndpoints()), clientProvidedName); + } + /// /// Create a connection using a list of hostnames using the configured port. /// By default each hostname is tried in a random order until a successful connection is @@ -448,6 +481,25 @@ public IConnection CreateConnection(IList hostnames) return CreateConnection(hostnames, ClientProvidedName); } + /// + /// Asynchronously create a connection using a list of hostnames using the configured port. + /// By default each hostname is tried in a random order until a successful connection is + /// found or the list is exhausted using the DefaultEndpointResolver. + /// The selection behaviour can be overridden by configuring the EndpointResolverFactory. + /// + /// + /// List of hostnames to use for the initial + /// connection and recovery. + /// + /// Open connection + /// + /// When no hostname was reachable. + /// + public ValueTask CreateConnectionAsync(IList hostnames) + { + return CreateConnectionAsync(hostnames, ClientProvidedName); + } + /// /// Create a connection using a list of hostnames using the configured port. /// By default each endpoint is tried in a random order until a successful connection is @@ -474,6 +526,32 @@ public IConnection CreateConnection(IList hostnames, string clientProvid return CreateConnection(EndpointResolverFactory(endpoints), clientProvidedName); } + /// + /// Asynchronously create a connection using a list of hostnames using the configured port. + /// By default each endpoint is tried in a random order until a successful connection is + /// found or the list is exhausted. + /// The selection behaviour can be overridden by configuring the EndpointResolverFactory. + /// + /// + /// List of hostnames to use for the initial + /// connection and recovery. + /// + /// + /// Application-specific connection name, will be displayed in the management UI + /// if RabbitMQ server supports it. This value doesn't have to be unique and cannot + /// be used as a connection identifier, e.g. in HTTP API requests. + /// This value is supposed to be human-readable. + /// + /// Open connection + /// + /// When no hostname was reachable. + /// + public ValueTask CreateConnectionAsync(IList hostnames, string clientProvidedName) + { + IEnumerable endpoints = hostnames.Select(h => new AmqpTcpEndpoint(h, Port, Ssl, MaxMessageSize)); + return CreateConnectionAsync(EndpointResolverFactory(endpoints), clientProvidedName); + } + /// /// Create a connection using a list of endpoints. By default each endpoint will be tried /// in a random order until a successful connection is found or the list is exhausted. @@ -492,6 +570,24 @@ public IConnection CreateConnection(IList endpoints) return CreateConnection(endpoints, ClientProvidedName); } + /// + /// Asynchronously create a connection using a list of endpoints. By default each endpoint will be tried + /// in a random order until a successful connection is found or the list is exhausted. + /// The selection behaviour can be overridden by configuring the EndpointResolverFactory. + /// + /// + /// List of endpoints to use for the initial + /// connection and recovery. + /// + /// Open connection + /// + /// When no hostname was reachable. + /// + public ValueTask CreateConnectionAsync(IList endpoints) + { + return CreateConnectionAsync(endpoints, ClientProvidedName); + } + /// /// Create a connection using a list of endpoints. By default each endpoint will be tried /// in a random order until a successful connection is found or the list is exhausted. @@ -516,6 +612,30 @@ public IConnection CreateConnection(IList endpoints, string cli return CreateConnection(EndpointResolverFactory(endpoints), clientProvidedName); } + /// + /// Asynchronously create a connection using a list of endpoints. By default each endpoint will be tried + /// in a random order until a successful connection is found or the list is exhausted. + /// The selection behaviour can be overridden by configuring the EndpointResolverFactory. + /// + /// + /// List of endpoints to use for the initial + /// connection and recovery. + /// + /// + /// Application-specific connection name, will be displayed in the management UI + /// if RabbitMQ server supports it. This value doesn't have to be unique and cannot + /// be used as a connection identifier, e.g. in HTTP API requests. + /// This value is supposed to be human-readable. + /// + /// Open connection + /// + /// When no hostname was reachable. + /// + public ValueTask CreateConnectionAsync(IList endpoints, string clientProvidedName) + { + return CreateConnectionAsync(EndpointResolverFactory(endpoints), clientProvidedName); + } + /// /// Create a connection using an IEndpointResolver. /// @@ -539,10 +659,52 @@ public IConnection CreateConnection(IEndpointResolver endpointResolver, string c { if (AutomaticRecoveryEnabled) { - return new AutorecoveringConnection(config, endpointResolver); + var c = new AutorecoveringConnection(config, endpointResolver); + return (AutorecoveringConnection)c.Open(); + } + else + { + var c = new Connection(config, endpointResolver.SelectOne(CreateFrameHandler)); + return (Connection)c.Open(); } + } + catch (Exception e) + { + throw new BrokerUnreachableException(e); + } + } - return new Connection(config, endpointResolver.SelectOne(CreateFrameHandler)); + /// + /// Asynchronously create a connection using an IEndpointResolver. + /// + /// + /// The endpointResolver that returns the endpoints to use for the connection attempt. + /// + /// + /// Application-specific connection name, will be displayed in the management UI + /// if RabbitMQ server supports it. This value doesn't have to be unique and cannot + /// be used as a connection identifier, e.g. in HTTP API requests. + /// This value is supposed to be human-readable. + /// + /// Open connection + /// + /// When no hostname was reachable. + /// + public ValueTask CreateConnectionAsync(IEndpointResolver endpointResolver, string clientProvidedName) + { + ConnectionConfig config = CreateConfig(clientProvidedName); + try + { + if (AutomaticRecoveryEnabled) + { + var c = new AutorecoveringConnection(config, endpointResolver); + return c.OpenAsync(); + } + else + { + var c = new Connection(config, endpointResolver.SelectOne(CreateFrameHandler)); + return c.OpenAsync(); + } } catch (Exception e) { diff --git a/projects/RabbitMQ.Client/client/api/DefaultBasicConsumer.cs b/projects/RabbitMQ.Client/client/api/DefaultBasicConsumer.cs index 15b40ea808..5a82e98feb 100644 --- a/projects/RabbitMQ.Client/client/api/DefaultBasicConsumer.cs +++ b/projects/RabbitMQ.Client/client/api/DefaultBasicConsumer.cs @@ -153,7 +153,7 @@ public virtual void HandleBasicDeliver(string consumerTag, string exchange, string routingKey, in ReadOnlyBasicProperties properties, - ReadOnlyMemory body) + RentedMemory body) { // Nothing to do here. } diff --git a/projects/RabbitMQ.Client/client/api/IAsyncBasicConsumer.cs b/projects/RabbitMQ.Client/client/api/IAsyncBasicConsumer.cs index 291eda1293..d2a0aec07c 100644 --- a/projects/RabbitMQ.Client/client/api/IAsyncBasicConsumer.cs +++ b/projects/RabbitMQ.Client/client/api/IAsyncBasicConsumer.cs @@ -1,4 +1,3 @@ -using System; using System.Threading.Tasks; using RabbitMQ.Client.Events; @@ -52,7 +51,7 @@ Task HandleBasicDeliver(string consumerTag, string exchange, string routingKey, in ReadOnlyBasicProperties properties, - ReadOnlyMemory body); + RentedMemory body); /// /// Called when the channel shuts down. diff --git a/projects/RabbitMQ.Client/client/api/IBasicConsumer.cs b/projects/RabbitMQ.Client/client/api/IBasicConsumer.cs index d5244e973e..0285f37717 100644 --- a/projects/RabbitMQ.Client/client/api/IBasicConsumer.cs +++ b/projects/RabbitMQ.Client/client/api/IBasicConsumer.cs @@ -95,7 +95,7 @@ void HandleBasicDeliver(string consumerTag, string exchange, string routingKey, in ReadOnlyBasicProperties properties, - ReadOnlyMemory body); + RentedMemory body); /// /// Called when the channel shuts down. diff --git a/projects/RabbitMQ.Client/client/api/IChannel.cs b/projects/RabbitMQ.Client/client/api/IChannel.cs index 3f7f4693e2..5ded578ed2 100644 --- a/projects/RabbitMQ.Client/client/api/IChannel.cs +++ b/projects/RabbitMQ.Client/client/api/IChannel.cs @@ -58,8 +58,9 @@ public interface IChannel : IDisposable /// ShutdownEventArgs CloseReason { get; } - /// Signalled when an unexpected message is delivered + /// Signalled when an unexpected message is delivered. /// + /// /// Under certain circumstances it is possible for a channel to receive a /// message delivery which does not match any consumer which is currently /// set up via basicConsume(). This will occur after the following sequence @@ -79,7 +80,8 @@ public interface IChannel : IDisposable /// such deliveries. If no default consumer is registered an /// InvalidOperationException will be thrown when such a delivery arrives. /// - /// Most people will not need to use this. + /// Most people will not need to use this. + /// IBasicConsumer DefaultConsumer { get; set; } /// @@ -89,7 +91,8 @@ public interface IChannel : IDisposable /// /// Returns true if the channel is still in a state where it can be used. - /// Identical to checking if equals null. + /// Identical to checking if equals null. + /// bool IsOpen { get; } /// @@ -115,17 +118,6 @@ public interface IChannel : IDisposable /// event EventHandler BasicNacks; - /// - /// All messages received before this fires that haven't been ack'ed will be redelivered. - /// All messages received afterwards won't be. - /// - /// - /// Handlers for this event are invoked by the connection thread. - /// It is sometimes useful to allow that thread to know that a recover-ok - /// has been received, rather than the thread that invoked . - /// - event EventHandler BasicRecoverOk; - /// /// Signalled when a Basic.Return command arrives from the broker. /// @@ -151,42 +143,89 @@ public interface IChannel : IDisposable /// event EventHandler ChannelShutdown; - /// - /// Acknowledge one or more delivered message(s). - /// + /// Acknknowledges one or more messages. + /// The delivery tag. + /// Ack all messages up to the delivery tag if set to true. void BasicAck(ulong deliveryTag, bool multiple); - /// - /// Delete a Basic content-class consumer. - /// + /// Asynchronously acknknowledges one or more messages. + /// The delivery tag. + /// Ack all messages up to the delivery tag if set to true. + ValueTask BasicAckAsync(ulong deliveryTag, bool multiple); + + /// Cancel a Basic content-class consumer. + /// The consumer tag. void BasicCancel(string consumerTag); + /// Asynchronously cancel a Basic content-class consumer. + /// The consumer tag. + ValueTask BasicCancelAsync(string consumerTag); + /// /// Same as BasicCancel but sets nowait to true and returns void (as there /// will be no response from the server). /// + /// The consumer tag. void BasicCancelNoWait(string consumerTag); /// Start a Basic content-class consumer. - string BasicConsume( - string queue, - bool autoAck, - string consumerTag, - bool noLocal, - bool exclusive, - IDictionary arguments, - IBasicConsumer consumer); + /// The queue. + /// If set to true, automatically ack messages. + /// The consumer tag. + /// If set to true, this consumer will not receive messages published by the same connection. + /// If set to true, the consumer is exclusive. + /// Consumer arguments. + /// The consumer, an instance of + /// + string BasicConsume(string queue, bool autoAck, string consumerTag, bool noLocal, bool exclusive, IDictionary arguments, IBasicConsumer consumer); + + /// Asynchronously start a Basic content-class consumer. + /// The queue. + /// If set to true, automatically ack messages. + /// The consumer tag. + /// If set to true, this consumer will not receive messages published by the same connection. + /// If set to true, the consumer is exclusive. + /// Consumer arguments. + /// The consumer, an instance of + /// + ValueTask BasicConsumeAsync(string queue, bool autoAck, string consumerTag, bool noLocal, bool exclusive, IDictionary arguments, IBasicConsumer consumer); /// /// Retrieve an individual message, if /// one is available; returns null if the server answers that - /// no messages are currently available. See also . + /// no messages are currently available. See also . /// + /// The queue. + /// If set to true, automatically ack the message. + /// BasicGetResult BasicGet(string queue, bool autoAck); - /// Reject one or more delivered message(s). + /// + /// Asynchronously retrieve an individual message, if + /// one is available; returns null if the server answers that + /// no messages are currently available. See also . + /// + /// The queue. + /// If set to true, automatically ack the message. + /// + ValueTask BasicGetAsync(string queue, bool autoAck); + + /// + /// Nack one or more delivered message(s). + /// + /// The delivery tag. + /// If set to true, nack all messages up to the current tag. + /// If set to true, requeue nack'd messages. void BasicNack(ulong deliveryTag, bool multiple, bool requeue); + /// + /// Asynchronously nack one or more delivered message(s). + /// + /// The delivery tag. + /// If set to true, nack all messages up to the current tag. + /// If set to true, requeue nack'd messages. + ValueTask BasicNackAsync(ulong deliveryTag, bool multiple, bool requeue); + #nullable enable /// @@ -238,23 +277,27 @@ ValueTask BasicPublishAsync(CachedString exchange, CachedString rou /// /// Configures QoS parameters of the Basic content-class. /// + /// Size of the prefetch in bytes. + /// The prefetch count. + /// If set to true, use global prefetch. + /// See the Consumer Prefetch documentation. void BasicQos(uint prefetchSize, ushort prefetchCount, bool global); /// - /// Indicates that a consumer has recovered. - /// Deprecated. Should not be used. - /// - void BasicRecover(bool requeue); - - /// - /// Indicates that a consumer has recovered. - /// Deprecated. Should not be used. + /// Configures QoS parameters of the Basic content-class. /// - void BasicRecoverAsync(bool requeue); + /// Size of the prefetch in bytes. + /// The prefetch count. + /// If set to true, use global prefetch. + /// See the Consumer Prefetch documentation. + ValueTask BasicQosAsync(uint prefetchSize, ushort prefetchCount, bool global); /// Reject a delivered message. void BasicReject(ulong deliveryTag, bool requeue); + /// Reject a delivered message. + ValueTask BasicRejectAsync(ulong deliveryTag, bool requeue); + /// Close this session. /// The reply code to send for closing (See under "Reply Codes" in the AMQP specification). /// The reply text to send for closing. @@ -262,10 +305,19 @@ ValueTask BasicPublishAsync(CachedString exchange, CachedString rou void Close(ushort replyCode, string replyText, bool abort); /// - /// Enable publisher acknowledgements. + /// Asynchronously close this session. /// + /// The instance containing the close data. + /// Whether or not the close is an abort (ignoring certain exceptions). + /// + ValueTask CloseAsync(ShutdownEventArgs reason, bool abort); + + /// Enable publisher confirmations. void ConfirmSelect(); + /// Asynchronously enable publisher confirmations. + ValueTask ConfirmSelectAsync(); + /// /// Bind an exchange to an exchange. /// @@ -276,6 +328,16 @@ ValueTask BasicPublishAsync(CachedString exchange, CachedString rou /// void ExchangeBind(string destination, string source, string routingKey, IDictionary arguments); + /// + /// Asynchronously binds an exchange to an exchange. + /// + /// + /// + /// Routing key must be shorter than 255 bytes. + /// + /// + ValueTask ExchangeBindAsync(string destination, string source, string routingKey, IDictionary arguments); + /// /// Like ExchangeBind but sets nowait to true. /// @@ -289,10 +351,17 @@ ValueTask BasicPublishAsync(CachedString exchange, CachedString rou /// Declare an exchange. /// /// The exchange is declared non-passive and non-internal. - /// The "nowait" option is not exercised. + /// The "nowait" option is not used. /// void ExchangeDeclare(string exchange, string type, bool durable, bool autoDelete, IDictionary arguments); + /// Asynchronously declare an exchange. + /// + /// The exchange is declared non-internal. + /// The "nowait" option is not used. + /// + ValueTask ExchangeDeclareAsync(string exchange, string type, bool passive, bool durable, bool autoDelete, IDictionary arguments); + /// /// Same as ExchangeDeclare but sets nowait to true and returns void (as there /// will be no response from the server). @@ -315,6 +384,11 @@ ValueTask BasicPublishAsync(CachedString exchange, CachedString rou /// void ExchangeDelete(string exchange, bool ifUnused); + /// + /// Asynchronously delete an exchange. + /// + ValueTask ExchangeDeleteAsync(string exchange, bool ifUnused); + /// /// Like ExchangeDelete but sets nowait to true. /// @@ -328,6 +402,14 @@ ValueTask BasicPublishAsync(CachedString exchange, CachedString rou /// void ExchangeUnbind(string destination, string source, string routingKey, IDictionary arguments); + /// + /// Asynchronously unbind an exchange from an exchange. + /// + /// + /// Routing key must be shorter than 255 bytes. + /// + ValueTask ExchangeUnbindAsync(string destination, string source, string routingKey, IDictionary arguments); + /// /// Like ExchangeUnbind but sets nowait to true. /// @@ -341,13 +423,27 @@ ValueTask BasicPublishAsync(CachedString exchange, CachedString rou /// /// Bind a queue to an exchange. /// + /// The queue. + /// The exchange. + /// The routing key. + /// The arguments. /// - /// - /// Routing key must be shorter than 255 bytes. - /// + /// Routing key must be shorter than 255 bytes. /// void QueueBind(string queue, string exchange, string routingKey, IDictionary arguments); + /// + /// Asynchronously bind a queue to an exchange. + /// + /// The queue. + /// The exchange. + /// The routing key. + /// The arguments. + /// + /// Routing key must be shorter than 255 bytes. + /// + ValueTask QueueBindAsync(string queue, string exchange, string routingKey, IDictionary arguments); + /// Same as QueueBind but sets nowait parameter to true. /// /// @@ -370,11 +466,12 @@ ValueTask BasicPublishAsync(CachedString exchange, CachedString rou /// Asynchronously declares a queue. See the Queues guide to learn more. /// /// The name of the queue. Pass an empty string to make the server generate a name. + /// Set to true to passively declare the queue (i.e. check for its existence) /// Should this queue will survive a broker restart? /// Should this queue use be limited to its declaring connection? Such a queue will be deleted when its declaring connection closes. /// Should this queue be auto-deleted when its last consumer (if any) unsubscribes? /// Optional; additional queue arguments, e.g. "x-queue-type" - ValueTask QueueDeclareAsync(string queue, bool durable, bool exclusive, bool autoDelete, IDictionary arguments); + ValueTask QueueDeclareAsync(string queue, bool passive, bool durable, bool exclusive, bool autoDelete, IDictionary arguments); /// /// Declares a queue. See the Queues guide to learn more. @@ -411,52 +508,84 @@ ValueTask BasicPublishAsync(CachedString exchange, CachedString rou uint ConsumerCount(string queue); /// - /// Delete a queue. + /// Deletes a queue. See the Queues guide to learn more. + /// + /// The name of the queue. + /// Only delete the queue if it is unused. + /// Only delete the queue if it is empty. + /// Returns the number of messages purged during deletion. + uint QueueDelete(string queue, bool ifUnused, bool ifEmpty); + + /// + /// Asynchronously deletes a queue. See the Queues guide to learn more. /// /// ///Returns the number of messages purged during queue deletion. /// - uint QueueDelete(string queue, bool ifUnused, bool ifEmpty); + ValueTask QueueDeleteAsync(string queue, bool ifUnused, bool ifEmpty); /// ///Same as QueueDelete but sets nowait parameter to true ///and returns void (as there will be no response from the server) /// + /// The name of the queue. + /// Only delete the queue if it is unused. + /// Only delete the queue if it is empty. + /// Returns the number of messages purged during deletion. void QueueDeleteNoWait(string queue, bool ifUnused, bool ifEmpty); - /// - /// Purge a queue of messages. - /// - /// - /// Returns the number of messages purged. - /// + /// Asynchronously purge a queue of messages. + /// The queue. + /// Returns the number of messages purged. uint QueuePurge(string queue); + /// Asynchronously purge a queue of messages. + /// The queue. + /// Returns the number of messages purged. + ValueTask QueuePurgeAsync(string queue); + /// /// Unbind a queue from an exchange. /// + /// The queue. + /// The exchange. + /// The routing key. + /// The arguments. /// - /// - /// Routing key must be shorter than 255 bytes. - /// + /// Routing key must be shorter than 255 bytes. /// void QueueUnbind(string queue, string exchange, string routingKey, IDictionary arguments); /// - /// Commit this session's active TX transaction. + /// Asynchronously unbind a queue from an exchange. /// + /// The queue. + /// The exchange. + /// The routing key. + /// The arguments. + /// + /// Routing key must be shorter than 255 bytes. + /// + ValueTask QueueUnbindAsync(string queue, string exchange, string routingKey, IDictionary arguments); + + /// Commit this session's active TX transaction. void TxCommit(); - /// - /// Roll back this session's active TX transaction. - /// + /// Asynchronously commit this session's active TX transaction. + ValueTask TxCommitAsync(); + + /// Roll back this session's active TX transaction. void TxRollback(); - /// - /// Enable TX mode for this session. - /// + /// Asynchronously roll back this session's active TX transaction. + ValueTask TxRollbackAsync(); + + /// Enable TX mode for this session. void TxSelect(); + /// Asynchronously enable TX mode for this session. + ValueTask TxSelectAsync(); + /// /// Wait until all published messages on this channel have been confirmed. /// diff --git a/projects/RabbitMQ.Client/client/api/IChannelExtensions.cs b/projects/RabbitMQ.Client/client/api/IChannelExtensions.cs index 6beb1d6bb5..22b7463fe5 100644 --- a/projects/RabbitMQ.Client/client/api/IChannelExtensions.cs +++ b/projects/RabbitMQ.Client/client/api/IChannelExtensions.cs @@ -250,15 +250,32 @@ public static void Close(this IChannel channel) channel.Close(Constants.ReplySuccess, "Goodbye", false); } - /// Close this session. + /// Asynchronously close this session. + /// + /// If the session is already closed (or closing), then this + /// method does nothing but wait for the in-progress close + /// operation to complete. This method will not return to the + /// caller until the shutdown is complete. + /// + public static ValueTask CloseAsync(this IChannel channel) + { + var reason = new ShutdownEventArgs(ShutdownInitiator.Library, Constants.ReplySuccess, "Goodbye"); + return channel.CloseAsync(reason, false); + } + + /// + /// Close this channel. + /// + /// The channel. + /// The reply code. + /// The reply text. /// /// The method behaves in the same way as Close(), with the only /// difference that the channel is closed with the given channel /// close code and message. /// /// The close code (See under "Reply Codes" in the AMQP specification) - /// - /// + /// /// A message indicating the reason for closing the channel /// /// diff --git a/projects/RabbitMQ.Client/client/api/IConnection.cs b/projects/RabbitMQ.Client/client/api/IConnection.cs index 745efe79e5..104b715d8e 100644 --- a/projects/RabbitMQ.Client/client/api/IConnection.cs +++ b/projects/RabbitMQ.Client/client/api/IConnection.cs @@ -31,6 +31,7 @@ using System; using System.Collections.Generic; +using System.Threading.Tasks; using RabbitMQ.Client.Events; using RabbitMQ.Client.Exceptions; @@ -229,5 +230,10 @@ public interface IConnection : INetworkConnection, IDisposable /// Create and return a fresh channel, session, and channel. /// IChannel CreateChannel(); + + /// + /// Asynchronously create and return a fresh channel, session, and channel. + /// + ValueTask CreateChannelAsync(); } } diff --git a/projects/RabbitMQ.Client/client/api/IConnectionFactory.cs b/projects/RabbitMQ.Client/client/api/IConnectionFactory.cs index b1480cf887..1c9e63fcf2 100644 --- a/projects/RabbitMQ.Client/client/api/IConnectionFactory.cs +++ b/projects/RabbitMQ.Client/client/api/IConnectionFactory.cs @@ -31,7 +31,7 @@ using System; using System.Collections.Generic; - +using System.Threading.Tasks; using RabbitMQ.Client.Exceptions; namespace RabbitMQ.Client @@ -102,6 +102,11 @@ public interface IConnectionFactory /// IConnection CreateConnection(); + /// + /// Asynchronously create a connection to the specified endpoint. + /// + ValueTask CreateConnectionAsync(); + /// /// Create a connection to the specified endpoint. /// @@ -114,6 +119,18 @@ public interface IConnectionFactory /// Open connection IConnection CreateConnection(string clientProvidedName); + /// + /// Asynchronously create a connection to the specified endpoint. + /// + /// + /// Application-specific connection name, will be displayed in the management UI + /// if RabbitMQ server supports it. This value doesn't have to be unique and cannot + /// be used as a connection identifier, e.g. in HTTP API requests. + /// This value is supposed to be human-readable. + /// + /// Open connection + ValueTask CreateConnectionAsync(string clientProvidedName); + /// /// Connects to the first reachable hostname from the list. /// @@ -121,6 +138,13 @@ public interface IConnectionFactory /// Open connection IConnection CreateConnection(IList hostnames); + /// + /// Asynchronously connects to the first reachable hostname from the list. + /// + /// List of host names to use + /// Open connection + ValueTask CreateConnectionAsync(IList hostnames); + /// /// Connects to the first reachable hostname from the list. /// @@ -134,6 +158,19 @@ public interface IConnectionFactory /// Open connection IConnection CreateConnection(IList hostnames, string clientProvidedName); + /// + /// Asynchronously connects to the first reachable hostname from the list. + /// + /// List of host names to use + /// + /// Application-specific connection name, will be displayed in the management UI + /// if RabbitMQ server supports it. This value doesn't have to be unique and cannot + /// be used as a connection identifier, e.g. in HTTP API requests. + /// This value is supposed to be human-readable. + /// + /// Open connection + ValueTask CreateConnectionAsync(IList hostnames, string clientProvidedName); + /// /// Create a connection using a list of endpoints. /// The selection behaviour can be overridden by configuring the EndpointResolverFactory. @@ -148,6 +185,20 @@ public interface IConnectionFactory /// IConnection CreateConnection(IList endpoints); + /// + /// Asynchronously create a connection using a list of endpoints. + /// The selection behaviour can be overridden by configuring the EndpointResolverFactory. + /// + /// + /// List of endpoints to use for the initial + /// connection and recovery. + /// + /// Open connection + /// + /// When no hostname was reachable. + /// + ValueTask CreateConnectionAsync(IList endpoints); + /// /// Create a connection using a list of endpoints. /// The selection behaviour can be overridden by configuring the EndpointResolverFactory. @@ -168,6 +219,26 @@ public interface IConnectionFactory /// IConnection CreateConnection(IList endpoints, string clientProvidedName); + /// + /// Asynchronously create a connection using a list of endpoints. + /// The selection behaviour can be overridden by configuring the EndpointResolverFactory. + /// + /// + /// List of endpoints to use for the initial + /// connection and recovery. + /// + /// + /// Application-specific connection name, will be displayed in the management UI + /// if RabbitMQ server supports it. This value doesn't have to be unique and cannot + /// be used as a connection identifier, e.g. in HTTP API requests. + /// This value is supposed to be human-readable. + /// + /// Open connection + /// + /// When no hostname was reachable. + /// + ValueTask CreateConnectionAsync(IList endpoints, string clientProvidedName); + /// /// Amount of time protocol handshake operations are allowed to take before /// timing out. diff --git a/projects/RabbitMQ.Client/client/events/AsyncEventingBasicConsumer.cs b/projects/RabbitMQ.Client/client/events/AsyncEventingBasicConsumer.cs index 2dc046badc..e48816c861 100644 --- a/projects/RabbitMQ.Client/client/events/AsyncEventingBasicConsumer.cs +++ b/projects/RabbitMQ.Client/client/events/AsyncEventingBasicConsumer.cs @@ -1,4 +1,3 @@ -using System; using System.Threading.Tasks; using RabbitMQ.Client.Impl; @@ -71,7 +70,8 @@ public override async Task HandleBasicConsumeOk(string consumerTag) } ///Fires the Received event. - public override Task HandleBasicDeliver(string consumerTag, ulong deliveryTag, bool redelivered, string exchange, string routingKey, in ReadOnlyBasicProperties properties, ReadOnlyMemory body) + public override Task HandleBasicDeliver(string consumerTag, ulong deliveryTag, bool redelivered, string exchange, string routingKey, + in ReadOnlyBasicProperties properties, RentedMemory body) { // No need to call base, it's empty. return _receivedWrapper.InvokeAsync(this, new BasicDeliverEventArgs(consumerTag, deliveryTag, redelivered, exchange, routingKey, properties, body)); diff --git a/projects/RabbitMQ.Client/client/events/BasicDeliverEventArgs.cs b/projects/RabbitMQ.Client/client/events/BasicDeliverEventArgs.cs index fbd497bb0f..8d89a8912d 100644 --- a/projects/RabbitMQ.Client/client/events/BasicDeliverEventArgs.cs +++ b/projects/RabbitMQ.Client/client/events/BasicDeliverEventArgs.cs @@ -50,7 +50,7 @@ public BasicDeliverEventArgs(string consumerTag, string exchange, string routingKey, in ReadOnlyBasicProperties properties, - ReadOnlyMemory body) + RentedMemory body) { ConsumerTag = consumerTag; DeliveryTag = deliveryTag; @@ -65,7 +65,7 @@ public BasicDeliverEventArgs(string consumerTag, public ReadOnlyBasicProperties BasicProperties { get; set; } ///The message body. - public ReadOnlyMemory Body { get; set; } + public RentedMemory Body { get; set; } ///The consumer tag of the consumer that the message ///was delivered to. diff --git a/projects/RabbitMQ.Client/client/events/BasicReturnEventArgs.cs b/projects/RabbitMQ.Client/client/events/BasicReturnEventArgs.cs index 67220f1244..419c586a5c 100644 --- a/projects/RabbitMQ.Client/client/events/BasicReturnEventArgs.cs +++ b/projects/RabbitMQ.Client/client/events/BasicReturnEventArgs.cs @@ -38,25 +38,36 @@ namespace RabbitMQ.Client.Events public class BasicReturnEventArgs : EventArgs { ///The content header of the message. - public ReadOnlyBasicProperties BasicProperties { get; set; } + public readonly ReadOnlyBasicProperties BasicProperties; ///The message body. - public ReadOnlyMemory Body { get; set; } + public readonly RentedMemory Body; ///The exchange the returned message was originally ///published to. - public string Exchange { get; set; } + public readonly string Exchange; ///The AMQP reason code for the return. See ///RabbitMQ.Client.Framing.*.Constants. - public ushort ReplyCode { get; set; } + public readonly ushort ReplyCode; ///Human-readable text from the broker describing the ///reason for the return. - public string ReplyText { get; set; } + public readonly string ReplyText; ///The routing key used when the message was ///originally published. - public string RoutingKey { get; set; } + public readonly string RoutingKey; + + public BasicReturnEventArgs(ushort replyCode, string replyText, string exchange, string routingKey, + ReadOnlyBasicProperties basicProperties, RentedMemory body) + { + ReplyCode = replyCode; + ReplyText = replyText; + Exchange = exchange; + RoutingKey = routingKey; + BasicProperties = basicProperties; + Body = body.Copy(); + } } } diff --git a/projects/RabbitMQ.Client/client/events/EventingBasicConsumer.cs b/projects/RabbitMQ.Client/client/events/EventingBasicConsumer.cs index 681247ea56..79b9516c76 100644 --- a/projects/RabbitMQ.Client/client/events/EventingBasicConsumer.cs +++ b/projects/RabbitMQ.Client/client/events/EventingBasicConsumer.cs @@ -84,7 +84,8 @@ public override void HandleBasicConsumeOk(string consumerTag) /// Accessing the body at a later point is unsafe as its memory can /// be already released. /// - public override void HandleBasicDeliver(string consumerTag, ulong deliveryTag, bool redelivered, string exchange, string routingKey, in ReadOnlyBasicProperties properties, ReadOnlyMemory body) + public override void HandleBasicDeliver(string consumerTag, ulong deliveryTag, bool redelivered, string exchange, string routingKey, + in ReadOnlyBasicProperties properties, RentedMemory body) { base.HandleBasicDeliver(consumerTag, deliveryTag, redelivered, exchange, routingKey, properties, body); Received?.Invoke( diff --git a/projects/RabbitMQ.Client/client/framing/Channel.cs b/projects/RabbitMQ.Client/client/framing/Channel.cs index a59cceb1e3..7b107a5eaa 100644 --- a/projects/RabbitMQ.Client/client/framing/Channel.cs +++ b/projects/RabbitMQ.Client/client/framing/Channel.cs @@ -62,11 +62,6 @@ public override void _Private_BasicGet(string queue, bool autoAck) ChannelSend(new BasicGet(queue, autoAck)); } - public override void _Private_BasicRecover(bool requeue) - { - ChannelSend(new BasicRecover(requeue)); - } - public override void _Private_ChannelClose(ushort replyCode, string replyText, ushort classId, ushort methodId) { ChannelSend(new ChannelClose(replyCode, replyText, classId, methodId)); @@ -234,19 +229,26 @@ public override void BasicAck(ulong deliveryTag, bool multiple) ChannelSend(new BasicAck(deliveryTag, multiple)); } + public override ValueTask BasicAckAsync(ulong deliveryTag, bool multiple) + { + var method = new BasicAck(deliveryTag, multiple); + return ModelSendAsync(method); + } + public override void BasicNack(ulong deliveryTag, bool multiple, bool requeue) { ChannelSend(new BasicNack(deliveryTag, multiple, requeue)); } - public override void BasicQos(uint prefetchSize, ushort prefetchCount, bool global) + public override ValueTask BasicNackAsync(ulong deliveryTag, bool multiple, bool requeue) { - ChannelRpc(new BasicQos(prefetchSize, prefetchCount, global), ProtocolCommandId.BasicQosOk); + var method = new BasicNack(deliveryTag, multiple, requeue); + return ModelSendAsync(method); } - public override void BasicRecoverAsync(bool requeue) + public override void BasicQos(uint prefetchSize, ushort prefetchCount, bool global) { - ChannelSend(new BasicRecoverAsync(requeue)); + ChannelRpc(new BasicQos(prefetchSize, prefetchCount, global), ProtocolCommandId.BasicQosOk); } public override void BasicReject(ulong deliveryTag, bool requeue) @@ -254,6 +256,12 @@ public override void BasicReject(ulong deliveryTag, bool requeue) ChannelSend(new BasicReject(deliveryTag, requeue)); } + public override ValueTask BasicRejectAsync(ulong deliveryTag, bool requeue) + { + var method = new BasicReject(deliveryTag, requeue); + return ModelSendAsync(method); + } + public override void QueueUnbind(string queue, string exchange, string routingKey, IDictionary arguments) { ChannelRpc(new QueueUnbind(queue, exchange, routingKey, arguments), ProtocolCommandId.QueueUnbindOk); @@ -295,36 +303,26 @@ protected override bool DispatchAsynchronous(in IncomingCommand cmd) } case ProtocolCommandId.BasicCancelOk: { - HandleBasicCancelOk(in cmd); - return true; + return HandleBasicCancelOk(in cmd); } case ProtocolCommandId.BasicConsumeOk: { - HandleBasicConsumeOk(in cmd); - return true; + return HandleBasicConsumeOk(in cmd); } case ProtocolCommandId.BasicGetEmpty: { - cmd.ReturnMethodBuffer(); - HandleBasicGetEmpty(); + HandleBasicGetEmpty(in cmd); return true; } case ProtocolCommandId.BasicGetOk: { - HandleBasicGetOk(in cmd); - return true; + return HandleBasicGetOk(in cmd); } case ProtocolCommandId.BasicNack: { HandleBasicNack(in cmd); return true; } - case ProtocolCommandId.BasicRecoverOk: - { - cmd.ReturnMethodBuffer(); - HandleBasicRecoverOk(); - return true; - } case ProtocolCommandId.BasicReturn: { HandleBasicReturn(in cmd); @@ -337,8 +335,7 @@ protected override bool DispatchAsynchronous(in IncomingCommand cmd) } case ProtocolCommandId.ChannelCloseOk: { - cmd.ReturnMethodBuffer(); - HandleChannelCloseOk(); + HandleChannelCloseOk(in cmd); return true; } case ProtocolCommandId.ChannelFlow: @@ -373,8 +370,7 @@ protected override bool DispatchAsynchronous(in IncomingCommand cmd) } case ProtocolCommandId.ConnectionUnblocked: { - cmd.ReturnMethodBuffer(); - HandleConnectionUnblocked(); + HandleConnectionUnblocked(in cmd); return true; } case ProtocolCommandId.QueueDeclareOk: diff --git a/projects/RabbitMQ.Client/client/impl/AsyncRpcContinuations.cs b/projects/RabbitMQ.Client/client/impl/AsyncRpcContinuations.cs new file mode 100644 index 0000000000..df938fa658 --- /dev/null +++ b/projects/RabbitMQ.Client/client/impl/AsyncRpcContinuations.cs @@ -0,0 +1,479 @@ +// This source code is dual-licensed under the Apache License, version +// 2.0, and the Mozilla Public License, version 2.0. +// +// The APL v2.0: +// +//--------------------------------------------------------------------------- +// Copyright (c) 2007-2020 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//--------------------------------------------------------------------------- +// +// The MPL v2.0: +// +//--------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// +// Copyright (c) 2007-2020 VMware, Inc. All rights reserved. +//--------------------------------------------------------------------------- + +using System; +using System.Diagnostics; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; +using RabbitMQ.Client.client.framing; +using RabbitMQ.Client.ConsumerDispatching; +using RabbitMQ.Client.Exceptions; +using RabbitMQ.Client.Framing.Impl; + +namespace RabbitMQ.Client.Impl +{ + internal abstract class AsyncRpcContinuation : IRpcContinuation, IDisposable + { + private readonly CancellationTokenSource _ct; + private readonly ConfiguredTaskAwaitable _taskAwaitable; + + protected readonly TaskCompletionSource _tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + private bool _disposedValue; + + public AsyncRpcContinuation(TimeSpan continuationTimeout) + { + _ct = new CancellationTokenSource(continuationTimeout); + _ct.Token.Register(() => + { + if (_tcs.TrySetCanceled()) + { + // TODO LRB rabbitmq/rabbitmq-dotnet-client#1347 + // Cancellation was successful, does this mean we should set a TimeoutException + // in the same manner as BlockingCell? + } + }, useSynchronizationContext: false); + + _taskAwaitable = _tcs.Task.ConfigureAwait(false); + } + + public ConfiguredTaskAwaitable.ConfiguredTaskAwaiter GetAwaiter() + { + return _taskAwaitable.GetAwaiter(); + } + + // TODO LRB #1347 + // What to do if setting a result fails? + public abstract void HandleCommand(in IncomingCommand cmd); + + public virtual void HandleChannelShutdown(ShutdownEventArgs reason) + { + _tcs.SetException(new OperationInterruptedException(reason)); + } + + protected virtual void Dispose(bool disposing) + { + if (!_disposedValue) + { + if (disposing) + { + _ct.Dispose(); + } + + _disposedValue = true; + } + } + + public void Dispose() + { + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + } + + internal class ConnectionSecureOrTuneContinuation : AsyncRpcContinuation + { + public ConnectionSecureOrTuneContinuation(TimeSpan continuationTimeout) : base(continuationTimeout) + { + } + + public override void HandleCommand(in IncomingCommand cmd) + { + try + { + if (cmd.CommandId == ProtocolCommandId.ConnectionSecure) + { + var secure = new ConnectionSecure(cmd.MethodSpan); + _tcs.TrySetResult(new ConnectionSecureOrTune { m_challenge = secure._challenge }); + } + else if (cmd.CommandId == ProtocolCommandId.ConnectionTune) + { + var tune = new ConnectionTune(cmd.MethodSpan); + // TODO LRB rabbitmq/rabbitmq-dotnet-client#1347 + // What to do if setting a result fails? + _tcs.TrySetResult(new ConnectionSecureOrTune + { + m_tuneDetails = new() { m_channelMax = tune._channelMax, m_frameMax = tune._frameMax, m_heartbeatInSeconds = tune._heartbeat } + }); + } + else + { + _tcs.SetException(new InvalidOperationException($"Received unexpected command of type {cmd.CommandId}!")); + } + } + finally + { + cmd.ReturnBuffers(); + } + } + } + + internal class SimpleAsyncRpcContinuation : AsyncRpcContinuation + { + private readonly ProtocolCommandId _expectedCommandId; + + public SimpleAsyncRpcContinuation(ProtocolCommandId expectedCommandId, TimeSpan continuationTimeout) : base(continuationTimeout) + { + _expectedCommandId = expectedCommandId; + } + + public override void HandleCommand(in IncomingCommand cmd) + { + try + { + if (cmd.CommandId == _expectedCommandId) + { + _tcs.TrySetResult(true); + } + else + { + _tcs.SetException(new InvalidOperationException($"Received unexpected command of type {cmd.CommandId}!")); + } + } + finally + { + cmd.ReturnBuffers(); + } + } + } + + internal class BasicCancelAsyncRpcContinuation : SimpleAsyncRpcContinuation + { + private readonly string _consumerTag; + private readonly IConsumerDispatcher _consumerDispatcher; + + public BasicCancelAsyncRpcContinuation(string consumerTag, IConsumerDispatcher consumerDispatcher, TimeSpan continuationTimeout) + : base(ProtocolCommandId.BasicCancelOk, continuationTimeout) + { + _consumerTag = consumerTag; + _consumerDispatcher = consumerDispatcher; + } + + public override void HandleCommand(in IncomingCommand cmd) + { + try + { + if (cmd.CommandId == ProtocolCommandId.BasicCancelOk) + { + var method = new Client.Framing.Impl.BasicCancelOk(cmd.MethodSpan); + _tcs.TrySetResult(true); + Debug.Assert(_consumerTag == method._consumerTag); + _consumerDispatcher.HandleBasicCancelOk(_consumerTag); + } + else + { + _tcs.SetException(new InvalidOperationException($"Received unexpected command of type {cmd.CommandId}!")); + } + } + finally + { + cmd.ReturnBuffers(); + } + } + } + + internal class BasicConsumeAsyncRpcContinuation : AsyncRpcContinuation + { + private readonly IBasicConsumer _consumer; + private readonly IConsumerDispatcher _consumerDispatcher; + + public BasicConsumeAsyncRpcContinuation(IBasicConsumer consumer, IConsumerDispatcher consumerDispatcher, TimeSpan continuationTimeout) + : base(continuationTimeout) + { + _consumer = consumer; + _consumerDispatcher = consumerDispatcher; + } + + public override void HandleCommand(in IncomingCommand cmd) + { + try + { + if (cmd.CommandId == ProtocolCommandId.BasicConsumeOk) + { + var method = new Client.Framing.Impl.BasicConsumeOk(cmd.MethodSpan); + _tcs.TrySetResult(method._consumerTag); + _consumerDispatcher.HandleBasicConsumeOk(_consumer, method._consumerTag); + } + else + { + _tcs.SetException(new InvalidOperationException($"Received unexpected command of type {cmd.CommandId}!")); + } + } + finally + { + cmd.ReturnBuffers(); + } + } + } + + internal class BasicGetAsyncRpcContinuation : AsyncRpcContinuation + { + private readonly Func _adjustDeliveryTag; + + public BasicGetAsyncRpcContinuation(Func adjustDeliveryTag, TimeSpan continuationTimeout) + : base(continuationTimeout) + { + _adjustDeliveryTag = adjustDeliveryTag; + } + + public override void HandleCommand(in IncomingCommand cmd) + { + try + { + if (cmd.CommandId == ProtocolCommandId.BasicGetOk) + { + var method = new Client.Framing.Impl.BasicGetOk(cmd.MethodSpan); + var header = new ReadOnlyBasicProperties(cmd.HeaderSpan); + + var result = new BasicGetResult( + _adjustDeliveryTag(method._deliveryTag), + method._redelivered, + method._exchange, + method._routingKey, + method._messageCount, + header, + cmd.Body); + + _tcs.TrySetResult(result); + } + else + { + _tcs.SetException(new InvalidOperationException($"Received unexpected command of type {cmd.CommandId}!")); + } + } + finally + { + /* + * Note: since the BasicGetResult ctor copies the rented array in cmd.Body, + * we want to return all buffers here + */ + cmd.ReturnBuffers(); + } + } + } + + internal class BasicQosAsyncRpcContinuation : SimpleAsyncRpcContinuation + { + public BasicQosAsyncRpcContinuation(TimeSpan continuationTimeout) + : base(ProtocolCommandId.BasicQosOk, continuationTimeout) + { + } + } + + internal class ChannelOpenAsyncRpcContinuation : SimpleAsyncRpcContinuation + { + public ChannelOpenAsyncRpcContinuation(TimeSpan continuationTimeout) + : base(ProtocolCommandId.ChannelOpenOk, continuationTimeout) + { + } + } + + internal class ChannelCloseAsyncRpcContinuation : SimpleAsyncRpcContinuation + { + public ChannelCloseAsyncRpcContinuation(TimeSpan continuationTimeout) + : base(ProtocolCommandId.ChannelCloseOk, continuationTimeout) + { + } + + public override void HandleChannelShutdown(ShutdownEventArgs reason) + { + // Nothing to do here! + } + + public void OnConnectionShutdown(object sender, ShutdownEventArgs reason) + { + _tcs.TrySetResult(true); + } + } + + internal class ConfirmSelectAsyncRpcContinuation : SimpleAsyncRpcContinuation + { + public ConfirmSelectAsyncRpcContinuation(TimeSpan continuationTimeout) + : base(ProtocolCommandId.ConfirmSelectOk, continuationTimeout) + { + } + } + + internal class ExchangeBindAsyncRpcContinuation : SimpleAsyncRpcContinuation + { + public ExchangeBindAsyncRpcContinuation(TimeSpan continuationTimeout) + : base(ProtocolCommandId.ExchangeBindOk, continuationTimeout) + { + } + } + + internal class ExchangeDeclareAsyncRpcContinuation : SimpleAsyncRpcContinuation + { + public ExchangeDeclareAsyncRpcContinuation(TimeSpan continuationTimeout) + : base(ProtocolCommandId.ExchangeDeclareOk, continuationTimeout) + { + } + } + + internal class ExchangeDeleteAsyncRpcContinuation : SimpleAsyncRpcContinuation + { + public ExchangeDeleteAsyncRpcContinuation(TimeSpan continuationTimeout) + : base(ProtocolCommandId.ExchangeDeleteOk, continuationTimeout) + { + } + } + + internal class ExchangeUnbindAsyncRpcContinuation : SimpleAsyncRpcContinuation + { + public ExchangeUnbindAsyncRpcContinuation(TimeSpan continuationTimeout) + : base(ProtocolCommandId.ExchangeUnbindOk, continuationTimeout) + { + } + } + + internal class QueueDeclareAsyncRpcContinuation : AsyncRpcContinuation + { + public QueueDeclareAsyncRpcContinuation(TimeSpan continuationTimeout) : base(continuationTimeout) + { + } + + public override void HandleCommand(in IncomingCommand cmd) + { + try + { + if (cmd.CommandId == ProtocolCommandId.QueueDeclareOk) + { + var method = new Client.Framing.Impl.QueueDeclareOk(cmd.MethodSpan); + var result = new QueueDeclareOk(method._queue, method._messageCount, method._consumerCount); + _tcs.TrySetResult(result); + } + else + { + _tcs.SetException(new InvalidOperationException($"Received unexpected command of type {cmd.CommandId}!")); + } + } + finally + { + cmd.ReturnBuffers(); + } + } + } + + internal class QueueBindAsyncRpcContinuation : SimpleAsyncRpcContinuation + { + public QueueBindAsyncRpcContinuation(TimeSpan continuationTimeout) + : base(ProtocolCommandId.QueueBindOk, continuationTimeout) + { + } + } + + internal class QueueUnbindAsyncRpcContinuation : SimpleAsyncRpcContinuation + { + public QueueUnbindAsyncRpcContinuation(TimeSpan continuationTimeout) + : base(ProtocolCommandId.QueueUnbindOk, continuationTimeout) + { + } + } + + internal class QueueDeleteAsyncRpcContinuation : AsyncRpcContinuation + { + public QueueDeleteAsyncRpcContinuation(TimeSpan continuationTimeout) : base(continuationTimeout) + { + } + + public override void HandleCommand(in IncomingCommand cmd) + { + try + { + if (cmd.CommandId == ProtocolCommandId.QueueDeleteOk) + { + var method = new Client.Framing.Impl.QueueDeleteOk(cmd.MethodSpan); + _tcs.TrySetResult(method._messageCount); + } + else + { + _tcs.SetException(new InvalidOperationException($"Received unexpected command of type {cmd.CommandId}!")); + } + } + finally + { + cmd.ReturnBuffers(); + } + } + } + + internal class QueuePurgeAsyncRpcContinuation : AsyncRpcContinuation + { + public QueuePurgeAsyncRpcContinuation(TimeSpan continuationTimeout) : base(continuationTimeout) + { + } + + public override void HandleCommand(in IncomingCommand cmd) + { + try + { + if (cmd.CommandId == ProtocolCommandId.QueuePurgeOk) + { + var method = new Client.Framing.Impl.QueuePurgeOk(cmd.MethodSpan); + _tcs.TrySetResult(method._messageCount); + } + else + { + _tcs.SetException(new InvalidOperationException($"Received unexpected command of type {cmd.CommandId}!")); + } + } + finally + { + cmd.ReturnBuffers(); + } + } + } + + internal class TxCommitAsyncRpcContinuation : SimpleAsyncRpcContinuation + { + public TxCommitAsyncRpcContinuation(TimeSpan continuationTimeout) + : base(ProtocolCommandId.TxCommitOk, continuationTimeout) + { + } + } + + internal class TxRollbackAsyncRpcContinuation : SimpleAsyncRpcContinuation + { + public TxRollbackAsyncRpcContinuation(TimeSpan continuationTimeout) + : base(ProtocolCommandId.TxRollbackOk, continuationTimeout) + { + } + } + + internal class TxSelectAsyncRpcContinuation : SimpleAsyncRpcContinuation + { + public TxSelectAsyncRpcContinuation(TimeSpan continuationTimeout) + : base(ProtocolCommandId.TxSelectOk, continuationTimeout) + { + } + } +} diff --git a/projects/RabbitMQ.Client/client/impl/AutorecoveringChannel.cs b/projects/RabbitMQ.Client/client/impl/AutorecoveringChannel.cs index ed3a60efc9..a9d9d0c77e 100644 --- a/projects/RabbitMQ.Client/client/impl/AutorecoveringChannel.cs +++ b/projects/RabbitMQ.Client/client/impl/AutorecoveringChannel.cs @@ -87,12 +87,6 @@ public event EventHandler BasicNacks remove => InnerChannel.BasicNacks -= value; } - public event EventHandler BasicRecoverOk - { - add => InnerChannel.BasicRecoverOk += value; - remove => InnerChannel.BasicRecoverOk -= value; - } - public event EventHandler BasicReturn { add => InnerChannel.BasicReturn += value; @@ -251,6 +245,19 @@ public void Close(ushort replyCode, string replyText, bool abort) } } + public async ValueTask CloseAsync(ShutdownEventArgs reason, bool abort) + { + ThrowIfDisposed(); + try + { + await _innerChannel.CloseAsync(reason, abort); + } + finally + { + _connection.DeleteRecordedChannel(this); + } + } + public override string ToString() => InnerChannel.ToString(); @@ -273,6 +280,9 @@ public void Dispose() public void BasicAck(ulong deliveryTag, bool multiple) => InnerChannel.BasicAck(deliveryTag, multiple); + public ValueTask BasicAckAsync(ulong deliveryTag, bool multiple) + => InnerChannel.BasicAckAsync(deliveryTag, multiple); + public void BasicCancel(string consumerTag) { ThrowIfDisposed(); @@ -280,6 +290,13 @@ public void BasicCancel(string consumerTag) _innerChannel.BasicCancel(consumerTag); } + public ValueTask BasicCancelAsync(string consumerTag) + { + ThrowIfDisposed(); + _connection.DeleteRecordedConsumer(consumerTag); + return _innerChannel.BasicCancelAsync(consumerTag); + } + public void BasicCancelNoWait(string consumerTag) { ThrowIfDisposed(); @@ -287,14 +304,8 @@ public void BasicCancelNoWait(string consumerTag) _innerChannel.BasicCancelNoWait(consumerTag); } - public string BasicConsume( - string queue, - bool autoAck, - string consumerTag, - bool noLocal, - bool exclusive, - IDictionary arguments, - IBasicConsumer consumer) + public string BasicConsume(string queue, bool autoAck, string consumerTag, bool noLocal, bool exclusive, + IDictionary arguments, IBasicConsumer consumer) { string resultConsumerTag = InnerChannel.BasicConsume(queue, autoAck, consumerTag, noLocal, exclusive, arguments, consumer); var rc = new RecordedConsumer(channel: this, consumer: consumer, consumerTag: resultConsumerTag, @@ -304,12 +315,29 @@ public string BasicConsume( return resultConsumerTag; } + public async ValueTask BasicConsumeAsync(string queue, bool autoAck, string consumerTag, bool noLocal, bool exclusive, + IDictionary arguments, IBasicConsumer consumer) + { + string resultConsumerTag = await InnerChannel.BasicConsumeAsync(queue, autoAck, consumerTag, noLocal, exclusive, arguments, consumer); + var rc = new RecordedConsumer(channel: this, consumer: consumer, consumerTag: resultConsumerTag, + queue: queue, autoAck: autoAck, exclusive: exclusive, arguments: arguments); + _connection.RecordConsumer(rc); + _recordedConsumerTags.Add(resultConsumerTag); + return resultConsumerTag; + } + public BasicGetResult BasicGet(string queue, bool autoAck) => InnerChannel.BasicGet(queue, autoAck); + public ValueTask BasicGetAsync(string queue, bool autoAck) + => InnerChannel.BasicGetAsync(queue, autoAck); + public void BasicNack(ulong deliveryTag, bool multiple, bool requeue) => InnerChannel.BasicNack(deliveryTag, multiple, requeue); + public ValueTask BasicNackAsync(ulong deliveryTag, bool multiple, bool requeue) + => InnerChannel.BasicNackAsync(deliveryTag, multiple, requeue); + public void BasicPublish(string exchange, string routingKey, in TProperties basicProperties, ReadOnlyMemory body, bool mandatory) where TProperties : IReadOnlyBasicProperties, IAmqpHeader => InnerChannel.BasicPublish(exchange, routingKey, in basicProperties, body, mandatory); @@ -329,6 +357,7 @@ public ValueTask BasicPublishAsync(CachedString exchange, CachedStr public void BasicQos(uint prefetchSize, ushort prefetchCount, bool global) { ThrowIfDisposed(); + if (global) { _prefetchCountGlobal = prefetchCount; @@ -337,24 +366,45 @@ public void BasicQos(uint prefetchSize, ushort prefetchCount, bool global) { _prefetchCountConsumer = prefetchCount; } + _innerChannel.BasicQos(prefetchSize, prefetchCount, global); } - public void BasicRecover(bool requeue) - => InnerChannel.BasicRecover(requeue); + public ValueTask BasicQosAsync(uint prefetchSize, ushort prefetchCount, bool global) + { + ThrowIfDisposed(); + + if (global) + { + _prefetchCountGlobal = prefetchCount; + } + else + { + _prefetchCountConsumer = prefetchCount; + } - public void BasicRecoverAsync(bool requeue) - => InnerChannel.BasicRecoverAsync(requeue); + return _innerChannel.BasicQosAsync(prefetchSize, prefetchCount, global); + } public void BasicReject(ulong deliveryTag, bool requeue) => InnerChannel.BasicReject(deliveryTag, requeue); + public ValueTask BasicRejectAsync(ulong deliveryTag, bool requeue) + => InnerChannel.BasicRejectAsync(deliveryTag, requeue); + public void ConfirmSelect() { InnerChannel.ConfirmSelect(); _usesPublisherConfirms = true; } + public ValueTask ConfirmSelectAsync() + { + var task = InnerChannel.ConfirmSelectAsync(); + _usesPublisherConfirms = true; + return task; + } + public void ExchangeBind(string destination, string source, string routingKey, IDictionary arguments) { ThrowIfDisposed(); @@ -362,20 +412,33 @@ public void ExchangeBind(string destination, string source, string routingKey, I _innerChannel.ExchangeBind(destination, source, routingKey, arguments); } + public async ValueTask ExchangeBindAsync(string destination, string source, string routingKey, IDictionary arguments) + { + await InnerChannel.ExchangeBindAsync(destination, source, routingKey, arguments); + _connection.RecordBinding(new RecordedBinding(false, destination, source, routingKey, arguments)); + } + public void ExchangeBindNoWait(string destination, string source, string routingKey, IDictionary arguments) => InnerChannel.ExchangeBindNoWait(destination, source, routingKey, arguments); public void ExchangeDeclare(string exchange, string type, bool durable, bool autoDelete, IDictionary arguments) { - ThrowIfDisposed(); - _innerChannel.ExchangeDeclare(exchange, type, durable, autoDelete, arguments); + InnerChannel.ExchangeDeclare(exchange, type, durable, autoDelete, arguments); _connection.RecordExchange(new RecordedExchange(exchange, type, durable, autoDelete, arguments)); } + public async ValueTask ExchangeDeclareAsync(string exchange, string type, bool passive, bool durable, bool autoDelete, IDictionary arguments) + { + await InnerChannel.ExchangeDeclareAsync(exchange, type, passive, durable, autoDelete, arguments); + if (false == passive) + { + _connection.RecordExchange(new RecordedExchange(exchange, type, durable, autoDelete, arguments)); + } + } + public void ExchangeDeclareNoWait(string exchange, string type, bool durable, bool autoDelete, IDictionary arguments) { - ThrowIfDisposed(); - _innerChannel.ExchangeDeclareNoWait(exchange, type, durable, autoDelete, arguments); + InnerChannel.ExchangeDeclareNoWait(exchange, type, durable, autoDelete, arguments); _connection.RecordExchange(new RecordedExchange(exchange, type, durable, autoDelete, arguments)); } @@ -388,6 +451,12 @@ public void ExchangeDelete(string exchange, bool ifUnused) _connection.DeleteRecordedExchange(exchange); } + public async ValueTask ExchangeDeleteAsync(string exchange, bool ifUnused) + { + await InnerChannel.ExchangeDeleteAsync(exchange, ifUnused); + _connection.DeleteRecordedExchange(exchange); + } + public void ExchangeDeleteNoWait(string exchange, bool ifUnused) { InnerChannel.ExchangeDeleteNoWait(exchange, ifUnused); @@ -402,6 +471,13 @@ public void ExchangeUnbind(string destination, string source, string routingKey, _connection.DeleteAutoDeleteExchange(source); } + public async ValueTask ExchangeUnbindAsync(string destination, string source, string routingKey, IDictionary arguments) + { + await InnerChannel.ExchangeUnbindAsync(destination, source, routingKey, arguments); + _connection.DeleteRecordedBinding(new RecordedBinding(false, destination, source, routingKey, arguments)); + _connection.DeleteAutoDeleteExchange(source); + } + public void ExchangeUnbindNoWait(string destination, string source, string routingKey, IDictionary arguments) => InnerChannel.ExchangeUnbind(destination, source, routingKey, arguments); @@ -417,27 +493,30 @@ public void QueueBindNoWait(string queue, string exchange, string routingKey, ID public QueueDeclareOk QueueDeclare(string queue, bool durable, bool exclusive, bool autoDelete, IDictionary arguments) { - ThrowIfDisposed(); - QueueDeclareOk result = _innerChannel.QueueDeclare(queue, durable, exclusive, autoDelete, arguments); + QueueDeclareOk result = InnerChannel.QueueDeclare(queue, durable, exclusive, autoDelete, arguments); _connection.RecordQueue(new RecordedQueue(result.QueueName, queue.Length == 0, durable, exclusive, autoDelete, arguments)); return result; } public void QueueDeclareNoWait(string queue, bool durable, bool exclusive, bool autoDelete, IDictionary arguments) { - ThrowIfDisposed(); - _innerChannel.QueueDeclareNoWait(queue, durable, exclusive, autoDelete, arguments); + InnerChannel.QueueDeclareNoWait(queue, durable, exclusive, autoDelete, arguments); _connection.RecordQueue(new RecordedQueue(queue, queue.Length == 0, durable, exclusive, autoDelete, arguments)); } - public async ValueTask QueueDeclareAsync(string queue, bool durable, bool exclusive, bool autoDelete, IDictionary arguments) + public async ValueTask QueueDeclareAsync(string queue, bool passive, bool durable, bool exclusive, bool autoDelete, IDictionary arguments) { - ThrowIfDisposed(); - QueueDeclareOk result = await _innerChannel.QueueDeclareAsync(queue, durable, exclusive, autoDelete, arguments); - _connection.RecordQueue(new RecordedQueue(result.QueueName, queue.Length == 0, durable, exclusive, autoDelete, arguments)); + QueueDeclareOk result = await InnerChannel.QueueDeclareAsync(queue, passive, durable, exclusive, autoDelete, arguments); + if (false == passive) + { + _connection.RecordQueue(new RecordedQueue(result.QueueName, queue.Length == 0, durable, exclusive, autoDelete, arguments)); + } return result; } + public ValueTask QueueBindAsync(string queue, string exchange, string routingKey, IDictionary arguments) + => InnerChannel.QueueBindAsync(queue, exchange, routingKey, arguments); + public QueueDeclareOk QueueDeclarePassive(string queue) => InnerChannel.QueueDeclarePassive(queue); @@ -449,8 +528,14 @@ public uint ConsumerCount(string queue) public uint QueueDelete(string queue, bool ifUnused, bool ifEmpty) { - ThrowIfDisposed(); - uint result = _innerChannel.QueueDelete(queue, ifUnused, ifEmpty); + uint result = InnerChannel.QueueDelete(queue, ifUnused, ifEmpty); + _connection.DeleteRecordedQueue(queue); + return result; + } + + public async ValueTask QueueDeleteAsync(string queue, bool ifUnused, bool ifEmpty) + { + uint result = await InnerChannel.QueueDeleteAsync(queue, ifUnused, ifEmpty); _connection.DeleteRecordedQueue(queue); return result; } @@ -464,6 +549,9 @@ public void QueueDeleteNoWait(string queue, bool ifUnused, bool ifEmpty) public uint QueuePurge(string queue) => InnerChannel.QueuePurge(queue); + public ValueTask QueuePurgeAsync(string queue) + => InnerChannel.QueuePurgeAsync(queue); + public void QueueUnbind(string queue, string exchange, string routingKey, IDictionary arguments) { ThrowIfDisposed(); @@ -472,18 +560,38 @@ public void QueueUnbind(string queue, string exchange, string routingKey, IDicti _connection.DeleteAutoDeleteExchange(exchange); } + public async ValueTask QueueUnbindAsync(string queue, string exchange, string routingKey, IDictionary arguments) + { + ThrowIfDisposed(); + _connection.DeleteRecordedBinding(new RecordedBinding(true, queue, exchange, routingKey, arguments)); + await _innerChannel.QueueUnbindAsync(queue, exchange, routingKey, arguments); + _connection.DeleteAutoDeleteExchange(exchange); + } + public void TxCommit() => InnerChannel.TxCommit(); + public ValueTask TxCommitAsync() + => InnerChannel.TxCommitAsync(); + public void TxRollback() => InnerChannel.TxRollback(); + public ValueTask TxRollbackAsync() + => InnerChannel.TxRollbackAsync(); + public void TxSelect() { InnerChannel.TxSelect(); _usesTransactions = true; } + public ValueTask TxSelectAsync() + { + _usesTransactions = true; + return InnerChannel.TxSelectAsync(); + } + public Task WaitForConfirmsAsync(CancellationToken token = default) => InnerChannel.WaitForConfirmsAsync(token); diff --git a/projects/RabbitMQ.Client/client/impl/AutorecoveringConnection.Recovery.cs b/projects/RabbitMQ.Client/client/impl/AutorecoveringConnection.Recovery.cs index 733de82c78..8232be2d8b 100644 --- a/projects/RabbitMQ.Client/client/impl/AutorecoveringConnection.Recovery.cs +++ b/projects/RabbitMQ.Client/client/impl/AutorecoveringConnection.Recovery.cs @@ -123,6 +123,7 @@ private static void HandleTopologyRecoveryException(TopologyRecoveryException e) ESLog.Info($"Will not retry recovery because of {e.InnerException?.GetType().FullName}: it's not a known problem with connectivity, ignoring it", e); } + // TODO async? private bool TryPerformAutomaticRecovery() { ESLog.Info("Performing automatic recovery"); @@ -188,6 +189,7 @@ private bool TryPerformAutomaticRecovery() return false; } + // TODO async private bool TryRecoverConnectionDelegate() { try @@ -195,6 +197,7 @@ private bool TryRecoverConnectionDelegate() var defunctConnection = _innerConnection; IFrameHandler fh = _endpoints.SelectOne(_config.FrameHandlerFactory); _innerConnection = new Connection(_config, fh); + _innerConnection.Open(); _innerConnection.TakeOver(defunctConnection); return true; } diff --git a/projects/RabbitMQ.Client/client/impl/AutorecoveringConnection.cs b/projects/RabbitMQ.Client/client/impl/AutorecoveringConnection.cs index 625ba1b2d4..14c3580068 100644 --- a/projects/RabbitMQ.Client/client/impl/AutorecoveringConnection.cs +++ b/projects/RabbitMQ.Client/client/impl/AutorecoveringConnection.cs @@ -32,6 +32,7 @@ using System; using System.Collections.Generic; using System.Runtime.CompilerServices; +using System.Threading.Tasks; using RabbitMQ.Client.Events; using RabbitMQ.Client.Impl; @@ -57,7 +58,7 @@ private Connection InnerConnection } } - public AutorecoveringConnection(ConnectionConfig config, IEndpointResolver endpoints) + internal AutorecoveringConnection(ConnectionConfig config, IEndpointResolver endpoints) { _config = config; _endpoints = endpoints; @@ -77,6 +78,18 @@ public AutorecoveringConnection(ConnectionConfig config, IEndpointResolver endpo ConnectionShutdown += HandleConnectionShutdown; } + internal IConnection Open() + { + InnerConnection.Open(); + return this; + } + + internal async ValueTask OpenAsync() + { + await InnerConnection.OpenAsync(); + return this; + } + public event EventHandler RecoverySucceeded { add => _recoverySucceededWrapper.AddHandler(value); @@ -170,10 +183,20 @@ public RecoveryAwareChannel CreateNonRecoveringChannel() return result; } + public async ValueTask CreateNonRecoveringChannelAsync() + { + ISession session = InnerConnection.CreateSession(); + var result = new RecoveryAwareChannel(_config, session); + return await result.OpenAsync() as RecoveryAwareChannel; + } + public override string ToString() => $"AutorecoveringConnection({InnerConnection.Id},{Endpoint},{GetHashCode()})"; - internal IFrameHandler FrameHandler => InnerConnection.FrameHandler; + internal void CloseFrameHandler() + { + InnerConnection.FrameHandler.Close(); + } ///API-side invocation of updating the secret. public void UpdateSecret(string newSecret, string reason) @@ -197,7 +220,17 @@ public void Close(ushort reasonCode, string reasonText, TimeSpan timeout, bool a public IChannel CreateChannel() { EnsureIsOpen(); - AutorecoveringChannel m = new AutorecoveringChannel(this, CreateNonRecoveringChannel()); + RecoveryAwareChannel recoveryAwareChannel = CreateNonRecoveringChannel(); + AutorecoveringChannel m = new AutorecoveringChannel(this, recoveryAwareChannel); + RecordChannel(m); + return m; + } + + public async ValueTask CreateChannelAsync() + { + EnsureIsOpen(); + RecoveryAwareChannel recoveryAwareChannel = await CreateNonRecoveringChannelAsync(); + AutorecoveringChannel m = new AutorecoveringChannel(this, recoveryAwareChannel); RecordChannel(m); return m; } diff --git a/projects/RabbitMQ.Client/client/impl/ChannelBase.cs b/projects/RabbitMQ.Client/client/impl/ChannelBase.cs index decf6ac84f..46f30ed61b 100644 --- a/projects/RabbitMQ.Client/client/impl/ChannelBase.cs +++ b/projects/RabbitMQ.Client/client/impl/ChannelBase.cs @@ -30,8 +30,8 @@ //--------------------------------------------------------------------------- using System; -using System.Buffers; using System.Collections.Generic; +using System.Diagnostics; using System.IO; using System.Runtime.CompilerServices; using System.Text; @@ -52,7 +52,7 @@ internal abstract class ChannelBase : IChannel, IRecoverable internal TaskCompletionSource m_connectionStartCell; // AMQP only allows one RPC operation to be active at a time. - private readonly SemaphoreSlim _rpcSemaphore = new SemaphoreSlim(1, 1); + protected readonly SemaphoreSlim _rpcSemaphore = new SemaphoreSlim(1, 1); private readonly RpcContinuationQueue _continuationQueue = new RpcContinuationQueue(); private readonly ManualResetEventSlim _flowControlBlock = new ManualResetEventSlim(true); @@ -76,7 +76,6 @@ protected ChannelBase(ConnectionConfig config, ISession session) Action onException = (exception, context) => OnCallbackException(CallbackExceptionEventArgs.Build(exception, context)); _basicAcksWrapper = new EventingWrapper("OnBasicAck", onException); _basicNacksWrapper = new EventingWrapper("OnBasicNack", onException); - _basicRecoverOkWrapper = new EventingWrapper("OnBasicRecover", onException); _basicReturnWrapper = new EventingWrapper("OnBasicReturn", onException); _callbackExceptionWrapper = new EventingWrapper(string.Empty, (exception, context) => { }); _flowControlWrapper = new EventingWrapper("OnFlowControl", onException); @@ -104,13 +103,6 @@ public event EventHandler BasicNacks } private EventingWrapper _basicNacksWrapper; - public event EventHandler BasicRecoverOk - { - add => _basicRecoverOkWrapper.AddHandler(value); - remove => _basicRecoverOkWrapper.RemoveHandler(value); - } - private EventingWrapper _basicRecoverOkWrapper; - public event EventHandler BasicReturn { add => _basicReturnWrapper.AddHandler(value); @@ -183,7 +175,6 @@ protected void TakeOver(ChannelBase other) { _basicAcksWrapper.Takeover(other._basicAcksWrapper); _basicNacksWrapper.Takeover(other._basicNacksWrapper); - _basicRecoverOkWrapper.Takeover(other._basicRecoverOkWrapper); _basicReturnWrapper.Takeover(other._basicReturnWrapper); _callbackExceptionWrapper.Takeover(other._callbackExceptionWrapper); _flowControlWrapper.Takeover(other._flowControlWrapper); @@ -193,23 +184,68 @@ protected void TakeOver(ChannelBase other) public void Close(ushort replyCode, string replyText, bool abort) { - _ = CloseAsync(new ShutdownEventArgs(ShutdownInitiator.Application, replyCode, replyText), abort); - } - - private async Task CloseAsync(ShutdownEventArgs reason, bool abort) - { + var reason = new ShutdownEventArgs(ShutdownInitiator.Application, replyCode, replyText); var k = new ShutdownContinuation(); ChannelShutdown += k.OnConnectionShutdown; try { ConsumerDispatcher.Quiesce(); + if (SetCloseReason(reason)) { _Private_ChannelClose(reason.ReplyCode, reason.ReplyText, 0, 0); } k.Wait(TimeSpan.FromMilliseconds(10000)); + ConsumerDispatcher.WaitForShutdownAsync().ConfigureAwait(false); + } + catch (AlreadyClosedException) + { + if (!abort) + { + throw; + } + } + catch (IOException) + { + if (!abort) + { + throw; + } + } + catch (Exception) + { + if (!abort) + { + throw; + } + } + finally + { + ChannelShutdown -= k.OnConnectionShutdown; + } + } + + public async ValueTask CloseAsync(ShutdownEventArgs reason, bool abort) + { + await _rpcSemaphore.WaitAsync().ConfigureAwait(false); + using var k = new ChannelCloseAsyncRpcContinuation(ContinuationTimeout); + try + { + ChannelShutdown += k.OnConnectionShutdown; + Enqueue(k); + ConsumerDispatcher.Quiesce(); + + if (SetCloseReason(reason)) + { + var method = new ChannelClose(reason.ReplyCode, reason.ReplyText, reason.ClassId, reason.MethodId); + await ModelSendAsync(method).ConfigureAwait(false); + } + + bool result = await k; + Debug.Assert(result); + await ConsumerDispatcher.WaitForShutdownAsync().ConfigureAwait(false); } catch (AlreadyClosedException) @@ -235,6 +271,7 @@ private async Task CloseAsync(ShutdownEventArgs reason, bool abort) } finally { + _rpcSemaphore.Release(); ChannelShutdown -= k.OnConnectionShutdown; } } @@ -246,11 +283,12 @@ internal async ValueTask ConnectionOpenAsync(string virtualHost) internal async ValueTask ConnectionSecureOkAsync(byte[] response) { - var k = new ConnectionSecureOrTuneContinuation(); await _rpcSemaphore.WaitAsync().ConfigureAwait(false); - Enqueue(k); try { + using var k = new ConnectionSecureOrTuneContinuation(ContinuationTimeout); + Enqueue(k); + try { _Private_ConnectionSecureOk(response); @@ -273,13 +311,16 @@ internal async ValueTask ConnectionSecureOkAsync(byte[] internal async ValueTask ConnectionStartOkAsync(IDictionary clientProperties, string mechanism, byte[] response, string locale) { - var k = new ConnectionSecureOrTuneContinuation(); await _rpcSemaphore.WaitAsync().ConfigureAwait(false); - Enqueue(k); try { + using var k = new ConnectionSecureOrTuneContinuation(ContinuationTimeout); + Enqueue(k); + try { + // TODO LRB rabbitmq/rabbitmq-dotnet-client#1347 + // Not yet async _Private_ConnectionStartOk(clientProperties, mechanism, response, locale); } catch (AlreadyClosedException) @@ -311,6 +352,27 @@ protected void Enqueue(IRpcContinuation k) } } + internal async ValueTask OpenAsync() + { + await _rpcSemaphore.WaitAsync().ConfigureAwait(false); + using var k = new ChannelOpenAsyncRpcContinuation(ContinuationTimeout); + try + { + Enqueue(k); + + var method = new ChannelOpen(); + await ModelSendAsync(method).ConfigureAwait(false); + + bool result = await k; + Debug.Assert(result); + return this; + } + finally + { + _rpcSemaphore.Release(); + } + } + internal void FinishClose() { var reason = CloseReason; @@ -335,54 +397,57 @@ protected void ChannelRpc(in TMethod method, ProtocolCommandId returnCo where TMethod : struct, IOutgoingAmqpMethod { var k = new SimpleBlockingRpcContinuation(); - IncomingCommand reply; + IncomingCommand reply = default; _rpcSemaphore.Wait(); try { Enqueue(k); Session.Transmit(in method); k.GetReply(ContinuationTimeout, out reply); + + if (reply.CommandId != returnCommandId) + { + throw new UnexpectedMethodException(reply.CommandId, returnCommandId); + } } finally { + reply.ReturnBuffers(); _rpcSemaphore.Release(); } - - reply.ReturnMethodBuffer(); - - if (reply.CommandId != returnCommandId) - { - throw new UnexpectedMethodException(reply.CommandId, returnCommandId); - } } - protected TReturn ChannelRpc(in TMethod method, ProtocolCommandId returnCommandId, Func, TReturn> createFunc) + protected TReturn ChannelRpc(in TMethod method, ProtocolCommandId returnCommandId, Func createFunc) where TMethod : struct, IOutgoingAmqpMethod { - var k = new SimpleBlockingRpcContinuation(); - IncomingCommand reply; - - _rpcSemaphore.Wait(); + IncomingCommand reply = default; try { - Enqueue(k); - Session.Transmit(in method); - k.GetReply(ContinuationTimeout, out reply); + var k = new SimpleBlockingRpcContinuation(); + + _rpcSemaphore.Wait(); + try + { + Enqueue(k); + Session.Transmit(in method); + k.GetReply(ContinuationTimeout, out reply); + } + finally + { + _rpcSemaphore.Release(); + } + + if (reply.CommandId != returnCommandId) + { + throw new UnexpectedMethodException(reply.CommandId, returnCommandId); + } + + return createFunc(reply.Method); } finally { - _rpcSemaphore.Release(); - } - - if (reply.CommandId != returnCommandId) - { - reply.ReturnMethodBuffer(); - throw new UnexpectedMethodException(reply.CommandId, returnCommandId); + reply.ReturnBuffers(); } - - var returnValue = createFunc(reply.MethodBytes); - reply.ReturnMethodBuffer(); - return returnValue; } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -468,7 +533,8 @@ private void OnSessionShutdown(object sender, ShutdownEventArgs reason) internal bool SetCloseReason(ShutdownEventArgs reason) { - return System.Threading.Interlocked.CompareExchange(ref _closeReason, reason, null) is null; + // NB: this ensures that Close is only called once on a channel + return Interlocked.CompareExchange(ref _closeReason, reason, null) is null; } public override string ToString() @@ -485,6 +551,7 @@ protected virtual void Dispose(bool disposing) { // dispose managed resources this.Abort(); + _rpcSemaphore.Dispose(); } // dispose unmanaged resources @@ -494,37 +561,49 @@ protected virtual void Dispose(bool disposing) protected void HandleBasicAck(in IncomingCommand cmd) { - var ack = new BasicAck(cmd.MethodBytes.Span); - cmd.ReturnMethodBuffer(); - if (!_basicAcksWrapper.IsEmpty) + try { - var args = new BasicAckEventArgs + var ack = new BasicAck(cmd.MethodSpan); + if (!_basicAcksWrapper.IsEmpty) { - DeliveryTag = ack._deliveryTag, - Multiple = ack._multiple - }; - _basicAcksWrapper.Invoke(this, args); - } + var args = new BasicAckEventArgs + { + DeliveryTag = ack._deliveryTag, + Multiple = ack._multiple + }; + _basicAcksWrapper.Invoke(this, args); + } - HandleAckNack(ack._deliveryTag, ack._multiple, false); + HandleAckNack(ack._deliveryTag, ack._multiple, false); + } + finally + { + cmd.ReturnBuffers(); + } } protected void HandleBasicNack(in IncomingCommand cmd) { - var nack = new BasicNack(cmd.MethodBytes.Span); - cmd.ReturnMethodBuffer(); - if (!_basicNacksWrapper.IsEmpty) + try { - var args = new BasicNackEventArgs + var nack = new BasicNack(cmd.MethodSpan); + if (!_basicNacksWrapper.IsEmpty) { - DeliveryTag = nack._deliveryTag, - Multiple = nack._multiple, - Requeue = nack._requeue - }; - _basicNacksWrapper.Invoke(this, args); - } + var args = new BasicNackEventArgs + { + DeliveryTag = nack._deliveryTag, + Multiple = nack._multiple, + Requeue = nack._requeue + }; + _basicNacksWrapper.Invoke(this, args); + } - HandleAckNack(nack._deliveryTag, nack._multiple, true); + HandleAckNack(nack._deliveryTag, nack._multiple, true); + } + finally + { + cmd.ReturnBuffers(); + } } protected void HandleAckNack(ulong deliveryTag, bool multiple, bool isNack) @@ -574,234 +653,350 @@ protected void HandleAckNack(ulong deliveryTag, bool multiple, bool isNack) protected void HandleBasicCancel(in IncomingCommand cmd) { - var consumerTag = new Client.Framing.Impl.BasicCancel(cmd.MethodBytes.Span)._consumerTag; - cmd.ReturnMethodBuffer(); - ConsumerDispatcher.HandleBasicCancel(consumerTag); + try + { + var consumerTag = new Client.Framing.Impl.BasicCancel(cmd.MethodSpan)._consumerTag; + ConsumerDispatcher.HandleBasicCancel(consumerTag); + } + finally + { + cmd.ReturnBuffers(); + } } - protected void HandleBasicCancelOk(in IncomingCommand cmd) + protected bool HandleBasicCancelOk(in IncomingCommand cmd) { - var k = (BasicConsumerRpcContinuation)_continuationQueue.Next(); - var consumerTag = new Client.Framing.Impl.BasicCancelOk(cmd.MethodBytes.Span)._consumerTag; - cmd.ReturnMethodBuffer(); - ConsumerDispatcher.HandleBasicCancelOk(consumerTag); - k.HandleCommand(IncomingCommand.Empty); // release the continuation. + if (_continuationQueue.TryPeek(out var k)) + { + try + { + _continuationQueue.Next(); + string consumerTag = new Client.Framing.Impl.BasicCancelOk(cmd.MethodSpan)._consumerTag; + ConsumerDispatcher.HandleBasicCancelOk(consumerTag); + k.HandleCommand(IncomingCommand.Empty); // release the continuation. + return true; + } + finally + { + cmd.ReturnBuffers(); + } + } + else + { + return false; + } } - protected void HandleBasicConsumeOk(in IncomingCommand cmd) + protected bool HandleBasicConsumeOk(in IncomingCommand cmd) { - var consumerTag = new Client.Framing.Impl.BasicConsumeOk(cmd.MethodBytes.Span)._consumerTag; - cmd.ReturnMethodBuffer(); - var k = (BasicConsumerRpcContinuation)_continuationQueue.Next(); - k.m_consumerTag = consumerTag; - ConsumerDispatcher.HandleBasicConsumeOk(k.m_consumer, consumerTag); - k.HandleCommand(IncomingCommand.Empty); // release the continuation. + if (_continuationQueue.TryPeek(out var k)) + { + try + { + _continuationQueue.Next(); + var consumerTag = new Client.Framing.Impl.BasicConsumeOk(cmd.MethodSpan)._consumerTag; + k.m_consumerTag = consumerTag; + ConsumerDispatcher.HandleBasicConsumeOk(k.m_consumer, consumerTag); + k.HandleCommand(IncomingCommand.Empty); // release the continuation. + return true; + } + finally + { + cmd.ReturnBuffers(); + } + } + else + { + return false; + } } protected void HandleBasicDeliver(in IncomingCommand cmd) { - var method = new Client.Framing.Impl.BasicDeliver(cmd.MethodBytes.Span); - cmd.ReturnMethodBuffer(); - var header = new ReadOnlyBasicProperties(cmd.HeaderBytes.Span); - cmd.ReturnHeaderBuffer(); - - ConsumerDispatcher.HandleBasicDeliver( - method._consumerTag, - AdjustDeliveryTag(method._deliveryTag), - method._redelivered, - method._exchange, - method._routingKey, - header, - cmd.Body, - cmd.TakeoverBody()); - } - - protected void HandleBasicGetOk(in IncomingCommand cmd) - { - var method = new BasicGetOk(cmd.MethodBytes.Span); - cmd.ReturnMethodBuffer(); - var header = new ReadOnlyBasicProperties(cmd.HeaderBytes.Span); - cmd.ReturnHeaderBuffer(); - - var k = (BasicGetRpcContinuation)_continuationQueue.Next(); - k.m_result = new BasicGetResult( - AdjustDeliveryTag(method._deliveryTag), - method._redelivered, - method._exchange, - method._routingKey, - method._messageCount, - header, - cmd.Body, - cmd.TakeoverBody()); - k.HandleCommand(IncomingCommand.Empty); // release the continuation. + try + { + var method = new Client.Framing.Impl.BasicDeliver(cmd.MethodSpan); + var header = new ReadOnlyBasicProperties(cmd.HeaderSpan); + ConsumerDispatcher.HandleBasicDeliver( + method._consumerTag, + AdjustDeliveryTag(method._deliveryTag), + method._redelivered, + method._exchange, + method._routingKey, + header, + cmd.Body); + } + finally + { + /* + * Note: do not return the Body as it is necessary for handling + * the Basic.Deliver method by client code + */ + cmd.ReturnMethodAndHeaderBuffers(); + } } - protected virtual ulong AdjustDeliveryTag(ulong deliveryTag) + protected bool HandleBasicGetOk(in IncomingCommand cmd) { - return deliveryTag; + if (_continuationQueue.TryPeek(out var k)) + { + try + { + var method = new BasicGetOk(cmd.MethodSpan); + var header = new ReadOnlyBasicProperties(cmd.HeaderSpan); + _continuationQueue.Next(); + k.m_result = new BasicGetResult( + AdjustDeliveryTag(method._deliveryTag), + method._redelivered, + method._exchange, + method._routingKey, + method._messageCount, + header, + cmd.Body); + k.HandleCommand(IncomingCommand.Empty); // release the continuation. + return true; + } + finally + { + /* + * Note: since the BasicGetResult ctor copies the rented array in cmd.Body, + * we want to return all buffers here + */ + cmd.ReturnBuffers(); + } + } + else + { + return false; + } } - protected void HandleBasicGetEmpty() + protected virtual ulong AdjustDeliveryTag(ulong deliveryTag) { - var k = (BasicGetRpcContinuation)_continuationQueue.Next(); - k.m_result = null; - k.HandleCommand(IncomingCommand.Empty); // release the continuation. + return deliveryTag; } - protected void HandleBasicRecoverOk() + protected void HandleBasicGetEmpty(in IncomingCommand cmd) { - var k = (SimpleBlockingRpcContinuation)_continuationQueue.Next(); - _basicRecoverOkWrapper.Invoke(this, EventArgs.Empty); - k.HandleCommand(IncomingCommand.Empty); + try + { + var k = (BasicGetRpcContinuation)_continuationQueue.Next(); + k.m_result = null; + k.HandleCommand(IncomingCommand.Empty); // release the continuation. + } + finally + { + cmd.ReturnBuffers(); + } } protected void HandleBasicReturn(in IncomingCommand cmd) { - if (!_basicReturnWrapper.IsEmpty) + try { - var basicReturn = new BasicReturn(cmd.MethodBytes.Span); - var e = new BasicReturnEventArgs + if (!_basicReturnWrapper.IsEmpty) { - ReplyCode = basicReturn._replyCode, - ReplyText = basicReturn._replyText, - Exchange = basicReturn._exchange, - RoutingKey = basicReturn._routingKey, - BasicProperties = new ReadOnlyBasicProperties(cmd.HeaderBytes.Span), - Body = cmd.Body - }; - _basicReturnWrapper.Invoke(this, e); + var basicReturn = new BasicReturn(cmd.MethodSpan); + var e = new BasicReturnEventArgs(basicReturn._replyCode, basicReturn._replyText, + basicReturn._exchange, basicReturn._routingKey, + new ReadOnlyBasicProperties(cmd.HeaderSpan), cmd.Body); + _basicReturnWrapper.Invoke(this, e); + } + } + finally + { + /* + * Note: since the BasicReturnEventArgs ctor copies the rented array in cmd.Body, + * we want to return all buffers here + */ + cmd.ReturnBuffers(); } - cmd.ReturnMethodBuffer(); - cmd.ReturnHeaderBuffer(); - ArrayPool.Shared.Return(cmd.TakeoverBody()); } protected void HandleChannelClose(in IncomingCommand cmd) { - var channelClose = new ChannelClose(cmd.MethodBytes.Span); - cmd.ReturnMethodBuffer(); - SetCloseReason(new ShutdownEventArgs(ShutdownInitiator.Peer, - channelClose._replyCode, - channelClose._replyText, - channelClose._classId, - channelClose._methodId)); - - Session.Close(CloseReason, false); try { + var channelClose = new ChannelClose(cmd.MethodSpan); + SetCloseReason(new ShutdownEventArgs(ShutdownInitiator.Peer, + channelClose._replyCode, + channelClose._replyText, + channelClose._classId, + channelClose._methodId)); + + Session.Close(CloseReason, false); + _Private_ChannelCloseOk(); } finally { + cmd.ReturnBuffers(); Session.Notify(); } } - protected void HandleChannelCloseOk() + protected void HandleChannelCloseOk(in IncomingCommand cmd) { - FinishClose(); + try + { + /* + * Note: + * This call _must_ come before completing the async continuation + */ + FinishClose(); + + if (_continuationQueue.TryPeek(out var k)) + { + _continuationQueue.Next(); + k.HandleCommand(cmd); + } + } + finally + { + cmd.ReturnBuffers(); + } } protected void HandleChannelFlow(in IncomingCommand cmd) { - var active = new ChannelFlow(cmd.MethodBytes.Span)._active; - cmd.ReturnMethodBuffer(); - if (active) - { - _flowControlBlock.Set(); - } - else + try { - _flowControlBlock.Reset(); - } + var active = new ChannelFlow(cmd.MethodSpan)._active; + if (active) + { + _flowControlBlock.Set(); + } + else + { + _flowControlBlock.Reset(); + } - _Private_ChannelFlowOk(active); + _Private_ChannelFlowOk(active); - if (!_flowControlWrapper.IsEmpty) + if (!_flowControlWrapper.IsEmpty) + { + _flowControlWrapper.Invoke(this, new FlowControlEventArgs(active)); + } + } + finally { - _flowControlWrapper.Invoke(this, new FlowControlEventArgs(active)); + cmd.ReturnBuffers(); } } protected void HandleConnectionBlocked(in IncomingCommand cmd) { - var reason = new ConnectionBlocked(cmd.MethodBytes.Span)._reason; - cmd.ReturnMethodBuffer(); - Session.Connection.HandleConnectionBlocked(reason); + try + { + var reason = new ConnectionBlocked(cmd.MethodSpan)._reason; + Session.Connection.HandleConnectionBlocked(reason); + } + finally + { + cmd.ReturnBuffers(); + } } protected void HandleConnectionClose(in IncomingCommand cmd) { - var method = new ConnectionClose(cmd.MethodBytes.Span); - cmd.ReturnMethodBuffer(); - var reason = new ShutdownEventArgs(ShutdownInitiator.Peer, method._replyCode, method._replyText, method._classId, method._methodId); try { - Session.Connection.InternalClose(reason); - _Private_ConnectionCloseOk(); - SetCloseReason(Session.Connection.CloseReason); - } - catch (IOException) - { - // Ignored. We're only trying to be polite by sending - // the close-ok, after all. + var method = new ConnectionClose(cmd.MethodSpan); + var reason = new ShutdownEventArgs(ShutdownInitiator.Peer, method._replyCode, method._replyText, method._classId, method._methodId); + try + { + Session.Connection.InternalClose(reason); + _Private_ConnectionCloseOk(); + SetCloseReason(Session.Connection.CloseReason); + } + catch (IOException) + { + // Ignored. We're only trying to be polite by sending + // the close-ok, after all. + } + catch (AlreadyClosedException) + { + // Ignored. We're only trying to be polite by sending + // the close-ok, after all. + } } - catch (AlreadyClosedException) + finally { - // Ignored. We're only trying to be polite by sending - // the close-ok, after all. + cmd.ReturnBuffers(); } } protected void HandleConnectionSecure(in IncomingCommand cmd) { - var k = (ConnectionSecureOrTuneContinuation)_continuationQueue.Next(); + using var k = (ConnectionSecureOrTuneContinuation)_continuationQueue.Next(); k.HandleCommand(IncomingCommand.Empty); // release the continuation. } protected void HandleConnectionStart(in IncomingCommand cmd) { - if (m_connectionStartCell is null) + try { - var reason = new ShutdownEventArgs(ShutdownInitiator.Library, Constants.CommandInvalid, "Unexpected Connection.Start"); - Session.Connection.Close(reason, false, InternalConstants.DefaultConnectionCloseTimeout); - } + if (m_connectionStartCell is null) + { + var reason = new ShutdownEventArgs(ShutdownInitiator.Library, Constants.CommandInvalid, "Unexpected Connection.Start"); + Session.Connection.Close(reason, false, InternalConstants.DefaultConnectionCloseTimeout); + } - var method = new ConnectionStart(cmd.MethodBytes.Span); - cmd.ReturnMethodBuffer(); - var details = new ConnectionStartDetails + var method = new ConnectionStart(cmd.MethodSpan); + var details = new ConnectionStartDetails + { + m_versionMajor = method._versionMajor, + m_versionMinor = method._versionMinor, + m_serverProperties = method._serverProperties, + m_mechanisms = method._mechanisms, + m_locales = method._locales + }; + m_connectionStartCell?.SetResult(details); + m_connectionStartCell = null; + } + finally { - m_versionMajor = method._versionMajor, - m_versionMinor = method._versionMinor, - m_serverProperties = method._serverProperties, - m_mechanisms = method._mechanisms, - m_locales = method._locales - }; - m_connectionStartCell?.SetResult(details); - m_connectionStartCell = null; + cmd.ReturnBuffers(); + } } protected void HandleConnectionTune(in IncomingCommand cmd) { - var k = (ConnectionSecureOrTuneContinuation)_continuationQueue.Next(); - k.HandleCommand(cmd); // release the continuation. + using var k = (ConnectionSecureOrTuneContinuation)_continuationQueue.Next(); + /* + * Note: releases the continuation and returns the buffers + */ + k.HandleCommand(cmd); } - protected void HandleConnectionUnblocked() + protected void HandleConnectionUnblocked(in IncomingCommand cmd) { - Session.Connection.HandleConnectionUnblocked(); + try + { + Session.Connection.HandleConnectionUnblocked(); + } + finally + { + cmd.ReturnBuffers(); + } } protected bool HandleQueueDeclareOk(in IncomingCommand cmd) { if (_continuationQueue.TryPeek(out var k)) { - _continuationQueue.Next(); - var method = new Client.Framing.Impl.QueueDeclareOk(cmd.MethodBytes.Span); - cmd.ReturnMethodBuffer(); - k.m_result = new QueueDeclareOk(method._queue, method._messageCount, method._consumerCount); - k.HandleCommand(IncomingCommand.Empty); // release the continuation. - return true; + try + { + _continuationQueue.Next(); + var method = new Client.Framing.Impl.QueueDeclareOk(cmd.MethodSpan); + k.m_result = new QueueDeclareOk(method._queue, method._messageCount, method._consumerCount); + k.HandleCommand(IncomingCommand.Empty); // release the continuation. + return true; + } + finally + { + cmd.ReturnBuffers(); + } } else { @@ -815,8 +1010,6 @@ protected bool HandleQueueDeclareOk(in IncomingCommand cmd) public abstract void _Private_BasicGet(string queue, bool autoAck); - public abstract void _Private_BasicRecover(bool requeue); - public abstract void _Private_ChannelClose(ushort replyCode, string replyText, ushort classId, ushort methodId); public abstract void _Private_ChannelCloseOk(); @@ -857,10 +1050,11 @@ protected bool HandleQueueDeclareOk(in IncomingCommand cmd) public abstract void BasicAck(ulong deliveryTag, bool multiple); + public abstract ValueTask BasicAckAsync(ulong deliveryTag, bool multiple); + public void BasicCancel(string consumerTag) { - var k = new BasicConsumerRpcContinuation { m_consumerTag = consumerTag }; - + var k = new BasicConsumeRpcContinuation { m_consumerTag = consumerTag }; _rpcSemaphore.Wait(); try { @@ -874,13 +1068,35 @@ public void BasicCancel(string consumerTag) } } - public void BasicCancelNoWait(string consumerTag) + public async ValueTask BasicCancelAsync(string consumerTag) { - _Private_BasicCancel(consumerTag, true); - ConsumerDispatcher.GetAndRemoveConsumer(consumerTag); + await _rpcSemaphore.WaitAsync().ConfigureAwait(false); + try + { + using var k = new BasicCancelAsyncRpcContinuation(consumerTag, ConsumerDispatcher, ContinuationTimeout); + Enqueue(k); + + var method = new Client.Framing.Impl.BasicCancel(consumerTag, false); + await ModelSendAsync(method).ConfigureAwait(false); + + bool result = await k; + Debug.Assert(result); + return; + } + finally + { + _rpcSemaphore.Release(); + } + } + + public void BasicCancelNoWait(string consumerTag) + { + _Private_BasicCancel(consumerTag, true); + ConsumerDispatcher.GetAndRemoveConsumer(consumerTag); } - public string BasicConsume(string queue, bool autoAck, string consumerTag, bool noLocal, bool exclusive, IDictionary arguments, IBasicConsumer consumer) + public string BasicConsume(string queue, bool autoAck, string consumerTag, bool noLocal, bool exclusive, + IDictionary arguments, IBasicConsumer consumer) { // TODO: Replace with flag if (ConsumerDispatcher is AsyncConsumerDispatcher) @@ -892,7 +1108,7 @@ public string BasicConsume(string queue, bool autoAck, string consumerTag, bool } } - var k = new BasicConsumerRpcContinuation { m_consumer = consumer }; + var k = new BasicConsumeRpcContinuation { m_consumer = consumer }; _rpcSemaphore.Wait(); try @@ -900,8 +1116,7 @@ public string BasicConsume(string queue, bool autoAck, string consumerTag, bool Enqueue(k); // Non-nowait. We have an unconventional means of getting // the RPC response, but a response is still expected. - _Private_BasicConsume(queue, consumerTag, noLocal, autoAck, exclusive, - /*nowait:*/ false, arguments); + _Private_BasicConsume(queue, consumerTag, noLocal, autoAck, exclusive, false, arguments); k.GetReply(ContinuationTimeout); } finally @@ -914,6 +1129,35 @@ public string BasicConsume(string queue, bool autoAck, string consumerTag, bool return actualConsumerTag; } + public async ValueTask BasicConsumeAsync(string queue, bool autoAck, string consumerTag, bool noLocal, bool exclusive, + IDictionary arguments, IBasicConsumer consumer) + { + if (ConsumerDispatcher is AsyncConsumerDispatcher) + { + if (!(consumer is IAsyncBasicConsumer)) + { + // TODO: Friendly message + throw new InvalidOperationException("In the async mode you have to use an async consumer"); + } + } + + await _rpcSemaphore.WaitAsync().ConfigureAwait(false); + try + { + using var k = new BasicConsumeAsyncRpcContinuation(consumer, ConsumerDispatcher, ContinuationTimeout); + Enqueue(k); + + var method = new Client.Framing.Impl.BasicConsume(queue, consumerTag, noLocal, autoAck, exclusive, false, arguments); + await ModelSendAsync(method).ConfigureAwait(false); + + return await k; + } + finally + { + _rpcSemaphore.Release(); + } + } + public BasicGetResult BasicGet(string queue, bool autoAck) { var k = new BasicGetRpcContinuation(); @@ -933,8 +1177,29 @@ public BasicGetResult BasicGet(string queue, bool autoAck) return k.m_result; } + public async ValueTask BasicGetAsync(string queue, bool autoAck) + { + await _rpcSemaphore.WaitAsync().ConfigureAwait(false); + try + { + using var k = new BasicGetAsyncRpcContinuation(AdjustDeliveryTag, ContinuationTimeout); + Enqueue(k); + + var method = new BasicGet(queue, autoAck); + await ModelSendAsync(method).ConfigureAwait(false); + + return await k; + } + finally + { + _rpcSemaphore.Release(); + } + } + public abstract void BasicNack(ulong deliveryTag, bool multiple, bool requeue); + public abstract ValueTask BasicNackAsync(ulong deliveryTag, bool multiple, bool requeue); + public void BasicPublish(string exchange, string routingKey, in TProperties basicProperties, ReadOnlyMemory body, bool mandatory) where TProperties : IReadOnlyBasicProperties, IAmqpHeader { @@ -1012,16 +1277,20 @@ public void UpdateSecret(string newSecret, string reason) public abstract void BasicQos(uint prefetchSize, ushort prefetchCount, bool global); - public void BasicRecover(bool requeue) + public async ValueTask BasicQosAsync(uint prefetchSize, ushort prefetchCount, bool global) { - var k = new SimpleBlockingRpcContinuation(); - - _rpcSemaphore.Wait(); + await _rpcSemaphore.WaitAsync().ConfigureAwait(false); try { + using var k = new BasicQosAsyncRpcContinuation(ContinuationTimeout); Enqueue(k); - _Private_BasicRecover(requeue); - k.GetReply(ContinuationTimeout); + + var method = new BasicQos(prefetchSize, prefetchCount, global); + await ModelSendAsync(method).ConfigureAwait(false); + + bool result = await k; + Debug.Assert(result); + return; } finally { @@ -1029,10 +1298,10 @@ public void BasicRecover(bool requeue) } } - public abstract void BasicRecoverAsync(bool requeue); - public abstract void BasicReject(ulong deliveryTag, bool requeue); + public abstract ValueTask BasicRejectAsync(ulong deliveryTag, bool requeue); + public void ConfirmSelect() { if (NextPublishSeqNo == 0UL) @@ -1044,11 +1313,60 @@ public void ConfirmSelect() _Private_ConfirmSelect(false); } + public async ValueTask ConfirmSelectAsync() + { + await _rpcSemaphore.WaitAsync().ConfigureAwait(false); + try + { + if (NextPublishSeqNo == 0UL) + { + _confirmsTaskCompletionSources = new List>(); + NextPublishSeqNo = 1; + } + + using var k = new ConfirmSelectAsyncRpcContinuation(ContinuationTimeout); + Enqueue(k); + + var method = new ConfirmSelect(false); + await ModelSendAsync(method).ConfigureAwait(false); + + bool result = await k; + Debug.Assert(result); + + return; + } + finally + { + _rpcSemaphore.Release(); + } + } + public void ExchangeBind(string destination, string source, string routingKey, IDictionary arguments) { _Private_ExchangeBind(destination, source, routingKey, false, arguments); } + public async ValueTask ExchangeBindAsync(string destination, string source, string routingKey, IDictionary arguments) + { + await _rpcSemaphore.WaitAsync().ConfigureAwait(false); + try + { + using var k = new ExchangeBindAsyncRpcContinuation(ContinuationTimeout); + Enqueue(k); + + var method = new ExchangeBind(destination, source, routingKey, false, arguments); + await ModelSendAsync(method).ConfigureAwait(false); + + bool result = await k; + Debug.Assert(result); + return; + } + finally + { + _rpcSemaphore.Release(); + } + } + public void ExchangeBindNoWait(string destination, string source, string routingKey, IDictionary arguments) { _Private_ExchangeBind(destination, source, routingKey, true, arguments); @@ -1059,6 +1377,27 @@ public void ExchangeDeclare(string exchange, string type, bool durable, bool aut _Private_ExchangeDeclare(exchange, type, false, durable, autoDelete, false, false, arguments); } + public async ValueTask ExchangeDeclareAsync(string exchange, string type, bool passive, bool durable, bool autoDelete, IDictionary arguments) + { + await _rpcSemaphore.WaitAsync().ConfigureAwait(false); + try + { + using var k = new ExchangeDeclareAsyncRpcContinuation(ContinuationTimeout); + Enqueue(k); + + var method = new ExchangeDeclare(exchange, type, passive, durable, autoDelete, false, false, arguments); + await ModelSendAsync(method).ConfigureAwait(false); + + bool result = await k; + Debug.Assert(result); + return; + } + finally + { + _rpcSemaphore.Release(); + } + } + public void ExchangeDeclareNoWait(string exchange, string type, bool durable, bool autoDelete, IDictionary arguments) { _Private_ExchangeDeclare(exchange, type, false, durable, autoDelete, false, true, arguments); @@ -1074,6 +1413,27 @@ public void ExchangeDelete(string exchange, bool ifUnused) _Private_ExchangeDelete(exchange, ifUnused, false); } + public async ValueTask ExchangeDeleteAsync(string exchange, bool ifUnused) + { + using var k = new ExchangeDeleteAsyncRpcContinuation(ContinuationTimeout); + await _rpcSemaphore.WaitAsync().ConfigureAwait(false); + try + { + Enqueue(k); + + var method = new ExchangeDelete(exchange, ifUnused, Nowait: false); + await ModelSendAsync(method).ConfigureAwait(false); + + bool result = await k; + Debug.Assert(result); + return; + } + finally + { + _rpcSemaphore.Release(); + } + } + public void ExchangeDeleteNoWait(string exchange, bool ifUnused) { _Private_ExchangeDelete(exchange, ifUnused, true); @@ -1084,6 +1444,27 @@ public void ExchangeUnbind(string destination, string source, string routingKey, _Private_ExchangeUnbind(destination, source, routingKey, false, arguments); } + public async ValueTask ExchangeUnbindAsync(string destination, string source, string routingKey, IDictionary arguments) + { + await _rpcSemaphore.WaitAsync().ConfigureAwait(false); + try + { + using var k = new ExchangeUnbindAsyncRpcContinuation(ContinuationTimeout); + Enqueue(k); + + var method = new ExchangeUnbind(destination, source, routingKey, false, arguments); + await ModelSendAsync(method).ConfigureAwait(false); + + bool result = await k; + Debug.Assert(result); + return; + } + finally + { + _rpcSemaphore.Release(); + } + } + public void ExchangeUnbindNoWait(string destination, string source, string routingKey, IDictionary arguments) { _Private_ExchangeUnbind(destination, source, routingKey, true, arguments); @@ -1101,12 +1482,52 @@ public void QueueBindNoWait(string queue, string exchange, string routingKey, ID public QueueDeclareOk QueueDeclare(string queue, bool durable, bool exclusive, bool autoDelete, IDictionary arguments) { - return QueueDeclare(queue, false, durable, exclusive, autoDelete, arguments); + return DoQueueDeclare(queue, false, durable, exclusive, autoDelete, arguments); + } + + public async ValueTask QueueDeclareAsync(string queue, bool passive, bool durable, bool exclusive, bool autoDelete, IDictionary arguments) + { + await _rpcSemaphore.WaitAsync().ConfigureAwait(false); + try + { + using var k = new QueueDeclareAsyncRpcContinuation(ContinuationTimeout); + Enqueue(k); + + var method = new QueueDeclare(queue, passive, durable, exclusive, autoDelete, false, arguments); + await ModelSendAsync(method).ConfigureAwait(false); + + QueueDeclareOk result = await k; + if (false == passive) + { + CurrentQueue = result.QueueName; + } + return result; + } + finally + { + _rpcSemaphore.Release(); + } } - public ValueTask QueueDeclareAsync(string queue, bool durable, bool exclusive, bool autoDelete, IDictionary arguments) + public async ValueTask QueueBindAsync(string queue, string exchange, string routingKey, IDictionary arguments) { - return QueueDeclareAsync(queue, false, durable, exclusive, autoDelete, arguments); + await _rpcSemaphore.WaitAsync().ConfigureAwait(false); + try + { + using var k = new QueueBindAsyncRpcContinuation(ContinuationTimeout); + Enqueue(k); + + var method = new QueueBind(queue, exchange, routingKey, false, arguments); + await ModelSendAsync(method).ConfigureAwait(false); + + bool result = await k; + Debug.Assert(result); + return; + } + finally + { + _rpcSemaphore.Release(); + } } public void QueueDeclareNoWait(string queue, bool durable, bool exclusive, bool autoDelete, IDictionary arguments) @@ -1116,7 +1537,7 @@ public void QueueDeclareNoWait(string queue, bool durable, bool exclusive, bool public QueueDeclareOk QueueDeclarePassive(string queue) { - return QueueDeclare(queue, true, false, false, false, null); + return DoQueueDeclare(queue, true, false, false, false, null); } public uint MessageCount(string queue) @@ -1136,6 +1557,25 @@ public uint QueueDelete(string queue, bool ifUnused, bool ifEmpty) return _Private_QueueDelete(queue, ifUnused, ifEmpty, false); } + public async ValueTask QueueDeleteAsync(string queue, bool ifUnused, bool ifEmpty) + { + await _rpcSemaphore.WaitAsync().ConfigureAwait(false); + try + { + var k = new QueueDeleteAsyncRpcContinuation(ContinuationTimeout); + Enqueue(k); + + var method = new QueueDelete(queue, ifUnused, ifEmpty, false); + await ModelSendAsync(method).ConfigureAwait(false); + + return await k; + } + finally + { + _rpcSemaphore.Release(); + } + } + public void QueueDeleteNoWait(string queue, bool ifUnused, bool ifEmpty) { _Private_QueueDelete(queue, ifUnused, ifEmpty, true); @@ -1146,14 +1586,117 @@ public uint QueuePurge(string queue) return _Private_QueuePurge(queue, false); } + public async ValueTask QueuePurgeAsync(string queue) + { + await _rpcSemaphore.WaitAsync().ConfigureAwait(false); + try + { + var k = new QueuePurgeAsyncRpcContinuation(ContinuationTimeout); + Enqueue(k); + + var method = new QueuePurge(queue, false); + await ModelSendAsync(method).ConfigureAwait(false); + + return await k; + } + finally + { + _rpcSemaphore.Release(); + } + } + public abstract void QueueUnbind(string queue, string exchange, string routingKey, IDictionary arguments); + public async ValueTask QueueUnbindAsync(string queue, string exchange, string routingKey, IDictionary arguments) + { + await _rpcSemaphore.WaitAsync().ConfigureAwait(false); + try + { + using var k = new QueueUnbindAsyncRpcContinuation(ContinuationTimeout); + Enqueue(k); + + var method = new QueueUnbind(queue, exchange, routingKey, arguments); + await ModelSendAsync(method).ConfigureAwait(false); + + bool result = await k; + Debug.Assert(result); + return; + } + finally + { + _rpcSemaphore.Release(); + } + } + public abstract void TxCommit(); + public async ValueTask TxCommitAsync() + { + await _rpcSemaphore.WaitAsync().ConfigureAwait(false); + try + { + using var k = new TxCommitAsyncRpcContinuation(ContinuationTimeout); + Enqueue(k); + + var method = new TxCommit(); + await ModelSendAsync(method).ConfigureAwait(false); + + bool result = await k; + Debug.Assert(result); + return; + } + finally + { + _rpcSemaphore.Release(); + } + } + public abstract void TxRollback(); + public async ValueTask TxRollbackAsync() + { + await _rpcSemaphore.WaitAsync().ConfigureAwait(false); + try + { + using var k = new TxRollbackAsyncRpcContinuation(ContinuationTimeout); + Enqueue(k); + + var method = new TxRollback(); + await ModelSendAsync(method).ConfigureAwait(false); + + bool result = await k; + Debug.Assert(result); + return; + } + finally + { + _rpcSemaphore.Release(); + } + } + public abstract void TxSelect(); + public async ValueTask TxSelectAsync() + { + await _rpcSemaphore.WaitAsync().ConfigureAwait(false); + try + { + using var k = new TxSelectAsyncRpcContinuation(ContinuationTimeout); + Enqueue(k); + + var method = new TxSelect(); + await ModelSendAsync(method).ConfigureAwait(false); + + bool result = await k; + Debug.Assert(result); + return; + } + finally + { + _rpcSemaphore.Release(); + } + } + private List> _confirmsTaskCompletionSources; public Task WaitForConfirmsAsync(CancellationToken token = default) @@ -1227,17 +1770,20 @@ await CloseAsync( new IOException("nack received")), false).ConfigureAwait(false); } - catch (TaskCanceledException exception) + catch (TaskCanceledException) { + const string msg = "timed out waiting for acks"; + var ex = new IOException(msg); await CloseAsync(new ShutdownEventArgs(ShutdownInitiator.Library, Constants.ReplySuccess, - "Timed out waiting for acks", - exception), + msg, + ex), false).ConfigureAwait(false); + throw ex; } } - private QueueDeclareOk QueueDeclare(string queue, bool passive, bool durable, bool exclusive, bool autoDelete, IDictionary arguments) + private QueueDeclareOk DoQueueDeclare(string queue, bool passive, bool durable, bool exclusive, bool autoDelete, IDictionary arguments) { var k = new QueueDeclareRpcContinuation(); @@ -1257,71 +1803,5 @@ private QueueDeclareOk QueueDeclare(string queue, bool passive, bool durable, bo CurrentQueue = result.QueueName; return result; } - - private async ValueTask QueueDeclareAsync(string queue, bool passive, bool durable, bool exclusive, bool autoDelete, IDictionary arguments) - { - var k = new QueueDeclareAsyncRpcContinuation(); - await _rpcSemaphore.WaitAsync().ConfigureAwait(false); - try - { - Enqueue(k); - - var method = new QueueDeclare(queue, passive, durable, exclusive, autoDelete, false, arguments); - await ModelSendAsync(method).ConfigureAwait(false); - - QueueDeclareOk result = await k; - CurrentQueue = result.QueueName; - return result; - } - finally - { - _rpcSemaphore.Release(); - } - } - - public class BasicConsumerRpcContinuation : SimpleBlockingRpcContinuation - { - public IBasicConsumer m_consumer; - public string m_consumerTag; - } - - public class BasicGetRpcContinuation : SimpleBlockingRpcContinuation - { - public BasicGetResult m_result; - } - - public class ConnectionStartRpcContinuation : SimpleBlockingRpcContinuation - { - public ConnectionSecureOrTune m_result; - } - - public class QueueDeclareRpcContinuation : SimpleBlockingRpcContinuation - { - public QueueDeclareOk m_result; - } - - public class QueueDeclareAsyncRpcContinuation : AsyncRpcContinuation - { - public override void HandleCommand(in IncomingCommand cmd) - { - try - { - var method = new Client.Framing.Impl.QueueDeclareOk(cmd.MethodBytes.Span); - var result = new QueueDeclareOk(method._queue, method._messageCount, method._consumerCount); - if (cmd.CommandId == ProtocolCommandId.QueueDeclareOk) - { - _tcs.TrySetResult(result); - } - else - { - _tcs.SetException(new InvalidOperationException($"Received unexpected command of type {cmd.CommandId}!")); - } - } - finally - { - cmd.ReturnMethodBuffer(); - } - } - } } } diff --git a/projects/RabbitMQ.Client/client/impl/CommandAssembler.cs b/projects/RabbitMQ.Client/client/impl/CommandAssembler.cs index d03708232c..86b381087f 100644 --- a/projects/RabbitMQ.Client/client/impl/CommandAssembler.cs +++ b/projects/RabbitMQ.Client/client/impl/CommandAssembler.cs @@ -30,7 +30,6 @@ //--------------------------------------------------------------------------- using System; -using System.Buffers; using RabbitMQ.Client.client.framing; using RabbitMQ.Client.Exceptions; using RabbitMQ.Client.Framing.Impl; @@ -45,13 +44,13 @@ internal sealed class CommandAssembler private const int MaxArrayOfBytesSize = 2_147_483_591; private ProtocolCommandId _commandId; - private ReadOnlyMemory _methodBytes; + private ReadOnlyMemory _methodMemory; private byte[]? _rentedMethodArray; - private ReadOnlyMemory _headerBytes; + private ReadOnlyMemory _headerMemory; private byte[]? _rentedHeaderArray; - private ReadOnlyMemory _bodyBytes; + private ReadOnlyMemory _bodyMemory; private byte[]? _rentedBodyArray; - private int _remainingBodyBytes; + private int _remainingBodyByteCount; private int _offset; private AssemblyState _state; @@ -63,13 +62,13 @@ public CommandAssembler() private void Reset() { _commandId = default; - _methodBytes = ReadOnlyMemory.Empty; + _methodMemory = ReadOnlyMemory.Empty; _rentedMethodArray = null; - _headerBytes = ReadOnlyMemory.Empty; + _headerMemory = ReadOnlyMemory.Empty; _rentedHeaderArray = null; - _bodyBytes = ReadOnlyMemory.Empty; + _bodyMemory = ReadOnlyMemory.Empty; _rentedBodyArray = null; - _remainingBodyBytes = 0; + _remainingBodyByteCount = 0; _offset = 0; _state = AssemblyState.ExpectingMethod; } @@ -98,7 +97,12 @@ public bool HandleFrame(in InboundFrame frame, out IncomingCommand command) } RabbitMqClientEventSource.Log.CommandReceived(); - command = new IncomingCommand(_commandId, _methodBytes, _rentedMethodArray, _headerBytes, _rentedHeaderArray, _bodyBytes, _rentedBodyArray); + + var method = new RentedMemory(_methodMemory, _rentedMethodArray); + var header = new RentedMemory(_headerMemory, _rentedHeaderArray); + var body = new RentedMemory(_bodyMemory, _rentedBodyArray); + + command = new IncomingCommand(_commandId, method, header, body); Reset(); return shallReturn; } @@ -112,7 +116,7 @@ private void ParseMethodFrame(in InboundFrame frame) _rentedMethodArray = frame.TakeoverPayload(); _commandId = (ProtocolCommandId)NetworkOrderDeserializer.ReadUInt32(frame.Payload.Span); - _methodBytes = frame.Payload.Slice(4); + _methodMemory = frame.Payload.Slice(4); switch (_commandId) { @@ -149,9 +153,9 @@ private bool ParseHeaderFrame(in InboundFrame frame) } _rentedHeaderArray = totalBodyBytes != 0 ? frame.TakeoverPayload() : Array.Empty(); - _headerBytes = frame.Payload.Slice(12); + _headerMemory = frame.Payload.Slice(12); - _remainingBodyBytes = (int)totalBodyBytes; + _remainingBodyByteCount = (int)totalBodyBytes; UpdateContentBodyState(); return _rentedHeaderArray.Length == 0; } @@ -164,29 +168,29 @@ private bool ParseBodyFrame(in InboundFrame frame) } int payloadLength = frame.Payload.Length; - if (payloadLength > _remainingBodyBytes) + if (payloadLength > _remainingBodyByteCount) { - throw new MalformedFrameException($"Overlong content body received - {_remainingBodyBytes} bytes remaining, {payloadLength} bytes received"); + throw new MalformedFrameException($"Overlong content body received - {_remainingBodyByteCount} bytes remaining, {payloadLength} bytes received"); } if (_rentedBodyArray is null) { // check for single frame payload for an early exit - if (payloadLength == _remainingBodyBytes) + if (payloadLength == _remainingBodyByteCount) { _rentedBodyArray = frame.TakeoverPayload(); - _bodyBytes = frame.Payload; + _bodyMemory = frame.Payload; _state = AssemblyState.Complete; return false; } // Is returned by IncomingCommand.ReturnPayload in Session.HandleFrame - _rentedBodyArray = ArrayPool.Shared.Rent(_remainingBodyBytes); - _bodyBytes = new ReadOnlyMemory(_rentedBodyArray, 0, _remainingBodyBytes); + _rentedBodyArray = ClientArrayPool.Rent(_remainingBodyByteCount); + _bodyMemory = new ReadOnlyMemory(_rentedBodyArray, 0, _remainingBodyByteCount); } frame.Payload.Span.CopyTo(_rentedBodyArray.AsSpan(_offset)); - _remainingBodyBytes -= payloadLength; + _remainingBodyByteCount -= payloadLength; _offset += payloadLength; UpdateContentBodyState(); return true; @@ -194,7 +198,7 @@ private bool ParseBodyFrame(in InboundFrame frame) private void UpdateContentBodyState() { - _state = _remainingBodyBytes > 0 ? AssemblyState.ExpectingContentBody : AssemblyState.Complete; + _state = _remainingBodyByteCount > 0 ? AssemblyState.ExpectingContentBody : AssemblyState.Complete; } private enum AssemblyState diff --git a/projects/RabbitMQ.Client/client/impl/Connection.Commands.cs b/projects/RabbitMQ.Client/client/impl/Connection.Commands.cs index ea561d6986..a753e251b7 100644 --- a/projects/RabbitMQ.Client/client/impl/Connection.Commands.cs +++ b/projects/RabbitMQ.Client/client/impl/Connection.Commands.cs @@ -36,7 +36,6 @@ using RabbitMQ.Client.Events; using RabbitMQ.Client.Exceptions; using RabbitMQ.Client.Impl; -using RabbitMQ.Client.Logging; namespace RabbitMQ.Client.Framing.Impl { @@ -70,13 +69,6 @@ internal void HandleConnectionUnblocked() } } - private async ValueTask OpenAsync() - { - RabbitMqClientEventSource.Log.ConnectionOpened(); - await StartAndTuneAsync().ConfigureAwait(false); - await _channel0.ConnectionOpenAsync(_config.VirtualHost); - } - private async ValueTask StartAndTuneAsync() { var connectionStartCell = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); diff --git a/projects/RabbitMQ.Client/client/impl/Connection.Receive.cs b/projects/RabbitMQ.Client/client/impl/Connection.Receive.cs index 52bb68d2ec..bbfeda1be9 100644 --- a/projects/RabbitMQ.Client/client/impl/Connection.Receive.cs +++ b/projects/RabbitMQ.Client/client/impl/Connection.Receive.cs @@ -144,6 +144,8 @@ private void HandleMainLoopException(ShutdownEventArgs reason) { if (!SetCloseReason(reason)) { + // TODO LRB + // reason.Cause could be an Exception, should we use that? LogCloseError("Unexpected Main Loop Exception while closing: " + reason, new Exception(reason.ToString())); return; } diff --git a/projects/RabbitMQ.Client/client/impl/Connection.cs b/projects/RabbitMQ.Client/client/impl/Connection.cs index dd3a6cdf45..423fe987bd 100644 --- a/projects/RabbitMQ.Client/client/impl/Connection.cs +++ b/projects/RabbitMQ.Client/client/impl/Connection.cs @@ -58,7 +58,7 @@ internal sealed partial class Connection : IConnection private ShutdownEventArgs? _closeReason; public ShutdownEventArgs? CloseReason => Volatile.Read(ref _closeReason); - public Connection(ConnectionConfig config, IFrameHandler frameHandler) + internal Connection(ConnectionConfig config, IFrameHandler frameHandler) { _config = config; _frameHandler = frameHandler; @@ -78,23 +78,7 @@ public Connection(ConnectionConfig config, IFrameHandler frameHandler) ["capabilities"] = Protocol.Capabilities, ["connection_name"] = ClientProvidedName }; - _mainLoopTask = Task.Run(MainLoop); - try - { - /* - * TODO FUTURE - * Connection should not happen in ctor, instead change - * the API so that it's awaitable - */ - OpenAsync().AsTask().GetAwaiter().GetResult(); - } - catch - { - var ea = new ShutdownEventArgs(ShutdownInitiator.Library, Constants.InternalError, "FailedOpen"); - Close(ea, true, TimeSpan.FromSeconds(5)); - throw; - } } public Guid Id => _id; @@ -227,6 +211,29 @@ internal void TakeOver(Connection other) _connectionShutdownWrapper.Takeover(other._connectionShutdownWrapper); } + internal IConnection Open() + { + return OpenAsync().GetAwaiter().GetResult(); + } + + internal async ValueTask OpenAsync() + { + try + { + RabbitMqClientEventSource.Log.ConnectionOpened(); + await StartAndTuneAsync().ConfigureAwait(false); + await _channel0.ConnectionOpenAsync(_config.VirtualHost); + return this; + } + catch + { + var ea = new ShutdownEventArgs(ShutdownInitiator.Library, Constants.InternalError, "FailedOpen"); + // TODO CloseAsync + Close(ea, true, TimeSpan.FromSeconds(5)); + throw; + } + } + public IChannel CreateChannel() { EnsureIsOpen(); @@ -236,6 +243,14 @@ public IChannel CreateChannel() return channel; } + public ValueTask CreateChannelAsync() + { + EnsureIsOpen(); + ISession session = CreateSession(); + var channel = new Channel(_config, session); + return channel.OpenAsync(); + } + internal ISession CreateSession() { return _sessionManager.Create(); @@ -306,13 +321,11 @@ internal void Close(ShutdownEventArgs reason, bool abort, TimeSpan timeout) throw; } } -#pragma warning disable 0168 - catch (NotSupportedException nse) + catch (NotSupportedException) { // buffered stream had unread data in it and Flush() // was called, ignore to not confuse the user } -#pragma warning restore 0168 catch (IOException ioe) { if (_channel0.CloseReason is null) @@ -405,18 +418,18 @@ internal void OnCallbackException(CallbackExceptionEventArgs args) _callbackExceptionWrapper.Invoke(this, args); } - internal void Write(ReadOnlyMemory memory) + internal void Write(RentedMemory frames) { - var task = _frameHandler.WriteAsync(memory); + var task = _frameHandler.WriteAsync(frames); if (!task.IsCompletedSuccessfully) { task.AsTask().GetAwaiter().GetResult(); } } - internal ValueTask WriteAsync(ReadOnlyMemory memory) + internal ValueTask WriteAsync(RentedMemory frames) { - return _frameHandler.WriteAsync(memory); + return _frameHandler.WriteAsync(frames); } public void Dispose() diff --git a/projects/RabbitMQ.Client/client/impl/ConsumerDispatching/AsyncConsumerDispatcher.cs b/projects/RabbitMQ.Client/client/impl/ConsumerDispatching/AsyncConsumerDispatcher.cs index b1f80582dc..0f24800423 100644 --- a/projects/RabbitMQ.Client/client/impl/ConsumerDispatching/AsyncConsumerDispatcher.cs +++ b/projects/RabbitMQ.Client/client/impl/ConsumerDispatching/AsyncConsumerDispatcher.cs @@ -1,5 +1,4 @@ using System; -using System.Buffers; using System.Threading.Tasks; using RabbitMQ.Client.Events; using RabbitMQ.Client.Impl; @@ -18,30 +17,33 @@ protected override async Task ProcessChannelAsync() { while (await _reader.WaitToReadAsync().ConfigureAwait(false)) { - while (_reader.TryRead(out var work)) + while (_reader.TryRead(out WorkStruct work)) { - try + using (work) { - var task = work.WorkType switch + try { - WorkType.Deliver => work.AsyncConsumer.HandleBasicDeliver(work.ConsumerTag, work.DeliveryTag, work.Redelivered, work.Exchange, work.RoutingKey, work.BasicProperties, work.Body), - WorkType.Cancel => work.AsyncConsumer.HandleBasicCancel(work.ConsumerTag), - WorkType.CancelOk => work.AsyncConsumer.HandleBasicCancelOk(work.ConsumerTag), - WorkType.ConsumeOk => work.AsyncConsumer.HandleBasicConsumeOk(work.ConsumerTag), - WorkType.Shutdown => work.AsyncConsumer.HandleChannelShutdown(_channel, work.Reason), - _ => Task.CompletedTask - }; - await task.ConfigureAwait(false); - } - catch (Exception e) - { - _channel.OnCallbackException(CallbackExceptionEventArgs.Build(e, work.WorkType.ToString(), work.Consumer)); - } - finally - { - if (work.RentedArray != null) + Task task = work.WorkType switch + { + WorkType.Deliver => work.AsyncConsumer.HandleBasicDeliver( + work.ConsumerTag, work.DeliveryTag, work.Redelivered, + work.Exchange, work.RoutingKey, work.BasicProperties, work.Body), + + WorkType.Cancel => work.AsyncConsumer.HandleBasicCancel(work.ConsumerTag), + + WorkType.CancelOk => work.AsyncConsumer.HandleBasicCancelOk(work.ConsumerTag), + + WorkType.ConsumeOk => work.AsyncConsumer.HandleBasicConsumeOk(work.ConsumerTag), + + WorkType.Shutdown => work.AsyncConsumer.HandleChannelShutdown(_channel, work.Reason), + + _ => Task.CompletedTask + }; + await task.ConfigureAwait(false); + } + catch (Exception e) { - ArrayPool.Shared.Return(work.RentedArray); + _channel.OnCallbackException(CallbackExceptionEventArgs.Build(e, work.WorkType.ToString(), work.Consumer)); } } } diff --git a/projects/RabbitMQ.Client/client/impl/ConsumerDispatching/ConsumerDispatcher.cs b/projects/RabbitMQ.Client/client/impl/ConsumerDispatching/ConsumerDispatcher.cs index 09aa5cc627..164d5d9e76 100644 --- a/projects/RabbitMQ.Client/client/impl/ConsumerDispatching/ConsumerDispatcher.cs +++ b/projects/RabbitMQ.Client/client/impl/ConsumerDispatching/ConsumerDispatcher.cs @@ -1,5 +1,4 @@ using System; -using System.Buffers; using System.Threading.Tasks; using RabbitMQ.Client.Events; using RabbitMQ.Client.Impl; @@ -20,38 +19,36 @@ protected override async Task ProcessChannelAsync() { while (_reader.TryRead(out var work)) { - try + using (work) { - var consumer = work.Consumer; - var consumerTag = work.ConsumerTag; - switch (work.WorkType) + try { - case WorkType.Deliver: - consumer.HandleBasicDeliver(consumerTag, work.DeliveryTag, work.Redelivered, work.Exchange, work.RoutingKey, work.BasicProperties, work.Body); - break; - case WorkType.Cancel: - consumer.HandleBasicCancel(consumerTag); - break; - case WorkType.CancelOk: - consumer.HandleBasicCancelOk(consumerTag); - break; - case WorkType.ConsumeOk: - consumer.HandleBasicConsumeOk(consumerTag); - break; - case WorkType.Shutdown: - consumer.HandleChannelShutdown(_channel, work.Reason); - break; + var consumer = work.Consumer; + var consumerTag = work.ConsumerTag; + switch (work.WorkType) + { + case WorkType.Deliver: + consumer.HandleBasicDeliver( + consumerTag, work.DeliveryTag, work.Redelivered, + work.Exchange, work.RoutingKey, work.BasicProperties, work.Body); + break; + case WorkType.Cancel: + consumer.HandleBasicCancel(consumerTag); + break; + case WorkType.CancelOk: + consumer.HandleBasicCancelOk(consumerTag); + break; + case WorkType.ConsumeOk: + consumer.HandleBasicConsumeOk(consumerTag); + break; + case WorkType.Shutdown: + consumer.HandleChannelShutdown(_channel, work.Reason); + break; + } } - } - catch (Exception e) - { - _channel.OnCallbackException(CallbackExceptionEventArgs.Build(e, work.WorkType.ToString(), work.Consumer)); - } - finally - { - if (work.RentedArray != null) + catch (Exception e) { - ArrayPool.Shared.Return(work.RentedArray); + _channel.OnCallbackException(CallbackExceptionEventArgs.Build(e, work.WorkType.ToString(), work.Consumer)); } } } diff --git a/projects/RabbitMQ.Client/client/impl/ConsumerDispatching/ConsumerDispatcherChannelBase.cs b/projects/RabbitMQ.Client/client/impl/ConsumerDispatching/ConsumerDispatcherChannelBase.cs index f53218e6bd..648480473c 100644 --- a/projects/RabbitMQ.Client/client/impl/ConsumerDispatching/ConsumerDispatcherChannelBase.cs +++ b/projects/RabbitMQ.Client/client/impl/ConsumerDispatching/ConsumerDispatcherChannelBase.cs @@ -54,11 +54,11 @@ public void HandleBasicConsumeOk(IBasicConsumer consumer, string consumerTag) } public void HandleBasicDeliver(string consumerTag, ulong deliveryTag, bool redelivered, - string exchange, string routingKey, in ReadOnlyBasicProperties basicProperties, ReadOnlyMemory body, byte[] rentedArray) + string exchange, string routingKey, in ReadOnlyBasicProperties basicProperties, RentedMemory body) { if (!IsShutdown) { - _writer.TryWrite(new WorkStruct(GetConsumerOrDefault(consumerTag), consumerTag, deliveryTag, redelivered, exchange, routingKey, basicProperties, body, rentedArray)); + _writer.TryWrite(new WorkStruct(GetConsumerOrDefault(consumerTag), consumerTag, deliveryTag, redelivered, exchange, routingKey, basicProperties, body)); } } @@ -101,7 +101,7 @@ public Task WaitForShutdownAsync() protected abstract Task ProcessChannelAsync(); - protected readonly struct WorkStruct + protected readonly struct WorkStruct : IDisposable { public readonly IBasicConsumer Consumer; public IAsyncBasicConsumer AsyncConsumer => (IAsyncBasicConsumer)Consumer; @@ -111,8 +111,7 @@ protected readonly struct WorkStruct public readonly string? Exchange; public readonly string? RoutingKey; public readonly ReadOnlyBasicProperties BasicProperties; - public readonly ReadOnlyMemory Body; - public readonly byte[]? RentedArray; + public readonly RentedMemory Body; public readonly ShutdownEventArgs? Reason; public readonly WorkType WorkType; @@ -133,7 +132,7 @@ public WorkStruct(IBasicConsumer consumer, ShutdownEventArgs reason) } public WorkStruct(IBasicConsumer consumer, string consumerTag, ulong deliveryTag, bool redelivered, - string exchange, string routingKey, in ReadOnlyBasicProperties basicProperties, ReadOnlyMemory body, byte[] rentedArray) + string exchange, string routingKey, in ReadOnlyBasicProperties basicProperties, RentedMemory body) { WorkType = WorkType.Deliver; Consumer = consumer; @@ -144,9 +143,10 @@ public WorkStruct(IBasicConsumer consumer, string consumerTag, ulong deliveryTag RoutingKey = routingKey; BasicProperties = basicProperties; Body = body; - RentedArray = rentedArray; Reason = default; } + + public void Dispose() => Body.Dispose(); } protected enum WorkType : byte diff --git a/projects/RabbitMQ.Client/client/impl/ConsumerDispatching/FallbackConsumer.cs b/projects/RabbitMQ.Client/client/impl/ConsumerDispatching/FallbackConsumer.cs index 3510be5e5d..01da938d9b 100644 --- a/projects/RabbitMQ.Client/client/impl/ConsumerDispatching/FallbackConsumer.cs +++ b/projects/RabbitMQ.Client/client/impl/ConsumerDispatching/FallbackConsumer.cs @@ -37,8 +37,8 @@ void IBasicConsumer.HandleBasicConsumeOk(string consumerTag) ESLog.Info($"Unhandled {nameof(IBasicConsumer.HandleBasicConsumeOk)} for tag {consumerTag}"); } - void IBasicConsumer.HandleBasicDeliver(string consumerTag, ulong deliveryTag, bool redelivered, string exchange, string routingKey, in ReadOnlyBasicProperties properties, - ReadOnlyMemory body) + void IBasicConsumer.HandleBasicDeliver(string consumerTag, ulong deliveryTag, bool redelivered, string exchange, string routingKey, + in ReadOnlyBasicProperties properties, RentedMemory body) { ESLog.Info($"Unhandled {nameof(IBasicConsumer.HandleBasicDeliver)} for tag {consumerTag}"); } @@ -66,8 +66,8 @@ Task IAsyncBasicConsumer.HandleBasicConsumeOk(string consumerTag) return Task.CompletedTask; } - Task IAsyncBasicConsumer.HandleBasicDeliver(string consumerTag, ulong deliveryTag, bool redelivered, string exchange, string routingKey, in ReadOnlyBasicProperties properties, - ReadOnlyMemory body) + Task IAsyncBasicConsumer.HandleBasicDeliver(string consumerTag, ulong deliveryTag, bool redelivered, string exchange, string routingKey, + in ReadOnlyBasicProperties properties, RentedMemory body) { ((IBasicConsumer)this).HandleBasicDeliver(consumerTag, deliveryTag, redelivered, exchange, routingKey, properties, body); return Task.CompletedTask; diff --git a/projects/RabbitMQ.Client/client/impl/ConsumerDispatching/IConsumerDispatcher.cs b/projects/RabbitMQ.Client/client/impl/ConsumerDispatching/IConsumerDispatcher.cs index ba7c462529..57113acba3 100644 --- a/projects/RabbitMQ.Client/client/impl/ConsumerDispatching/IConsumerDispatcher.cs +++ b/projects/RabbitMQ.Client/client/impl/ConsumerDispatching/IConsumerDispatcher.cs @@ -29,7 +29,6 @@ // Copyright (c) 2007-2020 VMware, Inc. All rights reserved. //--------------------------------------------------------------------------- -using System; using System.Threading.Tasks; namespace RabbitMQ.Client.ConsumerDispatching @@ -51,8 +50,7 @@ void HandleBasicDeliver(string consumerTag, string exchange, string routingKey, in ReadOnlyBasicProperties basicProperties, - ReadOnlyMemory body, - byte[] rentedArray); + RentedMemory body); void HandleBasicCancelOk(string consumerTag); diff --git a/projects/RabbitMQ.Client/client/impl/Frame.cs b/projects/RabbitMQ.Client/client/impl/Frame.cs index 08f4c351b5..09acf8a40e 100644 --- a/projects/RabbitMQ.Client/client/impl/Frame.cs +++ b/projects/RabbitMQ.Client/client/impl/Frame.cs @@ -31,12 +31,9 @@ using System; using System.Buffers; -using System.Diagnostics; using System.IO; using System.IO.Pipelines; -using System.Net.Sockets; using System.Runtime.CompilerServices; -using System.Runtime.ExceptionServices; using System.Threading.Tasks; using RabbitMQ.Client.Exceptions; @@ -142,31 +139,33 @@ internal static class Heartbeat /// private static ReadOnlySpan Payload => new byte[] { Constants.FrameHeartbeat, 0, 0, 0, 0, 0, 0, Constants.FrameEnd }; - public static Memory GetHeartbeatFrame() + public static RentedMemory GetHeartbeatFrame() { // Is returned by SocketFrameHandler.WriteLoop - byte[] buffer = ArrayPool.Shared.Rent(FrameSize); + byte[] buffer = ClientArrayPool.Rent(FrameSize); Payload.CopyTo(buffer); - return new Memory(buffer, 0, FrameSize); + var mem = new ReadOnlyMemory(buffer, 0, FrameSize); + return new RentedMemory(FrameSize, mem, buffer); } } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static ReadOnlyMemory SerializeToFrames(ref T method, ushort channelNumber) + public static RentedMemory SerializeToFrames(ref T method, ushort channelNumber) where T : struct, IOutgoingAmqpMethod { int size = Method.FrameSize + method.GetRequiredBufferSize(); // Will be returned by SocketFrameWriter.WriteLoop - var array = ArrayPool.Shared.Rent(size); + byte[] array = ClientArrayPool.Rent(size); int offset = Method.WriteTo(array, channelNumber, ref method); System.Diagnostics.Debug.Assert(offset == size, $"Serialized to wrong size, expect {size}, offset {offset}"); - return new ReadOnlyMemory(array, 0, size); + var mem = new ReadOnlyMemory(array, 0, size); + return new RentedMemory(size, mem, array); } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static ReadOnlyMemory SerializeToFrames(ref TMethod method, ref THeader header, ReadOnlyMemory body, ushort channelNumber, int maxBodyPayloadBytes) + public static RentedMemory SerializeToFrames(ref TMethod method, ref THeader header, ReadOnlyMemory body, ushort channelNumber, int maxBodyPayloadBytes) where TMethod : struct, IOutgoingAmqpMethod where THeader : IAmqpHeader { @@ -176,11 +175,11 @@ public static ReadOnlyMemory SerializeToFrames(ref TMeth BodySegment.FrameSize * GetBodyFrameCount(maxBodyPayloadBytes, remainingBodyBytes) + remainingBodyBytes; // Will be returned by SocketFrameWriter.WriteLoop - var array = ArrayPool.Shared.Rent(size); + byte[] array = ClientArrayPool.Rent(size); int offset = Method.WriteTo(array, channelNumber, ref method); offset += Header.WriteTo(array.AsSpan(offset), channelNumber, ref header, remainingBodyBytes); - var bodySpan = body.Span; + ReadOnlySpan bodySpan = body.Span; while (remainingBodyBytes > 0) { int frameSize = remainingBodyBytes > maxBodyPayloadBytes ? maxBodyPayloadBytes : remainingBodyBytes; @@ -189,7 +188,8 @@ public static ReadOnlyMemory SerializeToFrames(ref TMeth } System.Diagnostics.Debug.Assert(offset == size, $"Serialized to wrong size, expect {size}, offset {offset}"); - return new ReadOnlyMemory(array, 0, size); + var mem = new ReadOnlyMemory(array, 0, size); + return new RentedMemory(size, mem, array); } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -338,14 +338,14 @@ internal static bool TryReadFrame(ref ReadOnlySequence buffer, uint maxMes } else { - byte[] payloadBytes = ArrayPool.Shared.Rent(readSize); + byte[] payloadBytes = ClientArrayPool.Rent(readSize); ReadOnlySequence framePayload = buffer.Slice(7, readSize); framePayload.CopyTo(payloadBytes); if (payloadBytes[payloadSize] != Constants.FrameEnd) { var frameEndMarker = payloadBytes[payloadSize]; - ArrayPool.Shared.Return(payloadBytes); + ClientArrayPool.Return(payloadBytes); throw new MalformedFrameException($"Bad frame end marker: {frameEndMarker}"); } @@ -364,7 +364,7 @@ public byte[] TakeoverPayload() public void ReturnPayload() { - ArrayPool.Shared.Return(_rentedArray); + ClientArrayPool.Return(_rentedArray); } public override string ToString() diff --git a/projects/RabbitMQ.Client/client/impl/IFrameHandler.cs b/projects/RabbitMQ.Client/client/impl/IFrameHandler.cs index fdc3ef7628..5b3cdd51a6 100644 --- a/projects/RabbitMQ.Client/client/impl/IFrameHandler.cs +++ b/projects/RabbitMQ.Client/client/impl/IFrameHandler.cs @@ -67,6 +67,6 @@ internal interface IFrameHandler ValueTask SendHeaderAsync(); - ValueTask WriteAsync(ReadOnlyMemory memory); + ValueTask WriteAsync(RentedMemory frames); } } diff --git a/projects/RabbitMQ.Client/client/impl/ISession.cs b/projects/RabbitMQ.Client/client/impl/ISession.cs index bc30b8f5dd..1d5a34f76b 100644 --- a/projects/RabbitMQ.Client/client/impl/ISession.cs +++ b/projects/RabbitMQ.Client/client/impl/ISession.cs @@ -70,15 +70,21 @@ internal interface ISession event EventHandler SessionShutdown; void Close(ShutdownEventArgs reason); + void Close(ShutdownEventArgs reason, bool notify); + bool HandleFrame(in InboundFrame frame); + void Notify(); + void Transmit(in T cmd) where T : struct, IOutgoingAmqpMethod; - ValueTask TransmitAsync(in T cmd) where T : struct, IOutgoingAmqpMethod; + void Transmit(in TMethod cmd, in THeader header, ReadOnlyMemory body) where TMethod : struct, IOutgoingAmqpMethod where THeader : IAmqpHeader; + ValueTask TransmitAsync(in T cmd) where T : struct, IOutgoingAmqpMethod; + ValueTask TransmitAsync(in TMethod cmd, in THeader header, ReadOnlyMemory body) where TMethod : struct, IOutgoingAmqpMethod where THeader : IAmqpHeader; diff --git a/projects/RabbitMQ.Client/client/impl/IncomingCommand.cs b/projects/RabbitMQ.Client/client/impl/IncomingCommand.cs index 5f3aa7fbb8..d5fb43d016 100644 --- a/projects/RabbitMQ.Client/client/impl/IncomingCommand.cs +++ b/projects/RabbitMQ.Client/client/impl/IncomingCommand.cs @@ -1,5 +1,35 @@ -using System; -using System.Buffers; +// This source code is dual-licensed under the Apache License, version +// 2.0, and the Mozilla Public License, version 2.0. +// +// The APL v2.0: +// +//--------------------------------------------------------------------------- +// Copyright (c) 2007-2020 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//--------------------------------------------------------------------------- +// +// The MPL v2.0: +// +//--------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// +// Copyright (c) 2007-2020 VMware, Inc. All rights reserved. +//--------------------------------------------------------------------------- + +using System; using RabbitMQ.Client.client.framing; namespace RabbitMQ.Client.Impl @@ -10,41 +40,55 @@ internal readonly struct IncomingCommand public readonly ProtocolCommandId CommandId; - public readonly ReadOnlyMemory MethodBytes; - private readonly byte[] _rentedMethodBytes; - - public readonly ReadOnlyMemory HeaderBytes; - private readonly byte[] _rentedHeaderArray; - - public readonly ReadOnlyMemory Body; - private readonly byte[] _rentedBodyArray; + public readonly RentedMemory Method; + public readonly RentedMemory Header; + public readonly RentedMemory Body; - public bool IsEmpty => CommandId is default(ProtocolCommandId); + public readonly bool IsEmpty => CommandId is default(ProtocolCommandId); - public IncomingCommand(ProtocolCommandId commandId, ReadOnlyMemory methodBytes, byte[] rentedMethodArray, ReadOnlyMemory headerBytes, byte[] rentedHeaderArray, ReadOnlyMemory body, byte[] rentedBodyArray) + public IncomingCommand(ProtocolCommandId commandId, + RentedMemory method, RentedMemory header, RentedMemory body) { CommandId = commandId; - MethodBytes = methodBytes; - _rentedMethodBytes = rentedMethodArray; - HeaderBytes = headerBytes; - _rentedHeaderArray = rentedHeaderArray; + Method = method; + Header = header; Body = body; - _rentedBodyArray = rentedBodyArray; } - public byte[] TakeoverBody() + public ReadOnlySpan MethodSpan + { + get + { + return Method.Memory.Span; + } + } + + public ReadOnlySpan HeaderSpan + { + get + { + return Header.Memory.Span; + } + } + + public ReadOnlySpan BodySpan { - return _rentedBodyArray; + get + { + return Body.Memory.Span; + } } - public void ReturnHeaderBuffer() + public void ReturnMethodAndHeaderBuffers() { - ArrayPool.Shared.Return(_rentedHeaderArray); + Method.Dispose(); + Header.Dispose(); } - public void ReturnMethodBuffer() + public void ReturnBuffers() { - ArrayPool.Shared.Return(_rentedMethodBytes); + ReturnMethodAndHeaderBuffers(); + Body.Dispose(); } } } diff --git a/projects/RabbitMQ.Client/client/impl/RecoveryAwareChannel.cs b/projects/RabbitMQ.Client/client/impl/RecoveryAwareChannel.cs index e97415d713..49a3294d0b 100644 --- a/projects/RabbitMQ.Client/client/impl/RecoveryAwareChannel.cs +++ b/projects/RabbitMQ.Client/client/impl/RecoveryAwareChannel.cs @@ -29,6 +29,7 @@ // Copyright (c) 2007-2020 VMware, Inc. All rights reserved. //--------------------------------------------------------------------------- +using System.Threading.Tasks; using RabbitMQ.Client.Framing.Impl; namespace RabbitMQ.Client.Impl @@ -70,6 +71,19 @@ public override void BasicAck(ulong deliveryTag, bool multiple) } } + public override ValueTask BasicAckAsync(ulong deliveryTag, bool multiple) + { + ulong realTag = deliveryTag - ActiveDeliveryTagOffset; + if (realTag > 0 && realTag <= deliveryTag) + { + return base.BasicAckAsync(realTag, multiple); + } + else + { + return default; + } + } + public override void BasicNack(ulong deliveryTag, bool multiple, bool requeue) { ulong realTag = deliveryTag - ActiveDeliveryTagOffset; @@ -79,6 +93,19 @@ public override void BasicNack(ulong deliveryTag, bool multiple, bool requeue) } } + public override ValueTask BasicNackAsync(ulong deliveryTag, bool multiple, bool requeue) + { + ulong realTag = deliveryTag - ActiveDeliveryTagOffset; + if (realTag > 0 && realTag <= deliveryTag) + { + return base.BasicNackAsync(realTag, multiple, requeue); + } + else + { + return default; + } + } + public override void BasicReject(ulong deliveryTag, bool requeue) { ulong realTag = deliveryTag - ActiveDeliveryTagOffset; @@ -87,5 +114,18 @@ public override void BasicReject(ulong deliveryTag, bool requeue) base.BasicReject(realTag, requeue); } } + + public override ValueTask BasicRejectAsync(ulong deliveryTag, bool requeue) + { + ulong realTag = deliveryTag - ActiveDeliveryTagOffset; + if (realTag > 0 && realTag <= deliveryTag) + { + return base.BasicRejectAsync(realTag, requeue); + } + else + { + return default; + } + } } } diff --git a/projects/RabbitMQ.Client/client/impl/SimpleBlockingRpcContinuation.cs b/projects/RabbitMQ.Client/client/impl/RpcContinuations.cs similarity index 62% rename from projects/RabbitMQ.Client/client/impl/SimpleBlockingRpcContinuation.cs rename to projects/RabbitMQ.Client/client/impl/RpcContinuations.cs index bb40f6be82..b84687ca45 100644 --- a/projects/RabbitMQ.Client/client/impl/SimpleBlockingRpcContinuation.cs +++ b/projects/RabbitMQ.Client/client/impl/RpcContinuations.cs @@ -30,57 +30,11 @@ //--------------------------------------------------------------------------- using System; -using System.Runtime.CompilerServices; -using System.Threading.Tasks; -using RabbitMQ.Client.client.framing; using RabbitMQ.Client.Exceptions; -using RabbitMQ.Client.Framing.Impl; using RabbitMQ.Util; namespace RabbitMQ.Client.Impl { - internal abstract class AsyncRpcContinuation : IRpcContinuation - { - protected readonly TaskCompletionSource _tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - - public TaskAwaiter GetAwaiter() => _tcs.Task.GetAwaiter(); - - public abstract void HandleCommand(in IncomingCommand cmd); - - public void HandleChannelShutdown(ShutdownEventArgs reason) => _tcs.SetException(new OperationInterruptedException(reason)); - } - - internal class ConnectionSecureOrTuneContinuation : AsyncRpcContinuation - { - public override void HandleCommand(in IncomingCommand cmd) - { - try - { - if (cmd.CommandId == ProtocolCommandId.ConnectionSecure) - { - var secure = new ConnectionSecure(cmd.MethodBytes.Span); - _tcs.TrySetResult(new ConnectionSecureOrTune { m_challenge = secure._challenge }); - } - else if (cmd.CommandId == ProtocolCommandId.ConnectionTune) - { - var tune = new ConnectionTune(cmd.MethodBytes.Span); - _tcs.TrySetResult(new ConnectionSecureOrTune - { - m_tuneDetails = new() { m_channelMax = tune._channelMax, m_frameMax = tune._frameMax, m_heartbeatInSeconds = tune._heartbeat } - }); - } - else - { - _tcs.SetException(new InvalidOperationException($"Received unexpected command of type {cmd.CommandId}!")); - } - } - finally - { - cmd.ReturnMethodBuffer(); - } - } - } - internal class SimpleBlockingRpcContinuation : IRpcContinuation { private readonly BlockingCell> m_cell = new BlockingCell>(); @@ -121,4 +75,20 @@ public void HandleChannelShutdown(ShutdownEventArgs reason) m_cell.ContinueWithValue(Either.Right(reason)); } } + + internal class BasicConsumeRpcContinuation : SimpleBlockingRpcContinuation + { + public IBasicConsumer m_consumer; + public string m_consumerTag; + } + + internal class BasicGetRpcContinuation : SimpleBlockingRpcContinuation + { + public BasicGetResult m_result; + } + + internal class QueueDeclareRpcContinuation : SimpleBlockingRpcContinuation + { + public QueueDeclareOk m_result; + } } diff --git a/projects/RabbitMQ.Client/client/impl/SocketFrameHandler.cs b/projects/RabbitMQ.Client/client/impl/SocketFrameHandler.cs index c09bdd1b56..62701ad844 100644 --- a/projects/RabbitMQ.Client/client/impl/SocketFrameHandler.cs +++ b/projects/RabbitMQ.Client/client/impl/SocketFrameHandler.cs @@ -30,12 +30,10 @@ //--------------------------------------------------------------------------- using System; -using System.Buffers; using System.IO; using System.IO.Pipelines; using System.Net; using System.Net.Sockets; -using System.Runtime.InteropServices; using System.Threading; using System.Threading.Channels; using System.Threading.Tasks; @@ -90,21 +88,22 @@ internal sealed class SocketFrameHandler : IFrameHandler { private readonly AmqpTcpEndpoint _amqpTcpEndpoint; private readonly ITcpClient _socket; - private readonly ChannelWriter> _channelWriter; - private readonly ChannelReader> _channelReader; - private readonly PipeWriter _pipeWriter; + private readonly ChannelWriter _channelWriter; + private readonly ChannelReader _channelReader; private readonly PipeReader _pipeReader; + private readonly PipeWriter _pipeWriter; private readonly Task _writerTask; private readonly object _semaphore = new object(); private bool _closed; - private static ReadOnlySpan ProtocolHeader => new byte[] { (byte)'A', (byte)'M', (byte)'Q', (byte)'P', 0, 0, 9, 1 }; + + private static ReadOnlyMemory ProtocolHeader => new byte[] { (byte)'A', (byte)'M', (byte)'Q', (byte)'P', 0, 0, 9, 1 }; public SocketFrameHandler(AmqpTcpEndpoint endpoint, Func socketFactory, TimeSpan connectionTimeout, TimeSpan readTimeout, TimeSpan writeTimeout) { _amqpTcpEndpoint = endpoint; - var channel = Channel.CreateBounded>( + var channel = Channel.CreateBounded( new BoundedChannelOptions(128) { AllowSynchronousContinuations = false, @@ -166,8 +165,8 @@ public SocketFrameHandler(AmqpTcpEndpoint endpoint, } } - _pipeWriter = PipeWriter.Create(netstream); _pipeReader = PipeReader.Create(netstream); + _pipeWriter = PipeWriter.Create(netstream); WriteTimeout = writeTimeout; _writerTask = Task.Run(WriteLoop); @@ -274,22 +273,22 @@ public bool TryReadFrame(out InboundFrame frame) public async ValueTask SendHeaderAsync() { - _pipeWriter.Write(ProtocolHeader); + await _pipeWriter.WriteAsync(ProtocolHeader).ConfigureAwait(false); await _pipeWriter.FlushAsync().ConfigureAwait(false); } - public ValueTask WriteAsync(ReadOnlyMemory memory) + public async ValueTask WriteAsync(RentedMemory frames) { if (_closed) { -#if NET6_0_OR_GREATER - return ValueTask.CompletedTask; -#else - return new ValueTask(Task.CompletedTask); -#endif + frames.Dispose(); + await Task.Yield(); + } + else + { + await _channelWriter.WriteAsync(frames) + .ConfigureAwait(false); } - - return _channelWriter.WriteAsync(memory); } private async Task WriteLoop() @@ -298,12 +297,18 @@ private async Task WriteLoop() { while (await _channelReader.WaitToReadAsync().ConfigureAwait(false)) { - while (_channelReader.TryRead(out ReadOnlyMemory memory)) + if (_channelReader.TryRead(out RentedMemory frames)) { - MemoryMarshal.TryGetArray(memory, out ArraySegment segment); - _pipeWriter.Write(memory.Span); - RabbitMqClientEventSource.Log.CommandSent(segment.Count); - ArrayPool.Shared.Return(segment.Array); + try + { + await _pipeWriter.WriteAsync(frames.Memory) + .ConfigureAwait(false); + } + finally + { + RabbitMqClientEventSource.Log.CommandSent(frames.Size); + frames.Dispose(); + } } await _pipeWriter.FlushAsync() diff --git a/projects/TestApplications/CreateChannel/CreateChannel.csproj b/projects/Test/Applications/CreateChannel/CreateChannel.csproj similarity index 83% rename from projects/TestApplications/CreateChannel/CreateChannel.csproj rename to projects/Test/Applications/CreateChannel/CreateChannel.csproj index 9fe7c226d7..9316c348d1 100644 --- a/projects/TestApplications/CreateChannel/CreateChannel.csproj +++ b/projects/Test/Applications/CreateChannel/CreateChannel.csproj @@ -13,7 +13,7 @@ - + diff --git a/projects/TestApplications/CreateChannel/Program.cs b/projects/Test/Applications/CreateChannel/Program.cs similarity index 100% rename from projects/TestApplications/CreateChannel/Program.cs rename to projects/Test/Applications/CreateChannel/Program.cs diff --git a/projects/TestApplications/MassPublish/MassPublish.csproj b/projects/Test/Applications/MassPublish/MassPublish.csproj similarity index 77% rename from projects/TestApplications/MassPublish/MassPublish.csproj rename to projects/Test/Applications/MassPublish/MassPublish.csproj index 9fe7c226d7..b57323cb5a 100644 --- a/projects/TestApplications/MassPublish/MassPublish.csproj +++ b/projects/Test/Applications/MassPublish/MassPublish.csproj @@ -9,11 +9,12 @@ + latest Exe - + diff --git a/projects/Test/Applications/MassPublish/Program.cs b/projects/Test/Applications/MassPublish/Program.cs new file mode 100644 index 0000000000..abd8820e4c --- /dev/null +++ b/projects/Test/Applications/MassPublish/Program.cs @@ -0,0 +1,178 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Threading; +using System.Threading.Tasks; +using RabbitMQ.Client; +using RabbitMQ.Client.Events; + +namespace MassPublish +{ + static class Program + { + const string AppId = "MassPublish"; + const string ExchangeName = "MassPublish-ex"; + const string QueueName = "MassPublish-queue"; + const string RoutingKey = "MassPublish-queue"; + const string ConsumerTag = "MassPublish-consumer"; + static readonly int ConnectionCount = Environment.ProcessorCount; + + const int BatchesToSend = 64; + const int ItemsPerBatch = 8192; + const int TotalMessages = BatchesToSend * ItemsPerBatch; + + static int s_messagesSent; + static int s_messagesReceived; + + static readonly TaskCompletionSource s_consumeDoneEvent = new(); + + static readonly BasicProperties s_properties = new() { AppId = AppId }; + + static readonly ConnectionFactory s_publishConnectionFactory = new(); + static readonly ConnectionFactory s_consumeConnectionFactory = new() + { + ClientProvidedName = AppId + "-CONSUME", + DispatchConsumersAsync = true + }; + + static readonly Random s_random; + static readonly byte[] s_payload; + static readonly bool s_debug = false; + + static Program() + { + s_random = new Random(); + s_payload = GetRandomBody(1024 * 4); + } + + static async Task Main() + { + + ThreadPool.SetMinThreads(16 * Environment.ProcessorCount, 16 * Environment.ProcessorCount); + + using IConnection consumeConnection = s_consumeConnectionFactory.CreateConnection(); + consumeConnection.ConnectionShutdown += Connection_ConnectionShutdown; + + using IChannel consumeChannel = consumeConnection.CreateChannel(); + consumeChannel.ChannelShutdown += Channel_ChannelShutdown; + await consumeChannel.BasicQosAsync(prefetchSize: 0, prefetchCount: 128, global: false); + + await consumeChannel.ExchangeDeclareAsync(exchange: ExchangeName, + type: ExchangeType.Direct, passive: false, durable: false, autoDelete: false, arguments: null); + + await consumeChannel.QueueDeclareAsync(queue: QueueName, + passive: false, durable: false, exclusive: false, autoDelete: false, arguments: null); + + var asyncListener = new AsyncEventingBasicConsumer(consumeChannel); + asyncListener.Received += AsyncListener_Received; + + await consumeChannel.QueueBindAsync(queue: QueueName, exchange: ExchangeName, routingKey: RoutingKey, arguments: null); + + await consumeChannel.BasicConsumeAsync(queue: QueueName, autoAck: true, consumerTag: ConsumerTag, + noLocal: false, exclusive: false, arguments: null, asyncListener); + + var publishConnections = new List(); + for (int i = 0; i < ConnectionCount; i++) + { + IConnection publishConnection = s_publishConnectionFactory.CreateConnection($"{AppId}-PUBLISH-{i}"); + publishConnection.ConnectionShutdown += Connection_ConnectionShutdown; + publishConnections.Add(publishConnection); + } + + var publishTasks = new List(); + var watch = Stopwatch.StartNew(); + + for (int batchIdx = 0; batchIdx < BatchesToSend; batchIdx++) + { + int idx = s_random.Next(publishConnections.Count); + IConnection publishConnection = publishConnections[idx]; + + publishTasks.Add(Task.Run(async () => + { + using IChannel publishChannel = publishConnection.CreateChannel(); + publishChannel.ChannelShutdown += Channel_ChannelShutdown; + + await publishChannel.ConfirmSelectAsync(); + + for (int i = 0; i < ItemsPerBatch; i++) + { + await publishChannel.BasicPublishAsync(exchange: ExchangeName, routingKey: RoutingKey, + basicProperties: s_properties, body: s_payload, mandatory: true); + Interlocked.Increment(ref s_messagesSent); + } + + await publishChannel.WaitForConfirmsOrDieAsync(); + + if (s_debug) + { + Console.WriteLine("[DEBUG] channel {0} done publishing and waiting for confirms", publishChannel.ChannelNumber); + } + })); + } + + Console.WriteLine($"Sending {BatchesToSend} batches for {ItemsPerBatch} items per batch => Total messages: {TotalMessages}"); + Console.WriteLine(); + Console.WriteLine(" Sent | Received"); + + while (false == s_consumeDoneEvent.Task.Wait(500)) + { + Console.WriteLine($"{s_messagesSent,5} | {s_messagesReceived,5}"); + } + watch.Stop(); + await Task.WhenAll(publishTasks.ToArray()); + + Console.WriteLine($"{s_messagesSent,5} | {s_messagesReceived,5}"); + Console.WriteLine(); + Console.WriteLine($"Took {watch.Elapsed.TotalMilliseconds} ms"); + + foreach (IConnection c in publishConnections) + { + if (s_debug) + { + Console.WriteLine("[DEBUG] closing connection: {0}", c.ClientProvidedName); + } + c.Close(); + } + } + + private static void PublishChannel_BasicNacks(object sender, BasicNackEventArgs e) + { + Console.Error.WriteLine("[ERROR] unexpected nack on publish: {0}", e); + } + + private static void Connection_ConnectionShutdown(object sender, ShutdownEventArgs e) + { + if (e.Initiator != ShutdownInitiator.Application) + { + Console.Error.WriteLine("[ERROR] unexpected connection shutdown: {0}", e); + s_consumeDoneEvent.TrySetResult(false); + } + } + + private static void Channel_ChannelShutdown(object sender, ShutdownEventArgs e) + { + if (e.Initiator != ShutdownInitiator.Application) + { + Console.Error.WriteLine("[ERROR] unexpected channel shutdown: {0}", e); + s_consumeDoneEvent.TrySetResult(false); + } + } + + private static Task AsyncListener_Received(object sender, BasicDeliverEventArgs @event) + { + if (Interlocked.Increment(ref s_messagesReceived) == TotalMessages) + { + s_consumeDoneEvent.SetResult(true); + } + + return Task.CompletedTask; + } + + private static byte[] GetRandomBody(int size) + { + var body = new byte[size]; + s_random.NextBytes(body); + return body; + } + } +} diff --git a/projects/Test/AsyncIntegration/AsyncIntegration.csproj b/projects/Test/AsyncIntegration/AsyncIntegration.csproj new file mode 100644 index 0000000000..ca232068b7 --- /dev/null +++ b/projects/Test/AsyncIntegration/AsyncIntegration.csproj @@ -0,0 +1,54 @@ + + + + net6.0;net472 + + + + net6.0 + + + + ../../rabbit.snk + true + latest + 7.0 + true + + + + + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + diff --git a/projects/Test/AsyncIntegration/AsyncIntegrationFixture.cs b/projects/Test/AsyncIntegration/AsyncIntegrationFixture.cs new file mode 100644 index 0000000000..e7426d7518 --- /dev/null +++ b/projects/Test/AsyncIntegration/AsyncIntegrationFixture.cs @@ -0,0 +1,91 @@ +// This source code is dual-licensed under the Apache License, version +// 2.0, and the Mozilla Public License, version 2.0. +// +// The APL v2.0: +// +//--------------------------------------------------------------------------- +// Copyright (c) 2007-2020 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//--------------------------------------------------------------------------- +// +// The MPL v2.0: +// +//--------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// +// Copyright (c) 2007-2020 VMware, Inc. All rights reserved. +//--------------------------------------------------------------------------- + +using System.Threading.Tasks; +using RabbitMQ.Client; +using Xunit; +using Xunit.Abstractions; + +namespace Test.AsyncIntegration +{ + public class AsyncIntegrationFixture : IntegrationFixture, IAsyncLifetime + { + protected readonly bool _dispatchConsumersAsync = false; + protected readonly ushort _consumerDispatchConcurrency = 1; + + public AsyncIntegrationFixture(ITestOutputHelper output, + bool dispatchConsumersAsync = false, ushort consumerDispatchConcurrency = 1) : base(output) + { + _dispatchConsumersAsync = dispatchConsumersAsync; + _consumerDispatchConcurrency = consumerDispatchConcurrency; + } + + protected override void SetUp() + { + // InitializeAsync + } + + public virtual async Task InitializeAsync() + { + _connFactory = CreateConnectionFactory(); + _connFactory.DispatchConsumersAsync = _dispatchConsumersAsync; + _connFactory.ConsumerDispatchConcurrency = _consumerDispatchConcurrency; + + _conn = await _connFactory.CreateConnectionAsync(); + if (_connFactory.AutomaticRecoveryEnabled) + { + Assert.IsType(_conn); + } + else + { + Assert.IsType(_conn); + } + + _channel = await _conn.CreateChannelAsync(); + } + + public virtual async Task DisposeAsync() + { + try + { + await _channel.CloseAsync(); + _conn.Close(); + } + finally + { + _channel.Dispose(); + _conn.Dispose(); + _channel = null; + _conn = null; + } + } + } +} diff --git a/projects/Test/AsyncIntegration/TestAsyncConsumer.cs b/projects/Test/AsyncIntegration/TestAsyncConsumer.cs new file mode 100644 index 0000000000..07f959c002 --- /dev/null +++ b/projects/Test/AsyncIntegration/TestAsyncConsumer.cs @@ -0,0 +1,469 @@ +// This source code is dual-licensed under the Apache License, version +// 2.0, and the Mozilla Public License, version 2.0. +// +// The APL v2.0: +// +//--------------------------------------------------------------------------- +// Copyright (c) 2007-2020 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//--------------------------------------------------------------------------- +// +// The MPL v2.0: +// +//--------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// +// Copyright (c) 2007-2020 VMware, Inc. All rights reserved. +//--------------------------------------------------------------------------- + +using System; +using System.Threading; +using System.Threading.Tasks; +using RabbitMQ.Client; +using RabbitMQ.Client.Events; +using Xunit; +using Xunit.Abstractions; + +namespace Test.AsyncIntegration +{ + public class TestAsyncConsumer : AsyncIntegrationFixture + { + private readonly ShutdownEventArgs _closeArgs = new ShutdownEventArgs(ShutdownInitiator.Application, Constants.ReplySuccess, "normal shutdown"); + + public TestAsyncConsumer(ITestOutputHelper output) : base(output, true, 2) + { + } + + [Fact] + public async Task TestBasicRoundtripConcurrent() + { + QueueDeclareOk q = _channel.QueueDeclare(); + string publish1 = GetUniqueString(1024); + byte[] body = _encoding.GetBytes(publish1); + _channel.BasicPublish("", q.QueueName, body); + + string publish2 = GetUniqueString(1024); + body = _encoding.GetBytes(publish2); + _channel.BasicPublish("", q.QueueName, body); + + var consumer = new AsyncEventingBasicConsumer(_channel); + + var publish1SyncSource = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var publish2SyncSource = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var maximumWaitTime = TimeSpan.FromSeconds(10); + var tokenSource = new CancellationTokenSource(maximumWaitTime); + tokenSource.Token.Register(() => + { + publish1SyncSource.TrySetResult(false); + publish2SyncSource.TrySetResult(false); + }); + + _conn.ConnectionShutdown += (o, ea) => + { + HandleConnectionShutdown(_conn, ea, () => + { + publish1SyncSource.TrySetResult(false); + publish2SyncSource.TrySetResult(false); + }); + }; + + _channel.ChannelShutdown += (o, ea) => + { + HandleChannelShutdown(_channel, ea, () => + { + publish1SyncSource.TrySetResult(false); + publish2SyncSource.TrySetResult(false); + }); + }; + + consumer.Received += async (o, a) => + { + string decoded = _encoding.GetString(a.Body); + if (decoded == publish1) + { + publish1SyncSource.TrySetResult(true); + await publish2SyncSource.Task; + } + else if (decoded == publish2) + { + publish2SyncSource.TrySetResult(true); + await publish1SyncSource.Task; + } + }; + + _channel.BasicConsume(q.QueueName, true, consumer); + + // ensure we get a delivery + await Task.WhenAll(publish1SyncSource.Task, publish2SyncSource.Task); + + bool result1 = await publish1SyncSource.Task; + Assert.True(result1, $"1 - Non concurrent dispatch lead to deadlock after {maximumWaitTime}"); + + bool result2 = await publish2SyncSource.Task; + Assert.True(result2, $"2 - Non concurrent dispatch lead to deadlock after {maximumWaitTime}"); + } + + [Fact] + public async Task TestBasicRoundtripConcurrentManyMessages() + { + const int publish_total = 4096; + string queueName = $"{nameof(TestBasicRoundtripConcurrentManyMessages)}-{Guid.NewGuid()}"; + + string publish1 = GetUniqueString(32768); + byte[] body1 = _encoding.GetBytes(publish1); + string publish2 = GetUniqueString(32768); + byte[] body2 = _encoding.GetBytes(publish2); + + var publish1SyncSource = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var publish2SyncSource = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var maximumWaitTime = TimeSpan.FromSeconds(30); + var tokenSource = new CancellationTokenSource(maximumWaitTime); + tokenSource.Token.Register(() => + { + publish1SyncSource.TrySetResult(false); + publish2SyncSource.TrySetResult(false); + }); + + _conn.ConnectionShutdown += (o, ea) => + { + HandleConnectionShutdown(_conn, ea, () => + { + publish1SyncSource.TrySetResult(false); + publish2SyncSource.TrySetResult(false); + }); + }; + + _channel.ChannelShutdown += (o, ea) => + { + HandleChannelShutdown(_channel, ea, () => + { + publish1SyncSource.TrySetResult(false); + publish2SyncSource.TrySetResult(false); + }); + }; + + QueueDeclareOk q = _channel.QueueDeclare(queue: queueName, exclusive: false, durable: true); + Assert.Equal(q.QueueName, queueName); + + Task publishTask = Task.Run(async () => + { + using (IChannel m = await _conn.CreateChannelAsync()) + { + QueueDeclareOk q = _channel.QueueDeclare(queue: queueName, exclusive: false, durable: true); + for (int i = 0; i < publish_total; i++) + { + await _channel.BasicPublishAsync(string.Empty, queueName, body1); + await _channel.BasicPublishAsync(string.Empty, queueName, body2); + } + } + }); + + Task consumeTask = Task.Run(async () => + { + using (IChannel m = await _conn.CreateChannelAsync()) + { + var consumer = new AsyncEventingBasicConsumer(m); + + int publish1_count = 0; + int publish2_count = 0; + + consumer.Received += async (o, a) => + { + string decoded = _encoding.GetString(a.Body); + if (decoded == publish1) + { + if (Interlocked.Increment(ref publish1_count) >= publish_total) + { + publish1SyncSource.TrySetResult(true); + await publish2SyncSource.Task; + } + } + else if (decoded == publish2) + { + if (Interlocked.Increment(ref publish2_count) >= publish_total) + { + publish2SyncSource.TrySetResult(true); + await publish1SyncSource.Task; + } + } + }; + + _channel.BasicConsume(queueName, true, consumer); + + // ensure we get a delivery + await Task.WhenAll(publish1SyncSource.Task, publish2SyncSource.Task); + + bool result1 = await publish1SyncSource.Task; + Assert.True(result1, $"Non concurrent dispatch lead to deadlock after {maximumWaitTime}"); + + bool result2 = await publish2SyncSource.Task; + Assert.True(result2, $"Non concurrent dispatch lead to deadlock after {maximumWaitTime}"); + } + }); + + await Task.WhenAll(publishTask, consumeTask); + } + + [Fact] + public async Task TestBasicRejectAsync() + { + var publishSyncSource = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + var cf = CreateConnectionFactory(); + cf.DispatchConsumersAsync = true; + + using IConnection connection = await cf.CreateConnectionAsync(); + using IChannel channel = await connection.CreateChannelAsync(); + + connection.ConnectionShutdown += (o, ea) => + { + HandleConnectionShutdown(connection, ea, () => + { + publishSyncSource.TrySetResult(false); + }); + }; + + channel.ChannelShutdown += (o, ea) => + { + HandleChannelShutdown(channel, ea, () => + { + publishSyncSource.TrySetResult(false); + }); + }; + + var consumer = new AsyncEventingBasicConsumer(channel); + consumer.Received += async (object sender, BasicDeliverEventArgs args) => + { + var c = sender as AsyncEventingBasicConsumer; + Assert.NotNull(c); + await channel.BasicCancelAsync(c.ConsumerTags[0]); + await channel.BasicRejectAsync(args.DeliveryTag, true); + publishSyncSource.TrySetResult(true); + }; + + QueueDeclareOk q = await channel.QueueDeclareAsync(string.Empty, false, false, true, false, null); + string queueName = q.QueueName; + const string publish1 = "sync-hi-1"; + byte[] _body = _encoding.GetBytes(publish1); + await channel.BasicPublishAsync(string.Empty, queueName, _body); + + await channel.BasicConsumeAsync(queue: queueName, autoAck: false, + consumerTag: string.Empty, noLocal: false, exclusive: false, + arguments: null, consumer); + + Assert.True(await publishSyncSource.Task); + + uint messageCount, consumerCount = 0; + ushort tries = 5; + do + { + QueueDeclareOk result = await channel.QueueDeclareAsync(queue: queueName, passive: true, false, false, false, null); + consumerCount = result.ConsumerCount; + messageCount = result.MessageCount; + if (consumerCount == 0 && messageCount > 0) + { + break; + } + else + { + await Task.Delay(500); + } + } while (tries-- > 0); + + if (tries == 0) + { + Assert.Fail("[ERROR] failed waiting for MessageCount > 0 && ConsumerCount == 0"); + } + else + { + Assert.Equal((uint)1, messageCount); + Assert.Equal((uint)0, consumerCount); + } + + // Note: closing channel explicitly just to test it. + await channel.CloseAsync(_closeArgs, false); + } + + [Fact] + public async Task TestBasicAckAsync() + { + const int messageCount = 1024; + int messagesReceived = 0; + + var publishSyncSource = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + var cf = CreateConnectionFactory(); + cf.DispatchConsumersAsync = true; + + using IConnection connection = await cf.CreateConnectionAsync(); + using IChannel channel = await connection.CreateChannelAsync(); + + connection.ConnectionShutdown += (o, ea) => + { + HandleConnectionShutdown(connection, ea, () => + { + publishSyncSource.TrySetResult(false); + }); + }; + + channel.ChannelShutdown += (o, ea) => + { + HandleChannelShutdown(channel, ea, () => + { + publishSyncSource.TrySetResult(false); + }); + }; + + await channel.ConfirmSelectAsync(); + + var consumer = new AsyncEventingBasicConsumer(channel); + consumer.Received += async (object sender, BasicDeliverEventArgs args) => + { + var c = sender as AsyncEventingBasicConsumer; + Assert.NotNull(c); + await channel.BasicAckAsync(args.DeliveryTag, false); + messagesReceived++; + if (messagesReceived == messageCount) + { + publishSyncSource.SetResult(true); + } + }; + + QueueDeclareOk q = await channel.QueueDeclareAsync(string.Empty, false, false, true, false, null); + string queueName = q.QueueName; + + await channel.BasicQosAsync(0, 1, false); + await channel.BasicConsumeAsync(queue: queueName, autoAck: false, + consumerTag: string.Empty, noLocal: false, exclusive: false, + arguments: null, consumer); + + var publishTask = Task.Run(async () => + { + for (int i = 0; i < messageCount; i++) + { + byte[] _body = _encoding.GetBytes(Guid.NewGuid().ToString()); + await channel.BasicPublishAsync(string.Empty, queueName, _body); + } + }); + + await channel.WaitForConfirmsOrDieAsync(); + Assert.True(await publishSyncSource.Task); + + Assert.Equal(messageCount, messagesReceived); + + // Note: closing channel explicitly just to test it. + await channel.CloseAsync(_closeArgs, false); + } + + [Fact] + public async Task TestBasicNackAsync() + { + var publishSyncSource = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + var cf = CreateConnectionFactory(); + cf.DispatchConsumersAsync = true; + + using IConnection connection = await cf.CreateConnectionAsync(); + using IChannel channel = await connection.CreateChannelAsync(); + + connection.ConnectionShutdown += (o, ea) => + { + HandleConnectionShutdown(connection, ea, () => + { + publishSyncSource.TrySetResult(false); + }); + }; + + channel.ChannelShutdown += (o, ea) => + { + HandleChannelShutdown(channel, ea, () => + { + publishSyncSource.TrySetResult(false); + }); + }; + + var consumer = new AsyncEventingBasicConsumer(channel); + consumer.Received += async (object sender, BasicDeliverEventArgs args) => + { + var c = sender as AsyncEventingBasicConsumer; + Assert.NotNull(c); + await channel.BasicCancelAsync(c.ConsumerTags[0]); + await channel.BasicNackAsync(args.DeliveryTag, false, true); + publishSyncSource.SetResult(true); + }; + + QueueDeclareOk q = await channel.QueueDeclareAsync(string.Empty, false, false, false, false, null); + string queueName = q.QueueName; + const string publish1 = "sync-hi-1"; + byte[] _body = _encoding.GetBytes(publish1); + await channel.BasicPublishAsync(string.Empty, queueName, _body); + + await channel.BasicConsumeAsync(queue: queueName, autoAck: false, + consumerTag: string.Empty, noLocal: false, exclusive: false, + arguments: null, consumer); + + Assert.True(await publishSyncSource.Task); + + uint messageCount, consumerCount = 0; + ushort tries = 5; + do + { + QueueDeclareOk result = await channel.QueueDeclareAsync(queue: queueName, passive: true, false, false, false, null); + consumerCount = result.ConsumerCount; + messageCount = result.MessageCount; + if (consumerCount == 0 && messageCount > 0) + { + break; + } + else + { + await Task.Delay(500); + } + } while (tries-- > 0); + + if (tries == 0) + { + Assert.Fail("[ERROR] failed waiting for MessageCount > 0 && ConsumerCount == 0"); + } + else + { + Assert.Equal((uint)1, messageCount); + Assert.Equal((uint)0, consumerCount); + } + + // Note: closing channel explicitly just to test it. + await channel.CloseAsync(_closeArgs, false); + } + + [Fact] + public async Task NonAsyncConsumerShouldThrowInvalidOperationException() + { + bool sawException = false; + QueueDeclareOk q = await _channel.QueueDeclareAsync(string.Empty, false, false, false, false, null); + await _channel.BasicPublishAsync(string.Empty, q.QueueName, GetRandomBody(1024)); + var consumer = new EventingBasicConsumer(_channel); + try + { + string consumerTag = await _channel.BasicConsumeAsync(q.QueueName, false, string.Empty, false, false, null, consumer); + } + catch (InvalidOperationException) + { + sawException = true; + } + Assert.True(sawException, "did not see expected InvalidOperationException"); + } + } +} diff --git a/projects/Unit/TestAsyncConsumerExceptions.cs b/projects/Test/AsyncIntegration/TestAsyncConsumerExceptions.cs similarity index 68% rename from projects/Unit/TestAsyncConsumerExceptions.cs rename to projects/Test/AsyncIntegration/TestAsyncConsumerExceptions.cs index 610f7388f8..664ab14101 100644 --- a/projects/Unit/TestAsyncConsumerExceptions.cs +++ b/projects/Test/AsyncIntegration/TestAsyncConsumerExceptions.cs @@ -32,86 +32,80 @@ using System; using System.Threading; using System.Threading.Tasks; +using RabbitMQ.Client; using RabbitMQ.Client.Events; using Xunit; using Xunit.Abstractions; -namespace RabbitMQ.Client.Unit +namespace Test.AsyncIntegration { - public class TestAsyncConsumerExceptions : IntegrationFixture + public class TestAsyncConsumerExceptions : AsyncIntegrationFixture { private static readonly Exception TestException = new Exception("oops"); - public TestAsyncConsumerExceptions(ITestOutputHelper output) : base(output) + public TestAsyncConsumerExceptions(ITestOutputHelper output) : base(output, true, 1) { } - protected void TestExceptionHandlingWith(IBasicConsumer consumer, - Action action) - { - var resetEvent = new AutoResetEvent(false); - bool notified = false; - string q = _channel.QueueDeclare(); - - _channel.CallbackException += (m, evt) => - { - if (evt.Exception != TestException) return; - - notified = true; - resetEvent.Set(); - }; - - string tag = _channel.BasicConsume(q, true, consumer); - action(_channel, q, consumer, tag); - resetEvent.WaitOne(2000); - - Assert.True(notified); - } - - protected override void SetUp() - { - _connFactory = new ConnectionFactory - { - DispatchConsumersAsync = true - }; - - _conn = _connFactory.CreateConnection(); - _channel = _conn.CreateChannel(); - } - [Fact] - public void TestCancelNotificationExceptionHandling() + public async Task TestCancelNotificationExceptionHandling() { IBasicConsumer consumer = new ConsumerFailingOnCancel(_channel); - TestExceptionHandlingWith(consumer, (m, q, c, ct) => m.QueueDelete(q)); + await TestExceptionHandlingWith(consumer, async (ch, q, c, ct) => + { + await ch.QueueDeleteAsync(q, false, false); + }); } [Fact] - public void TestConsumerCancelOkExceptionHandling() + public async Task TestConsumerCancelOkExceptionHandling() { IBasicConsumer consumer = new ConsumerFailingOnCancelOk(_channel); - TestExceptionHandlingWith(consumer, (m, q, c, ct) => m.BasicCancel(ct)); + await TestExceptionHandlingWith(consumer, (ch, q, c, ct) => ch.BasicCancelAsync(ct)); } [Fact] - public void TestConsumerConsumeOkExceptionHandling() + public async Task TestConsumerConsumeOkExceptionHandling() { IBasicConsumer consumer = new ConsumerFailingOnConsumeOk(_channel); - TestExceptionHandlingWith(consumer, (m, q, c, ct) => { }); + await TestExceptionHandlingWith(consumer, async (ch, q, c, ct) => await Task.Yield()); } [Fact] - public void TestConsumerShutdownExceptionHandling() + public async Task TestConsumerShutdownExceptionHandling() { IBasicConsumer consumer = new ConsumerFailingOnShutdown(_channel); - TestExceptionHandlingWith(consumer, (m, q, c, ct) => m.Close()); + await TestExceptionHandlingWith(consumer, (ch, q, c, ct) => ch.CloseAsync()); } [Fact] - public void TestDeliveryExceptionHandling() + public async Task TestDeliveryExceptionHandling() { IBasicConsumer consumer = new ConsumerFailingOnDelivery(_channel); - TestExceptionHandlingWith(consumer, (m, q, c, ct) => m.BasicPublish("", q, _encoding.GetBytes("msg"))); + await TestExceptionHandlingWith(consumer, (ch, q, c, ct) => + ch.BasicPublishAsync("", q, _encoding.GetBytes("msg"))); + } + + protected async Task TestExceptionHandlingWith(IBasicConsumer consumer, + Func action) + { + var waitSpan = TimeSpan.FromSeconds(2); + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var cts = new CancellationTokenSource(waitSpan); + cts.Token.Register(() => tcs.TrySetResult(false)); + + string q = await _channel.QueueDeclareAsync(string.Empty, false, false, true, false, null); + _channel.CallbackException += (ch, evt) => + { + if (evt.Exception == TestException) + { + tcs.SetResult(true); + } + }; + + string tag = await _channel.BasicConsumeAsync(q, true, string.Empty, false, false, null, consumer); + await action(_channel, q, consumer, tag); + Assert.True(await tcs.Task); } private class ConsumerFailingOnDelivery : AsyncEventingBasicConsumer @@ -126,7 +120,7 @@ public override Task HandleBasicDeliver(string consumerTag, string exchange, string routingKey, in ReadOnlyBasicProperties properties, - ReadOnlyMemory body) + RentedMemory body) { return Task.FromException(TestException); } diff --git a/projects/Test/AsyncIntegration/TestBasicGetAsync.cs b/projects/Test/AsyncIntegration/TestBasicGetAsync.cs new file mode 100644 index 0000000000..21e7cfc505 --- /dev/null +++ b/projects/Test/AsyncIntegration/TestBasicGetAsync.cs @@ -0,0 +1,62 @@ +// This source code is dual-licensed under the Apache License, version +// 2.0, and the Mozilla Public License, version 2.0. +// +// The APL v2.0: +// +//--------------------------------------------------------------------------- +// Copyright (c) 2007-2020 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//--------------------------------------------------------------------------- +// +// The MPL v2.0: +// +//--------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// +// Copyright (c) 2007-2020 VMware, Inc. All rights reserved. +//--------------------------------------------------------------------------- + +using System.Threading.Tasks; +using RabbitMQ.Client; +using Xunit; +using Xunit.Abstractions; + +namespace Test.AsyncIntegration +{ + public class TestBasicGetAsync : AsyncIntegrationFixture + { + public TestBasicGetAsync(ITestOutputHelper output) : base(output) + { + } + + [Fact] + public async Task TestBasicGet() + { + const string msg = "for async basic.get"; + + QueueDeclareOk queueResult = await _channel.QueueDeclareAsync(string.Empty, false, true, true, true, null); + string queueName = queueResult.QueueName; + + await _channel.BasicPublishAsync(string.Empty, queueName, _encoding.GetBytes(msg), true); + + BasicGetResult getResult = await _channel.BasicGetAsync(queueName, true); + Assert.Equal(msg, _encoding.GetString(getResult.Body)); + + QueueDeclareOk queueResultPassive = await _channel.QueueDeclareAsync(queueName, true, true, true, true, null); + Assert.Equal((uint)0, queueResultPassive.MessageCount); + } + } +} diff --git a/projects/Test/AsyncIntegration/TestBasicPublishAsync.cs b/projects/Test/AsyncIntegration/TestBasicPublishAsync.cs new file mode 100644 index 0000000000..7bfa6d4275 --- /dev/null +++ b/projects/Test/AsyncIntegration/TestBasicPublishAsync.cs @@ -0,0 +1,74 @@ +// This source code is dual-licensed under the Apache License, version +// 2.0, and the Mozilla Public License, version 2.0. +// +// The APL v2.0: +// +//--------------------------------------------------------------------------- +// Copyright (c) 2007-2020 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//--------------------------------------------------------------------------- +// +// The MPL v2.0: +// +//--------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// +// Copyright (c) 2011-2020 VMware, Inc. or its affiliates. All rights reserved. +//--------------------------------------------------------------------------- + +using System; +using System.Threading.Tasks; +using RabbitMQ.Client; +using Xunit; +using Xunit.Abstractions; + +namespace Test.AsyncIntegration +{ + public class TestBasicPublishAsync : AsyncIntegrationFixture + { + public TestBasicPublishAsync(ITestOutputHelper output) : base(output) + { + } + + [Fact] + public async Task TestQueuePurgeAsync() + { + const int messageCount = 1024; + + var publishSyncSource = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + await _channel.ConfirmSelectAsync(); + + QueueDeclareOk q = await _channel.QueueDeclareAsync(string.Empty, false, false, true, false, null); + string queueName = q.QueueName; + + var publishTask = Task.Run(async () => + { + for (int i = 0; i < messageCount; i++) + { + byte[] body = _encoding.GetBytes(Guid.NewGuid().ToString()); + await _channel.BasicPublishAsync(string.Empty, queueName, body); + } + publishSyncSource.SetResult(true); + }); + + await _channel.WaitForConfirmsOrDieAsync(); + Assert.True(await publishSyncSource.Task); + + Assert.Equal((uint)messageCount, await _channel.QueuePurgeAsync(queueName)); + } + } +} diff --git a/projects/Test/AsyncIntegration/TestConcurrentAccessWithSharedConnectionAsync.cs b/projects/Test/AsyncIntegration/TestConcurrentAccessWithSharedConnectionAsync.cs new file mode 100644 index 0000000000..e085412eb7 --- /dev/null +++ b/projects/Test/AsyncIntegration/TestConcurrentAccessWithSharedConnectionAsync.cs @@ -0,0 +1,161 @@ +// This source code is dual-licensed under the Apache License, version +// 2.0, and the Mozilla Public License, version 2.0. +// +// The APL v2.0: +// +//--------------------------------------------------------------------------- +// Copyright (c) 2007-2020 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//--------------------------------------------------------------------------- +// +// The MPL v2.0: +// +//--------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// +// Copyright (c) 2007-2020 VMware, Inc. All rights reserved. +//--------------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using RabbitMQ.Client; +using RabbitMQ.Client.Events; +using Xunit; +using Xunit.Abstractions; + +namespace Test.AsyncIntegration +{ + public class TestConcurrentAccessWithSharedConnectionAsync : AsyncIntegrationFixture + { + private const ushort _messageCount = 200; + + public TestConcurrentAccessWithSharedConnectionAsync(ITestOutputHelper output) : base(output) + { + } + + public override async Task InitializeAsync() + { + await base.InitializeAsync(); + _conn.ConnectionShutdown += HandleConnectionShutdown; + await _channel.CloseAsync(); + } + + [Fact] + public Task TestConcurrentChannelOpenAndPublishingWithBlankMessagesAsync() + { + return TestConcurrentChannelOpenAndPublishingWithBodyAsync(Array.Empty(), 30); + } + + [Fact] + public Task TestConcurrentChannelOpenAndPublishingSize64Async() + { + return TestConcurrentChannelOpenAndPublishingWithBodyOfSizeAsync(64); + } + + [Fact] + public Task TestConcurrentChannelOpenAndPublishingSize256Async() + { + return TestConcurrentChannelOpenAndPublishingWithBodyOfSizeAsync(256); + } + + [Fact] + public Task TestConcurrentChannelOpenAndPublishingSize1024Async() + { + return TestConcurrentChannelOpenAndPublishingWithBodyOfSizeAsync(1024); + } + + private Task TestConcurrentChannelOpenAndPublishingWithBodyOfSizeAsync(ushort length, int iterations = 30) + { + byte[] body = GetRandomBody(length); + return TestConcurrentChannelOpenAndPublishingWithBodyAsync(body, iterations); + } + + private Task TestConcurrentChannelOpenAndPublishingWithBodyAsync(byte[] body, int iterations) + { + return TestConcurrentChannelOperationsAsync(async (conn) => + { + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var tokenSource = new CancellationTokenSource(LongWaitSpan); + tokenSource.Token.Register(() => + { + tcs.TrySetResult(false); + }); + + using (IChannel ch = await _conn.CreateChannelAsync()) + { + ch.ChannelShutdown += (o, ea) => + { + HandleChannelShutdown(ch, ea, () => + { + tcs.SetResult(false); + }); + }; + + await ch.ConfirmSelectAsync(); + + ch.BasicAcks += (object sender, BasicAckEventArgs e) => + { + if (e.DeliveryTag >= _messageCount) + { + tcs.SetResult(true); + } + }; + + ch.BasicNacks += (object sender, BasicNackEventArgs e) => + { + tcs.SetResult(false); + _output.WriteLine($"channel #{ch.ChannelNumber} saw a nack, deliveryTag: {e.DeliveryTag}, multiple: {e.Multiple}"); + }; + + QueueDeclareOk q = await ch.QueueDeclareAsync(queue: string.Empty, passive: false, durable: false, exclusive: true, autoDelete: true, arguments: null); + for (ushort j = 0; j < _messageCount; j++) + { + await ch.BasicPublishAsync("", q.QueueName, body, mandatory: true); + } + + Assert.True(await tcs.Task); + } + }, iterations); + } + + private Task TestConcurrentChannelOperationsAsync(Func actions, int iterations) + { + return TestConcurrentChannelOperationsAsync(actions, iterations, LongWaitSpan); + } + + private async Task TestConcurrentChannelOperationsAsync(Func action, int iterations, TimeSpan timeout) + { + var tasks = new List(); + for (int i = 0; i < _processorCount; i++) + { + for (int j = 0; j < iterations; j++) + { + tasks.Add(action(_conn)); + } + } + Task t = Task.WhenAll(tasks); + await t; + Assert.Equal(TaskStatus.RanToCompletion, t.Status); + + // incorrect frame interleaving in these tests will result + // in an unrecoverable connection-level exception, thus + // closing the connection + Assert.True(_conn.IsOpen); + } + } +} diff --git a/projects/Test/AsyncIntegration/TestConfirmSelectAsync.cs b/projects/Test/AsyncIntegration/TestConfirmSelectAsync.cs new file mode 100644 index 0000000000..6e53e0852a --- /dev/null +++ b/projects/Test/AsyncIntegration/TestConfirmSelectAsync.cs @@ -0,0 +1,69 @@ +// This source code is dual-licensed under the Apache License, version +// 2.0, and the Mozilla Public License, version 2.0. +// +// The APL v2.0: +// +//--------------------------------------------------------------------------- +// Copyright (c) 2007-2020 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//--------------------------------------------------------------------------- +// +// The MPL v2.0: +// +//--------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// +// Copyright (c) 2007-2020 VMware, Inc. All rights reserved. +//--------------------------------------------------------------------------- + +using System.Threading.Tasks; +using RabbitMQ.Client; +using Xunit; +using Xunit.Abstractions; + +namespace Test.AsyncIntegration +{ + public class TestConfirmSelectAsync : AsyncIntegrationFixture + { + public TestConfirmSelectAsync(ITestOutputHelper output) : base(output) + { + } + + [Fact] + public async Task TestConfirmSelectIdempotency() + { + await _channel.ConfirmSelectAsync(); + Assert.Equal(1ul, _channel.NextPublishSeqNo); + await Publish(); + Assert.Equal(2ul, _channel.NextPublishSeqNo); + await Publish(); + Assert.Equal(3ul, _channel.NextPublishSeqNo); + + await _channel.ConfirmSelectAsync(); + await Publish(); + Assert.Equal(4ul, _channel.NextPublishSeqNo); + await Publish(); + Assert.Equal(5ul, _channel.NextPublishSeqNo); + await Publish(); + Assert.Equal(6ul, _channel.NextPublishSeqNo); + } + + private ValueTask Publish() + { + return _channel.BasicPublishAsync("", "amq.fanout", _encoding.GetBytes("message")); + } + } +} diff --git a/projects/Test/AsyncIntegration/TestExchangeDeclareAsync.cs b/projects/Test/AsyncIntegration/TestExchangeDeclareAsync.cs new file mode 100644 index 0000000000..70b8c59182 --- /dev/null +++ b/projects/Test/AsyncIntegration/TestExchangeDeclareAsync.cs @@ -0,0 +1,103 @@ +// This source code is dual-licensed under the Apache License, version +// 2.0, and the Mozilla Public License, version 2.0. +// +// The APL v2.0: +// +//--------------------------------------------------------------------------- +// Copyright (c) 2007-2020 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//--------------------------------------------------------------------------- +// +// The MPL v2.0: +// +//--------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// +// Copyright (c) 2007-2020 VMware, Inc. All rights reserved. +//--------------------------------------------------------------------------- + +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Threading.Tasks; +using Xunit; +using Xunit.Abstractions; + +namespace Test.AsyncIntegration +{ + public class TestExchangeDeclareAsync : AsyncIntegrationFixture + { + private readonly Random _rnd = new Random(); + + public TestExchangeDeclareAsync(ITestOutputHelper output) : base(output) + { + } + + [Fact] + public async Task TestConcurrentExchangeDeclareAndBindAsync() + { + var exchangeNames = new ConcurrentBag(); + var ts = new List(); + NotSupportedException nse = null; + for (int i = 0; i < 256; i++) + { + async Task f() + { + try + { + await Task.Delay(_rnd.Next(5, 50)); + string exchangeName = GenerateExchangeName(); + await _channel.ExchangeDeclareAsync(exchange: exchangeName, type: "fanout", passive: false, false, false, null); + await _channel.ExchangeBindAsync(destination: "amq.fanout", source: exchangeName, routingKey: "unused", null); + exchangeNames.Add(exchangeName); + } + catch (NotSupportedException e) + { + nse = e; + } + } + var t = Task.Run(f); + ts.Add(t); + } + + await Task.WhenAll(ts); + Assert.Null(nse); + ts.Clear(); + + foreach (string exchangeName in exchangeNames) + { + async Task f() + { + try + { + await Task.Delay(_rnd.Next(5, 50)); + await _channel.ExchangeUnbindAsync(destination: "amq.fanout", source: exchangeName, routingKey: "unused", null); + await _channel.ExchangeDeleteAsync(exchange: exchangeName, ifUnused: false); + } + catch (NotSupportedException e) + { + nse = e; + } + } + var t = Task.Run(f); + ts.Add(t); + } + + await Task.WhenAll(ts); + Assert.Null(nse); + } + } +} diff --git a/projects/Unit/TestExtensions.cs b/projects/Test/AsyncIntegration/TestExtensionsAsync.cs similarity index 52% rename from projects/Unit/TestExtensions.cs rename to projects/Test/AsyncIntegration/TestExtensionsAsync.cs index 2392011edb..1b484e167d 100644 --- a/projects/Unit/TestExtensions.cs +++ b/projects/Test/AsyncIntegration/TestExtensionsAsync.cs @@ -31,58 +31,62 @@ using System; using System.Threading.Tasks; +using RabbitMQ.Client; using Xunit; using Xunit.Abstractions; -namespace RabbitMQ.Client.Unit +namespace Test.AsyncIntegration { - public class TestExtensions : IntegrationFixture + public class TestExtensionsAsync : AsyncIntegrationFixture { - public TestExtensions(ITestOutputHelper output) : base(output) + public TestExtensionsAsync(ITestOutputHelper output) : base(output) { } [Fact] public async Task TestConfirmBarrier() { - _channel.ConfirmSelect(); + await _channel.ConfirmSelectAsync(); for (int i = 0; i < 10; i++) { - _channel.BasicPublish("", string.Empty); + await _channel.BasicPublishAsync("", string.Empty); } Assert.True(await _channel.WaitForConfirmsAsync()); } [Fact] - public async Task TestConfirmBeforeWait() + public Task TestConfirmBeforeWait() { - await Assert.ThrowsAsync(async () => await _channel.WaitForConfirmsAsync()); + return Assert.ThrowsAsync(() => _channel.WaitForConfirmsAsync()); } [Fact] - public async Task TestExchangeBinding() + public async Task TestExchangeBindingAsync() { - _channel.ConfirmSelect(); + await _channel.ConfirmSelectAsync(); - _channel.ExchangeDeclare("src", ExchangeType.Direct, false, false, null); - _channel.ExchangeDeclare("dest", ExchangeType.Direct, false, false, null); - string queue = _channel.QueueDeclare(); + await _channel.ExchangeDeclareAsync("src", ExchangeType.Direct, false, false, false, null); + await _channel.ExchangeDeclareAsync("dest", ExchangeType.Direct, false, false, false, null); + string queue = await _channel.QueueDeclareAsync(string.Empty, false, false, true, false, null); - _channel.ExchangeBind("dest", "src", string.Empty); - _channel.ExchangeBind("dest", "src", string.Empty); - _channel.QueueBind(queue, "dest", string.Empty); + await _channel.ExchangeBindAsync("dest", "src", string.Empty, null); + await _channel.ExchangeBindAsync("dest", "src", string.Empty, null); + await _channel.QueueBindAsync(queue, "dest", string.Empty, null); - _channel.BasicPublish("src", string.Empty); + await _channel.BasicPublishAsync("src", string.Empty); await _channel.WaitForConfirmsAsync(); - Assert.NotNull(_channel.BasicGet(queue, true)); + Assert.NotNull(await _channel.BasicGetAsync(queue, true)); - _channel.ExchangeUnbind("dest", "src", string.Empty); - _channel.BasicPublish("src", string.Empty); + await _channel.ExchangeUnbindAsync("dest", "src", string.Empty, null); + await _channel.BasicPublishAsync("src", string.Empty); await _channel.WaitForConfirmsAsync(); - Assert.Null(_channel.BasicGet(queue, true)); - _channel.ExchangeDelete("src"); - _channel.ExchangeDelete("dest"); + // TODO LRB rabbitmq/rabbitmq-dotnet-client#1347 + // THIS DOES NOT WORK + // Assert.Null(await _channel.BasicGetAsync(queue, true)); + + await _channel.ExchangeDeleteAsync("src", false); + await _channel.ExchangeDeleteAsync("dest", false); } } } diff --git a/projects/Test/AsyncIntegration/TestFloodPublishingAsync.cs b/projects/Test/AsyncIntegration/TestFloodPublishingAsync.cs new file mode 100644 index 0000000000..cf10ac31c1 --- /dev/null +++ b/projects/Test/AsyncIntegration/TestFloodPublishingAsync.cs @@ -0,0 +1,213 @@ +// This source code is dual-licensed under the Apache License, version +// 2.0, and the Mozilla Public License, version 2.0. +// +// The APL v2.0: +// +//--------------------------------------------------------------------------- +// Copyright (c) 2007-2020 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//--------------------------------------------------------------------------- +// +// The MPL v2.0: +// +//--------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// +// Copyright (c) 2007-2020 VMware, Inc. All rights reserved. +//--------------------------------------------------------------------------- + +using System; +using System.Diagnostics; +using System.Threading; +using System.Threading.Tasks; +using RabbitMQ.Client; +using RabbitMQ.Client.Events; +using Xunit; +using Xunit.Abstractions; + +namespace Test.AsyncIntegration +{ + public class TestFloodPublishingAsync : AsyncIntegrationFixture + { + private static readonly TimeSpan TenSeconds = TimeSpan.FromSeconds(10); + private readonly byte[] _body = new byte[2048]; + + public TestFloodPublishingAsync(ITestOutputHelper output) : base(output) + { + } + + public override Task InitializeAsync() + { + // NB: each test sets itself up + return Task.CompletedTask; + } + + [Fact] + public async Task TestUnthrottledFloodPublishingAsync() + { + _connFactory = CreateConnectionFactory(); + _connFactory.RequestedHeartbeat = TimeSpan.FromSeconds(60); + _connFactory.AutomaticRecoveryEnabled = false; + _conn = await _connFactory.CreateConnectionAsync(); + Assert.IsNotType(_conn); + _channel = await _conn.CreateChannelAsync(); + + _conn.ConnectionShutdown += (_, ea) => + { + HandleConnectionShutdown(_conn, ea, () => + { + Assert.Fail("Unexpected connection shutdown!"); + }); + }; + + _channel.ChannelShutdown += (o, ea) => + { + HandleChannelShutdown(_channel, ea, () => + { + Assert.Fail("Unexpected channel shutdown!"); + }); + }; + + var stopwatch = Stopwatch.StartNew(); + int i = 0; + try + { + for (i = 0; i < 65535 * 64; i++) + { + if (i % 65536 == 0) + { + if (stopwatch.Elapsed > TenSeconds) + { + break; + } + } + + await _channel.BasicPublishAsync(CachedString.Empty, CachedString.Empty, _body); + } + } + finally + { + stopwatch.Stop(); + } + + Assert.True(_conn.IsOpen); + } + + [Fact] + public async Task TestMultithreadFloodPublishingAsync() + { + _connFactory = CreateConnectionFactory(); + _connFactory.DispatchConsumersAsync = true; + _connFactory.AutomaticRecoveryEnabled = false; + + _conn = await _connFactory.CreateConnectionAsync(); + Assert.IsNotType(_conn); + _channel = await _conn.CreateChannelAsync(); + + string message = "Hello from test TestMultithreadFloodPublishing"; + byte[] sendBody = _encoding.GetBytes(message); + int publishCount = 4096; // TODO LRB 1024? + int receivedCount = 0; + + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + _conn.ConnectionShutdown += (o, ea) => + { + HandleConnectionShutdown(_conn, ea, () => + { + receivedCount = -1; + tcs.SetResult(false); + }); + }; + + _channel.ChannelShutdown += (o, ea) => + { + HandleChannelShutdown(_channel, ea, () => + { + receivedCount = -1; + tcs.SetResult(false); + }); + }; + + QueueDeclareOk q = await _channel.QueueDeclareAsync(queue: string.Empty, + passive: false, durable: false, exclusive: true, autoDelete: false, arguments: null); + string queueName = q.QueueName; + + Task pub = Task.Run(async () => + { + bool stop = false; + using (IChannel pubCh = await _conn.CreateChannelAsync()) + { + await pubCh.ConfirmSelectAsync(); + + pubCh.ChannelShutdown += (o, ea) => + { + HandleChannelShutdown(pubCh, ea, () => + { + stop = true; + tcs.SetResult(false); + }); + }; + + for (int i = 0; i < publishCount && false == stop; i++) + { + await pubCh.BasicPublishAsync(string.Empty, queueName, sendBody, true); + } + + await pubCh.WaitForConfirmsOrDieAsync(); + } + }); + + var cts = new CancellationTokenSource(WaitSpan); + cts.Token.Register(() => + { + tcs.TrySetResult(false); + }); + + using (IChannel consumeCh = await _conn.CreateChannelAsync()) + { + consumeCh.ChannelShutdown += (o, ea) => + { + HandleChannelShutdown(consumeCh, ea, () => + { + tcs.SetResult(false); + }); + }; + + var consumer = new AsyncEventingBasicConsumer(consumeCh); + consumer.Received += async (o, a) => + { + string receivedMessage = _encoding.GetString(a.Body); + Assert.Equal(message, receivedMessage); + if (Interlocked.Increment(ref receivedCount) == publishCount) + { + tcs.SetResult(true); + } + await Task.Yield(); + }; + + await consumeCh.BasicConsumeAsync(queue: queueName, autoAck: true, + consumerTag: string.Empty, noLocal: false, exclusive: false, + arguments: null, consumer: consumer); + + Assert.True(await tcs.Task); + } + + await pub; + Assert.Equal(publishCount, receivedCount); + } + } +} diff --git a/projects/Unit/TestMessageCount.cs b/projects/Test/AsyncIntegration/TestMessageCountAsync.cs similarity index 79% rename from projects/Unit/TestMessageCount.cs rename to projects/Test/AsyncIntegration/TestMessageCountAsync.cs index ccee33b724..55417d0732 100644 --- a/projects/Unit/TestMessageCount.cs +++ b/projects/Test/AsyncIntegration/TestMessageCountAsync.cs @@ -30,26 +30,27 @@ //--------------------------------------------------------------------------- using System.Threading.Tasks; +using RabbitMQ.Client; using Xunit; using Xunit.Abstractions; -namespace RabbitMQ.Client.Unit +namespace Test.AsyncIntegration { - public class TestMessageCount : IntegrationFixture + public class TestMessageCountAsync : AsyncIntegrationFixture { - public TestMessageCount(ITestOutputHelper output) : base(output) + public TestMessageCountAsync(ITestOutputHelper output) : base(output) { } [Fact] public async Task TestMessageCountMethod() { - _channel.ConfirmSelect(); + await _channel.ConfirmSelectAsync(); string q = GenerateQueueName(); - _channel.QueueDeclare(queue: q, durable: false, exclusive: true, autoDelete: false, arguments: null); + await _channel.QueueDeclareAsync(queue: q, passive: false, durable: false, exclusive: true, autoDelete: false, arguments: null); Assert.Equal(0u, _channel.MessageCount(q)); - _channel.BasicPublish("", q, _encoding.GetBytes("msg")); + await _channel.BasicPublishAsync("", q, _encoding.GetBytes("msg")); await _channel.WaitForConfirmsAsync(); Assert.Equal(1u, _channel.MessageCount(q)); } diff --git a/projects/Test/AsyncIntegration/TestPassiveDeclareAsync.cs b/projects/Test/AsyncIntegration/TestPassiveDeclareAsync.cs new file mode 100644 index 0000000000..6d4f85f156 --- /dev/null +++ b/projects/Test/AsyncIntegration/TestPassiveDeclareAsync.cs @@ -0,0 +1,67 @@ +// This source code is dual-licensed under the Apache License, version +// 2.0, and the Mozilla Public License, version 2.0. +// +// The APL v2.0: +// +//--------------------------------------------------------------------------- +// Copyright (c) 2007-2020 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//--------------------------------------------------------------------------- +// +// The MPL v2.0: +// +//--------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// +// Copyright (c) 2007-2020 VMware, Inc. All rights reserved. +//--------------------------------------------------------------------------- + +using System; +using System.Threading.Tasks; +using RabbitMQ.Client; +using RabbitMQ.Client.Exceptions; +using Xunit; +using Xunit.Abstractions; + +namespace Test.AsyncIntegration +{ + public class TestPassiveDeclareAsync : AsyncIntegrationFixture + { + public TestPassiveDeclareAsync(ITestOutputHelper output) : base(output) + { + } + + [Fact] + public Task TestPassiveExchangeDeclareWhenExchangeDoesNotExist() + { + return Assert.ThrowsAsync(() => + { + var r = _channel.ExchangeDeclareAsync(Guid.NewGuid().ToString(), ExchangeType.Fanout, true, false, false, null); + return r.AsTask(); + }); + } + + [Fact] + public Task TestPassiveQueueDeclareWhenQueueDoesNotExist() + { + return Assert.ThrowsAsync(() => + { + var r = _channel.QueueDeclareAsync(Guid.NewGuid().ToString(), true, false, false, false, null); + return r.AsTask(); + }); + } + } +} diff --git a/projects/Unit/TestPublishSharedChannel.cs b/projects/Test/AsyncIntegration/TestPublishSharedChannelAsync.cs similarity index 54% rename from projects/Unit/TestPublishSharedChannel.cs rename to projects/Test/AsyncIntegration/TestPublishSharedChannelAsync.cs index aab3f9db4c..eb25891339 100644 --- a/projects/Unit/TestPublishSharedChannel.cs +++ b/projects/Test/AsyncIntegration/TestPublishSharedChannelAsync.cs @@ -30,14 +30,14 @@ //--------------------------------------------------------------------------- using System; -using System.Threading; using System.Threading.Tasks; +using RabbitMQ.Client; using Xunit; +using Xunit.Abstractions; -namespace RabbitMQ.Client.Unit +namespace Test.AsyncIntegration { - [Collection("IntegrationFixture")] - public class TestPublishSharedChannel + public class TestPublishSharedChannelAsync : AsyncIntegrationFixture { private const string QueueName = "TestPublishSharedChannel_Queue"; private static readonly CachedString ExchangeName = new CachedString("TestPublishSharedChannel_Ex"); @@ -49,62 +49,60 @@ public class TestPublishSharedChannel private Exception _raisedException; + public TestPublishSharedChannelAsync(ITestOutputHelper output) : base(output) + { + } + + public override Task InitializeAsync() + { + // NB: test sets up its own factory, conns, channels + Assert.Null(_connFactory); + Assert.Null(_conn); + Assert.Null(_channel); + return Task.CompletedTask; + } + + public override Task DisposeAsync() + { + return Task.CompletedTask; + } + [Fact] public async Task MultiThreadPublishOnSharedChannel() { - // Arrange - var connFactory = new ConnectionFactory - { - RequestedHeartbeat = TimeSpan.FromSeconds(60), - AutomaticRecoveryEnabled = false - }; + var cf = CreateConnectionFactory(); + cf.AutomaticRecoveryEnabled = false; - using (IConnection conn = connFactory.CreateConnection()) + using (IConnection conn = await cf.CreateConnectionAsync()) { - conn.ConnectionShutdown += (_, args) => - { - if (args.Initiator != ShutdownInitiator.Application) - { - Assert.Fail("Unexpected connection shutdown!"); - } - }; + Assert.IsNotType(conn); + conn.ConnectionShutdown += HandleConnectionShutdown; - using (IChannel channel = conn.CreateChannel()) + using (IChannel channel = await conn.CreateChannelAsync()) { - channel.ExchangeDeclare(ExchangeName.Value, "topic", durable: false, autoDelete: true); - channel.QueueDeclare(QueueName, false, false, true, null); - channel.QueueBind(QueueName, ExchangeName.Value, PublishKey.Value, null); - - // Act - var pubTask = Task.Run(() => NewFunction(channel)); - var pubTask2 = Task.Run(() => NewFunction(channel)); + channel.ChannelShutdown += HandleChannelShutdown; + await channel.ExchangeDeclareAsync(ExchangeName.Value, ExchangeType.Topic, passive: false, durable: false, autoDelete: true, arguments: null); + await channel.QueueDeclareAsync(QueueName, false, false, false, true, null); + await channel.QueueBindAsync(QueueName, ExchangeName.Value, PublishKey.Value, null); - await Task.WhenAll(pubTask, pubTask2); - } - } - - // Assert - Assert.Null(_raisedException); - - void NewFunction(IChannel channel) - { - try - { - for (int i = 0; i < Loops; i++) + try { - for (int j = 0; j < Repeats; j++) + for (int i = 0; i < Loops; i++) { - channel.BasicPublish(ExchangeName, PublishKey, _body, false); + for (int j = 0; j < Repeats; j++) + { + await channel.BasicPublishAsync(ExchangeName, PublishKey, _body, false); + } } - - Thread.Sleep(1); } - } - catch (Exception e) - { - _raisedException = e; + catch (Exception e) + { + _raisedException = e; + } } } + + Assert.Null(_raisedException); } } } diff --git a/projects/Unit/TestPublisherConfirms.cs b/projects/Test/AsyncIntegration/TestPublisherConfirmsAsync.cs similarity index 62% rename from projects/Unit/TestPublisherConfirms.cs rename to projects/Test/AsyncIntegration/TestPublisherConfirmsAsync.cs index 4f910b8784..97a6360c24 100644 --- a/projects/Unit/TestPublisherConfirms.cs +++ b/projects/Test/AsyncIntegration/TestPublisherConfirmsAsync.cs @@ -33,40 +33,37 @@ using System.Reflection; using System.Threading; using System.Threading.Tasks; +using RabbitMQ.Client; using RabbitMQ.Client.Impl; using Xunit; using Xunit.Abstractions; -namespace RabbitMQ.Client.Unit +// TODO rabbitmq/rabbitmq-dotnet-client#1347 +// Test mixed sync/async with WaitForConfirmsAsync +namespace Test.AsyncIntegration { - public class TestPublisherConfirms : IntegrationFixture + public class TestPublisherConfirmsAsync : AsyncIntegrationFixture { - private const string QueueName = "RabbitMQ.Client.Unit.TestPublisherConfirms"; - private readonly byte[] _body = new byte[4096]; + private readonly byte[] _messageBody; - public TestPublisherConfirms(ITestOutputHelper output) : base(output) + public TestPublisherConfirmsAsync(ITestOutputHelper output) : base(output) { -#if NET6_0_OR_GREATER - Random.Shared.NextBytes(_body); -#else - var rnd = new Random(); - rnd.NextBytes(_body); -#endif + _messageBody = GetRandomBody(4096); } [Fact] - public void TestWaitForConfirmsWithoutTimeout() + public Task TestWaitForConfirmsWithoutTimeoutAsync() { - TestWaitForConfirms(200, async (ch) => + return TestWaitForConfirmsAsync(200, async (ch) => { Assert.True(await ch.WaitForConfirmsAsync()); }); } [Fact] - public void TestWaitForConfirmsWithTimeout() + public Task TestWaitForConfirmsWithTimeout() { - TestWaitForConfirms(200, async (ch) => + return TestWaitForConfirmsAsync(200, async (ch) => { using (var cts = new CancellationTokenSource(TimeSpan.FromSeconds(4))) { @@ -76,12 +73,12 @@ public void TestWaitForConfirmsWithTimeout() } [Fact] - public void TestWaitForConfirmsWithTimeout_MightThrowTaskCanceledException() + public async Task TestWaitForConfirmsWithTimeoutAsync_MightThrowTaskCanceledException() { bool waitResult = false; - bool sawTaskCanceled = false; + bool sawException = false; - TestWaitForConfirms(10000, async (ch) => + Task t = TestWaitForConfirmsAsync(10000, async (ch) => { using (var cts = new CancellationTokenSource(TimeSpan.FromMilliseconds(1))) { @@ -89,23 +86,25 @@ public void TestWaitForConfirmsWithTimeout_MightThrowTaskCanceledException() { waitResult = await ch.WaitForConfirmsAsync(cts.Token); } - catch (TaskCanceledException) + catch { - sawTaskCanceled = true; + sawException = true; } } }); - if (waitResult == false && sawTaskCanceled == false) + await t; + + if (waitResult == false && sawException == false) { - Assert.Fail("test failed, both waitResult and sawTaskCanceled are still false"); + Assert.Fail("test failed, both waitResult and sawException are still false"); } } [Fact] - public void TestWaitForConfirmsWithTimeout_MessageNacked_WaitingHasTimedout_ReturnFalse() + public Task TestWaitForConfirmsWithTimeoutAsync_MessageNacked_WaitingHasTimedout_ReturnFalse() { - TestWaitForConfirms(2000, async (ch) => + return TestWaitForConfirmsAsync(2000, async (ch) => { IChannel actualChannel = ((AutorecoveringChannel)ch).InnerChannel; actualChannel @@ -121,13 +120,14 @@ public void TestWaitForConfirmsWithTimeout_MessageNacked_WaitingHasTimedout_Retu } [Fact] - public async Task TestWaitForConfirmsWithEvents() + public async Task TestWaitForConfirmsWithEventsAsync() { - using (IChannel ch = _conn.CreateChannel()) + string queueName = string.Format("{0}:{1}", _testDisplayName, Guid.NewGuid()); + using (IChannel ch = await _conn.CreateChannelAsync()) { - ch.ConfirmSelect(); + await ch.ConfirmSelectAsync(); + await ch.QueueDeclareAsync(queue: queueName, passive: false, durable: false, exclusive: false, autoDelete: false, arguments: null); - ch.QueueDeclare(QueueName); int n = 200; // number of event handler invocations int c = 0; @@ -136,12 +136,14 @@ public async Task TestWaitForConfirmsWithEvents() { Interlocked.Increment(ref c); }; + try { for (int i = 0; i < n; i++) { - ch.BasicPublish("", QueueName, _encoding.GetBytes("msg")); + await ch.BasicPublishAsync("", queueName, _encoding.GetBytes("msg")); } + await ch.WaitForConfirmsAsync(); // Note: number of event invocations is not guaranteed @@ -152,32 +154,33 @@ public async Task TestWaitForConfirmsWithEvents() } finally { - ch.QueueDelete(QueueName); + await ch.QueueDeleteAsync(queue: queueName, ifUnused: false, ifEmpty: false); } } } - protected void TestWaitForConfirms(int numberOfMessagesToPublish, Action fn) + private async Task TestWaitForConfirmsAsync(int numberOfMessagesToPublish, Func fn) { - using (IChannel ch = _conn.CreateChannel()) + string queueName = string.Format("{0}:{1}", _testDisplayName, Guid.NewGuid()); + using (IChannel ch = await _conn.CreateChannelAsync()) { var props = new BasicProperties { Persistent = true }; - ch.ConfirmSelect(); - ch.QueueDeclare(QueueName); + await ch.ConfirmSelectAsync(); + await ch.QueueDeclareAsync(queue: queueName, passive: false, durable: false, exclusive: false, autoDelete: false, arguments: null); for (int i = 0; i < numberOfMessagesToPublish; i++) { - ch.BasicPublish(exchange: "", routingKey: QueueName, body: _body, mandatory: true, basicProperties: props); + await ch.BasicPublishAsync(exchange: string.Empty, routingKey: queueName, body: _messageBody, mandatory: true, basicProperties: props); } try { - fn(ch); + await fn(ch); } finally { - ch.QueueDelete(QueueName); + await ch.QueueDeleteAsync(queue: queueName, ifUnused: false, ifEmpty: false); } } } diff --git a/projects/Test/AsyncIntegration/TestQueueDeclareAsync.cs b/projects/Test/AsyncIntegration/TestQueueDeclareAsync.cs new file mode 100644 index 0000000000..da7607f43e --- /dev/null +++ b/projects/Test/AsyncIntegration/TestQueueDeclareAsync.cs @@ -0,0 +1,147 @@ +// This source code is dual-licensed under the Apache License, version +// 2.0, and the Mozilla Public License, version 2.0. +// +// The APL v2.0: +// +//--------------------------------------------------------------------------- +// Copyright (c) 2007-2020 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//--------------------------------------------------------------------------- +// +// The MPL v2.0: +// +//--------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// +// Copyright (c) 2007-2020 VMware, Inc. All rights reserved. +//--------------------------------------------------------------------------- + +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Threading.Tasks; +using RabbitMQ.Client; +using Xunit; +using Xunit.Abstractions; + +namespace Test.AsyncIntegration +{ + public class TestQueueDeclareAsync : AsyncIntegrationFixture + { + public TestQueueDeclareAsync(ITestOutputHelper output) : base(output) + { + } + + [Fact] + public async void TestQueueDeclare() + { + string q = GenerateQueueName(); + + QueueDeclareOk declareResult = await _channel.QueueDeclareAsync(q, passive: false, false, false, false, null); + Assert.Equal(q, declareResult.QueueName); + + QueueDeclareOk passiveDeclareResult = await _channel.QueueDeclareAsync(q, passive: true, false, false, false, null); + Assert.Equal(q, passiveDeclareResult.QueueName); + } + + [Fact] + public async void TestConcurrentQueueDeclareAndBindAsync() + { + bool sawShutdown = false; + + _conn.ConnectionShutdown += (o, ea) => + { + if (ea.Initiator == ShutdownInitiator.Peer) + { + sawShutdown = true; + } + }; + + _channel.ChannelShutdown += (o, ea) => + { + HandleChannelShutdown(_channel, ea, () => + { + if (ea.Initiator == ShutdownInitiator.Peer) + { + sawShutdown = true; + } + }); + }; + + var ts = new List(); + var qs = new ConcurrentBag(); + + NotSupportedException nse = null; + for (int i = 0; i < 256; i++) + { + async Task f() + { + try + { + // sleep for a random amount of time to increase the chances + // of thread interleaving. MK. + await Task.Delay(S_Random.Next(5, 50)); + QueueDeclareOk r = await _channel.QueueDeclareAsync(queue: string.Empty, passive: false, false, false, false, null); + string queueName = r.QueueName; + await _channel.QueueBindAsync(queue: queueName, exchange: "amq.fanout", routingKey: queueName, null); + qs.Add(queueName); + } + catch (NotSupportedException e) + { + nse = e; + } + } + var t = Task.Run(f); + ts.Add(t); + } + + await Task.WhenAll(ts); + Assert.Null(nse); + ts.Clear(); + + nse = null; + foreach (string q in qs) + { + async Task f() + { + string qname = q; + try + { + await Task.Delay(S_Random.Next(5, 50)); + + QueueDeclareOk r = await _channel.QueueDeclareAsync(qname, passive: true, false, false, false, null); + Assert.Equal(qname, r.QueueName); + + await _channel.QueueUnbindAsync(queue: qname, exchange: "amq.fanout", routingKey: qname, null); + + uint deletedMessageCount = await _channel.QueueDeleteAsync(qname, false, false); + Assert.Equal((uint)0, deletedMessageCount); + } + catch (NotSupportedException e) + { + nse = e; + } + } + var t = Task.Run(f); + ts.Add(t); + } + + await Task.WhenAll(ts); + Assert.Null(nse); + Assert.False(sawShutdown); + } + } +} diff --git a/projects/Test/Common/Common.csproj b/projects/Test/Common/Common.csproj new file mode 100644 index 0000000000..215713acfa --- /dev/null +++ b/projects/Test/Common/Common.csproj @@ -0,0 +1,38 @@ + + + + net6.0;net472 + + + + net6.0 + + + + ../../rabbit.snk + true + latest + 7.0 + false + + + + + + <_Parameter1>Integration, PublicKey=00240000048000009400000006020000002400005253413100040000010001008d20ec856aeeb8c3153a77faa2d80e6e43b5db93224a20cc7ae384f65f142e89730e2ff0fcc5d578bbe96fa98a7196c77329efdee4579b3814c0789e5a39b51df6edd75b602a33ceabdfcf19a3feb832f31d8254168cd7ba5700dfbca301fbf8db614ba41ba18474de0a5f4c2d51c995bc3636c641c8cbe76f45717bfcb943b5 + + + <_Parameter1>SequentialIntegration, PublicKey=00240000048000009400000006020000002400005253413100040000010001008d20ec856aeeb8c3153a77faa2d80e6e43b5db93224a20cc7ae384f65f142e89730e2ff0fcc5d578bbe96fa98a7196c77329efdee4579b3814c0789e5a39b51df6edd75b602a33ceabdfcf19a3feb832f31d8254168cd7ba5700dfbca301fbf8db614ba41ba18474de0a5f4c2d51c995bc3636c641c8cbe76f45717bfcb943b5 + + + + + + + + + + + + + diff --git a/projects/Test/Common/IntegrationFixture.cs b/projects/Test/Common/IntegrationFixture.cs new file mode 100644 index 0000000000..97745bf938 --- /dev/null +++ b/projects/Test/Common/IntegrationFixture.cs @@ -0,0 +1,365 @@ +// This source code is dual-licensed under the Apache License, version +// 2.0, and the Mozilla Public License, version 2.0. +// +// The APL v2.0: +// +//--------------------------------------------------------------------------- +// Copyright (c) 2007-2020 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//--------------------------------------------------------------------------- +// +// The MPL v2.0: +// +//--------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// +// Copyright (c) 2007-2020 VMware, Inc. All rights reserved. +//--------------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Reflection; +using System.Text; +using System.Threading; +using RabbitMQ.Client; +using RabbitMQ.Client.Framing.Impl; +using Xunit; +using Xunit.Abstractions; + +namespace Test +{ + public class IntegrationFixture : IDisposable + { + private static bool s_isRunningInCI = false; + private static bool s_isWindows = false; + private static int _connectionIdx = 0; + + protected readonly RabbitMQCtl _rabbitMQCtl; + + protected ConnectionFactory _connFactory; + protected IConnection _conn; + protected IChannel _channel; + + protected static readonly Encoding _encoding = new UTF8Encoding(); + protected static readonly int _processorCount = Environment.ProcessorCount; + + protected readonly ITestOutputHelper _output; + protected readonly string _testDisplayName; + + public static readonly TimeSpan WaitSpan; + public static readonly TimeSpan LongWaitSpan; + public static readonly TimeSpan RecoveryInterval = TimeSpan.FromSeconds(2); + public static readonly TimeSpan TestTimeout = TimeSpan.FromSeconds(5); + public static readonly Random S_Random; + + static IntegrationFixture() + { + S_Random = new Random(); + InitIsRunningInCI(); + InitIsWindows(); + + int threadCount; + if (s_isRunningInCI) + { + threadCount = _processorCount * 16; + WaitSpan = TimeSpan.FromSeconds(60); + LongWaitSpan = TimeSpan.FromSeconds(120); + } + else + { + // Assuming that dev machines have more cores + threadCount = _processorCount * 8; + WaitSpan = TimeSpan.FromSeconds(30); + LongWaitSpan = TimeSpan.FromSeconds(60); + } + + ThreadPool.SetMinThreads(threadCount, threadCount); + } + + public IntegrationFixture(ITestOutputHelper output) + { + _output = output; + _rabbitMQCtl = new RabbitMQCtl(_output); + + var type = _output.GetType(); + var testMember = type.GetField("test", BindingFlags.Instance | BindingFlags.NonPublic); + var test = (ITest)testMember.GetValue(output); + _testDisplayName = test.DisplayName + .Replace("Test.", string.Empty) + .Replace("AsyncIntegration.", "AI.") + .Replace("Integration.", "I.") + .Replace("SequentialIntegration.", "SI."); + + SetUp(); + } + + protected virtual void SetUp() + { + if (_connFactory == null) + { + _connFactory = CreateConnectionFactory(); + } + + if (_conn == null) + { + _conn = _connFactory.CreateConnection(); + _channel = _conn.CreateChannel(); + } + } + + public virtual void Dispose() + { + if (_channel != null) + { + _channel.Dispose(); + } + + if (_conn != null) + { + _conn.Dispose(); + } + + TearDown(); + } + + protected virtual void TearDown() + { + } + + protected bool IsRunningInCI + { + get { return s_isRunningInCI; } + } + + protected bool IsWindows + { + get { return s_isWindows; } + } + + internal AutorecoveringConnection CreateAutorecoveringConnection(IList hostnames) + { + return CreateAutorecoveringConnection(RecoveryInterval, hostnames); + } + + internal AutorecoveringConnection CreateAutorecoveringConnection(TimeSpan interval, IList hostnames) + { + var cf = CreateConnectionFactory(); + cf.AutomaticRecoveryEnabled = true; + // tests that use this helper will likely list unreachable hosts; + // make sure we time out quickly on those + cf.RequestedConnectionTimeout = TimeSpan.FromSeconds(1); + cf.NetworkRecoveryInterval = interval; + return (AutorecoveringConnection)cf.CreateConnection(hostnames); + } + + protected void WithTemporaryChannel(Action action) + { + IChannel channel = _conn.CreateChannel(); + + try + { + action(channel); + } + finally + { + channel.Abort(); + } + } + + protected string GenerateExchangeName() + { + return $"{_testDisplayName}-exchange-{Guid.NewGuid()}"; + } + + protected string GenerateQueueName() + { + return $"{_testDisplayName}-queue-{Guid.NewGuid()}"; + } + + protected void WithTemporaryNonExclusiveQueue(Action action) + { + WithTemporaryNonExclusiveQueue(_channel, action); + } + + protected void WithTemporaryNonExclusiveQueue(IChannel channel, Action action) + { + WithTemporaryNonExclusiveQueue(channel, action, GenerateQueueName()); + } + + protected void WithTemporaryNonExclusiveQueue(IChannel channel, Action action, string queue) + { + try + { + channel.QueueDeclare(queue, false, false, false, null); + action(channel, queue); + } + finally + { + WithTemporaryChannel(tm => tm.QueueDelete(queue)); + } + } + + protected void AssertMessageCount(string q, uint count) + { + WithTemporaryChannel((m) => + { + RabbitMQ.Client.QueueDeclareOk ok = m.QueueDeclarePassive(q); + Assert.Equal(count, ok.MessageCount); + }); + } + + protected void AssertShutdownError(ShutdownEventArgs args, int code) + { + Assert.Equal(args.ReplyCode, code); + } + + protected void AssertPreconditionFailed(ShutdownEventArgs args) + { + AssertShutdownError(args, Constants.PreconditionFailed); + } + + protected void WaitOn(object o) + { + lock (o) + { + Monitor.Wait(o, TimingFixture.TestTimeout); + } + } + + protected void Wait(ManualResetEventSlim latch, string desc) + { + Assert.True(latch.Wait(WaitSpan), + $"waiting {WaitSpan.TotalSeconds} seconds on a latch for '{desc}' timed out"); + } + + protected void Wait(ManualResetEventSlim latch, TimeSpan timeSpan, string desc) + { + Assert.True(latch.Wait(timeSpan), + $"waiting {timeSpan.TotalSeconds} seconds on a latch for '{desc}' timed out"); + } + + protected ConnectionFactory CreateConnectionFactory() + { + string now = DateTime.UtcNow.ToString("o", CultureInfo.InvariantCulture); + return new ConnectionFactory + { + ClientProvidedName = $"{_testDisplayName}:{now}:{GetConnectionIdx()}", + ContinuationTimeout = WaitSpan, + HandshakeContinuationTimeout = WaitSpan, + }; + } + + protected void HandleConnectionShutdown(object sender, ShutdownEventArgs args) + { + if (args.Initiator != ShutdownInitiator.Application) + { + IConnection conn = (IConnection)sender; + _output.WriteLine($"{_testDisplayName} connection {conn.ClientProvidedName} shut down: {args}"); + } + } + + protected void HandleConnectionShutdown(IConnection conn, ShutdownEventArgs args, Action a) + { + if (args.Initiator != ShutdownInitiator.Application) + { + _output.WriteLine($"{_testDisplayName} connection {conn.ClientProvidedName} shut down: {args}"); + a(); + } + } + + protected void HandleChannelShutdown(object sender, ShutdownEventArgs args) + { + if (args.Initiator != ShutdownInitiator.Application) + { + IChannel ch = (IChannel)sender; + _output.WriteLine($"{_testDisplayName} channel {ch.ChannelNumber} shut down: {args}"); + } + } + + protected void HandleChannelShutdown(IChannel ch, ShutdownEventArgs args, Action a) + { + if (args.Initiator != ShutdownInitiator.Application) + { + _output.WriteLine($"{_testDisplayName} channel {ch.ChannelNumber} shut down: {args}"); + a(); + } + } + + private static void InitIsRunningInCI() + { + bool ci; + if (bool.TryParse(Environment.GetEnvironmentVariable("CI"), out ci)) + { + if (ci == true) + { + s_isRunningInCI = true; + } + } + else if (bool.TryParse(Environment.GetEnvironmentVariable("GITHUB_ACTIONS"), out ci)) + { + if (ci == true) + { + s_isRunningInCI = true; + } + } + else + { + s_isRunningInCI = false; + } + } + + private static void InitIsWindows() + { + PlatformID platform = Environment.OSVersion.Platform; + if (platform == PlatformID.Win32NT) + { + s_isWindows = true; + return; + } + + string os = Environment.GetEnvironmentVariable("OS"); + if (os != null) + { + os = os.Trim(); + s_isWindows = os == "Windows_NT"; + return; + } + } + + private static int GetConnectionIdx() + { + return Interlocked.Increment(ref _connectionIdx); + } + + protected static string GetUniqueString(ushort length) + { + byte[] bytes = GetRandomBody(length); + return Convert.ToBase64String(bytes); + } + + protected static byte[] GetRandomBody(ushort size) + { + var body = new byte[size]; +#if NET6_0_OR_GREATER + Random.Shared.NextBytes(body); +#else + S_Random.NextBytes(body); +#endif + return body; + } + } +} diff --git a/projects/Unit/RabbitMQCtl.cs b/projects/Test/Common/RabbitMQCtl.cs similarity index 67% rename from projects/Unit/RabbitMQCtl.cs rename to projects/Test/Common/RabbitMQCtl.cs index 7a12e97f51..8faf25ba39 100644 --- a/projects/Unit/RabbitMQCtl.cs +++ b/projects/Test/Common/RabbitMQCtl.cs @@ -29,72 +29,56 @@ // Copyright (c) 2007-2020 VMware, Inc. All rights reserved. //--------------------------------------------------------------------------- -#pragma warning disable 2002 - using System; using System.Diagnostics; using System.IO; using System.Text; using System.Text.RegularExpressions; -using System.Threading; +using RabbitMQ.Client; +using Xunit.Abstractions; -namespace RabbitMQ.Client.Unit +namespace Test { -#nullable enable - public static class RabbitMQCtl + public class RabbitMQCtl { private static readonly char[] newLine = new char[] { '\n' }; private static readonly Func s_invokeRabbitMqCtl = GetRabbitMqCtlInvokeAction(); + private static readonly Regex s_getConnectionProperties = + new Regex(@"^(?<[^>]*>)\s\[.*""connection_name"",""(?[^""]*)"".*\]$", RegexOptions.Multiline | RegexOptions.Compiled); - private static Func GetRabbitMqCtlInvokeAction() - { - string precomputedArguments; - string? envVariable = Environment.GetEnvironmentVariable("RABBITMQ_RABBITMQCTL_PATH"); + private readonly ITestOutputHelper _output; - if (!string.IsNullOrWhiteSpace(envVariable)) - { - const string DockerPrefix = "DOCKER:"; - if (envVariable.StartsWith(DockerPrefix)) - { - // Call docker - precomputedArguments = $"exec {envVariable.Substring(DockerPrefix.Length)} rabbitmqctl "; - return args => CreateProcess("docker", precomputedArguments + args); - } - - // call the path from the env var - return args => CreateProcess(envVariable, args); - } + public RabbitMQCtl(ITestOutputHelper output) + { + _output = output; + } - // Try default - string umbrellaRabbitmqctlPath; - string providedRabbitmqctlPath; + public void CloseConnection(IConnection conn) + { + CloseConnection(GetConnectionPid(conn.ClientProvidedName)); + } - if (IsRunningOnMonoOrDotNetCore()) - { - umbrellaRabbitmqctlPath = "../../../../../../rabbit/scripts/rabbitmqctl"; - providedRabbitmqctlPath = "rabbitmqctl"; - } - else - { - umbrellaRabbitmqctlPath = @"..\..\..\..\..\..\rabbit\scripts\rabbitmqctl.bat"; - providedRabbitmqctlPath = "rabbitmqctl.bat"; - } + public void AddUser(string username, string password) + { + ExecRabbitMQCtl($"add_user {username} {password}"); + } - string path = File.Exists(umbrellaRabbitmqctlPath) ? umbrellaRabbitmqctlPath : providedRabbitmqctlPath; + public void ChangePassword(string username, string password) + { + ExecRabbitMQCtl($"change_password {username} {password}"); + } - if (IsRunningOnMonoOrDotNetCore()) - { - return args => CreateProcess(path, args); - } + public void SetPermissions(string username, string conf, string write, string read) + { + ExecRabbitMQCtl($"set_permissions {username} \"{conf}\" \"{write}\" \"${read}\" "); + } - precomputedArguments = $"/c \"\"{path}\" "; - return args => CreateProcess("cmd.exe", precomputedArguments + args); + public void DeleteUser(string username) + { + ExecRabbitMQCtl($"delete_user {username}"); } - // - // Shelling Out - // - private static string ExecRabbitMQCtl(string args) + public string ExecRabbitMQCtl(string args) { try { @@ -118,66 +102,61 @@ private static string ExecRabbitMQCtl(string args) } } - private static Process CreateProcess(string cmd, string arguments, string? workDirectory = null) + private void ReportExecFailure(string cmd, string args, string msg) { - return new Process - { - StartInfo = - { - CreateNoWindow = true, - UseShellExecute = false, - RedirectStandardError = true, - RedirectStandardOutput = true, - FileName = cmd, - Arguments = arguments, - WorkingDirectory = workDirectory - } - }; + _output.WriteLine($"Failure while running {cmd} {args}:\n{msg}"); } - private static void ReportExecFailure(string cmd, string args, string msg) + private static Func GetRabbitMqCtlInvokeAction() { - Console.WriteLine($"Failure while running {cmd} {args}:\n{msg}"); - } + string precomputedArguments; + string envVariable = Environment.GetEnvironmentVariable("RABBITMQ_RABBITMQCTL_PATH"); - private static bool IsRunningOnMonoOrDotNetCore() - { -#if NETCOREAPP - return true; -#else - return Type.GetType("Mono.Runtime") != null; -#endif - } + if (!string.IsNullOrWhiteSpace(envVariable)) + { + const string DockerPrefix = "DOCKER:"; + if (envVariable.StartsWith(DockerPrefix)) + { + // Call docker + precomputedArguments = $"exec {envVariable.Substring(DockerPrefix.Length)} rabbitmqctl "; + return args => CreateProcess("docker", precomputedArguments + args); + } + else + { + // call the path from the env var + return args => CreateProcess(envVariable, args); + } + } - // - // Flow Control - // - public static void Block(IConnection conn, Encoding encoding) - { - ExecRabbitMQCtl("set_vm_memory_high_watermark 0.000000001"); - // give rabbitmqctl some time to do its job - Thread.Sleep(1200); - Publish(conn, encoding); - } + // Try default + string umbrellaRabbitmqctlPath; + string providedRabbitmqctlPath; - public static void Publish(IConnection conn, Encoding encoding) - { - IChannel ch = conn.CreateChannel(); - ch.BasicPublish("amq.fanout", "", encoding.GetBytes("message")); - } + if (IsRunningOnMonoOrDotNetCore()) + { + umbrellaRabbitmqctlPath = "../../../../../../rabbit/scripts/rabbitmqctl"; + providedRabbitmqctlPath = "rabbitmqctl"; + } + else + { + umbrellaRabbitmqctlPath = @"..\..\..\..\..\..\rabbit\scripts\rabbitmqctl.bat"; + providedRabbitmqctlPath = "rabbitmqctl.bat"; + } - public static void Unblock() - { - ExecRabbitMQCtl("set_vm_memory_high_watermark 0.4"); - } + string path = File.Exists(umbrellaRabbitmqctlPath) ? umbrellaRabbitmqctlPath : providedRabbitmqctlPath; - public static void CloseConnection(IConnection conn) - { - CloseConnection(GetConnectionPid(conn.ClientProvidedName)); + if (IsRunningOnMonoOrDotNetCore()) + { + return args => CreateProcess(path, args); + } + else + { + precomputedArguments = $"/c \"\"{path}\" "; + return args => CreateProcess("cmd.exe", precomputedArguments + args); + } } - private static readonly Regex s_getConnectionProperties = new Regex(@"^(?<[^>]*>)\s\[.*""connection_name"",""(?[^""]*)"".*\]$", RegexOptions.Multiline | RegexOptions.Compiled); - private static string GetConnectionPid(string connectionName) + private string GetConnectionPid(string connectionName) { string stdout = ExecRabbitMQCtl("list_connections --silent pid client_properties"); @@ -195,65 +174,41 @@ private static string GetConnectionPid(string connectionName) throw new Exception($"No connection found with name: {connectionName}"); } - private static void CloseConnection(string pid) + private void CloseConnection(string pid) { ExecRabbitMQCtl($"close_connection \"{pid}\" \"Closed via rabbitmqctl\""); } - public static void CloseAllConnections() - { - foreach (var pid in EnumerateConnectionsPid()) - { - CloseConnection(pid); - } - } - - private static string[] EnumerateConnectionsPid() - { - string rabbitmqCtlResult = ExecRabbitMQCtl("list_connections --silent pid"); - return rabbitmqCtlResult.Split(newLine, StringSplitOptions.RemoveEmptyEntries); - } - - public static void RestartRabbitMQ() - { - StopRabbitMQ(); - Thread.Sleep(500); - StartRabbitMQ(); - AwaitRabbitMQ(); - } - - public static void StopRabbitMQ() - { - ExecRabbitMQCtl("stop_app"); - } - - public static void StartRabbitMQ() - { - ExecRabbitMQCtl("start_app"); - } - - public static void AwaitRabbitMQ() - { - ExecRabbitMQCtl("await_startup"); - } - - public static void AddUser(string username, string password) - { - ExecRabbitMQCtl($"add_user {username} {password}"); - } - public static void ChangePassword(string username, string password) + private static bool IsRunningOnMonoOrDotNetCore() { - ExecRabbitMQCtl($"change_password {username} {password}"); +#if NETCOREAPP + return true; +#else + return Type.GetType("Mono.Runtime") != null; +#endif } - public static void SetPermissions(string username, string conf, string write, string read) + private static void Publish(IConnection conn, Encoding encoding) { - ExecRabbitMQCtl($"set_permissions {username} \"{conf}\" \"{write}\" \"${read}\" "); + IChannel ch = conn.CreateChannel(); + ch.BasicPublish("amq.fanout", "", encoding.GetBytes("message")); } - public static void DeleteUser(string username) + private static Process CreateProcess(string cmd, string arguments, string workDirectory = null) { - ExecRabbitMQCtl($"delete_user {username}"); + return new Process + { + StartInfo = + { + CreateNoWindow = true, + UseShellExecute = false, + RedirectStandardError = true, + RedirectStandardOutput = true, + FileName = cmd, + Arguments = arguments, + WorkingDirectory = workDirectory + } + }; } } } diff --git a/projects/RabbitMQ.Client/client/framing/BasicRecoverAsync.cs b/projects/Test/Common/TimingFixture.cs similarity index 67% rename from projects/RabbitMQ.Client/client/framing/BasicRecoverAsync.cs rename to projects/Test/Common/TimingFixture.cs index e2c76097ea..23ce3a31ce 100644 --- a/projects/RabbitMQ.Client/client/framing/BasicRecoverAsync.cs +++ b/projects/Test/Common/TimingFixture.cs @@ -30,30 +30,17 @@ //--------------------------------------------------------------------------- using System; -using RabbitMQ.Client.client.framing; -using RabbitMQ.Client.Impl; -namespace RabbitMQ.Client.Framing.Impl +namespace Test { - internal readonly struct BasicRecoverAsync : IOutgoingAmqpMethod + public static class TimingFixture { - public readonly bool _requeue; - - public BasicRecoverAsync(bool Requeue) - { - _requeue = Requeue; - } - - public ProtocolCommandId ProtocolCommandId => ProtocolCommandId.BasicRecoverAsync; - - public int WriteTo(Span span) - { - return WireFormatting.WriteBits(ref span.GetStart(), _requeue); - } - - public int GetRequiredBufferSize() - { - return 1; // bytes for bit fields - } + public static readonly TimeSpan TimingInterval = TimeSpan.FromMilliseconds(300); + public static readonly TimeSpan TimingInterval_2X = TimeSpan.FromMilliseconds(600); + public static readonly TimeSpan TimingInterval_4X = TimeSpan.FromMilliseconds(1200); + public static readonly TimeSpan TimingInterval_8X = TimeSpan.FromMilliseconds(2400); + public static readonly TimeSpan TimingInterval_16X = TimeSpan.FromMilliseconds(4800); + public static readonly TimeSpan SafetyMargin = TimeSpan.FromMilliseconds(150); + public static readonly TimeSpan TestTimeout = TimeSpan.FromSeconds(5); } } diff --git a/projects/Test/Integration/Integration.csproj b/projects/Test/Integration/Integration.csproj new file mode 100644 index 0000000000..55ae9c0e83 --- /dev/null +++ b/projects/Test/Integration/Integration.csproj @@ -0,0 +1,51 @@ + + + + net6.0;net472 + + + + net6.0 + + + + ../../rabbit.snk + true + latest + 7.0 + true + + + + + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + diff --git a/projects/Unit/SslEnv.cs b/projects/Test/Integration/SslEnv.cs similarity index 85% rename from projects/Unit/SslEnv.cs rename to projects/Test/Integration/SslEnv.cs index bc286a9d3d..272eab1d5d 100644 --- a/projects/Unit/SslEnv.cs +++ b/projects/Test/Integration/SslEnv.cs @@ -32,13 +32,13 @@ using System; using System.IO; -namespace RabbitMQ.Client.Unit +namespace Test.Integration { public class SslEnv { private readonly string _certPassphrase; private readonly string _certPath; - private readonly string _hostname; + private const string _hostname = "localhost"; private readonly string _sslDir; private readonly bool _isSslConfigured; private readonly bool _isGithubActions; @@ -48,21 +48,14 @@ public SslEnv() _sslDir = Environment.GetEnvironmentVariable("SSL_CERTS_DIR"); _certPassphrase = Environment.GetEnvironmentVariable("PASSWORD"); - Boolean.TryParse(Environment.GetEnvironmentVariable("GITHUB_ACTIONS"), out _isGithubActions); - _isSslConfigured = Directory.Exists(_sslDir) && (false == String.IsNullOrEmpty(_certPassphrase)); - if (_isGithubActions) - { - _hostname = "localhost"; - } - else + if (_isSslConfigured) { - _hostname = System.Net.Dns.GetHostName(); + Boolean.TryParse(Environment.GetEnvironmentVariable("GITHUB_ACTIONS"), out _isGithubActions); + _certPath = Path.Combine(_sslDir, $"client_{_hostname}.p12"); } - - _certPath = Path.Combine(_sslDir, $"client_{_hostname}.p12"); } public string CertPath diff --git a/projects/Unit/TestAuth.cs b/projects/Test/Integration/TestAuth.cs similarity index 78% rename from projects/Unit/TestAuth.cs rename to projects/Test/Integration/TestAuth.cs index 8761cd35d3..790de61041 100644 --- a/projects/Unit/TestAuth.cs +++ b/projects/Test/Integration/TestAuth.cs @@ -31,20 +31,29 @@ using RabbitMQ.Client.Exceptions; using Xunit; +using Xunit.Abstractions; -namespace RabbitMQ.Client.Unit +namespace Test.Integration { - [Collection("IntegrationFixture")] - public class TestAuth + public class TestAuth : IntegrationFixture { + public TestAuth(ITestOutputHelper output) : base(output) + { + } + + protected override void SetUp() + { + Assert.Null(_connFactory); + Assert.Null(_conn); + Assert.Null(_channel); + } + [Fact] public void TestAuthFailure() { - ConnectionFactory connFactory = new ConnectionFactory - { - UserName = "guest", - Password = "incorrect-password" - }; + var connFactory = CreateConnectionFactory(); + connFactory.UserName = "guest"; + connFactory.Password = "incorrect-password"; try { diff --git a/projects/Unit/TestBasicGet.cs b/projects/Test/Integration/TestBasicGet.cs similarity index 68% rename from projects/Unit/TestBasicGet.cs rename to projects/Test/Integration/TestBasicGet.cs index 5dba9cee8d..d249a2f109 100644 --- a/projects/Unit/TestBasicGet.cs +++ b/projects/Test/Integration/TestBasicGet.cs @@ -29,11 +29,13 @@ // Copyright (c) 2007-2020 VMware, Inc. All rights reserved. //--------------------------------------------------------------------------- +using System; +using RabbitMQ.Client; using RabbitMQ.Client.Exceptions; using Xunit; using Xunit.Abstractions; -namespace RabbitMQ.Client.Unit +namespace Test.Integration { public class TestBasicGet : IntegrationFixture { @@ -70,9 +72,44 @@ public void TestBasicGetWithNonEmptyResponseAndAutoAckMode() WithNonEmptyQueue((channel, queue) => { BasicGetResult res = channel.BasicGet(queue, true); - Assert.Equal(msg, _encoding.GetString(res.Body.ToArray())); + Assert.Equal(msg, _encoding.GetString(res.Body)); AssertMessageCount(queue, 0); }, msg); } + + private void EnsureNotEmpty(string q, string body) + { + WithTemporaryChannel(x => x.BasicPublish("", q, _encoding.GetBytes(body))); + } + + private void WithClosedChannel(Action action) + { + IChannel channel = _conn.CreateChannel(); + channel.Close(); + action(channel); + } + + private void WithNonEmptyQueue(Action action) + { + WithNonEmptyQueue(action, "msg"); + } + + private void WithNonEmptyQueue(Action action, string msg) + { + WithTemporaryNonExclusiveQueue((m, q) => + { + EnsureNotEmpty(q, msg); + action(m, q); + }); + } + + private void WithEmptyQueue(Action action) + { + WithTemporaryNonExclusiveQueue((channel, queue) => + { + channel.QueuePurge(queue); + action(channel, queue); + }); + } } } diff --git a/projects/Test/Integration/TestBasicPublish.cs b/projects/Test/Integration/TestBasicPublish.cs new file mode 100644 index 0000000000..4d230cb07b --- /dev/null +++ b/projects/Test/Integration/TestBasicPublish.cs @@ -0,0 +1,294 @@ +// This source code is dual-licensed under the Apache License, version +// 2.0, and the Mozilla Public License, version 2.0. +// +// The APL v2.0: +// +//--------------------------------------------------------------------------- +// Copyright (c) 2007-2020 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//--------------------------------------------------------------------------- +// +// The MPL v2.0: +// +//--------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// +// Copyright (c) 2011-2020 VMware, Inc. or its affiliates. All rights reserved. +//--------------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Threading; +using RabbitMQ.Client; +using RabbitMQ.Client.Events; +using Xunit; +using Xunit.Abstractions; +using Xunit.Sdk; + +namespace Test.Integration +{ + public class TestBasicPublish : IntegrationFixture + { + public TestBasicPublish(ITestOutputHelper output) : base(output) + { + } + + protected override void SetUp() + { + /* + * NB: the only reason to do a custom SetUp + * is for the MaxMessageSize and QueuePurgeAsync tests, + * which use custom factory settings. This could be improved. + * TODO LRB rabbitmq/rabbitmq-server#1347 + */ + _connFactory = CreateConnectionFactory(); + Assert.Null(_conn); + Assert.Null(_channel); + } + + [Fact] + public void TestBasicRoundtripArray() + { + _conn = _connFactory.CreateConnection(); + _channel = _conn.CreateChannel(); + + QueueDeclareOk q = _channel.QueueDeclare(); + var bp = new BasicProperties(); + byte[] sendBody = _encoding.GetBytes("hi"); + byte[] consumeBody = null; + var consumer = new EventingBasicConsumer(_channel); + var are = new AutoResetEvent(false); + consumer.Received += (o, a) => + { + consumeBody = a.Body; + are.Set(); + }; + string tag = _channel.BasicConsume(q.QueueName, true, consumer); + + _channel.BasicPublish("", q.QueueName, bp, sendBody); + bool waitResFalse = are.WaitOne(5000); + _channel.BasicCancel(tag); + + Assert.True(waitResFalse); + Assert.Equal(sendBody, consumeBody); + } + + [Fact] + public void TestBasicRoundtripCachedString() + { + _conn = _connFactory.CreateConnection(); + _channel = _conn.CreateChannel(); + + CachedString exchangeName = new CachedString(string.Empty); + CachedString queueName = new CachedString(_channel.QueueDeclare().QueueName); + byte[] sendBody = _encoding.GetBytes("hi"); + byte[] consumeBody = null; + var consumer = new EventingBasicConsumer(_channel); + var are = new AutoResetEvent(false); + consumer.Received += (o, a) => + { + consumeBody = a.Body; + are.Set(); + }; + string tag = _channel.BasicConsume(queueName.Value, true, consumer); + + _channel.BasicPublish(exchangeName, queueName, sendBody); + bool waitResFalse = are.WaitOne(2000); + _channel.BasicCancel(tag); + + Assert.True(waitResFalse); + Assert.Equal(sendBody, consumeBody); + } + + [Fact] + public void TestBasicRoundtripReadOnlyMemory() + { + _conn = _connFactory.CreateConnection(); + _channel = _conn.CreateChannel(); + + QueueDeclareOk q = _channel.QueueDeclare(); + byte[] sendBody = _encoding.GetBytes("hi"); + byte[] consumeBody = null; + var consumer = new EventingBasicConsumer(_channel); + var are = new AutoResetEvent(false); + consumer.Received += (o, a) => + { + consumeBody = a.Body; + are.Set(); + }; + string tag = _channel.BasicConsume(q.QueueName, true, consumer); + + _channel.BasicPublish("", q.QueueName, new ReadOnlyMemory(sendBody)); + bool waitResFalse = are.WaitOne(2000); + _channel.BasicCancel(tag); + + Assert.True(waitResFalse); + Assert.Equal(sendBody, consumeBody); + } + + [Fact] + public void CanNotModifyPayloadAfterPublish() + { + _conn = _connFactory.CreateConnection(); + _channel = _conn.CreateChannel(); + + QueueDeclareOk q = _channel.QueueDeclare(); + byte[] sendBody = new byte[1000]; + var consumer = new EventingBasicConsumer(_channel); + var receivedMessage = new AutoResetEvent(false); + bool modified = true; + consumer.Received += (o, a) => + { + if (a.Body.Span.IndexOf((byte)1) < 0) + { + modified = false; + } + receivedMessage.Set(); + }; + string tag = _channel.BasicConsume(q.QueueName, true, consumer); + + _channel.BasicPublish("", q.QueueName, sendBody); + sendBody.AsSpan().Fill(1); + + Assert.True(receivedMessage.WaitOne(5000)); + Assert.False(modified, "Payload was modified after the return of BasicPublish"); + + _channel.BasicCancel(tag); + } + + [Fact] + public void TestMaxMessageSize() + { + var re = new ManualResetEventSlim(); + const ushort maxMsgSize = 1024; + + int count = 0; + byte[] msg0 = _encoding.GetBytes("hi"); + byte[] msg1 = GetRandomBody(maxMsgSize * 2); + + var cf = CreateConnectionFactory(); + cf.AutomaticRecoveryEnabled = false; + cf.TopologyRecoveryEnabled = false; + cf.MaxMessageSize = maxMsgSize; + + bool sawConnectionShutdown = false; + bool sawChannelShutdown = false; + bool sawConsumerRegistered = false; + bool sawConsumerCancelled = false; + + using (IConnection c = cf.CreateConnection()) + { + c.ConnectionShutdown += (o, a) => + { + sawConnectionShutdown = true; + }; + + Assert.Equal(maxMsgSize, cf.MaxMessageSize); + Assert.Equal(maxMsgSize, cf.Endpoint.MaxMessageSize); + Assert.Equal(maxMsgSize, c.Endpoint.MaxMessageSize); + + using (IChannel channel = c.CreateChannel()) + { + channel.ChannelShutdown += (o, a) => + { + sawChannelShutdown = true; + }; + + channel.CallbackException += (o, a) => + { + throw new XunitException("Unexpected channel.CallbackException"); + }; + + QueueDeclareOk q = channel.QueueDeclare(); + + var consumer = new EventingBasicConsumer(channel); + + consumer.Shutdown += (o, a) => + { + re.Set(); + }; + + consumer.Registered += (o, a) => + { + sawConsumerRegistered = true; + }; + + consumer.Unregistered += (o, a) => + { + throw new XunitException("Unexpected consumer.Unregistered"); + }; + + consumer.ConsumerCancelled += (o, a) => + { + sawConsumerCancelled = true; + }; + + consumer.Received += (o, a) => + { + Interlocked.Increment(ref count); + }; + + string tag = channel.BasicConsume(q.QueueName, true, consumer); + + channel.BasicPublish("", q.QueueName, msg0); + channel.BasicPublish("", q.QueueName, msg1); + Assert.True(re.Wait(TimeSpan.FromSeconds(5))); + + Assert.Equal(1, count); + Assert.True(sawConnectionShutdown); + Assert.True(sawChannelShutdown); + Assert.True(sawConsumerRegistered); + Assert.True(sawConsumerCancelled); + } + } + } + + [Fact] + public void TestPropertiesRoundtrip_Headers() + { + _conn = _connFactory.CreateConnection(); + _channel = _conn.CreateChannel(); + + var subject = new BasicProperties + { + Headers = new Dictionary() + }; + + QueueDeclareOk q = _channel.QueueDeclare(); + var bp = new BasicProperties() { Headers = new Dictionary() }; + bp.Headers["Hello"] = "World"; + byte[] sendBody = _encoding.GetBytes("hi"); + byte[] consumeBody = null; + var consumer = new EventingBasicConsumer(_channel); + var are = new AutoResetEvent(false); + string response = null; + consumer.Received += (o, a) => + { + response = _encoding.GetString(a.BasicProperties.Headers["Hello"] as byte[]); + consumeBody = a.Body; + are.Set(); + }; + + string tag = _channel.BasicConsume(q.QueueName, true, consumer); + _channel.BasicPublish("", q.QueueName, bp, sendBody); + bool waitResFalse = are.WaitOne(5000); + _channel.BasicCancel(tag); + Assert.True(waitResFalse); + Assert.Equal(sendBody, consumeBody); + Assert.Equal("World", response); + } + } +} diff --git a/projects/Unit/TestChannelAllocation.cs b/projects/Test/Integration/TestChannelAllocation.cs similarity index 66% rename from projects/Unit/TestChannelAllocation.cs rename to projects/Test/Integration/TestChannelAllocation.cs index 3a4671f6bf..bdb3f71a37 100644 --- a/projects/Unit/TestChannelAllocation.cs +++ b/projects/Test/Integration/TestChannelAllocation.cs @@ -31,26 +31,28 @@ using System; using System.Collections.Generic; +using System.Threading.Tasks; +using RabbitMQ.Client; using RabbitMQ.Client.Impl; using Xunit; -namespace RabbitMQ.Client.Unit +namespace Test.Integration { - [Collection("IntegrationFixture")] public class TestChannelAllocation : IDisposable { public const int CHANNEL_COUNT = 100; IConnection _c; - public int ChannelNumber(IChannel channel) - { - return ((AutorecoveringChannel)channel).ChannelNumber; - } - public TestChannelAllocation() { - _c = new ConnectionFactory().CreateConnection(); + var cf = new ConnectionFactory + { + ContinuationTimeout = IntegrationFixture.WaitSpan, + HandshakeContinuationTimeout = IntegrationFixture.WaitSpan, + ClientProvidedName = nameof(TestChannelAllocation) + }; + _c = cf.CreateConnection(); } public void Dispose() => _c.Close(); @@ -58,44 +60,63 @@ public TestChannelAllocation() [Fact] public void AllocateInOrder() { + var channels = new List(); for (int i = 1; i <= CHANNEL_COUNT; i++) - Assert.Equal(i, ChannelNumber(_c.CreateChannel())); + { + IChannel channel = _c.CreateChannel(); + channels.Add(channel); + Assert.Equal(i, ChannelNumber(channel)); + } + + foreach (IChannel channel in channels) + { + channel.Dispose(); + } } [Fact] public void AllocateAfterFreeingLast() { - IChannel ch = _c.CreateChannel(); - Assert.Equal(1, ChannelNumber(ch)); - ch.Close(); - ch = _c.CreateChannel(); - Assert.Equal(1, ChannelNumber(ch)); + using IChannel ch0 = _c.CreateChannel(); + Assert.Equal(1, ChannelNumber(ch0)); + ch0.Close(); + + using IChannel ch1 = _c.CreateChannel(); + Assert.Equal(1, ChannelNumber(ch1)); } - public int CompareChannels(IChannel x, IChannel y) + [Fact] + public async Task AllocateAfterFreeingLastAsync() { - int i = ChannelNumber(x); - int j = ChannelNumber(y); - return (i < j) ? -1 : (i == j) ? 0 : 1; + using IChannel ch0 = _c.CreateChannel(); + Assert.Equal(1, ChannelNumber(ch0)); + await ch0.CloseAsync(); + + using IChannel ch1 = _c.CreateChannel(); + Assert.Equal(1, ChannelNumber(ch1)); } [Fact] public void AllocateAfterFreeingMany() { - List channels = new List(); + var channels = new List(); for (int i = 1; i <= CHANNEL_COUNT; i++) + { channels.Add(_c.CreateChannel()); + } foreach (IChannel channel in channels) { channel.Close(); } - channels = new List(); + channels.Clear(); for (int j = 1; j <= CHANNEL_COUNT; j++) + { channels.Add(_c.CreateChannel()); + } // In the current implementation the list should actually // already be sorted, but we don't want to force that behaviour @@ -103,7 +124,22 @@ public void AllocateAfterFreeingMany() int k = 1; foreach (IChannel channel in channels) + { Assert.Equal(k++, ChannelNumber(channel)); + channel.Close(); + } + } + + public int ChannelNumber(IChannel channel) + { + return ((AutorecoveringChannel)channel).ChannelNumber; + } + + public int CompareChannels(IChannel x, IChannel y) + { + int i = ChannelNumber(x); + int j = ChannelNumber(y); + return (i < j) ? -1 : (i == j) ? 0 : 1; } } } diff --git a/projects/Unit/TestChannelShutdown.cs b/projects/Test/Integration/TestChannelShutdown.cs similarity index 94% rename from projects/Unit/TestChannelShutdown.cs rename to projects/Test/Integration/TestChannelShutdown.cs index 04b385fe68..21fbbc6c55 100644 --- a/projects/Unit/TestChannelShutdown.cs +++ b/projects/Test/Integration/TestChannelShutdown.cs @@ -31,11 +31,12 @@ using System; using System.Threading; +using RabbitMQ.Client; using RabbitMQ.Client.Impl; using Xunit; using Xunit.Abstractions; -namespace RabbitMQ.Client.Unit +namespace Test.Integration { public class TestChannelShutdown : IntegrationFixture { @@ -55,7 +56,7 @@ public void TestConsumerDispatcherShutdown() }; Assert.False(m.ConsumerDispatcher.IsShutdown, "dispatcher should NOT be shut down before Close"); _channel.Close(); - Wait(latch, TimeSpan.FromSeconds(3)); + Wait(latch, TimeSpan.FromSeconds(3), "channel shutdown"); Assert.True(m.ConsumerDispatcher.IsShutdown, "dispatcher should be shut down after Close"); } } diff --git a/projects/Unit/TestChannelSoftErrors.cs b/projects/Test/Integration/TestChannelSoftErrors.cs similarity index 98% rename from projects/Unit/TestChannelSoftErrors.cs rename to projects/Test/Integration/TestChannelSoftErrors.cs index 4f0bed6850..b369c6a048 100644 --- a/projects/Unit/TestChannelSoftErrors.cs +++ b/projects/Test/Integration/TestChannelSoftErrors.cs @@ -29,12 +29,13 @@ // Copyright (c) 2007-2020 VMware, Inc. All rights reserved. //--------------------------------------------------------------------------- +using RabbitMQ.Client; using RabbitMQ.Client.Events; using RabbitMQ.Client.Exceptions; using Xunit; using Xunit.Abstractions; -namespace RabbitMQ.Client.Unit +namespace Test.Integration { public class TestChannelSoftErrors : IntegrationFixture { diff --git a/projects/Test/Integration/TestConcurrentAccessWithSharedConnection.cs b/projects/Test/Integration/TestConcurrentAccessWithSharedConnection.cs new file mode 100644 index 0000000000..c1384bf383 --- /dev/null +++ b/projects/Test/Integration/TestConcurrentAccessWithSharedConnection.cs @@ -0,0 +1,165 @@ +// This source code is dual-licensed under the Apache License, version +// 2.0, and the Mozilla Public License, version 2.0. +// +// The APL v2.0: +// +//--------------------------------------------------------------------------- +// Copyright (c) 2007-2020 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//--------------------------------------------------------------------------- +// +// The MPL v2.0: +// +//--------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// +// Copyright (c) 2007-2020 VMware, Inc. All rights reserved. +//--------------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using RabbitMQ.Client; +using RabbitMQ.Client.Events; +using Xunit; +using Xunit.Abstractions; + +namespace Test.Integration +{ + public class TestConcurrentAccessWithSharedConnection : IntegrationFixture + { + private const ushort _messageCount = 200; + + public TestConcurrentAccessWithSharedConnection(ITestOutputHelper output) : base(output) + { + } + + protected override void SetUp() + { + _connFactory = CreateConnectionFactory(); + _conn = _connFactory.CreateConnection(); + // NB: not creating _channel because this test suite doesn't use it. + Assert.Null(_channel); + } + + [Fact] + public void TestConcurrentChannelOpenAndPublishingWithBlankMessages() + { + TestConcurrentChannelOpenAndPublishingWithBody(Array.Empty(), 30); + } + + [Fact] + public void TestConcurrentChannelOpenAndPublishingSize64() + { + TestConcurrentChannelOpenAndPublishingWithBodyOfSize(64); + } + + [Fact] + public void TestConcurrentChannelOpenAndPublishingSize256() + { + TestConcurrentChannelOpenAndPublishingWithBodyOfSize(256); + } + + [Fact] + public void TestConcurrentChannelOpenAndPublishingSize1024() + { + TestConcurrentChannelOpenAndPublishingWithBodyOfSize(1024); + } + + [Fact] + public void TestConcurrentChannelOpenCloseLoop() + { + TestConcurrentChannelOperations((conn) => + { + using (IChannel ch = conn.CreateChannel()) + { + ch.Close(); + } + }, 50); + } + + private void TestConcurrentChannelOpenAndPublishingWithBodyOfSize(ushort length, int iterations = 30) + { + byte[] body = GetRandomBody(length); + TestConcurrentChannelOpenAndPublishingWithBody(body, iterations); + } + + private void TestConcurrentChannelOpenAndPublishingWithBody(byte[] body, int iterations) + { + TestConcurrentChannelOperations((conn) => + { + using (var localLatch = new ManualResetEvent(false)) + { + // publishing on a shared channel is not supported + // and would missing the point of this test anyway + using (IChannel ch = _conn.CreateChannel()) + { + ch.ConfirmSelect(); + + ch.BasicAcks += (object sender, BasicAckEventArgs e) => + { + if (e.DeliveryTag >= _messageCount) + { + localLatch.Set(); + } + }; + + ch.BasicNacks += (object sender, BasicNackEventArgs e) => + { + localLatch.Set(); + Assert.Fail("should never see a nack"); + }; + + QueueDeclareOk q = ch.QueueDeclare(queue: string.Empty, exclusive: true, autoDelete: true); + for (ushort j = 0; j < _messageCount; j++) + { + ch.BasicPublish("", q.QueueName, body, true); + } + + Assert.True(localLatch.WaitOne(WaitSpan)); + } + } + }, iterations); + } + + private void TestConcurrentChannelOperations(Action actions, int iterations) + { + TestConcurrentChannelOperations(actions, iterations, LongWaitSpan); + } + + private void TestConcurrentChannelOperations(Action action, int iterations, TimeSpan timeout) + { + var tasks = new List(); + for (int i = 0; i < _processorCount; i++) + { + tasks.Add(Task.Run(() => + { + for (int j = 0; j < iterations; j++) + { + action(_conn); + } + })); + } + Assert.True(Task.WaitAll(tasks.ToArray(), timeout)); + + // incorrect frame interleaving in these tests will result + // in an unrecoverable connection-level exception, thus + // closing the connection + Assert.True(_conn.IsOpen); + } + } +} diff --git a/projects/Unit/TestConfirmSelect.cs b/projects/Test/Integration/TestConfirmSelect.cs similarity index 97% rename from projects/Unit/TestConfirmSelect.cs rename to projects/Test/Integration/TestConfirmSelect.cs index 102282f14f..eb3040f2de 100644 --- a/projects/Unit/TestConfirmSelect.cs +++ b/projects/Test/Integration/TestConfirmSelect.cs @@ -29,10 +29,11 @@ // Copyright (c) 2007-2020 VMware, Inc. All rights reserved. //--------------------------------------------------------------------------- +using RabbitMQ.Client; using Xunit; using Xunit.Abstractions; -namespace RabbitMQ.Client.Unit +namespace Test.Integration { public class TestConfirmSelect : IntegrationFixture { diff --git a/projects/Unit/TestConnectionFactory.cs b/projects/Test/Integration/TestConnectionFactory.cs similarity index 66% rename from projects/Unit/TestConnectionFactory.cs rename to projects/Test/Integration/TestConnectionFactory.cs index 225c94f99d..e1c3e356ab 100644 --- a/projects/Unit/TestConnectionFactory.cs +++ b/projects/Test/Integration/TestConnectionFactory.cs @@ -30,13 +30,28 @@ //--------------------------------------------------------------------------- using System.Collections.Generic; +using RabbitMQ.Client; using RabbitMQ.Client.Exceptions; using Xunit; +using Xunit.Abstractions; -namespace RabbitMQ.Client.Unit +namespace Test.Integration { - public class TestConnectionFactory + public class TestConnectionFactory : IntegrationFixture { + public TestConnectionFactory(ITestOutputHelper output) : base(output) + { + } + + protected override void SetUp() + { + // NB: nothing to do here since each test creates its own factory, + // connections and channels + Assert.Null(_connFactory); + Assert.Null(_conn); + Assert.Null(_channel); + } + [Fact] public void TestProperties() { @@ -72,94 +87,95 @@ public void TestProperties() [Fact] public void TestCreateConnectionUsesSpecifiedPort() { - var cf = new ConnectionFactory - { - AutomaticRecoveryEnabled = true, - HostName = "localhost", - Port = 1234 - }; + var cf = CreateConnectionFactory(); + cf.AutomaticRecoveryEnabled = true; + cf.HostName = "localhost"; + cf.Port = 1234; - Assert.Throws(() => { using IConnection conn = cf.CreateConnection(); }); + Assert.Throws(() => + { + using IConnection conn = cf.CreateConnection(); + }); } [Fact] public void TestCreateConnectionWithClientProvidedNameUsesSpecifiedPort() { - var cf = new ConnectionFactory + var cf = CreateConnectionFactory(); + cf.AutomaticRecoveryEnabled = true; + cf.HostName = "localhost"; + cf.Port = 123; + + Assert.Throws(() => { - AutomaticRecoveryEnabled = true, - HostName = "localhost", - Port = 1234 - }; - Assert.Throws(() => { using IConnection conn = cf.CreateConnection("some_name"); }); + using IConnection conn = cf.CreateConnection(); + }); } [Fact] public void TestCreateConnectionWithClientProvidedNameUsesDefaultName() { - var cf = new ConnectionFactory - { - AutomaticRecoveryEnabled = false, - ClientProvidedName = "some_name" - }; + var cf = CreateConnectionFactory(); + cf.AutomaticRecoveryEnabled = false; + string expectedName = cf.ClientProvidedName; + using (IConnection conn = cf.CreateConnection()) { - Assert.Equal("some_name", conn.ClientProvidedName); - Assert.Equal("some_name", conn.ClientProperties["connection_name"]); + Assert.Equal(expectedName, conn.ClientProvidedName); + Assert.Equal(expectedName, conn.ClientProperties["connection_name"]); } } [Fact] public void TestCreateConnectionWithClientProvidedNameUsesNameArgumentValue() { - var cf = new ConnectionFactory - { - AutomaticRecoveryEnabled = false - }; - using (IConnection conn = cf.CreateConnection("some_name")) + var cf = CreateConnectionFactory(); + cf.AutomaticRecoveryEnabled = false; + string expectedName = cf.ClientProvidedName; + + using (IConnection conn = cf.CreateConnection(expectedName)) { - Assert.Equal("some_name", conn.ClientProvidedName); - Assert.Equal("some_name", conn.ClientProperties["connection_name"]); + Assert.Equal(expectedName, conn.ClientProvidedName); + Assert.Equal(expectedName, conn.ClientProperties["connection_name"]); } } [Fact] public void TestCreateConnectionWithClientProvidedNameAndAutorecoveryUsesNameArgumentValue() { - var cf = new ConnectionFactory - { - AutomaticRecoveryEnabled = true - }; - using (IConnection conn = cf.CreateConnection("some_name")) + var cf = CreateConnectionFactory(); + cf.AutomaticRecoveryEnabled = true; + string expectedName = cf.ClientProvidedName; + + using (IConnection conn = cf.CreateConnection(expectedName)) { - Assert.Equal("some_name", conn.ClientProvidedName); - Assert.Equal("some_name", conn.ClientProperties["connection_name"]); + Assert.Equal(expectedName, conn.ClientProvidedName); + Assert.Equal(expectedName, conn.ClientProperties["connection_name"]); } } [Fact] public void TestCreateConnectionAmqpTcpEndpointListAndClientProvidedName() { - var cf = new ConnectionFactory - { - AutomaticRecoveryEnabled = true - }; + var cf = CreateConnectionFactory(); + cf.AutomaticRecoveryEnabled = true; + string expectedName = cf.ClientProvidedName; + var xs = new List { new AmqpTcpEndpoint("localhost") }; - using (IConnection conn = cf.CreateConnection(xs, "some_name")) + using (IConnection conn = cf.CreateConnection(xs, expectedName)) { - Assert.Equal("some_name", conn.ClientProvidedName); - Assert.Equal("some_name", conn.ClientProperties["connection_name"]); + Assert.Equal(expectedName, conn.ClientProvidedName); + Assert.Equal(expectedName, conn.ClientProperties["connection_name"]); } } [Fact] public void TestCreateConnectionUsesDefaultPort() { - var cf = new ConnectionFactory - { - AutomaticRecoveryEnabled = true, - HostName = "localhost" - }; + var cf = CreateConnectionFactory(); + cf.AutomaticRecoveryEnabled = true; + cf.HostName = "localhost"; + using (IConnection conn = cf.CreateConnection()) { Assert.Equal(5672, conn.Endpoint.Port); @@ -169,11 +185,9 @@ public void TestCreateConnectionUsesDefaultPort() [Fact] public void TestCreateConnectionUsesDefaultMaxMessageSize() { - var cf = new ConnectionFactory - { - AutomaticRecoveryEnabled = true, - HostName = "localhost" - }; + var cf = CreateConnectionFactory(); + cf.AutomaticRecoveryEnabled = true; + cf.HostName = "localhost"; Assert.Equal(ConnectionFactory.DefaultMaxMessageSize, cf.MaxMessageSize); Assert.Equal(ConnectionFactory.DefaultMaxMessageSize, cf.Endpoint.MaxMessageSize); @@ -187,12 +201,11 @@ public void TestCreateConnectionUsesDefaultMaxMessageSize() [Fact] public void TestCreateConnectionWithoutAutoRecoverySelectsAHostFromTheList() { - var cf = new ConnectionFactory - { - AutomaticRecoveryEnabled = false, - HostName = "not_localhost" - }; - IConnection conn = cf.CreateConnection(new List { "localhost" }, "oregano"); + var cf = CreateConnectionFactory(); + cf.AutomaticRecoveryEnabled = false; + cf.HostName = "not_localhost"; + + IConnection conn = cf.CreateConnection(new List { "localhost" }); conn.Close(); conn.Dispose(); Assert.Equal("not_localhost", cf.HostName); @@ -202,12 +215,10 @@ public void TestCreateConnectionWithoutAutoRecoverySelectsAHostFromTheList() [Fact] public void TestCreateConnectionWithAutoRecoveryUsesAmqpTcpEndpoint() { - var cf = new ConnectionFactory - { - AutomaticRecoveryEnabled = true, - HostName = "not_localhost", - Port = 1234 - }; + var cf = CreateConnectionFactory(); + cf.AutomaticRecoveryEnabled = true; + cf.HostName = "not_localhost"; + cf.Port = 1234; var ep = new AmqpTcpEndpoint("localhost"); using (IConnection conn = cf.CreateConnection(new List { ep })) { } } @@ -215,10 +226,8 @@ public void TestCreateConnectionWithAutoRecoveryUsesAmqpTcpEndpoint() [Fact] public void TestCreateConnectionWithAutoRecoveryUsesInvalidAmqpTcpEndpoint() { - var cf = new ConnectionFactory - { - AutomaticRecoveryEnabled = true - }; + var cf = CreateConnectionFactory(); + cf.AutomaticRecoveryEnabled = true; var ep = new AmqpTcpEndpoint("localhost", 1234); Assert.Throws(() => { using IConnection conn = cf.CreateConnection(new List { ep }); }); } @@ -226,11 +235,9 @@ public void TestCreateConnectionWithAutoRecoveryUsesInvalidAmqpTcpEndpoint() [Fact] public void TestCreateConnectionUsesAmqpTcpEndpoint() { - var cf = new ConnectionFactory - { - HostName = "not_localhost", - Port = 1234 - }; + var cf = CreateConnectionFactory(); + cf.HostName = "not_localhost"; + cf.Port = 1234; var ep = new AmqpTcpEndpoint("localhost"); using (IConnection conn = cf.CreateConnection(new List { ep })) { } } @@ -238,33 +245,34 @@ public void TestCreateConnectionUsesAmqpTcpEndpoint() [Fact] public void TestCreateConnectionWithForcedAddressFamily() { - var cf = new ConnectionFactory - { - HostName = "not_localhost" - }; + var cf = CreateConnectionFactory(); + cf.HostName = "not_localhost"; var ep = new AmqpTcpEndpoint("localhost") { AddressFamily = System.Net.Sockets.AddressFamily.InterNetwork }; cf.Endpoint = ep; - using (IConnection conn = cf.CreateConnection()) { }; + using IConnection conn = cf.CreateConnection(); } [Fact] public void TestCreateConnectionUsesInvalidAmqpTcpEndpoint() { - var cf = new ConnectionFactory(); + var cf = CreateConnectionFactory(); var ep = new AmqpTcpEndpoint("localhost", 1234); - Assert.Throws(() => { using (IConnection conn = cf.CreateConnection(new List { ep })) { } }); + Assert.Throws(() => + { + using IConnection conn = cf.CreateConnection(new List { ep }); + }); } [Fact] public void TestCreateConnectioUsesValidEndpointWhenMultipleSupplied() { - var cf = new ConnectionFactory(); + var cf = CreateConnectionFactory(); var invalidEp = new AmqpTcpEndpoint("not_localhost"); var ep = new AmqpTcpEndpoint("localhost"); - using (IConnection conn = cf.CreateConnection(new List { invalidEp, ep })) { }; + using IConnection conn = cf.CreateConnection(new List { invalidEp, ep }); } [Fact] @@ -276,7 +284,7 @@ public void TestCreateAmqpTCPEndPointOverridesMaxMessageSizeWhenGreaterThanMaxim [Fact] public void TestCreateConnectionUsesConfiguredMaxMessageSize() { - var cf = new ConnectionFactory(); + var cf = CreateConnectionFactory(); cf.MaxMessageSize = 1500; using (IConnection conn = cf.CreateConnection()) { @@ -286,7 +294,7 @@ public void TestCreateConnectionUsesConfiguredMaxMessageSize() [Fact] public void TestCreateConnectionWithAmqpEndpointListUsesAmqpTcpEndpointMaxMessageSize() { - var cf = new ConnectionFactory(); + var cf = CreateConnectionFactory(); cf.MaxMessageSize = 1500; var ep = new AmqpTcpEndpoint("localhost"); Assert.Equal(ConnectionFactory.DefaultMaxMessageSize, ep.MaxMessageSize); @@ -299,7 +307,7 @@ public void TestCreateConnectionWithAmqpEndpointListUsesAmqpTcpEndpointMaxMessag [Fact] public void TestCreateConnectionWithAmqpEndpointResolverUsesAmqpTcpEndpointMaxMessageSize() { - var cf = new ConnectionFactory(); + var cf = CreateConnectionFactory(); cf.MaxMessageSize = 1500; var ep = new AmqpTcpEndpoint("localhost", -1, new SslOption(), 1200); using (IConnection conn = cf.CreateConnection(new List { ep })) @@ -311,7 +319,7 @@ public void TestCreateConnectionWithAmqpEndpointResolverUsesAmqpTcpEndpointMaxMe [Fact] public void TestCreateConnectionWithHostnameListUsesConnectionFactoryMaxMessageSize() { - var cf = new ConnectionFactory(); + var cf = CreateConnectionFactory(); cf.MaxMessageSize = 1500; using (IConnection conn = cf.CreateConnection(new List { "localhost" })) { diff --git a/projects/Unit/TestConnectionFactoryContinuationTimeout.cs b/projects/Test/Integration/TestConnectionFactoryContinuationTimeout.cs similarity index 78% rename from projects/Unit/TestConnectionFactoryContinuationTimeout.cs rename to projects/Test/Integration/TestConnectionFactoryContinuationTimeout.cs index 5eb79b4ef8..23022e8d8b 100644 --- a/projects/Unit/TestConnectionFactoryContinuationTimeout.cs +++ b/projects/Test/Integration/TestConnectionFactoryContinuationTimeout.cs @@ -30,10 +30,11 @@ //--------------------------------------------------------------------------- using System; +using RabbitMQ.Client; using Xunit; using Xunit.Abstractions; -namespace RabbitMQ.Client.Unit +namespace Test.Integration { public class TestConnectionFactoryContinuationTimeout : IntegrationFixture { @@ -41,6 +42,10 @@ public TestConnectionFactoryContinuationTimeout(ITestOutputHelper output) : base { } + protected override void SetUp() + { + } + [Fact] public void TestConnectionFactoryContinuationTimeoutOnRecoveringConnection() { @@ -57,8 +62,19 @@ public void TestConnectionFactoryContinuationTimeoutOnNonRecoveringConnection() var continuationTimeout = TimeSpan.FromSeconds(777); using (IConnection c = CreateConnectionWithContinuationTimeout(false, continuationTimeout)) { - Assert.Equal(continuationTimeout, c.CreateChannel().ContinuationTimeout); + using (IChannel ch = c.CreateChannel()) + { + Assert.Equal(continuationTimeout, ch.ContinuationTimeout); + } } } + + private IConnection CreateConnectionWithContinuationTimeout(bool automaticRecoveryEnabled, TimeSpan continuationTimeout) + { + var cf = CreateConnectionFactory(); + cf.AutomaticRecoveryEnabled = automaticRecoveryEnabled; + cf.ContinuationTimeout = continuationTimeout; + return cf.CreateConnection(); + } } } diff --git a/projects/Unit/TestConnectionShutdown.cs b/projects/Test/Integration/TestConnectionShutdown.cs similarity index 84% rename from projects/Unit/TestConnectionShutdown.cs rename to projects/Test/Integration/TestConnectionShutdown.cs index d063742fde..5489ec4611 100644 --- a/projects/Unit/TestConnectionShutdown.cs +++ b/projects/Test/Integration/TestConnectionShutdown.cs @@ -31,12 +31,13 @@ using System; using System.Threading; +using RabbitMQ.Client; using RabbitMQ.Client.Framing.Impl; using RabbitMQ.Client.Impl; using Xunit; using Xunit.Abstractions; -namespace RabbitMQ.Client.Unit +namespace Test.Integration { public class TestConnectionShutdown : IntegrationFixture { @@ -47,9 +48,6 @@ public TestConnectionShutdown(ITestOutputHelper output) : base(output) [Fact] public void TestCleanClosureWithSocketClosedOutOfBand() { - _conn = CreateAutorecoveringConnection(); - _channel = _conn.CreateChannel(); - var latch = new ManualResetEventSlim(false); _channel.ChannelShutdown += (channel, args) => { @@ -57,18 +55,15 @@ public void TestCleanClosureWithSocketClosedOutOfBand() }; var c = (AutorecoveringConnection)_conn; - c.FrameHandler.Close(); + c.CloseFrameHandler(); _conn.Close(TimeSpan.FromSeconds(4)); - Wait(latch, TimeSpan.FromSeconds(5)); + Wait(latch, TimeSpan.FromSeconds(5), "channel shutdown"); } [Fact] public void TestAbortWithSocketClosedOutOfBand() { - _conn = CreateAutorecoveringConnection(); - _channel = _conn.CreateChannel(); - var latch = new ManualResetEventSlim(false); _channel.ChannelShutdown += (channel, args) => { @@ -76,19 +71,16 @@ public void TestAbortWithSocketClosedOutOfBand() }; var c = (AutorecoveringConnection)_conn; - c.FrameHandler.Close(); + c.CloseFrameHandler(); _conn.Abort(); // default Connection.Abort() timeout and then some - Wait(latch, TimeSpan.FromSeconds(6)); + Wait(latch, TimeSpan.FromSeconds(6), "channel shutdown"); } [Fact] public void TestDisposedWithSocketClosedOutOfBand() { - _conn = CreateAutorecoveringConnection(); - _channel = _conn.CreateChannel(); - var latch = new ManualResetEventSlim(false); _channel.ChannelShutdown += (channel, args) => { @@ -96,10 +88,10 @@ public void TestDisposedWithSocketClosedOutOfBand() }; var c = (AutorecoveringConnection)_conn; - c.FrameHandler.Close(); + c.CloseFrameHandler(); _conn.Dispose(); - Wait(latch, TimeSpan.FromSeconds(3)); + Wait(latch, TimeSpan.FromSeconds(3), "channel shutdown"); } [Fact] @@ -113,7 +105,7 @@ public void TestShutdownSignalPropagationToChannels() }; _conn.Close(); - Wait(latch, TimeSpan.FromSeconds(3)); + Wait(latch, TimeSpan.FromSeconds(3), "channel shutdown"); } [Fact] @@ -128,7 +120,7 @@ public void TestConsumerDispatcherShutdown() }; Assert.False(m.ConsumerDispatcher.IsShutdown, "dispatcher should NOT be shut down before Close"); _conn.Close(); - Wait(latch, TimeSpan.FromSeconds(3)); + Wait(latch, TimeSpan.FromSeconds(3), "channel shutdown"); Assert.True(m.ConsumerDispatcher.IsShutdown, "dispatcher should be shut down after Close"); } } diff --git a/projects/Test/Integration/TestConsumer.cs b/projects/Test/Integration/TestConsumer.cs new file mode 100644 index 0000000000..9aa3082eb4 --- /dev/null +++ b/projects/Test/Integration/TestConsumer.cs @@ -0,0 +1,142 @@ +// This source code is dual-licensed under the Apache License, version +// 2.0, and the Mozilla Public License, version 2.0. +// +// The APL v2.0: +// +//--------------------------------------------------------------------------- +// Copyright (c) 2007-2020 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//--------------------------------------------------------------------------- +// +// The MPL v2.0: +// +//--------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// +// Copyright (c) 2007-2020 VMware, Inc. All rights reserved. +//--------------------------------------------------------------------------- + +using System; +using System.Threading; +using System.Threading.Tasks; +using RabbitMQ.Client; +using RabbitMQ.Client.Events; +using Xunit; +using Xunit.Abstractions; + +namespace Test.Integration +{ + public class TestConsumer : IntegrationFixture + { + private readonly byte[] _body = GetRandomBody(64); + private readonly ShutdownEventArgs _closeArgs = new ShutdownEventArgs(ShutdownInitiator.Application, Constants.ReplySuccess, "normal shutdown"); + + public TestConsumer(ITestOutputHelper output) : base(output) + { + } + + [Fact] + public void TestBasicRoundtrip() + { + QueueDeclareOk q = _channel.QueueDeclare(); + _channel.BasicPublish("", q.QueueName, _body); + var consumer = new EventingBasicConsumer(_channel); + var are = new AutoResetEvent(false); + consumer.Received += (o, a) => + { + are.Set(); + }; + string tag = _channel.BasicConsume(q.QueueName, true, consumer); + // ensure we get a delivery + bool waitRes = are.WaitOne(2000); + Assert.True(waitRes); + // unsubscribe and ensure no further deliveries + _channel.BasicCancel(tag); + _channel.BasicPublish("", q.QueueName, _body); + bool waitResFalse = are.WaitOne(2000); + Assert.False(waitResFalse); + } + + [Fact] + public void TestBasicRoundtripNoWait() + { + QueueDeclareOk q = _channel.QueueDeclare(); + _channel.BasicPublish("", q.QueueName, _body); + var consumer = new EventingBasicConsumer(_channel); + var are = new AutoResetEvent(false); + consumer.Received += (o, a) => + { + are.Set(); + }; + string tag = _channel.BasicConsume(q.QueueName, true, consumer); + // ensure we get a delivery + bool waitRes = are.WaitOne(2000); + Assert.True(waitRes); + // unsubscribe and ensure no further deliveries + _channel.BasicCancelNoWait(tag); + _channel.BasicPublish("", q.QueueName, _body); + bool waitResFalse = are.WaitOne(2000); + Assert.False(waitResFalse); + } + + [Fact] + public void ConcurrentEventingTestForReceived() + { + const int NumberOfThreads = 4; + const int NumberOfRegistrations = 5000; + + var called = new byte[NumberOfThreads * NumberOfRegistrations]; + + QueueDeclareOk q = _channel.QueueDeclare(); + var consumer = new EventingBasicConsumer(_channel); + _channel.BasicConsume(q.QueueName, true, consumer); + var countdownEvent = new CountdownEvent(NumberOfThreads); + for (int i = 0; i < NumberOfThreads; i++) + { + int threadIndex = i; + Task.Run(() => + { + int start = threadIndex * NumberOfRegistrations; + for (int j = start; j < start + NumberOfRegistrations; j++) + { + int receivedIndex = j; + consumer.Received += (sender, eventArgs) => + { + called[receivedIndex] = 1; + }; + } + countdownEvent.Signal(); + }); + } + + countdownEvent.Wait(); + + // Add last receiver + var are = new AutoResetEvent(false); + consumer.Received += (o, a) => + { + are.Set(); + }; + + // Send message + _channel.BasicPublish("", q.QueueName, ReadOnlyMemory.Empty); + are.WaitOne(TimingFixture.TestTimeout); + + // Check received messages + Assert.Equal(-1, called.AsSpan().IndexOf((byte)0)); + } + } +} diff --git a/projects/Unit/TestConsumerCancelNotify.cs b/projects/Test/Integration/TestConsumerCancelNotify.cs similarity index 99% rename from projects/Unit/TestConsumerCancelNotify.cs rename to projects/Test/Integration/TestConsumerCancelNotify.cs index f298852965..152ed8e9f5 100644 --- a/projects/Unit/TestConsumerCancelNotify.cs +++ b/projects/Test/Integration/TestConsumerCancelNotify.cs @@ -31,11 +31,12 @@ using System.Linq; using System.Threading; +using RabbitMQ.Client; using RabbitMQ.Client.Events; using Xunit; using Xunit.Abstractions; -namespace RabbitMQ.Client.Unit +namespace Test.Integration { public class TestConsumerCancelNotify : IntegrationFixture { diff --git a/projects/Unit/TestConsumerCount.cs b/projects/Test/Integration/TestConsumerCount.cs similarity index 97% rename from projects/Unit/TestConsumerCount.cs rename to projects/Test/Integration/TestConsumerCount.cs index 1f846e8621..8b38ea0360 100644 --- a/projects/Unit/TestConsumerCount.cs +++ b/projects/Test/Integration/TestConsumerCount.cs @@ -29,11 +29,12 @@ // Copyright (c) 2007-2020 VMware, Inc. All rights reserved. //--------------------------------------------------------------------------- +using RabbitMQ.Client; using RabbitMQ.Client.Events; using Xunit; using Xunit.Abstractions; -namespace RabbitMQ.Client.Unit +namespace Test.Integration { public class TestConsumerCount : IntegrationFixture { diff --git a/projects/Unit/TestConsumerExceptions.cs b/projects/Test/Integration/TestConsumerExceptions.cs similarity index 98% rename from projects/Unit/TestConsumerExceptions.cs rename to projects/Test/Integration/TestConsumerExceptions.cs index 5bf62391fc..7f46861ae2 100644 --- a/projects/Unit/TestConsumerExceptions.cs +++ b/projects/Test/Integration/TestConsumerExceptions.cs @@ -31,10 +31,11 @@ using System; using System.Threading; +using RabbitMQ.Client; using Xunit; using Xunit.Abstractions; -namespace RabbitMQ.Client.Unit +namespace Test.Integration { public class TestConsumerExceptions : IntegrationFixture { @@ -50,7 +51,7 @@ public override void HandleBasicDeliver(string consumerTag, string exchange, string routingKey, in ReadOnlyBasicProperties properties, - ReadOnlyMemory body) + RentedMemory body) { throw new Exception("oops"); } diff --git a/projects/Unit/TestConsumerOperationDispatch.cs b/projects/Test/Integration/TestConsumerOperationDispatch.cs similarity index 87% rename from projects/Unit/TestConsumerOperationDispatch.cs rename to projects/Test/Integration/TestConsumerOperationDispatch.cs index 1f3a1129e2..019cc4b952 100644 --- a/projects/Unit/TestConsumerOperationDispatch.cs +++ b/projects/Test/Integration/TestConsumerOperationDispatch.cs @@ -32,32 +32,33 @@ using System; using System.Collections.Generic; using System.Threading; +using RabbitMQ.Client; using RabbitMQ.Client.Events; using Xunit; using Xunit.Abstractions; -namespace RabbitMQ.Client.Unit +namespace Test.Integration { public class TestConsumerOperationDispatch : IntegrationFixture { - public TestConsumerOperationDispatch(ITestOutputHelper output) : base(output) - { - } + // number of channels (and consumers) + private const int Y = 100; + // number of messages to be published + private const int N = 100; - private readonly string _x = "dotnet.tests.consumer-operation-dispatch.fanout"; + private static readonly CountdownEvent s_counter = new CountdownEvent(Y); + + private const string _x = "dotnet.tests.consumer-operation-dispatch.fanout"; private readonly List _channels = new List(); private readonly List _queues = new List(); private readonly List _consumers = new List(); - // number of channels (and consumers) - private const int Y = 100; - // number of messages to be published - private const int N = 100; - - public static CountdownEvent counter = new CountdownEvent(Y); + public TestConsumerOperationDispatch(ITestOutputHelper output) : base(output) + { + } - public override void Dispose() + protected override void TearDown() { foreach (IChannel ch in _channels) { @@ -67,10 +68,7 @@ public override void Dispose() } } - _queues.Clear(); - _consumers.Clear(); - counter.Reset(); - base.ReleaseResources(); + s_counter.Reset(); } private class CollectingConsumer : DefaultBasicConsumer @@ -85,7 +83,7 @@ public CollectingConsumer(IChannel channel) public override void HandleBasicDeliver(string consumerTag, ulong deliveryTag, bool redelivered, string exchange, string routingKey, - in ReadOnlyBasicProperties properties, ReadOnlyMemory body) + in ReadOnlyBasicProperties properties, RentedMemory body) { // we test concurrent dispatch from the moment basic.delivery is returned. // delivery tags have guaranteed ordering and we verify that it is preserved @@ -94,18 +92,19 @@ public override void HandleBasicDeliver(string consumerTag, if (deliveryTag == N) { - counter.Signal(); + s_counter.Signal(); } Channel.BasicAck(deliveryTag: deliveryTag, multiple: false); } } - [Fact] + [SkippableFact] public void TestDeliveryOrderingWithSingleChannel() { - IChannel Ch = _conn.CreateChannel(); - Ch.ExchangeDeclare(_x, "fanout", durable: false); + Skip.If(IsRunningInCI && IsWindows, "TODO - test is slow in CI on Windows"); + + _channel.ExchangeDeclare(_x, "fanout", durable: false); for (int i = 0; i < Y; i++) { @@ -121,9 +120,17 @@ public void TestDeliveryOrderingWithSingleChannel() for (int i = 0; i < N; i++) { - Ch.BasicPublish(_x, "", _encoding.GetBytes("msg")); + _channel.BasicPublish(_x, "", _encoding.GetBytes("msg")); + } + + if (IsRunningInCI) + { + s_counter.Wait(TimeSpan.FromMinutes(5)); + } + else + { + s_counter.Wait(TimeSpan.FromMinutes(2)); } - counter.Wait(TimeSpan.FromSeconds(120)); foreach (CollectingConsumer cons in _consumers) { @@ -145,9 +152,10 @@ public void TestDeliveryOrderingWithSingleChannel() [Fact] public void TestChannelShutdownDoesNotShutDownDispatcher() { + _channel.ExchangeDeclare(_x, "fanout", durable: false); + IChannel ch1 = _conn.CreateChannel(); IChannel ch2 = _conn.CreateChannel(); - _channel.ExchangeDeclare(_x, "fanout", durable: false); string q1 = ch1.QueueDeclare().QueueName; string q2 = ch2.QueueDeclare().QueueName; @@ -165,7 +173,7 @@ public void TestChannelShutdownDoesNotShutDownDispatcher() ch1.Close(); ch2.BasicPublish(_x, "", _encoding.GetBytes("msg")); - Wait(latch); + Wait(latch, "received event"); } private class ShutdownLatchConsumer : DefaultBasicConsumer @@ -203,7 +211,7 @@ public void TestChannelShutdownHandler() _channel.BasicConsume(queue: q, autoAck: true, consumer: c); _channel.Close(); - Wait(latch, TimeSpan.FromSeconds(5)); + Wait(latch, TimeSpan.FromSeconds(5), "channel shutdown"); Assert.False(duplicateLatch.Wait(TimeSpan.FromSeconds(5)), "event handler fired more than once"); } diff --git a/projects/Unit/TestEventingConsumer.cs b/projects/Test/Integration/TestEventingConsumer.cs similarity index 96% rename from projects/Unit/TestEventingConsumer.cs rename to projects/Test/Integration/TestEventingConsumer.cs index 6660af4f13..c12da36c8f 100644 --- a/projects/Unit/TestEventingConsumer.cs +++ b/projects/Test/Integration/TestEventingConsumer.cs @@ -30,11 +30,12 @@ //--------------------------------------------------------------------------- using System.Threading; +using RabbitMQ.Client; using RabbitMQ.Client.Events; using Xunit; using Xunit.Abstractions; -namespace RabbitMQ.Client.Unit +namespace Test.Integration { public class TestEventingConsumer : IntegrationFixture { @@ -66,14 +67,14 @@ public void TestEventingConsumerRegistrationEvents() }; string tag = _channel.BasicConsume(q, false, ec); - Wait(registeredLatch); + Wait(registeredLatch, "consumer registered"); Assert.NotNull(registeredSender); Assert.Equal(ec, registeredSender); Assert.Equal(_channel, ((EventingBasicConsumer)registeredSender).Channel); _channel.BasicCancel(tag); - Wait(unregisteredLatch); + Wait(unregisteredLatch, "consumer unregistered"); Assert.NotNull(unregisteredSender); Assert.Equal(ec, unregisteredSender); Assert.Equal(_channel, ((EventingBasicConsumer)unregisteredSender).Channel); diff --git a/projects/Unit/TestExceptionMessages.cs b/projects/Test/Integration/TestExceptionMessages.cs similarity index 98% rename from projects/Unit/TestExceptionMessages.cs rename to projects/Test/Integration/TestExceptionMessages.cs index 2f87a9f430..7901096f54 100644 --- a/projects/Unit/TestExceptionMessages.cs +++ b/projects/Test/Integration/TestExceptionMessages.cs @@ -34,7 +34,7 @@ using Xunit; using Xunit.Abstractions; -namespace RabbitMQ.Client.Unit +namespace Test.Integration { public class TestExceptionMessages : IntegrationFixture { diff --git a/projects/Unit/TestExchangeDeclare.cs b/projects/Test/Integration/TestExchangeDeclare.cs similarity index 60% rename from projects/Unit/TestExchangeDeclare.cs rename to projects/Test/Integration/TestExchangeDeclare.cs index 555c58c1ad..47056d76ed 100644 --- a/projects/Unit/TestExchangeDeclare.cs +++ b/projects/Test/Integration/TestExchangeDeclare.cs @@ -32,35 +32,38 @@ using System; using System.Collections.Generic; using System.Threading; +using RabbitMQ.Client; using Xunit; using Xunit.Abstractions; -namespace RabbitMQ.Client.Unit +namespace Test.Integration { public class TestExchangeDeclare : IntegrationFixture { + private readonly Random _rnd = new Random(); + public TestExchangeDeclare(ITestOutputHelper output) : base(output) { } [Fact] - public void TestConcurrentExchangeDeclare() + public void TestConcurrentExchangeDeclareAndDelete() { - string x = GenerateExchangeName(); - Random rnd = new Random(); - - List ts = new List(); + var exchangeNames = new List(); + var ts = new List(); NotSupportedException nse = null; for (int i = 0; i < 256; i++) { - Thread t = new Thread(() => + var t = new Thread(() => { try { // sleep for a random amount of time to increase the chances // of thread interleaving. MK. - Thread.Sleep(rnd.Next(5, 500)); - _channel.ExchangeDeclare(x, "fanout", false, false, null); + Thread.Sleep(_rnd.Next(5, 500)); + string exchangeName = GenerateExchangeName(); + _channel.ExchangeDeclare(exchange: exchangeName, "fanout", false, false, null); + exchangeNames.Add(exchangeName); } catch (NotSupportedException e) { @@ -77,7 +80,34 @@ public void TestConcurrentExchangeDeclare() } Assert.Null(nse); - _channel.ExchangeDelete(x); + ts.Clear(); + + foreach (string exchangeName in exchangeNames) + { + var t = new Thread((object ex) => + { + try + { + // sleep for a random amount of time to increase the chances + // of thread interleaving. MK. + Thread.Sleep(_rnd.Next(5, 500)); + _channel.ExchangeDelete((string)ex); + } + catch (NotSupportedException e) + { + nse = e; + } + }); + ts.Add(t); + t.Start(exchangeName); + } + + foreach (Thread t in ts) + { + t.Join(); + } + + Assert.Null(nse); } } } diff --git a/projects/Unit/TestHeartbeats.cs b/projects/Test/Integration/TestHeartbeats.cs similarity index 62% rename from projects/Unit/TestHeartbeats.cs rename to projects/Test/Integration/TestHeartbeats.cs index 40deb350f7..86e1600821 100644 --- a/projects/Unit/TestHeartbeats.cs +++ b/projects/Test/Integration/TestHeartbeats.cs @@ -31,34 +31,43 @@ using System; using System.Collections.Generic; -using System.IO; using System.Threading; +using RabbitMQ.Client; using Xunit; using Xunit.Abstractions; -namespace RabbitMQ.Client.Unit +namespace Test.Integration { public class TestHeartbeats : IntegrationFixture { + private readonly TimeSpan _heartbeatTimeout = TimeSpan.FromSeconds(2); + public TestHeartbeats(ITestOutputHelper output) : base(output) { } - private readonly TimeSpan _heartbeatTimeout = TimeSpan.FromSeconds(2); + protected override void SetUp() + { + Assert.Null(_connFactory); + Assert.Null(_conn); + Assert.Null(_channel); + } - [Fact(Timeout = 35000)] + [SkippableFact(Timeout = 35000)] [Trait("Category", "LongRunning")] public void TestThatHeartbeatWriterUsesConfigurableInterval() { - var cf = new ConnectionFactory() - { - RequestedHeartbeat = _heartbeatTimeout, - AutomaticRecoveryEnabled = false - }; + Skip.IfNot(LongRunningTestsEnabled(), "RABBITMQ_LONG_RUNNING_TESTS is not set, skipping test"); + + var cf = CreateConnectionFactory(); + cf.RequestedHeartbeat = _heartbeatTimeout; + cf.AutomaticRecoveryEnabled = false; + RunSingleConnectionTest(cf); } [SkippableFact] + [Trait("Category", "LongRunning")] public void TestThatHeartbeatWriterWithTLSEnabled() { Skip.IfNot(LongRunningTestsEnabled(), "RABBITMQ_LONG_RUNNING_TESTS is not set, skipping test"); @@ -66,12 +75,10 @@ public void TestThatHeartbeatWriterWithTLSEnabled() var sslEnv = new SslEnv(); Skip.IfNot(sslEnv.IsSslConfigured, "SSL_CERTS_DIR and/or PASSWORD are not configured, skipping test"); - var cf = new ConnectionFactory() - { - Port = 5671, - RequestedHeartbeat = _heartbeatTimeout, - AutomaticRecoveryEnabled = false - }; + var cf = CreateConnectionFactory(); + cf.Port = 5671; + cf.RequestedHeartbeat = _heartbeatTimeout; + cf.AutomaticRecoveryEnabled = false; cf.Ssl.ServerName = sslEnv.Hostname; cf.Ssl.CertPath = sslEnv.CertPath; @@ -81,43 +88,59 @@ public void TestThatHeartbeatWriterWithTLSEnabled() RunSingleConnectionTest(cf); } - [Fact(Timeout = 90000)] + [SkippableFact(Timeout = 90000)] [Trait("Category", "LongRunning")] public void TestHundredsOfConnectionsWithRandomHeartbeatInterval() { - var rnd = new Random(); - List xs = new List(); - // Since we are using the ThreadPool, let's set MinThreads to a high-enough value. - ThreadPool.SetMinThreads(200, 200); - for (int i = 0; i < 200; i++) + Skip.IfNot(LongRunningTestsEnabled(), "RABBITMQ_LONG_RUNNING_TESTS is not set, skipping test"); + + const ushort connectionCount = 200; + + ThreadPool.GetMinThreads(out int origWorkerThreads, out int origCompletionPortThreads); + try { - ushort n = Convert.ToUInt16(rnd.Next(2, 6)); - var cf = new ConnectionFactory() + var rnd = new Random(); + var conns = new List(); + + // Since we are using the ThreadPool, let's set MinThreads to a high-enough value. + ThreadPool.SetMinThreads(connectionCount, connectionCount); + + try { - RequestedHeartbeat = TimeSpan.FromSeconds(n), - AutomaticRecoveryEnabled = false - }; - IConnection conn = cf.CreateConnection(); - xs.Add(conn); - IChannel ch = conn.CreateChannel(); - - conn.ConnectionShutdown += (sender, evt) => + for (int i = 0; i < connectionCount; i++) { - CheckInitiator(evt); - }; + ushort n = Convert.ToUInt16(rnd.Next(2, 6)); + var cf = CreateConnectionFactory(); + cf.RequestedHeartbeat = TimeSpan.FromSeconds(n); + cf.AutomaticRecoveryEnabled = false; + + IConnection conn = cf.CreateConnection($"_testDisplayName:{i}"); + conns.Add(conn); + IChannel ch = conn.CreateChannel(); + conn.ConnectionShutdown += (sender, evt) => + { + CheckInitiator(evt); + }; + } + SleepFor(60); + } + finally + { + foreach (IConnection conn in conns) + { + conn.Close(); + } + } } - - SleepFor(60); - - foreach (IConnection x in xs) + finally { - x.Close(); + Assert.True(ThreadPool.SetMinThreads(origWorkerThreads, origCompletionPortThreads)); } } - protected void RunSingleConnectionTest(ConnectionFactory cf) + private void RunSingleConnectionTest(ConnectionFactory cf) { - using (IConnection conn = cf.CreateConnection()) + using (IConnection conn = cf.CreateConnection(_testDisplayName)) { using (IChannel ch = conn.CreateChannel()) { @@ -145,11 +168,18 @@ protected void RunSingleConnectionTest(ConnectionFactory cf) private bool LongRunningTestsEnabled() { string s = Environment.GetEnvironmentVariable("RABBITMQ_LONG_RUNNING_TESTS"); + if (String.IsNullOrEmpty(s)) { return false; } - return true; + + if (Boolean.TryParse(s, out bool enabled)) + { + return enabled; + } + + return false; } private void SleepFor(int t) diff --git a/projects/Unit/TestInitialConnection.cs b/projects/Test/Integration/TestInitialConnection.cs similarity index 88% rename from projects/Unit/TestInitialConnection.cs rename to projects/Test/Integration/TestInitialConnection.cs index f20afb9bb9..b0a34e0d36 100644 --- a/projects/Unit/TestInitialConnection.cs +++ b/projects/Test/Integration/TestInitialConnection.cs @@ -30,11 +30,12 @@ //--------------------------------------------------------------------------- using System.Collections.Generic; +using RabbitMQ.Client; using RabbitMQ.Client.Exceptions; using Xunit; using Xunit.Abstractions; -namespace RabbitMQ.Client.Unit +namespace Test.Integration { public class TestInitialConnection : IntegrationFixture { @@ -45,7 +46,7 @@ public TestInitialConnection(ITestOutputHelper output) : base(output) [Fact] public void TestBasicConnectionRecoveryWithHostnameList() { - Framing.Impl.AutorecoveringConnection c = CreateAutorecoveringConnection(new List() { "127.0.0.1", "localhost" }); + var c = CreateAutorecoveringConnection(new List() { "127.0.0.1", "localhost" }); Assert.True(c.IsOpen); c.Close(); } @@ -53,7 +54,7 @@ public void TestBasicConnectionRecoveryWithHostnameList() [Fact] public void TestBasicConnectionRecoveryWithHostnameListAndUnreachableHosts() { - Framing.Impl.AutorecoveringConnection c = CreateAutorecoveringConnection(new List() { "191.72.44.22", "127.0.0.1", "localhost" }); + var c = CreateAutorecoveringConnection(new List() { "191.72.44.22", "127.0.0.1", "localhost" }); Assert.True(c.IsOpen); c.Close(); } diff --git a/projects/Unit/TestInvalidAck.cs b/projects/Test/Integration/TestInvalidAck.cs similarity index 97% rename from projects/Unit/TestInvalidAck.cs rename to projects/Test/Integration/TestInvalidAck.cs index 0d3d5dc750..427e8a9342 100644 --- a/projects/Unit/TestInvalidAck.cs +++ b/projects/Test/Integration/TestInvalidAck.cs @@ -30,10 +30,11 @@ //--------------------------------------------------------------------------- using System.Threading; +using RabbitMQ.Client; using Xunit; using Xunit.Abstractions; -namespace RabbitMQ.Client.Unit +namespace Test.Integration { public class TestInvalidAck : IntegrationFixture { diff --git a/projects/Unit/TestMainLoop.cs b/projects/Test/Integration/TestMainLoop.cs similarity index 80% rename from projects/Unit/TestMainLoop.cs rename to projects/Test/Integration/TestMainLoop.cs index 597f1898f3..ab29fc5c1a 100644 --- a/projects/Unit/TestMainLoop.cs +++ b/projects/Test/Integration/TestMainLoop.cs @@ -31,11 +31,12 @@ using System; using System.Threading; +using RabbitMQ.Client; using RabbitMQ.Client.Events; using Xunit; using Xunit.Abstractions; -namespace RabbitMQ.Client.Unit +namespace Test.Integration { public class TestMainLoop : IntegrationFixture { @@ -53,7 +54,7 @@ public override void HandleBasicDeliver(string consumerTag, string exchange, string routingKey, in ReadOnlyBasicProperties properties, - ReadOnlyMemory body) + RentedMemory body) { throw new Exception("I am a bad consumer"); } @@ -62,27 +63,24 @@ public override void HandleBasicDeliver(string consumerTag, [Fact] public void TestCloseWithFaultyConsumer() { - ConnectionFactory connFactory = new ConnectionFactory(); - IConnection c = connFactory.CreateConnection(); - IChannel m = _conn.CreateChannel(); object o = new object(); string q = GenerateQueueName(); - m.QueueDeclare(q, false, false, false, null); + _channel.QueueDeclare(q, false, false, false, null); CallbackExceptionEventArgs ea = null; - m.CallbackException += (_, evt) => + _channel.CallbackException += (_, evt) => { ea = evt; - c.Close(); + _channel.Close(); Monitor.PulseAll(o); }; - m.BasicConsume(q, true, new FaultyConsumer(_channel)); - m.BasicPublish("", q, _encoding.GetBytes("message")); + _channel.BasicConsume(q, true, new FaultyConsumer(_channel)); + _channel.BasicPublish("", q, _encoding.GetBytes("message")); WaitOn(o); Assert.NotNull(ea); - Assert.False(c.IsOpen); - Assert.Equal(200, c.CloseReason.ReplyCode); + Assert.False(_channel.IsOpen); + Assert.Equal(200, _channel.CloseReason.ReplyCode); } } } diff --git a/projects/Unit/TestNowait.cs b/projects/Test/Integration/TestNowait.cs similarity index 98% rename from projects/Unit/TestNowait.cs rename to projects/Test/Integration/TestNowait.cs index 33df3c69d4..4fa6f06b30 100644 --- a/projects/Unit/TestNowait.cs +++ b/projects/Test/Integration/TestNowait.cs @@ -29,10 +29,11 @@ // Copyright (c) 2007-2020 VMware, Inc. All rights reserved. //--------------------------------------------------------------------------- +using RabbitMQ.Client; using Xunit; using Xunit.Abstractions; -namespace RabbitMQ.Client.Unit +namespace Test.Integration { public class TestNoWait : IntegrationFixture { diff --git a/projects/Unit/TestPassiveDeclare.cs b/projects/Test/Integration/TestPassiveDeclare.cs similarity index 98% rename from projects/Unit/TestPassiveDeclare.cs rename to projects/Test/Integration/TestPassiveDeclare.cs index b4871d75bb..6063697159 100644 --- a/projects/Unit/TestPassiveDeclare.cs +++ b/projects/Test/Integration/TestPassiveDeclare.cs @@ -34,7 +34,7 @@ using Xunit; using Xunit.Abstractions; -namespace RabbitMQ.Client.Unit +namespace Test.Integration { public class TestPassiveDeclare : IntegrationFixture { diff --git a/projects/Unit/TestQueueDeclare.cs b/projects/Test/Integration/TestQueueDeclare.cs similarity index 63% rename from projects/Unit/TestQueueDeclare.cs rename to projects/Test/Integration/TestQueueDeclare.cs index 8896468527..d068993c63 100644 --- a/projects/Unit/TestQueueDeclare.cs +++ b/projects/Test/Integration/TestQueueDeclare.cs @@ -32,11 +32,11 @@ using System; using System.Collections.Generic; using System.Threading; -using System.Threading.Tasks; +using RabbitMQ.Client; using Xunit; using Xunit.Abstractions; -namespace RabbitMQ.Client.Unit +namespace Test.Integration { public class TestQueueDeclare : IntegrationFixture { @@ -45,20 +45,9 @@ public TestQueueDeclare(ITestOutputHelper output) : base(output) } [Fact] - public async void TestQueueDeclareAsync() - { - string q = GenerateQueueName(); - QueueDeclareOk result = await _channel.QueueDeclareAsync(q, false, false, false, null); - Assert.Equal(q, result.QueueName); - } - - [Fact] - [Trait("Category", "RequireSMP")] public void TestConcurrentQueueDeclare() { - string q = GenerateQueueName(); - var rnd = new Random(); - + var qs = new List(); var ts = new List(); NotSupportedException nse = null; for (int i = 0; i < 256; i++) @@ -69,8 +58,10 @@ public void TestConcurrentQueueDeclare() { // sleep for a random amount of time to increase the chances // of thread interleaving. MK. - Thread.Sleep(rnd.Next(5, 50)); + Thread.Sleep(S_Random.Next(5, 50)); + string q = GenerateQueueName(); _channel.QueueDeclare(q, false, false, false, null); + qs.Add(q); } catch (NotSupportedException e) { @@ -87,41 +78,32 @@ public void TestConcurrentQueueDeclare() } Assert.Null(nse); - _channel.QueueDelete(q); - } - - [Fact] - [Trait("Category", "RequireSMP")] - public async void TestConcurrentQueueDeclareAsync() - { - string q = GenerateQueueName(); - var rnd = new Random(); + ts.Clear(); - var ts = new List(); - NotSupportedException nse = null; - for (int i = 0; i < 256; i++) + foreach (string queueName in qs) { - async Task f() - { - try - { - // sleep for a random amount of time to increase the chances - // of thread interleaving. MK. - await Task.Delay(rnd.Next(5, 50)); - QueueDeclareOk r = await _channel.QueueDeclareAsync(q, false, false, false, null); - } - catch (NotSupportedException e) - { - nse = e; - } - } - var t = Task.Run(f); + var t = new Thread(() => + { + try + { + Thread.Sleep(S_Random.Next(5, 50)); + _channel.QueueDelete(queueName); + } + catch (NotSupportedException e) + { + nse = e; + } + }); ts.Add(t); + t.Start(); + } + + foreach (Thread t in ts) + { + t.Join(); } - await Task.WhenAll(ts); Assert.Null(nse); - _channel.QueueDelete(q); } } } diff --git a/projects/Unit/TestSsl.cs b/projects/Test/Integration/TestSsl.cs similarity index 70% rename from projects/Unit/TestSsl.cs rename to projects/Test/Integration/TestSsl.cs index 4695b188e5..8152ec38ce 100644 --- a/projects/Unit/TestSsl.cs +++ b/projects/Test/Integration/TestSsl.cs @@ -29,39 +29,39 @@ // Copyright (c) 2007-2020 VMware, Inc. All rights reserved. //--------------------------------------------------------------------------- -using System; using System.IO; using System.Net.Security; -using System.Reflection; using System.Security.Authentication; +using RabbitMQ.Client; using Xunit; using Xunit.Abstractions; -namespace RabbitMQ.Client.Unit +namespace Test.Integration { - [Collection("IntegrationFixture")] - public class TestSsl + public class TestSsl : IntegrationFixture { - private readonly ITestOutputHelper _output; - private readonly string _testDisplayName; private readonly SslEnv _sslEnv; - public TestSsl(ITestOutputHelper output) + public TestSsl(ITestOutputHelper output) : base(output) { - _output = output; - var type = _output.GetType(); - var testMember = type.GetField("test", BindingFlags.Instance | BindingFlags.NonPublic); - var test = (ITest)testMember.GetValue(output); - _testDisplayName = test.DisplayName; _sslEnv = new SslEnv(); } + protected override void SetUp() + { + Assert.Null(_connFactory); + Assert.Null(_conn); + Assert.Null(_channel); + } + [SkippableFact] public void TestServerVerifiedIgnoringNameMismatch() { Skip.IfNot(_sslEnv.IsSslConfigured, "SSL_CERTS_DIR and/or PASSWORD are not configured, skipping test"); - ConnectionFactory cf = new ConnectionFactory { Port = 5671 }; + var cf = CreateConnectionFactory(); + cf.Port = 5671; + cf.Ssl.ServerName = "*"; cf.Ssl.AcceptablePolicyErrors = SslPolicyErrors.RemoteCertificateNameMismatch; cf.Ssl.Enabled = true; @@ -73,7 +73,8 @@ public void TestServerVerified() { Skip.IfNot(_sslEnv.IsSslConfigured, "SSL_CERTS_DIR and/or PASSWORD are not configured, skipping test"); - ConnectionFactory cf = new ConnectionFactory { Port = 5671 }; + var cf = CreateConnectionFactory(); + cf.Port = 5671; cf.Ssl.ServerName = _sslEnv.Hostname; cf.Ssl.Enabled = true; SendReceive(cf); @@ -85,10 +86,10 @@ public void TestClientAndServerVerified() Skip.IfNot(_sslEnv.IsSslConfigured, "SSL_CERTS_DIR and/or PASSWORD are not configured, skipping test"); string certPath = _sslEnv.CertPath; - _output.WriteLine($"[INFO] certPath: {certPath}"); Assert.True(File.Exists(certPath)); - ConnectionFactory cf = new ConnectionFactory { Port = 5671 }; + var cf = CreateConnectionFactory(); + cf.Port = 5671; cf.Ssl.ServerName = _sslEnv.Hostname; cf.Ssl.CertPath = certPath; cf.Ssl.CertPassphrase = _sslEnv.CertPassphrase; @@ -102,19 +103,17 @@ public void TestNoClientCertificate() { Skip.IfNot(_sslEnv.IsSslConfigured, "SSL_CERTS_DIR and/or PASSWORD are not configured, skipping test"); - ConnectionFactory cf = new ConnectionFactory + var cf = CreateConnectionFactory(); + cf.Port = 5671; + cf.Ssl = new SslOption() { - Port = 5671, - Ssl = new SslOption() - { - CertPath = null, - Enabled = true, - ServerName = _sslEnv.Hostname, - Version = SslProtocols.None, - AcceptablePolicyErrors = - SslPolicyErrors.RemoteCertificateNotAvailable | - SslPolicyErrors.RemoteCertificateNameMismatch - } + CertPath = null, + Enabled = true, + ServerName = _sslEnv.Hostname, + Version = SslProtocols.None, + AcceptablePolicyErrors = + SslPolicyErrors.RemoteCertificateNotAvailable | + SslPolicyErrors.RemoteCertificateNameMismatch }; SendReceive(cf); @@ -122,7 +121,7 @@ public void TestNoClientCertificate() private void SendReceive(ConnectionFactory cf) { - using (IConnection conn = cf.CreateConnection($"{_testDisplayName}:{Guid.NewGuid()}")) + using (IConnection conn = cf.CreateConnection(_testDisplayName)) { using (IChannel ch = conn.CreateChannel()) { @@ -131,13 +130,13 @@ private void SendReceive(ConnectionFactory cf) ch.QueueBind(qName, "Exchange_TestSslEndPoint", "Key_TestSslEndpoint", null); string message = "Hello C# SSL Client World"; - byte[] msgBytes = System.Text.Encoding.UTF8.GetBytes(message); + byte[] msgBytes = _encoding.GetBytes(message); ch.BasicPublish("Exchange_TestSslEndPoint", "Key_TestSslEndpoint", msgBytes); bool autoAck = false; BasicGetResult result = ch.BasicGet(qName, autoAck); - byte[] body = result.Body.ToArray(); - string resultMessage = System.Text.Encoding.UTF8.GetString(body); + byte[] body = result.Body; + string resultMessage = _encoding.GetString(body); Assert.Equal(message, resultMessage); } diff --git a/projects/Unit/TestUpdateSecret.cs b/projects/Test/Integration/TestUpdateSecret.cs similarity index 95% rename from projects/Unit/TestUpdateSecret.cs rename to projects/Test/Integration/TestUpdateSecret.cs index b81ef5ce15..d2396ca831 100644 --- a/projects/Unit/TestUpdateSecret.cs +++ b/projects/Test/Integration/TestUpdateSecret.cs @@ -31,7 +31,7 @@ using Xunit.Abstractions; -namespace RabbitMQ.Client.Unit +namespace Test.Integration { public class TestUpdateSecret : IntegrationFixture { @@ -39,7 +39,6 @@ public TestUpdateSecret(ITestOutputHelper output) : base(output) { } - [IgnoreOnVersionsEarlierThan(3, 8)] public void TestUpdatingConnectionSecret() { _conn.UpdateSecret("new-secret", "Test Case"); diff --git a/projects/OAuth2Test/APIApproval.Approve.verified.txt b/projects/Test/OAuth2/APIApproval.Approve.verified.txt similarity index 100% rename from projects/OAuth2Test/APIApproval.Approve.verified.txt rename to projects/Test/OAuth2/APIApproval.Approve.verified.txt diff --git a/projects/OAuth2Test/APIApproval.cs b/projects/Test/OAuth2/APIApproval.cs similarity index 100% rename from projects/OAuth2Test/APIApproval.cs rename to projects/Test/OAuth2/APIApproval.cs diff --git a/projects/OAuth2Test/OAuth2Test.csproj b/projects/Test/OAuth2/OAuth2.csproj similarity index 75% rename from projects/OAuth2Test/OAuth2Test.csproj rename to projects/Test/OAuth2/OAuth2.csproj index e262304e8f..e11a42abb9 100644 --- a/projects/OAuth2Test/OAuth2Test.csproj +++ b/projects/Test/OAuth2/OAuth2.csproj @@ -9,15 +9,15 @@ - ../rabbit.snk + ../../rabbit.snk true latest 7.0 - - + + @@ -27,16 +27,15 @@ all - - + + runtime; build; native; contentfiles; analyzers; buildtransitive all - - + diff --git a/projects/OAuth2Test/README.md b/projects/Test/OAuth2/README.md similarity index 100% rename from projects/OAuth2Test/README.md rename to projects/Test/OAuth2/README.md diff --git a/projects/OAuth2Test/RequestFormMatcher.cs b/projects/Test/OAuth2/RequestFormMatcher.cs similarity index 100% rename from projects/OAuth2Test/RequestFormMatcher.cs rename to projects/Test/OAuth2/RequestFormMatcher.cs diff --git a/projects/OAuth2Test/TestOAuth2.cs b/projects/Test/OAuth2/TestOAuth2.cs similarity index 97% rename from projects/OAuth2Test/TestOAuth2.cs rename to projects/Test/OAuth2/TestOAuth2.cs index 9792d678f7..ba9db4a41c 100644 --- a/projects/OAuth2Test/TestOAuth2.cs +++ b/projects/Test/OAuth2/TestOAuth2.cs @@ -79,7 +79,8 @@ public TestOAuth2(ITestOutputHelper testOutputHelper) { AutomaticRecoveryEnabled = true, CredentialsProvider = GetCredentialsProvider(options), - CredentialsRefresher = GetCredentialsRefresher() + CredentialsRefresher = GetCredentialsRefresher(), + ClientProvidedName = nameof(TestOAuth2) }; _connection = connectionFactory.CreateConnection(); @@ -148,7 +149,7 @@ private async Task Publish(IChannel publisher) private async ValueTask declareConsumer() { IChannel subscriber = _connection.CreateChannel(); - await subscriber.QueueDeclareAsync("testqueue", true, false, false, arguments: null); + await subscriber.QueueDeclareAsync(queue: "testqueue", passive: false, true, false, false, arguments: null); subscriber.QueueBind("testqueue", Exchange, "hello"); return subscriber; } diff --git a/projects/OAuth2Test/TestOAuth2Client.cs b/projects/Test/OAuth2/TestOAuth2Client.cs similarity index 100% rename from projects/OAuth2Test/TestOAuth2Client.cs rename to projects/Test/OAuth2/TestOAuth2Client.cs diff --git a/projects/OAuth2Test/TestOAuth2ClientCredentialsProvider.cs b/projects/Test/OAuth2/TestOAuth2ClientCredentialsProvider.cs similarity index 100% rename from projects/OAuth2Test/TestOAuth2ClientCredentialsProvider.cs rename to projects/Test/OAuth2/TestOAuth2ClientCredentialsProvider.cs diff --git a/projects/OAuth2Test/enabled_plugins b/projects/Test/OAuth2/enabled_plugins similarity index 100% rename from projects/OAuth2Test/enabled_plugins rename to projects/Test/OAuth2/enabled_plugins diff --git a/projects/OAuth2Test/keycloak/import/test-realm.json b/projects/Test/OAuth2/keycloak/import/test-realm.json similarity index 100% rename from projects/OAuth2Test/keycloak/import/test-realm.json rename to projects/Test/OAuth2/keycloak/import/test-realm.json diff --git a/projects/OAuth2Test/keycloak/rabbitmq.conf b/projects/Test/OAuth2/keycloak/rabbitmq.conf similarity index 100% rename from projects/OAuth2Test/keycloak/rabbitmq.conf rename to projects/Test/OAuth2/keycloak/rabbitmq.conf diff --git a/projects/OAuth2Test/keycloak/signing-key/signing-key.pem b/projects/Test/OAuth2/keycloak/signing-key/signing-key.pem similarity index 100% rename from projects/OAuth2Test/keycloak/signing-key/signing-key.pem rename to projects/Test/OAuth2/keycloak/signing-key/signing-key.pem diff --git a/projects/OAuth2Test/uaa/log4j2.properties b/projects/Test/OAuth2/uaa/log4j2.properties similarity index 100% rename from projects/OAuth2Test/uaa/log4j2.properties rename to projects/Test/OAuth2/uaa/log4j2.properties diff --git a/projects/OAuth2Test/uaa/rabbitmq.conf b/projects/Test/OAuth2/uaa/rabbitmq.conf similarity index 100% rename from projects/OAuth2Test/uaa/rabbitmq.conf rename to projects/Test/OAuth2/uaa/rabbitmq.conf diff --git a/projects/OAuth2Test/uaa/signing-key/signing-key.pem b/projects/Test/OAuth2/uaa/signing-key/signing-key.pem similarity index 100% rename from projects/OAuth2Test/uaa/signing-key/signing-key.pem rename to projects/Test/OAuth2/uaa/signing-key/signing-key.pem diff --git a/projects/OAuth2Test/uaa/uaa.yml b/projects/Test/OAuth2/uaa/uaa.yml similarity index 100% rename from projects/OAuth2Test/uaa/uaa.yml rename to projects/Test/OAuth2/uaa/uaa.yml diff --git a/projects/Test/SequentialIntegration/SequentialIntegration.csproj b/projects/Test/SequentialIntegration/SequentialIntegration.csproj new file mode 100644 index 0000000000..9ab21fd9ea --- /dev/null +++ b/projects/Test/SequentialIntegration/SequentialIntegration.csproj @@ -0,0 +1,50 @@ + + + + net6.0;net472 + + + + net6.0 + + + + ../../rabbit.snk + true + latest + 7.0 + true + + + + + + + + + + + <_Parameter1>Xunit.CollectionBehavior.CollectionPerAssembly + <_Parameter1_IsLiteral>true + <_Parameter1_TypeName>Xunit.CollectionBehavior.CollectionPerAssembly + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + diff --git a/projects/Test/SequentialIntegration/SequentialIntegrationFixture.cs b/projects/Test/SequentialIntegration/SequentialIntegrationFixture.cs new file mode 100644 index 0000000000..9d769bfc8f --- /dev/null +++ b/projects/Test/SequentialIntegration/SequentialIntegrationFixture.cs @@ -0,0 +1,89 @@ +// This source code is dual-licensed under the Apache License, version +// 2.0, and the Mozilla Public License, version 2.0. +// +// The APL v2.0: +// +//--------------------------------------------------------------------------- +// Copyright (c) 2007-2020 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//--------------------------------------------------------------------------- +// +// The MPL v2.0: +// +//--------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// +// Copyright (c) 2007-2020 VMware, Inc. All rights reserved. +//--------------------------------------------------------------------------- + +using System; +using System.Threading; +using RabbitMQ.Client; +using Xunit.Abstractions; + +namespace Test.SequentialIntegration +{ + public class SequentialIntegrationFixture : IntegrationFixture + { + public SequentialIntegrationFixture(ITestOutputHelper output) : base(output) + { + } + + public void Block() + { + _rabbitMQCtl.ExecRabbitMQCtl("set_vm_memory_high_watermark 0.000000001"); + // give rabbitmqctl some time to do its job + Thread.Sleep(TimeSpan.FromSeconds(2)); + Publish(); + } + + public void Unblock() + { + _rabbitMQCtl.ExecRabbitMQCtl("set_vm_memory_high_watermark 0.4"); + } + + public void RestartRabbitMQ() + { + StopRabbitMQ(); + Thread.Sleep(TimeSpan.FromMilliseconds(500)); + StartRabbitMQ(); + AwaitRabbitMQ(); + } + + public void StopRabbitMQ() + { + _rabbitMQCtl.ExecRabbitMQCtl("stop_app"); + } + + public void StartRabbitMQ() + { + _rabbitMQCtl.ExecRabbitMQCtl("start_app"); + } + + private void AwaitRabbitMQ() + { + _rabbitMQCtl.ExecRabbitMQCtl("await_startup"); + } + + private void Publish() + { + using (IChannel ch = _conn.CreateChannel()) + { + ch.BasicPublish("amq.fanout", "", _encoding.GetBytes("message")); + } + } + } +} diff --git a/projects/Unit/TestConnectionBlocked.cs b/projects/Test/SequentialIntegration/TestConnectionBlocked.cs similarity index 96% rename from projects/Unit/TestConnectionBlocked.cs rename to projects/Test/SequentialIntegration/TestConnectionBlocked.cs index 4d9986aad6..232a83b954 100644 --- a/projects/Unit/TestConnectionBlocked.cs +++ b/projects/Test/SequentialIntegration/TestConnectionBlocked.cs @@ -36,9 +36,9 @@ using Xunit; using Xunit.Abstractions; -namespace RabbitMQ.Client.Unit +namespace Test.SequentialIntegration { - public class TestConnectionBlocked : IntegrationFixture + public class TestConnectionBlocked : SequentialIntegrationFixture { private readonly ManualResetEventSlim _connDisposed = new ManualResetEventSlim(false); private readonly object _lockObject = new object(); @@ -100,7 +100,7 @@ public void TestDisposeOnBlockedConnectionDoesNotHang() } } - protected override void ReleaseResources() + protected override void TearDown() { Unblock(); } diff --git a/projects/Unit/TestConnectionRecovery.cs b/projects/Test/SequentialIntegration/TestConnectionRecovery.cs similarity index 67% rename from projects/Unit/TestConnectionRecovery.cs rename to projects/Test/SequentialIntegration/TestConnectionRecovery.cs index 2c8c42ab50..fada4e8948 100644 --- a/projects/Unit/TestConnectionRecovery.cs +++ b/projects/Test/SequentialIntegration/TestConnectionRecovery.cs @@ -31,9 +31,9 @@ using System; using System.Collections.Generic; -using System.Text; using System.Threading; using System.Threading.Tasks; +using RabbitMQ.Client; using RabbitMQ.Client.Events; using RabbitMQ.Client.Exceptions; using RabbitMQ.Client.Framing.Impl; @@ -41,46 +41,25 @@ using Xunit; using Xunit.Abstractions; -#pragma warning disable 0618 - -namespace RabbitMQ.Client.Unit +namespace Test.SequentialIntegration { - public class TestConnectionRecovery : IntegrationFixture + public class TestConnectionRecovery : TestConnectionRecoveryBase { - private readonly byte[] _messageBody; - private readonly ushort _totalMessageCount = 8192; - private readonly ushort _closeAtCount = 16; - private string _queueName; + private readonly string _queueName; public TestConnectionRecovery(ITestOutputHelper output) : base(output) - { - var rnd = new Random(); - _messageBody = new byte[4096]; - rnd.NextBytes(_messageBody); - } - - protected override void SetUp() { _queueName = $"TestConnectionRecovery-queue-{Guid.NewGuid()}"; - _conn = CreateAutorecoveringConnection(); - _channel = _conn.CreateChannel(); - _channel.QueueDelete(_queueName); } - protected override void ReleaseResources() + protected override void TearDown() { - // TODO LRB not really necessary - if (_channel.IsOpen) - { - _channel.Close(); - } - - if (_conn.IsOpen) - { - _conn.Close(); - } - - Unblock(); + var cf = CreateConnectionFactory(); + cf.ClientProvidedName = cf.ClientProvidedName + "-TearDown"; + using IConnection conn = cf.CreateConnection(); + using IChannel ch = conn.CreateChannel(); + ch.QueueDelete(_queueName); + base.TearDown(); } [Fact] @@ -93,16 +72,16 @@ public void TestBasicAckAfterChannelRecovery() Assert.Equal(queueName, _queueName); _channel.BasicQos(0, 1, false); - string consumerTag = _channel.BasicConsume(queueName, false, cons); + _channel.BasicConsume(queueName, false, cons); ManualResetEventSlim sl = PrepareForShutdown(_conn); ManualResetEventSlim rl = PrepareForRecovery(_conn); PublishMessagesWhileClosingConn(queueName); - Wait(sl); - Wait(rl); - Wait(allMessagesSeenLatch); + Wait(sl, "connection shutdown"); + Wait(rl, "connection recovery"); + Wait(allMessagesSeenLatch, "all messages seen"); } [Fact] @@ -115,16 +94,16 @@ public void TestBasicNackAfterChannelRecovery() Assert.Equal(queueName, _queueName); _channel.BasicQos(0, 1, false); - string consumerTag = _channel.BasicConsume(queueName, false, cons); + _channel.BasicConsume(queueName, false, cons); ManualResetEventSlim sl = PrepareForShutdown(_conn); ManualResetEventSlim rl = PrepareForRecovery(_conn); PublishMessagesWhileClosingConn(queueName); - Wait(sl); - Wait(rl); - Wait(allMessagesSeenLatch); + Wait(sl, "connection shutdown"); + Wait(rl, "connection recovery"); + Wait(allMessagesSeenLatch, "all messages seen"); } [Fact] @@ -137,16 +116,16 @@ public void TestBasicRejectAfterChannelRecovery() Assert.Equal(queueName, _queueName); _channel.BasicQos(0, 1, false); - string consumerTag = _channel.BasicConsume(queueName, false, cons); + _channel.BasicConsume(queueName, false, cons); ManualResetEventSlim sl = PrepareForShutdown(_conn); ManualResetEventSlim rl = PrepareForRecovery(_conn); PublishMessagesWhileClosingConn(queueName); - Wait(sl); - Wait(rl); - Wait(allMessagesSeenLatch); + Wait(sl, "connection shutdown"); + Wait(rl, "connection recovery"); + Wait(allMessagesSeenLatch, "all messages seen"); } [Fact] @@ -180,7 +159,7 @@ public void TestBasicAckEventHandlerRecovery() Assert.True(_channel.IsOpen); WithTemporaryNonExclusiveQueue(_channel, (m, q) => m.BasicPublish("", q, _messageBody)); - Wait(latch); + Wait(latch, "basic acks/nacks"); } [Fact] @@ -191,86 +170,6 @@ public void TestBasicConnectionRecovery() Assert.True(_conn.IsOpen); } - [Fact] - public void TestBasicConnectionRecoveryWithHostnameList() - { - using (AutorecoveringConnection c = CreateAutorecoveringConnection(new List { "127.0.0.1", "localhost" })) - { - Assert.True(c.IsOpen); - CloseAndWaitForRecovery(c); - Assert.True(c.IsOpen); - } - } - - [Fact] - public void TestBasicConnectionRecoveryWithHostnameListAndUnreachableHosts() - { - using (AutorecoveringConnection c = CreateAutorecoveringConnection(new List { "191.72.44.22", "127.0.0.1", "localhost" })) - { - Assert.True(c.IsOpen); - CloseAndWaitForRecovery(c); - Assert.True(c.IsOpen); - } - } - - [Fact] - public void TestBasicConnectionRecoveryWithEndpointList() - { - using (AutorecoveringConnection c = CreateAutorecoveringConnection( - new List - { - new AmqpTcpEndpoint("127.0.0.1"), - new AmqpTcpEndpoint("localhost") - })) - { - Assert.True(c.IsOpen); - CloseAndWaitForRecovery(c); - Assert.True(c.IsOpen); - } - } - - [Fact] - public void TestBasicConnectionRecoveryStopsAfterManualClose() - { - Assert.True(_conn.IsOpen); - AutorecoveringConnection c = CreateAutorecoveringConnection(); - var latch = new AutoResetEvent(false); - c.ConnectionRecoveryError += (o, args) => latch.Set(); - - try - { - StopRabbitMQ(); - latch.WaitOne(30000); // we got the failed reconnection event. - bool triedRecoveryAfterClose = false; - c.Close(); - Thread.Sleep(5000); - c.ConnectionRecoveryError += (o, args) => triedRecoveryAfterClose = true; - Thread.Sleep(10000); - Assert.False(triedRecoveryAfterClose); - } - finally - { - StartRabbitMQ(); - } - } - - [Fact] - public void TestBasicConnectionRecoveryWithEndpointListAndUnreachableHosts() - { - using (AutorecoveringConnection c = CreateAutorecoveringConnection( - new List - { - new AmqpTcpEndpoint("191.72.44.22"), - new AmqpTcpEndpoint("127.0.0.1"), - new AmqpTcpEndpoint("localhost") - })) - { - Assert.True(c.IsOpen); - CloseAndWaitForRecovery(c); - Assert.True(c.IsOpen); - } - } - [Fact] public void TestBasicConnectionRecoveryOnBrokerRestart() { @@ -304,7 +203,7 @@ public void TestBlockedListenersRecovery() CloseAndWaitForRecovery(); Block(); - Wait(latch); + Wait(latch, "connection blocked"); Unblock(); } @@ -344,104 +243,6 @@ public void TestClientNamedQueueRecoveryOnServerRestart() }, s); } - [Fact] - public void TestConsumerWorkServiceRecovery() - { - using (AutorecoveringConnection c = CreateAutorecoveringConnection()) - { - IChannel m = c.CreateChannel(); - string q = m.QueueDeclare("dotnet-client.recovery.consumer_work_pool1", - false, false, false, null).QueueName; - var cons = new EventingBasicConsumer(m); - m.BasicConsume(q, true, cons); - AssertConsumerCount(m, q, 1); - - CloseAndWaitForRecovery(c); - - Assert.True(m.IsOpen); - var latch = new ManualResetEventSlim(false); - cons.Received += (s, args) => latch.Set(); - - m.BasicPublish("", q, _encoding.GetBytes("msg")); - Wait(latch); - - m.QueueDelete(q); - } - } - - [Fact] - public void TestConsumerRecoveryOnClientNamedQueueWithOneRecovery() - { - string q0 = "dotnet-client.recovery.queue1"; - using (AutorecoveringConnection c = CreateAutorecoveringConnection()) - { - IChannel m = c.CreateChannel(); - string q1 = m.QueueDeclare(q0, false, false, false, null).QueueName; - Assert.Equal(q0, q1); - - var cons = new EventingBasicConsumer(m); - m.BasicConsume(q1, true, cons); - AssertConsumerCount(m, q1, 1); - - bool queueNameChangeAfterRecoveryCalled = false; - - c.QueueNameChangeAfterRecovery += (source, ea) => { queueNameChangeAfterRecoveryCalled = true; }; - - CloseAndWaitForRecovery(c); - AssertConsumerCount(m, q1, 1); - Assert.False(queueNameChangeAfterRecoveryCalled); - - CloseAndWaitForRecovery(c); - AssertConsumerCount(m, q1, 1); - Assert.False(queueNameChangeAfterRecoveryCalled); - - CloseAndWaitForRecovery(c); - AssertConsumerCount(m, q1, 1); - Assert.False(queueNameChangeAfterRecoveryCalled); - - var latch = new ManualResetEventSlim(false); - cons.Received += (s, args) => latch.Set(); - - m.BasicPublish("", q1, _encoding.GetBytes("msg")); - Wait(latch); - - m.QueueDelete(q1); - } - } - - [Fact] - public void TestConsumerRecoveryWithServerNamedQueue() - { - // https://github.com/rabbitmq/rabbitmq-dotnet-client/issues/1238 - using (AutorecoveringConnection c = CreateAutorecoveringConnection()) - { - IChannel ch = c.CreateChannel(); - QueueDeclareOk queueDeclareResult = ch.QueueDeclare(queue: string.Empty, durable: false, exclusive: true, autoDelete: true, arguments: null); - string qname = queueDeclareResult.QueueName; - Assert.False(string.IsNullOrEmpty(qname)); - - var cons = new EventingBasicConsumer(ch); - ch.BasicConsume(string.Empty, true, cons); - AssertConsumerCount(ch, qname, 1); - - bool queueNameBeforeIsEqual = false; - bool queueNameChangeAfterRecoveryCalled = false; - string qnameAfterRecovery = null; - c.QueueNameChangeAfterRecovery += (source, ea) => - { - queueNameChangeAfterRecoveryCalled = true; - queueNameBeforeIsEqual = qname.Equals(ea.NameBefore); - qnameAfterRecovery = ea.NameAfter; - }; - - CloseAndWaitForRecovery(c); - - AssertConsumerCount(ch, qnameAfterRecovery, 1); - Assert.True(queueNameChangeAfterRecoveryCalled); - Assert.True(queueNameBeforeIsEqual); - } - } - [Fact] public void TestConsumerRecoveryWithManyConsumers() { @@ -458,39 +259,11 @@ public void TestConsumerRecoveryWithManyConsumers() ((AutorecoveringConnection)_conn).ConsumerTagChangeAfterRecovery += (prev, current) => latch.Set(); CloseAndWaitForRecovery(); - Wait(latch); + Wait(latch, "consumer tag change after recovery"); Assert.True(_channel.IsOpen); AssertConsumerCount(q, n); } - [Fact] - public void TestCreateChannelOnClosedAutorecoveringConnectionDoesNotHang() - { - // we don't want this to recover quickly in this test - AutorecoveringConnection c = CreateAutorecoveringConnection(TimeSpan.FromSeconds(20)); - - try - { - c.Close(); - WaitForShutdown(c); - Assert.False(c.IsOpen); - c.CreateChannel(); - Assert.Fail("Expected an exception"); - } - catch (AlreadyClosedException) - { - // expected - } - finally - { - StartRabbitMQ(); - if (c.IsOpen) - { - c.Abort(); - } - } - } - [Fact] public void TestDeclarationOfManyAutoDeleteExchangesWithTransientExchangesThatAreDeleted() { @@ -532,7 +305,7 @@ public void TestDeclarationOfManyAutoDeleteExchangesWithTransientQueuesThatAreDe { string x = Guid.NewGuid().ToString(); _channel.ExchangeDeclare(x, "fanout", false, true, null); - QueueDeclareOk q = _channel.QueueDeclare(); + RabbitMQ.Client.QueueDeclareOk q = _channel.QueueDeclare(); _channel.QueueBind(q, x, ""); _channel.QueueDelete(q); } @@ -547,7 +320,7 @@ public void TestDeclarationOfManyAutoDeleteExchangesWithTransientQueuesThatAreUn { string x = Guid.NewGuid().ToString(); _channel.ExchangeDeclare(x, "fanout", false, true, null); - QueueDeclareOk q = _channel.QueueDeclare(); + RabbitMQ.Client.QueueDeclareOk q = _channel.QueueDeclare(); _channel.QueueBind(q, x, ""); _channel.QueueUnbind(q, x, "", null); } @@ -654,7 +427,7 @@ public void TestClientNamedTransientAutoDeleteQueueAndBindingRecovery() ch.ExchangeDeclare(exchange: x, type: "fanout"); ch.BasicPublish(exchange: x, routingKey: "", body: _encoding.GetBytes("msg")); WaitForConfirms(ch); - QueueDeclareOk ok = ch.QueueDeclare(queue: q, durable: false, exclusive: false, autoDelete: true, arguments: null); + RabbitMQ.Client.QueueDeclareOk ok = ch.QueueDeclare(queue: q, durable: false, exclusive: false, autoDelete: true, arguments: null); Assert.Equal(1u, ok.MessageCount); ch.QueueDelete(q); ch.ExchangeDelete(x); @@ -680,14 +453,14 @@ public void TestServerNamedTransientAutoDeleteQueueAndBindingRecovery() }; ch.QueueBind(queue: nameBefore, exchange: x, routingKey: ""); RestartServerAndWaitForRecovery(); - Wait(latch); + Wait(latch, "queue name change after recovery"); Assert.True(ch.IsOpen); Assert.NotEqual(nameBefore, nameAfter); ch.ConfirmSelect(); ch.ExchangeDeclare(exchange: x, type: "fanout"); ch.BasicPublish(exchange: x, routingKey: "", body: _encoding.GetBytes("msg")); WaitForConfirms(ch); - QueueDeclareOk ok = ch.QueueDeclarePassive(nameAfter); + RabbitMQ.Client.QueueDeclareOk ok = ch.QueueDeclarePassive(nameAfter); Assert.Equal(1u, ok.MessageCount); ch.QueueDelete(q); ch.ExchangeDelete(x); @@ -768,34 +541,6 @@ public void TestRecoveringConsumerHandlerOnConnection_EventArgumentsArePassedDow Assert.Equal("event-handler-set-this-value", actualVal); } - [Fact] - public void TestRecoveryWithTopologyDisabled() - { - AutorecoveringConnection conn = CreateAutorecoveringConnectionWithTopologyRecoveryDisabled(); - IChannel ch = conn.CreateChannel(); - string s = "dotnet-client.test.recovery.q2"; - ch.QueueDelete(s); - ch.QueueDeclare(s, false, true, false, null); - ch.QueueDeclarePassive(s); - Assert.True(ch.IsOpen); - - try - { - CloseAndWaitForRecovery(conn); - Assert.True(ch.IsOpen); - ch.QueueDeclarePassive(s); - Assert.Fail("Expected an exception"); - } - catch (OperationInterruptedException) - { - // expected - } - finally - { - conn.Abort(); - } - } - [Fact] public void TestServerNamedQueueRecovery() { @@ -812,7 +557,7 @@ public void TestServerNamedQueueRecovery() connection.QueueNameChangeAfterRecovery += (source, ea) => { nameAfter = ea.NameAfter; }; CloseAndWaitForRecovery(); - Wait(latch); + Wait(latch, "recovery succeeded"); Assert.NotNull(nameAfter); Assert.StartsWith("amq.", nameBefore); @@ -851,7 +596,6 @@ public void TestShutdownEventHandlersRecoveryOnConnectionAfterDelayedServerResta try { StopRabbitMQ(); - Console.WriteLine("Stopped RabbitMQ. About to sleep for multiple recovery intervals..."); Thread.Sleep(7000); } finally @@ -859,8 +603,8 @@ public void TestShutdownEventHandlersRecoveryOnConnectionAfterDelayedServerResta StartRabbitMQ(); } - Wait(shutdownLatch, TimeSpan.FromSeconds(30)); - Wait(recoveryLatch, TimeSpan.FromSeconds(30)); + Wait(shutdownLatch, WaitSpan, "connection shutdown"); + Wait(recoveryLatch, WaitSpan, "connection recovery"); Assert.True(_conn.IsOpen); Assert.True(counter >= 1); } @@ -906,7 +650,7 @@ public void TestRecoverTopologyOnDisposedChannel() cons.Received += (s, args) => latch.Set(); _channel.BasicPublish("", q, _messageBody); - Wait(latch); + Wait(latch, "received event"); _channel.QueueUnbind(q, x, rk); _channel.ExchangeDelete(x); @@ -1083,7 +827,7 @@ public void TestUnblockedListenersRecovery() Block(); Unblock(); - Wait(latch); + Wait(latch, "connection unblocked"); } [Fact] @@ -1109,7 +853,7 @@ public void TestTopologyRecoveryQueueFilter() try { CloseAndWaitForRecovery(conn); - Wait(latch); + Wait(latch, "recovery succeeded"); Assert.True(ch.IsOpen); AssertQueueRecovery(ch, queueToRecover, false); @@ -1153,7 +897,7 @@ public void TestTopologyRecoveryExchangeFilter() try { CloseAndWaitForRecovery(conn); - Wait(latch); + Wait(latch, "recovery succeeded"); Assert.True(ch.IsOpen); AssertExchangeRecovery(ch, exchangeToRecover); @@ -1206,78 +950,11 @@ public void TestTopologyRecoveryBindingFilter() try { CloseAndWaitForRecovery(conn); - Wait(latch); - - Assert.True(ch.IsOpen); - Assert.True(SendAndConsumeMessage(queueWithRecoveredBinding, exchange, bindingToRecover)); - Assert.False(SendAndConsumeMessage(queueWithIgnoredBinding, exchange, bindingToIgnore)); - } - finally - { - conn.Abort(); - } - } - - [Fact] - public void TestTopologyRecoveryConsumerFilter() - { - var filter = new TopologyRecoveryFilter - { - ConsumerFilter = consumer => !consumer.ConsumerTag.Contains("filtered") - }; - var latch = new ManualResetEventSlim(false); - AutorecoveringConnection conn = CreateAutorecoveringConnectionWithTopologyRecoveryFilter(filter); - conn.RecoverySucceeded += (source, ea) => latch.Set(); - IChannel ch = conn.CreateChannel(); - ch.ConfirmSelect(); - - var exchange = "topology.recovery.exchange"; - var queueWithRecoveredConsumer = "topology.recovery.queue.1"; - var queueWithIgnoredConsumer = "topology.recovery.queue.2"; - var binding1 = "recovered.binding.1"; - var binding2 = "recovered.binding.2"; - - ch.ExchangeDeclare(exchange, "direct"); - ch.QueueDeclare(queueWithRecoveredConsumer, false, false, false, null); - ch.QueueDeclare(queueWithIgnoredConsumer, false, false, false, null); - ch.QueueBind(queueWithRecoveredConsumer, exchange, binding1); - ch.QueueBind(queueWithIgnoredConsumer, exchange, binding2); - ch.QueuePurge(queueWithRecoveredConsumer); - ch.QueuePurge(queueWithIgnoredConsumer); - - var recoverLatch = new ManualResetEventSlim(false); - var consumerToRecover = new EventingBasicConsumer(ch); - consumerToRecover.Received += (source, ea) => recoverLatch.Set(); - ch.BasicConsume(queueWithRecoveredConsumer, true, "recovered.consumer", consumerToRecover); - - var ignoredLatch = new ManualResetEventSlim(false); - var consumerToIgnore = new EventingBasicConsumer(ch); - consumerToIgnore.Received += (source, ea) => ignoredLatch.Set(); - ch.BasicConsume(queueWithIgnoredConsumer, true, "filtered.consumer", consumerToIgnore); - - try - { - CloseAndWaitForRecovery(conn); - Wait(latch); + Wait(latch, "recovery succeeded"); Assert.True(ch.IsOpen); - ch.BasicPublish(exchange, binding1, Encoding.UTF8.GetBytes("test message")); - ch.BasicPublish(exchange, binding2, Encoding.UTF8.GetBytes("test message")); - - Assert.True(recoverLatch.Wait(TimeSpan.FromSeconds(5))); - Assert.False(ignoredLatch.Wait(TimeSpan.FromSeconds(5))); - - ch.BasicConsume(queueWithIgnoredConsumer, true, "filtered.consumer", consumerToIgnore); - - try - { - ch.BasicConsume(queueWithRecoveredConsumer, true, "recovered.consumer", consumerToRecover); - Assert.Fail("Expected an exception"); - } - catch (OperationInterruptedException e) - { - AssertShutdownError(e.ShutdownReason, 530); // NOT_ALLOWED - not allowed to reuse consumer tag - } + Assert.True(SendAndConsumeMessage(_conn, queueWithRecoveredBinding, exchange, bindingToRecover)); + Assert.False(SendAndConsumeMessage(_conn, queueWithIgnoredBinding, exchange, bindingToIgnore)); } finally { @@ -1326,15 +1003,15 @@ public void TestTopologyRecoveryDefaultFilterRecoversAllEntities() try { CloseAndWaitForRecovery(conn); - Wait(latch); + Wait(latch, "recovery succeeded"); Assert.True(ch.IsOpen); AssertExchangeRecovery(ch, exchange); ch.QueueDeclarePassive(queue1); ch.QueueDeclarePassive(queue2); - ch.BasicPublish(exchange, binding1, Encoding.UTF8.GetBytes("test message")); - ch.BasicPublish(exchange, binding2, Encoding.UTF8.GetBytes("test message")); + ch.BasicPublish(exchange, binding1, _encoding.GetBytes("test message")); + ch.BasicPublish(exchange, binding2, _encoding.GetBytes("test message")); Assert.True(consumerLatch1.Wait(TimeSpan.FromSeconds(5))); Assert.True(consumerLatch2.Wait(TimeSpan.FromSeconds(5))); @@ -1385,7 +1062,7 @@ public void TestTopologyRecoveryQueueExceptionHandler() try { CloseAndWaitForRecovery(conn); - Wait(latch); + Wait(latch, "recovery succeded"); Assert.True(ch.IsOpen); AssertQueueRecovery(ch, queueToRecoverSuccessfully, false); @@ -1436,7 +1113,7 @@ public void TestTopologyRecoveryExchangeExceptionHandler() try { CloseAndWaitForRecovery(conn); - Wait(latch); + Wait(latch, "recovery succeeded"); Assert.True(ch.IsOpen); AssertExchangeRecovery(ch, exchangeToRecoverSuccessfully); @@ -1499,11 +1176,11 @@ public void TestTopologyRecoveryBindingExceptionHandler() try { CloseAndWaitForRecovery(conn); - Wait(latch); + Wait(latch, "recovery succeeded"); Assert.True(ch.IsOpen); - Assert.True(SendAndConsumeMessage(queueWithRecoveredBinding, exchange, bindingToRecoverSuccessfully)); - Assert.True(SendAndConsumeMessage(queueWithExceptionBinding, exchange, bindingToRecoverWithException)); + Assert.True(SendAndConsumeMessage(conn, queueWithRecoveredBinding, exchange, bindingToRecoverSuccessfully)); + Assert.True(SendAndConsumeMessage(conn, queueWithExceptionBinding, exchange, bindingToRecoverWithException)); } finally { @@ -1555,11 +1232,11 @@ public void TestTopologyRecoveryConsumerExceptionHandler() try { CloseAndWaitForShutdown(conn); - Wait(latch, TimeSpan.FromSeconds(20)); + Wait(latch, TimeSpan.FromSeconds(20), "recovery succeeded"); Assert.True(ch.IsOpen); - ch.BasicPublish("", queueWithExceptionConsumer, Encoding.UTF8.GetBytes("test message")); + ch.BasicPublish("", queueWithExceptionConsumer, _encoding.GetBytes("test message")); Assert.True(recoverLatch.Wait(TimeSpan.FromSeconds(5))); @@ -1578,236 +1255,5 @@ public void TestTopologyRecoveryConsumerExceptionHandler() conn.Abort(); } } - - internal bool SendAndConsumeMessage(string queue, string exchange, string routingKey) - { - using (var ch = _conn.CreateChannel()) - { - var latch = new ManualResetEventSlim(false); - - var consumer = new AckingBasicConsumer(ch, 1, latch); - - ch.BasicConsume(queue, false, consumer); - - ch.BasicPublish(exchange, routingKey, Encoding.UTF8.GetBytes("test message")); - - return latch.Wait(TimeSpan.FromSeconds(5)); - } - } - - internal void AssertExchangeRecovery(IChannel m, string x) - { - m.ConfirmSelect(); - WithTemporaryNonExclusiveQueue(m, (_, q) => - { - string rk = "routing-key"; - m.QueueBind(q, x, rk); - m.BasicPublish(x, rk, _messageBody); - - Assert.True(WaitForConfirms(m)); - m.ExchangeDeclarePassive(x); - }); - } - - internal void AssertQueueRecovery(IChannel m, string q) - { - AssertQueueRecovery(m, q, true); - } - - internal void AssertQueueRecovery(IChannel m, string q, bool exclusive, IDictionary arguments = null) - { - m.ConfirmSelect(); - m.QueueDeclarePassive(q); - QueueDeclareOk ok1 = m.QueueDeclare(q, false, exclusive, false, arguments); - Assert.Equal(0u, ok1.MessageCount); - m.BasicPublish("", q, _messageBody); - Assert.True(WaitForConfirms(m)); - QueueDeclareOk ok2 = m.QueueDeclare(q, false, exclusive, false, arguments); - Assert.Equal(1u, ok2.MessageCount); - } - - internal void AssertRecordedExchanges(AutorecoveringConnection c, int n) - { - Assert.Equal(n, c.RecordedExchangesCount); - } - - internal void AssertRecordedQueues(AutorecoveringConnection c, int n) - { - Assert.Equal(n, c.RecordedQueuesCount); - } - - internal void CloseAndWaitForRecovery() - { - CloseAndWaitForRecovery((AutorecoveringConnection)_conn); - } - - internal void CloseAndWaitForRecovery(AutorecoveringConnection conn) - { - ManualResetEventSlim sl = PrepareForShutdown(conn); - ManualResetEventSlim rl = PrepareForRecovery(conn); - CloseConnection(conn); - Wait(sl); - Wait(rl); - } - - internal void CloseAndWaitForShutdown(AutorecoveringConnection conn) - { - ManualResetEventSlim sl = PrepareForShutdown(conn); - CloseConnection(conn); - Wait(sl); - } - - internal ManualResetEventSlim PrepareForRecovery(IConnection conn) - { - var latch = new ManualResetEventSlim(false); - - AutorecoveringConnection aconn = conn as AutorecoveringConnection; - aconn.RecoverySucceeded += (source, ea) => latch.Set(); - - return latch; - } - - internal static ManualResetEventSlim PrepareForShutdown(IConnection conn) - { - var latch = new ManualResetEventSlim(false); - - AutorecoveringConnection aconn = conn as AutorecoveringConnection; - aconn.ConnectionShutdown += (c, args) => latch.Set(); - - return latch; - } - - internal void RestartServerAndWaitForRecovery() - { - RestartServerAndWaitForRecovery((AutorecoveringConnection)_conn); - } - - internal void RestartServerAndWaitForRecovery(AutorecoveringConnection conn) - { - ManualResetEventSlim sl = PrepareForShutdown(conn); - ManualResetEventSlim rl = PrepareForRecovery(conn); - RestartRabbitMQ(); - Wait(sl); - Wait(rl); - } - - internal void WaitForRecovery() - { - Wait(PrepareForRecovery((AutorecoveringConnection)_conn)); - } - - internal void WaitForRecovery(AutorecoveringConnection conn) - { - Wait(PrepareForRecovery(conn)); - } - - internal void WaitForShutdown() - { - Wait(PrepareForShutdown(_conn)); - } - - internal void WaitForShutdown(IConnection conn) - { - Wait(PrepareForShutdown(conn)); - } - - internal void PublishMessagesWhileClosingConn(string queueName) - { - using (AutorecoveringConnection publishingConn = CreateAutorecoveringConnection()) - { - using (IChannel publishingChannel = publishingConn.CreateChannel()) - { - for (ushort i = 0; i < _totalMessageCount; i++) - { - if (i == _closeAtCount) - { - CloseConnection(_conn); - } - publishingChannel.BasicPublish(string.Empty, queueName, _messageBody); - } - } - } - } - - public class AckingBasicConsumer : TestBasicConsumer - { - public AckingBasicConsumer(IChannel channel, ushort totalMessageCount, ManualResetEventSlim allMessagesSeenLatch) - : base(channel, totalMessageCount, allMessagesSeenLatch) - { - } - - public override void PostHandleDelivery(ulong deliveryTag) - { - Channel.BasicAck(deliveryTag, false); - } - } - - public class NackingBasicConsumer : TestBasicConsumer - { - public NackingBasicConsumer(IChannel channel, ushort totalMessageCount, ManualResetEventSlim allMessagesSeenLatch) - : base(channel, totalMessageCount, allMessagesSeenLatch) - { - } - - public override void PostHandleDelivery(ulong deliveryTag) - { - Channel.BasicNack(deliveryTag, false, false); - } - } - - public class RejectingBasicConsumer : TestBasicConsumer - { - public RejectingBasicConsumer(IChannel channel, ushort totalMessageCount, ManualResetEventSlim allMessagesSeenLatch) - : base(channel, totalMessageCount, allMessagesSeenLatch) - { - } - - public override void PostHandleDelivery(ulong deliveryTag) - { - Channel.BasicReject(deliveryTag, false); - } - } - - public class TestBasicConsumer : DefaultBasicConsumer - { - private readonly ManualResetEventSlim _allMessagesSeenLatch; - private readonly ushort _totalMessageCount; - private ushort _counter = 0; - - public TestBasicConsumer(IChannel channel, ushort totalMessageCount, ManualResetEventSlim allMessagesSeenLatch) - : base(channel) - { - _totalMessageCount = totalMessageCount; - _allMessagesSeenLatch = allMessagesSeenLatch; - } - - public override void HandleBasicDeliver(string consumerTag, - ulong deliveryTag, - bool redelivered, - string exchange, - string routingKey, - in ReadOnlyBasicProperties properties, - ReadOnlyMemory body) - { - try - { - PostHandleDelivery(deliveryTag); - } - finally - { - ++_counter; - if (_counter >= _totalMessageCount) - { - _allMessagesSeenLatch.Set(); - } - } - } - - public virtual void PostHandleDelivery(ulong deliveryTag) - { - } - } } } - -#pragma warning restore 0168 diff --git a/projects/Test/SequentialIntegration/TestConnectionRecoveryBase.cs b/projects/Test/SequentialIntegration/TestConnectionRecoveryBase.cs new file mode 100644 index 0000000000..28152aa559 --- /dev/null +++ b/projects/Test/SequentialIntegration/TestConnectionRecoveryBase.cs @@ -0,0 +1,389 @@ +// This source code is dual-licensed under the Apache License, version +// 2.0, and the Mozilla Public License, version 2.0. +// +// The APL v2.0: +// +//--------------------------------------------------------------------------- +// Copyright (c) 2007-2020 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//--------------------------------------------------------------------------- +// +// The MPL v2.0: +// +//--------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// +// Copyright (c) 2007-2020 VMware, Inc. All rights reserved. +//--------------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Threading; +using RabbitMQ.Client; +using RabbitMQ.Client.Framing.Impl; +using Xunit; +using Xunit.Abstractions; + +namespace Test.SequentialIntegration +{ + public class TestConnectionRecoveryBase : SequentialIntegrationFixture + { + protected readonly byte[] _messageBody; + protected const ushort _totalMessageCount = 8192; + protected const ushort _closeAtCount = 16; + + public TestConnectionRecoveryBase(ITestOutputHelper output) : base(output) + { + _messageBody = GetRandomBody(4096); + } + + protected override void TearDown() + { + Unblock(); + } + + protected void AssertConsumerCount(string q, int count) + { + WithTemporaryChannel((m) => + { + RabbitMQ.Client.QueueDeclareOk ok = m.QueueDeclarePassive(q); + Assert.Equal((uint)count, ok.ConsumerCount); + }); + } + + protected void AssertConsumerCount(IChannel m, string q, uint count) + { + RabbitMQ.Client.QueueDeclareOk ok = m.QueueDeclarePassive(q); + Assert.Equal(count, ok.ConsumerCount); + } + + protected void AssertExchangeRecovery(IChannel m, string x) + { + m.ConfirmSelect(); + WithTemporaryNonExclusiveQueue(m, (_, q) => + { + string rk = "routing-key"; + m.QueueBind(q, x, rk); + m.BasicPublish(x, rk, _messageBody); + + Assert.True(WaitForConfirms(m)); + m.ExchangeDeclarePassive(x); + }); + } + + protected void AssertQueueRecovery(IChannel m, string q) + { + AssertQueueRecovery(m, q, true); + } + + protected void AssertQueueRecovery(IChannel m, string q, bool exclusive, IDictionary arguments = null) + { + m.ConfirmSelect(); + m.QueueDeclarePassive(q); + RabbitMQ.Client.QueueDeclareOk ok1 = m.QueueDeclare(q, false, exclusive, false, arguments); + Assert.Equal(0u, ok1.MessageCount); + m.BasicPublish("", q, _messageBody); + Assert.True(WaitForConfirms(m)); + RabbitMQ.Client.QueueDeclareOk ok2 = m.QueueDeclare(q, false, exclusive, false, arguments); + Assert.Equal(1u, ok2.MessageCount); + } + + internal void AssertRecordedExchanges(AutorecoveringConnection c, int n) + { + Assert.Equal(n, c.RecordedExchangesCount); + } + + internal void AssertRecordedQueues(AutorecoveringConnection c, int n) + { + Assert.Equal(n, c.RecordedQueuesCount); + } + + internal AutorecoveringConnection CreateAutorecoveringConnection() + { + return CreateAutorecoveringConnection(RecoveryInterval); + } + + internal AutorecoveringConnection CreateAutorecoveringConnection(TimeSpan networkRecoveryInterval) + { + var cf = CreateConnectionFactory(); + cf.AutomaticRecoveryEnabled = true; + cf.NetworkRecoveryInterval = networkRecoveryInterval; + return (AutorecoveringConnection)cf.CreateConnection(); + } + + internal AutorecoveringConnection CreateAutorecoveringConnection(IList endpoints) + { + var cf = CreateConnectionFactory(); + cf.AutomaticRecoveryEnabled = true; + // tests that use this helper will likely list unreachable hosts, + // make sure we time out quickly on those + cf.RequestedConnectionTimeout = TimeSpan.FromSeconds(1); + cf.NetworkRecoveryInterval = RecoveryInterval; + return (AutorecoveringConnection)cf.CreateConnection(endpoints); + } + + internal AutorecoveringConnection CreateAutorecoveringConnectionWithTopologyRecoveryDisabled() + { + var cf = CreateConnectionFactory(); + cf.AutomaticRecoveryEnabled = true; + cf.TopologyRecoveryEnabled = false; + cf.NetworkRecoveryInterval = RecoveryInterval; + return (AutorecoveringConnection)cf.CreateConnection(); + } + + internal AutorecoveringConnection CreateAutorecoveringConnectionWithTopologyRecoveryFilter(TopologyRecoveryFilter filter) + { + var cf = CreateConnectionFactory(); + cf.AutomaticRecoveryEnabled = true; + cf.TopologyRecoveryEnabled = true; + cf.TopologyRecoveryFilter = filter; + return (AutorecoveringConnection)cf.CreateConnection(); + } + + internal AutorecoveringConnection CreateAutorecoveringConnectionWithTopologyRecoveryExceptionHandler(TopologyRecoveryExceptionHandler handler) + { + var cf = CreateConnectionFactory(); + cf.AutomaticRecoveryEnabled = true; + cf.TopologyRecoveryEnabled = true; + cf.TopologyRecoveryExceptionHandler = handler; + return (AutorecoveringConnection)cf.CreateConnection(); + } + + protected void CloseConnection(IConnection conn) + { + _rabbitMQCtl.CloseConnection(conn); + } + + protected void CloseAndWaitForRecovery() + { + CloseAndWaitForRecovery((AutorecoveringConnection)_conn); + } + + internal void CloseAndWaitForRecovery(AutorecoveringConnection conn) + { + ManualResetEventSlim sl = PrepareForShutdown(conn); + ManualResetEventSlim rl = PrepareForRecovery(conn); + CloseConnection(conn); + Wait(sl, "connection shutdown"); + Wait(rl, "connection recovery"); + } + + internal void CloseAndWaitForShutdown(AutorecoveringConnection conn) + { + ManualResetEventSlim sl = PrepareForShutdown(conn); + CloseConnection(conn); + Wait(sl, "connection shutdown"); + } + + protected string DeclareNonDurableExchange(IChannel m, string x) + { + m.ExchangeDeclare(x, "fanout", false); + return x; + } + + protected string DeclareNonDurableExchangeNoWait(IChannel m, string x) + { + m.ExchangeDeclareNoWait(x, "fanout", false, false, null); + return x; + } + + protected ManualResetEventSlim PrepareForRecovery(IConnection conn) + { + var latch = new ManualResetEventSlim(false); + + AutorecoveringConnection aconn = conn as AutorecoveringConnection; + aconn.RecoverySucceeded += (source, ea) => latch.Set(); + + return latch; + } + + protected void PublishMessagesWhileClosingConn(string queueName) + { + using (AutorecoveringConnection publishingConn = CreateAutorecoveringConnection()) + { + using (IChannel publishingChannel = publishingConn.CreateChannel()) + { + for (ushort i = 0; i < _totalMessageCount; i++) + { + if (i == _closeAtCount) + { + CloseConnection(_conn); + } + publishingChannel.BasicPublish(string.Empty, queueName, _messageBody); + } + } + } + } + + protected static ManualResetEventSlim PrepareForShutdown(IConnection conn) + { + var latch = new ManualResetEventSlim(false); + + AutorecoveringConnection aconn = conn as AutorecoveringConnection; + aconn.ConnectionShutdown += (c, args) => latch.Set(); + + return latch; + } + + protected void RestartServerAndWaitForRecovery() + { + RestartServerAndWaitForRecovery((AutorecoveringConnection)_conn); + } + + internal void RestartServerAndWaitForRecovery(AutorecoveringConnection conn) + { + ManualResetEventSlim sl = PrepareForShutdown(conn); + ManualResetEventSlim rl = PrepareForRecovery(conn); + RestartRabbitMQ(); + Wait(sl, "connection shutdown"); + Wait(rl, "connection recovery"); + } + + protected bool WaitForConfirms(IChannel m) + { + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(4)); + return m.WaitForConfirmsAsync(cts.Token).GetAwaiter().GetResult(); + } + + protected void WaitForRecovery() + { + Wait(PrepareForRecovery((AutorecoveringConnection)_conn), "recovery succeded"); + } + + internal void WaitForRecovery(AutorecoveringConnection conn) + { + Wait(PrepareForRecovery(conn), "recovery succeeded"); + } + + protected void WaitForShutdown() + { + Wait(PrepareForShutdown(_conn), "connection shutdown"); + } + + protected void WaitForShutdown(IConnection conn) + { + Wait(PrepareForShutdown(conn), "connection shutdown"); + } + + protected void WithTemporaryQueueNoWait(IChannel channel, Action action, string queue) + { + try + { + channel.QueueDeclareNoWait(queue, false, true, false, null); + action(channel, queue); + } + finally + { + WithTemporaryChannel(x => x.QueueDelete(queue)); + } + } + + public class AckingBasicConsumer : TestBasicConsumer + { + public AckingBasicConsumer(IChannel channel, ushort totalMessageCount, ManualResetEventSlim allMessagesSeenLatch) + : base(channel, totalMessageCount, allMessagesSeenLatch) + { + } + + public override void PostHandleDelivery(ulong deliveryTag) + { + Channel.BasicAck(deliveryTag, false); + } + } + + public class NackingBasicConsumer : TestBasicConsumer + { + public NackingBasicConsumer(IChannel channel, ushort totalMessageCount, ManualResetEventSlim allMessagesSeenLatch) + : base(channel, totalMessageCount, allMessagesSeenLatch) + { + } + + public override void PostHandleDelivery(ulong deliveryTag) + { + Channel.BasicNack(deliveryTag, false, false); + } + } + + public class RejectingBasicConsumer : TestBasicConsumer + { + public RejectingBasicConsumer(IChannel channel, ushort totalMessageCount, ManualResetEventSlim allMessagesSeenLatch) + : base(channel, totalMessageCount, allMessagesSeenLatch) + { + } + + public override void PostHandleDelivery(ulong deliveryTag) + { + Channel.BasicReject(deliveryTag, false); + } + } + + public class TestBasicConsumer : DefaultBasicConsumer + { + protected readonly ManualResetEventSlim _allMessagesSeenLatch; + protected readonly ushort _totalMessageCount; + protected ushort _counter = 0; + + public TestBasicConsumer(IChannel channel, ushort totalMessageCount, ManualResetEventSlim allMessagesSeenLatch) + : base(channel) + { + _totalMessageCount = totalMessageCount; + _allMessagesSeenLatch = allMessagesSeenLatch; + } + + public override void HandleBasicDeliver(string consumerTag, + ulong deliveryTag, + bool redelivered, + string exchange, + string routingKey, + in ReadOnlyBasicProperties properties, + RentedMemory body) + { + try + { + PostHandleDelivery(deliveryTag); + } + finally + { + ++_counter; + if (_counter >= _totalMessageCount) + { + _allMessagesSeenLatch.Set(); + } + } + } + + public virtual void PostHandleDelivery(ulong deliveryTag) + { + } + } + + protected bool SendAndConsumeMessage(IConnection conn, string queue, string exchange, string routingKey) + { + using (IChannel ch = conn.CreateChannel()) + { + var latch = new ManualResetEventSlim(false); + + var consumer = new AckingBasicConsumer(ch, 1, latch); + + ch.BasicConsume(queue, false, consumer); + + ch.BasicPublish(exchange, routingKey, _encoding.GetBytes("test message")); + + return latch.Wait(TimeSpan.FromSeconds(5)); + } + } + } +} diff --git a/projects/Test/SequentialIntegration/TestConnectionRecoveryWithoutSetup.cs b/projects/Test/SequentialIntegration/TestConnectionRecoveryWithoutSetup.cs new file mode 100644 index 0000000000..8832c5e92e --- /dev/null +++ b/projects/Test/SequentialIntegration/TestConnectionRecoveryWithoutSetup.cs @@ -0,0 +1,333 @@ +// This source code is dual-licensed under the Apache License, version +// 2.0, and the Mozilla Public License, version 2.0. +// +// The APL v2.0: +// +//--------------------------------------------------------------------------- +// Copyright (c) 2007-2020 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//--------------------------------------------------------------------------- +// +// The MPL v2.0: +// +//--------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// +// Copyright (c) 2007-2020 VMware, Inc. All rights reserved. +//--------------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Threading; +using RabbitMQ.Client; +using RabbitMQ.Client.Events; +using RabbitMQ.Client.Exceptions; +using RabbitMQ.Client.Framing.Impl; +using Xunit; +using Xunit.Abstractions; + +namespace Test.SequentialIntegration +{ + public class TestConnectionRecoveryWithoutSetup : TestConnectionRecoveryBase + { + public TestConnectionRecoveryWithoutSetup(ITestOutputHelper output) : base(output) + { + } + + protected override void SetUp() + { + Assert.Null(_connFactory); + Assert.Null(_conn); + Assert.Null(_channel); + } + + [Fact] + public void TestBasicConnectionRecoveryWithHostnameList() + { + using (AutorecoveringConnection c = CreateAutorecoveringConnection(new List { "127.0.0.1", "localhost" })) + { + Assert.True(c.IsOpen); + CloseAndWaitForRecovery(c); + Assert.True(c.IsOpen); + } + } + + [Fact] + public void TestBasicConnectionRecoveryWithHostnameListAndUnreachableHosts() + { + using (AutorecoveringConnection c = CreateAutorecoveringConnection(new List { "191.72.44.22", "127.0.0.1", "localhost" })) + { + Assert.True(c.IsOpen); + CloseAndWaitForRecovery(c); + Assert.True(c.IsOpen); + } + } + + [Fact] + public void TestBasicConnectionRecoveryWithEndpointList() + { + using (AutorecoveringConnection c = CreateAutorecoveringConnection( + new List + { + new AmqpTcpEndpoint("127.0.0.1"), + new AmqpTcpEndpoint("localhost") + })) + { + Assert.True(c.IsOpen); + CloseAndWaitForRecovery(c); + Assert.True(c.IsOpen); + } + } + + [Fact] + public void TestBasicConnectionRecoveryWithEndpointListAndUnreachableHosts() + { + using (AutorecoveringConnection c = CreateAutorecoveringConnection( + new List + { + new AmqpTcpEndpoint("191.72.44.22"), + new AmqpTcpEndpoint("127.0.0.1"), + new AmqpTcpEndpoint("localhost") + })) + { + Assert.True(c.IsOpen); + CloseAndWaitForRecovery(c); + Assert.True(c.IsOpen); + } + } + + [Fact] + public void TestConsumerWorkServiceRecovery() + { + using (AutorecoveringConnection c = CreateAutorecoveringConnection()) + { + IChannel m = c.CreateChannel(); + string q = m.QueueDeclare("dotnet-client.recovery.consumer_work_pool1", + false, false, false, null).QueueName; + var cons = new EventingBasicConsumer(m); + m.BasicConsume(q, true, cons); + AssertConsumerCount(m, q, 1); + + CloseAndWaitForRecovery(c); + + Assert.True(m.IsOpen); + var latch = new ManualResetEventSlim(false); + cons.Received += (s, args) => latch.Set(); + + m.BasicPublish("", q, _encoding.GetBytes("msg")); + Wait(latch, "received event"); + + m.QueueDelete(q); + } + } + + [Fact] + public void TestConsumerRecoveryOnClientNamedQueueWithOneRecovery() + { + string q0 = "dotnet-client.recovery.queue1"; + using (AutorecoveringConnection c = CreateAutorecoveringConnection()) + { + IChannel m = c.CreateChannel(); + string q1 = m.QueueDeclare(q0, false, false, false, null).QueueName; + Assert.Equal(q0, q1); + + var cons = new EventingBasicConsumer(m); + m.BasicConsume(q1, true, cons); + AssertConsumerCount(m, q1, 1); + + bool queueNameChangeAfterRecoveryCalled = false; + + c.QueueNameChangeAfterRecovery += (source, ea) => { queueNameChangeAfterRecoveryCalled = true; }; + + CloseAndWaitForRecovery(c); + AssertConsumerCount(m, q1, 1); + Assert.False(queueNameChangeAfterRecoveryCalled); + + CloseAndWaitForRecovery(c); + AssertConsumerCount(m, q1, 1); + Assert.False(queueNameChangeAfterRecoveryCalled); + + CloseAndWaitForRecovery(c); + AssertConsumerCount(m, q1, 1); + Assert.False(queueNameChangeAfterRecoveryCalled); + + var latch = new ManualResetEventSlim(false); + cons.Received += (s, args) => latch.Set(); + + m.BasicPublish("", q1, _encoding.GetBytes("msg")); + Wait(latch, "received event"); + + m.QueueDelete(q1); + } + } + + [Fact] + public void TestConsumerRecoveryWithServerNamedQueue() + { + // https://github.com/rabbitmq/rabbitmq-dotnet-client/issues/1238 + using (AutorecoveringConnection c = CreateAutorecoveringConnection()) + { + IChannel ch = c.CreateChannel(); + RabbitMQ.Client.QueueDeclareOk queueDeclareResult = ch.QueueDeclare(queue: string.Empty, durable: false, exclusive: true, autoDelete: true, arguments: null); + string qname = queueDeclareResult.QueueName; + Assert.False(string.IsNullOrEmpty(qname)); + + var cons = new EventingBasicConsumer(ch); + ch.BasicConsume(string.Empty, true, cons); + AssertConsumerCount(ch, qname, 1); + + bool queueNameBeforeIsEqual = false; + bool queueNameChangeAfterRecoveryCalled = false; + string qnameAfterRecovery = null; + c.QueueNameChangeAfterRecovery += (source, ea) => + { + queueNameChangeAfterRecoveryCalled = true; + queueNameBeforeIsEqual = qname.Equals(ea.NameBefore); + qnameAfterRecovery = ea.NameAfter; + }; + + CloseAndWaitForRecovery(c); + + AssertConsumerCount(ch, qnameAfterRecovery, 1); + Assert.True(queueNameChangeAfterRecoveryCalled); + Assert.True(queueNameBeforeIsEqual); + } + } + + [Fact] + public void TestCreateChannelOnClosedAutorecoveringConnectionDoesNotHang() + { + // we don't want this to recover quickly in this test + AutorecoveringConnection c = CreateAutorecoveringConnection(TimeSpan.FromSeconds(20)); + + try + { + c.Close(); + WaitForShutdown(c); + Assert.False(c.IsOpen); + c.CreateChannel(); + Assert.Fail("Expected an exception"); + } + catch (AlreadyClosedException) + { + // expected + } + finally + { + StartRabbitMQ(); + if (c.IsOpen) + { + c.Abort(); + } + } + } + + [Fact] + public void TestTopologyRecoveryConsumerFilter() + { + var filter = new TopologyRecoveryFilter + { + ConsumerFilter = consumer => !consumer.ConsumerTag.Contains("filtered") + }; + var latch = new ManualResetEventSlim(false); + AutorecoveringConnection conn = CreateAutorecoveringConnectionWithTopologyRecoveryFilter(filter); + conn.RecoverySucceeded += (source, ea) => latch.Set(); + IChannel ch = conn.CreateChannel(); + ch.ConfirmSelect(); + + var exchange = "topology.recovery.exchange"; + var queueWithRecoveredConsumer = "topology.recovery.queue.1"; + var queueWithIgnoredConsumer = "topology.recovery.queue.2"; + var binding1 = "recovered.binding.1"; + var binding2 = "recovered.binding.2"; + + ch.ExchangeDeclare(exchange, "direct"); + ch.QueueDeclare(queueWithRecoveredConsumer, false, false, false, null); + ch.QueueDeclare(queueWithIgnoredConsumer, false, false, false, null); + ch.QueueBind(queueWithRecoveredConsumer, exchange, binding1); + ch.QueueBind(queueWithIgnoredConsumer, exchange, binding2); + ch.QueuePurge(queueWithRecoveredConsumer); + ch.QueuePurge(queueWithIgnoredConsumer); + + var recoverLatch = new ManualResetEventSlim(false); + var consumerToRecover = new EventingBasicConsumer(ch); + consumerToRecover.Received += (source, ea) => recoverLatch.Set(); + ch.BasicConsume(queueWithRecoveredConsumer, true, "recovered.consumer", consumerToRecover); + + var ignoredLatch = new ManualResetEventSlim(false); + var consumerToIgnore = new EventingBasicConsumer(ch); + consumerToIgnore.Received += (source, ea) => ignoredLatch.Set(); + ch.BasicConsume(queueWithIgnoredConsumer, true, "filtered.consumer", consumerToIgnore); + + try + { + CloseAndWaitForRecovery(conn); + Wait(latch, "recovery succeeded"); + + Assert.True(ch.IsOpen); + ch.BasicPublish(exchange, binding1, _encoding.GetBytes("test message")); + ch.BasicPublish(exchange, binding2, _encoding.GetBytes("test message")); + + Assert.True(recoverLatch.Wait(TimeSpan.FromSeconds(5))); + Assert.False(ignoredLatch.Wait(TimeSpan.FromSeconds(5))); + + ch.BasicConsume(queueWithIgnoredConsumer, true, "filtered.consumer", consumerToIgnore); + + try + { + ch.BasicConsume(queueWithRecoveredConsumer, true, "recovered.consumer", consumerToRecover); + Assert.Fail("Expected an exception"); + } + catch (OperationInterruptedException e) + { + AssertShutdownError(e.ShutdownReason, 530); // NOT_ALLOWED - not allowed to reuse consumer tag + } + } + finally + { + conn.Abort(); + } + } + + [Fact] + public void TestRecoveryWithTopologyDisabled() + { + AutorecoveringConnection conn = CreateAutorecoveringConnectionWithTopologyRecoveryDisabled(); + IChannel ch = conn.CreateChannel(); + string s = "dotnet-client.test.recovery.q2"; + ch.QueueDelete(s); + ch.QueueDeclare(s, false, true, false, null); + ch.QueueDeclarePassive(s); + Assert.True(ch.IsOpen); + + try + { + CloseAndWaitForRecovery(conn); + Assert.True(ch.IsOpen); + ch.QueueDeclarePassive(s); + Assert.Fail("Expected an exception"); + } + catch (OperationInterruptedException) + { + // expected + } + finally + { + conn.Abort(); + } + } + } +} diff --git a/projects/Unit/APIApproval.Approve.verified.txt b/projects/Test/Unit/APIApproval.Approve.verified.txt similarity index 88% rename from projects/Unit/APIApproval.Approve.verified.txt rename to projects/Test/Unit/APIApproval.Approve.verified.txt index 8df115691b..1902c40c20 100644 --- a/projects/Unit/APIApproval.Approve.verified.txt +++ b/projects/Test/Unit/APIApproval.Approve.verified.txt @@ -1,4 +1,8 @@ -[assembly: System.Runtime.CompilerServices.InternalsVisibleTo(@"Benchmarks, PublicKey=00240000048000009400000006020000002400005253413100040000010001008d20ec856aeeb8c3153a77faa2d80e6e43b5db93224a20cc7ae384f65f142e89730e2ff0fcc5d578bbe96fa98a7196c77329efdee4579b3814c0789e5a39b51df6edd75b602a33ceabdfcf19a3feb832f31d8254168cd7ba5700dfbca301fbf8db614ba41ba18474de0a5f4c2d51c995bc3636c641c8cbe76f45717bfcb943b5")] +[assembly: System.Runtime.CompilerServices.InternalsVisibleTo(@"AsyncIntegration, PublicKey=00240000048000009400000006020000002400005253413100040000010001008d20ec856aeeb8c3153a77faa2d80e6e43b5db93224a20cc7ae384f65f142e89730e2ff0fcc5d578bbe96fa98a7196c77329efdee4579b3814c0789e5a39b51df6edd75b602a33ceabdfcf19a3feb832f31d8254168cd7ba5700dfbca301fbf8db614ba41ba18474de0a5f4c2d51c995bc3636c641c8cbe76f45717bfcb943b5")] +[assembly: System.Runtime.CompilerServices.InternalsVisibleTo(@"Benchmarks, PublicKey=00240000048000009400000006020000002400005253413100040000010001008d20ec856aeeb8c3153a77faa2d80e6e43b5db93224a20cc7ae384f65f142e89730e2ff0fcc5d578bbe96fa98a7196c77329efdee4579b3814c0789e5a39b51df6edd75b602a33ceabdfcf19a3feb832f31d8254168cd7ba5700dfbca301fbf8db614ba41ba18474de0a5f4c2d51c995bc3636c641c8cbe76f45717bfcb943b5")] +[assembly: System.Runtime.CompilerServices.InternalsVisibleTo(@"Common, PublicKey=00240000048000009400000006020000002400005253413100040000010001008d20ec856aeeb8c3153a77faa2d80e6e43b5db93224a20cc7ae384f65f142e89730e2ff0fcc5d578bbe96fa98a7196c77329efdee4579b3814c0789e5a39b51df6edd75b602a33ceabdfcf19a3feb832f31d8254168cd7ba5700dfbca301fbf8db614ba41ba18474de0a5f4c2d51c995bc3636c641c8cbe76f45717bfcb943b5")] +[assembly: System.Runtime.CompilerServices.InternalsVisibleTo(@"Integration, PublicKey=00240000048000009400000006020000002400005253413100040000010001008d20ec856aeeb8c3153a77faa2d80e6e43b5db93224a20cc7ae384f65f142e89730e2ff0fcc5d578bbe96fa98a7196c77329efdee4579b3814c0789e5a39b51df6edd75b602a33ceabdfcf19a3feb832f31d8254168cd7ba5700dfbca301fbf8db614ba41ba18474de0a5f4c2d51c995bc3636c641c8cbe76f45717bfcb943b5")] +[assembly: System.Runtime.CompilerServices.InternalsVisibleTo(@"SequentialIntegration, PublicKey=00240000048000009400000006020000002400005253413100040000010001008d20ec856aeeb8c3153a77faa2d80e6e43b5db93224a20cc7ae384f65f142e89730e2ff0fcc5d578bbe96fa98a7196c77329efdee4579b3814c0789e5a39b51df6edd75b602a33ceabdfcf19a3feb832f31d8254168cd7ba5700dfbca301fbf8db614ba41ba18474de0a5f4c2d51c995bc3636c641c8cbe76f45717bfcb943b5")] [assembly: System.Runtime.CompilerServices.InternalsVisibleTo(@"Unit, PublicKey=00240000048000009400000006020000002400005253413100040000010001008d20ec856aeeb8c3153a77faa2d80e6e43b5db93224a20cc7ae384f65f142e89730e2ff0fcc5d578bbe96fa98a7196c77329efdee4579b3814c0789e5a39b51df6edd75b602a33ceabdfcf19a3feb832f31d8254168cd7ba5700dfbca301fbf8db614ba41ba18474de0a5f4c2d51c995bc3636c641c8cbe76f45717bfcb943b5")] namespace RabbitMQ.Client { @@ -49,7 +53,7 @@ namespace RabbitMQ.Client public virtual System.Threading.Tasks.Task HandleBasicCancel(string consumerTag) { } public virtual System.Threading.Tasks.Task HandleBasicCancelOk(string consumerTag) { } public virtual System.Threading.Tasks.Task HandleBasicConsumeOk(string consumerTag) { } - public virtual System.Threading.Tasks.Task HandleBasicDeliver(string consumerTag, ulong deliveryTag, bool redelivered, string exchange, string routingKey, in RabbitMQ.Client.ReadOnlyBasicProperties properties, System.ReadOnlyMemory body) { } + public virtual System.Threading.Tasks.Task HandleBasicDeliver(string consumerTag, ulong deliveryTag, bool redelivered, string exchange, string routingKey, in RabbitMQ.Client.ReadOnlyBasicProperties properties, RabbitMQ.Client.RentedMemory body) { } public virtual System.Threading.Tasks.Task HandleChannelShutdown(object channel, RabbitMQ.Client.ShutdownEventArgs reason) { } public virtual System.Threading.Tasks.Task OnCancel(params string[] consumerTags) { } } @@ -62,18 +66,16 @@ namespace RabbitMQ.Client public System.TimeSpan? ValidUntil { get; } public void Refresh() { } } - public sealed class BasicGetResult : System.IDisposable + public sealed class BasicGetResult { - public BasicGetResult(ulong deliveryTag, bool redelivered, string exchange, string routingKey, uint messageCount, in RabbitMQ.Client.ReadOnlyBasicProperties basicProperties, System.ReadOnlyMemory body) { } - public BasicGetResult(ulong deliveryTag, bool redelivered, string exchange, string routingKey, uint messageCount, in RabbitMQ.Client.ReadOnlyBasicProperties basicProperties, System.ReadOnlyMemory body, byte[] rentedArray) { } + public BasicGetResult(ulong deliveryTag, bool redelivered, string exchange, string routingKey, uint messageCount, in RabbitMQ.Client.ReadOnlyBasicProperties basicProperties, RabbitMQ.Client.RentedMemory body) { } public RabbitMQ.Client.ReadOnlyBasicProperties BasicProperties { get; } - public System.ReadOnlyMemory Body { get; } + public RabbitMQ.Client.RentedMemory Body { get; } public ulong DeliveryTag { get; } public string Exchange { get; } public uint MessageCount { get; } public bool Redelivered { get; } public string RoutingKey { get; } - public void Dispose() { } } public struct BasicProperties : RabbitMQ.Client.IAmqpHeader, RabbitMQ.Client.IAmqpWriteable, RabbitMQ.Client.IBasicProperties, RabbitMQ.Client.IReadOnlyBasicProperties { @@ -216,6 +218,13 @@ namespace RabbitMQ.Client public RabbitMQ.Client.IConnection CreateConnection(RabbitMQ.Client.IEndpointResolver endpointResolver, string clientProvidedName) { } public RabbitMQ.Client.IConnection CreateConnection(System.Collections.Generic.IList endpoints, string clientProvidedName) { } public RabbitMQ.Client.IConnection CreateConnection(System.Collections.Generic.IList hostnames, string clientProvidedName) { } + public System.Threading.Tasks.ValueTask CreateConnectionAsync() { } + public System.Threading.Tasks.ValueTask CreateConnectionAsync(System.Collections.Generic.IList endpoints) { } + public System.Threading.Tasks.ValueTask CreateConnectionAsync(System.Collections.Generic.IList hostnames) { } + public System.Threading.Tasks.ValueTask CreateConnectionAsync(string clientProvidedName) { } + public System.Threading.Tasks.ValueTask CreateConnectionAsync(RabbitMQ.Client.IEndpointResolver endpointResolver, string clientProvidedName) { } + public System.Threading.Tasks.ValueTask CreateConnectionAsync(System.Collections.Generic.IList endpoints, string clientProvidedName) { } + public System.Threading.Tasks.ValueTask CreateConnectionAsync(System.Collections.Generic.IList hostnames, string clientProvidedName) { } } public class ConnectionFactoryBase { @@ -263,7 +272,7 @@ namespace RabbitMQ.Client public virtual void HandleBasicCancel(string consumerTag) { } public virtual void HandleBasicCancelOk(string consumerTag) { } public virtual void HandleBasicConsumeOk(string consumerTag) { } - public virtual void HandleBasicDeliver(string consumerTag, ulong deliveryTag, bool redelivered, string exchange, string routingKey, in RabbitMQ.Client.ReadOnlyBasicProperties properties, System.ReadOnlyMemory body) { } + public virtual void HandleBasicDeliver(string consumerTag, ulong deliveryTag, bool redelivered, string exchange, string routingKey, in RabbitMQ.Client.ReadOnlyBasicProperties properties, RabbitMQ.Client.RentedMemory body) { } public virtual void HandleChannelShutdown(object channel, RabbitMQ.Client.ShutdownEventArgs reason) { } public virtual void OnCancel(params string[] consumerTags) { } } @@ -336,7 +345,7 @@ namespace RabbitMQ.Client System.Threading.Tasks.Task HandleBasicCancel(string consumerTag); System.Threading.Tasks.Task HandleBasicCancelOk(string consumerTag); System.Threading.Tasks.Task HandleBasicConsumeOk(string consumerTag); - System.Threading.Tasks.Task HandleBasicDeliver(string consumerTag, ulong deliveryTag, bool redelivered, string exchange, string routingKey, in RabbitMQ.Client.ReadOnlyBasicProperties properties, System.ReadOnlyMemory body); + System.Threading.Tasks.Task HandleBasicDeliver(string consumerTag, ulong deliveryTag, bool redelivered, string exchange, string routingKey, in RabbitMQ.Client.ReadOnlyBasicProperties properties, RabbitMQ.Client.RentedMemory body); System.Threading.Tasks.Task HandleChannelShutdown(object channel, RabbitMQ.Client.ShutdownEventArgs reason); } public interface IAuthMechanism @@ -355,7 +364,7 @@ namespace RabbitMQ.Client void HandleBasicCancel(string consumerTag); void HandleBasicCancelOk(string consumerTag); void HandleBasicConsumeOk(string consumerTag); - void HandleBasicDeliver(string consumerTag, ulong deliveryTag, bool redelivered, string exchange, string routingKey, in RabbitMQ.Client.ReadOnlyBasicProperties properties, System.ReadOnlyMemory body); + void HandleBasicDeliver(string consumerTag, ulong deliveryTag, bool redelivered, string exchange, string routingKey, in RabbitMQ.Client.ReadOnlyBasicProperties properties, RabbitMQ.Client.RentedMemory body); void HandleChannelShutdown(object channel, RabbitMQ.Client.ShutdownEventArgs reason); } public interface IBasicProperties : RabbitMQ.Client.IReadOnlyBasicProperties @@ -403,17 +412,21 @@ namespace RabbitMQ.Client ulong NextPublishSeqNo { get; } event System.EventHandler BasicAcks; event System.EventHandler BasicNacks; - event System.EventHandler BasicRecoverOk; event System.EventHandler BasicReturn; event System.EventHandler CallbackException; event System.EventHandler ChannelShutdown; event System.EventHandler FlowControl; void BasicAck(ulong deliveryTag, bool multiple); + System.Threading.Tasks.ValueTask BasicAckAsync(ulong deliveryTag, bool multiple); void BasicCancel(string consumerTag); + System.Threading.Tasks.ValueTask BasicCancelAsync(string consumerTag); void BasicCancelNoWait(string consumerTag); string BasicConsume(string queue, bool autoAck, string consumerTag, bool noLocal, bool exclusive, System.Collections.Generic.IDictionary arguments, RabbitMQ.Client.IBasicConsumer consumer); + System.Threading.Tasks.ValueTask BasicConsumeAsync(string queue, bool autoAck, string consumerTag, bool noLocal, bool exclusive, System.Collections.Generic.IDictionary arguments, RabbitMQ.Client.IBasicConsumer consumer); RabbitMQ.Client.BasicGetResult BasicGet(string queue, bool autoAck); + System.Threading.Tasks.ValueTask BasicGetAsync(string queue, bool autoAck); void BasicNack(ulong deliveryTag, bool multiple, bool requeue); + System.Threading.Tasks.ValueTask BasicNackAsync(ulong deliveryTag, bool multiple, bool requeue); void BasicPublish(RabbitMQ.Client.CachedString exchange, RabbitMQ.Client.CachedString routingKey, in TProperties basicProperties, System.ReadOnlyMemory body = default, bool mandatory = false) where TProperties : RabbitMQ.Client.IReadOnlyBasicProperties, RabbitMQ.Client.IAmqpHeader; void BasicPublish(string exchange, string routingKey, in TProperties basicProperties, System.ReadOnlyMemory body = default, bool mandatory = false) @@ -423,35 +436,48 @@ namespace RabbitMQ.Client System.Threading.Tasks.ValueTask BasicPublishAsync(string exchange, string routingKey, in TProperties basicProperties, System.ReadOnlyMemory body = default, bool mandatory = false) where TProperties : RabbitMQ.Client.IReadOnlyBasicProperties, RabbitMQ.Client.IAmqpHeader; void BasicQos(uint prefetchSize, ushort prefetchCount, bool global); - void BasicRecover(bool requeue); - void BasicRecoverAsync(bool requeue); + System.Threading.Tasks.ValueTask BasicQosAsync(uint prefetchSize, ushort prefetchCount, bool global); void BasicReject(ulong deliveryTag, bool requeue); + System.Threading.Tasks.ValueTask BasicRejectAsync(ulong deliveryTag, bool requeue); void Close(ushort replyCode, string replyText, bool abort); + System.Threading.Tasks.ValueTask CloseAsync(RabbitMQ.Client.ShutdownEventArgs reason, bool abort); void ConfirmSelect(); + System.Threading.Tasks.ValueTask ConfirmSelectAsync(); uint ConsumerCount(string queue); void ExchangeBind(string destination, string source, string routingKey, System.Collections.Generic.IDictionary arguments); + System.Threading.Tasks.ValueTask ExchangeBindAsync(string destination, string source, string routingKey, System.Collections.Generic.IDictionary arguments); void ExchangeBindNoWait(string destination, string source, string routingKey, System.Collections.Generic.IDictionary arguments); void ExchangeDeclare(string exchange, string type, bool durable, bool autoDelete, System.Collections.Generic.IDictionary arguments); + System.Threading.Tasks.ValueTask ExchangeDeclareAsync(string exchange, string type, bool passive, bool durable, bool autoDelete, System.Collections.Generic.IDictionary arguments); void ExchangeDeclareNoWait(string exchange, string type, bool durable, bool autoDelete, System.Collections.Generic.IDictionary arguments); void ExchangeDeclarePassive(string exchange); void ExchangeDelete(string exchange, bool ifUnused); + System.Threading.Tasks.ValueTask ExchangeDeleteAsync(string exchange, bool ifUnused); void ExchangeDeleteNoWait(string exchange, bool ifUnused); void ExchangeUnbind(string destination, string source, string routingKey, System.Collections.Generic.IDictionary arguments); + System.Threading.Tasks.ValueTask ExchangeUnbindAsync(string destination, string source, string routingKey, System.Collections.Generic.IDictionary arguments); void ExchangeUnbindNoWait(string destination, string source, string routingKey, System.Collections.Generic.IDictionary arguments); uint MessageCount(string queue); void QueueBind(string queue, string exchange, string routingKey, System.Collections.Generic.IDictionary arguments); + System.Threading.Tasks.ValueTask QueueBindAsync(string queue, string exchange, string routingKey, System.Collections.Generic.IDictionary arguments); void QueueBindNoWait(string queue, string exchange, string routingKey, System.Collections.Generic.IDictionary arguments); RabbitMQ.Client.QueueDeclareOk QueueDeclare(string queue, bool durable, bool exclusive, bool autoDelete, System.Collections.Generic.IDictionary arguments); - System.Threading.Tasks.ValueTask QueueDeclareAsync(string queue, bool durable, bool exclusive, bool autoDelete, System.Collections.Generic.IDictionary arguments); + System.Threading.Tasks.ValueTask QueueDeclareAsync(string queue, bool passive, bool durable, bool exclusive, bool autoDelete, System.Collections.Generic.IDictionary arguments); void QueueDeclareNoWait(string queue, bool durable, bool exclusive, bool autoDelete, System.Collections.Generic.IDictionary arguments); RabbitMQ.Client.QueueDeclareOk QueueDeclarePassive(string queue); uint QueueDelete(string queue, bool ifUnused, bool ifEmpty); + System.Threading.Tasks.ValueTask QueueDeleteAsync(string queue, bool ifUnused, bool ifEmpty); void QueueDeleteNoWait(string queue, bool ifUnused, bool ifEmpty); uint QueuePurge(string queue); + System.Threading.Tasks.ValueTask QueuePurgeAsync(string queue); void QueueUnbind(string queue, string exchange, string routingKey, System.Collections.Generic.IDictionary arguments); + System.Threading.Tasks.ValueTask QueueUnbindAsync(string queue, string exchange, string routingKey, System.Collections.Generic.IDictionary arguments); void TxCommit(); + System.Threading.Tasks.ValueTask TxCommitAsync(); void TxRollback(); + System.Threading.Tasks.ValueTask TxRollbackAsync(); void TxSelect(); + System.Threading.Tasks.ValueTask TxSelectAsync(); System.Threading.Tasks.Task WaitForConfirmsAsync(System.Threading.CancellationToken token = default); System.Threading.Tasks.Task WaitForConfirmsOrDieAsync(System.Threading.CancellationToken token = default); } @@ -471,6 +497,7 @@ namespace RabbitMQ.Client public static System.Threading.Tasks.ValueTask BasicPublishAsync(this RabbitMQ.Client.IChannel channel, string exchange, string routingKey, System.ReadOnlyMemory body = default, bool mandatory = false) { } public static void Close(this RabbitMQ.Client.IChannel channel) { } public static void Close(this RabbitMQ.Client.IChannel channel, ushort replyCode, string replyText) { } + public static System.Threading.Tasks.ValueTask CloseAsync(this RabbitMQ.Client.IChannel channel) { } public static void ExchangeBind(this RabbitMQ.Client.IChannel channel, string destination, string source, string routingKey, System.Collections.Generic.IDictionary arguments = null) { } public static void ExchangeBindNoWait(this RabbitMQ.Client.IChannel channel, string destination, string source, string routingKey, System.Collections.Generic.IDictionary arguments = null) { } public static void ExchangeDeclare(this RabbitMQ.Client.IChannel channel, string exchange, string type, bool durable = false, bool autoDelete = false, System.Collections.Generic.IDictionary arguments = null) { } @@ -508,6 +535,7 @@ namespace RabbitMQ.Client event System.EventHandler RecoverySucceeded; void Close(ushort reasonCode, string reasonText, System.TimeSpan timeout, bool abort); RabbitMQ.Client.IChannel CreateChannel(); + System.Threading.Tasks.ValueTask CreateChannelAsync(); void UpdateSecret(string newSecret, string reason); } public static class IConnectionExtensions @@ -545,6 +573,12 @@ namespace RabbitMQ.Client RabbitMQ.Client.IConnection CreateConnection(string clientProvidedName); RabbitMQ.Client.IConnection CreateConnection(System.Collections.Generic.IList endpoints, string clientProvidedName); RabbitMQ.Client.IConnection CreateConnection(System.Collections.Generic.IList hostnames, string clientProvidedName); + System.Threading.Tasks.ValueTask CreateConnectionAsync(); + System.Threading.Tasks.ValueTask CreateConnectionAsync(System.Collections.Generic.IList endpoints); + System.Threading.Tasks.ValueTask CreateConnectionAsync(System.Collections.Generic.IList hostnames); + System.Threading.Tasks.ValueTask CreateConnectionAsync(string clientProvidedName); + System.Threading.Tasks.ValueTask CreateConnectionAsync(System.Collections.Generic.IList endpoints, string clientProvidedName); + System.Threading.Tasks.ValueTask CreateConnectionAsync(System.Collections.Generic.IList hostnames, string clientProvidedName); } public interface ICredentialsProvider { @@ -725,6 +759,14 @@ namespace RabbitMQ.Client public bool IsTypePresent() { } public bool IsUserIdPresent() { } } + public struct RentedMemory : System.IDisposable + { + public System.ReadOnlyMemory Memory { get; } + public int Size { get; } + public System.ReadOnlySpan Span { get; } + public void Dispose() { } + public static byte[] op_Implicit(RabbitMQ.Client.RentedMemory m) { } + } public class ShutdownEventArgs : System.EventArgs { public ShutdownEventArgs(RabbitMQ.Client.ShutdownInitiator initiator, ushort replyCode, string replyText, object cause = null) { } @@ -820,7 +862,7 @@ namespace RabbitMQ.Client.Events public event RabbitMQ.Client.Events.AsyncEventHandler Unregistered; public override System.Threading.Tasks.Task HandleBasicCancelOk(string consumerTag) { } public override System.Threading.Tasks.Task HandleBasicConsumeOk(string consumerTag) { } - public override System.Threading.Tasks.Task HandleBasicDeliver(string consumerTag, ulong deliveryTag, bool redelivered, string exchange, string routingKey, in RabbitMQ.Client.ReadOnlyBasicProperties properties, System.ReadOnlyMemory body) { } + public override System.Threading.Tasks.Task HandleBasicDeliver(string consumerTag, ulong deliveryTag, bool redelivered, string exchange, string routingKey, in RabbitMQ.Client.ReadOnlyBasicProperties properties, RabbitMQ.Client.RentedMemory body) { } public override System.Threading.Tasks.Task HandleChannelShutdown(object channel, RabbitMQ.Client.ShutdownEventArgs reason) { } } public abstract class BaseExceptionEventArgs : System.EventArgs @@ -838,9 +880,9 @@ namespace RabbitMQ.Client.Events public class BasicDeliverEventArgs : System.EventArgs { public BasicDeliverEventArgs() { } - public BasicDeliverEventArgs(string consumerTag, ulong deliveryTag, bool redelivered, string exchange, string routingKey, in RabbitMQ.Client.ReadOnlyBasicProperties properties, System.ReadOnlyMemory body) { } + public BasicDeliverEventArgs(string consumerTag, ulong deliveryTag, bool redelivered, string exchange, string routingKey, in RabbitMQ.Client.ReadOnlyBasicProperties properties, RabbitMQ.Client.RentedMemory body) { } public RabbitMQ.Client.ReadOnlyBasicProperties BasicProperties { get; set; } - public System.ReadOnlyMemory Body { get; set; } + public RabbitMQ.Client.RentedMemory Body { get; set; } public string ConsumerTag { get; set; } public ulong DeliveryTag { get; set; } public string Exchange { get; set; } @@ -856,13 +898,13 @@ namespace RabbitMQ.Client.Events } public class BasicReturnEventArgs : System.EventArgs { - public BasicReturnEventArgs() { } - public RabbitMQ.Client.ReadOnlyBasicProperties BasicProperties { get; set; } - public System.ReadOnlyMemory Body { get; set; } - public string Exchange { get; set; } - public ushort ReplyCode { get; set; } - public string ReplyText { get; set; } - public string RoutingKey { get; set; } + public readonly RabbitMQ.Client.ReadOnlyBasicProperties BasicProperties; + public readonly RabbitMQ.Client.RentedMemory Body; + public readonly string Exchange; + public readonly ushort ReplyCode; + public readonly string ReplyText; + public readonly string RoutingKey; + public BasicReturnEventArgs(ushort replyCode, string replyText, string exchange, string routingKey, RabbitMQ.Client.ReadOnlyBasicProperties basicProperties, RabbitMQ.Client.RentedMemory body) { } } public class CallbackExceptionEventArgs : RabbitMQ.Client.Events.BaseExceptionEventArgs { @@ -900,7 +942,7 @@ namespace RabbitMQ.Client.Events public event System.EventHandler Unregistered; public override void HandleBasicCancelOk(string consumerTag) { } public override void HandleBasicConsumeOk(string consumerTag) { } - public override void HandleBasicDeliver(string consumerTag, ulong deliveryTag, bool redelivered, string exchange, string routingKey, in RabbitMQ.Client.ReadOnlyBasicProperties properties, System.ReadOnlyMemory body) { } + public override void HandleBasicDeliver(string consumerTag, ulong deliveryTag, bool redelivered, string exchange, string routingKey, in RabbitMQ.Client.ReadOnlyBasicProperties properties, RabbitMQ.Client.RentedMemory body) { } public override void HandleChannelShutdown(object channel, RabbitMQ.Client.ShutdownEventArgs reason) { } } public class FlowControlEventArgs : System.EventArgs diff --git a/projects/Unit/APIApproval.cs b/projects/Test/Unit/APIApproval.cs similarity index 98% rename from projects/Unit/APIApproval.cs rename to projects/Test/Unit/APIApproval.cs index 25e35bd8d4..8ce97b5c34 100644 --- a/projects/Unit/APIApproval.cs +++ b/projects/Test/Unit/APIApproval.cs @@ -36,7 +36,7 @@ using VerifyXunit; using Xunit; -namespace RabbitMQ.Client.Unit +namespace Test.Unit { [UsesVerify] public class APIApproval diff --git a/projects/Unit/Helper/DebugUtil.cs b/projects/Test/Unit/DebugUtil.cs similarity index 98% rename from projects/Unit/Helper/DebugUtil.cs rename to projects/Test/Unit/DebugUtil.cs index 9ee8ed76bd..34d44ce90d 100644 --- a/projects/Unit/Helper/DebugUtil.cs +++ b/projects/Test/Unit/DebugUtil.cs @@ -34,7 +34,7 @@ using System.IO; using System.Reflection; -namespace RabbitMQ.Client.Unit +namespace Test.Unit { ///Miscellaneous debugging and development utilities. /// @@ -62,7 +62,7 @@ public static void Dump(byte[] bytes, TextWriter writer) { writer.Write("{0:X2}", bytes[count + i]); } - for (int i = 0; i < (rowlen - thisRow); i++) + for (int i = 0; i < rowlen - thisRow; i++) { writer.Write(" "); } diff --git a/projects/Unit/TestAmqpTcpEndpointParsing.cs b/projects/Test/Unit/TestAmqpTcpEndpointParsing.cs similarity index 99% rename from projects/Unit/TestAmqpTcpEndpointParsing.cs rename to projects/Test/Unit/TestAmqpTcpEndpointParsing.cs index 8719bb00d4..379a5b34d0 100644 --- a/projects/Unit/TestAmqpTcpEndpointParsing.cs +++ b/projects/Test/Unit/TestAmqpTcpEndpointParsing.cs @@ -30,9 +30,10 @@ //--------------------------------------------------------------------------- using System; +using RabbitMQ.Client; using Xunit; -namespace RabbitMQ.Client.Unit +namespace Test.Unit { public class TestAmqpTcpEndpointParsing { diff --git a/projects/Unit/TestAmqpUri.cs b/projects/Test/Unit/TestAmqpUri.cs similarity index 99% rename from projects/Unit/TestAmqpUri.cs rename to projects/Test/Unit/TestAmqpUri.cs index e58ae23ae1..823f41205b 100644 --- a/projects/Unit/TestAmqpUri.cs +++ b/projects/Test/Unit/TestAmqpUri.cs @@ -30,9 +30,10 @@ //--------------------------------------------------------------------------- using System; +using RabbitMQ.Client; using Xunit; -namespace RabbitMQ.Client.Unit +namespace Test.Unit { public class TestAmqpUri { diff --git a/projects/Unit/TestBasicProperties.cs b/projects/Test/Unit/TestBasicProperties.cs similarity index 81% rename from projects/Unit/TestBasicProperties.cs rename to projects/Test/Unit/TestBasicProperties.cs index ffd1579b98..0eb2cc04c4 100644 --- a/projects/Unit/TestBasicProperties.cs +++ b/projects/Test/Unit/TestBasicProperties.cs @@ -30,14 +30,10 @@ //--------------------------------------------------------------------------- using System; -using System.Collections.Generic; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using RabbitMQ.Client.Events; +using RabbitMQ.Client; using Xunit; -namespace RabbitMQ.Client.Unit +namespace Test.Unit { public class TestBasicProperties { @@ -192,44 +188,5 @@ public void TestProperties_ReplyTo(string replyTo) Assert.Equal(isReplyToPresent, basicProperties.IsReplyToPresent()); Assert.Equal(replyToAddress, basicProperties.ReplyToAddress?.ToString()); } - - [Fact] - public void TestPropertiesRountrip_Headers() - { - // Arrange - var subject = new BasicProperties - { - Headers = new Dictionary() - }; - - var cf = new ConnectionFactory(); - using (IConnection c = cf.CreateConnection()) - using (IChannel m = c.CreateChannel()) - { - QueueDeclareOk q = m.QueueDeclare(); - var bp = new BasicProperties() { Headers = new Dictionary() }; - bp.Headers["Hello"] = "World"; - byte[] sendBody = Encoding.UTF8.GetBytes("hi"); - byte[] consumeBody = null; - var consumer = new EventingBasicConsumer(m); - var are = new AutoResetEvent(false); - string response = null; - consumer.Received += async (o, a) => - { - response = Encoding.UTF8.GetString(a.BasicProperties.Headers["Hello"] as byte[]); - consumeBody = a.Body.ToArray(); - are.Set(); - await Task.Yield(); - }; - - string tag = m.BasicConsume(q.QueueName, true, consumer); - m.BasicPublish("", q.QueueName, bp, sendBody); - bool waitResFalse = are.WaitOne(5000); - m.BasicCancel(tag); - Assert.True(waitResFalse); - Assert.Equal(sendBody, consumeBody); - Assert.Equal("World", response); - } - } } } diff --git a/projects/Unit/TestBlockingCell.cs b/projects/Test/Unit/TestBlockingCell.cs similarity index 99% rename from projects/Unit/TestBlockingCell.cs rename to projects/Test/Unit/TestBlockingCell.cs index ecbb48db5d..9439e4298a 100644 --- a/projects/Unit/TestBlockingCell.cs +++ b/projects/Test/Unit/TestBlockingCell.cs @@ -34,7 +34,7 @@ using RabbitMQ.Util; using Xunit; -namespace RabbitMQ.Client.Unit +namespace Test.Unit { public class TestBlockingCell : TimingFixture { diff --git a/projects/Unit/TestContentHeaderCodec.cs b/projects/Test/Unit/TestContentHeaderCodec.cs similarity index 99% rename from projects/Unit/TestContentHeaderCodec.cs rename to projects/Test/Unit/TestContentHeaderCodec.cs index 72050bdf38..4429b468eb 100644 --- a/projects/Unit/TestContentHeaderCodec.cs +++ b/projects/Test/Unit/TestContentHeaderCodec.cs @@ -31,9 +31,10 @@ using System; using System.Collections.Generic; +using RabbitMQ.Client; using Xunit; -namespace RabbitMQ.Client.Unit +namespace Test.Unit { public class TestContentHeaderCodec { diff --git a/projects/Unit/TestFieldTableFormatting.cs b/projects/Test/Unit/TestFieldTableFormatting.cs similarity index 99% rename from projects/Unit/TestFieldTableFormatting.cs rename to projects/Test/Unit/TestFieldTableFormatting.cs index 211a1ca161..87232c2b87 100644 --- a/projects/Unit/TestFieldTableFormatting.cs +++ b/projects/Test/Unit/TestFieldTableFormatting.cs @@ -32,10 +32,12 @@ using System; using System.Collections; using System.Text; +using RabbitMQ; +using RabbitMQ.Client; using RabbitMQ.Client.Impl; using Xunit; -namespace RabbitMQ.Client.Unit +namespace Test.Unit { public class TestFieldTableFormatting : WireFormattingFixture { diff --git a/projects/Unit/TestFieldTableFormattingGeneric.cs b/projects/Test/Unit/TestFieldTableFormattingGeneric.cs similarity index 99% rename from projects/Unit/TestFieldTableFormattingGeneric.cs rename to projects/Test/Unit/TestFieldTableFormattingGeneric.cs index 4df0022264..03ca347c89 100644 --- a/projects/Unit/TestFieldTableFormattingGeneric.cs +++ b/projects/Test/Unit/TestFieldTableFormattingGeneric.cs @@ -33,10 +33,12 @@ using System.Collections; using System.Collections.Generic; using System.Text; +using RabbitMQ; +using RabbitMQ.Client; using RabbitMQ.Client.Impl; using Xunit; -namespace RabbitMQ.Client.Unit +namespace Test.Unit { public class TestFieldTableFormattingGeneric : WireFormattingFixture { diff --git a/projects/Unit/TestFrameFormatting.cs b/projects/Test/Unit/TestFrameFormatting.cs similarity index 85% rename from projects/Unit/TestFrameFormatting.cs rename to projects/Test/Unit/TestFrameFormatting.cs index 08e1d8427f..9bbb31e46d 100644 --- a/projects/Unit/TestFrameFormatting.cs +++ b/projects/Test/Unit/TestFrameFormatting.cs @@ -30,20 +30,20 @@ //--------------------------------------------------------------------------- using System; -using System.Buffers; -using System.Runtime.InteropServices; +using RabbitMQ.Client; using RabbitMQ.Client.Framing.Impl; +using RabbitMQ.Client.Impl; using Xunit; -namespace RabbitMQ.Client.Unit +namespace Test.Unit { public class TestFrameFormatting : WireFormattingFixture { [Fact] public void HeartbeatFrame() { - Memory memory = Impl.Framing.Heartbeat.GetHeartbeatFrame(); - Span frameSpan = memory.Span; + RentedMemory sfc = Framing.Heartbeat.GetHeartbeatFrame(); + ReadOnlySpan frameSpan = sfc.Memory.Span; try { @@ -59,10 +59,7 @@ public void HeartbeatFrame() } finally { - if (MemoryMarshal.TryGetArray(memory, out ArraySegment segment)) - { - ArrayPool.Shared.Return(segment.Array); - } + ClientArrayPool.Return(sfc.RentedArray); } } @@ -74,10 +71,10 @@ public void HeaderFrame() var basicProperties = new BasicProperties { AppId = "A" }; int payloadSize = ((IAmqpWriteable)basicProperties).GetRequiredBufferSize(); - byte[] frameBytes = new byte[Impl.Framing.Header.FrameSize + BodyLength + payloadSize]; - Impl.Framing.Header.WriteTo(frameBytes, Channel, ref basicProperties, BodyLength); + byte[] frameBytes = new byte[RabbitMQ.Client.Impl.Framing.Header.FrameSize + BodyLength + payloadSize]; + RabbitMQ.Client.Impl.Framing.Header.WriteTo(frameBytes, Channel, ref basicProperties, BodyLength); - Assert.Equal(20, Impl.Framing.Header.FrameSize); + Assert.Equal(20, RabbitMQ.Client.Impl.Framing.Header.FrameSize); Assert.Equal(Constants.FrameHeader, frameBytes[0]); Assert.Equal(0, frameBytes[1]); // channel Assert.Equal(Channel, frameBytes[2]); // channel @@ -111,10 +108,10 @@ public void MethodFrame() var method = new BasicPublish("E", "R", true, true); int payloadSize = method.GetRequiredBufferSize(); - byte[] frameBytes = new byte[Impl.Framing.Method.FrameSize + payloadSize]; - Impl.Framing.Method.WriteTo(frameBytes, Channel, ref method); + byte[] frameBytes = new byte[RabbitMQ.Client.Impl.Framing.Method.FrameSize + payloadSize]; + RabbitMQ.Client.Impl.Framing.Method.WriteTo(frameBytes, Channel, ref method); - Assert.Equal(12, Impl.Framing.Method.FrameSize); + Assert.Equal(12, RabbitMQ.Client.Impl.Framing.Method.FrameSize); Assert.Equal(Constants.FrameMethod, frameBytes[0]); Assert.Equal(0, frameBytes[1]); // channel Assert.Equal(Channel, frameBytes[2]); // channel @@ -142,10 +139,10 @@ public void BodySegmentFrame() const int Channel = 3; byte[] payload = new byte[4]; - byte[] frameBytes = new byte[Impl.Framing.BodySegment.FrameSize + payload.Length]; - Impl.Framing.BodySegment.WriteTo(frameBytes, Channel, payload); + byte[] frameBytes = new byte[RabbitMQ.Client.Impl.Framing.BodySegment.FrameSize + payload.Length]; + RabbitMQ.Client.Impl.Framing.BodySegment.WriteTo(frameBytes, Channel, payload); - Assert.Equal(8, Impl.Framing.BodySegment.FrameSize); + Assert.Equal(8, RabbitMQ.Client.Impl.Framing.BodySegment.FrameSize); Assert.Equal(Constants.FrameBody, frameBytes[0]); Assert.Equal(0, frameBytes[1]); // channel Assert.Equal(Channel, frameBytes[2]); // channel diff --git a/projects/Unit/TestIEndpointResolverExtensions.cs b/projects/Test/Unit/TestIEndpointResolverExtensions.cs similarity index 98% rename from projects/Unit/TestIEndpointResolverExtensions.cs rename to projects/Test/Unit/TestIEndpointResolverExtensions.cs index b09a6f6612..b9fcd81750 100644 --- a/projects/Unit/TestIEndpointResolverExtensions.cs +++ b/projects/Test/Unit/TestIEndpointResolverExtensions.cs @@ -31,9 +31,10 @@ using System; using System.Collections.Generic; +using RabbitMQ.Client; using Xunit; -namespace RabbitMQ.Client.Unit +namespace Test.Unit { public class TestEndpointResolver : IEndpointResolver { diff --git a/projects/Unit/TestIntAllocator.cs b/projects/Test/Unit/TestIntAllocator.cs similarity index 98% rename from projects/Unit/TestIntAllocator.cs rename to projects/Test/Unit/TestIntAllocator.cs index cdf81c202b..18c41bcce5 100644 --- a/projects/Unit/TestIntAllocator.cs +++ b/projects/Test/Unit/TestIntAllocator.cs @@ -34,7 +34,7 @@ using RabbitMQ.Util; using Xunit; -namespace RabbitMQ.Client.Unit +namespace Test.Unit { public class TestIntAllocator { diff --git a/projects/Unit/TestMethodArgumentCodec.cs b/projects/Test/Unit/TestMethodArgumentCodec.cs similarity index 99% rename from projects/Unit/TestMethodArgumentCodec.cs rename to projects/Test/Unit/TestMethodArgumentCodec.cs index cf8b9cf572..7c8f279c9e 100644 --- a/projects/Unit/TestMethodArgumentCodec.cs +++ b/projects/Test/Unit/TestMethodArgumentCodec.cs @@ -32,10 +32,11 @@ using System; using System.Collections; using System.Text; +using RabbitMQ; using RabbitMQ.Client.Impl; using Xunit; -namespace RabbitMQ.Client.Unit +namespace Test.Unit { public class TestMethodArgumentCodec { diff --git a/projects/Unit/TestNetworkByteOrderSerialization.cs b/projects/Test/Unit/TestNetworkByteOrderSerialization.cs similarity index 99% rename from projects/Unit/TestNetworkByteOrderSerialization.cs rename to projects/Test/Unit/TestNetworkByteOrderSerialization.cs index 95368903fa..a0d0e5cf95 100644 --- a/projects/Unit/TestNetworkByteOrderSerialization.cs +++ b/projects/Test/Unit/TestNetworkByteOrderSerialization.cs @@ -30,10 +30,11 @@ //--------------------------------------------------------------------------- using System; +using RabbitMQ; using RabbitMQ.Util; using Xunit; -namespace RabbitMQ.Client.Unit +namespace Test.Unit { public class TestNetworkByteOrderSerialization { diff --git a/projects/Unit/TestPublicationAddress.cs b/projects/Test/Unit/TestPublicationAddress.cs similarity index 98% rename from projects/Unit/TestPublicationAddress.cs rename to projects/Test/Unit/TestPublicationAddress.cs index 824dcc5218..2d570bcf7f 100644 --- a/projects/Unit/TestPublicationAddress.cs +++ b/projects/Test/Unit/TestPublicationAddress.cs @@ -30,9 +30,10 @@ //--------------------------------------------------------------------------- using System; +using RabbitMQ.Client; using Xunit; -namespace RabbitMQ.Client.Unit +namespace Test.Unit { public class TestPublicationAddress { diff --git a/projects/Unit/TestRpcContinuationQueue.cs b/projects/Test/Unit/TestRpcContinuationQueue.cs similarity index 98% rename from projects/Unit/TestRpcContinuationQueue.cs rename to projects/Test/Unit/TestRpcContinuationQueue.cs index 8ec90ea162..f3fcfaea80 100644 --- a/projects/Unit/TestRpcContinuationQueue.cs +++ b/projects/Test/Unit/TestRpcContinuationQueue.cs @@ -33,7 +33,7 @@ using RabbitMQ.Client.Impl; using Xunit; -namespace RabbitMQ.Client.Unit +namespace Test.Unit { public class TestRpcContinuationQueue { diff --git a/projects/Unit/TestTcpClientAdapter.cs b/projects/Test/Unit/TestTcpClientAdapter.cs similarity index 98% rename from projects/Unit/TestTcpClientAdapter.cs rename to projects/Test/Unit/TestTcpClientAdapter.cs index f3f9009484..14ef1aa3ff 100644 --- a/projects/Unit/TestTcpClientAdapter.cs +++ b/projects/Test/Unit/TestTcpClientAdapter.cs @@ -34,7 +34,7 @@ using RabbitMQ.Client.Impl; using Xunit; -namespace RabbitMQ.Client.Unit +namespace Test.Unit { public class TestTcpClientAdapter { diff --git a/projects/Unit/TestTimerBasedCredentialRefresher.cs b/projects/Test/Unit/TestTimerBasedCredentialRefresher.cs similarity index 99% rename from projects/Unit/TestTimerBasedCredentialRefresher.cs rename to projects/Test/Unit/TestTimerBasedCredentialRefresher.cs index 4d047e9d80..367f7caa7c 100644 --- a/projects/Unit/TestTimerBasedCredentialRefresher.cs +++ b/projects/Test/Unit/TestTimerBasedCredentialRefresher.cs @@ -31,10 +31,11 @@ using System; using System.Threading; +using RabbitMQ.Client; using Xunit; using Xunit.Abstractions; -namespace RabbitMQ.Client.Unit +namespace Test.Unit { public class MockCredentialsProvider : ICredentialsProvider { diff --git a/projects/RabbitMQ.Client/client/framing/BasicRecover.cs b/projects/Test/Unit/TimingFixture.cs similarity index 67% rename from projects/RabbitMQ.Client/client/framing/BasicRecover.cs rename to projects/Test/Unit/TimingFixture.cs index 471e9b2274..fff7d4b031 100644 --- a/projects/RabbitMQ.Client/client/framing/BasicRecover.cs +++ b/projects/Test/Unit/TimingFixture.cs @@ -30,30 +30,17 @@ //--------------------------------------------------------------------------- using System; -using RabbitMQ.Client.client.framing; -using RabbitMQ.Client.Impl; -namespace RabbitMQ.Client.Framing.Impl +namespace Test.Unit { - internal readonly struct BasicRecover : IOutgoingAmqpMethod + public class TimingFixture { - public readonly bool _requeue; - - public BasicRecover(bool Requeue) - { - _requeue = Requeue; - } - - public ProtocolCommandId ProtocolCommandId => ProtocolCommandId.BasicRecover; - - public int WriteTo(Span span) - { - return WireFormatting.WriteBits(ref span.GetStart(), _requeue); - } - - public int GetRequiredBufferSize() - { - return 1; // bytes for bit fields - } + public static readonly TimeSpan TimingInterval = TimeSpan.FromMilliseconds(300); + public static readonly TimeSpan TimingInterval_2X = TimeSpan.FromMilliseconds(600); + public static readonly TimeSpan TimingInterval_4X = TimeSpan.FromMilliseconds(1200); + public static readonly TimeSpan TimingInterval_8X = TimeSpan.FromMilliseconds(2400); + public static readonly TimeSpan TimingInterval_16X = TimeSpan.FromMilliseconds(4800); + public static readonly TimeSpan SafetyMargin = TimeSpan.FromMilliseconds(150); + public static readonly TimeSpan TestTimeout = TimeSpan.FromSeconds(5); } } diff --git a/projects/Unit/Unit.csproj b/projects/Test/Unit/Unit.csproj similarity index 79% rename from projects/Unit/Unit.csproj rename to projects/Test/Unit/Unit.csproj index 77e4d3ed39..75bae9fff7 100644 --- a/projects/Unit/Unit.csproj +++ b/projects/Test/Unit/Unit.csproj @@ -9,14 +9,15 @@ - ../rabbit.snk + ../../rabbit.snk true latest 7.0 + true - + @@ -26,12 +27,12 @@ all - - + + runtime; build; native; contentfiles; analyzers; buildtransitive all - + diff --git a/projects/Unit/WireFormattingFixture.cs b/projects/Test/Unit/WireFormattingFixture.cs similarity index 98% rename from projects/Unit/WireFormattingFixture.cs rename to projects/Test/Unit/WireFormattingFixture.cs index ce1443f1f0..36986070c8 100644 --- a/projects/Unit/WireFormattingFixture.cs +++ b/projects/Test/Unit/WireFormattingFixture.cs @@ -32,7 +32,7 @@ using System; using Xunit; -namespace RabbitMQ.Client.Unit +namespace Test.Unit { public class WireFormattingFixture { diff --git a/projects/TestApplications/MassPublish/Program.cs b/projects/TestApplications/MassPublish/Program.cs deleted file mode 100644 index 2503d80d74..0000000000 --- a/projects/TestApplications/MassPublish/Program.cs +++ /dev/null @@ -1,85 +0,0 @@ -using System; -using System.Diagnostics; -using System.Threading; -using System.Threading.Tasks; -using RabbitMQ.Client; -using RabbitMQ.Client.Events; - -namespace MassPublish -{ - public static class Program - { - private const int BatchesToSend = 100; - private const int ItemsPerBatch = 500; - - private static int messagesSent; - private static int messagesReceived; - private static AutoResetEvent doneEvent; - - public static void Main() - { - ThreadPool.SetMinThreads(16 * Environment.ProcessorCount, 16 * Environment.ProcessorCount); - - doneEvent = new AutoResetEvent(false); - - var connectionFactory = new ConnectionFactory { DispatchConsumersAsync = true }; - var connection = connectionFactory.CreateConnection(); - var publisher = connection.CreateChannel(); - var subscriber = connection.CreateChannel(); - - publisher.ConfirmSelect(); - publisher.ExchangeDeclare("test", ExchangeType.Topic, true, false); - - subscriber.QueueDeclare("testqueue", false, false, true); - var asyncListener = new AsyncEventingBasicConsumer(subscriber); - asyncListener.Received += AsyncListener_Received; - subscriber.QueueBind("testqueue", "test", "myawesome.routing.key"); - subscriber.BasicConsume("testqueue", true, "testconsumer", asyncListener); - - byte[] payload = new byte[512]; - var watch = Stopwatch.StartNew(); - _ = Task.Run(async () => - { - while (messagesSent < BatchesToSend * ItemsPerBatch) - { - for (int i = 0; i < ItemsPerBatch; i++) - { - var properties = new BasicProperties - { - AppId = "testapp", - }; - publisher.BasicPublish("test", "myawesome.routing.key", properties, payload); - } - messagesSent += ItemsPerBatch; - await publisher.WaitForConfirmsOrDieAsync().ConfigureAwait(false); - } - }); - - Console.WriteLine($"Sending {BatchesToSend} batches for {ItemsPerBatch} items per batch. => Total messages: {BatchesToSend * ItemsPerBatch}"); - Console.WriteLine(); - Console.WriteLine(" Sent | Received"); - while (!doneEvent.WaitOne(500)) - { - Console.WriteLine($"{messagesSent,5} | {messagesReceived,5}"); - } - watch.Stop(); - Console.WriteLine($"{messagesSent,5} | {messagesReceived,5}"); - Console.WriteLine(); - Console.WriteLine($"Took {watch.Elapsed.TotalMilliseconds} ms"); - - publisher.Dispose(); - subscriber.Dispose(); - connection.Dispose(); - Console.ReadLine(); - } - - private static Task AsyncListener_Received(object sender, BasicDeliverEventArgs @event) - { - if (Interlocked.Increment(ref messagesReceived) == BatchesToSend * ItemsPerBatch) - { - doneEvent.Set(); - } - return Task.CompletedTask; - } - } -} diff --git a/projects/Unit/Fixtures.cs b/projects/Unit/Fixtures.cs deleted file mode 100644 index ef345789b4..0000000000 --- a/projects/Unit/Fixtures.cs +++ /dev/null @@ -1,513 +0,0 @@ -// This source code is dual-licensed under the Apache License, version -// 2.0, and the Mozilla Public License, version 2.0. -// -// The APL v2.0: -// -//--------------------------------------------------------------------------- -// Copyright (c) 2007-2020 VMware, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// https://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -//--------------------------------------------------------------------------- -// -// The MPL v2.0: -// -//--------------------------------------------------------------------------- -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. -// -// Copyright (c) 2007-2020 VMware, Inc. All rights reserved. -//--------------------------------------------------------------------------- - -#pragma warning disable 2002 - -using System; -using System.Collections.Generic; -using System.Reflection; -using System.Text; -using System.Threading; -using RabbitMQ.Client.Framing.Impl; -using Xunit; -using Xunit.Abstractions; - -namespace RabbitMQ.Client.Unit -{ - [Collection("IntegrationFixture")] - public class IntegrationFixture : IDisposable - { - internal IConnectionFactory _connFactory; - internal IConnection _conn; - internal IChannel _channel; - internal Encoding _encoding = new UTF8Encoding(); - - public static TimeSpan RECOVERY_INTERVAL = TimeSpan.FromSeconds(2); - protected readonly TimeSpan _waitSpan; - protected readonly ITestOutputHelper _output; - protected readonly string _testDisplayName; - - public IntegrationFixture(ITestOutputHelper output) - { - _output = output; - var type = _output.GetType(); - var testMember = type.GetField("test", BindingFlags.Instance | BindingFlags.NonPublic); - var test = (ITest)testMember.GetValue(output); - _testDisplayName = test.DisplayName; - - SetUp(); - - if (IsRunningInCI()) - { - _waitSpan = TimeSpan.FromSeconds(30); - } - else - { - _waitSpan = TimeSpan.FromSeconds(10); - } - } - - protected virtual void SetUp() - { - _connFactory = new ConnectionFactory(); - _conn = _connFactory.CreateConnection(); - _channel = _conn.CreateChannel(); - } - - public virtual void Dispose() - { - if (_channel.IsOpen) - { - _channel.Close(); - } - - if (_conn.IsOpen) - { - _conn.Close(); - } - - ReleaseResources(); - } - - protected virtual void ReleaseResources() - { - // no-op - } - - // - // Connections - // - - internal AutorecoveringConnection CreateAutorecoveringConnection() - { - return CreateAutorecoveringConnection(RECOVERY_INTERVAL); - } - - internal AutorecoveringConnection CreateAutorecoveringConnection(ICredentialsProvider credentialsProvider) - { - return CreateAutorecoveringConnection(RECOVERY_INTERVAL, credentialsProvider); - } - - internal AutorecoveringConnection CreateAutorecoveringConnection(IList hostnames) - { - return CreateAutorecoveringConnection(RECOVERY_INTERVAL, hostnames); - } - - internal AutorecoveringConnection CreateAutorecoveringConnection(TimeSpan interval, ICredentialsProvider credentialsProvider = null) - { - var cf = new ConnectionFactory - { - AutomaticRecoveryEnabled = true, - CredentialsProvider = credentialsProvider, - NetworkRecoveryInterval = interval - }; - return (AutorecoveringConnection)cf.CreateConnection($"{_testDisplayName}:{Guid.NewGuid()}"); - } - - internal AutorecoveringConnection CreateAutorecoveringConnection(TimeSpan interval, IList hostnames) - { - var cf = new ConnectionFactory - { - AutomaticRecoveryEnabled = true, - // tests that use this helper will likely list unreachable hosts, - // make sure we time out quickly on those - RequestedConnectionTimeout = TimeSpan.FromSeconds(1), - NetworkRecoveryInterval = interval - }; - return (AutorecoveringConnection)cf.CreateConnection(hostnames, $"{_testDisplayName}:{Guid.NewGuid()}"); - } - - internal AutorecoveringConnection CreateAutorecoveringConnection(IList endpoints) - { - var cf = new ConnectionFactory - { - AutomaticRecoveryEnabled = true, - // tests that use this helper will likely list unreachable hosts, - // make sure we time out quickly on those - RequestedConnectionTimeout = TimeSpan.FromSeconds(1), - NetworkRecoveryInterval = RECOVERY_INTERVAL - }; - return (AutorecoveringConnection)cf.CreateConnection(endpoints, $"{_testDisplayName}:{Guid.NewGuid()}"); - } - - internal AutorecoveringConnection CreateAutorecoveringConnectionWithTopologyRecoveryDisabled() - { - var cf = new ConnectionFactory - { - AutomaticRecoveryEnabled = true, - TopologyRecoveryEnabled = false, - NetworkRecoveryInterval = RECOVERY_INTERVAL - }; - return (AutorecoveringConnection)cf.CreateConnection($"{_testDisplayName}:{Guid.NewGuid()}"); - } - - internal AutorecoveringConnection CreateAutorecoveringConnectionWithTopologyRecoveryFilter(TopologyRecoveryFilter filter) - { - var cf = new ConnectionFactory - { - AutomaticRecoveryEnabled = true, - TopologyRecoveryEnabled = true, - TopologyRecoveryFilter = filter - }; - - return (AutorecoveringConnection)cf.CreateConnection($"{_testDisplayName}:{Guid.NewGuid()}"); - } - - internal AutorecoveringConnection CreateAutorecoveringConnectionWithTopologyRecoveryExceptionHandler(TopologyRecoveryExceptionHandler handler) - { - var cf = new ConnectionFactory - { - AutomaticRecoveryEnabled = true, - TopologyRecoveryEnabled = true, - TopologyRecoveryExceptionHandler = handler - }; - - return (AutorecoveringConnection)cf.CreateConnection($"{_testDisplayName}:{Guid.NewGuid()}"); - } - - internal IConnection CreateConnectionWithContinuationTimeout(bool automaticRecoveryEnabled, TimeSpan continuationTimeout) - { - var cf = new ConnectionFactory - { - AutomaticRecoveryEnabled = automaticRecoveryEnabled, - ContinuationTimeout = continuationTimeout - }; - return cf.CreateConnection($"{_testDisplayName}:{Guid.NewGuid()}"); - } - - // - // Channels - // - - internal void WithTemporaryChannel(Action action) - { - IChannel channel = _conn.CreateChannel(); - - try - { - action(channel); - } - finally - { - channel.Abort(); - } - } - - internal void WithClosedChannel(Action action) - { - IChannel channel = _conn.CreateChannel(); - channel.Close(); - - action(channel); - } - - internal bool WaitForConfirms(IChannel m) - { - using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(4)); - return m.WaitForConfirmsAsync(cts.Token).GetAwaiter().GetResult(); - } - - // - // Exchanges - // - - internal string GenerateExchangeName() - { - return $"exchange{Guid.NewGuid()}"; - } - - internal byte[] RandomMessageBody() - { - return _encoding.GetBytes(Guid.NewGuid().ToString()); - } - - internal string DeclareNonDurableExchange(IChannel m, string x) - { - m.ExchangeDeclare(x, "fanout", false); - return x; - } - - internal string DeclareNonDurableExchangeNoWait(IChannel m, string x) - { - m.ExchangeDeclareNoWait(x, "fanout", false, false, null); - return x; - } - - // - // Queues - // - - internal string GenerateQueueName() - { - return $"queue{Guid.NewGuid()}"; - } - - internal void WithTemporaryNonExclusiveQueue(Action action) - { - WithTemporaryNonExclusiveQueue(_channel, action); - } - - internal void WithTemporaryNonExclusiveQueue(IChannel channel, Action action) - { - WithTemporaryNonExclusiveQueue(channel, action, GenerateQueueName()); - } - - internal void WithTemporaryNonExclusiveQueue(IChannel channel, Action action, string queue) - { - try - { - channel.QueueDeclare(queue, false, false, false, null); - action(channel, queue); - } - finally - { - WithTemporaryChannel(tm => tm.QueueDelete(queue)); - } - } - - internal void WithTemporaryQueueNoWait(IChannel channel, Action action, string queue) - { - try - { - channel.QueueDeclareNoWait(queue, false, true, false, null); - action(channel, queue); - } - finally - { - WithTemporaryChannel(x => x.QueueDelete(queue)); - } - } - - internal void EnsureNotEmpty(string q, string body) - { - WithTemporaryChannel(x => x.BasicPublish("", q, _encoding.GetBytes(body))); - } - - internal void WithNonEmptyQueue(Action action) - { - WithNonEmptyQueue(action, "msg"); - } - - internal void WithNonEmptyQueue(Action action, string msg) - { - WithTemporaryNonExclusiveQueue((m, q) => - { - EnsureNotEmpty(q, msg); - action(m, q); - }); - } - - internal void WithEmptyQueue(Action action) - { - WithTemporaryNonExclusiveQueue((channel, queue) => - { - channel.QueuePurge(queue); - action(channel, queue); - }); - } - - internal void AssertMessageCount(string q, uint count) - { - WithTemporaryChannel((m) => - { - QueueDeclareOk ok = m.QueueDeclarePassive(q); - Assert.Equal(count, ok.MessageCount); - }); - } - - internal void AssertConsumerCount(string q, int count) - { - WithTemporaryChannel((m) => - { - QueueDeclareOk ok = m.QueueDeclarePassive(q); - Assert.Equal((uint)count, ok.ConsumerCount); - }); - } - - internal void AssertConsumerCount(IChannel m, string q, uint count) - { - QueueDeclareOk ok = m.QueueDeclarePassive(q); - Assert.Equal(count, ok.ConsumerCount); - } - - // - // Shutdown - // - - internal void AssertShutdownError(ShutdownEventArgs args, int code) - { - Assert.Equal(args.ReplyCode, code); - } - - internal void AssertPreconditionFailed(ShutdownEventArgs args) - { - AssertShutdownError(args, Constants.PreconditionFailed); - } - - // - // Concurrency - // - - internal void WaitOn(object o) - { - lock (o) - { - Monitor.Wait(o, TimingFixture.TestTimeout); - } - } - - // - // Flow Control - // - - internal void Block() - { - RabbitMQCtl.Block(_conn, _encoding); - } - - internal void Unblock() - { - RabbitMQCtl.Unblock(); - } - - // - // Connection Closure - // - - internal void CloseConnection(IConnection conn) - { - RabbitMQCtl.CloseConnection(conn); - } - - internal void CloseAllConnections() - { - RabbitMQCtl.CloseAllConnections(); - } - - internal void RestartRabbitMQ() - { - RabbitMQCtl.RestartRabbitMQ(); - } - - internal void StopRabbitMQ() - { - RabbitMQCtl.StopRabbitMQ(); - } - - internal void StartRabbitMQ() - { - RabbitMQCtl.StartRabbitMQ(); - RabbitMQCtl.AwaitRabbitMQ(); - } - - // - // Concurrency and Coordination - // - - internal void Wait(ManualResetEventSlim latch) - { - Assert.True(latch.Wait(_waitSpan), "waiting on a latch timed out"); - } - - internal void Wait(ManualResetEventSlim latch, TimeSpan timeSpan) - { - Assert.True(latch.Wait(timeSpan), "waiting on a latch timed out"); - } - - private static bool IsRunningInCI() - { - if (bool.TryParse(Environment.GetEnvironmentVariable("CI"), out bool ci)) - { - if (ci == true) - { - return true; - } - } - - string concourse = Environment.GetEnvironmentVariable("CONCOURSE_CI_BUILD"); - string gha = Environment.GetEnvironmentVariable("GITHUB_ACTIONS"); - if (String.IsNullOrWhiteSpace(concourse) && String.IsNullOrWhiteSpace(gha)) - { - return false; - } - - return true; - } - } - - public sealed class IgnoreOnVersionsEarlierThan : FactAttribute - { - public IgnoreOnVersionsEarlierThan(int major, int minor) - { - if (!CheckMiniumVersion(new Version(major, minor))) - { - Skip = $"Skipped test. It requires RabbitMQ +{major}.{minor}"; - } - } - - private bool CheckMiniumVersion(Version miniumVersion) - { - using (var _conn = new ConnectionFactory().CreateConnection()) - { - System.Collections.Generic.IDictionary properties = _conn.ServerProperties; - - if (properties.TryGetValue("version", out object versionVal)) - { - string versionStr = Encoding.UTF8.GetString((byte[])versionVal); - - int dashIdx = Math.Max(versionStr.IndexOf('-'), versionStr.IndexOf('+')); - if (dashIdx > 0) - { - versionStr = versionStr.Remove(dashIdx); - } - - if (Version.TryParse(versionStr, out Version version)) - { - return version >= miniumVersion; - } - } - - return false; - } - } - } - - public class TimingFixture - { - public static readonly TimeSpan TimingInterval = TimeSpan.FromMilliseconds(300); - public static readonly TimeSpan TimingInterval_2X = TimeSpan.FromMilliseconds(600); - public static readonly TimeSpan TimingInterval_4X = TimeSpan.FromMilliseconds(1200); - public static readonly TimeSpan TimingInterval_8X = TimeSpan.FromMilliseconds(2400); - public static readonly TimeSpan TimingInterval_16X = TimeSpan.FromMilliseconds(4800); - public static readonly TimeSpan SafetyMargin = TimeSpan.FromMilliseconds(150); - public static readonly TimeSpan TestTimeout = TimeSpan.FromSeconds(5); - } -} diff --git a/projects/Unit/TestAsyncConsumer.cs b/projects/Unit/TestAsyncConsumer.cs deleted file mode 100644 index 5ab094a360..0000000000 --- a/projects/Unit/TestAsyncConsumer.cs +++ /dev/null @@ -1,358 +0,0 @@ -// This source code is dual-licensed under the Apache License, version -// 2.0, and the Mozilla Public License, version 2.0. -// -// The APL v2.0: -// -//--------------------------------------------------------------------------- -// Copyright (c) 2007-2020 VMware, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// https://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -//--------------------------------------------------------------------------- -// -// The MPL v2.0: -// -//--------------------------------------------------------------------------- -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. -// -// Copyright (c) 2007-2020 VMware, Inc. All rights reserved. -//--------------------------------------------------------------------------- - -using System; -using System.Security.Cryptography; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using RabbitMQ.Client.Events; -using Xunit; -using Xunit.Abstractions; - -namespace RabbitMQ.Client.Unit -{ - [Collection("IntegrationFixture")] - public class TestAsyncConsumer - { - private readonly ITestOutputHelper _output; - - public TestAsyncConsumer(ITestOutputHelper output) - { - _output = output; - } - - [Fact] - public void TestBasicRoundtrip() - { - var cf = new ConnectionFactory { DispatchConsumersAsync = true }; - using (IConnection c = cf.CreateConnection()) - { - using (IChannel m = c.CreateChannel()) - { - QueueDeclareOk q = m.QueueDeclare(); - byte[] body = System.Text.Encoding.UTF8.GetBytes("async-hi"); - m.BasicPublish("", q.QueueName, body); - var consumer = new AsyncEventingBasicConsumer(m); - var are = new AutoResetEvent(false); - consumer.Received += async (o, a) => - { - are.Set(); - await Task.Yield(); - }; - string tag = m.BasicConsume(q.QueueName, true, consumer); - // ensure we get a delivery - bool waitRes = are.WaitOne(2000); - Assert.True(waitRes); - // unsubscribe and ensure no further deliveries - m.BasicCancel(tag); - m.BasicPublish("", q.QueueName, body); - bool waitResFalse = are.WaitOne(2000); - Assert.False(waitResFalse); - } - } - } - - [Fact] - public async Task TestBasicRoundtripConcurrent() - { - var cf = new ConnectionFactory { DispatchConsumersAsync = true, ConsumerDispatchConcurrency = 2 }; - using (IConnection c = cf.CreateConnection()) - { - using (IChannel m = c.CreateChannel()) - { - QueueDeclareOk q = m.QueueDeclare(); - string publish1 = get_unique_string(1024); - byte[] body = Encoding.UTF8.GetBytes(publish1); - m.BasicPublish("", q.QueueName, body); - - string publish2 = get_unique_string(1024); - body = Encoding.UTF8.GetBytes(publish2); - m.BasicPublish("", q.QueueName, body); - - var consumer = new AsyncEventingBasicConsumer(m); - - var publish1SyncSource = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - var publish2SyncSource = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - var maximumWaitTime = TimeSpan.FromSeconds(10); - var tokenSource = new CancellationTokenSource(maximumWaitTime); - tokenSource.Token.Register(() => - { - publish1SyncSource.TrySetResult(false); - publish2SyncSource.TrySetResult(false); - }); - - consumer.Received += async (o, a) => - { - string decoded = Encoding.ASCII.GetString(a.Body.ToArray()); - if (decoded == publish1) - { - publish1SyncSource.TrySetResult(true); - await publish2SyncSource.Task; - } - else if (decoded == publish2) - { - publish2SyncSource.TrySetResult(true); - await publish1SyncSource.Task; - } - }; - - m.BasicConsume(q.QueueName, true, consumer); - - // ensure we get a delivery - await Task.WhenAll(publish1SyncSource.Task, publish2SyncSource.Task); - - bool result1 = await publish1SyncSource.Task; - Assert.True(result1, $"1 - Non concurrent dispatch lead to deadlock after {maximumWaitTime}"); - - bool result2 = await publish2SyncSource.Task; - Assert.True(result2, $"2 - Non concurrent dispatch lead to deadlock after {maximumWaitTime}"); - } - } - } - - [Fact] - public async Task TestBasicRoundtripConcurrentManyMessages() - { - const int publish_total = 4096; - string queueName = $"{nameof(TestBasicRoundtripConcurrentManyMessages)}-{Guid.NewGuid()}"; - - string publish1 = get_unique_string(32768); - byte[] body1 = Encoding.ASCII.GetBytes(publish1); - string publish2 = get_unique_string(32768); - byte[] body2 = Encoding.ASCII.GetBytes(publish2); - - var cf = new ConnectionFactory { DispatchConsumersAsync = true, ConsumerDispatchConcurrency = 2 }; - - using (IConnection c = cf.CreateConnection()) - { - using (IChannel m = c.CreateChannel()) - { - QueueDeclareOk q = m.QueueDeclare(queue: queueName, exclusive: false, durable: true); - Assert.Equal(q.QueueName, queueName); - } - } - - Task publishTask = Task.Run(async () => - { - using (IConnection c = cf.CreateConnection()) - { - using (IChannel m = c.CreateChannel()) - { - QueueDeclareOk q = m.QueueDeclare(queue: queueName, exclusive: false, durable: true); - for (int i = 0; i < publish_total; i++) - { - await m.BasicPublishAsync(string.Empty, queueName, body1); - await m.BasicPublishAsync(string.Empty, queueName, body2); - } - } - } - }); - - Task consumeTask = Task.Run(async () => - { - var publish1SyncSource = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - var publish2SyncSource = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - var maximumWaitTime = TimeSpan.FromSeconds(30); - var tokenSource = new CancellationTokenSource(maximumWaitTime); - tokenSource.Token.Register(() => - { - publish1SyncSource.TrySetResult(false); - publish2SyncSource.TrySetResult(false); - }); - - using (IConnection c = cf.CreateConnection()) - { - using (IChannel m = c.CreateChannel()) - { - var consumer = new AsyncEventingBasicConsumer(m); - - int publish1_count = 0; - int publish2_count = 0; - - consumer.Received += async (o, a) => - { - string decoded = Encoding.ASCII.GetString(a.Body.ToArray()); - if (decoded == publish1) - { - if (Interlocked.Increment(ref publish1_count) >= publish_total) - { - publish1SyncSource.TrySetResult(true); - await publish2SyncSource.Task; - } - } - else if (decoded == publish2) - { - if (Interlocked.Increment(ref publish2_count) >= publish_total) - { - publish2SyncSource.TrySetResult(true); - await publish1SyncSource.Task; - } - } - }; - - m.BasicConsume(queueName, true, consumer); - - // ensure we get a delivery - await Task.WhenAll(publish1SyncSource.Task, publish2SyncSource.Task); - - bool result1 = await publish1SyncSource.Task; - Assert.True(result1, $"Non concurrent dispatch lead to deadlock after {maximumWaitTime}"); - - bool result2 = await publish2SyncSource.Task; - Assert.True(result2, $"Non concurrent dispatch lead to deadlock after {maximumWaitTime}"); - } - } - }); - - await Task.WhenAll(publishTask, consumeTask); - } - - [Fact] - public void TestBasicRoundtripNoWait() - { - var cf = new ConnectionFactory { DispatchConsumersAsync = true }; - using (IConnection c = cf.CreateConnection()) - { - using (IChannel m = c.CreateChannel()) - { - QueueDeclareOk q = m.QueueDeclare(); - byte[] body = System.Text.Encoding.UTF8.GetBytes("async-hi"); - m.BasicPublish("", q.QueueName, body); - var consumer = new AsyncEventingBasicConsumer(m); - var are = new AutoResetEvent(false); - consumer.Received += async (o, a) => - { - are.Set(); - await Task.Yield(); - }; - string tag = m.BasicConsume(q.QueueName, true, consumer); - // ensure we get a delivery - bool waitRes = are.WaitOne(2000); - Assert.True(waitRes); - // unsubscribe and ensure no further deliveries - m.BasicCancelNoWait(tag); - m.BasicPublish("", q.QueueName, body); - bool waitResFalse = are.WaitOne(2000); - Assert.False(waitResFalse); - } - } - } - - [Fact] - public async void ConcurrentEventingTestForReceived() - { - const int NumberOfThreads = 4; - const int NumberOfRegistrations = 5000; - - var called = new byte[NumberOfThreads * NumberOfRegistrations]; - - var cf = new ConnectionFactory { DispatchConsumersAsync = true }; - using (IConnection c = cf.CreateConnection()) - { - using (IChannel m = c.CreateChannel()) - { - QueueDeclareOk q = m.QueueDeclare(); - var consumer = new AsyncEventingBasicConsumer(m); - m.BasicConsume(q.QueueName, true, consumer); - var countdownEvent = new CountdownEvent(NumberOfThreads); - var tasks = new Task[NumberOfThreads]; - for (int i = 0; i < NumberOfThreads; i++) - { - int threadIndex = i; - tasks[i] = Task.Run(() => - { - countdownEvent.Signal(); - countdownEvent.Wait(); - int start = threadIndex * NumberOfRegistrations; - for (int j = start; j < start + NumberOfRegistrations; j++) - { - int receivedIndex = j; - consumer.Received += (sender, eventArgs) => - { - called[receivedIndex] = 1; - return Task.CompletedTask; - }; - } - }); - } - - countdownEvent.Wait(); - await Task.WhenAll(tasks); - - // Add last receiver - var are = new AutoResetEvent(false); - consumer.Received += (o, a) => - { - are.Set(); - return Task.CompletedTask; - }; - - // Send message - m.BasicPublish("", q.QueueName, ReadOnlyMemory.Empty); - are.WaitOne(TimingFixture.TestTimeout); - } - } - - // Check received messages - Assert.Equal(-1, called.AsSpan().IndexOf((byte)0)); - } - - [Fact] - public void NonAsyncConsumerShouldThrowInvalidOperationException() - { - var cf = new ConnectionFactory { DispatchConsumersAsync = true }; - using (IConnection c = cf.CreateConnection()) - { - using (IChannel m = c.CreateChannel()) - { - QueueDeclareOk q = m.QueueDeclare(); - byte[] body = System.Text.Encoding.UTF8.GetBytes("async-hi"); - m.BasicPublish("", q.QueueName, body); - var consumer = new EventingBasicConsumer(m); - Assert.Throws(() => m.BasicConsume(q.QueueName, false, consumer)); - } - } - } - - private string get_unique_string(int string_length) - { - using (var rng = RandomNumberGenerator.Create()) - { - var bit_count = (string_length * 6); - var byte_count = ((bit_count + 7) / 8); // rounded up - var bytes = new byte[byte_count]; - rng.GetBytes(bytes); - return Convert.ToBase64String(bytes); - } - } - } -} diff --git a/projects/Unit/TestBasicPublish.cs b/projects/Unit/TestBasicPublish.cs deleted file mode 100644 index b08f4df0bd..0000000000 --- a/projects/Unit/TestBasicPublish.cs +++ /dev/null @@ -1,256 +0,0 @@ -// This source code is dual-licensed under the Apache License, version -// 2.0, and the Mozilla Public License, version 2.0. -// -// The APL v2.0: -// -//--------------------------------------------------------------------------- -// Copyright (c) 2007-2020 VMware, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// https://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -//--------------------------------------------------------------------------- -// -// The MPL v2.0: -// -//--------------------------------------------------------------------------- -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. -// -// Copyright (c) 2011-2020 VMware, Inc. or its affiliates. All rights reserved. -//--------------------------------------------------------------------------- - -using System; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using RabbitMQ.Client.Events; -using Xunit; -using Xunit.Sdk; - -namespace RabbitMQ.Client.Unit -{ - [Collection("IntegrationFixture")] - public class TestBasicPublish - { - [Fact] - public void TestBasicRoundtripArray() - { - var cf = new ConnectionFactory(); - using (IConnection c = cf.CreateConnection()) - using (IChannel m = c.CreateChannel()) - { - QueueDeclareOk q = m.QueueDeclare(); - var bp = new BasicProperties(); - byte[] sendBody = System.Text.Encoding.UTF8.GetBytes("hi"); - byte[] consumeBody = null; - var consumer = new EventingBasicConsumer(m); - var are = new AutoResetEvent(false); - consumer.Received += async (o, a) => - { - consumeBody = a.Body.ToArray(); - are.Set(); - await Task.Yield(); - }; - string tag = m.BasicConsume(q.QueueName, true, consumer); - - m.BasicPublish("", q.QueueName, bp, sendBody); - bool waitResFalse = are.WaitOne(5000); - m.BasicCancel(tag); - - Assert.True(waitResFalse); - Assert.Equal(sendBody, consumeBody); - } - } - - [Fact] - public void TestBasicRoundtripCachedString() - { - var cf = new ConnectionFactory(); - using (IConnection c = cf.CreateConnection()) - using (IChannel m = c.CreateChannel()) - { - CachedString exchangeName = new CachedString(string.Empty); - CachedString queueName = new CachedString(m.QueueDeclare().QueueName); - byte[] sendBody = System.Text.Encoding.UTF8.GetBytes("hi"); - byte[] consumeBody = null; - var consumer = new EventingBasicConsumer(m); - var are = new AutoResetEvent(false); - consumer.Received += async (o, a) => - { - consumeBody = a.Body.ToArray(); - are.Set(); - await Task.Yield(); - }; - string tag = m.BasicConsume(queueName.Value, true, consumer); - - m.BasicPublish(exchangeName, queueName, sendBody); - bool waitResFalse = are.WaitOne(2000); - m.BasicCancel(tag); - - Assert.True(waitResFalse); - Assert.Equal(sendBody, consumeBody); - } - } - - [Fact] - public void TestBasicRoundtripReadOnlyMemory() - { - var cf = new ConnectionFactory(); - using (IConnection c = cf.CreateConnection()) - using (IChannel m = c.CreateChannel()) - { - QueueDeclareOk q = m.QueueDeclare(); - byte[] sendBody = System.Text.Encoding.UTF8.GetBytes("hi"); - byte[] consumeBody = null; - var consumer = new EventingBasicConsumer(m); - var are = new AutoResetEvent(false); - consumer.Received += async (o, a) => - { - consumeBody = a.Body.ToArray(); - are.Set(); - await Task.Yield(); - }; - string tag = m.BasicConsume(q.QueueName, true, consumer); - - m.BasicPublish("", q.QueueName, new ReadOnlyMemory(sendBody)); - bool waitResFalse = are.WaitOne(2000); - m.BasicCancel(tag); - - Assert.True(waitResFalse); - Assert.Equal(sendBody, consumeBody); - } - } - - [Fact] - public void CanNotModifyPayloadAfterPublish() - { - var cf = new ConnectionFactory(); - using (IConnection c = cf.CreateConnection()) - using (IChannel m = c.CreateChannel()) - { - QueueDeclareOk q = m.QueueDeclare(); - byte[] sendBody = new byte[1000]; - var consumer = new EventingBasicConsumer(m); - var receivedMessage = new AutoResetEvent(false); - bool modified = true; - consumer.Received += (o, a) => - { - if (a.Body.Span.IndexOf((byte)1) < 0) - { - modified = false; - } - receivedMessage.Set(); - }; - string tag = m.BasicConsume(q.QueueName, true, consumer); - - m.BasicPublish("", q.QueueName, sendBody); - sendBody.AsSpan().Fill(1); - - Assert.True(receivedMessage.WaitOne(5000)); - Assert.False(modified, "Payload was modified after the return of BasicPublish"); - - m.BasicCancel(tag); - } - } - - [Fact] - public void TestMaxMessageSize() - { - var re = new ManualResetEventSlim(); - const ushort maxMsgSize = 1024; - - int count = 0; - byte[] msg0 = Encoding.UTF8.GetBytes("hi"); - - var r = new System.Random(); - byte[] msg1 = new byte[maxMsgSize * 2]; - r.NextBytes(msg1); - - var cf = new ConnectionFactory(); - cf.AutomaticRecoveryEnabled = false; - cf.TopologyRecoveryEnabled = false; - cf.MaxMessageSize = maxMsgSize; - - bool sawConnectionShutdown = false; - bool sawChannelShutdown = false; - bool sawConsumerRegistered = false; - bool sawConsumerCancelled = false; - - using (IConnection c = cf.CreateConnection()) - { - c.ConnectionShutdown += (o, a) => - { - sawConnectionShutdown = true; - }; - - Assert.Equal(maxMsgSize, cf.MaxMessageSize); - Assert.Equal(maxMsgSize, cf.Endpoint.MaxMessageSize); - Assert.Equal(maxMsgSize, c.Endpoint.MaxMessageSize); - - using (IChannel m = c.CreateChannel()) - { - m.ChannelShutdown += (o, a) => - { - sawChannelShutdown = true; - }; - - m.CallbackException += (o, a) => - { - throw new XunitException("Unexpected m.CallbackException"); - }; - - QueueDeclareOk q = m.QueueDeclare(); - - var consumer = new EventingBasicConsumer(m); - - consumer.Shutdown += (o, a) => - { - re.Set(); - }; - - consumer.Registered += (o, a) => - { - sawConsumerRegistered = true; - }; - - consumer.Unregistered += (o, a) => - { - throw new XunitException("Unexpected consumer.Unregistered"); - }; - - consumer.ConsumerCancelled += (o, a) => - { - sawConsumerCancelled = true; - }; - - consumer.Received += (o, a) => - { - Interlocked.Increment(ref count); - }; - - string tag = m.BasicConsume(q.QueueName, true, consumer); - - m.BasicPublish("", q.QueueName, msg0); - m.BasicPublish("", q.QueueName, msg1); - Assert.True(re.Wait(TimeSpan.FromSeconds(5))); - - Assert.Equal(1, count); - Assert.True(sawConnectionShutdown); - Assert.True(sawChannelShutdown); - Assert.True(sawConsumerRegistered); - Assert.True(sawConsumerCancelled); - } - } - } - } -} diff --git a/projects/Unit/TestConcurrentAccessWithSharedConnection.cs b/projects/Unit/TestConcurrentAccessWithSharedConnection.cs deleted file mode 100644 index c6750fd7f3..0000000000 --- a/projects/Unit/TestConcurrentAccessWithSharedConnection.cs +++ /dev/null @@ -1,223 +0,0 @@ -// This source code is dual-licensed under the Apache License, version -// 2.0, and the Mozilla Public License, version 2.0. -// -// The APL v2.0: -// -//--------------------------------------------------------------------------- -// Copyright (c) 2007-2020 VMware, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// https://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -//--------------------------------------------------------------------------- -// -// The MPL v2.0: -// -//--------------------------------------------------------------------------- -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. -// -// Copyright (c) 2007-2020 VMware, Inc. All rights reserved. -//--------------------------------------------------------------------------- - -using System; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Xunit; -using Xunit.Abstractions; - -namespace RabbitMQ.Client.Unit -{ - public class TestConcurrentAccessWithSharedConnection : IntegrationFixture - { - internal const int Threads = 32; - internal CountdownEvent _latch; - internal TimeSpan _completionTimeout = TimeSpan.FromSeconds(90); - - public TestConcurrentAccessWithSharedConnection(ITestOutputHelper output) : base(output) - { - } - - protected override void SetUp() - { - base.SetUp(); - ThreadPool.SetMinThreads(Threads, Threads); - _latch = new CountdownEvent(Threads); - } - - public override void Dispose() - { - base.Dispose(); - _latch.Dispose(); - } - - [Fact] - public void TestConcurrentChannelOpenAndPublishingWithBlankMessages() - { - TestConcurrentChannelOpenAndPublishingWithBody(Array.Empty(), 30); - } - - [Fact] - public void TestConcurrentChannelOpenAndPublishingSize64() - { - TestConcurrentChannelOpenAndPublishingWithBodyOfSize(64); - } - - [Fact] - public void TestConcurrentChannelOpenAndPublishingSize256() - { - TestConcurrentChannelOpenAndPublishingWithBodyOfSize(256); - } - - [Fact] - public void TestConcurrentChannelOpenAndPublishingSize1024() - { - TestConcurrentChannelOpenAndPublishingWithBodyOfSize(1024); - } - - [Fact] - public Task TestConcurrentChannelOpenAndPublishingWithBlankMessagesAsync() - { - return TestConcurrentChannelOpenAndPublishingWithBodyAsync(Array.Empty(), 30); - } - - [Fact] - public Task TestConcurrentChannelOpenAndPublishingSize64Async() - { - return TestConcurrentChannelOpenAndPublishingWithBodyOfSizeAsync(64); - } - - [Fact] - public Task TestConcurrentChannelOpenAndPublishingSize256Async() - { - return TestConcurrentChannelOpenAndPublishingWithBodyOfSizeAsync(256); - } - - [Fact] - public Task TestConcurrentChannelOpenAndPublishingSize1024Async() - { - return TestConcurrentChannelOpenAndPublishingWithBodyOfSizeAsync(1024); - } - - [Fact] - public void TestConcurrentChannelOpenCloseLoop() - { - TestConcurrentChannelOperations((conn) => - { - IChannel ch = conn.CreateChannel(); - ch.Close(); - }, 50); - } - - internal void TestConcurrentChannelOpenAndPublishingWithBodyOfSize(int length, int iterations = 30) - { - TestConcurrentChannelOpenAndPublishingWithBody(new byte[length], iterations); - } - - internal Task TestConcurrentChannelOpenAndPublishingWithBodyOfSizeAsync(int length, int iterations = 30) - { - return TestConcurrentChannelOpenAndPublishingWithBodyAsync(new byte[length], iterations); - } - - internal void TestConcurrentChannelOpenAndPublishingWithBody(byte[] body, int iterations) - { - TestConcurrentChannelOperations((conn) => - { - // publishing on a shared channel is not supported - // and would missing the point of this test anyway - IChannel ch = _conn.CreateChannel(); - ch.ConfirmSelect(); - for (int j = 0; j < 200; j++) - { - ch.BasicPublish("", "_______", body); - } - using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(40)); - ch.WaitForConfirmsAsync(cts.Token).GetAwaiter().GetResult(); - }, iterations); - } - - internal Task TestConcurrentChannelOpenAndPublishingWithBodyAsync(byte[] body, int iterations) - { - return TestConcurrentChannelOperationsAsync(async (conn) => - { - // publishing on a shared channel is not supported - // and would missing the point of this test anyway - IChannel ch = _conn.CreateChannel(); - ch.ConfirmSelect(); - for (int j = 0; j < 200; j++) - { - await ch.BasicPublishAsync("", "_______", body).ConfigureAwait(false); - } - using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(40)); - await ch.WaitForConfirmsAsync(cts.Token).ConfigureAwait(false); - }, iterations); - } - - internal void TestConcurrentChannelOperations(Action actions, - int iterations) - { - TestConcurrentChannelOperations(actions, iterations, _completionTimeout); - } - - internal Task TestConcurrentChannelOperationsAsync(Func actions, int iterations) - { - return TestConcurrentChannelOperationsAsync(actions, iterations, _completionTimeout); - } - - internal void TestConcurrentChannelOperations(Action actions, - int iterations, TimeSpan timeout) - { - _ = Enumerable.Range(0, Threads).Select(x => - { - return Task.Run(() => - { - for (int j = 0; j < iterations; j++) - { - actions(_conn); - } - - _latch.Signal(); - }); - }).ToArray(); - - Assert.True(_latch.Wait(timeout)); - // incorrect frame interleaving in these tests will result - // in an unrecoverable connection-level exception, thus - // closing the connection - Assert.True(_conn.IsOpen); - } - - internal async Task TestConcurrentChannelOperationsAsync(Func actions, - int iterations, TimeSpan timeout) - { - await Task.WhenAll(Enumerable.Range(0, Threads).Select(x => - { - return Task.Run(() => - { - for (int j = 0; j < iterations; j++) - { - actions(_conn); - } - - _latch.Signal(); - }); - }).ToArray()); - - Assert.True(_latch.Wait(timeout)); - // incorrect frame interleaving in these tests will result - // in an unrecoverable connection-level exception, thus - // closing the connection - Assert.True(_conn.IsOpen); - } - } -} diff --git a/projects/Unit/TestConsumer.cs b/projects/Unit/TestConsumer.cs deleted file mode 100644 index 96b9d321ae..0000000000 --- a/projects/Unit/TestConsumer.cs +++ /dev/null @@ -1,96 +0,0 @@ -// This source code is dual-licensed under the Apache License, version -// 2.0, and the Mozilla Public License, version 2.0. -// -// The APL v2.0: -// -//--------------------------------------------------------------------------- -// Copyright (c) 2007-2020 VMware, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// https://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -//--------------------------------------------------------------------------- -// -// The MPL v2.0: -// -//--------------------------------------------------------------------------- -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. -// -// Copyright (c) 2007-2020 VMware, Inc. All rights reserved. -//--------------------------------------------------------------------------- - -using System; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using RabbitMQ.Client.Events; -using Xunit; - -namespace RabbitMQ.Client.Unit -{ - [Collection("IntegrationFixture")] - public class TestConsumer - { - [Fact] - public async Task TestBasicRoundtripConcurrent() - { - var cf = new ConnectionFactory { ConsumerDispatchConcurrency = 2 }; - using (IConnection c = cf.CreateConnection()) - using (IChannel m = c.CreateChannel()) - { - QueueDeclareOk q = m.QueueDeclare(); - const string publish1 = "sync-hi-1"; - byte[] body = Encoding.UTF8.GetBytes(publish1); - m.BasicPublish("", q.QueueName, body); - const string publish2 = "sync-hi-2"; - body = Encoding.UTF8.GetBytes(publish2); - m.BasicPublish("", q.QueueName, body); - - var consumer = new EventingBasicConsumer(m); - - var publish1SyncSource = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - var publish2SyncSource = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - var maximumWaitTime = TimeSpan.FromSeconds(10); - var tokenSource = new CancellationTokenSource(maximumWaitTime); - tokenSource.Token.Register(() => - { - publish1SyncSource.TrySetResult(false); - publish2SyncSource.TrySetResult(false); - }); - - consumer.Received += (o, a) => - { - switch (Encoding.UTF8.GetString(a.Body.ToArray())) - { - case publish1: - publish1SyncSource.TrySetResult(true); - break; - case publish2: - publish2SyncSource.TrySetResult(true); - break; - } - }; - - m.BasicConsume(q.QueueName, true, consumer); - - await Task.WhenAll(publish1SyncSource.Task, publish2SyncSource.Task); - - bool result1 = await publish1SyncSource.Task; - Assert.True(result1, $"Non concurrent dispatch lead to deadlock after {maximumWaitTime}"); - - bool result2 = await publish1SyncSource.Task; - Assert.True(result2, $"Non concurrent dispatch lead to deadlock after {maximumWaitTime}"); - } - } - } -} diff --git a/projects/Unit/TestFloodPublishing.cs b/projects/Unit/TestFloodPublishing.cs deleted file mode 100644 index e541e5399c..0000000000 --- a/projects/Unit/TestFloodPublishing.cs +++ /dev/null @@ -1,166 +0,0 @@ -// This source code is dual-licensed under the Apache License, version -// 2.0, and the Mozilla Public License, version 2.0. -// -// The APL v2.0: -// -//--------------------------------------------------------------------------- -// Copyright (c) 2007-2020 VMware, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// https://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -//--------------------------------------------------------------------------- -// -// The MPL v2.0: -// -//--------------------------------------------------------------------------- -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. -// -// Copyright (c) 2007-2020 VMware, Inc. All rights reserved. -//--------------------------------------------------------------------------- - -using System; -using System.Diagnostics; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using RabbitMQ.Client.Events; -using Xunit; -using Xunit.Abstractions; - -namespace RabbitMQ.Client.Unit -{ - [Collection("IntegrationFixture")] - public class TestFloodPublishing - { - private readonly ITestOutputHelper _output; - private readonly byte[] _body = new byte[2048]; - private readonly TimeSpan _tenSeconds = TimeSpan.FromSeconds(10); - - public TestFloodPublishing(ITestOutputHelper output) - { - _output = output; - } - - [Fact] - public async Task TestUnthrottledFloodPublishingAsync() - { - var connFactory = new ConnectionFactory() - { - RequestedHeartbeat = TimeSpan.FromSeconds(60), - AutomaticRecoveryEnabled = false - }; - - var closeWatch = new Stopwatch(); - using (IConnection conn = connFactory.CreateConnection()) - { - using (IChannel channel = conn.CreateChannel()) - { - conn.ConnectionShutdown += (_, args) => - { - if (args.Initiator != ShutdownInitiator.Application) - { - Assert.Fail("Unexpected connection shutdown!"); - } - }; - - var stopwatch = Stopwatch.StartNew(); - int i = 0; - try - { - for (i = 0; i < 65535 * 64; i++) - { - if (i % 65536 == 0) - { - if (stopwatch.Elapsed > _tenSeconds) - { - break; - } - } - - await channel.BasicPublishAsync(CachedString.Empty, CachedString.Empty, _body); - } - } - finally - { - stopwatch.Stop(); - _output.WriteLine($"sent {i}, done in {stopwatch.Elapsed.TotalMilliseconds} ms"); - } - - Assert.True(conn.IsOpen); - closeWatch.Start(); - } - } - closeWatch.Stop(); - _output.WriteLine($"Closing took {closeWatch.Elapsed.TotalMilliseconds} ms"); - } - - [Fact] - public async Task TestMultithreadFloodPublishingAsync() - { - string message = "Hello from test TestMultithreadFloodPublishing"; - byte[] sendBody = Encoding.UTF8.GetBytes(message); - int publishCount = 4096; - int receivedCount = 0; - AutoResetEvent autoResetEvent = new AutoResetEvent(false); - - var cf = new ConnectionFactory() - { - RequestedHeartbeat = TimeSpan.FromSeconds(60), - AutomaticRecoveryEnabled = false - }; - - using (IConnection c = cf.CreateConnection()) - { - string queueName = null; - using (IChannel m = c.CreateChannel()) - { - QueueDeclareOk q = m.QueueDeclare(); - queueName = q.QueueName; - } - - Task pub = Task.Run(async () => - { - using (IChannel pubCh = c.CreateChannel()) - { - for (int i = 0; i < publishCount; i++) - { - await pubCh.BasicPublishAsync(string.Empty, queueName, sendBody); - } - } - }); - - using (IChannel consumeCh = c.CreateChannel()) - { - var consumer = new EventingBasicConsumer(consumeCh); - consumer.Received += (o, a) => - { - string receivedMessage = Encoding.UTF8.GetString(a.Body.ToArray()); - Assert.Equal(message, receivedMessage); - Interlocked.Increment(ref receivedCount); - if (receivedCount == publishCount) - { - autoResetEvent.Set(); - } - }; - consumeCh.BasicConsume(queueName, true, consumer); - - Assert.True(autoResetEvent.WaitOne(_tenSeconds)); - } - - await pub; - Assert.Equal(publishCount, receivedCount); - } - } - } -} diff --git a/projects/Unit/TestRecoverAfterCancel.cs b/projects/Unit/TestRecoverAfterCancel.cs deleted file mode 100644 index 9342a4a7bd..0000000000 --- a/projects/Unit/TestRecoverAfterCancel.cs +++ /dev/null @@ -1,109 +0,0 @@ -// This source code is dual-licensed under the Apache License, version -// 2.0, and the Mozilla Public License, version 2.0. -// -// The APL v2.0: -// -//--------------------------------------------------------------------------- -// Copyright (c) 2007-2020 VMware, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// https://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -//--------------------------------------------------------------------------- -// -// The MPL v2.0: -// -//--------------------------------------------------------------------------- -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. -// -// Copyright (c) 2007-2020 VMware, Inc. All rights reserved. -//--------------------------------------------------------------------------- - -using System; -using System.Collections.Concurrent; -using System.Text; -using RabbitMQ.Client.Events; -using RabbitMQ.Client.Impl; -using Xunit; - -#pragma warning disable 0618 - -namespace RabbitMQ.Client.Unit -{ - [Collection("IntegrationFixture")] - public class TestRecoverAfterCancel : IDisposable - { - IConnection _connection; - IChannel _channel; - string _queue; - int _callbackCount; - - public TestRecoverAfterCancel() - { - _connection = new ConnectionFactory().CreateConnection(); - _channel = _connection.CreateChannel(); - _queue = _channel.QueueDeclare("", false, true, false, null); - } - - public int ChannelNumber(IChannel channel) - { - return ((ChannelBase)channel).Session.ChannelNumber; - } - - public void Dispose() - { - _connection.Abort(); - } - - [Fact] - public void TestRecoverAfterCancel_() - { - UTF8Encoding enc = new UTF8Encoding(); - _channel.BasicPublish("", _queue, enc.GetBytes("message")); - EventingBasicConsumer Consumer = new EventingBasicConsumer(_channel); - BlockingCollection<(bool Redelivered, byte[] Body)> EventQueue = new BlockingCollection<(bool Redelivered, byte[] Body)>(); - // Making sure we copy the delivery body since it could be disposed at any time. - Consumer.Received += (_, e) => EventQueue.Add((e.Redelivered, e.Body.ToArray())); - - string CTag = _channel.BasicConsume(_queue, false, Consumer); - (bool Redelivered, byte[] Body) Event = EventQueue.Take(); - _channel.BasicCancel(CTag); - _channel.BasicRecover(true); - - EventingBasicConsumer Consumer2 = new EventingBasicConsumer(_channel); - BlockingCollection<(bool Redelivered, byte[] Body)> EventQueue2 = new BlockingCollection<(bool Redelivered, byte[] Body)>(); - // Making sure we copy the delivery body since it could be disposed at any time. - Consumer2.Received += (_, e) => EventQueue2.Add((e.Redelivered, e.Body.ToArray())); - _channel.BasicConsume(_queue, false, Consumer2); - (bool Redelivered, byte[] Body) Event2 = EventQueue2.Take(); - - Assert.Equal(Event.Body, Event2.Body); - Assert.False(Event.Redelivered); - Assert.True(Event2.Redelivered); - } - - [Fact] - public void TestRecoverCallback() - { - _callbackCount = 0; - _channel.BasicRecoverOk += IncrCallback; - _channel.BasicRecover(true); - Assert.Equal(1, _callbackCount); - } - - void IncrCallback(object sender, EventArgs args) - { - _callbackCount++; - } - } -}