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/Makefile b/Makefile new file mode 100644 index 0000000000..88b8227407 --- /dev/null +++ b/Makefile @@ -0,0 +1,18 @@ +# vim: noexpandtab:ts=4:sw=4 +.PHONY: build test test-all + +RABBITMQ_DOCKER_NAME ?= rabbitmq-dotnet-client-rabbitmq + +build: + dotnet build $(CURDIR)/Build.csproj + +test: + dotnet test $(CURDIR)/projects/Test/Unit/Unit.csproj --logger 'console;verbosity=detailed' + dotnet test --environment "RABBITMQ_RABBITMQCTL_PATH=DOCKER:$$(docker inspect --format='{{.Id}}' $(RABBITMQ_DOCKER_NAME))" $(CURDIR)/projects/Test/AsyncIntegration/AsyncIntegration.csproj --logger 'console;verbosity=detailed' + dotnet test --environment "RABBITMQ_RABBITMQCTL_PATH=DOCKER:$$(docker inspect --format='{{.Id}}' $(RABBITMQ_DOCKER_NAME))" $(CURDIR)/projects/Test/Integration/Integration.csproj --logger 'console;verbosity=detailed' + dotnet test --environment "RABBITMQ_RABBITMQCTL_PATH=DOCKER:$$(docker inspect --format='{{.Id}}' $(RABBITMQ_DOCKER_NAME))" $(CURDIR)/projects/Test/SequentialIntegration/SequentialIntegration.csproj --logger 'console;verbosity=detailed' + +# Note: +# You must have the expected OAuth2 environment set up for this target +test-all: + dotnet test --environment "RABBITMQ_RABBITMQCTL_PATH=DOCKER:$$(docker inspect --format='{{.Id}}' $(RABBITMQ_DOCKER_NAME))" $(CURDIR)/Build.csproj --logger 'console;verbosity=detailed' diff --git a/RUNNING_TESTS.md b/RUNNING_TESTS.md index a25a008221..56de830df6 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 @@ -79,7 +79,7 @@ RABBITMQ_RABBITMQCTL_PATH=/path/to/rabbitmqctl dotnet test projects/Unit It is also possible to run a RabbitMQ node in a [Docker](https://www.docker.com/) container. Set the environment variable `RABBITMQ_RABBITMQCTL_PATH` to `DOCKER:` (for example -`DOCKER:rabbitmq01`). This tells the unit tests to run the `rabbitmqctl` +`DOCKER:rabbitmq-dotnet-client-rabbitmq`). This tells the unit tests to run the `rabbitmqctl` commands through Docker, in the format `docker exec rabbitmq01 rabbitmqctl `: @@ -87,6 +87,12 @@ commands through Docker, in the format `docker exec rabbitmq01 rabbitmqctl docker run -d --hostname rabbitmq01 --name rabbitmq01 -p 15672:15672 -p 5672:5672 rabbitmq:3-management ``` +You should also be able to run the same script that sets up the Ubuntu 22 GitHub actions worker: + +```shell +./.ci/ubuntu/gha-setup.sh +``` + ## Running All Tests Then, to run the tests use: @@ -94,14 +100,18 @@ Then, to run the tests use: ### Windows +Note that the `-RunTests` does not run the OAuth2 test suite. + ```powershell build.ps1 -RunTests ``` ### MacOS, Linux, BSD: +Note that the `test` target does not run the OAuth2 test suite. + ```shell -dotnet test ./Build.csproj +make test ``` ## Running Individual Suites or Test Cases @@ -110,9 +120,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..2dbe9b0835 100644 --- a/build.ps1 +++ b/build.ps1 @@ -8,20 +8,30 @@ Write-Host "`tPSScriptRoot: $PSScriptRoot" Write-Host "`tRunTests: $RunTests" Write-Host "`tdotnet --version: $(dotnet --version)" -Write-Host "Building all projects (Build.csproj traversal)..." -ForegroundColor "Magenta" +Write-Host "[INFO] building all projects (Build.csproj traversal)..." -ForegroundColor "Magenta" dotnet build "$PSScriptRoot\Build.csproj" -Write-Host "Done building." -ForegroundColor "Green" +Write-Host "[INFO] 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') - 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) { - Write-Host "Error with tests, aborting build." -Foreground "Red" - Exit 1 + $tests_dir = Join-Path -Path $PSScriptRoot -ChildPath 'projects' | Join-Path -ChildPath 'Test' + $unit_csproj_file = Resolve-Path -LiteralPath (Join-Path -Path $tests_dir -ChildPath 'Unit' | Join-Path -ChildPath 'Unit.csproj') + $integration_csproj_file = Resolve-Path -LiteralPath (Join-Path -Path $tests_dir -ChildPath 'Integration' | Join-Path -ChildPath 'Integration.csproj') + $async_integration_csproj_file = Resolve-Path -LiteralPath (Join-Path -Path $tests_dir -ChildPath 'AsyncIntegration' | Join-Path -ChildPath 'AsyncIntegration.csproj') + $sequential_integration_csproj_file = Resolve-Path -LiteralPath (Join-Path -Path $tests_dir -ChildPath 'SequentialIntegration' | Join-Path -ChildPath 'SequentialIntegration.csproj') + + foreach ($csproj_file in $unit_csproj_file, $integration_csproj_file, $async_integration_csproj_file, $sequential_integration_csproj_file) + { + Write-Host "[INFO] running Unit / Integration tests from '$csproj_file' (all frameworks)" -ForegroundColor "Magenta" + dotnet test $csproj_file --no-restore --no-build --logger "console;verbosity=detailed" + if ($LASTEXITCODE -ne 0) + { + Write-Host "[ERROR] tests errored, exiting" -Foreground "Red" + Exit 1 + } + else + { + Write-Host "[INFO] tests passed" -ForegroundColor "Green" + } } - Write-Host "Tests passed!" -ForegroundColor "Green" } - -Write-Host "Done." diff --git a/projects/Benchmarks/ConsumerDispatching/AsyncBasicConsumerFake.cs b/projects/Benchmarks/ConsumerDispatching/AsyncBasicConsumerFake.cs index 573db2a277..d9cf669f42 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, ReadOnlyMemory body) { if (Interlocked.Increment(ref _current) == Count) { diff --git a/projects/Benchmarks/ConsumerDispatching/ConsumerDispatcher.cs b/projects/Benchmarks/ConsumerDispatching/ConsumerDispatcher.cs index b751091c4d..d5918dc06d 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; @@ -7,7 +8,7 @@ namespace RabbitMQ.Benchmarks { [Config(typeof(Config))] [BenchmarkCategory("ConsumerDispatcher")] - public class ConsumerDispatcherBase + internal class ConsumerDispatcherBase { protected static readonly ManualResetEventSlim _autoResetEvent = new ManualResetEventSlim(false); @@ -18,10 +19,18 @@ 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 + internal class BasicDeliverConsumerDispatching : ConsumerDispatcherBase { [Params(1, 30)] public int Count { get; set; } @@ -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..b6fe369179 100644 --- a/projects/RabbitMQ.Client/RabbitMQ.Client.csproj +++ b/projects/RabbitMQ.Client/RabbitMQ.Client.csproj @@ -38,16 +38,28 @@ - + - + + + <_Parameter1>Common, PublicKey=00240000048000009400000006020000002400005253413100040000010001008d20ec856aeeb8c3153a77faa2d80e6e43b5db93224a20cc7ae384f65f142e89730e2ff0fcc5d578bbe96fa98a7196c77329efdee4579b3814c0789e5a39b51df6edd75b602a33ceabdfcf19a3feb832f31d8254168cd7ba5700dfbca301fbf8db614ba41ba18474de0a5f4c2d51c995bc3636c641c8cbe76f45717bfcb943b5 + <_Parameter1>Unit, PublicKey=00240000048000009400000006020000002400005253413100040000010001008d20ec856aeeb8c3153a77faa2d80e6e43b5db93224a20cc7ae384f65f142e89730e2ff0fcc5d578bbe96fa98a7196c77329efdee4579b3814c0789e5a39b51df6edd75b602a33ceabdfcf19a3feb832f31d8254168cd7ba5700dfbca301fbf8db614ba41ba18474de0a5f4c2d51c995bc3636c641c8cbe76f45717bfcb943b5 + + <_Parameter1>AsyncIntegration, PublicKey=00240000048000009400000006020000002400005253413100040000010001008d20ec856aeeb8c3153a77faa2d80e6e43b5db93224a20cc7ae384f65f142e89730e2ff0fcc5d578bbe96fa98a7196c77329efdee4579b3814c0789e5a39b51df6edd75b602a33ceabdfcf19a3feb832f31d8254168cd7ba5700dfbca301fbf8db614ba41ba18474de0a5f4c2d51c995bc3636c641c8cbe76f45717bfcb943b5 + + + <_Parameter1>Integration, PublicKey=00240000048000009400000006020000002400005253413100040000010001008d20ec856aeeb8c3153a77faa2d80e6e43b5db93224a20cc7ae384f65f142e89730e2ff0fcc5d578bbe96fa98a7196c77329efdee4579b3814c0789e5a39b51df6edd75b602a33ceabdfcf19a3feb832f31d8254168cd7ba5700dfbca301fbf8db614ba41ba18474de0a5f4c2d51c995bc3636c641c8cbe76f45717bfcb943b5 + + + <_Parameter1>SequentialIntegration, PublicKey=00240000048000009400000006020000002400005253413100040000010001008d20ec856aeeb8c3153a77faa2d80e6e43b5db93224a20cc7ae384f65f142e89730e2ff0fcc5d578bbe96fa98a7196c77329efdee4579b3814c0789e5a39b51df6edd75b602a33ceabdfcf19a3feb832f31d8254168cd7ba5700dfbca301fbf8db614ba41ba18474de0a5f4c2d51c995bc3636c641c8cbe76f45717bfcb943b5 + <_Parameter1>Benchmarks, PublicKey=00240000048000009400000006020000002400005253413100040000010001008d20ec856aeeb8c3153a77faa2d80e6e43b5db93224a20cc7ae384f65f142e89730e2ff0fcc5d578bbe96fa98a7196c77329efdee4579b3814c0789e5a39b51df6edd75b602a33ceabdfcf19a3feb832f31d8254168cd7ba5700dfbca301fbf8db614ba41ba18474de0a5f4c2d51c995bc3636c641c8cbe76f45717bfcb943b5 @@ -58,8 +70,8 @@ - - + + diff --git a/projects/RabbitMQ.Client/client/ClientArrayPool.cs b/projects/RabbitMQ.Client/client/ClientArrayPool.cs new file mode 100644 index 0000000000..c682879915 --- /dev/null +++ b/projects/RabbitMQ.Client/client/ClientArrayPool.cs @@ -0,0 +1,114 @@ +// 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 (bool.TryParse( + Environment.GetEnvironmentVariable("RABBITMQ_CLIENT_TRACK_ARRAY_POOL_CHECKOUTS"), + out s_trackCheckouts)) + { + 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..b4679e3b6d --- /dev/null +++ b/projects/RabbitMQ.Client/client/RentedMemory.cs @@ -0,0 +1,88 @@ +// 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 +{ + internal struct RentedMemory : IDisposable + { + private bool _disposedValue; + + internal RentedMemory(byte[] rentedArray) + : this(new ReadOnlyMemory(rentedArray), rentedArray) + { + } + + internal RentedMemory(ReadOnlyMemory memory, byte[] rentedArray) + { + Memory = memory; + RentedArray = rentedArray; + } + + internal readonly ReadOnlyMemory Memory; + + internal readonly byte[] ToArray() + { + return Memory.ToArray(); + } + + internal readonly int Size => Memory.Length; + + internal readonly ReadOnlySpan Span => Memory.Span; + + internal readonly byte[] RentedArray; + + internal readonly ReadOnlyMemory CopyToMemory() + { + return new ReadOnlyMemory(Memory.ToArray()); + } + + 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/TaskExtensions.cs b/projects/RabbitMQ.Client/client/TaskExtensions.cs new file mode 100644 index 0000000000..cd1daf9e8e --- /dev/null +++ b/projects/RabbitMQ.Client/client/TaskExtensions.cs @@ -0,0 +1,118 @@ +// 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; + +namespace RabbitMQ.Client +{ + internal static class TaskExtensions + { +#if !NET6_0_OR_GREATER + private static readonly TaskContinuationOptions s_tco = TaskContinuationOptions.OnlyOnFaulted | TaskContinuationOptions.ExecuteSynchronously; + private static void continuation(Task t, object s) => t.Exception.Handle(e => true); +#endif + + public static Task TimeoutAfter(this Task task, TimeSpan timeout) + { +#if NET6_0_OR_GREATER + if (task.IsCompletedSuccessfully) + { + return task; + } + + return task.WaitAsync(timeout); +#else + if (task.Status == TaskStatus.RanToCompletion) + { + return task; + } + + return DoTimeoutAfter(task, timeout); + + static async Task DoTimeoutAfter(Task task, TimeSpan timeout) + { + if (task == await Task.WhenAny(task, Task.Delay(timeout)).ConfigureAwait(false)) + { + await task.ConfigureAwait(false); + } + else + { + Task supressErrorTask = task.ContinueWith( + continuationAction: continuation, + state: null, + cancellationToken: CancellationToken.None, + continuationOptions: s_tco, + scheduler: TaskScheduler.Default); + throw new TimeoutException(); + } + } +#endif + } + + public static async ValueTask TimeoutAfter(this ValueTask task, TimeSpan timeout) + { + if (task.IsCompletedSuccessfully) + { + return; + } + +#if NET6_0_OR_GREATER + Task actualTask = task.AsTask(); + await actualTask.WaitAsync(timeout) + .ConfigureAwait(false); +#else + await DoTimeoutAfter(task, timeout) + .ConfigureAwait(false); + + async static ValueTask DoTimeoutAfter(ValueTask task, TimeSpan timeout) + { + Task actualTask = task.AsTask(); + if (actualTask == await Task.WhenAny(actualTask, Task.Delay(timeout)).ConfigureAwait(false)) + { + await actualTask.ConfigureAwait(false); + } + else + { + Task supressErrorTask = actualTask.ContinueWith( + continuationAction: continuation, + state: null, + cancellationToken: CancellationToken.None, + continuationOptions: s_tco, + scheduler: TaskScheduler.Default); + throw new TimeoutException(); + } + } +#endif + } + } +} diff --git a/projects/RabbitMQ.Client/client/api/AmqpTimestamp.cs b/projects/RabbitMQ.Client/client/api/AmqpTimestamp.cs index 2465fd167b..e540f837ab 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. /// @@ -65,7 +62,7 @@ public AmqpTimestamp(long unixTime) : this() /// /// Unix time in seconds. /// - public long UnixTime { get; } + public readonly long UnixTime; public bool Equals(AmqpTimestamp other) => UnixTime == other.UnixTime; diff --git a/projects/RabbitMQ.Client/client/api/AsyncDefaultBasicConsumer.cs b/projects/RabbitMQ.Client/client/api/AsyncDefaultBasicConsumer.cs index a0c0af161a..a7a0fe4035 100644 --- a/projects/RabbitMQ.Client/client/api/AsyncDefaultBasicConsumer.cs +++ b/projects/RabbitMQ.Client/client/api/AsyncDefaultBasicConsumer.cs @@ -141,7 +141,8 @@ public virtual async Task OnCancel(params string[] consumerTags) IsRunning = false; if (!_consumerCancelledWrapper.IsEmpty) { - await _consumerCancelledWrapper.InvokeAsync(this, new ConsumerEventArgs(consumerTags)).ConfigureAwait(false); + await _consumerCancelledWrapper.InvokeAsync(this, new ConsumerEventArgs(consumerTags)) + .ConfigureAwait(false); } foreach (string consumerTag in consumerTags) { @@ -165,7 +166,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, ReadOnlyMemory 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..64a23a5f1f 100644 --- a/projects/RabbitMQ.Client/client/api/BasicGetResult.cs +++ b/projects/RabbitMQ.Client/client/api/BasicGetResult.cs @@ -30,7 +30,6 @@ //--------------------------------------------------------------------------- using System; -using System.Buffers; namespace RabbitMQ.Client { @@ -38,10 +37,8 @@ namespace RabbitMQ.Client /// /// 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. /// @@ -64,49 +61,25 @@ public sealed class BasicGetResult : IDisposable Body = body; } - /// - /// 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. /// - public ReadOnlyBasicProperties BasicProperties { get; } + public readonly ReadOnlyBasicProperties BasicProperties; /// /// Retrieves the body of this message. /// - public ReadOnlyMemory Body { get; } + public readonly ReadOnlyMemory Body; /// /// Retrieve the delivery tag for this message. See also . /// - public ulong DeliveryTag { get; } + public readonly ulong DeliveryTag; /// /// Retrieve the exchange this message was published to. /// - public string Exchange { get; } + public readonly string Exchange; /// /// Retrieve the number of messages pending on the queue, excluding the message being delivered. @@ -115,25 +88,16 @@ public sealed class BasicGetResult : IDisposable /// Note that this figure is indicative, not reliable, and can /// change arbitrarily as messages are added to the queue and removed by other clients. /// - public uint MessageCount { get; } + public readonly uint MessageCount; /// /// Retrieve the redelivered flag for this message. /// - public bool Redelivered { get; } + public readonly bool Redelivered; /// /// Retrieve the routing key with which this message was published. /// - public string RoutingKey { get; } - - /// - public void Dispose() - { - if (_rentedArray != null) - { - ArrayPool.Shared.Return(_rentedArray); - } - } + public readonly string RoutingKey; } } diff --git a/projects/RabbitMQ.Client/client/api/ConnectionConfig.cs b/projects/RabbitMQ.Client/client/api/ConnectionConfig.cs index 4ff107df22..6d7817e13e 100644 --- a/projects/RabbitMQ.Client/client/api/ConnectionConfig.cs +++ b/projects/RabbitMQ.Client/client/api/ConnectionConfig.cs @@ -45,17 +45,17 @@ public sealed class ConnectionConfig /// /// Virtual host to access during this connection. /// - public string VirtualHost { get; } + public readonly string VirtualHost; /// /// Username to use when authenticating to the server. /// - public string UserName { get; } + public readonly string UserName; /// /// Password to use when authenticating to the server. /// - public string Password { get; } + public readonly string Password; /// /// Default CredentialsProvider implementation. If set, this @@ -67,86 +67,86 @@ public sealed class ConnectionConfig /// /// SASL auth mechanisms to use. /// - public IList AuthMechanisms { get; } + public readonly IEnumerable AuthMechanisms; /// /// Dictionary of client properties to be sent to the server. /// - public IDictionary ClientProperties { get; } + public readonly IDictionary ClientProperties; /// /// Default client provided name to be used for connections. /// - public string? ClientProvidedName { get; } + public readonly string? ClientProvidedName; /// /// Maximum channel number to ask for. /// - public ushort MaxChannelCount { get; } + public readonly ushort MaxChannelCount; /// /// Frame-max parameter to ask for (in bytes). /// - public uint MaxFrameSize { get; } + public readonly uint MaxFrameSize; /// /// Set to false to make automatic connection recovery not recover topology (exchanges, queues, bindings, etc). /// - public bool TopologyRecoveryEnabled { get; } + public readonly bool TopologyRecoveryEnabled; /// /// Filter to include/exclude entities from topology recovery. /// Default filter includes all entities in topology recovery. /// - public TopologyRecoveryFilter TopologyRecoveryFilter { get; } + public readonly TopologyRecoveryFilter TopologyRecoveryFilter; /// /// Custom logic for handling topology recovery exceptions that match the specified filters. /// - public TopologyRecoveryExceptionHandler TopologyRecoveryExceptionHandler { get; } + public readonly TopologyRecoveryExceptionHandler TopologyRecoveryExceptionHandler; /// /// Amount of time client will wait for before re-trying to recover connection. /// - public TimeSpan NetworkRecoveryInterval { get; } + public readonly TimeSpan NetworkRecoveryInterval; /// /// Heartbeat timeout to use when negotiating with the server. /// - public TimeSpan HeartbeatInterval { get; } + public readonly TimeSpan HeartbeatInterval; /// /// Amount of time protocol operations (e.g. queue.declare) are allowed to take before timing out. /// - public TimeSpan ContinuationTimeout { get; } + public readonly TimeSpan ContinuationTimeout; /// /// Amount of time protocol handshake operations are allowed to take before timing out. /// + public readonly TimeSpan HandshakeContinuationTimeout; - public TimeSpan HandshakeContinuationTimeout { get; } /// /// Timeout setting for connection attempts. /// - public TimeSpan RequestedConnectionTimeout { get; } + public readonly TimeSpan RequestedConnectionTimeout; /// /// Set to true will enable an asynchronous consumer dispatcher which is compatible with . /// - public bool DispatchConsumersAsync { get; } + public readonly bool DispatchConsumersAsync; /// /// Set to a value greater than one to enable concurrent processing. For a concurrency greater than one /// will be offloaded to the worker thread pool so it is important to choose the value for the concurrency wisely to avoid thread pool overloading. /// can handle concurrency much more efficiently due to the non-blocking nature of the consumer. /// - public int DispatchConsumerConcurrency { get; } + public readonly int DispatchConsumerConcurrency; - internal Func FrameHandlerFactory { get; } + internal readonly Func FrameHandlerFactory; internal ConnectionConfig(string virtualHost, string userName, string password, ICredentialsProvider credentialsProvider, ICredentialsRefresher credentialsRefresher, - IList authMechanisms, + IEnumerable authMechanisms, IDictionary clientProperties, string? clientProvidedName, ushort maxChannelCount, uint maxFrameSize, bool topologyRecoveryEnabled, TopologyRecoveryFilter topologyRecoveryFilter, TopologyRecoveryExceptionHandler topologyRecoveryExceptionHandler, diff --git a/projects/RabbitMQ.Client/client/api/ConnectionFactory.cs b/projects/RabbitMQ.Client/client/api/ConnectionFactory.cs index 82d7ff6d24..769176203d 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; @@ -149,12 +150,12 @@ public sealed class ConnectionFactory : ConnectionFactoryBase, IConnectionFactor /// /// Default SASL auth mechanisms to use. /// - public static readonly IList DefaultAuthMechanisms = new List(1) { new PlainMechanismFactory() }; + public static readonly IEnumerable DefaultAuthMechanisms = new[] { new PlainMechanismFactory() }; /// /// SASL auth mechanisms to use. /// - public IList AuthMechanisms { get; set; } = DefaultAuthMechanisms; + public IEnumerable AuthMechanisms { get; set; } = DefaultAuthMechanisms; /// /// Address family used by default. @@ -377,17 +378,21 @@ public Uri Uri /// Given a list of mechanism names supported by the server, select a preferred mechanism, /// or null if we have none in common. /// - public IAuthMechanismFactory AuthMechanismFactory(IList mechanismNames) + public IAuthMechanismFactory AuthMechanismFactory(IEnumerable argServerMechanismNames) { + string[] serverMechanismNames = argServerMechanismNames.ToArray(); + // Our list is in order of preference, the server one is not. - for (int index = 0; index < AuthMechanisms.Count; index++) + IAuthMechanismFactory[] authMechanisms = AuthMechanisms.ToArray(); + + for (int index = 0; index < authMechanisms.Length; index++) { - IAuthMechanismFactory factory = AuthMechanisms[index]; + IAuthMechanismFactory factory = authMechanisms[index]; string factoryName = factory.Name; - for (int i = 0; i < mechanismNames.Count; i++) + for (int i = 0; i < serverMechanismNames.Length; i++) { - if (string.Equals(mechanismNames[i], factoryName, StringComparison.OrdinalIgnoreCase)) + if (string.Equals(serverMechanismNames[i], factoryName, StringComparison.OrdinalIgnoreCase)) { return factory; } @@ -410,6 +415,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 +447,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 @@ -443,11 +480,30 @@ public IConnection CreateConnection(string clientProvidedName) /// /// When no hostname was reachable. /// - public IConnection CreateConnection(IList hostnames) + public IConnection CreateConnection(IEnumerable 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(IEnumerable 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 @@ -468,12 +524,38 @@ public IConnection CreateConnection(IList hostnames) /// /// When no hostname was reachable. /// - public IConnection CreateConnection(IList hostnames, string clientProvidedName) + public IConnection CreateConnection(IEnumerable hostnames, string clientProvidedName) { IEnumerable endpoints = hostnames.Select(h => new AmqpTcpEndpoint(h, Port, Ssl, MaxMessageSize)); 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(IEnumerable 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. @@ -487,11 +569,29 @@ public IConnection CreateConnection(IList hostnames, string clientProvid /// /// When no hostname was reachable. /// - public IConnection CreateConnection(IList endpoints) + public IConnection CreateConnection(IEnumerable 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(IEnumerable 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. @@ -511,11 +611,35 @@ public IConnection CreateConnection(IList endpoints) /// /// When no hostname was reachable. /// - public IConnection CreateConnection(IList endpoints, string clientProvidedName) + public IConnection CreateConnection(IEnumerable endpoints, string clientProvidedName) { 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(IEnumerable endpoints, string clientProvidedName) + { + return CreateConnectionAsync(EndpointResolverFactory(endpoints), clientProvidedName); + } + /// /// Create a connection using an IEndpointResolver. /// @@ -539,10 +663,54 @@ 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 async ValueTask CreateConnectionAsync(IEndpointResolver endpointResolver, string clientProvidedName) + { + ConnectionConfig config = CreateConfig(clientProvidedName); + try + { + if (AutomaticRecoveryEnabled) + { + var c = new AutorecoveringConnection(config, endpointResolver); + return await c.OpenAsync() + .ConfigureAwait(false); + } + else + { + var c = new Connection(config, endpointResolver.SelectOne(CreateFrameHandler)); + return await c.OpenAsync() + .ConfigureAwait(false); + } } catch (Exception e) { diff --git a/projects/RabbitMQ.Client/client/api/IChannel.cs b/projects/RabbitMQ.Client/client/api/IChannel.cs index 3f7f4693e2..9d3248d22a 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,27 @@ ValueTask BasicPublishAsync(CachedString exchange, CachedString rou void Close(ushort replyCode, string replyText, bool abort); /// - /// Enable publisher acknowledgements. + /// Asynchronously 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. + /// Whether or not the close is an abort (ignoring certain exceptions). + ValueTask CloseAsync(ushort replyCode, string replyText, bool abort); + + /// + /// 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 +336,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 +359,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 +392,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 +410,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 +431,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 +474,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 +516,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..ab5c9cac10 100644 --- a/projects/RabbitMQ.Client/client/api/IChannelExtensions.cs +++ b/projects/RabbitMQ.Client/client/api/IChannelExtensions.cs @@ -51,10 +51,33 @@ public static class IChannelExtensions return channel.BasicConsume(queue, autoAck, consumerTag, noLocal, exclusive, arguments, consumer); } + /// Asynchronously start a Basic content-class consumer. + public static ValueTask BasicConsumeAsync(this IChannel channel, + IBasicConsumer consumer, + string queue, + bool autoAck = false, + string consumerTag = "", + bool noLocal = false, + bool exclusive = false, + IDictionary arguments = null) + { + return channel.BasicConsumeAsync(queue, autoAck, consumerTag, noLocal, exclusive, arguments, consumer); + } + /// Start a Basic content-class consumer. - public static string BasicConsume(this IChannel channel, string queue, bool autoAck, IBasicConsumer consumer) + public static string BasicConsume(this IChannel channel, string queue, + bool autoAck, + IBasicConsumer consumer) + { + return channel.BasicConsume(queue, autoAck, string.Empty, false, false, null, consumer); + } + + /// Asynchronously start a Basic content-class consumer. + public static ValueTask BasicConsumeAsync(this IChannel channel, string queue, + bool autoAck, + IBasicConsumer consumer) { - return channel.BasicConsume(queue, autoAck, "", false, false, null, consumer); + return channel.BasicConsumeAsync(queue, autoAck, string.Empty, false, false, null, consumer); } /// Start a Basic content-class consumer. @@ -66,6 +89,15 @@ public static string BasicConsume(this IChannel channel, string queue, bool auto return channel.BasicConsume(queue, autoAck, consumerTag, false, false, null, consumer); } + /// Asynchronously start a Basic content-class consumer. + public static ValueTask BasicConsumeAsync(this IChannel channel, string queue, + bool autoAck, + string consumerTag, + IBasicConsumer consumer) + { + return channel.BasicConsumeAsync(queue, autoAck, consumerTag, false, false, null, consumer); + } + /// Start a Basic content-class consumer. public static string BasicConsume(this IChannel channel, string queue, bool autoAck, @@ -76,6 +108,16 @@ public static string BasicConsume(this IChannel channel, string queue, bool auto return channel.BasicConsume(queue, autoAck, consumerTag, false, false, arguments, consumer); } + /// Asynchronously start a Basic content-class consumer. + public static ValueTask BasicConsumeAsync(this IChannel channel, string queue, + bool autoAck, + string consumerTag, + IDictionary arguments, + IBasicConsumer consumer) + { + return channel.BasicConsumeAsync(queue, autoAck, consumerTag, false, false, arguments, consumer); + } + #nullable enable /// /// (Extension method) Convenience overload of BasicPublish. @@ -89,21 +131,27 @@ public static void BasicPublish(this IChannel channel, PublicationAddress add channel.BasicPublish(addr.ExchangeName, addr.RoutingKey, in basicProperties, body); } - public static void BasicPublish(this IChannel channel, string exchange, string routingKey, ReadOnlyMemory body = default, bool mandatory = false) - => channel.BasicPublish(exchange, routingKey, in EmptyBasicProperty.Empty, body, mandatory); + public static ValueTask BasicPublishAsync(this IChannel channel, PublicationAddress addr, in T basicProperties, ReadOnlyMemory body) + where T : IReadOnlyBasicProperties, IAmqpHeader + { + return channel.BasicPublishAsync(addr.ExchangeName, addr.RoutingKey, in basicProperties, body); + } - public static void BasicPublish(this IChannel channel, CachedString exchange, CachedString routingKey, ReadOnlyMemory body = default, bool mandatory = false) + public static void BasicPublish(this IChannel channel, string exchange, string routingKey, ReadOnlyMemory body = default, bool mandatory = false) => channel.BasicPublish(exchange, routingKey, in EmptyBasicProperty.Empty, body, mandatory); public static ValueTask BasicPublishAsync(this IChannel channel, string exchange, string routingKey, ReadOnlyMemory body = default, bool mandatory = false) => channel.BasicPublishAsync(exchange, routingKey, in EmptyBasicProperty.Empty, body, mandatory); + public static void BasicPublish(this IChannel channel, CachedString exchange, CachedString routingKey, ReadOnlyMemory body = default, bool mandatory = false) + => channel.BasicPublish(exchange, routingKey, in EmptyBasicProperty.Empty, body, mandatory); + public static ValueTask BasicPublishAsync(this IChannel channel, CachedString exchange, CachedString routingKey, ReadOnlyMemory body = default, bool mandatory = false) => channel.BasicPublishAsync(exchange, routingKey, in EmptyBasicProperty.Empty, body, mandatory); #nullable disable /// - /// (Spec method) Declare a queue. + /// Declare a queue. /// public static QueueDeclareOk QueueDeclare(this IChannel channel, string queue = "", bool durable = false, bool exclusive = true, bool autoDelete = true, IDictionary arguments = null) @@ -112,7 +160,17 @@ public static ValueTask BasicPublishAsync(this IChannel channel, CachedString ex } /// - /// (Extension method) Bind an exchange to an exchange. + /// Asynchronously declare a queue. + /// + public static ValueTask QueueDeclareAsync(this IChannel channel, string queue = "", bool durable = false, bool exclusive = true, + bool autoDelete = true, IDictionary arguments = null) + { + return channel.QueueDeclareAsync(queue: queue, passive: false, + durable: durable, exclusive: exclusive, autoDelete: autoDelete, arguments: arguments); + } + + /// + /// Bind an exchange to an exchange. /// public static void ExchangeBind(this IChannel channel, string destination, string source, string routingKey, IDictionary arguments = null) { @@ -120,7 +178,15 @@ public static void ExchangeBind(this IChannel channel, string destination, strin } /// - /// (Extension method) Like exchange bind but sets nowait to true. + /// Asynchronously bind an exchange to an exchange. + /// + public static ValueTask ExchangeBindAsync(this IChannel channel, string destination, string source, string routingKey, IDictionary arguments = null) + { + return channel.ExchangeBindAsync(destination, source, routingKey, arguments); + } + + /// + /// Like exchange bind but sets nowait to true. /// public static void ExchangeBindNoWait(this IChannel channel, string destination, string source, string routingKey, IDictionary arguments = null) { @@ -128,7 +194,7 @@ public static void ExchangeBindNoWait(this IChannel channel, string destination, } /// - /// (Spec method) Declare an exchange. + /// Declare an exchange. /// public static void ExchangeDeclare(this IChannel channel, string exchange, string type, bool durable = false, bool autoDelete = false, IDictionary arguments = null) @@ -137,7 +203,16 @@ public static void ExchangeBindNoWait(this IChannel channel, string destination, } /// - /// (Extension method) Like ExchangeDeclare but sets nowait to true. + /// Asynchronously declare an exchange. + /// + public static ValueTask ExchangeDeclareAsync(this IChannel channel, string exchange, string type, bool durable = false, bool autoDelete = false, + IDictionary arguments = null) + { + return channel.ExchangeDeclareAsync(exchange, type, durable, autoDelete, arguments); + } + + /// + /// Like ExchangeDeclare but sets nowait to true. /// public static void ExchangeDeclareNoWait(this IChannel channel, string exchange, string type, bool durable = false, bool autoDelete = false, IDictionary arguments = null) @@ -146,7 +221,7 @@ public static void ExchangeBindNoWait(this IChannel channel, string destination, } /// - /// (Spec method) Unbinds an exchange. + /// Unbinds an exchange. /// public static void ExchangeUnbind(this IChannel channel, string destination, string source, @@ -157,7 +232,18 @@ public static void ExchangeBindNoWait(this IChannel channel, string destination, } /// - /// (Spec method) Deletes an exchange. + /// Asynchronously unbinds an exchange. + /// + public static ValueTask ExchangeUnbindAsync(this IChannel channel, string destination, + string source, + string routingKey, + IDictionary arguments = null) + { + return channel.ExchangeUnbindAsync(destination, source, routingKey, arguments); + } + + /// + /// Deletes an exchange. /// public static void ExchangeDelete(this IChannel channel, string exchange, bool ifUnused = false) { @@ -165,7 +251,15 @@ public static void ExchangeDelete(this IChannel channel, string exchange, bool i } /// - /// (Extension method) Like ExchangeDelete but sets nowait to true. + /// Asynchronously deletes an exchange. + /// + public static ValueTask ExchangeDeleteAsync(this IChannel channel, string exchange, bool ifUnused = false) + { + return channel.ExchangeDeleteAsync(exchange, ifUnused); + } + + /// + /// Like ExchangeDelete but sets nowait to true. /// public static void ExchangeDeleteNoWait(this IChannel channel, string exchange, bool ifUnused = false) { @@ -173,7 +267,7 @@ public static void ExchangeDeleteNoWait(this IChannel channel, string exchange, } /// - /// (Spec method) Binds a queue. + /// Binds a queue. /// public static void QueueBind(this IChannel channel, string queue, string exchange, string routingKey, IDictionary arguments = null) { @@ -181,7 +275,15 @@ public static void QueueBind(this IChannel channel, string queue, string exchang } /// - /// (Spec method) Deletes a queue. + /// Asynchronously binds a queue. + /// + public static ValueTask QueueBindAsync(this IChannel channel, string queue, string exchange, string routingKey, IDictionary arguments = null) + { + return channel.QueueBindAsync(queue, exchange, routingKey, arguments); + } + + /// + /// Deletes a queue. /// public static uint QueueDelete(this IChannel channel, string queue, bool ifUnused = false, bool ifEmpty = false) { @@ -189,7 +291,15 @@ public static uint QueueDelete(this IChannel channel, string queue, bool ifUnuse } /// - /// (Extension method) Like QueueDelete but sets nowait to true. + /// Asynchronously deletes a queue. + /// + public static ValueTask QueueDeleteAsync(this IChannel channel, string queue, bool ifUnused = false, bool ifEmpty = false) + { + return channel.QueueDeleteAsync(queue, ifUnused, ifEmpty); + } + + /// + /// Like QueueDelete but sets nowait to true. /// public static void QueueDeleteNoWait(this IChannel channel, string queue, bool ifUnused = false, bool ifEmpty = false) { @@ -197,13 +307,21 @@ public static void QueueDeleteNoWait(this IChannel channel, string queue, bool i } /// - /// (Spec method) Unbinds a queue. + /// Unbinds a queue. /// public static void QueueUnbind(this IChannel channel, string queue, string exchange, string routingKey, IDictionary arguments = null) { channel.QueueUnbind(queue, exchange, routingKey, arguments); } + /// + /// Asynchronously unbinds a queue. + /// + public static ValueTask QueueUnbindAsync(this IChannel channel, string queue, string exchange, string routingKey, IDictionary arguments = null) + { + return channel.QueueUnbindAsync(queue, exchange, routingKey, arguments); + } + /// /// Abort this session. /// @@ -220,6 +338,22 @@ public static void Abort(this IChannel channel) channel.Close(Constants.ReplySuccess, "Goodbye", true); } + /// + /// Asynchronously abort 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. + /// In comparison to normal method, will not throw + /// or or any other during closing channel. + /// + public static ValueTask AbortAsync(this IChannel channel) + { + return channel.CloseAsync(Constants.ReplySuccess, "Goodbye", true); + } + /// /// Abort this session. /// @@ -250,15 +384,31 @@ 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) + { + return channel.CloseAsync(Constants.ReplySuccess, "Goodbye", 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 /// /// @@ -266,5 +416,26 @@ public static void Close(this IChannel channel, ushort replyCode, string replyTe { channel.Close(replyCode, replyText, false); } + + /// + /// Asynchronously 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 + /// + /// + public static ValueTask CloseAsync(this IChannel channel, ushort replyCode, string replyText) + { + return channel.CloseAsync(replyCode, replyText, false); + } } } diff --git a/projects/RabbitMQ.Client/client/api/IConnection.cs b/projects/RabbitMQ.Client/client/api/IConnection.cs index 745efe79e5..4078e005af 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; @@ -122,7 +123,7 @@ public interface IConnection : INetworkConnection, IDisposable /// Returns the list of objects that contain information /// about any errors reported while closing the connection in the order they appeared /// - IList ShutdownReport { get; } + IEnumerable ShutdownReport { get; } /// /// Application-specific connection name, will be displayed in the management UI @@ -190,7 +191,7 @@ public interface IConnection : INetworkConnection, IDisposable /// /// This event will never fire for connections that disable automatic recovery. /// - event EventHandler QueueNameChangeAfterRecovery; + event EventHandler QueueNameChangedAfterRecovery; /// /// Raised when a consumer is about to be recovered. This event raises when topology recovery @@ -225,9 +226,24 @@ public interface IConnection : INetworkConnection, IDisposable /// Whether or not this close is an abort (ignores certain exceptions). void Close(ushort reasonCode, string reasonText, TimeSpan timeout, bool abort); + /// + /// Asynchronously close this connection and all its channels + /// and wait with a timeout for all the in-progress close operations to complete. + /// + /// The close code (See under "Reply Codes" in the AMQP 0-9-1 specification). + /// A message indicating the reason for closing the connection. + /// Operation timeout. + /// Whether or not this close is an abort (ignores certain exceptions). + ValueTask CloseAsync(ushort reasonCode, string reasonText, TimeSpan timeout, bool abort); + /// /// 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/IConnectionExtensions.cs b/projects/RabbitMQ.Client/client/api/IConnectionExtensions.cs index 0457caa184..fecb606b78 100644 --- a/projects/RabbitMQ.Client/client/api/IConnectionExtensions.cs +++ b/projects/RabbitMQ.Client/client/api/IConnectionExtensions.cs @@ -1,6 +1,7 @@ using System; using System.IO; using System.Threading; +using System.Threading.Tasks; namespace RabbitMQ.Client { @@ -22,6 +23,22 @@ public static void Close(this IConnection connection) connection.Close(Constants.ReplySuccess, "Goodbye", InternalConstants.DefaultConnectionCloseTimeout, false); } + /// + /// Asynchronously close this connection and all its channels. + /// + /// + /// Note that all active channels and sessions will be + /// closed if this method is called. It will wait for the in-progress + /// close operation to complete. This method will not return to the caller + /// until the shutdown is complete. If the connection is already closed + /// (or closing), then this method will do nothing. + /// It can also throw when socket was closed unexpectedly. + /// + public static ValueTask CloseAsync(this IConnection connection) + { + return connection.CloseAsync(Constants.ReplySuccess, "Goodbye", InternalConstants.DefaultConnectionCloseTimeout, false); + } + /// /// Close this connection and all its channels. /// @@ -40,6 +57,24 @@ public static void Close(this IConnection connection, ushort reasonCode, string connection.Close(reasonCode, reasonText, InternalConstants.DefaultConnectionCloseTimeout, false); } + /// + /// Asynchronously close this connection and all its channels. + /// + /// + /// The method behaves in the same way as , with the only + /// difference that the connection is closed with the given connection close code and message. + /// + /// The close code (See under "Reply Codes" in the AMQP specification). + /// + /// + /// A message indicating the reason for closing the connection. + /// + /// + public static ValueTask CloseAsync(this IConnection connection, ushort reasonCode, string reasonText) + { + return connection.CloseAsync(reasonCode, reasonText, InternalConstants.DefaultConnectionCloseTimeout, false); + } + /// /// Close this connection and all its channels /// and wait with a timeout for all the in-progress close operations to complete. @@ -60,6 +95,26 @@ public static void Close(this IConnection connection, TimeSpan timeout) connection.Close(Constants.ReplySuccess, "Goodbye", timeout, false); } + /// + /// Asynchronously close this connection and all its channels + /// and wait with a timeout for all the in-progress close operations to complete. + /// + /// + /// Note that all active channels and sessions will be + /// closed if this method is called. It will wait for the in-progress + /// close operation to complete with a timeout. If the connection is + /// already closed (or closing), then this method will do nothing. + /// It can also throw when socket was closed unexpectedly. + /// If timeout is reached and the close operations haven't finished, then socket is forced to close. + /// + /// To wait infinitely for the close operations to complete use . + /// + /// + public static ValueTask CloseAsync(this IConnection connection, TimeSpan timeout) + { + return connection.CloseAsync(Constants.ReplySuccess, "Goodbye", timeout, false); + } + /// /// Close this connection and all its channels /// and wait with a timeout for all the in-progress close operations to complete. @@ -82,6 +137,28 @@ public static void Close(this IConnection connection, ushort reasonCode, string connection.Close(reasonCode, reasonText, timeout, false); } + /// + /// Asynchronously close this connection and all its channels + /// and wait with a timeout for all the in-progress close operations to complete. + /// + /// + /// The method behaves in the same way as , with the only + /// difference that the connection is closed with the given connection close code and message. + /// + /// The close code (See under "Reply Codes" in the AMQP 0-9-1 specification). + /// + /// + /// A message indicating the reason for closing the connection. + /// + /// + /// Operation timeout. + /// + /// + public static ValueTask CloseAsync(this IConnection connection, ushort reasonCode, string reasonText, TimeSpan timeout) + { + return connection.CloseAsync(reasonCode, reasonText, timeout, false); + } + /// /// Abort this connection and all its channels. /// @@ -96,6 +173,20 @@ public static void Abort(this IConnection connection) connection.Close(Constants.ReplySuccess, "Connection close forced", InternalConstants.DefaultConnectionAbortTimeout, true); } + /// + /// Asynchronously abort this connection and all its channels. + /// + /// + /// Note that all active channels and sessions will be closed if this method is called. + /// In comparison to normal method, will not throw + /// during closing connection. + ///This method waits infinitely for the in-progress close operation to complete. + /// + public static ValueTask AbortAsync(this IConnection connection) + { + return connection.CloseAsync(Constants.ReplySuccess, "Connection close forced", InternalConstants.DefaultConnectionAbortTimeout, true); + } + /// /// Abort this connection and all its channels. /// @@ -114,6 +205,24 @@ public static void Abort(this IConnection connection, ushort reasonCode, string connection.Close(reasonCode, reasonText, InternalConstants.DefaultConnectionAbortTimeout, true); } + /// + /// Asynchronously abort this connection and all its channels. + /// + /// + /// The method behaves in the same way as , with the only + /// difference that the connection is closed with the given connection close code and message. + /// + /// The close code (See under "Reply Codes" in the AMQP 0-9-1 specification) + /// + /// + /// A message indicating the reason for closing the connection + /// + /// + public static ValueTask AbortAsync(this IConnection connection, ushort reasonCode, string reasonText) + { + return connection.CloseAsync(reasonCode, reasonText, InternalConstants.DefaultConnectionAbortTimeout, true); + } + /// /// Abort this connection and all its channels and wait with a /// timeout for all the in-progress close operations to complete. @@ -132,6 +241,24 @@ public static void Abort(this IConnection connection, TimeSpan timeout) connection.Close(Constants.ReplySuccess, "Connection close forced", timeout, true); } + /// + /// Asynchronously abort this connection and all its channels and wait with a + /// timeout for all the in-progress close operations to complete. + /// + /// + /// This method, behaves in a similar way as method with the + /// only difference that it explicitly specifies a timeout given + /// for all the in-progress close operations to complete. + /// If timeout is reached and the close operations haven't finished, then socket is forced to close. + /// + /// To wait infinitely for the close operations to complete use . + /// + /// + public static ValueTask AbortAsync(this IConnection connection, TimeSpan timeout) + { + return connection.CloseAsync(Constants.ReplySuccess, "Connection close forced", timeout, true); + } + /// /// Abort this connection and all its channels and wait with a /// timeout for all the in-progress close operations to complete. @@ -150,5 +277,24 @@ public static void Abort(this IConnection connection, ushort reasonCode, string { connection.Close(reasonCode, reasonText, timeout, true); } + + /// + /// Asynchronously abort this connection and all its channels and wait with a + /// timeout for all the in-progress close operations to complete. + /// + /// + /// The method behaves in the same way as , with the only + /// difference that the connection is closed with the given connection close code and message. + /// + /// The close code (See under "Reply Codes" in the AMQP 0-9-1 specification). + /// + /// + /// A message indicating the reason for closing the connection. + /// + /// + public static ValueTask AbortAsync(this IConnection connection, ushort reasonCode, string reasonText, TimeSpan timeout) + { + return connection.CloseAsync(reasonCode, reasonText, timeout, true); + } } } diff --git a/projects/RabbitMQ.Client/client/api/IConnectionFactory.cs b/projects/RabbitMQ.Client/client/api/IConnectionFactory.cs index b1480cf887..56c21d29db 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 @@ -95,13 +95,18 @@ public interface IConnectionFactory /// Given a list of mechanism names supported by the server, select a preferred mechanism, /// or null if we have none in common. /// - IAuthMechanismFactory AuthMechanismFactory(IList mechanismNames); + IAuthMechanismFactory AuthMechanismFactory(IEnumerable mechanismNames); /// /// Create a connection to the specified endpoint. /// IConnection CreateConnection(); + /// + /// Asynchronously create a connection to the specified endpoint. + /// + ValueTask CreateConnectionAsync(); + /// /// Create a connection to the specified endpoint. /// @@ -114,12 +119,31 @@ 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. /// /// List of host names to use /// Open connection - IConnection CreateConnection(IList hostnames); + IConnection CreateConnection(IEnumerable hostnames); + + /// + /// Asynchronously connects to the first reachable hostname from the list. + /// + /// List of host names to use + /// Open connection + ValueTask CreateConnectionAsync(IEnumerable hostnames); /// /// Connects to the first reachable hostname from the list. @@ -132,7 +156,20 @@ public interface IConnectionFactory /// This value is supposed to be human-readable. /// /// Open connection - IConnection CreateConnection(IList hostnames, string clientProvidedName); + IConnection CreateConnection(IEnumerable 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(IEnumerable hostnames, string clientProvidedName); /// /// Create a connection using a list of endpoints. @@ -146,7 +183,21 @@ public interface IConnectionFactory /// /// When no hostname was reachable. /// - IConnection CreateConnection(IList endpoints); + IConnection CreateConnection(IEnumerable 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(IEnumerable endpoints); /// /// Create a connection using a list of endpoints. @@ -166,7 +217,27 @@ public interface IConnectionFactory /// /// When no hostname was reachable. /// - IConnection CreateConnection(IList endpoints, string clientProvidedName); + IConnection CreateConnection(IEnumerable 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(IEnumerable endpoints, string clientProvidedName); /// /// Amount of time protocol handshake operations are allowed to take before diff --git a/projects/RabbitMQ.Client/client/api/Protocols.cs b/projects/RabbitMQ.Client/client/api/Protocols.cs index 74b3115ec4..507ea25127 100644 --- a/projects/RabbitMQ.Client/client/api/Protocols.cs +++ b/projects/RabbitMQ.Client/client/api/Protocols.cs @@ -39,11 +39,11 @@ public static class Protocols /// /// Protocol version 0-9-1 as modified by Pivotal. /// - public static IProtocol AMQP_0_9_1 { get; } = new Framing.Protocol(); + public readonly static IProtocol AMQP_0_9_1 = new Framing.Protocol(); /// /// Retrieve the current default protocol variant (currently AMQP_0_9_1). /// - public static IProtocol DefaultProtocol => AMQP_0_9_1; + public readonly static IProtocol DefaultProtocol = AMQP_0_9_1; } } diff --git a/projects/RabbitMQ.Client/client/api/PublicationAddress.cs b/projects/RabbitMQ.Client/client/api/PublicationAddress.cs index f35863b551..6af9dce657 100644 --- a/projects/RabbitMQ.Client/client/api/PublicationAddress.cs +++ b/projects/RabbitMQ.Client/client/api/PublicationAddress.cs @@ -79,17 +79,17 @@ public PublicationAddress(string exchangeType, string exchangeName, string routi /// /// Retrieve the exchange name. /// - public string ExchangeName { get; } + public readonly string ExchangeName; /// /// Retrieve the exchange type string. /// - public string ExchangeType { get; } + public readonly string ExchangeType; /// ///Retrieve the routing key. /// - public string RoutingKey { get; } + public readonly string RoutingKey; /// /// Parse a out of the given string, diff --git a/projects/RabbitMQ.Client/client/api/QueueDeclareOk.cs b/projects/RabbitMQ.Client/client/api/QueueDeclareOk.cs index dd20ebfa33..cfe2f6fa5e 100644 --- a/projects/RabbitMQ.Client/client/api/QueueDeclareOk.cs +++ b/projects/RabbitMQ.Client/client/api/QueueDeclareOk.cs @@ -52,17 +52,17 @@ public QueueDeclareOk(string queueName, uint messageCount, uint consumerCount) /// /// Consumer count. /// - public uint ConsumerCount { get; } + public readonly uint ConsumerCount; /// /// Message count. /// - public uint MessageCount { get; } + public readonly uint MessageCount; /// /// Queue name. /// - public string QueueName { get; } + public readonly string QueueName; public static implicit operator string(QueueDeclareOk declareOk) { diff --git a/projects/RabbitMQ.Client/client/api/TcpClientAdapter.cs b/projects/RabbitMQ.Client/client/api/TcpClientAdapter.cs index faf27dd520..0d0b3797c8 100644 --- a/projects/RabbitMQ.Client/client/api/TcpClientAdapter.cs +++ b/projects/RabbitMQ.Client/client/api/TcpClientAdapter.cs @@ -22,14 +22,16 @@ public TcpClientAdapter(Socket socket) public virtual async Task ConnectAsync(string host, int port) { AssertSocket(); - IPAddress[] adds = await Dns.GetHostAddressesAsync(host).ConfigureAwait(false); + IPAddress[] adds = await Dns.GetHostAddressesAsync(host) + .ConfigureAwait(false); IPAddress ep = GetMatchingHost(adds, _sock.AddressFamily); if (ep == default(IPAddress)) { throw new ArgumentException($"No ip address could be resolved for {host}"); } - await ConnectAsync(ep, port).ConfigureAwait(false); + await ConnectAsync(ep, port) + .ConfigureAwait(false); } public virtual Task ConnectAsync(IPAddress ep, int port) diff --git a/projects/RabbitMQ.Client/client/events/AsyncEventHandler.cs b/projects/RabbitMQ.Client/client/events/AsyncEventHandler.cs index 65786dfdf1..f172922eda 100644 --- a/projects/RabbitMQ.Client/client/events/AsyncEventHandler.cs +++ b/projects/RabbitMQ.Client/client/events/AsyncEventHandler.cs @@ -1,3 +1,34 @@ +// 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; namespace RabbitMQ.Client.Events diff --git a/projects/RabbitMQ.Client/client/events/AsyncEventingBasicConsumer.cs b/projects/RabbitMQ.Client/client/events/AsyncEventingBasicConsumer.cs index 2dc046badc..7264f7e507 100644 --- a/projects/RabbitMQ.Client/client/events/AsyncEventingBasicConsumer.cs +++ b/projects/RabbitMQ.Client/client/events/AsyncEventingBasicConsumer.cs @@ -53,25 +53,30 @@ public AsyncEventingBasicConsumer(IChannel channel) : base(channel) ///Fires when the server confirms successful consumer cancellation. public override async Task HandleBasicCancelOk(string consumerTag) { - await base.HandleBasicCancelOk(consumerTag).ConfigureAwait(false); + await base.HandleBasicCancelOk(consumerTag) + .ConfigureAwait(false); if (!_unregisteredWrapper.IsEmpty) { - await _unregisteredWrapper.InvokeAsync(this, new ConsumerEventArgs(new[] { consumerTag })).ConfigureAwait(false); + await _unregisteredWrapper.InvokeAsync(this, new ConsumerEventArgs(new[] { consumerTag })) + .ConfigureAwait(false); } } ///Fires when the server confirms successful consumer registration. public override async Task HandleBasicConsumeOk(string consumerTag) { - await base.HandleBasicConsumeOk(consumerTag).ConfigureAwait(false); + await base.HandleBasicConsumeOk(consumerTag) + .ConfigureAwait(false); if (!_registeredWrapper.IsEmpty) { - await _registeredWrapper.InvokeAsync(this, new ConsumerEventArgs(new[] { consumerTag })).ConfigureAwait(false); + await _registeredWrapper.InvokeAsync(this, new ConsumerEventArgs(new[] { consumerTag })) + .ConfigureAwait(false); } } ///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, ReadOnlyMemory body) { // No need to call base, it's empty. return _receivedWrapper.InvokeAsync(this, new BasicDeliverEventArgs(consumerTag, deliveryTag, redelivered, exchange, routingKey, properties, body)); @@ -80,10 +85,12 @@ public override Task HandleBasicDeliver(string consumerTag, ulong deliveryTag, b ///Fires the Shutdown event. public override async Task HandleChannelShutdown(object channel, ShutdownEventArgs reason) { - await base.HandleChannelShutdown(channel, reason).ConfigureAwait(false); + await base.HandleChannelShutdown(channel, reason) + .ConfigureAwait(false); if (!_shutdownWrapper.IsEmpty) { - await _shutdownWrapper.InvokeAsync(this, reason).ConfigureAwait(false); + await _shutdownWrapper.InvokeAsync(this, reason) + .ConfigureAwait(false); } } } diff --git a/projects/RabbitMQ.Client/client/events/BasicAckEventArgs.cs b/projects/RabbitMQ.Client/client/events/BasicAckEventArgs.cs index 8783e5cc14..b7a7cfe990 100644 --- a/projects/RabbitMQ.Client/client/events/BasicAckEventArgs.cs +++ b/projects/RabbitMQ.Client/client/events/BasicAckEventArgs.cs @@ -37,13 +37,20 @@ namespace RabbitMQ.Client.Events ///from an AMQP broker within the Basic content-class. public class BasicAckEventArgs : EventArgs { + public BasicAckEventArgs(ulong deliveryTag, bool multiple) + : base() + { + DeliveryTag = deliveryTag; + Multiple = multiple; + } + ///The sequence number of the acknowledged message, or ///the closed upper bound of acknowledged messages if multiple ///is true. - public ulong DeliveryTag { get; set; } + public readonly ulong DeliveryTag; ///Whether this acknowledgement applies to one message ///or multiple messages. - public bool Multiple { get; set; } + public readonly bool Multiple; } } diff --git a/projects/RabbitMQ.Client/client/events/BasicDeliverEventArgs.cs b/projects/RabbitMQ.Client/client/events/BasicDeliverEventArgs.cs index fbd497bb0f..9d0585cc24 100644 --- a/projects/RabbitMQ.Client/client/events/BasicDeliverEventArgs.cs +++ b/projects/RabbitMQ.Client/client/events/BasicDeliverEventArgs.cs @@ -37,11 +37,6 @@ namespace RabbitMQ.Client.Events ///from an AMQP broker within the Basic content-class. public class BasicDeliverEventArgs : EventArgs { - ///Default constructor. - public BasicDeliverEventArgs() - { - } - ///Constructor that fills the event's properties from ///its arguments. public BasicDeliverEventArgs(string consumerTag, @@ -50,7 +45,7 @@ public BasicDeliverEventArgs() string exchange, string routingKey, in ReadOnlyBasicProperties properties, - ReadOnlyMemory body) + ReadOnlyMemory body) : base() { ConsumerTag = consumerTag; DeliveryTag = deliveryTag; @@ -62,28 +57,28 @@ public BasicDeliverEventArgs() } ///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 ReadOnlyMemory Body; ///The consumer tag of the consumer that the message ///was delivered to. - public string ConsumerTag { get; set; } + public readonly string ConsumerTag; ///The delivery tag for this delivery. See ///IChannel.BasicAck. - public ulong DeliveryTag { get; set; } + public readonly ulong DeliveryTag; ///The exchange the message was originally published ///to. - public string Exchange { get; set; } + public readonly string Exchange; ///The AMQP "redelivered" flag. - public bool Redelivered { get; set; } + public readonly bool Redelivered; ///The routing key used when the message was ///originally published. - public string RoutingKey { get; set; } + public readonly string RoutingKey; } } diff --git a/projects/RabbitMQ.Client/client/events/BasicNackEventArgs.cs b/projects/RabbitMQ.Client/client/events/BasicNackEventArgs.cs index 4c6b863e91..60f4d9cfc9 100644 --- a/projects/RabbitMQ.Client/client/events/BasicNackEventArgs.cs +++ b/projects/RabbitMQ.Client/client/events/BasicNackEventArgs.cs @@ -37,17 +37,25 @@ namespace RabbitMQ.Client.Events ///from an AMQP broker within the Basic content-class. public class BasicNackEventArgs : EventArgs { + public BasicNackEventArgs(ulong deliveryTag, bool multiple, bool requeue) + : base() + { + DeliveryTag = deliveryTag; + Multiple = multiple; + Requeue = requeue; + } + ///The sequence number of the nack'd message, or the ///closed upper bound of nack'd messages if multiple is ///true. - public ulong DeliveryTag { get; set; } + public readonly ulong DeliveryTag; ///Whether this nack applies to one message or ///multiple messages. - public bool Multiple { get; set; } + public readonly bool Multiple; ///Ignore ///Clients should ignore this field. - public bool Requeue { get; set; } + public readonly bool Requeue; } } diff --git a/projects/RabbitMQ.Client/client/events/BasicReturnEventArgs.cs b/projects/RabbitMQ.Client/client/events/BasicReturnEventArgs.cs index 67220f1244..2a0bea1e13 100644 --- a/projects/RabbitMQ.Client/client/events/BasicReturnEventArgs.cs +++ b/projects/RabbitMQ.Client/client/events/BasicReturnEventArgs.cs @@ -37,26 +37,42 @@ namespace RabbitMQ.Client.Events ///from an AMQP broker within the Basic content-class. public class BasicReturnEventArgs : EventArgs { + public BasicReturnEventArgs( + ushort replyCode, + string replyText, + string exchange, + string routingKey, + ReadOnlyBasicProperties basicProperties, + ReadOnlyMemory body) : base() + { + ReplyCode = replyCode; + ReplyText = replyText; + Exchange = exchange; + RoutingKey = routingKey; + BasicProperties = basicProperties; + Body = body; + } + ///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 ReadOnlyMemory 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; } } diff --git a/projects/RabbitMQ.Client/client/events/CallbackExceptionEventArgs.cs b/projects/RabbitMQ.Client/client/events/CallbackExceptionEventArgs.cs index 61d47b8190..145e4255c5 100644 --- a/projects/RabbitMQ.Client/client/events/CallbackExceptionEventArgs.cs +++ b/projects/RabbitMQ.Client/client/events/CallbackExceptionEventArgs.cs @@ -45,13 +45,12 @@ protected BaseExceptionEventArgs(IDictionary detail, Exception e ///Access helpful information about the context in ///which the wrapped exception was thrown. - public IDictionary Detail { get; } + public readonly IDictionary Detail; ///Access the wrapped exception. - public Exception Exception { get; } + public readonly Exception Exception; } - ///Describes an exception that was thrown during the ///library's invocation of an application-supplied callback ///handler. diff --git a/projects/RabbitMQ.Client/client/events/ConnectionBlockedEventArgs.cs b/projects/RabbitMQ.Client/client/events/ConnectionBlockedEventArgs.cs index ca8ef8aeea..9ea17c6d9f 100644 --- a/projects/RabbitMQ.Client/client/events/ConnectionBlockedEventArgs.cs +++ b/projects/RabbitMQ.Client/client/events/ConnectionBlockedEventArgs.cs @@ -46,6 +46,6 @@ public ConnectionBlockedEventArgs(string reason) /// /// Access the reason why connection is blocked. /// - public string Reason { get; } + public readonly string Reason; } } diff --git a/projects/RabbitMQ.Client/client/events/ConnectionRecoveryErrorEventArgs.cs b/projects/RabbitMQ.Client/client/events/ConnectionRecoveryErrorEventArgs.cs index 2d6284c102..8252036a84 100644 --- a/projects/RabbitMQ.Client/client/events/ConnectionRecoveryErrorEventArgs.cs +++ b/projects/RabbitMQ.Client/client/events/ConnectionRecoveryErrorEventArgs.cs @@ -40,6 +40,6 @@ public ConnectionRecoveryErrorEventArgs(Exception ex) Exception = ex; } - public Exception Exception { get; } + public readonly Exception Exception; } } diff --git a/projects/RabbitMQ.Client/client/events/ConsumerEventArgs.cs b/projects/RabbitMQ.Client/client/events/ConsumerEventArgs.cs index 9669a8b81e..13b354174c 100644 --- a/projects/RabbitMQ.Client/client/events/ConsumerEventArgs.cs +++ b/projects/RabbitMQ.Client/client/events/ConsumerEventArgs.cs @@ -46,6 +46,6 @@ public ConsumerEventArgs(string[] consumerTags) ///Access the consumer-tags of the consumer the event ///relates to. - public string[] ConsumerTags { get; } + public readonly string[] ConsumerTags; } } diff --git a/projects/RabbitMQ.Client/client/events/ConsumerTagChangedAfterRecoveryEventArgs.cs b/projects/RabbitMQ.Client/client/events/ConsumerTagChangedAfterRecoveryEventArgs.cs index 10e88cd6d6..864b89cd14 100644 --- a/projects/RabbitMQ.Client/client/events/ConsumerTagChangedAfterRecoveryEventArgs.cs +++ b/projects/RabbitMQ.Client/client/events/ConsumerTagChangedAfterRecoveryEventArgs.cs @@ -49,11 +49,11 @@ public ConsumerTagChangedAfterRecoveryEventArgs(string tagBefore, string tagAfte /// /// Gets the tag before. /// - public string TagBefore { get; } + public readonly string TagBefore; /// /// Gets the tag after. /// - public string TagAfter { get; } + public readonly string TagAfter; } } diff --git a/projects/RabbitMQ.Client/client/events/EventingBasicConsumer.cs b/projects/RabbitMQ.Client/client/events/EventingBasicConsumer.cs index 681247ea56..4a6bad730b 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, ReadOnlyMemory body) { base.HandleBasicDeliver(consumerTag, deliveryTag, redelivered, exchange, routingKey, properties, body); Received?.Invoke( diff --git a/projects/RabbitMQ.Client/client/events/FlowControlEventArgs.cs b/projects/RabbitMQ.Client/client/events/FlowControlEventArgs.cs index b8c95a218f..2d3ecd156b 100644 --- a/projects/RabbitMQ.Client/client/events/FlowControlEventArgs.cs +++ b/projects/RabbitMQ.Client/client/events/FlowControlEventArgs.cs @@ -46,6 +46,6 @@ public FlowControlEventArgs(bool active) /// /// Access the flow control setting. /// - public bool Active { get; } + public readonly bool Active; } } diff --git a/projects/RabbitMQ.Client/client/events/QueueNameChangedAfterRecoveryEventArgs.cs b/projects/RabbitMQ.Client/client/events/QueueNameChangedAfterRecoveryEventArgs.cs index ead403354f..3edc891368 100644 --- a/projects/RabbitMQ.Client/client/events/QueueNameChangedAfterRecoveryEventArgs.cs +++ b/projects/RabbitMQ.Client/client/events/QueueNameChangedAfterRecoveryEventArgs.cs @@ -49,11 +49,11 @@ public QueueNameChangedAfterRecoveryEventArgs(string nameBefore, string nameAfte /// /// Gets the name before. /// - public string NameBefore { get; } + public readonly string NameBefore; /// /// Gets the name after. /// - public string NameAfter { get; } + public readonly string NameAfter; } } diff --git a/projects/RabbitMQ.Client/client/framing/Channel.cs b/projects/RabbitMQ.Client/client/framing/Channel.cs index a59cceb1e3..9b7246799e 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)); @@ -105,26 +100,6 @@ public override void _Private_ConnectionCloseOk() ChannelSend(new ConnectionCloseOk()); } - public override void _Private_ConnectionOpen(string virtualHost) - { - ChannelSend(new ConnectionOpen(virtualHost)); - } - - public override ValueTask _Private_ConnectionOpenAsync(string virtualHost) - { - return ModelSendAsync(new ConnectionOpen(virtualHost)); - } - - public override void _Private_ConnectionSecureOk(byte[] response) - { - ChannelSend(new ConnectionSecureOk(response)); - } - - public override void _Private_ConnectionStartOk(IDictionary clientProperties, string mechanism, byte[] response, string locale) - { - ChannelSend(new ConnectionStartOk(clientProperties, mechanism, response, locale)); - } - public override void _Private_UpdateSecret(byte[] newSecret, string reason) { ChannelRpc(new ConnectionUpdateSecret(newSecret, reason), ProtocolCommandId.ConnectionUpdateSecretOk); @@ -234,19 +209,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 +236,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 +283,25 @@ 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(); - return true; + return HandleBasicGetEmpty(in cmd); } 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 +314,7 @@ protected override bool DispatchAsynchronous(in IncomingCommand cmd) } case ProtocolCommandId.ChannelCloseOk: { - cmd.ReturnMethodBuffer(); - HandleChannelCloseOk(); + HandleChannelCloseOk(in cmd); return true; } case ProtocolCommandId.ChannelFlow: @@ -373,8 +349,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..04e25b2e18 --- /dev/null +++ b/projects/RabbitMQ.Client/client/impl/AsyncRpcContinuations.cs @@ -0,0 +1,484 @@ +// 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 _cancellationTokenSource; + private readonly ConfiguredTaskAwaitable _taskAwaitable; + + protected readonly TaskCompletionSource _tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + private bool _disposedValue; + + public AsyncRpcContinuation(TimeSpan continuationTimeout) + { + /* + * Note: we can't use an ObjectPool for these because the netstandard2.0 + * version of CancellationTokenSource can't be reset prior to checking + * in to the ObjectPool + */ + _cancellationTokenSource = new CancellationTokenSource(continuationTimeout); + + _cancellationTokenSource.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(); + } + + 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) + { + _cancellationTokenSource.Dispose(); + } + + _disposedValue = true; + } + } + + public void Dispose() + { + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + } + + internal class ConnectionSecureOrTuneAsyncRpcContinuation : AsyncRpcContinuation + { + public ConnectionSecureOrTuneAsyncRpcContinuation(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.ToArray()); + + _tcs.TrySetResult(result); + } + else if (cmd.CommandId == ProtocolCommandId.BasicGetEmpty) + { + _tcs.TrySetResult(null); + } + else + { + _tcs.SetException(new InvalidOperationException($"Received unexpected command of type {cmd.CommandId}!")); + } + } + finally + { + // Note: since we copy the body buffer above, 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..3c58f3b09c 100644 --- a/projects/RabbitMQ.Client/client/impl/AutorecoveringChannel.cs +++ b/projects/RabbitMQ.Client/client/impl/AutorecoveringChannel.cs @@ -45,7 +45,7 @@ internal sealed class AutorecoveringChannel : IChannel, IRecoverable private AutorecoveringConnection _connection; private RecoveryAwareChannel _innerChannel; private bool _disposed; - private List _recordedConsumerTags = new List(); + private readonly List _recordedConsumerTags = new(); private ushort _prefetchCountConsumer; private ushort _prefetchCountGlobal; @@ -87,12 +87,6 @@ public AutorecoveringChannel(AutorecoveringConnection conn, RecoveryAwareChannel remove => InnerChannel.BasicNacks -= value; } - public event EventHandler BasicRecoverOk - { - add => InnerChannel.BasicRecoverOk += value; - remove => InnerChannel.BasicRecoverOk -= value; - } - public event EventHandler BasicReturn { add => InnerChannel.BasicReturn += value; @@ -128,98 +122,66 @@ public IEnumerable ConsumerTags get { ThrowIfDisposed(); + // TODO should this be copied "on its way out"? return _recordedConsumerTags; } } - public int ChannelNumber - { - get - { - ThrowIfDisposed(); - return InnerChannel.ChannelNumber; - } - } + public int ChannelNumber => InnerChannel.ChannelNumber; - public ShutdownEventArgs CloseReason - { - get - { - ThrowIfDisposed(); - return InnerChannel.CloseReason; - } - } + public ShutdownEventArgs CloseReason => InnerChannel.CloseReason; public IBasicConsumer DefaultConsumer { - get - { - ThrowIfDisposed(); - return InnerChannel.DefaultConsumer; - } - - set - { - ThrowIfDisposed(); - InnerChannel.DefaultConsumer = value; - } + get => InnerChannel.DefaultConsumer; + set => InnerChannel.DefaultConsumer = value; } public bool IsClosed => !IsOpen; - public bool IsOpen - { - get - { - ThrowIfDisposed(); - return _innerChannel != null && _innerChannel.IsOpen; - } - } + public bool IsOpen => _innerChannel != null && _innerChannel.IsOpen; - public ulong NextPublishSeqNo - { - get - { - ThrowIfDisposed(); - return InnerChannel.NextPublishSeqNo; - } - } + public ulong NextPublishSeqNo => InnerChannel.NextPublishSeqNo; + + public string CurrentQueue => InnerChannel.CurrentQueue; - public string CurrentQueue + internal async ValueTask AutomaticallyRecoverAsync(AutorecoveringConnection conn, bool recoverConsumers, + bool recordedEntitiesSemaphoreHeld = false) { - get + if (false == recordedEntitiesSemaphoreHeld) { - ThrowIfDisposed(); - return InnerChannel.CurrentQueue; + throw new InvalidOperationException("recordedEntitiesSemaphore must be held"); } - } - internal void AutomaticallyRecover(AutorecoveringConnection conn, bool recoverConsumers) - { ThrowIfDisposed(); _connection = conn; - var newChannel = conn.CreateNonRecoveringChannel(); + RecoveryAwareChannel newChannel = await conn.CreateNonRecoveringChannelAsync() + .ConfigureAwait(false); newChannel.TakeOver(_innerChannel); if (_prefetchCountConsumer != 0) { - newChannel.BasicQos(0, _prefetchCountConsumer, false); + await newChannel.BasicQosAsync(0, _prefetchCountConsumer, false) + .ConfigureAwait(false); } if (_prefetchCountGlobal != 0) { - newChannel.BasicQos(0, _prefetchCountGlobal, true); + await newChannel.BasicQosAsync(0, _prefetchCountGlobal, true) + .ConfigureAwait(false); } if (_usesPublisherConfirms) { - newChannel.ConfirmSelect(); + await newChannel.ConfirmSelectAsync() + .ConfigureAwait(false); } if (_usesTransactions) { - newChannel.TxSelect(); + await newChannel.TxSelectAsync() + .ConfigureAwait(false); } /* @@ -232,7 +194,8 @@ internal void AutomaticallyRecover(AutorecoveringConnection conn, bool recoverCo if (recoverConsumers) { - _connection.RecoverConsumers(this, newChannel); + await _connection.RecoverConsumersAsync(this, newChannel, recordedEntitiesSemaphoreHeld) + .ConfigureAwait(false); } _innerChannel.RunRecoveryEventHandlers(this); @@ -247,7 +210,31 @@ public void Close(ushort replyCode, string replyText, bool abort) } finally { - _connection.DeleteRecordedChannel(this); + _connection.DeleteRecordedChannel(this, + channelsSemaphoreHeld: false, recordedEntitiesSemaphoreHeld: false); + } + } + + public ValueTask CloseAsync(ushort replyCode, string replyText, bool abort) + { + ThrowIfDisposed(); + var args = new ShutdownEventArgs(ShutdownInitiator.Library, replyCode, replyText); + return CloseAsync(args, abort); + } + + public async ValueTask CloseAsync(ShutdownEventArgs args, bool abort) + { + ThrowIfDisposed(); + try + { + await _innerChannel.CloseAsync(args, abort) + .ConfigureAwait(false); + } + finally + { + await _connection.DeleteRecordedChannelAsync(this, + channelsSemaphoreHeld: false, recordedEntitiesSemaphoreHeld: false) + .ConfigureAwait(false); } } @@ -264,7 +251,6 @@ public void Dispose() this.Abort(); _recordedConsumerTags.Clear(); - _recordedConsumerTags = null; _connection = null; _innerChannel = null; _disposed = true; @@ -273,33 +259,49 @@ 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(); - _connection.DeleteRecordedConsumer(consumerTag); + _connection.DeleteRecordedConsumer(consumerTag, recordedEntitiesSemaphoreHeld: false); _innerChannel.BasicCancel(consumerTag); } + public ValueTask BasicCancelAsync(string consumerTag) + { + ThrowIfDisposed(); + _connection.DeleteRecordedConsumer(consumerTag, recordedEntitiesSemaphoreHeld: false); + return _innerChannel.BasicCancelAsync(consumerTag); + } + public void BasicCancelNoWait(string consumerTag) { ThrowIfDisposed(); - _connection.DeleteRecordedConsumer(consumerTag); + _connection.DeleteRecordedConsumer(consumerTag, recordedEntitiesSemaphoreHeld: false); _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, queue: queue, autoAck: autoAck, exclusive: exclusive, arguments: arguments); - _connection.RecordConsumer(rc); + _connection.RecordConsumer(rc, recordedEntitiesSemaphoreHeld: false); + _recordedConsumerTags.Add(resultConsumerTag); + 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); + await _connection.RecordConsumerAsync(rc, recordedEntitiesSemaphoreHeld: false) + .ConfigureAwait(false); _recordedConsumerTags.Add(resultConsumerTag); return resultConsumerTag; } @@ -307,9 +309,15 @@ public void BasicCancelNoWait(string consumerTag) 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 +337,7 @@ public ValueTask BasicPublishAsync(CachedString exchange, CachedStr public void BasicQos(uint prefetchSize, ushort prefetchCount, bool global) { ThrowIfDisposed(); + if (global) { _prefetchCountGlobal = prefetchCount; @@ -337,46 +346,89 @@ 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 async ValueTask ConfirmSelectAsync() + { + await InnerChannel.ConfirmSelectAsync() + .ConfigureAwait(false); + _usesPublisherConfirms = true; + } + public void ExchangeBind(string destination, string source, string routingKey, IDictionary arguments) { ThrowIfDisposed(); - _connection.RecordBinding(new RecordedBinding(false, destination, source, routingKey, arguments)); + var recordedBinding = new RecordedBinding(false, destination, source, routingKey, arguments); + _connection.RecordBinding(recordedBinding, recordedEntitiesSemaphoreHeld: false); _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) + .ConfigureAwait(false); + var recordedBinding = new RecordedBinding(false, destination, source, routingKey, arguments); + await _connection.RecordBindingAsync(recordedBinding, recordedEntitiesSemaphoreHeld: false) + .ConfigureAwait(false); + } + 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); - _connection.RecordExchange(new RecordedExchange(exchange, type, durable, autoDelete, arguments)); + InnerChannel.ExchangeDeclare(exchange, type, durable, autoDelete, arguments); + var recordedExchange = new RecordedExchange(exchange, type, durable, autoDelete, arguments); + _connection.RecordExchange(recordedExchange, recordedEntitiesSemaphoreHeld: false); + } + + 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) + .ConfigureAwait(false); + if (false == passive) + { + var recordedExchange = new RecordedExchange(exchange, type, durable, autoDelete, arguments); + await _connection.RecordExchangeAsync(recordedExchange, recordedEntitiesSemaphoreHeld: false) + .ConfigureAwait(false); + } } public void ExchangeDeclareNoWait(string exchange, string type, bool durable, bool autoDelete, IDictionary arguments) { - ThrowIfDisposed(); - _innerChannel.ExchangeDeclareNoWait(exchange, type, durable, autoDelete, arguments); - _connection.RecordExchange(new RecordedExchange(exchange, type, durable, autoDelete, arguments)); + InnerChannel.ExchangeDeclareNoWait(exchange, type, durable, autoDelete, arguments); + var recordedExchange = new RecordedExchange(exchange, type, durable, autoDelete, arguments); + _connection.RecordExchange(recordedExchange, recordedEntitiesSemaphoreHeld: false); } public void ExchangeDeclarePassive(string exchange) @@ -385,21 +437,42 @@ public void ExchangeDeclarePassive(string exchange) public void ExchangeDelete(string exchange, bool ifUnused) { InnerChannel.ExchangeDelete(exchange, ifUnused); - _connection.DeleteRecordedExchange(exchange); + _connection.DeleteRecordedExchange(exchange, recordedEntitiesSemaphoreHeld: false); + } + + public async ValueTask ExchangeDeleteAsync(string exchange, bool ifUnused) + { + await InnerChannel.ExchangeDeleteAsync(exchange, ifUnused) + .ConfigureAwait(false); + await _connection.DeleteRecordedExchangeAsync(exchange, recordedEntitiesSemaphoreHeld: false) + .ConfigureAwait(false); } public void ExchangeDeleteNoWait(string exchange, bool ifUnused) { InnerChannel.ExchangeDeleteNoWait(exchange, ifUnused); - _connection.DeleteRecordedExchange(exchange); + _connection.DeleteRecordedExchange(exchange, recordedEntitiesSemaphoreHeld: false); } public void ExchangeUnbind(string destination, string source, string routingKey, IDictionary arguments) { ThrowIfDisposed(); - _connection.DeleteRecordedBinding(new RecordedBinding(false, destination, source, routingKey, arguments)); + var recordedBinding = new RecordedBinding(false, destination, source, routingKey, arguments); + _connection.DeleteRecordedBinding(recordedBinding, recordedEntitiesSemaphoreHeld: false); _innerChannel.ExchangeUnbind(destination, source, routingKey, arguments); - _connection.DeleteAutoDeleteExchange(source); + _connection.DeleteAutoDeleteExchange(source, recordedEntitiesSemaphoreHeld: false); + } + + public async ValueTask ExchangeUnbindAsync(string destination, string source, string routingKey, IDictionary arguments) + { + ThrowIfDisposed(); + var recordedBinding = new RecordedBinding(false, destination, source, routingKey, arguments); + await _connection.DeleteRecordedBindingAsync(recordedBinding, recordedEntitiesSemaphoreHeld: false) + .ConfigureAwait(false); + await InnerChannel.ExchangeUnbindAsync(destination, source, routingKey, arguments) + .ConfigureAwait(false); + await _connection.DeleteAutoDeleteExchangeAsync(source, recordedEntitiesSemaphoreHeld: false) + .ConfigureAwait(false); } public void ExchangeUnbindNoWait(string destination, string source, string routingKey, IDictionary arguments) @@ -408,7 +481,8 @@ public void ExchangeUnbindNoWait(string destination, string source, string routi public void QueueBind(string queue, string exchange, string routingKey, IDictionary arguments) { ThrowIfDisposed(); - _connection.RecordBinding(new RecordedBinding(true, queue, exchange, routingKey, arguments)); + var recordedBinding = new RecordedBinding(true, queue, exchange, routingKey, arguments); + _connection.RecordBinding(recordedBinding, recordedEntitiesSemaphoreHeld: false); _innerChannel.QueueBind(queue, exchange, routingKey, arguments); } @@ -417,27 +491,35 @@ 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); - _connection.RecordQueue(new RecordedQueue(result.QueueName, queue.Length == 0, durable, exclusive, autoDelete, arguments)); + QueueDeclareOk result = InnerChannel.QueueDeclare(queue, durable, exclusive, autoDelete, arguments); + var recordedQueue = new RecordedQueue(result.QueueName, queue.Length == 0, durable, exclusive, autoDelete, arguments); + _connection.RecordQueue(recordedQueue, recordedEntitiesSemaphoreHeld: false); return result; } public void QueueDeclareNoWait(string queue, bool durable, bool exclusive, bool autoDelete, IDictionary arguments) { - ThrowIfDisposed(); - _innerChannel.QueueDeclareNoWait(queue, durable, exclusive, autoDelete, arguments); - _connection.RecordQueue(new RecordedQueue(queue, queue.Length == 0, durable, exclusive, autoDelete, arguments)); + InnerChannel.QueueDeclareNoWait(queue, durable, exclusive, autoDelete, arguments); + var recordedQueue = new RecordedQueue(queue, queue.Length == 0, durable, exclusive, autoDelete, arguments); + _connection.RecordQueue(recordedQueue, recordedEntitiesSemaphoreHeld: false); } - 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) + .ConfigureAwait(false); + if (false == passive) + { + var recordedQueue = new RecordedQueue(result.QueueName, queue.Length == 0, durable, exclusive, autoDelete, arguments); + await _connection.RecordQueueAsync(recordedQueue, recordedEntitiesSemaphoreHeld: false) + .ConfigureAwait(false); + } 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,41 +531,76 @@ public uint ConsumerCount(string queue) public uint QueueDelete(string queue, bool ifUnused, bool ifEmpty) { - ThrowIfDisposed(); - uint result = _innerChannel.QueueDelete(queue, ifUnused, ifEmpty); - _connection.DeleteRecordedQueue(queue); + uint result = InnerChannel.QueueDelete(queue, ifUnused, ifEmpty); + _connection.DeleteRecordedQueue(queue, recordedEntitiesSemaphoreHeld: false); + return result; + } + + public async ValueTask QueueDeleteAsync(string queue, bool ifUnused, bool ifEmpty) + { + uint result = await InnerChannel.QueueDeleteAsync(queue, ifUnused, ifEmpty); + await _connection.DeleteRecordedQueueAsync(queue, recordedEntitiesSemaphoreHeld: false) + .ConfigureAwait(false); return result; } public void QueueDeleteNoWait(string queue, bool ifUnused, bool ifEmpty) { InnerChannel.QueueDeleteNoWait(queue, ifUnused, ifEmpty); - _connection.DeleteRecordedQueue(queue); + _connection.DeleteRecordedQueue(queue, recordedEntitiesSemaphoreHeld: false); } 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(); - _connection.DeleteRecordedBinding(new RecordedBinding(true, queue, exchange, routingKey, arguments)); + var recordedBinding = new RecordedBinding(true, queue, exchange, routingKey, arguments); + _connection.DeleteRecordedBinding(recordedBinding, recordedEntitiesSemaphoreHeld: false); _innerChannel.QueueUnbind(queue, exchange, routingKey, arguments); - _connection.DeleteAutoDeleteExchange(exchange); + _connection.DeleteAutoDeleteExchange(exchange, recordedEntitiesSemaphoreHeld: false); + } + + public async ValueTask QueueUnbindAsync(string queue, string exchange, string routingKey, IDictionary arguments) + { + ThrowIfDisposed(); + var recordedBinding = new RecordedBinding(true, queue, exchange, routingKey, arguments); + await _connection.DeleteRecordedBindingAsync(recordedBinding, recordedEntitiesSemaphoreHeld: false) + .ConfigureAwait(false); + await _innerChannel.QueueUnbindAsync(queue, exchange, routingKey, arguments) + .ConfigureAwait(false); + await _connection.DeleteAutoDeleteExchangeAsync(exchange, recordedEntitiesSemaphoreHeld: false) + .ConfigureAwait(false); } 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.Recording.cs b/projects/RabbitMQ.Client/client/impl/AutorecoveringConnection.Recording.cs index 56d5027be8..c3c3e11f40 100644 --- a/projects/RabbitMQ.Client/client/impl/AutorecoveringConnection.Recording.cs +++ b/projects/RabbitMQ.Client/client/impl/AutorecoveringConnection.Recording.cs @@ -31,6 +31,8 @@ using System.Collections.Generic; using System.Linq; +using System.Threading; +using System.Threading.Tasks; using RabbitMQ.Client.Impl; namespace RabbitMQ.Client.Framing.Impl @@ -38,7 +40,8 @@ namespace RabbitMQ.Client.Framing.Impl #nullable enable internal sealed partial class AutorecoveringConnection { - private readonly object _recordedEntitiesLock = new object(); + private readonly SemaphoreSlim _recordedEntitiesSemaphore = new SemaphoreSlim(1, 1); + private readonly SemaphoreSlim _channelsSemaphore = new SemaphoreSlim(1, 1); private readonly Dictionary _recordedExchanges = new Dictionary(); private readonly Dictionary _recordedQueues = new Dictionary(); private readonly HashSet _recordedBindings = new HashSet(); @@ -47,17 +50,90 @@ internal sealed partial class AutorecoveringConnection internal int RecordedExchangesCount => _recordedExchanges.Count; - internal void RecordExchange(in RecordedExchange exchange) + internal void RecordExchange(in RecordedExchange exchange, + bool recordedEntitiesSemaphoreHeld) { - lock (_recordedEntitiesLock) + if (_disposed) { - _recordedExchanges[exchange.Name] = exchange; + return; + } + + if (recordedEntitiesSemaphoreHeld) + { + DoRecordExchange(exchange); + } + else + { + _recordedEntitiesSemaphore.Wait(); + try + { + DoRecordExchange(exchange); + } + finally + { + _recordedEntitiesSemaphore.Release(); + } + } + } + + internal async ValueTask RecordExchangeAsync(RecordedExchange exchange, + bool recordedEntitiesSemaphoreHeld) + { + if (_disposed) + { + return; + } + + if (recordedEntitiesSemaphoreHeld) + { + DoRecordExchange(exchange); + } + else + { + await _recordedEntitiesSemaphore.WaitAsync() + .ConfigureAwait(false); + try + { + DoRecordExchange(exchange); + } + finally + { + _recordedEntitiesSemaphore.Release(); + } } } - internal void DeleteRecordedExchange(string exchangeName) + private void DoRecordExchange(in RecordedExchange exchange) + { + _recordedExchanges[exchange.Name] = exchange; + } + + internal void DeleteRecordedExchange(string exchangeName, + bool recordedEntitiesSemaphoreHeld) { - lock (_recordedEntitiesLock) + if (_disposed) + { + return; + } + + if (recordedEntitiesSemaphoreHeld) + { + DoDeleteRecordedExchange(exchangeName); + } + else + { + _recordedEntitiesSemaphore.Wait(); + try + { + DoDeleteRecordedExchange(exchangeName); + } + finally + { + _recordedEntitiesSemaphore.Release(); + } + } + + void DoDeleteRecordedExchange(string exchangeName) { _recordedExchanges.Remove(exchangeName); @@ -66,26 +142,126 @@ internal void DeleteRecordedExchange(string exchangeName) { if (binding.Destination == exchangeName) { - DeleteRecordedBinding(binding); - DeleteAutoDeleteExchange(binding.Source); + DeleteRecordedBinding(binding, + recordedEntitiesSemaphoreHeld: true); + DeleteAutoDeleteExchange(binding.Source, + recordedEntitiesSemaphoreHeld: true); } } } } - internal void DeleteAutoDeleteExchange(string exchangeName) + internal async ValueTask DeleteRecordedExchangeAsync(string exchangeName, + bool recordedEntitiesSemaphoreHeld) { - lock (_recordedEntitiesLock) + if (_disposed) { - if (_recordedExchanges.TryGetValue(exchangeName, out var recordedExchange) && recordedExchange.AutoDelete) + return; + } + + if (recordedEntitiesSemaphoreHeld) + { + await DoDeleteRecordedExchangeAsync(exchangeName) + .ConfigureAwait(false); + } + else + { + await _recordedEntitiesSemaphore.WaitAsync() + .ConfigureAwait(false); + try + { + await DoDeleteRecordedExchangeAsync(exchangeName) + .ConfigureAwait(false); + } + finally + { + _recordedEntitiesSemaphore.Release(); + } + } + + async Task DoDeleteRecordedExchangeAsync(string exchangeName) + { + _recordedExchanges.Remove(exchangeName); + + // find bindings that need removal, check if some auto-delete exchanges might need the same + foreach (RecordedBinding binding in _recordedBindings.ToArray()) { - if (!AnyBindingsOnExchange(exchangeName)) + if (binding.Destination == exchangeName) { - // last binding where this exchange is the source is gone, remove recorded exchange if it is auto-deleted. - _recordedExchanges.Remove(exchangeName); + await DeleteRecordedBindingAsync(binding, + recordedEntitiesSemaphoreHeld: true) + .ConfigureAwait(false); + await DeleteAutoDeleteExchangeAsync(binding.Source, + recordedEntitiesSemaphoreHeld: true) + .ConfigureAwait(false); } } } + } + + internal void DeleteAutoDeleteExchange(string exchangeName, + bool recordedEntitiesSemaphoreHeld) + { + if (_disposed) + { + return; + } + + if (recordedEntitiesSemaphoreHeld) + { + DoDeleteAutoDeleteExchange(exchangeName); + } + else + { + _recordedEntitiesSemaphore.Wait(); + try + { + DoDeleteAutoDeleteExchange(exchangeName); + } + finally + { + _recordedEntitiesSemaphore.Release(); + } + } + } + + internal async ValueTask DeleteAutoDeleteExchangeAsync(string exchangeName, + bool recordedEntitiesSemaphoreHeld) + { + if (_disposed) + { + return; + } + + if (recordedEntitiesSemaphoreHeld) + { + DoDeleteAutoDeleteExchange(exchangeName); + } + else + { + await _recordedEntitiesSemaphore.WaitAsync() + .ConfigureAwait(false); + try + { + DoDeleteAutoDeleteExchange(exchangeName); + } + finally + { + _recordedEntitiesSemaphore.Release(); + } + } + } + + private void DoDeleteAutoDeleteExchange(string exchangeName) + { + if (_recordedExchanges.TryGetValue(exchangeName, out var recordedExchange) && recordedExchange.AutoDelete) + { + if (!AnyBindingsOnExchange(exchangeName)) + { + // last binding where this exchange is the source is gone, remove recorded exchange if it is auto-deleted. + _recordedExchanges.Remove(exchangeName); + } + } bool AnyBindingsOnExchange(string exchange) { @@ -103,163 +279,583 @@ bool AnyBindingsOnExchange(string exchange) internal int RecordedQueuesCount => _recordedQueues.Count; - internal void RecordQueue(in RecordedQueue queue) + internal void RecordQueue(in RecordedQueue queue, + bool recordedEntitiesSemaphoreHeld) { - lock (_recordedEntitiesLock) + if (_disposed) + { + return; + } + + if (recordedEntitiesSemaphoreHeld) { - _recordedQueues[queue.Name] = queue; + DoRecordQueue(queue); + } + else + { + _recordedEntitiesSemaphore.Wait(); + try + { + DoRecordQueue(queue); + } + finally + { + _recordedEntitiesSemaphore.Release(); + } } } - internal void DeleteRecordedQueue(string queueName) + internal async ValueTask RecordQueueAsync(RecordedQueue queue, + bool recordedEntitiesSemaphoreHeld) { - lock (_recordedEntitiesLock) + if (_disposed) + { + return; + } + + if (recordedEntitiesSemaphoreHeld) + { + DoRecordQueue(queue); + } + else + { + await _recordedEntitiesSemaphore.WaitAsync() + .ConfigureAwait(false); + try + { + DoRecordQueue(queue); + } + finally + { + _recordedEntitiesSemaphore.Release(); + } + } + } + + private void DoRecordQueue(RecordedQueue queue) + { + _recordedQueues[queue.Name] = queue; + } + + internal void DeleteRecordedQueue(string queueName, + bool recordedEntitiesSemaphoreHeld) + { + if (_disposed) + { + return; + } + + if (recordedEntitiesSemaphoreHeld) + { + DoDeleteRecordedQueue(queueName); + } + else + { + _recordedEntitiesSemaphore.Wait(); + try + { + DoDeleteRecordedQueue(queueName); + } + finally + { + _recordedEntitiesSemaphore.Release(); + } + } + + void DoDeleteRecordedQueue(string queueName) { _recordedQueues.Remove(queueName); // find bindings that need removal, check if some auto-delete exchanges might need the same - foreach (var binding in _recordedBindings.ToArray()) + foreach (RecordedBinding binding in _recordedBindings.ToArray()) { if (binding.Destination == queueName) { - DeleteRecordedBinding(binding); - DeleteAutoDeleteExchange(binding.Source); + DeleteRecordedBinding(binding, + recordedEntitiesSemaphoreHeld: true); + DeleteAutoDeleteExchange(binding.Source, + recordedEntitiesSemaphoreHeld: true); } } } } - private void UpdateBindingsDestination(string oldName, string newName) + internal async ValueTask DeleteRecordedQueueAsync(string queueName, + bool recordedEntitiesSemaphoreHeld) { - lock (_recordedEntitiesLock) + if (_disposed) + { + return; + } + + if (recordedEntitiesSemaphoreHeld) + { + await DoDeleteRecordedQueueAsync(queueName) + .ConfigureAwait(false); + } + else { - foreach (RecordedBinding b in _recordedBindings.ToArray()) + await _recordedEntitiesSemaphore.WaitAsync() + .ConfigureAwait(false); + try { - if (b.Destination == oldName) + await DoDeleteRecordedQueueAsync(queueName) + .ConfigureAwait(false); + } + finally + { + _recordedEntitiesSemaphore.Release(); + } + } + + async ValueTask DoDeleteRecordedQueueAsync(string queueName) + { + _recordedQueues.Remove(queueName); + + // find bindings that need removal, check if some auto-delete exchanges might need the same + foreach (RecordedBinding binding in _recordedBindings.ToArray()) + { + if (binding.Destination == queueName) { - _recordedBindings.Remove(b); - _recordedBindings.Add(new RecordedBinding(newName, b)); + await DeleteRecordedBindingAsync(binding, + recordedEntitiesSemaphoreHeld: true) + .ConfigureAwait(false); + await DeleteAutoDeleteExchangeAsync(binding.Source, + recordedEntitiesSemaphoreHeld: true) + .ConfigureAwait(false); } } } + } - private void UpdateConsumerQueue(string oldName, string newName) + internal void RecordBinding(in RecordedBinding binding, + bool recordedEntitiesSemaphoreHeld) { - lock (_recordedEntitiesLock) + if (_disposed) + { + return; + } + + if (recordedEntitiesSemaphoreHeld) + { + DoRecordBinding(binding); + } + else { - foreach (RecordedConsumer consumer in _recordedConsumers.Values.ToArray()) + _recordedEntitiesSemaphore.Wait(); + try { - if (consumer.Queue == oldName) - { - _recordedConsumers[consumer.ConsumerTag] = RecordedConsumer.WithNewQueueName(newName, consumer); - } + DoRecordBinding(binding); + } + finally + { + _recordedEntitiesSemaphore.Release(); } } } - internal void RecordBinding(in RecordedBinding rb) + internal async ValueTask RecordBindingAsync(RecordedBinding binding, + bool recordedEntitiesSemaphoreHeld) { - lock (_recordedEntitiesLock) + if (_disposed) + { + return; + } + + if (recordedEntitiesSemaphoreHeld) { - _recordedBindings.Add(rb); + DoRecordBinding(binding); + } + else + { + await _recordedEntitiesSemaphore.WaitAsync() + .ConfigureAwait(false); + try + { + DoRecordBinding(binding); + } + finally + { + _recordedEntitiesSemaphore.Release(); + } } } - internal void DeleteRecordedBinding(in RecordedBinding rb) + private void DoRecordBinding(in RecordedBinding binding) { - lock (_recordedEntitiesLock) + _recordedBindings.Add(binding); + } + + internal void DeleteRecordedBinding(in RecordedBinding rb, + bool recordedEntitiesSemaphoreHeld) + { + if (_disposed) + { + return; + } + + if (recordedEntitiesSemaphoreHeld) { - _recordedBindings.Remove(rb); + DoDeleteRecordedBinding(rb); + } + else + { + _recordedEntitiesSemaphore.Wait(); + try + { + DoDeleteRecordedBinding(rb); + } + finally + { + _recordedEntitiesSemaphore.Release(); + } } } - internal void RecordConsumer(in RecordedConsumer consumer) + internal async ValueTask DeleteRecordedBindingAsync(RecordedBinding rb, + bool recordedEntitiesSemaphoreHeld) { + if (_disposed) + { + return; + } + + if (recordedEntitiesSemaphoreHeld) + { + DoDeleteRecordedBinding(rb); + } + else + { + await _recordedEntitiesSemaphore.WaitAsync() + .ConfigureAwait(false); + try + { + DoDeleteRecordedBinding(rb); + } + finally + { + _recordedEntitiesSemaphore.Release(); + } + } + } + + private void DoDeleteRecordedBinding(in RecordedBinding rb) + { + _recordedBindings.Remove(rb); + } + + internal void RecordConsumer(in RecordedConsumer consumer, + bool recordedEntitiesSemaphoreHeld) + { + if (_disposed) + { + return; + } + if (!_config.TopologyRecoveryEnabled) { return; } - lock (_recordedEntitiesLock) + if (recordedEntitiesSemaphoreHeld) + { + DoRecordConsumer(consumer); + } + else { - _recordedConsumers[consumer.ConsumerTag] = consumer; + _recordedEntitiesSemaphore.Wait(); + try + { + DoRecordConsumer(consumer); + } + finally + { + _recordedEntitiesSemaphore.Release(); + } } } - internal void DeleteRecordedConsumer(string consumerTag) + internal async ValueTask RecordConsumerAsync(RecordedConsumer consumer, + bool recordedEntitiesSemaphoreHeld) { + if (_disposed) + { + return; + } + if (!_config.TopologyRecoveryEnabled) { return; } - lock (_recordedEntitiesLock) + if (recordedEntitiesSemaphoreHeld) { - if (_recordedConsumers.Remove(consumerTag, out var recordedConsumer)) + DoRecordConsumer(consumer); + } + else + { + await _recordedEntitiesSemaphore.WaitAsync() + .ConfigureAwait(false); + try { - DeleteAutoDeleteQueue(recordedConsumer.Queue); + DoRecordConsumer(consumer); } + finally + { + _recordedEntitiesSemaphore.Release(); + } + } + } + + private void DoRecordConsumer(in RecordedConsumer consumer) + { + _recordedConsumers[consumer.ConsumerTag] = consumer; + } + + internal void DeleteRecordedConsumer(string consumerTag, + bool recordedEntitiesSemaphoreHeld) + { + if (_disposed) + { + return; } - void DeleteAutoDeleteQueue(string queue) + if (!_config.TopologyRecoveryEnabled) { - if (_recordedQueues.TryGetValue(queue, out var recordedQueue) && recordedQueue.AutoDelete) + return; + } + + if (recordedEntitiesSemaphoreHeld) + { + DoDeleteRecordedConsumer(consumerTag); + } + else + { + _recordedEntitiesSemaphore.Wait(); + try { - // last consumer on this connection is gone, remove recorded queue if it is auto-deleted. - if (!AnyConsumersOnQueue(queue)) - { - _recordedQueues.Remove(queue); - } + DoDeleteRecordedConsumer(consumerTag); + } + finally + { + _recordedEntitiesSemaphore.Release(); + } + } + } + + internal async ValueTask DeleteRecordedConsumerAsync(string consumerTag, + bool recordedEntitiesSemaphoreHeld) + { + if (_disposed) + { + return; + } + + if (!_config.TopologyRecoveryEnabled) + { + return; + } + + if (recordedEntitiesSemaphoreHeld) + { + DoDeleteRecordedConsumer(consumerTag); + } + else + { + await _recordedEntitiesSemaphore.WaitAsync() + .ConfigureAwait(false); + try + { + DoDeleteRecordedConsumer(consumerTag); } + finally + { + _recordedEntitiesSemaphore.Release(); + } + } + } + + private void DoDeleteRecordedConsumer(string consumerTag) + { + if (_recordedConsumers.Remove(consumerTag, out var recordedConsumer)) + { + DeleteAutoDeleteQueue(recordedConsumer.Queue); } + } - bool AnyConsumersOnQueue(string queue) + private void DeleteAutoDeleteQueue(string queue) + { + if (_recordedQueues.TryGetValue(queue, out var recordedQueue) && recordedQueue.AutoDelete) { - foreach (var pair in _recordedConsumers) + // last consumer on this connection is gone, remove recorded queue if it is auto-deleted. + if (!AnyConsumersOnQueue(queue)) { - if (pair.Value.Queue == queue) - { - return true; - } + _recordedQueues.Remove(queue); } + } + } - return false; + private bool AnyConsumersOnQueue(string queue) + { + foreach (var pair in _recordedConsumers) + { + if (pair.Value.Queue == queue) + { + return true; + } } + + return false; } - private void UpdateConsumer(string oldTag, string newTag, in RecordedConsumer consumer) + private void RecordChannel(in AutorecoveringChannel channel, + bool channelsSemaphoreHeld = false) { - lock (_recordedEntitiesLock) + if (channelsSemaphoreHeld) { - // make sure server-generated tags are re-added - _recordedConsumers.Remove(oldTag); - _recordedConsumers.Add(newTag, consumer); + DoAddRecordedChannel(channel); } + else + { + _channelsSemaphore.Wait(); + try + { + DoAddRecordedChannel(channel); + } + finally + { + _channelsSemaphore.Release(); + } + } + } + + private async Task RecordChannelAsync(AutorecoveringChannel channel, + bool channelsSemaphoreHeld = false) + { + if (channelsSemaphoreHeld) + { + DoAddRecordedChannel(channel); + } + else + { + await _channelsSemaphore.WaitAsync() + .ConfigureAwait(false); + try + { + DoAddRecordedChannel(channel); + } + finally + { + _channelsSemaphore.Release(); + } + } + } + + private void DoAddRecordedChannel(AutorecoveringChannel channel) + { + _channels.Add(channel); } - private void RecordChannel(AutorecoveringChannel m) + internal void DeleteRecordedChannel(in AutorecoveringChannel channel, + bool channelsSemaphoreHeld, bool recordedEntitiesSemaphoreHeld) { - lock (_channels) + if (_disposed) + { + return; + } + + if (recordedEntitiesSemaphoreHeld) + { + DoDeleteRecordedConsumers(channel); + } + else + { + _recordedEntitiesSemaphore.Wait(); + try + { + DoDeleteRecordedConsumers(channel); + } + finally + { + _recordedEntitiesSemaphore.Release(); + } + } + + if (channelsSemaphoreHeld) { - _channels.Add(m); + DoDeleteRecordedChannel(channel); + } + else + { + _channelsSemaphore.Wait(); + try + { + DoDeleteRecordedChannel(channel); + } + finally + { + _channelsSemaphore.Release(); + } } } - internal void DeleteRecordedChannel(AutorecoveringChannel channel) + internal async Task DeleteRecordedChannelAsync(AutorecoveringChannel channel, + bool channelsSemaphoreHeld, bool recordedEntitiesSemaphoreHeld) { - lock (_recordedEntitiesLock) + if (_disposed) { - foreach (string ct in channel.ConsumerTags) + return; + } + + if (recordedEntitiesSemaphoreHeld) + { + DoDeleteRecordedConsumers(channel); + } + else + { + await _recordedEntitiesSemaphore.WaitAsync() + .ConfigureAwait(false); + try { - DeleteRecordedConsumer(ct); + DoDeleteRecordedConsumers(channel); + } + finally + { + _recordedEntitiesSemaphore.Release(); } } - lock (_channels) + if (channelsSemaphoreHeld) { - _channels.Remove(channel); + DoDeleteRecordedChannel(channel); } + else + { + await _channelsSemaphore.WaitAsync() + .ConfigureAwait(false); + try + { + DoDeleteRecordedChannel(channel); + } + finally + { + _channelsSemaphore.Release(); + } + } + } + + private void DoDeleteRecordedConsumers(in AutorecoveringChannel channel) + { + foreach (string ct in channel.ConsumerTags) + { + DeleteRecordedConsumer(ct, recordedEntitiesSemaphoreHeld: true); + } + } + + private void DoDeleteRecordedChannel(in AutorecoveringChannel channel) + { + _channels.Remove(channel); } } } diff --git a/projects/RabbitMQ.Client/client/impl/AutorecoveringConnection.Recovery.cs b/projects/RabbitMQ.Client/client/impl/AutorecoveringConnection.Recovery.cs index 733de82c78..668d5fc951 100644 --- a/projects/RabbitMQ.Client/client/impl/AutorecoveringConnection.Recovery.cs +++ b/projects/RabbitMQ.Client/client/impl/AutorecoveringConnection.Recovery.cs @@ -46,6 +46,7 @@ internal sealed partial class AutorecoveringConnection private Task? _recoveryTask; private CancellationTokenSource? _recoveryCancellationTokenSource; + // TODO dispose the CTS private CancellationTokenSource RecoveryCancellationTokenSource => _recoveryCancellationTokenSource ??= new CancellationTokenSource(); private void HandleConnectionShutdown(object _, ShutdownEventArgs args) @@ -70,12 +71,14 @@ private async Task RecoverConnectionAsync() { try { - var token = RecoveryCancellationTokenSource.Token; + CancellationToken token = RecoveryCancellationTokenSource.Token; bool success; do { - await Task.Delay(_config.NetworkRecoveryInterval, token).ConfigureAwait(false); - success = TryPerformAutomaticRecovery(); + await Task.Delay(_config.NetworkRecoveryInterval, token) + .ConfigureAwait(false); + success = await TryPerformAutomaticRecoveryAsync(token) + .ConfigureAwait(false); } while (!success && !token.IsCancellationRequested); } catch (OperationCanceledException) @@ -86,9 +89,11 @@ private async Task RecoverConnectionAsync() { ESLog.Error("Main recovery loop threw unexpected exception.", e); } - - // clear recovery task - _recoveryTask = null; + finally + { + // clear recovery task + _recoveryTask = null; + } } /// @@ -97,7 +102,7 @@ private async Task RecoverConnectionAsync() /// private void StopRecoveryLoop() { - var task = _recoveryTask; + Task? task = _recoveryTask; if (task is null) { return; @@ -111,6 +116,28 @@ private void StopRecoveryLoop() } } + /// + /// Async cancels the main recovery loop and will block until the loop finishes, or the timeout + /// expires, to prevent Close operations overlapping with recovery operations. + /// + private async ValueTask StopRecoveryLoopAsync() + { + Task? task = _recoveryTask; + if (task is not null) + { + RecoveryCancellationTokenSource.Cancel(); + try + { + await task.TimeoutAfter(_config.RequestedConnectionTimeout) + .ConfigureAwait(false); + } + catch (TimeoutException) + { + ESLog.Warn("Timeout while trying to stop background AutorecoveringConnection recovery loop."); + } + } + } + private static void HandleTopologyRecoveryException(TopologyRecoveryException e) { ESLog.Error("Topology recovery exception", e); @@ -123,16 +150,19 @@ 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); } - private bool TryPerformAutomaticRecovery() + // TODO propagate token + private async ValueTask TryPerformAutomaticRecoveryAsync(CancellationToken token) { ESLog.Info("Performing automatic recovery"); try { ThrowIfDisposed(); - if (TryRecoverConnectionDelegate()) + if (await TryRecoverConnectionDelegate().ConfigureAwait(false)) { - lock (_recordedEntitiesLock) + await _recordedEntitiesSemaphore.WaitAsync(token) + .ConfigureAwait(false); + try { ThrowIfDisposed(); if (_config.TopologyRecoveryEnabled) @@ -145,13 +175,21 @@ private bool TryPerformAutomaticRecovery() // 4. Recover consumers using (var recoveryChannelFactory = new RecoveryChannelFactory(_innerConnection)) { - RecoverExchanges(recoveryChannelFactory); - RecoverQueues(recoveryChannelFactory); - RecoverBindings(recoveryChannelFactory); + await RecoverExchangesAsync(recoveryChannelFactory, recordedEntitiesSemaphoreHeld: true) + .ConfigureAwait(false); + await RecoverQueuesAsync(recoveryChannelFactory, recordedEntitiesSemaphoreHeld: true) + .ConfigureAwait(false); + await RecoverBindingsAsync(recoveryChannelFactory, recordedEntitiesSemaphoreHeld: true) + .ConfigureAwait(false); } } - RecoverChannelsAndItsConsumers(); + await RecoverChannelsAndItsConsumersAsync(recordedEntitiesSemaphoreHeld: true) + .ConfigureAwait(false); + } + finally + { + _recordedEntitiesSemaphore.Release(); } ESLog.Info("Connection recovery completed"); @@ -188,13 +226,16 @@ private bool TryPerformAutomaticRecovery() return false; } - private bool TryRecoverConnectionDelegate() + private async ValueTask TryRecoverConnectionDelegate() { try { - var defunctConnection = _innerConnection; + Connection defunctConnection = _innerConnection; IFrameHandler fh = _endpoints.SelectOne(_config.FrameHandlerFactory); _innerConnection = new Connection(_config, fh); + // TODO cancellation token + await _innerConnection.OpenAsync() + .ConfigureAwait(false); _innerConnection.TakeOver(defunctConnection); return true; } @@ -204,6 +245,7 @@ private bool TryRecoverConnectionDelegate() // Trigger recovery error events if (!_connectionRecoveryErrorWrapper.IsEmpty) { + // Note: recordedEntities semaphore is _NOT_ held at this point _connectionRecoveryErrorWrapper.Invoke(this, new ConnectionRecoveryErrorEventArgs(e)); } } @@ -211,20 +253,44 @@ private bool TryRecoverConnectionDelegate() return false; } - private void RecoverExchanges(RecoveryChannelFactory recoveryChannelFactory) + private async ValueTask RecoverExchangesAsync(RecoveryChannelFactory recoveryChannelFactory, + bool recordedEntitiesSemaphoreHeld = false) { - foreach (var recordedExchange in _recordedExchanges.Values.Where(x => _config.TopologyRecoveryFilter?.ExchangeFilter(x) ?? true)) + if (_disposed) + { + return; + } + + if (false == recordedEntitiesSemaphoreHeld) + { + throw new InvalidOperationException("recordedEntitiesSemaphore must be held"); + } + + foreach (RecordedExchange recordedExchange in _recordedExchanges.Values.Where(x => _config.TopologyRecoveryFilter?.ExchangeFilter(x) ?? true)) { try { - recordedExchange.Recover(recoveryChannelFactory.RecoveryChannel); + using (IChannel ch = await recoveryChannelFactory.CreateRecoveryChannelAsync().ConfigureAwait(false)) + { + await recordedExchange.RecoverAsync(ch) + .ConfigureAwait(false); + } } catch (Exception ex) { if (_config.TopologyRecoveryExceptionHandler.ExchangeRecoveryExceptionHandler != null && _config.TopologyRecoveryExceptionHandler.ExchangeRecoveryExceptionCondition(recordedExchange, ex)) { - _config.TopologyRecoveryExceptionHandler.ExchangeRecoveryExceptionHandler(recordedExchange, ex, this); + try + { + _recordedEntitiesSemaphore.Release(); + _config.TopologyRecoveryExceptionHandler.ExchangeRecoveryExceptionHandler(recordedExchange, ex, this); + } + finally + { + await _recordedEntitiesSemaphore.WaitAsync() + .ConfigureAwait(false); + } } else { @@ -234,14 +300,30 @@ private void RecoverExchanges(RecoveryChannelFactory recoveryChannelFactory) } } - private void RecoverQueues(RecoveryChannelFactory recoveryChannelFactory) + private async Task RecoverQueuesAsync(RecoveryChannelFactory recoveryChannelFactory, + bool recordedEntitiesSemaphoreHeld = false) { - foreach (var recordedQueue in _recordedQueues.Values.Where(x => _config.TopologyRecoveryFilter?.QueueFilter(x) ?? true).ToArray()) + if (_disposed) + { + return; + } + + if (false == recordedEntitiesSemaphoreHeld) + { + throw new InvalidOperationException("recordedEntitiesSemaphore must be held"); + } + + foreach (RecordedQueue recordedQueue in _recordedQueues.Values.Where(x => _config.TopologyRecoveryFilter?.QueueFilter(x) ?? true).ToArray()) { try { - var newName = recordedQueue.Recover(recoveryChannelFactory.RecoveryChannel); - var oldName = recordedQueue.Name; + string newName = string.Empty; + using (IChannel ch = await recoveryChannelFactory.CreateRecoveryChannelAsync().ConfigureAwait(false)) + { + newName = await recordedQueue.RecoverAsync(ch) + .ConfigureAwait(false); + } + string oldName = recordedQueue.Name; if (oldName != newName) { @@ -255,13 +337,24 @@ private void RecoverQueues(RecoveryChannelFactory recoveryChannelFactory) // see rabbitmq/rabbitmq-dotnet-client#43 if (recordedQueue.IsServerNamed) { - DeleteRecordedQueue(oldName); + DeleteRecordedQueue(oldName, + recordedEntitiesSemaphoreHeld: recordedEntitiesSemaphoreHeld); } - RecordQueue(new RecordedQueue(newName, recordedQueue)); + RecordQueue(new RecordedQueue(newName, recordedQueue), + recordedEntitiesSemaphoreHeld: recordedEntitiesSemaphoreHeld); - if (!_queueNameChangeAfterRecoveryWrapper.IsEmpty) + if (!_queueNameChangedAfterRecoveryWrapper.IsEmpty) { - _queueNameChangeAfterRecoveryWrapper.Invoke(this, new QueueNameChangedAfterRecoveryEventArgs(oldName, newName)); + try + { + _recordedEntitiesSemaphore.Release(); + _queueNameChangedAfterRecoveryWrapper.Invoke(this, new QueueNameChangedAfterRecoveryEventArgs(oldName, newName)); + } + finally + { + await _recordedEntitiesSemaphore.WaitAsync() + .ConfigureAwait(false); + } } } } @@ -270,30 +363,86 @@ private void RecoverQueues(RecoveryChannelFactory recoveryChannelFactory) if (_config.TopologyRecoveryExceptionHandler.QueueRecoveryExceptionHandler != null && _config.TopologyRecoveryExceptionHandler.QueueRecoveryExceptionCondition(recordedQueue, ex)) { - _config.TopologyRecoveryExceptionHandler.QueueRecoveryExceptionHandler(recordedQueue, ex, this); + try + { + _recordedEntitiesSemaphore.Release(); + _config.TopologyRecoveryExceptionHandler.QueueRecoveryExceptionHandler(recordedQueue, ex, this); + } + finally + { + await _recordedEntitiesSemaphore.WaitAsync() + .ConfigureAwait(false); + } } else { HandleTopologyRecoveryException(new TopologyRecoveryException($"Caught an exception while recovering queue '{recordedQueue}'", ex)); } } + + void UpdateBindingsDestination(string oldName, string newName) + { + foreach (RecordedBinding b in _recordedBindings.ToArray()) + { + if (b.Destination == oldName) + { + _recordedBindings.Remove(b); + _recordedBindings.Add(new RecordedBinding(newName, b)); + } + } + } + + void UpdateConsumerQueue(string oldName, string newName) + { + foreach (RecordedConsumer consumer in _recordedConsumers.Values.ToArray()) + { + if (consumer.Queue == oldName) + { + _recordedConsumers[consumer.ConsumerTag] = RecordedConsumer.WithNewQueueName(newName, consumer); + } + } + } } } - private void RecoverBindings(RecoveryChannelFactory recoveryChannelFactory) + private async ValueTask RecoverBindingsAsync(RecoveryChannelFactory recoveryChannelFactory, + bool recordedEntitiesSemaphoreHeld = false) { - foreach (var binding in _recordedBindings.Where(x => _config.TopologyRecoveryFilter?.BindingFilter(x) ?? true)) + if (_disposed) + { + return; + } + + if (false == recordedEntitiesSemaphoreHeld) + { + throw new InvalidOperationException("recordedEntitiesSemaphore must be held"); + } + + foreach (RecordedBinding binding in _recordedBindings.Where(x => _config.TopologyRecoveryFilter?.BindingFilter(x) ?? true)) { try { - binding.Recover(recoveryChannelFactory.RecoveryChannel); + using (IChannel ch = await recoveryChannelFactory.CreateRecoveryChannelAsync().ConfigureAwait(false)) + { + await binding.RecoverAsync(ch) + .ConfigureAwait(false); + } } catch (Exception ex) { if (_config.TopologyRecoveryExceptionHandler.BindingRecoveryExceptionHandler != null && _config.TopologyRecoveryExceptionHandler.BindingRecoveryExceptionCondition(binding, ex)) { - _config.TopologyRecoveryExceptionHandler.BindingRecoveryExceptionHandler(binding, ex, this); + try + { + _recordedEntitiesSemaphore.Release(); + _config.TopologyRecoveryExceptionHandler.BindingRecoveryExceptionHandler(binding, ex, this); + } + finally + { + await _recordedEntitiesSemaphore.WaitAsync() + .ConfigureAwait(false); + } } else { @@ -303,26 +452,55 @@ private void RecoverBindings(RecoveryChannelFactory recoveryChannelFactory) } } - internal void RecoverConsumers(AutorecoveringChannel channelToRecover, IChannel channelToUse) + internal async ValueTask RecoverConsumersAsync(AutorecoveringChannel channelToRecover, IChannel channelToUse, + bool recordedEntitiesSemaphoreHeld = false) { - foreach (var consumer in _recordedConsumers.Values.Where(x => _config.TopologyRecoveryFilter?.ConsumerFilter(x) ?? true).ToArray()) + if (_disposed) + { + return; + } + + if (false == recordedEntitiesSemaphoreHeld) + { + throw new InvalidOperationException("recordedEntitiesSemaphore must be held"); + } + + foreach (RecordedConsumer consumer in _recordedConsumers.Values.Where(x => _config.TopologyRecoveryFilter?.ConsumerFilter(x) ?? true).ToArray()) { if (consumer.Channel != channelToRecover) { continue; } - _consumerAboutToBeRecovered.Invoke(this, new RecoveringConsumerEventArgs(consumer.ConsumerTag, consumer.Arguments)); + try + { + _recordedEntitiesSemaphore.Release(); + _consumerAboutToBeRecovered.Invoke(this, new RecoveringConsumerEventArgs(consumer.ConsumerTag, consumer.Arguments)); + } + finally + { + _recordedEntitiesSemaphore.Wait(); + } - var oldTag = consumer.ConsumerTag; + string oldTag = consumer.ConsumerTag; try { - var newTag = consumer.Recover(channelToUse); - UpdateConsumer(oldTag, newTag, RecordedConsumer.WithNewConsumerTag(newTag, consumer)); + string newTag = await consumer.RecoverAsync(channelToUse); + RecordedConsumer consumerWithNewConsumerTag = RecordedConsumer.WithNewConsumerTag(newTag, consumer); + UpdateConsumer(oldTag, newTag, consumerWithNewConsumerTag); if (!_consumerTagChangeAfterRecoveryWrapper.IsEmpty) { - _consumerTagChangeAfterRecoveryWrapper.Invoke(this, new ConsumerTagChangedAfterRecoveryEventArgs(oldTag, newTag)); + try + { + _recordedEntitiesSemaphore.Release(); + _consumerTagChangeAfterRecoveryWrapper.Invoke(this, new ConsumerTagChangedAfterRecoveryEventArgs(oldTag, newTag)); + } + finally + { + await _recordedEntitiesSemaphore.WaitAsync() + .ConfigureAwait(false); + } } } catch (Exception ex) @@ -330,7 +508,16 @@ internal void RecoverConsumers(AutorecoveringChannel channelToRecover, IChannel if (_config.TopologyRecoveryExceptionHandler.ConsumerRecoveryExceptionHandler != null && _config.TopologyRecoveryExceptionHandler.ConsumerRecoveryExceptionCondition(consumer, ex)) { - _config.TopologyRecoveryExceptionHandler.ConsumerRecoveryExceptionHandler(consumer, ex, this); + try + { + _recordedEntitiesSemaphore.Release(); + _config.TopologyRecoveryExceptionHandler.ConsumerRecoveryExceptionHandler(consumer, ex, this); + } + finally + { + await _recordedEntitiesSemaphore.WaitAsync() + .ConfigureAwait(false); + } } else { @@ -338,16 +525,27 @@ internal void RecoverConsumers(AutorecoveringChannel channelToRecover, IChannel } } } + + void UpdateConsumer(string oldTag, string newTag, in RecordedConsumer consumer) + { + // make sure server-generated tags are re-added + _recordedConsumers.Remove(oldTag); + _recordedConsumers.Add(newTag, consumer); + } } - private void RecoverChannelsAndItsConsumers() + private async ValueTask RecoverChannelsAndItsConsumersAsync(bool recordedEntitiesSemaphoreHeld = false) { - lock (_channels) + if (false == recordedEntitiesSemaphoreHeld) { - foreach (AutorecoveringChannel m in _channels) - { - m.AutomaticallyRecover(this, _config.TopologyRecoveryEnabled); - } + throw new InvalidOperationException("recordedEntitiesSemaphore must be held"); + } + + foreach (AutorecoveringChannel channel in _channels) + { + await channel.AutomaticallyRecoverAsync(this, _config.TopologyRecoveryEnabled, + recordedEntitiesSemaphoreHeld: recordedEntitiesSemaphoreHeld) + .ConfigureAwait(false); } } @@ -361,23 +559,22 @@ public RecoveryChannelFactory(IConnection connection) _connection = connection; } - public IChannel RecoveryChannel + public async ValueTask CreateRecoveryChannelAsync() { - get + if (_recoveryChannel == null) { - if (_recoveryChannel == null) - { - _recoveryChannel = _connection.CreateChannel(); - } - - if (_recoveryChannel.IsClosed) - { - _recoveryChannel.Dispose(); - _recoveryChannel = _connection.CreateChannel(); - } + _recoveryChannel = await _connection.CreateChannelAsync() + .ConfigureAwait(false); + } - return _recoveryChannel; + if (_recoveryChannel.IsClosed) + { + _recoveryChannel.Dispose(); + _recoveryChannel = await _connection.CreateChannelAsync() + .ConfigureAwait(false); } + + return _recoveryChannel; } public void Dispose() diff --git a/projects/RabbitMQ.Client/client/impl/AutorecoveringConnection.cs b/projects/RabbitMQ.Client/client/impl/AutorecoveringConnection.cs index 625ba1b2d4..8746730a1f 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; @@ -72,11 +73,24 @@ public AutorecoveringConnection(ConnectionConfig config, IEndpointResolver endpo _recoverySucceededWrapper = new EventingWrapper("OnConnectionRecovery", onException); _connectionRecoveryErrorWrapper = new EventingWrapper("OnConnectionRecoveryError", onException); _consumerTagChangeAfterRecoveryWrapper = new EventingWrapper("OnConsumerRecovery", onException); - _queueNameChangeAfterRecoveryWrapper = new EventingWrapper("OnQueueRecovery", onException); + _queueNameChangedAfterRecoveryWrapper = new EventingWrapper("OnQueueRecovery", onException); ConnectionShutdown += HandleConnectionShutdown; } + internal IConnection Open() + { + InnerConnection.Open(); + return this; + } + + internal async ValueTask OpenAsync() + { + await InnerConnection.OpenAsync() + .ConfigureAwait(false); + return this; + } + public event EventHandler RecoverySucceeded { add => _recoverySucceededWrapper.AddHandler(value); @@ -122,12 +136,12 @@ public AutorecoveringConnection(ConnectionConfig config, IEndpointResolver endpo } private EventingWrapper _consumerTagChangeAfterRecoveryWrapper; - public event EventHandler QueueNameChangeAfterRecovery + public event EventHandler QueueNameChangedAfterRecovery { - add => _queueNameChangeAfterRecoveryWrapper.AddHandler(value); - remove => _queueNameChangeAfterRecoveryWrapper.RemoveHandler(value); + add => _queueNameChangedAfterRecoveryWrapper.AddHandler(value); + remove => _queueNameChangedAfterRecoveryWrapper.RemoveHandler(value); } - private EventingWrapper _queueNameChangeAfterRecoveryWrapper; + private EventingWrapper _queueNameChangedAfterRecoveryWrapper; public event EventHandler RecoveringConsumer { @@ -158,7 +172,7 @@ public AutorecoveringConnection(ConnectionConfig config, IEndpointResolver endpo public IDictionary ServerProperties => InnerConnection.ServerProperties; - public IList ShutdownReport => InnerConnection.ShutdownReport; + public IEnumerable ShutdownReport => InnerConnection.ShutdownReport; public IProtocol Protocol => Endpoint.Protocol; @@ -170,10 +184,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) @@ -194,12 +218,37 @@ public void Close(ushort reasonCode, string reasonText, TimeSpan timeout, bool a } } + ///Asynchronous API-side invocation of connection.close with timeout. + public async ValueTask CloseAsync(ushort reasonCode, string reasonText, TimeSpan timeout, bool abort) + { + ThrowIfDisposed(); + await StopRecoveryLoopAsync() + .ConfigureAwait(false); + if (_innerConnection.IsOpen) + { + await _innerConnection.CloseAsync(reasonCode, reasonText, timeout, abort) + .ConfigureAwait(false); + } + } + public IChannel CreateChannel() { EnsureIsOpen(); - AutorecoveringChannel m = new AutorecoveringChannel(this, CreateNonRecoveringChannel()); - RecordChannel(m); - return m; + RecoveryAwareChannel recoveryAwareChannel = CreateNonRecoveringChannel(); + AutorecoveringChannel channel = new AutorecoveringChannel(this, recoveryAwareChannel); + RecordChannel(channel); + return channel; + } + + public async ValueTask CreateChannelAsync() + { + EnsureIsOpen(); + RecoveryAwareChannel recoveryAwareChannel = await CreateNonRecoveringChannelAsync() + .ConfigureAwait(false); + AutorecoveringChannel channel = new AutorecoveringChannel(this, recoveryAwareChannel); + await RecordChannelAsync(channel, channelsSemaphoreHeld: false) + .ConfigureAwait(false); + return channel; } public void Dispose() @@ -222,6 +271,8 @@ public void Dispose() _channels.Clear(); _innerConnection = null; _disposed = true; + _recordedEntitiesSemaphore.Dispose(); + _channelsSemaphore.Dispose(); } } diff --git a/projects/RabbitMQ.Client/client/impl/ChannelBase.cs b/projects/RabbitMQ.Client/client/impl/ChannelBase.cs index decf6ac84f..624fee7b7c 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); @@ -64,19 +64,18 @@ internal abstract class ChannelBase : IChannel, IRecoverable private ShutdownEventArgs _closeReason; public ShutdownEventArgs CloseReason => Volatile.Read(ref _closeReason); - internal IConsumerDispatcher ConsumerDispatcher { get; } + internal readonly IConsumerDispatcher ConsumerDispatcher; protected ChannelBase(ConnectionConfig config, ISession session) { ContinuationTimeout = config.ContinuationTimeout; ConsumerDispatcher = config.DispatchConsumersAsync ? - (IConsumerDispatcher)new AsyncConsumerDispatcher(this, config.DispatchConsumerConcurrency) : + new AsyncConsumerDispatcher(this, config.DispatchConsumerConcurrency) : new ConsumerDispatcher(this, config.DispatchConsumerConcurrency); 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 @@ protected ChannelBase(ConnectionConfig config, ISession session) } 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,24 +184,80 @@ 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); + _Private_ChannelClose(reason.ReplyCode, reason.ReplyText, reason.ClassId, reason.MethodId); } k.Wait(TimeSpan.FromMilliseconds(10000)); - await ConsumerDispatcher.WaitForShutdownAsync().ConfigureAwait(false); + + ConsumerDispatcher.WaitForShutdown(); + } + catch (AlreadyClosedException) + { + if (!abort) + { + throw; + } + } + catch (IOException) + { + if (!abort) + { + throw; + } + } + catch (Exception) + { + if (!abort) + { + throw; + } + } + finally + { + ChannelShutdown -= k.OnConnectionShutdown; + } + } + + public ValueTask CloseAsync(ushort replyCode, string replyText, bool abort) + { + var args = new ShutdownEventArgs(ShutdownInitiator.Application, replyCode, replyText); + return CloseAsync(args, abort); + } + + public async ValueTask CloseAsync(ShutdownEventArgs args, bool abort) + { + using var k = new ChannelCloseAsyncRpcContinuation(ContinuationTimeout); + await _rpcSemaphore.WaitAsync() + .ConfigureAwait(false); + try + { + ChannelShutdown += k.OnConnectionShutdown; + Enqueue(k); + ConsumerDispatcher.Quiesce(); + + if (SetCloseReason(args)) + { + var method = new ChannelClose( + args.ReplyCode, args.ReplyText, args.ClassId, args.MethodId); + await ModelSendAsync(method) + .ConfigureAwait(false); + } + + bool result = await k; + Debug.Assert(result); + + await ConsumerDispatcher.WaitForShutdownAsync() + .ConfigureAwait(false); } catch (AlreadyClosedException) { @@ -235,25 +282,33 @@ private async Task CloseAsync(ShutdownEventArgs reason, bool abort) } finally { + _rpcSemaphore.Release(); ChannelShutdown -= k.OnConnectionShutdown; } } internal async ValueTask ConnectionOpenAsync(string virtualHost) { - await _Private_ConnectionOpenAsync(virtualHost).TimeoutAfter(HandshakeContinuationTimeout); + var m = new ConnectionOpen(virtualHost); + await ModelSendAsync(m) + .TimeoutAfter(HandshakeContinuationTimeout) + .ConfigureAwait(false); } internal async ValueTask ConnectionSecureOkAsync(byte[] response) { - var k = new ConnectionSecureOrTuneContinuation(); - await _rpcSemaphore.WaitAsync().ConfigureAwait(false); - Enqueue(k); + await _rpcSemaphore.WaitAsync() + .ConfigureAwait(false); try { + using var k = new ConnectionSecureOrTuneAsyncRpcContinuation(ContinuationTimeout); + Enqueue(k); + try { - _Private_ConnectionSecureOk(response); + var method = new ConnectionSecureOk(response); + await ModelSendAsync(method) + .ConfigureAwait(false); } catch (AlreadyClosedException) { @@ -273,14 +328,18 @@ 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); + await _rpcSemaphore.WaitAsync() + .ConfigureAwait(false); try { + using var k = new ConnectionSecureOrTuneAsyncRpcContinuation(ContinuationTimeout); + Enqueue(k); + try { - _Private_ConnectionStartOk(clientProperties, mechanism, response, locale); + var method = new ConnectionStartOk(clientProperties, mechanism, response, locale); + await ModelSendAsync(method) + .ConfigureAwait(false); } catch (AlreadyClosedException) { @@ -311,6 +370,29 @@ 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 +417,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)] @@ -463,12 +548,13 @@ private void OnSessionShutdown(object sender, ShutdownEventArgs reason) ConsumerDispatcher.Quiesce(); SetCloseReason(reason); OnChannelShutdown(reason); - ConsumerDispatcher.ShutdownAsync(reason).GetAwaiter().GetResult(); + ConsumerDispatcher.Shutdown(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 +571,8 @@ protected virtual void Dispose(bool disposing) { // dispose managed resources this.Abort(); + ConsumerDispatcher.Dispose(); + _rpcSemaphore.Dispose(); } // dispose unmanaged resources @@ -494,37 +582,41 @@ 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(ack._deliveryTag, 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( + nack._deliveryTag, nack._multiple, 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 +666,352 @@ 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.ToArray()); + k.HandleCommand(IncomingCommand.Empty); // release the continuation. + return true; + } + finally + { + // Note: since we copy the body buffer above, 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 bool HandleBasicGetEmpty(in IncomingCommand cmd) { - var k = (SimpleBlockingRpcContinuation)_continuationQueue.Next(); - _basicRecoverOkWrapper.Invoke(this, EventArgs.Empty); - k.HandleCommand(IncomingCommand.Empty); + if (_continuationQueue.TryPeek(out var k)) + { + try + { + _continuationQueue.Next(); + k.m_result = null; + k.HandleCommand(IncomingCommand.Empty); // release the continuation. + return true; + } + finally + { + cmd.ReturnBuffers(); + } + } + else + { + return false; + } } 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.Memory); + _basicReturnWrapper.Invoke(this, e); + } + } + finally + { + // Note: we can return all the buffers here since the event has been invoked and has returned + 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 = (ConnectionSecureOrTuneAsyncRpcContinuation)_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 = (ConnectionSecureOrTuneAsyncRpcContinuation)_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 +1025,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(); @@ -829,19 +1037,11 @@ protected bool HandleQueueDeclareOk(in IncomingCommand cmd) public abstract void _Private_ConnectionCloseOk(); - public abstract void _Private_ConnectionOpen(string virtualHost); + public abstract void _Private_UpdateSecret(byte[] @newSecret, string @reason); - public abstract ValueTask _Private_ConnectionOpenAsync(string virtualHost); + public abstract void _Private_ExchangeBind(string destination, string source, string routingKey, bool nowait, IDictionary arguments); - public abstract void _Private_ConnectionSecureOk(byte[] response); - - public abstract void _Private_ConnectionStartOk(IDictionary clientProperties, string mechanism, byte[] response, string locale); - - public abstract void _Private_UpdateSecret(byte[] @newSecret, string @reason); - - public abstract void _Private_ExchangeBind(string destination, string source, string routingKey, bool nowait, IDictionary arguments); - - public abstract void _Private_ExchangeDeclare(string exchange, string type, bool passive, bool durable, bool autoDelete, bool @internal, bool nowait, IDictionary arguments); + public abstract void _Private_ExchangeDeclare(string exchange, string type, bool passive, bool durable, bool autoDelete, bool @internal, bool nowait, IDictionary arguments); public abstract void _Private_ExchangeDelete(string exchange, bool ifUnused, bool nowait); @@ -857,10 +1057,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 +1075,37 @@ public void BasicCancel(string consumerTag) } } + public async ValueTask BasicCancelAsync(string 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 +1117,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 +1125,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 +1138,38 @@ 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) + { + // TODO: Replace with flag + 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 +1189,31 @@ 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 +1291,22 @@ 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 +1314,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 +1329,64 @@ 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 +1397,29 @@ 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 +1435,29 @@ 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 +1468,29 @@ 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 +1508,56 @@ 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 +1567,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 +1587,27 @@ 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 +1618,127 @@ 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) @@ -1207,7 +1792,8 @@ private async Task WaitForConfirmsWithTokenAsync(TaskCompletionSource arguments) + private QueueDeclareOk DoQueueDeclare(string queue, bool passive, bool durable, bool exclusive, bool autoDelete, IDictionary arguments) { var k = new QueueDeclareRpcContinuation(); @@ -1257,71 +1849,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..b1a640b4e4 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) { @@ -136,7 +140,7 @@ private bool ParseHeaderFrame(in InboundFrame frame) } ReadOnlySpan span = frame.Payload.Span; - var classId = NetworkOrderDeserializer.ReadUInt16(span); + ushort classId = NetworkOrderDeserializer.ReadUInt16(span); if (classId != ClassConstants.Basic) { throw new UnknownClassOrMethodException(classId, 0); @@ -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..d277026d76 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,20 +69,14 @@ 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); _channel0.m_connectionStartCell = connectionStartCell; _channel0.HandshakeContinuationTimeout = _config.HandshakeContinuationTimeout; _frameHandler.ReadTimeout = _config.HandshakeContinuationTimeout; - await _frameHandler.SendHeaderAsync().ConfigureAwait(false); + await _frameHandler.SendProtocolHeaderAsync() + .ConfigureAwait(false); ConnectionStartDetails connectionStart = await connectionStartCell.Task.ConfigureAwait(false); if (connectionStart is null) @@ -123,7 +116,8 @@ private async ValueTask StartAndTuneAsync() } else { - res = await _channel0.ConnectionSecureOkAsync(response).ConfigureAwait(false); + res = await _channel0.ConnectionSecureOkAsync(response) + .ConfigureAwait(false); } if (res.m_challenge is null) diff --git a/projects/RabbitMQ.Client/client/impl/Connection.Receive.cs b/projects/RabbitMQ.Client/client/impl/Connection.Receive.cs index 52bb68d2ec..777750fd9f 100644 --- a/projects/RabbitMQ.Client/client/impl/Connection.Receive.cs +++ b/projects/RabbitMQ.Client/client/impl/Connection.Receive.cs @@ -47,7 +47,8 @@ private async Task MainLoop() { try { - await ReceiveLoop().ConfigureAwait(false); + await ReceiveLoop() + .ConfigureAwait(false); } catch (EndOfStreamException eose) { @@ -56,7 +57,8 @@ private async Task MainLoop() } catch (HardProtocolException hpe) { - await HardProtocolExceptionHandler(hpe).ConfigureAwait(false); + await HardProtocolExceptionHandler(hpe) + .ConfigureAwait(false); } catch (Exception ex) { @@ -144,6 +146,7 @@ private void HandleMainLoopException(ShutdownEventArgs reason) { if (!SetCloseReason(reason)) { + // TODO 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..cdb379bc79 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; @@ -116,7 +100,7 @@ public Connection(ConnectionConfig config, IFrameHandler frameHandler) public IDictionary? ServerProperties { get; private set; } - public IList ShutdownReport => _shutdownReport; + public IEnumerable ShutdownReport => _shutdownReport; private ShutdownReportEntry[] _shutdownReport = Array.Empty(); ///Explicit implementation of IConnection.Protocol. @@ -213,7 +197,7 @@ internal IFrameHandler FrameHandler /// /// This event is never fired by non-recovering connections but it is a part of the interface. /// - public event EventHandler QueueNameChangeAfterRecovery + public event EventHandler QueueNameChangedAfterRecovery { add { } remove { } @@ -227,6 +211,31 @@ internal void TakeOver(Connection other) _connectionShutdownWrapper.Takeover(other._connectionShutdownWrapper); } + internal IConnection Open() + { + return OpenAsync() + .ConfigureAwait(false).GetAwaiter().GetResult(); + } + + internal async ValueTask OpenAsync() + { + try + { + RabbitMqClientEventSource.Log.ConnectionOpened(); + await StartAndTuneAsync() + .ConfigureAwait(false); + await _channel0.ConnectionOpenAsync(_config.VirtualHost) + .ConfigureAwait(false); + return this; + } + catch + { + var ea = new ShutdownEventArgs(ShutdownInitiator.Library, Constants.InternalError, "FailedOpen"); + await CloseAsync(ea, true, TimeSpan.FromSeconds(5)); + throw; + } + } + public IChannel CreateChannel() { EnsureIsOpen(); @@ -236,6 +245,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(); @@ -262,6 +279,12 @@ public void Close(ushort reasonCode, string reasonText, TimeSpan timeout, bool a Close(new ShutdownEventArgs(ShutdownInitiator.Application, reasonCode, reasonText), abort, timeout); } + ///Asynchronous API-side invocation of connection.close with timeout. + public ValueTask CloseAsync(ushort reasonCode, string reasonText, TimeSpan timeout, bool abort) + { + return CloseAsync(new ShutdownEventArgs(ShutdownInitiator.Application, reasonCode, reasonText), abort, timeout); + } + ///Try to close connection in a graceful way /// /// @@ -306,13 +329,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) @@ -340,9 +361,104 @@ internal void Close(ShutdownEventArgs reason, bool abort, TimeSpan timeout) _frameHandler.Close(); } } + catch (AggregateException) // TODO this could be more than just a timeout + { + } + } + + ///Asychronously try to close connection in a graceful way + /// + /// + ///Shutdown reason contains code and text assigned when closing the connection, + ///as well as the information about what initiated the close + /// + /// + ///Abort flag, if true, signals to close the ongoing connection immediately + ///and do not report any errors if it was already closed. + /// + /// + ///Timeout determines how much time internal close operations should be given + ///to complete. + /// + /// + internal async ValueTask CloseAsync(ShutdownEventArgs reason, bool abort, TimeSpan timeout) + { + // TODO CloseAsync and Close share a lot of code + if (!SetCloseReason(reason)) + { + if (!abort) + { + ThrowAlreadyClosedException(CloseReason!); + } + } + else + { + OnShutdown(reason); + _session0.SetSessionClosing(false); + + try + { + // Try to send connection.close wait for CloseOk in the MainLoop + if (!_closed) + { + var method = new ConnectionClose(reason.ReplyCode, reason.ReplyText, 0, 0); + await _session0.TransmitAsync(method) + .ConfigureAwait(false); + } + } + catch (AlreadyClosedException) + { + if (!abort) + { + throw; + } + } + catch (NotSupportedException) + { + // buffered stream had unread data in it and Flush() + // was called, ignore to not confuse the user + } + catch (IOException ioe) + { + if (_channel0.CloseReason is null) + { + if (!abort) + { + throw; + } + else + { + LogCloseError("Couldn't close connection cleanly. Socket closed unexpectedly", ioe); + } + } + } + finally + { + TerminateMainloop(); + } + } + + try + { + await _mainLoopTask.TimeoutAfter(timeout) + .ConfigureAwait(false); + } + catch (TimeoutException) + { + } catch (AggregateException) { - _frameHandler.Close(); + } + finally + { + try + { + await _frameHandler.CloseAsync() + .ConfigureAwait(false); + } + catch + { + } } } @@ -363,7 +479,7 @@ internal void InternalClose(ShutdownEventArgs reason) } // Only call at the end of the Mainloop or HeartbeatLoop - + // TODO async private void FinishClose() { _closed = true; @@ -384,7 +500,7 @@ private void OnShutdown(ShutdownEventArgs reason) private bool SetCloseReason(ShutdownEventArgs reason) { - return System.Threading.Interlocked.CompareExchange(ref _closeReason, reason, null) is null; + return Interlocked.CompareExchange(ref _closeReason, reason, null) is null; } private void LogCloseError(string error, Exception ex) @@ -405,18 +521,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); + ValueTask task = _frameHandler.WriteAsync(frames); if (!task.IsCompletedSuccessfully) { - task.AsTask().GetAwaiter().GetResult(); + task.ConfigureAwait(false).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..761e6d23ea 100644 --- a/projects/RabbitMQ.Client/client/impl/ConsumerDispatching/AsyncConsumerDispatcher.cs +++ b/projects/RabbitMQ.Client/client/impl/ConsumerDispatching/AsyncConsumerDispatcher.cs @@ -1,5 +1,5 @@ using System; -using System.Buffers; +using System.Threading; using System.Threading.Tasks; using RabbitMQ.Client.Events; using RabbitMQ.Client.Impl; @@ -9,39 +9,42 @@ namespace RabbitMQ.Client.ConsumerDispatching #nullable enable internal sealed class AsyncConsumerDispatcher : ConsumerDispatcherChannelBase { - public AsyncConsumerDispatcher(ChannelBase channel, int concurrency) + internal AsyncConsumerDispatcher(ChannelBase channel, int concurrency) : base(channel, concurrency) { } - protected override async Task ProcessChannelAsync() + protected override async Task ProcessChannelAsync(CancellationToken token) { - while (await _reader.WaitToReadAsync().ConfigureAwait(false)) + while (await _reader.WaitToReadAsync(token).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.Memory), + + 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..380d95be6c 100644 --- a/projects/RabbitMQ.Client/client/impl/ConsumerDispatching/ConsumerDispatcher.cs +++ b/projects/RabbitMQ.Client/client/impl/ConsumerDispatching/ConsumerDispatcher.cs @@ -1,5 +1,5 @@ using System; -using System.Buffers; +using System.Threading; using System.Threading.Tasks; using RabbitMQ.Client.Events; using RabbitMQ.Client.Impl; @@ -9,49 +9,47 @@ namespace RabbitMQ.Client.ConsumerDispatching #nullable enable internal sealed class ConsumerDispatcher : ConsumerDispatcherChannelBase { - public ConsumerDispatcher(ChannelBase channel, int concurrency) + internal ConsumerDispatcher(ChannelBase channel, int concurrency) : base(channel, concurrency) { } - protected override async Task ProcessChannelAsync() + protected override async Task ProcessChannelAsync(CancellationToken token) { - while (await _reader.WaitToReadAsync().ConfigureAwait(false)) + while (await _reader.WaitToReadAsync(token).ConfigureAwait(false)) { 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; + IBasicConsumer consumer = work.Consumer; + string? consumerTag = work.ConsumerTag; + switch (work.WorkType) + { + case WorkType.Deliver: + consumer.HandleBasicDeliver( + consumerTag, work.DeliveryTag, work.Redelivered, + work.Exchange, work.RoutingKey, work.BasicProperties, work.Body.Memory); + 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/ConsumerDispatcherBase.cs b/projects/RabbitMQ.Client/client/impl/ConsumerDispatching/ConsumerDispatcherBase.cs index c6b90f4287..d61a783544 100644 --- a/projects/RabbitMQ.Client/client/impl/ConsumerDispatching/ConsumerDispatcherBase.cs +++ b/projects/RabbitMQ.Client/client/impl/ConsumerDispatching/ConsumerDispatcherBase.cs @@ -7,14 +7,13 @@ namespace RabbitMQ.Client.ConsumerDispatching #nullable enable internal abstract class ConsumerDispatcherBase { - private static readonly FallbackConsumer fallbackConsumer = new FallbackConsumer(); - private readonly Dictionary _consumers; + private static readonly FallbackConsumer fallbackConsumer = new(); + private readonly Dictionary _consumers = new(); public IBasicConsumer? DefaultConsumer { get; set; } protected ConsumerDispatcherBase() { - _consumers = new Dictionary(); } protected void AddConsumer(IBasicConsumer consumer, string tag) @@ -41,7 +40,19 @@ public IBasicConsumer GetAndRemoveConsumer(string tag) } } + public void Shutdown(ShutdownEventArgs reason) + { + DoShutdownConsumers(reason); + InternalShutdown(); + } + public Task ShutdownAsync(ShutdownEventArgs reason) + { + DoShutdownConsumers(reason); + return InternalShutdownAsync(); + } + + private void DoShutdownConsumers(ShutdownEventArgs reason) { lock (_consumers) { @@ -51,12 +62,12 @@ public Task ShutdownAsync(ShutdownEventArgs reason) } _consumers.Clear(); } - - return InternalShutdownAsync(); } protected abstract void ShutdownConsumer(IBasicConsumer consumer, ShutdownEventArgs reason); + protected abstract void InternalShutdown(); + protected abstract Task InternalShutdownAsync(); // Do not inline as it's not the default case on a hot path diff --git a/projects/RabbitMQ.Client/client/impl/ConsumerDispatching/ConsumerDispatcherChannelBase.cs b/projects/RabbitMQ.Client/client/impl/ConsumerDispatching/ConsumerDispatcherChannelBase.cs index f53218e6bd..06682b2c6c 100644 --- a/projects/RabbitMQ.Client/client/impl/ConsumerDispatching/ConsumerDispatcherChannelBase.cs +++ b/projects/RabbitMQ.Client/client/impl/ConsumerDispatching/ConsumerDispatcherChannelBase.cs @@ -1,23 +1,31 @@ using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; using System.Threading.Channels; using System.Threading.Tasks; using RabbitMQ.Client.Impl; +using RabbitMQ.Client.Logging; namespace RabbitMQ.Client.ConsumerDispatching { #nullable enable internal abstract class ConsumerDispatcherChannelBase : ConsumerDispatcherBase, IConsumerDispatcher { + protected readonly CancellationTokenSource _consumerDispatcherCts = new(); + protected readonly CancellationToken _consumerDispatcherToken; + protected readonly ChannelBase _channel; protected readonly ChannelReader _reader; private readonly ChannelWriter _writer; private readonly Task _worker; + private bool _quiesce = false; + private bool _disposed; - public bool IsShutdown { get; private set; } - - protected ConsumerDispatcherChannelBase(ChannelBase channel, int concurrency) + internal ConsumerDispatcherChannelBase(ChannelBase channel, int concurrency) { + _consumerDispatcherToken = _consumerDispatcherCts.Token; _channel = channel; var workChannel = Channel.CreateUnbounded(new UnboundedChannelOptions { @@ -28,25 +36,34 @@ protected ConsumerDispatcherChannelBase(ChannelBase channel, int concurrency) _reader = workChannel.Reader; _writer = workChannel.Writer; - Func loopStart = ProcessChannelAsync; + Func loopStart = + () => ProcessChannelAsync(_consumerDispatcherToken); if (concurrency == 1) { - _worker = Task.Run(loopStart); + _worker = Task.Run(loopStart, _consumerDispatcherToken); } else { var tasks = new Task[concurrency]; for (int i = 0; i < concurrency; i++) { - tasks[i] = Task.Run(loopStart); + tasks[i] = Task.Run(loopStart, _consumerDispatcherToken); } _worker = Task.WhenAll(tasks); } } + public bool IsShutdown + { + get + { + return _quiesce; + } + } + public void HandleBasicConsumeOk(IBasicConsumer consumer, string consumerTag) { - if (!IsShutdown) + if (false == _disposed && false == _quiesce) { AddConsumer(consumer, consumerTag); _writer.TryWrite(new WorkStruct(WorkType.ConsumeOk, consumer, consumerTag)); @@ -54,17 +71,17 @@ 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) + if (false == _disposed && false == _quiesce) { - _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)); } } public void HandleBasicCancelOk(string consumerTag) { - if (!IsShutdown) + if (false == _disposed && false == _quiesce) { _writer.TryWrite(new WorkStruct(WorkType.CancelOk, GetAndRemoveConsumer(consumerTag), consumerTag)); } @@ -72,36 +89,130 @@ public void HandleBasicCancelOk(string consumerTag) public void HandleBasicCancel(string consumerTag) { - if (!IsShutdown) + if (false == _disposed && false == _quiesce) { _writer.TryWrite(new WorkStruct(WorkType.Cancel, GetAndRemoveConsumer(consumerTag), consumerTag)); } } - protected sealed override void ShutdownConsumer(IBasicConsumer consumer, ShutdownEventArgs reason) + public void Quiesce() { - _writer.TryWrite(new WorkStruct(consumer, reason)); + _quiesce = true; } - public void Quiesce() + private bool IsCancellationRequested { - IsShutdown = true; + get + { + try + { + return _consumerDispatcherCts.IsCancellationRequested; + } + catch (ObjectDisposedException) + { + return true; + } + } } - protected override Task InternalShutdownAsync() + public void WaitForShutdown() + { + if (_disposed) + { + return; + } + + if (_quiesce) + { + if (IsCancellationRequested) + { + try + { + if (false == _reader.Completion.Wait(TimeSpan.FromSeconds(2))) + { + ESLog.Warn("consumer dispatcher did not shut down in a timely fashion (sync)"); + } + if (false == _worker.Wait(TimeSpan.FromSeconds(2))) + { + ESLog.Warn("consumer dispatcher did not shut down in a timely fashion (sync)"); + } + } + catch (AggregateException aex) + { + AggregateException aexf = aex.Flatten(); + IEnumerable nonTaskCanceled = aexf.InnerExceptions.Where(iex => iex is not TaskCanceledException); + if (nonTaskCanceled.Any()) + { + ESLog.Warn("consumer dispatcher task had unexpected exceptions"); + } + } + catch (TaskCanceledException) + { + } + } + } + else + { + throw new InvalidOperationException("WaitForShutdown called but _quiesce is false"); + } + } + + public async Task WaitForShutdownAsync() + { + if (_disposed) + { + return; + } + + if (_quiesce) + { + try + { + await _reader.Completion + .ConfigureAwait(false); + await _worker + .ConfigureAwait(false); + } + catch (AggregateException aex) + { + AggregateException aexf = aex.Flatten(); + IEnumerable nonTaskCanceled = aexf.InnerExceptions.Where(iex => iex is not TaskCanceledException); + if (nonTaskCanceled.Any()) + { + ESLog.Warn("consumer dispatcher task had unexpected exceptions (async)"); + } + } + catch (TaskCanceledException) + { + } + } + else + { + throw new InvalidOperationException("WaitForShutdownAsync called but _quiesce is false"); + } + } + + protected sealed override void ShutdownConsumer(IBasicConsumer consumer, ShutdownEventArgs reason) + { + _writer.TryWrite(new WorkStruct(consumer, reason)); + } + + protected override void InternalShutdown() { _writer.Complete(); - return _worker; + CancelConsumerDispatcherCts(); } - public Task WaitForShutdownAsync() + protected override Task InternalShutdownAsync() { + _writer.Complete(); + CancelConsumerDispatcherCts(); return _worker; } - protected abstract Task ProcessChannelAsync(); + protected abstract Task ProcessChannelAsync(CancellationToken token); - protected readonly struct WorkStruct + protected readonly struct WorkStruct : IDisposable { public readonly IBasicConsumer Consumer; public IAsyncBasicConsumer AsyncConsumer => (IAsyncBasicConsumer)Consumer; @@ -111,8 +222,7 @@ public Task WaitForShutdownAsync() 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 +243,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 +254,10 @@ public WorkStruct(IBasicConsumer consumer, ShutdownEventArgs reason) RoutingKey = routingKey; BasicProperties = basicProperties; Body = body; - RentedArray = rentedArray; Reason = default; } + + public void Dispose() => Body.Dispose(); } protected enum WorkType : byte @@ -157,5 +268,47 @@ protected enum WorkType : byte Deliver, ConsumeOk } + + protected void CancelConsumerDispatcherCts() + { + try + { + _consumerDispatcherCts.Cancel(); + } + catch (ObjectDisposedException) + { + } + } + + protected virtual void Dispose(bool disposing) + { + if (!_disposed) + { + try + { + if (disposing) + { + Quiesce(); + CancelConsumerDispatcherCts(); + _consumerDispatcherCts.Dispose(); + } + } + catch + { + // CHOMP + } + finally + { + _disposed = true; + } + } + } + + public void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: true); + GC.SuppressFinalize(this); + } } } diff --git a/projects/RabbitMQ.Client/client/impl/ConsumerDispatching/FallbackConsumer.cs b/projects/RabbitMQ.Client/client/impl/ConsumerDispatching/FallbackConsumer.cs index 3510be5e5d..b901a4ae71 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, ReadOnlyMemory 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, ReadOnlyMemory 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..faa44499e0 100644 --- a/projects/RabbitMQ.Client/client/impl/ConsumerDispatching/IConsumerDispatcher.cs +++ b/projects/RabbitMQ.Client/client/impl/ConsumerDispatching/IConsumerDispatcher.cs @@ -35,7 +35,7 @@ namespace RabbitMQ.Client.ConsumerDispatching { #nullable enable - internal interface IConsumerDispatcher + internal interface IConsumerDispatcher : IDisposable { IBasicConsumer? DefaultConsumer { get; set; } @@ -51,8 +51,7 @@ internal interface IConsumerDispatcher string exchange, string routingKey, in ReadOnlyBasicProperties basicProperties, - ReadOnlyMemory body, - byte[] rentedArray); + RentedMemory body); void HandleBasicCancelOk(string consumerTag); @@ -60,8 +59,10 @@ internal interface IConsumerDispatcher void Quiesce(); - Task ShutdownAsync(ShutdownEventArgs reason); + void Shutdown(ShutdownEventArgs reason); + void WaitForShutdown(); + Task ShutdownAsync(ShutdownEventArgs reason); Task WaitForShutdownAsync(); } } diff --git a/projects/RabbitMQ.Client/client/impl/EventingWrapper.cs b/projects/RabbitMQ.Client/client/impl/EventingWrapper.cs index 2395c3ce73..b716af5ff2 100644 --- a/projects/RabbitMQ.Client/client/impl/EventingWrapper.cs +++ b/projects/RabbitMQ.Client/client/impl/EventingWrapper.cs @@ -128,7 +128,8 @@ private static async Task InternalInvoke(Delegate[] handlers, object sender, T p { foreach (AsyncEventHandler action in handlers) { - await action(sender, parameter).ConfigureAwait(false); + await action(sender, parameter) + .ConfigureAwait(false); } } diff --git a/projects/RabbitMQ.Client/client/impl/Frame.cs b/projects/RabbitMQ.Client/client/impl/Frame.cs index 08f4c351b5..ad5eab4c19 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(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(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 T method, ushort cha 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 T method, ushort cha } 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(mem, array); } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -231,7 +231,7 @@ private static void ProcessProtocolHeader(ReadOnlySequence buffer) throw new EndOfStreamException(); } - var bufferSpan = buffer.First.Span; + ReadOnlySpan bufferSpan = buffer.First.Span; if (bufferSpan[1] != 'M' || bufferSpan[2] != 'Q' || bufferSpan[3] != 'P') { @@ -312,6 +312,9 @@ internal static bool TryReadFrame(ref ReadOnlySequence buffer, uint maxMes return false; } + // TODO check this? + // buffer.IsSingleSegment; + byte firstByte = buffer.First.Span[0]; if (firstByte == 'A') { @@ -338,19 +341,19 @@ 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); + byte frameEndMarker = payloadBytes[payloadSize]; + ClientArrayPool.Return(payloadBytes); throw new MalformedFrameException($"Bad frame end marker: {frameEndMarker}"); } RabbitMqClientEventSource.Log.DataReceived(payloadSize + Framing.BaseFrameSize); - frame = new InboundFrame(type, channel, new Memory(payloadBytes, 0, payloadSize), payloadBytes); + frame = new InboundFrame(type, channel, new ReadOnlyMemory(payloadBytes, 0, payloadSize), payloadBytes); // Advance the buffer buffer = buffer.Slice(7 + readSize); return true; @@ -364,7 +367,7 @@ public byte[] TakeoverPayload() public void ReturnPayload() { - ArrayPool.Shared.Return(_rentedArray); + ClientArrayPool.Return(_rentedArray); } public override string ToString() @@ -375,7 +378,11 @@ public override string ToString() [MethodImpl(MethodImplOptions.AggressiveInlining)] private static void MaybeThrowEndOfStream(ReadResult result, ReadOnlySequence buffer) { - if (result.IsCompleted || buffer.Length == 0) + // TODO + // https://blog.marcgravell.com/2018/07/pipe-dreams-part-1.html + // Uses && + // if (result.IsCompleted && buffer.IsEmpty) + if (result.IsCompleted || buffer.IsEmpty) { throw new EndOfStreamException("Pipe is completed."); } diff --git a/projects/RabbitMQ.Client/client/impl/IFrameHandler.cs b/projects/RabbitMQ.Client/client/impl/IFrameHandler.cs index fdc3ef7628..5d8686df4e 100644 --- a/projects/RabbitMQ.Client/client/impl/IFrameHandler.cs +++ b/projects/RabbitMQ.Client/client/impl/IFrameHandler.cs @@ -54,6 +54,7 @@ internal interface IFrameHandler TimeSpan WriteTimeout { set; } void Close(); + ValueTask CloseAsync(); ///Read a frame from the underlying ///transport. Returns null if the read operation timed out @@ -65,8 +66,8 @@ internal interface IFrameHandler /// bool TryReadFrame(out InboundFrame frame); - ValueTask SendHeaderAsync(); + ValueTask SendProtocolHeaderAsync(); - 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 @@ namespace RabbitMQ.Client.Impl 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/RecordedBinding.cs b/projects/RabbitMQ.Client/client/impl/RecordedBinding.cs index 0a4eb281e8..1d3ed4221f 100644 --- a/projects/RabbitMQ.Client/client/impl/RecordedBinding.cs +++ b/projects/RabbitMQ.Client/client/impl/RecordedBinding.cs @@ -31,6 +31,7 @@ using System; using System.Collections.Generic; +using System.Threading.Tasks; namespace RabbitMQ.Client.Impl { @@ -66,15 +67,15 @@ public RecordedBinding(string destination, in RecordedBinding old) _arguments = old._arguments; } - public void Recover(IChannel channel) + public ValueTask RecoverAsync(IChannel channel) { if (_isQueueBinding) { - channel.QueueBind(_destination, _source, _routingKey, _arguments); + return channel.QueueBindAsync(_destination, _source, _routingKey, _arguments); } else { - channel.ExchangeBind(_destination, _source, _routingKey, _arguments); + return channel.ExchangeBindAsync(_destination, _source, _routingKey, _arguments); } } diff --git a/projects/RabbitMQ.Client/client/impl/RecordedConsumer.cs b/projects/RabbitMQ.Client/client/impl/RecordedConsumer.cs index a68202091e..6cb909967f 100644 --- a/projects/RabbitMQ.Client/client/impl/RecordedConsumer.cs +++ b/projects/RabbitMQ.Client/client/impl/RecordedConsumer.cs @@ -31,6 +31,7 @@ using System; using System.Collections.Generic; +using System.Threading.Tasks; namespace RabbitMQ.Client.Impl { @@ -104,9 +105,9 @@ public static RecordedConsumer WithNewQueueName(string newQueueName, in Recorded return new RecordedConsumer(old.Channel, old.Consumer, old.ConsumerTag, newQueueName, old.AutoAck, old.Exclusive, old.Arguments); } - public string Recover(IChannel channel) + public ValueTask RecoverAsync(IChannel channel) { - return channel.BasicConsume(Queue, AutoAck, ConsumerTag, false, Exclusive, Arguments, Consumer); + return channel.BasicConsumeAsync(Queue, AutoAck, ConsumerTag, false, Exclusive, Arguments, Consumer); } } } diff --git a/projects/RabbitMQ.Client/client/impl/RecordedExchange.cs b/projects/RabbitMQ.Client/client/impl/RecordedExchange.cs index e1c81ce4ba..d5aeeb646d 100644 --- a/projects/RabbitMQ.Client/client/impl/RecordedExchange.cs +++ b/projects/RabbitMQ.Client/client/impl/RecordedExchange.cs @@ -30,6 +30,7 @@ //--------------------------------------------------------------------------- using System.Collections.Generic; +using System.Threading.Tasks; namespace RabbitMQ.Client.Impl { @@ -57,9 +58,10 @@ public RecordedExchange(string name, string type, bool durable, bool autoDelete, _arguments = arguments; } - public void Recover(IChannel channel) + public ValueTask RecoverAsync(IChannel channel) { - channel.ExchangeDeclare(Name, _type, _durable, AutoDelete, _arguments); + return channel.ExchangeDeclareAsync(exchange: Name, type: _type, passive: false, + durable: _durable, autoDelete: AutoDelete, arguments: _arguments); } public override string ToString() diff --git a/projects/RabbitMQ.Client/client/impl/RecordedQueue.cs b/projects/RabbitMQ.Client/client/impl/RecordedQueue.cs index 74022526c1..43f3f19a9e 100644 --- a/projects/RabbitMQ.Client/client/impl/RecordedQueue.cs +++ b/projects/RabbitMQ.Client/client/impl/RecordedQueue.cs @@ -30,6 +30,7 @@ //--------------------------------------------------------------------------- using System.Collections.Generic; +using System.Threading.Tasks; namespace RabbitMQ.Client.Impl { @@ -70,10 +71,11 @@ public RecordedQueue(string newName, in RecordedQueue old) _arguments = old._arguments; } - public string Recover(IChannel channel) + public ValueTask RecoverAsync(IChannel channel) { - var queueName = IsServerNamed ? string.Empty : Name; - return channel.QueueDeclare(queueName, _durable, _exclusive, AutoDelete, _arguments).QueueName; + string queueName = IsServerNamed ? string.Empty : Name; + return channel.QueueDeclareAsync(queue: queueName, passive: false, + durable: _durable, exclusive: _exclusive, autoDelete: AutoDelete, arguments: _arguments); } public override string ToString() 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 0e91276779..0a2870e42d 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; @@ -44,67 +42,26 @@ namespace RabbitMQ.Client.Impl { - internal static class TaskExtensions - { - public static async Task TimeoutAfter(this Task task, TimeSpan timeout) - { -#if NET6_0_OR_GREATER - await task.WaitAsync(timeout).ConfigureAwait(false); -#else - if (task == await Task.WhenAny(task, Task.Delay(timeout)).ConfigureAwait(false)) - { - await task.ConfigureAwait(false); - } - else - { - Task supressErrorTask = task.ContinueWith((t, s) => t.Exception.Handle(e => true), null, CancellationToken.None, TaskContinuationOptions.OnlyOnFaulted | TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default); - throw new TimeoutException(); - } -#endif - } - - public static async ValueTask TimeoutAfter(this ValueTask task, TimeSpan timeout) - { - if (!task.IsCompletedSuccessfully) - { - var actualTask = task.AsTask(); -#if NET6_0_OR_GREATER - await actualTask.WaitAsync(timeout).ConfigureAwait(false); -#else - if (actualTask == await Task.WhenAny(actualTask, Task.Delay(timeout)).ConfigureAwait(false)) - { - await actualTask.ConfigureAwait(false); - } - else - { - Task supressErrorTask = - actualTask.ContinueWith((t, s) => t.Exception.Handle(e => true), null, CancellationToken.None, TaskContinuationOptions.OnlyOnFaulted | TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default); - throw new TimeoutException(); - } -#endif - } - } - } - internal sealed class SocketFrameHandler : IFrameHandler { private readonly AmqpTcpEndpoint _amqpTcpEndpoint; private readonly ITcpClient _socket; - private readonly ChannelWriter> _channelWriter; - private readonly ChannelReader> _channelReader; + private readonly ChannelWriter _channelWriter; + private readonly ChannelReader _channelReader; private readonly PipeWriter _pipeWriter; private readonly PipeReader _pipeReader; private readonly Task _writerTask; - private readonly object _semaphore = new object(); + private readonly SemaphoreSlim _closingSemaphore = new SemaphoreSlim(1, 1); private bool _closed; - private static ReadOnlySpan ProtocolHeader => new byte[] { (byte)'A', (byte)'M', (byte)'Q', (byte)'P', 0, 0, 9, 1 }; + + private static ReadOnlyMemory Amqp091ProtocolHeader => 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, @@ -112,8 +69,8 @@ internal sealed class SocketFrameHandler : IFrameHandler SingleWriter = false }); - _channelReader = channel.Reader; _channelWriter = channel.Writer; + _channelReader = channel.Reader; // Resolve the hostname to know if it's even possible to even try IPv6 IPAddress[] adds = Dns.GetHostAddresses(endpoint.HostName); @@ -226,40 +183,51 @@ public TimeSpan WriteTimeout public void Close() { - lock (_semaphore) + CloseAsync().ConfigureAwait(false).GetAwaiter().GetResult(); + } + + public async ValueTask CloseAsync() + { + if (_closed || _socket == null) { - if (_closed || _socket == null) - { - return; - } - else + return; + } + + await _closingSemaphore.WaitAsync() + .ConfigureAwait(false); + try + { + try { - try + _channelWriter.Complete(); + if (_writerTask is not null) { - _channelWriter.Complete(); - _writerTask?.GetAwaiter().GetResult(); - _pipeWriter.Complete(); - _pipeReader.Complete(); - } - catch - { - // ignore, we are closing anyway + await _writerTask.ConfigureAwait(false); } + await _pipeWriter.CompleteAsync() + .ConfigureAwait(false); + await _pipeReader.CompleteAsync() + .ConfigureAwait(false); + } + catch + { + // ignore, we are closing anyway + } - try - { - _socket.Close(); - } - catch - { - // ignore, we are closing anyway - } - finally - { - _closed = true; - } + try + { + _socket.Close(); + } + catch + { + // ignore, we are closing anyway } } + finally + { + _closingSemaphore.Release(); + _closed = true; + } } public ValueTask ReadFrameAsync() @@ -272,24 +240,26 @@ public bool TryReadFrame(out InboundFrame frame) return InboundFrame.TryReadFrameFromPipe(_pipeReader, _amqpTcpEndpoint.MaxMessageSize, out frame); } - public async ValueTask SendHeaderAsync() + public async ValueTask SendProtocolHeaderAsync() { - _pipeWriter.Write(ProtocolHeader); - await _pipeWriter.FlushAsync().ConfigureAwait(false); + await _pipeWriter.WriteAsync(Amqp091ProtocolHeader) + .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 +268,18 @@ private async Task WriteLoop() { while (await _channelReader.WaitToReadAsync().ConfigureAwait(false)) { - while (_channelReader.TryRead(out ReadOnlyMemory memory)) + while (_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); + RabbitMqClientEventSource.Log.CommandSent(frames.Size); + } + finally + { + frames.Dispose(); + } } await _pipeWriter.FlushAsync() @@ -353,6 +329,7 @@ private static bool ShouldTryIPv6(AmqpTcpEndpoint endpoint) } } + // TODO async private void ConnectOrFail(ITcpClient socket, IPEndPoint endpoint, TimeSpan timeout) { try diff --git a/projects/RabbitMQ.Client/client/impl/SslHelper.cs b/projects/RabbitMQ.Client/client/impl/SslHelper.cs index beeb0a968a..8f4b2825da 100644 --- a/projects/RabbitMQ.Client/client/impl/SslHelper.cs +++ b/projects/RabbitMQ.Client/client/impl/SslHelper.cs @@ -71,6 +71,7 @@ public static Stream TcpUpgrade(Stream tcpStream, SslOption options) }; try { + // TODO async TryAuthenticating(options); } catch (ArgumentException e) when (e.ParamName == "sslProtocolType" && options.Version == SslProtocols.None) 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..52aa6131d0 --- /dev/null +++ b/projects/Test/Applications/MassPublish/Program.cs @@ -0,0 +1,197 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Net.Sockets; +using System.Threading; +using System.Threading.Tasks; +using RabbitMQ.Client; +using RabbitMQ.Client.Events; + +namespace MassPublish +{ + static class Program + { + const string RmqHost = "localhost"; + + 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 Func s_socketFactory = (AddressFamily af) => + { + var socket = new Socket(af, SocketType.Stream, ProtocolType.Tcp) + { + NoDelay = true, + }; + return new TcpClient(socket); + }; + + static readonly ConnectionFactory s_publishConnectionFactory = new() + { + HostName = RmqHost, + ClientProvidedName = AppId + "-PUBLISH", + }; + + static readonly ConnectionFactory s_consumeConnectionFactory = new() + { + HostName = RmqHost, + 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); + + s_publishConnectionFactory.SocketFactory = s_socketFactory; + s_consumeConnectionFactory.SocketFactory = s_socketFactory; + } + + static async Task Main() + { + using IConnection consumeConnection = await s_consumeConnectionFactory.CreateConnectionAsync(); + consumeConnection.ConnectionShutdown += Connection_ConnectionShutdown; + + using IChannel consumeChannel = await consumeConnection.CreateChannelAsync(); + 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 = await s_publishConnectionFactory.CreateConnectionAsync($"{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 = await publishConnection.CreateChannelAsync(); + 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); + } + + await c.CloseAsync(); + } + } + + 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/Applications/MassPublish/TcpClient.cs b/projects/Test/Applications/MassPublish/TcpClient.cs new file mode 100644 index 0000000000..dab6c12de1 --- /dev/null +++ b/projects/Test/Applications/MassPublish/TcpClient.cs @@ -0,0 +1,101 @@ +using System; +using System.Net; +using System.Net.Sockets; +using System.Threading.Tasks; +using RabbitMQ.Client; + +namespace MassPublish +{ + internal class TcpClient : ITcpClient + { + private Socket _sock; + + public TcpClient(Socket socket) + { + _sock = socket ?? throw new InvalidOperationException("socket must not be null"); + } + + public virtual async Task ConnectAsync(string host, int port) + { + AssertSocket(); + await _sock.ConnectAsync(host, port).ConfigureAwait(false); + } + + public virtual Task ConnectAsync(IPAddress ep, int port) + { + AssertSocket(); + return _sock.ConnectAsync(ep, port); + } + + public virtual void Close() + { + _sock?.Dispose(); + _sock = null; + } + + [Obsolete("Override Dispose(bool) instead.")] + public virtual void Dispose() + { + Dispose(true); + } + + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + // dispose managed resources + Close(); + } + + // dispose unmanaged resources + } + + public virtual NetworkStream GetStream() + { + AssertSocket(); + return new NetworkStream(_sock); + } + + public virtual Socket Client + { + get + { + return _sock; + } + } + + public virtual bool Connected + { + get + { + if (_sock is null) + { + return false; + } + return _sock.Connected; + } + } + + public virtual TimeSpan ReceiveTimeout + { + get + { + AssertSocket(); + return TimeSpan.FromMilliseconds(_sock.ReceiveTimeout); + } + set + { + AssertSocket(); + _sock.ReceiveTimeout = (int)value.TotalMilliseconds; + } + } + + private void AssertSocket() + { + if (_sock is null) + { + throw new InvalidOperationException("Cannot perform operation as socket is null"); + } + } + } +} 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..508b9b96ad --- /dev/null +++ b/projects/Test/AsyncIntegration/AsyncIntegrationFixture.cs @@ -0,0 +1,124 @@ +// 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.Collections.Generic; +using System.Threading.Tasks; +using RabbitMQ.Client; +using Xunit; +using Xunit.Abstractions; + +namespace Test.AsyncIntegration +{ + public class AsyncIntegrationFixture : IntegrationFixtureBase, IAsyncLifetime + { + protected readonly bool _dispatchConsumersAsync = false; + protected readonly ushort _consumerDispatchConcurrency = 1; + protected readonly bool _openChannel = true; + + public AsyncIntegrationFixture(ITestOutputHelper output, + bool dispatchConsumersAsync = false, + ushort consumerDispatchConcurrency = 1, + bool openChannel = true) : base(output) + { + _dispatchConsumersAsync = dispatchConsumersAsync; + _consumerDispatchConcurrency = consumerDispatchConcurrency; + _openChannel = openChannel; + } + + protected override void SetUp() + { + // InitializeAsync + } + + protected static Task AssertRanToCompletion(params Task[] tasks) + { + return DoAssertRanToCompletion(tasks); + } + + protected static Task AssertRanToCompletion(IEnumerable tasks) + { + return DoAssertRanToCompletion(tasks); + } + + 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); + } + + if (_openChannel) + { + _channel = await _conn.CreateChannelAsync(); + } + + base.AddCallbackHandlers(); + } + + public virtual async Task DisposeAsync() + { + try + { + if (_channel != null) + { + await _channel.CloseAsync(); + } + await _conn.CloseAsync(); + } + finally + { + if (_channel != null) + { + _channel.Dispose(); + } + _conn.Dispose(); + _channel = null; + _conn = null; + } + } + + private static async Task DoAssertRanToCompletion(IEnumerable tasks) + { + Task whenAllTask = Task.WhenAll(tasks); + await whenAllTask; + Assert.Equal(TaskStatus.RanToCompletion, whenAllTask.Status); + } + } +} diff --git a/projects/Test/AsyncIntegration/TestAsyncConsumer.cs b/projects/Test/AsyncIntegration/TestAsyncConsumer.cs new file mode 100644 index 0000000000..d802e5522a --- /dev/null +++ b/projects/Test/AsyncIntegration/TestAsyncConsumer.cs @@ -0,0 +1,491 @@ +// 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, dispatchConsumersAsync: true, consumerDispatchConcurrency: 2) + { + } + + [Fact] + public async Task TestBasicRoundtripConcurrent() + { + QueueDeclareOk q = await _channel.QueueDeclareAsync(); + string publish1 = GetUniqueString(1024); + byte[] body = _encoding.GetBytes(publish1); + await _channel.BasicPublishAsync("", q.QueueName, body); + + string publish2 = GetUniqueString(1024); + body = _encoding.GetBytes(publish2); + await _channel.BasicPublishAsync("", 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, (args) => + { + if (args.Initiator == ShutdownInitiator.Peer) + { + publish1SyncSource.TrySetResult(false); + publish2SyncSource.TrySetResult(false); + } + }); + }; + + _channel.ChannelShutdown += (o, ea) => + { + HandleChannelShutdown(_channel, ea, (args) => + { + if (args.Initiator == ShutdownInitiator.Peer) + { + publish1SyncSource.TrySetResult(false); + publish2SyncSource.TrySetResult(false); + } + }); + }; + + consumer.Received += async (o, a) => + { + string decoded = _encoding.GetString(a.Body.ToArray()); + if (decoded == publish1) + { + publish1SyncSource.TrySetResult(true); + await publish2SyncSource.Task; + } + else if (decoded == publish2) + { + publish2SyncSource.TrySetResult(true); + await publish1SyncSource.Task; + } + }; + + await _channel.BasicConsumeAsync(q.QueueName, true, string.Empty, false, false, null, consumer); + + // ensure we get a delivery + await AssertRanToCompletion(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, (args) => + { + if (args.Initiator == ShutdownInitiator.Peer) + { + publish1SyncSource.TrySetResult(false); + publish2SyncSource.TrySetResult(false); + } + }); + }; + + _channel.ChannelShutdown += (o, ea) => + { + HandleChannelShutdown(_channel, ea, (args) => + { + if (args.Initiator == ShutdownInitiator.Peer) + { + publish1SyncSource.TrySetResult(false); + publish2SyncSource.TrySetResult(false); + } + }); + }; + + QueueDeclareOk q = await _channel.QueueDeclareAsync(queue: queueName, exclusive: false, durable: true); + Assert.Equal(q, 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.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; + } + } + }; + + await _channel.BasicConsumeAsync(queueName, true, string.Empty, false, false, null, consumer); + + // ensure we get a delivery + await AssertRanToCompletion(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 AssertRanToCompletion(publishTask, consumeTask); + } + + [Fact] + public async Task TestBasicRejectAsync() + { + var publishSyncSource = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + _conn.ConnectionShutdown += (o, ea) => + { + HandleConnectionShutdown(_conn, ea, (args) => + { + if (args.Initiator == ShutdownInitiator.Peer) + { + publishSyncSource.TrySetResult(false); + } + }); + }; + + _channel.ChannelShutdown += (o, ea) => + { + HandleChannelShutdown(_channel, ea, (args) => + { + if (args.Initiator == ShutdownInitiator.Peer) + { + 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); + } + } + + [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, (args) => + { + if (args.Initiator == ShutdownInitiator.Peer) + { + publishSyncSource.TrySetResult(false); + } + }); + }; + + channel.ChannelShutdown += (o, ea) => + { + HandleChannelShutdown(channel, ea, (args) => + { + if (args.Initiator == ShutdownInitiator.Peer) + { + 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, (args) => + { + if (args.Initiator == ShutdownInitiator.Peer) + { + publishSyncSource.TrySetResult(false); + } + }); + }; + + channel.ChannelShutdown += (o, ea) => + { + HandleChannelShutdown(channel, ea, (args) => + { + if (args.Initiator == ShutdownInitiator.Peer) + { + 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..d26b85fd68 100644 --- a/projects/Unit/TestAsyncConsumerExceptions.cs +++ b/projects/Test/AsyncIntegration/TestAsyncConsumerExceptions.cs @@ -32,86 +32,81 @@ 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, dispatchConsumersAsync: true, consumerDispatchConcurrency: 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 Task TestCancelNotificationExceptionHandling() { IBasicConsumer consumer = new ConsumerFailingOnCancel(_channel); - TestExceptionHandlingWith(consumer, (m, q, c, ct) => m.QueueDelete(q)); + return TestExceptionHandlingWith(consumer, async (ch, q, c, ct) => + { + await ch.QueueDeleteAsync(q, false, false); + }); } [Fact] - public void TestConsumerCancelOkExceptionHandling() + public Task TestConsumerCancelOkExceptionHandling() { IBasicConsumer consumer = new ConsumerFailingOnCancelOk(_channel); - TestExceptionHandlingWith(consumer, (m, q, c, ct) => m.BasicCancel(ct)); + return TestExceptionHandlingWith(consumer, (ch, q, c, ct) => ch.BasicCancelAsync(ct)); } [Fact] - public void TestConsumerConsumeOkExceptionHandling() + public Task TestConsumerConsumeOkExceptionHandling() { IBasicConsumer consumer = new ConsumerFailingOnConsumeOk(_channel); - TestExceptionHandlingWith(consumer, (m, q, c, ct) => { }); + return TestExceptionHandlingWith(consumer, async (ch, q, c, ct) => await Task.Yield()); } [Fact] - public void TestConsumerShutdownExceptionHandling() + public Task TestConsumerShutdownExceptionHandling() { IBasicConsumer consumer = new ConsumerFailingOnShutdown(_channel); - TestExceptionHandlingWith(consumer, (m, q, c, ct) => m.Close()); + return TestExceptionHandlingWith(consumer, (ch, q, c, ct) => ch.CloseAsync()); } [Fact] - public void TestDeliveryExceptionHandling() + public Task TestDeliveryExceptionHandling() { IBasicConsumer consumer = new ConsumerFailingOnDelivery(_channel); - TestExceptionHandlingWith(consumer, (m, q, c, ct) => m.BasicPublish("", q, _encoding.GetBytes("msg"))); + return TestExceptionHandlingWith(consumer, (ch, q, c, ct) => + ch.BasicPublishAsync("", q, _encoding.GetBytes("msg"))); + } + + protected async Task TestExceptionHandlingWith(IBasicConsumer consumer, + Func action) + { + var waitSpan = TimeSpan.FromSeconds(5); + 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 diff --git a/projects/Test/AsyncIntegration/TestBasicGetAsync.cs b/projects/Test/AsyncIntegration/TestBasicGetAsync.cs new file mode 100644 index 0000000000..6d43ebd0a3 --- /dev/null +++ b/projects/Test/AsyncIntegration/TestBasicGetAsync.cs @@ -0,0 +1,64 @@ +// 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.ToArray())); + + QueueDeclareOk queueResultPassive = await _channel.QueueDeclareAsync(queueName, true, true, true, true, null); + Assert.Equal((uint)0, queueResultPassive.MessageCount); + + Assert.Null(await _channel.BasicGetAsync(queueName, true)); + } + } +} diff --git a/projects/Test/AsyncIntegration/TestBasicPublishAsync.cs b/projects/Test/AsyncIntegration/TestBasicPublishAsync.cs new file mode 100644 index 0000000000..e6a526a130 --- /dev/null +++ b/projects/Test/AsyncIntegration/TestBasicPublishAsync.cs @@ -0,0 +1,71 @@ +// 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.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); + + var publishTask = Task.Run(async () => + { + byte[] body = GetRandomBody(512); + for (int i = 0; i < messageCount; i++) + { + await _channel.BasicPublishAsync(string.Empty, q, body); + } + await _channel.WaitForConfirmsOrDieAsync(); + publishSyncSource.SetResult(true); + }); + + Assert.True(await publishSyncSource.Task); + Assert.Equal((uint)messageCount, await _channel.QueuePurgeAsync(q)); + } + } +} diff --git a/projects/Test/AsyncIntegration/TestConcurrentAccessWithSharedConnectionAsync.cs b/projects/Test/AsyncIntegration/TestConcurrentAccessWithSharedConnectionAsync.cs new file mode 100644 index 0000000000..9626bf4fa8 --- /dev/null +++ b/projects/Test/AsyncIntegration/TestConcurrentAccessWithSharedConnectionAsync.cs @@ -0,0 +1,162 @@ +// 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, openChannel: false) + { + } + + public override async Task InitializeAsync() + { + await base.InitializeAsync(); + _conn.ConnectionShutdown += HandleConnectionShutdown; + } + + [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, (args) => + { + if (args.Initiator == ShutdownInitiator.Peer) + { + tcs.TrySetResult(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)); + } + } + await AssertRanToCompletion(tasks); + + // 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..e755c3e5ef --- /dev/null +++ b/projects/Test/AsyncIntegration/TestConfirmSelectAsync.cs @@ -0,0 +1,71 @@ +// 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 + { + readonly byte[] _message = GetRandomBody(64); + + 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", _message); + } + } +} diff --git a/projects/Test/AsyncIntegration/TestExchangeDeclareAsync.cs b/projects/Test/AsyncIntegration/TestExchangeDeclareAsync.cs new file mode 100644 index 0000000000..6fee43ebf4 --- /dev/null +++ b/projects/Test/AsyncIntegration/TestExchangeDeclareAsync.cs @@ -0,0 +1,101 @@ +// 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 + { + public TestExchangeDeclareAsync(ITestOutputHelper output) : base(output) + { + } + + [Fact] + public async Task TestConcurrentExchangeDeclareAndBindAsync() + { + var exchangeNames = new ConcurrentBag(); + var tasks = new List(); + NotSupportedException nse = null; + for (int i = 0; i < 256; i++) + { + async Task f() + { + try + { + await Task.Delay(S_Random.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); + tasks.Add(t); + } + + await AssertRanToCompletion(tasks); + Assert.Null(nse); + tasks.Clear(); + + foreach (string exchangeName in exchangeNames) + { + async Task f() + { + try + { + await Task.Delay(S_Random.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); + tasks.Add(t); + } + + await AssertRanToCompletion(tasks); + Assert.Null(nse); + } + } +} diff --git a/projects/Unit/TestExtensions.cs b/projects/Test/AsyncIntegration/TestExtensionsAsync.cs similarity index 55% rename from projects/Unit/TestExtensions.cs rename to projects/Test/AsyncIntegration/TestExtensionsAsync.cs index 2392011edb..91cb8e9107 100644 --- a/projects/Unit/TestExtensions.cs +++ b/projects/Test/AsyncIntegration/TestExtensionsAsync.cs @@ -31,58 +31,59 @@ 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, 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.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"); + 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..ce845b5294 --- /dev/null +++ b/projects/Test/AsyncIntegration/TestFloodPublishingAsync.cs @@ -0,0 +1,233 @@ +// 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 = GetRandomBody(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() + { + bool sawUnexpectedShutdown = false; + _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, (args) => + { + if (args.Initiator == ShutdownInitiator.Peer) + { + sawUnexpectedShutdown = true; + } + }); + }; + + _channel.ChannelShutdown += (o, ea) => + { + HandleChannelShutdown(_channel, ea, (args) => + { + if (args.Initiator == ShutdownInitiator.Peer) + { + sawUnexpectedShutdown = true; + } + }); + }; + + 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); + Assert.False(sawUnexpectedShutdown); + } + + [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; + int receivedCount = 0; + + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + _conn.ConnectionShutdown += (o, ea) => + { + HandleConnectionShutdown(_conn, ea, (args) => + { + if (args.Initiator == ShutdownInitiator.Peer) + { + receivedCount = -1; + tcs.SetResult(false); + } + }); + }; + + _channel.ChannelShutdown += (o, ea) => + { + HandleChannelShutdown(_channel, ea, (args) => + { + if (args.Initiator == ShutdownInitiator.Peer) + { + 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, (args) => + { + if (args.Initiator == ShutdownInitiator.Peer) + { + stop = true; + tcs.TrySetResult(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, (args) => + { + if (args.Initiator == ShutdownInitiator.Peer) + { + tcs.TrySetResult(false); + } + }); + }; + + var consumer = new AsyncEventingBasicConsumer(consumeCh); + consumer.Received += async (o, a) => + { + string receivedMessage = _encoding.GetString(a.Body.ToArray()); + 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..54eb1fc032 --- /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(() => + { + ValueTask r = _channel.ExchangeDeclareAsync(Guid.NewGuid().ToString(), ExchangeType.Fanout, true, false, false, null); + return r.AsTask(); + }); + } + + [Fact] + public Task TestPassiveQueueDeclareWhenQueueDoesNotExist() + { + return Assert.ThrowsAsync(() => + { + ValueTask 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 63% rename from projects/Unit/TestPublisherConfirms.cs rename to projects/Test/AsyncIntegration/TestPublisherConfirmsAsync.cs index 4f910b8784..45cebdcb4c 100644 --- a/projects/Unit/TestPublisherConfirms.cs +++ b/projects/Test/AsyncIntegration/TestPublisherConfirmsAsync.cs @@ -33,40 +33,36 @@ 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 +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, openChannel: false) { -#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 +72,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 +85,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 +119,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 +135,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 +153,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..f7ca9367d4 --- /dev/null +++ b/projects/Test/AsyncIntegration/TestQueueDeclareAsync.cs @@ -0,0 +1,150 @@ +// 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) => + { + HandleConnectionShutdown(_conn, ea, (args) => + { + if (ea.Initiator == ShutdownInitiator.Peer) + { + sawShutdown = true; + } + }); + }; + + _channel.ChannelShutdown += (o, ea) => + { + HandleChannelShutdown(_channel, ea, (args) => + { + if (args.Initiator == ShutdownInitiator.Peer) + { + sawShutdown = true; + } + }); + }; + + var tasks = new List(); + var queues = 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); + queues.Add(queueName); + } + catch (NotSupportedException e) + { + nse = e; + } + } + var t = Task.Run(f); + tasks.Add(t); + } + + await AssertRanToCompletion(tasks); + Assert.Null(nse); + tasks.Clear(); + + nse = null; + foreach (string q in queues) + { + 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); + tasks.Add(t); + } + + await AssertRanToCompletion(tasks); + 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/IntegrationFixtureBase.cs b/projects/Test/Common/IntegrationFixtureBase.cs new file mode 100644 index 0000000000..61f4498bb7 --- /dev/null +++ b/projects/Test/Common/IntegrationFixtureBase.cs @@ -0,0 +1,410 @@ +// 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 abstract class IntegrationFixtureBase : IDisposable + { + private static bool s_isRunningInCI = false; + private static bool s_isWindows = false; + private static bool s_isVerbose = 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 IntegrationFixtureBase() + { + S_Random = new Random(); + InitIsRunningInCI(); + InitIsWindows(); + InitIsVerbose(); + + if (s_isRunningInCI) + { + WaitSpan = TimeSpan.FromSeconds(60); + LongWaitSpan = TimeSpan.FromSeconds(120); + } + else + { + WaitSpan = TimeSpan.FromSeconds(30); + LongWaitSpan = TimeSpan.FromSeconds(60); + } + } + + public IntegrationFixtureBase(ITestOutputHelper output) + { + _output = output; + _rabbitMQCtl = new RabbitMQCtl(_output); + + Type type = _output.GetType(); + FieldInfo testMember = type.GetField("test", BindingFlags.Instance | BindingFlags.NonPublic); + ITest test = (ITest)testMember.GetValue(output); + _testDisplayName = test.DisplayName + .Replace("Test.", string.Empty) + .Replace("AsyncIntegration.", "AI.") + .Replace("Integration.", "I.") + .Replace("SequentialI.", "SI."); + + SetUp(); + } + + protected virtual void SetUp() + { + if (_connFactory == null) + { + _connFactory = CreateConnectionFactory(); + } + + if (_conn == null) + { + _conn = _connFactory.CreateConnection(); + _channel = _conn.CreateChannel(); + AddCallbackHandlers(); + } + } + + public virtual void Dispose() + { + if (_channel != null) + { + _channel.Dispose(); + } + + if (_conn != null) + { + _conn.Dispose(); + } + + TearDown(); + } + + protected virtual void TearDown() + { + } + + protected virtual void AddCallbackHandlers() + { + if (IntegrationFixtureBase.IsVerbose) + { + if (_conn != null) + { + _conn.CallbackException += (o, ea) => + { + _output.WriteLine("{0} connection callback exception: {1}", + _testDisplayName, ea.Exception); + }; + + _conn.ConnectionShutdown += (o, ea) => + { + HandleConnectionShutdown(_conn, ea, (args) => + { + _output.WriteLine("{0} connection shutdown, args: {1}", + _testDisplayName, args); + }); + }; + } + + if (_channel != null) + { + _channel.CallbackException += (o, ea) => + { + _output.WriteLine("{0} channel callback exception: {1}", + _testDisplayName, ea.Exception); + }; + + _channel.ChannelShutdown += (o, ea) => + { + HandleChannelShutdown(_channel, ea, (args) => + { + _output.WriteLine("{0} channel shutdown, args: {1}", + _testDisplayName, args); + }); + }; + } + } + } + + protected static bool IsRunningInCI + { + get { return s_isRunningInCI; } + } + + protected static bool IsWindows + { + get { return s_isWindows; } + } + + protected static bool IsVerbose + { + get { return s_isVerbose; } + } + + 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 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.Peer) + { + 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.Peer) + { + _output.WriteLine($"{_testDisplayName} connection {conn.ClientProvidedName} shut down: {args}"); + } + a(args); + } + + protected void HandleChannelShutdown(object sender, ShutdownEventArgs args) + { + if (args.Initiator == ShutdownInitiator.Peer) + { + 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.Peer) + { + _output.WriteLine($"{_testDisplayName} channel {ch.ChannelNumber} shut down: {args}"); + } + a(args); + } + + 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 void InitIsVerbose() + { + if (bool.TryParse( + Environment.GetEnvironmentVariable("RABBITMQ_CLIENT_TESTS_VERBOSE"), out _)) + { + s_isVerbose = true; + } + } + + 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/Test/Integration/IntegrationFixture.cs b/projects/Test/Integration/IntegrationFixture.cs new file mode 100644 index 0000000000..b3bffde877 --- /dev/null +++ b/projects/Test/Integration/IntegrationFixture.cs @@ -0,0 +1,56 @@ +// 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; +using Xunit.Abstractions; + +namespace Test.Integration +{ + public class IntegrationFixture : IntegrationFixtureBase + { + public IntegrationFixture(ITestOutputHelper output) + : base(output) + { + int threadCount; + if (IsRunningInCI) + { + threadCount = _processorCount * 16; + } + else + { + // Assuming that dev machines have more cores + threadCount = _processorCount * 8; + } + + ThreadPool.SetMinThreads(threadCount, threadCount); + } + } +} 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 70% rename from projects/Unit/TestBasicGet.cs rename to projects/Test/Integration/TestBasicGet.cs index 5dba9cee8d..1bb486b9b9 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 { @@ -74,5 +76,40 @@ public void TestBasicGetWithNonEmptyResponseAndAutoAckMode() 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..78287d61e0 --- /dev/null +++ b/projects/Test/Integration/TestBasicPublish.cs @@ -0,0 +1,288 @@ +// 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() + { + _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.ToArray(); + 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.ToArray(); + 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.ToArray(); + 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.ToArray(); + 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..ceb4639b2b 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 { @@ -53,9 +54,10 @@ public void TestConsumerDispatcherShutdown() { latch.Set(); }; + Assert.False(m.ConsumerDispatcher.IsShutdown, "dispatcher should NOT be shut down before Close"); _channel.Close(); - Wait(latch, TimeSpan.FromSeconds(3)); + Wait(latch, TimeSpan.FromSeconds(5), "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 69% rename from projects/Unit/TestConnectionFactory.cs rename to projects/Test/Integration/TestConnectionFactory.cs index 2410e60e66..39b243080c 100644 --- a/projects/Unit/TestConnectionFactory.cs +++ b/projects/Test/Integration/TestConnectionFactory.cs @@ -31,13 +31,28 @@ using System.Collections.Generic; using System.Net.Sockets; +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() { @@ -108,94 +123,95 @@ public void TestConnectionFactoryWithCustomSocketFactory() [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); @@ -205,11 +221,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); @@ -223,12 +237,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); @@ -238,12 +251,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 })) { } } @@ -251,10 +262,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 }); }); } @@ -262,11 +271,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 })) { } } @@ -274,33 +281,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] @@ -312,7 +320,7 @@ public void TestCreateAmqpTCPEndPointOverridesMaxMessageSizeWhenGreaterThanMaxim [Fact] public void TestCreateConnectionUsesConfiguredMaxMessageSize() { - var cf = new ConnectionFactory(); + var cf = CreateConnectionFactory(); cf.MaxMessageSize = 1500; using (IConnection conn = cf.CreateConnection()) { @@ -322,7 +330,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); @@ -335,7 +343,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 })) @@ -347,7 +355,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 78% rename from projects/Unit/TestConsumerCancelNotify.cs rename to projects/Test/Integration/TestConsumerCancelNotify.cs index f298852965..630a3e6035 100644 --- a/projects/Unit/TestConsumerCancelNotify.cs +++ b/projects/Test/Integration/TestConsumerCancelNotify.cs @@ -31,18 +31,19 @@ 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 { - protected readonly object lockObject = new object(); - protected bool notifiedCallback; - protected bool notifiedEvent; - protected string consumerTag; + private readonly ManualResetEventSlim _latch = new ManualResetEventSlim(); + private bool _notifiedCallback; + private bool _notifiedEvent; + private string _consumerTag; public TestConsumerCancelNotify(ITestOutputHelper output) : base(output) { @@ -51,13 +52,13 @@ public TestConsumerCancelNotify(ITestOutputHelper output) : base(output) [Fact] public void TestConsumerCancelNotification() { - TestConsumerCancel("queue_consumer_cancel_notify", false, ref notifiedCallback); + TestConsumerCancel("queue_consumer_cancel_notify", false, ref _notifiedCallback); } [Fact] public void TestConsumerCancelEvent() { - TestConsumerCancel("queue_consumer_cancel_event", true, ref notifiedEvent); + TestConsumerCancel("queue_consumer_cancel_event", true, ref _notifiedEvent); } [Fact] @@ -76,15 +77,12 @@ public void TestCorrectConsumerTag() string notifiedConsumerTag = null; consumer.ConsumerCancelled += (sender, args) => { - lock (lockObject) - { - notifiedConsumerTag = args.ConsumerTags.First(); - Monitor.PulseAll(lockObject); - } + notifiedConsumerTag = args.ConsumerTags.First(); + _latch.Set(); }; _channel.QueueDelete(q1); - WaitOn(lockObject); + Wait(_latch, "ConsumerCancelled event"); Assert.Equal(consumerTag1, notifiedConsumerTag); _channel.QueueDelete(q2); @@ -97,9 +95,9 @@ private void TestConsumerCancel(string queue, bool EventMode, ref bool notified) string actualConsumerTag = _channel.BasicConsume(queue, false, consumer); _channel.QueueDelete(queue); - WaitOn(lockObject); + Wait(_latch, "HandleBasicCancel / Cancelled event"); Assert.True(notified); - Assert.Equal(actualConsumerTag, consumerTag); + Assert.Equal(actualConsumerTag, _consumerTag); } private class CancelNotificationConsumer : DefaultBasicConsumer @@ -122,24 +120,18 @@ public override void HandleBasicCancel(string consumerTag) { if (!_eventMode) { - lock (_testClass.lockObject) - { - _testClass.notifiedCallback = true; - _testClass.consumerTag = consumerTag; - Monitor.PulseAll(_testClass.lockObject); - } + _testClass._notifiedCallback = true; + _testClass._consumerTag = consumerTag; + _testClass._latch.Set(); } base.HandleBasicCancel(consumerTag); } private void Cancelled(object sender, ConsumerEventArgs arg) { - lock (_testClass.lockObject) - { - _testClass.notifiedEvent = true; - _testClass.consumerTag = arg.ConsumerTags[0]; - Monitor.PulseAll(_testClass.lockObject); - } + _testClass._notifiedEvent = true; + _testClass._consumerTag = arg.ConsumerTags[0]; + _testClass._latch.Set(); } } } 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 96% rename from projects/Unit/TestConsumerExceptions.cs rename to projects/Test/Integration/TestConsumerExceptions.cs index 5bf62391fc..d8ec356e68 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 { @@ -107,20 +108,19 @@ public override void HandleBasicCancelOk(string consumerTag) protected void TestExceptionHandlingWith(IBasicConsumer consumer, Action action) { - object o = new object(); + var mre = new ManualResetEventSlim(false); bool notified = false; string q = _channel.QueueDeclare(); - _channel.CallbackException += (m, evt) => { notified = true; - Monitor.PulseAll(o); + mre.Set(); }; string tag = _channel.BasicConsume(q, true, consumer); action(_channel, q, consumer, tag); - WaitOn(o); + Wait(mre, "callback exception"); Assert.True(notified); } diff --git a/projects/Unit/TestConsumerOperationDispatch.cs b/projects/Test/Integration/TestConsumerOperationDispatch.cs similarity index 81% rename from projects/Unit/TestConsumerOperationDispatch.cs rename to projects/Test/Integration/TestConsumerOperationDispatch.cs index 1f3a1129e2..3e2588aa88 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 static readonly CountdownEvent s_counter = new CountdownEvent(Y); - private readonly string _x = "dotnet.tests.consumer-operation-dispatch.fanout"; + 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 @@ -94,18 +92,19 @@ public CollectingConsumer(IChannel channel) 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(IntegrationFixture.IsRunningInCI && IntegrationFixtureBase.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 (IntegrationFixture.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,20 +173,20 @@ public void TestChannelShutdownDoesNotShutDownDispatcher() ch1.Close(); ch2.BasicPublish(_x, "", _encoding.GetBytes("msg")); - Wait(latch); + Wait(latch, "received event"); } private class ShutdownLatchConsumer : DefaultBasicConsumer { - public ManualResetEventSlim Latch { get; } - public ManualResetEventSlim DuplicateLatch { get; } - - public ShutdownLatchConsumer(ManualResetEventSlim latch, ManualResetEventSlim duplicateLatch) + public ShutdownLatchConsumer() { - Latch = latch; - DuplicateLatch = duplicateLatch; + Latch = new(); + DuplicateLatch = new(); } + public readonly ManualResetEventSlim Latch; + public readonly ManualResetEventSlim DuplicateLatch; + public override void HandleChannelShutdown(object channel, ShutdownEventArgs reason) { // keep track of duplicates @@ -196,16 +204,14 @@ public override void HandleChannelShutdown(object channel, ShutdownEventArgs rea [Fact] public void TestChannelShutdownHandler() { - var latch = new ManualResetEventSlim(false); - var duplicateLatch = new ManualResetEventSlim(false); - string q = _channel.QueueDeclare().QueueName; - var c = new ShutdownLatchConsumer(latch, duplicateLatch); + string q = _channel.QueueDeclare(); + var c = new ShutdownLatchConsumer(); _channel.BasicConsume(queue: q, autoAck: true, consumer: c); _channel.Close(); - Wait(latch, TimeSpan.FromSeconds(5)); - Assert.False(duplicateLatch.Wait(TimeSpan.FromSeconds(5)), - "event handler fired more than once"); + + Wait(c.Latch, TimeSpan.FromSeconds(5), "channel shutdown"); + Assert.False(c.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 90% rename from projects/Unit/TestEventingConsumer.cs rename to projects/Test/Integration/TestEventingConsumer.cs index 6660af4f13..f3e33f1908 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); @@ -82,25 +83,26 @@ public void TestEventingConsumerRegistrationEvents() [Fact] public void TestEventingConsumerDeliveryEvents() { + var mre = new ManualResetEventSlim(false); string q = _channel.QueueDeclare(); - object o = new object(); bool receivedInvoked = false; object receivedSender = null; - EventingBasicConsumer ec = new EventingBasicConsumer(_channel); + var ec = new EventingBasicConsumer(_channel); ec.Received += (s, args) => { receivedInvoked = true; receivedSender = s; - - Monitor.PulseAll(o); + mre.Set(); }; _channel.BasicConsume(q, true, ec); _channel.BasicPublish("", q, _encoding.GetBytes("msg")); - WaitOn(o); + Wait(mre, "received event"); + mre.Reset(); + Assert.True(receivedInvoked); Assert.NotNull(receivedSender); Assert.Equal(ec, receivedSender); @@ -113,12 +115,12 @@ public void TestEventingConsumerDeliveryEvents() { shutdownInvoked = true; shutdownSender = s; - - Monitor.PulseAll(o); + mre.Set(); }; _channel.Close(); - WaitOn(o); + + Wait(mre, "shutdown event"); Assert.True(shutdownInvoked); Assert.NotNull(shutdownSender); 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 92% rename from projects/Unit/TestInvalidAck.cs rename to projects/Test/Integration/TestInvalidAck.cs index 0d3d5dc750..d2f2888e88 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 { @@ -44,18 +45,18 @@ public TestInvalidAck(ITestOutputHelper output) : base(output) [Fact] public void TestAckWithUnknownConsumerTagAndMultipleFalse() { - object o = new object(); + var mre = new ManualResetEventSlim(); bool shutdownFired = false; ShutdownEventArgs shutdownArgs = null; _channel.ChannelShutdown += (s, args) => { shutdownFired = true; shutdownArgs = args; - Monitor.PulseAll(o); + mre.Set(); }; _channel.BasicAck(123456, false); - WaitOn(o); + Wait(mre, "ChannelShutdown"); Assert.True(shutdownFired); AssertPreconditionFailed(shutdownArgs); } diff --git a/projects/Unit/TestMainLoop.cs b/projects/Test/Integration/TestMainLoop.cs similarity index 78% rename from projects/Unit/TestMainLoop.cs rename to projects/Test/Integration/TestMainLoop.cs index 597f1898f3..c01f385d92 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 { @@ -62,27 +63,25 @@ private sealed class FaultyConsumer : DefaultBasicConsumer [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); + var mre = new ManualResetEventSlim(); + QueueDeclareOk q = _channel.QueueDeclare(string.Empty, false, false, false, null); CallbackExceptionEventArgs ea = null; - m.CallbackException += (_, evt) => + _channel.CallbackException += (_, evt) => { ea = evt; - c.Close(); - Monitor.PulseAll(o); + _channel.Close(); + mre.Set(); }; - m.BasicConsume(q, true, new FaultyConsumer(_channel)); - m.BasicPublish("", q, _encoding.GetBytes("message")); - WaitOn(o); + + _channel.BasicConsume(q, true, new FaultyConsumer(_channel)); + _channel.BasicPublish(string.Empty, q, _encoding.GetBytes("message")); + + Wait(mre, "CallbackException"); 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 71% rename from projects/Unit/TestSsl.cs rename to projects/Test/Integration/TestSsl.cs index 4695b188e5..570c2b9c5f 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); + 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..4f8dd82a3e --- /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 : IntegrationFixtureBase + { + 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/Test/SequentialIntegration/TestConnectionRecovery.cs b/projects/Test/SequentialIntegration/TestConnectionRecovery.cs new file mode 100644 index 0000000000..c0d282c060 --- /dev/null +++ b/projects/Test/SequentialIntegration/TestConnectionRecovery.cs @@ -0,0 +1,832 @@ +// 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 RabbitMQ.Client.Exceptions; +using RabbitMQ.Client.Framing.Impl; +using RabbitMQ.Client.Impl; +using Xunit; +using Xunit.Abstractions; + +namespace Test.SequentialIntegration +{ + public class TestConnectionRecovery : TestConnectionRecoveryBase + { + private readonly string _queueName; + + public TestConnectionRecovery(ITestOutputHelper output) : base(output) + { + _queueName = $"{nameof(TestConnectionRecovery)}-{Guid.NewGuid()}"; + } + + protected override void TearDown() + { + var cf = CreateConnectionFactory(); + cf.ClientProvidedName = cf.ClientProvidedName + "-TearDown"; + using IConnection conn = cf.CreateConnection(); + using IChannel ch = conn.CreateChannel(); + ch.QueueDelete(_queueName); + base.TearDown(); + } + + [Fact] + public void TestBasicAckAfterChannelRecovery() + { + var allMessagesSeenLatch = new ManualResetEventSlim(false); + var cons = new AckingBasicConsumer(_channel, _totalMessageCount, allMessagesSeenLatch); + + string queueName = _channel.QueueDeclare(_queueName, false, false, false, null).QueueName; + Assert.Equal(queueName, _queueName); + + _channel.BasicQos(0, 1, false); + _channel.BasicConsume(queueName, false, cons); + + ManualResetEventSlim sl = PrepareForShutdown(_conn); + ManualResetEventSlim rl = PrepareForRecovery(_conn); + + PublishMessagesWhileClosingConn(queueName); + + Wait(sl, "connection shutdown"); + Wait(rl, "connection recovery"); + Wait(allMessagesSeenLatch, "all messages seen"); + } + + [Fact] + public void TestBasicNackAfterChannelRecovery() + { + var allMessagesSeenLatch = new ManualResetEventSlim(false); + var cons = new NackingBasicConsumer(_channel, _totalMessageCount, allMessagesSeenLatch); + + string queueName = _channel.QueueDeclare(_queueName, false, false, false, null).QueueName; + Assert.Equal(queueName, _queueName); + + _channel.BasicQos(0, 1, false); + _channel.BasicConsume(queueName, false, cons); + + ManualResetEventSlim sl = PrepareForShutdown(_conn); + ManualResetEventSlim rl = PrepareForRecovery(_conn); + + PublishMessagesWhileClosingConn(queueName); + + Wait(sl, "connection shutdown"); + Wait(rl, "connection recovery"); + Wait(allMessagesSeenLatch, "all messages seen"); + } + + [Fact] + public void TestBasicRejectAfterChannelRecovery() + { + var allMessagesSeenLatch = new ManualResetEventSlim(false); + var cons = new RejectingBasicConsumer(_channel, _totalMessageCount, allMessagesSeenLatch); + + string queueName = _channel.QueueDeclare(_queueName, false, false, false, null).QueueName; + Assert.Equal(queueName, _queueName); + + _channel.BasicQos(0, 1, false); + _channel.BasicConsume(queueName, false, cons); + + ManualResetEventSlim sl = PrepareForShutdown(_conn); + ManualResetEventSlim rl = PrepareForRecovery(_conn); + + PublishMessagesWhileClosingConn(queueName); + + Wait(sl, "connection shutdown"); + Wait(rl, "connection recovery"); + Wait(allMessagesSeenLatch, "all messages seen"); + } + + [Fact] + public void TestBasicAckAfterBasicGetAndChannelRecovery() + { + string q = GenerateQueueName(); + _channel.QueueDeclare(q, false, false, false, null); + // create an offset + _channel.BasicPublish("", q, _messageBody); + Thread.Sleep(50); + BasicGetResult g = _channel.BasicGet(q, false); + CloseAndWaitForRecovery(); + Assert.True(_conn.IsOpen); + Assert.True(_channel.IsOpen); + // ack the message after recovery - this should be out of range and ignored + _channel.BasicAck(g.DeliveryTag, false); + // do a sync operation to 'check' there is no channel exception + _channel.BasicGet(q, false); + } + + [Fact] + public void TestBasicAckEventHandlerRecovery() + { + _channel.ConfirmSelect(); + var latch = new ManualResetEventSlim(false); + ((AutorecoveringChannel)_channel).BasicAcks += (m, args) => latch.Set(); + ((AutorecoveringChannel)_channel).BasicNacks += (m, args) => latch.Set(); + + CloseAndWaitForRecovery(); + CloseAndWaitForRecovery(); + Assert.True(_channel.IsOpen); + + WithTemporaryNonExclusiveQueue(_channel, (m, q) => m.BasicPublish("", q, _messageBody)); + Wait(latch, "basic acks/nacks"); + } + + [Fact] + public void TestBasicConnectionRecovery() + { + Assert.True(_conn.IsOpen); + CloseAndWaitForRecovery(); + Assert.True(_conn.IsOpen); + } + + [Fact] + public void TestBasicConnectionRecoveryOnBrokerRestart() + { + Assert.True(_conn.IsOpen); + RestartServerAndWaitForRecovery(); + Assert.True(_conn.IsOpen); + } + + [Fact] + public void TestBasicChannelRecovery() + { + Assert.True(_channel.IsOpen); + CloseAndWaitForRecovery(); + Assert.True(_channel.IsOpen); + } + + [Fact] + public void TestBasicChannelRecoveryOnServerRestart() + { + Assert.True(_channel.IsOpen); + RestartServerAndWaitForRecovery(); + Assert.True(_channel.IsOpen); + } + + [Fact] + public void TestBlockedListenersRecovery() + { + var latch = new ManualResetEventSlim(false); + _conn.ConnectionBlocked += (c, reason) => latch.Set(); + CloseAndWaitForRecovery(); + CloseAndWaitForRecovery(); + + Block(); + Wait(latch, "connection blocked"); + + Unblock(); + } + + [Fact] + public void TestClientNamedQueueRecovery() + { + string s = "dotnet-client.test.recovery.q1"; + WithTemporaryNonExclusiveQueue(_channel, (m, q) => + { + CloseAndWaitForRecovery(); + AssertQueueRecovery(m, q, false); + _channel.QueueDelete(q); + }, s); + } + + [Fact] + public void TestClientNamedQueueRecoveryNoWait() + { + string s = "dotnet-client.test.recovery.q1-nowait"; + WithTemporaryQueueNoWait(_channel, (m, q) => + { + CloseAndWaitForRecovery(); + AssertQueueRecovery(m, q); + }, s); + } + + [Fact] + public void TestClientNamedQueueRecoveryOnServerRestart() + { + string s = "dotnet-client.test.recovery.q1"; + WithTemporaryNonExclusiveQueue(_channel, (m, q) => + { + RestartServerAndWaitForRecovery(); + AssertQueueRecovery(m, q, false); + _channel.QueueDelete(q); + }, s); + } + + [Fact] + public void TestConsumerRecoveryWithManyConsumers() + { + string q = _channel.QueueDeclare(GenerateQueueName(), false, false, false, null).QueueName; + int n = 1024; + + for (int i = 0; i < n; i++) + { + var cons = new EventingBasicConsumer(_channel); + _channel.BasicConsume(q, true, cons); + } + + var latch = new ManualResetEventSlim(false); + ((AutorecoveringConnection)_conn).ConsumerTagChangeAfterRecovery += (prev, current) => latch.Set(); + + CloseAndWaitForRecovery(); + Wait(latch, "consumer tag change after recovery"); + Assert.True(_channel.IsOpen); + AssertConsumerCount(q, n); + } + + [Fact] + public void TestDeclarationOfManyAutoDeleteExchangesWithTransientExchangesThatAreDeleted() + { + AssertRecordedExchanges((AutorecoveringConnection)_conn, 0); + for (int i = 0; i < 3; i++) + { + string x1 = $"source-{Guid.NewGuid()}"; + _channel.ExchangeDeclare(x1, "fanout", false, true, null); + string x2 = $"destination-{Guid.NewGuid()}"; + _channel.ExchangeDeclare(x2, "fanout", false, false, null); + _channel.ExchangeBind(x2, x1, ""); + _channel.ExchangeDelete(x2); + } + AssertRecordedExchanges((AutorecoveringConnection)_conn, 0); + } + + [Fact] + public void TestDeclarationOfManyAutoDeleteExchangesWithTransientExchangesThatAreUnbound() + { + AssertRecordedExchanges((AutorecoveringConnection)_conn, 0); + for (int i = 0; i < 1000; i++) + { + string x1 = $"source-{Guid.NewGuid()}"; + _channel.ExchangeDeclare(x1, "fanout", false, true, null); + string x2 = $"destination-{Guid.NewGuid()}"; + _channel.ExchangeDeclare(x2, "fanout", false, false, null); + _channel.ExchangeBind(x2, x1, ""); + _channel.ExchangeUnbind(x2, x1, ""); + _channel.ExchangeDelete(x2); + } + AssertRecordedExchanges((AutorecoveringConnection)_conn, 0); + } + + [Fact] + public void TestDeclarationOfManyAutoDeleteExchangesWithTransientQueuesThatAreDeleted() + { + AssertRecordedExchanges((AutorecoveringConnection)_conn, 0); + for (int i = 0; i < 1000; i++) + { + string x = Guid.NewGuid().ToString(); + _channel.ExchangeDeclare(x, "fanout", false, true, null); + RabbitMQ.Client.QueueDeclareOk q = _channel.QueueDeclare(); + _channel.QueueBind(q, x, ""); + _channel.QueueDelete(q); + } + AssertRecordedExchanges((AutorecoveringConnection)_conn, 0); + } + + [Fact] + public void TestDeclarationOfManyAutoDeleteExchangesWithTransientQueuesThatAreUnbound() + { + AssertRecordedExchanges((AutorecoveringConnection)_conn, 0); + for (int i = 0; i < 1000; i++) + { + string x = Guid.NewGuid().ToString(); + _channel.ExchangeDeclare(x, "fanout", false, true, null); + RabbitMQ.Client.QueueDeclareOk q = _channel.QueueDeclare(); + _channel.QueueBind(q, x, ""); + _channel.QueueUnbind(q, x, "", null); + } + AssertRecordedExchanges((AutorecoveringConnection)_conn, 0); + } + + [Fact] + public void TestDeclarationOfManyAutoDeleteQueuesWithTransientConsumer() + { + AssertRecordedQueues((AutorecoveringConnection)_conn, 0); + for (int i = 0; i < 1000; i++) + { + string q = Guid.NewGuid().ToString(); + _channel.QueueDeclare(q, false, false, true, null); + var dummy = new EventingBasicConsumer(_channel); + string tag = _channel.BasicConsume(q, true, dummy); + _channel.BasicCancel(tag); + } + AssertRecordedQueues((AutorecoveringConnection)_conn, 0); + } + + [Fact] + public void TestExchangeRecovery() + { + string x = "dotnet-client.test.recovery.x1"; + DeclareNonDurableExchange(_channel, x); + CloseAndWaitForRecovery(); + AssertExchangeRecovery(_channel, x); + _channel.ExchangeDelete(x); + } + + [Fact] + public void TestExchangeRecoveryWithNoWait() + { + string x = "dotnet-client.test.recovery.x1-nowait"; + DeclareNonDurableExchangeNoWait(_channel, x); + CloseAndWaitForRecovery(); + AssertExchangeRecovery(_channel, x); + _channel.ExchangeDelete(x); + } + + [Fact] + public void TestExchangeToExchangeBindingRecovery() + { + string q = _channel.QueueDeclare("", false, false, false, null).QueueName; + string x1 = "amq.fanout"; + string x2 = GenerateExchangeName(); + + _channel.ExchangeDeclare(x2, "fanout"); + _channel.ExchangeBind(x1, x2, ""); + _channel.QueueBind(q, x1, ""); + + try + { + CloseAndWaitForRecovery(); + Assert.True(_channel.IsOpen); + _channel.BasicPublish(x2, "", _encoding.GetBytes("msg")); + AssertMessageCount(q, 1); + } + finally + { + WithTemporaryChannel(m => + { + m.ExchangeDelete(x2); + m.QueueDelete(q); + }); + } + } + + [Fact] + public void TestQueueRecoveryWithManyQueues() + { + var qs = new List(); + int n = 1024; + for (int i = 0; i < n; i++) + { + qs.Add(_channel.QueueDeclare(GenerateQueueName(), false, false, false, null).QueueName); + } + CloseAndWaitForRecovery(); + Assert.True(_channel.IsOpen); + foreach (string q in qs) + { + AssertQueueRecovery(_channel, q, false); + _channel.QueueDelete(q); + } + } + + // rabbitmq/rabbitmq-dotnet-client#43 + [Fact] + public void TestClientNamedTransientAutoDeleteQueueAndBindingRecovery() + { + string q = Guid.NewGuid().ToString(); + string x = "tmp-fanout"; + IChannel ch = _conn.CreateChannel(); + ch.QueueDelete(q); + ch.ExchangeDelete(x); + ch.ExchangeDeclare(exchange: x, type: "fanout"); + ch.QueueDeclare(queue: q, durable: false, exclusive: false, autoDelete: true, arguments: null); + ch.QueueBind(queue: q, exchange: x, routingKey: ""); + RestartServerAndWaitForRecovery(); + Assert.True(ch.IsOpen); + ch.ConfirmSelect(); + ch.QueuePurge(q); + ch.ExchangeDeclare(exchange: x, type: "fanout"); + ch.BasicPublish(exchange: x, routingKey: "", body: _encoding.GetBytes("msg")); + WaitForConfirms(ch); + 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); + } + + // rabbitmq/rabbitmq-dotnet-client#43 + [Fact] + public void TestServerNamedTransientAutoDeleteQueueAndBindingRecovery() + { + string x = "tmp-fanout"; + _channel.ExchangeDelete(x); + _channel.ExchangeDeclare(exchange: x, type: "fanout"); + string q = _channel.QueueDeclare(queue: "", durable: false, exclusive: false, autoDelete: true, arguments: null).QueueName; + string nameBefore = q; + string nameAfter = null; + var latch = new ManualResetEventSlim(false); + ((AutorecoveringConnection)_conn).QueueNameChangedAfterRecovery += (source, ea) => + { + nameBefore = ea.NameBefore; + nameAfter = ea.NameAfter; + latch.Set(); + }; + _channel.QueueBind(queue: nameBefore, exchange: x, routingKey: ""); + RestartServerAndWaitForRecovery(); + Wait(latch, "queue name change after recovery"); + Assert.True(_channel.IsOpen); + Assert.NotEqual(nameBefore, nameAfter); + _channel.ConfirmSelect(); + _channel.ExchangeDeclare(exchange: x, type: "fanout"); + _channel.BasicPublish(exchange: x, routingKey: "", body: _encoding.GetBytes("msg")); + WaitForConfirms(_channel); + RabbitMQ.Client.QueueDeclareOk ok = _channel.QueueDeclarePassive(nameAfter); + Assert.Equal(1u, ok.MessageCount); + _channel.QueueDelete(q); + _channel.ExchangeDelete(x); + } + + [Fact] + public void TestRecoveryEventHandlersOnConnection() + { + int counter = 0; + ((AutorecoveringConnection)_conn).RecoverySucceeded += (source, ea) => Interlocked.Increment(ref counter); + + CloseAndWaitForRecovery(); + CloseAndWaitForRecovery(); + CloseAndWaitForRecovery(); + CloseAndWaitForRecovery(); + Assert.True(_conn.IsOpen); + Assert.True(counter >= 3); + } + + [Fact] + public void TestRecoveryEventHandlersOnChannel() + { + int counter = 0; + ((AutorecoveringChannel)_channel).Recovery += (source, ea) => Interlocked.Increment(ref counter); + + CloseAndWaitForRecovery(); + CloseAndWaitForRecovery(); + CloseAndWaitForRecovery(); + CloseAndWaitForRecovery(); + Assert.True(_channel.IsOpen); + Assert.True(counter >= 3); + } + + [Theory] + [InlineData(1)] + [InlineData(3)] + public void TestRecoveringConsumerHandlerOnConnection(int iterations) + { + string q = _channel.QueueDeclare(GenerateQueueName(), false, false, false, null).QueueName; + var cons = new EventingBasicConsumer(_channel); + _channel.BasicConsume(q, true, cons); + + int counter = 0; + ((AutorecoveringConnection)_conn).RecoveringConsumer += (sender, args) => Interlocked.Increment(ref counter); + + for (int i = 0; i < iterations; i++) + { + CloseAndWaitForRecovery(); + } + + Assert.Equal(iterations, counter); + } + + [Fact] + public void TestRecoveringConsumerHandlerOnConnection_EventArgumentsArePassedDown() + { + var myArgs = new Dictionary { { "first-argument", "some-value" } }; + string q = _channel.QueueDeclare(GenerateQueueName(), false, false, false, null).QueueName; + var cons = new EventingBasicConsumer(_channel); + string expectedCTag = _channel.BasicConsume(cons, q, arguments: myArgs); + + bool ctagMatches = false; + bool consumerArgumentMatches = false; + ((AutorecoveringConnection)_conn).RecoveringConsumer += (sender, args) => + { + // We cannot assert here because NUnit throws when an assertion fails. This exception is caught and + // passed to a CallbackExceptionHandler, instead of failing the test. Instead, we have to do this trick + // and assert in the test function. + ctagMatches = args.ConsumerTag == expectedCTag; + consumerArgumentMatches = (string)args.ConsumerArguments["first-argument"] == "some-value"; + args.ConsumerArguments["first-argument"] = "event-handler-set-this-value"; + }; + + CloseAndWaitForRecovery(); + Assert.True(ctagMatches, "expected consumer tag to match"); + Assert.True(consumerArgumentMatches, "expected consumer arguments to match"); + string actualVal = (string)Assert.Contains("first-argument", myArgs as IDictionary); + Assert.Equal("event-handler-set-this-value", actualVal); + } + + [Fact] + public void TestServerNamedQueueRecovery() + { + string q = _channel.QueueDeclare("", false, false, false, null).QueueName; + string x = "amq.fanout"; + _channel.QueueBind(q, x, ""); + + string nameBefore = q; + string nameAfter = null; + + var latch = new ManualResetEventSlim(false); + var connection = (AutorecoveringConnection)_conn; + connection.RecoverySucceeded += (source, ea) => latch.Set(); + connection.QueueNameChangedAfterRecovery += (source, ea) => { nameAfter = ea.NameAfter; }; + + CloseAndWaitForRecovery(); + Wait(latch, "recovery succeeded"); + + Assert.NotNull(nameAfter); + Assert.StartsWith("amq.", nameBefore); + Assert.StartsWith("amq.", nameAfter); + Assert.NotEqual(nameBefore, nameAfter); + + _channel.QueueDeclarePassive(nameAfter); + } + + [Fact] + public void TestShutdownEventHandlersRecoveryOnConnection() + { + int counter = 0; + _conn.ConnectionShutdown += (c, args) => Interlocked.Increment(ref counter); + + Assert.True(_conn.IsOpen); + CloseAndWaitForRecovery(); + CloseAndWaitForRecovery(); + CloseAndWaitForRecovery(); + CloseAndWaitForRecovery(); + Assert.True(_conn.IsOpen); + + Assert.True(counter >= 3); + } + + [Fact] + public void TestShutdownEventHandlersRecoveryOnConnectionAfterDelayedServerRestart() + { + int counter = 0; + _conn.ConnectionShutdown += (c, args) => Interlocked.Increment(ref counter); + ManualResetEventSlim shutdownLatch = PrepareForShutdown(_conn); + ManualResetEventSlim recoveryLatch = PrepareForRecovery((AutorecoveringConnection)_conn); + + Assert.True(_conn.IsOpen); + + try + { + StopRabbitMQ(); + Thread.Sleep(7000); + } + finally + { + StartRabbitMQ(); + } + + Wait(shutdownLatch, WaitSpan, "connection shutdown"); + Wait(recoveryLatch, WaitSpan, "connection recovery"); + Assert.True(_conn.IsOpen); + Assert.True(counter >= 1); + } + + [Fact] + public void TestShutdownEventHandlersRecoveryOnChannel() + { + int counter = 0; + _channel.ChannelShutdown += (c, args) => Interlocked.Increment(ref counter); + + Assert.True(_channel.IsOpen); + CloseAndWaitForRecovery(); + CloseAndWaitForRecovery(); + CloseAndWaitForRecovery(); + CloseAndWaitForRecovery(); + Assert.True(_channel.IsOpen); + + Assert.True(counter >= 3); + } + + [Fact] + public void TestRecoverTopologyOnDisposedChannel() + { + string x = GenerateExchangeName(); + string q = GenerateQueueName(); + const string rk = "routing-key"; + + using (IChannel ch = _conn.CreateChannel()) + { + ch.ExchangeDeclare(exchange: x, type: "fanout"); + ch.QueueDeclare(q, false, false, false, null); + ch.QueueBind(q, x, rk); + } + + var cons = new EventingBasicConsumer(_channel); + _channel.BasicConsume(q, true, cons); + AssertConsumerCount(_channel, q, 1); + + CloseAndWaitForRecovery(); + AssertConsumerCount(_channel, q, 1); + + var latch = new ManualResetEventSlim(false); + cons.Received += (s, args) => latch.Set(); + + _channel.BasicPublish("", q, _messageBody); + Wait(latch, "received event"); + + _channel.QueueUnbind(q, x, rk); + _channel.ExchangeDelete(x); + _channel.QueueDelete(q); + } + + [Fact(Skip = "TODO-FLAKY")] + public void TestPublishRpcRightAfterReconnect() + { + string testQueueName = $"dotnet-client.test.{nameof(TestPublishRpcRightAfterReconnect)}"; + _channel.QueueDeclare(testQueueName, false, false, false, null); + var replyConsumer = new EventingBasicConsumer(_channel); + _channel.BasicConsume("amq.rabbitmq.reply-to", true, replyConsumer); + var properties = new BasicProperties(); + properties.ReplyTo = "amq.rabbitmq.reply-to"; + + TimeSpan doneSpan = TimeSpan.FromMilliseconds(100); + var done = new ManualResetEventSlim(false); + Task.Run(() => + { + try + { + + CloseAndWaitForRecovery(); + } + finally + { + done.Set(); + } + }); + + while (!done.IsSet) + { + try + { + _channel.BasicPublish(string.Empty, testQueueName, properties, _messageBody); + } + catch (Exception e) + { + if (e is AlreadyClosedException a) + { + // 406 is received, when the reply consumer isn't yet recovered + Assert.NotEqual(406, a.ShutdownReason.ReplyCode); + } + } + done.Wait(doneSpan); + } + } + + [Fact] + public void TestThatCancelledConsumerDoesNotReappearOnRecovery() + { + string q = _channel.QueueDeclare(GenerateQueueName(), false, false, false, null).QueueName; + int n = 1024; + + for (int i = 0; i < n; i++) + { + var cons = new EventingBasicConsumer(_channel); + string tag = _channel.BasicConsume(q, true, cons); + _channel.BasicCancel(tag); + } + CloseAndWaitForRecovery(); + Assert.True(_channel.IsOpen); + AssertConsumerCount(q, 0); + } + + [Fact] + public void TestThatDeletedExchangeBindingsDontReappearOnRecovery() + { + string q = _channel.QueueDeclare("", false, false, false, null).QueueName; + string x1 = "amq.fanout"; + string x2 = GenerateExchangeName(); + + _channel.ExchangeDeclare(x2, "fanout"); + _channel.ExchangeBind(x1, x2, ""); + _channel.QueueBind(q, x1, ""); + _channel.ExchangeUnbind(x1, x2, "", null); + + try + { + CloseAndWaitForRecovery(); + Assert.True(_channel.IsOpen); + _channel.BasicPublish(x2, "", _encoding.GetBytes("msg")); + AssertMessageCount(q, 0); + } + finally + { + WithTemporaryChannel(m => + { + m.ExchangeDelete(x2); + m.QueueDelete(q); + }); + } + } + + [Fact] + public void TestThatDeletedExchangesDontReappearOnRecovery() + { + string x = GenerateExchangeName(); + _channel.ExchangeDeclare(x, "fanout"); + _channel.ExchangeDelete(x); + + try + { + CloseAndWaitForRecovery(); + Assert.True(_channel.IsOpen); + _channel.ExchangeDeclarePassive(x); + Assert.Fail("Expected an exception"); + } + catch (OperationInterruptedException e) + { + // expected + AssertShutdownError(e.ShutdownReason, 404); + } + } + + [Fact] + public void TestThatDeletedQueueBindingsDontReappearOnRecovery() + { + string q = _channel.QueueDeclare("", false, false, false, null).QueueName; + string x1 = "amq.fanout"; + string x2 = GenerateExchangeName(); + + _channel.ExchangeDeclare(x2, "fanout"); + _channel.ExchangeBind(x1, x2, ""); + _channel.QueueBind(q, x1, ""); + _channel.QueueUnbind(q, x1, "", null); + + try + { + CloseAndWaitForRecovery(); + Assert.True(_channel.IsOpen); + _channel.BasicPublish(x2, "", _encoding.GetBytes("msg")); + AssertMessageCount(q, 0); + } + finally + { + WithTemporaryChannel(m => + { + m.ExchangeDelete(x2); + m.QueueDelete(q); + }); + } + } + + [Fact] + public void TestThatDeletedQueuesDontReappearOnRecovery() + { + string q = "dotnet-client.recovery.q1"; + _channel.QueueDeclare(q, false, false, false, null); + _channel.QueueDelete(q); + + try + { + CloseAndWaitForRecovery(); + Assert.True(_channel.IsOpen); + _channel.QueueDeclarePassive(q); + Assert.Fail("Expected an exception"); + } + catch (OperationInterruptedException e) + { + // expected + AssertShutdownError(e.ShutdownReason, 404); + } + } + + [Fact] + public void TestUnblockedListenersRecovery() + { + var latch = new ManualResetEventSlim(false); + _conn.ConnectionUnblocked += (source, ea) => latch.Set(); + CloseAndWaitForRecovery(); + CloseAndWaitForRecovery(); + + Block(); + Unblock(); + Wait(latch, "connection unblocked"); + } + } +} diff --git a/projects/Test/SequentialIntegration/TestConnectionRecoveryBase.cs b/projects/Test/SequentialIntegration/TestConnectionRecoveryBase.cs new file mode 100644 index 0000000000..e4617bec97 --- /dev/null +++ b/projects/Test/SequentialIntegration/TestConnectionRecoveryBase.cs @@ -0,0 +1,390 @@ +// 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) + .ConfigureAwait(false).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, + ReadOnlyMemory 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..b88c90451f --- /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.QueueNameChangedAfterRecovery += (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.QueueNameChangedAfterRecovery += (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/Test/SequentialIntegration/TestConnectionTopologyRecovery.cs b/projects/Test/SequentialIntegration/TestConnectionTopologyRecovery.cs new file mode 100644 index 0000000000..2435dbb90a --- /dev/null +++ b/projects/Test/SequentialIntegration/TestConnectionTopologyRecovery.cs @@ -0,0 +1,475 @@ +// 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 TestConnectionTopologyRecovery : TestConnectionRecoveryBase + { + public TestConnectionTopologyRecovery(ITestOutputHelper output) : base(output) + { + } + + [Fact] + public void TestRecoverTopologyOnDisposedChannel() + { + string x = GenerateExchangeName(); + string q = GenerateQueueName(); + const string rk = "routing-key"; + + using (IChannel ch = _conn.CreateChannel()) + { + ch.ExchangeDeclare(exchange: x, type: "fanout"); + ch.QueueDeclare(q, false, false, false, null); + ch.QueueBind(q, x, rk); + } + + var cons = new EventingBasicConsumer(_channel); + _channel.BasicConsume(q, true, cons); + AssertConsumerCount(_channel, q, 1); + + CloseAndWaitForRecovery(); + AssertConsumerCount(_channel, q, 1); + + var latch = new ManualResetEventSlim(false); + cons.Received += (s, args) => latch.Set(); + + _channel.BasicPublish("", q, _messageBody); + Wait(latch, "received event"); + + _channel.QueueUnbind(q, x, rk); + _channel.ExchangeDelete(x); + _channel.QueueDelete(q); + } + + [Fact] + public void TestTopologyRecoveryQueueFilter() + { + var latch = new ManualResetEventSlim(false); + + var filter = new TopologyRecoveryFilter + { + QueueFilter = queue => !queue.Name.Contains("filtered") + }; + + using AutorecoveringConnection conn = CreateAutorecoveringConnectionWithTopologyRecoveryFilter(filter); + conn.RecoverySucceeded += (source, ea) => latch.Set(); + using IChannel ch = conn.CreateChannel(); + + string queueToRecover = "recovered.queue"; + string queueToIgnore = "filtered.queue"; + ch.QueueDeclare(queueToRecover, false, false, false, null); + ch.QueueDeclare(queueToIgnore, false, false, false, null); + + _channel.QueueDelete(queueToRecover); + _channel.QueueDelete(queueToIgnore); + + CloseAndWaitForRecovery(conn); + Wait(latch, "recovery succeeded"); + + Assert.True(ch.IsOpen); + AssertQueueRecovery(ch, queueToRecover, false); + + try + { + AssertQueueRecovery(ch, queueToIgnore, false); + Assert.Fail("Expected an exception"); + } + catch (OperationInterruptedException e) + { + AssertShutdownError(e.ShutdownReason, 404); + } + } + + [Fact] + public void TestTopologyRecoveryExchangeFilter() + { + var latch = new ManualResetEventSlim(false); + + var filter = new TopologyRecoveryFilter + { + ExchangeFilter = exchange => exchange.Type == "topic" && !exchange.Name.Contains("filtered") + }; + + using AutorecoveringConnection conn = CreateAutorecoveringConnectionWithTopologyRecoveryFilter(filter); + conn.RecoverySucceeded += (source, ea) => latch.Set(); + using IChannel ch = conn.CreateChannel(); + + string exchangeToRecover = "recovered.exchange"; + string exchangeToIgnore = "filtered.exchange"; + ch.ExchangeDeclare(exchangeToRecover, "topic", false, true); + ch.ExchangeDeclare(exchangeToIgnore, "direct", false, true); + + _channel.ExchangeDelete(exchangeToRecover); + _channel.ExchangeDelete(exchangeToIgnore); + + CloseAndWaitForRecovery(conn); + Wait(latch, "recovery succeeded"); + + Assert.True(ch.IsOpen); + AssertExchangeRecovery(ch, exchangeToRecover); + + try + { + AssertExchangeRecovery(ch, exchangeToIgnore); + Assert.Fail("Expected an exception"); + } + catch (OperationInterruptedException e) + { + AssertShutdownError(e.ShutdownReason, 404); + } + } + + [Fact] + public void TestTopologyRecoveryBindingFilter() + { + var latch = new ManualResetEventSlim(false); + + var filter = new TopologyRecoveryFilter + { + BindingFilter = binding => !binding.RoutingKey.Contains("filtered") + }; + + using AutorecoveringConnection conn = CreateAutorecoveringConnectionWithTopologyRecoveryFilter(filter); + conn.RecoverySucceeded += (source, ea) => latch.Set(); + using IChannel ch = conn.CreateChannel(); + + string exchange = "topology.recovery.exchange"; + string queueWithRecoveredBinding = "topology.recovery.queue.1"; + string queueWithIgnoredBinding = "topology.recovery.queue.2"; + string bindingToRecover = "recovered.binding"; + string bindingToIgnore = "filtered.binding"; + + ch.ExchangeDeclare(exchange, "direct"); + ch.QueueDeclare(queueWithRecoveredBinding, false, false, false, null); + ch.QueueDeclare(queueWithIgnoredBinding, false, false, false, null); + ch.QueueBind(queueWithRecoveredBinding, exchange, bindingToRecover); + ch.QueueBind(queueWithIgnoredBinding, exchange, bindingToIgnore); + ch.QueuePurge(queueWithRecoveredBinding); + ch.QueuePurge(queueWithIgnoredBinding); + + _channel.QueueUnbind(queueWithRecoveredBinding, exchange, bindingToRecover); + _channel.QueueUnbind(queueWithIgnoredBinding, exchange, bindingToIgnore); + + CloseAndWaitForRecovery(conn); + Wait(latch, "recovery succeeded"); + + Assert.True(ch.IsOpen); + Assert.True(SendAndConsumeMessage(_conn, queueWithRecoveredBinding, exchange, bindingToRecover)); + Assert.False(SendAndConsumeMessage(_conn, queueWithIgnoredBinding, exchange, bindingToIgnore)); + } + + [Fact] + public void TestTopologyRecoveryDefaultFilterRecoversAllEntities() + { + var latch = new ManualResetEventSlim(false); + var filter = new TopologyRecoveryFilter(); + using AutorecoveringConnection conn = CreateAutorecoveringConnectionWithTopologyRecoveryFilter(filter); + conn.RecoverySucceeded += (source, ea) => latch.Set(); + using IChannel ch = conn.CreateChannel(); + ch.ConfirmSelect(); + + string exchange = "topology.recovery.exchange"; + string queue1 = "topology.recovery.queue.1"; + string queue2 = "topology.recovery.queue.2"; + string binding1 = "recovered.binding"; + string binding2 = "filtered.binding"; + + ch.ExchangeDeclare(exchange, "direct"); + ch.QueueDeclare(queue1, false, false, false, null); + ch.QueueDeclare(queue2, false, false, false, null); + ch.QueueBind(queue1, exchange, binding1); + ch.QueueBind(queue2, exchange, binding2); + ch.QueuePurge(queue1); + ch.QueuePurge(queue2); + + var consumerLatch1 = new ManualResetEventSlim(false); + var consumer1 = new EventingBasicConsumer(ch); + consumer1.Received += (source, ea) => consumerLatch1.Set(); + ch.BasicConsume(queue1, true, "recovered.consumer", consumer1); + + var consumerLatch2 = new ManualResetEventSlim(false); + var consumer2 = new EventingBasicConsumer(ch); + consumer2.Received += (source, ea) => consumerLatch2.Set(); + ch.BasicConsume(queue2, true, "filtered.consumer", consumer2); + + _channel.ExchangeDelete(exchange); + _channel.QueueDelete(queue1); + _channel.QueueDelete(queue2); + + CloseAndWaitForRecovery(conn); + Wait(latch, "recovery succeeded"); + + Assert.True(ch.IsOpen); + AssertExchangeRecovery(ch, exchange); + ch.QueueDeclarePassive(queue1); + ch.QueueDeclarePassive(queue2); + + 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))); + } + + [Fact] + public void TestTopologyRecoveryQueueExceptionHandler() + { + var latch = new ManualResetEventSlim(false); + + var changedQueueArguments = new Dictionary + { + { Headers.XMaxPriority, 20 } + }; + + var exceptionHandler = new TopologyRecoveryExceptionHandler + { + QueueRecoveryExceptionCondition = (rq, ex) => + { + return rq.Name.Contains("exception") + && ex is OperationInterruptedException operationInterruptedException + && operationInterruptedException.ShutdownReason.ReplyCode == Constants.PreconditionFailed; + }, + QueueRecoveryExceptionHandler = (rq, ex, connection) => + { + using (IChannel channel = connection.CreateChannel()) + { + channel.QueueDeclare(rq.Name, false, false, false, changedQueueArguments); + } + } + }; + + using AutorecoveringConnection conn = CreateAutorecoveringConnectionWithTopologyRecoveryExceptionHandler(exceptionHandler); + conn.RecoverySucceeded += (source, ea) => latch.Set(); + using IChannel ch = conn.CreateChannel(); + + string queueToRecoverWithException = "recovery.exception.queue"; + string queueToRecoverSuccessfully = "successfully.recovered.queue"; + ch.QueueDeclare(queueToRecoverWithException, false, false, false, null); + ch.QueueDeclare(queueToRecoverSuccessfully, false, false, false, null); + + _channel.QueueDelete(queueToRecoverSuccessfully); + _channel.QueueDelete(queueToRecoverWithException); + _channel.QueueDeclare(queueToRecoverWithException, false, false, false, changedQueueArguments); + + try + { + CloseAndWaitForRecovery(conn); + Wait(latch, "recovery succeded"); + + Assert.True(ch.IsOpen); + AssertQueueRecovery(ch, queueToRecoverSuccessfully, false); + AssertQueueRecovery(ch, queueToRecoverWithException, false, changedQueueArguments); + } + finally + { + _channel.QueueDelete(queueToRecoverWithException); + } + } + + [Fact] + public void TestTopologyRecoveryExchangeExceptionHandler() + { + var latch = new ManualResetEventSlim(false); + + var exceptionHandler = new TopologyRecoveryExceptionHandler + { + ExchangeRecoveryExceptionCondition = (re, ex) => + { + return re.Name.Contains("exception") + && ex is OperationInterruptedException operationInterruptedException + && operationInterruptedException.ShutdownReason.ReplyCode == Constants.PreconditionFailed; + }, + ExchangeRecoveryExceptionHandler = (re, ex, connection) => + { + using (IChannel channel = connection.CreateChannel()) + { + channel.ExchangeDeclare(re.Name, "topic", false, false); + } + } + }; + + using AutorecoveringConnection conn = CreateAutorecoveringConnectionWithTopologyRecoveryExceptionHandler(exceptionHandler); + conn.RecoverySucceeded += (source, ea) => latch.Set(); + + string exchangeToRecoverWithException = "recovery.exception.exchange"; + string exchangeToRecoverSuccessfully = "successfully.recovered.exchange"; + using IChannel ch = conn.CreateChannel(); + ch.ExchangeDeclare(exchangeToRecoverWithException, "direct", false, false); + ch.ExchangeDeclare(exchangeToRecoverSuccessfully, "direct", false, false); + + _channel.ExchangeDelete(exchangeToRecoverSuccessfully); + _channel.ExchangeDelete(exchangeToRecoverWithException); + _channel.ExchangeDeclare(exchangeToRecoverWithException, "topic", false, false); + + try + { + CloseAndWaitForRecovery(conn); + Wait(latch, "recovery succeeded"); + + Assert.True(_channel.IsOpen); + AssertExchangeRecovery(_channel, exchangeToRecoverSuccessfully); + AssertExchangeRecovery(_channel, exchangeToRecoverWithException); + } + finally + { + _channel.ExchangeDelete(exchangeToRecoverWithException); + } + } + + [Fact] + public void TestTopologyRecoveryBindingExceptionHandler() + { + var latch = new ManualResetEventSlim(false); + + string exchange = "topology.recovery.exchange"; + string queueWithExceptionBinding = "recovery.exception.queue"; + string bindingToRecoverWithException = "recovery.exception.binding"; + + var exceptionHandler = new TopologyRecoveryExceptionHandler + { + BindingRecoveryExceptionCondition = (b, ex) => + { + return b.RoutingKey.Contains("exception") + && ex is OperationInterruptedException operationInterruptedException + && operationInterruptedException.ShutdownReason.ReplyCode == Constants.NotFound; + }, + BindingRecoveryExceptionHandler = (b, ex, connection) => + { + using (IChannel channel = connection.CreateChannel()) + { + channel.QueueDeclare(queueWithExceptionBinding, false, false, false, null); + channel.QueueBind(queueWithExceptionBinding, exchange, bindingToRecoverWithException); + } + } + }; + + using AutorecoveringConnection conn = CreateAutorecoveringConnectionWithTopologyRecoveryExceptionHandler(exceptionHandler); + conn.RecoverySucceeded += (source, ea) => latch.Set(); + using IChannel ch = conn.CreateChannel(); + + string queueWithRecoveredBinding = "successfully.recovered.queue"; + string bindingToRecoverSuccessfully = "successfully.recovered.binding"; + + _channel.QueueDeclare(queueWithExceptionBinding, false, false, false, null); + + ch.ExchangeDeclare(exchange, "direct"); + ch.QueueDeclare(queueWithRecoveredBinding, false, false, false, null); + ch.QueueBind(queueWithRecoveredBinding, exchange, bindingToRecoverSuccessfully); + ch.QueueBind(queueWithExceptionBinding, exchange, bindingToRecoverWithException); + ch.QueuePurge(queueWithRecoveredBinding); + ch.QueuePurge(queueWithExceptionBinding); + + _channel.QueueUnbind(queueWithRecoveredBinding, exchange, bindingToRecoverSuccessfully); + _channel.QueueUnbind(queueWithExceptionBinding, exchange, bindingToRecoverWithException); + _channel.QueueDelete(queueWithExceptionBinding); + + CloseAndWaitForRecovery(conn); + Wait(latch, "recovery succeeded"); + + Assert.True(ch.IsOpen); + Assert.True(SendAndConsumeMessage(conn, queueWithRecoveredBinding, exchange, bindingToRecoverSuccessfully)); + Assert.True(SendAndConsumeMessage(conn, queueWithExceptionBinding, exchange, bindingToRecoverWithException)); + } + + [Fact] + public void TestTopologyRecoveryConsumerExceptionHandler() + { + var latch = new ManualResetEventSlim(false); + + string queueWithExceptionConsumer = "recovery.exception.queue"; + + var exceptionHandler = new TopologyRecoveryExceptionHandler + { + ConsumerRecoveryExceptionCondition = (c, ex) => + { + return c.ConsumerTag.Contains("exception") + && ex is OperationInterruptedException operationInterruptedException + && operationInterruptedException.ShutdownReason.ReplyCode == Constants.NotFound; + }, + ConsumerRecoveryExceptionHandler = (c, ex, connection) => + { + using (IChannel channel = connection.CreateChannel()) + { + channel.QueueDeclare(queueWithExceptionConsumer, false, false, false, null); + } + + // So topology recovery runs again. This time he missing queue should exist, making + // it possible to recover the consumer successfully. + throw ex; + } + }; + + using AutorecoveringConnection conn = CreateAutorecoveringConnectionWithTopologyRecoveryExceptionHandler(exceptionHandler); + conn.RecoverySucceeded += (source, ea) => latch.Set(); + using IChannel ch = conn.CreateChannel(); + ch.ConfirmSelect(); + + _channel.QueueDeclare(queueWithExceptionConsumer, false, false, false, null); + _channel.QueuePurge(queueWithExceptionConsumer); + + var recoverLatch = new ManualResetEventSlim(false); + var consumerToRecover = new EventingBasicConsumer(ch); + consumerToRecover.Received += (source, ea) => recoverLatch.Set(); + ch.BasicConsume(queueWithExceptionConsumer, true, "exception.consumer", consumerToRecover); + + _channel.QueueDelete(queueWithExceptionConsumer); + + CloseAndWaitForShutdown(conn); + Wait(latch, TimeSpan.FromSeconds(20), "recovery succeeded"); + + Assert.True(ch.IsOpen); + + ch.BasicPublish("", queueWithExceptionConsumer, _encoding.GetBytes("test message")); + + Assert.True(recoverLatch.Wait(TimeSpan.FromSeconds(5))); + + try + { + ch.BasicConsume(queueWithExceptionConsumer, true, "exception.consumer", consumerToRecover); + Assert.Fail("Expected an exception"); + } + catch (OperationInterruptedException e) + { + AssertShutdownError(e.ShutdownReason, 530); // NOT_ALLOWED - not allowed to reuse consumer tag + } + } + } +} diff --git a/projects/Unit/APIApproval.Approve.verified.txt b/projects/Test/Unit/APIApproval.Approve.verified.txt similarity index 77% rename from projects/Unit/APIApproval.Approve.verified.txt rename to projects/Test/Unit/APIApproval.Approve.verified.txt index d4ccb460b4..c2f83298d3 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 { @@ -28,8 +32,8 @@ namespace RabbitMQ.Client } public readonly struct AmqpTimestamp : System.IEquatable { + public readonly long UnixTime; public AmqpTimestamp(long unixTime) { } - public long UnixTime { get; } public bool Equals(RabbitMQ.Client.AmqpTimestamp other) { } public override bool Equals(object obj) { } public override int GetHashCode() { } @@ -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 readonly RabbitMQ.Client.ReadOnlyBasicProperties BasicProperties; + public readonly System.ReadOnlyMemory Body; + public readonly ulong DeliveryTag; + public readonly string Exchange; + public readonly uint MessageCount; + public readonly bool Redelivered; + public readonly string RoutingKey; 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 RabbitMQ.Client.ReadOnlyBasicProperties BasicProperties { get; } - public System.ReadOnlyMemory 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 { @@ -140,26 +142,26 @@ namespace RabbitMQ.Client } public sealed class ConnectionConfig { + public readonly System.Collections.Generic.IEnumerable AuthMechanisms; + public readonly System.Collections.Generic.IDictionary ClientProperties; + public readonly string? ClientProvidedName; + public readonly System.TimeSpan ContinuationTimeout; public RabbitMQ.Client.ICredentialsProvider CredentialsProvider; public RabbitMQ.Client.ICredentialsRefresher CredentialsRefresher; - public System.Collections.Generic.IList AuthMechanisms { get; } - public System.Collections.Generic.IDictionary ClientProperties { get; } - public string? ClientProvidedName { get; } - public System.TimeSpan ContinuationTimeout { get; } - public int DispatchConsumerConcurrency { get; } - public bool DispatchConsumersAsync { get; } - public System.TimeSpan HandshakeContinuationTimeout { get; } - public System.TimeSpan HeartbeatInterval { get; } - public ushort MaxChannelCount { get; } - public uint MaxFrameSize { get; } - public System.TimeSpan NetworkRecoveryInterval { get; } - public string Password { get; } - public System.TimeSpan RequestedConnectionTimeout { get; } - public bool TopologyRecoveryEnabled { get; } - public RabbitMQ.Client.TopologyRecoveryExceptionHandler TopologyRecoveryExceptionHandler { get; } - public RabbitMQ.Client.TopologyRecoveryFilter TopologyRecoveryFilter { get; } - public string UserName { get; } - public string VirtualHost { get; } + public readonly int DispatchConsumerConcurrency; + public readonly bool DispatchConsumersAsync; + public readonly System.TimeSpan HandshakeContinuationTimeout; + public readonly System.TimeSpan HeartbeatInterval; + public readonly ushort MaxChannelCount; + public readonly uint MaxFrameSize; + public readonly System.TimeSpan NetworkRecoveryInterval; + public readonly string Password; + public readonly System.TimeSpan RequestedConnectionTimeout; + public readonly bool TopologyRecoveryEnabled; + public readonly RabbitMQ.Client.TopologyRecoveryExceptionHandler TopologyRecoveryExceptionHandler; + public readonly RabbitMQ.Client.TopologyRecoveryFilter TopologyRecoveryFilter; + public readonly string UserName; + public readonly string VirtualHost; } public sealed class ConnectionFactory : RabbitMQ.Client.ConnectionFactoryBase, RabbitMQ.Client.IConnectionFactory { @@ -170,13 +172,13 @@ namespace RabbitMQ.Client public const string DefaultUser = "guest"; public const string DefaultVHost = "/"; public const uint MaximumMaxMessageSize = 536870912u; - public static readonly System.Collections.Generic.IList DefaultAuthMechanisms; + public static readonly System.Collections.Generic.IEnumerable DefaultAuthMechanisms; public static readonly System.TimeSpan DefaultConnectionTimeout; public static readonly RabbitMQ.Client.ICredentialsRefresher DefaultCredentialsRefresher; public static readonly System.TimeSpan DefaultHeartbeat; public ConnectionFactory() { } public System.Security.Authentication.SslProtocols AmqpUriSslProtocols { get; set; } - public System.Collections.Generic.IList AuthMechanisms { get; set; } + public System.Collections.Generic.IEnumerable AuthMechanisms { get; set; } public bool AutomaticRecoveryEnabled { get; set; } public System.Collections.Generic.IDictionary ClientProperties { get; set; } public string ClientProvidedName { get; set; } @@ -208,14 +210,21 @@ namespace RabbitMQ.Client public string VirtualHost { get; set; } public static System.Net.Sockets.AddressFamily DefaultAddressFamily { get; set; } public static System.Security.Authentication.SslProtocols DefaultAmqpUriSslProtocols { get; set; } - public RabbitMQ.Client.IAuthMechanismFactory AuthMechanismFactory(System.Collections.Generic.IList mechanismNames) { } + public RabbitMQ.Client.IAuthMechanismFactory AuthMechanismFactory(System.Collections.Generic.IEnumerable argServerMechanismNames) { } public RabbitMQ.Client.IConnection CreateConnection() { } - public RabbitMQ.Client.IConnection CreateConnection(System.Collections.Generic.IList endpoints) { } - public RabbitMQ.Client.IConnection CreateConnection(System.Collections.Generic.IList hostnames) { } + public RabbitMQ.Client.IConnection CreateConnection(System.Collections.Generic.IEnumerable endpoints) { } + public RabbitMQ.Client.IConnection CreateConnection(System.Collections.Generic.IEnumerable hostnames) { } public RabbitMQ.Client.IConnection CreateConnection(string clientProvidedName) { } 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 RabbitMQ.Client.IConnection CreateConnection(System.Collections.Generic.IEnumerable endpoints, string clientProvidedName) { } + public RabbitMQ.Client.IConnection CreateConnection(System.Collections.Generic.IEnumerable hostnames, string clientProvidedName) { } + public System.Threading.Tasks.ValueTask CreateConnectionAsync() { } + public System.Threading.Tasks.ValueTask CreateConnectionAsync(System.Collections.Generic.IEnumerable endpoints) { } + public System.Threading.Tasks.ValueTask CreateConnectionAsync(System.Collections.Generic.IEnumerable 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.IEnumerable endpoints, string clientProvidedName) { } + public System.Threading.Tasks.ValueTask CreateConnectionAsync(System.Collections.Generic.IEnumerable hostnames, string clientProvidedName) { } } public class ConnectionFactoryBase { @@ -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,49 @@ 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); + System.Threading.Tasks.ValueTask CloseAsync(ushort replyCode, string replyText, 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); } @@ -459,30 +486,47 @@ namespace RabbitMQ.Client { public static void Abort(this RabbitMQ.Client.IChannel channel) { } public static void Abort(this RabbitMQ.Client.IChannel channel, ushort replyCode, string replyText) { } + public static System.Threading.Tasks.ValueTask AbortAsync(this RabbitMQ.Client.IChannel channel) { } public static string BasicConsume(this RabbitMQ.Client.IChannel channel, string queue, bool autoAck, RabbitMQ.Client.IBasicConsumer consumer) { } public static string BasicConsume(this RabbitMQ.Client.IChannel channel, string queue, bool autoAck, string consumerTag, RabbitMQ.Client.IBasicConsumer consumer) { } public static string BasicConsume(this RabbitMQ.Client.IChannel channel, string queue, bool autoAck, string consumerTag, System.Collections.Generic.IDictionary arguments, RabbitMQ.Client.IBasicConsumer consumer) { } public static string BasicConsume(this RabbitMQ.Client.IChannel channel, RabbitMQ.Client.IBasicConsumer consumer, string queue, bool autoAck = false, string consumerTag = "", bool noLocal = false, bool exclusive = false, System.Collections.Generic.IDictionary arguments = null) { } + public static System.Threading.Tasks.ValueTask BasicConsumeAsync(this RabbitMQ.Client.IChannel channel, string queue, bool autoAck, RabbitMQ.Client.IBasicConsumer consumer) { } + public static System.Threading.Tasks.ValueTask BasicConsumeAsync(this RabbitMQ.Client.IChannel channel, string queue, bool autoAck, string consumerTag, RabbitMQ.Client.IBasicConsumer consumer) { } + public static System.Threading.Tasks.ValueTask BasicConsumeAsync(this RabbitMQ.Client.IChannel channel, string queue, bool autoAck, string consumerTag, System.Collections.Generic.IDictionary arguments, RabbitMQ.Client.IBasicConsumer consumer) { } + public static System.Threading.Tasks.ValueTask BasicConsumeAsync(this RabbitMQ.Client.IChannel channel, RabbitMQ.Client.IBasicConsumer consumer, string queue, bool autoAck = false, string consumerTag = "", bool noLocal = false, bool exclusive = false, System.Collections.Generic.IDictionary arguments = null) { } public static void BasicPublish(this RabbitMQ.Client.IChannel channel, RabbitMQ.Client.CachedString exchange, RabbitMQ.Client.CachedString routingKey, System.ReadOnlyMemory body = default, bool mandatory = false) { } public static void BasicPublish(this RabbitMQ.Client.IChannel channel, string exchange, string routingKey, System.ReadOnlyMemory body = default, bool mandatory = false) { } public static void BasicPublish(this RabbitMQ.Client.IChannel channel, RabbitMQ.Client.PublicationAddress addr, in T basicProperties, System.ReadOnlyMemory body) where T : RabbitMQ.Client.IReadOnlyBasicProperties, RabbitMQ.Client.IAmqpHeader { } public static System.Threading.Tasks.ValueTask BasicPublishAsync(this RabbitMQ.Client.IChannel channel, RabbitMQ.Client.CachedString exchange, RabbitMQ.Client.CachedString routingKey, System.ReadOnlyMemory body = default, bool mandatory = false) { } 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 System.Threading.Tasks.ValueTask BasicPublishAsync(this RabbitMQ.Client.IChannel channel, RabbitMQ.Client.PublicationAddress addr, in T basicProperties, System.ReadOnlyMemory body) + where T : RabbitMQ.Client.IReadOnlyBasicProperties, RabbitMQ.Client.IAmqpHeader { } 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 System.Threading.Tasks.ValueTask CloseAsync(this RabbitMQ.Client.IChannel channel, ushort replyCode, string replyText) { } public static void ExchangeBind(this RabbitMQ.Client.IChannel channel, string destination, string source, string routingKey, System.Collections.Generic.IDictionary arguments = null) { } + public static System.Threading.Tasks.ValueTask ExchangeBindAsync(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) { } + public static System.Threading.Tasks.ValueTask ExchangeDeclareAsync(this RabbitMQ.Client.IChannel channel, string exchange, string type, bool durable = false, bool autoDelete = false, System.Collections.Generic.IDictionary arguments = null) { } public static void ExchangeDeclareNoWait(this RabbitMQ.Client.IChannel channel, string exchange, string type, bool durable = false, bool autoDelete = false, System.Collections.Generic.IDictionary arguments = null) { } public static void ExchangeDelete(this RabbitMQ.Client.IChannel channel, string exchange, bool ifUnused = false) { } + public static System.Threading.Tasks.ValueTask ExchangeDeleteAsync(this RabbitMQ.Client.IChannel channel, string exchange, bool ifUnused = false) { } public static void ExchangeDeleteNoWait(this RabbitMQ.Client.IChannel channel, string exchange, bool ifUnused = false) { } public static void ExchangeUnbind(this RabbitMQ.Client.IChannel channel, string destination, string source, string routingKey, System.Collections.Generic.IDictionary arguments = null) { } + public static System.Threading.Tasks.ValueTask ExchangeUnbindAsync(this RabbitMQ.Client.IChannel channel, string destination, string source, string routingKey, System.Collections.Generic.IDictionary arguments = null) { } public static void QueueBind(this RabbitMQ.Client.IChannel channel, string queue, string exchange, string routingKey, System.Collections.Generic.IDictionary arguments = null) { } + public static System.Threading.Tasks.ValueTask QueueBindAsync(this RabbitMQ.Client.IChannel channel, string queue, string exchange, string routingKey, System.Collections.Generic.IDictionary arguments = null) { } public static RabbitMQ.Client.QueueDeclareOk QueueDeclare(this RabbitMQ.Client.IChannel channel, string queue = "", bool durable = false, bool exclusive = true, bool autoDelete = true, System.Collections.Generic.IDictionary arguments = null) { } + public static System.Threading.Tasks.ValueTask QueueDeclareAsync(this RabbitMQ.Client.IChannel channel, string queue = "", bool durable = false, bool exclusive = true, bool autoDelete = true, System.Collections.Generic.IDictionary arguments = null) { } public static uint QueueDelete(this RabbitMQ.Client.IChannel channel, string queue, bool ifUnused = false, bool ifEmpty = false) { } + public static System.Threading.Tasks.ValueTask QueueDeleteAsync(this RabbitMQ.Client.IChannel channel, string queue, bool ifUnused = false, bool ifEmpty = false) { } public static void QueueDeleteNoWait(this RabbitMQ.Client.IChannel channel, string queue, bool ifUnused = false, bool ifEmpty = false) { } public static void QueueUnbind(this RabbitMQ.Client.IChannel channel, string queue, string exchange, string routingKey, System.Collections.Generic.IDictionary arguments = null) { } + public static System.Threading.Tasks.ValueTask QueueUnbindAsync(this RabbitMQ.Client.IChannel channel, string queue, string exchange, string routingKey, System.Collections.Generic.IDictionary arguments = null) { } } public interface IConnection : RabbitMQ.Client.INetworkConnection, System.IDisposable { @@ -496,18 +540,20 @@ namespace RabbitMQ.Client bool IsOpen { get; } RabbitMQ.Client.IProtocol Protocol { get; } System.Collections.Generic.IDictionary ServerProperties { get; } - System.Collections.Generic.IList ShutdownReport { get; } + System.Collections.Generic.IEnumerable ShutdownReport { get; } event System.EventHandler CallbackException; event System.EventHandler ConnectionBlocked; event System.EventHandler ConnectionRecoveryError; event System.EventHandler ConnectionShutdown; event System.EventHandler ConnectionUnblocked; event System.EventHandler ConsumerTagChangeAfterRecovery; - event System.EventHandler QueueNameChangeAfterRecovery; + event System.EventHandler QueueNameChangedAfterRecovery; event System.EventHandler RecoveringConsumer; event System.EventHandler RecoverySucceeded; void Close(ushort reasonCode, string reasonText, System.TimeSpan timeout, bool abort); + System.Threading.Tasks.ValueTask CloseAsync(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 @@ -516,10 +562,18 @@ namespace RabbitMQ.Client public static void Abort(this RabbitMQ.Client.IConnection connection, System.TimeSpan timeout) { } public static void Abort(this RabbitMQ.Client.IConnection connection, ushort reasonCode, string reasonText) { } public static void Abort(this RabbitMQ.Client.IConnection connection, ushort reasonCode, string reasonText, System.TimeSpan timeout) { } + public static System.Threading.Tasks.ValueTask AbortAsync(this RabbitMQ.Client.IConnection connection) { } + public static System.Threading.Tasks.ValueTask AbortAsync(this RabbitMQ.Client.IConnection connection, System.TimeSpan timeout) { } + public static System.Threading.Tasks.ValueTask AbortAsync(this RabbitMQ.Client.IConnection connection, ushort reasonCode, string reasonText) { } + public static System.Threading.Tasks.ValueTask AbortAsync(this RabbitMQ.Client.IConnection connection, ushort reasonCode, string reasonText, System.TimeSpan timeout) { } public static void Close(this RabbitMQ.Client.IConnection connection) { } public static void Close(this RabbitMQ.Client.IConnection connection, System.TimeSpan timeout) { } public static void Close(this RabbitMQ.Client.IConnection connection, ushort reasonCode, string reasonText) { } public static void Close(this RabbitMQ.Client.IConnection connection, ushort reasonCode, string reasonText, System.TimeSpan timeout) { } + public static System.Threading.Tasks.ValueTask CloseAsync(this RabbitMQ.Client.IConnection connection) { } + public static System.Threading.Tasks.ValueTask CloseAsync(this RabbitMQ.Client.IConnection connection, System.TimeSpan timeout) { } + public static System.Threading.Tasks.ValueTask CloseAsync(this RabbitMQ.Client.IConnection connection, ushort reasonCode, string reasonText) { } + public static System.Threading.Tasks.ValueTask CloseAsync(this RabbitMQ.Client.IConnection connection, ushort reasonCode, string reasonText, System.TimeSpan timeout) { } } public interface IConnectionFactory { @@ -538,13 +592,19 @@ namespace RabbitMQ.Client System.Uri Uri { get; set; } string UserName { get; set; } string VirtualHost { get; set; } - RabbitMQ.Client.IAuthMechanismFactory AuthMechanismFactory(System.Collections.Generic.IList mechanismNames); + RabbitMQ.Client.IAuthMechanismFactory AuthMechanismFactory(System.Collections.Generic.IEnumerable mechanismNames); RabbitMQ.Client.IConnection CreateConnection(); - RabbitMQ.Client.IConnection CreateConnection(System.Collections.Generic.IList endpoints); - RabbitMQ.Client.IConnection CreateConnection(System.Collections.Generic.IList hostnames); + RabbitMQ.Client.IConnection CreateConnection(System.Collections.Generic.IEnumerable endpoints); + RabbitMQ.Client.IConnection CreateConnection(System.Collections.Generic.IEnumerable hostnames); 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); + RabbitMQ.Client.IConnection CreateConnection(System.Collections.Generic.IEnumerable endpoints, string clientProvidedName); + RabbitMQ.Client.IConnection CreateConnection(System.Collections.Generic.IEnumerable hostnames, string clientProvidedName); + System.Threading.Tasks.ValueTask CreateConnectionAsync(); + System.Threading.Tasks.ValueTask CreateConnectionAsync(System.Collections.Generic.IEnumerable endpoints); + System.Threading.Tasks.ValueTask CreateConnectionAsync(System.Collections.Generic.IEnumerable hostnames); + System.Threading.Tasks.ValueTask CreateConnectionAsync(string clientProvidedName); + System.Threading.Tasks.ValueTask CreateConnectionAsync(System.Collections.Generic.IEnumerable endpoints, string clientProvidedName); + System.Threading.Tasks.ValueTask CreateConnectionAsync(System.Collections.Generic.IEnumerable hostnames, string clientProvidedName); } public interface ICredentialsProvider { @@ -669,26 +729,26 @@ namespace RabbitMQ.Client } public static class Protocols { - public static RabbitMQ.Client.IProtocol AMQP_0_9_1 { get; } - public static RabbitMQ.Client.IProtocol DefaultProtocol { get; } + public static readonly RabbitMQ.Client.IProtocol AMQP_0_9_1; + public static readonly RabbitMQ.Client.IProtocol DefaultProtocol; } public class PublicationAddress { + public readonly string ExchangeName; + public readonly string ExchangeType; + public readonly string RoutingKey; public static readonly System.Text.RegularExpressions.Regex PSEUDO_URI_PARSER; public PublicationAddress(string exchangeType, string exchangeName, string routingKey) { } - public string ExchangeName { get; } - public string ExchangeType { get; } - public string RoutingKey { get; } public override string ToString() { } public static RabbitMQ.Client.PublicationAddress Parse(string uriLikeString) { } public static bool TryParse(string uriLikeString, out RabbitMQ.Client.PublicationAddress result) { } } public class QueueDeclareOk { + public readonly uint ConsumerCount; + public readonly uint MessageCount; + public readonly string QueueName; public QueueDeclareOk(string queueName, uint messageCount, uint consumerCount) { } - public uint ConsumerCount { get; } - public uint MessageCount { get; } - public string QueueName { get; } public static string op_Implicit(RabbitMQ.Client.QueueDeclareOk declareOk) { } } public readonly struct ReadOnlyBasicProperties : RabbitMQ.Client.IReadOnlyBasicProperties @@ -839,44 +899,43 @@ namespace RabbitMQ.Client.Events } public abstract class BaseExceptionEventArgs : System.EventArgs { + public readonly System.Collections.Generic.IDictionary Detail; + public readonly System.Exception Exception; protected BaseExceptionEventArgs(System.Collections.Generic.IDictionary detail, System.Exception exception) { } - public System.Collections.Generic.IDictionary Detail { get; } - public System.Exception Exception { get; } } public class BasicAckEventArgs : System.EventArgs { - public BasicAckEventArgs() { } - public ulong DeliveryTag { get; set; } - public bool Multiple { get; set; } + public readonly ulong DeliveryTag; + public readonly bool Multiple; + public BasicAckEventArgs(ulong deliveryTag, bool multiple) { } } public class BasicDeliverEventArgs : System.EventArgs { - public BasicDeliverEventArgs() { } + public readonly RabbitMQ.Client.ReadOnlyBasicProperties BasicProperties; + public readonly System.ReadOnlyMemory Body; + public readonly string ConsumerTag; + public readonly ulong DeliveryTag; + public readonly string Exchange; + public readonly bool Redelivered; + public readonly string RoutingKey; public BasicDeliverEventArgs(string consumerTag, ulong deliveryTag, bool redelivered, string exchange, string routingKey, in RabbitMQ.Client.ReadOnlyBasicProperties properties, System.ReadOnlyMemory body) { } - public RabbitMQ.Client.ReadOnlyBasicProperties BasicProperties { get; set; } - public System.ReadOnlyMemory Body { get; set; } - public string ConsumerTag { get; set; } - public ulong DeliveryTag { get; set; } - public string Exchange { get; set; } - public bool Redelivered { get; set; } - public string RoutingKey { get; set; } } public class BasicNackEventArgs : System.EventArgs { - public BasicNackEventArgs() { } - public ulong DeliveryTag { get; set; } - public bool Multiple { get; set; } - public bool Requeue { get; set; } + public readonly ulong DeliveryTag; + public readonly bool Multiple; + public readonly bool Requeue; + public BasicNackEventArgs(ulong deliveryTag, bool multiple, bool requeue) { } } 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 System.ReadOnlyMemory 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, System.ReadOnlyMemory body) { } } public class CallbackExceptionEventArgs : RabbitMQ.Client.Events.BaseExceptionEventArgs { @@ -886,24 +945,24 @@ namespace RabbitMQ.Client.Events } public class ConnectionBlockedEventArgs : System.EventArgs { + public readonly string Reason; public ConnectionBlockedEventArgs(string reason) { } - public string Reason { get; } } public sealed class ConnectionRecoveryErrorEventArgs : System.EventArgs { + public readonly System.Exception Exception; public ConnectionRecoveryErrorEventArgs(System.Exception ex) { } - public System.Exception Exception { get; } } public class ConsumerEventArgs : System.EventArgs { + public readonly string[] ConsumerTags; public ConsumerEventArgs(string[] consumerTags) { } - public string[] ConsumerTags { get; } } public sealed class ConsumerTagChangedAfterRecoveryEventArgs : System.EventArgs { + public readonly string TagAfter; + public readonly string TagBefore; public ConsumerTagChangedAfterRecoveryEventArgs(string tagBefore, string tagAfter) { } - public string TagAfter { get; } - public string TagBefore { get; } } public class EventingBasicConsumer : RabbitMQ.Client.DefaultBasicConsumer { @@ -919,14 +978,14 @@ namespace RabbitMQ.Client.Events } public class FlowControlEventArgs : System.EventArgs { + public readonly bool Active; public FlowControlEventArgs(bool active) { } - public bool Active { get; } } public sealed class QueueNameChangedAfterRecoveryEventArgs : System.EventArgs { + public readonly string NameAfter; + public readonly string NameBefore; public QueueNameChangedAfterRecoveryEventArgs(string nameBefore, string nameAfter) { } - public string NameAfter { get; } - public string NameBefore { get; } } public class RecoveringConsumerEventArgs { 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 1ba197a526..9e522bc6ee 100644 --- a/projects/Unit/TestTcpClientAdapter.cs +++ b/projects/Test/Unit/TestTcpClientAdapter.cs @@ -31,9 +31,10 @@ using System.Net; using System.Net.Sockets; +using RabbitMQ.Client; 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 fdbf59f6eb..0000000000 --- a/projects/TestApplications/MassPublish/Program.cs +++ /dev/null @@ -1,87 +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 = 64; - private const int ItemsPerBatch = 8192; - - private static int messagesSent; - private static int messagesReceived; - private static AutoResetEvent doneEvent; - - public static void Main() - { - var r = new Random(); - byte[] payload = new byte[4096]; - r.NextBytes(payload); - - 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); - - 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(); - } - - 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/TestConnectionRecovery.cs b/projects/Unit/TestConnectionRecovery.cs deleted file mode 100644 index 2c8c42ab50..0000000000 --- a/projects/Unit/TestConnectionRecovery.cs +++ /dev/null @@ -1,1813 +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.Generic; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using RabbitMQ.Client.Events; -using RabbitMQ.Client.Exceptions; -using RabbitMQ.Client.Framing.Impl; -using RabbitMQ.Client.Impl; -using Xunit; -using Xunit.Abstractions; - -#pragma warning disable 0618 - -namespace RabbitMQ.Client.Unit -{ - public class TestConnectionRecovery : IntegrationFixture - { - private readonly byte[] _messageBody; - private readonly ushort _totalMessageCount = 8192; - private readonly ushort _closeAtCount = 16; - private 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() - { - // TODO LRB not really necessary - if (_channel.IsOpen) - { - _channel.Close(); - } - - if (_conn.IsOpen) - { - _conn.Close(); - } - - Unblock(); - } - - [Fact] - public void TestBasicAckAfterChannelRecovery() - { - var allMessagesSeenLatch = new ManualResetEventSlim(false); - var cons = new AckingBasicConsumer(_channel, _totalMessageCount, allMessagesSeenLatch); - - string queueName = _channel.QueueDeclare(_queueName, false, false, false, null).QueueName; - Assert.Equal(queueName, _queueName); - - _channel.BasicQos(0, 1, false); - string consumerTag = _channel.BasicConsume(queueName, false, cons); - - ManualResetEventSlim sl = PrepareForShutdown(_conn); - ManualResetEventSlim rl = PrepareForRecovery(_conn); - - PublishMessagesWhileClosingConn(queueName); - - Wait(sl); - Wait(rl); - Wait(allMessagesSeenLatch); - } - - [Fact] - public void TestBasicNackAfterChannelRecovery() - { - var allMessagesSeenLatch = new ManualResetEventSlim(false); - var cons = new NackingBasicConsumer(_channel, _totalMessageCount, allMessagesSeenLatch); - - string queueName = _channel.QueueDeclare(_queueName, false, false, false, null).QueueName; - Assert.Equal(queueName, _queueName); - - _channel.BasicQos(0, 1, false); - string consumerTag = _channel.BasicConsume(queueName, false, cons); - - ManualResetEventSlim sl = PrepareForShutdown(_conn); - ManualResetEventSlim rl = PrepareForRecovery(_conn); - - PublishMessagesWhileClosingConn(queueName); - - Wait(sl); - Wait(rl); - Wait(allMessagesSeenLatch); - } - - [Fact] - public void TestBasicRejectAfterChannelRecovery() - { - var allMessagesSeenLatch = new ManualResetEventSlim(false); - var cons = new RejectingBasicConsumer(_channel, _totalMessageCount, allMessagesSeenLatch); - - string queueName = _channel.QueueDeclare(_queueName, false, false, false, null).QueueName; - Assert.Equal(queueName, _queueName); - - _channel.BasicQos(0, 1, false); - string consumerTag = _channel.BasicConsume(queueName, false, cons); - - ManualResetEventSlim sl = PrepareForShutdown(_conn); - ManualResetEventSlim rl = PrepareForRecovery(_conn); - - PublishMessagesWhileClosingConn(queueName); - - Wait(sl); - Wait(rl); - Wait(allMessagesSeenLatch); - } - - [Fact] - public void TestBasicAckAfterBasicGetAndChannelRecovery() - { - string q = GenerateQueueName(); - _channel.QueueDeclare(q, false, false, false, null); - // create an offset - _channel.BasicPublish("", q, _messageBody); - Thread.Sleep(50); - BasicGetResult g = _channel.BasicGet(q, false); - CloseAndWaitForRecovery(); - Assert.True(_conn.IsOpen); - Assert.True(_channel.IsOpen); - // ack the message after recovery - this should be out of range and ignored - _channel.BasicAck(g.DeliveryTag, false); - // do a sync operation to 'check' there is no channel exception - _channel.BasicGet(q, false); - } - - [Fact] - public void TestBasicAckEventHandlerRecovery() - { - _channel.ConfirmSelect(); - var latch = new ManualResetEventSlim(false); - ((AutorecoveringChannel)_channel).BasicAcks += (m, args) => latch.Set(); - ((AutorecoveringChannel)_channel).BasicNacks += (m, args) => latch.Set(); - - CloseAndWaitForRecovery(); - CloseAndWaitForRecovery(); - Assert.True(_channel.IsOpen); - - WithTemporaryNonExclusiveQueue(_channel, (m, q) => m.BasicPublish("", q, _messageBody)); - Wait(latch); - } - - [Fact] - public void TestBasicConnectionRecovery() - { - Assert.True(_conn.IsOpen); - CloseAndWaitForRecovery(); - 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() - { - Assert.True(_conn.IsOpen); - RestartServerAndWaitForRecovery(); - Assert.True(_conn.IsOpen); - } - - [Fact] - public void TestBasicChannelRecovery() - { - Assert.True(_channel.IsOpen); - CloseAndWaitForRecovery(); - Assert.True(_channel.IsOpen); - } - - [Fact] - public void TestBasicChannelRecoveryOnServerRestart() - { - Assert.True(_channel.IsOpen); - RestartServerAndWaitForRecovery(); - Assert.True(_channel.IsOpen); - } - - [Fact] - public void TestBlockedListenersRecovery() - { - var latch = new ManualResetEventSlim(false); - _conn.ConnectionBlocked += (c, reason) => latch.Set(); - CloseAndWaitForRecovery(); - CloseAndWaitForRecovery(); - - Block(); - Wait(latch); - - Unblock(); - } - - [Fact] - public void TestClientNamedQueueRecovery() - { - string s = "dotnet-client.test.recovery.q1"; - WithTemporaryNonExclusiveQueue(_channel, (m, q) => - { - CloseAndWaitForRecovery(); - AssertQueueRecovery(m, q, false); - _channel.QueueDelete(q); - }, s); - } - - [Fact] - public void TestClientNamedQueueRecoveryNoWait() - { - string s = "dotnet-client.test.recovery.q1-nowait"; - WithTemporaryQueueNoWait(_channel, (m, q) => - { - CloseAndWaitForRecovery(); - AssertQueueRecovery(m, q); - }, s); - } - - [Fact] - public void TestClientNamedQueueRecoveryOnServerRestart() - { - string s = "dotnet-client.test.recovery.q1"; - WithTemporaryNonExclusiveQueue(_channel, (m, q) => - { - RestartServerAndWaitForRecovery(); - AssertQueueRecovery(m, q, false); - _channel.QueueDelete(q); - }, 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() - { - string q = _channel.QueueDeclare(GenerateQueueName(), false, false, false, null).QueueName; - int n = 1024; - - for (int i = 0; i < n; i++) - { - var cons = new EventingBasicConsumer(_channel); - _channel.BasicConsume(q, true, cons); - } - - var latch = new ManualResetEventSlim(false); - ((AutorecoveringConnection)_conn).ConsumerTagChangeAfterRecovery += (prev, current) => latch.Set(); - - CloseAndWaitForRecovery(); - Wait(latch); - 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() - { - AssertRecordedExchanges((AutorecoveringConnection)_conn, 0); - for (int i = 0; i < 3; i++) - { - string x1 = $"source-{Guid.NewGuid()}"; - _channel.ExchangeDeclare(x1, "fanout", false, true, null); - string x2 = $"destination-{Guid.NewGuid()}"; - _channel.ExchangeDeclare(x2, "fanout", false, false, null); - _channel.ExchangeBind(x2, x1, ""); - _channel.ExchangeDelete(x2); - } - AssertRecordedExchanges((AutorecoveringConnection)_conn, 0); - } - - [Fact] - public void TestDeclarationOfManyAutoDeleteExchangesWithTransientExchangesThatAreUnbound() - { - AssertRecordedExchanges((AutorecoveringConnection)_conn, 0); - for (int i = 0; i < 1000; i++) - { - string x1 = $"source-{Guid.NewGuid()}"; - _channel.ExchangeDeclare(x1, "fanout", false, true, null); - string x2 = $"destination-{Guid.NewGuid()}"; - _channel.ExchangeDeclare(x2, "fanout", false, false, null); - _channel.ExchangeBind(x2, x1, ""); - _channel.ExchangeUnbind(x2, x1, ""); - _channel.ExchangeDelete(x2); - } - AssertRecordedExchanges((AutorecoveringConnection)_conn, 0); - } - - [Fact] - public void TestDeclarationOfManyAutoDeleteExchangesWithTransientQueuesThatAreDeleted() - { - AssertRecordedExchanges((AutorecoveringConnection)_conn, 0); - for (int i = 0; i < 1000; i++) - { - string x = Guid.NewGuid().ToString(); - _channel.ExchangeDeclare(x, "fanout", false, true, null); - QueueDeclareOk q = _channel.QueueDeclare(); - _channel.QueueBind(q, x, ""); - _channel.QueueDelete(q); - } - AssertRecordedExchanges((AutorecoveringConnection)_conn, 0); - } - - [Fact] - public void TestDeclarationOfManyAutoDeleteExchangesWithTransientQueuesThatAreUnbound() - { - AssertRecordedExchanges((AutorecoveringConnection)_conn, 0); - for (int i = 0; i < 1000; i++) - { - string x = Guid.NewGuid().ToString(); - _channel.ExchangeDeclare(x, "fanout", false, true, null); - QueueDeclareOk q = _channel.QueueDeclare(); - _channel.QueueBind(q, x, ""); - _channel.QueueUnbind(q, x, "", null); - } - AssertRecordedExchanges((AutorecoveringConnection)_conn, 0); - } - - [Fact] - public void TestDeclarationOfManyAutoDeleteQueuesWithTransientConsumer() - { - AssertRecordedQueues((AutorecoveringConnection)_conn, 0); - for (int i = 0; i < 1000; i++) - { - string q = Guid.NewGuid().ToString(); - _channel.QueueDeclare(q, false, false, true, null); - var dummy = new EventingBasicConsumer(_channel); - string tag = _channel.BasicConsume(q, true, dummy); - _channel.BasicCancel(tag); - } - AssertRecordedQueues((AutorecoveringConnection)_conn, 0); - } - - [Fact] - public void TestExchangeRecovery() - { - string x = "dotnet-client.test.recovery.x1"; - DeclareNonDurableExchange(_channel, x); - CloseAndWaitForRecovery(); - AssertExchangeRecovery(_channel, x); - _channel.ExchangeDelete(x); - } - - [Fact] - public void TestExchangeRecoveryWithNoWait() - { - string x = "dotnet-client.test.recovery.x1-nowait"; - DeclareNonDurableExchangeNoWait(_channel, x); - CloseAndWaitForRecovery(); - AssertExchangeRecovery(_channel, x); - _channel.ExchangeDelete(x); - } - - [Fact] - public void TestExchangeToExchangeBindingRecovery() - { - string q = _channel.QueueDeclare("", false, false, false, null).QueueName; - string x1 = "amq.fanout"; - string x2 = GenerateExchangeName(); - - _channel.ExchangeDeclare(x2, "fanout"); - _channel.ExchangeBind(x1, x2, ""); - _channel.QueueBind(q, x1, ""); - - try - { - CloseAndWaitForRecovery(); - Assert.True(_channel.IsOpen); - _channel.BasicPublish(x2, "", _encoding.GetBytes("msg")); - AssertMessageCount(q, 1); - } - finally - { - WithTemporaryChannel(m => - { - m.ExchangeDelete(x2); - m.QueueDelete(q); - }); - } - } - - [Fact] - public void TestQueueRecoveryWithManyQueues() - { - var qs = new List(); - int n = 1024; - for (int i = 0; i < n; i++) - { - qs.Add(_channel.QueueDeclare(GenerateQueueName(), false, false, false, null).QueueName); - } - CloseAndWaitForRecovery(); - Assert.True(_channel.IsOpen); - foreach (string q in qs) - { - AssertQueueRecovery(_channel, q, false); - _channel.QueueDelete(q); - } - } - - // rabbitmq/rabbitmq-dotnet-client#43 - [Fact] - public void TestClientNamedTransientAutoDeleteQueueAndBindingRecovery() - { - string q = Guid.NewGuid().ToString(); - string x = "tmp-fanout"; - IChannel ch = _conn.CreateChannel(); - ch.QueueDelete(q); - ch.ExchangeDelete(x); - ch.ExchangeDeclare(exchange: x, type: "fanout"); - ch.QueueDeclare(queue: q, durable: false, exclusive: false, autoDelete: true, arguments: null); - ch.QueueBind(queue: q, exchange: x, routingKey: ""); - RestartServerAndWaitForRecovery(); - Assert.True(ch.IsOpen); - ch.ConfirmSelect(); - ch.QueuePurge(q); - 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); - Assert.Equal(1u, ok.MessageCount); - ch.QueueDelete(q); - ch.ExchangeDelete(x); - } - - // rabbitmq/rabbitmq-dotnet-client#43 - [Fact] - public void TestServerNamedTransientAutoDeleteQueueAndBindingRecovery() - { - string x = "tmp-fanout"; - IChannel ch = _conn.CreateChannel(); - ch.ExchangeDelete(x); - ch.ExchangeDeclare(exchange: x, type: "fanout"); - string q = ch.QueueDeclare(queue: "", durable: false, exclusive: false, autoDelete: true, arguments: null).QueueName; - string nameBefore = q; - string nameAfter = null; - var latch = new ManualResetEventSlim(false); - ((AutorecoveringConnection)_conn).QueueNameChangeAfterRecovery += (source, ea) => - { - nameBefore = ea.NameBefore; - nameAfter = ea.NameAfter; - latch.Set(); - }; - ch.QueueBind(queue: nameBefore, exchange: x, routingKey: ""); - RestartServerAndWaitForRecovery(); - Wait(latch); - 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); - Assert.Equal(1u, ok.MessageCount); - ch.QueueDelete(q); - ch.ExchangeDelete(x); - } - - [Fact] - public void TestRecoveryEventHandlersOnConnection() - { - int counter = 0; - ((AutorecoveringConnection)_conn).RecoverySucceeded += (source, ea) => Interlocked.Increment(ref counter); - - CloseAndWaitForRecovery(); - CloseAndWaitForRecovery(); - CloseAndWaitForRecovery(); - CloseAndWaitForRecovery(); - Assert.True(_conn.IsOpen); - Assert.True(counter >= 3); - } - - [Fact] - public void TestRecoveryEventHandlersOnChannel() - { - int counter = 0; - ((AutorecoveringChannel)_channel).Recovery += (source, ea) => Interlocked.Increment(ref counter); - - CloseAndWaitForRecovery(); - CloseAndWaitForRecovery(); - CloseAndWaitForRecovery(); - CloseAndWaitForRecovery(); - Assert.True(_channel.IsOpen); - Assert.True(counter >= 3); - } - - [Theory] - [InlineData(1)] - [InlineData(3)] - public void TestRecoveringConsumerHandlerOnConnection(int iterations) - { - string q = _channel.QueueDeclare(GenerateQueueName(), false, false, false, null).QueueName; - var cons = new EventingBasicConsumer(_channel); - _channel.BasicConsume(q, true, cons); - - int counter = 0; - ((AutorecoveringConnection)_conn).RecoveringConsumer += (sender, args) => Interlocked.Increment(ref counter); - - for (int i = 0; i < iterations; i++) - { - CloseAndWaitForRecovery(); - } - - Assert.Equal(iterations, counter); - } - - [Fact] - public void TestRecoveringConsumerHandlerOnConnection_EventArgumentsArePassedDown() - { - var myArgs = new Dictionary { { "first-argument", "some-value" } }; - string q = _channel.QueueDeclare(GenerateQueueName(), false, false, false, null).QueueName; - var cons = new EventingBasicConsumer(_channel); - string expectedCTag = _channel.BasicConsume(cons, q, arguments: myArgs); - - bool ctagMatches = false; - bool consumerArgumentMatches = false; - ((AutorecoveringConnection)_conn).RecoveringConsumer += (sender, args) => - { - // We cannot assert here because NUnit throws when an assertion fails. This exception is caught and - // passed to a CallbackExceptionHandler, instead of failing the test. Instead, we have to do this trick - // and assert in the test function. - ctagMatches = args.ConsumerTag == expectedCTag; - consumerArgumentMatches = (string)args.ConsumerArguments["first-argument"] == "some-value"; - args.ConsumerArguments["first-argument"] = "event-handler-set-this-value"; - }; - - CloseAndWaitForRecovery(); - Assert.True(ctagMatches, "expected consumer tag to match"); - Assert.True(consumerArgumentMatches, "expected consumer arguments to match"); - string actualVal = (string)Assert.Contains("first-argument", myArgs as IDictionary); - 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() - { - string q = _channel.QueueDeclare("", false, false, false, null).QueueName; - string x = "amq.fanout"; - _channel.QueueBind(q, x, ""); - - string nameBefore = q; - string nameAfter = null; - - var latch = new ManualResetEventSlim(false); - var connection = (AutorecoveringConnection)_conn; - connection.RecoverySucceeded += (source, ea) => latch.Set(); - connection.QueueNameChangeAfterRecovery += (source, ea) => { nameAfter = ea.NameAfter; }; - - CloseAndWaitForRecovery(); - Wait(latch); - - Assert.NotNull(nameAfter); - Assert.StartsWith("amq.", nameBefore); - Assert.StartsWith("amq.", nameAfter); - Assert.NotEqual(nameBefore, nameAfter); - - _channel.QueueDeclarePassive(nameAfter); - } - - [Fact] - public void TestShutdownEventHandlersRecoveryOnConnection() - { - int counter = 0; - _conn.ConnectionShutdown += (c, args) => Interlocked.Increment(ref counter); - - Assert.True(_conn.IsOpen); - CloseAndWaitForRecovery(); - CloseAndWaitForRecovery(); - CloseAndWaitForRecovery(); - CloseAndWaitForRecovery(); - Assert.True(_conn.IsOpen); - - Assert.True(counter >= 3); - } - - [Fact] - public void TestShutdownEventHandlersRecoveryOnConnectionAfterDelayedServerRestart() - { - int counter = 0; - _conn.ConnectionShutdown += (c, args) => Interlocked.Increment(ref counter); - ManualResetEventSlim shutdownLatch = PrepareForShutdown(_conn); - ManualResetEventSlim recoveryLatch = PrepareForRecovery((AutorecoveringConnection)_conn); - - Assert.True(_conn.IsOpen); - - try - { - StopRabbitMQ(); - Console.WriteLine("Stopped RabbitMQ. About to sleep for multiple recovery intervals..."); - Thread.Sleep(7000); - } - finally - { - StartRabbitMQ(); - } - - Wait(shutdownLatch, TimeSpan.FromSeconds(30)); - Wait(recoveryLatch, TimeSpan.FromSeconds(30)); - Assert.True(_conn.IsOpen); - Assert.True(counter >= 1); - } - - [Fact] - public void TestShutdownEventHandlersRecoveryOnChannel() - { - int counter = 0; - _channel.ChannelShutdown += (c, args) => Interlocked.Increment(ref counter); - - Assert.True(_channel.IsOpen); - CloseAndWaitForRecovery(); - CloseAndWaitForRecovery(); - CloseAndWaitForRecovery(); - CloseAndWaitForRecovery(); - Assert.True(_channel.IsOpen); - - Assert.True(counter >= 3); - } - - [Fact] - public void TestRecoverTopologyOnDisposedChannel() - { - string x = GenerateExchangeName(); - string q = GenerateQueueName(); - const string rk = "routing-key"; - - using (IChannel m = _conn.CreateChannel()) - { - m.ExchangeDeclare(exchange: x, type: "fanout"); - m.QueueDeclare(q, false, false, false, null); - m.QueueBind(q, x, rk); - } - - var cons = new EventingBasicConsumer(_channel); - _channel.BasicConsume(q, true, cons); - AssertConsumerCount(_channel, q, 1); - - CloseAndWaitForRecovery(); - AssertConsumerCount(_channel, q, 1); - - var latch = new ManualResetEventSlim(false); - cons.Received += (s, args) => latch.Set(); - - _channel.BasicPublish("", q, _messageBody); - Wait(latch); - - _channel.QueueUnbind(q, x, rk); - _channel.ExchangeDelete(x); - _channel.QueueDelete(q); - } - - [Fact(Skip = "TODO-FLAKY")] - public void TestPublishRpcRightAfterReconnect() - { - string testQueueName = $"dotnet-client.test.{nameof(TestPublishRpcRightAfterReconnect)}"; - _channel.QueueDeclare(testQueueName, false, false, false, null); - var replyConsumer = new EventingBasicConsumer(_channel); - _channel.BasicConsume("amq.rabbitmq.reply-to", true, replyConsumer); - var properties = new BasicProperties(); - properties.ReplyTo = "amq.rabbitmq.reply-to"; - - TimeSpan doneSpan = TimeSpan.FromMilliseconds(100); - var done = new ManualResetEventSlim(false); - Task.Run(() => - { - try - { - - CloseAndWaitForRecovery(); - } - finally - { - done.Set(); - } - }); - - while (!done.IsSet) - { - try - { - _channel.BasicPublish(string.Empty, testQueueName, properties, _messageBody); - } - catch (Exception e) - { - if (e is AlreadyClosedException a) - { - // 406 is received, when the reply consumer isn't yet recovered - Assert.NotEqual(406, a.ShutdownReason.ReplyCode); - } - } - done.Wait(doneSpan); - } - } - - [Fact] - public void TestThatCancelledConsumerDoesNotReappearOnRecovery() - { - string q = _channel.QueueDeclare(GenerateQueueName(), false, false, false, null).QueueName; - int n = 1024; - - for (int i = 0; i < n; i++) - { - var cons = new EventingBasicConsumer(_channel); - string tag = _channel.BasicConsume(q, true, cons); - _channel.BasicCancel(tag); - } - CloseAndWaitForRecovery(); - Assert.True(_channel.IsOpen); - AssertConsumerCount(q, 0); - } - - [Fact] - public void TestThatDeletedExchangeBindingsDontReappearOnRecovery() - { - string q = _channel.QueueDeclare("", false, false, false, null).QueueName; - string x1 = "amq.fanout"; - string x2 = GenerateExchangeName(); - - _channel.ExchangeDeclare(x2, "fanout"); - _channel.ExchangeBind(x1, x2, ""); - _channel.QueueBind(q, x1, ""); - _channel.ExchangeUnbind(x1, x2, "", null); - - try - { - CloseAndWaitForRecovery(); - Assert.True(_channel.IsOpen); - _channel.BasicPublish(x2, "", _encoding.GetBytes("msg")); - AssertMessageCount(q, 0); - } - finally - { - WithTemporaryChannel(m => - { - m.ExchangeDelete(x2); - m.QueueDelete(q); - }); - } - } - - [Fact] - public void TestThatDeletedExchangesDontReappearOnRecovery() - { - string x = GenerateExchangeName(); - _channel.ExchangeDeclare(x, "fanout"); - _channel.ExchangeDelete(x); - - try - { - CloseAndWaitForRecovery(); - Assert.True(_channel.IsOpen); - _channel.ExchangeDeclarePassive(x); - Assert.Fail("Expected an exception"); - } - catch (OperationInterruptedException e) - { - // expected - AssertShutdownError(e.ShutdownReason, 404); - } - } - - [Fact] - public void TestThatDeletedQueueBindingsDontReappearOnRecovery() - { - string q = _channel.QueueDeclare("", false, false, false, null).QueueName; - string x1 = "amq.fanout"; - string x2 = GenerateExchangeName(); - - _channel.ExchangeDeclare(x2, "fanout"); - _channel.ExchangeBind(x1, x2, ""); - _channel.QueueBind(q, x1, ""); - _channel.QueueUnbind(q, x1, "", null); - - try - { - CloseAndWaitForRecovery(); - Assert.True(_channel.IsOpen); - _channel.BasicPublish(x2, "", _encoding.GetBytes("msg")); - AssertMessageCount(q, 0); - } - finally - { - WithTemporaryChannel(m => - { - m.ExchangeDelete(x2); - m.QueueDelete(q); - }); - } - } - - [Fact] - public void TestThatDeletedQueuesDontReappearOnRecovery() - { - string q = "dotnet-client.recovery.q1"; - _channel.QueueDeclare(q, false, false, false, null); - _channel.QueueDelete(q); - - try - { - CloseAndWaitForRecovery(); - Assert.True(_channel.IsOpen); - _channel.QueueDeclarePassive(q); - Assert.Fail("Expected an exception"); - } - catch (OperationInterruptedException e) - { - // expected - AssertShutdownError(e.ShutdownReason, 404); - } - } - - [Fact] - public void TestUnblockedListenersRecovery() - { - var latch = new ManualResetEventSlim(false); - _conn.ConnectionUnblocked += (source, ea) => latch.Set(); - CloseAndWaitForRecovery(); - CloseAndWaitForRecovery(); - - Block(); - Unblock(); - Wait(latch); - } - - [Fact] - public void TestTopologyRecoveryQueueFilter() - { - var filter = new TopologyRecoveryFilter - { - QueueFilter = queue => !queue.Name.Contains("filtered") - }; - var latch = new ManualResetEventSlim(false); - AutorecoveringConnection conn = CreateAutorecoveringConnectionWithTopologyRecoveryFilter(filter); - conn.RecoverySucceeded += (source, ea) => latch.Set(); - IChannel ch = conn.CreateChannel(); - - var queueToRecover = "recovered.queue"; - var queueToIgnore = "filtered.queue"; - ch.QueueDeclare(queueToRecover, false, false, false, null); - ch.QueueDeclare(queueToIgnore, false, false, false, null); - - _channel.QueueDelete(queueToRecover); - _channel.QueueDelete(queueToIgnore); - - try - { - CloseAndWaitForRecovery(conn); - Wait(latch); - - Assert.True(ch.IsOpen); - AssertQueueRecovery(ch, queueToRecover, false); - - try - { - AssertQueueRecovery(ch, queueToIgnore, false); - Assert.Fail("Expected an exception"); - } - catch (OperationInterruptedException e) - { - AssertShutdownError(e.ShutdownReason, 404); - } - } - finally - { - conn.Abort(); - } - } - - [Fact] - public void TestTopologyRecoveryExchangeFilter() - { - var filter = new TopologyRecoveryFilter - { - ExchangeFilter = exchange => exchange.Type == "topic" && !exchange.Name.Contains("filtered") - }; - var latch = new ManualResetEventSlim(false); - AutorecoveringConnection conn = CreateAutorecoveringConnectionWithTopologyRecoveryFilter(filter); - conn.RecoverySucceeded += (source, ea) => latch.Set(); - IChannel ch = conn.CreateChannel(); - - var exchangeToRecover = "recovered.exchange"; - var exchangeToIgnore = "filtered.exchange"; - ch.ExchangeDeclare(exchangeToRecover, "topic", false, true); - ch.ExchangeDeclare(exchangeToIgnore, "direct", false, true); - - _channel.ExchangeDelete(exchangeToRecover); - _channel.ExchangeDelete(exchangeToIgnore); - - try - { - CloseAndWaitForRecovery(conn); - Wait(latch); - - Assert.True(ch.IsOpen); - AssertExchangeRecovery(ch, exchangeToRecover); - - try - { - AssertExchangeRecovery(ch, exchangeToIgnore); - Assert.Fail("Expected an exception"); - } - catch (OperationInterruptedException e) - { - AssertShutdownError(e.ShutdownReason, 404); - } - } - finally - { - conn.Abort(); - } - } - - [Fact] - public void TestTopologyRecoveryBindingFilter() - { - var filter = new TopologyRecoveryFilter - { - BindingFilter = binding => !binding.RoutingKey.Contains("filtered") - }; - var latch = new ManualResetEventSlim(false); - AutorecoveringConnection conn = CreateAutorecoveringConnectionWithTopologyRecoveryFilter(filter); - conn.RecoverySucceeded += (source, ea) => latch.Set(); - IChannel ch = conn.CreateChannel(); - - var exchange = "topology.recovery.exchange"; - var queueWithRecoveredBinding = "topology.recovery.queue.1"; - var queueWithIgnoredBinding = "topology.recovery.queue.2"; - var bindingToRecover = "recovered.binding"; - var bindingToIgnore = "filtered.binding"; - - ch.ExchangeDeclare(exchange, "direct"); - ch.QueueDeclare(queueWithRecoveredBinding, false, false, false, null); - ch.QueueDeclare(queueWithIgnoredBinding, false, false, false, null); - ch.QueueBind(queueWithRecoveredBinding, exchange, bindingToRecover); - ch.QueueBind(queueWithIgnoredBinding, exchange, bindingToIgnore); - ch.QueuePurge(queueWithRecoveredBinding); - ch.QueuePurge(queueWithIgnoredBinding); - - _channel.QueueUnbind(queueWithRecoveredBinding, exchange, bindingToRecover); - _channel.QueueUnbind(queueWithIgnoredBinding, exchange, bindingToIgnore); - - 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); - - 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 - } - } - finally - { - conn.Abort(); - } - } - - [Fact] - public void TestTopologyRecoveryDefaultFilterRecoversAllEntities() - { - var filter = new TopologyRecoveryFilter(); - 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 queue1 = "topology.recovery.queue.1"; - var queue2 = "topology.recovery.queue.2"; - var binding1 = "recovered.binding"; - var binding2 = "filtered.binding"; - - ch.ExchangeDeclare(exchange, "direct"); - ch.QueueDeclare(queue1, false, false, false, null); - ch.QueueDeclare(queue2, false, false, false, null); - ch.QueueBind(queue1, exchange, binding1); - ch.QueueBind(queue2, exchange, binding2); - ch.QueuePurge(queue1); - ch.QueuePurge(queue2); - - var consumerLatch1 = new ManualResetEventSlim(false); - var consumer1 = new EventingBasicConsumer(ch); - consumer1.Received += (source, ea) => consumerLatch1.Set(); - ch.BasicConsume(queue1, true, "recovered.consumer", consumer1); - - var consumerLatch2 = new ManualResetEventSlim(false); - var consumer2 = new EventingBasicConsumer(ch); - consumer2.Received += (source, ea) => consumerLatch2.Set(); - ch.BasicConsume(queue2, true, "filtered.consumer", consumer2); - - _channel.ExchangeDelete(exchange); - _channel.QueueDelete(queue1); - _channel.QueueDelete(queue2); - - try - { - CloseAndWaitForRecovery(conn); - Wait(latch); - - 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")); - - Assert.True(consumerLatch1.Wait(TimeSpan.FromSeconds(5))); - Assert.True(consumerLatch2.Wait(TimeSpan.FromSeconds(5))); - } - finally - { - conn.Abort(); - } - } - - [Fact] - public void TestTopologyRecoveryQueueExceptionHandler() - { - var changedQueueArguments = new Dictionary - { - { Headers.XMaxPriority, 20 } - }; - var exceptionHandler = new TopologyRecoveryExceptionHandler - { - QueueRecoveryExceptionCondition = (rq, ex) => - { - return rq.Name.Contains("exception") - && ex is OperationInterruptedException operationInterruptedException - && operationInterruptedException.ShutdownReason.ReplyCode == Constants.PreconditionFailed; - }, - QueueRecoveryExceptionHandler = (rq, ex, connection) => - { - using (var channel = connection.CreateChannel()) - { - channel.QueueDeclare(rq.Name, false, false, false, changedQueueArguments); - } - } - }; - var latch = new ManualResetEventSlim(false); - AutorecoveringConnection conn = CreateAutorecoveringConnectionWithTopologyRecoveryExceptionHandler(exceptionHandler); - conn.RecoverySucceeded += (source, ea) => latch.Set(); - IChannel ch = conn.CreateChannel(); - - var queueToRecoverWithException = "recovery.exception.queue"; - var queueToRecoverSuccessfully = "successfully.recovered.queue"; - ch.QueueDeclare(queueToRecoverWithException, false, false, false, null); - ch.QueueDeclare(queueToRecoverSuccessfully, false, false, false, null); - - _channel.QueueDelete(queueToRecoverSuccessfully); - _channel.QueueDelete(queueToRecoverWithException); - _channel.QueueDeclare(queueToRecoverWithException, false, false, false, changedQueueArguments); - - try - { - CloseAndWaitForRecovery(conn); - Wait(latch); - - Assert.True(ch.IsOpen); - AssertQueueRecovery(ch, queueToRecoverSuccessfully, false); - AssertQueueRecovery(ch, queueToRecoverWithException, false, changedQueueArguments); - } - finally - { - //Cleanup - _channel.QueueDelete(queueToRecoverWithException); - - conn.Abort(); - } - } - - [Fact] - public void TestTopologyRecoveryExchangeExceptionHandler() - { - var exceptionHandler = new TopologyRecoveryExceptionHandler - { - ExchangeRecoveryExceptionCondition = (re, ex) => - { - return re.Name.Contains("exception") - && ex is OperationInterruptedException operationInterruptedException - && operationInterruptedException.ShutdownReason.ReplyCode == Constants.PreconditionFailed; - }, - ExchangeRecoveryExceptionHandler = (re, ex, connection) => - { - using (var channel = connection.CreateChannel()) - { - channel.ExchangeDeclare(re.Name, "topic", false, false); - } - } - }; - var latch = new ManualResetEventSlim(false); - AutorecoveringConnection conn = CreateAutorecoveringConnectionWithTopologyRecoveryExceptionHandler(exceptionHandler); - conn.RecoverySucceeded += (source, ea) => latch.Set(); - IChannel ch = conn.CreateChannel(); - - var exchangeToRecoverWithException = "recovery.exception.exchange"; - var exchangeToRecoverSuccessfully = "successfully.recovered.exchange"; - ch.ExchangeDeclare(exchangeToRecoverWithException, "direct", false, false); - ch.ExchangeDeclare(exchangeToRecoverSuccessfully, "direct", false, false); - - _channel.ExchangeDelete(exchangeToRecoverSuccessfully); - _channel.ExchangeDelete(exchangeToRecoverWithException); - _channel.ExchangeDeclare(exchangeToRecoverWithException, "topic", false, false); - - try - { - CloseAndWaitForRecovery(conn); - Wait(latch); - - Assert.True(ch.IsOpen); - AssertExchangeRecovery(ch, exchangeToRecoverSuccessfully); - AssertExchangeRecovery(ch, exchangeToRecoverWithException); - } - finally - { - //Cleanup - _channel.ExchangeDelete(exchangeToRecoverWithException); - - conn.Abort(); - } - } - - [Fact] - public void TestTopologyRecoveryBindingExceptionHandler() - { - var exchange = "topology.recovery.exchange"; - var queueWithExceptionBinding = "recovery.exception.queue"; - var bindingToRecoverWithException = "recovery.exception.binding"; - - var exceptionHandler = new TopologyRecoveryExceptionHandler - { - BindingRecoveryExceptionCondition = (b, ex) => - { - return b.RoutingKey.Contains("exception") - && ex is OperationInterruptedException operationInterruptedException - && operationInterruptedException.ShutdownReason.ReplyCode == Constants.NotFound; - }, - BindingRecoveryExceptionHandler = (b, ex, connection) => - { - using (var channel = connection.CreateChannel()) - { - channel.QueueDeclare(queueWithExceptionBinding, false, false, false, null); - channel.QueueBind(queueWithExceptionBinding, exchange, bindingToRecoverWithException); - } - } - }; - var latch = new ManualResetEventSlim(false); - AutorecoveringConnection conn = CreateAutorecoveringConnectionWithTopologyRecoveryExceptionHandler(exceptionHandler); - conn.RecoverySucceeded += (source, ea) => latch.Set(); - IChannel ch = conn.CreateChannel(); - - var queueWithRecoveredBinding = "successfully.recovered.queue"; - var bindingToRecoverSuccessfully = "successfully.recovered.binding"; - - _channel.QueueDeclare(queueWithExceptionBinding, false, false, false, null); - - ch.ExchangeDeclare(exchange, "direct"); - ch.QueueDeclare(queueWithRecoveredBinding, false, false, false, null); - ch.QueueBind(queueWithRecoveredBinding, exchange, bindingToRecoverSuccessfully); - ch.QueueBind(queueWithExceptionBinding, exchange, bindingToRecoverWithException); - ch.QueuePurge(queueWithRecoveredBinding); - ch.QueuePurge(queueWithExceptionBinding); - - _channel.QueueUnbind(queueWithRecoveredBinding, exchange, bindingToRecoverSuccessfully); - _channel.QueueUnbind(queueWithExceptionBinding, exchange, bindingToRecoverWithException); - _channel.QueueDelete(queueWithExceptionBinding); - - try - { - CloseAndWaitForRecovery(conn); - Wait(latch); - - Assert.True(ch.IsOpen); - Assert.True(SendAndConsumeMessage(queueWithRecoveredBinding, exchange, bindingToRecoverSuccessfully)); - Assert.True(SendAndConsumeMessage(queueWithExceptionBinding, exchange, bindingToRecoverWithException)); - } - finally - { - conn.Abort(); - } - } - - [Fact] - public void TestTopologyRecoveryConsumerExceptionHandler() - { - var queueWithExceptionConsumer = "recovery.exception.queue"; - - var exceptionHandler = new TopologyRecoveryExceptionHandler - { - ConsumerRecoveryExceptionCondition = (c, ex) => - { - return c.ConsumerTag.Contains("exception") - && ex is OperationInterruptedException operationInterruptedException - && operationInterruptedException.ShutdownReason.ReplyCode == Constants.NotFound; - }, - ConsumerRecoveryExceptionHandler = (c, ex, connection) => - { - using (var channel = connection.CreateChannel()) - { - channel.QueueDeclare(queueWithExceptionConsumer, false, false, false, null); - } - - // So topology recovery runs again. This time he missing queue should exist, making - // it possible to recover the consumer successfully. - throw ex; - } - }; - var latch = new ManualResetEventSlim(false); - AutorecoveringConnection conn = CreateAutorecoveringConnectionWithTopologyRecoveryExceptionHandler(exceptionHandler); - conn.RecoverySucceeded += (source, ea) => latch.Set(); - IChannel ch = conn.CreateChannel(); - ch.ConfirmSelect(); - - _channel.QueueDeclare(queueWithExceptionConsumer, false, false, false, null); - _channel.QueuePurge(queueWithExceptionConsumer); - - var recoverLatch = new ManualResetEventSlim(false); - var consumerToRecover = new EventingBasicConsumer(ch); - consumerToRecover.Received += (source, ea) => recoverLatch.Set(); - ch.BasicConsume(queueWithExceptionConsumer, true, "exception.consumer", consumerToRecover); - - _channel.QueueDelete(queueWithExceptionConsumer); - - try - { - CloseAndWaitForShutdown(conn); - Wait(latch, TimeSpan.FromSeconds(20)); - - Assert.True(ch.IsOpen); - - ch.BasicPublish("", queueWithExceptionConsumer, Encoding.UTF8.GetBytes("test message")); - - Assert.True(recoverLatch.Wait(TimeSpan.FromSeconds(5))); - - try - { - ch.BasicConsume(queueWithExceptionConsumer, true, "exception.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(); - } - } - - 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/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++; - } - } -}