diff --git a/.azuredevops/policies/approvercountpolicy.yml b/.azuredevops/policies/approvercountpolicy.yml index 78e0c2aa0f..0a9c3868f9 100644 --- a/.azuredevops/policies/approvercountpolicy.yml +++ b/.azuredevops/policies/approvercountpolicy.yml @@ -3,10 +3,10 @@ # that govern how PRs are approved in general. The settings here dictate how # the validator behaves, and it can also prevent PRs from completing. # -# Suggested by Merlinbot (https://sqlclientdrivers.visualstudio.com/ADO.Net/_git/dotnet-sqlclient/pullrequest/4982) +# https://eng.ms/docs/coreai/devdiv/one-engineering-system-1es/1es-docs/policy-service/policy-as-code/approver-count-policy-overview name: approver_count -description: Approver count policy for dotnet-sqlclient +description: Approver count policy for dotnet-sqlclient [internal/release/6.1] resource: repository where: configuration: @@ -22,8 +22,5 @@ configuration: resetRejectionsOnSourcePush: false blockLastPusherVote: true branchNames: - - refs/heads/internal/main - - refs/heads/internal/release/6.0 - - refs/heads/internal/release/5.2 - - refs/heads/internal/release/5.1 - displayName: dotnet-sqlclient Approver Count Policy + - internal/release/6.1 + displayName: dotnet-sqlclient Approver Count Policy [internal/release/6.1] diff --git a/.editorconfig b/.editorconfig index f0ea20ec32..ff6d9f3bd7 100644 --- a/.editorconfig +++ b/.editorconfig @@ -14,6 +14,9 @@ indent_size = 4 [*.{json,jsonc}] indent_size = 2 +[*.{yml,yaml}] +indent_size = 2 + # C# files [*.cs] # New line preferences diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 0000000000..ebf6a8cec5 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,98 @@ +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +# +# ******** NOTE ******** +# We have attempted to detect the languages in your repository. Please check +# the `language` matrix defined below to confirm you have the correct set of +# supported CodeQL languages. +# +name: "CodeQL Advanced" + +on: + push: + branches: [ "release/6.1" ] + pull_request: + branches: [ "release/6.1" ] + schedule: + - cron: '15 22 * * 6' + +jobs: + analyze: + name: Analyze (${{ matrix.language }}) + # Runner size impacts CodeQL analysis time. To learn more, please see: + # - https://gh.io/recommended-hardware-resources-for-running-codeql + # - https://gh.io/supported-runners-and-hardware-resources + # - https://gh.io/using-larger-runners (GitHub.com only) + # Consider using larger runners or machines with greater resources for possible analysis time improvements. + runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }} + permissions: + # required for all workflows + security-events: write + + # required to fetch internal or private CodeQL packs + packages: read + + # only required for workflows in private repositories + actions: read + contents: read + + strategy: + fail-fast: false + matrix: + include: + - language: csharp + build-mode: manual + # CodeQL supports the following values keywords for 'language': 'actions', 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'rust', 'swift' + # Use `c-cpp` to analyze code written in C, C++ or both + # Use 'java-kotlin' to analyze code written in Java, Kotlin or both + # Use 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both + # To learn more about changing the languages that are analyzed or customizing the build mode for your analysis, + # see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning. + # If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how + # your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup .NET Core SDK + uses: actions/setup-dotnet@v5.0.1 + with: + # TODO: Update this to .NET 10 once PR #3686 is complete. + # TODO: Replace this with global-json-file once PR #3797 is complete. + dotnet-version: 9.x + dotnet-quality: ga + #global-json-file: global.json + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v4 + with: + languages: ${{ matrix.language }} + build-mode: ${{ matrix.build-mode }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + + # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs + # queries: security-extended,security-and-quality + + # If the analyze step fails for one of the languages you are analyzing with + # "We were unable to automatically build your code", modify the matrix above + # to set the build mode to "manual" for that language. Then modify this step + # to build your code. + # ℹ️ Command-line programs to run using the OS shell. + # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun + - name: Run manual build steps + if: matrix.build-mode == 'manual' + shell: bash + run: | + mkdir packages + dotnet build src/ + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v4 + with: + category: "/language:${{matrix.language}}" diff --git a/build.proj b/build.proj index 90f5f0fc37..fbae7ed6d4 100644 --- a/build.proj +++ b/build.proj @@ -58,9 +58,14 @@ + + + + - + + @@ -221,6 +226,7 @@ -p:TestTargetOS=Windows$(TargetGroup) --collect "Code coverage" --results-directory $(ResultsDirectory) + --filter "category!=failing%26category!=flaky" --logger:"trx;LogFilePrefix=Unit-Windows$(TargetGroup)-$(TestSet)" $(TestCommand.Replace($([System.Environment]::NewLine), " ")) @@ -241,8 +247,9 @@ -p:TestTargetOS=Unixnetcoreapp --collect "Code coverage" --results-directory $(ResultsDirectory) + --filter "category!=failing%26category!=flaky" --logger:"trx;LogFilePrefix=Unit-Unixnetcoreapp-$(TestSet)" - + $(TestCommand.Replace($([System.Environment]::NewLine), " ")) diff --git a/doc/snippets/Microsoft.Data.SqlClient/SqlCommand.xml b/doc/snippets/Microsoft.Data.SqlClient/SqlCommand.xml index 5e28842000..6b269de720 100644 --- a/doc/snippets/Microsoft.Data.SqlClient/SqlCommand.xml +++ b/doc/snippets/Microsoft.Data.SqlClient/SqlCommand.xml @@ -3604,7 +3604,9 @@ Before you call , specify t If you call an `Execute` method after calling , any parameter value that is larger than the value specified by the property is automatically truncated to the original specified size of the parameter, and no truncation errors are returned. -Output parameters (whether prepared or not) must have a user-specified data type. If you specify a variable length data type, you must also specify the maximum . +Output parameters (whether prepared or not) must have a user-specified data type. If you specify a variable length data type except vector, you must also specify the maximum . + +For vector data types, the property is ignored. The size of the vector is inferred from the of type . Prior to Visual Studio 2010, threw an exception. Beginning in Visual Studio 2010, this method does not throw an exception. diff --git a/doc/snippets/Microsoft.Data.SqlClient/SqlConnectionStringBuilder.xml b/doc/snippets/Microsoft.Data.SqlClient/SqlConnectionStringBuilder.xml index 96f72293df..a68c0a323b 100644 --- a/doc/snippets/Microsoft.Data.SqlClient/SqlConnectionStringBuilder.xml +++ b/doc/snippets/Microsoft.Data.SqlClient/SqlConnectionStringBuilder.xml @@ -361,7 +361,7 @@ The following example supplies a simple SQL Server connection string in the connection string. > [!NOTE] -> Since version 5.x the default value for none Azure endpoints is 1 and for Azure SQL and Azure Synapse has increased to 2 and 5 to imporve the recovery against on high demand Azure endpoints. It should be detected first, and Synapse could be detected as a regular Azure SQL DB endpoint. +> Since version 5.x the default value for none Azure endpoints is 1 and for Azure SQL and Azure Synapse has increased to 2 and 5 to improve the recovery against on high demand Azure endpoints. It should be detected first, and Synapse could be detected as a regular Azure SQL DB endpoint. ]]> @@ -539,7 +539,7 @@ This property corresponds to the "Connect Retry Count" key within the - This property corresponds to the "Data Source", "server", "address", "addr", and "network address" keys within the connection string. Regardless of which of these values has been supplied within the supplied connection string, the connection string created by the SqlConnectionStringBuilder will use the well-known "Data Source" key. The port number can be specified after the server name: server=tcp:servername,portnumber . + This property corresponds to the "Data Source", "server", "address", "addr", and "network address" keys within the connection string. Regardless of which of these values has been supplied within the supplied connection string, the connection string created by the SqlConnectionStringBuilder will use the well-known "Data Source" key. The port number can be specified after the server name: server=tcp:servername,port . When specifying a local instance, always use (local). To force a protocol, add one of the following prefixes: np:(local), tcp:(local), lpc:(local) . @@ -664,7 +664,7 @@ When `TrustServerCertificate` is false and `Encrypt` is - Gets or sets a Boolean value that indicates whether the SQL Server connection pooler automatically enlists the connection in the creation thread's current transaction context. + Gets or sets a Boolean value that indicates whether the SQL Server connection pool automatically enlists the connection in the creation thread's current transaction context. The value of the property, or if none has been supplied. @@ -727,7 +727,7 @@ This property corresponds to the "FailoverPartnerSPN" and "Failover Partner SPN" [!NOTE] > This property only applies when using `Encrypt` in or mode, otherwise it is ignored. diff --git a/doc/snippets/Microsoft.Data.SqlClient/SqlDataReader.xml b/doc/snippets/Microsoft.Data.SqlClient/SqlDataReader.xml index ff54d18a17..1c287a0de4 100644 --- a/doc/snippets/Microsoft.Data.SqlClient/SqlDataReader.xml +++ b/doc/snippets/Microsoft.Data.SqlClient/SqlDataReader.xml @@ -967,10 +967,10 @@ The method retur - Gets the value of the specified column as a . + Gets the value of the specified column as a . - A object representing the column at the given ordinal. + A object representing the column at the given ordinal. The index passed was outside the range of 0 to - 1 @@ -979,7 +979,7 @@ The method retur An attempt was made to read or access columns in a closed . - The retrieved data is not compatible with the type. + The retrieved data is not compatible with the type. No conversions are performed; therefore, the data retrieved must already be a vector value, or an exception is generated. diff --git a/doc/snippets/Microsoft.Data.SqlTypes/SqlVector.xml b/doc/snippets/Microsoft.Data.SqlTypes/SqlVector.xml index 0157a05787..7dea042145 100644 --- a/doc/snippets/Microsoft.Data.SqlTypes/SqlVector.xml +++ b/doc/snippets/Microsoft.Data.SqlTypes/SqlVector.xml @@ -5,20 +5,11 @@ Represents a vector value in SQL Server. - - - Constructs a null vector of the given length. SQL Server requires vector arguments to specify their length even when null. - - - Vector length must be non-negative. - - - Constructs a vector with the given values. - + @@ -37,13 +28,17 @@ Returns the number of elements in the vector. - - - Returns the number of bytes required to represent this vector when communicating with SQL Server. - - Returns the vector values as a memory region. No copies are made. + + + + Constructs a null vector of the given length. SQL Server requires vector arguments to specify their length even when null. + + + Vector length must be non-negative. + + diff --git a/eng/pipelines/common/templates/jobs/ci-code-coverage-job.yml b/eng/pipelines/common/templates/jobs/ci-code-coverage-job.yml index 186d8cfdee..068b3a6699 100644 --- a/eng/pipelines/common/templates/jobs/ci-code-coverage-job.yml +++ b/eng/pipelines/common/templates/jobs/ci-code-coverage-job.yml @@ -3,160 +3,194 @@ # The .NET Foundation licenses this file to you under the MIT license. # # See the LICENSE file in the project root for more information. # ################################################################################# + +# This job processes code coverage reports generated during test runs, +# merges them, generates code coverage reports, publishes them to the +# pipeline, and uploads them to CodeCov. + parameters: + + # True to include debug steps. - name: debug type: boolean default: false - - name: upload + # The pool image to use. + - name: image type: string - default: $(ci_var_uploadTestResult) - - name: poolName + # The agent pool name. + - name: pool type: string - default: $(defaultHostedPoolName) - - name: image - type: string - default: 'windows-2022' - - - name: downloadArtifactsSteps - type: stepList - default: [] + # Array of target frameworks to process code coverage for: + # + # e.g. [net462, net8.0, net9.0] + # + - name: targetFrameworks + type: object + + # True to upload code coverage results to CodeCov. + - name: upload + type: boolean + default: true jobs: -- job: CodeCoverage - displayName: 'Merge Code Coverage' - - variables: - uploadTestResult: ${{ parameters.upload }} - - pool: - name: '${{ parameters.poolName }}' - vmImage: ${{ parameters.image }} - - steps: - - ${{if eq(parameters.debug, true)}}: - - powershell: | - Get-ChildItem env: | Sort-Object Name - displayName: 'List Environment Variables [debug]' - - - task: NuGetAuthenticate@1 - displayName: 'NuGet Authenticate' - - - template: ../steps/ensure-dotnet-version.yml@self - parameters: - packageType: 'sdk' - version: '8.0' - - - ${{ parameters.downloadArtifactsSteps }} - - - ${{ if eq(parameters.debug, true)}}: - - powershell: | - Get-ChildItem $(Build.SourcesDirectory)\coverageNetFx\ -Recurse -File -Filter *.coverage - displayName: 'List coverageNetFx files [debug]' - - - powershell: | - Get-ChildItem $(Build.SourcesDirectory)\coverageNetCore\ -Recurse -File -Filter *.coverage - displayName: 'List coverageNetCore files [debug]' - - - pwsh: | - dotnet tool install --global dotnet-coverage - - function MergeFiles { - param( - [string]$InputDirectoryPath, - [string]$OutputDirectoryName - ) - - $files = Get-ChildItem $InputDirectoryPath -Recurse -File -Filter *.coverage - - # echo $files - mkdir $OutputDirectoryName - $counter=0 - - $toProcess = @() - - foreach ($file in $files) { - $toProcess += @{ - File = $file.FullName - OutputFile = "$OutputDirectoryName\$counter.coveragexml" + - job: CodeCoverage + displayName: Publish Code Coverage + + pool: + name: ${{ parameters.pool }} + ${{ if eq(parameters.pool, 'Azure Pipelines') }}: + vmImage: ${{ parameters.image }} + ${{ else }}: + demands: + - imageOverride -equals ${{ parameters.image }} + + variables: + netFxDir: $(Build.SourcesDirectory)\coverageNetFx + netCoreDir: $(Build.SourcesDirectory)\coverageNetCore + + steps: + - ${{if eq(parameters.debug, true)}}: + - pwsh: | + Get-ChildItem env: | Sort-Object Name + displayName: 'List Environment Variables [debug]' + + - pwsh: Get-Volume + displayName: '[Debug] Show Disk Usage' + + - pwsh: 'Get-ChildItem env: | Sort-Object Name' + displayName: '[Debug] List Environment Variables' + + - template: ../steps/ensure-dotnet-version.yml@self + parameters: + packageType: sdk + version: '9.0' + + - pwsh: | + dotnet tool install --global dotnet-coverage + dotnet tool install --global dotnet-reportgenerator-globaltool + displayName: Install dotnet tools + + - ${{ each targetFramework in parameters.targetFrameworks }}: + - task: DownloadPipelineArtifact@2 + displayName: 'Download Coverage Reports [${{ targetFramework }}]' + inputs: + itemPattern: '**\${{ targetFramework }}*' + ${{ if startsWith(targetFramework, 'net4') }}: + targetPath: $(netFxDir) + ${{ else }}: + targetPath: $(netCoreDir) + + - ${{if eq(parameters.debug, true)}}: + - pwsh: Get-Volume + displayName: '[Debug] Show Disk Usage' + + - pwsh: Get-ChildItem $(netFxDir) -Recurse -File -Filter *.coverage + displayName: '[Debug] List coverageNetFx files' + + - pwsh: Get-ChildItem $(netCoreDir) -Recurse -File -Filter *.coverage + displayName: '[Debug] List coverageNetCore files' + + - pwsh: | + function MergeFiles { + param( + [string]$InputDirectoryPath, + [string]$OutputDirectoryName + ) + + $files = Get-ChildItem $InputDirectoryPath -Recurse -File -Filter *.coverage + + # echo $files + mkdir $OutputDirectoryName + $counter=0 + $toProcess = @() + + foreach ($file in $files) { + $toProcess += @{ + File = $file.FullName + OutputFile = "$OutputDirectoryName\$counter.coveragexml" + } + + $counter++ } - $counter++ - } - - $jobs = @() - foreach ($file in $toProcess){ - $jobs += Start-ThreadJob -ScriptBlock { - $params = $using:file - & dotnet-coverage merge $($params.File) --output $($params.OutputFile) --output-format xml - } - } - - Write-Host "Merging started..." - Wait-Job -Job $jobs - - foreach ($job in $jobs) { - Receive-Job -Job $job -Wait -AutoRemoveJob - } - } - - MergeFiles -InputDirectoryPath "$(Build.SourcesDirectory)\coverageNetFx\" -OutputDirectoryName "coverageNetFxXml" - MergeFiles -InputDirectoryPath "$(Build.SourcesDirectory)\coverageNetCore\" -OutputDirectoryName "coverageNetCoreXml" - - # dir coverageNetFxXml\ - # dir coverageNetCoreXml\ - - Write-Host "Clean up disk ... [removing coverageNetFx & coverageNetCore]" - - Remove-Item $(Build.SourcesDirectory)\coverageNetFx -Recurse -Force - Remove-Item $(Build.SourcesDirectory)\coverageNetCore -Recurse -Force - - displayName: 'Convert coverage files to xml' - - - ${{ if eq(parameters.debug, true)}}: - - powershell: | - dir coverageNetFxXml\ - dir coverageNetCoreXml\ - displayName: 'List converted files [debug]' - - - pwsh: | - dotnet tool install dotnet-reportgenerator-globaltool --tool-path tools - - $jobs = @() - $jobs += Start-ThreadJob -ScriptBlock { - & tools\reportgenerator "-reports:coverageNetFxXml\*.coveragexml" "-targetdir:coveragereportNetFx" "-reporttypes:Cobertura;" "-assemblyfilters:+microsoft.data.sqlclient.dll" "-sourcedirs:$(Build.SourcesDirectory)\src\Microsoft.Data.SqlClient\netfx\src;$(Build.SourcesDirectory)\src\Microsoft.Data.SqlClient\src" "-classfilters:+Microsoft.Data.*" - } - - $jobs += Start-ThreadJob -ScriptBlock { - & tools\reportgenerator "-reports:coverageNetCoreXml\*.coveragexml" "-targetdir:coveragereportAddOns" "-reporttypes:Cobertura;" "-assemblyfilters:+microsoft.data.sqlclient.alwaysencrypted.azurekeyvaultprovider.dll" "-sourcedirs:$(Build.SourcesDirectory)\src\Microsoft.Data.SqlClient\add-ons\AzureKeyVaultProvider" "-classfilters:+Microsoft.Data.*" - } - - $jobs += Start-ThreadJob -ScriptBlock { - & tools\reportgenerator "-reports:coverageNetCoreXml\*.coveragexml" "-targetdir:coveragereportNetCore" "-reporttypes:Cobertura;" "-assemblyfilters:+microsoft.data.sqlclient.dll" "-sourcedirs:$(Build.SourcesDirectory)\src\Microsoft.Data.SqlClient\netcore\src;$(Build.SourcesDirectory)\src\Microsoft.Data.SqlClient\src" "-classfilters:+Microsoft.Data.*" - } - - Write-Host "Running ReportGenerator..." - Wait-Job -Job $jobs - - foreach ($job in $jobs) { - Receive-Job -Job $job -Wait -AutoRemoveJob - } - displayName: 'Run ReportGenerator' - - - task: PublishCodeCoverageResults@2 - displayName: 'Publish code coverage from netcore' - inputs: - summaryFileLocation: '*\Cobertura.xml' - - - powershell: | - #download Codecov CLI - $ProgressPreference = 'SilentlyContinue' - Invoke-WebRequest -Uri https://cli.codecov.io/latest/windows/codecov.exe -Outfile codecov.exe - - ./codecov --verbose upload-process --fail-on-error -t $(CODECOV_TOKEN) -f "coveragereportNetFx\Cobertura.xml" -F netfx - ./codecov --verbose upload-process --fail-on-error -t $(CODECOV_TOKEN) -f "coveragereportNetCore\Cobertura.xml" -F netcore - ./codecov --verbose upload-process --fail-on-error -t $(CODECOV_TOKEN) -f "coveragereportAddOns\Cobertura.xml" -F addons - displayName: 'Upload to CodeCov' - condition: and(succeeded(), eq(variables['uploadTestResult'], 'true')) + $jobs = @() + foreach ($file in $toProcess){ + $jobs += Start-ThreadJob -ScriptBlock { + $params = $using:file + & dotnet-coverage merge $($params.File) --output $($params.OutputFile) --output-format xml + } + } + + Write-Host "Merging started..." + Wait-Job -Job $jobs + + foreach ($job in $jobs) { + Receive-Job -Job $job -Wait -AutoRemoveJob + } + } + + MergeFiles -InputDirectoryPath "$(netFxDir)" -OutputDirectoryName "coverageNetFxXml" + MergeFiles -InputDirectoryPath "$(netCoreDir)" -OutputDirectoryName "coverageNetCoreXml" + + Write-Host "Removing original coverage files..." + Remove-Item $(netFxDir) -Recurse -Force -ErrorAction SilentlyContinue + Remove-Item $(netCoreDir) -Recurse -Force -ErrorAction SilentlyContinue + displayName: Convert coverage files to xml + + - ${{if eq(parameters.debug, true)}}: + - pwsh: Get-Volume + displayName: '[Debug] Show Disk Usage' + + - pwsh: | + dir coverageNetFxXml\ + dir coverageNetCoreXml\ + displayName: '[Debug] List converted files' + + - pwsh: | + $jobs = @() + $jobs += Start-ThreadJob -ScriptBlock { + & reportgenerator "-reports:coverageNetFxXml\*.coveragexml" "-targetdir:coveragereportNetFx" "-reporttypes:Cobertura;" "-assemblyfilters:+microsoft.data.sqlclient.dll" "-sourcedirs:$(Build.SourcesDirectory)\src\Microsoft.Data.SqlClient\netfx\src;$(Build.SourcesDirectory)\src\Microsoft.Data.SqlClient\src" "-classfilters:+Microsoft.Data.*" + } + + $jobs += Start-ThreadJob -ScriptBlock { + & reportgenerator "-reports:coverageNetCoreXml\*.coveragexml" "-targetdir:coveragereportNetCore" "-reporttypes:Cobertura;" "-assemblyfilters:+microsoft.data.sqlclient.dll" "-sourcedirs:$(Build.SourcesDirectory)\src\Microsoft.Data.SqlClient\netcore\src;$(Build.SourcesDirectory)\src\Microsoft.Data.SqlClient\src" "-classfilters:+Microsoft.Data.*" + } + + $jobs += Start-ThreadJob -ScriptBlock { + & reportgenerator "-reports:coverageNetCoreXml\*.coveragexml" "-targetdir:coveragereportAddOns" "-reporttypes:Cobertura;" "-assemblyfilters:+microsoft.data.sqlclient.alwaysencrypted.azurekeyvaultprovider.dll" "-sourcedirs:$(Build.SourcesDirectory)\src\Microsoft.Data.SqlClient\add-ons\AzureKeyVaultProvider" "-classfilters:+Microsoft.Data.*" + } + + Write-Host "Running ReportGenerator..." + Wait-Job -Job $jobs + + foreach ($job in $jobs) { + Receive-Job -Job $job -Wait -AutoRemoveJob + } + + Write-Host "Removing merged XML files..." + Remove-Item coverageNetFxXml -Recurse -Force -ErrorAction SilentlyContinue + Remove-Item coverageNetCoreXml -Recurse -Force -ErrorAction SilentlyContinue + displayName: Run ReportGenerator + + - ${{if eq(parameters.debug, true)}}: + - pwsh: Get-Volume + displayName: '[Debug] Show Disk Usage' + + - task: PublishCodeCoverageResults@2 + displayName: Publish code coverage results + inputs: + summaryFileLocation: '*\Cobertura.xml' + + - ${{if eq(parameters.upload, true)}}: + - pwsh: | + #download Codecov CLI + $ProgressPreference = 'SilentlyContinue' + Invoke-WebRequest -Uri https://cli.codecov.io/latest/windows/codecov.exe -Outfile codecov.exe + + ./codecov --verbose upload-process --fail-on-error -t $(CODECOV_TOKEN) -f "coveragereportNetFx\Cobertura.xml" -F netfx + ./codecov --verbose upload-process --fail-on-error -t $(CODECOV_TOKEN) -f "coveragereportNetCore\Cobertura.xml" -F netcore + ./codecov --verbose upload-process --fail-on-error -t $(CODECOV_TOKEN) -f "coveragereportAddOns\Cobertura.xml" -F addons + displayName: Upload to CodeCov diff --git a/eng/pipelines/common/templates/jobs/ci-run-tests-job.yml b/eng/pipelines/common/templates/jobs/ci-run-tests-job.yml index 9ad2c86e6a..dbf5b10028 100644 --- a/eng/pipelines/common/templates/jobs/ci-run-tests-job.yml +++ b/eng/pipelines/common/templates/jobs/ci-run-tests-job.yml @@ -73,9 +73,18 @@ parameters: - Project - Package + # The timeout, in minutes, for this job. + - name: timeout + type: string + default: 90 + jobs: - job: ${{ format('{0}', coalesce(parameters.jobDisplayName, parameters.image, 'unknown_image')) }} + # Some of our tests take longer than the default 60 minutes to run on some + # OSes and configurations. + timeoutInMinutes: ${{ parameters.timeout }} + pool: name: '${{ parameters.poolName }}' ${{ if eq(parameters.hostedPool, true) }}: diff --git a/eng/pipelines/common/templates/jobs/run-tests-package-reference-job.yml b/eng/pipelines/common/templates/jobs/run-tests-package-reference-job.yml index e729aaea46..14aea42411 100644 --- a/eng/pipelines/common/templates/jobs/run-tests-package-reference-job.yml +++ b/eng/pipelines/common/templates/jobs/run-tests-package-reference-job.yml @@ -20,11 +20,21 @@ parameters: - name: isPreview type: boolean + # The timeout, in minutes, for this job. + - name: timeout + type: string + default: 90 + jobs: - job: run_tests_package_reference displayName: 'Run tests with package reference' ${{ if ne(parameters.dependsOn, 'empty')}}: dependsOn: '${{parameters.dependsOn }}' + + # Some of our tests take longer than the default 60 minutes to run on some + # OSes and configurations. + timeoutInMinutes: ${{ parameters.timeout }} + pool: type: windows # read more about custom job pool types at https://aka.ms/obpipelines/yaml/jobs isCustom: true diff --git a/eng/pipelines/common/templates/stages/ci-run-tests-stage.yml b/eng/pipelines/common/templates/stages/ci-run-tests-stage.yml index 3c1671a486..e07685407f 100644 --- a/eng/pipelines/common/templates/stages/ci-run-tests-stage.yml +++ b/eng/pipelines/common/templates/stages/ci-run-tests-stage.yml @@ -30,6 +30,11 @@ parameters: type: jobList default: [] + # The timeout, in minutes, for each test job. + - name: testsTimeout + type: string + default: 90 + stages: - ${{ each config in parameters.testConfigurations }}: - ${{ each image in config.value.images }}: @@ -47,6 +52,7 @@ stages: parameters: debug: ${{ parameters.debug }} buildType: ${{ parameters.buildType }} + timeout: ${{ parameters.testsTimeout }} poolName: ${{ config.value.pool }} hostedPool: ${{ eq(config.value.hostedPool, true) }} image: ${{ image.value }} @@ -72,6 +78,7 @@ stages: parameters: debug: ${{ parameters.debug }} buildType: ${{ parameters.buildType }} + timeout: ${{ parameters.testsTimeout }} poolName: ${{ config.value.pool }} hostedPool: ${{ eq(config.value.hostedPool, true) }} image: ${{ image.value }} diff --git a/eng/pipelines/common/templates/steps/configure-sql-server-linux-step.yml b/eng/pipelines/common/templates/steps/configure-sql-server-linux-step.yml index 5cff58cd4e..7568f01608 100644 --- a/eng/pipelines/common/templates/steps/configure-sql-server-linux-step.yml +++ b/eng/pipelines/common/templates/steps/configure-sql-server-linux-step.yml @@ -3,6 +3,11 @@ # The .NET Foundation licenses this file to you under the MIT license. # # See the LICENSE file in the project root for more information. # ################################################################################# + +# This step configures an existing SQL Server running on the local Linux host. +# For example, our 1ES Hosted Pool has images like ADO-UB20-SQL22 that come with +# SQL Server 2022 pre-installed and running. + parameters: - name: password type: string diff --git a/eng/pipelines/common/templates/steps/configure-sql-server-macos-step.yml b/eng/pipelines/common/templates/steps/configure-sql-server-macos-step.yml index 3e83d6b830..289a113729 100644 --- a/eng/pipelines/common/templates/steps/configure-sql-server-macos-step.yml +++ b/eng/pipelines/common/templates/steps/configure-sql-server-macos-step.yml @@ -3,6 +3,10 @@ # The .NET Foundation licenses this file to you under the MIT license. # # See the LICENSE file in the project root for more information. # ################################################################################# + +# This step installs the latest SQL Server 2022 onto the macOS host and +# configures it for use. + parameters: - name: password type: string @@ -13,7 +17,7 @@ parameters: default: and(succeeded(), eq(variables['Agent.OS'], 'Darwin')) steps: -# Linux only steps +# macOS only steps - bash: | # The "user" pipeline variable conflicts with homebrew, causing errors during install. Set it back to the pipeline user. USER=`whoami` @@ -30,7 +34,7 @@ steps: docker pull mcr.microsoft.com/mssql/server:2022-latest # Password for the SA user (required) - MSSQL_SA_PW=${{parameters.password }} + MSSQL_SA_PW=${{ parameters.password }} docker run -e "ACCEPT_EULA=Y" -e "MSSQL_SA_PASSWORD=$MSSQL_SA_PW" -p 1433:1433 -p 1434:1434 --name sql1 --hostname sql1 -d mcr.microsoft.com/mssql/server:2022-latest @@ -38,29 +42,55 @@ steps: docker ps -a - # Connect to server and get the version: - counter=1 - errstatus=1 - while [ $counter -le 20 ] && [ $errstatus = 1 ] + # Connect to the SQL Server container and get its version. + # + # It can take a while for the docker container to start listening and be + # ready for connections, so we will wait for up to 2 minutes, checking every + # 3 seconds. + + # Wait 3 seconds between attempts. + delay=3 + + # Try up to 40 times (2 minutes) to connect. + maxAttempts=40 + + # Attempt counter. + attempt=1 + + # Flag to indicate when SQL Server is ready to accept connections. + ready=0 + + while [ $attempt -le $maxAttempts ] do - echo Waiting for SQL Server to start... - sleep 3 - sqlcmd -S 0.0.0.0 -No -U sa -P $MSSQL_SA_PW -Q "SELECT @@VERSION" 2>$SQLCMD_ERRORS - errstatus=$? - ((counter++)) + echo "Waiting for SQL Server to start (attempt #$attempt of $maxAttempts)..." + + sqlcmd -S 127.0.0.1 -No -U sa -P $MSSQL_SA_PW -Q "SELECT @@VERSION" >> $SQLCMD_ERRORS 2>&1 + + # If the command was successful, then the SQL Server is ready. + if [ $? -eq 0 ]; then + ready=1 + break + fi + + # Increment the attempt counter. + ((attempt++)) + + # Wait before trying again. + sleep $delay done - # Display error if connection failed: - if [ $errstatus = 1 ] + # Is the SQL Server ready? + if [ $ready -eq 0 ] then - echo Cannot connect to SQL Server, installation aborted + # No, so report the error(s) and exit. + echo Cannot connect to SQL Server; installation aborted; errors were: cat $SQLCMD_ERRORS rm -f $SQLCMD_ERRORS - exit $errstatus - else - rm -f $SQLCMD_ERRORS + exit 1 fi + rm -f $SQLCMD_ERRORS + echo "Use sqlcmd to show which IP addresses are being listened on..." echo 0.0.0.0 sqlcmd -S 0.0.0.0 -No -U sa -P $MSSQL_SA_PW -Q "SELECT @@VERSION" -l 2 @@ -74,7 +104,7 @@ steps: sqlcmd -No -U sa -P $MSSQL_SA_PW -Q "SELECT @@VERSION" -l 2 echo "Configuring Dedicated Administer Connections to allow remote connections..." - sqlcmd -S 0.0.0.0 -No -U sa -P $MSSQL_SA_PW -Q "sp_configure 'remote admin connections', 1; RECONFIGURE;" + sqlcmd -S 127.0.0.1 -No -U sa -P $MSSQL_SA_PW -Q "sp_configure 'remote admin connections', 1; RECONFIGURE;" if [ $? = 1 ] then echo "Error configuring DAC for remote access." diff --git a/eng/pipelines/common/templates/steps/configure-sql-server-win-step.yml b/eng/pipelines/common/templates/steps/configure-sql-server-win-step.yml index d159191b01..6586450e2e 100644 --- a/eng/pipelines/common/templates/steps/configure-sql-server-win-step.yml +++ b/eng/pipelines/common/templates/steps/configure-sql-server-win-step.yml @@ -3,6 +3,11 @@ # The .NET Foundation licenses this file to you under the MIT license. # # See the LICENSE file in the project root for more information. # ################################################################################# + +# This step configures an existing SQL Server running on the local Windows host. +# For example, our 1ES Hosted Pool has images like ADO-MMS22-SQL22 that come +# with SQL Server 2022 pre-installed and running. + parameters: # Windows only parameters - name: instanceName @@ -63,7 +68,7 @@ parameters: default: and(succeeded(), eq(variables['Agent.OS'], 'Windows_NT')) steps: -# windows only steps +# Windows only steps - powershell: | try { diff --git a/eng/pipelines/dotnet-sqlclient-ci-core.yml b/eng/pipelines/dotnet-sqlclient-ci-core.yml index 4fa8c6bcbc..09e48415f6 100644 --- a/eng/pipelines/dotnet-sqlclient-ci-core.yml +++ b/eng/pipelines/dotnet-sqlclient-ci-core.yml @@ -65,25 +65,40 @@ parameters: - Project - Package +- name: buildConfiguration + displayName: 'Build Configuration' + default: Release + values: + - Release + - Debug + - name: defaultPoolName type: string default: $(ci_var_defaultPoolName) +- name: enableStressTests + displayName: Enable Stress Tests + type: boolean + default: false + +# The timeout, in minutes, for each test job. +- name: testsTimeout + type: string + default: 90 + variables: - template: libraries/ci-build-variables.yml@self - name: artifactName value: Artifacts - - name: defaultHostedPoolName - value: 'Azure Pipelines' - stages: - stage: build_nugets displayName: 'Build NuGet Packages' jobs: - template: common/templates/jobs/ci-build-nugets-job.yml@self parameters: + configuration: ${{ parameters.buildConfiguration }} artifactName: $(artifactName) ${{if ne(parameters.SNIVersion, '')}}: prebuildSteps: @@ -92,10 +107,21 @@ stages: SNIVersion: ${{parameters.SNIVersion}} SNIValidationFeed: ${{parameters.SNIValidationFeed}} + - ${{ if eq(parameters.enableStressTests, true) }}: + - template: stages/stress-tests-ci-stage.yml@self + parameters: + buildConfiguration: ${{ parameters.buildConfiguration }} + dependsOn: [build_nugets] + pipelineArtifactName: $(artifactName) + mdsPackageVersion: $(NugetPackageVersion) + ${{ if eq(parameters.debug, 'true') }}: + verbosity: 'detailed' + - template: common/templates/stages/ci-run-tests-stage.yml@self parameters: debug: ${{ parameters.debug }} buildType: ${{ parameters.buildType }} + testsTimeout: ${{ parameters.testsTimeout }} ${{ if eq(parameters.buildType, 'Package') }}: dependsOn: build_nugets @@ -123,23 +149,15 @@ stages: - template: common/templates/jobs/ci-code-coverage-job.yml@self parameters: debug: ${{ parameters.debug }} - downloadArtifactsSteps: - - ${{ each targetFramework in parameters.codeCovTargetFrameworks }}: - - task: DownloadPipelineArtifact@2 - displayName: 'Download Coverage Reports [${{ targetFramework }}]' - inputs: - itemPattern: '**\${{ targetFramework }}*' - ${{ if contains(targetFramework, 'net4') }}: - targetPath: '$(Build.SourcesDirectory)\coverageNetFx' - ${{ else }}: - targetPath: '$(Build.SourcesDirectory)\coverageNetCore' + image: ADO-MMS22-CodeCov + pool: ${{ parameters.defaultPoolName }} + targetFrameworks: ${{ parameters.codeCovTargetFrameworks }} # test stages configurations # self hosted SQL Server on Windows testConfigurations: windows_sql_19_x64: # configuration name pool: ${{parameters.defaultPoolName }} # pool name - hostedPool: false # whether the pool is hosted or not images: # list of images to run tests on Win22_Sql19: ADO-MMS22-SQL19 # stage display name: image name from the pool TargetFrameworks: ${{parameters.targetFrameworks }} #[net462, net8.0] # list of target frameworks to run @@ -181,7 +199,6 @@ stages: windows_sql_19_x86: # configuration name pool: ${{parameters.defaultPoolName }} # pool name - hostedPool: false # whether the pool is hosted or not images: # list of images to run tests on Win22_Sql19_x86: ADO-MMS22-SQL19 # stage display name: image name from the pool TargetFrameworks: [net8.0] #[net462, net8.0] # list of target frameworks to run @@ -483,7 +500,7 @@ stages: # Self hosted SQL Server on Mac mac_sql_22: - pool: $(defaultHostedPoolName) + pool: Azure Pipelines hostedPool: true images: MacOSLatest_Sql22: macos-latest diff --git a/eng/pipelines/dotnet-sqlclient-ci-package-reference-pipeline.yml b/eng/pipelines/dotnet-sqlclient-ci-package-reference-pipeline.yml index 4956b15c89..953a123487 100644 --- a/eng/pipelines/dotnet-sqlclient-ci-package-reference-pipeline.yml +++ b/eng/pipelines/dotnet-sqlclient-ci-package-reference-pipeline.yml @@ -5,36 +5,59 @@ ################################################################################# name: $(DayOfYear)$(Rev:rr) -trigger: - batch: true + +# Trigger PR validation runs for all pushes to PRs that target the specified +# branches. +# +# https://learn.microsoft.com/en-us/azure/devops/pipelines/repos/github?view=azure-devops&tabs=yaml#pr-triggers +# +pr: branches: include: - - main - - internal/main + - release/6.1 + paths: include: - - src\Microsoft.Data.SqlClient\netcore\ref - - src\Microsoft.Data.SqlClient\netfx\ref - - src\Microsoft.Data.SqlClient\ref + - .azuredevops + - .config + - doc - eng + - src - tools - - .config - - Nuget.config + - azurepipelines-coverage.yml + - build.proj + - NuGet.config -schedules: -- cron: '0 4 * * Fri' - displayName: Weekly Thursday 9:00 PM (UTC - 7) Build +# Commit triggers for CI runs on specified branches. +# +# No paths filters are specified, so all commits to the branches will trigger a +# build. +# +trigger: + batch: true branches: include: - - internal/main - always: true + - release/6.1 + - internal/release/6.1 -- cron: '0 0 * * Mon-Fri' - displayName: Daily build 5:00 PM (UTC - 7) Build - branches: - include: - - main - always: true +# Scheduled runs. +schedules: + + # GitHub on Sundays 04:30 UTC. + - cron: '30 4 * * Sun' + displayName: Sunday Run + branches: + include: + - release/6.1 + always: true + + # ADO on Sundays 05:30 UTC. + - cron: '30 5 * * Sun' + displayName: Sunday Run + branches: + include: + - internal/release/6.1 + always: true parameters: # parameters are shown up in ADO UI in a build queue time - name: 'debug' @@ -81,6 +104,24 @@ parameters: # parameters are shown up in ADO UI in a build queue time - Project - Package +- name: buildConfiguration + displayName: 'Build Configuration' + default: Release + values: + - Release + - Debug + +- name: enableStressTests + displayName: Enable Stress Tests + type: boolean + default: false + +# The timeout, in minutes, for each test job. +- name: testsTimeout + displayName: 'Tests timeout (in minutes)' + type: string + default: 90 + extends: template: dotnet-sqlclient-ci-core.yml@self parameters: @@ -92,3 +133,6 @@ extends: useManagedSNI: ${{ parameters.useManagedSNI }} codeCovTargetFrameworks: ${{ parameters.codeCovTargetFrameworks }} buildType: ${{ parameters.buildType }} + buildConfiguration: ${{ parameters.buildConfiguration }} + enableStressTests: ${{ parameters.enableStressTests }} + testsTimeout: ${{ parameters.testsTimeout }} diff --git a/eng/pipelines/dotnet-sqlclient-ci-project-reference-pipeline.yml b/eng/pipelines/dotnet-sqlclient-ci-project-reference-pipeline.yml index ecdaacfafb..628295dca9 100644 --- a/eng/pipelines/dotnet-sqlclient-ci-project-reference-pipeline.yml +++ b/eng/pipelines/dotnet-sqlclient-ci-project-reference-pipeline.yml @@ -5,28 +5,59 @@ ################################################################################# name: $(DayOfYear)$(Rev:rr) -trigger: - batch: true + +# Trigger PR validation runs for all pushes to PRs that target the specified +# branches. +# +# https://learn.microsoft.com/en-us/azure/devops/pipelines/repos/github?view=azure-devops&tabs=yaml#pr-triggers +# +pr: branches: include: - - main - - internal/main + - release/6.1 + paths: include: - - src + - .azuredevops + - .config + - doc - eng + - src - tools - - .config + - azurepipelines-coverage.yml - build.proj - - Nuget.config + - NuGet.config -schedules: -- cron: '0 5 * * Thu' - displayName: Weekly Wednesday 10:00 PM (UTC - 7) Build +# Commit triggers for CI runs on specified branches. +# +# No paths filters are specified, so all commits to the branches will trigger a +# build. +# +trigger: + batch: true branches: include: - - internal/main - always: true + - release/6.1 + - internal/release/6.1 + +# Scheduled runs. +schedules: + + # GitHub on Sundays 04:00 UTC. + - cron: '0 4 * * Sun' + displayName: Sunday Run + branches: + include: + - release/6.1 + always: true + + # ADO on Sundays 05:00 UTC. + - cron: '0 5 * * Sun' + displayName: Sunday Run + branches: + include: + - internal/release/6.1 + always: true parameters: # parameters are shown up in ADO UI in a build queue time - name: 'debug' @@ -73,6 +104,24 @@ parameters: # parameters are shown up in ADO UI in a build queue time - Project - Package +- name: buildConfiguration + displayName: 'Build Configuration' + default: Release + values: + - Release + - Debug + +- name: enableStressTests + displayName: Enable Stress Tests + type: boolean + default: false + +# The timeout, in minutes, for each test job. +- name: testsTimeout + displayName: 'Tests timeout (in minutes)' + type: string + default: 90 + extends: template: dotnet-sqlclient-ci-core.yml@self parameters: @@ -84,3 +133,6 @@ extends: useManagedSNI: ${{ parameters.useManagedSNI }} codeCovTargetFrameworks: ${{ parameters.codeCovTargetFrameworks }} buildType: ${{ parameters.buildType }} + buildConfiguration: ${{ parameters.buildConfiguration }} + enableStressTests: ${{ parameters.enableStressTests }} + testsTimeout: ${{ parameters.testsTimeout }} diff --git a/eng/pipelines/dotnet-sqlclient-signing-pipeline.yml b/eng/pipelines/dotnet-sqlclient-signing-pipeline.yml index c5448be6bc..cabb39e11f 100644 --- a/eng/pipelines/dotnet-sqlclient-signing-pipeline.yml +++ b/eng/pipelines/dotnet-sqlclient-signing-pipeline.yml @@ -8,7 +8,7 @@ name: $(Year:YY)$(DayOfYear)$(Rev:.r) trigger: branches: include: - - internal/main + - internal/release/6.1 paths: include: - src @@ -21,19 +21,13 @@ trigger: - '*.sh' schedules: -- cron: '30 4 * * Mon' - displayName: Weekly Sunday 9:30 PM (UTC - 7) Build +- cron: '0 5 * * Mon' + displayName: Weekly Sunday 10:00 PM (UTC - 7) Build branches: include: - - internal/main + - internal/release/6.1 always: true -- cron: '30 3 * * Mon-Fri' - displayName: Mon-Fri 8:30 PM (UTC - 7) Build - branches: - include: - - internal/main - parameters: # parameters are shown up in ADO UI in a build queue time - name: 'debug' displayName: 'Enable debug output' @@ -62,6 +56,12 @@ parameters: # parameters are shown up in ADO UI in a build queue time type: boolean default: false +# The timeout, in minutes, for each test job. +- name: testsTimeout + displayName: 'Tests timeout (in minutes)' + type: string + default: 90 + variables: - template: /eng/pipelines/libraries/variables.yml@self - name: packageFolderName @@ -161,6 +161,7 @@ extends: parameters: packageFolderName: $(packageFolderName) isPreview: ${{ parameters['isPreview'] }} + timeout: ${{ parameters.testsTimeout }} downloadPackageStep: download: current artifact: $(packageFolderName) diff --git a/eng/pipelines/jobs/build-akv-official-job.yml b/eng/pipelines/jobs/build-akv-official-job.yml index a4374b773b..af8f546eff 100644 --- a/eng/pipelines/jobs/build-akv-official-job.yml +++ b/eng/pipelines/jobs/build-akv-official-job.yml @@ -91,6 +91,7 @@ jobs: assemblyFileVersion: '${{ parameters.assemblyFileVersion }}' buildConfiguration: '${{ parameters.buildConfiguration }}' mdsPackageVersion: '${{ parameters.mdsPackageVersion }}' + signingKeyPath: '$(Agent.TempDirectory)/netfxKeypair.snk' - ${{ each targetFramework in parameters.targetFrameworks }}: - template: ../steps/compound-extract-akv-apiscan-files-step.yml @@ -105,6 +106,7 @@ jobs: parameters: buildConfiguration: '${{ parameters.buildConfiguration }}' mdsPackageVersion: '${{ parameters.mdsPackageVersion }}' + signingKeyPath: '$(Agent.TempDirectory)/netfxKeypair.snk' - template: ../steps/compound-esrp-code-signing-step.yml@self parameters: diff --git a/eng/pipelines/jobs/stress-tests-ci-job.yml b/eng/pipelines/jobs/stress-tests-ci-job.yml new file mode 100644 index 0000000000..2e01470fe5 --- /dev/null +++ b/eng/pipelines/jobs/stress-tests-ci-job.yml @@ -0,0 +1,214 @@ +################################################################################ +# Licensed to the .NET Foundation under one or more agreements. The .NET +# Foundation licenses this file to you under the MIT license. See the LICENSE +# file in the project root for more information. +################################################################################ + +# This stage builds and runs stress tests against an MDS NuGet package available +# as a pipeline artifact. +# +# The stress tests are located here: +# +# src/Microsoft.Data.SqlClient/tests/StressTests +# +# This template defines a job named 'run_stress_tests_job_' that can be +# depended on by downstream jobs. + +parameters: + # The suffix to append to the job name. + - name: jobNameSuffix + type: string + default: '' + + # The prefix to prepend to the job's display name: + # + # [] Run Stress Tests + # + - name: displayNamePrefix + type: string + default: '' + + # The name of the Azure Pipelines pool to use. + - name: poolName + type: string + default: '' + + # The pool VM image to use. + - name: vmImage + type: string + default: '' + + # The pipeline step to run to configure SQL Server. + # + # This step is expected to require no parameters. It must configure a SQL + # Server instance listening on localhost for SQL auth via the 'sa' user with + # the pipeline variable $(Password) as the password. + - name: sqlSetupStep + type: string + default: '' + + # The name of the pipeline artifact to download that contains the MDS package + # to stress test. + - name: pipelineArtifactName + type: string + default: '' + + # The solution file to restore/build. + - name: solution + type: string + default: '' + + # The test project to run. + - name: testProject + type: string + default: '' + + # dotnet CLI arguments for the restore step. + - name: restoreArguments + type: string + default: '' + + # dotnet CLI arguments for the build and run steps. + - name: buildArguments + type: string + default: '' + + # The list of .NET runtimes to test against. + - name: netTestRuntimes + type: object + default: [] + + # The list of .NET Framework runtimes to test against. + - name: netFrameworkTestRuntimes + type: object + default: [] + + # The stress test config file contents to write to the config file. + # + # This should point to the SQL Server configured via the sqlSetupStep + # parameter, with user 'sa' and password of $(Password). + - name: configContent + type: string + default: '' + +jobs: +- job: run_stress_tests_job_${{ parameters.jobNameSuffix }} + displayName: '[${{ parameters.displayNamePrefix }}] Run Stress Tests' + pool: + name: ${{ parameters.poolName }} + ${{ if eq(parameters.poolName, 'Azure Pipelines') }}: + vmImage: ${{ parameters.vmImage }} + ${{ else }}: + demands: + - imageOverride -equals ${{ parameters.vmImage }} + + variables: + # Stress test command-line arguments. + - name: testArguments + value: -a SqlClient.Stress.Tests -console + + # Explicitly unset the $PLATFORM environment variable that is set by the + # 'ADO Build properties' Library in the ADO SqlClientDrivers public project. + # This is defined with a non-standard Platform of 'AnyCPU', and will fail + # the builds if left defined. The stress tests solution does not require + # any specific Platform, and so its solution file doesn't support any + # non-standard platforms. + # + # Note that Azure Pipelines will inject this variable as PLATFORM into the + # environment of all tasks in this job. + # + # See: + # https://learn.microsoft.com/en-us/azure/devops/pipelines/process/variables?view=azure-devops&tabs=yaml%2Cbatch + # + - name: Platform + value: '' + + # Do the same for $CONFIGURATION since we explicitly set it using our + # 'buildConfiguration' parameter, and we don't want the environment to + # override us. + - name: Configuration + value: '' + + steps: + + # Install the .NET 9.0 SDK. + - task: UseDotNet@2 + displayName: Install .NET 9.0 SDK + inputs: + packageType: sdk + version: 9.x + + # Install the .NET 8.0 runtime. + - task: UseDotNet@2 + displayName: Install .NET 8.0 Runtime + inputs: + packageType: runtime + version: 8.x + + # Download the pipeline artifact that contains the MDS package to test. + - task: DownloadPipelineArtifact@2 + displayName: Download Pipeline Artifact + inputs: + artifactName: ${{ parameters.pipelineArtifactName }} + # The stress tests solution has a NuGet.config file that configures + # sources to look in this packages/ directory. + targetPath: $(Build.SourcesDirectory)/packages + + # Setup the local SQL Server. + - template: ${{ parameters.sqlSetupStep }}@self + + # We use the 'custom' command because the DotNetCoreCLI@2 task doesn't support + # all of our argument combinations for the different build steps. + + # Restore the solution. + - task: DotNetCoreCLI@2 + displayName: Restore Solution + inputs: + command: custom + custom: restore + projects: ${{ parameters.solution }} + arguments: ${{ parameters.restoreArguments }} + + # Build the solution. + - task: DotNetCoreCLI@2 + displayName: Build Solution + inputs: + command: custom + custom: build + projects: ${{ parameters.solution }} + arguments: ${{ parameters.buildArguments }} --no-restore + + # Write the config file. + - task: PowerShell@2 + displayName: Write Config File + inputs: + pwsh: true + targetType: inline + script: | + # Capture the multi-line JSON content into a variable. + $content = @" + ${{ parameters.configContent }} + "@ + + # Write the JSON content to the config file. + $content | Out-File -FilePath "config.json" + + # Run the stress tests for each .NET runtime. + - ${{ each runtime in parameters.netTestRuntimes }}: + - task: DotNetCoreCLI@2 + displayName: Test [${{runtime}}] + inputs: + command: custom + custom: run + projects: ${{ parameters.testProject }} + arguments: ${{ parameters.buildArguments }} --no-build -f ${{runtime}} -e STRESS_CONFIG_FILE=config.json -- $(testArguments) + + # Run the stress tests for each .NET Framework runtime. + - ${{ each runtime in parameters.netFrameworkTestRuntimes }}: + - task: DotNetCoreCLI@2 + displayName: Test [${{runtime}}] + inputs: + command: custom + custom: run + projects: ${{ parameters.testProject }} + arguments: ${{ parameters.buildArguments }} --no-build -f ${{runtime}} -e STRESS_CONFIG_FILE=config.json -- $(testArguments) diff --git a/eng/pipelines/libraries/common-variables.yml b/eng/pipelines/libraries/common-variables.yml index 512175a056..b1b5d5ce7f 100644 --- a/eng/pipelines/libraries/common-variables.yml +++ b/eng/pipelines/libraries/common-variables.yml @@ -32,13 +32,13 @@ variables: - name: Minor value: '1' - name: Patch - value: '0' + value: '1' # Update this for preview releases. - name: Preview value: '-preview' - name: Revision - value: '2' + value: '3' - name: NugetPackageVersion value: $(Major).$(Minor).$(Patch) diff --git a/eng/pipelines/stages/stress-tests-ci-stage.yml b/eng/pipelines/stages/stress-tests-ci-stage.yml new file mode 100644 index 0000000000..06b41cd421 --- /dev/null +++ b/eng/pipelines/stages/stress-tests-ci-stage.yml @@ -0,0 +1,191 @@ +################################################################################ +# Licensed to the .NET Foundation under one or more agreements. The .NET +# Foundation licenses this file to you under the MIT license. See the LICENSE +# file in the project root for more information. +################################################################################ + +# This stage builds and runs stress tests against an MDS NuGet package available +# as a pipeline artifact. +# +# The stress tests are located here: +# +# src/Microsoft.Data.SqlClient/tests/StressTests +# +# All tests use a localhost SQL Server configured for SQL auth via the 'sa' user +# and password of '$(Password)'. The $(Password) variable is defined in the ADO +# Library "ADO Test Configuration properties", brought in by +# common/templates/libraries/ci-build-variables.yml. +# +# This template defines a stage named 'run_stress_tests_stage' that can be +# depended on by downstream stages. + +parameters: + # The type of build to produce (Release or Debug) + - name: buildConfiguration + displayName: Build Configuration + type: string + default: Release + values: + - Release + - Debug + + # The names of any stages this stage depends on, for example the stages + # that publish the MDS package artifacts we will test. + - name: dependsOn + displayName: Depends On Stages + type: object + default: [] + + # The name of the pipeline artifact to download that contains the MDS package + # to stress test. + - name: pipelineArtifactName + displayName: Pipeline Artifact Name + type: string + default: Artifacts + + # The MDS package version to stress test. This version must be available in + # one of the configured NuGet sources. + - name: mdsPackageVersion + displayName: MDS Package Version + type: string + default: '' + + # The list of .NET runtimes to test against. + - name: netTestRuntimes + displayName: .NET Test Runtimes + type: object + default: [net8.0, net9.0] + + # The list of .NET Framework runtimes to test against. + - name: netFrameworkTestRuntimes + displayName: .NET Framework Test Runtimes + type: object + default: [net462, net47, net471, net472, net48, net481] + + # The verbosity level for the dotnet CLI commands. + - name: verbosity + displayName: Dotnet CLI verbosity + type: string + default: normal + values: + - quiet + - minimal + - normal + - detailed + - diagnostic + +stages: + - stage: run_stress_tests_stage + displayName: Run Stress Tests + dependsOn: ${{ parameters.dependsOn }} + + variables: + # The directory where dotnet artifacts will be staged. Not to be + # confused with pipeline artifact. + - name: dotnetArtifactsDir + value: $(Build.StagingDirectory)/dotnetArtifacts + + # The solution file to use for all dotnet CLI commands. + - name: solution + value: src/Microsoft.Data.SqlClient/tests/StressTests/StressTests.slnx + + # The stress test project to run. + - name: testProject + value: src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Runner/SqlClient.Stress.Runner.csproj + + # dotnet CLI arguments common to all commands. + - name: commonArguments + value: >- + --verbosity ${{parameters.verbosity}} + --artifacts-path $(dotnetArtifactsDir) + -p:MdsPackageVersion=${{parameters.mdsPackageVersion}} + + # dotnet CLI arguments for build/run commands. + - name: buildArguments + value: >- + $(commonArguments) + --configuration ${{parameters.buildConfiguration}} + + # The contents of the config file to use for all tests. We will write + # this to a JSON file for each test job, and then point to it via the + # STRESS_CONFIG_FILE environment variable. + - name: ConfigContent + value: | + [ + { + "name": "Azure SQL", + "type": "SqlServer", + "isDefault": true, + "dataSource": "localhost", + "user": "sa", + "password": "$(Password)", + "supportsWindowsAuthentication": false, + "isLocal": false, + "disableMultiSubnetFailover": true, + "disableNamedPipes": true, + "encrypt": false + } + ] + + jobs: + + # -------------------------------------------------------------------------- + # Build and test on Linux. + + - template: ../jobs/stress-tests-ci-job.yml@self + parameters: + jobNameSuffix: linux + displayNamePrefix: Linux + poolName: $(ci_var_defaultPoolName) + vmImage: ADO-UB20-SQL22 + sqlSetupStep: /eng/pipelines/common/templates/steps/configure-sql-server-linux-step.yml + pipelineArtifactName: ${{ parameters.pipelineArtifactName }} + solution: $(solution) + testProject: $(testProject) + restoreArguments: $(commonArguments) + buildArguments: $(buildArguments) + netTestRuntimes: ${{ parameters.netTestRuntimes }} + configContent: $(ConfigContent) + + # -------------------------------------------------------------------------- + # Build and test on Windows + + - template: ../jobs/stress-tests-ci-job.yml + parameters: + jobNameSuffix: windows + displayNamePrefix: Win + poolName: $(ci_var_defaultPoolName) + # The Windows images include a suitable .NET Framework runtime, so we + # don't have to install one explicitly. + vmImage: ADO-MMS22-SQL22 + sqlSetupStep: /eng/pipelines/common/templates/steps/configure-sql-server-win-step.yml + pipelineArtifactName: ${{ parameters.pipelineArtifactName }} + solution: $(solution) + testProject: $(testProject) + restoreArguments: $(commonArguments) + buildArguments: $(buildArguments) + netTestRuntimes: ${{ parameters.netTestRuntimes }} + # Note that we include the .NET Framework runtimes for test runs on + # Windows. + netFrameworkTestRuntimes: ${{ parameters.netFrameworkTestRuntimes }} + configContent: $(ConfigContent) + + # -------------------------------------------------------------------------- + # Build and test on macOS. + + - template: ../jobs/stress-tests-ci-job.yml + parameters: + jobNameSuffix: macos + displayNamePrefix: macOS + # We don't have any 1ES Hosted Pool images for macOS, so we use a + # generic one from Azure Pipelines. + poolName: Azure Pipelines + vmImage: macos-latest + sqlSetupStep: /eng/pipelines/common/templates/steps/configure-sql-server-macos-step.yml + pipelineArtifactName: ${{ parameters.pipelineArtifactName }} + solution: $(solution) + testProject: $(testProject) + restoreArguments: $(commonArguments) + buildArguments: $(buildArguments) + netTestRuntimes: ${{ parameters.netTestRuntimes }} + configContent: $(ConfigContent) diff --git a/eng/pipelines/steps/compound-build-akv-step.yml b/eng/pipelines/steps/compound-build-akv-step.yml index 906dcfaf72..fb6b0e2a06 100644 --- a/eng/pipelines/steps/compound-build-akv-step.yml +++ b/eng/pipelines/steps/compound-build-akv-step.yml @@ -19,6 +19,9 @@ parameters: - name: mdsPackageVersion type: string + - name: signingKeyPath + type: string + steps: - task: DownloadSecureFile@1 displayName: 'Download Signing Key' @@ -48,7 +51,7 @@ steps: -p:AssemblyFileVersion=${{ parameters.assemblyFileVersion }} -p:NugetPackageVersion=${{ parameters.mdsPackageVersion }} -p:ReferenceType=Package - -p:SigningKeyPath=$(Agent.TempDirectory)/netfxKeypair.snk + -p:SigningKeyPath=${{ parameters.signingKeyPath }} - script: tree /a /f $(BUILD_OUTPUT) displayName: Output Build Output Tree diff --git a/eng/pipelines/steps/roslyn-analyzers-akv-step.yml b/eng/pipelines/steps/roslyn-analyzers-akv-step.yml index 0e05177d5a..d65ec57ca4 100644 --- a/eng/pipelines/steps/roslyn-analyzers-akv-step.yml +++ b/eng/pipelines/steps/roslyn-analyzers-akv-step.yml @@ -4,9 +4,13 @@ # See the LICENSE file in the project root for more information. # ################################################################################# -# @TODO: This can probably be made generic and pass in the command lines for msbuild -# BUT, they should be kept separate by now as we rebuild build.proj in parallel, we won't -# affect >1 project at a time. +# NOTE: Because Roslyn analyzers run with the build process, this step must happen within our +# build in order to generate logs that Guardian/SDL can consume. HOWEVER - this step will rebuild +# the project and overwrite any previously build output! Therefore, the command line params in +# this step and the build step must be the same to avoid packaging invalid binaries! +# There is a way to avoid using this task and have analyzers run during the main build, but this +# task will ensure we are using the latest analyzers as per SDL. +# For more info, please see: https://eng.ms/docs/cloud-ai-platform/devdiv/one-engineering-system-1es/1es-mohanb/security-integration/guardian-wiki/sdl-azdo-extension/roslyn-analyzers-build-task parameters: - name: buildConfiguration @@ -15,6 +19,9 @@ parameters: - name: mdsPackageVersion type: string + - name: signingKeyPath + type: string + steps: - task: securedevelopmentteam.vss-secure-development-tools.build-task-roslynanalyzers.RoslynAnalyzers@3 displayName: 'Roslyn Analyzers' @@ -27,5 +34,6 @@ steps: -p:Configuration=${{ parameters.buildConfiguration }} -p:NugetPackageVersion=${{ parameters.mdsPackageVersion }} -p:ReferenceType=Package + -p:SigningKeyPath=${{ parameters.signingKeyPath }} msBuildVersion: 17.0 setupCommandLinePicker: vs2022 diff --git a/eng/pipelines/variables/akv-official-variables.yml b/eng/pipelines/variables/akv-official-variables.yml index 4fa517341d..30176ac98b 100644 --- a/eng/pipelines/variables/akv-official-variables.yml +++ b/eng/pipelines/variables/akv-official-variables.yml @@ -22,15 +22,15 @@ variables: # Base Variables ------------------------------------------------------- - name: mdsPackageVersion - value: '6.0.1' + value: '6.1.1' # @TODO: Version should ideally be pulled from one location (versions.props?) - name: versionMajor value: '6' - name: versionMinor - value: '0' + value: '1' - name: versionPatch - value: '0' + value: '2' - name: versionPreview value: '-preview1' @@ -38,6 +38,7 @@ variables: - name: assemblyFileVersion value: '${{ variables.versionMajor }}.${{ variables.versionMinor }}${{ variables.versionPatch }}.$(Build.BuildNumber)' - name: nugetPackageVersion - value: '${{ variables.versionMajor }}.${{ variables.versionMinor }}.${{ variables.versionPatch }}${{ variables.versionPreview }}' + value: '${{ variables.versionMajor }}.${{ variables.versionMinor }}.${{ variables.versionPatch }}' + #value: '${{ variables.versionMajor }}.${{ variables.versionMinor }}.${{ variables.versionPatch }}${{ variables.versionPreview }}' diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index 7d94425123..5cd85e5a1f 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -13,7 +13,7 @@ - + @@ -25,7 +25,7 @@ - + diff --git a/src/Microsoft.Data.SqlClient.sln b/src/Microsoft.Data.SqlClient.sln index e4d29d999c..c3a9eeb55b 100644 --- a/src/Microsoft.Data.SqlClient.sln +++ b/src/Microsoft.Data.SqlClient.sln @@ -287,6 +287,9 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "variables", "variables", "{ EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "jobs", "jobs", "{09352F1D-878F-4F55-8AA2-6E47F1AD37D5}" + ProjectSection(SolutionItems) = preProject + ..\eng\pipelines\jobs\build-akv-official-job.yml = ..\eng\pipelines\jobs\build-akv-official-job.yml + EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "steps", "steps", "{AD738BD4-6A02-4B88-8F93-FBBBA49A74C8}" ProjectSection(SolutionItems) = preProject diff --git a/src/Microsoft.Data.SqlClient/add-ons/AzureKeyVaultProvider/AzureSqlKeyCryptographer.cs b/src/Microsoft.Data.SqlClient/add-ons/AzureKeyVaultProvider/AzureSqlKeyCryptographer.cs index 0dff6ac786..c4e9eb8396 100644 --- a/src/Microsoft.Data.SqlClient/add-ons/AzureKeyVaultProvider/AzureSqlKeyCryptographer.cs +++ b/src/Microsoft.Data.SqlClient/add-ons/AzureKeyVaultProvider/AzureSqlKeyCryptographer.cs @@ -7,12 +7,12 @@ using Azure.Security.KeyVault.Keys.Cryptography; using System; using System.Collections.Concurrent; -using System.Threading.Tasks; +using System.Threading; using static Azure.Security.KeyVault.Keys.Cryptography.SignatureAlgorithm; namespace Microsoft.Data.SqlClient.AlwaysEncrypted.AzureKeyVaultProvider { - internal class AzureSqlKeyCryptographer + internal sealed class AzureSqlKeyCryptographer : IDisposable { /// /// TokenCredential to be used with the KeyClient @@ -25,16 +25,14 @@ internal class AzureSqlKeyCryptographer private readonly ConcurrentDictionary _keyClientDictionary = new(); /// - /// Holds references to the fetch key tasks and maps them to their corresponding Azure Key Vault Key Identifier (URI). - /// These tasks will be used for returning the key in the event that the fetch task has not finished depositing the - /// key into the key dictionary. + /// Holds references to the Azure Key Vault keys and maps them to their corresponding Azure Key Vault Key Identifier (URI). /// - private readonly ConcurrentDictionary>> _keyFetchTaskDictionary = new(); + private readonly ConcurrentDictionary _keyDictionary = new(); /// - /// Holds references to the Azure Key Vault keys and maps them to their corresponding Azure Key Vault Key Identifier (URI). + /// SemaphoreSlim to ensure thread safety when accessing the key dictionary or making network calls to Azure Key Vault to fetch keys. /// - private readonly ConcurrentDictionary _keyDictionary = new(); + private SemaphoreSlim _keyDictionarySemaphore = new(1, 1); /// /// Holds references to the Azure Key Vault CryptographyClient objects and maps them to their corresponding Azure Key Vault Key Identifier (URI). @@ -50,20 +48,44 @@ internal AzureSqlKeyCryptographer(TokenCredential tokenCredential) TokenCredential = tokenCredential; } + /// + /// Disposes the SemaphoreSlim used for thread safety. + /// + public void Dispose() + { + _keyDictionarySemaphore.Dispose(); + } + /// /// Adds the key, specified by the Key Identifier URI, to the cache. + /// Validates the key type and fetches the key from Azure Key Vault if it is not already cached. /// /// internal void AddKey(string keyIdentifierUri) { - if (TheKeyHasNotBeenCached(keyIdentifierUri)) + // Allow only one thread to proceed to ensure thread safety + // as we will need to fetch key information from Azure Key Vault if the key is not found in cache. + _keyDictionarySemaphore.Wait(); + + try { - ParseAKVPath(keyIdentifierUri, out Uri vaultUri, out string keyName, out string keyVersion); - CreateKeyClient(vaultUri); - FetchKey(vaultUri, keyName, keyVersion, keyIdentifierUri); - } + if (!_keyDictionary.ContainsKey(keyIdentifierUri)) + { + ParseAKVPath(keyIdentifierUri, out Uri vaultUri, out string keyName, out string keyVersion); + + // Fetch the KeyClient for the Key vault URI. + KeyClient keyClient = GetOrCreateKeyClient(vaultUri); + + // Fetch the key from Azure Key Vault. + KeyVaultKey key = FetchKeyFromKeyVault(keyClient, keyName, keyVersion); - bool TheKeyHasNotBeenCached(string k) => !_keyDictionary.ContainsKey(k) && !_keyFetchTaskDictionary.ContainsKey(k); + _keyDictionary.AddOrUpdate(keyIdentifierUri, key, (k, v) => key); + } + } + finally + { + _keyDictionarySemaphore.Release(); + } } /// @@ -75,18 +97,12 @@ internal KeyVaultKey GetKey(string keyIdentifierUri) { if (_keyDictionary.TryGetValue(keyIdentifierUri, out KeyVaultKey key)) { - AKVEventSource.Log.TryTraceEvent("Fetched master key from cache"); + AKVEventSource.Log.TryTraceEvent("Fetched key name={0} from cache", key.Name); return key; } - if (_keyFetchTaskDictionary.TryGetValue(keyIdentifierUri, out Task> task)) - { - AKVEventSource.Log.TryTraceEvent("New Master key fetched."); - return Task.Run(() => task).GetAwaiter().GetResult(); - } - // Not a public exception - not likely to occur. - AKVEventSource.Log.TryTraceEvent("Master key not found."); + AKVEventSource.Log.TryTraceEvent("Key not found; URI={0}", keyIdentifierUri); throw ADP.MasterKeyNotFound(keyIdentifierUri); } @@ -95,10 +111,7 @@ internal KeyVaultKey GetKey(string keyIdentifierUri) /// /// The key vault key identifier URI /// - internal int GetKeySize(string keyIdentifierUri) - { - return GetKey(keyIdentifierUri).Key.N.Length; - } + internal int GetKeySize(string keyIdentifierUri) => GetKey(keyIdentifierUri).Key.N.Length; /// /// Generates signature based on RSA PKCS#v1.5 scheme using a specified Azure Key Vault Key URL. @@ -142,41 +155,50 @@ private CryptographyClient GetCryptographyClient(string keyIdentifierUri) CryptographyClient cryptographyClient = new(GetKey(keyIdentifierUri).Id, TokenCredential); _cryptoClientDictionary.TryAdd(keyIdentifierUri, cryptographyClient); - return cryptographyClient; } /// - /// + /// Fetches the column encryption key from the Azure Key Vault. /// - /// The Azure Key Vault URI + /// The KeyClient instance /// The name of the Azure Key Vault key /// The version of the Azure Key Vault key - /// The Azure Key Vault key identifier - private void FetchKey(Uri vaultUri, string keyName, string keyVersion, string keyResourceUri) + private KeyVaultKey FetchKeyFromKeyVault(KeyClient keyClient, string keyName, string keyVersion) { - Task> fetchKeyTask = FetchKeyFromKeyVault(vaultUri, keyName, keyVersion); - _keyFetchTaskDictionary.AddOrUpdate(keyResourceUri, fetchKeyTask, (k, v) => fetchKeyTask); + AKVEventSource.Log.TryTraceEvent("Fetching key name={0}", keyName); - fetchKeyTask - .ContinueWith(k => ValidateRsaKey(k.GetAwaiter().GetResult())) - .ContinueWith(k => _keyDictionary.AddOrUpdate(keyResourceUri, k.GetAwaiter().GetResult(), (key, v) => k.GetAwaiter().GetResult())); + Azure.Response keyResponse = keyClient?.GetKey(keyName, keyVersion); - Task.Run(() => fetchKeyTask); + // Handle the case where the key response is null or contains an error + // This can happen if the key does not exist or if there is an issue with the KeyClient. + // In such cases, we log the error and throw an exception. + if (keyResponse == null || keyResponse.Value == null || keyResponse.GetRawResponse().IsError) + { + AKVEventSource.Log.TryTraceEvent("Get Key failed to fetch Key from Azure Key Vault for key {0}, version {1}", keyName, keyVersion); + if (keyResponse?.GetRawResponse() is Azure.Response response) + { + AKVEventSource.Log.TryTraceEvent("Response status {0} : {1}", response.Status, response.ReasonPhrase); + } + throw ADP.GetKeyFailed(keyName); + } + + KeyVaultKey key = keyResponse.Value; + + // Validate that the key is of type RSA + key = ValidateRsaKey(key); + return key; } /// - /// Looks up the KeyClient object by it's URI and then fetches the key by name. + /// Gets or creates a KeyClient for the specified Azure Key Vault URI. /// - /// The Azure Key Vault URI - /// Then name of the key - /// Then version of the key + /// Key Identifier URL /// - private Task> FetchKeyFromKeyVault(Uri vaultUri, string keyName, string keyVersion) + private KeyClient GetOrCreateKeyClient(Uri vaultUri) { - _keyClientDictionary.TryGetValue(vaultUri, out KeyClient keyClient); - AKVEventSource.Log.TryTraceEvent("Fetching requested master key: {0}", keyName); - return keyClient?.GetKeyAsync(keyName, keyVersion); + return _keyClientDictionary.GetOrAdd( + vaultUri, (_) => new KeyClient(vaultUri, TokenCredential)); } /// @@ -184,7 +206,7 @@ private void FetchKey(Uri vaultUri, string keyName, string keyVersion, string ke /// /// /// - private KeyVaultKey ValidateRsaKey(KeyVaultKey key) + private static KeyVaultKey ValidateRsaKey(KeyVaultKey key) { if (key.KeyType != KeyType.Rsa && key.KeyType != KeyType.RsaHsm) { @@ -195,18 +217,6 @@ private KeyVaultKey ValidateRsaKey(KeyVaultKey key) return key; } - /// - /// Instantiates and adds a KeyClient to the KeyClient dictionary - /// - /// The Azure Key Vault URI - private void CreateKeyClient(Uri vaultUri) - { - if (!_keyClientDictionary.ContainsKey(vaultUri)) - { - _keyClientDictionary.TryAdd(vaultUri, new KeyClient(vaultUri, TokenCredential)); - } - } - /// /// Validates and parses the Azure Key Vault URI and key name. /// @@ -214,7 +224,7 @@ private void CreateKeyClient(Uri vaultUri) /// The Azure Key Vault URI /// The name of the key /// The version of the key - private void ParseAKVPath(string masterKeyPath, out Uri vaultUri, out string masterKeyName, out string masterKeyVersion) + private static void ParseAKVPath(string masterKeyPath, out Uri vaultUri, out string masterKeyName, out string masterKeyVersion) { Uri masterKeyPathUri = new(masterKeyPath); vaultUri = new Uri(masterKeyPathUri.GetLeftPart(UriPartial.Authority)); diff --git a/src/Microsoft.Data.SqlClient/add-ons/AzureKeyVaultProvider/LocalCache.cs b/src/Microsoft.Data.SqlClient/add-ons/AzureKeyVaultProvider/LocalCache.cs index 3e17f5d951..7fbffe0ae6 100644 --- a/src/Microsoft.Data.SqlClient/add-ons/AzureKeyVaultProvider/LocalCache.cs +++ b/src/Microsoft.Data.SqlClient/add-ons/AzureKeyVaultProvider/LocalCache.cs @@ -2,8 +2,8 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -using Microsoft.Extensions.Caching.Memory; using System; +using Microsoft.Extensions.Caching.Memory; using static System.Math; namespace Microsoft.Data.SqlClient.AlwaysEncrypted.AzureKeyVaultProvider @@ -92,6 +92,7 @@ internal TValue GetOrCreate(TKey key, Func createItem) /// /// Determines whether the LocalCache contains the specified key. + /// Used in unit tests to verify that the cache contains the expected entries. /// /// /// diff --git a/src/Microsoft.Data.SqlClient/add-ons/AzureKeyVaultProvider/Microsoft.Data.SqlClient.AlwaysEncrypted.AzureKeyVaultProvider.csproj b/src/Microsoft.Data.SqlClient/add-ons/AzureKeyVaultProvider/Microsoft.Data.SqlClient.AlwaysEncrypted.AzureKeyVaultProvider.csproj index 51af5632e3..dcd2e49477 100644 --- a/src/Microsoft.Data.SqlClient/add-ons/AzureKeyVaultProvider/Microsoft.Data.SqlClient.AlwaysEncrypted.AzureKeyVaultProvider.csproj +++ b/src/Microsoft.Data.SqlClient/add-ons/AzureKeyVaultProvider/Microsoft.Data.SqlClient.AlwaysEncrypted.AzureKeyVaultProvider.csproj @@ -18,14 +18,14 @@ true true + - + true $(SigningKeyPath) - - $(SigningKeyPath) + $([System.IO.Path]::Combine('$(IntermediateOutputPath)','$(GeneratedSourceFileName)')) diff --git a/src/Microsoft.Data.SqlClient/add-ons/AzureKeyVaultProvider/SqlColumnEncryptionAzureKeyVaultProvider.cs b/src/Microsoft.Data.SqlClient/add-ons/AzureKeyVaultProvider/SqlColumnEncryptionAzureKeyVaultProvider.cs index eb8c8d77c4..d4740a1183 100644 --- a/src/Microsoft.Data.SqlClient/add-ons/AzureKeyVaultProvider/SqlColumnEncryptionAzureKeyVaultProvider.cs +++ b/src/Microsoft.Data.SqlClient/add-ons/AzureKeyVaultProvider/SqlColumnEncryptionAzureKeyVaultProvider.cs @@ -4,6 +4,7 @@ using System; using System.Text; +using System.Threading; using Azure.Core; using Azure.Security.KeyVault.Keys.Cryptography; using static Microsoft.Data.SqlClient.AlwaysEncrypted.AzureKeyVaultProvider.Validator; @@ -55,6 +56,8 @@ public class SqlColumnEncryptionAzureKeyVaultProvider : SqlColumnEncryptionKeySt private readonly static KeyWrapAlgorithm s_keyWrapAlgorithm = KeyWrapAlgorithm.RsaOaep; + private SemaphoreSlim _cacheSemaphore = new(1, 1); + /// /// List of Trusted Endpoints /// @@ -69,7 +72,7 @@ public class SqlColumnEncryptionAzureKeyVaultProvider : SqlColumnEncryptionKeySt /// /// A cache for storing the results of signature verification of column master key metadata. /// - private readonly LocalCache, bool> _columnMasterKeyMetadataSignatureVerificationCache = + private readonly LocalCache, bool> _columnMasterKeyMetadataSignatureVerificationCache = new(maxSizeLimit: 2000) { TimeToLive = TimeSpan.FromDays(10) }; /// @@ -230,7 +233,7 @@ byte[] DecryptEncryptionKey() // Get ciphertext byte[] cipherText = new byte[cipherTextLength]; Array.Copy(encryptedColumnEncryptionKey, currentIndex, cipherText, 0, cipherTextLength); - + currentIndex += cipherTextLength; // Get signature @@ -394,17 +397,10 @@ private byte[] CompileMasterKeyMetadata(string masterKeyPath, bool allowEnclaveC /// An array of bytes to convert. /// A string of hexadecimal characters /// - /// Produces a string of hexadecimal character pairs preceded with "0x", where each pair represents the corresponding element in value; for example, "0x7F2C4A00". + /// Produces a string of hexadecimal character pairs preceded with "0x", where each pair represents the corresponding element in source; for example, "0x7F2C4A00". /// private string ToHexString(byte[] source) - { - if (source is null) - { - return null; - } - - return "0x" + BitConverter.ToString(source).Replace("-", ""); - } + => source is null ? null : "0x" + BitConverter.ToString(source).Replace("-", ""); /// /// Returns the cached decrypted column encryption key, or unwraps the encrypted column encryption key if not present. @@ -415,8 +411,21 @@ private string ToHexString(byte[] source) /// /// /// - private byte[] GetOrCreateColumnEncryptionKey(string encryptedColumnEncryptionKey, Func createItem) - => _columnEncryptionKeyCache.GetOrCreate(encryptedColumnEncryptionKey, createItem); + private byte[] GetOrCreateColumnEncryptionKey(string encryptedColumnEncryptionKey, Func createItem) + { + // Allow only one thread to access the cache at a time. + _cacheSemaphore.Wait(); + + try + { + return _columnEncryptionKeyCache.GetOrCreate(encryptedColumnEncryptionKey, createItem); + } + finally + { + // Release the semaphore to allow other threads to access the cache. + _cacheSemaphore.Release(); + } + } /// /// Returns the cached signature verification result, or proceeds to verify if not present. diff --git a/src/Microsoft.Data.SqlClient/add-ons/AzureKeyVaultProvider/Strings.Designer.cs b/src/Microsoft.Data.SqlClient/add-ons/AzureKeyVaultProvider/Strings.Designer.cs index fc5d88930a..c3b9ce9104 100644 --- a/src/Microsoft.Data.SqlClient/add-ons/AzureKeyVaultProvider/Strings.Designer.cs +++ b/src/Microsoft.Data.SqlClient/add-ons/AzureKeyVaultProvider/Strings.Designer.cs @@ -8,6 +8,8 @@ // //------------------------------------------------------------------------------ +using System.Globalization; + namespace Microsoft.Data.SqlClient.AlwaysEncrypted.AzureKeyVaultProvider { /// @@ -88,6 +90,17 @@ internal static string EmptyArgumentInternal } } + /// + /// Looks up a localized string similar to: Failed to fetch key from Azure Key Vault. Key: {0}. + /// + internal static string GetKeyFailed + { + get + { + return ResourceManager.GetString("GetKeyFailed", resourceCulture); + } + } + /// /// Looks up a localized string similar to Signed hash length does not match the RSA key size.. /// @@ -199,7 +212,18 @@ internal static string InvalidSignatureTemplate } /// - /// Looks up a localized string similar to Invalid trusted endpoint specified: '{0}'; a trusted endpoint must have a value.. + /// Looks up a localized string similar to The key with identifier '{0}' was not found.. + /// + internal static string MasterKeyNotFound + { + get + { + return ResourceManager.GetString("MasterKeyNotFound", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to One or more of the elements in '{0}' are null or empty or consist of only whitespace.. /// internal static string NullOrWhitespaceForEach { diff --git a/src/Microsoft.Data.SqlClient/add-ons/AzureKeyVaultProvider/Strings.resx b/src/Microsoft.Data.SqlClient/add-ons/AzureKeyVaultProvider/Strings.resx index 039d1079d5..8775b345ab 100644 --- a/src/Microsoft.Data.SqlClient/add-ons/AzureKeyVaultProvider/Strings.resx +++ b/src/Microsoft.Data.SqlClient/add-ons/AzureKeyVaultProvider/Strings.resx @@ -118,13 +118,16 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - One or more of the elements in {0} are null or empty or consist of only whitespace. + One or more of the elements in '{0}' are null or empty or consist of only whitespace. CipherText length does not match the RSA key size. - Internal error. Empty {0} specified. + Internal error. Empty '{0}' specified. + + + Failed to fetch key from Azure Key Vault. Key: {0}. The key with identifier '{0}' was not found. diff --git a/src/Microsoft.Data.SqlClient/add-ons/AzureKeyVaultProvider/Utils.cs b/src/Microsoft.Data.SqlClient/add-ons/AzureKeyVaultProvider/Utils.cs index f71080ffab..0eb4dc1c9b 100644 --- a/src/Microsoft.Data.SqlClient/add-ons/AzureKeyVaultProvider/Utils.cs +++ b/src/Microsoft.Data.SqlClient/add-ons/AzureKeyVaultProvider/Utils.cs @@ -86,7 +86,10 @@ internal static ArgumentException NullOrWhitespaceForEach(string name) => new(string.Format(Strings.NullOrWhitespaceForEach, name)); internal static KeyNotFoundException MasterKeyNotFound(string masterKeyPath) => - new(string.Format(CultureInfo.InvariantCulture, Strings.InvalidSignatureTemplate, masterKeyPath)); + new(string.Format(CultureInfo.InvariantCulture, Strings.MasterKeyNotFound, masterKeyPath)); + + internal static KeyNotFoundException GetKeyFailed(string masterKeyPath) => + new(string.Format(CultureInfo.InvariantCulture, Strings.GetKeyFailed, masterKeyPath)); internal static FormatException NonRsaKeyFormat(string keyType) => new(string.Format(CultureInfo.InvariantCulture, Strings.NonRsaKeyTemplate, keyType)); diff --git a/src/Microsoft.Data.SqlClient/netcore/ref/Microsoft.Data.SqlClient.cs b/src/Microsoft.Data.SqlClient/netcore/ref/Microsoft.Data.SqlClient.cs index 55b68fd9b3..a07133eb07 100644 --- a/src/Microsoft.Data.SqlClient/netcore/ref/Microsoft.Data.SqlClient.cs +++ b/src/Microsoft.Data.SqlClient/netcore/ref/Microsoft.Data.SqlClient.cs @@ -123,23 +123,21 @@ public SqlJson(System.Text.Json.JsonDocument jsonDoc) { } } /// - public sealed class SqlVector : System.Data.SqlTypes.INullable + public readonly struct SqlVector : System.Data.SqlTypes.INullable where T : unmanaged { /// - public SqlVector(int length) { } - /// public SqlVector(System.ReadOnlyMemory memory) { } /// public bool IsNull => throw null; /// - public static SqlVector Null => throw null; + public static SqlVector? Null => throw null; /// public int Length { get { throw null; } } - /// - public int Size { get { throw null; } } /// public System.ReadOnlyMemory Memory { get { throw null; } } + /// + public static SqlVector CreateNull(int length) { throw null; } } } namespace Microsoft.Data.SqlClient diff --git a/src/Microsoft.Data.SqlClient/netcore/ref/Microsoft.Data.SqlClient.csproj b/src/Microsoft.Data.SqlClient/netcore/ref/Microsoft.Data.SqlClient.csproj index 60eff24c1c..0944a4aea0 100644 --- a/src/Microsoft.Data.SqlClient/netcore/ref/Microsoft.Data.SqlClient.csproj +++ b/src/Microsoft.Data.SqlClient/netcore/ref/Microsoft.Data.SqlClient.csproj @@ -32,6 +32,7 @@ + diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft.Data.SqlClient.csproj b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft.Data.SqlClient.csproj index 9dae90ffd0..9152859367 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft.Data.SqlClient.csproj +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft.Data.SqlClient.csproj @@ -285,6 +285,9 @@ Microsoft\Data\SqlClient\ManagedSni\ConcurrentQueueSemaphore.netcore.cs + + Microsoft\Data\SqlClient\ManagedSni\ResolvedServerSpn.cs + Microsoft\Data\SqlClient\ManagedSni\SniAsyncCallback.netcore.cs @@ -351,9 +354,6 @@ Microsoft\Data\SqlClient\OnChangedEventHandler.cs - - Microsoft\Data\SqlClient\Packet.cs - Microsoft\Data\SqlClient\ParameterPeekAheadValue.cs @@ -759,9 +759,6 @@ Microsoft\Data\SqlClient\TdsParserStateObject.cs - - Microsoft\Data\SqlClient\TdsParserStateObject.Multiplexer.cs - Microsoft\Data\SqlClient\TdsParserStaticMethods.cs @@ -1034,6 +1031,7 @@ + diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlCommand.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlCommand.cs index 47349b492e..0497cd76b4 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlCommand.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlCommand.cs @@ -3623,7 +3623,7 @@ private void CheckNotificationStateAndAutoEnlist() } Notification.Options = SqlDependency.GetDefaultComposedOptions(_activeConnection.DataSource, - InternalTdsConnection.ServerProvidedFailOverPartner, + InternalTdsConnection.ServerProvidedFailoverPartner, identityUserName, _activeConnection.Database); } @@ -4180,7 +4180,7 @@ private SqlDataReader TryFetchInputParameterEncryptionInfo(int timeout, { // In BatchRPCMode, the actual T-SQL query is in the first parameter and not present as the rpcName, as is the case with non-BatchRPCMode. // So input parameters start at parameters[1]. parameters[0] is the actual T-SQL Statement. rpcName is sp_executesql. - if (_RPCList[i].systemParams.Length > 1) + if (_RPCList[i].systemParams != null && _RPCList[i].systemParams.Length > 1) { _RPCList[i].needsFetchParameterEncryptionMetadata = true; diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlConnection.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlConnection.cs index 45df50badb..72dec5a28e 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlConnection.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlConnection.cs @@ -236,7 +236,7 @@ private SqlConnection(SqlConnection connection) internal static bool TryGetSystemColumnEncryptionKeyStoreProvider(string keyStoreName, out SqlColumnEncryptionKeyStoreProvider provider) { - return s_systemColumnEncryptionKeyStoreProviders.TryGetValue(keyStoreName, out provider); + return s_systemColumnEncryptionKeyStoreProviders.TryGetValue(keyStoreName, out provider); } /// @@ -276,9 +276,9 @@ internal static List GetColumnEncryptionSystemKeyStoreProvidersNames() { if (s_systemColumnEncryptionKeyStoreProviders.Count > 0) { - return new List(s_systemColumnEncryptionKeyStoreProviders.Keys); + return [.. s_systemColumnEncryptionKeyStoreProviders.Keys]; } - return new List(0); + return []; } /// @@ -291,13 +291,13 @@ internal List GetColumnEncryptionCustomKeyStoreProvidersNames() if (_customColumnEncryptionKeyStoreProviders is not null && _customColumnEncryptionKeyStoreProviders.Count > 0) { - return new List(_customColumnEncryptionKeyStoreProviders.Keys); + return [.. _customColumnEncryptionKeyStoreProviders.Keys]; } if (s_globalCustomColumnEncryptionKeyStoreProviders is not null) { - return new List(s_globalCustomColumnEncryptionKeyStoreProviders.Keys); + return [.. s_globalCustomColumnEncryptionKeyStoreProviders.Keys]; } - return new List(0); + return []; } /// @@ -325,7 +325,9 @@ public static void RegisterColumnEncryptionKeyStoreProviders(IDictionary + internal override void ResetConnection() { // For implicit pooled connections, if connection reset behavior is specified, // reset the database and language properties back to default. It is important @@ -1515,7 +1508,7 @@ private void OpenLoginEnlist(TimeoutTimer timeout, throw SQL.ROR_FailoverNotSupportedConnString(); } - if (ServerProvidedFailOverPartner != null) + if (ServerProvidedFailoverPartner != null) { throw SQL.ROR_FailoverNotSupportedServer(this); } @@ -1643,7 +1636,7 @@ private void LoginNoFailover(ServerInfo serverInfo, newSecurePassword, attemptOneLoginTimeout); - if (connectionOptions.MultiSubnetFailover && ServerProvidedFailOverPartner != null) + if (connectionOptions.MultiSubnetFailover && ServerProvidedFailoverPartner != null) { // connection succeeded: trigger exception if server sends failover partner and MultiSubnetFailover is used throw SQL.MultiSubnetFailoverWithFailoverPartner(serverProvidedFailoverPartner: true, internalConnection: this); @@ -1671,7 +1664,7 @@ private void LoginNoFailover(ServerInfo serverInfo, _currentPacketSize = ConnectionOptions.PacketSize; _currentLanguage = _originalLanguage = ConnectionOptions.CurrentLanguage; CurrentDatabase = _originalDatabase = ConnectionOptions.InitialCatalog; - _currentFailoverPartner = null; + ServerProvidedFailoverPartner = null; _instanceName = string.Empty; routingAttempts++; @@ -1710,7 +1703,7 @@ private void LoginNoFailover(ServerInfo serverInfo, // We only get here when we failed to connect, but are going to re-try // Switch to failover logic if the server provided a partner - if (ServerProvidedFailOverPartner != null) + if (ServerProvidedFailoverPartner != null) { if (connectionOptions.MultiSubnetFailover) { @@ -1726,7 +1719,7 @@ private void LoginNoFailover(ServerInfo serverInfo, LoginWithFailover( true, // start by using failover partner, since we already failed to connect to the primary serverInfo, - ServerProvidedFailOverPartner, + ServerProvidedFailoverPartner, newPassword, newSecurePassword, redirectedUserInstance, @@ -1748,8 +1741,13 @@ private void LoginNoFailover(ServerInfo serverInfo, { // We must wait for CompleteLogin to finish for to have the // env change from the server to know its designated failover - // partner; save this information in _currentFailoverPartner. - PoolGroupProviderInfo.FailoverCheck(false, connectionOptions, ServerProvidedFailOverPartner); + // partner; save this information in ServerProvidedFailoverPartner. + + // When ignoring server provided failover partner, we must pass in the original failover partner from the connection string. + // Otherwise the pool group's failover partner designation will be updated to point to the server provided value. + string actualFailoverPartner = LocalAppContextSwitches.IgnoreServerProvidedFailoverPartner ? string.Empty : ServerProvidedFailoverPartner; + + PoolGroupProviderInfo.FailoverCheck(false, connectionOptions, actualFailoverPartner); } CurrentDataSource = originalServerInfo.UserServerName; } @@ -1810,7 +1808,7 @@ TimeoutTimer timeout ServerInfo failoverServerInfo = new ServerInfo(connectionOptions, failoverHost, connectionOptions.FailoverPartnerSPN); ResolveExtendedServerName(primaryServerInfo, !redirectedUserInstance, connectionOptions); - if (ServerProvidedFailOverPartner == null) + if (ServerProvidedFailoverPartner == null) { ResolveExtendedServerName(failoverServerInfo, !redirectedUserInstance && failoverHost != primaryServerInfo.UserServerName, connectionOptions); } @@ -1869,12 +1867,21 @@ TimeoutTimer timeout failoverDemandDone = true; } - // Primary server may give us a different failover partner than the connection string indicates. Update it - if (ServerProvidedFailOverPartner != null && failoverServerInfo.ResolvedServerName != ServerProvidedFailOverPartner) + // Primary server may give us a different failover partner than the connection string indicates. + // Update it only if we are respecting server-provided failover partner values. + if (ServerProvidedFailoverPartner != null && failoverServerInfo.ResolvedServerName != ServerProvidedFailoverPartner) { - SqlClientEventSource.Log.TryAdvancedTraceEvent(" {0}, new failover partner={1}", ObjectID, ServerProvidedFailOverPartner); - failoverServerInfo.SetDerivedNames(string.Empty, ServerProvidedFailOverPartner); + if (LocalAppContextSwitches.IgnoreServerProvidedFailoverPartner) + { + SqlClientEventSource.Log.TryTraceEvent(" {0}, Ignoring server provided failover partner '{1}' due to IgnoreServerProvidedFailoverPartner AppContext switch.", ObjectID, ServerProvidedFailoverPartner); + } + else + { + SqlClientEventSource.Log.TryAdvancedTraceEvent(" {0}, new failover partner={1}", ObjectID, ServerProvidedFailoverPartner); + failoverServerInfo.SetDerivedNames(string.Empty, ServerProvidedFailoverPartner); + } } + currentServerInfo = failoverServerInfo; _timeoutErrorInternal.SetInternalSourceType(SqlConnectionInternalSourceType.Failover); } @@ -1924,7 +1931,7 @@ TimeoutTimer timeout _currentPacketSize = connectionOptions.PacketSize; _currentLanguage = _originalLanguage = ConnectionOptions.CurrentLanguage; CurrentDatabase = _originalDatabase = connectionOptions.InitialCatalog; - _currentFailoverPartner = null; + ServerProvidedFailoverPartner = null; _instanceName = string.Empty; AttemptOneLogin( @@ -1986,7 +1993,7 @@ TimeoutTimer timeout _activeDirectoryAuthTimeoutRetryHelper.State = ActiveDirectoryAuthenticationTimeoutRetryState.HasLoggedIn; // if connected to failover host, but said host doesn't have DbMirroring set up, throw an error - if (useFailoverHost && ServerProvidedFailOverPartner == null) + if (useFailoverHost && ServerProvidedFailoverPartner == null) { throw SQL.InvalidPartnerConfiguration(failoverHost, CurrentDatabase); } @@ -1995,8 +2002,13 @@ TimeoutTimer timeout { // We must wait for CompleteLogin to finish for to have the // env change from the server to know its designated failover - // partner; save this information in _currentFailoverPartner. - PoolGroupProviderInfo.FailoverCheck(useFailoverHost, connectionOptions, ServerProvidedFailOverPartner); + // partner. + + // When ignoring server provided failover partner, we must pass in the original failover partner from the connection string. + // Otherwise the pool group's failover partner designation will be updated to point to the server provided value. + string actualFailoverPartner = LocalAppContextSwitches.IgnoreServerProvidedFailoverPartner ? failoverHost : ServerProvidedFailoverPartner; + + PoolGroupProviderInfo.FailoverCheck(useFailoverHost, connectionOptions, actualFailoverPartner); } CurrentDataSource = (useFailoverHost ? failoverHost : primaryServerInfo.UserServerName); } @@ -2246,7 +2258,8 @@ internal void OnEnvChange(SqlEnvChange rec) { throw SQL.ROR_FailoverNotSupportedServer(this); } - _currentFailoverPartner = rec._newValue; + + ServerProvidedFailoverPartner = rec._newValue; break; case TdsEnums.ENV_PROMOTETRANSACTION: diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParser.Unix.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParser.Unix.cs index d71332a25a..3944de7915 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParser.Unix.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParser.Unix.cs @@ -23,20 +23,18 @@ private void WaitForSSLHandShakeToComplete(ref uint error, ref int protocolVersi // No - Op } - private SNIErrorDetails GetSniErrorDetails() + private TdsParserStateObject.SniErrorDetails GetSniErrorDetails() { - SNIErrorDetails details; SniError sniError = SniProxy.Instance.GetLastError(); - details.sniErrorNumber = sniError.sniError; - details.errorMessage = sniError.errorMessage; - details.nativeError = sniError.nativeError; - details.provider = (int)sniError.provider; - details.lineNumber = sniError.lineNumber; - details.function = sniError.function; - details.exception = sniError.exception; - - return details; - } + return new( + sniError.errorMessage, + sniError.nativeError, + sniError.sniError, + (int)sniError.provider, + sniError.lineNumber, + sniError.function, + sniError.exception); + } } } diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParser.Windows.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParser.Windows.cs index 25bc6f87c5..7576efe32a 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParser.Windows.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParser.Windows.cs @@ -26,10 +26,6 @@ internal void PostReadAsyncForMars() _pMarsPhysicalConObj.IncrementPendingCallbacks(); SessionHandle handle = _pMarsPhysicalConObj.SessionHandle; - // we do not need to consider partial packets when making this read because we - // expect this read to pend. a partial packet should not exist at setup of the - // parser - Debug.Assert(_physicalStateObj.PartialPacket==null); temp = _pMarsPhysicalConObj.ReadAsync(handle, out error); Debug.Assert(temp.Type == PacketHandle.NativePointerType, "unexpected packet type when requiring NativePointer"); @@ -63,32 +59,31 @@ private void WaitForSSLHandShakeToComplete(ref uint error, ref int protocolVersi } } - private SNIErrorDetails GetSniErrorDetails() + private TdsParserStateObject.SniErrorDetails GetSniErrorDetails() { - SNIErrorDetails details = new SNIErrorDetails(); - if (TdsParserStateObjectFactory.UseManagedSNI) { SniError sniError = SniProxy.Instance.GetLastError(); - details.sniErrorNumber = sniError.sniError; - details.errorMessage = sniError.errorMessage; - details.nativeError = sniError.nativeError; - details.provider = (int)sniError.provider; - details.lineNumber = sniError.lineNumber; - details.function = sniError.function; - details.exception = sniError.exception; + return new( + sniError.errorMessage, + sniError.nativeError, + sniError.sniError, + (int)sniError.provider, + sniError.lineNumber, + sniError.function, + sniError.exception); } else { SniNativeWrapper.SniGetLastError(out Interop.Windows.Sni.SniError sniError); - details.sniErrorNumber = sniError.sniError; - details.errorMessage = sniError.errorMessage; - details.nativeError = sniError.nativeError; - details.provider = (int)sniError.provider; - details.lineNumber = sniError.lineNumber; - details.function = sniError.function; + return new( + sniError.errorMessage, + sniError.nativeError, + sniError.sniError, + (int)sniError.provider, + sniError.lineNumber, + sniError.function); } - return details; } } // tdsparser diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParser.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParser.cs index e02711fe8d..c020e7bb3e 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParser.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParser.cs @@ -121,8 +121,6 @@ internal sealed partial class TdsParser private bool _is2022 = false; - private string[] _serverSpn = null; - // SqlStatistics private SqlStatistics _statistics = null; @@ -395,7 +393,6 @@ internal void Connect(ServerInfo serverInfo, } else { - _serverSpn = null; SqlClientEventSource.Log.TryTraceEvent("TdsParser.Connect | SEC | Connection Object Id {0}, Authentication Mode: {1}", _connHandler.ObjectID, authType == SqlAuthenticationMethod.NotSpecified ? SqlAuthenticationMethod.SqlPassword.ToString() : authType.ToString()); } @@ -407,7 +404,6 @@ internal void Connect(ServerInfo serverInfo, SqlClientEventSource.Log.TryTraceEvent(" Encryption will be disabled as target server is a SQL Local DB instance."); } - _serverSpn = null; _authenticationProvider = null; // AD Integrated behaves like Windows integrated when connecting to a non-fedAuth server @@ -446,7 +442,7 @@ internal void Connect(ServerInfo serverInfo, serverInfo.ExtendedServerName, timeout, out instanceName, - ref _serverSpn, + out var resolvedServerSpn, false, true, fParallel, @@ -459,8 +455,6 @@ internal void Connect(ServerInfo serverInfo, hostNameInCertificate, serverCertificateFilename); - _authenticationProvider?.Initialize(serverInfo, _physicalStateObj, this); - if (TdsEnums.SNI_SUCCESS != _physicalStateObj.Status) { _physicalStateObj.AddError(ProcessSNIError(_physicalStateObj)); @@ -546,7 +540,7 @@ internal void Connect(ServerInfo serverInfo, serverInfo.ExtendedServerName, timeout, out instanceName, - ref _serverSpn, + out resolvedServerSpn, true, true, fParallel, @@ -559,8 +553,6 @@ internal void Connect(ServerInfo serverInfo, hostNameInCertificate, serverCertificateFilename); - _authenticationProvider?.Initialize(serverInfo, _physicalStateObj, this); - if (TdsEnums.SNI_SUCCESS != _physicalStateObj.Status) { _physicalStateObj.AddError(ProcessSNIError(_physicalStateObj)); @@ -599,6 +591,11 @@ internal void Connect(ServerInfo serverInfo, } SqlClientEventSource.Log.TryTraceEvent(" Prelogin handshake successful"); + if (_authenticationProvider is { }) + { + _authenticationProvider.Initialize(serverInfo, _physicalStateObj, this, resolvedServerSpn.Primary, resolvedServerSpn.Secondary); + } + if (_fMARS && marsCapable) { // if user explicitly disables mars or mars not supported, don't create the session pool @@ -744,7 +741,7 @@ private void SendPreLoginHandshake( // UNDONE - need to do some length verification to ensure packet does not // get too big!!! Not beyond it's max length! - + for (int option = (int)PreLoginOptions.VERSION; option < (int)PreLoginOptions.NUMOPT; option++) { int optionDataSize = 0; @@ -935,7 +932,7 @@ private PreLoginHandshakeStatus ConsumePreLoginHandshake( string serverCertificateFilename) { // Assign default values - marsCapable = _fMARS; + marsCapable = _fMARS; fedAuthRequired = false; Debug.Assert(_physicalStateObj._syncOverAsync, "Should not attempt pends in a synchronous call"); TdsOperationStatus result = _physicalStateObj.TryReadNetworkPacket(); @@ -1450,12 +1447,12 @@ internal SqlError ProcessSNIError(TdsParserStateObject stateObj) SqlClientEventSource.Log.TryTraceEvent(" SNIContext must not be None = {0}, _fMARS = {1}, TDS Parser State = {2}", stateObj.DebugOnlyCopyOfSniContext, _fMARS, _state); #endif - SNIErrorDetails details = GetSniErrorDetails(); + TdsParserStateObject.SniErrorDetails details = GetSniErrorDetails(); - if (details.sniErrorNumber != 0) + if (details.SniErrorNumber != 0) { // handle special SNI error codes that are converted into exception which is not a SqlException. - switch (details.sniErrorNumber) + switch (details.SniErrorNumber) { case SniErrors.MultiSubnetFailoverWithMoreThan64IPs: // Connecting with the MultiSubnetFailover connection option to a SQL Server instance configured with more than 64 IP addresses is not supported. @@ -1476,8 +1473,8 @@ internal SqlError ProcessSNIError(TdsParserStateObject stateObj) } // PInvoke code automatically sets the length of the string for us // So no need to look for \0 - string errorMessage = details.errorMessage; - SqlClientEventSource.Log.TryAdvancedTraceEvent("< sc.TdsParser.ProcessSNIError |ERR|ADV > Error message Detail: {0}", details.errorMessage); + string errorMessage = details.ErrorMessage; + SqlClientEventSource.Log.TryAdvancedTraceEvent("< sc.TdsParser.ProcessSNIError |ERR|ADV > Error message Detail: {0}", details.ErrorMessage); /* Format SNI errors and add Context Information * @@ -1494,23 +1491,23 @@ internal SqlError ProcessSNIError(TdsParserStateObject stateObj) if (TdsParserStateObjectFactory.UseManagedSNI) { - Debug.Assert(!string.IsNullOrEmpty(details.errorMessage) || details.sniErrorNumber != 0, "Empty error message received from SNI"); - SqlClientEventSource.Log.TryAdvancedTraceEvent(" Empty error message received from SNI. Error Message = {0}, SNI Error Number ={1}", details.errorMessage, details.sniErrorNumber); + Debug.Assert(!string.IsNullOrEmpty(details.ErrorMessage) || details.SniErrorNumber != 0, "Empty error message received from SNI"); + SqlClientEventSource.Log.TryAdvancedTraceEvent(" Empty error message received from SNI. Error Message = {0}, SNI Error Number ={1}", details.ErrorMessage, details.SniErrorNumber); } else { - Debug.Assert(!string.IsNullOrEmpty(details.errorMessage), "Empty error message received from SNI"); - SqlClientEventSource.Log.TryAdvancedTraceEvent(" Empty error message received from SNI. Error Message = {0}", details.errorMessage); + Debug.Assert(!string.IsNullOrEmpty(details.ErrorMessage), "Empty error message received from SNI"); + SqlClientEventSource.Log.TryAdvancedTraceEvent(" Empty error message received from SNI. Error Message = {0}", details.ErrorMessage); } string sqlContextInfo = StringsHelper.GetResourceString(stateObj.SniContext.ToString()); - string providerRid = string.Format("SNI_PN{0}", details.provider); + string providerRid = string.Format("SNI_PN{0}", details.Provider); string providerName = StringsHelper.GetResourceString(providerRid); Debug.Assert(!string.IsNullOrEmpty(providerName), $"invalid providerResourceId '{providerRid}'"); - uint win32ErrorCode = details.nativeError; + int win32ErrorCode = details.NativeError; SqlClientEventSource.Log.TryAdvancedTraceEvent(" SNI Native Error Code = {0}", win32ErrorCode); - if (details.sniErrorNumber == 0) + if (details.SniErrorNumber == 0) { // Provider error. The message from provider is preceeded with non-localizable info from SNI // strip provider info from SNI @@ -1544,7 +1541,7 @@ internal SqlError ProcessSNIError(TdsParserStateObject stateObj) if (TdsParserStateObjectFactory.UseManagedSNI) { // SNI error. Append additional error message info if available and hasn't been included. - string sniLookupMessage = SQL.GetSNIErrorMessage(details.sniErrorNumber); + string sniLookupMessage = SQL.GetSNIErrorMessage(details.SniErrorNumber); errorMessage = (string.IsNullOrEmpty(errorMessage) || errorMessage.Contains(sniLookupMessage)) ? sniLookupMessage : (sniLookupMessage + ": " + errorMessage); @@ -1552,25 +1549,25 @@ internal SqlError ProcessSNIError(TdsParserStateObject stateObj) else { // SNI error. Replace the entire message. - errorMessage = SQL.GetSNIErrorMessage(details.sniErrorNumber); + errorMessage = SQL.GetSNIErrorMessage(details.SniErrorNumber); // If its a LocalDB error, then nativeError actually contains a LocalDB-specific error code, not a win32 error code - if (details.sniErrorNumber == SniErrors.LocalDBErrorCode) + if (details.SniErrorNumber == SniErrors.LocalDBErrorCode) { - errorMessage += LocalDbApi.GetLocalDbMessage((int)details.nativeError); + errorMessage += LocalDbApi.GetLocalDbMessage(details.NativeError); win32ErrorCode = 0; } SqlClientEventSource.Log.TryAdvancedTraceEvent(" Extracting the latest exception from native SNI. errorMessage: {0}", errorMessage); } } errorMessage = string.Format("{0} (provider: {1}, error: {2} - {3})", - sqlContextInfo, providerName, (int)details.sniErrorNumber, errorMessage); + sqlContextInfo, providerName, (int)details.SniErrorNumber, errorMessage); SqlClientEventSource.Log.TryAdvancedTraceErrorEvent(" SNI Error Message. Native Error = {0}, Line Number ={1}, Function ={2}, Exception ={3}, Server = {4}", - (int)details.nativeError, (int)details.lineNumber, details.function, details.exception, _server); + details.NativeError, (int)details.LineNumber, details.Function, details.Exception, _server); - return new SqlError(infoNumber: (int)details.nativeError, errorState: 0x00, TdsEnums.FATAL_ERROR_CLASS, _server, - errorMessage, details.function, (int)details.lineNumber, win32ErrorCode: details.nativeError, details.exception); + return new SqlError(infoNumber: details.NativeError, errorState: 0x00, TdsEnums.FATAL_ERROR_CLASS, _server, + errorMessage, details.Function, (int)details.LineNumber, win32ErrorCode: details.NativeError, details.Exception); } } @@ -2052,19 +2049,11 @@ internal TdsOperationStatus TryRun(RunBehavior runBehavior, SqlCommand cmdHandle if (!IsValidTdsToken(token)) { -#if DEBUG - string message = stateObj.DumpBuffer(); - Debug.Fail(message); -#endif + Debug.Fail($"unexpected token; token = {token,-2:X2}"); _state = TdsParserState.Broken; _connHandler.BreakConnection(); SqlClientEventSource.Log.TryTraceEvent(" Potential multi-threaded misuse of connection, unexpected TDS token found {0}", ObjectID); -#if DEBUG - throw new InvalidOperationException(message); -#else throw SQL.ParsingError(); -#endif - } int tokenLength; @@ -2181,7 +2170,7 @@ internal TdsOperationStatus TryRun(RunBehavior runBehavior, SqlCommand cmdHandle dataStream.BrowseModeInfoConsumed = true; } else - { + { // no dataStream result = stateObj.TrySkipBytes(tokenLength); if (result != TdsOperationStatus.Done) @@ -2195,7 +2184,7 @@ internal TdsOperationStatus TryRun(RunBehavior runBehavior, SqlCommand cmdHandle case TdsEnums.SQLDONE: case TdsEnums.SQLDONEPROC: case TdsEnums.SQLDONEINPROC: - { + { // RunBehavior can be modified - see SQL BU DT 269516 & 290090 result = TryProcessDone(cmdHandler, dataStream, ref runBehavior, stateObj); if (result != TdsOperationStatus.Done) @@ -4122,7 +4111,6 @@ internal TdsOperationStatus TryProcessReturnValue(int length, { return result; } - byte len; result = stateObj.TryReadByte(out len); if (result != TdsOperationStatus.Done) @@ -4330,7 +4318,7 @@ internal TdsOperationStatus TryProcessReturnValue(int length, { return result; } - + if (rec.collation.IsUTF8) { // UTF8 collation rec.encoding = s_utf8EncodingWithoutBom; @@ -4790,13 +4778,13 @@ internal TdsOperationStatus TryProcessAltMetaData(int cColumns, TdsParserStateOb { // internal meta data class _SqlMetaData col = altMetaDataSet[i]; - + result = stateObj.TryReadByte(out _); if (result != TdsOperationStatus.Done) { return result; } - + result = stateObj.TryReadUInt16(out _); if (result != TdsOperationStatus.Done) { @@ -5287,7 +5275,7 @@ private TdsOperationStatus TryCommonProcessMetaData(TdsParserStateObject stateOb { // If the column is encrypted, we should have a valid cipherTable if (cipherTable != null) - { + { result = TryProcessTceCryptoMetadata(stateObj, col, cipherTable, columnEncryptionSetting, isReturnValue: false); if (result != TdsOperationStatus.Done) { @@ -5489,7 +5477,7 @@ private TdsOperationStatus TryProcessColInfo(_SqlMetaDataSet columns, SqlDataRea for (int i = 0; i < columns.Length; i++) { _SqlMetaData col = columns[i]; - + TdsOperationStatus result = stateObj.TryReadByte(out _); if (result != TdsOperationStatus.Done) { @@ -5573,12 +5561,6 @@ private TdsOperationStatus TryProcessColumnHeaderNoNBC(SqlMetaDataPriv col, TdsP { if (col.metaType.IsLong && !col.metaType.IsPlp) { - if (stateObj.IsSnapshotContinuing()) - { - length = (ulong)stateObj.GetSnapshotStorageLength(); - isNull = length == 0; - return TdsOperationStatus.Done; - } // // we don't care about TextPtrs, simply go after the data after it // @@ -6032,17 +6014,34 @@ private TdsOperationStatus TryReadSqlStringValue(SqlBuffer value, byte type, int if (isPlp) { - result = TryReadPlpUnicodeCharsWithContinue( - stateObj, - length, - out string resultString - ); + char[] cc = null; + bool buffIsRented = false; + result = TryReadPlpUnicodeChars(ref cc, 0, length >> 1, stateObj, out length, supportRentedBuff: true, rentedBuff: ref buffIsRented); if (result == TdsOperationStatus.Done) { - s = resultString; + if (length > 0) + { + s = new string(cc, 0, length); + } + else + { + s = string.Empty; + } } - else + + if (buffIsRented) + { + // do not use clearArray:true on the rented array because it can be massively larger + // than the space we've used and we would incur performance clearing memory that + // we haven't used and can't leak out information. + // clear only the length that we know we have used. + cc.AsSpan(0, length).Clear(); + ArrayPool.Shared.Return(cc, clearArray: false); + cc = null; + } + + if (result != TdsOperationStatus.Done) { return result; } @@ -6402,7 +6401,9 @@ internal TdsOperationStatus TryReadSqlValue(SqlBuffer value, } else { - result = stateObj.TryReadByteArrayWithContinue(length, out b); + //Debug.Assert(length > 0 && length < (long)(Int32.MaxValue), "Bad length for column"); + b = new byte[length]; + result = stateObj.TryReadByteArray(b, length); if (result != TdsOperationStatus.Done) { return result; @@ -6473,7 +6474,8 @@ internal TdsOperationStatus TryReadSqlValue(SqlBuffer value, case TdsEnums.SQLVECTOR: // Vector data is read as non-plp binary value. // This is same as reading varbinary(8000). - result = stateObj.TryReadByteArrayWithContinue(length, out b); + byte[] buff = new byte[length]; + result = stateObj.TryReadByteArray(buff, length); if (result != TdsOperationStatus.Done) { return result; @@ -6481,13 +6483,13 @@ internal TdsOperationStatus TryReadSqlValue(SqlBuffer value, // Internally, we use Sqlbinary to deal with varbinary data and store it in // SqlBuffer as SqlBinary value. - value.SqlBinary = SqlBinary.WrapBytes(b); + value.SqlBinary = SqlBinary.WrapBytes(buff); // Extract the metadata from the payload and set it as the vector attributes // in the SqlBuffer. This metadata is further used when constructing a SqlVector // object from binary payload. - int elementCount = BinaryPrimitives.ReadUInt16LittleEndian(b.AsSpan(2)); - byte elementType = b[4]; + int elementCount = BinaryPrimitives.ReadUInt16LittleEndian(buff.AsSpan(2)); + byte elementType = buff[4]; value.SetVectorInfo(elementCount, elementType, false); break; @@ -7394,7 +7396,7 @@ private byte[] SerializeSqlMoney(SqlMoney value, int length, TdsParserStateObjec private void WriteSqlMoney(SqlMoney value, int length, TdsParserStateObject stateObj) { - // UNDONE: can I use SqlMoney.ToInt64()? + // UNDONE: can I use SqlMoney.ToInt64()? int[] bits = decimal.GetBits(value.Value); // this decimal should be scaled by 10000 (regardless of what the incoming decimal was scaled by) @@ -8126,22 +8128,14 @@ internal TdsOperationStatus TryGetTokenLength(byte token, TdsParserStateObject s case TdsEnums.SQLVarCnt: if (0 != (token & 0x80)) { - if (stateObj.IsSnapshotContinuing()) - { - tokenLength = stateObj.GetSnapshotStorageLength(); - Debug.Assert(tokenLength != 0, "stored buffer length on continue must contain the length of the data required for the token"); - } - else + ushort value; + result = stateObj.TryReadUInt16(out value); + if (result != TdsOperationStatus.Done) { - ushort value; - result = stateObj.TryReadUInt16(out value); - if (result != TdsOperationStatus.Done) - { - tokenLength = 0; - return result; - } - tokenLength = value; + tokenLength = 0; + return result; } + tokenLength = value; return TdsOperationStatus.Done; } else if (0 == (token & 0x0c)) @@ -9967,7 +9961,7 @@ private Task TDSExecuteRPCAddParameter(TdsParserStateObject stateObj, SqlParamet WriteUDTMetaData(value, names[0], names[1], names[2], stateObj); - // UNDONE - re-org to use code below to write value! + // UNDONE - re-org to use code below to write value! if (!isNull) { WriteUnsignedLong((ulong)udtVal.Length, stateObj); // PLP length @@ -12441,7 +12435,7 @@ private Task WriteUnterminatedValue(object value, MetaType type, byte scale, int case TdsEnums.SQLNVARCHAR: case TdsEnums.SQLNTEXT: case TdsEnums.SQLXMLTYPE: - case TdsEnums.SQLJSON: + case TdsEnums.SQLJSON: { Debug.Assert(!isDataFeed || (value is TextDataFeed || value is XmlDataFeed), "Value must be a TextReader or XmlReader"); Debug.Assert(isDataFeed || (value is string || value is byte[]), "Value is a byte array or string"); @@ -13078,85 +13072,6 @@ internal int ReadPlpUnicodeChars(ref char[] buff, int offst, int len, TdsParserS return charsRead; } - internal TdsOperationStatus TryReadPlpUnicodeCharsWithContinue(TdsParserStateObject stateObj, int length, out string resultString) - { - resultString = null; - char[] temp = null; - bool buffIsRented = false; - int startOffset = 0; - (bool canContinue, bool isStarting, bool isContinuing) = stateObj.GetSnapshotStatuses(); - - if (canContinue) - { - if (isContinuing || isStarting) - { - temp = stateObj.TryTakeSnapshotStorage() as char[]; - Debug.Assert(temp == null || length == int.MaxValue || temp.Length == length, "stored buffer length must be null or must have been created with the correct length"); - } - if (temp != null) - { - startOffset = stateObj.GetSnapshotTotalSize(); - } - } - - TdsOperationStatus result = TryReadPlpUnicodeChars( - ref temp, - 0, - length >> 1, - stateObj, - out length, - supportRentedBuff: !canContinue, // do not use the arraypool if we are going to keep the buffer in the snapshot - rentedBuff: ref buffIsRented, - startOffset, - isStarting || isContinuing - ); - - if (result == TdsOperationStatus.Done) - { - if (length > 0) - { - resultString = new string(temp, 0, length); - } - else - { - resultString = string.Empty; - } - - if (buffIsRented) - { - // do not use clearArray:true on the rented array because it can be massively larger - // than the space we've used and we would incur performance clearing memory that - // we haven't used and can't leak out information. - // clear only the length that we know we have used. - temp.AsSpan(0, length).Clear(); - ArrayPool.Shared.Return(temp, clearArray: false); - temp = null; - } - } - else if (result == TdsOperationStatus.NeedMoreData) - { - if (isStarting || isContinuing) - { - stateObj.SetSnapshotStorage(temp); - } - } - - return result; - } - - internal TdsOperationStatus TryReadPlpUnicodeChars( - ref char[] buff, - int offst, - int len, - TdsParserStateObject stateObj, - out int totalCharsRead, - bool supportRentedBuff, - ref bool rentedBuff - ) - { - return TryReadPlpUnicodeChars(ref buff, offst, len, stateObj, out totalCharsRead, supportRentedBuff, ref rentedBuff, 0, false); - } - // Reads the requested number of chars from a plp data stream, or the entire data if // requested length is -1 or larger than the actual length of data. First call to this method // should be preceeded by a call to ReadPlpLength or ReadDataLength. @@ -13168,13 +13083,11 @@ internal TdsOperationStatus TryReadPlpUnicodeChars( TdsParserStateObject stateObj, out int totalCharsRead, bool supportRentedBuff, - ref bool rentedBuff, - int startOffsetByteCount, - bool writeDataSizeToSnapshot - ) + ref bool rentedBuff) { int charsRead = 0; int charsLeft = 0; + char[] newbuf; if (stateObj._longlen == 0) { @@ -13184,22 +13097,16 @@ bool writeDataSizeToSnapshot } Debug.Assert((ulong)stateObj._longlen != TdsEnums.SQL_PLP_NULL, "Out of sync plp read request"); - Debug.Assert( - (buff == null && offst == 0) - || - (buff.Length >= offst + len) - || - (buff.Length >= (startOffsetByteCount >> 1) + 1), - "Invalid length sent to ReadPlpUnicodeChars()!" - ); + + Debug.Assert((buff == null && offst == 0) || (buff.Length >= offst + len), "Invalid length sent to ReadPlpUnicodeChars()!"); charsLeft = len; // If total data length is known up front from the plp header by being not SQL_PLP_UNKNOWNLEN // and the number of chars required is less than int.max/2 allocate the entire buffer now to avoid // later needing to repeatedly allocate new target buffers and copy data as we discover new data - if (buff == null && stateObj._longlen != TdsEnums.SQL_PLP_UNKNOWNLEN && stateObj._longlen < (int.MaxValue >> 1)) + if (buff == null && stateObj._longlen != TdsEnums.SQL_PLP_UNKNOWNLEN && len < (int.MaxValue >> 1)) { - if (supportRentedBuff && stateObj._longlen < 1073741824) // 1 Gib + if (supportRentedBuff && len < 1073741824) // 1 Gib { buff = ArrayPool.Shared.Rent((int)Math.Min((int)stateObj._longlen, len)); rentedBuff = true; @@ -13212,11 +13119,6 @@ bool writeDataSizeToSnapshot } TdsOperationStatus result; - - bool partialReadInProgress = (startOffsetByteCount & 0x1) == 1; - bool restartingDataSizeCount = startOffsetByteCount == 0; - int currentPacketId = 0; - if (stateObj._longlenleft == 0) { result = stateObj.TryReadPlpLength(false, out _); @@ -13232,103 +13134,63 @@ bool writeDataSizeToSnapshot } } - totalCharsRead = (startOffsetByteCount >> 1); - charsLeft -= totalCharsRead; - offst += totalCharsRead; - + totalCharsRead = 0; while (charsLeft > 0) { - if (!partialReadInProgress) + charsRead = (int)Math.Min((stateObj._longlenleft + 1) >> 1, (ulong)charsLeft); + if ((buff == null) || (buff.Length < (offst + charsRead))) { - charsRead = (int)Math.Min((stateObj._longlenleft + 1) >> 1, (ulong)charsLeft); - if ((buff == null) || (buff.Length < (offst + charsRead))) + bool returnRentedBufferAfterCopy = rentedBuff; + if (supportRentedBuff && (offst + charsRead) < 1073741824) // 1 Gib { - char[] newbuf; - bool returnRentedBufferAfterCopy = rentedBuff; - if (supportRentedBuff && (offst + charsRead) < 1073741824) // 1 Gib - { - newbuf = ArrayPool.Shared.Rent(offst + charsRead); - rentedBuff = true; - } - else - { - // grow by an arbitrary number of packets to avoid needing to reallocate - // the newbuf on each loop iteration of long packet sequences which causes - // a performance problem as we spend large amounts of time copying and in gc - newbuf = new char[offst + charsRead + (stateObj.GetPacketSize() * 8)]; - rentedBuff = false; - } - - if (buff != null) - { - Buffer.BlockCopy(buff, 0, newbuf, 0, offst * 2); - if (returnRentedBufferAfterCopy) - { - buff.AsSpan(0, offst).Clear(); - ArrayPool.Shared.Return(buff, clearArray: false); - } - } - buff = newbuf; - newbuf = null; + newbuf = ArrayPool.Shared.Rent(offst + charsRead); + rentedBuff = true; } - if (charsRead > 0) + else { - result = TryReadPlpUnicodeCharsChunk(buff, offst, charsRead, stateObj, out charsRead); - if (result != TdsOperationStatus.Done) - { - return result; - } - charsLeft -= charsRead; - offst += charsRead; - totalCharsRead += charsRead; + newbuf = new char[offst + charsRead]; + rentedBuff = false; + } - if (writeDataSizeToSnapshot) + if (buff != null) + { + Buffer.BlockCopy(buff, 0, newbuf, 0, offst * 2); + if (returnRentedBufferAfterCopy) { - currentPacketId = IncrementSnapshotDataSize(stateObj, restartingDataSizeCount, currentPacketId, charsRead * 2); + buff.AsSpan(0, offst).Clear(); + ArrayPool.Shared.Return(buff, clearArray: false); } } + buff = newbuf; } - - // Special case single byte - if ( - (stateObj._longlenleft == 1 || partialReadInProgress) - && (charsLeft > 0) - ) + if (charsRead > 0) { - byte b1 = 0; - byte b2 = 0; - if (partialReadInProgress) + result = TryReadPlpUnicodeCharsChunk(buff, offst, charsRead, stateObj, out charsRead); + if (result != TdsOperationStatus.Done) { - partialReadInProgress = false; - // we're resuming with a partial char in the buffer so we need to load the byte - // from the char buffer and put it into b1 so we can combine it with the second - // half later - b1 = (byte)(buff[offst] & 0x00ff); + return result; } - else + charsLeft -= charsRead; + offst += charsRead; + totalCharsRead += charsRead; + } + // Special case single byte left + if (stateObj._longlenleft == 1 && (charsLeft > 0)) + { + byte b1; + result = stateObj.TryReadByte(out b1); + if (result != TdsOperationStatus.Done) { - result = stateObj.TryReadByte(out b1); - if (result != TdsOperationStatus.Done) - { - return result; - } - stateObj._longlenleft--; - if (writeDataSizeToSnapshot) - { - // we need to write the single b1 byte to the array because we may run out of data - // and need to wait for another packet - buff[offst] = (char)((b1 & 0xff)); - currentPacketId = IncrementSnapshotDataSize(stateObj, restartingDataSizeCount, currentPacketId, 1); - } - - result = stateObj.TryReadPlpLength(false, out _); - if (result != TdsOperationStatus.Done) - { - return result; - } - Debug.Assert((stateObj._longlenleft != 0), "ReadPlpUnicodeChars: Odd byte left at the end!"); + return result; } - + stateObj._longlenleft--; + result = stateObj.TryReadPlpLength(false, out _); + if (result != TdsOperationStatus.Done) + { + return result; + } + Debug.Assert((stateObj._longlenleft != 0), "ReadPlpUnicodeChars: Odd byte left at the end!"); + byte b2; result = stateObj.TryReadByte(out b2); if (result != TdsOperationStatus.Done) { @@ -13341,11 +13203,6 @@ bool writeDataSizeToSnapshot charsRead++; charsLeft--; totalCharsRead++; - - if (writeDataSizeToSnapshot) - { - currentPacketId = IncrementSnapshotDataSize(stateObj, restartingDataSizeCount, currentPacketId, 1); - } } if (stateObj._longlenleft == 0) { @@ -13358,41 +13215,9 @@ bool writeDataSizeToSnapshot } if (stateObj._longlenleft == 0) // Data read complete - { break; - } } return TdsOperationStatus.Done; - - static int IncrementSnapshotDataSize(TdsParserStateObject stateObj, bool resetting, int previousPacketId, int value) - { - int current = 0; - if (resetting) - { - int currentPacketId = stateObj.GetSnapshotPacketID(); - if (previousPacketId == currentPacketId) - { - // we have already reset it the first time we saw it so just add normally - current = stateObj.GetSnapshotDataSize(); - } - else - { - // a packet we haven't seen before, reset the size - current = 0; - } - - stateObj.SetSnapshotDataSize(current + value); - - // return new packetid so next time we see this packet we know it isn't new - return currentPacketId; - } - else - { - current = stateObj.GetSnapshotDataSize(); - stateObj.SetSnapshotDataSize(current + value); - return previousPacketId; - } - } } internal int ReadPlpAnsiChars(ref char[] buff, int offst, int len, SqlMetaDataPriv metadata, TdsParserStateObject stateObj) @@ -13659,15 +13484,14 @@ private TdsOperationStatus TryProcessUDTMetaData(SqlMetaDataPriv metaData, TdsPa + " _connHandler = {14}\n\t" + " _fMARS = {15}\n\t" + " _sessionPool = {16}\n\t" - + " _sniSpnBuffer = {17}\n\t" - + " _errors = {18}\n\t" - + " _warnings = {19}\n\t" - + " _attentionErrors = {20}\n\t" - + " _attentionWarnings = {21}\n\t" - + " _statistics = {22}\n\t" - + " _statisticsIsInTransaction = {23}\n\t" - + " _fPreserveTransaction = {24}" - + " _fParallel = {25}" + + " _errors = {17}\n\t" + + " _warnings = {18}\n\t" + + " _attentionErrors = {19}\n\t" + + " _attentionWarnings = {20}\n\t" + + " _statistics = {21}\n\t" + + " _statisticsIsInTransaction = {22}\n\t" + + " _fPreserveTransaction = {23}" + + " _fParallel = {24}" ; internal string TraceString() { @@ -13690,7 +13514,6 @@ internal string TraceString() _connHandler == null ? "(null)" : _connHandler.ObjectID.ToString((IFormatProvider)null), _fMARS ? bool.TrueString : bool.FalseString, _sessionPool == null ? "(null)" : _sessionPool.TraceString(), - _serverSpn == null ? "(null)" : _serverSpn.Length.ToString((IFormatProvider)null), _physicalStateObj != null ? "(null)" : _physicalStateObj.ErrorCount.ToString((IFormatProvider)null), _physicalStateObj != null ? "(null)" : _physicalStateObj.WarningCount.ToString((IFormatProvider)null), _physicalStateObj != null ? "(null)" : _physicalStateObj.PreAttentionErrorCount.ToString((IFormatProvider)null), diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParser.netcore.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParser.netcore.cs index 71ef5e9381..95dd0d9731 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParser.netcore.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParser.netcore.cs @@ -10,17 +10,6 @@ namespace Microsoft.Data.SqlClient { internal sealed partial class TdsParser { - internal struct SNIErrorDetails - { - public string errorMessage; - public uint nativeError; - public uint sniErrorNumber; - public int provider; - public uint lineNumber; - public string function; - public Exception exception; - } - internal static void FillGuidBytes(Guid guid, Span buffer) => guid.TryWriteBytes(buffer); internal static void FillDoubleBytes(double value, Span buffer) => BinaryPrimitives.TryWriteInt64LittleEndian(buffer, BitConverter.DoubleToInt64Bits(value)); diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParserStateObject.netcore.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParserStateObject.netcore.cs index bb3b07b0e8..4ab44115e0 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParserStateObject.netcore.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParserStateObject.netcore.cs @@ -11,6 +11,7 @@ using System.Threading.Tasks; using Microsoft.Data.Common; using Microsoft.Data.ProviderBase; +using Microsoft.Data.SqlClient.ManagedSni; namespace Microsoft.Data.SqlClient { @@ -55,7 +56,7 @@ internal TdsParserStateObject(TdsParser parser, TdsParserStateObject physicalCon AddError(parser.ProcessSNIError(this)); ThrowExceptionAndWarning(); } - + // we post a callback that represents the call to dispose; once the // object is disposed, the next callback will cause the GC Handle to // be released. @@ -71,7 +72,7 @@ internal abstract void CreatePhysicalSNIHandle( string serverName, TimeoutTimer timeout, out byte[] instanceName, - ref string[] spns, + out ResolvedServerSpn resolvedSpn, bool flushCache, bool async, bool fParallel, @@ -166,22 +167,14 @@ private void ReadSniError(TdsParserStateObject stateObj, uint error) stateObj.SendAttention(mustTakeWriteLock: true); PacketHandle syncReadPacket = default; - bool readFromNetwork = true; RuntimeHelpers.PrepareConstrainedRegions(); bool shouldDecrement = false; try { Interlocked.Increment(ref _readingCount); shouldDecrement = true; - readFromNetwork = !PartialPacketContainsCompletePacket(); - if (readFromNetwork) - { - syncReadPacket = ReadSyncOverAsync(stateObj.GetTimeoutRemaining(), out error); - } - else - { - error = TdsEnums.SNI_SUCCESS; - } + + syncReadPacket = ReadSyncOverAsync(stateObj.GetTimeoutRemaining(), out error); Interlocked.Decrement(ref _readingCount); shouldDecrement = false; @@ -194,7 +187,7 @@ private void ReadSniError(TdsParserStateObject stateObj, uint error) } else { - Debug.Assert(!readFromNetwork || !IsValidPacket(syncReadPacket), "unexpected syncReadPacket without corresponding SNIPacketRelease"); + Debug.Assert(!IsValidPacket(syncReadPacket), "unexpected syncReadPacket without corresponding SNIPacketRelease"); fail = true; // Subsequent read failed, time to give up. } } @@ -205,7 +198,7 @@ private void ReadSniError(TdsParserStateObject stateObj, uint error) Interlocked.Decrement(ref _readingCount); } - if (readFromNetwork && !IsPacketEmpty(syncReadPacket)) + if (!IsPacketEmpty(syncReadPacket)) { // Be sure to release packet, otherwise it will be leaked by native. ReleasePacket(syncReadPacket); @@ -246,9 +239,60 @@ private void ReadSniError(TdsParserStateObject stateObj, uint error) AssertValidState(); } - private uint GetSniPacket(PacketHandle packet, ref uint dataSize) + public void ProcessSniPacket(PacketHandle packet, uint error) { - return SniPacketGetData(packet, _inBuff, ref dataSize); + if (error != 0) + { + if ((_parser.State == TdsParserState.Closed) || (_parser.State == TdsParserState.Broken)) + { + // Do nothing with callback if closed or broken and error not 0 - callback can occur + // after connection has been closed. PROBLEM IN NETLIB - DESIGN FLAW. + return; + } + + AddError(_parser.ProcessSNIError(this)); + AssertValidState(); + } + else + { + uint dataSize = 0; + + uint getDataError = SniPacketGetData(packet, _inBuff, ref dataSize); + + if (getDataError == TdsEnums.SNI_SUCCESS) + { + if (_inBuff.Length < dataSize) + { + Debug.Assert(true, "Unexpected dataSize on Read"); + throw SQL.InvalidInternalPacketSize(StringsHelper.GetString(Strings.SqlMisc_InvalidArraySizeMessage)); + } + + _lastSuccessfulIOTimer._value = DateTime.UtcNow.Ticks; + _inBytesRead = (int)dataSize; + _inBytesUsed = 0; + + if (_snapshot != null) + { + _snapshot.AppendPacketData(_inBuff, _inBytesRead); + if (_snapshotReplay) + { + _snapshot.MoveNext(); +#if DEBUG + _snapshot.AssertCurrent(); +#endif + } + } + + SniReadStatisticsAndTracing(); + SqlClientEventSource.Log.TryAdvancedTraceBinEvent("TdsParser.ReadNetworkPacketAsyncCallback | INFO | ADV | State Object Id {0}, Packet read. In Buffer: {1}, In Bytes Read: {2}", ObjectID, _inBuff, _inBytesRead); + + AssertValidState(); + } + else + { + throw SQL.ParsingError(ParsingErrorState.ProcessSniPacketFailed); + } + } } private void SetBufferSecureStrings() @@ -320,7 +364,7 @@ public void ReadAsyncCallback(IntPtr key, PacketHandle packet, uint error) bool processFinallyBlock = true; try { - Debug.Assert((packet.Type == 0 && PartialPacketContainsCompletePacket()) || (CheckPacket(packet, source) && source != null), "AsyncResult null on callback"); + Debug.Assert(CheckPacket(packet, source) && source != null, "AsyncResult null on callback"); if (_parser.MARSOn) { diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParserStateObjectManaged.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParserStateObjectManaged.cs index a403f8b556..2845ab68d0 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParserStateObjectManaged.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParserStateObjectManaged.cs @@ -80,7 +80,7 @@ internal override void CreatePhysicalSNIHandle( string serverName, TimeoutTimer timeout, out byte[] instanceName, - ref string[] spns, + out ResolvedServerSpn resolvedSpn, bool flushCache, bool async, bool parallel, @@ -93,7 +93,7 @@ internal override void CreatePhysicalSNIHandle( string hostNameInCertificate, string serverCertificateFilename) { - SniHandle? sessionHandle = SniProxy.CreateConnectionHandle(serverName, timeout, out instanceName, ref spns, serverSPN, + SniHandle? sessionHandle = SniProxy.CreateConnectionHandle(serverName, timeout, out instanceName, out resolvedSpn, serverSPN, flushCache, async, parallel, isIntegratedSecurity, iPAddressPreference, cachedFQDN, ref pendingDNSInfo, tlsFirst, hostNameInCertificate, serverCertificateFilename); diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParserStateObjectNative.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParserStateObjectNative.cs index 10e9d96eca..14c2609cf3 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParserStateObjectNative.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParserStateObjectNative.cs @@ -155,7 +155,7 @@ internal override void CreatePhysicalSNIHandle( string serverName, TimeoutTimer timeout, out byte[] instanceName, - ref string[] spns, + out Microsoft.Data.SqlClient.ManagedSni.ResolvedServerSpn resolvedSpn, bool flushCache, bool async, bool fParallel, @@ -189,7 +189,7 @@ internal override void CreatePhysicalSNIHandle( _sessionHandle = new SNIHandle(myInfo, serverName, ref serverSPN, timeout.MillisecondsRemainingInt, out instanceName, flushCache, !async, fParallel, ipPreference, cachedDNSInfo, hostNameInCertificate); - spns = new[] { serverSPN.TrimEnd() }; + resolvedSpn = new(serverSPN.TrimEnd()); } protected override uint SniPacketGetData(PacketHandle packet, byte[] _inBuff, ref uint dataSize) @@ -424,7 +424,7 @@ internal override uint WaitForSSLHandShakeToComplete(out int protocolVersion) } else if (nativeProtocol.HasFlag(NativeProtocols.SP_PROT_SSL3_CLIENT) || nativeProtocol.HasFlag(NativeProtocols.SP_PROT_SSL3_SERVER)) { -// SSL 2.0 and 3.0 are only referenced to log a warning, not explicitly used for connections + // SSL 2.0 and 3.0 are only referenced to log a warning, not explicitly used for connections #pragma warning disable CS0618, CA5397 protocolVersion = (int)SslProtocols.Ssl3; } diff --git a/src/Microsoft.Data.SqlClient/netfx/ref/Microsoft.Data.SqlClient.cs b/src/Microsoft.Data.SqlClient/netfx/ref/Microsoft.Data.SqlClient.cs index d7f280ca33..636c39c932 100644 --- a/src/Microsoft.Data.SqlClient/netfx/ref/Microsoft.Data.SqlClient.cs +++ b/src/Microsoft.Data.SqlClient/netfx/ref/Microsoft.Data.SqlClient.cs @@ -2417,22 +2417,20 @@ public SqlJson(System.Text.Json.JsonDocument jsonDoc) { } } /// - public sealed class SqlVector : System.Data.SqlTypes.INullable + public readonly struct SqlVector : System.Data.SqlTypes.INullable where T : unmanaged { /// - public SqlVector(int length) { } - /// public SqlVector(System.ReadOnlyMemory memory) { } /// public bool IsNull => throw null; /// - public static SqlVector Null => throw null; + public static SqlVector? Null => throw null; /// public int Length { get { throw null; } } - /// - public int Size { get { throw null; } } /// public System.ReadOnlyMemory Memory { get { throw null; } } + /// + public static SqlVector CreateNull(int length) { throw null; } } } diff --git a/src/Microsoft.Data.SqlClient/netfx/ref/Microsoft.Data.SqlClient.csproj b/src/Microsoft.Data.SqlClient/netfx/ref/Microsoft.Data.SqlClient.csproj index 6b507b5a0a..445fcca7a7 100644 --- a/src/Microsoft.Data.SqlClient/netfx/ref/Microsoft.Data.SqlClient.csproj +++ b/src/Microsoft.Data.SqlClient/netfx/ref/Microsoft.Data.SqlClient.csproj @@ -32,6 +32,7 @@ + diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft.Data.SqlClient.csproj b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft.Data.SqlClient.csproj index 9ce3b09334..172f4c3897 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft.Data.SqlClient.csproj +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft.Data.SqlClient.csproj @@ -459,9 +459,6 @@ Microsoft\Data\SqlClient\OnChangedEventHandler.cs - - Microsoft\Data\SqlClient\Packet.cs - Microsoft\Data\SqlClient\PacketHandle.Windows.cs @@ -855,9 +852,6 @@ Microsoft\Data\SqlClient\TdsParserStateObject.cs - - Microsoft\Data\SqlClient\TdsParserStateObject.Multiplexer.cs - Microsoft\Data\SqlClient\TdsParserStateObjectFactory.Windows.cs @@ -961,6 +955,7 @@ + diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlCommand.cs b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlCommand.cs index 97212843f5..5418fc22f2 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlCommand.cs +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlCommand.cs @@ -3763,7 +3763,7 @@ private void CheckNotificationStateAndAutoEnlist() } Notification.Options = SqlDependency.GetDefaultComposedOptions(_activeConnection.DataSource, - InternalTdsConnection.ServerProvidedFailOverPartner, + InternalTdsConnection.ServerProvidedFailoverPartner, identityUserName, _activeConnection.Database); } @@ -4304,7 +4304,7 @@ private SqlDataReader TryFetchInputParameterEncryptionInfo(int timeout, { // In _batchRPCMode, the actual T-SQL query is in the first parameter and not present as the rpcName, as is the case with non-_batchRPCMode. // So input parameters start at parameters[1]. parameters[0] is the actual T-SQL Statement. rpcName is sp_executesql. - if (_RPCList[i].systemParams.Length > 1) + if (_RPCList[i].systemParams != null && _RPCList[i].systemParams.Length > 1) { _RPCList[i].needsFetchParameterEncryptionMetadata = true; diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlConnection.cs b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlConnection.cs index 46800b4140..f03c58fd15 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlConnection.cs +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlConnection.cs @@ -318,7 +318,9 @@ public static void RegisterColumnEncryptionKeyStoreProviders(IDictionary + internal override void ResetConnection() { // For implicit pooled connections, if connection reset behavior is specified, // reset the database and language properties back to default. It is important @@ -1521,7 +1514,7 @@ private void OpenLoginEnlist(TimeoutTimer timeout, throw SQL.ROR_FailoverNotSupportedConnString(); } - if (ServerProvidedFailOverPartner != null) + if (ServerProvidedFailoverPartner != null) { throw SQL.ROR_FailoverNotSupportedServer(this); } @@ -1671,7 +1664,7 @@ private void LoginNoFailover(ServerInfo serverInfo, isFirstTransparentAttempt: isFirstTransparentAttempt, disableTnir: disableTnir); - if (connectionOptions.MultiSubnetFailover && ServerProvidedFailOverPartner != null) + if (connectionOptions.MultiSubnetFailover && ServerProvidedFailoverPartner != null) { // connection succeeded: trigger exception if server sends failover partner and MultiSubnetFailover is used throw SQL.MultiSubnetFailoverWithFailoverPartner(serverProvidedFailoverPartner: true, internalConnection: this); @@ -1699,7 +1692,7 @@ private void LoginNoFailover(ServerInfo serverInfo, _currentPacketSize = ConnectionOptions.PacketSize; _currentLanguage = _originalLanguage = ConnectionOptions.CurrentLanguage; CurrentDatabase = _originalDatabase = ConnectionOptions.InitialCatalog; - _currentFailoverPartner = null; + ServerProvidedFailoverPartner = null; _instanceName = string.Empty; routingAttempts++; @@ -1718,8 +1711,11 @@ private void LoginNoFailover(ServerInfo serverInfo, continue; } + // If state != closed, indicates that the parser encountered an error while processing the + // login response (e.g. an explicit error token). Transient network errors that impact + // connectivity will result in parser state being closed. if (_parser == null - || TdsParserState.Closed != _parser.State + || _parser.State != TdsParserState.Closed || IsDoNotRetryConnectError(sqlex) || timeout.IsExpired) { @@ -1738,7 +1734,7 @@ private void LoginNoFailover(ServerInfo serverInfo, // We only get here when we failed to connect, but are going to re-try // Switch to failover logic if the server provided a partner - if (ServerProvidedFailOverPartner != null) + if (ServerProvidedFailoverPartner != null) { if (connectionOptions.MultiSubnetFailover) { @@ -1754,7 +1750,7 @@ private void LoginNoFailover(ServerInfo serverInfo, LoginWithFailover( true, // start by using failover partner, since we already failed to connect to the primary serverInfo, - ServerProvidedFailOverPartner, + ServerProvidedFailoverPartner, newPassword, newSecurePassword, redirectedUserInstance, @@ -1776,8 +1772,13 @@ private void LoginNoFailover(ServerInfo serverInfo, { // We must wait for CompleteLogin to finish for to have the // env change from the server to know its designated failover - // partner; save this information in _currentFailoverPartner. - PoolGroupProviderInfo.FailoverCheck(false, connectionOptions, ServerProvidedFailOverPartner); + // partner; save this information in ServerProvidedFailoverPartner. + + // When ignoring server provided failover partner, we must pass in the original failover partner from the connection string. + // Otherwise the pool group's failover partner designation will be updated to point to the server provided value. + string actualFailoverPartner = LocalAppContextSwitches.IgnoreServerProvidedFailoverPartner ? string.Empty : ServerProvidedFailoverPartner; + + PoolGroupProviderInfo.FailoverCheck(false, connectionOptions, actualFailoverPartner); } CurrentDataSource = originalServerInfo.UserServerName; } @@ -1861,7 +1862,7 @@ TimeoutTimer timeout ServerInfo failoverServerInfo = new ServerInfo(connectionOptions, failoverHost, connectionOptions.FailoverPartnerSPN); ResolveExtendedServerName(primaryServerInfo, !redirectedUserInstance, connectionOptions); - if (ServerProvidedFailOverPartner == null) + if (ServerProvidedFailoverPartner == null) { ResolveExtendedServerName(failoverServerInfo, !redirectedUserInstance && failoverHost != primaryServerInfo.UserServerName, connectionOptions); } @@ -1918,12 +1919,21 @@ TimeoutTimer timeout failoverDemandDone = true; } - // Primary server may give us a different failover partner than the connection string indicates. Update it - if (ServerProvidedFailOverPartner != null && failoverServerInfo.ResolvedServerName != ServerProvidedFailOverPartner) + // Primary server may give us a different failover partner than the connection string indicates. + // Update it only if we are respecting server-provided failover partner values. + if (ServerProvidedFailoverPartner != null && failoverServerInfo.ResolvedServerName != ServerProvidedFailoverPartner) { - SqlClientEventSource.Log.TryAdvancedTraceEvent(" {0}, new failover partner={1}", ObjectID, ServerProvidedFailOverPartner); - failoverServerInfo.SetDerivedNames(protocol, ServerProvidedFailOverPartner); + if (LocalAppContextSwitches.IgnoreServerProvidedFailoverPartner) + { + SqlClientEventSource.Log.TryTraceEvent(" {0}, Ignoring server provided failover partner '{1}' due to IgnoreServerProvidedFailoverPartner AppContext switch.", ObjectID, ServerProvidedFailoverPartner); + } + else + { + SqlClientEventSource.Log.TryAdvancedTraceEvent(" {0}, new failover partner={1}", ObjectID, ServerProvidedFailoverPartner); + failoverServerInfo.SetDerivedNames(protocol, ServerProvidedFailoverPartner); + } } + currentServerInfo = failoverServerInfo; _timeoutErrorInternal.SetInternalSourceType(SqlConnectionInternalSourceType.Failover); } @@ -1973,7 +1983,7 @@ TimeoutTimer timeout _currentPacketSize = connectionOptions.PacketSize; _currentLanguage = _originalLanguage = ConnectionOptions.CurrentLanguage; CurrentDatabase = _originalDatabase = connectionOptions.InitialCatalog; - _currentFailoverPartner = null; + ServerProvidedFailoverPartner = null; _instanceName = string.Empty; AttemptOneLogin( @@ -1999,6 +2009,9 @@ TimeoutTimer timeout throw; // Caller will call LoginFailure() } + // TODO: It doesn't make sense to connect to an azure sql server instance with a failover partner + // specified. Azure SQL Server does not support failover partners. Other availability technologies + // like Failover Groups should be used instead. if (!ADP.IsAzureSqlServerEndpoint(connectionOptions.DataSource) && IsConnectionDoomed) { throw; @@ -2035,7 +2048,7 @@ TimeoutTimer timeout _activeDirectoryAuthTimeoutRetryHelper.State = ActiveDirectoryAuthenticationTimeoutRetryState.HasLoggedIn; // if connected to failover host, but said host doesn't have DbMirroring set up, throw an error - if (useFailoverHost && ServerProvidedFailOverPartner == null) + if (useFailoverHost && ServerProvidedFailoverPartner == null) { throw SQL.InvalidPartnerConfiguration(failoverHost, CurrentDatabase); } @@ -2044,8 +2057,13 @@ TimeoutTimer timeout { // We must wait for CompleteLogin to finish for to have the // env change from the server to know its designated failover - // partner; save this information in _currentFailoverPartner. - PoolGroupProviderInfo.FailoverCheck(useFailoverHost, connectionOptions, ServerProvidedFailOverPartner); + // partner. + + // When ignoring server provided failover partner, we must pass in the original failover partner from the connection string. + // Otherwise the pool group's failover partner designation will be updated to point to the server provided value. + string actualFailoverPartner = LocalAppContextSwitches.IgnoreServerProvidedFailoverPartner ? failoverHost : ServerProvidedFailoverPartner; + + PoolGroupProviderInfo.FailoverCheck(useFailoverHost, connectionOptions, actualFailoverPartner); } CurrentDataSource = (useFailoverHost ? failoverHost : primaryServerInfo.UserServerName); } @@ -2291,7 +2309,7 @@ internal void OnEnvChange(SqlEnvChange rec) break; case TdsEnums.ENV_LOGSHIPNODE: - _currentFailoverPartner = rec._newValue; + ServerProvidedFailoverPartner = rec._newValue; break; case TdsEnums.ENV_PROMOTETRANSACTION: diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/TdsParser.cs b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/TdsParser.cs index 0715bd8205..cce4c8426e 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/TdsParser.cs +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/TdsParser.cs @@ -121,8 +121,6 @@ internal sealed partial class TdsParser private bool _is2022 = false; - private string _serverSpn = null; - // SqlStatistics private SqlStatistics _statistics = null; @@ -396,6 +394,8 @@ internal void Connect(ServerInfo serverInfo, Debug.Fail("SNI returned status != success, but no error thrown?"); } + string serverSpn = null; + //Create LocalDB instance if necessary if (connHandler.ConnectionOptions.LocalDBInstance != null) { @@ -415,13 +415,13 @@ internal void Connect(ServerInfo serverInfo, if (!string.IsNullOrEmpty(serverInfo.ServerSPN)) { - _serverSpn = serverInfo.ServerSPN; + serverSpn = serverInfo.ServerSPN; SqlClientEventSource.Log.TryTraceEvent(" Server SPN `{0}` from the connection string is used.", serverInfo.ServerSPN); } else { // Empty signifies to interop layer that SPN needs to be generated - _serverSpn = string.Empty; + serverSpn = string.Empty; } SqlClientEventSource.Log.TryTraceEvent(" SSPI or Active Directory Authentication Library for SQL Server based integrated authentication"); @@ -429,7 +429,6 @@ internal void Connect(ServerInfo serverInfo, else { _authenticationProvider = null; - _serverSpn = null; switch (authType) { @@ -508,7 +507,7 @@ internal void Connect(ServerInfo serverInfo, serverInfo.ExtendedServerName, timeout, out instanceName, - ref _serverSpn, + ref serverSpn, false, true, fParallel, @@ -518,8 +517,6 @@ internal void Connect(ServerInfo serverInfo, FQDNforDNSCache, hostNameInCertificate); - _authenticationProvider?.Initialize(serverInfo, _physicalStateObj, this); - if (TdsEnums.SNI_SUCCESS != _physicalStateObj.Status) { _physicalStateObj.AddError(ProcessSNIError(_physicalStateObj)); @@ -602,7 +599,7 @@ internal void Connect(ServerInfo serverInfo, serverInfo.ExtendedServerName, timeout, out instanceName, - ref _serverSpn, + ref serverSpn, true, true, fParallel, @@ -612,8 +609,6 @@ internal void Connect(ServerInfo serverInfo, serverInfo.ResolvedServerName, hostNameInCertificate); - _authenticationProvider?.Initialize(serverInfo, _physicalStateObj, this); - if (TdsEnums.SNI_SUCCESS != _physicalStateObj.Status) { _physicalStateObj.AddError(ProcessSNIError(_physicalStateObj)); @@ -648,6 +643,8 @@ internal void Connect(ServerInfo serverInfo, } SqlClientEventSource.Log.TryTraceEvent(" Prelogin handshake successful"); + _authenticationProvider?.Initialize(serverInfo, _physicalStateObj, this, serverSpn); + if (_fMARS && marsCapable) { // if user explicitly disables mars or mars not supported, don't create the session pool @@ -1589,7 +1586,7 @@ internal SqlError ProcessSNIError(TdsParserStateObject stateObj) string providerRid = string.Format("SNI_PN{0}", (int)details.provider); string providerName = StringsHelper.GetString(providerRid); Debug.Assert(!string.IsNullOrEmpty(providerName), $"invalid providerResourceId '{providerRid}'"); - uint win32ErrorCode = details.nativeError; + int win32ErrorCode = details.nativeError; if (details.sniError == 0) { @@ -1627,14 +1624,14 @@ internal SqlError ProcessSNIError(TdsParserStateObject stateObj) // If its a LocalDB error, then nativeError actually contains a LocalDB-specific error code, not a win32 error code if (details.sniError == SniErrors.LocalDBErrorCode) { - errorMessage += LocalDbApi.GetLocalDbMessage((int)details.nativeError); + errorMessage += LocalDbApi.GetLocalDbMessage(details.nativeError); win32ErrorCode = 0; } } errorMessage = string.Format("{0} (provider: {1}, error: {2} - {3})", sqlContextInfo, providerName, (int)details.sniError, errorMessage); - return new SqlError((int)details.nativeError, 0x00, TdsEnums.FATAL_ERROR_CLASS, + return new SqlError(details.nativeError, 0x00, TdsEnums.FATAL_ERROR_CLASS, _server, errorMessage, details.function, (int)details.lineNumber, win32ErrorCode); } @@ -4174,7 +4171,6 @@ internal TdsOperationStatus TryProcessReturnValue(int length, { return result; } - byte len; result = stateObj.TryReadByte(out len); if (result != TdsOperationStatus.Done) @@ -5770,12 +5766,6 @@ private TdsOperationStatus TryProcessColumnHeaderNoNBC(SqlMetaDataPriv col, TdsP { if (col.metaType.IsLong && !col.metaType.IsPlp) { - if (stateObj.IsSnapshotContinuing()) - { - length = (ulong)stateObj.GetSnapshotStorageLength(); - isNull = length == 0; - return TdsOperationStatus.Done; - } // // we don't care about TextPtrs, simply go after the data after it // @@ -6229,19 +6219,20 @@ private TdsOperationStatus TryReadSqlStringValue(SqlBuffer value, byte type, int if (isPlp) { - result = TryReadPlpUnicodeCharsWithContinue( - stateObj, - length, - out string resultString - ); + char[] cc = null; + result = TryReadPlpUnicodeChars(ref cc, 0, length >> 1, stateObj, out length); - if (result == TdsOperationStatus.Done) + if (result != TdsOperationStatus.Done) { - s = resultString; + return result; + } + if (length > 0) + { + s = new string(cc, 0, length); } else { - return result; + s = string.Empty; } } else @@ -6598,7 +6589,9 @@ internal TdsOperationStatus TryReadSqlValue(SqlBuffer value, } else { - result = stateObj.TryReadByteArrayWithContinue(length, out b); + //Debug.Assert(length > 0 && length < (long)(Int32.MaxValue), "Bad length for column"); + b = new byte[length]; + result = stateObj.TryReadByteArray(b, length); if (result != TdsOperationStatus.Done) { return result; @@ -6669,7 +6662,8 @@ internal TdsOperationStatus TryReadSqlValue(SqlBuffer value, case TdsEnums.SQLVECTOR: // Vector data is read as non-plp binary value. // This is same as reading varbinary(8000). - result = stateObj.TryReadByteArrayWithContinue(length, out b); + byte[] buff = new byte[length]; + result = stateObj.TryReadByteArray(buff, length); if (result != TdsOperationStatus.Done) { return result; @@ -6677,13 +6671,13 @@ internal TdsOperationStatus TryReadSqlValue(SqlBuffer value, // Internally, we use Sqlbinary to deal with varbinary data and store it in // SqlBuffer as SqlBinary value. - value.SqlBinary = SqlTypeWorkarounds.SqlBinaryCtor(b, true); + value.SqlBinary = SqlTypeWorkarounds.SqlBinaryCtor(buff, true); // Extract the metadata from the payload and set it as the vector attributes // in the SqlBuffer. This metadata is further used when constructing a SqlVector // object from binary payload. - int elementCount = BinaryPrimitives.ReadUInt16LittleEndian(b.AsSpan(2)); - byte elementType = b[4]; + int elementCount = BinaryPrimitives.ReadUInt16LittleEndian(buff.AsSpan(2)); + byte elementType = buff[4]; value.SetVectorInfo(elementCount, elementType, false); break; @@ -8308,22 +8302,14 @@ internal TdsOperationStatus TryGetTokenLength(byte token, TdsParserStateObject s case TdsEnums.SQLVarCnt: if (0 != (token & 0x80)) { - if (stateObj.IsSnapshotContinuing()) - { - tokenLength = stateObj.GetSnapshotStorageLength(); - Debug.Assert(tokenLength != 0, "stored buffer length on continue must contain the length of the data required for the token"); - } - else + ushort value; + result = stateObj.TryReadUInt16(out value); + if (result != TdsOperationStatus.Done) { - ushort value; - result = stateObj.TryReadUInt16(out value); - if (result != TdsOperationStatus.Done) - { - tokenLength = 0; - return result; - } - tokenLength = value; + tokenLength = 0; + return result; } + tokenLength = value; return TdsOperationStatus.Done; } else if (0 == (token & 0x0c)) @@ -13249,9 +13235,8 @@ private TdsOperationStatus TryReadPlpUnicodeCharsChunk(char[] buff, int offst, i internal int ReadPlpUnicodeChars(ref char[] buff, int offst, int len, TdsParserStateObject stateObj) { int charsRead; - bool rentedBuff = false; Debug.Assert(stateObj._syncOverAsync, "Should not attempt pends in a synchronous call"); - TdsOperationStatus result = TryReadPlpUnicodeChars(ref buff, offst, len, stateObj, out charsRead, supportRentedBuff: false, ref rentedBuff); + TdsOperationStatus result = TryReadPlpUnicodeChars(ref buff, offst, len, stateObj, out charsRead); if (result != TdsOperationStatus.Done) { throw SQL.SynchronousCallMayNotPend(); @@ -13259,85 +13244,6 @@ internal int ReadPlpUnicodeChars(ref char[] buff, int offst, int len, TdsParserS return charsRead; } - internal TdsOperationStatus TryReadPlpUnicodeCharsWithContinue(TdsParserStateObject stateObj, int length, out string resultString) - { - resultString = null; - char[] temp = null; - bool buffIsRented = false; - int startOffset = 0; - (bool canContinue, bool isStarting, bool isContinuing) = stateObj.GetSnapshotStatuses(); - - if (canContinue) - { - if (isContinuing || isStarting) - { - temp = stateObj.TryTakeSnapshotStorage() as char[]; - Debug.Assert(temp == null || length == int.MaxValue || temp.Length == length, "stored buffer length must be null or must have been created with the correct length"); - } - if (temp != null) - { - startOffset = stateObj.GetSnapshotTotalSize(); - } - } - - TdsOperationStatus result = TryReadPlpUnicodeChars( - ref temp, - 0, - length >> 1, - stateObj, - out length, - supportRentedBuff: !canContinue, // do not use the arraypool if we are going to keep the buffer in the snapshot - rentedBuff: ref buffIsRented, - startOffset, - isStarting || isContinuing - ); - - if (result == TdsOperationStatus.Done) - { - if (length > 0) - { - resultString = new string(temp, 0, length); - } - else - { - resultString = string.Empty; - } - - if (buffIsRented) - { - // do not use clearArray:true on the rented array because it can be massively larger - // than the space we've used and we would incur performance clearing memory that - // we haven't used and can't leak out information. - // clear only the length that we know we have used. - temp.AsSpan(0, length).Clear(); - ArrayPool.Shared.Return(temp, clearArray: false); - temp = null; - } - } - else if (result == TdsOperationStatus.NeedMoreData) - { - if (isStarting || isContinuing) - { - stateObj.SetSnapshotStorage(temp); - } - } - - return result; - } - - internal TdsOperationStatus TryReadPlpUnicodeChars( - ref char[] buff, - int offst, - int len, - TdsParserStateObject stateObj, - out int totalCharsRead, - bool supportRentedBuff, - ref bool rentedBuff - ) - { - return TryReadPlpUnicodeChars(ref buff, offst, len, stateObj, out totalCharsRead, supportRentedBuff, ref rentedBuff, 0, false); - } - // Reads the requested number of chars from a plp data stream, or the entire data if // requested length is -1 or larger than the actual length of data. First call to this method // should be preceeded by a call to ReadPlpLength or ReadDataLength. @@ -13347,15 +13253,12 @@ internal TdsOperationStatus TryReadPlpUnicodeChars( int offst, int len, TdsParserStateObject stateObj, - out int totalCharsRead, - bool supportRentedBuff, - ref bool rentedBuff, - int startOffsetByteCount, - bool writeDataSizeToSnapshot - ) + out int totalCharsRead) { int charsRead = 0; int charsLeft = 0; + char[] newbuf; + TdsOperationStatus result; if (stateObj._longlen == 0) { @@ -13364,40 +13267,18 @@ bool writeDataSizeToSnapshot return TdsOperationStatus.Done; // No data } - Debug.Assert((ulong)stateObj._longlen != TdsEnums.SQL_PLP_NULL, "Out of sync plp read request"); - Debug.Assert( - (buff == null && offst == 0) - || - (buff.Length >= offst + len) - || - (buff.Length >= (startOffsetByteCount >> 1) + 1), - "Invalid length sent to ReadPlpUnicodeChars()!" - ); + Debug.Assert(((ulong)stateObj._longlen != TdsEnums.SQL_PLP_NULL), + "Out of sync plp read request"); + + Debug.Assert((buff == null && offst == 0) || (buff.Length >= offst + len), "Invalid length sent to ReadPlpUnicodeChars()!"); charsLeft = len; - // If total length is known up front, the length isn't specified as unknown - // and the caller doesn't pass int.max/2 indicating that it doesn't know the length - // allocate the whole buffer in one shot instead of realloc'ing and copying over each time - if (buff == null && stateObj._longlen != TdsEnums.SQL_PLP_UNKNOWNLEN && stateObj._longlen < (int.MaxValue >> 1)) + // If total length is known up front, allocate the whole buffer in one shot instead of realloc'ing and copying over each time + if (buff == null && stateObj._longlen != TdsEnums.SQL_PLP_UNKNOWNLEN) { - if (supportRentedBuff && stateObj._longlen < 1073741824) // 1 Gib - { - buff = ArrayPool.Shared.Rent((int)Math.Min((int)stateObj._longlen, len)); - rentedBuff = true; - } - else - { - buff = new char[(int)Math.Min((int)stateObj._longlen, len)]; - rentedBuff = false; - } + buff = new char[(int)Math.Min((int)stateObj._longlen, len)]; } - TdsOperationStatus result; - - bool partialReadInProgress = (startOffsetByteCount & 0x1) == 1; - bool restartingDataSizeCount = startOffsetByteCount == 0; - int currentPacketId = 0; - if (stateObj._longlenleft == 0) { result = stateObj.TryReadPlpLength(false, out _); @@ -13413,104 +13294,48 @@ bool writeDataSizeToSnapshot } } - totalCharsRead = (startOffsetByteCount >> 1); - charsLeft -= totalCharsRead; - offst += totalCharsRead; - - + totalCharsRead = 0; while (charsLeft > 0) { - if (!partialReadInProgress) + charsRead = (int)Math.Min((stateObj._longlenleft + 1) >> 1, (ulong)charsLeft); + if ((buff == null) || (buff.Length < (offst + charsRead))) { - charsRead = (int)Math.Min((stateObj._longlenleft + 1) >> 1, (ulong)charsLeft); - if ((buff == null) || (buff.Length < (offst + charsRead))) + // Grow the array + newbuf = new char[offst + charsRead]; + if (buff != null) { - char[] newbuf; - bool returnRentedBufferAfterCopy = rentedBuff; - if (supportRentedBuff && (offst + charsRead) < 1073741824) // 1 Gib - { - newbuf = ArrayPool.Shared.Rent(offst + charsRead); - rentedBuff = true; - } - else - { - // grow by an arbitrary number of packets to avoid needing to reallocate - // the newbuf on each loop iteration of long packet sequences which causes - // a performance problem as we spend large amounts of time copying and in gc - newbuf = new char[offst + charsRead + (stateObj.GetPacketSize() * 8)]; - rentedBuff = false; - } - - if (buff != null) - { - Buffer.BlockCopy(buff, 0, newbuf, 0, offst * 2); - if (returnRentedBufferAfterCopy) - { - buff.AsSpan(0, offst).Clear(); - ArrayPool.Shared.Return(buff, clearArray: false); - } - } - buff = newbuf; - newbuf = null; + Buffer.BlockCopy(buff, 0, newbuf, 0, offst * 2); } - if (charsRead > 0) + buff = newbuf; + } + if (charsRead > 0) + { + result = TryReadPlpUnicodeCharsChunk(buff, offst, charsRead, stateObj, out charsRead); + if (result != TdsOperationStatus.Done) { - result = TryReadPlpUnicodeCharsChunk(buff, offst, charsRead, stateObj, out charsRead); - if (result != TdsOperationStatus.Done) - { - return result; - } - charsLeft -= charsRead; - offst += charsRead; - totalCharsRead += charsRead; - - if (writeDataSizeToSnapshot) - { - currentPacketId = IncrementSnapshotDataSize(stateObj, restartingDataSizeCount, currentPacketId, charsRead * 2); - } + return result; } + charsLeft -= charsRead; + offst += charsRead; + totalCharsRead += charsRead; } - - // Special case single byte - if ( - (stateObj._longlenleft == 1 || partialReadInProgress) - && (charsLeft > 0) - ) + // Special case single byte left + if (stateObj._longlenleft == 1 && (charsLeft > 0)) { - byte b1 = 0; - byte b2 = 0; - if (partialReadInProgress) + byte b1; + result = stateObj.TryReadByte(out b1); + if (result != TdsOperationStatus.Done) { - partialReadInProgress = false; - // we're resuming with a partial char in the buffer so we need to load the byte - // from the char buffer and put it into b1 so we can combine it with the second - // half later - b1 = (byte)(buff[offst] & 0x00ff); + return result; } - else + stateObj._longlenleft--; + result = stateObj.TryReadPlpLength(false, out _); + if (result != TdsOperationStatus.Done) { - result = stateObj.TryReadByte(out b1); - if (result != TdsOperationStatus.Done) - { - return result; - } - stateObj._longlenleft--; - if (writeDataSizeToSnapshot) - { - // we need to write the single b1 byte to the array because we may run out of data - // and need to wait for another packet - buff[offst] = (char)((b1 & 0xff)); - currentPacketId = IncrementSnapshotDataSize(stateObj, restartingDataSizeCount, currentPacketId, 1); - } - - result = stateObj.TryReadPlpLength(false, out _); - if (result != TdsOperationStatus.Done) - { - return result; - } - Debug.Assert((stateObj._longlenleft != 0), "ReadPlpUnicodeChars: Odd byte left at the end!"); + return result; } - + Debug.Assert((stateObj._longlenleft != 0), "ReadPlpUnicodeChars: Odd byte left at the end!"); + byte b2; result = stateObj.TryReadByte(out b2); if (result != TdsOperationStatus.Done) { @@ -13523,11 +13348,6 @@ bool writeDataSizeToSnapshot charsRead++; charsLeft--; totalCharsRead++; - - if (writeDataSizeToSnapshot) - { - currentPacketId = IncrementSnapshotDataSize(stateObj, restartingDataSizeCount, currentPacketId, 1); - } } if (stateObj._longlenleft == 0) { @@ -13540,41 +13360,9 @@ bool writeDataSizeToSnapshot } if (stateObj._longlenleft == 0) // Data read complete - { break; - } } return TdsOperationStatus.Done; - - static int IncrementSnapshotDataSize(TdsParserStateObject stateObj, bool resetting, int previousPacketId, int value) - { - int current = 0; - if (resetting) - { - int currentPacketId = stateObj.GetSnapshotPacketID(); - if (previousPacketId == currentPacketId) - { - // we have already reset it the first time we saw it so just add normally - current = stateObj.GetSnapshotDataSize(); - } - else - { - // a packet we haven't seen before, reset the size - current = 0; - } - - stateObj.SetSnapshotDataSize(current + value); - - // return new packetid so next time we see this packet we know it isn't new - return currentPacketId; - } - else - { - current = stateObj.GetSnapshotDataSize(); - stateObj.SetSnapshotDataSize(current + value); - return previousPacketId; - } - } } internal int ReadPlpAnsiChars(ref char[] buff, int offst, int len, SqlMetaDataPriv metadata, TdsParserStateObject stateObj) @@ -13761,15 +13549,14 @@ internal ulong PlpBytesTotalLength(TdsParserStateObject stateObj) + " _connHandler = {14}\n\t" + " _fMARS = {15}\n\t" + " _sessionPool = {16}\n\t" - + " _sniSpnBuffer = {17}\n\t" - + " _errors = {18}\n\t" - + " _warnings = {19}\n\t" - + " _attentionErrors = {20}\n\t" - + " _attentionWarnings = {21}\n\t" - + " _statistics = {22}\n\t" - + " _statisticsIsInTransaction = {23}\n\t" - + " _fPreserveTransaction = {24}" - + " _fParallel = {25}" + + " _errors = {17}\n\t" + + " _warnings = {18}\n\t" + + " _attentionErrors = {19}\n\t" + + " _attentionWarnings = {20}\n\t" + + " _statistics = {21}\n\t" + + " _statisticsIsInTransaction = {22}\n\t" + + " _fPreserveTransaction = {23}" + + " _fParallel = {24}" ; internal string TraceString() { @@ -13792,7 +13579,6 @@ internal string TraceString() _connHandler == null ? "(null)" : _connHandler.ObjectID.ToString((IFormatProvider)null), _fMARS ? bool.TrueString : bool.FalseString, _sessionPool == null ? "(null)" : _sessionPool.TraceString(), - _serverSpn == null ? "(null)" : _serverSpn.Length.ToString((IFormatProvider)null), _physicalStateObj != null ? "(null)" : _physicalStateObj.ErrorCount.ToString((IFormatProvider)null), _physicalStateObj != null ? "(null)" : _physicalStateObj.WarningCount.ToString((IFormatProvider)null), _physicalStateObj != null ? "(null)" : _physicalStateObj.PreAttentionErrorCount.ToString((IFormatProvider)null), diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/TdsParserStateObject.netfx.cs b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/TdsParserStateObject.netfx.cs index cf54e0340f..a64afdce80 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/TdsParserStateObject.netfx.cs +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/TdsParserStateObject.netfx.cs @@ -259,22 +259,14 @@ private void ReadSniError(TdsParserStateObject stateObj, uint error) stateObj.SendAttention(mustTakeWriteLock: true); PacketHandle syncReadPacket = default; - bool readFromNetwork = true; RuntimeHelpers.PrepareConstrainedRegions(); bool shouldDecrement = false; try { Interlocked.Increment(ref _readingCount); shouldDecrement = true; - readFromNetwork = !PartialPacketContainsCompletePacket(); - if (readFromNetwork) - { - syncReadPacket = ReadSyncOverAsync(stateObj.GetTimeoutRemaining(), out error); - } - else - { - error = TdsEnums.SNI_SUCCESS; - } + + syncReadPacket = ReadSyncOverAsync(stateObj.GetTimeoutRemaining(), out error); Interlocked.Decrement(ref _readingCount); shouldDecrement = false; @@ -287,7 +279,7 @@ private void ReadSniError(TdsParserStateObject stateObj, uint error) } else { - Debug.Assert(!readFromNetwork || !IsValidPacket(syncReadPacket), "unexpected syncReadPacket without corresponding SNIPacketRelease"); + Debug.Assert(!IsValidPacket(syncReadPacket), "unexpected syncReadPacket without corresponding SNIPacketRelease"); fail = true; // Subsequent read failed, time to give up. } } @@ -298,7 +290,7 @@ private void ReadSniError(TdsParserStateObject stateObj, uint error) Interlocked.Decrement(ref _readingCount); } - if (readFromNetwork && !IsPacketEmpty(syncReadPacket)) + if (!IsPacketEmpty(syncReadPacket)) { // Be sure to release packet, otherwise it will be leaked by native. ReleasePacket(syncReadPacket); @@ -339,9 +331,60 @@ private void ReadSniError(TdsParserStateObject stateObj, uint error) AssertValidState(); } - private uint GetSniPacket(PacketHandle packet, ref uint dataSize) + public void ProcessSniPacket(PacketHandle packet, uint error) { - return SniPacketGetData(packet, _inBuff, ref dataSize); + if (error != 0) + { + if ((_parser.State == TdsParserState.Closed) || (_parser.State == TdsParserState.Broken)) + { + // Do nothing with callback if closed or broken and error not 0 - callback can occur + // after connection has been closed. PROBLEM IN NETLIB - DESIGN FLAW. + return; + } + + AddError(_parser.ProcessSNIError(this)); + AssertValidState(); + } + else + { + uint dataSize = 0; + + uint getDataError = SniPacketGetData(packet, _inBuff, ref dataSize); + + if (getDataError == TdsEnums.SNI_SUCCESS) + { + if (_inBuff.Length < dataSize) + { + Debug.Assert(true, "Unexpected dataSize on Read"); + throw SQL.InvalidInternalPacketSize(StringsHelper.GetString(Strings.SqlMisc_InvalidArraySizeMessage)); + } + + _lastSuccessfulIOTimer._value = DateTime.UtcNow.Ticks; + _inBytesRead = (int)dataSize; + _inBytesUsed = 0; + + if (_snapshot != null) + { + _snapshot.AppendPacketData(_inBuff, _inBytesRead); + if (_snapshotReplay) + { + _snapshot.MoveNext(); +#if DEBUG + _snapshot.AssertCurrent(); +#endif + } + } + + SniReadStatisticsAndTracing(); + SqlClientEventSource.Log.TryAdvancedTraceBinEvent("TdsParser.ReadNetworkPacketAsyncCallback | INFO | ADV | State Object Id {0}, Packet read. In Buffer: {1}, In Bytes Read: {2}", ObjectID, _inBuff, _inBytesRead); + + AssertValidState(); + } + else + { + throw SQL.ParsingError(ParsingErrorState.ProcessSniPacketFailed); + } + } } public void ReadAsyncCallback(IntPtr key, PacketHandle packet, uint error) diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlTypes/SqlTypeWorkarounds.netfx.cs b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlTypes/SqlTypeWorkarounds.netfx.cs index 941e3325b6..af7269ed8b 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlTypes/SqlTypeWorkarounds.netfx.cs +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlTypes/SqlTypeWorkarounds.netfx.cs @@ -5,8 +5,6 @@ using System; using System.Data.SqlTypes; using System.Reflection; -using System.Reflection.Emit; -using System.Runtime.Serialization; using Microsoft.Data.SqlClient; namespace Microsoft.Data.SqlTypes @@ -20,7 +18,10 @@ namespace Microsoft.Data.SqlTypes internal static partial class SqlTypeWorkarounds { #region Work around inability to access SqlMoney.ctor(long, int) and SqlMoney.ToSqlInternalRepresentation - private static readonly Func s_sqlMoneyfactory = CtorHelper.CreateFactory(); // binds to SqlMoney..ctor(long, int) if it exists + // Documentation for internal ctor: + // https://learn.microsoft.com/en-us/dotnet/framework/additional-apis/system.data.sqltypes.sqlmoney.-ctor + private static readonly Func s_sqlMoneyfactory = + CtorHelper.CreateFactory(); // binds to SqlMoney..ctor(long, int) if it exists /// /// Constructs a SqlMoney from a long value without scaling. The ignored parameter exists @@ -70,6 +71,11 @@ internal static SqlMoneyToLongDelegate GetSqlMoneyToLong() private static SqlMoneyToLongDelegate GetFastSqlMoneyToLong() { + // Note: Although it would be faster to use the m_value member variable in + // SqlMoney, but because it is not documented, we cannot use it. The method + // we are calling below *is* documented, despite it being internal. + // Documentation for internal method: + // https://learn.microsoft.com/en-us/dotnet/framework/additional-apis/system.data.sqltypes.sqlmoney.tosqlinternalrepresentation MethodInfo toSqlInternalRepresentation = typeof(SqlMoney).GetMethod("ToSqlInternalRepresentation", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.ExactBinding, null, CallingConventions.Any, new Type[] { }, null); @@ -113,145 +119,45 @@ private static long FallbackSqlMoneyToLong(ref SqlMoney value) } #endregion - #region Work around inability to access SqlDecimal._data1/2/3/4 - internal static void SqlDecimalExtractData(SqlDecimal d, out uint data1, out uint data2, out uint data3, out uint data4) - { - SqlDecimalHelper.s_decompose(d, out data1, out data2, out data3, out data4); - } + #region Work around SqlDecimal.WriteTdsValue not existing in netfx - private static class SqlDecimalHelper + /// + /// Implementation that mimics netcore's WriteTdsValue method. + /// + /// + /// Although calls to this method could just be replaced with calls to + /// , using this mimic method allows netfx and netcore + /// implementations to be more cleanly switched. + /// + /// SqlDecimal value to get data from. + /// First data field will be written here. + /// Second data field will be written here. + /// Third data field will be written here. + /// Fourth data field will be written here. + internal static void SqlDecimalExtractData( + SqlDecimal value, + out uint data1, + out uint data2, + out uint data3, + out uint data4) { - internal delegate void Decomposer(SqlDecimal value, out uint data1, out uint data2, out uint data3, out uint data4); - internal static readonly Decomposer s_decompose = GetDecomposer(); - - private static Decomposer GetDecomposer() - { - Decomposer decomposer = null; - try - { - decomposer = GetFastDecomposer(); - } - catch - { - // If an exception occurs for any reason, swallow & use the fallback code path. - } - - return decomposer ?? FallbackDecomposer; - } - - private static Decomposer GetFastDecomposer() - { - // This takes advantage of the fact that for [Serializable] types, the member fields are implicitly - // part of the type's serialization contract. This includes the fields' names and types. By default, - // [Serializable]-compliant serializers will read all the member fields and shove the data into a - // SerializationInfo dictionary. We mimic this behavior in a manner consistent with the [Serializable] - // pattern, but much more efficiently. - // - // In order to make sure we're staying compliant, we need to gate our checks to fulfill some core - // assumptions. Importantly, the type must be [Serializable] but cannot be ISerializable, as the - // presence of the interface means that the type wants to be responsible for its own serialization, - // and that member fields are not guaranteed to be part of the serialization contract. Additionally, - // we need to check for [OnSerializing] and [OnDeserializing] methods, because we cannot account - // for any logic which might be present within them. - - if (!typeof(SqlDecimal).IsSerializable) - { - SqlClientEventSource.Log.TryTraceEvent("SqlTypeWorkarounds.SqlDecimalHelper.GetFastDecomposer | Info | SqlDecimal isn't Serializable. Less efficient fallback method will be used."); - return null; // type is not serializable - cannot use fast path assumptions - } - - if (typeof(ISerializable).IsAssignableFrom(typeof(SqlDecimal))) - { - SqlClientEventSource.Log.TryTraceEvent("SqlTypeWorkarounds.SqlDecimalHelper.GetFastDecomposer | Info | SqlDecimal is ISerializable. Less efficient fallback method will be used."); - return null; // type contains custom logic - cannot use fast path assumptions - } - - foreach (MethodInfo method in typeof(SqlDecimal).GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)) - { - if (method.IsDefined(typeof(OnDeserializingAttribute)) || method.IsDefined(typeof(OnDeserializedAttribute))) - { - SqlClientEventSource.Log.TryTraceEvent("SqlTypeWorkarounds.SqlDecimalHelper.GetFastDecomposer | Info | SqlDecimal contains custom serialization logic. Less efficient fallback method will be used."); - return null; // type contains custom logic - cannot use fast path assumptions - } - } - - // GetSerializableMembers filters out [NonSerialized] fields for us automatically. - - FieldInfo fiData1 = null, fiData2 = null, fiData3 = null, fiData4 = null; - foreach (MemberInfo candidate in FormatterServices.GetSerializableMembers(typeof(SqlDecimal))) - { - if (candidate is FieldInfo fi && fi.FieldType == typeof(uint)) - { - if (fi.Name == "m_data1") - { fiData1 = fi; } - else if (fi.Name == "m_data2") - { fiData2 = fi; } - else if (fi.Name == "m_data3") - { fiData3 = fi; } - else if (fi.Name == "m_data4") - { fiData4 = fi; } - } - } - - if (fiData1 is null || fiData2 is null || fiData3 is null || fiData4 is null) - { - SqlClientEventSource.Log.TryTraceEvent("SqlTypeWorkarounds.SqlDecimalHelper.GetFastDecomposer | Info | Expected SqlDecimal fields are missing. Less efficient fallback method will be used."); - return null; // missing one of the expected member fields - cannot use fast path assumptions - } - - Type refToUInt32 = typeof(uint).MakeByRefType(); - DynamicMethod dm = new( - name: "sqldecimal-decomposer", - returnType: typeof(void), - parameterTypes: new[] { typeof(SqlDecimal), refToUInt32, refToUInt32, refToUInt32, refToUInt32 }, - restrictedSkipVisibility: true); // perf: JITs method at delegate creation time - - ILGenerator ilGen = dm.GetILGenerator(); - ilGen.Emit(OpCodes.Ldarg_1); // eval stack := [UInt32&] - ilGen.Emit(OpCodes.Ldarg_0); // eval stack := [UInt32&] [SqlDecimal] - ilGen.Emit(OpCodes.Ldfld, fiData1); // eval stack := [UInt32&] [UInt32] - ilGen.Emit(OpCodes.Stind_I4); // eval stack := - ilGen.Emit(OpCodes.Ldarg_2); // eval stack := [UInt32&] - ilGen.Emit(OpCodes.Ldarg_0); // eval stack := [UInt32&] [SqlDecimal] - ilGen.Emit(OpCodes.Ldfld, fiData2); // eval stack := [UInt32&] [UInt32] - ilGen.Emit(OpCodes.Stind_I4); // eval stack := - ilGen.Emit(OpCodes.Ldarg_3); // eval stack := [UInt32&] - ilGen.Emit(OpCodes.Ldarg_0); // eval stack := [UInt32&] [SqlDecimal] - ilGen.Emit(OpCodes.Ldfld, fiData3); // eval stack := [UInt32&] [UInt32] - ilGen.Emit(OpCodes.Stind_I4); // eval stack := - ilGen.Emit(OpCodes.Ldarg_S, (byte)4); // eval stack := [UInt32&] - ilGen.Emit(OpCodes.Ldarg_0); // eval stack := [UInt32&] [SqlDecimal] - ilGen.Emit(OpCodes.Ldfld, fiData4); // eval stack := [UInt32&] [UInt32] - ilGen.Emit(OpCodes.Stind_I4); // eval stack := - ilGen.Emit(OpCodes.Ret); - - return (Decomposer)dm.CreateDelegate(typeof(Decomposer), null /* target */); - } - - // Used in case we can't use a [Serializable]-like mechanism. - private static void FallbackDecomposer(SqlDecimal value, out uint data1, out uint data2, out uint data3, out uint data4) - { - if (value.IsNull) - { - data1 = default; - data2 = default; - data3 = default; - data4 = default; - } - else - { - int[] data = value.Data; // allocation - data4 = (uint)data[3]; // write in reverse to avoid multiple bounds checks - data3 = (uint)data[2]; - data2 = (uint)data[1]; - data1 = (uint)data[0]; - } - } + // Note: Although it would be faster to use the m_data[1-4] member variables in + // SqlDecimal, we cannot use them because they are not documented. The Data property + // is less ideal, but is documented. + int[] data = value.Data; + data1 = (uint)data[0]; + data2 = (uint)data[1]; + data3 = (uint)data[2]; + data4 = (uint)data[3]; } + #endregion #region Work around inability to access SqlBinary.ctor(byte[], bool) - private static readonly Func s_sqlBinaryfactory = CtorHelper.CreateFactory(); // binds to SqlBinary..ctor(byte[], bool) if it exists + // Documentation of internal constructor: + // https://learn.microsoft.com/en-us/dotnet/framework/additional-apis/system.data.sqltypes.sqlbinary.-ctor + private static readonly Func s_sqlBinaryfactory = + CtorHelper.CreateFactory(); internal static SqlBinary SqlBinaryCtor(byte[] value, bool ignored) { @@ -270,7 +176,10 @@ internal static SqlBinary SqlBinaryCtor(byte[] value, bool ignored) #endregion #region Work around inability to access SqlGuid.ctor(byte[], bool) - private static readonly Func s_sqlGuidfactory = CtorHelper.CreateFactory(); // binds to SqlGuid..ctor(byte[], bool) if it exists + // Documentation for internal constructor: + // https://learn.microsoft.com/en-us/dotnet/framework/additional-apis/system.data.sqltypes.sqlguid.-ctor + private static readonly Func s_sqlGuidfactory = + CtorHelper.CreateFactory(); internal static SqlGuid SqlGuidCtor(byte[] value, bool ignored) { diff --git a/src/Microsoft.Data.SqlClient/src/Interop/Windows/Sni/SniError.cs b/src/Microsoft.Data.SqlClient/src/Interop/Windows/Sni/SniError.cs index b349c2876d..2e519c15ab 100644 --- a/src/Microsoft.Data.SqlClient/src/Interop/Windows/Sni/SniError.cs +++ b/src/Microsoft.Data.SqlClient/src/Interop/Windows/Sni/SniError.cs @@ -12,7 +12,7 @@ internal struct SniError internal Provider provider; [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 261)] internal string errorMessage; - internal uint nativeError; + internal int nativeError; internal uint sniError; [MarshalAs(UnmanagedType.LPWStr)] internal string fileName; diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft.Data.SqlClient.csproj b/src/Microsoft.Data.SqlClient/src/Microsoft.Data.SqlClient.csproj index ff8274c66f..3a1d19a96d 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft.Data.SqlClient.csproj +++ b/src/Microsoft.Data.SqlClient/src/Microsoft.Data.SqlClient.csproj @@ -8,8 +8,14 @@ + + + + + + @@ -17,12 +23,7 @@ - - - - - diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/Common/AdapterUtil.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/Common/AdapterUtil.cs index 470cb8e35e..928dc6bab6 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/Common/AdapterUtil.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/Common/AdapterUtil.cs @@ -24,6 +24,7 @@ using Microsoft.Identity.Client; using Microsoft.SqlServer.Server; using System.Security.Authentication; +using System.Collections.Generic; #if NETFRAMEWORK using System.Reflection; @@ -617,6 +618,13 @@ internal static Delegate FindBuilder(MulticastDelegate mcd) internal static long TimerCurrent() => DateTime.UtcNow.ToFileTimeUtc(); + internal static long FastTimerCurrent() => Environment.TickCount; + + internal static uint CalculateTickCountElapsed(long startTick, long endTick) + { + return (uint)(endTick - startTick); + } + internal static long TimerFromSeconds(int seconds) { long result = checked((long)seconds * TimeSpan.TicksPerSecond); @@ -768,7 +776,7 @@ internal static Version GetAssemblyVersion() /// This array includes endpoint URLs for Azure SQL in global, Germany, US Government, /// China, and Fabric environments. These endpoints are used to identify and interact with Azure SQL services /// in their respective regions or environments. - internal static readonly string[] s_azureSqlServerEndpoints = { AZURE_SQL, + internal static readonly List s_azureSqlServerEndpoints = new() { AZURE_SQL, AZURE_SQL_GERMANY, AZURE_SQL_USGOV, AZURE_SQL_CHINA, @@ -808,7 +816,7 @@ internal static bool IsAzureSqlServerEndpoint(string dataSource) } // This method assumes dataSource parameter is in TCP connection string format. - private static bool IsEndpoint(string dataSource, string[] endpoints) + private static bool IsEndpoint(string dataSource, ICollection endpoints) { int length = dataSource.Length; // remove server port diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/Common/ConnectionString/DbConnectionStringDefaults.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/Common/ConnectionString/DbConnectionStringDefaults.cs index 41a8058b5c..ca5ebb2226 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/Common/ConnectionString/DbConnectionStringDefaults.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/Common/ConnectionString/DbConnectionStringDefaults.cs @@ -57,7 +57,7 @@ internal static class DbConnectionStringDefaults #if NETFRAMEWORK internal const bool ConnectionReset = true; - internal static readonly bool TransparentNetworkIPResolution = !LocalAppContextSwitches.DisableTnirByDefault; + internal static bool TransparentNetworkIpResolution => !LocalAppContextSwitches.DisableTnirByDefault; internal const string NetworkLibrary = ""; #endif } diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/ProviderBase/DbConnectionClosed.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/ProviderBase/DbConnectionClosed.cs index d4ea183312..f4e87010a6 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/ProviderBase/DbConnectionClosed.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/ProviderBase/DbConnectionClosed.cs @@ -32,6 +32,7 @@ internal override void CloseConnection(DbConnection owningObject, DbConnectionFa // not much to do here... } + /// protected override void Deactivate() => ADP.ClosedConnectionError(); public override void EnlistTransaction(System.Transactions.Transaction transaction) => throw ADP.ClosedConnectionError(); @@ -43,6 +44,9 @@ protected internal override DataTable GetSchema(DbConnectionFactory factory, DbC internal override bool TryOpenConnection(DbConnection outerConnection, DbConnectionFactory connectionFactory, TaskCompletionSource retry, DbConnectionOptions userOptions) => base.TryOpenConnectionInternal(outerConnection, connectionFactory, retry, userOptions); + + /// + internal override void ResetConnection() => throw ADP.ClosedConnectionError(); } internal abstract class DbConnectionBusy : DbConnectionClosed diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/ProviderBase/DbConnectionInternal.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/ProviderBase/DbConnectionInternal.cs index b4dfb4f214..eafddd8ae2 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/ProviderBase/DbConnectionInternal.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/ProviderBase/DbConnectionInternal.cs @@ -797,6 +797,13 @@ internal void PrePush(object expectedOwner) internal void RemoveWeakReference(object value) => ReferenceCollection?.Remove(value); + /// + /// Idempotently resets the connection so that it may be recycled without leaking state. + /// May preserve transaction state if the connection is enlisted in a distributed transaction. + /// Should be called before the first action is taken on a recycled connection. + /// + internal abstract void ResetConnection(); + internal void SetInStasis() { IsTxRootWaitingForTxEnd = true; @@ -834,6 +841,11 @@ internal virtual bool TryReplaceConnection( #region Protected Methods + /// + /// Activates the connection, preparing it for active use. + /// An activated connection has an owner and is checked out from the connection pool (if pooling is enabled). + /// + /// The transaction in which the connection should enlist. protected abstract void Activate(Transaction transaction); /// @@ -850,6 +862,11 @@ protected virtual DbReferenceCollection CreateReferenceCollection() throw ADP.InternalError(ADP.InternalErrorCode.AttemptingToConstructReferenceCollectionOnStaticObject); } + /// + /// Deactivates the connection, cleaning up any state as necessary. + /// A deactivated connection is one that is no longer in active use and does not have an owner. + /// A deactivated connection may be open (connected to a server) and is checked into the connection pool (if pooling is enabled). + /// protected abstract void Deactivate(); protected internal void DoNotPoolThisConnection() diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ActiveDirectoryAuthenticationProvider.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ActiveDirectoryAuthenticationProvider.cs index b374a550b3..9cbec9516d 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ActiveDirectoryAuthenticationProvider.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ActiveDirectoryAuthenticationProvider.cs @@ -223,7 +223,11 @@ public override async Task AcquireTokenAsync(SqlAuthenti { if (!string.IsNullOrEmpty(parameters.UserId)) { + // The AcquireTokenByIntegratedWindowsAuth method is marked as obsolete in MSAL.NET + // but it is still a supported way to acquire tokens for Active Directory Integrated authentication. +#pragma warning disable CS0618 // Type or member is obsolete result = await app.AcquireTokenByIntegratedWindowsAuth(scopes) +#pragma warning restore CS0618 // Type or member is obsolete .WithCorrelationId(parameters.ConnectionId) .WithUsername(parameters.UserId) .ExecuteAsync(cancellationToken: cts.Token) @@ -231,7 +235,9 @@ public override async Task AcquireTokenAsync(SqlAuthenti } else { +#pragma warning disable CS0618 // Type or member is obsolete result = await app.AcquireTokenByIntegratedWindowsAuth(scopes) +#pragma warning restore CS0618 // Type or member is obsolete .WithCorrelationId(parameters.ConnectionId) .ExecuteAsync(cancellationToken: cts.Token) .ConfigureAwait(false); @@ -582,7 +588,28 @@ private static TokenCredentialData CreateTokenCredentialInstance(TokenCredential defaultAzureCredentialOptions.WorkloadIdentityClientId = tokenCredentialKey._clientId; } - return new TokenCredentialData(new DefaultAzureCredential(defaultAzureCredentialOptions), GetHash(secret)); + // SqlClient is a library and provides support to acquire access + // token using 'DefaultAzureCredential' on user demand when they + // specify 'Authentication = Active Directory Default' in + // connection string. + // + // Default Azure Credential is instantiated by the calling + // application when using "Active Directory Default" + // authentication code to connect to Azure SQL instance. + // SqlClient is a library, doesn't instantiate the credential + // without running application instructions. + // + // Note that CodeQL suppression support can only detect + // suppression comments that appear immediately above the + // flagged statement, or appended to the end of the statement. + // Multi-line justifications are not supported. + // + // https://eng.ms/docs/cloud-ai-platform/devdiv/one-engineering-system-1es/1es-docs/codeql/codeql-semmle#guidance-on-suppressions + // + // CodeQL [SM05137] See above for justification. + DefaultAzureCredential cred = new(defaultAzureCredentialOptions); + + return new TokenCredentialData(cred, GetHash(secret)); } TokenCredentialOptions tokenCredentialOptions = new() { AuthorityHost = new Uri(tokenCredentialKey._authority) }; diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ConnectionPool/WaitHandleDbConnectionPool.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ConnectionPool/WaitHandleDbConnectionPool.cs index 77d2b3bae1..cb099e27d3 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ConnectionPool/WaitHandleDbConnectionPool.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ConnectionPool/WaitHandleDbConnectionPool.cs @@ -382,7 +382,6 @@ internal WaitHandle[] GetHandles(bool withCreate) return withCreate ? _handlesWithCreate : _handlesWithoutCreate; } } - private const int MAX_Q_SIZE = 0x00100000; // The order of these is important; we want the WaitAny call to be signaled @@ -1321,32 +1320,36 @@ private bool TryGetConnection(DbConnection owningObject, uint waitForMultipleObj if (onlyOneCheckConnection) { - if (_waitHandles.CreationSemaphore.WaitOne(unchecked((int)waitForMultipleObjectsTimeout))) - { + bool obtained = false; #if NETFRAMEWORK - RuntimeHelpers.PrepareConstrainedRegions(); + RuntimeHelpers.PrepareConstrainedRegions(); #endif - try + try + { + obtained = _waitHandles.CreationSemaphore.WaitOne(unchecked((int)waitForMultipleObjectsTimeout)); + if (obtained) { SqlClientEventSource.Log.TryPoolerTraceEvent(" {0}, Creating new connection.", Id); obj = UserCreateRequest(owningObject, userOptions); } - finally + else { - _waitHandles.CreationSemaphore.Release(1); + // Timeout waiting for creation semaphore - return null + SqlClientEventSource.Log.TryPoolerTraceEvent(" {0}, Wait timed out.", Id); + connection = null; + return false; } } - else + finally { - // Timeout waiting for creation semaphore - return null - SqlClientEventSource.Log.TryPoolerTraceEvent(" {0}, Wait timed out.", Id); - connection = null; - return false; + if (obtained) + { + _waitHandles.CreationSemaphore.Release(1); + } } } } break; - case WAIT_ABANDONED + SEMAPHORE_HANDLE: SqlClientEventSource.Log.TryPoolerTraceEvent(" {0}, Semaphore handle abandonded.", Id); Interlocked.Decrement(ref _waitCount); @@ -1553,25 +1556,16 @@ private void PoolCreateRequest(object state) { return; } - int waitResult = BOGUS_HANDLE; - #if NETFRAMEWORK RuntimeHelpers.PrepareConstrainedRegions(); #endif + bool obtained = false; try { // Obtain creation mutex so we're the only one creating objects - // and we must have the wait result -#if NETFRAMEWORK - RuntimeHelpers.PrepareConstrainedRegions(); -#endif - try - { } - finally - { - waitResult = WaitHandle.WaitAny(_waitHandles.GetHandles(withCreate: true), CreationTimeout); - } - if (CREATION_HANDLE == waitResult) + obtained = _waitHandles.CreationSemaphore.WaitOne(CreationTimeout); + + if (obtained) { DbConnectionInternal newObj; @@ -1606,17 +1600,12 @@ private void PoolCreateRequest(object state) } } } - else if (WaitHandle.WaitTimeout == waitResult) + else { // do not wait forever and potential block this worker thread // instead wait for a period of time and just requeue to try again QueuePoolCreateRequest(); } - else - { - // trace waitResult and ignore the failure - SqlClientEventSource.Log.TryPoolerTraceEvent(" {0}, PoolCreateRequest called WaitForSingleObject failed {1}", Id, waitResult); - } } catch (Exception e) { @@ -1632,9 +1621,8 @@ private void PoolCreateRequest(object state) } finally { - if (CREATION_HANDLE == waitResult) + if (obtained) { - // reuse waitResult and ignore its value _waitHandles.CreationSemaphore.Release(1); } } @@ -1694,8 +1682,6 @@ public void PutObjectFromTransactedPool(DbConnectionInternal obj) Debug.Assert(obj != null, "null pooledObject?"); Debug.Assert(obj.EnlistedTransaction == null, "pooledObject is still enlisted?"); - obj.DeactivateConnection(); - // called by the transacted connection pool , once it's removed the // connection from it's list. We put the connection back in general // circulation. @@ -1708,6 +1694,7 @@ public void PutObjectFromTransactedPool(DbConnectionInternal obj) if (State is Running && obj.CanBePooled) { + obj.ResetConnection(); PutNewObject(obj); } else diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Diagnostics/SqlClientMetrics.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Diagnostics/SqlClientMetrics.cs index f62c65b122..9f49080361 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Diagnostics/SqlClientMetrics.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Diagnostics/SqlClientMetrics.cs @@ -92,7 +92,7 @@ internal sealed partial class SqlClientMetrics private string? _instanceName; #endif - public SqlClientMetrics(SqlClientEventSource eventSource) + public SqlClientMetrics(SqlClientEventSource eventSource, bool enableMetrics) { _eventSource = eventSource; @@ -100,6 +100,11 @@ public SqlClientMetrics(SqlClientEventSource eventSource) // On .NET Framework, metrics are exposed as performance counters and are always enabled. // On .NET Core, metrics are exposed as EventCounters, and require explicit enablement. EnablePerformanceCounters(); +#else + if (enableMetrics) + { + EnableEventCounters(); + } #endif } @@ -489,25 +494,25 @@ private void RemovePerformanceCounters() private PerformanceCounter? CreatePerformanceCounter(string counterName, PerformanceCounterType counterType) { - PerformanceCounter? instance = null; - _instanceName ??= GetInstanceName(); try { - instance = new PerformanceCounter(); + PerformanceCounter instance = new(); instance.CategoryName = PerformanceCounterCategoryName; instance.CounterName = counterName; instance.InstanceName = _instanceName; instance.InstanceLifetime = PerformanceCounterInstanceLifetime.Process; instance.ReadOnly = false; instance.RawValue = 0; // make sure we start out at zero + + return instance; } catch (InvalidOperationException e) { ADP.TraceExceptionWithoutRethrow(e); - } - return instance; + return null; + } } // SxS: this method uses GetCurrentProcessId to construct the instance name. diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/LocalAppContextSwitches.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/LocalAppContextSwitches.cs index dfd94453f2..702be5f842 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/LocalAppContextSwitches.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/LocalAppContextSwitches.cs @@ -20,9 +20,8 @@ private enum Tristate : byte internal const string SuppressInsecureTlsWarningString = @"Switch.Microsoft.Data.SqlClient.SuppressInsecureTLSWarning"; internal const string UseMinimumLoginTimeoutString = @"Switch.Microsoft.Data.SqlClient.UseOneSecFloorInTimeoutCalculationDuringLogin"; internal const string LegacyVarTimeZeroScaleBehaviourString = @"Switch.Microsoft.Data.SqlClient.LegacyVarTimeZeroScaleBehaviour"; - internal const string UseCompatibilityProcessSniString = @"Switch.Microsoft.Data.SqlClient.UseCompatibilityProcessSni"; - internal const string UseCompatibilityAsyncBehaviourString = @"Switch.Microsoft.Data.SqlClient.UseCompatibilityAsyncBehaviour"; internal const string UseConnectionPoolV2String = @"Switch.Microsoft.Data.SqlClient.UseConnectionPoolV2"; + private const string IgnoreServerProvidedFailoverPartnerString = @"Switch.Microsoft.Data.SqlClient.IgnoreServerProvidedFailoverPartner"; // this field is accessed through reflection in tests and should not be renamed or have the type changed without refactoring NullRow related tests private static Tristate s_legacyRowVersionNullBehavior; @@ -31,9 +30,9 @@ private enum Tristate : byte private static Tristate s_useMinimumLoginTimeout; // this field is accessed through reflection in Microsoft.Data.SqlClient.Tests.SqlParameterTests and should not be renamed or have the type changed without refactoring related tests private static Tristate s_legacyVarTimeZeroScaleBehaviour; - private static Tristate s_useCompatibilityProcessSni; - private static Tristate s_useCompatibilityAsyncBehaviour; private static Tristate s_useConnectionPoolV2; + private static Tristate s_ignoreServerProvidedFailoverPartner; + #if NET static LocalAppContextSwitches() @@ -89,66 +88,6 @@ public static bool DisableTnirByDefault } } #endif - /// - /// In TdsParser the ProcessSni function changed significantly when the packet - /// multiplexing code needed for high speed multi-packet column values was added. - /// In case of compatibility problems this switch will change TdsParser to use - /// the previous version of the function. - /// - public static bool UseCompatibilityProcessSni - { - get - { - if (s_useCompatibilityProcessSni == Tristate.NotInitialized) - { - if (AppContext.TryGetSwitch(UseCompatibilityProcessSniString, out bool returnedValue) && returnedValue) - { - s_useCompatibilityProcessSni = Tristate.True; - } - else - { - s_useCompatibilityProcessSni = Tristate.False; - } - } - return s_useCompatibilityProcessSni == Tristate.True; - } - } - - /// - /// In TdsParser the async multi-packet column value fetch behaviour is capable of - /// using a continue snapshot state in addition to the original replay from start - /// logic. - /// This switch disables use of the continue snapshot state. This switch will always - /// return true if is enabled because the - /// continue state is not stable without the multiplexer. - /// - public static bool UseCompatibilityAsyncBehaviour - { - get - { - if (UseCompatibilityProcessSni) - { - // If ProcessSni compatibility mode has been enabled then the packet - // multiplexer has been disabled. The new async behaviour using continue - // point capture is only stable if the multiplexer is enabled so we must - // return true to enable compatibility async behaviour using only restarts. - return true; - } - - if (s_useCompatibilityAsyncBehaviour == Tristate.NotInitialized) - { - if (AppContext.TryGetSwitch(UseCompatibilityAsyncBehaviourString, out bool returnedValue) && returnedValue) - { - s_useCompatibilityAsyncBehaviour = Tristate.True; - } - else - { - s_useCompatibilityAsyncBehaviour = Tristate.False; - } - } - return s_useCompatibilityAsyncBehaviour == Tristate.True; - } - } /// /// When using Encrypt=false in the connection string, a security warning is output to the console if the TLS version is 1.2 or lower. @@ -251,7 +190,7 @@ public static bool UseMinimumLoginTimeout /// When set to 'true' this will output a scale value of 7 (DEFAULT_VARTIME_SCALE) when the scale /// is explicitly set to zero for VarTime data types ('datetime2', 'datetimeoffset' and 'time') /// If no scale is set explicitly it will continue to output scale of 7 (DEFAULT_VARTIME_SCALE) - /// regardsless of switch value. + /// regardless of switch value. /// This app context switch defaults to 'true'. /// public static bool LegacyVarTimeZeroScaleBehaviour @@ -296,5 +235,33 @@ public static bool UseConnectionPoolV2 return s_useConnectionPoolV2 == Tristate.True; } } + + /// + /// When set to true, the failover partner provided by the server during connection + /// will be ignored. This is useful in scenarios where the application wants to + /// control the failover behavior explicitly (e.g. using a custom port). The application + /// must be kept up to date with the failover configuration of the server. + /// The application will not automatically discover a newly configured failover partner. + /// + /// This app context switch defaults to 'false'. + /// + public static bool IgnoreServerProvidedFailoverPartner + { + get + { + if (s_ignoreServerProvidedFailoverPartner == Tristate.NotInitialized) + { + if (AppContext.TryGetSwitch(IgnoreServerProvidedFailoverPartnerString, out bool returnedValue) && returnedValue) + { + s_ignoreServerProvidedFailoverPartner = Tristate.True; + } + else + { + s_ignoreServerProvidedFailoverPartner = Tristate.False; + } + } + return s_ignoreServerProvidedFailoverPartner == Tristate.True; + } + } } } diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ManagedSni/ResolvedServerSpn.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ManagedSni/ResolvedServerSpn.cs new file mode 100644 index 0000000000..fe49c77f6b --- /dev/null +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ManagedSni/ResolvedServerSpn.cs @@ -0,0 +1,43 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +#nullable enable + +namespace Microsoft.Data.SqlClient.ManagedSni +{ + /// + /// This is used to hold the ServerSpn for a given connection. Most connection types have a single format, although TCP connections may allow + /// with and without a port. Depending on how the SPN is registered on the server, either one may be the correct name. + /// + /// + /// + /// + /// + /// SQL Server SPN format follows these patterns: + /// + /// + /// Default instance, no port (primary): + /// MSSQLSvc/fully-qualified-domain-name + /// + /// + /// Default instance, default port (secondary): + /// MSSQLSvc/fully-qualified-domain-name:1433 + /// + /// + /// Named instance or custom port: + /// MSSQLSvc/fully-qualified-domain-name:port_or_instance_name + /// + /// + /// For TCP connections to named instances, the port number is used in SPN. + /// For Named Pipe connections to named instances, the instance name is used in SPN. + /// When hostname resolution fails, the user-provided hostname is used instead of FQDN. + /// For default instances with TCP protocol, both forms (with and without port) may be returned. + /// + internal readonly struct ResolvedServerSpn(string primary, string? secondary = null) + { + public string Primary => primary; + + public string? Secondary => secondary; + } +} diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ManagedSni/SniCommon.netcore.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ManagedSni/SniCommon.netcore.cs index 6aaf23f877..9653c94d34 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ManagedSni/SniCommon.netcore.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ManagedSni/SniCommon.netcore.cs @@ -189,7 +189,7 @@ internal static IPAddress[] GetDnsIpAddresses(string serverName) /// SNI error code /// Error message /// - internal static uint ReportSNIError(SniProviders provider, uint nativeError, uint sniError, string errorMessage) + internal static uint ReportSNIError(SniProviders provider, int nativeError, uint sniError, string errorMessage) { SqlClientEventSource.Log.TrySNITraceEvent(nameof(SniCommon), EventType.ERR, "Provider = {0}, native Error = {1}, SNI Error = {2}, Error Message = {3}", args0: provider, args1: nativeError, args2: sniError, args3: errorMessage); return ReportSNIError(new SniError(provider, nativeError, sniError, errorMessage)); @@ -203,7 +203,7 @@ internal static uint ReportSNIError(SniProviders provider, uint nativeError, uin /// SNI Exception /// Native SNI error code /// - internal static uint ReportSNIError(SniProviders provider, uint sniError, Exception sniException, uint nativeErrorCode = 0) + internal static uint ReportSNIError(SniProviders provider, uint sniError, Exception sniException, int nativeErrorCode = 0) { SqlClientEventSource.Log.TrySNITraceEvent(nameof(SniCommon), EventType.ERR, "Provider = {0}, SNI Error = {1}, Exception = {2}", args0: provider, args1: sniError, args2: sniException?.Message); return ReportSNIError(new SniError(provider, sniError, sniException, nativeErrorCode)); diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ManagedSni/SniError.netcore.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ManagedSni/SniError.netcore.cs index 1a33de1d96..9cecb1e70f 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ManagedSni/SniError.netcore.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ManagedSni/SniError.netcore.cs @@ -5,6 +5,8 @@ #if NET using System; +using System.ComponentModel; +using System.Net.Sockets; namespace Microsoft.Data.SqlClient.ManagedSni { @@ -14,17 +16,18 @@ namespace Microsoft.Data.SqlClient.ManagedSni internal class SniError { // Error numbers from native SNI implementation - internal const uint CertificateValidationErrorCode = 2148074277; + // This is signed int representation of the error code 0x80090325 + internal const int CertificateValidationErrorCode = -2146893019; public readonly SniProviders provider; public readonly string errorMessage; - public readonly uint nativeError; + public readonly int nativeError; public readonly uint sniError; public readonly string function; public readonly uint lineNumber; public readonly Exception exception; - public SniError(SniProviders provider, uint nativeError, uint sniErrorCode, string errorMessage) + public SniError(SniProviders provider, int nativeError, uint sniErrorCode, string errorMessage) { lineNumber = 0; function = string.Empty; @@ -35,12 +38,25 @@ public SniError(SniProviders provider, uint nativeError, uint sniErrorCode, stri exception = null; } - public SniError(SniProviders provider, uint sniErrorCode, Exception sniException, uint nativeErrorCode = 0) + public SniError(SniProviders provider, uint sniErrorCode, Exception sniException, int nativeErrorCode = 0) { lineNumber = 0; function = string.Empty; this.provider = provider; nativeError = nativeErrorCode; + if (nativeErrorCode == 0) + { + if (sniException is SocketException socketException) + { + // SocketErrorCode values are cross-plat consistent in .NET (matching native Windows error codes) + // underlying type of SocketErrorCode is int + nativeError = (int)socketException.SocketErrorCode; + } + else if (sniException is Win32Exception win32Exception) + { + nativeError = win32Exception.NativeErrorCode; // Replicates native SNI behavior + } + } sniError = sniErrorCode; errorMessage = string.Empty; exception = sniException; diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ManagedSni/SniNpHandle.netcore.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ManagedSni/SniNpHandle.netcore.cs index e244209f23..7a1c2ec660 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ManagedSni/SniNpHandle.netcore.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ManagedSni/SniNpHandle.netcore.cs @@ -203,7 +203,7 @@ public override uint Receive(out SniPacket packet, int timeout) packet = null; var e = new Win32Exception(); SqlClientEventSource.Log.TrySNITraceEvent(nameof(SniNpHandle), EventType.ERR, "Connection Id {0}, Packet length found 0, Win32 exception raised: {1}", args0: _connectionId, args1: e?.Message); - return ReportErrorAndReleasePacket(errorPacket, (uint)e.NativeErrorCode, 0, e.Message); + return ReportErrorAndReleasePacket(errorPacket, e.NativeErrorCode, 0, e.Message); } } catch (ObjectDisposedException ode) @@ -413,7 +413,7 @@ private uint ReportErrorAndReleasePacket(SniPacket packet, Exception sniExceptio return SniCommon.ReportSNIError(SniProviders.NP_PROV, SniCommon.InternalExceptionError, sniException); } - private uint ReportErrorAndReleasePacket(SniPacket packet, uint nativeError, uint sniError, string errorMessage) + private uint ReportErrorAndReleasePacket(SniPacket packet, int nativeError, uint sniError, string errorMessage) { if (packet != null) { diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ManagedSni/SniProxy.netcore.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ManagedSni/SniProxy.netcore.cs index c09e5a7b27..817ad1f4f2 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ManagedSni/SniProxy.netcore.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ManagedSni/SniProxy.netcore.cs @@ -33,7 +33,7 @@ internal class SniProxy /// Full server name from connection string /// Timer expiration /// Instance name - /// SPNs + /// SPN /// pre-defined SPN /// Flush packet cache /// Asynchronous connection @@ -50,7 +50,7 @@ internal static SniHandle CreateConnectionHandle( string fullServerName, TimeoutTimer timeout, out byte[] instanceName, - ref string[] spns, + out ResolvedServerSpn resolvedSpn, string serverSPN, bool flushCache, bool async, @@ -64,6 +64,7 @@ internal static SniHandle CreateConnectionHandle( string serverCertificateFilename) { instanceName = new byte[1]; + resolvedSpn = default; bool errorWithLocalDBProcessing; string localDBDataSource = GetLocalDBDataSource(fullServerName, out errorWithLocalDBProcessing); @@ -102,7 +103,7 @@ internal static SniHandle CreateConnectionHandle( { try { - spns = GetSqlServerSPNs(details, serverSPN); + resolvedSpn = GetSqlServerSPNs(details, serverSPN); } catch (Exception e) { @@ -114,12 +115,12 @@ internal static SniHandle CreateConnectionHandle( return sniHandle; } - private static string[] GetSqlServerSPNs(DataSource dataSource, string serverSPN) + private static ResolvedServerSpn GetSqlServerSPNs(DataSource dataSource, string serverSPN) { Debug.Assert(!string.IsNullOrWhiteSpace(dataSource.ServerName)); if (!string.IsNullOrWhiteSpace(serverSPN)) { - return new[] { serverSPN }; + return new(serverSPN); } string hostName = dataSource.ServerName; @@ -137,7 +138,7 @@ private static string[] GetSqlServerSPNs(DataSource dataSource, string serverSPN return GetSqlServerSPNs(hostName, postfix, dataSource.ResolvedProtocol); } - private static string[] GetSqlServerSPNs(string hostNameOrAddress, string portOrInstanceName, DataSource.Protocol protocol) + private static ResolvedServerSpn GetSqlServerSPNs(string hostNameOrAddress, string portOrInstanceName, DataSource.Protocol protocol) { Debug.Assert(!string.IsNullOrWhiteSpace(hostNameOrAddress)); IPHostEntry hostEntry = null; @@ -168,12 +169,12 @@ private static string[] GetSqlServerSPNs(string hostNameOrAddress, string portOr string serverSpnWithDefaultPort = serverSpn + $":{DefaultSqlServerPort}"; // Set both SPNs with and without Port as Port is optional for default instance SqlClientEventSource.Log.TryAdvancedTraceEvent("SNIProxy.GetSqlServerSPN | Info | ServerSPNs {0} and {1}", serverSpn, serverSpnWithDefaultPort); - return new[] { serverSpn, serverSpnWithDefaultPort }; + return new(serverSpn, serverSpnWithDefaultPort); } // else Named Pipes do not need to valid port SqlClientEventSource.Log.TryAdvancedTraceEvent("SNIProxy.GetSqlServerSPN | Info | ServerSPN {0}", serverSpn); - return new[] { serverSpn }; + return new(serverSpn); } /// diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ManagedSni/SniTcpHandle.netcore.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ManagedSni/SniTcpHandle.netcore.cs index 656060beeb..8435a3d3d5 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ManagedSni/SniTcpHandle.netcore.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ManagedSni/SniTcpHandle.netcore.cs @@ -375,6 +375,8 @@ private static Socket Connect(string serverName, int port, TimeoutTimer timeout, IEnumerable ipAddresses = GetHostAddressesSortedByPreference(serverName, ipPreference); + SocketException lastSocketException = null; + foreach (IPAddress ipAddress in ipAddresses) { bool isSocketSelected = false; @@ -426,7 +428,9 @@ private static Socket Connect(string serverName, int port, TimeoutTimer timeout, { if (timeout.IsExpired) { - return null; + throw new Win32Exception( + TdsEnums.SNI_WAIT_TIMEOUT, + StringsHelper.GetString(Strings.SQL_ConnectTimeout)); } int socketSelectTimeout = @@ -442,10 +446,24 @@ private static Socket Connect(string serverName, int port, TimeoutTimer timeout, Socket.Select(checkReadLst, checkWriteLst, checkErrorLst, socketSelectTimeout); // nothing selected means timeout + SqlClientEventSource.Log.TrySNITraceEvent(nameof(SniTcpHandle), EventType.INFO, + "Socket.Select results: checkReadLst.Count: {0}, checkWriteLst.Count: {1}, checkErrorLst.Count: {2}", + checkReadLst.Count, checkWriteLst.Count, checkErrorLst.Count); } while (checkReadLst.Count == 0 && checkWriteLst.Count == 0 && checkErrorLst.Count == 0); // workaround: false positive socket.Connected on linux: https://github.com/dotnet/runtime/issues/55538 isConnected = socket.Connected && checkErrorLst.Count == 0; + if (!isConnected) + { + // Retrieve the socket error code + int socketErrorCode = (int)socket.GetSocketOption(SocketOptionLevel.Socket, SocketOptionName.Error); + SocketError socketError = (SocketError)socketErrorCode; + + SqlClientEventSource.Log.TrySNITraceEvent(nameof(SniTcpHandle), EventType.ERR, + "Socket connection failed. SocketError: {0} ({1})", socketError, socketErrorCode); + + lastSocketException = new SocketException(socketErrorCode); + } } if (isConnected) @@ -463,6 +481,8 @@ private static Socket Connect(string serverName, int port, TimeoutTimer timeout, } pendingDNSInfo = new SQLDNSInfo(cachedFQDN, iPv4String, iPv6String, port.ToString()); isSocketSelected = true; + SqlClientEventSource.Log.TrySNITraceEvent(nameof(SniTcpHandle), EventType.INFO, + "Connected to socket: {0}", socket.RemoteEndPoint); return socket; } } @@ -471,6 +491,7 @@ private static Socket Connect(string serverName, int port, TimeoutTimer timeout, SqlClientEventSource.Log.TryAdvancedTraceEvent( "{0}.{1}{2}THIS EXCEPTION IS BEING SWALLOWED: {3}", nameof(SniTcpHandle), nameof(Connect), EventType.ERR, e); + lastSocketException = e; } finally { @@ -479,6 +500,14 @@ private static Socket Connect(string serverName, int port, TimeoutTimer timeout, } } + if (lastSocketException != null) + { + SqlClientEventSource.Log.TryAdvancedTraceEvent( + "{0}.{1}{2}Last Socket Exception: {3}", + nameof(SniTcpHandle), nameof(Connect), EventType.ERR, lastSocketException); + throw lastSocketException; + } + return null; } } @@ -574,6 +603,20 @@ private static Socket ParallelConnect(IPAddress[] serverAddresses, int port, Tim Socket.Select(checkReadLst, checkWriteLst, checkErrorLst, socketSelectTimeout); // nothing selected means select timed out } while (checkReadLst.Count == 0 && checkWriteLst.Count == 0 && checkErrorLst.Count == 0 && !timeout.IsExpired); + foreach (Socket socket in checkErrorLst) + { + // Retrieve the socket error code + int socketErrorCode = (int)socket.GetSocketOption(SocketOptionLevel.Socket, SocketOptionName.Error); + SocketError socketError = (SocketError)socketErrorCode; + + // Log any failed sockets + SqlClientEventSource.Log.TrySNITraceEvent(nameof(SniTcpHandle), EventType.INFO, + "Socket connection failed for {0}. SocketError: {1} ({2})", + sockets[socket], socketError, socketErrorCode); + + lastError = new SocketException(socketErrorCode); + } + } catch (SocketException e) { @@ -588,6 +631,7 @@ private static Socket ParallelConnect(IPAddress[] serverAddresses, int port, Tim { SqlClientEventSource.Log.TryAdvancedTraceEvent( "{0}.{1}{2}ParallelConnect timeout expired.", nameof(SniTcpHandle), nameof(ParallelConnect), EventType.INFO); + // We will throw below after cleanup break; } @@ -654,9 +698,21 @@ private static Socket ParallelConnect(IPAddress[] serverAddresses, int port, Tim if (connectedSocket == null) { + if (timeout.IsExpired) + { + throw new Win32Exception( + TdsEnums.SNI_WAIT_TIMEOUT, + StringsHelper.GetString(Strings.SQL_ConnectTimeout)); + } + SqlClientEventSource.Log.TryAdvancedTraceEvent( - "{0}.{1}{2}No socket connections succeeded. Last error: {3}", + "{0}.{1}{2} No socket connections succeeded. Last error: {3}", nameof(SniTcpHandle), nameof(ParallelConnect), EventType.ERR, lastError); + + if (lastError != null) + { + throw lastError; + } } return connectedSocket; @@ -861,7 +917,7 @@ public override uint Receive(out SniPacket packet, int timeoutInMilliseconds) packet = null; var e = new Win32Exception(); SqlClientEventSource.Log.TrySNITraceEvent(nameof(SniTcpHandle), EventType.ERR, "Connection Id {0}, Win32 exception occurred: {1}", args0: _connectionId, args1: e?.Message); - return ReportErrorAndReleasePacket(errorPacket, (uint)e.NativeErrorCode, 0, e.Message); + return ReportErrorAndReleasePacket(errorPacket, e.NativeErrorCode, 0, e.Message); } SqlClientEventSource.Log.TrySNITraceEvent(nameof(SniTcpHandle), EventType.INFO, "Connection Id {0}, Data read from stream synchronously", args0: _connectionId); @@ -897,9 +953,30 @@ public override uint Receive(out SniPacket packet, int timeoutInMilliseconds) } finally { - // Reset the socket timeout to Timeout.Infinite after the receive operation is done - // to avoid blocking the thread in case of a timeout error. - _socket.ReceiveTimeout = Timeout.Infinite; + const int resetTimeout = Timeout.Infinite; + + try + { + // Reset the socket timeout to Timeout.Infinite after + // the receive operation is done to avoid blocking the + // thread in case of a timeout error. + _socket.ReceiveTimeout = resetTimeout; + + } + catch (SocketException ex) + { + // We sometimes see setting the ReceiveTimeout fail + // on macOS. There's isn't much we can do about it + // though, so just log and move on. + SqlClientEventSource.Log.TrySNITraceEvent( + nameof(SniTcpHandle), + EventType.ERR, + "Connection Id {0}, Failed to reset socket " + + "receive timeout to {1}: {2}", + _connectionId, + resetTimeout, + ex.Message); + } } } } @@ -992,13 +1069,13 @@ public override uint CheckConnection() return TdsEnums.SNI_SUCCESS; } - private uint ReportTcpSNIError(Exception sniException, uint nativeErrorCode = 0) + private uint ReportTcpSNIError(Exception sniException, int nativeErrorCode = 0) { _status = TdsEnums.SNI_ERROR; return SniCommon.ReportSNIError(SniProviders.TCP_PROV, SniCommon.InternalExceptionError, sniException, nativeErrorCode); } - private uint ReportTcpSNIError(uint nativeError, uint sniError, string errorMessage) + private uint ReportTcpSNIError(int nativeError, uint sniError, string errorMessage) { _status = TdsEnums.SNI_ERROR; return SniCommon.ReportSNIError(SniProviders.TCP_PROV, nativeError, sniError, errorMessage); @@ -1013,7 +1090,7 @@ private uint ReportErrorAndReleasePacket(SniPacket packet, Exception sniExceptio return ReportTcpSNIError(sniException); } - private uint ReportErrorAndReleasePacket(SniPacket packet, uint nativeError, uint sniError, string errorMessage) + private uint ReportErrorAndReleasePacket(SniPacket packet, int nativeError, uint sniError, string errorMessage) { if (packet != null) { diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Packet.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Packet.cs deleted file mode 100644 index b81270bf08..0000000000 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Packet.cs +++ /dev/null @@ -1,189 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -using System; -using System.Diagnostics; -using System.Runtime.CompilerServices; - -namespace Microsoft.Data.SqlClient -{ - /// - /// Contains a buffer for a partial or full packet and methods to get information about the status of - /// the packet that the buffer represents.
- /// This class is used to contain partial packet data and helps ensure that the packet data is completely - /// received before a full packet is made available to the rest of the library - ///
- internal sealed partial class Packet - { - public const int UnknownDataLength = -1; - - private int _dataLength; - private int _totalLength; - private byte[] _buffer; - - public Packet() - { - _dataLength = UnknownDataLength; - } - - /// - /// If the packet data has enough bytes available to determine the amount of data that should be present - /// in the packet then this property will be set to the count of data bytes in the packet,
- /// otherwise this will be -1 - ///
- public int DataLength - { - get - { - CheckDisposed(); - return _dataLength; - } - set - { - CheckDisposed(); - _dataLength = value; - } - } - - /// - /// A byte array containing bytes of data - /// - public byte[] Buffer - { - get - { - CheckDisposed(); - return _buffer; - } - set - { - CheckDisposed(); - _buffer = value; - } - } - - /// - /// The total count of bytes currently in the array including the tds header bytes - /// - public int CurrentLength - { - get - { - CheckDisposed(); - return _totalLength; - } - set - { - CheckDisposed(); - _totalLength = value; - } - } - - /// - /// If the packet data has enough bytes available to determine the length amount of data that should be present - /// in the packet then this property will return the count of data bytes that are expected to be in the packet.
- /// If there are not enough bytes to determine the data byte count then this property will throw an exception.
- ///
- /// Call to check if there will be a value before using this property. - ///
- public int RequiredLength - { - get - { - CheckDisposed(); - if (!HasDataLength) - { - throw new InvalidOperationException($"cannot get {nameof(RequiredLength)} when {nameof(HasDataLength)} is false"); - } - return TdsEnums.HEADER_LEN + _dataLength; - } - } - - /// - /// returns a boolean value indicating if there are enough total bytes available in the to read the tds header - /// - public bool HasHeader => _totalLength >= TdsEnums.HEADER_LEN; - - /// - /// returns a boolean value indicating if the value has been set. - /// - public bool HasDataLength => _dataLength >= 0; - - /// - /// returns a boolean value indicating whether the contains enough - /// data for a valid tds header, has a set and that the - /// is greater than or equal to the + tds header length.
- ///
- public bool ContainsCompletePacket => _dataLength != UnknownDataLength && (TdsEnums.HEADER_LEN + _dataLength) <= _totalLength; - - /// - /// returns a containing the first 8 bytes of the array which will - /// contain the TDS header bytes. This can be passed to static functions on to extract information from the - /// tds packet header.
- /// Call before using this function. - ///
- /// - public ReadOnlySpan GetHeaderSpan() => _buffer.AsSpan(0, TdsEnums.HEADER_LEN); - - [Conditional("DEBUG")] - internal void CheckDisposed() => CheckDisposedImpl(); - - [Conditional("DEBUG")] - internal void SetCreatedBy(int creator) => SetCreatedByImpl(creator); - - partial void SetCreatedByImpl(int creator); - - partial void CheckDisposedImpl(); - - public static void ThrowDisposed() - { - throw new ObjectDisposedException(nameof(Packet)); - } - - internal static byte GetStatusFromHeader(ReadOnlySpan header) => header[1]; - - internal static int GetDataLengthFromHeader(ReadOnlySpan header) - { - return (header[TdsEnums.HEADER_LEN_FIELD_OFFSET] << 8 | header[TdsEnums.HEADER_LEN_FIELD_OFFSET + 1]) - TdsEnums.HEADER_LEN; - } - internal static int GetSpidFromHeader(ReadOnlySpan header) - { - return (header[TdsEnums.SPID_OFFSET] << 8 | header[TdsEnums.SPID_OFFSET + 1]); - } - internal static int GetIDFromHeader(ReadOnlySpan header) - { - return header[TdsEnums.HEADER_LEN_FIELD_OFFSET + 4]; - } - - internal static int GetDataLengthFromHeader(Packet packet) => GetDataLengthFromHeader(packet.GetHeaderSpan()); - - internal static bool GetIsEOMFromHeader(ReadOnlySpan header) => GetStatusFromHeader(header) == 1; - } - -#if DEBUG - internal sealed partial class Packet - { - private int _createdBy; - private bool _disposed; - - public int CreatedBy => _createdBy; - - [Conditional("DEBUG")] - public void Dispose() - { - _disposed = true; - } - - partial void SetCreatedByImpl(int creator) => _createdBy = creator; - - partial void CheckDisposedImpl() - { - if (_disposed) - { - ThrowDisposed(); - } - } - } -#endif -} diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SSPI/NegotiateSspiContextProvider.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SSPI/NegotiateSspiContextProvider.cs index 5dc52010b3..a74651cf2d 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SSPI/NegotiateSspiContextProvider.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SSPI/NegotiateSspiContextProvider.cs @@ -2,26 +2,25 @@ using System; using System.Buffers; +using System.Diagnostics; using System.Net.Security; #nullable enable namespace Microsoft.Data.SqlClient { - internal sealed class NegotiateSspiContextProvider : SspiContextProvider + internal sealed class NegotiateSspiContextProvider : SspiContextProvider, IDisposable { - private NegotiateAuthentication? _negotiateAuth = null; + private NegotiateAuthentication? _negotiateAuth; protected override bool GenerateSspiClientContext(ReadOnlySpan incomingBlob, IBufferWriter outgoingBlobWriter, SspiAuthenticationParameters authParams) { - NegotiateAuthenticationStatusCode statusCode = NegotiateAuthenticationStatusCode.UnknownCredentials; - - _negotiateAuth ??= new(new NegotiateAuthenticationClientOptions { Package = "Negotiate", TargetName = authParams.Resource }); - var sendBuff = _negotiateAuth.GetOutgoingBlob(incomingBlob, out statusCode)!; + var negotiateAuth = GetNegotiateAuthenticationForParams(authParams); + var sendBuff = negotiateAuth.GetOutgoingBlob(incomingBlob, out var statusCode)!; // Log session id, status code and the actual SPN used in the negotiation SqlClientEventSource.Log.TryTraceEvent("{0}.{1} | Info | Session Id {2}, StatusCode={3}, SPN={4}", nameof(NegotiateSspiContextProvider), - nameof(GenerateSspiClientContext), _physicalStateObj.SessionId, statusCode, _negotiateAuth.TargetName); + nameof(GenerateSspiClientContext), _physicalStateObj.SessionId, statusCode, negotiateAuth.TargetName); if (statusCode == NegotiateAuthenticationStatusCode.Completed || statusCode == NegotiateAuthenticationStatusCode.ContinueNeeded) { @@ -31,6 +30,27 @@ protected override bool GenerateSspiClientContext(ReadOnlySpan incomingBlo return false; } + + public void Dispose() + { + _negotiateAuth?.Dispose(); + } + + private NegotiateAuthentication GetNegotiateAuthenticationForParams(SspiAuthenticationParameters authParams) + { + if (_negotiateAuth is { }) + { + if (string.Equals(_negotiateAuth.TargetName, authParams.Resource, StringComparison.Ordinal)) + { + return _negotiateAuth; + } + + // Dispose of it since we're not going to use it now + _negotiateAuth.Dispose(); + } + + return _negotiateAuth = new(new NegotiateAuthenticationClientOptions { Package = "Negotiate", TargetName = authParams.Resource }); + } } } #endif diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SSPI/SspiContextProvider.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SSPI/SspiContextProvider.cs index ff83422f10..f45ccee4fd 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SSPI/SspiContextProvider.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SSPI/SspiContextProvider.cs @@ -10,15 +10,48 @@ internal abstract class SspiContextProvider { private TdsParser _parser = null!; private ServerInfo _serverInfo = null!; + + private SspiAuthenticationParameters? _primaryAuthParams; + private SspiAuthenticationParameters? _secondaryAuthParams; + private protected TdsParserStateObject _physicalStateObj = null!; - internal void Initialize(ServerInfo serverInfo, TdsParserStateObject physicalStateObj, TdsParser parser) +#if NET + /// + /// for details as to what and means and why there are two. + /// +#endif + internal void Initialize( + ServerInfo serverInfo, + TdsParserStateObject physicalStateObj, + TdsParser parser, + string primaryServerSpn, + string? secondaryServerSpn = null + ) { _parser = parser; _physicalStateObj = physicalStateObj; _serverInfo = serverInfo; + var options = parser.Connection.ConnectionOptions; + + SqlClientEventSource.Log.StateDumpEvent(" Initializing provider {0} with SPN={1} and alternate={2}", GetType().FullName, primaryServerSpn, secondaryServerSpn); + + _primaryAuthParams = CreateAuthParams(options, primaryServerSpn); + + if (secondaryServerSpn is { }) + { + _secondaryAuthParams = CreateAuthParams(options, secondaryServerSpn); + } + Initialize(); + + static SspiAuthenticationParameters CreateAuthParams(SqlConnectionString connString, string serverSpn) => new(connString.DataSource, serverSpn) + { + DatabaseName = connString.InitialCatalog, + UserId = connString.UserID, + Password = connString.Password, + }; } private protected virtual void Initialize() @@ -27,46 +60,41 @@ private protected virtual void Initialize() protected abstract bool GenerateSspiClientContext(ReadOnlySpan incomingBlob, IBufferWriter outgoingBlobWriter, SspiAuthenticationParameters authParams); - internal void SSPIData(ReadOnlySpan receivedBuff, IBufferWriter outgoingBlobWriter, string serverSpn) + internal void WriteSSPIContext(ReadOnlySpan receivedBuff, IBufferWriter outgoingBlobWriter) { using var _ = TrySNIEventScope.Create(nameof(SspiContextProvider)); - if (!RunGenerateSspiClientContext(receivedBuff, outgoingBlobWriter, serverSpn)) + if (_primaryAuthParams is { }) { - // If we've hit here, the SSPI context provider implementation failed to generate the SSPI context. - SSPIError(SQLMessage.SSPIGenerateError(), TdsEnums.GEN_CLIENT_CONTEXT); - } - } + if (RunGenerateSspiClientContext(receivedBuff, outgoingBlobWriter, _primaryAuthParams)) + { + return; + } - internal void SSPIData(ReadOnlySpan receivedBuff, IBufferWriter outgoingBlobWriter, ReadOnlySpan serverSpns) - { - using var _ = TrySNIEventScope.Create(nameof(SspiContextProvider)); + // remove _primaryAuth from future attempts as it failed + _primaryAuthParams = null; + } - foreach (var serverSpn in serverSpns) + if (_secondaryAuthParams is { }) { - if (RunGenerateSspiClientContext(receivedBuff, outgoingBlobWriter, serverSpn)) + if (RunGenerateSspiClientContext(receivedBuff, outgoingBlobWriter, _secondaryAuthParams)) { return; } + + // remove _secondaryAuthParams from future attempts as it failed + _secondaryAuthParams = null; } // If we've hit here, the SSPI context provider implementation failed to generate the SSPI context. SSPIError(SQLMessage.SSPIGenerateError(), TdsEnums.GEN_CLIENT_CONTEXT); } - private bool RunGenerateSspiClientContext(ReadOnlySpan incomingBlob, IBufferWriter outgoingBlobWriter, string serverSpn) + private bool RunGenerateSspiClientContext(ReadOnlySpan incomingBlob, IBufferWriter outgoingBlobWriter, SspiAuthenticationParameters authParams) { - var options = _parser.Connection.ConnectionOptions; - var authParams = new SspiAuthenticationParameters(options.DataSource, serverSpn) - { - DatabaseName = options.InitialCatalog, - UserId = options.UserID, - Password = options.Password, - }; - try { - SqlClientEventSource.Log.TryTraceEvent("{0}.{1} | Info | SPN={1}", GetType().FullName, nameof(GenerateSspiClientContext), serverSpn); + SqlClientEventSource.Log.TryTraceEvent("{0}.{1} | Info | SPN={1}", GetType().FullName, nameof(GenerateSspiClientContext), authParams.Resource); return GenerateSspiClientContext(incomingBlob, outgoingBlobWriter, authParams); } diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlAuthenticationProviderManager.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlAuthenticationProviderManager.cs index e29746dc6c..1fe587720c 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlAuthenticationProviderManager.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlAuthenticationProviderManager.cs @@ -186,7 +186,7 @@ public bool SetProvider(SqlAuthenticationMethod authenticationMethod, SqlAuthent if (candidateMethod == authenticationMethod) { _sqlAuthLogger.LogError(nameof(SqlAuthenticationProviderManager), methodName, $"Failed to add provider {GetProviderType(provider)} because a user-defined provider with type {GetProviderType(_providers[authenticationMethod])} already existed for authentication {authenticationMethod}."); - break; + return false; // return here to avoid replacing user-defined provider } } } @@ -206,9 +206,18 @@ public bool SetProvider(SqlAuthenticationMethod authenticationMethod, SqlAuthent return true; } + /// + /// Fetches provided configuration section from app.config file. + /// Does not support reading from appsettings.json yet. + /// + /// + /// + /// private static T FetchConfigurationSection(string name) { Type t = typeof(T); + + // TODO: Support reading configuration from appsettings.json for .NET runtime applications. object section = ConfigurationManager.GetSection(name); if (section != null) { diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlBuffer.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlBuffer.cs index 39d2758d62..bf27581b51 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlBuffer.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlBuffer.cs @@ -993,7 +993,7 @@ internal SqlVector GetSqlVector() where T : unmanaged { if (IsNull) { - return new SqlVector(_value._vectorInfo._elementCount); + return SqlVector.CreateNull(_value._vectorInfo._elementCount); } return new SqlVector(SqlBinary.Value); } diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCachedBuffer.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCachedBuffer.cs index 82ef37fb6b..2b656501a5 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCachedBuffer.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCachedBuffer.cs @@ -37,24 +37,10 @@ private SqlCachedBuffer(List cachedBytes) ///
internal static TdsOperationStatus TryCreate(SqlMetaDataPriv metadata, TdsParser parser, TdsParserStateObject stateObj, out SqlCachedBuffer buffer) { - buffer = null; - - (bool canContinue, bool isStarting, _) = stateObj.GetSnapshotStatuses(); + byte[] byteArr; - List cachedBytes = null; - if (canContinue) - { - cachedBytes = stateObj.TryTakeSnapshotStorage() as List; - if (isStarting) - { - cachedBytes = null; - } - } - - if (cachedBytes == null) - { - cachedBytes = new List(); - } + List cachedBytes = new(); + buffer = null; // the very first length is already read. TdsOperationStatus result = parser.TryPlpBytesLeft(stateObj, out ulong plplength); @@ -73,25 +59,13 @@ internal static TdsOperationStatus TryCreate(SqlMetaDataPriv metadata, TdsParser } do { - bool returnAfterAdd = false; int cb = (plplength > (ulong)MaxChunkSize) ? MaxChunkSize : (int)plplength; - byte[] byteArr = new byte[cb]; - // pass false for the writeDataSizeToSnapshot parameter because we want to only take data - // from the current packet and not try to do a continue-capable multi packet read - result = stateObj.TryReadPlpBytes(ref byteArr, 0, cb, out cb, canContinue, writeDataSizeToSnapshot: false, compatibilityMode: false); + byteArr = new byte[cb]; + result = stateObj.TryReadPlpBytes(ref byteArr, 0, cb, out cb); if (result != TdsOperationStatus.Done) { - if (result == TdsOperationStatus.NeedMoreData && canContinue && cb == byteArr.Length) - { - // succeeded in getting the data but failed to find the next plp length - returnAfterAdd = true; - } - else - { - return result; - } + return result; } - Debug.Assert(cb == byteArr.Length); if (cachedBytes.Count == 0) { @@ -100,13 +74,6 @@ internal static TdsOperationStatus TryCreate(SqlMetaDataPriv metadata, TdsParser } cachedBytes.Add(byteArr); plplength -= (ulong)cb; - - if (returnAfterAdd) - { - stateObj.SetSnapshotStorage(cachedBytes); - return result; - } - } while (plplength > 0); result = parser.TryPlpBytesLeft(stateObj, out plplength); diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlClientEventSource.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlClientEventSource.cs index 90a69b5670..4988a4f4da 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlClientEventSource.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlClientEventSource.cs @@ -15,11 +15,13 @@ namespace Microsoft.Data.SqlClient [EventSource(Name = "Microsoft.Data.SqlClient.EventSource")] internal partial class SqlClientEventSource : EventSource { + private static bool s_initialMetricsEnabled = false; + // Defines the singleton instance for the Resources ETW provider public static readonly SqlClientEventSource Log = new(); // Provides access to metrics. - public static readonly SqlClientMetrics Metrics = new SqlClientMetrics(Log); + public static readonly SqlClientMetrics Metrics = new SqlClientMetrics(Log, s_initialMetricsEnabled); private SqlClientEventSource() { } @@ -33,7 +35,14 @@ protected override void OnEventCommand(EventCommandEventArgs command) if (command.Command == EventCommand.Enable) { - Metrics.EnableEventCounters(); + if (Metrics == null) + { + s_initialMetricsEnabled = true; + } + else + { + Metrics.EnableEventCounters(); + } } } #endif @@ -424,7 +433,7 @@ internal long TryScopeEnterEvent(string className, [System.Runtime.CompilerServi { StringBuilder sb = new StringBuilder(className); sb.Append(".").Append(memberName).Append(" | INFO | SCOPE | Entering Scope {0}"); - return SNIScopeEnter(sb.ToString()); + return ScopeEnter(sb.ToString()); } return 0; } @@ -893,6 +902,12 @@ internal void StateDumpEvent(string message, T0 args0, T1 args1) { StateDump(string.Format(message, args0?.ToString() ?? NullStr, args1?.ToString() ?? NullStr)); } + + [NonEvent] + internal void StateDumpEvent(string message, T0 args0, T1 args1, T2 args2) + { + StateDump(string.Format(message, args0?.ToString() ?? NullStr, args1?.ToString() ?? NullStr, args2?.ToString() ?? NullStr)); + } #endregion #region SNI Trace diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlConnectionString.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlConnectionString.cs index 3816e74e49..b96a946ac7 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlConnectionString.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlConnectionString.cs @@ -67,7 +67,7 @@ internal static class DEFAULT internal const string FailoverPartnerSPN = DbConnectionStringDefaults.FailoverPartnerSPN; internal const bool Context_Connection = DbConnectionStringDefaults.ContextConnection; #if NETFRAMEWORK - internal static readonly bool TransparentNetworkIPResolution = DbConnectionStringDefaults.TransparentNetworkIPResolution; + internal static bool TransparentNetworkIPResolution => DbConnectionStringDefaults.TransparentNetworkIpResolution; internal const bool Connection_Reset = DbConnectionStringDefaults.ConnectionReset; internal const string Network_Library = DbConnectionStringDefaults.NetworkLibrary; #endif // NETFRAMEWORK diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlConnectionStringBuilder.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlConnectionStringBuilder.cs index 6c3e5989f4..7d971cda3e 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlConnectionStringBuilder.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlConnectionStringBuilder.cs @@ -130,7 +130,7 @@ private enum Keywords #if NETFRAMEWORK private bool _connectionReset = DbConnectionStringDefaults.ConnectionReset; - private bool _transparentNetworkIPResolution = DbConnectionStringDefaults.TransparentNetworkIPResolution; + private bool _transparentNetworkIPResolution = DbConnectionStringDefaults.TransparentNetworkIpResolution; private string _networkLibrary = DbConnectionStringDefaults.NetworkLibrary; #endif #endregion //Fields @@ -549,7 +549,7 @@ private void Reset(Keywords index) _connectionReset = DbConnectionStringDefaults.ConnectionReset; break; case Keywords.TransparentNetworkIPResolution: - _transparentNetworkIPResolution = DbConnectionStringDefaults.TransparentNetworkIPResolution; + _transparentNetworkIPResolution = DbConnectionStringDefaults.TransparentNetworkIpResolution; break; case Keywords.NetworkLibrary: _networkLibrary = DbConnectionStringDefaults.NetworkLibrary; diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlDataReader.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlDataReader.cs index d1f3f5c1a5..353dd0346f 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlDataReader.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlDataReader.cs @@ -3365,10 +3365,7 @@ private T GetFieldValueFromSqlBufferInternal(SqlBuffer data, _SqlMetaData met { if (typeof(T) == typeof(string) && metaData.metaType.SqlDbType == SqlDbTypeExtensions.Vector) { - if (data.IsNull) - return (T)(object)data.String; - else - return (T)(object)data.GetSqlVector().GetString(); + return (T)(object)data.String; } // the requested type is likely to be one that isn't supported so try the cast and // unless there is a null value conversion then feedback the cast exception with @@ -4598,12 +4595,7 @@ private TdsOperationStatus TryResetBlobState() #if DEBUG else { - Debug.Assert( - (_sharedState._columnDataBytesRemaining == 0 || _sharedState._columnDataBytesRemaining == -1) - && - (_stateObj._longlen == 0 || _stateObj.IsSnapshotContinuing()), - "Haven't read header yet, but column is partially read?" - ); + Debug.Assert((_sharedState._columnDataBytesRemaining == 0 || _sharedState._columnDataBytesRemaining == -1) && _stateObj._longlen == 0, "Haven't read header yet, but column is partially read?"); } #endif diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlError.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlError.cs index 6390729bad..fc3a1247f3 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlError.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlError.cs @@ -33,16 +33,16 @@ public sealed class SqlError // to find and invoke the functions, changing the signatures will break many // things elsewhere - internal SqlError(int infoNumber, byte errorState, byte errorClass, string server, string errorMessage, string procedure, int lineNumber, uint win32ErrorCode, Exception exception = null) + internal SqlError(int infoNumber, byte errorState, byte errorClass, string server, string errorMessage, string procedure, int lineNumber, int win32ErrorCode, Exception exception = null) : this(infoNumber, errorState, errorClass, server, errorMessage, procedure, lineNumber, win32ErrorCode, exception, -1) { } - internal SqlError(int infoNumber, byte errorState, byte errorClass, string server, string errorMessage, string procedure, int lineNumber, uint win32ErrorCode, Exception exception, int batchIndex) + internal SqlError(int infoNumber, byte errorState, byte errorClass, string server, string errorMessage, string procedure, int lineNumber, int win32ErrorCode, Exception exception, int batchIndex) : this(infoNumber, errorState, errorClass, server, errorMessage, procedure, lineNumber, exception, batchIndex) { _server = server; - _win32ErrorCode = (int)win32ErrorCode; + _win32ErrorCode = win32ErrorCode; } internal SqlError(int infoNumber, byte errorState, byte errorClass, string server, string errorMessage, string procedure, int lineNumber, Exception exception = null) @@ -103,7 +103,7 @@ public override string ToString() /// public int LineNumber => _lineNumber; - internal int Win32ErrorCode => _win32ErrorCode; + internal int Win32ErrorCode => _win32ErrorCode; internal Exception Exception => _exception; diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnection.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnection.cs index 742c8b2865..0ea6c7c621 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnection.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnection.cs @@ -301,6 +301,7 @@ override protected DbReferenceCollection CreateReferenceCollection() return new SqlReferenceCollection(); } + /// override protected void Deactivate() { TdsParser bestEffortCleanupTarget = null; diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlParameter.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlParameter.cs index f9b940fec3..fb9cdbb8e0 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlParameter.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlParameter.cs @@ -773,7 +773,7 @@ private object GetVectorReturnValue() switch (elementType) { case MetaType.SqlVectorElementType.Float32: - return new SqlVector(elementCount); + return SqlVector.CreateNull(elementCount); default: throw SQL.VectorTypeNotSupported(elementType.ToString()); } @@ -857,8 +857,13 @@ public override int Size { throw ADP.InvalidSizeValue(value); } - PropertyChanging(); - _size = value; + + // We ignore the Size property for Vector types, as it is not applicable. + if (_metaType == null || _metaType.SqlDbType != SqlDbTypeExtensions.Vector) + { + PropertyChanging(); + _size = value; + } } } } @@ -1970,7 +1975,8 @@ internal void Prepare(SqlCommand cmd) { throw ADP.PrepareParameterType(cmd); } - else if (!ShouldSerializeSize() && !_metaType.IsFixed) + // For vector datatype we do not require size to be specified. It is inferred from the SqlParameter.Value. + else if (!ShouldSerializeSize() && !_metaType.IsFixed && _metaType.SqlDbType != SqlDbTypeExtensions.Vector) { throw ADP.PrepareParameterSize(cmd); } diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlStatistics.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlStatistics.cs index 1ea104d065..c88cddff24 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlStatistics.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlStatistics.cs @@ -38,7 +38,7 @@ internal static ValueSqlStatisticsScope TimedScope(SqlStatistics statistics) // internal values that are not exposed through properties internal long _closeTimestamp; internal long _openTimestamp; - internal long _startExecutionTimestamp; + internal long? _startExecutionTimestamp; internal long _startFetchTimestamp; internal long _startNetworkServerTimestamp; @@ -80,7 +80,7 @@ internal bool WaitForDoneAfterRow internal void ContinueOnNewConnection() { - _startExecutionTimestamp = 0; + _startExecutionTimestamp = null; _startFetchTimestamp = 0; _waitForDoneAfterRow = false; _waitForReply = false; @@ -108,7 +108,7 @@ internal IDictionary GetDictionary() { "UnpreparedExecs", _unpreparedExecs }, { "ConnectionTime", ADP.TimerToMilliseconds(_connectionTime) }, - { "ExecutionTime", ADP.TimerToMilliseconds(_executionTime) }, + { "ExecutionTime", _executionTime }, { "NetworkServerTime", ADP.TimerToMilliseconds(_networkServerTime) } }; Debug.Assert(dictionary.Count == Count); @@ -117,9 +117,9 @@ internal IDictionary GetDictionary() internal bool RequestExecutionTimer() { - if (_startExecutionTimestamp == 0) + if (!_startExecutionTimestamp.HasValue) { - _startExecutionTimestamp = ADP.TimerCurrent(); + _startExecutionTimestamp = ADP.FastTimerCurrent(); return true; } return false; @@ -127,7 +127,7 @@ internal bool RequestExecutionTimer() internal void RequestNetworkServerTimer() { - Debug.Assert(_startExecutionTimestamp != 0, "No network time expected outside execution period"); + Debug.Assert(_startExecutionTimestamp.HasValue, "No network time expected outside execution period"); if (_startNetworkServerTimestamp == 0) { _startNetworkServerTimestamp = ADP.TimerCurrent(); @@ -137,10 +137,11 @@ internal void RequestNetworkServerTimer() internal void ReleaseAndUpdateExecutionTimer() { - if (_startExecutionTimestamp > 0) + if (_startExecutionTimestamp.HasValue) { - _executionTime += (ADP.TimerCurrent() - _startExecutionTimestamp); - _startExecutionTimestamp = 0; + uint elapsed = ADP.CalculateTickCountElapsed(_startExecutionTimestamp.Value, ADP.FastTimerCurrent()); + _executionTime += elapsed; + _startExecutionTimestamp = null; } } @@ -176,7 +177,7 @@ internal void Reset() _unpreparedExecs = 0; _waitForDoneAfterRow = false; _waitForReply = false; - _startExecutionTimestamp = 0; + _startExecutionTimestamp = null; _startNetworkServerTimestamp = 0; } diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlSymmetricKeyCache.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlSymmetricKeyCache.cs index fb9ea2997d..961746fc2d 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlSymmetricKeyCache.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlSymmetricKeyCache.cs @@ -5,6 +5,7 @@ using System; using System.Diagnostics; using System.Text; +using System.Threading; using Microsoft.Extensions.Caching.Memory; namespace Microsoft.Data.SqlClient @@ -15,8 +16,8 @@ namespace Microsoft.Data.SqlClient sealed internal class SqlSymmetricKeyCache { private readonly MemoryCache _cache; - private static readonly SqlSymmetricKeyCache _singletonInstance = new SqlSymmetricKeyCache(); - + private static readonly SqlSymmetricKeyCache _singletonInstance = new(); + private static SemaphoreSlim _cacheLock = new(1, 1); private SqlSymmetricKeyCache() { @@ -35,7 +36,7 @@ internal SqlClientSymmetricKey GetKey(SqlEncryptionKeyInfo keyInfo, SqlConnectio { string serverName = connection.DataSource; Debug.Assert(serverName is not null, @"serverName should not be null."); - StringBuilder cacheLookupKeyBuilder = new StringBuilder(serverName, capacity: serverName.Length + SqlSecurityUtility.GetBase64LengthFromByteLength(keyInfo.encryptedKey.Length) + keyInfo.keyStoreName.Length + 2/*separators*/); + StringBuilder cacheLookupKeyBuilder = new(serverName, capacity: serverName.Length + SqlSecurityUtility.GetBase64LengthFromByteLength(keyInfo.encryptedKey.Length) + keyInfo.keyStoreName.Length + 2/*separators*/); #if DEBUG int capacity = cacheLookupKeyBuilder.Capacity; @@ -52,54 +53,66 @@ internal SqlClientSymmetricKey GetKey(SqlEncryptionKeyInfo keyInfo, SqlConnectio Debug.Assert(cacheLookupKey.Length <= capacity, "We needed to allocate a larger array"); #endif //DEBUG - // Lookup the key in cache - SqlClientSymmetricKey encryptionKey; - if (!(_cache.TryGetValue(cacheLookupKey, out encryptionKey))) - { - Debug.Assert(SqlConnection.ColumnEncryptionTrustedMasterKeyPaths is not null, @"SqlConnection.ColumnEncryptionTrustedMasterKeyPaths should not be null"); - - SqlSecurityUtility.ThrowIfKeyPathIsNotTrustedForServer(serverName, keyInfo.keyPath); + // Acquire the lock to ensure thread safety when accessing the cache + _cacheLock.Wait(); - // Key Not found, attempt to look up the provider and decrypt CEK - if (!SqlSecurityUtility.TryGetColumnEncryptionKeyStoreProvider(keyInfo.keyStoreName, out SqlColumnEncryptionKeyStoreProvider provider, connection, command)) + try + { + // Lookup the key in cache + if (!(_cache.TryGetValue(cacheLookupKey, out SqlClientSymmetricKey encryptionKey))) { - throw SQL.UnrecognizedKeyStoreProviderName(keyInfo.keyStoreName, - SqlConnection.GetColumnEncryptionSystemKeyStoreProvidersNames(), - SqlSecurityUtility.GetListOfProviderNamesThatWereSearched(connection, command)); - } + Debug.Assert(SqlConnection.ColumnEncryptionTrustedMasterKeyPaths is not null, @"SqlConnection.ColumnEncryptionTrustedMasterKeyPaths should not be null"); - // Decrypt the CEK - // We will simply bubble up the exception from the DecryptColumnEncryptionKey function. - byte[] plaintextKey; - try - { - // to prevent conflicts between CEK caches, global providers should not use their own CEK caches - provider.ColumnEncryptionKeyCacheTtl = new TimeSpan(0); - plaintextKey = provider.DecryptColumnEncryptionKey(keyInfo.keyPath, keyInfo.algorithmName, keyInfo.encryptedKey); - } - catch (Exception e) - { - // Generate a new exception and throw. - string keyHex = SqlSecurityUtility.GetBytesAsString(keyInfo.encryptedKey, fLast: true, countOfBytes: 10); - throw SQL.KeyDecryptionFailed(keyInfo.keyStoreName, keyHex, e); - } + SqlSecurityUtility.ThrowIfKeyPathIsNotTrustedForServer(serverName, keyInfo.keyPath); - encryptionKey = new SqlClientSymmetricKey(plaintextKey); + // Key Not found, attempt to look up the provider and decrypt CEK + if (!SqlSecurityUtility.TryGetColumnEncryptionKeyStoreProvider(keyInfo.keyStoreName, out SqlColumnEncryptionKeyStoreProvider provider, connection, command)) + { + throw SQL.UnrecognizedKeyStoreProviderName(keyInfo.keyStoreName, + SqlConnection.GetColumnEncryptionSystemKeyStoreProvidersNames(), + SqlSecurityUtility.GetListOfProviderNamesThatWereSearched(connection, command)); + } + + // Decrypt the CEK + // We will simply bubble up the exception from the DecryptColumnEncryptionKey function. + byte[] plaintextKey; + try + { + // AKV provider registration supports multi-user scenarios, so it is not safe to cache the CEK in the global provider. + // The CEK cache is a global cache, and is shared across all connections. + // To prevent conflicts between CEK caches, global providers should not use their own CEK caches + provider.ColumnEncryptionKeyCacheTtl = new TimeSpan(0); + plaintextKey = provider.DecryptColumnEncryptionKey(keyInfo.keyPath, keyInfo.algorithmName, keyInfo.encryptedKey); + } + catch (Exception e) + { + // Generate a new exception and throw. + string keyHex = SqlSecurityUtility.GetBytesAsString(keyInfo.encryptedKey, fLast: true, countOfBytes: 10); + throw SQL.KeyDecryptionFailed(keyInfo.keyStoreName, keyHex, e); + } - // If the cache TTL is zero, don't even bother inserting to the cache. - if (SqlConnection.ColumnEncryptionKeyCacheTtl != TimeSpan.Zero) - { - // In case multiple threads reach here at the same time, the first one wins. - // The allocated memory will be reclaimed by Garbage Collector. - MemoryCacheEntryOptions options = new MemoryCacheEntryOptions + encryptionKey = new SqlClientSymmetricKey(plaintextKey); + + // If the cache TTL is zero, don't even bother inserting to the cache. + if (SqlConnection.ColumnEncryptionKeyCacheTtl != TimeSpan.Zero) { - AbsoluteExpirationRelativeToNow = SqlConnection.ColumnEncryptionKeyCacheTtl - }; - _cache.Set(cacheLookupKey, encryptionKey, options); + // In case multiple threads reach here at the same time, the first one wins. + // The allocated memory will be reclaimed by Garbage Collector. + MemoryCacheEntryOptions options = new() + { + AbsoluteExpirationRelativeToNow = SqlConnection.ColumnEncryptionKeyCacheTtl + }; + _cache.Set(cacheLookupKey, encryptionKey, options); + } } - } - return encryptionKey; + return encryptionKey; + } + finally + { + // Release the lock to allow other threads to access the cache + _cacheLock.Release(); + } } } } diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsEnums.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsEnums.cs index 630633a6b4..e234fbbef6 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsEnums.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsEnums.cs @@ -604,7 +604,7 @@ public enum ActiveDirectoryWorkflow : byte public const uint SNI_UNINITIALIZED = unchecked((uint)-1); public const uint SNI_SUCCESS = 0; // The operation completed successfully. public const uint SNI_ERROR = 1; // Error - public const uint SNI_WAIT_TIMEOUT = 258; // The wait operation timed out. + public const int SNI_WAIT_TIMEOUT = 258; // The wait operation timed out. public const uint SNI_SUCCESS_IO_PENDING = 997; // Overlapped I/O operation is in progress. // Windows Sockets Error Codes diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParser.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParser.cs index 7e0fd18503..6226f958a5 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParser.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParser.cs @@ -39,7 +39,7 @@ internal void ProcessSSPI(int receivedLength) try { // make call for SSPI data - _authenticationProvider!.SSPIData(receivedBuff.AsSpan(0, receivedLength), writer, _serverSpn); + _authenticationProvider!.WriteSSPIContext(receivedBuff.AsSpan(0, receivedLength), writer); // DO NOT SEND LENGTH - TDS DOC INCORRECT! JUST SEND SSPI DATA! _physicalStateObj.WriteByteSpan(writer.WrittenSpan); @@ -179,7 +179,7 @@ internal void TdsLogin( // byte[] buffer and 0 for the int length. Debug.Assert(SniContext.Snix_Login == _physicalStateObj.SniContext, $"Unexpected SniContext. Expecting Snix_Login, actual value is '{_physicalStateObj.SniContext}'"); _physicalStateObj.SniContext = SniContext.Snix_LoginSspi; - _authenticationProvider.SSPIData(ReadOnlySpan.Empty, sspiWriter, _serverSpn); + _authenticationProvider.WriteSSPIContext(ReadOnlySpan.Empty, sspiWriter); _physicalStateObj.SniContext = SniContext.Snix_Login; diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParserStateObject.Multiplexer.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParserStateObject.Multiplexer.cs deleted file mode 100644 index 77bd1c982b..0000000000 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParserStateObject.Multiplexer.cs +++ /dev/null @@ -1,551 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -using System; -using System.Diagnostics; - -namespace Microsoft.Data.SqlClient -{ - partial class TdsParserStateObject - { - private Packet _partialPacket; - internal Packet PartialPacket => _partialPacket; - - public void ProcessSniPacket(PacketHandle packet, uint error) - { - if (LocalAppContextSwitches.UseCompatibilityProcessSni) - { - ProcessSniPacketCompat(packet, error); - return; - } - - if (error != 0) - { - if ((_parser.State == TdsParserState.Closed) || (_parser.State == TdsParserState.Broken)) - { - // Do nothing with callback if closed or broken and error not 0 - callback can occur - // after connection has been closed. PROBLEM IN NETLIB - DESIGN FLAW. - return; - } - - AddError(_parser.ProcessSNIError(this)); - AssertValidState(); - } - else - { - uint dataSize = 0; - bool usedPartialPacket = false; - uint getDataError = 0; - - if (PartialPacketContainsCompletePacket()) - { - Packet partialPacket = _partialPacket; - SetBuffer(partialPacket.Buffer, 0, partialPacket.CurrentLength); - ClearPartialPacket(); - getDataError = TdsEnums.SNI_SUCCESS; - usedPartialPacket = true; - } - else - { - if (_inBytesRead != 0) - { - SetBuffer(new byte[_inBuff.Length], 0, 0); - } - getDataError = GetSniPacket(packet, ref dataSize); - } - - if (getDataError == TdsEnums.SNI_SUCCESS) - { - if (_inBuff.Length < dataSize) - { - Debug.Assert(true, "Unexpected dataSize on Read"); - throw SQL.InvalidInternalPacketSize(StringsHelper.GetString(Strings.SqlMisc_InvalidArraySizeMessage)); - } - - if (!usedPartialPacket) - { - _lastSuccessfulIOTimer._value = DateTime.UtcNow.Ticks; - - SetBuffer(_inBuff, 0, (int)dataSize); - } - - bool recurse = false; - bool appended = false; - do - { - if (recurse && appended) - { - SetBuffer(new byte[_inBuff.Length], 0, 0); - appended = false; - } - MultiplexPackets( - _inBuff, _inBytesUsed, _inBytesRead, - PartialPacket, - out int newDataOffset, - out int newDataLength, - out Packet remainderPacket, - out bool consumeInputDirectly, - out bool consumePartialPacket, - out bool remainderPacketProduced, - out recurse - ); - bool bufferIsPartialCompleted = false; - - // if a partial packet was reconstructed it must be handled first - if (consumePartialPacket) - { - if (_snapshot != null) - { - _snapshot.AppendPacketData(PartialPacket.Buffer, PartialPacket.CurrentLength); - SetBuffer(new byte[_inBuff.Length], 0, 0); - appended = true; - } - else - { - SetBuffer(PartialPacket.Buffer, 0, PartialPacket.CurrentLength); - - } - bufferIsPartialCompleted = true; - ClearPartialPacket(); - } - - // if the remaining data can be processed directly it must be second - if (consumeInputDirectly) - { - // if some data was taken from the new packet adjust the counters - if (dataSize != newDataLength || 0 != newDataOffset) - { - SetBuffer(_inBuff, newDataOffset, newDataLength); - } - - if (_snapshot != null) - { - _snapshot.AppendPacketData(_inBuff, _inBytesRead); - // if we SetBuffer here to clear the packet buffer we will break the attention handling which relies - // on the attention containing packet remaining in the active buffer even if we're appending to the - // snapshot so we will have to use the appended variable to prevent the same buffer being added again - //// SetBuffer(new byte[_inBuff.Length], 0, 0); - appended = true; - } - else - { - SetBuffer(_inBuff, 0, _inBytesRead); - } - bufferIsPartialCompleted = true; - } - else - { - // whatever is in the input buffer should not be directly consumed - // and is contained in the partial or remainder packets so make sure - // we don't process it - if (!bufferIsPartialCompleted) - { - SetBuffer(_inBuff, 0, 0); - } - } - - // if there is a remainder it must be last - if (remainderPacketProduced) - { - SetPartialPacket(remainderPacket); - if (!bufferIsPartialCompleted) - { - // we are keeping the partial packet buffer so replace it with a new one - // unless we have already set the buffer to the partial packet buffer - SetBuffer(new byte[_inBuff.Length], 0, 0); - } - } - - } while (recurse && _snapshot != null); - - if (_snapshot != null) - { - if (_snapshotStatus != SnapshotStatus.NotActive && appended) - { - _snapshot.MoveNext(); - } - } - - SniReadStatisticsAndTracing(); - SqlClientEventSource.Log.TryAdvancedTraceBinEvent("TdsParser.ReadNetworkPacketAsyncCallback | INFO | ADV | State Object Id {0}, Packet read. In Buffer {1}, In Bytes Read: {2}", ObjectID, _inBuff, (ushort)_inBytesRead); - - AssertValidState(); - } - else - { - throw SQL.ParsingError(ParsingErrorState.ProcessSniPacketFailed); - } - } - } - - private void SetPartialPacket(Packet packet) - { - if (_partialPacket != null && packet != null) - { - throw new InvalidOperationException("partial packet cannot be non-null when setting to non=null"); - } - _partialPacket = packet; - } - - private void ClearPartialPacket() - { - Packet partialPacket = _partialPacket; - _partialPacket = null; -#if DEBUG - if (partialPacket != null) - { - partialPacket.Dispose(); - } -#endif - } - - // this check is used in two places that must be identical so it is - // extracted into a method, do not inline this method - internal bool PartialPacketContainsCompletePacket() - { - Packet partialPacket = _partialPacket; - return partialPacket != null && partialPacket.ContainsCompletePacket; - } - - private static void MultiplexPackets( - byte[] dataBuffer, int dataOffset, int dataLength, - Packet partialPacket, - out int newDataOffset, - out int newDataLength, - out Packet remainderPacket, - out bool consumeInputDirectly, - out bool consumePartialPacket, - out bool createdRemainderPacket, - out bool recurse - ) - { - Debug.Assert(dataBuffer != null); - - ReadOnlySpan data = dataBuffer.AsSpan(dataOffset, dataLength); - remainderPacket = null; - consumeInputDirectly = false; - consumePartialPacket = false; - createdRemainderPacket = false; - recurse = false; - - newDataLength = dataLength; - newDataOffset = dataOffset; - - int bytesConsumed = 0; - - if (partialPacket != null) - { - if (!partialPacket.HasDataLength) - { - // we need to get enough bytes to read the packet header - int headerBytesNeeded = Math.Max(0, TdsEnums.HEADER_LEN - partialPacket.CurrentLength); - if (headerBytesNeeded > 0) - { - int headerBytesAvailable = Math.Min(data.Length, headerBytesNeeded); - - Span headerTarget = partialPacket.Buffer.AsSpan(partialPacket.CurrentLength, headerBytesAvailable); - ReadOnlySpan headerSource = data.Slice(0, headerBytesAvailable); - headerSource.CopyTo(headerTarget); - - partialPacket.CurrentLength = partialPacket.CurrentLength + headerBytesAvailable; - bytesConsumed += headerBytesAvailable; - data = data.Slice(headerBytesAvailable); - } - if (partialPacket.HasHeader) - { - partialPacket.DataLength = Packet.GetDataLengthFromHeader(partialPacket); - } - } - - if (partialPacket.HasDataLength) - { - if (partialPacket.CurrentLength < partialPacket.RequiredLength) - { - // the packet length is known so take as much data as possible from the incoming - // data to try and complete the packet - - int payloadBytesNeeded = partialPacket.DataLength - (partialPacket.CurrentLength - TdsEnums.HEADER_LEN); - int payloadBytesAvailable = Math.Min(data.Length, payloadBytesNeeded); - - ReadOnlySpan payloadSource = data.Slice(0, payloadBytesAvailable); - Span payloadTarget = partialPacket.Buffer.AsSpan(partialPacket.CurrentLength, payloadBytesAvailable); - payloadSource.CopyTo(payloadTarget); - - partialPacket.CurrentLength = partialPacket.CurrentLength + payloadBytesAvailable; - bytesConsumed += payloadBytesAvailable; - data = data.Slice(payloadBytesAvailable); - } - else if (partialPacket.CurrentLength > partialPacket.RequiredLength) - { - // the partial packet contains a complete packet of data and also contains - // data from a following packet - - // the TDS spec requires that all packets be of the defined packet size apart from - // the last packet of a response. This means that is should not possible to have more than - // 2 packet fragments in a single buffer like this: - // - first packet caused the partial - // - second packet is the one we have just unpacked - // - third packet is the extra data we have found - // however, due to the timing of cancellation it is possible that a response token stream - // has ended before an attention message response is sent leaving us with a short final - // packet and an additional short cancel packet following it - - // this should only happen when the caller is trying to consume the partial packet - // and does not have new input data - - int remainderLength = partialPacket.CurrentLength - partialPacket.RequiredLength; - - partialPacket.CurrentLength = partialPacket.RequiredLength; - - remainderPacket = new Packet - { - Buffer = new byte[dataBuffer.Length], - CurrentLength = remainderLength, - }; - remainderPacket.SetCreatedBy(1); - - ReadOnlySpan remainderSource = partialPacket.Buffer.AsSpan(TdsEnums.HEADER_LEN + partialPacket.DataLength, remainderLength); - Span remainderTarget = remainderPacket.Buffer.AsSpan(0, remainderLength); - remainderSource.CopyTo(remainderTarget); - - createdRemainderPacket = true; - - recurse = SetupRemainderPacket(remainderPacket); - } - - if (partialPacket.CurrentLength == partialPacket.RequiredLength) - { - // partial packet has been completed - consumePartialPacket = true; - } - } - - if (bytesConsumed > 0) - { - if (data.Length > 0) - { - // some data has been taken from the buffer and put into the partial - // packet buffer. We have data left so move the data we have to the - // start of the buffer so we can pass the buffer back as zero based - // to the caller avoiding offset calculations in the rest of this method - Buffer.BlockCopy( - dataBuffer, dataOffset + bytesConsumed, // from - dataBuffer, dataOffset, // to - dataLength - bytesConsumed // for - ); -#if DEBUG - // for debugging purposes fill the removed data area with an easily - // recognisable pattern so we can see if it is misused - Span removed = dataBuffer.AsSpan(dataOffset + (dataLength - bytesConsumed), bytesConsumed); - removed.Fill(0xFF); -#endif - - // then recreate the data span so that we're looking at the data - // that has been moved - data = dataBuffer.AsSpan(dataOffset, dataLength - bytesConsumed); - } - - newDataLength = dataLength - bytesConsumed; - } - } - - // partial packet handling should not make decisions about consuming the input buffer - Debug.Assert(!consumeInputDirectly); - // partial packet handling may only create a remainder packet when it is trying to consume the partial packet and has no incoming data - Debug.Assert(!createdRemainderPacket || data.Length == 0); - - if (data.Length > 0) - { - if (data.Length >= TdsEnums.HEADER_LEN) - { - // we have enough bytes to read the packet header and see how - // much data we are expecting it to contain - int packetDataLength = Packet.GetDataLengthFromHeader(data); - - if (data.Length == TdsEnums.HEADER_LEN + packetDataLength) - { - if (!consumePartialPacket) - { - // we can tell the caller that they should directly consume the data in - // the input buffer, this is the happy path - consumeInputDirectly = true; - } - else - { - // we took some data from the input to reconstruct the partial packet - // so we can't tell the caller to directly consume the packet in the - // input buffer, we need to construct a new remainder packet and then - // tell them to consume it - remainderPacket = new Packet - { - Buffer = dataBuffer, - CurrentLength = data.Length - }; - remainderPacket.SetCreatedBy(2); - createdRemainderPacket = true; - recurse = SetupRemainderPacket(remainderPacket); - } - } - else if (data.Length < TdsEnums.HEADER_LEN + packetDataLength) - { - // an incomplete packet so create a remainder packet to pass back - remainderPacket = new Packet - { - Buffer = dataBuffer, - DataLength = packetDataLength, - CurrentLength = data.Length - }; - remainderPacket.SetCreatedBy(3); - createdRemainderPacket = true; - recurse = SetupRemainderPacket(remainderPacket); - } - else // implied: current length > required length - { - // more data than required so need to split it out, but we can't do that - // here so we need to tell the caller to take the remainder packet and then - // call this function again - if (consumePartialPacket) - { - // we are already telling the caller to consume the partial packet so we - // can't tell them it to also consume the data in the buffer directly - // so create a remainder packet and pass it back. - remainderPacket = new Packet - { - Buffer = new byte[dataBuffer.Length], - CurrentLength = data.Length - }; - remainderPacket.SetCreatedBy(4); - ReadOnlySpan remainderSource = data; - Span remainderTarget = remainderPacket.Buffer.AsSpan(0, remainderPacket.CurrentLength); - remainderSource.CopyTo(remainderTarget); - - createdRemainderPacket = true; - - recurse = SetupRemainderPacket(remainderPacket); - } - else - { - newDataLength = TdsEnums.HEADER_LEN + packetDataLength; - int remainderLength = data.Length - (TdsEnums.HEADER_LEN + packetDataLength); - remainderPacket = new Packet - { - Buffer = new byte[dataBuffer.Length], - CurrentLength = remainderLength - }; - remainderPacket.SetCreatedBy(5); - - ReadOnlySpan remainderSource = data.Slice(TdsEnums.HEADER_LEN + packetDataLength); - Span remainderTarget = remainderPacket.Buffer.AsSpan(0, remainderLength); - remainderSource.CopyTo(remainderTarget); -#if DEBUG - // for debugging purposes fill the removed data area with an easily - // recognisable pattern so we can see if it is misused - Span removed = dataBuffer.AsSpan(TdsEnums.HEADER_LEN + packetDataLength, remainderLength); - removed.Fill(0xFF); -#endif - createdRemainderPacket = true; - recurse = SetupRemainderPacket(remainderPacket); - - consumeInputDirectly = true; - } - } - } - else - { - // either: - // 1) we took some data from the input to reconstruct the partial packet - // 2) there was less than a single packet header of data received - // in both cases we can't tell the caller to directly consume the packet - // in the input buffer, we need to construct a new remainder packet with - // the incomplete data and let the caller deal with it - remainderPacket = new Packet - { - Buffer = dataBuffer, - CurrentLength = data.Length - }; - remainderPacket.SetCreatedBy(6); - createdRemainderPacket = true; - recurse = SetupRemainderPacket(remainderPacket); - } - } - - if (consumePartialPacket && consumeInputDirectly) - { - throw new InvalidOperationException($"MultiplexPackets cannot return both {nameof(consumePartialPacket)} and {nameof(consumeInputDirectly)}"); - } - } - - private static bool SetupRemainderPacket(Packet packet) - { - Debug.Assert(packet != null); - bool containsFullPacket = false; - if (packet.HasHeader) - { - packet.DataLength = Packet.GetDataLengthFromHeader(packet); - if (packet.HasDataLength && packet.CurrentLength >= packet.RequiredLength) - { - containsFullPacket = true; - } - } - - return containsFullPacket; - } - - - public void ProcessSniPacketCompat(PacketHandle packet, uint error) - { - if (error != 0) - { - if ((_parser.State == TdsParserState.Closed) || (_parser.State == TdsParserState.Broken)) - { - // Do nothing with callback if closed or broken and error not 0 - callback can occur - // after connection has been closed. PROBLEM IN NETLIB - DESIGN FLAW. - return; - } - - AddError(_parser.ProcessSNIError(this)); - AssertValidState(); - } - else - { - uint dataSize = 0; - uint getDataError = SniPacketGetData(packet, _inBuff, ref dataSize); - - if (getDataError == TdsEnums.SNI_SUCCESS) - { - if (_inBuff.Length < dataSize) - { - Debug.Assert(true, "Unexpected dataSize on Read"); - throw SQL.InvalidInternalPacketSize(StringsHelper.GetString(Strings.SqlMisc_InvalidArraySizeMessage)); - } - - _lastSuccessfulIOTimer._value = DateTime.UtcNow.Ticks; - _inBytesRead = (int)dataSize; - _inBytesUsed = 0; - - if (_snapshot != null) - { - _snapshot.AppendPacketData(_inBuff, _inBytesRead); - if (_snapshotStatus != SnapshotStatus.NotActive) - { - _snapshot.MoveNext(); -#if DEBUG - _snapshot.AssertCurrent(); -#endif - } - } - - SniReadStatisticsAndTracing(); - SqlClientEventSource.Log.TryAdvancedTraceBinEvent("TdsParser.ReadNetworkPacketAsyncCallback | INFO | ADV | State Object Id {0}, Packet read. In Buffer: {1}, In Bytes Read: {2}", ObjectID, _inBuff, _inBytesRead); - - AssertValidState(); - } - else - { - throw SQL.ParsingError(ParsingErrorState.ProcessSniPacketFailed); - } - } - } - } -} diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParserStateObject.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParserStateObject.cs index a1b820bb0d..da0e06cc14 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParserStateObject.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParserStateObject.cs @@ -7,7 +7,6 @@ using System.Collections.Generic; using System.Diagnostics; using System.Security; -using System.Security.Cryptography; using System.Text; using System.Threading; using System.Threading.Tasks; @@ -52,6 +51,28 @@ internal enum SnapshottedStateFlags : byte AttentionReceived = 1 << 5 // NOTE: Received is not volatile as it is only ever accessed\modified by TryRun its callees (i.e. single threaded access) } + internal readonly struct SniErrorDetails + { + public readonly string ErrorMessage; + public readonly int NativeError; + public readonly uint SniErrorNumber; + public readonly int Provider; + public readonly uint LineNumber; + public readonly string Function; + public readonly Exception Exception; + + internal SniErrorDetails(string errorMessage, int nativeError, uint sniErrorNumber, int provider, uint lineNumber, string function, Exception exception = null) + { + ErrorMessage = errorMessage; + NativeError = nativeError; + SniErrorNumber = sniErrorNumber; + Provider = provider; + LineNumber = lineNumber; + Function = function; + Exception = exception; + } + } + private sealed class TimeoutState { public const int Stopped = 0; @@ -69,14 +90,6 @@ public TimeoutState(int value) public int IdentityValue => _value; } - private enum SnapshotStatus - { - NotActive, - ReplayStarting, - ReplayRunning, - ContinueRunning - } - private const int AttentionTimeoutSeconds = 5; // Ticks to consider a connection "good" after a successful I/O (10,000 ticks = 1 ms) @@ -229,7 +242,7 @@ private enum SnapshotStatus internal TaskCompletionSource _networkPacketTaskSource; private Timer _networkPacketTimeout; internal bool _syncOverAsync = true; - private SnapshotStatus _snapshotStatus; + private bool _snapshotReplay; private StateSnapshot _snapshot; private StateSnapshot _cachedSnapshot; internal ExecutionContext _executionContext; @@ -1165,11 +1178,13 @@ internal TdsOperationStatus TryProcessHeader() if (_partialHeaderBytesRead == _inputHeaderLen) { // All read - ReadOnlySpan header = _partialHeaderBuffer.AsSpan(0, TdsEnums.HEADER_LEN); _partialHeaderBytesRead = 0; - _messageStatus = Packet.GetStatusFromHeader(header); - _inBytesPacket = Packet.GetDataLengthFromHeader(header); - _spid = Packet.GetSpidFromHeader(header); + _inBytesPacket = ((int)_partialHeaderBuffer[TdsEnums.HEADER_LEN_FIELD_OFFSET] << 8 | + (int)_partialHeaderBuffer[TdsEnums.HEADER_LEN_FIELD_OFFSET + 1]) - _inputHeaderLen; + + _messageStatus = _partialHeaderBuffer[1]; + _spid = _partialHeaderBuffer[TdsEnums.SPID_OFFSET] << 8 | + _partialHeaderBuffer[TdsEnums.SPID_OFFSET + 1]; SqlClientEventSource.Log.TryAdvancedTraceEvent("TdsParserStateObject.TryProcessHeader | ADV | State Object Id {0}, Client Connection Id {1}, Server process Id (SPID) {2}", _objectID, _parser?.Connection?.ClientConnectionId, _spid); } @@ -1205,10 +1220,11 @@ internal TdsOperationStatus TryProcessHeader() else { // normal header processing... - ReadOnlySpan header = _inBuff.AsSpan(_inBytesUsed, TdsEnums.HEADER_LEN); - _messageStatus = Packet.GetStatusFromHeader(header); - _inBytesPacket = Packet.GetDataLengthFromHeader(header); - _spid = Packet.GetSpidFromHeader(header); + _messageStatus = _inBuff[_inBytesUsed + 1]; + _inBytesPacket = (_inBuff[_inBytesUsed + TdsEnums.HEADER_LEN_FIELD_OFFSET] << 8 | + _inBuff[_inBytesUsed + TdsEnums.HEADER_LEN_FIELD_OFFSET + 1]) - _inputHeaderLen; + _spid = _inBuff[_inBytesUsed + TdsEnums.SPID_OFFSET] << 8 | + _inBuff[_inBytesUsed + TdsEnums.SPID_OFFSET + 1]; #if NET SqlClientEventSource.Log.TryAdvancedTraceEvent("TdsParserStateObject.TryProcessHeader | ADV | State Object Id {0}, Client Connection Id {1}, Server process Id (SPID) {2}", _objectID, _parser?.Connection?.ClientConnectionId, _spid); #endif @@ -1348,7 +1364,9 @@ internal bool SetPacketSize(int size) // Allocate or re-allocate _inBuff. if (_inBuff == null) { - SetBuffer(new byte[size], 0, 0); + _inBuff = new byte[size]; + _inBytesRead = 0; + _inBytesUsed = 0; } else if (size != _inBuff.Length) { @@ -1358,24 +1376,28 @@ internal bool SetPacketSize(int size) // if we still have data left in the buffer we must keep that array reference and then copy into new one byte[] temp = _inBuff; + _inBuff = new byte[size]; + // copy remainder of unused data int remainingData = _inBytesRead - _inBytesUsed; - if ((temp.Length < _inBytesUsed + remainingData) || (size < remainingData)) + if ((temp.Length < _inBytesUsed + remainingData) || (_inBuff.Length < remainingData)) { - string errormessage = StringsHelper.GetString(Strings.SQL_InvalidInternalPacketSize) + ' ' + temp.Length + ", " + _inBytesUsed + ", " + remainingData + ", " + size; + string errormessage = StringsHelper.GetString(Strings.SQL_InvalidInternalPacketSize) + ' ' + temp.Length + ", " + _inBytesUsed + ", " + remainingData + ", " + _inBuff.Length; throw SQL.InvalidInternalPacketSize(errormessage); } + Buffer.BlockCopy(temp, _inBytesUsed, _inBuff, 0, remainingData); - byte[] inBuff = new byte[size]; - Buffer.BlockCopy(temp, _inBytesUsed, inBuff, 0, remainingData); - SetBuffer(inBuff, 0, remainingData); + _inBytesRead = _inBytesRead - _inBytesUsed; + _inBytesUsed = 0; AssertValidState(); } else { // buffer is empty - just create the new one that is double the size of the old one - SetBuffer(new byte[size], 0, 0); + _inBuff = new byte[size]; + _inBytesRead = 0; + _inBytesUsed = 0; } } @@ -1390,11 +1412,6 @@ internal bool SetPacketSize(int size) return false; } - internal int GetPacketSize() - { - return _inBuff.Length; - } - /////////////////////////////////////// // Buffer read methods - data values // /////////////////////////////////////// @@ -1421,17 +1438,12 @@ internal TdsOperationStatus TryPeekByte(out byte value) // bytes from the in buffer. public TdsOperationStatus TryReadByteArray(Span buff, int len) { - return TryReadByteArray(buff, len, out _, 0, false); - } - - public TdsOperationStatus TryReadByteArray(Span buff, int len, out int totalRead) - { - return TryReadByteArray(buff, len, out totalRead, 0, false); + return TryReadByteArray(buff, len, out _); } // NOTE: This method must be retriable WITHOUT replaying a snapshot // Every time you call this method increment the offset and decrease len by the value of totalRead - public TdsOperationStatus TryReadByteArray(Span buff, int len, out int totalRead, int startOffset, bool writeDataSizeToSnapshot) + public TdsOperationStatus TryReadByteArray(Span buff, int len, out int totalRead) { totalRead = 0; @@ -1456,10 +1468,6 @@ public TdsOperationStatus TryReadByteArray(Span buff, int len, out int tot Debug.Assert(buff.IsEmpty || buff.Length >= len, "Invalid length sent to ReadByteArray()!"); - - totalRead += startOffset; - len -= startOffset; - // loop through and read up to array length while (len > 0) { @@ -1486,59 +1494,12 @@ public TdsOperationStatus TryReadByteArray(Span buff, int len, out int tot _inBytesPacket -= bytesToRead; len -= bytesToRead; - if (writeDataSizeToSnapshot) - { - SetSnapshotDataSize(bytesToRead); - } - AssertValidState(); } return TdsOperationStatus.Done; } - public TdsOperationStatus TryReadByteArrayWithContinue(int length, out byte[] bytes) - { - bytes = null; - int offset = 0; - byte[] temp = null; - (bool canContinue, bool isStarting, bool isContinuing) = GetSnapshotStatuses(); - if (canContinue) - { - if (isContinuing || isStarting) - { - temp = TryTakeSnapshotStorage() as byte[]; - Debug.Assert(bytes == null || bytes.Length == length, "stored buffer length must be null or must have been created with the correct length"); - } - if (temp != null) - { - offset = GetSnapshotTotalSize(); - } - } - - - if (temp == null) - { - temp = new byte[length]; - } - - TdsOperationStatus result = TryReadByteArray(temp, length, out _, offset, isStarting || isContinuing); - - if (result == TdsOperationStatus.Done) - { - bytes = temp; - } - else if (result == TdsOperationStatus.NeedMoreData) - { - if (isStarting || isContinuing) - { - SetSnapshotStorage(temp); - } - } - - return result; - } - // Takes no arguments and returns a byte from the buffer. If the buffer is empty, it is filled // before the byte is returned. internal TdsOperationStatus TryReadByte(out byte value) @@ -1658,7 +1619,7 @@ internal TdsOperationStatus TryReadInt32(out int value) TdsOperationStatus result = TryReadByteArray(buffer, 4); if (result != TdsOperationStatus.Done) { - value = 0; + value = default; return result; } } @@ -1885,13 +1846,21 @@ internal TdsOperationStatus TryReadString(int length, out string value) if (((_inBytesUsed + cBytes) > _inBytesRead) || (_inBytesPacket < cBytes)) { - TdsOperationStatus result = TryReadByteArrayWithContinue(cBytes, out buf); + if (_bTmp == null || _bTmp.Length < cBytes) + { + _bTmp = new byte[cBytes]; + } + + TdsOperationStatus result = TryReadByteArray(_bTmp, cBytes); if (result != TdsOperationStatus.Done) { value = null; return result; } + // assign local to point to parser scratch buffer + buf = _bTmp; + AssertValidState(); } else @@ -1939,7 +1908,6 @@ internal TdsOperationStatus TryReadStringWithEncoding(int length, System.Text.En } byte[] buf = null; int offset = 0; - (bool canContinue, bool isStarting, bool isContinuing) = GetSnapshotStatuses(); if (isPlp) { @@ -1956,40 +1924,21 @@ internal TdsOperationStatus TryReadStringWithEncoding(int length, System.Text.En { if (((_inBytesUsed + length) > _inBytesRead) || (_inBytesPacket < length)) { - int startOffset = 0; - if (canContinue) - { - if (isContinuing || isStarting) - { - buf = TryTakeSnapshotStorage() as byte[]; - Debug.Assert(buf == null || buf.Length == length, "stored buffer length must be null or must have been created with the correct length"); - } - if (buf != null) - { - startOffset = GetSnapshotTotalSize(); - } - } - - if (buf == null || buf.Length < length) + if (_bTmp == null || _bTmp.Length < length) { - buf = new byte[length]; + _bTmp = new byte[length]; } - TdsOperationStatus result = TryReadByteArray(buf, length, out _, startOffset, canContinue); - + TdsOperationStatus result = TryReadByteArray(_bTmp, length); if (result != TdsOperationStatus.Done) { - if (result == TdsOperationStatus.NeedMoreData) - { - if (isStarting || isContinuing) - { - SetSnapshotStorage(buf); - } - } value = null; return result; } + // assign local to point to parser scratch buffer + buf = _bTmp; + AssertValidState(); } else @@ -2103,27 +2052,17 @@ internal int ReadPlpBytesChunk(byte[] buff, int offset, int len) return value; } - internal TdsOperationStatus TryReadPlpBytes(ref byte[] buff, int offset, int len, out int totalBytesRead) - { - bool canContinue = false; - bool isStarting = false; - bool isContinuing = false; - bool compatibilityMode = LocalAppContextSwitches.UseCompatibilityAsyncBehaviour; - if (!compatibilityMode) - { - (canContinue, isStarting, isContinuing) = GetSnapshotStatuses(); - } - return TryReadPlpBytes(ref buff, offset, len, out totalBytesRead, canContinue, canContinue, compatibilityMode); - } // Reads the requested number of bytes from a plp data stream, or the entire data if // requested length is -1 or larger than the actual length of data. First call to this method // should be preceeded by a call to ReadPlpLength or ReadDataLength. // Returns the actual bytes read. // NOTE: This method must be retriable WITHOUT replaying a snapshot // Every time you call this method increment the offset and decrease len by the value of totalBytesRead - internal TdsOperationStatus TryReadPlpBytes(ref byte[] buff, int offset, int len, out int totalBytesRead, bool canContinue, bool writeDataSizeToSnapshot, bool compatibilityMode) + internal TdsOperationStatus TryReadPlpBytes(ref byte[] buff, int offset, int len, out int totalBytesRead) { - totalBytesRead = 0; + int bytesRead; + int bytesLeft; + byte[] newbuf; if (_longlen == 0) { @@ -2141,31 +2080,19 @@ internal TdsOperationStatus TryReadPlpBytes(ref byte[] buff, int offset, int len Debug.Assert(_longlen != TdsEnums.SQL_PLP_NULL, "Out of sync plp read request"); Debug.Assert((buff == null && offset == 0) || (buff.Length >= offset + len), "Invalid length sent to ReadPlpBytes()!"); - int bytesLeft = len; + bytesLeft = len; // If total length is known up front, allocate the whole buffer in one shot instead of realloc'ing and copying over each time if (buff == null && _longlen != TdsEnums.SQL_PLP_UNKNOWNLEN) { - if (compatibilityMode && _snapshot != null && _snapshotStatus != SnapshotStatus.NotActive) - { - // legacy replay path perf optimization - // if there is a snapshot which contains a stored plp buffer take it - // and try to use it if it is the right length - buff = TryTakeSnapshotStorage() as byte[]; - } - else if (writeDataSizeToSnapshot && canContinue && _snapshot != null) + if (_snapshot != null) { - // if there is a snapshot which it contains a stored plp buffer take it + // if there is a snapshot and it contains a stored plp buffer take it // and try to use it if it is the right length - buff = TryTakeSnapshotStorage() as byte[]; - if (buff != null) - { - offset = _snapshot.GetPacketDataOffset(); - totalBytesRead = offset; - } + buff = _snapshot._plpBuffer; + _snapshot._plpBuffer = null; } - if ((ulong)(buff?.Length ?? 0) != _longlen) { // if the buffer is null or the wrong length create one to use @@ -2194,19 +2121,20 @@ internal TdsOperationStatus TryReadPlpBytes(ref byte[] buff, int offset, int len buff = new byte[_longlenleft]; } + totalBytesRead = 0; + while (bytesLeft > 0) { int bytesToRead = (int)Math.Min(_longlenleft, (ulong)bytesLeft); if (buff.Length < (offset + bytesToRead)) { // Grow the array - byte[] newbuf = new byte[offset + bytesToRead]; + newbuf = new byte[offset + bytesToRead]; Buffer.BlockCopy(buff, 0, newbuf, 0, offset); buff = newbuf; - newbuf = null; } - TdsOperationStatus result = TryReadByteArray(buff.AsSpan(offset), bytesToRead, out int bytesRead); + TdsOperationStatus result = TryReadByteArray(buff.AsSpan(offset), bytesToRead, out bytesRead); Debug.Assert(bytesRead <= bytesLeft, "Read more bytes than we needed"); Debug.Assert((ulong)bytesRead <= _longlenleft, "Read more bytes than is available"); @@ -2216,29 +2144,14 @@ internal TdsOperationStatus TryReadPlpBytes(ref byte[] buff, int offset, int len _longlenleft -= (ulong)bytesRead; if (result != TdsOperationStatus.Done) { - if (compatibilityMode && _snapshot != null) - { - // legacy replay path perf optimization - // a partial read has happened so store the target buffer in the snapshot - // so it can be re-used when another packet arrives and we read again - SetSnapshotStorage(buff); - } - else if (canContinue) + if (_snapshot != null) { // a partial read has happened so store the target buffer in the snapshot // so it can be re-used when another packet arrives and we read again - SetSnapshotStorage(buff); - if (writeDataSizeToSnapshot) - { - SetSnapshotDataSize(bytesRead); - } + _snapshot._plpBuffer = buff; } return result; } - if (writeDataSizeToSnapshot && canContinue) - { - SetSnapshotDataSize(bytesRead); - } if (_longlenleft == 0) { @@ -2246,19 +2159,11 @@ internal TdsOperationStatus TryReadPlpBytes(ref byte[] buff, int offset, int len result = TryReadPlpLength(false, out _); if (result != TdsOperationStatus.Done) { - if (compatibilityMode && _snapshot != null) + if (_snapshot != null) { // a partial read has happened so store the target buffer in the snapshot // so it can be re-used when another packet arrives and we read again - SetSnapshotStorage(buff); - } - else if (canContinue && result == TdsOperationStatus.NeedMoreData) - { - SetSnapshotStorage(buff); - if (writeDataSizeToSnapshot) - { - SetSnapshotDataSize(bytesRead); - } + _snapshot._plpBuffer = buff; } return result; } @@ -2268,9 +2173,7 @@ internal TdsOperationStatus TryReadPlpBytes(ref byte[] buff, int offset, int len // Catch the point where we read the entire plp data stream and clean up state if (_longlenleft == 0) // Data read complete - { break; - } } return TdsOperationStatus.Done; } @@ -2704,30 +2607,14 @@ internal TdsOperationStatus TryReadNetworkPacket() TdsOperationStatus result = TdsOperationStatus.InvalidData; if (_snapshot != null) { - if (_snapshotStatus != SnapshotStatus.NotActive) + if (_snapshotReplay) { #if DEBUG - string stackTrace = null; - if (s_checkNetworkPacketRetryStacks) - { - // in debug builds stack traces contain line numbers so if we want to be - // able to compare the stack traces they must all be created in the same - // location in the code - stackTrace = Environment.StackTrace; - } + // in debug builds stack traces contain line numbers so if we want to be + // able to compare the stack traces they must all be created in the same + // location in the code + string stackTrace = Environment.StackTrace; #endif - bool capturedAsContinue = false; - if (_snapshotStatus == SnapshotStatus.ReplayRunning || _snapshotStatus == SnapshotStatus.ReplayStarting) - { - if (_bTmpRead == 0 && _partialHeaderBytesRead == 0 && _longlenleft == 0 && _snapshot.ContinueEnabled) - { - // no temp between packets - // mark this point as continue-able - _snapshot.CaptureAsContinue(this); - capturedAsContinue = true; - } - } - if (_snapshot.MoveNext()) { #if DEBUG @@ -2738,46 +2625,24 @@ internal TdsOperationStatus TryReadNetworkPacket() #endif return TdsOperationStatus.Done; } +#if DEBUG else { -#if DEBUG if (s_checkNetworkPacketRetryStacks) { _lastStack = stackTrace; } -#endif - if (_bTmpRead == 0 && _partialHeaderBytesRead == 0 && _longlenleft == 0 && _snapshot.ContinueEnabled && !capturedAsContinue) - { - // no temp between packets - // mark this point as continue-able - _snapshot.CaptureAsContinue(this); - capturedAsContinue = true; - } } +#endif } // previous buffer is in snapshot _inBuff = new byte[_inBuff.Length]; - result = TdsOperationStatus.NeedMoreData; - } - - if (result == TdsOperationStatus.InvalidData && PartialPacket != null && !PartialPacket.ContainsCompletePacket) - { - result = TdsOperationStatus.NeedMoreData; } if (_syncOverAsync) { ReadSniSyncOverAsync(); - while (_inBytesRead == 0) - { - // a partial packet must have taken the packet data so we - // need to read more data to complete the packet, but we - // can't return NeedMoreData in sync mode so we have to - // spin fetching more data here until we have something - // that the caller can read - ReadSniSyncOverAsync(); - } return TdsOperationStatus.Done; } @@ -2801,10 +2666,7 @@ internal TdsOperationStatus TryReadNetworkPacket() internal void PrepareReplaySnapshot() { _networkPacketTaskSource = null; - if (!_snapshot.MoveToContinue()) - { - _snapshot.MoveToStart(); - } + _snapshot.MoveToStart(); } internal void ReadSniSyncOverAsync() @@ -2815,7 +2677,7 @@ internal void ReadSniSyncOverAsync() } PacketHandle readPacket = default; - bool readFromNetwork = !PartialPacketContainsCompletePacket(); + uint error; RuntimeHelpers.PrepareConstrainedRegions(); @@ -2825,14 +2687,7 @@ internal void ReadSniSyncOverAsync() Interlocked.Increment(ref _readingCount); shouldDecrement = true; - if (readFromNetwork) - { - readPacket = ReadSyncOverAsync(GetTimeoutRemaining(), out error); - } - else - { - error = TdsEnums.SNI_SUCCESS; - } + readPacket = ReadSyncOverAsync(GetTimeoutRemaining(), out error); Interlocked.Decrement(ref _readingCount); shouldDecrement = false; @@ -2846,12 +2701,9 @@ internal void ReadSniSyncOverAsync() { // Success - process results! - if (readFromNetwork) - { - Debug.Assert(!IsPacketEmpty(readPacket), "ReadNetworkPacket cannot be null in synchronous operation!"); - } + Debug.Assert(!IsPacketEmpty(readPacket), "ReadNetworkPacket cannot be null in synchronous operation!"); - ProcessSniPacket(readPacket, TdsEnums.SNI_SUCCESS); + ProcessSniPacket(readPacket, 0); #if DEBUG if (s_forcePendingReadsToWaitForUser) { @@ -2863,12 +2715,9 @@ internal void ReadSniSyncOverAsync() #endif } else - { + { // Failure! - if (readFromNetwork) - { - Debug.Assert(!IsValidPacket(readPacket), "unexpected readPacket without corresponding SNIPacketRelease"); - } + Debug.Assert(!IsValidPacket(readPacket), "unexpected readPacket without corresponding SNIPacketRelease"); ReadSniError(this, error); } @@ -2880,12 +2729,9 @@ internal void ReadSniSyncOverAsync() Interlocked.Decrement(ref _readingCount); } - if (readFromNetwork) + if (!IsPacketEmpty(readPacket)) { - if (!IsPacketEmpty(readPacket)) - { - ReleasePacket(readPacket); - } + ReleasePacket(readPacket); } AssertValidState(); @@ -3109,7 +2955,6 @@ internal void ReadSni(TaskCompletionSource completion) PacketHandle readPacket = default; uint error = 0; - bool readFromNetwork = true; RuntimeHelpers.PrepareConstrainedRegions(); try @@ -3153,35 +2998,21 @@ internal void ReadSni(TaskCompletionSource completion) finally { Interlocked.Increment(ref _readingCount); - try - { - handle = SessionHandle; - readFromNetwork = !PartialPacketContainsCompletePacket(); - if (readFromNetwork) - { - if (!handle.IsNull) - { - IncrementPendingCallbacks(); + handle = SessionHandle; + if (!handle.IsNull) + { + IncrementPendingCallbacks(); - readPacket = ReadAsync(handle, out error); + readPacket = ReadAsync(handle, out error); - if (!(TdsEnums.SNI_SUCCESS == error || TdsEnums.SNI_SUCCESS_IO_PENDING == error)) - { - DecrementPendingCallbacks(false); // Failure - we won't receive callback! - } - } - } - else + if (!(TdsEnums.SNI_SUCCESS == error || TdsEnums.SNI_SUCCESS_IO_PENDING == error)) { - readPacket = default; - error = TdsEnums.SNI_SUCCESS; + DecrementPendingCallbacks(false); // Failure - we won't receive callback! } } - finally - { - Interlocked.Decrement(ref _readingCount); - } + + Interlocked.Decrement(ref _readingCount); } if (handle.IsNull) @@ -3191,12 +3022,12 @@ internal void ReadSni(TaskCompletionSource completion) if (TdsEnums.SNI_SUCCESS == error) { // Success - process results! - Debug.Assert(!readFromNetwork || IsValidPacket(readPacket) , "ReadNetworkPacket should not have been null on this async operation!"); + Debug.Assert(IsValidPacket(readPacket), "ReadNetworkPacket should not have been null on this async operation!"); // Evaluate this condition for MANAGED_SNI. This may not be needed because the network call is happening Async and only the callback can receive a success. ReadAsyncCallback(IntPtr.Zero, readPacket, 0); // Only release packet for Managed SNI as for Native SNI packet is released in finally block. - if (TdsParserStateObjectFactory.UseManagedSNI && readFromNetwork && !IsPacketEmpty(readPacket)) + if (TdsParserStateObjectFactory.UseManagedSNI && !IsPacketEmpty(readPacket)) { ReleasePacket(readPacket); } @@ -3234,7 +3065,7 @@ internal void ReadSni(TaskCompletionSource completion) { if (!TdsParserStateObjectFactory.UseManagedSNI) { - if (readFromNetwork && !IsPacketEmpty(readPacket)) + if (!IsPacketEmpty(readPacket)) { // Be sure to release packet, otherwise it will be leaked by native. ReleasePacket(readPacket); @@ -3496,9 +3327,8 @@ internal void SetSnapshot() snapshot.Clear(); } _snapshot = snapshot; - Debug.Assert(_snapshot._storage == null); _snapshot.CaptureAsStart(this); - _snapshotStatus = SnapshotStatus.NotActive; + _snapshotReplay = false; } internal void ResetSnapshot() @@ -3507,105 +3337,12 @@ internal void ResetSnapshot() { StateSnapshot snapshot = _snapshot; _snapshot = null; - // TODO(GH-3385): Not sure what this is trying to assert, but it - // currently fails the DataReader tests. - // Debug.Assert(snapshot._storage == null); snapshot.Clear(); Interlocked.CompareExchange(ref _cachedSnapshot, snapshot, null); } - _snapshotStatus = SnapshotStatus.NotActive; - } - - internal bool IsSnapshotAvailable() - { - return _snapshot != null && _snapshot.ContinueEnabled; - } - /// - /// Returns true if the state object is in the state of continuing from a previously stored snapshot packet - /// meaning that consumers should resume from the point where they last needed more data instead of beginning - /// to process packets in the snapshot from the beginning again - /// - /// - internal bool IsSnapshotContinuing() - { - return _snapshot != null && - _snapshot.ContinueEnabled && - _snapshotStatus == TdsParserStateObject.SnapshotStatus.ContinueRunning; - } - - internal (bool CanContinue, bool IsStarting, bool IsContinuing) GetSnapshotStatuses() - { - bool canContinue = _snapshot != null && _snapshot.ContinueEnabled && _snapshotStatus != SnapshotStatus.NotActive; - bool isStarting = false; - bool isContinuing = false; - if (canContinue) - { - isStarting = _snapshotStatus == SnapshotStatus.ReplayStarting; - isContinuing = _snapshotStatus == SnapshotStatus.ContinueRunning; - } - return (canContinue, isStarting, isContinuing); + _snapshotReplay = false; } - internal int GetSnapshotStorageLength() - { - Debug.Assert(_snapshot != null && _snapshot.ContinueEnabled, "should not access snapshot accessor functions without first checking that the snapshot is available"); - return (_snapshot?._storage as IList)?.Count ?? 0; - } - - internal object TryTakeSnapshotStorage() - { - Debug.Assert(_snapshot != null, "should not access snapshot accessor functions without first checking that the snapshot is present"); - object buffer = null; - if (_snapshot != null) - { - buffer = _snapshot._storage; - _snapshot._storage = null; - } - return buffer; - } - - internal void SetSnapshotStorage(object buffer) - { - Debug.Assert(_snapshot != null, "should not access snapshot accessor functions without first checking that the snapshot is available"); - // TODO(GH-3385): Not sure what this is trying to assert, but it - // currently fails the DataReader tests. - // Debug.Assert(_snapshot._storage == null, "should not overwrite snapshot stored buffer"); - if (_snapshot != null) - { - _snapshot._storage = buffer; - } - } - - /// - /// stores the countOfBytesCopiedFromCurrentPacket of bytes copied from the current packet in the snapshot allowing the total - /// countOfBytesCopiedFromCurrentPacket to be calculated - /// - /// - internal void SetSnapshotDataSize(int countOfBytesCopiedFromCurrentPacket) - { - Debug.Assert(_snapshot != null && _snapshot.ContinueEnabled, "_snapshot must exist to store packet data size"); - _snapshot.SetPacketDataSize(countOfBytesCopiedFromCurrentPacket); - } - - internal int GetSnapshotTotalSize() - { - Debug.Assert(_snapshot != null && _snapshot.ContinueEnabled, "_snapshot must exist to read total size"); - Debug.Assert(_snapshotStatus != SnapshotStatus.NotActive, "_snapshot must be active read total size"); - return _snapshot.GetPacketDataOffset(); - } - - internal int GetSnapshotDataSize() - { - Debug.Assert(_snapshot != null && _snapshot.ContinueEnabled, "_snapshot must exist to read packet data size"); - Debug.Assert(_snapshotStatus != SnapshotStatus.NotActive, "_snapshot must be active read packet data size"); - return _snapshot.GetPacketDataSize(); - } - - internal int GetSnapshotPacketID() - { - Debug.Assert(_snapshot != null && _snapshot.ContinueEnabled, "_snapshot must exist to read packet data size"); - return _snapshot.GetPacketID(); - } /// /// Debug Only: Ensures that the TdsParserStateObject has no lingering state and can safely be re-used @@ -3618,7 +3355,7 @@ internal void AssertStateIsClean() if ((parser != null) && (parser.State != TdsParserState.Closed) && (parser.State != TdsParserState.Broken)) { // Async reads - Debug.Assert(_snapshot == null && _snapshotStatus == SnapshotStatus.NotActive, "StateObj has leftover snapshot state"); + Debug.Assert(_snapshot == null && !_snapshotReplay, "StateObj has leftover snapshot state"); Debug.Assert(!_asyncReadWithoutSnapshot, "StateObj has AsyncReadWithoutSnapshot still enabled"); Debug.Assert(_executionContext == null, "StateObj has a stored execution context from an async read"); // Async writes @@ -3711,7 +3448,7 @@ internal void CloneCleanupAltMetaDataSetArray() } } - internal sealed partial class StateSnapshot + sealed partial class StateSnapshot { private sealed partial class PacketData { @@ -3720,33 +3457,6 @@ private sealed partial class PacketData public PacketData NextPacket; public PacketData PrevPacket; - /// - /// Stores the data size of the total snapshot so far so that enumeration is not needed - /// to get the offset of the previous packet data in the stored buffer - /// - public int RunningDataSize; - - public int PacketID => Packet.GetIDFromHeader(Buffer.AsSpan(0, TdsEnums.HEADER_LEN)); - - internal int GetPacketDataOffset() - { - int previous = 0; - if (PrevPacket != null) - { - previous = PrevPacket.RunningDataSize; - } - return previous; - } - internal int GetPacketDataSize() - { - int previous = 0; - if (PrevPacket != null) - { - previous = PrevPacket.RunningDataSize; - } - return Math.Max(RunningDataSize - previous, 0); - } - internal void Clear() { Buffer = null; @@ -3757,21 +3467,15 @@ internal void Clear() PrevPacket.NextPacket = null; PrevPacket = null; } - SetDebugStackImpl(null); - SetDebugPacketId(0); - SetDebugDataHash(); + SetDebugStackInternal(null); + SetDebugPacketIdInternal(0); } - internal void SetDebugStack(string value) => SetDebugStackImpl(value); - internal void SetDebugPacketId(int value) => SetDebugPacketIdImpl(value); - internal void SetDebugDataHash() => SetDebugDataHashImpl(); - - internal void CheckDebugDataHash() => CheckDebugDataHashImpl(); + internal void SetDebugStack(string value) => SetDebugStackInternal(value); + internal void SetDebugPacketId(int value) => SetDebugPacketIdInternal(value); - partial void SetDebugStackImpl(string value); - partial void SetDebugPacketIdImpl(int value); - partial void SetDebugDataHashImpl(); - partial void CheckDebugDataHashImpl(); + partial void SetDebugStackInternal(string value); + partial void SetDebugPacketIdInternal(int value); } #if DEBUG @@ -3793,189 +3497,84 @@ public PacketDataDebugView(PacketData data) _data = data; } - public string Type { - - get - { - if (_data != null && _data.Buffer!=null) - { - switch (_data.Buffer[0]) - { - case 1: return nameof(TdsEnums.MT_SQL); - case 2: return nameof(TdsEnums.MT_LOGIN); - case 3: return nameof(TdsEnums.MT_RPC); - case 4: return nameof(TdsEnums.MT_TOKENS); - case 5: return nameof(TdsEnums.MT_BINARY); - case 6: return nameof(TdsEnums.MT_ATTN); - case 7: return nameof(TdsEnums.MT_BULK); - case 8: return nameof(TdsEnums.MT_FEDAUTH); - case 9: return nameof(TdsEnums.MT_CLOSE); - case 10: return nameof(TdsEnums.MT_ERROR); - case 11: return nameof(TdsEnums.MT_ACK); - case 12: return nameof(TdsEnums.MT_ECHO); - case 13: return nameof(TdsEnums.MT_LOGOUT); - case 14: return nameof(TdsEnums.MT_TRANS); - case 15: return nameof(TdsEnums.MT_OLEDB); - case 16: return nameof(TdsEnums.MT_LOGIN7); - case 17: return nameof(TdsEnums.MT_SSPI); - case 18: return nameof(TdsEnums.MT_PRELOGIN); - default: return _data.Buffer[0].ToString("X2"); - } - } - return ""; - } - } - - public string Status + [DebuggerBrowsable(DebuggerBrowsableState.RootHidden)] + public PacketData[] Items { get { - if (_data != null && _data.Buffer != null && _data.Buffer.Length > 1) + PacketData[] items = Array.Empty(); + if (_data != null) { - int status = Packet.GetStatusFromHeader(_data.Buffer); - StringBuilder buffer = new StringBuilder(10); - - if ((status & TdsEnums.ST_EOM) == TdsEnums.ST_EOM) - { - if (buffer.Length > 0) - { - buffer.Append(','); - } - buffer.Append(nameof(TdsEnums.ST_EOM)); - } - if ((status & TdsEnums.ST_AACK) == TdsEnums.ST_AACK) - { - if (buffer.Length > 0) - { - buffer.Append(','); - } - buffer.Append(nameof(TdsEnums.ST_AACK)); - } - if ((status & TdsEnums.ST_BATCH) == TdsEnums.ST_BATCH) - { - if (buffer.Length > 0) - { - buffer.Append(','); - } - buffer.Append(nameof(TdsEnums.ST_BATCH)); - } - if ((status & TdsEnums.ST_RESET_CONNECTION) == TdsEnums.ST_RESET_CONNECTION) + int count = 0; + for (PacketData current = _data; current != null; current = current?.NextPacket) { - if (buffer.Length > 0) - { - buffer.Append(','); - } - buffer.Append(nameof(TdsEnums.ST_RESET_CONNECTION)); + count++; } - if ((status & TdsEnums.ST_RESET_CONNECTION_PRESERVE_TRANSACTION) == TdsEnums.ST_RESET_CONNECTION_PRESERVE_TRANSACTION) + items = new PacketData[count]; + int index = 0; + for (PacketData current = _data; current != null; current = current?.NextPacket, index++) { - if (buffer.Length > 0) - { - buffer.Append(','); - } - buffer.Append(nameof(TdsEnums.ST_RESET_CONNECTION_PRESERVE_TRANSACTION)); + items[index] = current; } - - return buffer.ToString(); } - - return ""; + return items; } } - - public int Length => _data.DataLength; - - public int Spid => _data.SPID; - - public int PacketID => _data.PacketID; - - public ReadOnlySpan HeaderBytes => _data.GetHeaderSpan(); - - public ReadOnlySpan Data => _data.Buffer.AsSpan(TdsEnums.HEADER_LEN); - - public int RunningDataSize => _data.RunningDataSize; - - public PacketData NextPacket => _data.NextPacket; - public PacketData PrevPacket => _data.PrevPacket; } - public int DebugPacketId; + public int PacketId; public string Stack; - public byte[] Hash; - - public int SPID => Packet.GetSpidFromHeader(Buffer.AsSpan(0, TdsEnums.HEADER_LEN)); - - public bool IsEOM => Packet.GetIsEOMFromHeader(Buffer.AsSpan(0, TdsEnums.HEADER_LEN)); - public int DataLength => Packet.GetDataLengthFromHeader(Buffer.AsSpan(0, TdsEnums.HEADER_LEN)); + partial void SetDebugStackInternal(string value) => Stack = value; - public ReadOnlySpan GetHeaderSpan() => Buffer.AsSpan(0, TdsEnums.HEADER_LEN); + partial void SetDebugPacketIdInternal(int value) => PacketId = value; - partial void SetDebugStackImpl(string value) => Stack = value; - partial void SetDebugPacketIdImpl(int value) => DebugPacketId = value; - - partial void SetDebugDataHashImpl() + public override string ToString() { - if (Buffer != null) + //return $"{PacketId}: [{Buffer.Length}] ( {GetPacketDataOffset():D4}, {GetPacketTotalSize():D4} ) {(NextPacket != null ? @"->" : string.Empty)}"; + string byteString = null; + if (Buffer != null && Buffer.Length >= 12) { - using (MD5 hasher = MD5.Create()) + ReadOnlySpan bytes = Buffer.AsSpan(0, 12); + StringBuilder buffer = new StringBuilder(12 * 3 + 10); + buffer.Append('{'); + for (int index = 0; index < bytes.Length; index++) { - Hash = hasher.ComputeHash(Buffer, 0, Read); + buffer.AppendFormat("{0:X2}", bytes[index]); + buffer.Append(", "); } + buffer.Append("..."); + buffer.Append('}'); + byteString = buffer.ToString(); } - else - { - Hash = null; - } - + return $"{PacketId}: [{Read}] {byteString} {(NextPacket != null ? @"->" : string.Empty)}"; } + } +#endif - partial void CheckDebugDataHashImpl() - { - if (Hash == null) - { - if (Buffer != null && Read > 0) - { - throw new InvalidOperationException("Packet modification detected. Hash is null but packet contains non-null buffer"); - } - } - else - { - byte[] checkHash = null; - using (MD5 hasher = MD5.Create()) - { - checkHash = hasher.ComputeHash(Buffer, 0, Read); - } - - for (int index = 0; index < Hash.Length; index++) - { - if (Hash[index] != checkHash[index]) - { - throw new InvalidOperationException("Packet modification detected. Hash from packet creation does not match hash from packet check"); - } - } - } - } + private sealed class PLPData + { + public readonly ulong SnapshotLongLen; + public readonly ulong SnapshotLongLenLeft; - public override string ToString() + public PLPData(ulong snapshotLongLen, ulong snapshotLongLenLeft) { - return $"{PacketID}({GetPacketDataOffset()},{GetPacketDataSize()})"; + SnapshotLongLen = snapshotLongLen; + SnapshotLongLenLeft = snapshotLongLenLeft; } } -#endif private sealed class StateObjectData { private int _inBytesUsed; private int _inBytesPacket; + private PLPData _plpData; private byte _messageStatus; internal NullBitmap _nullBitmapInfo; private _SqlMetaDataSet _cleanupMetaData; internal _SqlMetaDataSetCollection _cleanupAltMetaDataSetArray; private SnapshottedStateFlags _state; - private ulong _longLen; - private ulong _longLenLeft; internal void Capture(TdsParserStateObject stateObj, bool trackStack = true) { @@ -3983,8 +3582,10 @@ internal void Capture(TdsParserStateObject stateObj, bool trackStack = true) _inBytesPacket = stateObj._inBytesPacket; _messageStatus = stateObj._messageStatus; _nullBitmapInfo = stateObj._nullBitmapInfo; // _nullBitmapInfo must be cloned before it is updated - _longLen = stateObj._longlen; - _longLenLeft = stateObj._longlenleft; + if (stateObj._longlen != 0 || stateObj._longlenleft != 0) + { + _plpData = new PLPData(stateObj._longlen, stateObj._longlenleft); + } _cleanupMetaData = stateObj._cleanupMetaData; _cleanupAltMetaDataSetArray = stateObj._cleanupAltMetaDataSetArray; // _cleanupAltMetaDataSetArray must be cloned before it is updated _state = stateObj._snapshottedState; @@ -4004,8 +3605,7 @@ internal void Clear(TdsParserStateObject stateObj, bool trackStack = true) _inBytesPacket = 0; _messageStatus = 0; _nullBitmapInfo = default; - _longLen = 0; - _longLenLeft = 0; + _plpData = null; _cleanupMetaData = null; _cleanupAltMetaDataSetArray = null; _state = SnapshottedStateFlags.None; @@ -4038,26 +3638,24 @@ internal void Restore(TdsParserStateObject stateObj) //else _stateObj._hasOpenResult is already == _snapshotHasOpenResult stateObj._snapshottedState = _state; - // reset plp state - stateObj._longlen = _longLen; - stateObj._longlenleft = _longLenLeft; - // Reset partially read state (these only need to be maintained if doing async without snapshot) stateObj._bTmpRead = 0; stateObj._partialHeaderBytesRead = 0; + + // reset plp state + stateObj._longlen = _plpData?.SnapshotLongLen ?? 0; + stateObj._longlenleft = _plpData?.SnapshotLongLenLeft ?? 0; } } private TdsParserStateObject _stateObj; private StateObjectData _replayStateData; - private StateObjectData _continueStateData; - internal object _storage; + internal byte[] _plpBuffer; private PacketData _lastPacket; private PacketData _firstPacket; private PacketData _current; - private PacketData _continuePacket; private PacketData _sparePacket; #if DEBUG @@ -4100,10 +3698,7 @@ internal void CheckStack(string trace) Debug.Assert(_stateObj._permitReplayStackTraceToDiffer || prev.Stack == trace, "The stack trace on subsequent replays should be the same"); } } - #endif - public bool ContinueEnabled => !LocalAppContextSwitches.UseCompatibilityAsyncBehaviour; - internal void CloneNullBitmapInfo() { if (_stateObj._nullBitmapInfo.ReferenceEquals(_replayStateData?._nullBitmapInfo ?? default)) @@ -4124,19 +3719,10 @@ internal void AppendPacketData(byte[] buffer, int read) { Debug.Assert(buffer != null, "packet data cannot be null"); Debug.Assert(read >= TdsEnums.HEADER_LEN, "minimum packet length is TdsEnums.HEADER_LEN"); - Debug.Assert(TdsEnums.HEADER_LEN + Packet.GetDataLengthFromHeader(buffer) == read, "partially read packets cannot be appended to the snapshot"); #if DEBUG for (PacketData current = _firstPacket; current != null; current = current.NextPacket) { - if (ReferenceEquals(current.Buffer, buffer)) - { - // multiple packets are permitted to be in the same buffer because of partial packets - // but their contents cannot overlap - if ((current.Read + current.DataLength) > read) - { - Debug.Fail("duplicate or overlapping packet appended to snapshot"); - } - } + Debug.Assert(!ReferenceEquals(current.Buffer, buffer)); } #endif PacketData packetData = _sparePacket; @@ -4153,7 +3739,6 @@ internal void AppendPacketData(byte[] buffer, int read) #if DEBUG packetData.SetDebugStack(_stateObj._lastStack); packetData.SetDebugPacketId(Interlocked.Increment(ref _packetCounter)); - packetData.SetDebugDataHash(); #endif if (_firstPacket is null) { @@ -4170,28 +3755,24 @@ internal void AppendPacketData(byte[] buffer, int read) internal bool MoveNext() { bool retval = false; - SnapshotStatus moveToMode = SnapshotStatus.ReplayRunning; bool moved = false; if (_current == null) { _current = _firstPacket; - moveToMode = SnapshotStatus.ReplayStarting; moved = true; } else if (_current.NextPacket != null) { - if (_stateObj._snapshotStatus == SnapshotStatus.ContinueRunning) - { - moveToMode = SnapshotStatus.ContinueRunning; - } _current = _current.NextPacket; moved = true; } if (moved) { - _stateObj.SetBuffer(_current.Buffer, 0, _current.Read); - _stateObj._snapshotStatus = moveToMode; + _stateObj._inBuff = _current.Buffer; + _stateObj._inBytesUsed = 0; + _stateObj._inBytesRead = _current.Read; + _stateObj._snapshotReplay = true; retval = true; } @@ -4207,22 +3788,6 @@ internal void MoveToStart() _stateObj.AssertValidState(); } - internal bool MoveToContinue() - { - if (ContinueEnabled) - { - if (_continuePacket != null && _continuePacket != _current) - { - _continueStateData.Restore(_stateObj); - _stateObj.SetBuffer(_current.Buffer, 0, _current.Read); - _stateObj._snapshotStatus = SnapshotStatus.ContinueRunning; - _stateObj.AssertValidState(); - return true; - } - } - return false; - } - internal void CaptureAsStart(TdsParserStateObject stateObj) { _firstPacket = null; @@ -4232,6 +3797,7 @@ internal void CaptureAsStart(TdsParserStateObject stateObj) _stateObj = stateObj; _replayStateData ??= new StateObjectData(); _replayStateData.Capture(stateObj); + #if DEBUG _rollingPend = 0; _rollingPendCount = 0; @@ -4243,76 +3809,6 @@ internal void CaptureAsStart(TdsParserStateObject stateObj) AppendPacketData(stateObj._inBuff, stateObj._inBytesRead); } - internal void CaptureAsContinue(TdsParserStateObject stateObj) - { - if (ContinueEnabled) - { - Debug.Assert(_stateObj == stateObj); - if (_current is not null) - { - _continueStateData ??= new StateObjectData(); - _continueStateData.Capture(stateObj, trackStack: false); - _continuePacket = _current; - } - } - } - - internal void SetPacketDataSize(int size) - { - PacketData target = _current; - // special case for the start of a snapshot when we expect to have only a single packet - // but have no current packet because we haven't started to replay yet. - if ( - target == null && - _firstPacket != null && - _firstPacket == _lastPacket - ) - { - target = _firstPacket; - } - - if (target == null) - { - throw new InvalidOperationException(); - } - int total = 0; - if (target.PrevPacket != null) - { - total = target.PrevPacket.RunningDataSize; - } - target.RunningDataSize = total + size; - } - - internal int GetPacketDataOffset() - { - int offset = 0; - if (_current != null) - { - offset = _current.GetPacketDataOffset(); - } - return offset; - } - - internal int GetPacketDataSize() - { - int offset = 0; - if (_current != null) - { - offset = _current.GetPacketDataSize(); - } - return offset; - } - - internal int GetPacketID() - { - int id = 0; - if (_current != null) - { - id = _current.PacketID; - } - return id; - } - internal void Clear() { ClearState(); @@ -4324,7 +3820,6 @@ private void ClearPackets() PacketData packet = _firstPacket; _firstPacket = null; _lastPacket = null; - _continuePacket = null; _current = null; packet.Clear(); _sparePacket = packet; @@ -4332,17 +3827,11 @@ private void ClearPackets() private void ClearState() { - // TODO(GH-3385): Not sure what this is trying to assert, but it - // currently fails the DataReader tests. - // Debug.Assert(_storage == null); - _storage = null; _replayStateData.Clear(_stateObj); - _continueStateData?.Clear(_stateObj, trackStack: false); #if DEBUG _rollingPend = 0; _rollingPendCount = 0; _stateObj._lastStack = null; - _packetCounter = 0; #endif _stateObj = null; } diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlTypes/SqlVector.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlTypes/SqlVector.cs index e63a5b7462..cf04ff8636 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlTypes/SqlVector.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlTypes/SqlVector.cs @@ -16,7 +16,7 @@ namespace Microsoft.Data.SqlTypes; /// -public sealed class SqlVector : INullable, ISqlVector +public readonly struct SqlVector : INullable, ISqlVector where T : unmanaged { #region Constants @@ -31,13 +31,13 @@ public sealed class SqlVector : INullable, ISqlVector private readonly byte _elementType; private readonly byte _elementSize; private readonly byte[] _tdsBytes; - + private readonly int _size; + #endregion #region Constructors - /// - public SqlVector(int length) + private SqlVector(int length) { if (length < 0) { @@ -49,13 +49,16 @@ public SqlVector(int length) IsNull = true; Length = length; - Size = TdsEnums.VECTOR_HEADER_SIZE + (_elementSize * Length); + _size = TdsEnums.VECTOR_HEADER_SIZE + (_elementSize * Length); _tdsBytes = Array.Empty(); Memory = new(); } - /// + /// + public static SqlVector CreateNull(int length) => new(length); + + /// public SqlVector(ReadOnlyMemory memory) { (_elementType, _elementSize) = GetTypeFieldsOrThrow(); @@ -63,7 +66,7 @@ public SqlVector(ReadOnlyMemory memory) IsNull = false; Length = memory.Length; - Size = TdsEnums.VECTOR_HEADER_SIZE + (_elementSize * Length); + _size = TdsEnums.VECTOR_HEADER_SIZE + (_elementSize * Length); _tdsBytes = MakeTdsBytes(memory); Memory = memory; @@ -73,7 +76,7 @@ internal SqlVector(byte[] tdsBytes) { (_elementType, _elementSize) = GetTypeFieldsOrThrow(); - (Length, Size) = GetCountsOrThrow(tdsBytes); + (Length, _size) = GetCountsOrThrow(tdsBytes); IsNull = false; @@ -99,18 +102,16 @@ internal string GetString() #region Properties /// - public bool IsNull { get; init; } + public bool IsNull { get; } /// public static SqlVector? Null => null; /// - public int Length { get; init; } - /// - public int Size { get; init; } - + public int Length { get; } + /// - public ReadOnlyMemory Memory { get; init; } + public ReadOnlyMemory Memory { get; } #endregion @@ -118,6 +119,8 @@ internal string GetString() byte ISqlVector.ElementType => _elementType; byte ISqlVector.ElementSize => _elementSize; byte[] ISqlVector.VectorPayload => _tdsBytes; + int ISqlVector.Size => _size; + #endregion #region Helpers @@ -154,7 +157,7 @@ private byte[] MakeTdsBytes(ReadOnlyMemory values) // | Stream of Values | NN * sizeof(T) | [element bytes...] | Raw bytes for vector elements | // +------------------------+-----------------+----------------------+--------------------------------------------------------------+ - byte[] result = new byte[Size]; + byte[] result = new byte[_size]; // Header Bytes result[0] = VecHeaderMagicNo; diff --git a/src/Microsoft.Data.SqlClient/src/Resources/Strings.Designer.cs b/src/Microsoft.Data.SqlClient/src/Resources/Strings.Designer.cs index b41b331db7..9f460e7c04 100644 --- a/src/Microsoft.Data.SqlClient/src/Resources/Strings.Designer.cs +++ b/src/Microsoft.Data.SqlClient/src/Resources/Strings.Designer.cs @@ -9677,6 +9677,15 @@ internal static string SQL_ExRoutingDestination { } } + /// + /// Looks up a localized string similar to: The connection attempt timed out. + /// + internal static string SQL_ConnectTimeout { + get { + return ResourceManager.GetString("SQL_ConnectTimeout", resourceCulture); + } + } + /// /// Looks up a localized string similar to Timeout expired. The connection has been broken as a result.. /// diff --git a/src/Microsoft.Data.SqlClient/src/Resources/Strings.cs.resx b/src/Microsoft.Data.SqlClient/src/Resources/Strings.cs.resx index 889e78d915..7fc79f1934 100644 --- a/src/Microsoft.Data.SqlClient/src/Resources/Strings.cs.resx +++ b/src/Microsoft.Data.SqlClient/src/Resources/Strings.cs.resx @@ -231,18 +231,12 @@ Nelze použít pověření (Credential) s klíčovým slovem Integrated Security, které může být součástí připojovacího řetězce. - - Nelze použít pověření (Credential) s klíčovým slovem Context Connection. - Pokud je v připojovacím řetězci zadané UserID, UID, Password nebo PWD, nejde vlastnost AccessToken nastavit. Pokud je klíčové slovo Integrated Security připojovacího řetězce nastavené na hodnotu true nebo SSPI, nejde vlastnost AccessToken nastavit. - - Vlastnost AccessToken nejde nastavit s klíčovým slovem Context Connection. - Pokud už je nastavená vlastnost Credential, nejde vlastnost AccessToken nastavit. @@ -2073,9 +2067,6 @@ Neplatný posun cílové vyrovnávací paměti (velikost: {0}, posun: {1}) - - Pro řádek nebo sloupec neexistují data. - Číselná hodnota je příliš velká. Nemůže ji obsahovat desítková 96bitová hodnota. @@ -2529,6 +2520,9 @@ Tento příkaz vyžaduje asynchronní připojení. Nastavte v připojovacím řetězci hodnotu Asynchronous Processing=true. + + Časový limit pokusu o připojení vypršel. + Časový limit vypršel. V důsledku toho bylo připojení přerušeno. @@ -2778,30 +2772,6 @@ Délka dat {0} je menší než 0. - - Neplatný pokus o volání metody {0}, pokud je zavřena třída SqlResultSet. - - - Operaci nelze dokončit, protože třída SqlResultSet je zavřena. - - - {0} nelze volat, pokud je záznam určen jen pro čtení. - - - Operaci nelze dokončit, protože záznam je jen pro čtení. - - - Neplatný pokus o volání metody {0}, pokud je aktuální řádek odstraněn. - - - Operaci nelze dokončit, protože aktuální řádek je odstraněn. - - - Operaci nelze dokončit, protože příkaz, který vytvořil objekt SqlResultSet, byl odpojen od původního připojení. Objekt SqlResultSet je uzavřen. - - - SqlResultSet nebylo možné vytvořit pro daný dotaz s požadovanými možnostmi. - {0} nelze volat, pokud je třída SqlDataRecord určena jen pro čtení. @@ -2871,51 +2841,21 @@ Aktuálně načtená knihovna System.Transactions.dll nepodporuje globální transakce. Upgradujte prosím na .NET Framework 4.6.2 nebo novější. - - Dávkové aktualizace nejsou u připojení kontextu podporovány. - - - Jediné další klíčové slovo řetězce připojení, které by bylo možné používat při požadavku na připojení kontextu, je klíčové slovo Type System Version. - - - Připojení kontextu nepodporuje nastavení Type System Version=SQL Server 2000. - Připojení ke kontextu pomocí Microsoft.Data.SqlClient není podporováno. - - Připojení kontextu se už používá. - - - Požadovaná operace vyžaduje kontext SqlClr, který je k dispozici pouze při spuštění v procesu systému Sql Server. - - - Požadovaná operace vyžaduje vlákno prováděného procesu systému Sql Server. Aktuální vlákno bylo spuštěno uživatelským kódem či jiným kódem, který není kódem modulu systému Sql Server. - Vnořené objekty TransactionScopes nejsou podporovány. - - Požadovaná operace není dostupná u připojení kontextu. - - - Upozornění nejsou k dispozici u připojení kontextu. - Neočekávaná událost serveru: {0} - - Uživatelské instance nejsou povoleny při spuštění v procesu systému Sql Server. - Délka argumentu {0} se musí shodovat s délkou argumentu {1}. Hodnota SqlDbType {0} je pro {1} neplatná. Podporuje se pouze {2}. - - U předcházejícího řádku odeslaného do SqlPipe došlo k chybě. Před odesláním dalších dat se musí volat metoda SendResultsEnd. - Příliš mnoho hodnot @@ -3663,9 +3603,6 @@ Tento objekt SqlCommand je již spojen s jiným objektem SqlDependency. - - Objekt SqlDependency nelze vytvořit při spuštění z procesu systému SQL Server. - SQL Server Service Broker pro aktuální databázi není povolen. Nejsou proto podporována upozornění na výsledky dotazů. Chcete-li používat upozornění, povolte zprostředkovatele Service Broker pro tuto databázi. @@ -3726,21 +3663,6 @@ Ve výčtu SqlDataRecord nejsou žádné záznamy. Pokud chcete odeslat parametr vracející tabulku bez řádků, použijte pro hodnotu odkaz s hodnotou null. - - Třída SqlPipe nepodporuje provádění příkazu s připojením, které není připojením kontextu. - - - Délka zprávy {0} překračuje maximální podporovanou délku 4000. - - - Kanál nelze používat, pokud zpracovává jinou operaci. - - - Sada výsledků dotazu je momentálně odesílána do kanálu. Před voláním metody {0} ukončete aktuální sadu výsledků dotazu. - - - Sada výsledků dotazu nebyla inicializována. Před voláním metody {0} volejte metodu SendResultSetStart. - Zprostředkovatel HTTP: @@ -4743,4 +4665,19 @@ Seznam SqlBatchCommand nebyl inicializován. + + Pokus o načtení vektorových dat ze sloupce {0} byl neplatný. Vektory jsou podporovány pouze pro sloupce typu vektor. + + + Nepodporovaný typ vektoru „{0}“. + + + Hodnota null není podporována pro výstupní parametr „{0}“ vektoru SqlDbtype. + + + Byla přijata neplatná hlavička vektoru. + + + {0}: Neplatný řetězec JSON pro vektor. + \ No newline at end of file diff --git a/src/Microsoft.Data.SqlClient/src/Resources/Strings.de.resx b/src/Microsoft.Data.SqlClient/src/Resources/Strings.de.resx index 17adb6d62a..07411316df 100644 --- a/src/Microsoft.Data.SqlClient/src/Resources/Strings.de.resx +++ b/src/Microsoft.Data.SqlClient/src/Resources/Strings.de.resx @@ -231,18 +231,12 @@ Credential kann nicht mit dem Schlüsselwort für Verbindungszeichenfolgen "Integrated Security" verwendet werden. - - Credential kann nicht mit dem Schlüsselwort "Context Connection" verwendet werden. - AccessToken-Eigenschaft kann nicht festgelegt werden, wenn 'UserID', 'UID', 'Password' oder 'PWD' in der Verbindungszeichenfolge angegeben wurde. AccessToken-Eigenschaft kann nicht festgelegt werden, wenn das Schlüsselwort für Verbindungszeichenfolgen 'Integrated Security' auf 'true' oder 'SSPI' gesetzt wurde. - - AccessToken-Eigenschaft kann nicht mit dem Schlüsselwort 'Context Connection' festgelegt werden. - AccessToken-Eigenschaft kann nicht festgelegt werden, wenn bereits die Credential-Eigenschaft festgelegt wurde. @@ -2073,9 +2067,6 @@ Ungültiger Zielpufferoffset (Größe von {0}): {1} - - Keine Daten für die Zeile/Spalte. - Der numerische Wert ist zu groß, um in eine Dezimalzahl mit 96 Bits zu passen. @@ -2529,6 +2520,9 @@ Dieser Befehl erfordert eine asynchrone Verbindung. Legen Sie in der Verbindungszeichenfolge "Asynchronous Processing=true" fest. + + Timeout beim Versuch, eine Verbindung herzustellen. + Timeout abgelaufen. Die Verbindung wurde aus diesem Grund unterbrochen. @@ -2778,30 +2772,6 @@ Datenlänge '{0}' ist kleiner als 0. - - Ungültiger Versuch des Aufrufs von Methode {0}, wenn SqlResultSet geschlossen ist. - - - Operation kann nicht abgeschlossen werden, da SqlResultSet geschlossen ist. - - - '{0}' kann nicht aufgerufen werden, wenn der Datensatz schreibgeschützt ist. - - - Operation kann nicht abgeschlossen werden, da der Datensatz schreibgeschützt ist. - - - Ungültiger Versuch des Aufrufs von Methode {0}, wenn die aktuelle Zeile gelöscht wurde - - - Operation kann nicht abgeschlossen werden, da die aktuelle Zeile gelöscht wurde - - - Operation kann nicht abgeschlossen werden, da der Befehl, der SqlResultSet erstellt hat, von der ursprünglichen Verbindung getrennt wurde. SqlResultSet ist geschlossen. - - - SqlResultSet konnte für die angegebene Abfrage nicht mit den gewünschten Optionen erstellt werden. - '{0}' kann nicht aufgerufen werden, wenn SqlDataRecord schreibgeschützt ist. @@ -2871,51 +2841,21 @@ Die aktuell geladene System.Transactions.dll unterstützt keine globalen Transaktionen. Bitte aktualisieren Sie auf .NET Framework 4.6.2 oder höher. - - Batchupdates werden bei der Kontextverbindung nicht unterstützt. - - - Das einzige zusätzliche Schlüsselwort für die Verbindungszeichenfolge, das beim Anfordern der Kontextverbindung verwendet werden darf, lautet 'Type System Version'. - - - Die Kontextverbindung unterstützt nicht Type System Version=SQL Server 2000. - Das Herstellen einer Verbindung mit der Kontextverbindung über Microsoft.Data.SqlClient wird nicht unterstützt. - - Die Kontextverbindung wird bereits verwendet. - - - Die angeforderte Operation erfordert einen SqlClr -Kontext, der nur bei Ausführung im Sql Server-Prozess verfügbar ist. - - - Für die angeforderte Operation ist ein SQL Server-Ausführungsthread erforderlich. Der aktuelle Thread wurde durch Benutzercode oder einen anderen Nicht-Sql Server-Modulcode gestartet. - Geschachtelte 'TransactionScopes' werden nicht unterstützt. - - Die angeforderte Operation ist für die Kontextverbindung nicht verfügbar. - - - Die Benachrichtigungen sind für die Kontextverbindung nicht verfügbar. - Unerwartetes Serverereignis: {0}. - - Benutzerinstanzen sind bei der Ausführung im Sql Server-Prozess nicht zulässig. - Die Länge von '{0}' muss mit der Länge von '{1}' übereinstimmen. Der 'SqlDbType' '{0}' ist für {1} unzulässig. Unterstützt wird nur {2}. - - Fehler bei einer vorherigen Zeile, die an die SqlPipe gesendet wurde. 'SendResultsEnd' muss aufgerufen werden, bevor eine andere Zeile gesendet werden kann. - Zu viele Werte. @@ -3663,9 +3603,6 @@ Dieses SqlCommand-Objekt ist bereits mit einem anderen SqlDependency-Objekt verknüpft. - - Das 'SqlDependency'-Objekt kann nicht erstellt werden, wenn es im Rahmen des SQL Server-Prozesses ausgeführt wird. - Der SQL Server Service Broker für die aktuelle Datenbank ist nicht aktiviert. Als Folge davon werden keine Abfragebenachrichtigungen unterstützt. Aktivieren Sie den Service Broker für diese Datenbank, wenn Sie Benachrichtigungen verwenden möchten. @@ -3726,21 +3663,6 @@ Es sind keine Datensätze in der 'SqlDataRecord'-Enumeration vorhanden. Verwenden Sie zum Senden eines table-valued parameters ohne Zeilen stattdessen einen null-Verweis für den Wert. - - 'SqlPipe' unterstützt nicht die Ausführung eines Befehls bei einer Nicht-Kontextverbindung. - - - Die Nachrichtenlänge {0} überschreitet die zulässige Länge von 4.000. - - - Die Pipe konnte nicht verwendet werden, da sie zurzeit eine andere Operation ausführt. - - - Momentan wird ein Resultset an die Pipe gesendet. Beenden Sie das aktuelle Resultset vor dem Aufruf von {0}. - - - Das Resultset wurde nicht initiiert. Rufen Sie 'SendResultSetStart' vor dem Aufruf von {0} auf. - HTTP-Anbieter @@ -4743,4 +4665,19 @@ Die SqlBatchCommand-Liste wurde nicht initialisiert. + + Ungültiger Versuch, Vektordaten aus Spalte „{0}“ abzurufen. Vektoren werden nur für Spalten vom Typ Vektor unterstützt. + + + Nicht unterstützter Vektortyp „{0}“. + + + Der Nullwert wird für den Ausgabeparameter „{0}“ des SqlDbtype-Vektors nicht unterstützt. + + + Ungültiger Vektorheader empfangen. + + + {0} Ungültige JSON-Zeichenfolge für Vektor. + \ No newline at end of file diff --git a/src/Microsoft.Data.SqlClient/src/Resources/Strings.es.resx b/src/Microsoft.Data.SqlClient/src/Resources/Strings.es.resx index c27bac437e..9b537b9e6c 100644 --- a/src/Microsoft.Data.SqlClient/src/Resources/Strings.es.resx +++ b/src/Microsoft.Data.SqlClient/src/Resources/Strings.es.resx @@ -231,18 +231,12 @@ No se pueden utilizar Credential con la palabra clave de cadena de conexión de Integrated Security. - - No se pueden utilizar Credential con la palabra clave de Context Connection. - No se puede establecer la propiedad AccessToken si en la cadena de conexión se han especificado "UserID", "UID", "Password" o "PWD". No se puede establecer la propiedad AccessToken si la palabra clave de la cadena de conexión "Integrated Security" se ha establecido en "true" o "SSPI". - - No se puede establecer la propiedad AccessToken con la palabra clave "Context Connection". - No se puede establecer la propiedad AccessToken si la propiedad Credential ya está establecida. @@ -2073,9 +2067,6 @@ Desplazamiento del búfer de destino (tamaño de {0}) no válido: {1} - - No hay ningún dato disponible para la fila o columna. - El valor numérico es demasiado grande para ajustarlo a un valor decimal de 96 bits. @@ -2529,6 +2520,9 @@ Este comando requiere una conexión asincrónica. Establezca "Asynchronous Processing=true" en la cadena de conexión. + + Se agotó el tiempo de espera del intento de conexión. + Valor de tiempo de espera caducado. Por ello, se ha interrumpido la conexión. @@ -2778,30 +2772,6 @@ La longitud de los datos '{0}' es menor que 0. - - Intento no válido de llamar al método {0} cuando SqlResultSet está cerrado. - - - No se puede completar la operación porque SqlResultSet está cerrado. - - - No se puede llamar a '{0}' cuando el registro es de solo lectura. - - - No se puede completar la operación porque el registro es de solo lectura. - - - Intento no válido de llamar al método {0} cuando se ha eliminado la fila actual - - - No se puede completar la operación porque se ha eliminado la fila actual - - - No se puede completar la operación porque el comando que creó SqlResultSet se ha disociado de la conexión original. SqlResultSet está cerrado. - - - No se puede crear SqlResultSet para la consulta proporcionada con las opciones deseadas. - No se puede llamar a '{0}' cuando SqlDataRecord es de solo lectura. @@ -2871,51 +2841,21 @@ El archivo System.Transactions.dll cargado actualmente no es compatible con las transacciones globales. Actualice a .NET Framework 4.6.2 o posterior. - - La conexión de contexto no admite la actualización de procesamiento por lotes. - - - La palabra clave Type System Version es la única palabra clave de cadena de conexión adicional que se puede emplear al solicitar la conexión de contexto. - - - La conexión de contexto no admite Type System Version=SQL Server 2000. - No se admite la conexión a la conexión de contexto mediante Microsoft.Data.SqlClient. - - La conexión de contexto ya está en uso. - - - La operación solicitada necesita un contexto SqlClr que sólo está disponible cuando se utiliza con el proceso de Sql Server. - - - La operación solicitada necesita un proceso de ejecución de Sql Server. El proceso de ejecución actual comenzó por un código de usuario u otro código de motor de servidor que no es Sql. - No se admiten las TransactionScopes anidadas. - - La operación solicitada no está disponible en la conexión de contexto. - - - Las notificaciones no están disponibles en la conexión de contexto. - Evento de servidor inesperado: {0}. - - No se permiten las instancias de usuario al funcionar dentro de proceso del Sql Server. - La longitud de '{0}' debe coincidir con la longitud de '{1}'. SqlDbType '{0}' no es válido para {1}. Sólo admite {2}. - - Error al enviar la fila anterior a SqlPipe. Se debe llamar a SendResultsEnd antes de poder enviar algo más. - Demasiados valores. @@ -3663,9 +3603,6 @@ Este objeto SqlCommand ya está asociado a otro objeto SqlDependency. - - No se puede crear el objeto SqlDependency al funcionar dentro del proceso de SQL Server. - No está habilitado SQL Server Service Broker para la base de datos actual y, en consecuencia, no se admiten las notificaciones de consulta. Habilite Service Broker en esta base de datos si desea utilizar las notificaciones. @@ -3726,21 +3663,6 @@ No hay registros en la enumeración SqlDataRecord. Para enviar un table-valued parameter sin filas, utilice en su lugar una referencia null para el valor. - - SqlPipe no admite ejecutar un comando con una conexión que no es de contexto. - - - La longitud del mensaje {0} excede la longitud máxima admitida de 4000. - - - No se puede usar la canalización mientras esté ocupado con otra operación. - - - Se está enviando un conjunto de resultados a la canalización. Finalice el conjunto de resultados actual antes de llamar {0}. - - - No se ha iniciado el conjunto de resultados. Llame a SendResultSetStart antes de llamar {0}. - Proveedor HTTP @@ -4743,4 +4665,19 @@ No se ha inicializado la lista SqlBatchCommand. + + Intento no válido de obtener datos vectoriales de la columna '{0}'. Los vectores solo son compatibles con columnas del tipo vector. + + + El tipo de vector "{0}" no es compatible. + + + El valor 'null' no es compatible con el parámetro de salida '{0}' de SqlDbType.Vector. + + + Se recibió un encabezado de vector no válido. + + + Cadena JSON {0} no válida para el vector. + \ No newline at end of file diff --git a/src/Microsoft.Data.SqlClient/src/Resources/Strings.fr.resx b/src/Microsoft.Data.SqlClient/src/Resources/Strings.fr.resx index 3dc712fc5a..108f0a1f0f 100644 --- a/src/Microsoft.Data.SqlClient/src/Resources/Strings.fr.resx +++ b/src/Microsoft.Data.SqlClient/src/Resources/Strings.fr.resx @@ -231,18 +231,12 @@ Impossible d'utiliser de Credential avec le mot-clé de chaîne de connexion de Integrated Security. - - Impossible d'utiliser de Credential avec le mot clé de Context Connection. - Impossible de définir la propriété AccessToken si « UserID », « UID », « Password » ou « PWD » a été spécifié dans la chaîne de connexion. Impossible de définir la propriété AccessToken si le mot clé de chaîne de connexion « Integrated Security » a la valeur « true » ou « SSPI ». - - Impossible de définir la propriété AccessToken avec le mot clé « Context Connection ». - Impossible de définir la propriété AccessToken si la propriété Credential est déjà définie. @@ -2073,9 +2067,6 @@ Mémoire tampon de destination non valide (taille de {0}) offset : {1} - - Aucune donnée n'existe pour la ligne/colonne. - La valeur numérique est trop élevée pour tenir dans un décimal à 96 bits. @@ -2529,6 +2520,9 @@ Cette commande requiert une connexion asynchrone. Définissez "Asynchronous Processing=true" dans la chaîne de connexion. + + La tentative de connexion a expiré. + Le délai d'attente est arrivé à expiration. Par conséquent, la connexion a été interrompue. @@ -2778,30 +2772,6 @@ La longueur des données « {0} » est inférieure à 0. - - Tentative d'appel de la méthode {0} non valide avec SqlResultSet fermé. - - - Opération impossible : le SqlResultSet est fermé. - - - '{0}' ne peut pas être appelé lorsque l'enregistrement est en lecture seule. - - - Opération impossible : l'enregistrement est en lecture seule. - - - Tentative d'appel de la méthode {0} non valide si la ligne actuelle est supprimée - - - Opération impossible : la ligne actuelle a été supprimée - - - Opération impossible : la commande qui a créé le SqlResultSet est dissociée de la connexion d'origine. Le SqlResultSet est fermé. - - - Impossible de créer le SqlResultSet pour la requête fournie avec les options souhaitées. - Impossible d'appeler '{0}' si le SqlDataRecord est en lecture seule. @@ -2871,51 +2841,21 @@ Le fichier System.Transactions.dll actuellement chargé ne prend pas en charge les transactions globales. Effectuez une mise à niveau vers .NET Framework 4.6.2 ou ultérieur. - - Le traitement par lots des mises à jour n'est pas pris en charge sur la connexion du contexte. - - - Le seul mot clé de chaîne de connexion supplémentaire qui peut être utilisé lors de la demande de la connexion du contexte est le mot clé Type System Version. - - - La connexion du contexte ne prend pas en charge Type System Version=SQL Server 2000. - La connexion à la connexion de contexte à l’aide de Microsoft.Data.SqlClient n’est pas prise en charge. - - La connexion du contexte est déjà en cours d'utilisation. - - - L'opération demandée nécessite un contexte SqlClr, qui n'est disponible qu'en cas d'exécution dans le processus Sql Server. - - - L'opération demandée nécessite un thread d'exécution Sql Server. Le thread en cours a été démarré par code d'utilisateur ou autre code de moteur non-Sql Server. - Les TransactionScopes imbriquées ne sont pas prises en charge. - - L'opération demandée n'est pas disponible sur la connexion du contexte. - - - Les notifications ne sont pas disponibles sur la connexion du contexte. - Événement serveur inattendu : {0}. - - Les instances d'utilisateur ne sont pas autorisées en cas d'exécution dans le processus Sql Server. - La longueur de '{0}' doit correspondre à celle de '{1}'. Le type SqlDbType '{0}' n'est pas valide pour {1}. Seul {2} est pris en charge. - - Une erreur s’est produite avec une ligne précédente envoyée au SqlPipe. SendResultsEnd doit être appelé avant que quoi que ce soit d’autre puisse être envoyé. - Trop de valeurs. @@ -3663,9 +3603,6 @@ Cet objet SqlCommand est déjà associé à un autre objet SqlDependency. - - L'objet SqlDependency ne peut pas être créé en cas d'exécution dans le processus SQL Server. - Le SQL Server Service Broker de la base de données actuelle n'est pas activé. Par conséquent, les notifications de requête ne sont pas prises en charge. Activez le Service Broker pour cette base de données si vous souhaitez utiliser des notifications. @@ -3726,21 +3663,6 @@ Il n'existe aucun enregistrement dans l'énumération SqlDataRecord. Pour envoyer un table-valued parameter sans ligne, utilisez plutôt une référence null pour la valeur. - - SqlPipe ne prend pas en charge l'exécution d'une commande avec une connexion qui n'est pas une connexion du contexte. - - - La longueur de message {0} dépasse la longueur maximale prise en charge de 4000. - - - Impossible d'utiliser le canal tant qu'il est occupé par une autre opération. - - - Une série de résultats est actuellement envoyée au canal. Terminez la série des résultats avant d'appeler {0}. - - - La série de résultats n'a pas été initialisée. Appelez SendResultSetStart avant d'appeler {0}. - Fournisseur HTTP @@ -4743,4 +4665,19 @@ La liste SqlBatchCommand n'a pas été initialisée. + + Tentative non valide d’obtention de données vectorielles à partir de la colonne « {0} ». Les vecteurs ne sont pris en charge que pour les colonnes de type vecteur. + + + Type de vecteur « {0} » non pris en charge. + + + Valeur « null » non prise en charge pour le paramètre de sortie « {0} » du vecteur SqlDbtype. + + + En-tête de vecteur non valide reçu. + + + Chaîne JSON {0} non valide pour le vecteur. + \ No newline at end of file diff --git a/src/Microsoft.Data.SqlClient/src/Resources/Strings.it.resx b/src/Microsoft.Data.SqlClient/src/Resources/Strings.it.resx index 65271aee82..d56314dceb 100644 --- a/src/Microsoft.Data.SqlClient/src/Resources/Strings.it.resx +++ b/src/Microsoft.Data.SqlClient/src/Resources/Strings.it.resx @@ -231,18 +231,12 @@ Impossibile utilizzare Credential con la parola chiave per la stringa di connessione Integrated Security. - - Impossibile utilizzare Credential con la parola chiave Context Connection. - Non è possibile impostare la proprietà AccessToken se nella stringa di connessione è stato specificato 'UserID', 'UID', 'Password' o 'PWD'. Non è possibile impostare la proprietà AccessToken se la parola chiave della stringa di connessione 'Integrated Security' è stata impostata su 'true' o 'SSPI'. - - Non è possibile impostare la proprietà AccessToken con la parola chiave 'Context Connection'. - Non è possibile impostare la proprietà AccessToken se la proprietà Credential è già impostata. @@ -2073,9 +2067,6 @@ Offset buffer di destinazione (dimensione {0}) non valido: {1} - - Nessun dato esistente per la riga/colonna. - Il valore numerico non è contenibile in un numero decimale a 96 bit. @@ -2529,6 +2520,9 @@ Il comando richiede una connessione asincrona. Impostare "Asynchronous Processing=true" nella stringa di connessione. + + Timeout del tentativo di connessione. + Timeout scaduto. La connessione è stata interrotta. @@ -2778,30 +2772,6 @@ Lunghezza dei dati '{0}' minore di 0. - - Tentativo non valido di chiamare il metodo {0} quando SqlResultSet è chiuso. - - - Impossibile completare l'operazione, perché SqlResultSet è chiuso. - - - Impossibile chiamare "{0}" se il record è di sola lettura. - - - Impossibile completare l'operazione. Il record è di sola lettura. - - - Tentativo non valido di chiamare il metodo {0} quando la riga corrente è stata eliminata - - - Impossibile completare l'operazione perché la riga corrente è stata eliminata - - - Impossibile completare l'operazione. Il comando che ha creato SqlResultSet è stato dissociato dalla connessione originale. SqlResultSet è chiuso. - - - Impossibile creare SqlResultSet con le opzioni desiderate per la query specificata. - Impossibile chiamare "{0}" quando SqlDataRecord è di sola lettura. @@ -2871,51 +2841,21 @@ Il file System.Transactions.dll attualmente caricato non supporta le transazioni globali. Eseguire l'aggiornamento a .NET Framework 4.6.2 o versione successiva. - - L'esecuzione di batch di aggiornamenti non è supportata nella connessione contesto. - - - L'unica parola chiave per la stringa di connessione supplementare che può essere utilizzata quando si richiede la connessione contesto è Type System Version. - - - La connessione contesto non supporta Type System Version=SQL Server 2000. - La connessione alla connessione di contesto tramite Microsoft.Data.SqlClient non è supportata. - - La connessione contesto è giù in uso. - - - L'operazione richiesta richiede un contesto SqlClr che è disponibile solo quando si opera nel processo Sql Server. - - - L'operazione richiesta richiede un thread di esecuzione Sql Server Il thread corrente è stato avviato da un codice utente o da un altro codice motore diverso da Sql Server. - Non sono supportati TransactionScopes annidati. - - L'operazione richiesta non è disponibile nella connessione contesto. - - - Le notifiche non sono disponibili nella connessione contesto. - Evento server imprevisto: {0}. - - Le istanze utente non sono consentite quando è in funzione il processo Sql Server. - La lunghezza di '{0}' deve corrispondere alla lunghezza di '{1}'. SqlDbType '{0}' non è valido per {1}. Solo {2} è supportato. - - Si è verificato un errore con una riga precedente inviata alla SqlPipe. Prima di poter eseguire altri invii, è necessario chiamare SendResultsEnd. - Troppi valori. @@ -3663,9 +3603,6 @@ L'oggetto SqlCommand è già associato a un altro oggetto SqlDependency. - - Impossibile creare un oggetto SqlDependency durante l'esecuzione nel processo SQL Server. - SQL Server Service Broker non è attivato per il database corrente, pertanto non sono supportate le notifiche di query. Attivare il servizio Service Broker per questo database se si desidera utilizzare le notifiche. @@ -3726,21 +3663,6 @@ Non sono disponibili record nell'enumerazione SqlDataRecord. Utilizzare un riferimento con valore null, per inviare un table-valued parameter senza righe. - - SqlPipe non supporta l'esecuzione di un comando con una connessione diversa da una connessione contesto. - - - La lunghezza del messaggio {0} supera la lunghezza massima supportata di 4000. - - - Impossibile utilizzare la pipe se è occupata con un'altra operazione. - - - Si sta inviando alla pipe un set di risultati. Terminare il set di risultati corrente prima di chiamare {0}. - - - Il set di risultati non è stato iniziato. Chiamare SendResultSetStart prima di chiamare {0}. - Provider HTTP @@ -4743,4 +4665,19 @@ L'elenco SqlBatchCommand non è stato inizializzato. + + Tentativo non valido di ottenere dati vettoriali dalla colonna '{0}'. I vettori sono supportati solo per le colonne di tipo vettore. + + + Tipo di vettore '{0}' non supportato. + + + Il valore 'null' non è supportato per il parametro di output '{0}' di SqlDbtype Vector. + + + Intestazione del vettore non valida ricevuta. + + + {0} Stringa JSON non valida per il vettore. + \ No newline at end of file diff --git a/src/Microsoft.Data.SqlClient/src/Resources/Strings.ja.resx b/src/Microsoft.Data.SqlClient/src/Resources/Strings.ja.resx index 11ffabacb8..311e026244 100644 --- a/src/Microsoft.Data.SqlClient/src/Resources/Strings.ja.resx +++ b/src/Microsoft.Data.SqlClient/src/Resources/Strings.ja.resx @@ -231,18 +231,12 @@ Integrated Security 接続文字列キーワードを含む Credential は使用できません。 - - Context Connection キーワードを含む Credential は使用できません。 - 接続文字列で 'UserID'、'UID'、'Password'、または 'PWD' が指定されている場合は、AccessToken プロパティを設定できません。 'Integrated Security' 接続文字列キーワードが 'true' または 'SSPI' に設定されている場合は、AccessToken プロパティを設定できません。 - - 'Context Connection' キーワードを含む AccessToken プロパティは設定できません。 - Credential プロパティが既に設定されている場合は、AccessToken プロパティを設定できません。 @@ -2073,9 +2067,6 @@ ターゲット バッファー (サイズ {0}) オフセット : {1} が無効です。 - - 行および列にデータが存在しません。 - この数値は 10 進数の 96 ビットに適合させるには大きすぎます。 @@ -2529,6 +2520,9 @@ このコマンドには非同期接続が必要です。接続文字列に "Asynchronous Processing=true" を設定してください。 + + 接続試行がタイムアウトしました。 + タイムアウトに達しました。その結果、接続が切断されました。 @@ -2778,30 +2772,6 @@ データの長さ '{0}' が 0 未満です。 - - SqlResultSet が閉じている場合は、メソッド {0} の呼び出しは無効です。 - - - SqlResultSet が閉じているため、操作を完了できません。 - - - レコードが読み取り専用の場合は、'{0}' を呼び出すことはできません。 - - - レコードが読み取り専用のため、操作を完了できません。 - - - 現在の行が削除されている場合は、メソッド {0} の呼び出しは無効です。 - - - 現在の行が削除されているため、操作を完了できません。 - - - SqlResultSet を作成したコマンドが元の接続から切断されているため、操作を完了できません。SqlResultSet は閉じています。 - - - 要求されたオプションを使用して指定されたクエリの SqlResultSet を作成できません。 - SqlDataRecord が読み取り専用の場合は、'{0}' を呼び出すことはできません。 @@ -2871,51 +2841,21 @@ 現在読み込まれている System.Transactions.dll では、グローバル トランザクションはサポートされていません。.NET Framework 4.6.2 またはそれ以降にアップグレードしてください。 - - コンテキスト接続では、更新のバッチ処理はサポートされていません。 - - - コンテキスト接続を要求する際に、この他に使用される唯一の接続文字列キーワードは Type System Version キーワードです。 - - - コンテキスト接続は Type System Version=SQL Server 2000 をサポートしていません。 - Microsoft.Data.SqlClient を使用したコンテキスト接続への接続はサポートされていません。 - - コンテキスト接続は既に使用されています。 - - - 要求された操作には、SqlClr コンテキストが必要です。このコンテキストは、Sql Server プロセス内で実行する場合にのみ使用できます。 - - - 要求された操作には、Sql Server の実行スレッドが必要です。現在のスレッドはユーザー コードまたはその他の non-Sql Server エンジン コードによって開始されました。 - TransactionScopes を入れ子にすることはできません。 - - 要求された操作をコンテキスト接続で行うことはできません。 - - - コンテキスト接続で通知を使用することはできません。 - サーバーの予期しないイベント : {0}。 - - Sql Server プロセスで実行している場合は、ユーザー インスタンスは許可されません。 - '{0}' と '{1}' の長さは同じである必要があります。 {1} に対し、SqlDbType '{0}' は無効です。サポートしているのは {2} のみです。 - - SqlPipe に送信された前の行でエラーが発生しました。他のものを送信する前に SendResultsEnd を呼び出す必要があります。 - 値が多すぎます。 @@ -3663,9 +3603,6 @@ この SqlCommand オブジェクトは、既に別の SqlDependency オブジェクトに関連付けられています。 - - SQL Server プロセス内で実行されている場合、SqlDependency オブジェクトは作成できません。 - 現在のデータベースの SQL Server Service Broker は有効になっていません。このため、クエリ通知はサポートされません。通知を利用する場合は、このデータベースの Service Broker を有効にしてください。 @@ -3726,21 +3663,6 @@ SqlDataRecord 列挙にレコードがありません。行のない table-valued parameter を送信したい場合は、代わりにその値の null 参照を使用します。 - - SqlPipe では、コンテキスト接続ではない接続を使用してコマンドを実行することはできません。 - - - メッセージの長さ {0} が、サポートしている最大長である 4000 を超えています。 - - - 別の操作でパイプがビジー状態である場合、このパイプを使用できない可能性があります。 - - - 結果セットをパイプに送信しています。{0} を呼び出す前に現在の結果セットを終了してください。 - - - 結果セットが初期化されていません。{0} を呼び出す前に SendResultSetStart を呼び出してください。 - HTTP プロバイダー @@ -4743,4 +4665,19 @@ SqlBatchCommand リストが初期化されていません。 + + 列 '{0}' からベクター データを取得しようとしましたが無効です。ベクターは、ベクター型の列でのみサポートされます。 + + + サポートされていないベクター型 '{0}'。 + + + SqlDbtype ベクターの出力パラメーター '{0}' では、'null' 値はサポートされていません。 + + + 無効なベクター ヘッダーを受信しました。 + + + {0} ベクターの JSON 文字列が無効です。 + \ No newline at end of file diff --git a/src/Microsoft.Data.SqlClient/src/Resources/Strings.ko.resx b/src/Microsoft.Data.SqlClient/src/Resources/Strings.ko.resx index 5d37a7b8ec..8ed8543b04 100644 --- a/src/Microsoft.Data.SqlClient/src/Resources/Strings.ko.resx +++ b/src/Microsoft.Data.SqlClient/src/Resources/Strings.ko.resx @@ -231,18 +231,12 @@ Integrated Security 연결 문자열 키워드가 포함된 Credential을 사용할 수 없습니다. - - Context Connection 키워드가 포함된 Credential을 사용할 수 없습니다. - 연결 문자열에서 'UserID', 'UID', 'Password' 또는 'PWD'를 지정한 경우 AccessToken 속성을 설정할 수 없습니다. 'Integrated Security' 연결 문자열 키워드가 'true' 또는 'SSPI'로 설정된 경우 AccessToken 속성을 설정할 수 없습니다. - - 'Context Connection' 키워드를 사용하여 AccessToken 속성을 지정할 수 없습니다. - Credential 속성이 이미 설정된 경우 AccessToken 속성을 설정할 수 없습니다. @@ -2073,9 +2067,6 @@ 대상 버퍼(크기: {0}) 오프셋이 잘못되었습니다. {1} - - 행/열에 대한 데이터가 없습니다. - 숫자 값이 너무 커서 96비트 10진수에 맞지 않습니다. @@ -2529,6 +2520,9 @@ 이 명령을 사용하려면 비동기 연결이 필요합니다. 연결 문자열에서 "Asynchronous Processing=true"로 설정하십시오. + + 연결 시도 시간이 초과되었습니다. + 제한 시간이 만료되었으므로 연결이 끊겼습니다. @@ -2778,30 +2772,6 @@ 데이터 길이 '{0}'은(는) 0보다 작습니다. - - SqlResultSet이 닫혀 있으면 메서드 {0}을(를) 호출할 수 없습니다. - - - SqlResultSet이 닫혀 있으므로 작업을 완료할 수 없습니다. - - - 레코드가 읽기 전용이면 '{0}'을(를) 호출할 수 없습니다. - - - 레코드가 읽기 전용이므로 작업을 완료할 수 없습니다. - - - 현재 행이 삭제되면 메서드 {0}을(를) 호출할 수 없습니다. - - - 현재 행이 삭제되었으므로 작업을 완료할 수 없습니다. - - - SqlResultSet을 만든 명령이 원래 연결에서 분리되었기 때문에 작업을 완료할 수 없습니다. SqlResultSet이 닫혀 있습니다. - - - 지정된 쿼리에 대한 SqlResultSet을 원하는 옵션으로 만들 수 없습니다. - SqlDataRecord가 읽기 전용이면 '{0}'을(를) 호출할 수 없습니다. @@ -2871,51 +2841,21 @@ 현재 로드된 System.Transactions.dll은 전역 트랜잭션을 지원하지 않습니다. .NET Framework 4.6.2 이상으로 업그레이드하세요. - - 컨텍스트 연결에서는 일괄 처리 업데이트가 지원되지 않습니다. - - - 컨텍스트 연결을 요청할 때 유일하게 추가할 수 있는 연결 문자열 키워드는 Type System Version 키워드입니다. - - - 컨텍스트 연결은 Type System Version=SQL Server 2000을 지원하지 않습니다. - Microsoft.Data.SqlClient를 사용하여 컨텍스트 연결에 연결할 수 없습니다. - - 컨텍스트 연결이 이미 사용 중입니다. - - - 요청한 작업을 수행하려면 Sql Server 프로세스에서 실행 중인 경우에만 사용할 수 있는 SqlClr 컨텍스트가 필요합니다. - - - 요청한 작업을 수행하려면 Sql Server 실행 스레드가 필요합니다. 현재 스레드는 사용자 코드 또는 Sql Server가 아닌 다른 엔진 코드에서 시작했습니다. - 중첩 TransactionScopes는 지원되지 않습니다. - - 요청한 작업은 컨텍스트 연결에서 사용할 수 없습니다. - - - 알림은 컨텍스트 연결에서 사용할 수 없습니다. - 예기치 않은 서버 이벤트가 발생했습니다. {0} - - Sql Server 프로세스에서 실행할 때는 사용자 인스턴스가 허용되지 않습니다. - '{0}'의 길이는 '{1}'의 길이와 일치해야 합니다. SqlDbType '{0}'은(는) {1}에 사용할 수 없습니다. {2}만 지원됩니다. - - SqlPipe로 보낸 이전 행에서 오류가 발생했습니다. 다른 행을 보내려면 SendResultsEnd를 호출해야 합니다. - 값이 너무 많습니다. @@ -3663,9 +3603,6 @@ 이 SqlCommand 개체는 이미 다른 SqlDependency 개체에 연결되어 있습니다. - - SQL Server 프로세스 내에서 실행할 때는 SqlDependency 개체를 만들 수 없습니다. - 현재 데이터베이스에 대해 SQL Server Service Broker가 설정되어 있지 않으므로 쿼리 알림을 사용할 수 없습니다. 알림을 사용하려면 이 데이터베이스에 대해 Service Broker를 설정하십시오. @@ -3726,21 +3663,6 @@ SqlDataRecord 열거형에 레코드가 없습니다. 행이 없는 table-valued parameter를 보내려면 대신 값에 null 참조를 사용하십시오. - - SqlPipe는 컨텍스트 연결이 아닌 다른 연결 상태에서 명령을 실행할 수 없습니다. - - - 메시지 길이 {0}이(가) 지원되는 최대 길이 4000자를 초과했습니다. - - - 다른 작업을 수행 중인 동안에는 파이프를 사용할 수 없습니다. - - - 현재 결과 집합을 파이프로 보내고 있습니다. {0}을(를) 호출하기 전에 현재 결과 집합을 종료하십시오. - - - 결과 집합이 시작되지 않았습니다. {0}을(를) 호출하기 전에 SendResultSetStart를 호출하십시오. - HTTP 공급자 @@ -4743,4 +4665,19 @@ SqlBatchCommand 목록이 초기화되지 않았습니다. + + 열 '{0}'에서 벡터 데이터를 가져오려는 시도가 잘못되었습니다. 벡터는 벡터 형식의 열에서만 지원됩니다. + + + 지원되지 않는 벡터 형식 '{0}'입니다. + + + SqlDbType Vector의 출력 매개변수 '{0}'에는 'null' 값이 지원되지 않습니다. + + + 잘못된 벡터 헤더가 수신되었습니다. + + + {0} 벡터에 대한 JSON 문자열이 잘못되었습니다. + \ No newline at end of file diff --git a/src/Microsoft.Data.SqlClient/src/Resources/Strings.pl.resx b/src/Microsoft.Data.SqlClient/src/Resources/Strings.pl.resx index 8abe8236a1..fd8e76afb9 100644 --- a/src/Microsoft.Data.SqlClient/src/Resources/Strings.pl.resx +++ b/src/Microsoft.Data.SqlClient/src/Resources/Strings.pl.resx @@ -231,18 +231,12 @@ Nie można użyć elementu Credential ze słowem kluczowym parametrów połączenia Integrated Security. - - Nie można użyć elementu Credential ze słowem kluczowym Context Connection. - Nie można ustawić właściwości AccessToken, jeśli w parametrach połączenia określono element UserID, UID, Password lub PWD. Nie można ustawić właściwości AccessToken, jeśli dla słowa kluczowego „Integrated Security” w parametrach połączenia ustawiono wartość true lub SSPI. - - Nie można ustawić właściwości AccessToken ze słowem kluczowym „Context Connection”. - Nie można ustawić właściwości AccessToken, jeśli ustawiono już właściwość Credential. @@ -2073,9 +2067,6 @@ Nieprawidłowe przesunięcie buforu docelowego (rozmiar {0}): {1} - - Nie istnieją dane dla wiersza/kolumny. - Wartość numeryczna jest za duża – nie mieści się w 96-bitowej zmiennej dziesiętnej. @@ -2529,6 +2520,9 @@ To polecenie wymaga połączenia asynchronicznego. Ustaw wartość „Asynchronous Processing=true” w parametrach połączenia. + + Upłynął limit czasu próby połączenia. + Upłynął limit czasu. W wyniku tego połączenie zostało przerwane. @@ -2778,30 +2772,6 @@ Długość danych „{0}” ma wartość mniejszą niż 0. - - Nieprawidłowa próba wywołania metody {0} podjęta, gdy element SqlResultSet jest zamknięty. - - - Nie można ukończyć operacji, ponieważ element SqlResultSet jest zamknięty. - - - Nie można wywołać elementu „{0}”, jeśli rekord jest tylko do odczytu. - - - Nie można ukończyć operacji, ponieważ rekord jest tylko do odczytu. - - - Nieprawidłowa próba wywołania {0} podjęta, gdy bieżący wiersz został usunięty - - - Nie można ukończyć operacji, ponieważ bieżący wiersz został usunięty - - - Nie można ukończyć operacji, ponieważ polecenie, przy użyciu którego został utworzony element SqlResultSet, nie ma już skojarzenia z oryginalnym połączeniem. Element SqlResultSet jest zamknięty. - - - Nie można utworzyć elementu SqlResultSet dla podanego zapytania z żądanymi opcjami. - Nie można wywołać elementu {0}, jeśli element SqlDataRecord jest tylko do odczytu. @@ -2871,51 +2841,21 @@ Aktualnie załadowany System.Transactions.dll nie obsługuje transakcji globalnych. Uaktualnij do programu .NET Framework 4.6.2 lub nowszego. - - Przetwarzanie wsadowe aktualizacji nie jest obsługiwane dla połączenia kontekstu. - - - Jedynym dodatkowym słowem kluczowym parametrach połączenia, które może być używane podczas żądania połączenia kontekstu, jest słowo kluczowe dotyczące wersji systemu (Type System Version). - - - Połączenie kontekstu nie obsługuje wersji systemu Type System Version=SQL Server 2000. - Nawiązywanie połączenia kontekstowego przy użyciu Microsoft.Data.SqlClient nie jest obsługiwane. - - Połączenie kontekstu jest już używane. - - - Żądana operacja wymaga kontekstu SqlClr, który jest dostępny tylko podczas uruchamiania procesu programu Sql Server. - - - Żądana operacja wymaga wątku wykonywania programu Sql Server. Bieżący wątek został uruchomiony przez kod użytkownika lub inny kod aparatu programu innego niż Sql Server. - Zagnieżdżone elementy TransactionScopes nie są obsługiwane. - - Żądana operacja jest niedostępna dla połączenia kontekstu. - - - Powiadomienia nie są dostępne dla połączenia kontekstu. - Nieoczekiwane zdarzenie serwera: {0}. - - Wystąpienia użytkownika nie są dozwolone podczas uruchomienia procesu serwera Sql Server. - Długość {0} musi być zgodna z długością {1}. Element SqlDbType „{0}” jest nieprawidłowy dla {1}. Obsługiwany jest tylko element {2}. - - Wystąpił błąd z poprzednim wierszem wysłanym do elementu SqlPipe. Aby można było wysłać dowolne inne elementy, należy wywołać funkcję SendResultsEnd. - Zbyt wiele wartości. @@ -3663,9 +3603,6 @@ Ten obiekt SqlCommand został już skojarzony z innym obiektem SqlDependency. - - Obiektu SqlDependency nie można utworzyć podczas uruchomienia wewnątrz procesu programu SQL Server. - Usługa Microsoft SQL Server Service Broker dla bieżącej bazy danych nie jest włączony i w wyniku tego powiadomienia o zapytaniach nie są obsługiwane. Włącz usługę Service Broker dla tej bazy danych, jeśli chcesz używać powiadomień. @@ -3726,21 +3663,6 @@ Brak rekordów w wyliczeniu SqlDataRecord. Aby wysłać parametr z wartościami przechowywanymi w tabeli bez wierszy, użyj odwołania o wartości null. - - Element SqlPipe nie obsługuje wykonywania polecenia dla połączenia, które nie jest połączeniem kontekstu. - - - Długość komunikatu {0} przekracza maksymalną obsługiwaną długość wynoszącą 4000. - - - Nie można użyć potoku, gdy jest zajęty inną operacją. - - - Zestaw wyników jest obecnie wysyłany do potoku. Zakończ bieżący zestaw wyników przed wywołaniem {0}. - - - Zestaw wyników nie został zainicjowany. Wywołaj metodę SendResultSetStart przed wywołaniem {0}. - Dostawca HTTP @@ -4743,4 +4665,19 @@ Lista SqlBatchCommand nie została zainicjowana. + + Nieprawidłowa próba pobrania danych wektorowych z kolumny „{0}”. Wektory są obsługiwane tylko dla kolumn wektora typu. + + + Nieobsługiwany typ wektora „{0}”. + + + Wartość „null” nie jest obsługiwana dla parametru wyjściowego „{0}” wektora SqlDbtype. + + + Odebrano nieprawidłowy nagłówek wektora. + + + {0} Nieprawidłowy ciąg JSON wektora. + \ No newline at end of file diff --git a/src/Microsoft.Data.SqlClient/src/Resources/Strings.pt-BR.resx b/src/Microsoft.Data.SqlClient/src/Resources/Strings.pt-BR.resx index e057491201..03c8087d35 100644 --- a/src/Microsoft.Data.SqlClient/src/Resources/Strings.pt-BR.resx +++ b/src/Microsoft.Data.SqlClient/src/Resources/Strings.pt-BR.resx @@ -231,18 +231,12 @@ Não é possível usar a Credential com palavra-chave de cadeia de conexão de Integrated Security. - - Não é possível usar a Credential com palavra-chave de Context Connection. - Não é possível definir a propriedade AccessToken se 'UserID', 'UID', 'Password' ou 'PWD' tiver sido especificado na cadeia de conexão. Não é possível definir a propriedade AccessToken se a palavra-chave da cadeia de conexão 'Integrated Security' estiver sido definida para 'true' ou 'SSPI'. - - Não é possível definir a propriedade AccessToken com a palavra-chave 'Context Connection'. - Não é possível definir a propriedade AccessToken se a propriedade Credential já estiver definida. @@ -2073,9 +2067,6 @@ Deslocamento de buffer de destino inválido (tamanho de {0}): {1} - - Não existem dados para a linha ou coluna. - Valor numérico muito grande para um decimal de 96 bits. @@ -2529,6 +2520,9 @@ Este comando requer uma conexão assíncrona. Defina "Asynchronous Processing=true" na cadeia de conexão. + + A tentativa de conexão atingiu o tempo limite. + Tempo limite expirado. Como resultado a conexão foi quebrada. @@ -2778,30 +2772,6 @@ O comprimento de dados '{0}' é menor que 0. - - Tentativa inválida para chamar o método {0} quando o SqlResultSet está fechado. - - - A operação não pode ser concluída, pois o SqlResultSet está fechado. - - - '{0}' não pode ser chamado quando o registro é somente leitura. - - - A operação não pode ser concluída, pois o registro é somente leitura. - - - Tentativa inválida para chamar o método {0} quando a linha atual foi excluída - - - A operação não foi concluída, pois a linha atual foi excluída - - - A operação não pode ser concluída, pois o comando que criou o SqlResultSet foi dissociado da conexão original. O SqlResultSet está fechado. - - - O SqlResultSet não pôde ser criado para a consulta fornecida com as opções desejadas. - '{0}' não pode ser chamado quando o SqlDataRecord é somente leitura. @@ -2871,51 +2841,21 @@ O System.Transactions.dll carregado no momento não dá suporte a Transações Globais. Atualize para o .NET Framework 4.6.2 ou posterior. - - Não há suporte para as atualizações em lote na conexão de contexto. - - - A única palavra-chave da cadeia de conexão adicional que deve ser usada ao solicitar a conexão de contexto é a palavra-chave Type System Version. - - - Não há suporte para a conexão de contexto Type System Version=SQL Server 2000. - Não há suporte para a conexão de contexto usando Microsoft.Data.SqlClient. - - A conexão de contexto já está em uso. - - - A operação solicitada requer um contexto SqlClr que somente está disponível durante a execução no processo Sql Server. - - - A operação solicitada requer um thread de execução Sql Server. O thread atual foi iniciado pelo código do usuário ou outro código de mecanismo sem ser o Sql Server. - Não há suporte para TransactionScopes aninhados. - - A operação solicitada não está disponível na conexão de contexto. - - - Notificações não estão disponíveis na conexão de contexto. - Evento de servidor inesperado: {0}. - - Instâncias do usuário não são permitidas durante a execução no processo Sql Server. - O comprimento de '{0}' deve corresponder ao comprimento de '{1}'. A SqlDbType '{0}' é inválida para {1}. Há suporte apenas para {2}. - - Ocorreu um erro com uma linha anteriormente enviada para o SqlPipe. O SendResultsEnd deve ser chamado antes de qualquer outro envio. - Excesso de valores. @@ -3663,9 +3603,6 @@ Esse objeto SqlCommand já está associado a outro objeto SqlDependency. - - Não é possível criar o objeto SqlDependency ao executar dentro do processo do SQL Server. - O SQL Server Service Broker do banco de dados atual não está habilitado e, como resultado, não há suporte a notificações de consultas. Habilite o Service Broker para este banco de dados se desejar usar notificações. @@ -3726,21 +3663,6 @@ Não há registros na enumeração SqlDataRecord. Para enviar um table-valued parameter sem linhas, use uma referência null para o valor. - - O SqlPipe não oferece suporte à execução de um comando com uma conexão que não seja conexão de contexto. - - - {0} ultrapassa o tamanho máximo de mensagens aceito de 4.000. - - - Não é possível usar o pipe enquanto ele está ocupado com outra operação. - - - Um conjunto de resultados está sendo enviado para o pipe. Encerre o conjunto de resultados atual antes de chamar {0}. - - - O conjunto de resultados não foi iniciado. Chame SendResultSetStart antes de chamar {0}. - Provedor de HTTP @@ -4743,4 +4665,19 @@ A lista SqlBatchCommand não foi inicializada. + + Tentativa inválida de obter dados vetoriais da coluna '{0}'. Vetores são suportados apenas para colunas do tipo vetor. + + + Tipo de vetor não suportado '{0}'. + + + Valor 'null' não suportado para o parâmetro de saída '{0}' do tipo SqlDbType Vector. + + + Cabeçalho de vetor inválido recebido. + + + {0} Cadeia de caracteres JSON inválida para vetor. + \ No newline at end of file diff --git a/src/Microsoft.Data.SqlClient/src/Resources/Strings.resx b/src/Microsoft.Data.SqlClient/src/Resources/Strings.resx index 90a157876a..71cc24cc56 100644 --- a/src/Microsoft.Data.SqlClient/src/Resources/Strings.resx +++ b/src/Microsoft.Data.SqlClient/src/Resources/Strings.resx @@ -2529,6 +2529,9 @@ This command requires an asynchronous connection. Set "Asynchronous Processing=true" in the connection string. + + The connection attempt timed out. + Timeout expired. The connection has been broken as a result. @@ -4758,4 +4761,4 @@ {0} Invalid JSON string for vector. - \ No newline at end of file + diff --git a/src/Microsoft.Data.SqlClient/src/Resources/Strings.ru.resx b/src/Microsoft.Data.SqlClient/src/Resources/Strings.ru.resx index cedbe8e3cd..89ed2f634d 100644 --- a/src/Microsoft.Data.SqlClient/src/Resources/Strings.ru.resx +++ b/src/Microsoft.Data.SqlClient/src/Resources/Strings.ru.resx @@ -231,18 +231,12 @@ Нельзя использовать слово Credential с ключевым словом строки соединения Integrated Security. - - Нельзя использовать слово Credential с ключевым словом Context Connection. - Невозможно задать свойство AccessToken, если в строке подключения указаны ключевые слова "UserID", "UID", "Password" или "PWD". Невозможно задать свойство AccessToken, если для ключевого слова строки подключения "Integrated Security" заданы значения "true" или "SSPI". - - Невозможно задать свойство AccessToken с ключевым словом "Context Connection". - Невозможно задать свойство AccessToken, если свойство Credential уже задано. @@ -2073,9 +2067,6 @@ Недопустимое смещение в конечном буфере (размер: {0}): {1} - - Отсутствуют данные для строки или столбца. - Численное значение не умещается в 96-разрядном десятичном числе. @@ -2529,6 +2520,9 @@ Для этой команды необходимо асинхронное подключение. Укажите в строке подключения «Asynchronous Processing=true». + + Истекло время ожидания для попытки подключения. + Истекло время ожидания. Подключение разорвано. @@ -2778,30 +2772,6 @@ Длина данных "{0}" меньше 0. - - Недопустимая попытка вызвать метод {0}, когда закрыт SqlResultSet. - - - Невозможно завершить операцию, поскольку SqlResultSet закрыт. - - - Нельзя вызвать "{0}", если запись доступна только для чтения. - - - Невозможно завершить операцию, поскольку запись доступна только для чтения. - - - Недопустимая попытка вызвать метод {0}, когда текущая строка удалена. - - - Невозможно завершить операцию, поскольку текущая строка удалена. - - - Невозможно завершить операцию, поскольку команда, создавшая SqlResultSet, отделена от исходного подключения. SqlResultSet закрыт. - - - Для данного запроса не удалось создать SqlResultSet с нужными параметрами. - Нельзя вызвать "{0}", если SqlDataRecord доступна только для чтения. @@ -2871,51 +2841,21 @@ Загруженная библиотека System.Transactions.dll не поддерживает глобальные транзакции. Перейдите на .NET Framework 4.6.2 или более позднюю версию. - - Группировка обновлений для контекстного подключения не поддерживается. - - - Единственным дополнительным ключевым словом строки подключения, которое может использоваться при запросе контекстного подключения, является ключевое слово Type System Version. - - - Контекстное подключение не поддерживает Type System Version=SQL Server 2000. - Подключение к контексту с помощью Microsoft.Data.SqlClient не поддерживается. - - Контекстное подключение уже используется. - - - Для затребованной операции необходим контекст SqlClr, который доступен только при выполнении в процессе Sql Server. - - - Для затребованной операции необходим поток выполнения Sql Server. Текущий поток был запущен кодом пользователя или другим кодом, который не является кодом модуля Sql Server. - Вложенные TransactionScopes не поддерживаются. - - Для контекстного подключения затребованная операция не доступна. - - - Для контекстного подключения уведомления не доступны. - Недопустимое серверное событие: {0}. - - Пользовательские экземпляры не разрешены при выполнении в процессе Sql Server. - Длина "{0}" должна соответствовать длине "{1}". SqlDbType "{0}" недопустим для {1}. Поддерживается только {2}. - - Произошла ошибка, связанная с предыдущей строкой, отправленной в SqlPipe. Перед отправкой других строк необходимо вызвать SendResultsEnd. - Слишком много значений. @@ -3663,9 +3603,6 @@ Этот объект SqlCommand уже связан с другим объектом SqlDependency. - - Объект SqlDependency не может быть создан при выполнении внутри процесса SQL Server. - SQL Server Service Broker для текущей базы данных не включен, поэтому оповещения не поддерживаются. Включите Service Broker для этой базы данных, чтобы использовать оповещения. @@ -3726,21 +3663,6 @@ В перечислении SqlDataRecord нет записей. Чтобы передать имеющий табличное значение параметр table-valued parameter, который не содержит строк, используйте неопределенную ссылку null на значение. - - SqlPipe не поддерживает выполнения команды с подключением, которое не является контекстным. - - - Длина сообщения {0} превышает максимальное поддерживаемое значение 4000. - - - Не удалось использовать канал, поскольку он занят другой операцией. - - - Результирующий набор данных сейчас отправляется в канал. Перед вызовом {0} необходимо завершить текущий результирующий набор данных. - - - Результирующий набор данных не инициализирован. Вызовите SendResultSetStart перед вызовом {0}. - Поставщик HTTP @@ -4743,4 +4665,19 @@ Список SqlBatchCommand не инициализирован. + + Недопустимая попытка получить векторные данные из столбца "{0}". Векторы поддерживаются только для столбцов векторного типа. + + + Неподдерживаемый тип вектора "{0}". + + + Значение null не поддерживается для выходного параметра "{0}" вектора SqlDbType. + + + Получен недопустимый заголовок вектора. + + + {0} Недопустимая строка JSON для вектора. + \ No newline at end of file diff --git a/src/Microsoft.Data.SqlClient/src/Resources/Strings.tr.resx b/src/Microsoft.Data.SqlClient/src/Resources/Strings.tr.resx index 15b19580eb..e8b2fcd99d 100644 --- a/src/Microsoft.Data.SqlClient/src/Resources/Strings.tr.resx +++ b/src/Microsoft.Data.SqlClient/src/Resources/Strings.tr.resx @@ -231,18 +231,12 @@ Integrated Security anahtar sözcüğüyle Credential kullanılamaz. - - Context Connection anahtar sözcüğüyle Credential kullanılamaz. - 'UserID', 'UID', 'Password' veya 'PWD' bağlantı dizesinde belirtilmişse AccessToken özelliği ayarlanamaz. 'Integrated Security' bağlantı dizesi anahtar sözcüğü 'true' veya 'SSPI' olarak ayarlanmışsa AccessToken özelliği ayarlanamaz. - - 'Context Connection' anahtar sözcüğü ile AccessToken özelliği ayarlanamaz. - Credential özelliği zaten ayarlanmışsa AccessToken özelliği ayarlanamaz. @@ -2073,9 +2067,6 @@ Geçersiz hedef arabellek (boyutu {0}) uzaklığı: {1} - - Satır/sütun için hiç veri yok. - Sayı değeri 96 bit ondalık basamağa sığmayacak kadar büyük. @@ -2529,6 +2520,9 @@ Bu komut, zaman uyumsuz bağlantı gerektiriyor. Bağlantı dizesinde “Asynchronous Processing=true” ayarını yapın. + + Bağlantı denemesi zaman aşımına uğradı. + Zaman aşımı süresi sona erdi. Sonuç olarak bağlantı kesildi. @@ -2778,30 +2772,6 @@ Veri uzunluğu '{0}' 0'dan küçük. - - SqlResultSet kapalıyken geçersiz bir {0} metodu çağırma girişimi. - - - SqlResultSet kapalı olduğundan işlem tamamlanamıyor. - - - Kayıt salt okunur olduğunda '{0}' çağrılamaz. - - - Kayıt salt okunur olduğundan işlem tamamlanamıyor. - - - Geçerli satır silinmişken geçersiz bir {0} metodunu çağırma girişimi - - - Geçerli satır silindiğinden işlem tamamlanamıyor - - - SqlResultSet'i oluşturan komutun özgün bağlantıyla ilişkisi kesildiği için işlem tamamlanamıyor. SqlResultSet kapalı. - - - SqlResultSet, verilen sorgu için istenen seçeneklerle oluşturulamadı. - SqlDataRecord salt okunur olduğunda '{0}' çağrılamaz. @@ -2871,51 +2841,21 @@ Şu anda yüklü olan System.Transactions.dll, Genel İşlemleri desteklemiyor. Lütfen .NET Framework 4.6.2 veya sonraki bir sürüme yükseltin. - - Güncelleştirmelerin toplu işlenmesi, içerik bağlantısında desteklenmiyor. - - - İçerik bağlantısı istenirken kullanılabilecek tek ek bağlantı dizesi anahtar sözcüğü, Type System Version anahtar sözcüğüdür. - - - İçerik bağlantısı, Type System Version=SQL Server 2000 desteklemiyor. - Microsoft.Data.SqlClient kullanarak bağlam bağlantısına bağlanma desteklenmiyor. - - İçerik bağlantısı zaten kullanılıyor. - - - İstenen işlem, yalnızca Sql Server işleminde çalışılırken kullanılabilen bir SqlClr içeriği gerektiriyor. - - - İstenen işlem bir Sql Server yürütme iş parçacığı gerektirir. Geçerli iş parçacığı kullanıcı kodu veya diğer Sql Server motoru olmayan kodlar tarafından başlatılmıştır. - İç içe TransactionScopes desteklenmiyor. - - İstenen işlem içerik bağlantısında kullanılamıyor. - - - İçerik bağlantısında bildirim kullanılamıyor. - Beklenmeyen sunucu olayı: {0}. - - Sql Server işleminde çalışırken kullanıcı örneklerine izin verilmez. - ‘{0}’ uzunluğu, ‘{1}’ uzunluğu ile aynı olmalıdır. SqlDbType '{0}' {1} için geçersiz. Yalnızca {2} desteklenir. - - SqlPipe'a gönderilen önceki satırla ilgili bir hata oluştu. Başka bir şey gönderilmeden önce SendResultsEnd çağrılmalıdır. - Çok fazla değer. @@ -3663,9 +3603,6 @@ Bu SqlCommand nesnesi, zaten başka bir SqlDependency nesnesiyle ilişkilendirildi. - - SqlDependency nesnesi, SQL Server işleminde çalışılırken oluşturulamaz. - Geçerli veritabanı için SQL Server Hizmet Aracısı etkin değil ve sonuç olarak sorgu bildirimleri desteklenmiyor. Bildirimleri kullanmak istiyorsanız lütfen bu veritabanı için Hizmet Aracısını etkinleştirin. @@ -3726,21 +3663,6 @@ SqlDataRecord numaralandırması içinde kayıt yok. Satır içermeyen tablo değerli bir parametre göndermek için değer olarak bunun yerine bir null başvuru kullanın. - - SqlPipe, içerik bağlantısı olmayan bir bağlantıyla komut yürütmeyi desteklemez. - - - İleti uzunluğu {0}, desteklenen en fazla uzunluk olan 4000'i aşıyor. - - - Kanal, başka bir işlemle meşgulken kullanılamadı. - - - Şu anda kanala bir sonuç kümesi gönderiliyor. {0}‘i çağırmadan önce geçerli sonuç kümesini sonlandırın. - - - Sonuç kümesi başlatılmadı. {0}'i çağırmadan önce SendResultSetStart'ı çağırın. - HTTP Sağlayıcısı @@ -4743,4 +4665,19 @@ SqlBatchCommand listesi başlatılmadı. + + '{0}' sütunundan vektör verilerini alma denemesi geçersiz. Vektörler yalnızca vektör türündeki sütunlar için desteklenir. + + + '{0}' vektör türü desteklenmiyor. + + + SqlDbType Vector'ın '{0}' çıkış parametresi için 'null' değeri desteklenmiyor. + + + Geçersiz vektör başlığı alındı. + + + Vektör JSON dizesi {0} geçersiz. + \ No newline at end of file diff --git a/src/Microsoft.Data.SqlClient/src/Resources/Strings.zh-Hans.resx b/src/Microsoft.Data.SqlClient/src/Resources/Strings.zh-Hans.resx index 8ea45f3b3a..d21b03bca3 100644 --- a/src/Microsoft.Data.SqlClient/src/Resources/Strings.zh-Hans.resx +++ b/src/Microsoft.Data.SqlClient/src/Resources/Strings.zh-Hans.resx @@ -231,18 +231,12 @@ 无法使用含有 Integrated Security 连接字符串关键字的 Credential。 - - 无法使用含有 Context Connection 关键字的 Credential。 - 如果在连接字符串中指定了 "UserID"、"UID"、"Password" 或 "PWD",则无法设置 AccessToken 属性。 如果 "Integrated Security" 连接字符串关键字设置为 "true" 或 "SSPI",则无法设置 AccessToken 属性。 - - 无法设置使用 "Context Connection" 关键字的 AccessToken 属性。 - 如果已设置 Credential 属性,则无法设置 AccessToken 属性。 @@ -2073,9 +2067,6 @@ 无效的目标缓冲区(大小为 {0})偏移量: {1} - - 不存在此行/列的数据。 - 枚举值太大,不能适应 96 位十进制。 @@ -2529,6 +2520,9 @@ 此命令要求异步连接。请在连接字符串中设置“Asynchronous Processing=true”。 + + 连接尝试超时。 + 超时时间已到。因此,连接被中断。 @@ -2778,30 +2772,6 @@ 数据长度“{0}”小于 0。 - - SqlResultSet 关闭时,调用方法 {0} 的尝试无效。 - - - 不能完成操作,因为 SqlResultSet 已关闭。 - - - 记录为只读时不能调用“{0}”。 - - - 不能完成操作,因为记录为只读。 - - - 当前列被删除时,调用方法 {0} 的尝试无效 - - - 不能完成操作,因为当前列已被删除。 - - - 不能完成操作,因为创建 SqlResultSet 的命令已经与原始连接取消关联。SqlResultSet 已关闭。 - - - 不能使用期望选项为给定查询创建 SqlResultSet。 - SqlDataRecord 为只读时,不能调用“{0}”。 @@ -2871,51 +2841,21 @@ 当前加载的 System.Transactions.dll 不支持全局事务。请升级到 .NET Framework 4.6.2 或更高版本。 - - 在上下文连接上不支持批处理更新。 - - - 在请求上下文连接时唯一可以使用的其他连接字符串关键字是 Type System Version 关键字。 - - - 上下文连接不支持 Type System Version=SQL Server 2000。 - 不支持使用 Microsoft.Data.SqlClient 连接到上下文连接。 - - 上下文连接已经在使用。 - - - 所请求的操作需要使用 SqlClr 上下文,该上下文只有在 Sql Server 进程中运行时才可用。 - - - 所请求的操作需要使用 Sql Server 执行线程。当前线程由用户代码或其他非 Sql Server 引擎代码启动。 - 不支持嵌套的 TransactionScopes。 - - 所请求的操作在上下文连接上不可用。 - - - 通知在上下文连接上不可用。 - 意外的服务器事件: {0}。 - - 在 Sql Server 进程中运行时不允许使用用户实例。 - “{0}”的长度必须与“{1}”的长度匹配。 SqlDbType“{0}”对于 {1} 无效。只支持 {2}。 - - 以前发送到 SqlPipe 的某行出错。必须先调用 SendResultsEnd,才能发送其他内容。 - 值太多。 @@ -3663,9 +3603,6 @@ 此 SqlCommand 对象已与另一 SqlDependency 对象关联。 - - 在 SQL Server 进程内运行时,无法创建 SqlDependency 对象。 - 未启用当前数据库的 SQL Server Service Broker,因此查询通知不受支持。如果希望使用通知,请为此数据库启用 Service Broker。 @@ -3726,21 +3663,6 @@ SqlDataRecord 枚举中没有记录。要发送不包含行的 table-valued parameter,请对该值改用 null 引用。 - - SqlPipe 不支持使用非上下文连接执行命令。 - - - 消息长度 {0} 超过支持的最大长度 4000。 - - - 无法使用该管道,该管道正在处理其他操作。 - - - 结果集当前正在发送到该管道。在调用 {0} 之前结束当前结果集。 - - - 结果集尚未启动。在调用 {0} 之前调用 SendResultSetStart。 - HTTP 提供程序 @@ -4743,4 +4665,19 @@ SqlBatchCommand 列表尚未初始化。 + + 从列“{0}”获取矢量数据的尝试无效。仅矢量类型的列支持矢量。 + + + 矢量类型“{0}”不受支持。 + + + SqlDbtype 向量的输出参数“{0}”不支持 "null" 值。 + + + 收到的矢量标头无效。 + + + {0} 矢量的 JSON 字符串无效。 + \ No newline at end of file diff --git a/src/Microsoft.Data.SqlClient/src/Resources/Strings.zh-Hant.resx b/src/Microsoft.Data.SqlClient/src/Resources/Strings.zh-Hant.resx index 545ea55fa4..2f41424cf7 100644 --- a/src/Microsoft.Data.SqlClient/src/Resources/Strings.zh-Hant.resx +++ b/src/Microsoft.Data.SqlClient/src/Resources/Strings.zh-Hant.resx @@ -231,18 +231,12 @@ 無法將 Credential 與 Integrated Security 連接字串關鍵字搭配使用。 - - 無法將 Credential 與 Context Connection 關鍵字搭配使用。 - 如果連接字串中指定了 'UserID'、'UID'、'Password' 或 'PWD',就無法設定 AccessToken 屬性。 如果 'Integrated Security' 連接字串關鍵字已設為 'true' 或 'SSPI',就無法設定 AccessToken 屬性。 - - 無法使用 'Context Connection' 關鍵字設定 AccessToken 屬性。 - 如果已設定 Credential 屬性,就無法設定 AccessToken 屬性。 @@ -2073,9 +2067,6 @@ 無效的目的緩衝區 ({0} 的大小) 位移: {1} - - 資料列/資料行沒有資料。 - 數值太大,超過 96 位元小數。 @@ -2529,6 +2520,9 @@ 此命令需要非同步連接。請在連接字串中設定 "Asynchronous Processing=true"。 + + 連接嘗試逾時。 + 等候逾時已過時。結果為已連接中斷。 @@ -2778,30 +2772,6 @@ 資料長度 '{0}' 小於 0。 - - 當 SqlResultSet 已關閉時,嘗試呼叫方法 {0} 失敗。 - - - 無法完成操作,因為 SqlResultSet 已關閉。 - - - 當記錄為唯讀時無法呼叫 '{0}'。 - - - 無法完成操作,因為記錄為唯讀。 - - - 當目前的資料列已刪除時,嘗試呼叫方法 {0} 無效 - - - 無法完成操作,因為目前的資料列已刪除。 - - - 無法完成操作,因為已經從原始的連接中解除建立 SqlResultSet 命令的關聯。SqlResultSet 已關閉。 - - - 無法建立包含欲查選項指定查詢的 SqlResultSet。 - 當 SqlDataRecord 為唯讀時,無法呼叫 '{0}'。 @@ -2871,51 +2841,21 @@ 目前載入的 System.Transactions.dll 不支援全域交易。請升級為 .NET Framework 4.6.2 或更新版本。 - - 內容連接上並不支援批次更新。 - - - Type System Version 關鍵字是唯一在要求內容連接時可能需要使用的其他連接字串關鍵字。 - - - 此內容連接不支援 Type System Version=SQL Server 2000。 - 不支援使用 Microsoft.Data.SqlClient 連線至內容連線。 - - 此內容連接已在使用中。 - - - 要求的操作需要 SqlClr 內容,這是在 Sql Server 程序中執行時唯一可用的內容。 - - - 要求的操作需要 Sql Server 執行緒。目前的執行緒是由使用者編碼或其他非 Sql Server 引擎碼所啟動的。 - 不支援巢狀 TransactionScopes。 - - 在此內容連接上無法使用要求的操作。 - - - 在此內容連接尚無法使用通知。 - 未預期的伺服器事件: {0}。 - - 在 Sql Server 程序中執行時並不允許使用者執行個體。 - '{0}' 的長度必須和 '{1}' 的長度相符。 SqlDbType '{0}' 對 {1} 是無效的。只支援 {2}。 - - 發生錯誤,已傳送優先資料列至 SqlPipe。在進行其他傳送之前,必須先呼叫 SendResultsEnd。 - 太多數值。 @@ -3663,9 +3603,6 @@ SqlCommand 物件已與其他 SqlDependency 物件關聯。 - - 在 SQL Server 程序中執行時,SqlDependency 物件就不能建立。 - 尚未啟用目前資料庫的 SQL Server Service Broker,因此不支援查詢通知。如果您想要使用通知,請啟用這個資料庫的 Service Broker。 @@ -3726,21 +3663,6 @@ SqlDataRecord 列舉之中沒有記錄。若要傳送沒有資料列的 table-valued parameter,請使用該值的 null 參考來代替。 - - 當某命令的連接不是內容連接時,SqlPipe 不支援該命令的執行。 - - - 訊息長度 {0} 超過支援的最大長度 4000。 - - - 當管線忙碌於其他操作時便無法使用。 - - - 正傳送結果集至管線中。在呼叫 {0} 前請先結束目前的結果集。 - - - 結果集尚未初始化。在呼叫 {0} 前請先呼叫 SendResultSetStart。 - HTTP 提供者 @@ -4743,4 +4665,19 @@ SqlBatchCommand 列表尚未初始化。 + + 從資料行 '{0}' 取得向量資料的嘗試無效。向量僅支援向量類型的資料行。 + + + 不支援的向量類型 '{0}'。 + + + SqlDbType 向量的輸出參數 '{0}' 不支援 'null' 值。 + + + 收到的向量標頭無效。 + + + {0} 向量的 JSON 字串無效。 + \ No newline at end of file diff --git a/src/Microsoft.Data.SqlClient/tests/Common/LocalAppContextSwitchesHelper.cs b/src/Microsoft.Data.SqlClient/tests/Common/LocalAppContextSwitchesHelper.cs index 2970b1f1ce..734a4377fa 100644 --- a/src/Microsoft.Data.SqlClient/tests/Common/LocalAppContextSwitchesHelper.cs +++ b/src/Microsoft.Data.SqlClient/tests/Common/LocalAppContextSwitchesHelper.cs @@ -27,9 +27,8 @@ public sealed class LocalAppContextSwitchesHelper : IDisposable private readonly PropertyInfo _makeReadAsyncBlockingProperty; private readonly PropertyInfo _useMinimumLoginTimeoutProperty; private readonly PropertyInfo _legacyVarTimeZeroScaleBehaviourProperty; - private readonly PropertyInfo _useCompatibilityProcessSniProperty; - private readonly PropertyInfo _useCompatibilityAsyncBehaviourProperty; private readonly PropertyInfo _useConnectionPoolV2Property; + private readonly PropertyInfo _ignoreServerProvidedFailoverPartner; #if NETFRAMEWORK private readonly PropertyInfo _disableTnirByDefaultProperty; #endif @@ -45,16 +44,14 @@ public sealed class LocalAppContextSwitchesHelper : IDisposable private readonly Tristate _useMinimumLoginTimeoutOriginal; private readonly FieldInfo _legacyVarTimeZeroScaleBehaviourField; private readonly Tristate _legacyVarTimeZeroScaleBehaviourOriginal; - private readonly FieldInfo _useCompatibilityProcessSniField; - private readonly Tristate _useCompatibilityProcessSniOriginal; - private readonly FieldInfo _useCompatibilityAsyncBehaviourField; - private readonly Tristate _useCompatibilityAsyncBehaviourOriginal; private readonly FieldInfo _useConnectionPoolV2Field; private readonly Tristate _useConnectionPoolV2Original; - #if NETFRAMEWORK + private readonly FieldInfo _ignoreServerProvidedFailoverPartnerField; + private readonly Tristate _ignoreServerProvidedFailoverPartnerOriginal; +#if NETFRAMEWORK private readonly FieldInfo _disableTnirByDefaultField; private readonly Tristate _disableTnirByDefaultOriginal; - #endif +#endif #endregion @@ -128,18 +125,14 @@ void InitProperty(string name, out PropertyInfo property) "LegacyVarTimeZeroScaleBehaviour", out _legacyVarTimeZeroScaleBehaviourProperty); - InitProperty( - "UseCompatibilityProcessSni", - out _useCompatibilityProcessSniProperty); - - InitProperty( - "UseCompatibilityAsyncBehaviour", - out _useCompatibilityAsyncBehaviourProperty); - InitProperty( "UseConnectionPoolV2", out _useConnectionPoolV2Property); + InitProperty( + "IgnoreServerProvidedFailoverPartner", + out _ignoreServerProvidedFailoverPartner); + #if NETFRAMEWORK InitProperty( "DisableTnirByDefault", @@ -186,21 +179,16 @@ void InitField(string name, out FieldInfo field, out Tristate value) out _legacyVarTimeZeroScaleBehaviourField, out _legacyVarTimeZeroScaleBehaviourOriginal); - InitField( - "s_useCompatibilityProcessSni", - out _useCompatibilityProcessSniField, - out _useCompatibilityProcessSniOriginal); - - InitField( - "s_useCompatibilityAsyncBehaviour", - out _useCompatibilityAsyncBehaviourField, - out _useCompatibilityAsyncBehaviourOriginal); - InitField( "s_useConnectionPoolV2", out _useConnectionPoolV2Field, out _useConnectionPoolV2Original); + InitField( + "s_ignoreServerProvidedFailoverPartner", + out _ignoreServerProvidedFailoverPartnerField, + out _ignoreServerProvidedFailoverPartnerOriginal); + #if NETFRAMEWORK InitField( "s_disableTnirByDefault", @@ -253,18 +241,14 @@ void RestoreField(FieldInfo field, Tristate value) _legacyVarTimeZeroScaleBehaviourField, _legacyVarTimeZeroScaleBehaviourOriginal); - RestoreField( - _useCompatibilityProcessSniField, - _useCompatibilityProcessSniOriginal); - - RestoreField( - _useCompatibilityAsyncBehaviourField, - _useCompatibilityAsyncBehaviourOriginal); - RestoreField( _useConnectionPoolV2Field, _useConnectionPoolV2Original); + RestoreField( + _ignoreServerProvidedFailoverPartnerField, + _ignoreServerProvidedFailoverPartnerOriginal); + #if NETFRAMEWORK RestoreField( _disableTnirByDefaultField, @@ -279,7 +263,7 @@ void RestoreField(FieldInfo field, Tristate value) } } - #endregion +#endregion #region Public Properties @@ -325,23 +309,6 @@ public bool LegacyVarTimeZeroScaleBehaviour get => (bool)_legacyVarTimeZeroScaleBehaviourProperty.GetValue(null); } - /// - /// Access the LocalAppContextSwitches.UseCompatibilityProcessSni property. - /// - public bool UseCompatibilityProcessSni - { - get => (bool)_useCompatibilityProcessSniProperty.GetValue(null); - } - - /// - /// Access the LocalAppContextSwitches.UseCompatibilityAsyncBehaviour - /// property. - /// - public bool UseCompatibilityAsyncBehaviour - { - get => (bool)_useCompatibilityAsyncBehaviourProperty.GetValue(null); - } - /// /// Access the LocalAppContextSwitches.UseConnectionPoolV2 property. /// @@ -414,26 +381,6 @@ public Tristate LegacyVarTimeZeroScaleBehaviourField set => SetValue(_legacyVarTimeZeroScaleBehaviourField, value); } - /// - /// Get or set the LocalAppContextSwitches.UseCompatibilityProcessSni switch - /// value. - /// - public Tristate UseCompatibilityProcessSniField - { - get => GetValue(_useCompatibilityProcessSniField); - set => SetValue(_useCompatibilityProcessSniField, value); - } - - /// - /// Get or set the LocalAppContextSwitches.UseCompatibilityAsyncBehaviour - /// switch value. - /// - public Tristate UseCompatibilityAsyncBehaviourField - { - get => GetValue(_useCompatibilityAsyncBehaviourField); - set => SetValue(_useCompatibilityAsyncBehaviourField, value); - } - /// /// Get or set the LocalAppContextSwitches.UseConnectionPoolV2 switch value. /// @@ -443,6 +390,12 @@ public Tristate UseConnectionPoolV2Field set => SetValue(_useConnectionPoolV2Field, value); } + public Tristate IgnoreServerProvidedFailoverPartnerField + { + get => GetValue(_ignoreServerProvidedFailoverPartnerField); + set => SetValue(_ignoreServerProvidedFailoverPartnerField, value); + } + #if NETFRAMEWORK /// /// Get or set the LocalAppContextSwitches.DisableTnirByDefault switch @@ -455,7 +408,7 @@ public Tristate DisableTnirByDefaultField } #endif - #endregion +#endregion #region Private Helpers diff --git a/src/Microsoft.Data.SqlClient/tests/FunctionalTests/AADAuthenticationTests.cs b/src/Microsoft.Data.SqlClient/tests/FunctionalTests/AADAuthenticationTests.cs index e97b251613..f354e6f806 100644 --- a/src/Microsoft.Data.SqlClient/tests/FunctionalTests/AADAuthenticationTests.cs +++ b/src/Microsoft.Data.SqlClient/tests/FunctionalTests/AADAuthenticationTests.cs @@ -5,7 +5,7 @@ using System; using System.Security; using System.Threading.Tasks; -using Microsoft.Identity.Client; +using Microsoft.Data.SqlClient.FunctionalTests.DataCommon; using Xunit; namespace Microsoft.Data.SqlClient.Tests @@ -49,6 +49,27 @@ private void InvalidCombinationCheck(SqlCredential credential) Assert.Throws(() => connection.AccessToken = "SampleAccessToken"); } } + + #if NETFRAMEWORK + // This test is only valid for .NET Framework + + /// + /// Tests whether SQL Auth provider is overridden using app.config file. + /// This use case is only supported for .NET Framework applications, as driver doesn't support reading configuration from appsettings.json file. + /// In future if need be, appsettings.json support can be added. + /// + [Fact] + public async Task IsDummySqlAuthenticationProviderSetByDefault() + { + var provider = SqlAuthenticationProvider.GetProvider(SqlAuthenticationMethod.ActiveDirectoryInteractive); + + Assert.NotNull(provider); + Assert.Equal(typeof(DummySqlAuthenticationProvider), provider.GetType()); + + var token = await provider.AcquireTokenAsync(null); + Assert.Equal(token.AccessToken, DummySqlAuthenticationProvider.DUMMY_TOKEN_STR); + } + #endif [Fact] public void CustomActiveDirectoryProviderTest() diff --git a/src/Microsoft.Data.SqlClient/tests/FunctionalTests/DataCommon/DummySqlAuthenticationProvider.cs b/src/Microsoft.Data.SqlClient/tests/FunctionalTests/DataCommon/DummySqlAuthenticationProvider.cs new file mode 100644 index 0000000000..bb5e2e2e52 --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/FunctionalTests/DataCommon/DummySqlAuthenticationProvider.cs @@ -0,0 +1,27 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Threading.Tasks; + +namespace Microsoft.Data.SqlClient.FunctionalTests.DataCommon +{ + /// + /// Dummy class to override default Sql Authentication provider in functional tests. + /// This type returns a dummy access token and is only used for registration test from app.config file. + /// Since no actual connections are intended to be made in Functional tests, + /// this type is added by default to validate config file registration scenario. + /// + public class DummySqlAuthenticationProvider : SqlAuthenticationProvider + { + public static string DUMMY_TOKEN_STR = "dummy_access_token"; + + public override Task AcquireTokenAsync(SqlAuthenticationParameters parameters) + => Task.FromResult(new SqlAuthenticationToken(DUMMY_TOKEN_STR, new DateTimeOffset(DateTime.Now.AddHours(2)))); + + // Supported authentication modes don't matter for dummy test, but added to demonstrate config file usage. + public override bool IsSupported(SqlAuthenticationMethod authenticationMethod) + => authenticationMethod == SqlAuthenticationMethod.ActiveDirectoryInteractive; + } +} diff --git a/src/Microsoft.Data.SqlClient/tests/FunctionalTests/LocalAppContextSwitchesTests.cs b/src/Microsoft.Data.SqlClient/tests/FunctionalTests/LocalAppContextSwitchesTests.cs index 170e39a322..30896e545e 100644 --- a/src/Microsoft.Data.SqlClient/tests/FunctionalTests/LocalAppContextSwitchesTests.cs +++ b/src/Microsoft.Data.SqlClient/tests/FunctionalTests/LocalAppContextSwitchesTests.cs @@ -16,8 +16,6 @@ public class LocalAppContextSwitchesTests [InlineData("MakeReadAsyncBlocking", false)] [InlineData("UseMinimumLoginTimeout", true)] [InlineData("LegacyVarTimeZeroScaleBehaviour", true)] - [InlineData("UseCompatibilityProcessSni", false)] - [InlineData("UseCompatibilityAsyncBehaviour", false)] [InlineData("UseConnectionPoolV2", false)] #if NETFRAMEWORK [InlineData("DisableTnirByDefault", false)] diff --git a/src/Microsoft.Data.SqlClient/tests/FunctionalTests/LocalizationTest.cs b/src/Microsoft.Data.SqlClient/tests/FunctionalTests/LocalizationTest.cs index 77e7eee950..3676abb237 100644 --- a/src/Microsoft.Data.SqlClient/tests/FunctionalTests/LocalizationTest.cs +++ b/src/Microsoft.Data.SqlClient/tests/FunctionalTests/LocalizationTest.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Globalization; using System.Threading; +using Microsoft.SqlServer.TDS.Servers; using Xunit; namespace Microsoft.Data.SqlClient.Tests @@ -55,9 +56,11 @@ private string GetLocalizedErrorMessage(string culture) Thread.CurrentThread.CurrentCulture = new CultureInfo(culture); Thread.CurrentThread.CurrentUICulture = new CultureInfo(culture); - using TestTdsServer server = TestTdsServer.StartTestServer(); - var connStr = server.ConnectionString; - connStr = connStr.Replace("localhost", "dummy"); + using TdsServer server = new TdsServer(new TdsServerArguments()); + server.Start(); + var connStr = new SqlConnectionStringBuilder() { + DataSource = $"dummy,{server.EndPoint.Port}" + }.ConnectionString; using SqlConnection connection = new SqlConnection(connStr); try diff --git a/src/Microsoft.Data.SqlClient/tests/FunctionalTests/Microsoft.Data.SqlClient.FunctionalTests.csproj b/src/Microsoft.Data.SqlClient/tests/FunctionalTests/Microsoft.Data.SqlClient.FunctionalTests.csproj index 56265208b4..6bb1c80d72 100644 --- a/src/Microsoft.Data.SqlClient/tests/FunctionalTests/Microsoft.Data.SqlClient.FunctionalTests.csproj +++ b/src/Microsoft.Data.SqlClient/tests/FunctionalTests/Microsoft.Data.SqlClient.FunctionalTests.csproj @@ -24,11 +24,11 @@ + - @@ -39,8 +39,6 @@ - - @@ -66,13 +64,13 @@ - - - - - + + + + Always + @@ -94,6 +92,7 @@ + diff --git a/src/Microsoft.Data.SqlClient/tests/FunctionalTests/MultiplexerTests.cs b/src/Microsoft.Data.SqlClient/tests/FunctionalTests/MultiplexerTests.cs deleted file mode 100644 index 288586fb17..0000000000 --- a/src/Microsoft.Data.SqlClient/tests/FunctionalTests/MultiplexerTests.cs +++ /dev/null @@ -1,724 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -using System; -using System.Buffers.Binary; -using System.Collections.Generic; -using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; -using System.IO; -using System.Linq; -using System.Text; -using System.Text.RegularExpressions; -using Xunit; - -namespace Microsoft.Data.SqlClient.Tests -{ - public class MultiplexerTests - { - public static bool IsUsingCompatibilityProcessSni - { - get - { - if (AppContext.TryGetSwitch(@"Switch.Microsoft.Data.SqlClient.UseCompatibilityProcessSni", out bool foundValue)) - { - return foundValue; - } - return false; - } - } - - public static bool IsUsingModernProcessSni => !IsUsingCompatibilityProcessSni; - - [ExcludeFromCodeCoverage] - public static IEnumerable IsAsync() - { - yield return new object[] { false }; - yield return new object[] { true }; - } - - [ExcludeFromCodeCoverage] - public static IEnumerable OnlyAsync() { yield return new object[] { true }; } - - [ConditionalTheory(nameof(IsUsingModernProcessSni)), MemberData(nameof(IsAsync))] - public static void PassThroughSinglePacket(bool isAsync) - { - int dataSize = 20; - var a = CreatePacket(dataSize, 0xF); - List input = new List { a }; - List expected = new List { a }; - - Assert.Equal(SumPacketLengths(expected), SumPacketLengths(input)); - - var output = MultiplexPacketList(isAsync, dataSize, input); - - ComparePacketLists(dataSize, expected, output); - } - - [ConditionalTheory(nameof(IsUsingModernProcessSni)), MemberData(nameof(IsAsync))] - public static void PassThroughMultiplePacket(bool isAsync) - { - int dataSize = 40; - List input = CreatePackets(dataSize, 5, 6, 7, 8); - List expected = input; - - Assert.Equal(SumPacketLengths(expected), SumPacketLengths(input)); - - var output = MultiplexPacketList(isAsync, dataSize, input); - - ComparePacketLists(dataSize, expected, output); - } - - [ConditionalTheory(nameof(IsUsingModernProcessSni)), MemberData(nameof(IsAsync))] - public static void PassThroughMultiplePacketWithShortEnd(bool isAsync) - { - int dataSize = 40; - List input = CreatePackets((dataSize, 20), 5, 6, 7, 8); - List expected = input; - - Assert.Equal(SumPacketLengths(expected), SumPacketLengths(input)); - - var output = MultiplexPacketList(isAsync, dataSize, input); - - ComparePacketLists(dataSize, expected, output); - } - - [ConditionalTheory(nameof(IsUsingModernProcessSni)), MemberData(nameof(IsAsync))] - public static void ReconstructSinglePacket(bool isAsync) - { - int dataSize = 4; - var a = CreatePacket(dataSize, 0xF); - List input = SplitPacket(a, 6); - List expected = new List { a }; - - Assert.Equal(SumPacketLengths(expected), SumPacketLengths(input)); - - var output = MultiplexPacketList(isAsync, dataSize, input); - - ComparePacketLists(dataSize, expected, output); - } - - [ConditionalTheory(nameof(IsUsingModernProcessSni)), MemberData(nameof(IsAsync))] - public static void Reconstruct2Packets_Part_PartFull(bool isAsync) - { - int dataSize = 4; - var expected = CreatePackets(dataSize, 0xAA, 0xBB); - - var input = SplitPackets(dataSize, expected, - 6, // partial first packet - (6 + 6), // end of packet 0, start of packet 1 - 6 // end of packet 1 - ); - - Assert.Equal(SumPacketLengths(expected), SumPacketLengths(input)); - - var output = MultiplexPacketList(isAsync, dataSize, input); - - ComparePacketLists(dataSize, expected, output); - } - - [ConditionalTheory(nameof(IsUsingModernProcessSni)), MemberData(nameof(IsAsync))] - public static void Reconstruct2Packets_Full_FullPart_Part(bool isAsync) - { - int dataSize = 30; - var expected = new List { CreatePacket(30, 5), CreatePacket(10, 6), CreatePacket(30, 7) }; - - var input = SplitPackets(38, expected, - (8 + 30), // full - (8 + 10) + (8 + 12), // full, part next - 18 // part end - ); - - Assert.Equal(SumPacketLengths(expected), SumPacketLengths(input)); - - var output = MultiplexPacketList(isAsync, dataSize, input); - - ComparePacketLists(dataSize, expected, output); - } - - [ConditionalTheory(nameof(IsUsingModernProcessSni)), MemberData(nameof(IsAsync))] - public static void ReconstructMultiplePacketSequence(bool isAsync) - { - int dataSize = 40; - List expected = CreatePackets(dataSize, 5, 6, 7, 8); - List input = SplitPackets(dataSize, expected, - (8 + 40), - (8 + 23), - (17) + (8 + 23), - (17) + (8 + 23), - (17) - ); - - Assert.Equal(SumPacketLengths(expected), SumPacketLengths(input)); - - var output = MultiplexPacketList(isAsync, dataSize, input); - - ComparePacketLists(dataSize, expected, output); - } - - [ConditionalTheory(nameof(IsUsingModernProcessSni)), MemberData(nameof(IsAsync))] - public static void ReconstructMultiplePacketSequenceWithShortEnd(bool isAsync) - { - int dataSize = 40; - List expected = CreatePackets((dataSize, 20), 5, 6, 7, 8); - List input = SplitPackets(dataSize, expected, - (8 + 40), - (8 + 23), - (17) + (8 + 23), - (17) + (8 + 20) - ); - - Assert.Equal(SumPacketLengths(expected), SumPacketLengths(input)); - - var output = MultiplexPacketList(isAsync, dataSize, input); - - ComparePacketLists(dataSize, expected, output); - } - - [ConditionalTheory(nameof(IsUsingModernProcessSni)), MemberData(nameof(IsAsync))] - public static void Reconstruct3Packets_PartPartPart(bool isAsync) - { - int dataSize = 62; - - var expected = new List { CreatePacket(26, 5), CreatePacket(10, 6), CreatePacket(10, 7) }; - - var input = SplitPackets(70, expected, - (8 + 26) + (8 + 10) + (8 + 10) // = 70: full, full, part - ); - - var output = MultiplexPacketList(isAsync, dataSize, input); - - ComparePacketLists(dataSize, expected, output); - } - - [ConditionalFact(nameof(IsUsingModernProcessSni))] - public static void TrailingPartialPacketInSnapshotNotDuplicated() - { - int dataSize = 120; - - var expected = new List { CreatePacket(120, 5), CreatePacket(90, 6), CreatePacket(13, 7), }; - - var input = SplitPackets(120, expected, - (8 + 120), - (8 + 90) + (8 + 13) - ); - - Assert.Equal(SumPacketLengths(expected), SumPacketLengths(input)); - - var output = MultiplexPacketList(true, dataSize, input); - - ComparePacketLists(dataSize, expected, output); - } - - [ConditionalFact(nameof(IsUsingModernProcessSni))] - public static void BetweenAsyncAttentionPacket() - { - int dataSize = 120; - var normalPacket = CreatePacket(120, 5); - var attentionPacket = CreatePacket(13, 6); - var input = new List { normalPacket, attentionPacket }; - - using var stateObject = new TdsParserStateObject(input, TdsEnums.HEADER_LEN + dataSize, isAsync: true); - - for (int index = 0; index < input.Count; index++) - { - stateObject.Current = input[index]; - stateObject.ProcessSniPacket(default, 0); - } - - Assert.NotNull(stateObject._inBuff); - Assert.Equal(21, stateObject._inBytesRead); - Assert.Equal(0, stateObject._inBytesUsed); - Assert.NotNull(stateObject._snapshot); - Assert.NotNull(stateObject._snapshot.List); - Assert.Equal(2, stateObject._snapshot.List.Count); - - } - - [ConditionalFact(nameof(IsUsingModernProcessSni))] - public static void MultipleFullPacketsInRemainderAreSplitCorrectly() - { - int dataSize = 800 - TdsEnums.HEADER_LEN; - List expected = new List - { - CreatePacket(dataSize, 5), CreatePacket(80, 6), CreatePacket(21, 7) - }; - - - List input = SplitPacket(CombinePackets(expected), 700); - - using var stateObject = new TdsParserStateObject(input, dataSize, isAsync: false); - - var output = MultiplexPacketList(false, dataSize, input); - - ComparePacketLists(dataSize, expected, output); - } - - [ExcludeFromCodeCoverage] - private static List MultiplexPacketList(bool isAsync, int dataSize, List input) - { - using var stateObject = new TdsParserStateObject(input, TdsEnums.HEADER_LEN + dataSize, isAsync); - var output = new List(); - - for (int index = 0; index < input.Count; index++) - { - stateObject.Current = input[index]; - - stateObject.ProcessSniPacket(default, 0); - - if (stateObject._inBytesRead > 0) - { - if ( - stateObject._inBytesRead < TdsEnums.HEADER_LEN - || - stateObject._inBytesRead != (TdsEnums.HEADER_LEN + - Packet.GetDataLengthFromHeader( - stateObject._inBuff.AsSpan(0, TdsEnums.HEADER_LEN))) - ) - { - Assert.Fail("incomplete packet exposed after call to ProcessSniPacket"); - } - - if (!isAsync) - { - output.Add(PacketData.Copy(stateObject._inBuff, stateObject._inBytesUsed, - stateObject._inBytesRead)); - } - } - } - - - if (!isAsync) - { - while (stateObject.PartialPacket != null) - { - stateObject.Current = default; - - stateObject.ProcessSniPacket(default, 0); - - if (stateObject._inBytesRead > 0) - { - if ( - stateObject._inBytesRead < TdsEnums.HEADER_LEN - || - stateObject._inBytesRead != (TdsEnums.HEADER_LEN + - Packet.GetDataLengthFromHeader( - stateObject._inBuff.AsSpan(0, TdsEnums.HEADER_LEN))) - ) - { - Assert.Fail( - "incomplete packet exposed after call to ProcessSniPacket with usePartialPacket"); - } - - output.Add(PacketData.Copy(stateObject._inBuff, stateObject._inBytesUsed, - stateObject._inBytesRead)); - } - } - - } - else - { - output = stateObject._snapshot.List; - } - - return output; - } - - [ExcludeFromCodeCoverage] - private static void ComparePacketLists(int dataSize, List expected, List output) - { - Assert.NotNull(expected); - Assert.NotNull(output); - Assert.Equal(expected.Count, output.Count); - - for (int index = 0; index < expected.Count; index++) - { - var a = expected[index]; - var b = output[index]; - - var compare = a.AsSpan().SequenceCompareTo(b.AsSpan()); - - if (compare != 0) - { - Assert.Fail($"expected data does not match output data at packet index {index}"); - } - } - } - - [ExcludeFromCodeCoverage] - public static PacketData CreatePacket(int dataSize, byte dataValue, int startOffset = 0, int endPadding = 0) - { - byte[] buffer = new byte[startOffset + TdsEnums.HEADER_LEN + dataSize + endPadding]; - Span packet = buffer.AsSpan(startOffset, TdsEnums.HEADER_LEN + dataSize); - WritePacket(packet, dataSize, dataValue, 1); - return new PacketData(buffer, startOffset, buffer.Length - endPadding); - } - - [ExcludeFromCodeCoverage] - public static List CreatePackets(DataSize sizes, params byte[] dataValues) - { - int count = dataValues.Length; - List list = new List(count); - - for (byte index = 0; index < count; index++) - { - int dataSize = sizes.GetSize(index == dataValues.Length - 1); - int packetSize = TdsEnums.HEADER_LEN + dataSize; - byte[] array = new byte[packetSize]; - WritePacket(array, dataSize, dataValues[index], index); - list.Add(new PacketData(array, 0, packetSize)); - } - - return list; - } - - [ExcludeFromCodeCoverage] - private static void WritePacket(Span buffer, int dataSize, byte dataValue, byte id) - { - Span header = buffer.Slice(0, TdsEnums.HEADER_LEN); - header[0] = 4; // Type, 4 - Raw Data - header[1] = 0; // Status, 0 - normal message - BinaryPrimitives.TryWriteInt16BigEndian(header.Slice(TdsEnums.HEADER_LEN_FIELD_OFFSET, 2), - (short)(TdsEnums.HEADER_LEN + dataSize)); // total length - BinaryPrimitives.TryWriteInt16BigEndian(header.Slice(TdsEnums.SPID_OFFSET, 2), short.MaxValue); // SPID - header[TdsEnums.HEADER_LEN_FIELD_OFFSET + 4] = id; // PacketID - header[TdsEnums.HEADER_LEN_FIELD_OFFSET + 5] = 0; // Window - - Span data = buffer.Slice(TdsEnums.HEADER_LEN, dataSize); - data.Fill(dataValue); - } - - [ExcludeFromCodeCoverage] - public static List SplitPacket(PacketData packet, int length) - { - List list = new List(2); - while (packet.Length > length) - { - list.Add(new PacketData(packet.Array, packet.Start, length)); - packet = new PacketData(packet.Array, packet.Start + length, packet.Length - length); - } - - if (packet.Length > 0) - { - list.Add(packet); - } - - return list; - } - - [ExcludeFromCodeCoverage] - public static List SplitPackets(int dataSize, List packets, params int[] lengths) - { - List list = new List(lengths.Length); - int packetSize = TdsEnums.HEADER_LEN + dataSize; - byte[][] arrays = new byte[lengths.Length][]; - for (int index = 0; index < lengths.Length; index++) - { - if (lengths[index] > packetSize) - { - throw new ArgumentOutOfRangeException( - $"segment size of an individual part cannot exceed the packet buffer size of the state object, max packet size: {packetSize}, supplied length: {lengths[index]}, at index: {index}"); - } - - arrays[index] = new byte[lengths[index]]; - } - - int targetOffset = 0; - int targetIndex = 0; - - int sourceOffset = 0; - int sourceIndex = 0; - - - do - { - Span targetSpan = Span.Empty; - if (targetOffset < arrays[targetIndex].Length) - { - targetSpan = arrays[targetIndex].AsSpan(targetOffset); - } - else - { - targetIndex += 1; - targetOffset = 0; - continue; - } - - Span sourceSpan = Span.Empty; - if (sourceOffset < packets[sourceIndex].Length) - { - sourceSpan = packets[sourceIndex].AsSpan(sourceOffset); - } - else - { - sourceIndex += 1; - sourceOffset = 0; - continue; - } - - int copy = Math.Min(targetSpan.Length, sourceSpan.Length); - if (copy > 0) - { - targetOffset += copy; - sourceOffset += copy; - sourceSpan.Slice(0, copy).CopyTo(targetSpan.Slice(0, copy)); - } - } while (sourceIndex < packets.Count && targetIndex < arrays.Length); - - foreach (var array in arrays) - { - list.Add(new PacketData(array, 0, array.Length)); - } - - return list; - } - - [ExcludeFromCodeCoverage] - public static PacketData CombinePackets(List packets) - { - int totalLength = SumPacketLengths(packets); - byte[] buffer = new byte[totalLength]; - int offset = 0; - for (int index = 0; index < packets.Count; index++) - { - PacketData packet = packets[index]; - Array.Copy(packet.Array, packet.Start, buffer, offset, packet.Length); - offset += packet.Length; - } - - return new PacketData(buffer, 0, totalLength); - } - - [ExcludeFromCodeCoverage] - public static int PacketSizeFromDataSize(int dataSize) => TdsEnums.HEADER_LEN + dataSize; - - [ExcludeFromCodeCoverage] - public static int DataSizeFromPacketSize(int packetSize) => packetSize - TdsEnums.HEADER_LEN; - - [ExcludeFromCodeCoverage] - public static int SumPacketLengths(List list) - { - int total = 0; - for (int index = 0; index < list.Count; index++) - { - total += list[index].Length; - } - return total; - } - - [ExcludeFromCodeCoverage] - public static List LoadPacketBinFiles(string directoryName) - { - // expects a set of files contained in a directory with the name - // formatted as packet_{number}_{dataSize}.bin each packet will be - // loaded into a byte[] - - string[] files = Directory.GetFiles(directoryName, "packet*.bin", SearchOption.TopDirectoryOnly); - SortedDictionary packets = new SortedDictionary(); - foreach (string file in files) - { - Match match = Regex.Match(file, @"packet_(?\d+)_(?\d+)\.bin"); - int number = int.Parse(match.Groups["number"].Value); - int size = int.Parse(match.Groups["size"].Value); - packets.Add( - number, - new PacketData( - System.IO.File.ReadAllBytes(file), - 0, - size - ) - ); - } - - return packets.Values.ToList(); - } - - [ExcludeFromCodeCoverage] - public static List NaiveReconstructPacketStream(List input) - { - int dataSize = input[0].Array.Length; - List output = new List(input.Count); - - byte[] currentBuffer = new byte[dataSize]; - int currentBufferOffset = 0; - - foreach (PacketData inputPacket in input) - { - int inputPacketOffset = 0; - while (inputPacketOffset < inputPacket.Length) - { - if (currentBufferOffset < dataSize) - { - int requiredCount = dataSize - currentBufferOffset; - int availableCount = inputPacket.Length - inputPacketOffset; - int copyCount = Math.Min(requiredCount, availableCount); - ReadOnlySpan copyFrom = inputPacket.Array.AsSpan(inputPacketOffset, copyCount); - Span copyTo = currentBuffer.AsSpan(currentBufferOffset, copyCount); - copyFrom.CopyTo(copyTo); - currentBufferOffset += copyCount; - inputPacketOffset += copyCount; - } - - if (currentBufferOffset == dataSize) - { - output.Add(new PacketData(currentBuffer, 0, dataSize)); - currentBufferOffset = 0; - currentBuffer = new byte[dataSize]; - } - } - } - - if (currentBufferOffset > 0) - { - output.Add(new PacketData(currentBuffer, 0, currentBufferOffset)); - } - - for (int index = 0; index < output.Count; index++) - { - PacketData packet = output[index]; - int expectedLength = 8 + Packet.GetDataLengthFromHeader(packet.Array); - if (expectedLength != packet.Length) - { - if (index != output.Count - 1) - { - throw new InvalidOperationException( - "non-terminal packet has a length mismatch between the packet header and amount of data available"); - } - else - { - byte[] remainder = new byte[dataSize]; - int remainderSize = packet.Length - expectedLength; - Span copyFrom = packet.Array.AsSpan(expectedLength, remainderSize); - Span copyTo = remainder.AsSpan(0, remainderSize); - copyFrom.CopyTo(copyTo); - copyFrom.Fill(0); - - PacketData replacementPacket = new PacketData(packet.Array, 0, expectedLength); - PacketData additionalPacket = new PacketData(remainder, 0, remainderSize); - output[index] = replacementPacket; - output.Add(additionalPacket); - } - } - } - - return output; - } - } - - [ExcludeFromCodeCoverage] - [DebuggerDisplay("{ToDebugString(),nq}")] - public readonly struct PacketData - { - public readonly byte[] Array; - public readonly int Start; - public readonly int Length; - - public PacketData(byte[] array, int start, int length) - { - Array = array; - Start = start; - Length = length; - } - - public Span AsSpan() - { - return Array == null ? Span.Empty : Array.AsSpan(Start, Length); - } - - public Span AsSpan(int start) - { - Span span = AsSpan(); - return span.Slice(start); - } - - public static PacketData Copy(byte[] array, int start, int length) - { - byte[] newArray = null; - if (array != null) - { - newArray = new byte[array.Length]; - Buffer.BlockCopy(array, start, newArray, start, length); - } - - return new PacketData(newArray, start, length); - } - - [ExcludeFromCodeCoverage] - public string ToDebugString() - { - StringBuilder buffer = new StringBuilder(128); - buffer.Append(Length); - - if (Array != null && Array.Length > 0) - { - if (Array.Length != Length) - { - buffer.AppendFormat(" (arr: {0})", Array.Length); - } - - buffer.Append(": {"); - buffer.AppendFormat("{0:D2}", Array[0]); - - int max = Math.Min(32, Array.Length); - for (int index = 1; index < max; index++) - { - buffer.Append(','); - buffer.AppendFormat("{0:D2}", Array[index]); - } - - if (Length > max) - { - buffer.Append(" ..."); - } - - buffer.Append('}'); - } - - return buffer.ToString(); - } - - } - - [ExcludeFromCodeCoverage] - [DebuggerStepThrough] - public struct DataSize - { - public DataSize(int commonSize) - { - CommonSize = commonSize; - LastSize = commonSize; - } - - public DataSize(int commonSize, int lastSize) - { - CommonSize = commonSize; - LastSize = lastSize; - } - - public int LastSize { get; set; } - public int CommonSize { get; set; } - - public int GetSize(bool isLast) - { - if (isLast) - { - return LastSize; - } - else - { - return CommonSize; - } - } - - public static implicit operator DataSize(int commonSize) - { - return new DataSize(commonSize, commonSize); - } - - public static implicit operator DataSize((int commonSize, int lastSize) values) - { - return new DataSize(values.commonSize, values.lastSize); - } - } -} diff --git a/src/Microsoft.Data.SqlClient/tests/FunctionalTests/SqlAuthenticationProviderTest.cs b/src/Microsoft.Data.SqlClient/tests/FunctionalTests/SqlAuthenticationProviderTest.cs index d41f4b40d1..c15f1a9300 100644 --- a/src/Microsoft.Data.SqlClient/tests/FunctionalTests/SqlAuthenticationProviderTest.cs +++ b/src/Microsoft.Data.SqlClient/tests/FunctionalTests/SqlAuthenticationProviderTest.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +using Microsoft.Data.SqlClient.FunctionalTests.DataCommon; using Xunit; namespace Microsoft.Data.SqlClient.Tests @@ -11,7 +12,6 @@ public class SqlAuthenticationProviderTest [Theory] [InlineData(SqlAuthenticationMethod.ActiveDirectoryIntegrated)] [InlineData(SqlAuthenticationMethod.ActiveDirectoryPassword)] - [InlineData(SqlAuthenticationMethod.ActiveDirectoryInteractive)] [InlineData(SqlAuthenticationMethod.ActiveDirectoryServicePrincipal)] [InlineData(SqlAuthenticationMethod.ActiveDirectoryDeviceCodeFlow)] [InlineData(SqlAuthenticationMethod.ActiveDirectoryManagedIdentity)] @@ -22,5 +22,18 @@ public void DefaultAuthenticationProviders(SqlAuthenticationMethod method) { Assert.IsType(SqlAuthenticationProvider.GetProvider(method)); } + + #if NETFRAMEWORK + // This test is only valid for .NET Framework + + // Overridden by app.config in this project + [Theory] + [InlineData(SqlAuthenticationMethod.ActiveDirectoryInteractive)] + public void DefaultAuthenticationProviders_Interactive(SqlAuthenticationMethod method) + { + Assert.IsType(SqlAuthenticationProvider.GetProvider(method)); + } + + #endif } } diff --git a/src/Microsoft.Data.SqlClient/tests/FunctionalTests/SqlConnectionReadOnlyRoutingTests.cs b/src/Microsoft.Data.SqlClient/tests/FunctionalTests/SqlConnectionReadOnlyRoutingTests.cs deleted file mode 100644 index c3574dbc13..0000000000 --- a/src/Microsoft.Data.SqlClient/tests/FunctionalTests/SqlConnectionReadOnlyRoutingTests.cs +++ /dev/null @@ -1,140 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -using System; -using System.Collections.Generic; -using System.Net; -using System.Threading.Tasks; -using Microsoft.SqlServer.TDS.Servers; -using Xunit; - -namespace Microsoft.Data.SqlClient.Tests -{ - public class SqlConnectionReadOnlyRoutingTests - { - [Fact] - public void NonRoutedConnection() - { - using TestTdsServer server = TestTdsServer.StartTestServer(); - SqlConnectionStringBuilder builder = new SqlConnectionStringBuilder(server.ConnectionString) { ApplicationIntent = ApplicationIntent.ReadOnly }; - using SqlConnection connection = new SqlConnection(builder.ConnectionString); - connection.Open(); - } - - [Fact] - public async Task NonRoutedAsyncConnection() - { - using TestTdsServer server = TestTdsServer.StartTestServer(); - SqlConnectionStringBuilder builder = new SqlConnectionStringBuilder(server.ConnectionString) { ApplicationIntent = ApplicationIntent.ReadOnly }; - using SqlConnection connection = new SqlConnection(builder.ConnectionString); - await connection.OpenAsync(); - } - - [Fact] - public void RoutedConnection() - => RecursivelyRoutedConnection(1); - - [Fact] - public async Task RoutedAsyncConnection() - => await RecursivelyRoutedAsyncConnection(1); - - [Theory] - [InlineData(2)] - [InlineData(9)] - [InlineData(11)] // The driver rejects more than 10 redirects (11 layers of redirecting servers) - public void RecursivelyRoutedConnection(int layers) - { - TestTdsServer innerServer = TestTdsServer.StartTestServer(); - IPEndPoint lastEndpoint = innerServer.Endpoint; - Stack routingLayers = new(layers + 1); - string lastConnectionString = innerServer.ConnectionString; - - try - { - routingLayers.Push(innerServer); - for (int i = 0; i < layers; i++) - { - TestRoutingTdsServer router = TestRoutingTdsServer.StartTestServer(lastEndpoint); - - routingLayers.Push(router); - lastEndpoint = router.Endpoint; - lastConnectionString = router.ConnectionString; - } - - SqlConnectionStringBuilder builder = new SqlConnectionStringBuilder(lastConnectionString) { ApplicationIntent = ApplicationIntent.ReadOnly }; - using SqlConnection connection = new SqlConnection(builder.ConnectionString); - connection.Open(); - } - finally - { - while (routingLayers.Count > 0) - { - GenericTDSServer layer = routingLayers.Pop(); - - if (layer is IDisposable disp) - { - disp.Dispose(); - } - } - } - } - - [Theory] - [InlineData(2)] - [InlineData(9)] - [InlineData(11)] // The driver rejects more than 10 redirects (11 layers of redirecting servers) - public async Task RecursivelyRoutedAsyncConnection(int layers) - { - TestTdsServer innerServer = TestTdsServer.StartTestServer(); - IPEndPoint lastEndpoint = innerServer.Endpoint; - Stack routingLayers = new(layers + 1); - string lastConnectionString = innerServer.ConnectionString; - - try - { - routingLayers.Push(innerServer); - for (int i = 0; i < layers; i++) - { - TestRoutingTdsServer router = TestRoutingTdsServer.StartTestServer(lastEndpoint); - - routingLayers.Push(router); - lastEndpoint = router.Endpoint; - lastConnectionString = router.ConnectionString; - } - - SqlConnectionStringBuilder builder = new SqlConnectionStringBuilder(lastConnectionString) { ApplicationIntent = ApplicationIntent.ReadOnly }; - using SqlConnection connection = new SqlConnection(builder.ConnectionString); - await connection.OpenAsync(); - } - finally - { - while (routingLayers.Count > 0) - { - GenericTDSServer layer = routingLayers.Pop(); - - if (layer is IDisposable disp) - { - disp.Dispose(); - } - } - } - } - - [Fact] - public void ConnectionRoutingLimit() - { - SqlException sqlEx = Assert.Throws(() => RecursivelyRoutedConnection(12)); // This will fail on the 11th redirect - - Assert.Contains("Too many redirections have occurred.", sqlEx.Message, StringComparison.InvariantCultureIgnoreCase); - } - - [Fact] - public async Task AsyncConnectionRoutingLimit() - { - SqlException sqlEx = await Assert.ThrowsAsync(() => RecursivelyRoutedAsyncConnection(12)); // This will fail on the 11th redirect - - Assert.Contains("Too many redirections have occurred.", sqlEx.Message, StringComparison.InvariantCultureIgnoreCase); - } - } -} diff --git a/src/Microsoft.Data.SqlClient/tests/FunctionalTests/TdsParserStateObject.TestHarness.cs b/src/Microsoft.Data.SqlClient/tests/FunctionalTests/TdsParserStateObject.TestHarness.cs deleted file mode 100644 index 89807b0132..0000000000 --- a/src/Microsoft.Data.SqlClient/tests/FunctionalTests/TdsParserStateObject.TestHarness.cs +++ /dev/null @@ -1,198 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Reflection; -using Microsoft.Data.SqlClient.Tests; - -using SwitchesHelper = Microsoft.Data.SqlClient.Tests.Common.LocalAppContextSwitchesHelper; - -namespace Microsoft.Data.SqlClient -{ - internal struct PacketHandle - { - } - - internal partial class TdsParserStateObject : IDisposable - { - internal int ObjectID = 1; - - internal class SQL - { - internal static Exception InvalidInternalPacketSize(string v) => throw new Exception(v ?? nameof(InvalidInternalPacketSize)); - - internal static Exception ParsingError(ParsingErrorState state) => throw new Exception(state.ToString()); - } - - internal static class SqlClientEventSource - { - internal static class Log - { - internal static void TryAdvancedTraceBinEvent(string message, params object[] values) - { - } - } - } - - private enum SnapshotStatus - { - NotActive, - ReplayStarting, - ReplayRunning - } - - internal enum TdsParserState - { - Closed, - OpenNotLoggedIn, - OpenLoggedIn, - Broken, - } - - private uint GetSniPacket(PacketHandle packet, ref uint dataSize) - { - return SniPacketGetData(packet, _inBuff, ref dataSize); - } - - private class StringsHelper - { - internal static string GetString(string sqlMisc_InvalidArraySizeMessage) => Strings.SqlMisc_InvalidArraySizeMessage; - } - - internal class Strings - { - internal static string SqlMisc_InvalidArraySizeMessage = nameof(SqlMisc_InvalidArraySizeMessage); - - } - - public class Parser - { - internal object ProcessSNIError(TdsParserStateObject tdsParserStateObject) => "ProcessSNIError"; - public TdsParserState State = TdsParserState.OpenLoggedIn; - } - - sealed internal class LastIOTimer - { - internal long _value; - } - - internal sealed class Snapshot - { - public List List; - - public Snapshot() => List = new List(); - [DebuggerStepThrough] - internal void AssertCurrent() { } - [DebuggerStepThrough] - internal void AppendPacketData(byte[] buffer, int read) => List.Add(new PacketData(buffer, 0, read)); - [DebuggerStepThrough] - internal void MoveNext() - { - - } - } - - public List Input; - public PacketData Current; - public bool IsAsync { get => _snapshot != null; } - - public int _packetSize; - - internal Snapshot _snapshot; - public int _inBytesRead; - public int _inBytesUsed; - public byte[] _inBuff; - - [DebuggerStepThrough] - public TdsParserStateObject(List input, int packetSize, bool isAsync) - { - _packetSize = packetSize; - _inBuff = new byte[_packetSize]; - Input = input; - if (isAsync) - { - _snapshot = new Snapshot(); - } - } - - [DebuggerStepThrough] - public void Dispose() - { - LocalAppContextSwitches.Dispose(); - } - - [DebuggerStepThrough] - private uint SniPacketGetData(PacketHandle packet, byte[] inBuff, ref uint dataSize) - { - Span target = inBuff.AsSpan(0, _packetSize); - Span source = Current.Array.AsSpan(Current.Start, Current.Length); - source.CopyTo(target); - dataSize = (uint)Current.Length; - return TdsEnums.SNI_SUCCESS; - } - - [DebuggerStepThrough] - void SetBuffer(byte[] buffer, int inBytesUsed, int inBytesRead) - { - _inBuff = buffer; - _inBytesUsed = inBytesUsed; - _inBytesRead = inBytesRead; - } - - // stubs - private LastIOTimer _lastSuccessfulIOTimer = new LastIOTimer(); - private Parser _parser = new Parser(); - private SnapshotStatus _snapshotStatus = SnapshotStatus.NotActive; - - [DebuggerStepThrough] - private void SniReadStatisticsAndTracing() { } - [DebuggerStepThrough] - private void AssertValidState() { } - - [DebuggerStepThrough] - private void AddError(object value) => throw new Exception(value as string ?? "AddError"); - - private SwitchesHelper LocalAppContextSwitches = new(); - -#if NETFRAMEWORK - private SniNativeWrapperImpl _native; - internal SniNativeWrapperImpl SniNativeWrapper - { - get - { - if (_native == null) - { - _native = new SniNativeWrapperImpl(this); - } - return _native; - } - } - - internal class SniNativeWrapperImpl - { - private readonly TdsParserStateObject _parent; - internal SniNativeWrapperImpl(TdsParserStateObject parent) => _parent = parent; - - internal uint SniPacketGetData(PacketHandle packet, byte[] inBuff, ref uint dataSize) => _parent.SniPacketGetData(packet, inBuff, ref dataSize); - } -#endif - } - - internal static class TdsEnums - { - public const uint SNI_SUCCESS = 0; // The operation completed successfully. - // header constants - public const int HEADER_LEN = 8; - public const int HEADER_LEN_FIELD_OFFSET = 2; - public const int SPID_OFFSET = 4; - } - - internal enum ParsingErrorState - { - CorruptedTdsStream = 18, - ProcessSniPacketFailed = 19, - } -} diff --git a/src/Microsoft.Data.SqlClient/tests/FunctionalTests/TestRoutingTdsServer.cs b/src/Microsoft.Data.SqlClient/tests/FunctionalTests/TestRoutingTdsServer.cs deleted file mode 100644 index 130b50cad9..0000000000 --- a/src/Microsoft.Data.SqlClient/tests/FunctionalTests/TestRoutingTdsServer.cs +++ /dev/null @@ -1,64 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -using System; -using System.Net; -using System.Runtime.CompilerServices; -using Microsoft.SqlServer.TDS.EndPoint; -using Microsoft.SqlServer.TDS.Servers; - -namespace Microsoft.Data.SqlClient.Tests -{ - internal class TestRoutingTdsServer : RoutingTDSServer, IDisposable - { - private const int DefaultConnectionTimeout = 5; - - private TDSServerEndPoint _endpoint = null; - - private SqlConnectionStringBuilder _connectionStringBuilder; - - public TestRoutingTdsServer(RoutingTDSServerArguments args) : base(args) { } - - public static TestRoutingTdsServer StartTestServer(IPEndPoint destinationEndpoint, bool enableFedAuth = false, bool enableLog = false, int connectionTimeout = DefaultConnectionTimeout, bool excludeEncryption = false, [CallerMemberName] string methodName = "") - { - RoutingTDSServerArguments args = new RoutingTDSServerArguments() - { - Log = enableLog ? Console.Out : null, - RoutingTCPHost = destinationEndpoint.Address.ToString() == IPAddress.Any.ToString() ? IPAddress.Loopback.ToString() : destinationEndpoint.Address.ToString(), - RoutingTCPPort = (ushort)destinationEndpoint.Port, - }; - - if (enableFedAuth) - { - args.FedAuthRequiredPreLoginOption = SqlServer.TDS.PreLogin.TdsPreLoginFedAuthRequiredOption.FedAuthRequired; - } - if (excludeEncryption) - { - args.Encryption = SqlServer.TDS.PreLogin.TDSPreLoginTokenEncryptionType.None; - } - - TestRoutingTdsServer server = new TestRoutingTdsServer(args); - server._endpoint = new TDSServerEndPoint(server) { ServerEndPoint = new IPEndPoint(IPAddress.Any, 0) }; - server._endpoint.EndpointName = methodName; - // The server EventLog should be enabled as it logs the exceptions. - server._endpoint.EventLog = enableLog ? Console.Out : null; - server._endpoint.Start(); - - int port = server._endpoint.ServerEndPoint.Port; - server._connectionStringBuilder = excludeEncryption - // Allow encryption to be set when encryption is to be excluded from pre-login response. - ? new SqlConnectionStringBuilder() { DataSource = "localhost," + port, ConnectTimeout = connectionTimeout, Encrypt = SqlConnectionEncryptOption.Mandatory } - : new SqlConnectionStringBuilder() { DataSource = "localhost," + port, ConnectTimeout = connectionTimeout, Encrypt = SqlConnectionEncryptOption.Optional }; - server.ConnectionString = server._connectionStringBuilder.ConnectionString; - server.Endpoint = server._endpoint.ServerEndPoint; - return server; - } - - public void Dispose() => _endpoint?.Stop(); - - public string ConnectionString { get; private set; } - - public IPEndPoint Endpoint { get; private set; } - } -} diff --git a/src/Microsoft.Data.SqlClient/tests/FunctionalTests/TestTdsServer.cs b/src/Microsoft.Data.SqlClient/tests/FunctionalTests/TestTdsServer.cs deleted file mode 100644 index a5976fd6d5..0000000000 --- a/src/Microsoft.Data.SqlClient/tests/FunctionalTests/TestTdsServer.cs +++ /dev/null @@ -1,76 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -using System; -using System.Net; -using System.Runtime.CompilerServices; -using Microsoft.SqlServer.TDS.EndPoint; -using Microsoft.SqlServer.TDS.Servers; - -namespace Microsoft.Data.SqlClient.Tests -{ - internal class TestTdsServer : GenericTDSServer, IDisposable - { - private const int DefaultConnectionTimeout = 5; - - private TDSServerEndPoint _endpoint = null; - - private SqlConnectionStringBuilder _connectionStringBuilder; - - public TestTdsServer(TDSServerArguments args) : base(args) { } - - public TestTdsServer(QueryEngine engine, TDSServerArguments args) : base(args) - { - Engine = engine; - } - - public static TestTdsServer StartServerWithQueryEngine(QueryEngine engine, bool enableFedAuth = false, bool enableLog = false, int connectionTimeout = DefaultConnectionTimeout, bool excludeEncryption = false, Version serverVersion = null, [CallerMemberName] string methodName = "") - { - TDSServerArguments args = new TDSServerArguments() - { - Log = enableLog ? Console.Out : null, - }; - - if (enableFedAuth) - { - args.FedAuthRequiredPreLoginOption = SqlServer.TDS.PreLogin.TdsPreLoginFedAuthRequiredOption.FedAuthRequired; - } - if (excludeEncryption) - { - args.Encryption = SqlServer.TDS.PreLogin.TDSPreLoginTokenEncryptionType.None; - } - if (serverVersion != null) - { - args.ServerVersion = serverVersion; - } - - TestTdsServer server = engine == null ? new TestTdsServer(args) : new TestTdsServer(engine, args); - server._endpoint = new TDSServerEndPoint(server) { ServerEndPoint = new IPEndPoint(IPAddress.Any, 0) }; - server._endpoint.EndpointName = methodName; - // The server EventLog should be enabled as it logs the exceptions. - server._endpoint.EventLog = enableLog ? Console.Out : null; - server._endpoint.Start(); - - int port = server._endpoint.ServerEndPoint.Port; - server._connectionStringBuilder = excludeEncryption - // Allow encryption to be set when encryption is to be excluded from pre-login response. - ? new SqlConnectionStringBuilder() { DataSource = "localhost," + port, ConnectTimeout = connectionTimeout, Encrypt = SqlConnectionEncryptOption.Mandatory } - : new SqlConnectionStringBuilder() { DataSource = "localhost," + port, ConnectTimeout = connectionTimeout, Encrypt = SqlConnectionEncryptOption.Optional }; - server.ConnectionString = server._connectionStringBuilder.ConnectionString; - server.Endpoint = server._endpoint.ServerEndPoint; - return server; - } - - public static TestTdsServer StartTestServer(bool enableFedAuth = false, bool enableLog = false, int connectionTimeout = DefaultConnectionTimeout, bool excludeEncryption = false, Version serverVersion = null, [CallerMemberName] string methodName = "") - { - return StartServerWithQueryEngine(null, enableFedAuth, enableLog, connectionTimeout, excludeEncryption, serverVersion, methodName); - } - - public void Dispose() => _endpoint?.Stop(); - - public string ConnectionString { get; private set; } - - public IPEndPoint Endpoint { get; private set; } - } -} diff --git a/src/Microsoft.Data.SqlClient/tests/FunctionalTests/app.config b/src/Microsoft.Data.SqlClient/tests/FunctionalTests/app.config new file mode 100644 index 0000000000..fb7f63f65f --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/FunctionalTests/app.config @@ -0,0 +1,13 @@ + + + + +
+ + + + + + + + diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/ApiShould.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/ApiShould.cs index 184b5465ec..d3c40e2d34 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/ApiShould.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/ApiShould.cs @@ -45,6 +45,24 @@ public sealed class ApiShould : IClassFixture, IDis "'MSSQL_CERTIFICATE_STORE', 'MSSQL_CNG_STORE', 'MSSQL_CSP_PROVIDER'", $"'{NotRequiredProviderName}'"); + private HashSet _cancellationExceptionMessages = new HashSet() + { + "A severe error occurred on the current command. " + + "The results, if any, should be discarded." + Environment.NewLine + + "Operation cancelled by user.", + + "A severe error occurred on the current command. " + + "The results, if any, should be discarded.", + + "Operation cancelled by user.", + + "The request failed to run because the batch is aborted, " + + "this can be caused by abort signal sent from client, " + + "or another request is running in the same session, " + + "which makes the session busy." + Environment.NewLine + + "Operation cancelled by user." + }; + public ApiShould(PlatformSpecificTestContext context) { _fixture = context.Fixture; @@ -148,8 +166,8 @@ public void SqlParameterProperties(string connection) const string firstColumnName = @"firstColumn"; const string secondColumnName = @"secondColumn"; const string thirdColumnName = @"thirdColumn"; - string inputProcedureName = DataTestUtility.GetUniqueName("InputProc").ToString(); - string outputProcedureName = DataTestUtility.GetUniqueName("OutputProc").ToString(); + string inputProcedureName = DataTestUtility.GetShortName("InputProc").ToString(); + string outputProcedureName = DataTestUtility.GetShortName("OutputProc").ToString(); const int charColumnSize = 100; const int decimalColumnPrecision = 10; const int decimalColumnScale = 4; @@ -704,7 +722,7 @@ public void TestExecuteReader(string connection) [ClassData(typeof(AEConnectionStringProvider))] public async Task TestExecuteReaderAsyncWithLargeQuery(string connectionString) { - string randomName = DataTestUtility.GetUniqueName(Guid.NewGuid().ToString().Replace("-", ""), false); + string randomName = DataTestUtility.GetShortName(Guid.NewGuid().ToString().Replace("-", ""), false); if (randomName.Length > 50) { randomName = randomName.Substring(0, 50); @@ -894,8 +912,8 @@ public void TestEnclaveStoredProceduresWithAndWithoutParameters(string connectio using SqlCommand sqlCommand = new("", sqlConnection, transaction: null, columnEncryptionSetting: SqlCommandColumnEncryptionSetting.Enabled); - string procWithoutParams = DataTestUtility.GetUniqueName("EnclaveWithoutParams", withBracket: false); - string procWithParam = DataTestUtility.GetUniqueName("EnclaveWithParams", withBracket: false); + string procWithoutParams = DataTestUtility.GetShortName("EnclaveWithoutParams", withBracket: false); + string procWithParam = DataTestUtility.GetShortName("EnclaveWithParams", withBracket: false); try { @@ -1983,19 +2001,15 @@ public void TestBeginAndEndExecuteReaderWithAsyncCallback(string connection, Com } } - [SkipOnTargetFramework(TargetFrameworkMonikers.Netcoreapp)] [ConditionalTheory(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringSetupForAE))] [ClassData(typeof(AEConnectionStringProviderWithExecutionMethod))] - public void TestSqlCommandCancel(string connection, string value, int number) + public void TestSqlCommandCancel(string connection, string value) { CleanUpTable(connection, _tableName); string executeMethod = value; Assert.True(!string.IsNullOrWhiteSpace(executeMethod), @"executeMethod should not be null or empty"); - int numberOfCancelCalls = number; - Assert.True(numberOfCancelCalls >= 0, "numberofCancelCalls should be >=0."); - IList values = GetValues(dataHint: 58); Assert.True(values != null && values.Count >= 3, @"values should not be null and count should be >= 3."); @@ -2007,7 +2021,10 @@ public void TestSqlCommandCancel(string connection, string value, int number) { sqlConnection.Open(); - using (SqlCommand sqlCommand = new SqlCommand($@"SELECT * FROM [{_tableName}] WHERE FirstName = @FirstName AND CustomerId = @CustomerId", + // WAITFOR DELAY is present to ensure that the command is always executing by the time the cancellation occurs. + // Without this, a command which has completed by the time the cancellation occurs will fail the test. It's last + // in the command to ensure that cancellation is tested while row data or metadata is flowing. + using (SqlCommand sqlCommand = new SqlCommand($@"SELECT * FROM [{_tableName}] WHERE FirstName = @FirstName AND CustomerId = @CustomerId; WAITFOR DELAY '00:00:00.150'", sqlConnection, transaction: null, columnEncryptionSetting: SqlCommandColumnEncryptionSetting.Enabled)) @@ -2017,29 +2034,50 @@ public void TestSqlCommandCancel(string connection, string value, int number) CommandHelper.s_sleepDuringTryFetchInputParameterEncryptionInfo?.SetValue(null, true); - Thread[] threads = new Thread[2]; - - // Invoke ExecuteReader or ExecuteNonQuery in another thread. + Task[] tasks = new Task[2]; + ManualResetEventSlim startWorkloadSignal = new ManualResetEventSlim(false); + ManualResetEventSlim workloadCompleteSignal = new ManualResetEventSlim(false); + + /* + * Invoke ExecuteReader or ExecuteNonQuery in another thread. + * Use long-running tasks to create the thread. This enables any failed assertions to propagate, rather than + * allowing the exception to kill the thread and the process. + * These threads should progress in the sequence below: + * + * Workload Thread | Cancel Thread + * ------------------------------------ | ------------- + * Start thread | Start thread + * Wait for signal | - + * - | Set signal for workload start + * Start workload execution | Loop, cancelling workload until workload complete signal set + * Throw cancellation exception | - + * Set signal for workload complete | - + * End thread | Finish loop and end thread + */ if (executeMethod == @"ExecuteReader") { - threads[0] = new Thread(new ParameterizedThreadStart(Thread_ExecuteReader)); + tasks[0] = new Task(Thread_ExecuteReader, + new TestCommandCancelParams(sqlCommand, _tableName, startWorkloadSignal, workloadCompleteSignal), + TaskCreationOptions.LongRunning); } else { - threads[0] = new Thread(new ParameterizedThreadStart(Thread_ExecuteNonQuery)); + tasks[0] = new Task(Thread_ExecuteNonQuery, + new TestCommandCancelParams(sqlCommand, _tableName, startWorkloadSignal, workloadCompleteSignal), + TaskCreationOptions.LongRunning); } - - threads[1] = new Thread(new ParameterizedThreadStart(Thread_Cancel)); + tasks[1] = new Task(Thread_Cancel, + new TestCommandCancelParams(sqlCommand, _tableName, startWorkloadSignal, workloadCompleteSignal), + TaskCreationOptions.LongRunning); // Start the execute thread. - threads[0].Start(new TestCommandCancelParams(sqlCommand, _tableName, numberOfCancelCalls)); + tasks[0].Start(); // Start the thread which cancels the above command started by the execute thread. - threads[1].Start(new TestCommandCancelParams(sqlCommand, _tableName, numberOfCancelCalls)); + tasks[1].Start(); // Wait for the threads to finish. - threads[0].Join(); - threads[1].Join(); + Task.WaitAll(tasks); CommandHelper.s_sleepDuringTryFetchInputParameterEncryptionInfo?.SetValue(null, false); @@ -2061,37 +2099,48 @@ public void TestSqlCommandCancel(string connection, string value, int number) Assert.True(rowsAffected == numberOfRows, "Unexpected number of rows affected as returned by EndExecuteReader."); - // Verify the state of the sql command object. + // Verify the state of the sql command object. Also ensure that the exit lock was set (and didn't time out.) VerifySqlCommandStateAfterCompletionOrCancel(sqlCommand); + Assert.True(workloadCompleteSignal.IsSet); + startWorkloadSignal.Reset(); + workloadCompleteSignal.Reset(); CommandHelper.s_sleepDuringRunExecuteReaderTdsForSpDescribeParameterEncryption?.SetValue(null, true); // Invoke ExecuteReader or ExecuteNonQuery in another thread. - threads = new Thread[2]; + tasks = new Task[2]; if (executeMethod == @"ExecuteReader") { - threads[0] = new Thread(new ParameterizedThreadStart(Thread_ExecuteReader)); + tasks[0] = new Task(Thread_ExecuteReader, + new TestCommandCancelParams(sqlCommand, _tableName, startWorkloadSignal, workloadCompleteSignal), + TaskCreationOptions.LongRunning); } else { - threads[0] = new Thread(new ParameterizedThreadStart(Thread_ExecuteNonQuery)); + tasks[0] = new Task(Thread_ExecuteNonQuery, + new TestCommandCancelParams(sqlCommand, _tableName, startWorkloadSignal, workloadCompleteSignal), + TaskCreationOptions.LongRunning); } - threads[1] = new Thread(new ParameterizedThreadStart(Thread_Cancel)); + tasks[1] = new Task(Thread_Cancel, + new TestCommandCancelParams(sqlCommand, _tableName, startWorkloadSignal, workloadCompleteSignal), + TaskCreationOptions.LongRunning); // Start the execute thread. - threads[0].Start(new TestCommandCancelParams(sqlCommand, _tableName, numberOfCancelCalls)); + tasks[0].Start(); // Start the thread which cancels the above command started by the execute thread. - threads[1].Start(new TestCommandCancelParams(sqlCommand, _tableName, numberOfCancelCalls)); + tasks[1].Start(); // Wait for the threads to finish. - threads[0].Join(); - threads[1].Join(); + Task.WaitAll(tasks); CommandHelper.s_sleepDuringRunExecuteReaderTdsForSpDescribeParameterEncryption?.SetValue(null, false); - // Verify the state of the sql command object. + // Verify the state of the sql command object. Also ensure that the exit lock was set (and didn't time out.) VerifySqlCommandStateAfterCompletionOrCancel(sqlCommand); + Assert.True(workloadCompleteSignal.IsSet); + startWorkloadSignal.Reset(); + workloadCompleteSignal.Reset(); rowsAffected = 0; @@ -2113,31 +2162,37 @@ public void TestSqlCommandCancel(string connection, string value, int number) CommandHelper.s_sleepAfterReadDescribeEncryptionParameterResults?.SetValue(null, true); - threads = new Thread[2]; + tasks = new Task[2]; if (executeMethod == @"ExecuteReader") { - threads[0] = new Thread(new ParameterizedThreadStart(Thread_ExecuteReader)); + tasks[0] = new Task(Thread_ExecuteReader, + new TestCommandCancelParams(sqlCommand, _tableName, startWorkloadSignal, workloadCompleteSignal), + TaskCreationOptions.LongRunning); } else { - threads[0] = new Thread(new ParameterizedThreadStart(Thread_ExecuteNonQuery)); + tasks[0] = new Task(Thread_ExecuteNonQuery, + new TestCommandCancelParams(sqlCommand, _tableName, startWorkloadSignal, workloadCompleteSignal), + TaskCreationOptions.LongRunning); } - threads[1] = new Thread(new ParameterizedThreadStart(Thread_Cancel)); + tasks[1] = new Task(Thread_Cancel, + new TestCommandCancelParams(sqlCommand, _tableName, startWorkloadSignal, workloadCompleteSignal), + TaskCreationOptions.LongRunning); // Start the execute thread. - threads[0].Start(new TestCommandCancelParams(sqlCommand, _tableName, numberOfCancelCalls)); + tasks[0].Start(); // Start the thread which cancels the above command started by the execute thread. - threads[1].Start(new TestCommandCancelParams(sqlCommand, _tableName, numberOfCancelCalls)); + tasks[1].Start(); // Wait for the threads to finish. - threads[0].Join(); - threads[1].Join(); + Task.WaitAll(tasks); CommandHelper.s_sleepAfterReadDescribeEncryptionParameterResults?.SetValue(null, false); - // Verify the state of the sql command object. + // Verify the state of the sql command object. Also ensure that the exit lock was set (and didn't time out.) VerifySqlCommandStateAfterCompletionOrCancel(sqlCommand); + Assert.True(workloadCompleteSignal.IsSet); rowsAffected = 0; @@ -3130,47 +3185,97 @@ private void TestCancellationToken(FieldInfo failpoint, SqlCommand sqlCommand, i Assert.True(rowsAffected == numberOfRows, "Unexpected number of rows affected as returned by EndExecuteReader."); } - private void Thread_ExecuteReader(object cancelCommandTestParamsObject) + private void Thread_ExecuteReader(object state) { + TestCommandCancelParams cancelCommandTestParamsObject = state as TestCommandCancelParams; + SqlCommand sqlCommand = cancelCommandTestParamsObject?.SqlCommand; + SqlDataReader reader = null; + Assert.True(cancelCommandTestParamsObject != null, @"cancelCommandTestParamsObject should not be null."); - SqlCommand sqlCommand = ((TestCommandCancelParams)cancelCommandTestParamsObject).SqlCommand as SqlCommand; Assert.True(sqlCommand != null, "sqlCommand should not be null."); - string.Format(@"SELECT * FROM {0} WHERE FirstName = @FirstName AND CustomerId = @CustomerId", ((TestCommandCancelParams)cancelCommandTestParamsObject).TableName); - using (SqlDataReader reader = sqlCommand.ExecuteReader()) + try { - while (reader.Read()) + // Wait for the cancellation thread to open this lock... + cancelCommandTestParamsObject.StartWorkloadSignal.Wait(); + + Exception ex = Assert.ThrowsAny(() => { - Assert.Throws(() => sqlCommand.ExecuteReader()); + reader = sqlCommand.ExecuteReader(); + while (reader.Read()) + { } + }); + + // We don't use Assert.Contains() here because it truncates the + // actual and expected strings when outputting a failure. + if (!_cancellationExceptionMessages.Contains(ex.Message)) + { + Assert.Fail( + $"Exception message \"{ex.Message}\" not found in: [\"" + + string.Join("\", \"", _cancellationExceptionMessages) + "\"]"); } } + finally + { + reader?.Dispose(); + // ...and unlock the cancellation thread once we finish. + cancelCommandTestParamsObject.WorkloadCompleteSignal.Set(); + } } - private void Thread_ExecuteNonQuery(object cancelCommandTestParamsObject) + private void Thread_ExecuteNonQuery(object state) { + TestCommandCancelParams cancelCommandTestParamsObject = state as TestCommandCancelParams; + SqlCommand sqlCommand = cancelCommandTestParamsObject?.SqlCommand; + Assert.True(cancelCommandTestParamsObject != null, @"cancelCommandTestParamsObject should not be null."); - SqlCommand sqlCommand = ((TestCommandCancelParams)cancelCommandTestParamsObject).SqlCommand as SqlCommand; Assert.True(sqlCommand != null, "sqlCommand should not be null."); - string.Format(@"UPDATE {0} SET FirstName = @FirstName WHERE FirstName = @FirstName AND CustomerId = @CustomerId", ((TestCommandCancelParams)cancelCommandTestParamsObject).TableName); + try + { + // Wait for the cancellation thread to open this lock... + cancelCommandTestParamsObject.StartWorkloadSignal.Wait(); + + Exception ex = Assert.ThrowsAny(() => sqlCommand.ExecuteNonQuery()); - Exception ex = Assert.Throws(() => sqlCommand.ExecuteNonQuery()); - Assert.Equal(@"Operation cancelled by user.", ex.Message); + // We don't use Assert.Contains() here because it truncates the + // actual and expected strings when outputting a failure. + if (!_cancellationExceptionMessages.Contains(ex.Message)) + { + Assert.Fail( + $"Exception message \"{ex.Message}\" not found in: [\"" + + string.Join("\", \"", _cancellationExceptionMessages) + "\"]"); + } + } + finally + { + // ...and unlock the cancellation thread once we finish. + cancelCommandTestParamsObject.WorkloadCompleteSignal.Set(); + } } - private void Thread_Cancel(object cancelCommandTestParamsObject) + private void Thread_Cancel(object state) { + TestCommandCancelParams cancelCommandTestParamsObject = state as TestCommandCancelParams; + SqlCommand sqlCommand = cancelCommandTestParamsObject?.SqlCommand; + int cancellations = 0; + System.Diagnostics.Stopwatch cancellationStart = new System.Diagnostics.Stopwatch(); + Assert.True(cancelCommandTestParamsObject != null, @"cancelCommandTestParamsObject should not be null."); - SqlCommand sqlCommand = ((TestCommandCancelParams)cancelCommandTestParamsObject).SqlCommand as SqlCommand; Assert.True(sqlCommand != null, "sqlCommand should not be null."); - Thread.Sleep(millisecondsTimeout: 500); + cancellationStart.Start(); + cancelCommandTestParamsObject.StartWorkloadSignal.Set(); - // Repeatedly cancel. - for (int i = 0; i < ((TestCommandCancelParams)cancelCommandTestParamsObject).NumberofTimesToRunCancel; i++) + // Repeatedly cancel until the other thread signals that it's completed execution + // (or until the command timeout has passed.) + do { sqlCommand.Cancel(); - } + cancellations++; + } while (!cancelCommandTestParamsObject.WorkloadCompleteSignal.Wait(0) + && cancellationStart.ElapsedMilliseconds <= sqlCommand.CommandTimeout); + cancellationStart.Stop(); } public void Dispose() @@ -3298,67 +3403,42 @@ internal TestAsyncCallBackStateObject(SqlCommand sqlCommand, int expectedRowsAff internal class TestCommandCancelParams { /// - /// SqlCommand object. - /// - private readonly object _sqlCommand; - - /// - /// Name of the test/works as table name - /// - private readonly string _tableName; - - /// - /// number of times to run cancel. + /// Return the SqlCommand object. /// - private readonly int _numberofCancelCommands; + public SqlCommand SqlCommand { get; } /// - /// Return the SqlCommand object. + /// Return the table name (usually equal to the name of the test.) /// - public object SqlCommand - { - get - { - return _sqlCommand; - } - } + public string TableName { get; } /// - /// Return the tablename. + /// Lock set by the thread cancelling the SqlCommand. /// - public object TableName - { - get - { - return _tableName; - } - } + public ManualResetEventSlim StartWorkloadSignal { get; } /// - /// Return the number of times to run cancel. + /// Lock set by the thread executing the SqlCommand. /// - public int NumberofTimesToRunCancel - { - get - { - return _numberofCancelCommands; - } - } + public ManualResetEventSlim WorkloadCompleteSignal { get; } /// /// Constructor. /// /// /// - /// - public TestCommandCancelParams(object sqlCommand, string tableName, int numberofTimesToCancel) + /// + /// + /// + public TestCommandCancelParams(SqlCommand sqlCommand, string tableName, ManualResetEventSlim startWorkloadSignal, ManualResetEventSlim workloadCompleteSignal) { Assert.True(sqlCommand != null, "sqlCommand should not be null."); Assert.True(!string.IsNullOrWhiteSpace(tableName), "tableName should not be null or empty."); - _sqlCommand = sqlCommand; - _tableName = tableName; - _numberofCancelCommands = numberofTimesToCancel; + SqlCommand = sqlCommand; + TableName = tableName; + StartWorkloadSignal = startWorkloadSignal; + WorkloadCompleteSignal = workloadCompleteSignal; } } } diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/CspProviderExt.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/CspProviderExt.cs index bdd48967b5..54a4b0c175 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/CspProviderExt.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/CspProviderExt.cs @@ -54,7 +54,7 @@ public void TestRoundTripWithCspAndCertStoreProvider() [MemberData(nameof(TestEncryptDecryptWithCsp_Data))] public void TestEncryptDecryptWithCsp(string connectionString, string providerName, int providerType) { - string keyIdentifier = DataTestUtility.GetUniqueNameForSqlServer("CSP"); + string keyIdentifier = DataTestUtility.GetLongName("CSP"); CspParameters namedCspParameters = new CspParameters(providerType, providerName, keyIdentifier); using SQLSetupStrategyCspProvider sqlSetupStrategyCsp = new SQLSetupStrategyCspProvider(namedCspParameters); diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/ExceptionTestAKVStore.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/ExceptionTestAKVStore.cs index 2465633a03..15679176ac 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/ExceptionTestAKVStore.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/ExceptionTestAKVStore.cs @@ -54,7 +54,7 @@ public void NullEncryptionAlgorithm() public void EmptyColumnEncryptionKey() { Exception ex1 = Assert.Throws(() => _fixture.AkvStoreProvider.EncryptColumnEncryptionKey(_fixture.AkvKeyUrl, MasterKeyEncAlgo, new byte[] { })); - Assert.Matches($@"Internal error. Empty columnEncryptionKey specified.", ex1.Message); + Assert.Matches($@"Internal error. Empty 'columnEncryptionKey' specified.", ex1.Message); } [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.IsAKVSetupAvailable))] @@ -68,7 +68,7 @@ public void NullColumnEncryptionKey() public void EmptyEncryptedColumnEncryptionKey() { Exception ex1 = Assert.Throws(() => _fixture.AkvStoreProvider.DecryptColumnEncryptionKey(_fixture.AkvKeyUrl, MasterKeyEncAlgo, new byte[] { })); - Assert.Matches($@"Internal error. Empty encryptedColumnEncryptionKey specified", ex1.Message); + Assert.Matches($@"Internal error. Empty 'encryptedColumnEncryptionKey' specified", ex1.Message); } [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.IsAKVSetupAvailable))] @@ -250,7 +250,7 @@ public void InvalidTrustedEndpoints(string[] trustedEndpoints) SqlColumnEncryptionAzureKeyVaultProvider azureKeyProvider = new SqlColumnEncryptionAzureKeyVaultProvider( new SqlClientCustomTokenCredential(), trustedEndpoints); }); - Assert.Matches("One or more of the elements in trustedEndpoints are null or empty or consist of only whitespace.", ex.Message); + Assert.Matches("One or more of the elements in 'trustedEndpoints' are null or empty or consist of only whitespace.", ex.Message); } [InlineData(null)] @@ -264,7 +264,7 @@ public void InvalidTrustedEndpoint(string trustedEndpoint) SqlColumnEncryptionAzureKeyVaultProvider azureKeyProvider = new SqlColumnEncryptionAzureKeyVaultProvider( new SqlClientCustomTokenCredential(), trustedEndpoint); }); - Assert.Matches("One or more of the elements in trustedEndpoints are null or empty or consist of only whitespace.", ex.Message); + Assert.Matches("One or more of the elements in 'trustedEndpoints' are null or empty or consist of only whitespace.", ex.Message); } } } diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/SqlDataAdapterBatchUpdateTests.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/SqlDataAdapterBatchUpdateTests.cs new file mode 100644 index 0000000000..0622f4b9f6 --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/SqlDataAdapterBatchUpdateTests.cs @@ -0,0 +1,255 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Data; +using System.Threading.Tasks; +using System.Collections.Generic; +using Microsoft.Data.SqlClient; +using Microsoft.Data.SqlClient.ManualTesting.Tests.AlwaysEncrypted.Setup; +using Xunit; + +namespace Microsoft.Data.SqlClient.ManualTesting.Tests.AlwaysEncrypted +{ + public sealed class SqlDataAdapterBatchUpdateTests : IClassFixture, IDisposable + { + private readonly SQLSetupStrategy _fixture; + private readonly Dictionary tableNames = new(); + + public SqlDataAdapterBatchUpdateTests(SQLSetupStrategyCertStoreProvider context) + { + _fixture = context; + + // Provide table names to mirror repo patterns. + // If your fixture already exposes specific names for BuyerSeller and procs, wire them here. + // Otherwise use literal names as below. + tableNames["BuyerSeller"] = "BuyerSeller"; + tableNames["ProcInsertBuyerSeller"] = "InsertBuyerSeller"; + tableNames["ProcUpdateBuyerSeller"] = "UpdateBuyerSeller"; + } + + // ---------- TESTS ---------- + + [ConditionalTheory(typeof(DataTestUtility), nameof(DataTestUtility.IsTargetReadyForAeWithKeyStore))] + [ClassData(typeof(AEConnectionStringProvider))] + public async Task AdapterUpdate_BatchSizeGreaterThanOne_Succeeds(string connectionString) + { + // Arrange + // Ensure baseline rows exist + TruncateTables("BuyerSeller", connectionString); + PopulateTable("BuyerSeller", new (int id, string s1, string s2)[] { + (1, "123-45-6789", "987-65-4321"), + (2, "234-56-7890", "876-54-3210"), + (3, "345-67-8901", "765-43-2109"), + (4, "456-78-9012", "654-32-1098"), + }, connectionString); + + using var conn = new SqlConnection(GetOpenConnectionString(connectionString, encryptionEnabled: true)); + await conn.OpenAsync(); + + using var adapter = CreateAdapter(conn, updateBatchSize: 10); // failure repro: > 1 + var dataTable = BuildBuyerSellerDataTable(); + LoadCurrentRowsIntoDataTable(dataTable, conn); + + // Mutate values for update + MutateForUpdate(dataTable); + + // Act - With batch updates (UpdateBatchSize > 1), this previously threw NullReferenceException due to null systemParams in batch RPC mode + var updated = await Task.Run(() => adapter.Update(dataTable)); + + // Assert + Assert.Equal(dataTable.Rows.Count, updated); + + } + + [ConditionalTheory(typeof(DataTestUtility), nameof(DataTestUtility.IsTargetReadyForAeWithKeyStore))] + [ClassData(typeof(AEConnectionStringProvider))] + public async Task AdapterUpdate_BatchSizeOne_Succeeds(string connectionString) + { + // Arrange + TruncateTables("BuyerSeller", connectionString); + PopulateTable("BuyerSeller", new (int id, string s1, string s2)[] { + (1, "123-45-6789", "987-65-4321"), + (2, "234-56-7890", "876-54-3210"), + (3, "345-67-8901", "765-43-2109"), + (4, "456-78-9012", "654-32-1098"), + }, connectionString); + + using var conn = new SqlConnection(GetOpenConnectionString(connectionString, encryptionEnabled: true)); + await conn.OpenAsync(); + + using var adapter = CreateAdapter(conn, updateBatchSize: 1); // success path + var dataTable = BuildBuyerSellerDataTable(); + LoadCurrentRowsIntoDataTable(dataTable, conn); + + MutateForUpdate(dataTable); + + // Act (should not throw) + var updatedRows = await Task.Run(() => adapter.Update(dataTable)); + + // Assert + Assert.Equal(dataTable.Rows.Count, updatedRows); + + } + + // ---------- HELPERS ---------- + + private SqlDataAdapter CreateAdapter(SqlConnection connection, int updateBatchSize) + { + // Insert + var insertCmd = new SqlCommand(tableNames["ProcInsertBuyerSeller"], connection) + { + CommandType = CommandType.StoredProcedure + }; + insertCmd.Parameters.AddRange(new[] + { + new SqlParameter("@BuyerSellerID", SqlDbType.Int) { SourceColumn = "BuyerSellerID" }, + new SqlParameter("@SSN1", SqlDbType.VarChar, 255) { SourceColumn = "SSN1" }, + new SqlParameter("@SSN2", SqlDbType.VarChar, 255) { SourceColumn = "SSN2" }, + }); + insertCmd.UpdatedRowSource = UpdateRowSource.None; + + // Update + var updateCmd = new SqlCommand(tableNames["ProcUpdateBuyerSeller"], connection) + { + CommandType = CommandType.StoredProcedure + }; + updateCmd.Parameters.AddRange(new[] + { + new SqlParameter("@BuyerSellerID", SqlDbType.Int) { SourceColumn = "BuyerSellerID" }, + new SqlParameter("@SSN1", SqlDbType.VarChar, 255) { SourceColumn = "SSN1" }, + new SqlParameter("@SSN2", SqlDbType.VarChar, 255) { SourceColumn = "SSN2" }, + }); + updateCmd.UpdatedRowSource = UpdateRowSource.None; + + return new SqlDataAdapter + { + InsertCommand = insertCmd, + UpdateCommand = updateCmd, + UpdateBatchSize = updateBatchSize + }; + } + + private DataTable BuildBuyerSellerDataTable() + { + var dt = new DataTable(tableNames["BuyerSeller"]); + dt.Columns.AddRange(new[] + { + new DataColumn("BuyerSellerID", typeof(int)), + new DataColumn("SSN1", typeof(string)), + new DataColumn("SSN2", typeof(string)), + }); + dt.PrimaryKey = new[] { dt.Columns["BuyerSellerID"] }; + return dt; + } + + private void LoadCurrentRowsIntoDataTable(DataTable dt, SqlConnection conn) + { + using var cmd = new SqlCommand($"SELECT BuyerSellerID, SSN1, SSN2 FROM [dbo].[{tableNames["BuyerSeller"]}] ORDER BY BuyerSellerID", conn); + using var reader = cmd.ExecuteReader(); + while (reader.Read()) + { + dt.Rows.Add(reader.GetInt32(0), reader.GetString(1), reader.GetString(2)); + } + } + + private void MutateForUpdate(DataTable dt) + { + int i = 0; + var fixedTime = new DateTime(2023, 01, 01, 12, 34, 56); // Use any fixed value + string timeStr = fixedTime.ToString("HHmm"); + foreach (DataRow row in dt.Rows) + { + i++; + row["SSN1"] = $"{i:000}-11-{timeStr}"; + row["SSN2"] = $"{i:000}-22-{timeStr}"; + } + } + + internal void TruncateTables(string tableName, string connectionString) + { + using var connection = new SqlConnection(GetOpenConnectionString(connectionString, encryptionEnabled: true)); + connection.Open(); + SilentRunCommand($@"TRUNCATE TABLE [dbo].[{tableNames[tableName]}]", connection); + } + + internal void ExecuteQuery(SqlConnection connection, string commandText) + { + // Mirror AE-enabled command execution style used in repo tests + using var cmd = new SqlCommand( + commandText, + connection: connection, + transaction: null, + columnEncryptionSetting: SqlCommandColumnEncryptionSetting.Enabled); + cmd.ExecuteNonQuery(); + } + + internal void PopulateTable(string tableName, (int id, string s1, string s2)[] rows, string connectionString) + { + using var connection = new SqlConnection(GetOpenConnectionString(connectionString, encryptionEnabled: true)); + connection.Open(); + + foreach (var (id, s1, s2) in rows) + { + using var cmd = new SqlCommand( + $@"INSERT INTO [dbo].[{tableNames[tableName]}] (BuyerSellerID, SSN1, SSN2) VALUES (@id, @s1, @s2)", + connection, + null, + SqlCommandColumnEncryptionSetting.Enabled); + + cmd.Parameters.Add(new SqlParameter("@id", SqlDbType.Int) { Value = id }); + cmd.Parameters.Add(new SqlParameter("@s1", SqlDbType.VarChar, 255) { Value = s1 }); + cmd.Parameters.Add(new SqlParameter("@s2", SqlDbType.VarChar, 255) { Value = s2 }); + + cmd.ExecuteNonQuery(); + } + } + + public string GetOpenConnectionString(string baseConnectionString, bool encryptionEnabled) + { + var builder = new SqlConnectionStringBuilder(baseConnectionString) + { + // TrustServerCertificate can be set based on environment; mirror repo’s AE toggling idiom + ColumnEncryptionSetting = encryptionEnabled + ? SqlConnectionColumnEncryptionSetting.Enabled + : SqlConnectionColumnEncryptionSetting.Disabled + }; + return builder.ToString(); + } + + internal void SilentRunCommand(string commandText, SqlConnection connection) + { + try + { ExecuteQuery(connection, commandText); } + catch (SqlException ex) + { + // Only swallow "object does not exist" (error 208), log others + bool onlyObjectNotExist = true; + foreach (SqlError err in ex.Errors) + { + if (err.Number != 208) + { + onlyObjectNotExist = false; + break; + } + } + if (!onlyObjectNotExist) + { + Console.WriteLine($"SilentRunCommand: Unexpected SqlException during cleanup: {ex}"); + } + // Swallow all exceptions, but log unexpected ones + } + } + + public void Dispose() + { + foreach (string connectionString in DataTestUtility.AEConnStringsSetup) + { + using var connection = new SqlConnection(GetOpenConnectionString(connectionString, encryptionEnabled: true)); + connection.Open(); + SilentRunCommand($"DELETE FROM [dbo].[{tableNames["BuyerSeller"]}]", connection); + } + } + } +} diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/TestFixtures/DatabaseHelper.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/TestFixtures/DatabaseHelper.cs index 33d548cd85..9c85c4fc67 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/TestFixtures/DatabaseHelper.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/TestFixtures/DatabaseHelper.cs @@ -290,10 +290,8 @@ public IEnumerator GetEnumerator() { foreach (string connStrAE in DataTestUtility.AEConnStrings) { - yield return new object[] { connStrAE, @"ExecuteReader", 1 }; - yield return new object[] { connStrAE, @"ExecuteReader", 3 }; - yield return new object[] { connStrAE, @"ExecuteNonQuery", 1 }; - yield return new object[] { connStrAE, @"ExecuteNonQuery", 3 }; + yield return new object[] { connStrAE, @"ExecuteReader" }; + yield return new object[] { connStrAE, @"ExecuteNonQuery" }; } } IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/DataCommon/DataTestUtility.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/DataCommon/DataTestUtility.cs index aacc723453..41a9bf5923 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/DataCommon/DataTestUtility.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/DataCommon/DataTestUtility.cs @@ -7,7 +7,6 @@ using System.Data; using System.Data.SqlTypes; using System.Diagnostics.Tracing; -using System.Globalization; using System.IO; using System.Linq; using System.Net; @@ -18,6 +17,7 @@ using System.Security; using System.Security.Principal; using System.Text; +using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; using Azure.Core; @@ -25,6 +25,7 @@ using Microsoft.Data.SqlClient.TestUtilities; using Microsoft.Identity.Client; using Xunit; +using Xunit.Abstractions; namespace Microsoft.Data.SqlClient.ManualTesting.Tests { @@ -92,12 +93,6 @@ public static class DataTestUtility //SQL Server EngineEdition private static string s_sqlServerEngineEdition; - // JSON Column type - public static readonly bool IsJsonSupported = false; - - // VECTOR column type - public static readonly bool IsVectorSupported = false; - // Azure Synapse EngineEditionId == 6 // More could be read at https://learn.microsoft.com/en-us/sql/t-sql/functions/serverproperty-transact-sql?view=sql-server-ver16#propertyname public static bool IsAzureSynapse @@ -106,7 +101,7 @@ public static bool IsAzureSynapse { if (!string.IsNullOrEmpty(TCPConnectionString)) { - s_sqlServerEngineEdition ??= GetSqlServerProperty(TCPConnectionString, "EngineEdition"); + s_sqlServerEngineEdition ??= GetSqlServerProperty(TCPConnectionString, ServerProperty.EngineEdition); } _ = int.TryParse(s_sqlServerEngineEdition, out int engineEditon); return engineEditon == 6; @@ -117,7 +112,7 @@ public static bool TcpConnectionStringDoesNotUseAadAuth { get { - SqlConnectionStringBuilder builder = new (TCPConnectionString); + SqlConnectionStringBuilder builder = new(TCPConnectionString); return builder.Authentication == SqlAuthenticationMethod.SqlPassword || builder.Authentication == SqlAuthenticationMethod.NotSpecified; } } @@ -128,7 +123,7 @@ public static string SQLServerVersion { if (!string.IsNullOrEmpty(TCPConnectionString)) { - s_sQLServerVersion ??= GetSqlServerProperty(TCPConnectionString, "ProductMajorVersion"); + s_sQLServerVersion ??= GetSqlServerProperty(TCPConnectionString, ServerProperty.ProductMajorVersion); } return s_sQLServerVersion; } @@ -179,7 +174,6 @@ static DataTestUtility() ManagedIdentitySupported = c.ManagedIdentitySupported; IsManagedInstance = c.IsManagedInstance; AliasName = c.AliasName; - IsJsonSupported = c.IsJsonSupported; #if NETFRAMEWORK System.Net.ServicePointManager.SecurityProtocol |= System.Net.SecurityProtocolType.Tls12; @@ -242,7 +236,9 @@ public static IEnumerable GetConnectionStrings(bool withEnclave) yield return TCPConnectionString; } // Named Pipes are not supported on Unix platform and for Azure DB - if (Environment.OSVersion.Platform != PlatformID.Unix && IsNotAzureServer() && !string.IsNullOrEmpty(NPConnectionString)) + if (Environment.OSVersion.Platform != PlatformID.Unix && + IsNotAzureServer() && + !string.IsNullOrEmpty(NPConnectionString)) { yield return NPConnectionString; } @@ -291,29 +287,99 @@ private static Task AcquireTokenAsync(string authorityURL, string userID public static bool IsKerberosTest => !string.IsNullOrEmpty(KerberosDomainUser) && !string.IsNullOrEmpty(KerberosDomainPassword); - public static string GetSqlServerProperty(string connectionString, string propertyName) + #nullable enable + + /// + /// Returns the current test name as: + /// + /// ClassName.MethodName + /// + /// xUnit v2 doesn't provide access to a test context, so we use + /// reflection into the ITestOutputHelper to get the test name. + /// + /// + /// + /// The output helper instance for the currently running test. + /// + /// + /// The current test name. + public static string CurrentTestName(ITestOutputHelper outputHelper) + { + // Reflect our way to the ITest instance. + var type = outputHelper.GetType(); + Assert.NotNull(type); + var testMember = type.GetField("test", BindingFlags.Instance | BindingFlags.NonPublic); + Assert.NotNull(testMember); + var test = testMember.GetValue(outputHelper) as ITest; + Assert.NotNull(test); + + // The DisplayName is in the format: + // + // Namespace.ClassName.MethodName(args) + // + // We only want the ClassName.MethodName portion. + // + Match match = TestNameRegex.Match(test.DisplayName); + Assert.True(match.Success); + // There should be 2 groups: the overall match, and the capture + // group. + Assert.Equal(2, match.Groups.Count); + + // The portion we want is in the capture group. + return match.Groups[1].Value; + } + + private static readonly Regex TestNameRegex = new( + // Capture the ClassName.MethodName portion, which may terminate + // the name, or have (args...) appended. + @"\.((?:[^.]+)\.(?:[^.\(]+))(?:\(.*\))?$", + RegexOptions.Compiled); + + /// + /// SQL Server properties we can query. + /// + /// GOTCHA: The enum member names must match the property names + /// queryable via T-SQL SERVERPROPERTY(). See: + /// + /// https://learn.microsoft.com/en-us/sql/t-sql/functions/serverproperty-transact-sql + /// + public enum ServerProperty + { + ProductMajorVersion, + EngineEdition + } + + public static string GetSqlServerProperty(string connectionString, ServerProperty property) { - string propertyValue = string.Empty; using SqlConnection conn = new(connectionString); conn.Open(); - SqlCommand command = conn.CreateCommand(); - command.CommandText = $"SELECT SERVERProperty('{propertyName}')"; - SqlDataReader reader = command.ExecuteReader(); - if (reader.Read()) + return GetSqlServerProperty(conn, property); + } + + public static string GetSqlServerProperty(SqlConnection connection, ServerProperty property) + { + using SqlCommand command = connection.CreateCommand(); + command.CommandText = $"SELECT SERVERProperty('{property}')"; + using SqlDataReader reader = command.ExecuteReader(); + + Assert.True(reader.Read()); + + switch (property) { - switch (propertyName) - { - case "EngineEdition": - propertyValue = reader.GetInt32(0).ToString(); - break; - case "ProductMajorVersion": - propertyValue = reader.GetString(0); - break; - } + case ServerProperty.EngineEdition: + // EngineEdition is returned as an int. + return reader.GetInt32(0).ToString(); + case ServerProperty.ProductMajorVersion: + default: + // ProductMajorVersion is returned as a string. + // + // Assume any unknown property is also a string. + return reader.GetString(0); } - return propertyValue; } + #nullable disable + public static bool GetSQLServerStatusOnTDS8(string connectionString) { bool isTDS8Supported = false; @@ -404,6 +470,8 @@ public static bool IsEnclaveAzureDatabaseSetup() public static bool IsNotAzureSynapse() => !IsAzureSynapse; + public static bool IsNotManagedInstance() => !IsManagedInstance; + // Synapse: UDT Test Database not compatible with Azure Synapse. public static bool IsUdtTestDatabasePresent() => IsDatabasePresent(UdtTestDbName) && IsNotAzureSynapse(); @@ -451,6 +519,11 @@ public static bool IsAADAuthorityURLSetup() return !string.IsNullOrEmpty(AADAuthorityURL); } + public static bool IsAzureServer() + { + return AreConnStringsSetup() && Utils.IsAzureSqlServer(new SqlConnectionStringBuilder(TCPConnectionString).DataSource); + } + public static bool IsNotAzureServer() { return !AreConnStringsSetup() || !Utils.IsAzureSqlServer(new SqlConnectionStringBuilder(TCPConnectionString).DataSource); @@ -558,48 +631,176 @@ public static bool DoesHostAddressContainBothIPv4AndIPv6() } } + // Generate a new GUID and return the characters from its 1st and 4th + // parts, as shown here: + // + // 7ff01cb8-88c7-11f0-b433-00155d7e531e + // ^^^^^^^^ ^^^^ + // + // These 12 characters are concatenated together without any + // separators. These 2 parts typically comprise a timestamp and clock + // sequence, most likely to be unique for tests that generate names in + // quick succession. + private static string GetGuidParts() + { + var guid = Guid.NewGuid().ToString(); + // GOTCHA: The slice operator is inclusive of the start index and + // exclusive of the end index! + return guid.Substring(0, 8) + guid.Substring(19, 4); + } + /// - /// Generate a unique name to use in Sql Server; - /// some providers does not support names (Oracle supports up to 30). + /// Generate a short unique database object name, whose maximum length + /// is 30 characters, with the format: + /// + /// _ + /// + /// The Prefix will be truncated to satisfy the overall maximum length. + /// + /// The GUID parts will be the characters from the 1st and 4th blocks + /// from a traditional string representation, as shown here: + /// + /// 7ff01cb8-88c7-11f0-b433-00155d7e531e + /// ^^^^^^^^ ^^^^ + /// + /// These 2 parts typically comprise a timestamp and clock sequence, + /// most likely to be unique for tests that generate names in quick + /// succession. The 12 characters are concatenated together without any + /// separators. /// - /// The name length will be no more then (16 + prefix.Length + escapeLeft.Length + escapeRight.Length). - /// Name without brackets. - /// Unique name by considering the Sql Server naming rules. - public static string GetUniqueName(string prefix, bool withBracket = true) - { - string escapeLeft = withBracket ? "[" : string.Empty; - string escapeRight = withBracket ? "]" : string.Empty; - string uniqueName = string.Format("{0}{1}_{2}_{3}{4}", - escapeLeft, - prefix, - DateTime.Now.Ticks.ToString("X", CultureInfo.InvariantCulture), // up to 8 characters - Guid.NewGuid().ToString().Substring(0, 6), // take the first 6 characters only - escapeRight); - return uniqueName; + /// + /// + /// The prefix to use when generating the unique name, truncated to at + /// most 18 characters when withBracket is false, and 16 characters when + /// withBracket is true. + /// + /// This should not contain any characters that cannot be used in + /// database object names. See: + /// + /// https://learn.microsoft.com/en-us/sql/relational-databases/databases/database-identifiers?view=sql-server-ver17#rules-for-regular-identifiers + /// + /// + /// + /// When true, the entire generated name will be enclosed in square + /// brackets, for example: + /// + /// [MyPrefix_7ff01cb811f0] + /// + /// + /// + /// A unique database object name, no more than 30 characters long. + /// + public static string GetShortName(string prefix, bool withBracket = true) + { + StringBuilder name = new(30); + + if (withBracket) + { + name.Append('['); + } + + int maxPrefixLength = withBracket ? 16 : 18; + if (prefix.Length > maxPrefixLength) + { + prefix = prefix.Substring(0, maxPrefixLength); + } + + name.Append(prefix); + name.Append('_'); + name.Append(GetGuidParts()); + + if (withBracket) + { + name.Append(']'); + } + + return name.ToString(); } /// - /// Uses environment values `UserName` and `MachineName` in addition to the specified `prefix` and current date - /// to generate a unique name to use in Sql Server; - /// SQL Server supports long names (up to 128 characters), add extra info for troubleshooting. + /// Generate a long unique database object name, whose maximum length is + /// 96 characters, with the format: + /// + /// ___ + /// + /// The Prefix will be truncated to satisfy the overall maximum length. + /// + /// The GUID Parts will be the characters from the 1st and 4th blocks + /// from a traditional string representation, as shown here: + /// + /// 7ff01cb8-88c7-11f0-b433-00155d7e531e + /// ^^^^^^^^ ^^^^ + /// + /// These 2 parts typically comprise a timestamp and clock sequence, + /// most likely to be unique for tests that generate names in quick + /// succession. The 12 characters are concatenated together without any + /// separators. + /// + /// The UserName and MachineName are obtained from the Environment, + /// and will be truncated to satisfy the maximum overall length. /// - /// Add the prefix to the generate string. - /// Database name must be pass with brackets by default. - /// Unique name by considering the Sql Server naming rules. - public static string GetUniqueNameForSqlServer(string prefix, bool withBracket = true) + /// + /// + /// The prefix to use when generating the unique name, truncated to at + /// most 32 characters. + /// + /// This should not contain any characters that cannot be used in + /// database object names. See: + /// + /// https://learn.microsoft.com/en-us/sql/relational-databases/databases/database-identifiers?view=sql-server-ver17#rules-for-regular-identifiers + /// + /// + /// + /// When true, the entire generated name will be enclosed in square + /// brackets, for example: + /// + /// [MyPrefix_7ff01cb811f0_test_user_ci_agent_machine_name] + /// + /// + /// + /// A unique database object name, no more than 96 characters long. + /// + public static string GetLongName(string prefix, bool withBracket = true) { - string extendedPrefix = string.Format( - "{0}_{1}_{2}@{3}", - prefix, - Environment.UserName, - Environment.MachineName, - DateTime.Now.ToString("yyyy_MM_dd", CultureInfo.InvariantCulture)); - string name = GetUniqueName(extendedPrefix, withBracket); - if (name.Length > 128) + StringBuilder name = new(96); + + if (withBracket) + { + name.Append('['); + } + + if (prefix.Length > 32) { - throw new ArgumentOutOfRangeException("the name is too long - SQL Server names are limited to 128"); + prefix = prefix.Substring(0, 32); } - return name; + + name.Append(prefix); + name.Append('_'); + name.Append(GetGuidParts()); + name.Append('_'); + + var suffix = + Environment.UserName + '_' + + Environment.MachineName; + + int maxSuffixLength = 96 - name.Length; + if (withBracket) + { + --maxSuffixLength; + } + if (suffix.Length > maxSuffixLength) + { + suffix = suffix.Substring(0, maxSuffixLength); + } + + name.Append(suffix); + + if (withBracket) + { + name.Append(']'); + } + + return name.ToString(); } public static void CreateTable(SqlConnection sqlConnection, string tableName, string createBody) @@ -1090,65 +1291,102 @@ protected virtual void OnMatchingEventWritten(EventWrittenEventArgs eventData) } } - public readonly ref struct XEventScope // : IDisposable + #nullable enable + + public readonly ref struct XEventScope // IDisposable { + #region Private Fields + + // Maximum dispatch latency for XEvents, in seconds. + private const int MaxDispatchLatencySeconds = 5; + + // The connection to use for all operations. private readonly SqlConnection _connection; - private readonly bool _useDatabaseSession; - public string SessionName { get; } + // True if connected to an Azure SQL instance. + private readonly bool _isAzureSql; - public XEventScope(SqlConnection connection, string eventSpecification, string targetSpecification) - { - SessionName = GenerateRandomCharacters("Session"); - _connection = connection; - // SQL Azure only supports database-scoped XEvent sessions - _useDatabaseSession = GetSqlServerProperty(connection.ConnectionString, "EngineEdition") == "5"; + // True if connected to a non-Azure SQL Server 2025 (version 17) or + // higher. + private readonly bool _isVersion17OrHigher; - SetupXEvent(eventSpecification, targetSpecification); - } + // Duration for the XEvent session, in minutes. + private readonly ushort _durationInMinutes; - public void Dispose() - => DropXEvent(); + #endregion - public System.Xml.XmlDocument GetEvents() - { - string xEventQuery = _useDatabaseSession - ? $@"SELECT xet.target_data - FROM sys.dm_xe_database_session_targets AS xet - INNER JOIN sys.dm_xe_database_sessions AS xe - ON (xe.address = xet.event_session_address) - WHERE xe.name = '{SessionName}'" - : $@"SELECT xet.target_data - FROM sys.dm_xe_session_targets AS xet - INNER JOIN sys.dm_xe_sessions AS xe - ON (xe.address = xet.event_session_address) - WHERE xe.name = '{SessionName}'"; + #region Properties - using (SqlCommand command = new SqlCommand(xEventQuery, _connection)) - { - if (_connection.State == ConnectionState.Closed) - { - _connection.Open(); - } + /// + /// The name of the XEvent session, derived from the session name + /// provided at construction time, with a unique suffix appended. + /// + public string SessionName { get; } + + #endregion + + #region Construction + + /// + /// Construct with the specified parameters. + /// + /// This will use the connection to query the server properties and + /// setup and start the XEvent session. + /// + /// The base name of the session. + /// The SQL connection to use. (Must already be open.) + /// The event specification T-SQL string. + /// The target specification T-SQL string. + /// The duration of the session in minutes. + public XEventScope( + string sessionName, + // The connection must already be open. + SqlConnection connection, + string eventSpecification, + string targetSpecification, + ushort durationInMinutes = 5) + { + SessionName = GenerateRandomCharacters(sessionName); + + _connection = connection; + Assert.Equal(ConnectionState.Open, _connection.State); + + _durationInMinutes = durationInMinutes; - string targetData = command.ExecuteScalar() as string; - System.Xml.XmlDocument xmlDocument = new System.Xml.XmlDocument(); + // EngineEdition 5 indicates Azure SQL. + _isAzureSql = GetSqlServerProperty(connection, ServerProperty.EngineEdition) == "5"; - xmlDocument.LoadXml(targetData); - return xmlDocument; + // Determine if we're connected to a SQL Server instance version + // 17 or higher. + if (!_isAzureSql) + { + int majorVersion; + Assert.True( + int.TryParse( + GetSqlServerProperty(connection, ServerProperty.ProductMajorVersion), + out majorVersion)); + _isVersion17OrHigher = majorVersion >= 17; } - } - private void SetupXEvent(string eventSpecification, string targetSpecification) - { - string sessionLocation = _useDatabaseSession ? "DATABASE" : "SERVER"; - string xEventCreateAndStartCommandText = $@"CREATE EVENT SESSION [{SessionName}] ON {sessionLocation} + // Setup and start the XEvent session. + string sessionLocation = _isAzureSql ? "DATABASE" : "SERVER"; + + // Both Azure SQL and SQL Server 2025+ support setting a maximum + // duration for the XEvent session. + string duration = + _isAzureSql || _isVersion17OrHigher + ? $"MAX_DURATION={_durationInMinutes} MINUTES," + : string.Empty; + + string xEventCreateAndStartCommandText = + $@"CREATE EVENT SESSION [{SessionName}] ON {sessionLocation} {eventSpecification} {targetSpecification} WITH ( - MAX_MEMORY=4096 KB, + {duration} + MAX_MEMORY=16 MB, EVENT_RETENTION_MODE=ALLOW_SINGLE_EVENT_LOSS, - MAX_DISPATCH_LATENCY=30 SECONDS, + MAX_DISPATCH_LATENCY={MaxDispatchLatencySeconds} SECONDS, MAX_EVENT_SIZE=0 KB, MEMORY_PARTITION_MODE=NONE, TRACK_CAUSALITY=ON, @@ -1156,30 +1394,88 @@ private void SetupXEvent(string eventSpecification, string targetSpecification) ALTER EVENT SESSION [{SessionName}] ON {sessionLocation} STATE = START "; - using (SqlCommand createXEventSession = new SqlCommand(xEventCreateAndStartCommandText, _connection)) - { - if (_connection.State == ConnectionState.Closed) - { - _connection.Open(); - } + using SqlCommand createXEventSession = new SqlCommand(xEventCreateAndStartCommandText, _connection); + createXEventSession.ExecuteNonQuery(); + } + + /// + /// Disposal stops and drops the XEvent session. + /// + /// + /// Disposal isn't perfect - tests can abort without cleaning up the + /// events they have created. For Azure SQL targets that outlive the + /// test pipelines, it is beneficial to periodically log into the + /// database and drop old XEvent sessions using T-SQL similar to + /// this: + /// + /// DECLARE @sql NVARCHAR(MAX) = N''; + /// + /// -- Identify inactive (stopped) event sessions and generate DROP commands + /// SELECT @sql += N'DROP EVENT SESSION [' + name + N'] ON SERVER;' + CHAR(13) + CHAR(10) + /// FROM sys.server_event_sessions + /// WHERE running = 0; -- Filter for sessions that are not running (inactive) + /// + /// -- Print the generated commands for review (optional, but recommended) + /// PRINT @sql; + /// + /// -- Execute the generated commands + /// EXEC sys.sp_executesql @sql; + /// + public void Dispose() + { + string dropXEventSessionCommand = _isAzureSql + // We choose the sys.(database|server)_event_sessions views + // here to ensure we find sessions that may not be running. + ? $"IF EXISTS (select * from sys.database_event_sessions where name ='{SessionName}')" + + $" DROP EVENT SESSION [{SessionName}] ON DATABASE" + : $"IF EXISTS (select * from sys.server_event_sessions where name ='{SessionName}')" + + $" DROP EVENT SESSION [{SessionName}] ON SERVER"; - createXEventSession.ExecuteNonQuery(); - } + using SqlCommand command = new SqlCommand(dropXEventSessionCommand, _connection); + command.ExecuteNonQuery(); } - private void DropXEvent() + #endregion + + #region Public Methods + + /// + /// Query the XEvent session for its collected events, returning + /// them as an XML document. + /// + /// This always blocks the thread for MaxDispatchLatencySeconds to + /// ensure that all events have been flushed into the ring buffer. + /// + public System.Xml.XmlDocument GetEvents() { - string dropXEventSessionCommand = _useDatabaseSession - ? $"IF EXISTS (select * from sys.dm_xe_database_sessions where name ='{SessionName}')" + - $" DROP EVENT SESSION [{SessionName}] ON DATABASE" - : $"IF EXISTS (select * from sys.dm_xe_sessions where name ='{SessionName}')" + - $" DROP EVENT SESSION [{SessionName}] ON SERVER"; + string xEventQuery = _isAzureSql + ? $@"SELECT xet.target_data + FROM sys.dm_xe_database_session_targets AS xet + INNER JOIN sys.dm_xe_database_sessions AS xe + ON (xe.address = xet.event_session_address) + WHERE xe.name = '{SessionName}'" + : $@"SELECT xet.target_data + FROM sys.dm_xe_session_targets AS xet + INNER JOIN sys.dm_xe_sessions AS xe + ON (xe.address = xet.event_session_address) + WHERE xe.name = '{SessionName}'"; - using (SqlCommand command = new SqlCommand(dropXEventSessionCommand, _connection)) - { - command.ExecuteNonQuery(); - } + using SqlCommand command = new SqlCommand(xEventQuery, _connection); + + // Wait for maximum dispatch latency to ensure all events + // have been flushed to the ring buffer. + Thread.Sleep(MaxDispatchLatencySeconds * 1000); + + string? targetData = command.ExecuteScalar() as string; + Assert.NotNull(targetData); + + System.Xml.XmlDocument xmlDocument = new System.Xml.XmlDocument(); + + xmlDocument.LoadXml(targetData); + return xmlDocument; } + + #endregion } /// @@ -1208,4 +1504,6 @@ public static string GetMachineFQDN(string hostname) return fqdn.ToString(); } } + + #nullable disable } diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/Microsoft.Data.SqlClient.ManualTesting.Tests.csproj b/src/Microsoft.Data.SqlClient/tests/ManualTests/Microsoft.Data.SqlClient.ManualTesting.Tests.csproj index 7bd503c5b9..43574100b7 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/Microsoft.Data.SqlClient.ManualTesting.Tests.csproj +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/Microsoft.Data.SqlClient.ManualTesting.Tests.csproj @@ -12,6 +12,9 @@ $(BinFolder)$(Configuration).$(Platform).$(AssemblyName) true + + + @@ -185,6 +188,9 @@ + + + @@ -213,6 +219,9 @@ + + + @@ -252,6 +261,7 @@ + @@ -271,6 +281,7 @@ + @@ -294,13 +305,7 @@ - - - - - - diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/ProviderAgnostic/ReaderTest/ReaderTest.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/ProviderAgnostic/ReaderTest/ReaderTest.cs index c99fe94807..50e2b9253c 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/ProviderAgnostic/ReaderTest/ReaderTest.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/ProviderAgnostic/ReaderTest/ReaderTest.cs @@ -19,7 +19,7 @@ public static void TestMain() { string connectionString = DataTestUtility.TCPConnectionString; - string tempTable = DataTestUtility.GetUniqueNameForSqlServer("table"); + string tempTable = DataTestUtility.GetLongName("table"); DbProviderFactory provider = SqlClientFactory.Instance; try @@ -275,7 +275,7 @@ public static void TestMain() [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup), nameof(DataTestUtility.IsNotAzureSynapse))] public static void SqlDataReader_SqlBuffer_GetFieldValue() { - string tableName = DataTestUtility.GetUniqueNameForSqlServer("SqlBuffer_GetFieldValue"); + string tableName = DataTestUtility.GetLongName("SqlBuffer_GetFieldValue"); DateTimeOffset dtoffset = DateTimeOffset.Now; DateTime dt = DateTime.Now; //Exclude the millisecond because of rounding at some points by SQL Server. @@ -374,7 +374,7 @@ public static void SqlDataReader_SqlBuffer_GetFieldValue() [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup), nameof(DataTestUtility.IsNotAzureSynapse))] public static async Task SqlDataReader_SqlBuffer_GetFieldValue_Async() { - string tableName = DataTestUtility.GetUniqueNameForSqlServer("SqlBuffer_GetFieldValue_Async"); + string tableName = DataTestUtility.GetLongName("SqlBuffer_GetFieldValue_Async"); DateTimeOffset dtoffset = DateTimeOffset.Now; DateTime dt = DateTime.Now; //Exclude the millisecond because of rounding at some points by SQL Server. diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/AdapterTest/AdapterTest.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/AdapterTest/AdapterTest.cs index 439406dadb..552fbf3119 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/AdapterTest/AdapterTest.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/AdapterTest/AdapterTest.cs @@ -54,7 +54,7 @@ public class AdapterTest public AdapterTest() { // create random name for temp tables - _tempTable = DataTestUtility.GetUniqueName("AdapterTest"); + _tempTable = DataTestUtility.GetShortName("AdapterTest"); _tempTable = _tempTable.Replace('-', '_'); _randomGuid = Guid.NewGuid().ToString(); @@ -555,7 +555,7 @@ public void ParameterTest_AllTypes() [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup), nameof(DataTestUtility.IsNotAzureSynapse))] public void ParameterTest_InOut() { - string procName = DataTestUtility.GetUniqueName("P"); + string procName = DataTestUtility.GetShortName("P"); // input, output string spCreateInOut = "CREATE PROCEDURE " + procName + " @in int, @inout int OUTPUT, @out nvarchar(8) OUTPUT " + @@ -836,13 +836,13 @@ public void BulkUpdateTest() [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup), nameof(DataTestUtility.IsNotAzureSynapse))] public void UpdateRefreshTest() { - string identTableName = DataTestUtility.GetUniqueName("ID_"); + string identTableName = DataTestUtility.GetShortName("ID_"); string createIdentTable = $"CREATE TABLE {identTableName} (id int IDENTITY," + "LastName nvarchar(50) NULL," + "Firstname nvarchar(50) NULL)"; - string spName = DataTestUtility.GetUniqueName("sp_insert", withBracket: false); + string spName = DataTestUtility.GetShortName("sp_insert", withBracket: false); string spCreateInsert = $"CREATE PROCEDURE {spName}" + "(@FirstName nvarchar(50), @LastName nvarchar(50), @id int OUTPUT) " + @@ -1155,7 +1155,7 @@ public void AutoGenUpdateTest() [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup), nameof(DataTestUtility.IsNotAzureSynapse))] public void AutoGenErrorTest() { - string identTableName = DataTestUtility.GetUniqueName("ID_"); + string identTableName = DataTestUtility.GetShortName("ID_"); string createIdentTable = $"CREATE TABLE {identTableName} (id int IDENTITY," + "LastName nvarchar(50) NULL," + diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/AsyncTest/AsyncCancelledConnectionsTest.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/AsyncTest/AsyncCancelledConnectionsTest.cs index 0ae12be917..e71d6d62f6 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/AsyncTest/AsyncCancelledConnectionsTest.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/AsyncTest/AsyncCancelledConnectionsTest.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; using System.Text; @@ -37,13 +36,9 @@ public void CancelAsyncConnections() private void RunCancelAsyncConnections(SqlConnectionStringBuilder connectionStringBuilder) { SqlConnection.ClearAllPools(); - - ParallelLoopResult results = new ParallelLoopResult(); - ConcurrentDictionary tracker = new ConcurrentDictionary(); - - _random = new Random(4); // chosen via fair dice roll. _watch = Stopwatch.StartNew(); - + _random = new Random(4); // chosen via fair dice role. + ParallelLoopResult results = new ParallelLoopResult(); try { // Setup a timer so that we can see what is going on while our tasks run @@ -52,7 +47,7 @@ private void RunCancelAsyncConnections(SqlConnectionStringBuilder connectionStri results = Parallel.For( fromInclusive: 0, toExclusive: NumberOfTasks, - (int i) => DoManyAsync(i, tracker, connectionStringBuilder).GetAwaiter().GetResult()); + (int i) => DoManyAsync(connectionStringBuilder).GetAwaiter().GetResult()); } } catch (Exception ex) @@ -87,15 +82,15 @@ private void DisplaySummary() { count = _exceptionDetails.Count; } + _output.WriteLine($"{_watch.Elapsed} {_continue} Started:{_start} Done:{_done} InFlight:{_inFlight} RowsRead:{_rowsRead} ResultRead:{_resultRead} PoisonedEnded:{_poisonedEnded} nonPoisonedExceptions:{_nonPoisonedExceptions} PoisonedCleanupExceptions:{_poisonCleanUpExceptions} Count:{count} Found:{_found}"); } // This is the the main body that our Tasks run - private async Task DoManyAsync(int index, ConcurrentDictionary tracker, SqlConnectionStringBuilder connectionStringBuilder) + private async Task DoManyAsync(SqlConnectionStringBuilder connectionStringBuilder) { Interlocked.Increment(ref _start); Interlocked.Increment(ref _inFlight); - tracker[index] = true; using (SqlConnection marsConnection = new SqlConnection(connectionStringBuilder.ToString())) { @@ -105,15 +100,15 @@ private async Task DoManyAsync(int index, ConcurrentDictionary tracker } // First poison - await DoOneAsync(marsConnection, connectionStringBuilder.ToString(), poison: true, index); + await DoOneAsync(marsConnection, connectionStringBuilder.ToString(), poison: true); for (int i = 0; i < NumberOfNonPoisoned && _continue; i++) { // now run some without poisoning - await DoOneAsync(marsConnection, connectionStringBuilder.ToString(),false,index); + await DoOneAsync(marsConnection, connectionStringBuilder.ToString()); } } - tracker.TryRemove(index, out var _); + Interlocked.Decrement(ref _inFlight); Interlocked.Increment(ref _done); } @@ -122,7 +117,7 @@ private async Task DoManyAsync(int index, ConcurrentDictionary tracker // if we are poisoning we will // 1 - Interject some sleeps in the sql statement so that it will run long enough that we can cancel it // 2 - Setup a time bomb task that will cancel the command a random amount of time later - private async Task DoOneAsync(SqlConnection marsConnection, string connectionString, bool poison, int parent) + private async Task DoOneAsync(SqlConnection marsConnection, string connectionString, bool poison = false) { try { @@ -140,12 +135,12 @@ private async Task DoOneAsync(SqlConnection marsConnection, string connectionStr { if (marsConnection != null && marsConnection.State == System.Data.ConnectionState.Open) { - await RunCommand(marsConnection, builder.ToString(), poison, parent); + await RunCommand(marsConnection, builder.ToString(), poison); } else { await connection.OpenAsync(); - await RunCommand(connection, builder.ToString(), poison, parent); + await RunCommand(connection, builder.ToString(), poison); } } } @@ -181,7 +176,7 @@ private async Task DoOneAsync(SqlConnection marsConnection, string connectionStr } } - private async Task RunCommand(SqlConnection connection, string commandText, bool poison, int parent) + private async Task RunCommand(SqlConnection connection, string commandText, bool poison) { int rowsRead = 0; int resultRead = 0; @@ -216,7 +211,7 @@ private async Task RunCommand(SqlConnection connection, string commandText, bool } while (await reader.NextResultAsync() && _continue); } - catch (SqlException) when (poison) + catch when (poison) { // This looks a little strange, we failed to read above so this should fail too // But consider the case where this code is elsewhere (in the Dispose method of a class holding this logic) @@ -233,10 +228,6 @@ private async Task RunCommand(SqlConnection connection, string commandText, bool throw; } - catch (Exception ex) - { - Assert.Fail("unexpected exception: " + ex.GetType().Name + " " +ex.Message); - } } } finally diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/Common/SystemDataInternals/TdsParserStateObjectHelper.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/Common/SystemDataInternals/TdsParserStateObjectHelper.cs index dadbb5c58c..cd20701f74 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/Common/SystemDataInternals/TdsParserStateObjectHelper.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/Common/SystemDataInternals/TdsParserStateObjectHelper.cs @@ -3,7 +3,6 @@ // See the LICENSE file in the project root for more information. using System; -using System.Diagnostics; using System.Reflection; using Xunit; @@ -97,14 +96,17 @@ internal static object GetSessionHandle(object stateObject) { throw new ArgumentNullException(nameof(stateObject)); } + if (s_tdsParserStateObjectManaged is null) { throw new ArgumentException("Library being tested does not implement TdsParserStateObjectManaged", nameof(stateObject)); } + if (!s_tdsParserStateObjectManaged.IsInstanceOfType(stateObject)) { throw new ArgumentException("Object provided was not a TdsParserStateObjectManaged", nameof(stateObject)); } + return s_tdsParserStateObjectManagedSessionHandle.GetValue(stateObject); } } diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ConnectionPoolTest/ConnectionPoolStressTest.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ConnectionPoolTest/ConnectionPoolStressTest.cs new file mode 100644 index 0000000000..d3d41d4c9a --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ConnectionPoolTest/ConnectionPoolStressTest.cs @@ -0,0 +1,440 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Data.Common; +using System.Diagnostics; +using System.Linq; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; +using Xunit; + +#nullable enable + +namespace Microsoft.Data.SqlClient.ManualTesting.Tests +{ + /// + /// Connection pool stress test to validate pool behavior under various concurrent load scenarios. + /// + public class ConnectionPoolStressTest + { + #region Properties + + /// + /// Connection string + /// + internal string? ConnectionString { get; set; } + + /// + /// Maximum number of connections in the pool + /// + public int MaxPoolSize { get; set; } = 100; + + /// + /// SQL WAITFOR DELAY value for simulating slow queries + /// + public string WaitForDelay { get; set; } = "00:00:00.100"; + + /// + /// Number of concurrent connections to create + /// + public int ConcurrentConnections { get; set; } = 10; + + /// + /// Number of operations each thread should perform + /// + public int OperationsPerThread { get; set; } = 10; + + #endregion + + #region Connection Dooming + + // Reflection fields for accessing internal connection properties + private readonly FieldInfo? _internalConnectionField; + + public ConnectionPoolStressTest() + { + try + { + // Cache reflection info for Microsoft.Data.SqlClient + Type msDataConnectionType = typeof(SqlConnection); + _internalConnectionField = msDataConnectionType.GetField("_innerConnection", BindingFlags.NonPublic | BindingFlags.Instance); + } + catch (Exception ex) + { + Console.WriteLine($"Warning: Failed to initialize reflection for connection dooming: {ex.Message}"); + } + } + + /// + /// Dooms a Microsoft.Data.SqlClient connection by calling DoomThisConnection on its internal connection + /// + private bool DoomMicrosoftDataConnection(SqlConnection connection) + { + try + { + if(_internalConnectionField == null) + { + // Fail the test if reflection setup failed + return false; + } + + if (_internalConnectionField.GetValue(connection) is object internalConnection) + { + MethodInfo? doomMethod = internalConnection.GetType().GetMethod("DoomThisConnection", BindingFlags.NonPublic | BindingFlags.Instance); + if (doomMethod != null) + { + doomMethod.Invoke(internalConnection, null); + return true; + } + else + { + return false; + } + } + else + { + return false; + } + } + catch (Exception) + { + return false; + } + } + + #endregion + + #region Configuration + + /// + /// Sets up connection string + /// + /// Connection string to be set. + internal void SetConnectionString(string connectionString) + { + var connectionSB = new SqlConnectionStringBuilder(connectionString) + { + // Min size needs to be larger than the number of concurrent connections to trigger the pool exhaustion as it will make it more likely that PoolCreateRequest will run. + MinPoolSize = Math.Min(20, MaxPoolSize / 5), // Dynamic min pool size + MaxPoolSize = MaxPoolSize, + Pooling = true, // Explicitly enable pooling + TrustServerCertificate = true + }; + + ConnectionString = connectionSB.ConnectionString; + + // Ensure adequate thread pool capacity + ThreadPool.SetMaxThreads(Math.Max(ConcurrentConnections * 2, 100), 100); + } + + #endregion + + #region Stress Test Methods + + /// + /// Runs a synchronous stress test using Microsoft.Data.SqlClient with connection dooming + /// + internal void ConnectionPoolStress_MsData_Sync() + { + if (ConnectionString == null) + { + throw new InvalidOperationException("ConnectionString is not set. Call SetConnectionString() before running the test."); + } + + RunStressTest( + connectionString: ConnectionString, + doomAction: conn => DoomMicrosoftDataConnection((SqlConnection)conn), + async: false + ); + } + + /// + /// Runs asynchronous stress test using Microsoft.Data.SqlClient with connection dooming + /// + internal void ConnectionPoolStress_MsData_Async() + { + if (ConnectionString == null) + { + throw new InvalidOperationException("ConnectionString is not set. Call SetConnectionString() before running the test."); + } + + RunStressTest( + connectionString: ConnectionString, + doomAction: conn => DoomMicrosoftDataConnection((SqlConnection)conn), + async: true + ); + } + + /// + /// Generic stress test method that works with both SQL client libraries using DbConnection/DbCommand + /// + private void RunStressTest( + string connectionString, + Func doomAction, + bool async = false) + { + var threads = new Thread[ConcurrentConnections]; + using Barrier barrier = new(ConcurrentConnections); + using CountdownEvent countdown = new(ConcurrentConnections); + + var command = string.IsNullOrWhiteSpace(WaitForDelay) + ? "SELECT GETDATE()" + : $"WAITFOR DELAY '{WaitForDelay}'; SELECT GETDATE()"; + + // Create regular threads (don't doom connections) + for (int i = 0; i < ConcurrentConnections - 1; i++) + { + threads[i] = CreateWorkerThread( + connectionString, command, barrier, countdown, doomConnections: false, async); + } + + // Create special thread that dooms connections (if we have multiple threads) + if (ConcurrentConnections > 1) + { + threads[ConcurrentConnections - 1] = CreateWorkerThread( + connectionString, command, barrier, countdown, doomConnections: true, async, doomAction); + } + + // Start all threads + foreach (Thread thread in threads.Where(t => t != null)) + { + thread.Start(); + } + + // Wait for completion + countdown.Wait(); + } + + /// + /// Creates a worker thread that performs database operations using DbConnection/DbCommand + /// + private Thread CreateWorkerThread( + string connectionString, + string command, + Barrier barrier, + CountdownEvent countdown, + bool doomConnections, + bool async, + Func? doomAction = null) + { + return new Thread(async () => + { + try + { + barrier.SignalAndWait(); // Initial synchronization - all threads start together + + for (int j = 0; j < OperationsPerThread; j++) + { + if (doomConnections && doomAction != null) + { + // Dooming thread - barriers inside using block to doom before disposal + using var conn = new SqlConnection(connectionString); + if (async) + { + await conn.OpenAsync(); + } + else + { + conn.Open(); + } + + await ExecuteCommand(command, async, conn); + + // Synchronize after command execution, before dooming + barrier.SignalAndWait(); + + // Doom connection before it gets disposed/returned to pool + if (!doomAction(conn)) + { + throw new Exception("Unable to doom connection"); + } + + // Synchronize after dooming - ensures all threads see the effect + barrier.SignalAndWait(); + } + else + { + // Non-dooming threads - barriers after connection is closed + using (var conn = new SqlConnection(connectionString)) + { + if (async) + { + await conn.OpenAsync(); + } + else + { + conn.Open(); + } + + await ExecuteCommand(command, async, conn); + + } // Connection is closed/returned to pool here + + // Synchronize after connection is closed + barrier.SignalAndWait(); + + // Sync for coordination with dooming thread + barrier.SignalAndWait(); + } + } + } + finally + { + countdown.Signal(); + } + }) + { + IsBackground = true // Make threads background threads for cleaner shutdown + }; + } + + /// + /// Executes a database command with proper error handling + /// + private static async Task ExecuteCommand(string command, bool async, SqlConnection conn) + { + try + { + using var cmd = new SqlCommand(command, conn); + if (async) + { + await cmd.ExecuteScalarAsync(); + } + else + { + cmd.ExecuteScalar(); + } + } + catch (Exception ex) + { + Console.WriteLine($"Command execution failed: {ex.Message}"); + } + } + + #endregion + + #region Helpers + + private static bool RunSingleStressTest(Action testAction) + { + try + { + var stopwatch = Stopwatch.StartNew(); + testAction(); + stopwatch.Stop(); + } + catch (Exception ex) + { + if (ex.InnerException != null) + { + return false; + } + } + + return true; + } + + private static async Task TestConnectionPoolExhaustion(string connectionString, int maxPoolSize, bool async) + { + var connections = new List(); + + try + { + for (int i = 0; i < maxPoolSize; i++) + { + SqlConnection conn = new(connectionString); + if (async) + { + await conn.OpenAsync(); + } + else + { + conn.Open(); + } + connections.Add(conn); + } + Assert.Equal(maxPoolSize, connections.Count); + } + catch + { + return false; + } + finally + { + // Clean up all connections + foreach (SqlConnection conn in connections) + { + conn?.Dispose(); + } + } + + return true; + } + + #endregion + + #region Pool Exhaustion Tests + [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup))] + [TestCategory("LongRunning")] // Takes around 13 seconds. + public async Task ConnectionPoolStress_Sync() + { + var test = new ConnectionPoolStressTest + { + MaxPoolSize = 100, + ConcurrentConnections = 10, + WaitForDelay = "00:00:00.100", + OperationsPerThread = 100, + }; + + test.SetConnectionString(DataTestUtility.TCPConnectionString); + + // Run the stress tests + if (!RunSingleStressTest(test.ConnectionPoolStress_MsData_Sync)) + { + // fail the test + Assert.Fail("ConnectionPoolStress_MsData_Sync failed"); + } + + if (!await TestConnectionPoolExhaustion(test.ConnectionString!, test.MaxPoolSize, false)) + { + // fail the test + Assert.Fail("ConnectionPoolStress_MsData_Sync failed"); + } + } + + [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup))] + [TestCategory("LongRunning")] // Takes around 11 seconds. + public async Task ConnectionPoolStress_Async() + { + var test = new ConnectionPoolStressTest + { + MaxPoolSize = 100, + ConcurrentConnections = 10, + WaitForDelay = "00:00:00.100", + OperationsPerThread = 100, + }; + + test.SetConnectionString(DataTestUtility.TCPConnectionString); + + // Test Microsoft.Data.SqlClient Async + if (!RunSingleStressTest(test.ConnectionPoolStress_MsData_Async)) + { + // fail the test + Assert.Fail("ConnectionPoolStress_MsData_Async failed"); + } + + // Test connection pool exhaustion (async) + if (!await TestConnectionPoolExhaustion(test.ConnectionString!, test.MaxPoolSize, true)) + { + // fail the test + Assert.Fail("ConnectionPoolStress_MsData_Async failed"); + } + } + + #endregion + } +} diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ConnectionTestWithSSLCert/CertificateTestWithTdsServer.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ConnectionTestWithSSLCert/CertificateTestWithTdsServer.cs index 48b0c9273e..169f71704a 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ConnectionTestWithSSLCert/CertificateTestWithTdsServer.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ConnectionTestWithSSLCert/CertificateTestWithTdsServer.cs @@ -12,8 +12,10 @@ using System.ServiceProcess; using System.Text; using Microsoft.Data.SqlClient.ManualTesting.Tests.DataCommon; +using Microsoft.SqlServer.TDS.Servers; using Microsoft.Win32; using Xunit; +#nullable enable namespace Microsoft.Data.SqlClient.ManualTesting.Tests { @@ -129,18 +131,19 @@ private void ConnectionTest(ConnectionTestParameters connectionTestParameters) string userId = string.IsNullOrWhiteSpace(builder.UserID) ? "user" : builder.UserID; string password = string.IsNullOrWhiteSpace(builder.Password) ? "password" : builder.Password; - using TestTdsServer server = TestTdsServer.StartTestServer(enableFedAuth: false, enableLog: false, connectionTimeout: 15, - methodName: "", -#if NET9_0_OR_GREATER - X509CertificateLoader.LoadPkcs12FromFile(s_fullPathToPfx, "nopassword", X509KeyStorageFlags.UserKeySet), -#else - new X509Certificate2(s_fullPathToPfx, "nopassword", X509KeyStorageFlags.UserKeySet), -#endif - encryptionProtocols: connectionTestParameters.EncryptionProtocols, - encryptionType: connectionTestParameters.TdsEncryptionType); + using TdsServer server = new TdsServer(new TdsServerArguments + { + EncryptionCertificate = GetEncryptionCertificate(s_fullPathToPfx, "nopassword", X509KeyStorageFlags.UserKeySet), + EncryptionProtocols = connectionTestParameters.EncryptionProtocols, + Encryption = connectionTestParameters.TdsEncryptionType, + }); + + server.Start(); - builder = new(server.ConnectionString) + builder = new() { + DataSource = $"localhost,{server.EndPoint.Port}", + ConnectTimeout = 15, UserID = userId, Password = password, TrustServerCertificate = connectionTestParameters.TrustServerCertificate, @@ -231,6 +234,22 @@ private static void RunPowershellScript(string script) } } + /// + /// Loads the specified certificate. + /// + /// The full path of the certificate. + /// The certificate's password. + /// Key storage flags to apply when loading the certificate + /// An instance. + private X509Certificate2 GetEncryptionCertificate(string fileName, string? password, X509KeyStorageFlags keyStorageFlags) + { +#if NET9_0_OR_GREATER + return X509CertificateLoader.LoadPkcs12FromFile(fileName, password, keyStorageFlags); +#else + return new X509Certificate2(fileName, password, keyStorageFlags); +#endif + } + private void RemoveCertificate() { string thumbprint = File.ReadAllText(s_fullPathTothumbprint); @@ -249,7 +268,7 @@ private void RemoveCertificate() private static void RemoveForceEncryptionFromRegistryPath(string registryPath) { - RegistryKey key = Registry.LocalMachine.OpenSubKey(registryPath, true); + RegistryKey? key = Registry.LocalMachine.OpenSubKey(registryPath, true); key?.SetValue("ForceEncryption", 0, RegistryValueKind.DWord); key?.SetValue("Certificate", "", RegistryValueKind.String); ServiceController sc = new($"{s_instanceNamePrefix}{s_instanceName}"); diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ConnectivityTests/ConnectivityTest.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ConnectivityTests/ConnectivityTest.cs index e9e29e5940..8e21156bce 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ConnectivityTests/ConnectivityTest.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ConnectivityTests/ConnectivityTest.cs @@ -171,7 +171,7 @@ public class ConnectionWorker : IDisposable private static List s_workerList = new(); private ManualResetEventSlim _doneEvent = new(false); private double _timeElapsed; - private Thread _thread; + private Task _task; private string _connectionString; private int _numOfTry; @@ -180,7 +180,7 @@ public ConnectionWorker(string connectionString, int numOfTry) s_workerList.Add(this); _connectionString = connectionString; _numOfTry = numOfTry; - _thread = new Thread(new ThreadStart(SqlConnectionOpen)); + _task = new Task(SqlConnectionOpen, TaskCreationOptions.LongRunning); } public static List WorkerList => s_workerList; @@ -191,7 +191,7 @@ public static void Start() { foreach (ConnectionWorker w in s_workerList) { - w._thread.Start(); + w._task.Start(); } } @@ -393,7 +393,7 @@ public static async Task ConnectionOpenAsyncDisableRetry() { SqlConnectionStringBuilder connectionStringBuilder = new(DataTestUtility.TCPConnectionString) { - InitialCatalog = DataTestUtility.GetUniqueNameForSqlServer("DoesNotExist", false), + InitialCatalog = DataTestUtility.GetLongName("DoesNotExist", false), Pooling = false, ConnectTimeout = 15, ConnectRetryCount = 3 diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/DataClassificationTest/DataClassificationTest.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/DataClassificationTest/DataClassificationTest.cs index 83eecc5b23..3e7076d52d 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/DataClassificationTest/DataClassificationTest.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/DataClassificationTest/DataClassificationTest.cs @@ -18,7 +18,7 @@ public static class DataClassificationTest [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup), nameof(DataTestUtility.IsNotAzureSynapse), nameof(DataTestUtility.IsSupportedDataClassification))] public static void TestDataClassificationResultSetRank() { - s_tableName = DataTestUtility.GetUniqueNameForSqlServer("DC"); + s_tableName = DataTestUtility.GetLongName("DC"); using (SqlConnection sqlConnection = new SqlConnection(DataTestUtility.TCPConnectionString)) using (SqlCommand sqlCommand = sqlConnection.CreateCommand()) { @@ -41,7 +41,7 @@ public static void TestDataClassificationResultSetRank() [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup), nameof(DataTestUtility.IsSupportedDataClassification))] public static void TestDataClassificationResultSet() { - s_tableName = DataTestUtility.GetUniqueNameForSqlServer("DC"); + s_tableName = DataTestUtility.GetLongName("DC"); using (SqlConnection sqlConnection = new SqlConnection(DataTestUtility.TCPConnectionString)) using (SqlCommand sqlCommand = sqlConnection.CreateCommand()) { @@ -232,7 +232,7 @@ public static void TestDataClassificationBulkCopy() data.Rows.Add(Guid.NewGuid(), "Company 2", "sample2@contoso.com", 1); data.Rows.Add(Guid.NewGuid(), "Company 3", "sample3@contoso.com", 1); - var tableName = DataTestUtility.GetUniqueNameForSqlServer("DC"); + var tableName = DataTestUtility.GetLongName("DC"); using (var connection = new SqlConnection(DataTestUtility.TCPConnectionString)) { diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/DataReaderTest/DataReaderStreamsTest.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/DataReaderTest/DataReaderStreamsTest.cs index e5ff3c7559..cd516df696 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/DataReaderTest/DataReaderStreamsTest.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/DataReaderTest/DataReaderStreamsTest.cs @@ -29,7 +29,7 @@ public static class DataReaderStreamsTest )] public static async Task GetFieldValueAsync_OfStream(CommandBehavior behavior, bool isExecuteAsync) { - const int PacketSize = 512; // force minimum packet size so that the test data spans multiple packets to test sequential access spanning + const int PacketSize = 512; // force minimun packet size so that the test data spans multiple packets to test sequential access spanning string connectionString = SetConnectionStringPacketSize(DataTestUtility.TCPConnectionString, PacketSize); byte[] originalData = CreateBinaryData(PacketSize, forcedPacketCount: 4); string query = CreateBinaryDataQuery(originalData); diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/DataReaderTest/DataReaderTest.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/DataReaderTest/DataReaderTest.cs index 0d024170b6..532e0d06e5 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/DataReaderTest/DataReaderTest.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/DataReaderTest/DataReaderTest.cs @@ -7,7 +7,6 @@ using System.Collections.Generic; using System.Data; using System.Data.SqlTypes; -using System.Diagnostics; using System.Linq; using System.Reflection; using System.Text; @@ -131,7 +130,7 @@ public static void CheckSparseColumnBit() [InlineData("Georgian_Modern_Sort_CI_AS")] public static void CollatedDataReaderTest(string collation) { - string dbName = DataTestUtility.GetUniqueName("CollationTest", false); + string dbName = DataTestUtility.GetShortName("CollationTest", false); SqlConnectionStringBuilder builder = new(DataTestUtility.TCPConnectionString) { @@ -263,7 +262,7 @@ first_name varchar(100) null, } [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup), nameof(DataTestUtility.IsNotAzureSynapse))] - public static void CheckNullRowVersionIsDBNull() + public static void CheckNullRowVersionIsBDNull() { lock (s_rowVersionLock) { @@ -887,5 +886,196 @@ public static void CheckLegacyNullRowVersionIsEmptyArray() Assert.Equal(result, reader.GetFieldValue(0)); } } + + + private class _Row + { + public int Id; + public Guid DocumentIdentificationId; + public string Name; + public string Value; + + public _Row(int id, Guid documentIdentificationId, string name, string value) + { + Id = id; + DocumentIdentificationId = documentIdentificationId; + Name = name; + Value = value; + } + } + + [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup))] + public static async Task CanReadAwkwardDataLengths() + { + // The test data is taken from a production sample which exhibited multiple problems around async string reads. + // The lengths of the field names and data values are important to reproducing the problems. The values have been + // changed to randomized values. Changes to the column names or value lengths can cause the problems to move or + // fail to reproduce entirely because of packet and data alignment. The data is not randomly generated to help with + // debugging failures in the test. + _Row[] rows = new _Row[] + { + new _Row(27967675, new Guid("58EE5163-5F41-4C83-8175-34580FA54BDE"), "LfcK2K8CgjnXyed7Oi", "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"), + new _Row(27967690, new Guid("58EE5163-5F41-4C83-8175-34580FA54BDE"), "Gx1zdwPUdvrgDaIHAZSVhe5P8", "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"), + new _Row(27967683, new Guid("58EE5163-5F41-4C83-8175-34580FA54BDE"), "1O7MMFFVKh7OUY8Z17pNKIhNorz", "eNtDEFWnpoq25BbCffq41XbV4TsbcifhM0tBfh5PIXH38DLyhxznRdOugk1XJl5T520bchlIsoriQT5zNQ3oZKmdwWtxIy5IA99sUxB0BUSu37dzChCtItQZfMCVRKzlLIjWpcNk4S47PvoP75q9DFYfPeMCm4RWQp92zWuiXnjhodeOqnv6o6EmUsH5oOEaCN1dS1ACzzuPv2znMu1DbDfwXvT3E35RY0qYrjDYsyaHrD4osd6UvdrxhQNZ0LQb5IoLTeV3yHLjdbytaWVX6gB9ew71WbgTzX0X41CpLfqXTDV1WMPjEivbw12JrxtAt85NMKXQLN31aVmhZ2R0Vh0kSuIunKjvAHj4ATf49RdEo3qkQwAmuKSfuI9927nWFfv4JqSUAD3rxpyP85jES0eAg6w9GzTmGBqakYU9vJr0SdmX7gDivdQlh3HzLHprN22zAv2MSLwUn05Sbl454CsZjYGJC54nDoZVfOR3ernhQBKrRtGgOSOjvv6qUCVOr1JwNTHH2WE6xf36LdIG0mUBFLMjXPE1KCJoV4KVZo8PQNgfco5IaE6BHUET0ViBGW9mp4PLmhpGVyEw4sODTl01R2muvY72FDsX1tvcGdTMQOleYf1djLNOPrTkDrjrYCymWupfnMT7IaVpLgRjPRmALCnCNJuq8MxTEDrvbNc2GkWI790cpJJYmn5aSTI8lqX5QM2tSd9daNBHwemJqIny8tfZxk686Q3TkWANqpNdfjBMB2ovVbI3R3zrZDDHurCoGf7f9hJtyoSrCYU0z4zAhmdDwqNy7U7C3xHRfFkDfArlJBBTwrQhkKKpIXVJPSvUaSkxu7CHwTm85L3AxPul5ttOeBzp8SMQwGGXtLhqNoh6j0amIa58NA583p0FvP1F6AylEL8mhOA8ouSGr9XJwYlnvB4LiHoMlN1mk7W1wmvbZ0Ug86R4eSZPFrY28yPoTGOBfx87qujb1KkYK8VBHfTaDF6scXZBofnuZSwqoPla7FLipm8mf57S9pO4pAq9vGtCYf4xp8Tp1tsIgqWfiuHAjXHSvUgaTSw9dyXxRPMvpslil1LeSI0wrHEKWm0NoZRSZ6ixAHngFwmBWtsWQcK8fPSMqSWrsGxpdqIaxbDzbYJ1vtkEHVJozu2VYx8KdtFRPTmHE4ghppzbZRabUaR1nH3rgwpIl3how2KMy2MqAGR7jQ6KgWw7Nm10t4pzbmDGTzemapccjlom75mvfccG31OiQmKmtSp0qF1oTbKDWa8tMglTw10BiMbgDfqAAULi9WUVzVUbQdpJpcVC89vnTgt5ARxxIWlIqLR9DpCCqUe5wlc9r3fPZopvYBxR1hfbBPX6c4EJih65qwNZdqhenqA5DnfJxROyCvUcxRcPX78FGcxKx73pyogaqo5HDYoGmuaIrSXUMibq50B366MxC49gMcV0NrBwJnKzos0HD9enjfZ2mJ4cQ5ZIPB2QtIco7ZiItbJ0T2V9yuOwHs2PkciBQrLSlQ3ySchX5suCGS7lTwPm8alS0FAQhqqe0AFHKJU8SC2CRTqhU2q7zIKQ9TbNQYdy6m0DQ7HRcOclzy4TWl46dMl4yNqrJgYPI6bJd5t988w77JPJhqCVw8l6YCtvRPTXiyKn30lSESnz2s8PzDtSGz8Yo6PinWKDdk4NBchIJbSeDNpO0WfKYU8eEOEj6FiNVA4Ue88t1d9ghg0xXLKDTbUoH6O9uYQV7P3wAbIpa26KD1Q9TSOE1f8o2WrtuXK4D2VaSFPEB1EZh4tpEmirA8k9LoRu0T0eprRdZgdWyw6M4yUe2SiHxIc1PLktSBp6BuEnJss3OIj5e4K3V9bOlo6JcofIFPUlqXABhkkkBXv5oEQZ00blV3kAKezm4NX80sK6XiDX2BAQBC6PAXOEe5oAL6Hynmj8Krh04SrXUviy59Lehu7ZEf0W4RIJWgc5uLPu4kbHphis8LLF3HliyhxlLSflxxsAqrRGyv6cIbpBcHMK5CbPqot6LxDqbkiZVOJR71oNONGqsJFb1GZypxEzNTn8YtzL2Fsf13T7yzLUQCB0X0G8JZOfIqnxG9p2cNMJLZZwSqb9jzvFnT0u9qYVkXbEJPnOTIfM8fB1gghXvT5L4eico39NbRnLD9Vhx4wgGyzD5O8SozhKg7pJVkTw0tvd7IDcW9L66DdmFwyr8naWmClLaCakwjmt2C3Tl94tZPGyajpt1euo8oOaGhqORD4NGC4se91a0T6vjlbBrFByLne3Yz35iU54ynKviC1UGoY0CyG7PfkhdSOaaC6G2uOV7oWqP1x5nYjVZEuWIv0OvbsynmFV1RxNJD5dg0f3LY6q1Hvufdaz9ylsY6tNaFX8L81laY97i1wSaQmcA7D5Eq39ghjGiVJKCDwRLBJ3Z71B4pd7YBHIKIK8xb8UEeec6b203wMe2pXy4w7lgMAg6Q1ykmgd54QTSXtSTcKsLhgDLrvhRVGQ483dBCNQ8FW3Q3Cz6iozZDFREHdzMxVCuEkRImabdvJhQpVbkuPixwbWJrFU1AUkE7jnsTmcdzyBPE3FOQsQAkhWJF4KLTTCQmk5c4f0hn9kCXnU5noNvGvRG9Dk0wA2FePHjOLOprA2um8JDP9pzUmjc7vPXzDdS88xXwzz3YVtNAxRC186Kszg0N9Ci1Ed4DTwqPrpBYRzkiPFHi5IFstacJIyWIgdy2lys6UgWKoT5qdGH9pVlDZmCRkjQrYcYdxDbVvDYUd2qOz6s1oWCBipcmUJ7DkF6WcwunHuitQyV8TQpFCeRHnIUxMQrTxEM1V5hNax8tur3I7wZdkUy9Dr9X7V83G5tNgNguPMjzOyjRYNwhgtI0pNsCGem3Z2bseVAUllKEjntN02mhRgowalQEUS4RJ8QEw2c2nwjHPcYqRz1XiLJtgfLcEXIJTIRZ65SHiuOitblm3w4nyHThiHvvLntiyGOK0nKDwMX8jpfeXivvqIRRA9Y5yheNWZo6hGxVx1MppUfJ9fiJWYX51bm6NNiu7QjWnhnZj51CmUnytIj1Vk4QwqnQsVcTMu3Wvts7bAtTTWvx1K7QQwwJvAZmUhglsfp71B0DPYuusAImYKBEaVGSYPuvG86wDfysjPbJOrlENRp2xDhuaUt4aAVAff9DRuEIj6tdTKuQRVQoOzMalz2RL53nTlgGimrHBcfifKFMgxTriSNT8gjPFgMsmFkAdHMn9eKT5B28MMVfBatK9fjHYAzWowjlBJBUX8oZbmOd5hevUyLKNuzBLfQBabvvCJERRUnSN7gbJExcaXe04R2AvgtU6aPFEKbb7exvYys6sChLjLDA2C0PlI6b3eiAJnl5Aii6h2lApwa5Qjw5bNBw1FxcBGJRFmUVckSPekxCPYByCbkUiyr47Wq9bmN10knKG3AA6V0ZS6xqLPRR4LaygXo2r6nrPmTpVwWLXzf9aLfd3vtiAYs7QunuDKNhIRpNcHRoh4eeU8EkDP1Sc6ieYZw3sHdEfmnGw4W7MBj6BXmXSGuptWQTlwEpJqwwNKtDkgNwcBABayhYrtrGgsjPQkRUi2hyXevvyCZrvbZN1jZHd5fhABSTIhYUoN8tMmNiQ4iw3D6P5c0hB0XVzBEH5wgmO6crJ24QqeFI8hRnsVx5Gx2NBsxNszS1kbxNFAIGBlNrihPSXUIGJoyWN6Z9JaKcqemsH2OCHsGrnTkbdvkjotvOnYhDmQigXCINwguH1OoODBJLO9LFZDvjZeyZFBC14BpEqZ7Y1RLXjSZuTuZs23r5aYgQBCmPHVv2jpJKoz25KX7plNCRw4p8o8r4u0izz3oX5ddJVFSgxnCWGFZBxBqIxdGKy9WTFH0Kra0VB6jWy2fXVsbZdO3fctuO0wPAMNpMgaf0sLsvkdpeJTzQouDBtjZLsBP1LSoQmSDPZB8DQbYb5RnzODsLYXXtCLu2kx8UR7wsn26WVTlJOeuK40eHMHxP14NjbqswIJ7rEkxak78DvU6ENqoTgVjWXDNeu0fAbQB74R02O9MNBlvKNYOnIvt6gAjoFeg8AcAT8xvZ03mUrhn89tAIMs3O6qfzA7M03yJYguP7ABp92IFHajIrXH7Tu2PqQup67HtcXoJtMb23FPDzCSImfVnc0IhNq4hKbd23rEErxvXSV1XEvNE7Bv9TGZj0MbxuX5kwrfkJJOoHfuiXDmHhOcBGarb018FD9AFaZ9OkO7kGgdNPMOICRBuCGw1aochvzunkSXRhuJzZckEyi0jGfI6ILEEr1641ojtjSKLQpnVROGJ2SHVqu7VjlB3oWzLQICiBuYiO5eFFNlMVg3vbGLnhyHeRSvXynqLmPyThjrgEEX5DrYczwa51e5aP9u4GbtJ0jhydPexiBj0OOnQr0EbOB6cGqi3MmeDYtYSg4KKkYN3BFlTLwNYQlDRgnc4QUWoqE7GaeKh5Cr393DCGa2JZy9knErnmGasYSYrOwS5eABAAaT8bFhM86gnHKbhyptXVQIR0ra1FIaFQhVkpth4hW2eze2P62RW73SwQpF7kKJCVpQ8Jfkhf4HAkkYmLg9mVGVEf2k2ewYVMeZ0vMTY2rsi9GiwpZYQPn5aw7KeWqFpL9I7kT2eUMXDpSLAKRRwEd2B5n33EkqjITnMqYlO2920mD3yHSyIBUR1YKy51Xg97s6YnfAZWLTiEi7vlTBr2knvyOsVZ4aAW3URrl9UuWMj31HukPLBnc918GxVjAmxG2jBsTwAIOZrPGu0UPOt3QmP5Y1hQpOQiPHa3P9qgO2w2f16zEZdh7e25I5U3N2NC2nLeKe0vko0Gc9abPKscl7IhWkRVFw4LvSJglRasLmglQHdXTZ2V458TL1kkdCE4sOsnsKMMuVXCuEpdBt3LyfegyUxTul9aEWgisajXacfSv6bdGR5YDyvzhU4CBUwHjqzhCjdValPGyMEvP6DZt98t97b0BI7Tnu2N6VJyKMCX52yfnonsU1lfbGdaItoYk9Q09hG2qJnnZM6PMptlo6XqJbPWHON1mKUe8wF5VTRsRLc9JGJoe4PfYdJ3uwdjaQCaLeWfIQ7Y0jCst7VQx3uoo8SQ4G2BDsWOiIfGaEdNNoASC3ybZGxiS4C9mqphmtkx41QxtRbJYjkJevu1lAX97hq1QFcRP6Ffm3x9ESa8susw16o7Jk7E17Oiw5wgmpuUacPnAEQTOZbXLP62n79SGMKSDytIhJibJcCHezXTkxx5Dr7YPjpo8Gqsnqf4z2wPE3nFASRgY8UR52EtAY3nYQQivcdEOyqemMGNTzZdX0mjhI9LhjRiBYlYxt8frQEYcn4uqAoNaxNTCx8spTSQGHv352ivHHHsy8vCPc4klYTML3Vb9Lhn8IkrcuFvg837nyVheNliLmvKhNHKmCUnqr4AJCZznA8gcjhzhmDoR4QLW29NSUiereH8IQq8trg48vwtIb7KJhjjYRNpqim5KQIherfwJfIDcH3z5Q5abKzxCGipWj1KeSWNXtcYHrFJ5oBdANDRQIyyui1uk7wKAvbFeYm0hNS5MzGRAqIZwfhjOlwSiT5IqUzXcOqaVYnGKT3762OI1Wqf6ccIhEg7THLYGutM8yiTJEm9EdTdEmF1Tcl4wKr9V4yX693MWkuaWoCO5Eis7bTpSEbc4Et9rZTxAV6ItCmyg2kCqU7qXqbQZMJeQhWTkvz0EIR7iskoeMQAOkZKUKZS27L31sl6hmrQXEjd4v0KOa7NRaspFenoAwdaM2DVyWSGzFyb2SH8HubxcJpd7vQ3kbkvRj9C2RW2uxAG30t9hyf6qGY5o6wrh8pcdCj3Zq7vMgMr8wPk2b8azVxVqvoYfplzh2ZRxVJITIx2lfoZQzcJWe5HIN0t7dXXYVmrbgomGXsq900N9hJYPY9QDZMTKw7i8INrs0tLek8bELE1M2ZJWEZ4LJWdONUrpgo42OO3GAY7ictPF5vLrDNuISDG9qlX0qeKgTvgDVNb9E81MyZE00vbWl8UFgjZEU2ITmpiraUtZ3dZrh4hbeFxFy2D4CargbvrCmkMeHygq7oUJ9QBZMj8WUw4YX4DqjSov3McMh6L4OWT7cdRYVEb7vDCV1QM1ibbCYffNFm77OpWGW9IaftYEkn6PYYgyMoIYnT5KD663rP1aAyHSsNnGCDmkM4AtpNbv66xEh5KrwRpkMaTbdF4lZ7q2vXHAbb39t2UH94y2XbvNQbz23jjFkarM7fuk8h9HcwpwUsmmrPYpzmd8Dk4L1gPLYaxcKSRUcvuV8fgcVJHMHLIku1AoHmkL9GUcaY6U39y9yDmBLmox6PMHhAN4kk0VYl4RLBIpBuGOhdVQOTvMxHQRyKTEjprwNKMdLCp0W7y2dxMnxVq6TJIHVoDoBZ2W3UwO2wer8dmUdKxIDYrS8VAM0gD7QSsVL1hgHNSEeSoOj4Qv46hf5WV2tbvDPHmVHEwvEACu1EU5GKVXLD4B2v9vyh9pm0DZA7P1EGmjI6hiCfTCz6FxXQOTGcnu8CLrLbZbcXO0cNlkQBywLFZrXxLpzlkDO0ydfEivvyUJHLmYU95mmKKtwPIfdUeRjCpugEu22kWeIwB79djP3Y0pOhR6lCQHhoxSLZExYxqoIzA4WS3GgJt2LlAMqZrjEFjoO7aK4A564JzRzaNP8JbLlHepPpfasQY110ONlvAFo8gQ0PrjfDYzUdFm08V3K6FZP365x78QIsVZCCWbaJfoom5KMKOpjo2joAJzJF8d33EDpH3GjzV9oL4FocIM6UN1SNB3hHwXtorXtjkyRMl5rDzL71dtOvBHVW8Kmuth1FPiOCCghvhJyeCSWG5UvGvOsoaMoLoE7cQuXyZQTAEED9eNf4RolO2rcyN6Om1qgTjGtDu1KwcSmdmVwLLNQNAhJl7Shoz0mRrzECa6DzBQKwFNY1g3xnOWIliiVt7uYZ6DJSo7ehjhDl7ue2volVLEohqtja8iWU7sypfqNwbVX6Kh32eUJ1zKf8FiVMlNE44KPUwbQ5y7wmb4HgDmFrhnEY2MXTfUCdHo9Y9cvLPYJCcCHan4vIwhlwAXefBh6cmsNA9usIFA7hzJ17WCOufAZ8G6sCZKWdB8kLV8MbwatykOQH03T28NYvDgXBvRWVReBqn4tSZv7sguV4sAmvbA6WZ74gbmp9dvquzSAzfUqDvusZvXqhuyoxZ72w5sXjuaJdgoCUtDKbjUSULFCLk4z1AmSAOVw22qg69sS78RgkZkUpnfkXjVksfv5JF9OWZ6PtYbH2NM4vFsO9hpvk0xksNUmGKVbRKWMsyHZgxoT1ZqHnrG5LWlh3SQfAa5wauXz4E100BRkomPX43LBygSYWM2Mj6x7gpRvdtkKNyRc8VVyDmxffyOHR15KqMLe6VR3B9i7Mv4CGS6bDSnr7uK6MvYhrsWjvGoY4CSfG8gLECg0oVKhd1iFG18Dyb6ZqnuUtBcKaxYy0k8fnpMTh9NJa3J46MhdyJeNNzL01IlqbU091gU8UBbZVzHVJ4P04SaMtIo1hGHBqatnuhLZUJUuyrdWFwy9ZVpzJ98JapAsv5NDxSBFHrM1sl0AMBpSy3NfnAvnCR3kym03fM3YmzlBxqTfvGKJGzYNT5vLwrY7AyFzx1MAmvUl2XQDNElP3GszKWE4902lRtp0iY6q1anEkKoksjn4185FEFTQtLLMJSgxJhf3OX6rnYrzefDxjUKSu0XVSVJaOoqlHVtyBdLYKaUPVxf56cS1Q7EwPrxieJ5tqT7lQEK3UF8oPRzQ72MKqmLAr5q2VFr9hAi07uJk7ZW1vfIaWTCDMMGoG1UF2bdMauB1uvscRsHmPrfn79i5t6Dgsm6U93z99UtwfSuDTocQl2QZtVE3RUHwCF60XvPdrFNfASHsy9jn0XtaCMzYnKcLFoL6tKiFroFrQWn5AH2n2GmXDyg0d3FlVg8HLGmlbPrnUMdZ9bCBDIuIcnLdYoeSymaSIWqzsHK5tBDbGyR0riWd2WiC0zABy62RwKZkkIc0C3Gdc9kcaIjLXRQAlNZ8INj60WPz1QMT3JsGJ3FRjmp31sQLgnLLekxaJDTiehKDIUidOJbguVrtu30OqVwmbNqzOhvWnR0Vq3ntRMSLB1NpPrlNy7rjlITMYoWyTcanYdmbeLUO1O9cTTgKSQWaqzE8wawSnflRoLlx4HmA6eCsrurYRnCvuYlAYuQQJXyLdjtCOzDMyOu1Jjrcm8senPVRu2bMINpuGgh4hXKpLYEwNh2v8xB8MFFTsFDDe9tixOIRsikjC0VDzWyPjVAGiOeSFnaijpy42JCQM20QbbswFJwKhFl7oYBE5Jbdm6VaMwAljtqjVBTgeGyyiwBEpZdXf1duk9XnI2H12pWDcqCFwYxGptXJlZJCDidIOtD6TU7wptH7kbH3iCjqy3Fri6P93EUt7CgxUr62xrJ29bTWdY1OKjUdfznVeBrHozwnELmBDBPhQ9mmzXPNjSVnW7gs1WzGlOfhRtCmFx1sa9qF7joWAZMmqWO4ZOg2Fpk7W1Ik9RewMDa35AZ31ud2QwiUEk3x7IUNKCy64pqCGIwKWuofS5LGONqgT7EvzFPtIBtCJq4hi1o7MLj7jMxmVA97vLqDovRGyyTL9R8tPKK8aAytTVhTvGdeSQVlEnBY6AAA5EMX8jW0ovCYQqB6LCsfrrljuPTZ4aoiJ8ObyZFbuc1YkKEBFgzzh6SVCZ2FCaop1rk5aTvVhyVpjidcFGVoLh3n6cRoy2xhluyicWcrPdVizctooy8vY4nIecVYYQ7rlp7NxI6IAUGtQjJFedW24NWeDKHnP8mAxMNidpmmYVyOgiUgfcEPqenQvI6XxSm5FYqVGllDmH3W7ugd1B04IOp3IidCEU9mk6lp6FWmYt1vzqq7SJ1VyLGS0TjhKMgQNBPtKmBtjRi8dI0rp9iumnYV2T4APzEYc6U1ZHAsmTn39UeUkCe6Jx0LRiOVronC4wdPMM6Uq8THZzZwmvFW8ZukFZQO7Qmcy6X2LxPI9Vblvjcd8bSiq1pD2opKkxLcbvFPOJrsb6NUJt33P1m8rMn1afPU9ldBmEuOxWWNlsLy25OOq1xOo4bnBPKp7RHLQSVcx3IFFCbZHm3c9vAzsTI77Pl8sK7N6lYgAx7M9r3lP60I7dTWk8rNVMHVNgx1b8qUgyzdZThtTrJkqUZr3lic2Cckx100Y9pqDg5nrdw1N0zqMZk63Gre0TWiLKkroZXvrTzi8c0F6Me5UNEpsDzQDfTPwH2X5O615uCsmh0cJTzfC4fLEEPSy4TrA0StuOg0qFjXqxGujiPvpoG1SmU0mVtgdiHy7q1u4okZMhST2A5QseIPLYRbMAi4a7mrVfJxVRLp0crzyWKWstPA0YzD9d8zbNvfj1xhJH289fh4d7Xq1AL7w5rh060sAsVFBigDgO0m18XvsWhCSR1bE8Hl3LU6q9XTaD8gl2X6jdjpf4CcOKGo4AeQZYdoIC3ACCQEB1CE5X7s6CsxcmN3yhQV1QiDhM4l9tZGxZDWDuDEnUHLyiQVhB3pna07XetQIjWbPKFvKXROB020i9WNPOVw4KeSxEhWQverW3GTN1kiuvHU3Y29cHo1DpVswqpUpusngGDYrqz1qlW0vnoeZB3glrabVA9cz2J53BRGCMiKHXogchFkzlh5IBVOlLXddaxi8eZTd3sc04eR5edGg87wCOQbwL9OFON4CZiOrpa36XSZ7NA6M2oYKi1AXjPu4MUjJXSneZJJhRt9e2g590LYRbPdDotBFqEgQw9VOJjMkDLvlUqbT5coOiWUNaVhbHhVyT2oEYfvuWgRAAYvGaU3owdWabHeYO6glWkVbrOgQUM21XAGB5L5x9qbbS8jz0eG8Yf7jlwAthFtrMbcRsTY7CPJSSa7io3n2gMMD6EEXqmeO0DdoUCyo50KO9PpLHneBCuo3Qr0luhEhq3UTQ45CfM3qua0qsI1WbdQ762Q6fKLA4XZ1k1t36QZXz3RXe2O8oCS8OC7Dn5mo5DWi9yZJlWTlDrG74hvtLW2jxFXY0oNeL8nWKHQYsk4nIsY2RDDV1JoCGQiMha3EzdCd8RrZBpDUNwpMG1cn3WnPQSrzJx05Eu1OAyIYjd6TWpo24KDgajMRVqfKUsr6LEa9vql1O66cvQ8CmklaGZPRPxmSiXOufnptYjmuLnxMNyTfAuu6m1ly1y6z7NVqeFIXphcyEOZks5m9oOIdSlbEIU9usUXuIk62Mh3jwJ3JUHQVzXYBuARmpYXcbuA25iGtgGgaB5Z0hn5Cy7wVZuYMHEjhL3nHF0L4HTpesUrVTPE6C4pOEbWe8MHUlt4wzBs3kBLETjUkeLXylUkGThmSayfTFctjo1HHk1xO1LWq4tJG3aBCJbGdLOH4tMpHJofHKuqvdH5SgsVmz9GE8J5ErsjDIRB49n19xSFSOggCrAiY0tVSCZTDyIJf6dvPmRQVKqhrSkH7DrUhITMYfWWHNXKJwBTTAfBhCFXXXH469DkjMc2P5278HeDpg5jXvImFoKN2tCuAKOuaE9eWurvUjRTn7h1ypLGcR7W4qt6IwgD1fuGAa4D52LXceax0FZxzNhN5HZyaZvGJA2oAYFV3kdpLVe32xgw5fm0TdWhm2OZ7FB9CsIFoLVpym6BotRRSQbmQ2Y9WkTvkWxPQvquIVwKPgx1bJwltV5xEXaWfrsdTvDYywskbRkqF3AURxnpVo3xXttxYK9DJ5v0Fbb0SJowkRNtfWhLqpvbq5Ysf9yvGoWiaWEhz1jcb1WBkK5FYvgd9Zw2mu4AF6POxM7WPNT3etZtoJK4WYg1JTQn8PL2nYh7nVPO4TMuvgbm1qE516CQsFUQ22Y7f2KaZPXJZhShhrCNJeMH52G9nWzBsZbVlnxXNG0MRehPpRP2e7l5FTdQzHSQ9fgW7puvR50XesefeVLxRlYozzcVLm9sOKr9OZjTec2OBf9CA5Nz4uIbwvSvGxWBPYFQiaQrv3d4ysE0aAjYiCLu2lJyVhlhCrvkgQjWCBI03sZfrxnUgpT9ts1j2QcxEgK8lQcXH9VEJbCTQmJ8AOat7XKu8Kke7bpdbWQG7wOQwILU99SGz9axq9tiK4oqs0MRzF37SkZntra1NTEzqHm9o9hzZA8Oj0CVrK9AsPI0tnCNbsnetCxLwKQotOJmuDwqPN14SMlvhxj1Wpl4e7bf8yobLQl3M26B1mU0RtY0tdiRS3YmIoPmNJbfqCtI5xkxu1OjNVGhw8mbuOuRAGXt7DzlroimUr3UuEEBhu5TosRzo0D3ekICBJMMxCnFuy9oqWMnHMJuEtBxTn16OLn2gOzFnlvrZBUMBD1JjJbeLGltuP0eAbyTkMU3VmE1J4VejeOgGlC5LKqjhepoMuSunKekwQ2jlJtrRfBEchIxFWJ4UgWPcVEcxW7h2oFnmgsMP7DM8SZ0WpZPfg2442j8cS0n1JKaFZEAB4E3S9ugr8gUbV0ZPwL5u6iGIp4WxHkZ2kuwNwy655gFkyG4pCWJE1UKDBpxNGxoYlJUIU9X7HmG6Qcj53I8FUhq5BZne8eyKxgOv5xzKBe2s7D4Hhc0b4zFc0A1n2xGn09NVEikT0fmYQuCqKJ0SgRBVYRsnn1NQVlQSSiFvBkhgFsNYTvwXwdOxyeTsXnhmYm7xiPvD4SQNgij8YAflyijVSNSmN2MjXjIxLBsdQpLBYaTFcbKkJfxnwYwxyPcgyeBtNPSXkCLx8AQuYFYoCMtM0Q2b42CjaL0mVABusLKYURUpZZuOkgWp35e6uFctAgcxDGWgMYzNSpdBjgRAb4Hx8U84vV3eJUOSFT1X4VrHhJHCzh4k5vQwz0Dv6gHPMsvdjTQa6LWnTNd0ZgSZrEFX5wd9KIIu0DFXUAYihNM7EVamnzOPWl4gFSlhQ2jg3NucgSENxskSee1CQqMupTl1HszIUGMDJn1SdCZTLSKozkY2fkRQHH1CGHlTEFR54zKf3dFcZsw4XEovFxg9qoNQdaO6Z6o1K9QA7JHZzuXqKWdctujqUs2gDOtBHZIoDyCxFfEByaOjSVjyfAc694jOk4ijVlHz67XhnLXebueUmyUh6ZDiLFdAw31hphwWfAV685wzqfs43ufzpwB2oLoII50CZonHYZKGYZHwryvlQyzTJwcoPXacmgrp8Wc6cpMLqSEDeEMRTvEsVty8WTpFgS018yBtoLbVq1P7yUHt7kH3Wrxoo8JacVuR2FDIsy8XL3sb9Aifh01REPNomtRZaph3HTerFRL4t0hGSHisJWVFHH1orzZvxDupnMlPcheiR1laR4ifiIPgcwhFOexUuUlDx4sYvjahAIinlTDtp15Al6mmttBePcDzVVOyRcROMp59boRNjsi4Sze8Kykn4pXEXJFWVMRIE0xRzvRgNjifVLOKBh22WGL76KIdIwp1GKQ9T0Y9LclEsIaKcogvwRTYArzsvY1qjER5QIfanosVqAFmODFEnBy8c8AQkvo1OysxTUfAzoUeWvvkmcbNprJ7z1JepDSIEW8WkOS0BsrYiy07PxsT7CS7GL2sSh5hEncmSwD4TyeKPZJVTgjJF9aSc4XDq166bwMKf022vO7nj8Ky39hQsAu3gd2UHv5uQiI1ld8DL6AtNcEemD9zWoMsUP6B6zxrHHXYOkAilPf6YVqd2nf1PjS7r1PliRDjFNNFCxqRalrSpPbrxDHhRxLnsVzb8PRjMmoNHPs7apZRQRzSKf89OzMWsJMTLX3m2kro6IL51aDb6F7pmIGwaF8FzNAPNgbwVZltEC8DIpZq6BK8mGRJRDL7RMDUbIreed6TI0LiFmxPWed2lq2HbCzQg2PoQbz4GD8vHqbVirmnxFiIO15l0AvIipIOgZeei4vZ0KpyZ7imXrxpWfJ6O6tIu551ooEqSz2M2GVSTx672NvXNcDU9BGhDXM0BA9OeNe1OVSbjPi9JYTR8frKmouIlGYQRk4NwyaZzNHtoslqRshsxQLo0SYxHIMEhSaiKNckThXl90qc2JLB8lQnitI1EaQaKUKI43uu0436fdbtgINRkRswUWcQSgLuYJpZ2E0OOS4c0zTAMieaxEQ9W6T755CpgzowLDDJmjMyNClPDxgSbD8VgrtpNsR3wVWRlOD2WnBosJt7Ve4iOsHXUlqp5AuUsM7KVWqZ7B7PDsUbUUquBbEVyyir5rWpZj8puYjV2uHI0bDqdKuieTrBYqsWQuQ6VKk7MQOLcouIRmiZja94x2RLhNmBASR7CUUwR4NACLwzyMsek5dkAjA0HuYGv15ToWPgiF576S9oRcMga9XCiaBJzNfsvNLulhW1CV95xZBN9sUX12CfCwaoQfQlxpSbqOPEoIKKNB9QesbV1rIm3Z3L4A8Zln6WzhdaSygvzUNTmzwDfYuCaiPt6tC1wRGfwS4SkQvduwZ8bdAmyJUYJRfWwnKWVre3PU0qoWkHHr1KiFNiwVJmlMuvN39qxibZA5mpC93hj9aIDXxdDPAZpRkDv8F5DH1jv11PMCGZvY8pGme4UY1FybaUNb2SlCaB3K7lSjZ4L9uhbS32RIqITa9mLIxiQHpr4WZAKU2OQ7FFjvSRVVjsHQTWI3e7mgJSyjABsQ9YWCQE88DkwJEdvSHbSRqTQdscXoMr7KlxJbvDeaxKFQrsoLSZFUa0VUjXe8Ip63eQ5MrlBwtlm72yYiEMk2aOxNoHxvckAaVFADfG7ZabfZdU8G25FxWfqtNZXPWL95TinOqLddCojvF2bOLC6st1b4LpRxgBSvwMQqWgwcIpCGnaQ5cmi4KkckudqgYOcIDn1o4iyLSAftOvpSnm5N8Q12cywHif891YdKZgusOmDWgDpRLW6K1VdNG0upOnZO7qubz6LJ0w42T0TOaPEUFOUPcV0FnnICITcpC0i2VXvGEAzT9ZKhTVrXsUJMUGS3ULMrV4G3E3c5UbyGP8G9l1T25nlUg0rm81LU0Oncnk2aP06nfRnJm3MM8mmOgV0x7Ms6e7vnKbj23xjTyvHAmviLFhtNiEPpJ8RgJf0BhhJ42XLH7N37G4bDlNeRoxbcjrfrPC8bVeZbne2wtlWNmZ6N9SfjEiKzRX2etnsaKeix9zYY7XW7cKRuurXmXiO5AK9ENfYOV4jeLqfVUNlcJgwWtjS88IrOXttG86cLMlgzR8WR2JpbbfNERO14u1tm5qjMqj4PB0XP1iKTIDvqfQKpaOZK2o8Jy6Gzj0vKkXTcgnpTr5QjYDHVv4noI9vPp82FQdjovUfmBUcY7spN7pSZg8HOmXLvOea6qJzDklOBJ6qte4Q3BXYnxmaKt9aOhA2daJn1KnFbyPeky53Q3PESWihMsQo32LH8iqGhYA5unFKJsdYm2nB3vQ3YaIWfOioTEtoKvXVnnoOJKKtZGYa2ioYebm4sxb0NqHacDQUD9IT2o81TXLDiG0e2lmrVjloP9eUX4CIZzoFZjF91cCjHerFns9tIXZfFCfnO7dTCSlngH66DGz5yOUqzsdMBZZvvuY2HAet8FWe8oNvoFXAZPFyoMwO8LA05h8IxZbAAP3DKYrACuLsmI2yc1uJsJWOWL0rEcsY6joacQLTwqpM1cqSD3krHyhcKtq3PNYXeyVVfXPP5ok4RVnGsU2p8l8MjXqrnhGGR8gJqfZDixa132K6ZqtGwspSVNepxX6lOS5pa9DEf3oMT2pYwk4WtsbZ2djLKsvLaUrmHj2cylQBNcCDpl5VgE9dKT9IJaNn3o4cipBYNXNm3NfzDMkoVgUrzcFbuVKZy51E6jEXAPYaVTXJJLwGXUAoltbGAOY10sxcQQke8sxs9LTZOlPLE2J7J7E4YBAQ666rjCEep4lOVikTwbPWfiZZvv0FQnd9Di1h8RI7N8fwQ1sOTNq4OYfyrd4HMuo1xWY3sacCkTVAzVX9fFFRCDPDLm4BbWXhzML0fw5IBrMm1cCq5tUibqXkJiC6n7wOA7unsr2Au5JgWsFj71sDuBhZdz4CrVotGPEAeyJB9t0gk9ffbAm98mOksZgDOsrZ12SQ5rj54YYgMGsf10OGzUN7zVerOCxCo0V3x3KDQnyn4Fa86ZQ2q1vCvufOina1kviQpyEe3ogTRKyXjlbZ56Z7FMQnWWmV1aKBXJGRbBeG96GLJZsl2B9EEAkDgsencXv6GdHYcOWHI6Z3OikHs54AUayCjI6rv3FEEvj55GvwTDCPdxjlDqagbs2dJ1yy0HRj3ZPWwJjetVerXl6368k68wLTINanIsdlfeQ8AIkg3TkFkgJqrD7SFTRGosSWLT1btvJeI6cxfw9bW8rfIg64gggOJsPUTEJJWa2cxvwXnUN9I5ED1LQbwcl6g2VEXwTzdAPA85mFFhyaM4eHWZgod3b9qzGeS9DdGtgoWa4KPs8ef0k6XKUbRN4XBZZyg52UQAevEaY72u4u89bgETT94mnTyDQ4Lk7zHXhVbDTcWLd3b5DAt1hdwgISSrOubEsNT2fd5HAacoQknnATJRbeG2IFnjnCz6Mp9Zl1yvaV5rwJgallqCvjsOHbsN77FNxvaBB3Yj4tVQQUTiX3AEOHMd8ghGkXDMw2tZZpGPaVxJKtZ2qAWwYVuoOunh3xy8C0NjLxjGGcnnqdYUdNR10NTQNqoATTqQ0mf1QwMwBFzUCR738uZULKQHzifrth30z8gucAFY31QxvBpd1O3dZBPWbIgROZpw7z4KyYN7wUPy5yUAO2XvPSQYcv0vBznD9RXTJgYUJV3A3qgo5W9ykWj04xfuk5nr3vz7iRoSRIyd9vM1mpOtkCf7mEDxmQF5D7tbNCXM9z7T3MjNOCj7XiblLzrFKcWFfAODMGelBgAdB0Lr2dC0RafUOuy3m5kKnO4zchtkXqKpxJYlEDzZY2MTpIcGqDkha9l6IIy8RvOihJkcCACnS8zy4QUrxVSGeaWsLNLenx9koO8y1rvuFcK0VfAB1kKugx63d5R30niABZRZu5h8CzPm5AOJEsMdqq8hWioMp5BfG7f7bg4ZpSilUUjZvH0C5ijV5A5awmNPrGUNTMXURJCWcsFdxx0Bv0pp7B6mOf6CeY67pmJaJGsR1mdobA6jwCJCV2IToyTqBvw4oZglcOFehKJvRsTIS5Tits96Vxu1XceBN1Ufg4egaBMKfL0d8qAgfT2HW23X2EjyZ6jSw9dqzEVspnhUdjtieHwh7AUHRJYGLlIg20w9GJZy27covjM5ivDPHjTtNSuIsHoQsl1MjxthNBTqztFZd7uEVhN4AhqeZHogvTZs6P0fiNmZ0XB7KTCmCnbQTv7HmXerPmJoft5ycEtRdZqyrRGbmCh4MdEKDnhMW1DpldIGk6tE7s8B6XRE8E2Muu9klUcOCTJFT8ZlknazmjCfqHryaBqMXeawvGKSrw1L3OwnuHaYVL4IysMfByPEejSgjKv5o9I80oww8d5Mr7okwMkx5AhIyH8LOKwbAKPL6oXSBJvgi1yVNEuTMDVuZw0WBmAahjDgyGPTg5kZ2og2Oq6KwsTzgPx77sXOWpqk0d1fFPRLA6Wa8NJvDcQ7yjMyO1QcbyxQxWMbzp9W4Rtm4QE6x7ldpVqb1ZzsS812s7QUyadcUvKT2F2aTOT5gP4GJAMg1rdCPSIagpt5KYHimTQbIetkIcPZhD1hQoYj1eVzd15qcCG0fFCqj4ZG7c3RHy8yWE9RQ4wRTMdR9LgalC2rURFt92KiStTvjz6Hj76goEDJpQXUn4AaMzbXauBd2GxtFAYQWqNUXeq8oVXnRQB0boqBOloCn1U7xIrDitRy3WCmgazL3YJO4dDyhDh4aGQkDVk20vM00zoEB8dVZns6vYu2TL6zrcnFk3TcxyGQDl3wBbOflh4wFTblIAR6Uj5turS4WC8PcIeUM1qeab0LiUIYKpq8MuXL8KiolDJLVRSc7Xjm2N08SSXpnDJTJkKAxkNHl92Tc37h3sj96AAUndZekxVrieKD4KqBIMMwFRyuhvCYzb2EIuPXq7DxVkUhpF7lPMhPkgQ7zpA3XobQOOwanqrJWd1gGk5QHRknQX8zohxcciJRIKkpGQfbr4yuXobNcR6tgsoUnMIf7Zy9fkieo0829NonEeVN9iEIN4tP7cRGf7wKTvwlHO3jZ7dUkTxp3PI0rmAPhzih0WBzkl2O6rBQGiq5Vj6TWE66Cl3DFv38RpF69c0Vw7d61WBAINOw6qNnANule1Iz1izEht300WV0Jjn5qOqGyMxlft5fOYcaIxALm7DtluCI0jJYspH0lNgk4aOWJEfpHnxNaoUST6w94K3SDuIZIlsEuAKklfmYOVpeyNicqE8up8cE1Wu8FwI9KTvAV5JKxJKRJLFRFGNv801IdZWcHWzF2ZYWEg638rWIfBfSl3ZMHVKLCbBqoUbD3ozJeSllt7oEjvitY2lJUHZ7J2BAMr448lsJQtIh7d17dRdr4BrSGg3WZKLpfDeGPb8Noi6NomBpiZVjLtSIyNf11eKIfzTCyvc4st52THJWmX2sS98W7NvgTWoJxfqo1mLlaKYrUICwN7JVxCSGOfbXusXuhpfNFQ2c9O5o3QfXGyKcfVq7WQ5vnBXMyTywlyNdBwrfmO2wU5wTZiVHC190eLpvbil7gKx4aRFyFk7R1gA589RoqGAre9jsPuTSBaWcS8ehbYPn3ZbZEHXJz4eETZiq9R5FgWZ6kauQ6FKEVVVq1TGk0CrPNVmGFmocfHYWhI3hzSSuniTf36zLiL0q2dZMCNSCn1uFkcmbUBNamBPnovifPgt9h1GRhQplgRRsC3CJnheuDElShdL2sFij1K1n7OSAoaiuEnnXQ9sIAowTxdu9Bonmdtluac53RgGFTpSFvvj3JS1lIwzEGuGqrtBvbSolSPmXaDbFpKj2bjgUkZsbzQRw4zW7fxGnG0S6WByoyUp7PYQtBQvOIx38vB5Ep32Xlcw8oayLQVSLykLZsdWmcGAxiP1qbZVaV0d6ssqNZY0NCxbkP8zXYB6wAYxOWXJM1d5fxngzGFNG7uEIFaBRlzmtnefdyU5MVUWsfA49l1QBcwujkffbpjKTYn2H1tG7SWSjVl1LreZjrZIw2qtonToLScORP31dLgmVVjgW69wCWuF6ykmvqvTaHWnjC25JuH5zJLmuhwFmbAfP5FidGkRZtjxyL6uDZbV1QKYMBrdSsfqBOMi5I0DCRBeHIeOvy8bhzTaR2IgVUXUlKbpS3VtrBbYrYqV4EEof8exDcK6zzQrQbJxuSBauYIh8nE9J73XLptS2Xv1hc0zJnV8d5ndyRgODmG3TVdNwZah3u47BIc4mohRBGvO9XQ7FWeHLtJ8r7HPvJyYzLP7b0VyTqNauA9xAOQOfvqgp9WGnKBfdLLiP64RE7YVcfNGxZmBzQhBkPxoZIxkHzKtE3q30gJVV7jKzy4cGx517OcG4JNfM4uFK8D5y0AqZKKIpyGzXpWj1HEHtN4FuaU8AW1Vxn7DudxhX0sQItgDOX4ze2AaiZL8ax0fvXLfeIx1nMcGCjaUbxaX2e4EKDZtQ1eRwwnIQ9QgGGEeBhmPWlXFSm1YT6ipDHTc11oZM2EF7wh5T2OyCMYCG3wzy7ghzkRTIII8b32rAhSWDjpzgqvrFzHU2Doo5MovUf5FKJrBgyowh2syUrKIlnzAMym4vZySI5uR2hn5KEWRF7AlWjQMd2t6O85KygK76v8jHEs0QzUG5YdelAEhbL5mfEq2g0wXJmhSgKWHSkqSJXMiOTWXolEAG5NWW91bnxlJLG5NorOVdjBK57E4TrKeXWUOPEyIgJR4rB5jLI5iILOUV4jhi89iBtaWm9RmfElifVbFthJbBqAWnDRf7kuZz0VToR30NJTZO5D5VaB2iAhLYcaKFASOPRwMG8qb7uDab1rBetndWEqWt1JsM9JW7OravpKEwPKt35ojMaIj3y7aPeitF9cI5cDMMLbsiZakkZQyjfNTbL0ShqU8gQYld3VzYMDCHJkLoYwsiWovubntldXngtcBOE0Z6WSvbPJbDucAkgLLXmOaChAUU4dIMZxR1egsaJrRDuUho0GLdHoKSXbmL0PYuilHFHuJ0SJAn1zCBmck2SjTwtDT9FtHGM7x3DO8u5fFqtBO4lNwUOa13h3xgMOgp6GPXiiUnlBbinwOc1CyZtkY5oyvc55Am9kyHAb6kzO2vJMg1tiKgKz0hpwicbJGU5AjkJBX8WGB2NqYyzRzm7AVxjYKFrZu9pTkPajTaf8lWsn6sIxYkFS6WqAJ5Dv3BwXAANICCosNN0Sz81U1epmX7HJsW2D9NGykSGaLpSEADH2Ax46NEAFjyqzNODNstoKh4Pr3LjxqLvwh5m8nmn7ecoYLGcAyLzFTYUMio6FSD4vMzEm5mgvkEsPkbX4se6GVHX4dsCbHzUZGfQY04EOa4fhM9GaRXKkifi7LDCJO8OaA9XhaWy1W2FSxcbSzLNG4tywT7lQauWDRGiSGSJb1jmwIoiZ3dyfU6d1bbUq2CMTkExv1cfAGTpLxGfjP1pwU2NtLvbxx7BCnV1dj8f8COZ7aC2fFTSgDwJnJ9dDGfGIy8J4Q5BiXcPRLuJ2GhQ0uuFHqEowjha4T0Y1cYd3ZUSjIUI0d9ADPc8MMzzt6EfAfjqRUKJYuwpzHC5OUcQclTh9hz8eQfyrskF1IsPV64MsaHu1ZwgAjIq1JVOjG9IUIkEYEzO5HvHikzTQf1I3DbHK3ww6rBDI7ppRNbjmoIf2uIVjUWejlM1Fljdhuw3E0mlmtajCiXyIsTMo5pjDqqsJLDxBCZ3bzdkiBU3R1Bjby6czd19zIt34wjjXEJDsDnYmPDizEkTIoKXLYalUvJoMzBulzOSOBs7aYlkPBbpbaevwHK3SMskN3Pq31ofG91AKcPqZKJ4zRA25qhJODN8aEKnWsjL6ahU9F1APaGaF4HHq4HvZ8Iv1iSts4PvqQ7hdf3Wa9v8OP0Sl2my1pGdv7BJKBhVBTEknbPdMBs68TtTHKNyQCxb5jtZJyGvMf2GYNL5caKnd50SxDjjMoKJkLvkNTE9ZvImzBOLy2YVcfndiwS4p0GOif7gEdBVNxygYuNHxpbhHupMkXyBDRuHFsOTw00We7ktSU2NsnWHV5IIjZpXXPmxgDslYsTdEKGZCMyax2rp3MwioQrusZpKomMzyZnkIQn7HZfcd5lkNWmweICsAi1QbLyiIoJ2AAodDd4WVlTtyCCotDIGQoDnUIL3qKFOHIeMd7CmgBVbdTv9qKtaXpaROLNhYq7i8HqhsQKN139Vx0e4EgyzfSiGKa7HdhpDRCcd1kB8jcqMrZtbIsFJNByPQKfnHduC7qhpXVSLwZ1b43ZdnkTaHH4Xna9sniwAaeAuAyHn13Zc9iUeFKHMAwmHO4nlwu1tlVk37j3gqMpciC6uzkbFKyERv7NoU9FdMSUOrlnE5FpoTKEnbThlXNm8H72VGV4M2gMzyp0DrXPpAh0DJD8ExUayJR8eYGPYLm43FxLXGJdHnJnvRW6AUsySWnRLze1dAbUCwTGs6zZSoDKtuRhW8RL0aMgqU2gFRBloLTG8Yni2aj93Nz1e9BUiXSiAXpWIRiHg34hQAvkW8bDp3Eh4S7jesxXqRFgEEm3Eril4fQGYSRCzEm33dV41xaO6KX7rsYiCB0nPeAAyLAvn8VxigzTXbMvv9jVL25ZtjE0JmXpvK8e6fRQlfMLeDdz21rRNINaId0jPn48A75ZxZpFJtK3pVi4MB5jK6IrL9OcPneNHrduPz75OpmYIp3AFvOpVUlItNbxzJDO0DSfsxGRdqFPs7HrxmQIEY056vDsoHY6GvozK61olHLpWRcpSGPVHyiWYTsWU9eKEIkw6fSkk34OIygjHXTWvbBfK9JPjpwtploOt0PNZ8uMUe6pwqXuh60nxmyl2PhBVxOizpXa8W6xeOnMV4LuLkZxxMKsN4sZ9Bx6Rlbmoe9MOsGezUVESgsvXJgTjIsjLjN9KspRNmBhPSaIcxIawWLorbE8x1wjmdzNeuUTYHXvaMubR9DG8LgPQVGdIMzlsIeUMlb4toNEaL0xzxe1knaT8OR7pYFAEQVjFd4ulZxEFTt1oMmGixexGvhxrwJivMiLSVPSDoBvJbBBUbHSutQ7rwxQW6So35vY6E1iwLMB0IGhxYfOeYX5GwJdneiASKmEh1JeFtjRtEJvCIBVhOcFmv5wtt7TxWgPeLfHkxPDNJyvaLN0QJcqyoIR4946DBm43Dzp2SLhRIyswazYbmw6libCNDaSbFvstX5ZG3zynsJARh20gpl9MyFXW142oxdkOampqK5WkVFyCxREaMJBsO6XFcJTPGwuZQMR9osVV0uiti7Lig69inrb6MAhPiEWuAwwjO1EJg7pDsWsGUsL4qr56EYpH4SRzBpZlbxJwZPKZgDyIGqDOQOUNQthE6CITVcFZJV8BdElYBTaXac6tnyNSjB6R10vho5fmOBsiWQpSJeG4qennVjXQJlyluCFMTIbsYlTwSy5B53CPt8BzB6UlUl1p9UI8hky8vrUGaooCY2NJslWpQeH4jRxgFXCAU9tZ60QK9oCgCJjEN2A9ehCUmOG0EXoZvv9DIB7EqG6jJlFNotGiwWa3VOS0Rr0YZV4byvhsIevSu8trs2jah4ZQDhkZJTVikVt1NeaS13EhHhRdskrtVFBeJ6nayhHtgR1sUseS5KgRM4ZD3cymzyuxGxmJ821WKhKtjCeLws4JUehbl4vj8grKDnhZT9ehgIyMVx1kgFFxVv5MMpiv6XzDUrPZsQvj9TJ9sHclDpvfZPCW2J2MuykhavD15oAkAfOLxQI8NE03dKOqNdIidc5pAcMn2OGHkNxsYBZzGlsURUJShaGiTd0rlUSh38hm5YOMZ0sd307ijyzDsjnBQt4VixOrhag9yytnDxrUHiO0w7GbsoPkEcQveBH0CmTz8QADLA8dIldqlQMyicCutbXsnf5Ot96f6hT76Z5zW24fiehANcTp2Zhy8JkZARAuIEVfHobcEU68Iq0rRXgTlU1TUamxUB1H6Klbqa2bWpGuZP1NPL2597nKogwFT7IyAe1J88gBNeKCRqIfJQc3WLYyRYuOoKOBTwOBh4IinqlIAcFgSCreip9mCkBbWpM2AzzdIG2yDexWbwezM7UIvXTxUaoWELyfhIjbjYxojz6Y8lPeeUIb8wlZ3cvr9nVhpUmNy3Eg0aewKzO79VSHDeP3Oqu0Kk1aUd1Y6vcI2kYvh71jEiVX2Ls7nlFeLXH8xnC2GqS174vbR5K8pic8ZrvDJdtQ98eETmgVeVSnrI5aJB8imYa4GgR2b3sBSdGBmuwXmccDjbYpXaLrU45n3j8KZTbrhEyAyRzMUwgXJIqkyH7LNRPN2vZdoEx9IPNgG2wMshW3YaJJuAN6ErzCTJePuc7sSvD0M9SDWI3MJHdxE0HHRv0Dj57mpC0MRNj8ZbEugTudWNc2VGesoY7NJ4OVzQEfjZGiU6h10NOQleRZ9RjswUemPQr848phGg432FgR2iQV673ZrsZyKNRJS2ngKn0zPGAHztaDYaU73voHcQawJZgCCyCANh0DjT0lZyS1BE2lVGHsd5tKzz6eT3pJ167DVmLR0qWDddb9FcgJc5AnVm6qVtlc0WOs6drhlfvqHdWBwLAIAmEKCTN1OZgYJNXX5s9MA0FTs5J1GITgSkejBf83oSZsa44a23nGsiHlPFbHcIzNYXRfY0rE7XDJZ0yGciNWN9F5t7svXSZUd4ikfeOfUbHGfskIgTStd153Xk2sbVGW6M4TQsaZcUNbCvOrexViDqnymRQCME4X4NejYta4PvVMeg2MpNwI2RD5Pxljz0BO3LXnwEdYFgp7ujVHOic43RptedopDTFEZrCKrRScoh3gduhUsIki9pyctzgobb3KDDfx1HlijupRGlhBCG0tsjHo5m3f9Lg2iGpOLTEGd9CO6ehKN64kbfK4l1xW07eqFThacKNgoCz0EYbafkD4rbwoRmj270yDwZYdbTbvqlBEzyCMFQZ5n8H0XFQtqdcQYfjYQxEbvC9OK9sRs6v8HE0WpABnHosmYPOo6cznW1YtwYd4zzymQxwlO8Zqlw4XGjNvzA0Jh1kDMYQytLMul4jbzkCvsukAp72XUwBeDIplOQaUCjDT1j7jsZaZ39fLrqDIdxgtFayaPXlsRrrFRqvAaf60ZjMk9DKi45CQXJsMzkAnYfT7xQ3GDnl00LYhaCgimPSLVEYXqsNXo8X0PFqjs7JanVE1e7AIjgMNQXUm57r61YZvjoD39THOc90ao3d4Tsov6l1j1QoFfFwRafJ3WHo2HlJthYCjHAdowxTHY84Rn5GXA0pmjgU2pyZXRHrTYYoR7Zi4zmxeB5DM7lywLk3Jpa50ibuZ48dj6pXoL39gvXOi2Euuga2BjmnWlVO8sm7aExbQL9mb4u4z9ZUzEArbfv9KRUgdc6o7U0k38BNpzn2HJVVYJiw8X3KIFEdCzzwHgnZrEUVIqYjOFpZ6CPRQ9lKachPvbypqrsysebJKylu1jcT9eJaUzCyMGtRvXVu2Z0mWtUt50LjlepnXHhaD1B9IKIkvIar8GzTs8lH8jQDM7AMk61tGUbJVS8uZgrxEhiL1neyGVPQlTazI7Ai960yuHlqfglVAmbwSUaUruN3wqaBxkqNnO92ny0DoGy9jfp3SgrOeBB8wyAAufs3KEYBWrYlLmkmWn4LeLhIRNiZnnbuJ027vJfGypMoOlFgUYWvOApMdDkXN3zmEkIKa3OvsET0u11kgTLQYzdQ1ukxLVP4rrfHb77J2M9iAaqMfcPFIcaYtclabNaUQ1sxfBJk0mOxaspWpxquqmRkc0QTRhqJDH1w6twx8BhDjYYlJ6BP1X6vgS0IZkmAK9PM9TuqORZ9yCYTQG1G4fDIi3hnQ7zIaXNH522AOzi4lAGsRt1EmfL1qwnMbQnsOVd9aOxDxQ1bLcYzDW0OqJc50x6kbsXHUTM53iYkLHemoidpQD1uYKperegyBUl9I6NG8amUEzSQjm64uhBxHRCPsiqKhd8k4fqzlmdDhNFDy7skoszOYXZA9Gcby76MATHc0QDmrItUN3ebmVQe5riSaMOFLZmcSdZaiftHZrXO3CL3hVixoGJtq1q8KQ50bPaDt63sTSrIwpNQ2QcVFPAhsAHlbJwo5xAADGvQ9yPAN0L9VEYyDi67akwrOx2fA8YUy4SKY8tL9qtbt10get13VCs1sJQibydHG5YCVwXSwMeKiGr3yt9k9Df0MA1Ke5sfMHT6bSigqJYBB9FcRZEoK9TXOZf7fyhBFxPRvWR7uX0pQNwbqkybECvt3YbdEPUBQQAgrX9GBAGc0RNJ49aPL4U49YR3xy0eMyuPt06CFML0kQwd9eNDB9Ti9U1Lh76QIcs5sHhkcJ2E16eV02KlqhyN96ynwd2wlhHuCruH5G0wDVQNQ6hIqFhtRGZizpxxMUXxv9QMQ14wcE5fUgqEHdl3p8DbA31ka0lKzAIn0lS1n34r7ERq0DZbRtbKM2SeeDehG2ibQcfPiJoGkURc2EMnmOcnMe8fJ9i1wOGTc3huGT9I6CF9X2C13RCN6TT8TlxqXiWBSsEUyuNuJNO8T9cS1XXu1aIXOKEMphYsKXM0Qf7Yg3ae8ITKzXrobzRfhkO8n3mzz2yj97K2TKak5uaOM5v3ouy34OEbSBKvcQNjR1GKoq8xamqgCSCaXl8jqfVY7UkH7WcCxijco8gxt6GzTzJpRl5psgTWhCC2qgHwvAY68FTxdodJCg4pn8vDuEp3ruiMHp5mpwrdSzblfjQx3T8WVEhovLQadfBAo0IGYJPJJ43i8iqbhrcINC5DoblY1ycKbEei7tTABLR6cOsk4nZdBYS8CB67jQiSk6M76HUtK7M1zxEokQHiJuQrWUrh8H240DW9FKyP0RbVZW9Dz6lznk5yyR3GEBuDQTjNbe5AA9LwxL4qZYVrttMmFusLEijdvHuWE4tqwUHfShzQT0qssyjl0wNignhgYCKP879uIgnpzIJ5GT7M1tw35rUuipOACGp2uGfTkfMUKFYty38Nj6HkX1nRJtMCIl9JTq8gY4KKY7z3qu3OLRTbMMwywqGpTl1Jlpeh175ye1h6bCL1EJZUdbUP9fgsOHHqpT9aQ5LNhkljQyZOyRerMprz1DOF1SaIKTT6EsMVYAn6Ixf1ns50YGtxABOiSALDD6pOJycL2MRl7TIwZTSR0MXqPTqf3EyhbMYkYYJ1OAf06avlYGdfHAx1PEDPPNW7FXU6NL9qpG7i0aZOEpzPsklPuBue7fSJjXYPdE1z2nSc8DYpco3UjirE5qyiDFMbEx1U6OtumbsAtGiXXlcDw9HHqBzgqE2BHjMcEqxO2FVY2gCxyvvXDQwxLrkboJdI2uKQPDGbeAKFeTOt9pXQmO3R4pXbIVIzGacURhh2SpjoeyOSRFHWTDQ1JWXcAzgVH265ZmPhxc5u5MVMV8Myw3HKdKxtqBtzEltcyMr7JgLRlFMkRK20puVd43U7umKfLhiXHeZAG8lL0jpXhFU34SbZpuHEUod0h3oP8SGRpPHagmKscRcn063NGyRW1EpFo8cvDIetMxrqIiLmRIxcQVViMOTBaGCMpd9HPxW4S5SZFRChVuDIqVbahwmGqplBoTqGR6HOHkO9GTIROVj5lgpnWExJSNsMLO7KZl8ZxbZxly7PZYx1jD5zvGJb6E91fjm7ibOoHhvV1rikyEdUICEs3iev3vXuNcoAvjqZYTUaE4Qg7XSmnJZ0um6IJ6vBwDItXHqmaUS9y7SdX6FuWAXIvdTLxXGYfkusyS21W13OVm26qA2DBKUQjgPOgeOCxEiGYYCwRfjWAeExKs7MH1XmqJDVJTMaW4NLgwZqW1UMpuRBGwMpXL7XPL3fDC4LnjTDaZ3GZ6qP4kRxq7naHEF2RoOrdQfeqyH0d131ueUYXOe8xvuq9Va7UgC2m6jNMGHU3u5outibApTcM9QT1gDOtX54USgbsswFiUHyj92v2woJXTj4cnR1WvV88AAqYq3VkPOXvcrxLItbTloIwzO576BQCovFCqxo431ZJDAQmzQZV1DHs5NdOletW7tEFHHwSGDjGhswJ3Ms7ZDr6a4Gu3d7MJaGergDpRU20terpiQ3uOi1pZMJXa6Rk2yEsPgxAfiD5rZx0PSyUMZxKDaIrFpomoRH4KHaHGogBCSExfGMh7OVfk6eLyz6IINz7mXxr4woWFDtCuYTIqMBAV79vlaP47zK9oTFkTtjC6TnsQj2mXlsgy6gOZiVCvi9XmzBfX5ODSgYw5pqhDI2TgApyyPPztwg0uTLIuNqB12uAv1YQAzKWvGqnGKnIK35YbPBBYEhkggkPG04cvgYRUxOIsU735BcC9BoOqwLSZOE5efG730cCFQbVNmNKygrg05RoYVisWlmkauptR6lflZha9Mi7kUROKemO1dxDbwvzX0G42Ar98YWVCLzSNqeGXLRz3f8afgQ58cXfYpTwTpHeBQbJzFYLzLofelGURCKAX1fS2mZVmXSCIdlwTABqTLQVsV0dYfwqcjC9J4QuzdC9xELZXg8SNqZspf490cAumFm8D0C3wrACRNw9fNMbSLQ0bvqshkCC7rIQ6tFtwPyt1MtJBiynWdWdf4JLZOrfN0ThiSyPwo66IRI1VaT2KO9BhokNEZTNRHTPJW7XM8R6Zy3YGDqGmB7hwIUB2SVEmLOl3LyTq1rbd2fLDxdPcbz87QQ7VwviGxoOpuRmGTfIIqgObAyimUvnnhQiSKSa06ToYTgUwtzKzMxHKAVaH6w9tW4RSRlydDVNtfTIgvagcIt685wmgN7hz7MJRTX6tBKqHRJ9YSqFwqSGyfr4vpmXC8li21Te7BHuN9yYeJGflsaJsXWe7dluwlFX72GhgvADdLDaVM4G6Rub91CoNyA0WRbrb2I0Sl1iKdsiewaCsf03F7ONa2A33iwsR9YcZs5loODpEXg9LgBuGu0pYkxfgNqmi9w9SrdD1uVo1hPWWk5lEaURESjZM1kCbpJBaviHfniK4afu7xtCNnN5bRx9l8BiAKmQdW58QuVQZ3TduAQzhS6SPAmaDr3B1cUrFQMQ9IKmnvgWdeJxYPVGbVEUBf8P5uvvU8XgcBLlCxHpGEDVs29oBSs57OXDbpMhpBiMFhIRauMmk3ezANz54MRHghAN97pYy4LPGF1sECxg9ywv3TEyEvdt5O7B8Od4iwR2RQSgVbNje3VcTIejhQow5O6dyPoiKzkro7KQeHUtyrKGtstnCf9HmVfCTHiozKO8wUOjvoOTBvAQ9D0HYSEt7P9B3MwmcmuvjTLfzuHvDAT1Z63gI6LqQ7rE5swKMXHHyIfKqRQYbU4VOOIB4vnbgzNRtUR4LH3lGaO7bpcGTyLQ33TZKnwucePgDztKSJbdCxXDmt2B60qDAefBNw99PB6bORKfgdyvmziVG5SkGrdQwSh7NfStSeOzsYjdxy9weVlN3mMCtV2ppIcfLgQup9axP91AVpEMmqfWST25qjxXE833T6RSqV5soGunIO6IGxGSX8UR7iVbXxrfaXVC23dJ6CUcvLRE3gy33qif9BsqRuqGVPugkYrUS0CEiWyGrWGBSjnECZltvDDKtboCLOJsEpoylhWDh6teouV05IZ8FRibTZ4HG746KOUFcQ3xtSBswEp6c5WfdO6bv0Z9iAl253we6Gl9ryLx0BeAo0XsSTCebHcdMKqpMLFTKuLYDS5nt3je1OMm4QpsnaXnj6vzyPIkZNxGDBbQrDCfFnxbcQtLdajSLkORlme8EhkjSAgBcNT0zJf24ZZiNJt4IqaqNqd8F8CpY4YjA1V265nK6IQw7JubodEMubIcQurDRJAFMLqT0azWcvYlEQH02v31GPyGfuhBPrrru2TQdo61neU3lfiq7snztUQXyckhpLlbeJcsN3rMYNcamLFov4L7LTVnmfVLVBSq7DPJ78PqMEBgbipPRKnvNsUKmNOCdcKltV8Ru6X4TJv8td6uDqhT8fbjtF0eyGzayGhHvv9pnoRWUPfTafKsFTbqDx6hps0nHW7DRJvs2avLDQHwVSc8v9ZP9Xfepmw1D0GtcM6EtkwRrvtOvKw0khcFAWJwGZ2Rjbks8qBQA9yTmbmBOkgj1Uyv5uoyblaSKfXcZVpbvwkxWUnYMTAkn0g3tzMFkiFHEiCGjQRLJ8eWQwJSOTA3ssRuKzRiCk50ssDSgRSU1mwpKg2terwx85k24XH7k7qEawV0UsazM8Iab7H3AlXjMgl2lqX3fkmGwQFrNYoWEFLGEbGLb5NMkaP7MsdPfy47u49docG4bno5nWHjKhsxpmRQPWRt4v3hVRtXT9TizC7Rcg3CTCQAiU8A1iDj1DFE4JrKJTDRSTlEzS5Rt4jXGcDSj2QApvhzf9n7sgm1sxFAF55xIQNpvjZM6gpwQ9a2hRNfWLm9mHIM3msRoyHdqIH2pCXlmOaOuisXTV6dGRSsmkK9cgeMerQToMUWoARNbPlJwqHyvGpYHcY5vYPtIEhxOUCwEYGaflKN5OrOVJEMOieC5DyYMNZC3EoaEGDdvjCE1MKqoddJXvSewwAMWCs2b6FFhxJe5ZnR3U5amESOJs8QviTRAxfwdT68exvgXgWA5hy2aiBrRxtWfOH5N7wAPzUh5VZRFftjqWD274DZ3tCmjwRoB4XF5Z7oAmvFEEvxThBITPIF1tv21yuuoJeCnzAheJLkrfq9HuECmtZ1dF8UUe3eKGEn7UVaE7Q4lYgtQyRjsUWft9RDON9YCRo9GsNROG0G4koZUljHm8CZs30eL8bsHxRTS58Ooyhn1OYuEmGeUgOGKxRCzuwEQoURCVdzKncld5p03nmiGyg0SkTGk4pxK3rUTdC8sCrhR00ICSUY1wIIeWhBbx4pFxuXDWhuGJm2CbNtACrTPjQML7x85bEOUbwbYBX4wxZhK2KZw89J94GRTYU5kekPjU2ffmg5SoTfF2QM0l2Bb79iL8QUPTllQaJPR9cYQGxLzwuwVVJZuEGWfwR4S36evrM7C9JbTq42a2QDHmqwShl3UmeLpPX17ZkPOqIRviwpk8HCDEvs8w5pP5JvpWBDZt1NpHcGM4DLRNN0A6094AbPYZI2QySwvo7VDdky2WmcvqOdy6RhzJjzNLhjSVp7K18yy5EojIZbDF34khmcB6gcCneuUnQzb8CtMl2EYyXgXJhY8WrSNSQ0US2LoYQ9uDHErWPUYYDSDW1C8ZZcB25JUDAxb4Urk7ecx31tlNvLAweOZogTawAGylyAejNGkIY4hMVUKAs8iPYHuo1jQFtVBnxamu5xQ9kdJQ0tqFn6sAow1sOsT8JoMfiLx76ZnTYWd69yd0O41ooIhcf8MITO6bXp42z2Aw3qpr6JmhNyXAXIwLC4aBLDOpRlWM3M96p5FoiINeiKVXOVBhmOVssT4eW99xYGy8IMP3OCRFpT1CNi5sXVdR1Uo5XLler6coFVXQ6CgqGJztvFG1bdDTNwxghUPwntIfk64DEkiS1NTqlqUVp6CplFjq5brvdbSRW3nllBQyLODiHCor2epmJFT0W1AV315IUkGEQ2VvHy6hocurYMuYJZXuhN2KusU8bOcDXpqgOMQrm18p1Ro5oIzdOvDNqn0n0HEARdEcSVonv3D1EIeKTd0qePFK6FeFqXbiAaQeipcBHYxqr9mUhNx7KQ4YFe4zKGuqYerP9VfQzOTbuwEhHNU1Plpont8R8RDSwgKnhEpFs7CEqJNs9SfTTQrQ3uSjNheMBMzKhWW3th2yoomKhR53Ij21vBQGBZ6YJVP7f8TdJJ56oNYUbZ9Ol8kt6u7hply7UrYTea0hDEnj6Wk3tNzEbR9k8MOqy2uOAN8NLj6aor5DgBYrukhc9YI8iqvVO0MirXuX3k2kFb6rRCCBvug0jHIvwQxeKfImb5dqkn6ENKpXBpwBJV1cmMtdbf0jLSRKzvywThMwXPtLGWmV2uEtXCLMtVWUd4yQtUIean5RvLlxmUdaEZRXhrNDHDKBb8300beGwt0k4C2TwPADgbcX1S7mgVCzqYkARU8y3HHa3P5UW4SR7qBz2Y6y6m6uyGY0pbbcqCz83sCzfhyY3Oah8QSp9sSlxvLuuA83pXsZ15J42U7QtweYg87w87HGGuBGP3hKxbZr3xSif04Wi2DVDiENKk0BWsrc3LuGKAHGkKeXZFlqHm28yzAozjhRAsZuQuKgSpUwPJMAfGzl5Wdni1cbx0sLbra2cWEWe9ngDq2qSWKQm75k4yCLJtPwguP6cocUtR3V8Z0HHv6KygDBmRdfIQFgjxAPgyCDmoxLVHi3LmK2kKbrVXx8348kT5GSAGgTd4X8xCfl3R3dnjHKILEzvXHQvskciYqPTBsLIijZ7FZcKeq7WZzi480IptzABiR4YaSHxI7cx10r9tC71V0N47BGJgCYiTvBKGXwZxHGkwhNBL4p36sn6Z9kRc0AtkUchetI7fjdyBheIgu4JajTF0pfxN82aAChZ8HdgmGZf9ug999OFCsvQHXqYVtc4ZiwAQdUweBn53w5RK60xM8fFXxCCQg7ycKSq9pFhpyMt5QhcSnXqelybMS5Jvx9FAXkbHI5vJ9s4tZbq07pEWg22D9xkYDt6LnnLbMXsV7C49njViOGpVLEwSVx87PJ19a4OsfJ0x3ETeRq0Mkh5XH1nhHXlkxGKCl9X7oGJEHRKiGgDGqkYAjSApxjHJ0pxOUiNDjjnQYP6wMHR4wD8IIxRhjwtoz8068pU95Fzc0zVuVzaQhuk9ni6U0LGVEzDStNxDDuQZPCutUuCnW7bIHAKXkvciAZYmw2EsQhQxZxfMOTNwGbKIIlzoG7yARXaqUxMz1TbYtIPvXbAHqkGJ581xGIhYNHEVBt30b8Kuys7yEnTooao5JBTxJI7hjQ30FDPYG8ZilaG4oFKoEoMjt4F1ScCawxbybswxWvIUhsxxEerNdpbKRsiS1G9eI1ZAjE7zvGATeOksOwpN6eugp25hNqoMgjlqPWjptRtPwbnNg7Hq4BcLwZ2H8YVDyC8QYdDvn09F3XW3CoJ2Hd5g5Um25EsHGyeHnD7WNDBqr3YzQO8pn0yrHRUFtDJqBgyGak7J9JqRMq07G1lXyZ15zceMgviSBJTj860CSt3glJMlRnM8PESnSoZfROMhzQX7wwCuWDSvjWaRcg8EIYWjRd5ECPTJQulPOjcyNNyHzGRUiBtAuwO9DWk6jQHPAiX9SYHGSavqpHqQO8dU0JUERKuVY6QqIpIWGT5NJZdO6itE6gWShtl7A2BT1uxnIubyQPjDhnsPJPanKXdIkr1b0wxyrFOUspQABkbUMRn4TLaVS4PvSfcQNgjJtDbFw97TCINQhHofayAewgnEJgMxDjJoZ97JmI259lAEQtxW5xBa7CW4slLZ13qHBfb2fcT0x8o9vRP6i0wZJBrZPcimOBKYH1xSSCV4DBJ0jplaXFtnp3AU64ZhQsqE3ENPvAZQFTiWScmgK3Q6RpJDb6nFVwsUp2TR5oMsNdJIK6bjebtTgFf2MkfVESjWW1tjzkhSSqqOCrmRL1iGox7s1rIHcf7G9o8JXU1u7glH4qJgI8OJxXAXcAF77udym3YDQ6NrXmI7SQKRmU3wNzbvG7p0L6xaRLoLWToOLMz8yThEsKnJANYwVNZHSHmYi5Y2UbQ4t6zfL1a4eHUrJHazviOjrgUWjDJmK5dYhAIHJIQIvWG0VYpYSkWpVxkhWhU26VglfW5yYHztVRD8Ae8kDKdMTbNUfrFxcil8MFrw8Sv89YULGsIOGElTDEi5EsVH13NgZJtKrzSufnyVQnJRf8qCDe3fcsvk8dUQGeuR7YVLsjjsmcJyIZfCiOJtBOI6saS57mvyeSlInprqndqi0tp6SFfUf1CUHscpAaEYrYIHK46IuaJbRcObefxCgj1OhRAyyJvF7RyQdKBwPf14uOIWZ8u7kxi5spPpFHIH0xBFPVXNhTMCBvNJspQihegtS1iTmWuO8jbUIsa9TupdVtfc2aEteNqr83zwNE0iVv3DfCsJGeTuX7QsCRIDpGXMR3yPxJAZAGnIykXZYQ07shTw46uDWnWFqiro8y8dBkrZJlUklI1D4axKuJfLdD1G7M8mzGfnPpL1uyTqJ9YlfrV4rY1btHiHPCR4E5XBznKBsyL3CZPmsTShKLoNQHFaSmP59bFgBCKjG0Tm6CQCkB26Sv8dsOBJBPgOOmVFYXD5Z4lIVDq5fzwCNkCQjO3gnkn61ZUSHprSvb5KxSbw08Hl4RgK1pLOOFoiRv4QTtOiIMew6SjunhVIr6ZfpznhMj0GF8tSyB9zbbHhizXe9vZ7GUYxvIH8Q5A7u8TtR7BqJxdRlYZZSuGd4LtC3qA6EAlKyF12taVuDi438AdX6Vs6TjGpvJwm0nWfADijqlCXUVaUo6yoIP0EazJ8ZnyWDHf8xQ0IfwEeuGz7yfLEovz9UmOc8fKBnVffOlCSZdhUhN3VROxyJVPlyGIqEJGbtbBo3qZtaeXvIEcVwvaKnX30QJancIgHWBGnp7r3zrMOd8j9LuTv7bwU1WZzSriRQjAksC3TRBPPaLNGCwXrcjqGf7iwTBxxdw1osnFLETHe055SH6GtEUiUOA8CFQnY73wKAfw8a2gBschO3y4qcsT61jHEYaO2Y6fMzD0tSNiWdicvhq5tigZaDtJPfRz5ZmJeliHgM4OW7aqat2m5EQPoQp9vVXisrcGAhMbwY6Yqnueykqzsf3CMH11J2L7BLlUTLBJym0HHTAUSGnB1T5XZ4SiZmNvnuPApDXjCE6ctovcECpmNq0ZYZannoSivUCB255EVcF47w5PTffVWEAN5FMHBLFPaVbpamQgcKKhaUK9EBBmQIFngPHJibhayLKbhfsNWUSEERXNyNVU3AATmvgZZAJs1f0NReScbIhSTGB4vFmLIbnrZA3bCim803d6cupmSMai1GlbDRjvep9xTQ6zX3ssuXBHStw0fRrYcpgL0vEyn3VfsRasFC3twOn0VbotvR1cSeR67WBLoHbHidTM6E2fgkwHjqr6ZIddvaqafcde3J30tGstWBZLtYULDduV07MaUiNxyNSvcJFcqKo9s9J3v2zSt11bfpl8ywZ2bKafAAQAdPnHYEgv5VtupWjLnFkZgJArzH2UyG1s8stirWAojYlHQLnrssxlopjker1zNOs0jPib084RpXehsBiB6JKMPPARXzR8mTpCFoEcWCJTY8qBCxcFd7hlzvyKoTfMGWdyzp2pcgMkhocbvXb33p9OveDi4MnRbQ2xxW5E7BBgQWUSZBvQ3m"), + new _Row(27967670, new Guid("58EE5163-5F41-4C83-8175-34580FA54BDE"), "2yF66o0lSHl", "5ig"), + new _Row(27967672, new Guid("58EE5163-5F41-4C83-8175-34580FA54BDE"), "5tvcpUkTvlRdy", "bQl"), + new _Row(27967686, new Guid("58EE5163-5F41-4C83-8175-34580FA54BDE"), "MigVFhYMw6jG", "2ZK883Zo1CpaZH"), + new _Row(27967687, new Guid("58EE5163-5F41-4C83-8175-34580FA54BDE"), "BqqByWX5Wuc6DooWnEC", "PPKwf6FpnJfSTklfX"), + new _Row(27967684, new Guid("58EE5163-5F41-4C83-8175-34580FA54BDE"), "Lwt77ImTu24Fv", "1Sb8VpRuiBoqcVXblALJW"), + new _Row(27967676, new Guid("58EE5163-5F41-4C83-8175-34580FA54BDE"), "odZDwRGrx7hRmxR", "4SMlxm6uDqztEaWm5KgRstymBQh5yF8fLtXo"), + new _Row(27967677, new Guid("58EE5163-5F41-4C83-8175-34580FA54BDE"), "Y2dvJ9z80n0ri9hR", "dhq5"), + new _Row(27967689, new Guid("58EE5163-5F41-4C83-8175-34580FA54BDE"), "Dhhd7cliCo2E8Yv1FIK", "B1VC"), + new _Row(27967674, new Guid("58EE5163-5F41-4C83-8175-34580FA54BDE"), "sy79bLYyrZ1", "t50JSsD"), + new _Row(27967673, new Guid("58EE5163-5F41-4C83-8175-34580FA54BDE"), "37Wh8RDNCMh", "C4u4MFMySkFV"), + new _Row(27967671, new Guid("58EE5163-5F41-4C83-8175-34580FA54BDE"), "iCQZR01", "GoODhGbRzTKk7qBWjCc2"), + new _Row(27967685, new Guid("58EE5163-5F41-4C83-8175-34580FA54BDE"), "M8xQrKB8yEOLVGR", "LDJMXhpJvfGFKNjT3JDBatO"), + new _Row(27967666, new Guid("58EE5163-5F41-4C83-8175-34580FA54BDE"), "FfrqRLP", null), + new _Row(27967669, new Guid("58EE5163-5F41-4C83-8175-34580FA54BDE"), "QXSN3", "XCtCRP"), + new _Row(27967668, new Guid("58EE5163-5F41-4C83-8175-34580FA54BDE"), "LAnC6", "Z4gmN"), + new _Row(27967667, new Guid("58EE5163-5F41-4C83-8175-34580FA54BDE"), "fiE2vDYS7", "cB6hHKmoBiMa0dRG"), + new _Row(27967688, new Guid("58EE5163-5F41-4C83-8175-34580FA54BDE"), "YJxqBBgPfdgqH26XCUWD0gHCsv", "ZyhKMi2fBWVrNdy44Z5JIZK"), + new _Row(27942037, new Guid("08E06BB1-757A-4D8B-9768-4966D16F9173"), "LesQjDrs6rjgDj4MaD", "2kn27opfVyoEp3Y2S2w3wCUolQCLbEekWeuieECmqs3SanO7UpEm5jDog7VDVz86H5Qcst7ORppJ6aaDE93g94UWpqMHzJce3BTM6f26F8RWIQKXvcHHiZVrgGFUBXp85hAsTpIa0udvDktPZ6yDy633LH5AUXfUzBWjYWzpzSeOq1Qm1WwHfjVpvu7eixeol5T6whQeC2u7XhFGzoCYOEZ1FXoipOID55jiMidksGM9wlO86S6ZG0J6ufeluOPwvCPu6sOxH9JFlp6aWDGyJMeCLi5b3slp3Ef6TAVYT7TiG2bCKbP8NIaPqQKOwIlW6Ih1A4aBbH9iQPrwiPqzb1Y8urbacc92Ydk75BNZS0QtOawJjbYFZXTHhNsbDrp2meEnF6bfKm9sDAOLPEORGzGoMohwxsvhFmx5EeRYuNwp5qZfDr92YjOh9SphzxtYkZNtAnMBJZddU5cSiqx5vK6o43CBdvp0ZScToX1EH0rMRIanE35QqNRMq7Nkrm4YSeJw98U9TLnqavs6xFNZK2shnFsq3y12uhKuzKkLCLReGGjAnCKT6DSY1LfnfNf7aezO8pEFWonr9Rleuxrq3ja46kALo5L9xBEsDOr9YWK93BIYtKcJdIHy7xhNscjFbUlz3fOyQ03gzeuIkb7ZKP9AmcBmJK8GMFi4KueGtHKrGnmpJRE47PRArdBbRYpZm8rIfxajEP11eMbjcZirWvwf3HgFz4Zq17dxoaGdFJaGrxPlLwATovx3gutKG4EuqCDtG2ound7norbvVS80VhBKolFvgCFSlgkLBdhuA0sGul47kaaQnrvdfWKkCuSLjIobMiCf6uwdDcLlR8IMngHWpYFKmv8GeCvk3G30K9RkLkFsBuHjjoI1R21MCLwSlKRnl4fx5qDw7ZswogBOFW9vlVQNzPmONYqHCYy2AXXZje3bAB3q0gQsCruldhGaq9TXLg1rbRzjuDr5Yaf3WPWz4ArYMlOcTHZikqMlyB9HcQgvQuDvNMNecVIfeQKoYKjrayWtKW5e0lfF9YetktDVMKBBtkX0zYOVAhv5JbGTQKvnbqbSbk8y3pChMOAl08CYfQyxPzuqp8vyKtH1ihpYEhPgR45LypO3UCygcOd0U6h8iefV8WSZgXyjn6f9ZQYWeomJdAJWtpK8ecgmXdIAU7hIvwyUCCtMhcnQL5T8mG5scNl8qJDJkSg5bw4VgsECLzlV0fod4BTujq8mQk0E1aGyqi4FKNx8IO5YL6v6tT0eJd6wt2a6XgNEJUX50AajeYGMHAaMGd1Bpm1JyKc7NiMtTTcfkgFEMWGRjHVvQLinxjsU5xYkVnq1omwCAWu6E58SWrVQU3czcLHfl3XVfLiDMemiCBwO3t0d8Fv0UqifbNF4OyeJ6teRiTklxqJlKeCJRqiSCxPg0fyIEvh8ppjjkOYpcunrB5f9vvJPbQzUYEp2hKQAgQjOj9Y3UsBppuUh2FWpmQ4PjehZXTCyEuNAz2FXuRfiAG13wAVybPsHrE1gKMTiBPtbINgALfR0FsjX27etwVDVDWSseGCpwQM63tyNYF8gpmDTakZOfn94fLSBNFZbbFlrDWpSvGh4Jposs0nCOfFdJBJrhAWOMXANGYBSlmyVguLMfRcyu989aBuqhLbkMgLiNZdZMy5NXuE3mrQDttN99fOPwdZrnW7cursOAU22WDCzekE0hAxfqv2zJlIjYqgKQxQWTjK96S1qOPFVCSvusYP2e7Dg7R1rb8N9unMZOcYLPYPBk5snqOUVND2hSxzDfXWdRbp2b2TgZgSvx24378vZlyF5A2uHb6ayRExNANfQGFKkCq7H0NghgQ0nLR4i5rKB71xO4RRAQkzhu0S3poFS36NBrndz9ItBjLc9rXv1Wivk3rAONcgraTIqQDB4qtnMHK8vBtp0rKvvRgYEDUKy0fhGALJUjPiTo1WoYwFo29USeapeHIfUm3hrlTQ1L80TNA09vyr09ESBbqaJkRcy65UAgHFdesxHXLtjVjf0ykdZebuJg3w0mScxfn8LzL7BGxkRy25d9khktqbTKVCpAy7ERvhVq31eX5GuHJZWiQKjEKpMay5CfZQqD1U9UmLeAQjcyXB0WLA4YYhjAnJumHDaHblMkpDh6ayhQygqDDhUFp4rsENH6bzbtb6pc13yuyNmftN2EcZ7VOrz7LUcVtr247eJTriHC1SKn4XNHyhRNYtjtb6kU6Gec4XMrzBbyJRBqGyG5tCZhvmmDas40sHCQrSv0dwFoCy3xJn4pv7UNNtNIcsjwl8AkGCy7bZj4bVuYMK6TliXYGAaQbAuOLPZw7c9ugtxj3CcaMVi3j1gR4TpTarfEpLYv8g2BqwiEu7rJFCX5BJEXjnsLEt5MDCncPXjNCaFV43XS9I085AmT2EFvPPkubyItyZ1GQY4S6mATlxcPknKLi44m73axInyH4w5Jl6899cneKDS66tGkqf4ebdOSCLzddIWOryybAC5lLfpUY0PA8sJPfT42D97otuh6JIgWC0mZ3LiQeDWN795lTaWK4aYR2X7jIhvmhXNqPxrOwU1YR2aWYl39jh3Sb5EBZ8IkWSMfDcfdeVTWTlGPGDbmdTY73tKfztV6QXoRRYiDXH4Hu7ogYZr41o7B4Hc5jnBWBkrqW48kA6a6IFKIznpoS46KIx7692LCjvm8M4L8UOyVXwWjxhU5HIIcXNn6m6qqKauMapERVpMrVi8OeTSmOC8O4PnSl5htsiPi5RhalizgdFbkKJSLyEAhjAnnyq8LLuH2VnHMQXy0WpWSrlj8sayeKzWk13E6lPii2dU0kZoo8W7lgtVlK37Iu6NFSe5qkeakb5iOAnH2iZN6jG2lSRuEUoKKoBKJ1f4gf6tehGd3lLDcoyTy7Pljlp0xvmn0BuMHR9eCJf4GZBPm4kLLBLy9nTGbtdAEUeJjiPKGMihLzeJl5v571MQLw3gq0INAF5wjA6QVd0wancEcI9gepQPtNl0ORrqsagpIpmc0cPSZhDMhhThcV2GmpbmyHs4hkZUqfkkqCwwhyHcPD9kvOOuHTCfegbeM9z5mlFEaN4gnvZvSAMGXvJRV0EfTQTHpouYySHgGMcbhNSJFcPik8GkDAXAg58GRjTdnrwg4VhWOt3XCUQw1tHahQsPGvDwjeGGoYcqanbJjWi947wsskCWo7rv21UHF1B858qNamslJUQTBLLCRGCYi8nkWfnST3uqUsUOGiWDavYqEmuV6Dva4pD4NPMkRRhntqlDEjhBLv1Uv2OpYDEYOJXpwblwSP1VZJlIjM76TodHTx37Qmnb0oCif9RkjLKqoGvqW0y55glJfrgucgEleiIJGJtA1Drd0mXDN5vFRNhG6AKlVN1A5hICbxoWxWKtX0xOLg5XfTmNKFoEzs0rKo6U18FC55JsPMLBrRcY7xL0tULLnnD9JoPv3yddhRvQGA8Qd9RmsC1FcnHJKDzuKtrhFca7sGSQUvOhhrlaUs4T1o5jh9QK9A2cudR4C3yOcl8VirIehmK6RuL1PhdSjVxSM5XY0Q029h0xpy0mFGONt3f45Kd77XolEeVRgZyoPp6BDS86l9UvXfQx1t2laeZFmQji0Ik8d37LGaDg4sZCbei9A6ewWd1YJD9DM0MaMkR69ligrBeUFEQwkHcJHv6kKRPpo8FOvaPZ2pfwHVCJozL5LWf4oczGpnn9uQaajWL9iqE8kODalzfArdQ3x8S6dWNFQ6F43g3MpbtYYtHLnfaFW1tn28a6CX96gxn0HAtSZU4oWP83NvIhnOuirX69WyO0MjBNmiCWFSeGgfimJ5a3bGRox8LYbC3aX7O5JmBNGfurSNpEyeAUC0wjjMb7oyWRNyhyOl3gIuMqlQWuVd1FqbIOeuv6ROwGFNJ7LbkBCq3rMDOrUa40S3SlffyTMeYRdXc7vUwGIuNB1rwM6hsaEwVhXLhPiAcLPo3HHWJx6oPAGAeA6pcdp4XXM9yKxcM1ww4VJF4dyHbXMHphBfVZxUPYIUjRU16YSefbACIn4LcGHwNQY15OHR0fasxFTEVdOhvrNw4r3mqkMari4n7qMdjP8by9CqlR3RAjeyndx8TOLIUhVECN2LDDxRmhQMhP2bvi8NoE5L61SmhwHG0z7fzIGLodiUdy7BXmjyCzGTj8gElyXOdaa4dgmcYQeg9EKMh9prDEZX3tjtJllbZZafBWMJZgEEUrZG1z9WG3Gn5dWsHRKgmat9k5vE8D8zSkvJGTDnPPbAxtYUfPCxZAOnqFc9yGM2niBbZdNFLpqlfHtvtDuTzfRK8Zf56p1g6Z94qBQSFf322i731xWW9fX0yNGWX5HrQ8Rh4su0RAaYNUiqKHz3zz6dyny5KGTmEO80BeJ0y1Bh83YPB0waHNE3HsCoaHK4uQgdPrF8wyMnJWEcjhbFodkgV3GfcLTF9NVHEdCZiVi39fgiHFLEgYQGQyzqi00nxLxHmDJAauZRcSbZtz26rJatJaZACMsvBCK7b08iv0eFB0G4RfTQ7skC7PyJmlWlglfGuT54SorKHPAnDYb23ss52DN7cdSaE7mCBZIXkh5Bcl9N1IZiRABfXpY65uErm8j2uVxfGxhmKWfglcEIvx0ddCYP0fqNIf0L4fdFZM8s4glvOZVJKWdndZMgz6WMgreH1bSduv8QaSKNeX096WUp5qDY0m8FeDVvOFtR88i860rVVYg5aIYOqpoTUaCfb3jFiP1gpY7w5HNizQRc6dOvF7aRQ693l7Gng3ibhV5n71QnMXIHauFNoVHHIaig0XCc1SvPfgeE3zmMmfSRQmPSbSscfHSlxrPM4yJFIooIDbfWodZ3iaVXuXk6JwLaUjVy2318MhxwlsYoBYg5SYt3QWTvgvDTmT5mT1SJOB0e7DZNuTi2qTjG2qaO6oTj6mSvt8LYrzoN3q79UElbBTjavMbqVg2Y7CAgNjRzirxyhUo0h6cjRdPQNsIciBRas6mnCa20QmH6OIjOgPqA9owwISfRfZD1C9Y6jtACoLaNLr8UWA4DHK79jHSglkdPVzTJYN53OHYfqSfKEJDbjF2jZYCUtegX2UQXYXpozIln8WQgM3xBtJQ2Xkx7Kp0XHyyVujVZZ9v55Meu2zDw07TSMtti4fwIOwnDt1ywkoFnIZavlVDRptItDud7cHsJwGIv3khgf1HuxN0LN64XyGgtwsj5Ca1Xo0hau603f3MzTAF3CZnmOQDm4y9vncrgDkwkeOwPvPGazsLr8OQSWOOu4qseFUgOAZWNpFCyWdVOpYdu6kFv99uAf3lUlf50TOR5DG86sjTx3EdFHtzApgFUv2Qyh62ryD0DV57ZwGBlI2g9QvTc8ITT8sGjJYbV6BhlOIXAgzDaqStCoe5X2U70AujyIN4Yl2luWltUrXRrREuRXQ5M2QaigHYX5DK4cRhd3EitAXZOO5hbuHYb9iErxoxk8EW4nhdCd8CPDA0H9Jf3DB2AXI5ydDgqWVqmPtSoqJ5bLc4RTryUBkRms74l8ZjfnQil0RhoJIa767aEOU0XD5FiK1Hq2Dpqc9Ol9dVjzvw7iZ1jISSSQzsH9KI25mttFSoFa9BomxUhjW6gHcMoPd5rXsFFIKfMiUg5AtKpGhLyncRxkpOrIpP5zhP2w1WgYgdsTWvosr1cGJRPMkD1K99MEhDn9iKDmGl1iCtxWwcUSXkhyEz3SwSA9tbEkFTeo50jpsO9lFz548nRkBkQJox3odQBke52v1msPuxr1tcauZcUHbljWjokrSVW6Oid5z5dA1bagXgSuPB7WF7gQ5B918jA9wgzinKjj8Smjc9UfDOdi2BTxiBQc2qv5VGsSh82KgarsnCsPQRpr6duWmhHg8PMCN7KGVlRFO32eLuUHV09MXt3GJWA1TBHG3d1f59ro53ZFbRcjG4foaJQmNJpekDXYR1QtIfCwUhNlk9PAIXDrAtbb76ax5KA86IJgXDJKiRQDce34qmI9yTTSULklmqfwwkqrblpH1WYfik3ytvY0Gpx0nEU6Wyh1242H5xaQZS3yHfdgDaPDpMw4schAbgLCeb5l8l69NbH5Os1rWXgSPMsqJSfUXmKI4UxRSwt3QqnxMLsTb2THUUKWcxAh9lGdcRKatGtE591p8SNBaCmiqBH2E2F1r3BM4JryIXNSg8Y8itxD6a3i4uB8SN7Htc05dXO8bgx6vMR8gLfpGSQI7r1bAnYNdibTLFboC5b1zNjh5pm4M7xA5RHb19DciHfJNETIKUTNEa0RVfqicP0USjCDvBq7pjGoMjitf1qK3rjHX8RzHbs9Y2RlJsDrBUd09EIa9e9tO2oTGKAo3PCoeCXNcma24JRR8GLJ0KuOtqPeqGFAaASp7jdOStn8X5m3q6zhPioB9dApQzNdt42kjseKTAJSA8IvE5CGgCxwhTl7yNcsagrQVkec1DRgoHGmGxgSIp1NUXemEYIVGu4pVoZSgYcCmxawqBFE1pBKlLR0mTwZBbdkdEXCF1SBHORJox0hjehYKurrzygRVBdlclPC1KL8hn2TFFqsfeRQgTyj2vW2j6jWBD2ozL1AjKZDeLgMrqvW3N7vvRykzBNB6JN6nDJuKPMplXlX7C7jrKgbjxRlcsGcATv4GSE4VURpM0yWqozKPjNPlqvdLlnuCunk3XYQByj40HoU3qEyGtJDq9iMuVQYA5NcIQ6T3f14JHJqvstKDQMgZ6Lk1guHWpoK9LDMSl8MhH5Sfs9pzFwNnj9BdPawJSx9OXlFs17PhTylTG1f6wU8J4pnRpsZqB5yxg0CHpiZUHdS8glA6yoN6kx7khOLFrWsLvCy0XaMfSzeDOuZpIhnAtoCl4qbzYyQ0C6ht0fITHg4Y3gBECEsMM3XX7t0zRLa8W1CxHKpQicyp85Ftx6ItWrZ62cJBZOIobJ2LnoDQ9oD4PpY9RROOKhHz3YHldwSQtHeKJFaP0IgYykcUg7bHvNUdemzurxltSNARKnO7bJ0tAE2eqZUwzVa9Y6Ey1ck9yoihqI5Q4yhVo9FawzW86n8Tu1cNsbGb6lhkGIf1ADcoezCcIm8qZVWa4nlEv1C8FMvNXroxg8ZKSNgIxYpqGB5g20YA3hNOS0iZBXWTHwAbw1XfZwKxncl4fNKDokQWhE2xfuMNtVssBocDTLDiA27oHuNJYuW3LCLF8YbLHaT0vL4L0FsobCXxRaowYUoDsOUtp5Vqzs4i1gzRTmqza4XNYmF573ih6fkcBdcry2O4rVkFYL28XXe9V7xgBn4zoJvxAKELFZWViWEiSjFfWT2GxPjWPMLK47zLSNiyfsT1fpSYhPdZeCiCNl2cTIMbLyNShLWsb6EvV4Ln3kb0FK8SYCeOcG1S6lyw6iI8oVmaNJ2JpKVnZJCTaB5xuiKNp7hPsRC8XVIj41thRzFZ8p0GhlIE2BqGqkhaMlRkLOKBcUR2x9MTky2OgWJuruaATA8ezCLcsqENh8v4D6epjZAtTZ5QQxD5lbCkDsvaiTvVIPylSaccr5S1qQRKeORcv1VtwUX7KENYm7QlLlAi4XPtdZPxpYOJmbUmQ97p63kLjfs6vzoQ6yERH9llmdViE1ssJoXLcCz1A8zl63i4V0zThZhTSesYFbhET65TVb63tpgDoBXyuKTxM9ItbxWKDGn4C7heBlzqCdnFnnYO2B1rxKFThKZui039fWO6NcZ8ung8EDI56JqZxPZ6FsfTamEpEeafC1SUprWPvu6UKDlznIkOwbxSnaxM8W817PIdMjtDSaVaIs8ReSOzLrMtGQbK2uhmiM6rlXJE04j4o3SYURwPYEmmP84mqr9WUP2KvgW9KROaKwPSU1KRA46mPs5epdUldTAWYydK1Z1sn9YGrTniWi6ocggNf0t8i5GzbJyeLCYc3j9YvMpjrr3hriypUBvxidjDKbypehdxNrGdoyZsTzMstWWW6zHycpGDB4XmLDl9YtgnJWEqv13dYTrfATn7b3QKtAxLeiNh3OY7dtNRxrgCgrIcuIpmFyU9090SzPu7cob0OGYxPQektQio6qIz9AAEFmuA47WeharUReWKNlsyQg2EMkaPhoROdRsyr081mUPZS6OltfkuvbnRRtzPWzW5pM90h9dW6ujulmhJf83YhSkBGKtDUWmvG2kfKCi8kTDpTzGS5YI7OtXQ2gEn3mAowlHvMdrItKBlOqeNoEySju7B3sT574OTiKG3DjeDY5RlaXjNVSla8szL5p13opZkgp8CT3RBEx4QWlgFWcR8uVi22gQPgsoxopcCFiXyHwzX8BfthSnV336BAbXCNn0q3H9AekBULSeGoKnMEZrw3dAQ5EWIHCNKBYRNFAXbYlRbNqctkyVrOLcA0RM1uD6N6pZoKAhqlbRtaN1fzhhzUHgJV15GaOzWaQgULOxObPBZksd0wtwKSsZ7CNr9uCzIbSDJRUGkaUC1geNnxNiHcrnPgm00eZn8chWRawXaJentD0clh3Egn6aN8aDKtLrEDAL1jrcDLsSZJ0aDvBsXLF9PIflJ1TwesHtHQJt4Yav0BYzgHeMl9QuMyrTUS22ehGJ4yZDkk2dCDbIgj8mMG40GNoZXgXt5mwmwl6mYhUm9YkmD2PRiyLUxiLoCQllw6ELqqO8cBIJf7QAwaacP4zSpowy6PwkgQpEBuD5hUnHL1GyS8H6dH8WOHSEMw2h70n1gItxTQr0ByvPp1wVOhixKwldJqHf0Oyqko9h6Z8nKJWIOfHCE5SYNLX7j04qaohwJ6YCz9gZpKvHEJLetMeusVmZ1iC3ioiInjQppQnNjg5S6WTym9mP8lmQrXyoB2UNIDS41C2eaCFVSNnR7UI8V7jiNxlmoLxxe11E0Bj6DJuuBIC1lIymyyrq0BLCuqtyejxmeHywSMNdtLDPLAmnUIYV4stUE7ssDIclXlYmlQlxnsrHgXa5nUNyq3s1N9MrykHiSX1fRRaIaMYLRQMqL29C7ldySiSU2Yn1USE5OvPBzcOliaSYmM2I3Y5lYVI9yAL5eUQG31FU34kT1Tqcyo6TqdBDT3GsmUXy16hNACZbhj7bD86t1wW4II2ylVZ4QTyQlDsM3Mo7ToMbbmtJfx7lbqK7tmhFJ7AQV7GEpLMRKRPNukq9pfa94VQPxW9Zh6M7MPswBTiczj23hjYAOSa00yX8TXQaBguvckfP9Q2J49Qqp1TJGCePvs6j8Np2xzFspFCuGULyUkBFgkD5NXOzEhROxj9G4LuXZ3ArUouMG9NWEEuc3r3IHJm5f2dD0vfHgC6Sec4jSSCZREBcbAsDyV0yXhH9bCmMJdkmszv9aHLTV0bGSmysQbmyBfS5EFziWGVaiRRHRThiqryF9pHzeZRCdMtyIycnjYRy7Ny1OlvVB0rHSdQzbEbHXkXdrzXMad4N2N5Cka5wYtDNuTk1UqfGjHVl2QrG0UblR5r1rLwYDfFPuCxjja39qXIkHVTRoAwPFXBcJ2IwSnhy7rliBwRNK6J2i9R9m36qB7ndrqYr4WJ3RnK3aH8Qn0BYVoElpCmknw9uw2e8p4dNCkWB7J40IYnY4b4wVV9VbBkdyFiKgR3rp7KUHvpRD7s9rtsiPpIjZMWbKxH3c1FHu7UtXIY8qDlSrKiyReTyrUZCmC3kQ0VwAZgCiEHowOAy6lvFERh3WMA1FJkhTGXBlS6Olvr4E8j90Xy25U23serq3vKtljEy9sMmvtJ2P9CSSonPQoGxi0vajdGG2s9XSVJPcADuLXFt3dDM6NQdapPgKYs7FfS6aAUDfwExyLbOvFcFPmkv2BooTGL7HMzo89fVA33j9v37COLKB2UBjxwLchqbtGvPF2dDuPK9aj5lzx0uFIvvI3NGxss0wdcBWIXdUrKPAJ0lbpwIo4cwJrt7UMQAeL3iGe4KEUXwsAnQHDiee79tuOSy893tmWr8AjrTy94HuAKLRpuhSzKJ1St3z3K6YS13j47rdsj9mGqo9pOSIZCfnopESsAEiFfqR0h0jNNH14iCkcMdO94GwL2HvKNWNaLLXlOTKbY5fdzcFlgGomcnIGNmhaDLkQiKuyE0Vtk44P7XJNAbn27DlJXflfz2I3tDfLEMDyoYyWnVugxHMwAAQDzJOsie21Z43CbpgvlKxTYJEAooUPVOG9vGLUqiUJyBkRxvEWzA3jd4UWHvQAmrVYlQsiBlLEWveiFa6PCgiF8ukn4E3ljw8gprz3rbi7dqvfgiKsOjEVGIca7DbTFKgB2c3FmYrZQOBhfqmnhEvLjY8urOuQ5KO4vavlRPcc8yUgqhQYW6Y7gXQ2MyGNpm2N2HS4o8Zi8qtyJrYr0HqZ7szu36DToYjcdxDjorNN5oXvrH2f3dG2wy6YW3Eq2t3fVU4BUOBW4XGOS3WvjzWw7Ep8npmtjkWuKfUGwba5kl3Kx7xd9WKdug0LW4aOz26KMDPOgD8Q3HMBvZSp6crQiUFsrN7LHhiIUJwCAhKfRj0cAG431RpIvt0oPeqZxx7ef5g17FqrC66243LYIaO49yKpWhYjMyRt1iNoHrz0fRN8oGpZUhvbzzbfLqpxO80cqiXDjXNQdA7oTeb7EqPf5502wB5UzgVc1O6BnMEKoQBb1rmYnWt8hwBuIDzFZhsfUEasVnyfGkLYkaxmXoRiY4nXbNLzI41gqFG8pF4T6Wx8V6vV3WNP0hK9aroibMBKMRZJiaWTNd8UhtiecnFoEgvapNfNcPiJJdx0pfpLKfR5F86tbVLx1r45B8AreZr0u8QFKOCGVSzRXQ7uC333mjf2u7GZ1EBzBmKbGWAZdvJdIGRHHRa6CCSNB9pgofZU4x8z9y6pjwMdeGxBv7ctBI1Zu1lFdkE3YmS9muFsFKhG0VM2TQgUjG8ZP4v8qXmTA00yqLhUwRxkaXpIq4OyjIjjIldqboR08tiHZOBkYdDYmo6iI8JnMkGC2SLaAh8Z0SU3Xtg4rtXz3icMrqhqVo0W28Tz78ioIIWHJxleeyTtX3O0ij0A1Wprqcae3hsXyef9UoXJzESb3iTQmTCv1BmfV05CaVsv2NY5cHLgJAnBfwtk7vK9dVEUq7N7Rsx6Q3kwiCkdJpqOSSSFKqBUogDoKDVKU9nSqzW1NDs9YcCAYIsP0xxLGlNysz7CmE28VCMSi9F5FDwwEUokauExhPZyfwzF0XtiYNMqTFFE5J5XZIGifuY9LAocOLdjppiDBi8F08dwbetoeeFl8LIlUgwV5VFJWMa1Pwxl6lTStyqwbHk1YXWJn30153fOy3e9DzwfXqCdYiZHB2xq5pIwtI9DIRkP7QCexsbwftv2cpv7isrLy6yjKYoOOzWmqagXpIftR6zcklYaWa0PjEkqmC9vkvaSkuLtoAWx02W3EMq24ezH9ivvAWugVhmZF8uIpawhX5yktl9NRAdyU3QmebNai4Cnqqg0hLP4Wfvx1CEFhPjV8DQYnHNB7GVuvXZ2pW0qUFmg44yD2bMx7nVavkaOnrSXoMz43qomYIEDyO8VKg9lJKTGbPeOtrJ2feeKWQb8kgPiyMU5mXimGjIF04yaalOOk3vVaoLJiCt03PQXSDutazT2AtexwLcKcBAeWbAfH7kNBA4PB6z6uuEmRXf50QBTuTdda7vs0YSaM9FwBot6P0HjzAWD3DUuHVdhCQywLFEIZrD8FbwmpcOY4dPzb0sJCP5gS0cZmCCMcBpFy0nfaSz4YQAyeQXN90jk6HVfIH7bg0AIHRFC3Nb3Exa5PTxtfGe8K7I62bRDmUSak3rpvwaZfRmTPIu85GfS563QC0d0XAaiWpB28K1ZuGf4OgtoU8onuhWotUK7jC9H57q1aP3ULaXbbnMAohbgBq8f1OVK19rokjDEN7QpbfMB2lnr6824pEtEbyMtq7UOgp7k507Hw5jaGZe3BwvZmFqkjyXhqLWISdoUdWUuPMLvbaARsuN7g59FJchwGfsx7UlormqWKOuqVOj6SWYacw3vXWhcQTXOIrprqGIPugkQ0LLuh79xeqVNwQmJGalizhYUyp1BXXmc2iWW7DLArDEs0VUE7YdFoRLC23pJPwpGU2GnXZV36ZoONjpuswi09761rkWnIvvOQBbcf7sy2SA8F52VsjeA9pjpnyblywSLzYeH7QFkVBxTu8VVjRaIdjXHJZNg6O3gc51LuFod6VsUUyMeG2QQN07L1vCqaRMVaNMOG6C0vjUB8pZXZ3ethUAyhGFzwXjiLxhr0fkJBJHN3NGCWnNtTaBVjC7SK4KQUhPn2A6sQmGElpYsQIR7v3vswoqZLFcrjkARbHkU0m6beuQUxpIvCsCCSXp2vzgO3xw954fg4vDGRH1pp7zcvJkFyc8PdDU33ZyaBGWI16SHCcEy1wIc6rPDgSpDdjqpsP70tIvsPvE2sBSzi2tp1ovxefl8bAfEUuezB1d8ELauOtRYVteE3RFQe3zlS9dYKKcVtY03FjUJ6yFpXNlt3TPmvF2AUotinr16aIk0peRQjEwIJg6rDiZotOqUV7sJYXfc70EoTC4Z1N5Fr8VJBu6waObKhjCK75G1fepsxmOIyw2X0HZlSOhKVToUaIzyk5d5bWBeMxt8WK0uMXNQBjSgLM3hbqX8i8o6HRKwYQbXriov9kjCfFCeGEmoI9dOCfMEkpBTtDc2EV6RORuc8nrHvNr8y1dX3gIatjTVWohKTP1uegRYdztcsrC3WSoFs5A34WwxjGzXJkeWbMrXP6iXS9pPmKux8cVM137MP961bzTU8HdnpsbsqX3XgqDNR32EC77CoWFil5kExDs6GNAoD9PKMv6viS8g8J2qCqudSGhjv72wSl0RXqP7TDdJdiZDCBDqvzyKLgB0qKMkFJol9l72t5fNbwJWhP8sTwUktJ1yhjEh9i42mZH8F8DQ2Od4e5w34UejrMuQ597BrGjDvZU3meyI8LD1Yy6dUgcy3cFQxi3zoKpXSMRpMOsv1CuKeOOvlyV8u5ZRAAYYkHGdgbn41lUIj8RqAYnpgoJ5ZYSjNlXHWvOVSaIoUbGLIRRFZwnWqWPHnqrcBr0zIANfoOzMBdgVxjlwsKugedfLdtpGUQ5SJ0Jnn11wKUdhBlUdkft5x70PQWfCdsb54QqrJXxuGJbOSpwpoeW3S5vZNliNTOTvtyTYuNZXyN1naUShfvFvx3THecz2zUtO8lfTqHytVgfVHUZTpkLeURCE8Wfb1OGRYNGaaLi6oseiVlnufEIRRaOD19qxiZGvZsHah8hQru9izPowqwusfv9XUR1dpdj2wEwxCN9r8KViTieU19RNrkj10FSaohN3ZO9w9DB6zMjUOUBq35sHMdzOxBRwC5hRDSovMY2mvWKbw045vfwGA7nFdcBoRYcc2FsQTecKtmZnxEW1mGktdDeRFhNwErdVHSXkjaXcCbc67W3nRRlV3yAYCnCL8YQjyxPVI7UrKqcIgMSoDgM7AKx9HmKfMdB9zFd4aywwgqhKomM5L076jln9Ra1ZgPDh9aBOkj89U5MZeAzW7JXmmdpZJVsZDrjTKeM9sRbecIlN6nNIqo6KWUlksqOSrdOlD4QA2P7epWm19l1l5RygjY25QsMfxBbecVWMAxUiFhWwi00bi4YM1tbk2a28Rs24bjxOmK5cbhx8gNFbneNwBkfiVttpuUhddadOPJBonfJSmTUIAyqVTWKQs1NwYScfFvCSzggMTiIxusJyNiB2Fa8PGHxs7kGj2QilLOgqbAe59GzKeDgsf0Vl2LKH3zc7titgwnxbCqj4mts8zoPVXQDwHVBidaxhdp5aM4rJpKnjvcqs6UxDVIKeaSy2NUQRitmQcYdXKqoQoFBtHOH1an6vHvfJuGbyqD6f8AubujNDP5bSERw6IwA5CrAWt7lUnwd5QgcYCrxzUVTu8T0gIi3sD84JIBoRnWlWJdxr09JNAqxfpWIPxMWsiwaSp850zqloxbje35HipQayVgzz7gFchFjELhBoXkVE57AWTtrxfzzEChz3p5iHTIQwSc97NaqzOdUL4NLPmcysGxZiiFs4zFtw52yaj7KokqX5AExhvF4dPuWgjEHj43dgPkBLjvTMld6RTViPHHi1xu0E7XDK06APFqqPdGSLkKBpuIN3je7asNrzmVV4qio8I0gCJJVCAI3AdvqhDHUCKvFWuYKEczi2YPDKglSTpIRyxh7wnM8DIeT5bcL7HrFSw3CQo1u52vIRvAoCE7G3tvyBdrBlxeitwo9h4tUVJg08MXHWmQP2LqNUxcqhcr2eEFIbm6nNxROBwhEiGzgnxsRd2Oet8ZyAjUR8TpXVXlNaHCNqUkhGxPaiGvlDjofaRC2PyfVbgnbKBckZFXoHB3sZ8DJT4YBf9vFtiJn7XuhQ2a6Y6k6jxLzQKIeTukDfbFF9ec1Y9bfPASLOx5msfvqDklJnEBukrXjbKBsUvghHIDAQoQHqe0oiAZ0dVafJLmiWXYkWM0Pq86Gt7tHRZAalne41rLK5NtawsT3B9jBtRGRDjM8lvgAQO4PGoKIfuUb3pQBh9e2tzKUcMHpTPzRNCSligFccI0yBZciIDgxk7UutMKYjDmeZqsWE0MLkx9WGwrFej6g0CMxuqAWCrVqcBRh8JC8ZoD8r5XlgE262q6FWTxZx6nc5XyaCmvU1sWMF6PKTvqankMHLheN7YwxRbWbuat8Okj2RL4xGD3cjwdiOMJHjHn9VMipY1wguVoek40kbTZc6Cvzefthr9mcrHdrVDEqaRXwvHjHfv0H4FJ9sEf0EAn6MYcgDZh1mlP7t3EyHOzFuWTBLZYKko3I5EsIlRMLoQ1OBnnW3KvDhSqx0MOMTxEfqvAJfmNxGpUJ2uB8oGkGMLl8PXhUTyBXS2UofBMgPlk3m9Bt4IJYiSlmmNu8Jyofe6sc0fJ3eN5syHwqvkkWLLGorQ3ptxnNMpQjIxpAXcjwSCniPaubJxnX4cfWsdBqSi7ogvzCxdgsqlVhnkfGiexy77JPgw8k3j50CG6mH5Dfn8GQLlo1m7jotuLlk9D60iMdun68BsC2PXzwBV0JBB9TY7FiwMBrEusogVXslC5cPEECHKo02134y8lRjkih8xV9M0Ya8lAuBrhNJaY6zdWcnHvMKBIHfJl002cJHbXn34dxXXhBDWdkqsGyo7ySEBrD1UY1Wh9G7xJXsBCpu96fnlXtRe89RO2WfaAECWVlvF4uXLlofOrtZ6KuZFSzhWZzuzCldAtUvQuhNnlzi6Iih74ya6PbS7YCtLiazk90k5zFkvfR8Rlc0OlTOPsO05GVbbgVNSU6pPkP9DnaH4T64cAwXEsZNNxhyG3LVbklFKN3zKJlBALjpOpFf3EJ6gXlZ5ydzj2q2Q8fr4LL2aIZeuBzXY1NDHd31ERzmp6wvrwjJ8DbykNJRlYDntRCUnynV4W262BNsdibFkQ3CMbXd8YcBvZvh9j9zLUyRaEcNwpQflIY5Umuis6a4q0zGf3Gm4Wkgq6tpFYvi5BDPw0v8niX7b6DkVbEyVyoyIqKrrlhssBQLwChw68BZrDp9uc200r8YENA9GGwhAeZLGppeCvADry6jncIKoqm8JVymW9pArGmXCGA8AOYKiCTZo1G2hJL6HhVHJ5th6njIsEdULdWXr126w6oHrEU6M1nmZuJFuBjEVhSOLwOwFYuOJGZHay9oXrbsgtWFU8kRY7M3mlZEPp8rD5JGDcIJbFarzk2X1yfiY9wCZ4wxdfpu5OkKbyfSkpCxlslyjyRhZ7Hhwum1aOAU9ZibXX18qmRjMIGOBZuNfaz8922HwQXPO4pN6hoGMnwAFjWjLmSHHcZjS7j9L7BwjyuqLL1ewZ40WQjLLxCwqAC6D64Ev3qm8GN9K2u3vTgCtrLMJLV3hnMGA0mYxONlPpTsZsaggYIs9kZe0xRQRWQ8UTSJTgTDuDXd36w7kXSwxv3pvHKbWV5Vl5bNrOhgO4iSPCebbrrwgUCTmHTaLuy4ZctPIB8ONvrMHWKN8LgobrZukekfxpKziHXmCIJh3SK1JTxFarLTsonl06TMQ3avAUtVGtMv9d6IaFr36dlhQf5oDu90KiTZn1Ny9ahOM5XDo7viRFEQtBqZPjNrATjmiZHF1Gd6KqMzFBzMsSI44RYomwC3Rd3DwltsFmRPaWSLF3pCCj4IvkWvxqmdcxJc7pGEiqENvbW5xEXYB6Jwea69CBzIHwRFKq5C6Z3IBDNSJyyNY4YudUwIoADzx0yBa6YEbtjciz7Z2WuuqW9jqE7WsCuI0jPSr7C9YtLiqSvxrRMDFmRg1D1RWnVOXXfCYKcitoAdzMiQbj8WdcQJHzDxAZL8UlazLfrdldylnqU7l5ERV4XWICxFgBjQt9DJI7NdMyZk2R6shmPnhtfOyrlTMf7bhXKjBqauspSSJ24n6Cx9JLtHMtrNHmz4ymhxWst59IYRnsJVt8Bh3kRpQ7rC5WAQldyJeaouKOgIzgR0Y36H0ZDjrDwrXxITz63PrRHs1yL8H5QUHQ2Fy517m8aE6aAIr8F9vVo6nJKPxd4d2hw7l6ckHXFuTz69SulEzTP2Xj7pbUaGOUId2cBH7BUIOJ4KxsQLsZBoBHWaamU0USuDahY8Rn5DIpybE1VNvgR4Wb5yftSrZ2quEJ78W45y9obVeBAFBpfcrW1SrxlEDMa4ucyRO7Z4B9asiRogibKoSJyGeUSolrb3NLRqvBoDUye8NDVb7Bj5sg8gNPYDIhcQdulaulsntoxBFHHfOtUzBrglLii1z0k3JpMxqjZCm6qe7Itb1VtO86tNVP9joBvcOEvalVtfVognqaWSTBmch0PNNUhKOT1uRJY2sA0wDLE84i0B9WDhEQGXt7adwX6exToc3owvUgrcWLNakbHZ2mPq4RSxV32SzpH0mPA6WjYb0a9UHhpqGJhbzOYScg5w7po98iCRyCJEjA7CCNcDmmweAOwhHvr2qIWWcs0JVO7PLvQtXJf8HAlPm98UpmVB0qOBkhEiZ84IwNnP8IX3uM938ksbcw3yxFmpHyz7nTXDHeov9XtNa40epXxEJGaFzNzEd22AztEiumGkDJia9Ft3NwBO9CzYDrPdV7oFjvvaHD1T74F1S5a6tHLUX14CsdFuhITjAH6tUpCiBx95imgtrBUIxGhHlnXQ5pr8kGUvSFvo1jVyDQm7fju3bFX1f6I67e3EOvcFEh0JKNP5TYNYcC3dyzTDmOjtYeiR4lU2dMGN2ibsnEaG1Vj3fHIbXI9NQ1o08vWXB2ZEljBItTOCv5AqQ9xeCZSmQJG8hQZgs0RIgU2hbhbx1smwdRWhp7DQrdtOziq2wSoMlwx55Mx2oGKTZsbPvDh2foJCF5zwVJ9RXYcQcd8Ty2hazRdd1iRb63ncL6n87JY1pDX8yuO7RSLigGCugMd7dFsbRVIKflplMvtdcqcx8tQIdbk5MfAsKcvdpCZwuNxg800jAfDOdQ3PCk9ZvPAKtke1e8LJ0PsJmpky6U6jK5crP8mftLOvPuEdYOWGoRR9A7r3byz4kLO6OdHMpeXN8mkuNj08BVMPsJ6fb7RnPy0TWyWnwHXIxToVYkW6grX6OnVMO8HVfmIlJ6sb8MYoVm5IdcjYgvwvnkm61Hggcha0vOuXwKJPw5OMXktK83x80WIUVrX9XRJtG4tYH5qpi5eI6WdodOadhEqQSFAwG00hMpuBIV2aX7tbb8cu1MNO6qXpDdgjCSxtAtQq755yop8mjMsXFq5fDzyv0VDxZImXfBdJrqK8qVwhsFKFwlN8TsO1UKfkk8UIdD45PL7iu3VubtHGZMOHb51rCdqNEVERp80Li5gbT90inr7onh9taK7eUmBZAD4ydST0BaeKs1YkVBq7xSXGIzp5hXkxWGWbvXhYpNn9tjjcgHzCaaNG18zBRiEQjqanNQmTJ4K53nlqTP05dOAIYHjVYgStAVOHbhdaRIVQLxpLKt7v9ukoLYuARlOIaOgK9v6OomcXl6tvSUTO4ssNopYYwUQNTPik7RlUM3QpFmKJvGeBZarpQvLSeny725wS0Fnaag2hTrkVKwag7GVMQ5bP4LTL62aegWQsoNGcw430faFEbgLY7SMrnUkBpIqrm8qlQ4BR0mpyilsSr3fO31BflaodETqoeilfnkEceawvpukzIU2G1TzaJpna4f2kjj6JsKNwJ4gwxCZG12evD3kctWFls4w8GDXdF22a3ojpgqr5275efFX6O9FhuihHZhXMe2NA6vxy044BPQfQFNQqngsaSl1lMBdRqeqN9yCEs3tgnyVlyEOvPEsGGRruaR7T8VAd3F3Xt6u1duqcuvJHDfqMVoyXNBT8LaviawK31H5fLeQES5g3v8NZ6IAaQKP0pL7ajdkaW4wxiopoDp7HAop9fvpvR7SLPLE6Y9DckiIjtGAzTFgvMqBAbg4kFspl046dHxEKxfkg2PGKWZ55YyI5DxVFu0b9h8hbptwtSRrHoFi7mXI5bMuhIRmbRAKImM7pYGRWehcCXT3XTO334wHfDdARtnDQ4MmLFawiE8hBhufhREszsRW27gFt8orMg9YtIPyCelp5BOrdQ4JRDyRDPWXKaNoRt7f7E4bilPnUmPWYyIKnL78SCugJn6oihD3HyyN7yeIYmArFymmq4b8EXayz1z5232ttsgii2YOmborFXACAzHgV4hBin1YdME44ya0y2PLMHHTKtSBzjsT2zEpCT9xoB7TehbqKm5zkHFToGztjNTBqi84bNnGFS6j5bioVEruJ9adyBzUbhr6R6cQU4BLOVnlJYBVQRkGZASn9RDQUxsl3AdKo7biOg72tjXuY6km8Mo9bNdMIAeXYGxQGCBj55kMgagudSReEjlwYagIvkNl7QogH1TRGBcd0zWtvZACHa5AN3HTcXOCNqzGwFGN62ihGhhwHRmx38CvJCnRU53SuDvAp4iCdjfwAhWTKnI1WHru8AyLEiEDaXkD2Y2qHyNuukSnPmE1myoEOrarNnSV7OWqYqtD40gLfAfzUgwlMomYaIwyZv0UF5YboqhPY73OZt2tYBMVbtDesdUzHvO0SRLiZNAo5NSDmccexjIm0Lykr6l7M93r2j366WyBX9i5IsViKNozjnDupd4DBVnQSVpf807F1wKehl2h9KTeNUVR2htIwQqgHptYFo7fxEODUW6oIlU42YPFtADaofcUhbMHoZX3GnbZ1HeWM0ELNHHcFWhLHzdj99BP9kMxI22bdNEIevKLA3foCxJRO8IthL7tWPhCBlzkCxolZW8MrpEpyJ5fjuq93obCIfamUR2LGsBZlbEn67t7TLnsVbCxJSPCj4RwfR1srLISo3sNQkmWDjwBgmXUPbcdDRLHXgBkYXaXfT22urd2ODBlcnNfirlTIfzD5cMRbQR34X59yEMIUZf6Lcn7zG9ZR9sbSLQIpqP4L540QrOLTeb5RjFn2DnWQtjDB7XheXZdOp4ijFn2aEwmK5dW1QvQmQYd2Ni4Ysim5T4wLuzA6cNmQO5pGJBZXfHiAEmbARU5inGOpAoQfkhPBwzfoyz4qtFzYblra4eV8moi2h9QWilIeCdc2aCW8hVr374lxKMEcBdJmHPRbsJlVdw9BVrTyOHuoD2ghGRA9TngfWrOHNbjzTEMFPA3EQmsYMESN4QbFcJa9LxDS856miOus9gadrSVGFMfZVzVAfZUqGSswIqBalldSY6BGibZ1v4m7Qtmu3dYhaue2ityEEm7Gk8M4lbCCUpTiDuh6XRSvRuMYjDO3oXr82GTfvd2LsQHBb31uoAanqOylLnZxHkXVACIAYxB8w6wHZu1jH8yKchUlrsGkNkCrDHERuXirRRX3eSWcJhXmeZvck41eN3TDltQmDm5oUBfLcIGsXcHMXrz60O892oUXoyqIPGfaiCapoiAnJptk8dr3ZcV2NXe0BiS0gCXeB8QJu8CuR5asAghIo632PHj53TM1eVe3ogWyBAIMWQtE56qLGuuX58XERzNA77XOMOWxMMaCmJTEE0xYQeVtIfHbb1gE5qDbgyX9Yz7lIijsIVGnVUPuhXWlwRD2hXfe0AYZU6j5TPrj8gTq7epbX1xlsPiHje2uZLvPg14e3uKqRS1X9hCF565T2PsIEBF4DdfZk2I3uo2Dn2oxqgrpNTmbCkcvzirKyoouCCcm7xR9vSurHcaZ7anmjcshgrkPiGfE8nNf7J4Tx52BOtzXP046E0uvMqpcuuLCfSPuBU19PdvEFO2gaai4iBK2G2tbV4DGZaYQZRkyHi7mpBlaytnKx3XtgeNAMhxyf3V6O5NMYT5RFYFKAcKp8Do1h29BYzDklaVljWxvI92hhKzrQMxKTbgSSz9vElPmUDd59Fxzyn0TuXASzCVXx6Buf1RTEWw4gLN4ENUuDz8DG8x6Qj7qhNGmw371MqFezeFMUctI9c9ItsFwjepNQ03m8fjR1GO78H2nOqI1aXDPERSOLU2adG45NE3LZHbv5KaoIwBjXJmnMFMHEnWA6N48tG1j5rnSDiK8dIKjHxXDkNCatAf5VJc9swmX6bzDcwJEwTaQwNiwJTH7rVAneOQ5KABRmAaM159ROfj9Jubkv9H6NZhnT2wXE293vo9LExm6sFxnnmV2luSYA9F8d7VrC5PhUQwxChQW796AcyvZZYWboSihguyYuOqwdJh48IQCGUULLDYTiagI7r1qgdsVTpexx6Lx9IzSuTjASHtA5waNIuZ9Q69t7ZR2WADBa23JKlFap0nC96YwBbrgp7Cwyvdz4i61ESgm24clFxVTqoUAqpvUugXN8KGXzcNaoR0lgnZA6JQSLJUMiVkY6o7upOuEMfqxhCh7bQJo3mqZ8mjg3KR9iXIXAQQqwyn7l8v5eidsXBTWRLCYwmIUYEbAAOa77rfD6GY0yl5FidpdvyCHfR9R9MVZCD7UnQ5y77lZCjZxxxWsbwvxdjHPZ6h3xSQR0zTID51gh8X85dwnyoeKAXhrqpM9E9hcBnLQQnIgrCcZs3NPYlZbbFbY2ZixI1Z4gLTnB73DKdhiukVS2PIBp1nCEO95Mr6QFnCbksosDvNmr971kxdSjFWcB9U2P9aOt7fP94O1ymUV52XevZ7f1GzjakvtmCGOLv3DjkFnqINXpGjiYxAs3Za9pHqMoKR9rmgxijAcw36QybLLqHItUSauKQ5NNL6Vj5sVJItTzGk05A3CVjeR9ea0DqvW3yhJfP16Oi6iyKCU8yRcSFcdKxUyPFkFw4PQ7PeV77EtShikHgVrTTFoCz5ZXh5svFa4e3GyhkabKz5bdBIozMT68nMWrBWaTjNp2kTfgjwKMxxkcfXUo8QCv3xwllAg82A1lnSMKh3bHvyrewuDie48B8jPnMSkhKFnW4Prp7PLhQVDHuYuaRWMU6XXGqU8NXleriuPlfp6khSVjupDYqRH2yyyWjFfiLruRJQY4E2dKmJTjBgBLnxNYUYhphKJ5yHbpYoHh2iO3tOLvw37G2Srp9tv6x3kwfMX4JdYOMnfEdwqHQA1Bf32qDRfgCWYtHs3DVWXxSRepUJYt8qBo0Q1b1AX5YqTx0MKFFKKDzk12uo99J1wKRbGmQp7Z5wjzEQ3k8nQE06FOpL80OCVOT1IZOgfhzJ2FXBNCRLePD9FfkcVpy8tCEVQd1ga8lw36dI7ZRCUmcBQ7EfkUkSKoWBtRaoJTrRUIKXFnYix068jFnMXGonDQBDFXGTD9z2ugdP8cZ5TNeaMyXpRNF40AbmqpfElXdY7kjgV1DAMK0MiGcOT9YlJk6fmuqgLmbG2LX5dt14CehDga4kIMeZUIPKV2EccnsA1ZszbddU6jf7MiCJt2nOOZJd7kUI9pJoouzs2inSXQHQGawJzvhLzaLp8Xt3l7IutcyJJyVt6VvHnlLU6dmnGnHUzwBYOqAbI6yhSRIOUWTMCjvGPxWeq6Xsx0ItO719dtu4yELM0NsH9aE2Jjt1L758PyM4s93JIs4CsfVzWBbdPL0qpwEfewncZzs5yOjl0OmvANEdVwbf9I3t3E6nkt0qMXmZdMn5spFp7eQEk1jKLMhdYlYdHTUvEJza4yspAQLtuuQ7esXnO2sfGc5frX5b2k9uxUx54oaKjZrSRjKZnReyShJ5bw09ZSG072Kw5VZWDfKSqbQlYv0B1t4jRzZ4la5fnxg3kuwlWWKIzrn3EEOItrdYRT3OzqKyZve3c3EPvLZc2LvbWfPOEtXMiw4p6i0ahheT0ogRvsWJUQdXJI2mFlRo8ecG8f1rQU70Ux8G0XucljRUXMHaHqtsjQT9QMC6RbO7wdIMFmgu4dgFQ99hu0WROjz2N27wEM9HWcAqEJnflHvVznHp3IxSwTTmULO4ZJlV4JCuDwGIAgc87WqULd9opWAGwfTYiLPZATb84XjDhKAxeOIMEF3JwkLBZyhv8puqFqvDy8JfnDyiKzH1c7BxdyxzGI5laJRRr7vF8dFHUzs009lrbhILJxbPGpkbqbmKKJdfCk0BgaTd7z5tdEzkrk4fYecVpZk2KCoxfRVsbTkrdHbw37Zu66cphYNgGSfDrF5lVGy3DdtJ9XLmo4ApiY6NQOWIaGlGsrOJTjpSK5aDyPEbkfrdBjvXeUHCIHbNytYN9bUD7rnN8TCTddzaatd09O1sHjhiTIl9vPTplwwHtTqlQzogP1aXZQQwyJBhnwZyQzi7Gkve7R9Z3epQR8keVaN68dc7QSSRcHu6jhcq9P0zIoopLM8oosfuKlVjr8ZCagOJZCwptoR5vPqPQPJveTvSuV9MI0hX7ARHD4tddUEY6oIHPX0icXiG5CR149Dnt87UqYYfgs87ZKI11MmlBTTvAWqerhgjU6uoD2fJSYD2hR0F6ucXV8ibfpw4vKBds4cEB3gv7SNhjd8SYdb6mmVnftcGWUFq6QxuLSrTwiLaUkIiBjKU2YADlx5Jvek5MXlOdbiUlMTbmlt6fvQcQ2jUWiQUMgjtY6mpfbUbWx9iEIDxst1eOablStxBCCu0VnvWXCMXpkidMrnAhQMt3NKCFVLPz3P1fx338GhEu03UL5mToRqjYXkYclgYt849GmiHTSl9F8RVa7npJDzDPcP25U6z9dqcKHB0vEVq1PCryHyoRhYz5NcCvD2OGxv1B9mIzlAX3GUddepaCqbHK4HksKT5spKNEXtcBCssMXJ0fV5wwUkc4uAdVv3UfQYB36OikDygnS5vvF2Pa7cfnSTB6c37TQI2wit0JkGmjirH58umVCXAIlGpXLyWrt3ReXZ1HoDGMrqhOX14l0eT63FWE365fQjVRRwUxxH7jNnjW1UMo0FulZISngyRsglf0ib1uLI3xXrXRdcogn55liNFZlhAC12vsAgkMgfpjL3vH0BwFBnHpRnZRGmneGZxWbGaqNe3QmFiyDxMloWxiGFYdKDVgj0nHH3FnralCiLwKS0RTmEn39ZQywO50X4HZQXLLpR0mYYPNuTVVfocOtIuQ2fNmnmZIT6EISA1aGoQapZruBy01vYZH1OCp82sb5y8kJzNVBc7Oa0EPbWgtGoiUkGWReUzPP3QhHXpVqMitoAZIWJu62TVXkYOonSauefkSdvpakzg0ZiyUEYXkZZH7CJGyMUaYpqAKwDT6IsCSmqp3jgYZqDEcv1hznKnAZqpp9xgSEPFUtVvfKoJRIOOA0HEhqopp2Qbp1xa67PDEFYJiKuseXkxKSABqo7BVkBbre3VoIJbv0eavON78FKWtDgYHG4D7XwrN42Qy6E9wPSh9zWancPWHnCC8267JFf0ylLGyVpxyfKZgVUPtPBNGlGGmEp3Gt8nHEpP4kobVU8hOHn1Az5OvXmmBr4zGLDC9uYoi92u2QKcttndhppHHu4AD1BXugHCrfhg9kuF8zjGNXnSIWsOBM7XhyIQ1gg2J2ZRRgpIr3b2SGC5LxHSHnHsiWN1GUC6nEiT8IGufZDWSErSc8lAUJEYAJ10Ak9REfu164e0uKZSOAazYWSu5eJnWPkFhCvplR10PJo3P4BNo99oHeB4ZtwUug5i1JQreU7bXA3Y5HnheyJspscoIYsWPVD4BInqOrdKLu4YC25cITKNDMDwi75qbsSW8kK6n4FZDPF5SrKRKaLenf4OZhjfl9faYxHp4HX7kjaMseh621txL4DyXQ8DdoJIA6fZvhsHfiKtdtlUvrVCFqYEGj22xoq8MsM10aG8ydE6jmUJyNrPbMm3ZPi8pipfe0ba6bNN79mDnhUkrhYQe3E3Vi0yhWZ49Ef5maB0d30Elbh2IbkPUHZK9f6nefeK1mi19f20hKJVC9IUGjDtSqKPRC4XNsv3bdCg7LJFFNir2FVVgxNaehcHbmLpgdNHzhtcAVnsl5TLUK5XwecUSIOc4voDm0KVRltuCJEd6YZ4bCIBOyPTZDQPAVUcVwnYhPoxwepzrgF2Tdda6CuwTOqGd806Iv60zayQhZnXJrHoqfvJLZnQBnNS1hLH4XigXW0kAlxu5lGgu6dJZ6pX2g3cWR1ScngFrZHy6KenmNvpqpV7iqx3WrpOnU956J6mdqd1GvapZnfJoS8iQg7ANF5oYf5BOR5HwR7Fcoz91BBCyXvTwBwl6xlhxgV9gHAXQQFuy6nfukcaOoaV0w1FuCBC06TO2ml0hTD1Mzy4UnFl2vf8vnjFfXSteSbPHlPIPqJCqbOtD96qScISzCrkelAHQaBebyPU4cVdM5pL3e0xpCUNEabY6Wvh8DA5GkXj6USqzFLrLc4iPb7l4DUV6CHdpsOnkv5Gr0GDbbrYkHJIqXzuJIl37HNWKJPReCSbjSU0kJlLgSWa8JTE1063gz8lc1YFTi7iieEp3Ri91QZqjFXWCoMOv9iPpqKih4RME5hWCV2vRJIer0T3cuffiCnb3CTTidHbmUFsKoO46EsvGmrwkIztcbI2FeZ4vzKX5roywGy1oSUxuZ0uKmWu3PXqpfymlm1ToSVvgF0XwXwiJCOvUuKM60Dca9grbqkZbePXoZl6dzXGIM6Fw2VwWf2XvSWNONsEKwIRDa32J1eJuGXkazMM2FsKSPpqsoE7z2LAJNdN2HTHtJKunjoDFdZAUKnQtQRika0pcETjVOpIQ4BbCWFprl8jM2mYd0bMWFIkEZcbyCOYOazPMKFJYaq8WXdW81kmPctyhUAyXC9XHVLyVI6M7yGNf7CC4xNTuhSSZj0u6POO8OZxx91ttNZJTPywhRhsPT2yJUUGs89yAkdtd2fS7APGVRSQAnXYySPrxgpDeNLASKwh8avGNNjlUYQ7kDDqGCOYaeT3PeeqalxgxCNSCBZQhhjgbvnzh8wGQ6aJOYMcFRD7ceJlZ7Mz6BE38ZXMeHDkiK3CtTyP0nOUr6JYykW1fHU78KUK9IVngrGbz8JBkafosfDM4X5XMYDdnKVhhd6mkzxz40HRd7jAvtginmh1zjDbQpv6LSRUb2y8IxZw4UPfS6uqIZ5KXBC3nhoQWBURrYUFYVqcvbGJ5lG3THg4W42jBNT1gUGvfstTNq2MR4UOBmGQqijmsDdIoKNigzXvO25rk8t9LySdIl8FbrELtO50veQgReCq0Like7EKJqGXQiPjkdx4PzQoKQl5HiwzvYCkAsuHr56e8jb0tl6aqO6aWLbZROvRW5Z8dRWjoy6F1R9tY7aVdi8doysKlfEdogKInwMe3FFX57M626AIS4DWOVKL1InIKobQWJbZ6JALX7dCqsg2cZWlNbfOwFwyr4tiHSYcjWqSrs4pU5oQI3eQh8nsElO2OlfOXEsMJxKTzU0bSLNcLMxGc9Gwf2gQfYLykCSX8Pyhiwk5OOnLIfAJD7hJLvpeU8UYnLUlAw4IuWOiyLEXxSQt09VPzt3PZYPD2wZfOMqVdM01ISpjbJu5pVYW5DjgPYLH268RHtxzEQAsmwRTImDSFdKAILbNnLeNg9oFrBM11FJDWgkzGENoKphwlLwr0tKYme0UKyfhCJhc1X6y6DSFg2BKrdHYpCshGSzBvi2QykqMF5Y5KHeH1MWBE6lXRSsfloCRUTUWVPCSOUlgtcRm8OCfvbJSzZjgZ5FAcvdirHtRznvdJ38CIsJsDTeKYxCMHJI6ZeXy4V33CzCgGsCEJuLE6aMQ9iMNiwlZHaEATKDP76HeBdu5fPcKSY9vajo3LQRRo0sMWr2noV4Y9Stj7n9k84thyj8jPLCIklTa5IPLLSSf1OTydMkvThxaQZDx3d1Ja0T4c0At9LfxLyTznraNZNupyzBUs1SFVuL9nPaqysodWeIyMiaaDMn9618Jlu5hq37DmedMbZvohsuzNFXlJ4kWbU5cT0HsBnvpQHg7y5MQlYHgU5r9bR62YML2jl9FoPsfNt0hZ8VTcKcSaLkTQLTFzmUylzgYoXodsBkhRFQVBzr2Bd2Tfpf7j5kR5cLJW7vvjuACzR4paCmM2F92UzlPrL0uWm8yzrVjpsbmMDH4XwdRY9qaQZrHQBayEskw9Rmtvi8e0yLXcg9kzRWOvWkuoYYyjqXFV50tNmPjNKFRXWWhuc7rurO8Afy4YONC4Wt7mBw2Zt1KoxX52TzWNin3MB9aBjiYY0quM78fhn6Rd1dSr3OT2oKXIG9JvMzdYrY08cXm1dA5b2VC4OlLp9EWvseFeVK2WeF24ejgEoBFy1vbg3lduLcQPVl7pdV1mqnb3I49aS8ZYm58EGPcAgpvzmuyUsYWpKaMs3ObXWVLfWJw3G6sZHGhwXJqIs92kcgKKlYmBQhkmu7bkbWRxMmE6nccYY6sZtobryQfST8G41wl0ZtEbe4kuhDPAHRxGbs2P3bCDFveOGW6sQ5qyLe0a8rudyOdZbj1FSkGMZyGj8rlQKk9qKgNBbn9SWdj3cLh5fEdt9v3BcVN7g2e9aTWhGPxzYARDFh9zNMxr5zMIFHvCPZJUZN3dITT9sNZxVYdpZ7BughLDCtjnww3ydQGC40iIFsBDPeawTIcyfuJcuOKD0s6SAuQXmnnGVh55MLtzopLQn4YDpDkJPZi4tyU23SPFmq1Tm4GuTXWSMAO36CYRskTQAYdxWIxr811WSxlPsXcNrNHyQ4606b7cUucp7K01dpOnOxqgpf0VbTG277WmKphHzw7dFzxkmtVPs5v4TKWQdmcTppBy764sJPRULVXZxpRadsMuo2ZwuMbc4QIt4GTzX1WUZ61DgOEwhsUmld0sqsZiDPBRrwZ46IaeflRnjMGWT3WFpw90Q7PmU4TxuLbBvnpC1TYEXrO3CXgYN0OjOTuKgdJbF7ZMNQJN0bLTAIESdYosjtngiWTGEFL6jwWEhT9AFhmFK8Yj5lJGuirxpFi80cYopavWp42Zh80jpfX9aSenuGVf7i9Nf7polQ9AF0yJcvcWaau5yOsnbS8WD9NJZajcqprKKiO0HRdfDTGbMBDhc9PyxGzHEocE55fdomjmpieDQmwxQQVY7r8holOvo1qMt1Twg8FpVLq4hX5HXX4Z0OThqTY9is1XdfnCGoV2Uz2cA4rOOOrzVUcfJQVCqOqRUPMbHfJXM2PUv7mRoAaWq0LwahZpacxWsYKuZKj5hMJpBENzGXWVcHmyDvHuTbgm4s22omMnCGXEhKEpWVpK3Eg9lqdQlOkdBG872TYqQDUHXt4KmUu1HDcC6k19AsoFiw1ObP1lgwS7iuHddQWCpWTF4d8bbBg4VV69cz6XD29RriynSmXQ3uNVc4RtMm3bamfO4Pfo81Ywcbd9lfRoN7iDfdUrdXZ0zcm4JG11bTM1WNVIbW7j1GKeYnEUSmZdYE4SsToBy8535KmwoMpse54FBZawa8tu5ciX8tUtMJcUL9FPNJ6IzyEHUZmmq7Dah9L176uCUnA3xbz5VRDAcrCrpEYA24jME5hqugsURLpu1wT6pFE10WU1BH94I9c8R0dBqZGBYQxKgMjieS7tCL9LtJ4Uqu5v06qZa51nBH3N3gKmcinJgJichKD7aWxci5OU90xZQAFccclY1MkrrP1PS4FIee2TM0NRVSpToZao6GfuQpSo2VJpAKAYjCAg36npGIaYE4fS8o1gIR9Mz0Rk4WKAOAPbmxqmiRkeRH8OjCL4CP1whmwLno89cL1nnZSWLVYOmpNKszC1S38selQmshPuDDkPoxKXRfyr9cAWG3JcqexmvpH51kyu3uybqylsTqDu9gq6rPdwZHFDNhFWeBSjD1v50k4CGw1gL0JKgLMC3D55m6UvHuDzaQr8S5J12K2lX46vampJOICqX1uxXKZtTTttWVvef3a3IqIRKUlJ7Z2i7ZkDrHPrxjdLGi8TJfEoYSFbprZehEjCvtM4MsVoPUG251hDNvOMNo2plXMBoqp4NtD5l8N5HspELj4HkpbllTwBASbdTLWze41PgMR3YbRcMtYBFcUtrGTgTuHadRiFc4WFq8CX5VazKZsIVPwqQ1oGbE7h30y3qaVm5og3PmP4CPthzEaNCjkz0J69HZMD8hoqsSxjIn1hsVPc7BdLqmbonR9DNG47jZI0ORtX1hoLt1cvKH0a2kX2sz4tAM0PH9ANjdAc0yFdMPK41TVavnb7CAXAyXFkhscBwU2ildTZVl5p4fIehxnZApT2GEyU4WRyhl2HDrTIqRH4hQgsQD7nkiaOWV94aHLb32B2uHGqFQmzjLVQ5bNYXSStfFja0aXPYsdd8bvK1DjBqzCiOmwKMvpPPMdz3iijZAXaoBSFQPf2xgHpCLfhXyUr46TySgwqMmSm8Ihlwnxrh9T6ywh3EXFH5i"), + new _Row(27942047, new Guid("08E06BB1-757A-4D8B-9768-4966D16F9173"), "ho3Pv3aLmCusWltGlgEAbwwpH", "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"), + new _Row(27942040, new Guid("08E06BB1-757A-4D8B-9768-4966D16F9173"), "uARU2FodUEIb3ixMr401zs5gGq3", "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"), + new _Row(27942032, new Guid("08E06BB1-757A-4D8B-9768-4966D16F9173"), "UTaQip0jFLV", "6MF"), + new _Row(27942034, new Guid("08E06BB1-757A-4D8B-9768-4966D16F9173"), "5b8zlELFQw1rW", "7Y9"), + new _Row(27942043, new Guid("08E06BB1-757A-4D8B-9768-4966D16F9173"), "IriW7xhF2aor", "lvxx7PedzFWUBi"), + new _Row(27942044, new Guid("08E06BB1-757A-4D8B-9768-4966D16F9173"), "u8JMj7ChBMeJnN98eOt", "ITrPShAjSMoCTMfMj"), + new _Row(27942041, new Guid("08E06BB1-757A-4D8B-9768-4966D16F9173"), "ugXGwPnQblwxd", "tiddVfujPuz0HKaTXQLMe"), + new _Row(27942038, new Guid("08E06BB1-757A-4D8B-9768-4966D16F9173"), "Kxl8aC5uKR0zVJl", "QPZzAKNQjt2Dm6glZCdxvGBzpVPi9BMAVv4e"), + new _Row(27942039, new Guid("08E06BB1-757A-4D8B-9768-4966D16F9173"), "3B5ikbV50KkZ5Ffs", "aRhr"), + new _Row(27942046, new Guid("08E06BB1-757A-4D8B-9768-4966D16F9173"), "Nw6iJxi2wWKA8twN2wO", "7RY0"), + new _Row(27942036, new Guid("08E06BB1-757A-4D8B-9768-4966D16F9173"), "Fi4DiLijhnY", "mNhLjRF"), + new _Row(27942035, new Guid("08E06BB1-757A-4D8B-9768-4966D16F9173"), "uuzw6oLPddS", "0ADpjuhWDHJj"), + new _Row(27942033, new Guid("08E06BB1-757A-4D8B-9768-4966D16F9173"), "rLxgGM6", "JHMtkwRTVoTebnaPLUUY"), + new _Row(27942042, new Guid("08E06BB1-757A-4D8B-9768-4966D16F9173"), "U4crJOexXz6N0dx", "ZKnTZYlikloyaQiVWvYlbkp"), + new _Row(27942028, new Guid("08E06BB1-757A-4D8B-9768-4966D16F9173"), "j4Ipsks", null), + new _Row(27942031, new Guid("08E06BB1-757A-4D8B-9768-4966D16F9173"), "PU6Yf", "UxStUM"), + new _Row(27942030, new Guid("08E06BB1-757A-4D8B-9768-4966D16F9173"), "v1wYY", "6HQA0"), + new _Row(27942029, new Guid("08E06BB1-757A-4D8B-9768-4966D16F9173"), "t2xP2OnIo", "xl8Kc65mfbv4PX"), + new _Row(27942045, new Guid("08E06BB1-757A-4D8B-9768-4966D16F9173"), "IhJwQDjAVByAKvnYVJd1fseUvD", "wZBP5xrHTmSd4zF8vfVXfw5"), + new _Row(27939369, new Guid("E8F87ACF-C537-4347-9BC4-CDF125E759AC"), "5FEOfqCx1nSxMubDot", "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"), + new _Row(27939379, new Guid("E8F87ACF-C537-4347-9BC4-CDF125E759AC"), "Eqc2KebZ3jFKtPXOCZtiNMJMq", "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"), + new _Row(27939372, new Guid("E8F87ACF-C537-4347-9BC4-CDF125E759AC"), "h9zfijozkzKtEN0oIWWKtg1d37r", "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"), + new _Row(27939364, new Guid("E8F87ACF-C537-4347-9BC4-CDF125E759AC"), "pl4SEzxt5aj", "y2i"), + new _Row(27939366, new Guid("E8F87ACF-C537-4347-9BC4-CDF125E759AC"), "pHu8rACwoAeAR", "6P5"), + new _Row(27939375, new Guid("E8F87ACF-C537-4347-9BC4-CDF125E759AC"), "I5QPIDh0pOQq", "TyN7SRqp1e9xhN"), + new _Row(27939376, new Guid("E8F87ACF-C537-4347-9BC4-CDF125E759AC"), "tIs1cRUF6EQXIZLH5qL", "WdgCal1kzJsBfiLYg"), + new _Row(27939373, new Guid("E8F87ACF-C537-4347-9BC4-CDF125E759AC"), "7AAFQU8b5xQjz", "48YSumTpuwo90lHwHmcuW"), + new _Row(27939370, new Guid("E8F87ACF-C537-4347-9BC4-CDF125E759AC"), "bUkjxvr2Ij6Cb7I", "bQ4bNrK4yQHdJ9WTB5O8JxitdW24vEoHFhii"), + new _Row(27939371, new Guid("E8F87ACF-C537-4347-9BC4-CDF125E759AC"), "1aUWwkCStvQ0aa9H", "PKjk"), + new _Row(27939378, new Guid("E8F87ACF-C537-4347-9BC4-CDF125E759AC"), "JHNBoMSpcgbYBwqCTja", "0pUE"), + new _Row(27939368, new Guid("E8F87ACF-C537-4347-9BC4-CDF125E759AC"), "VETrliW5qrS", "W7VBDVZ"), + new _Row(27939367, new Guid("E8F87ACF-C537-4347-9BC4-CDF125E759AC"), "DZpLEj7a7x8", "fske7dTzHB0V"), + new _Row(27939365, new Guid("E8F87ACF-C537-4347-9BC4-CDF125E759AC"), "BFiGUeE", "6FfoJN00SRpBwaeJMzka"), + new _Row(27939374, new Guid("E8F87ACF-C537-4347-9BC4-CDF125E759AC"), "0VOL3LAUw09go4n", "joEoLSBPGNTxv4JSAV7YqRo"), + new _Row(27939360, new Guid("E8F87ACF-C537-4347-9BC4-CDF125E759AC"), "YjKaV7r", null), + new _Row(27939363, new Guid("E8F87ACF-C537-4347-9BC4-CDF125E759AC"), "fujpL", "5d3m8k"), + new _Row(27939362, new Guid("E8F87ACF-C537-4347-9BC4-CDF125E759AC"), "0nZnP", "zCOIN"), + new _Row(27939361, new Guid("E8F87ACF-C537-4347-9BC4-CDF125E759AC"), "IImbQh9yt", "8fh5FpTwXX0CT"), + new _Row(27939377, new Guid("E8F87ACF-C537-4347-9BC4-CDF125E759AC"), "nte3hSSnA5qUDrTeBps0GajHFV", "b7yn1aDZ0PopOOwVSSFwEZQ"), + new _Row(27967700, new Guid("C214F6BB-53EB-4858-A944-D227F9A2846F"), "6PsGeEZLzUcnKGKL0O", "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"), + new _Row(27967710, new Guid("C214F6BB-53EB-4858-A944-D227F9A2846F"), "4UemQLTq1dAnEVC19A6BwVHaG", "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"), + new _Row(27967703, new Guid("C214F6BB-53EB-4858-A944-D227F9A2846F"), "PztxRBuJCcvIULKp6mUwHuyHQHH", "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"), + new _Row(27967695, new Guid("C214F6BB-53EB-4858-A944-D227F9A2846F"), "ZOySIGfxJTT", "VRY"), + new _Row(27967697, new Guid("C214F6BB-53EB-4858-A944-D227F9A2846F"), "Qx3mI5MQCPF9k", "xVy"), + new _Row(27967706, new Guid("C214F6BB-53EB-4858-A944-D227F9A2846F"), "01TPl2U9dzNa", "bwDlLckZEHqokP"), + new _Row(27967707, new Guid("C214F6BB-53EB-4858-A944-D227F9A2846F"), "jgp5sCGrgL08A5QGfM5", "OM1DT9iSMoVZRMIGu"), + new _Row(27967704, new Guid("C214F6BB-53EB-4858-A944-D227F9A2846F"), "ffXeEmkW5ZiRP", "Hr9e5U4qbWHpPxNljTYUz"), + new _Row(27967701, new Guid("C214F6BB-53EB-4858-A944-D227F9A2846F"), "D83pFinFz60LiQd", "z99W2HKNyBShCLiqWrl3oKiOYmhK6TlvlxSC"), + new _Row(27967702, new Guid("C214F6BB-53EB-4858-A944-D227F9A2846F"), "h8kGa6byq7u6xwwH", "dFXM"), + new _Row(27967709, new Guid("C214F6BB-53EB-4858-A944-D227F9A2846F"), "dQTHfJT3W0ZoVvphWcL", "i3sH"), + new _Row(27967699, new Guid("C214F6BB-53EB-4858-A944-D227F9A2846F"), "iNlhTgyYc4f", "BJkaAdh"), + new _Row(27967698, new Guid("C214F6BB-53EB-4858-A944-D227F9A2846F"), "jst3eaOfwwy", "z6wGAgKIcwOp"), + new _Row(27967696, new Guid("C214F6BB-53EB-4858-A944-D227F9A2846F"), "GE5DZYd", "NyFnZKT73M4QV5Ih3puI"), + new _Row(27967705, new Guid("C214F6BB-53EB-4858-A944-D227F9A2846F"), "jf2cClMdD5wiEli", "M9ovCXpQGbLw52b4jlVS9eK"), + new _Row(27967691, new Guid("C214F6BB-53EB-4858-A944-D227F9A2846F"), "tJXdP5Q", "boSRIr1E8Eqe8BCPCdYDi"), + new _Row(27967694, new Guid("C214F6BB-53EB-4858-A944-D227F9A2846F"), "kmuCP", "2VzY"), + new _Row(27967693, new Guid("C214F6BB-53EB-4858-A944-D227F9A2846F"), "cLc27", "ujAix"), + new _Row(27967692, new Guid("C214F6BB-53EB-4858-A944-D227F9A2846F"), "UUHWcCtTe", "WAqv8AbdDPoSlh"), + new _Row(27967708, new Guid("C214F6BB-53EB-4858-A944-D227F9A2846F"), "1hlglR0vEPdA3ZxqVI3E59targ", "9yl8QMmpLjCGS8F2ETvyCeo"), + }; + + using (var cn = new Microsoft.Data.SqlClient.SqlConnection(DataTestUtility.TCPConnectionString)) + { + await cn.OpenAsync(); + + string tableName = DataTestUtility.GenerateObjectName(); + + try + { + using (var cmd = cn.CreateCommand()) + { + cmd.CommandText = $""" + create table [{tableName}] + ( + [Id] [int] NOT NULL, + [DocumentIdentificationId] [uniqueidentifier] NOT NULL, + [Name] [nvarchar](40) NULL, + [Value] [nvarchar](max) NULL + ) + """; + + await cmd.ExecuteNonQueryAsync(); + + cmd.CommandText = $"INSERT INTO [{tableName}] VALUES (@id,@docId,@name,@value)"; + SqlParameter id = cmd.Parameters.AddWithValue("@id", 0); + SqlParameter docId = cmd.Parameters.AddWithValue("@docId", Guid.Empty); + SqlParameter name = cmd.Parameters.AddWithValue("@name", ""); + SqlParameter value = cmd.Parameters.AddWithValue("@value", ""); + + foreach (var row in rows) + { + id.Value = row.Id; + docId.Value = row.DocumentIdentificationId; + name.Value = row.Name; + value.Value = row.Value == null ? DBNull.Value : row.Value; + await cmd.ExecuteNonQueryAsync(); + } + } + + int counter = 0; + while (counter < 10) + { + using (var cmd = cn.CreateCommand()) + { + cmd.CommandText = $"SELECT [d].[Id], [d].[DocumentIdentificationId], [d].[Name], [d].[Value] FROM [{tableName}] AS [d]"; + using (var reader = await cmd.ExecuteReaderAsync()) + { + int row = 0; + while (await reader.ReadAsync()) + { + int id = await reader.GetFieldValueAsync(0, default); + Guid docId = await reader.GetFieldValueAsync(1, default); + string name = await reader.GetFieldValueAsync(2, default); + string value = null; + if (!await reader.IsDBNullAsync(3, default)) + { + value = await reader.GetFieldValueAsync(3, default); + } + + Assert.Equal(id, rows[row].Id); + Assert.Equal(docId, rows[row].DocumentIdentificationId); + Assert.Equal(name, rows[row].Name); + Assert.Equal(value, rows[row].Value); + row += 1; + } + } + } + counter++; + } + } + finally + { + try + { + DataTestUtility.DropTable(cn, tableName); + } + catch + { + } + } + } + } } } diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/DataStreamTest/DataStreamTest.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/DataStreamTest/DataStreamTest.cs index ee9c0ed4cb..506d8f81df 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/DataStreamTest/DataStreamTest.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/DataStreamTest/DataStreamTest.cs @@ -13,13 +13,21 @@ using System.Threading.Tasks; using System.Xml; using Xunit; +using Xunit.Abstractions; namespace Microsoft.Data.SqlClient.ManualTesting.Tests { - public static class DataStreamTest + public class DataStreamTest { + private readonly string _testName; + + public DataStreamTest(ITestOutputHelper outputHelper) + { + _testName = DataTestUtility.CurrentTestName(outputHelper); + } + [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup), nameof(DataTestUtility.IsNotAzureServer))] - public static void RunAllTestsForSingleServer_NP() + public void RunAllTestsForSingleServer_NP() { if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { @@ -33,7 +41,7 @@ public static void RunAllTestsForSingleServer_NP() [ActiveIssue("5540")] [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup))] - public static void RunAllTestsForSingleServer_TCP() + public void RunAllTestsForSingleServer_TCP() { RunAllTestsForSingleServer(DataTestUtility.TCPConnectionString); } @@ -50,7 +58,7 @@ public static async Task AsyncMultiPacketStreamRead() byte[] inputData = null; byte[] outputData = null; - string tableName = DataTestUtility.GetUniqueNameForSqlServer("data"); + string tableName = DataTestUtility.GetLongName("data"); using (SqlConnection connection = new(connectionString)) { @@ -152,7 +160,8 @@ IF OBJECT_ID('dbo.{tableName}', 'U') IS NOT NULL return data; } - private static void RunAllTestsForSingleServer(string connectionString, bool usingNamePipes = false) + // @TODO: Split into separate tests! + private void RunAllTestsForSingleServer(string connectionString, bool usingNamePipes = false) { RowBuffer(connectionString); InvalidRead(connectionString); @@ -546,7 +555,7 @@ private static void RowBuffer(string connectionString) private static void TimestampRead(string connectionString) { - string tempTable = DataTestUtility.GetUniqueNameForSqlServer("##Temp"); + string tempTable = DataTestUtility.GetLongName("##Temp"); tempTable = tempTable.Replace('-', '_'); using (SqlConnection conn = new SqlConnection(connectionString)) @@ -1041,7 +1050,7 @@ private static void SequentialAccess(string connectionString) private static void NumericRead(string connectionString) { - string tempTable = DataTestUtility.GetUniqueNameForSqlServer("##Temp"); + string tempTable = DataTestUtility.GetLongName("##Temp"); tempTable = tempTable.Replace('-', '_'); using (SqlConnection conn = new SqlConnection(connectionString)) @@ -1871,8 +1880,8 @@ private static void StreamingBlobDataTypes(string connectionString) private static void VariantCollationsTest(string connectionString) { - string dbName = DataTestUtility.GetUniqueName("JPN"); - string tableName = DataTestUtility.GetUniqueName("T"); + string dbName = DataTestUtility.GetShortName("JPN"); + string tableName = DataTestUtility.GetShortName("T"); using (SqlConnection connection = new SqlConnection(connectionString)) { @@ -1911,56 +1920,61 @@ private static void VariantCollationsTest(string connectionString) } } - private static void TestXEventsStreaming(string connectionString) + #nullable enable + + private void TestXEventsStreaming(string connectionString) { // Create XEvent - using (SqlConnection xEventManagementConnection = new SqlConnection(connectionString)) - using (DataTestUtility.XEventScope xEventScope = new DataTestUtility.XEventScope(xEventManagementConnection, - "ADD EVENT sqlserver.user_event(ACTION(package0.event_sequence))", - "ADD TARGET package0.ring_buffer")) + using SqlConnection xEventManagementConnection = new SqlConnection(connectionString); + xEventManagementConnection.Open(); + + using DataTestUtility.XEventScope xEventScope = + new DataTestUtility.XEventScope( + _testName, + xEventManagementConnection, + "ADD EVENT sqlserver.user_event(ACTION(package0.event_sequence))", + "ADD TARGET package0.ring_buffer"); + + string sessionName = xEventScope.SessionName; + + Task.Factory.StartNew(() => { - string sessionName = xEventScope.SessionName; + // Read XEvents + int streamXeventCount = 3; + using SqlConnection xEventsReadConnection = new SqlConnection(connectionString); + xEventsReadConnection.Open(); - Task.Factory.StartNew(() => - { - // Read XEvents - int streamXeventCount = 3; - using (SqlConnection xEventsReadConnection = new SqlConnection(connectionString)) - { - xEventsReadConnection.Open(); - string xEventDataStreamCommand = "USE master; " + @"select [type], [data] from sys.fn_MSxe_read_event_stream ('" + sessionName + "',0)"; - using (SqlCommand cmd = new SqlCommand(xEventDataStreamCommand, xEventsReadConnection)) - { - SqlDataReader reader = cmd.ExecuteReader(System.Data.CommandBehavior.SequentialAccess); - for (int i = 0; i < streamXeventCount && reader.Read(); i++) - { - int colType = reader.GetInt32(0); - int cb = (int)reader.GetBytes(1, 0, null, 0, 0); + string xEventDataStreamCommand = "USE master; " + @"select [type], [data] from sys.fn_MSxe_read_event_stream ('" + sessionName + "',0)"; + using SqlCommand cmd = new SqlCommand(xEventDataStreamCommand, xEventsReadConnection); + using SqlDataReader reader = cmd.ExecuteReader(System.Data.CommandBehavior.SequentialAccess); - byte[] bytes = new byte[cb]; - long read = reader.GetBytes(1, 0, bytes, 0, cb); + for (int i = 0; i < streamXeventCount && reader.Read(); i++) + { + int colType = reader.GetInt32(0); + int cb = (int)reader.GetBytes(1, 0, null, 0, 0); - // Don't send data on the first read because there is already data in the buffer. - // Don't send data on the last iteration. We will not be reading that data. - if (i == 0 || i == streamXeventCount - 1) - continue; + byte[] bytes = new byte[cb]; + long read = reader.GetBytes(1, 0, bytes, 0, cb); - using (SqlConnection xEventWriteConnection = new SqlConnection(connectionString)) - { - xEventWriteConnection.Open(); - string xEventWriteCommandText = @"exec sp_trace_generateevent 90, N'Test2'"; - using (SqlCommand xEventWriteCommand = new SqlCommand(xEventWriteCommandText, xEventWriteConnection)) - { - xEventWriteCommand.ExecuteNonQuery(); - } - } - } - } + // Don't send data on the first read because there is already data in the buffer. + // Don't send data on the last iteration. We will not be reading that data. + if (i == 0 || i == streamXeventCount - 1) + { + continue; } - }).Wait(10000); - } + + using SqlConnection xEventWriteConnection = new SqlConnection(connectionString); + xEventWriteConnection.Open(); + + string xEventWriteCommandText = @"exec sp_trace_generateevent 90, N'Test2'"; + using SqlCommand xEventWriteCommand = new SqlCommand(xEventWriteCommandText, xEventWriteConnection); + xEventWriteCommand.ExecuteNonQuery(); + } + }).Wait(10000); } + #nullable disable + private static void TimeoutDuringReadAsyncWithClosedReaderTest(string connectionString) { // Create the proxy diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ExceptionTest/ConnectionExceptionTest.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ExceptionTest/ConnectionExceptionTest.cs index 6ee0681a0d..c44ed97ed0 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ExceptionTest/ConnectionExceptionTest.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ExceptionTest/ConnectionExceptionTest.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information. using System; +using Microsoft.SqlServer.TDS.Servers; using Xunit; namespace Microsoft.Data.SqlClient.ManualTesting.Tests @@ -23,8 +24,14 @@ public class ConnectionExceptionTest [ConditionalFact(nameof(IsNotKerberos))] public void TestConnectionStateWithErrorClass20() { - using TestTdsServer server = TestTdsServer.StartTestServer(); - using SqlConnection conn = new(server.ConnectionString); + using TdsServer server = new TdsServer(); + server.Start(); + using SqlConnection conn = new( + new SqlConnectionStringBuilder + { + DataSource = $"localhost,{server.EndPoint.Port}", + Encrypt = SqlConnectionEncryptOption.Optional + }.ConnectionString); conn.Open(); SqlCommand cmd = conn.CreateCommand(); diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/InstanceNameTest/InstanceNameTest.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/InstanceNameTest/InstanceNameTest.cs index 24e5a277af..02133e0df4 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/InstanceNameTest/InstanceNameTest.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/InstanceNameTest/InstanceNameTest.cs @@ -212,9 +212,9 @@ private static string GetSPNInfo(string dataSource, string inInstanceName) string serverSPN = ""; MethodInfo getSqlServerSPNs = sniProxyObj.GetType().GetMethod("GetSqlServerSPNs", BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic, null, CallingConventions.Any, getSqlServerSPNsTypesArray, null); - string[] result = (string[])getSqlServerSPNs.Invoke(sniProxyObj, new object[] { dataSrcInfo, serverSPN }); + object resolvedSpns = getSqlServerSPNs.Invoke(sniProxyObj, new object[] { dataSrcInfo, serverSPN }); - string spnInfo = result[0]; + string spnInfo = (string)resolvedSpns.GetType().GetProperty("Primary", BindingFlags.Instance | BindingFlags.Public).GetValue(resolvedSpns); return spnInfo; } diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/JsonTest/JsonBulkCopyTest.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/JsonTest/JsonBulkCopyTest.cs index c92ba237b4..15f16b1f66 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/JsonTest/JsonBulkCopyTest.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/JsonTest/JsonBulkCopyTest.cs @@ -7,18 +7,17 @@ using Newtonsoft.Json; using Xunit.Abstractions; using Xunit; -using System.Collections; namespace Microsoft.Data.SqlClient.ManualTesting.Tests.SQL.JsonTest { public class JsonBulkCopyTest { private readonly ITestOutputHelper _output; - private static readonly string _generatedJsonFile = DataTestUtility.GenerateRandomCharacters("randomRecords"); - private static readonly string _outputFile = DataTestUtility.GenerateRandomCharacters("serverResults"); - private static readonly string _sourceTableName = DataTestUtility.GenerateObjectName(); - private static readonly string _destinationTableName = DataTestUtility.GenerateObjectName(); - + private static readonly string _generatedJsonFile = DataTestUtility.GetShortName("randomRecords"); + private static readonly string _outputFile = DataTestUtility.GetShortName("serverResults"); + private static readonly string _sourceTableName = DataTestUtility.GetShortName("jsonBulkCopySrcTable", true); + private static readonly string _destinationTableName = DataTestUtility.GetShortName("jsonBulkCopyDestTable", true); + public JsonBulkCopyTest(ITestOutputHelper output) { _output = output; @@ -26,10 +25,10 @@ public JsonBulkCopyTest(ITestOutputHelper output) public static IEnumerable JsonBulkCopyTestData() { - yield return new object[] { CommandBehavior.Default, false, 300, 100 }; - yield return new object[] { CommandBehavior.Default, true, 300, 100 }; - yield return new object[] { CommandBehavior.SequentialAccess, false, 300, 100 }; - yield return new object[] { CommandBehavior.SequentialAccess, true, 300, 100 }; + yield return new object[] { CommandBehavior.Default, false, 30, 10 }; + yield return new object[] { CommandBehavior.Default, true, 30, 10 }; + yield return new object[] { CommandBehavior.SequentialAccess, false, 30, 10 }; + yield return new object[] { CommandBehavior.SequentialAccess, true, 30, 10 }; } private void PopulateData(int noOfRecords, int rows) @@ -87,7 +86,7 @@ private void PrintJsonDataToFileAndCompare(SqlConnection connection) try { DeleteFile(_outputFile); - using (SqlCommand command = new SqlCommand("SELECT [data] FROM [" + _destinationTableName + "]", connection)) + using (SqlCommand command = new SqlCommand("SELECT [data] FROM " + _destinationTableName, connection)) { using (SqlDataReader reader = command.ExecuteReader(CommandBehavior.SequentialAccess)) { @@ -125,7 +124,7 @@ private async Task PrintJsonDataToFileAndCompareAsync(SqlConnection connection) try { DeleteFile(_outputFile); - using (SqlCommand command = new SqlCommand("SELECT [data] FROM [" + _destinationTableName + "]", connection)) + using (SqlCommand command = new SqlCommand("SELECT [data] FROM " + _destinationTableName, connection)) { using (SqlDataReader reader = await command.ExecuteReaderAsync(CommandBehavior.SequentialAccess)) { @@ -159,7 +158,7 @@ private async Task PrintJsonDataToFileAndCompareAsync(SqlConnection connection) private void StreamJsonFileToServer(SqlConnection connection) { - using (SqlCommand cmd = new SqlCommand("INSERT INTO [" + _sourceTableName + "] (data) VALUES (@jsondata)", connection)) + using (SqlCommand cmd = new SqlCommand("INSERT INTO " + _sourceTableName + " (data) VALUES (@jsondata)", connection)) { using (StreamReader jsonFile = File.OpenText(_generatedJsonFile)) { @@ -171,7 +170,7 @@ private void StreamJsonFileToServer(SqlConnection connection) private async Task StreamJsonFileToServerAsync(SqlConnection connection) { - using (SqlCommand cmd = new SqlCommand("INSERT INTO [" + _sourceTableName + "] (data) VALUES (@jsondata)", connection)) + using (SqlCommand cmd = new SqlCommand("INSERT INTO " + _sourceTableName + " (data) VALUES (@jsondata)", connection)) { using (StreamReader jsonFile = File.OpenText(_generatedJsonFile)) { @@ -265,7 +264,7 @@ private async Task BulkCopyDataAsync(CommandBehavior cb, bool enableStraming, in } } - [ConditionalTheory(typeof(DataTestUtility), nameof(DataTestUtility.IsJsonSupported))] + [ConditionalTheory(typeof(DataTestUtility), nameof(DataTestUtility.IsAzureServer))] [MemberData( nameof(JsonBulkCopyTestData) #if NETFRAMEWORK @@ -289,7 +288,7 @@ public void TestJsonBulkCopy(CommandBehavior cb, bool enableStraming, int jsonAr } } - [ConditionalTheory(typeof(DataTestUtility), nameof(DataTestUtility.IsJsonSupported))] + [ConditionalTheory(typeof(DataTestUtility), nameof(DataTestUtility.IsAzureServer))] [MemberData( nameof(JsonBulkCopyTestData) #if NETFRAMEWORK diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/JsonTest/JsonStreamTest.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/JsonTest/JsonStreamTest.cs index a82fee1665..ed36457200 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/JsonTest/JsonStreamTest.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/JsonTest/JsonStreamTest.cs @@ -19,11 +19,11 @@ public class JsonRecord public string Name { get; set; } } - public class JsonStreamTest + public class JsonStreamTest { private readonly ITestOutputHelper _output; - private static readonly string _jsonFile = "randomRecords.json"; - private static readonly string _outputFile = "serverRecords.json"; + private static readonly string _jsonFile = DataTestUtility.GetShortName("randomRecords") + ".json"; + private static readonly string _outputFile = DataTestUtility.GetShortName("serverRecords") + ".json"; public JsonStreamTest(ITestOutputHelper output) { @@ -49,7 +49,7 @@ private void GenerateJsonFile(int noOfRecords, string filename) string json = JsonConvert.SerializeObject(records, Formatting.Indented); File.WriteAllText(filename, json); Assert.True(File.Exists(filename)); - _output.WriteLine("Generated JSON file "+filename); + _output.WriteLine("Generated JSON file " + filename); } private void CompareJsonFiles() @@ -157,10 +157,10 @@ private void DeleteFile(string filename) } } - [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.IsJsonSupported))] + [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.IsAzureServer))] public void TestJsonStreaming() { - GenerateJsonFile(10000, _jsonFile); + GenerateJsonFile(1000, _jsonFile); using (SqlConnection connection = new SqlConnection(DataTestUtility.TCPConnectionString)) { connection.Open(); @@ -173,10 +173,10 @@ public void TestJsonStreaming() } } - [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.IsJsonSupported))] + [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.IsAzureServer))] public async Task TestJsonStreamingAsync() { - GenerateJsonFile(10000, _jsonFile); + GenerateJsonFile(1000, _jsonFile); using (SqlConnection connection = new SqlConnection(DataTestUtility.TCPConnectionString)) { await connection.OpenAsync(); @@ -190,4 +190,3 @@ public async Task TestJsonStreamingAsync() } } } - diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/JsonTest/JsonTest.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/JsonTest/JsonTest.cs index ccf0c00919..55dffae12d 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/JsonTest/JsonTest.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/JsonTest/JsonTest.cs @@ -22,7 +22,7 @@ public JsonTest(ITestOutputHelper output) { _output = output; } - + private static readonly string JsonDataString = "[{\"name\":\"Dave\",\"skills\":[\"Python\"]},{\"name\":\"Ron\",\"surname\":\"Peter\"}]"; private void ValidateRowsAffected(int rowsAffected) @@ -73,7 +73,7 @@ private void ValidateNullJson(SqlDataReader reader) } } - [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.IsJsonSupported))] + [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.IsAzureServer))] public void TestJsonWrite() { string tableName = DataTestUtility.GenerateObjectName(); @@ -137,7 +137,7 @@ public void TestJsonWrite() } } - [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.IsJsonSupported))] + [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.IsAzureServer))] public async Task TestJsonWriteAsync() { string tableName = DataTestUtility.GenerateObjectName(); @@ -201,7 +201,7 @@ public async Task TestJsonWriteAsync() } } - [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.IsJsonSupported))] + [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.IsAzureServer))] public void TestJsonRead() { string tableName = DataTestUtility.GenerateObjectName(); @@ -260,7 +260,7 @@ public void TestJsonRead() } } - [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.IsJsonSupported))] + [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.IsAzureServer))] public async Task TestJsonReadAsync() { string tableName = DataTestUtility.GenerateObjectName(); @@ -319,7 +319,7 @@ public async Task TestJsonReadAsync() } } - [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.IsJsonSupported))] + [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.IsAzureServer))] public void TestNullJson() { string tableName = DataTestUtility.GenerateObjectName(); @@ -350,7 +350,7 @@ public void TestNullJson() DataTestUtility.DropTable(connection, tableName); } - [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.IsJsonSupported))] + [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.IsAzureServer))] public void TestJsonAPIs() { string tableName = DataTestUtility.GenerateObjectName(); @@ -398,7 +398,7 @@ public void TestJsonAPIs() } } - [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.IsJsonSupported))] + [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.IsAzureServer))] public void TestJsonWithMARS() { string table1Name = DataTestUtility.GenerateObjectName(); @@ -454,7 +454,7 @@ public void TestJsonWithMARS() } } - [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.IsJsonSupported))] + [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.IsAzureServer))] public void TestJsonSPParams() { string tableName = DataTestUtility.GenerateObjectName(); diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/MirroringTest/ConnectionOnMirroringTest.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/MirroringTest/ConnectionOnMirroringTest.cs index 93101d5d38..460d566a6d 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/MirroringTest/ConnectionOnMirroringTest.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/MirroringTest/ConnectionOnMirroringTest.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Data; using System.Threading; +using System.Threading.Tasks; using Xunit; namespace Microsoft.Data.SqlClient.ManualTesting.Tests @@ -28,17 +29,14 @@ public static void TestMultipleConnectionToMirroredServer() builder.ConnectTimeout = 0; TestWorker worker = new TestWorker(builder.ConnectionString); - Thread childThread = new Thread(() => worker.TestMultipleConnection()); - childThread.Start(); + Task childTask = Task.Factory.StartNew(() => worker.TestMultipleConnection(), TaskCreationOptions.LongRunning); if (workerCompletedEvent.WaitOne(10000)) { - childThread.Join(); + childTask.Wait(); } else { - // currently Thread.Abort() throws PlatformNotSupportedException in CoreFx. - childThread.Interrupt(); throw new Exception("SqlConnection could not open and close successfully in timely manner. Possibly connection hangs."); } } diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ParameterTest/DateTimeVariantTest.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ParameterTest/DateTimeVariantTest.cs index 31c232e3d0..20768e9329 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ParameterTest/DateTimeVariantTest.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ParameterTest/DateTimeVariantTest.cs @@ -75,7 +75,7 @@ private static void TestSimpleParameter_Type(object paramValue, string expectedT { string tag = "TestSimpleParameter_Type"; DisplayHeader(tag, paramValue, expectedBaseTypeName); - string procName = DataTestUtility.GetUniqueNameForSqlServer("paramProc1"); + string procName = DataTestUtility.GetLongName("paramProc1"); try { using SqlConnection conn = new(s_connStr); @@ -115,7 +115,7 @@ private static void TestSimpleParameter_Variant(object paramValue, string expect { string tag = "TestSimpleParameter_Variant"; DisplayHeader(tag, paramValue, expectedBaseTypeName); - string procName = DataTestUtility.GetUniqueNameForSqlServer("paramProc2"); + string procName = DataTestUtility.GetLongName("paramProc2"); try { using SqlConnection conn = new(s_connStr); @@ -153,7 +153,7 @@ private static void TestSqlDataRecordParameterToTVP_Type(object paramValue, stri { string tag = "TestSqlDataRecordParameterToTVP_Type"; DisplayHeader(tag, paramValue, expectedBaseTypeName); - string tvpTypeName = DataTestUtility.GetUniqueNameForSqlServer("tvpType"); + string tvpTypeName = DataTestUtility.GetLongName("tvpType"); try { using SqlConnection conn = new(s_connStr); @@ -200,7 +200,7 @@ private static void TestSqlDataRecordParameterToTVP_Variant(object paramValue, s { string tag = "TestSqlDataRecordParameterToTVP_Variant"; DisplayHeader(tag, paramValue, expectedBaseTypeName); - string tvpTypeName = DataTestUtility.GetUniqueNameForSqlServer("tvpVariant"); + string tvpTypeName = DataTestUtility.GetLongName("tvpVariant"); try { using SqlConnection conn = new(s_connStr); @@ -245,7 +245,7 @@ private static void TestSqlDataReaderParameterToTVP_Type(object paramValue, stri { string tag = "TestSqlDataReaderParameterToTVP_Type"; DisplayHeader(tag, paramValue, expectedBaseTypeName); - string tvpTypeName = DataTestUtility.GetUniqueNameForSqlServer("tvpType"); + string tvpTypeName = DataTestUtility.GetLongName("tvpType"); try { using SqlConnection conn = new(s_connStr); @@ -295,7 +295,7 @@ private static void TestSqlDataReaderParameterToTVP_Variant(object paramValue, s { string tag = "TestSqlDataReaderParameterToTVP_Variant"; DisplayHeader(tag, paramValue, expectedBaseTypeName); - string tvpTypeName = DataTestUtility.GetUniqueNameForSqlServer("tvpVariant"); + string tvpTypeName = DataTestUtility.GetLongName("tvpVariant"); try { using SqlConnection conn = new(s_connStr); @@ -347,10 +347,10 @@ private static void TestSqlDataReader_TVP_Type(object paramValue, string expecte { string tag = "TestSqlDataReader_TVP_Type"; DisplayHeader(tag, paramValue, expectedBaseTypeName); - string tvpTypeName = DataTestUtility.GetUniqueNameForSqlServer("tvpType"); - string InputTableName = DataTestUtility.GetUniqueNameForSqlServer("InputTable"); - string OutputTableName = DataTestUtility.GetUniqueNameForSqlServer("OutputTable"); - string ProcName = DataTestUtility.GetUniqueNameForSqlServer("spTVPProc"); + string tvpTypeName = DataTestUtility.GetLongName("tvpType"); + string InputTableName = DataTestUtility.GetLongName("InputTable"); + string OutputTableName = DataTestUtility.GetLongName("OutputTable"); + string ProcName = DataTestUtility.GetLongName("spTVPProc"); try { using SqlConnection conn = new(s_connStr); @@ -428,10 +428,10 @@ private static void TestSqlDataReader_TVP_Variant(object paramValue, string expe { string tag = "TestSqlDataReader_TVP_Variant"; DisplayHeader(tag, paramValue, expectedBaseTypeName); - string tvpTypeName = DataTestUtility.GetUniqueNameForSqlServer("tvpVariant_DRdrTVPVar"); - string InputTableName = DataTestUtility.GetUniqueNameForSqlServer("InputTable"); - string OutputTableName = DataTestUtility.GetUniqueNameForSqlServer("OutputTable"); - string ProcName = DataTestUtility.GetUniqueNameForSqlServer("spTVPProc_DRdrTVPVar"); + string tvpTypeName = DataTestUtility.GetLongName("tvpVariant_DRdrTVPVar"); + string InputTableName = DataTestUtility.GetLongName("InputTable"); + string OutputTableName = DataTestUtility.GetLongName("OutputTable"); + string ProcName = DataTestUtility.GetLongName("spTVPProc_DRdrTVPVar"); try { using SqlConnection conn = new(s_connStr); @@ -512,8 +512,8 @@ private static void TestSimpleDataReader_Type(object paramValue, string expected { string tag = "TestSimpleDataReader_Type"; DisplayHeader(tag, paramValue, expectedBaseTypeName); - string inputTable = DataTestUtility.GetUniqueNameForSqlServer("inputTable"); - string procName = DataTestUtility.GetUniqueNameForSqlServer("paramProc3"); + string inputTable = DataTestUtility.GetLongName("inputTable"); + string procName = DataTestUtility.GetLongName("paramProc3"); try { using SqlConnection conn = new(s_connStr); @@ -568,8 +568,8 @@ private static void TestSimpleDataReader_Variant(object paramValue, string expec { string tag = "TestSimpleDataReader_Variant"; DisplayHeader(tag, paramValue, expectedBaseTypeName); - string inputTable = DataTestUtility.GetUniqueNameForSqlServer("inputTable"); - string procName = DataTestUtility.GetUniqueNameForSqlServer("paramProc4"); + string inputTable = DataTestUtility.GetLongName("inputTable"); + string procName = DataTestUtility.GetLongName("paramProc4"); try { using SqlConnection conn = new(s_connStr); @@ -624,8 +624,8 @@ private static void SqlBulkCopySqlDataReader_Type(object paramValue, string expe { string tag = "SqlBulkCopySqlDataReader_Type"; DisplayHeader(tag, paramValue, expectedBaseTypeName); - string bulkCopySrcTableName = DataTestUtility.GetUniqueNameForSqlServer("bulkSrcTable"); - string bulkCopyTableName = DataTestUtility.GetUniqueNameForSqlServer("bulkDestTable"); + string bulkCopySrcTableName = DataTestUtility.GetLongName("bulkSrcTable"); + string bulkCopyTableName = DataTestUtility.GetLongName("bulkDestTable"); try { using SqlConnection conn = new(s_connStr); @@ -698,8 +698,8 @@ private static void SqlBulkCopySqlDataReader_Variant(object paramValue, string e { string tag = "SqlBulkCopySqlDataReader_Variant"; DisplayHeader(tag, paramValue, expectedBaseTypeName); - string bulkCopySrcTableName = DataTestUtility.GetUniqueNameForSqlServer("bulkSrcTable"); - string bulkCopyTableName = DataTestUtility.GetUniqueNameForSqlServer("bulkDestTable"); + string bulkCopySrcTableName = DataTestUtility.GetLongName("bulkSrcTable"); + string bulkCopyTableName = DataTestUtility.GetLongName("bulkDestTable"); try { using SqlConnection conn = new(s_connStr); @@ -776,7 +776,7 @@ private static void SqlBulkCopyDataTable_Type(object paramValue, string expected { string tag = "SqlBulkCopyDataTable_Type"; DisplayHeader(tag, paramValue, expectedBaseTypeName); - string bulkCopyTableName = DataTestUtility.GetUniqueNameForSqlServer("bulkDestType"); + string bulkCopyTableName = DataTestUtility.GetLongName("bulkDestType"); try { using SqlConnection conn = new(s_connStr); @@ -836,7 +836,7 @@ private static void SqlBulkCopyDataTable_Variant(object paramValue, string expec { string tag = "SqlBulkCopyDataTable_Variant"; DisplayHeader(tag, paramValue, expectedBaseTypeName); - string bulkCopyTableName = DataTestUtility.GetUniqueNameForSqlServer("bulkDestVariant"); + string bulkCopyTableName = DataTestUtility.GetLongName("bulkDestVariant"); try { using SqlConnection conn = new(s_connStr); @@ -886,7 +886,7 @@ private static void SqlBulkCopyDataRow_Type(object paramValue, string expectedTy { string tag = "SqlBulkCopyDataRow_Type"; DisplayHeader(tag, paramValue, expectedBaseTypeName); - string bulkCopyTableName = DataTestUtility.GetUniqueNameForSqlServer("bulkDestType"); + string bulkCopyTableName = DataTestUtility.GetLongName("bulkDestType"); try { using SqlConnection conn = new(s_connStr); @@ -941,7 +941,7 @@ private static void SqlBulkCopyDataRow_Variant(object paramValue, string expecte { string tag = "SqlBulkCopyDataRow_Variant"; DisplayHeader(tag, paramValue, expectedBaseTypeName); - string bulkCopyTableName = DataTestUtility.GetUniqueNameForSqlServer("bulkDestVariant"); + string bulkCopyTableName = DataTestUtility.GetLongName("bulkDestVariant"); try { using SqlConnection conn = new(s_connStr); diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ParameterTest/ParametersTest.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ParameterTest/ParametersTest.cs index d7603ec807..2f9a2ecb1c 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ParameterTest/ParametersTest.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ParameterTest/ParametersTest.cs @@ -8,6 +8,7 @@ using System.Data; using System.Data.SqlTypes; using System.Threading; +using System.Threading.Tasks; using Xunit; using System.Globalization; @@ -16,6 +17,8 @@ using Microsoft.Data.SqlClient.Server; #endif +using Microsoft.Data.SqlClient.Tests.Common; + namespace Microsoft.Data.SqlClient.ManualTesting.Tests { public static class ParametersTest @@ -117,13 +120,13 @@ public static void CodeCoverageSqlClient() public static void Test_Copy_SqlParameter() { using var conn = new SqlConnection(s_connString); - string cTableName = DataTestUtility.GetUniqueNameForSqlServer("#tmp"); + string cTableName = DataTestUtility.GetLongName("#tmp"); try { // Create tmp table var sCreateTable = "IF NOT EXISTS("; - sCreateTable += $"SELECT * FROM sysobjects WHERE name= '{ cTableName }' and xtype = 'U')"; - sCreateTable += $"CREATE TABLE { cTableName }( BinValue binary(16) null)"; + sCreateTable += $"SELECT * FROM sysobjects WHERE name= '{cTableName}' and xtype = 'U')"; + sCreateTable += $"CREATE TABLE {cTableName}( BinValue binary(16) null)"; conn.Open(); var cmd = new SqlCommand(sCreateTable, conn); @@ -140,7 +143,7 @@ public static void Test_Copy_SqlParameter() UpdatedRowSource = UpdateRowSource.None, Connection = conn, - CommandText = $"INSERT { cTableName } (BinValue) " + CommandText = $"INSERT {cTableName} (BinValue) " }; cmdInsert.CommandText += "Values(@BinValue)"; cmdInsert.Parameters.Add("@BinValue", SqlDbType.Binary, 16, "SourceBinValue"); @@ -259,9 +262,9 @@ public static void TestParametersWithDatatablesTVPInsert() }; using SqlConnection connection = new(builder.ConnectionString); - string tableName = DataTestUtility.GetUniqueNameForSqlServer("Table"); - string procName = DataTestUtility.GetUniqueNameForSqlServer("Proc"); - string typeName = DataTestUtility.GetUniqueName("Type"); + string tableName = DataTestUtility.GetLongName("Table"); + string procName = DataTestUtility.GetLongName("Proc"); + string typeName = DataTestUtility.GetShortName("Type"); try { connection.Open(); @@ -338,10 +341,10 @@ public static void TestParametersWithSqlRecordsTVPInsert() record1, record2, }; - + using SqlConnection connection = new(builder.ConnectionString); - string procName = DataTestUtility.GetUniqueNameForSqlServer("Proc"); - string typeName = DataTestUtility.GetUniqueName("Type"); + string procName = DataTestUtility.GetLongName("Proc"); + string typeName = DataTestUtility.GetShortName("Type"); try { connection.Open(); @@ -400,8 +403,8 @@ @newRoads as {typeName} READONLY [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup), nameof(DataTestUtility.IsNotAzureSynapse))] public static void TestDateOnlyTVPDataTable_CommandSP() { - string tableTypeName = "[dbo]." + DataTestUtility.GetUniqueNameForSqlServer("UDTTTestDateOnlyTVP"); - string spName = DataTestUtility.GetUniqueNameForSqlServer("spTestDateOnlyTVP"); + string tableTypeName = "[dbo]." + DataTestUtility.GetLongName("UDTTTestDateOnlyTVP"); + string spName = DataTestUtility.GetLongName("spTestDateOnlyTVP"); SqlConnection connection = new(s_connString); try { @@ -418,7 +421,7 @@ public static void TestDateOnlyTVPDataTable_CommandSP() { cmd.CommandText = spName; cmd.CommandType = CommandType.StoredProcedure; - + DataTable dtTest = new(); dtTest.Columns.Add(new DataColumn("DateColumn", typeof(DateOnly))); dtTest.Columns.Add(new DataColumn("TimeColumn", typeof(TimeOnly))); @@ -448,8 +451,8 @@ public static void TestDateOnlyTVPDataTable_CommandSP() [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup), nameof(DataTestUtility.IsNotAzureSynapse))] public static void TestDateOnlyTVPSqlDataRecord_CommandSP() { - string tableTypeName = "[dbo]." + DataTestUtility.GetUniqueNameForSqlServer("UDTTTestDateOnlySqlDataRecordTVP"); - string spName = DataTestUtility.GetUniqueNameForSqlServer("spTestDateOnlySqlDataRecordTVP"); + string tableTypeName = "[dbo]." + DataTestUtility.GetLongName("UDTTTestDateOnlySqlDataRecordTVP"); + string spName = DataTestUtility.GetLongName("spTestDateOnlySqlDataRecordTVP"); SqlConnection connection = new(s_connString); try { @@ -569,7 +572,9 @@ public static void SqlDecimalConvertToDecimal_TestOutOfRange(string sqlDecimalVa [ClassData(typeof(ConnectionStringsProvider))] public static void TestScaledDecimalParameter_CommandInsert(string connectionString, bool truncateScaledDecimal) { - string tableName = DataTestUtility.GetUniqueNameForSqlServer("TestDecimalParameterCMD"); + using LocalAppContextSwitchesHelper appContextSwitchesHelper = new(); + + string tableName = DataTestUtility.GetLongName("TestDecimalParameterCMD"); using SqlConnection connection = InitialDatabaseTable(connectionString, tableName); try { @@ -601,7 +606,9 @@ public static void TestScaledDecimalParameter_CommandInsert(string connectionStr [ClassData(typeof(ConnectionStringsProvider))] public static void TestScaledDecimalParameter_BulkCopy(string connectionString, bool truncateScaledDecimal) { - string tableName = DataTestUtility.GetUniqueNameForSqlServer("TestDecimalParameterBC"); + using LocalAppContextSwitchesHelper appContextSwitchesHelper = new(); + + string tableName = DataTestUtility.GetLongName("TestDecimalParameterBC"); using SqlConnection connection = InitialDatabaseTable(connectionString, tableName); try { @@ -635,9 +642,11 @@ public static void TestScaledDecimalParameter_BulkCopy(string connectionString, [ClassData(typeof(ConnectionStringsProvider))] public static void TestScaledDecimalTVP_CommandSP(string connectionString, bool truncateScaledDecimal) { - string tableName = DataTestUtility.GetUniqueNameForSqlServer("TestDecimalParameterBC"); - string tableTypeName = DataTestUtility.GetUniqueNameForSqlServer("UDTTTestDecimalParameterBC"); - string spName = DataTestUtility.GetUniqueNameForSqlServer("spTestDecimalParameterBC"); + using LocalAppContextSwitchesHelper appContextSwitchesHelper = new(); + + string tableName = DataTestUtility.GetLongName("TestDecimalParameterBC"); + string tableTypeName = DataTestUtility.GetLongName("UDTTTestDecimalParameterBC"); + string spName = DataTestUtility.GetLongName("spTestDecimalParameterBC"); using SqlConnection connection = InitialDatabaseUDTT(connectionString, tableName, tableTypeName, spName); try { @@ -922,7 +931,7 @@ private static void EnableOptimizedParameterBinding_ReturnSucceeds() { int firstInput = 12; - string sprocName = DataTestUtility.GetUniqueName("P"); + string sprocName = DataTestUtility.GetShortName("P"); // input, output string createSprocQuery = "CREATE PROCEDURE " + sprocName + " @in int " + @@ -957,30 +966,19 @@ private static void EnableOptimizedParameterBinding_ReturnSucceeds() [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup))] public static void ClosedConnection_SqlParameterValueTest() { - var threads = new List(); - for (int i = 0; i < 100; i++) + var tasks = new Task[100]; + for (int i = 0; i < tasks.Length; i++) { - var t = new Thread(() => + var t = Task.Factory.StartNew(() => { for (int j = 0; j < 1000; j++) { - try - { - RunParameterTest(); - } - catch (Exception e) - { - Assert.Fail($"Unexpected exception occurred: {e.Message}"); - } + RunParameterTest(); } - }); - t.Start(); - threads.Add(t); - } - for (int i = 0; i < threads.Count; i++) - { - threads[i].Join(); + }, TaskCreationOptions.LongRunning); + tasks[i] = t; } + Task.WaitAll(tasks); } private static void RunParameterTest() @@ -998,7 +996,7 @@ private static void RunParameterTest() cm.Parameters.Add(new SqlParameter("@id2", SqlDbType.UniqueIdentifier) { Direction = ParameterDirection.Output }); try { - System.Threading.Tasks.Task task = cm.ExecuteNonQueryAsync(cancellationToken.Token); + Task task = cm.ExecuteNonQueryAsync(cancellationToken.Token); task.Wait(); } catch (Exception) diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ParameterTest/SqlAdapterUpdateBatch.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ParameterTest/SqlAdapterUpdateBatch.cs index 7f383e8201..aa59bc319c 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ParameterTest/SqlAdapterUpdateBatch.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ParameterTest/SqlAdapterUpdateBatch.cs @@ -15,7 +15,7 @@ public class SqlAdapterUpdateBatch [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup), nameof(DataTestUtility.IsNotAzureSynapse))] public void SqlAdapterTest() { - string tableName = DataTestUtility.GetUniqueNameForSqlServer("Adapter"); + string tableName = DataTestUtility.GetLongName("Adapter"); string tableNameNoBrackets = tableName.Substring(1, tableName.Length - 2); try { diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ParameterTest/SqlVariantParam.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ParameterTest/SqlVariantParam.cs index 2d11274191..e1592825b1 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ParameterTest/SqlVariantParam.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ParameterTest/SqlVariantParam.cs @@ -108,7 +108,7 @@ private static void SendVariantParam(object paramValue, string expectedTypeName, /// private static void SendVariantBulkCopy(object paramValue, string expectedTypeName, string expectedBaseTypeName) { - string bulkCopyTableName = DataTestUtility.GetUniqueNameForSqlServer("bulkDest"); + string bulkCopyTableName = DataTestUtility.GetLongName("bulkDest"); // Fetch reader using type. using SqlDataReader dr = GetReaderForVariant(paramValue, false); @@ -194,7 +194,7 @@ private static void SendVariantBulkCopy(object paramValue, string expectedTypeNa /// private static void SendVariantTvp(object paramValue, string expectedTypeName, string expectedBaseTypeName) { - string tvpTypeName = DataTestUtility.GetUniqueNameForSqlServer("tvpVariant"); + string tvpTypeName = DataTestUtility.GetLongName("tvpVariant"); using SqlConnection connTvp = new(s_connStr); connTvp.Open(); diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/RandomStressTest/RandomStressTest.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/RandomStressTest/RandomStressTest.cs index 86b5438a1a..79bf05a7f8 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/RandomStressTest/RandomStressTest.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/RandomStressTest/RandomStressTest.cs @@ -8,6 +8,7 @@ using System.Diagnostics; using System.Text; using System.Threading; +using System.Threading.Tasks; using Xunit; namespace Microsoft.Data.SqlClient.ManualTesting.Tests @@ -70,8 +71,7 @@ public void TestMain() { for (int tcount = 0; tcount < ThreadCountDefault; tcount++) { - Thread t = new Thread(TestThread); - t.Start(); + _ = Task.Factory.StartNew(TestThread, TaskCreationOptions.LongRunning); } } diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/RetryLogic/SqlCommandReliabilityTest.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/RetryLogic/SqlCommandReliabilityTest.cs index 7c590ebe05..21165e7624 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/RetryLogic/SqlCommandReliabilityTest.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/RetryLogic/SqlCommandReliabilityTest.cs @@ -268,7 +268,7 @@ public void RetryExecuteUnauthorizedSqlStatementDML(string cnnString, SqlRetryLo public void DropDatabaseWithActiveConnection(string cnnString, SqlRetryLogicBaseProvider provider) { int currentRetries = 0; - string database = DataTestUtility.GetUniqueNameForSqlServer($"RetryLogic_{provider.RetryLogic.RetryIntervalEnumerator.GetType().Name}", false); + string database = DataTestUtility.GetLongName($"RetryLogic_{provider.RetryLogic.RetryIntervalEnumerator.GetType().Name}", false); var builder = new SqlConnectionStringBuilder(cnnString) { InitialCatalog = database, @@ -330,7 +330,7 @@ public void DropDatabaseWithActiveConnection(string cnnString, SqlRetryLogicBase public void UpdateALockedTable(string cnnString, SqlRetryLogicBaseProvider provider) { int currentRetries = 0; - string tableName = DataTestUtility.GetUniqueNameForSqlServer("Region"); + string tableName = DataTestUtility.GetLongName("Region"); string fieldName = "RegionDescription"; using (var cnn1 = new SqlConnection(cnnString)) diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/RetryLogic/SqlConnectionReliabilityTest.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/RetryLogic/SqlConnectionReliabilityTest.cs index 61a1b07c5e..e5ed05e09f 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/RetryLogic/SqlConnectionReliabilityTest.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/RetryLogic/SqlConnectionReliabilityTest.cs @@ -58,7 +58,7 @@ public void ConnectionCancelRetryOpenInvalidCatalog(string cnnString, SqlRetryLo public void CreateDatabaseWhileTryingToConnect(string cnnString, SqlRetryLogicBaseProvider provider) { int currentRetries = 0; - string database = DataTestUtility.GetUniqueNameForSqlServer($"RetryLogic_{provider.RetryLogic.RetryIntervalEnumerator.GetType().Name}", false); + string database = DataTestUtility.GetLongName($"RetryLogic_{provider.RetryLogic.RetryIntervalEnumerator.GetType().Name}", false); var builder = new SqlConnectionStringBuilder(cnnString) { InitialCatalog = database, diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlBulkCopyTest/AdjustPrecScaleForBulkCopy.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlBulkCopyTest/AdjustPrecScaleForBulkCopy.cs index 72bab47869..a845710d50 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlBulkCopyTest/AdjustPrecScaleForBulkCopy.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlBulkCopyTest/AdjustPrecScaleForBulkCopy.cs @@ -41,7 +41,7 @@ public static void RunTest() private static SqlDecimal BulkCopySqlDecimalToTable(SqlDecimal decimalValue, int sourcePrecision, int sourceScale, int targetPrecision, int targetScale) { - string tableName = DataTestUtility.GetUniqueNameForSqlServer("Table"); + string tableName = DataTestUtility.GetLongName("Table"); string connectionString = DataTestUtility.TCPConnectionString; SqlDecimal resultValue; diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlBulkCopyTest/AzureDistributedTransaction.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlBulkCopyTest/AzureDistributedTransaction.cs index 823bc50a9d..2a853d7ed4 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlBulkCopyTest/AzureDistributedTransaction.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlBulkCopyTest/AzureDistributedTransaction.cs @@ -11,7 +11,7 @@ namespace Microsoft.Data.SqlClient.ManualTesting.Tests public class AzureDistributedTransaction { private static readonly string s_connectionString = DataTestUtility.TCPConnectionString; - private static readonly string s_tableName = DataTestUtility.GetUniqueNameForSqlServer("Azure"); + private static readonly string s_tableName = DataTestUtility.GetLongName("Azure"); private static readonly string s_createTableCmd = $"CREATE TABLE {s_tableName} (NAME NVARCHAR(40), AGE INT)"; private static readonly string s_sqlBulkCopyCmd = "SELECT * FROM(VALUES ('Fuller', 33), ('Davon', 49)) AS q (FirstName, Age)"; private static readonly int s_commandTimeout = 30; diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlBulkCopyTest/CopyAllFromReader.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlBulkCopyTest/CopyAllFromReader.cs index 4d1dd14cfb..beb8df7992 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlBulkCopyTest/CopyAllFromReader.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlBulkCopyTest/CopyAllFromReader.cs @@ -52,8 +52,6 @@ public static void Test(string srcConstr, string dstConstr, string dstTable) Assert.True(0 < (long)stats["BytesReceived"], "BytesReceived is non-positive."); Assert.True(0 < (long)stats["BytesSent"], "BytesSent is non-positive."); - Assert.True((long)stats["ConnectionTime"] >= (long)stats["ExecutionTime"], "Connection Time is less than Execution Time."); - Assert.True((long)stats["ExecutionTime"] >= (long)stats["NetworkServerTime"], "Execution Time is less than Network Server Time."); DataTestUtility.AssertEqualsWithDescription((long)0, (long)stats["UnpreparedExecs"], "Non-zero UnpreparedExecs value: " + (long)stats["UnpreparedExecs"]); DataTestUtility.AssertEqualsWithDescription((long)0, (long)stats["PreparedExecs"], "Non-zero PreparedExecs value: " + (long)stats["PreparedExecs"]); DataTestUtility.AssertEqualsWithDescription((long)0, (long)stats["Prepares"], "Non-zero Prepares value: " + (long)stats["Prepares"]); diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlBulkCopyTest/CopyWidenNullInexactNumerics.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlBulkCopyTest/CopyWidenNullInexactNumerics.cs index 5ccda71fb9..f961521233 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlBulkCopyTest/CopyWidenNullInexactNumerics.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlBulkCopyTest/CopyWidenNullInexactNumerics.cs @@ -12,8 +12,8 @@ public class CopyWidenNullInexactNumerics { public static void Test(string sourceDatabaseConnectionString, string destinationDatabaseConnectionString) { - string sourceTableName = DataTestUtility.GetUniqueNameForSqlServer("BCP_SRC"); - string destTableName = DataTestUtility.GetUniqueNameForSqlServer("BCP_DST"); + string sourceTableName = DataTestUtility.GetLongName("BCP_SRC"); + string destTableName = DataTestUtility.GetLongName("BCP_DST"); // this test copies float and real inexact numeric types into decimal targets using bulk copy to check that the widening of the type succeeds. using (var sourceConnection = new SqlConnection(sourceDatabaseConnectionString)) diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlBulkCopyTest/DataConversionErrorMessageTest.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlBulkCopyTest/DataConversionErrorMessageTest.cs index 4c3d594ad1..4a722dd409 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlBulkCopyTest/DataConversionErrorMessageTest.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlBulkCopyTest/DataConversionErrorMessageTest.cs @@ -28,7 +28,7 @@ public InitialDatabase() srcConstr = DataTestUtility.TCPConnectionString; Connection = new SqlConnection(srcConstr); - TableName = DataTestUtility.GetUniqueNameForSqlServer("SqlBulkCopyTest_CopyStringToIntTest_"); + TableName = DataTestUtility.GetLongName("SqlBulkCopyTest_CopyStringToIntTest_"); InitialTable(Connection, TableName); } diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlBulkCopyTest/TestBulkCopyWithUTF8.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlBulkCopyTest/TestBulkCopyWithUTF8.cs index dc3779b4dc..5b7112476d 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlBulkCopyTest/TestBulkCopyWithUTF8.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlBulkCopyTest/TestBulkCopyWithUTF8.cs @@ -15,8 +15,8 @@ namespace Microsoft.Data.SqlClient.ManualTesting.Tests /// public sealed class TestBulkCopyWithUtf8 : IDisposable { - private static string s_sourceTable = DataTestUtility.GetUniqueName("SourceTableForUTF8Data"); - private static string s_destinationTable = DataTestUtility.GetUniqueName("DestinationTableForUTF8Data"); + private static string s_sourceTable = DataTestUtility.GetShortName("SourceTableForUTF8Data"); + private static string s_destinationTable = DataTestUtility.GetShortName("DestinationTableForUTF8Data"); private static string s_testValue = "test"; private static byte[] s_testValueInUtf8Bytes = new byte[] { 0x74, 0x65, 0x73, 0x74 }; private static readonly string s_insertQuery = $"INSERT INTO {s_sourceTable} VALUES('{s_testValue}')"; diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlBulkCopyTest/WriteToServerTest.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlBulkCopyTest/WriteToServerTest.cs index 343a7bcfe2..20f63a2e0a 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlBulkCopyTest/WriteToServerTest.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlBulkCopyTest/WriteToServerTest.cs @@ -12,8 +12,8 @@ namespace Microsoft.Data.SqlClient.ManualTesting.Tests public class WriteToServerTest { private readonly string _connectionString = null; - private readonly string _tableName1 = DataTestUtility.GetUniqueName("Bulk1"); - private readonly string _tableName2 = DataTestUtility.GetUniqueName("Bulk2"); + private readonly string _tableName1 = DataTestUtility.GetShortName("Bulk1"); + private readonly string _tableName2 = DataTestUtility.GetShortName("Bulk2"); public WriteToServerTest() { diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlCommand/SqlCommandCompletedTest.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlCommand/SqlCommandCompletedTest.cs index 8e38bee7c0..21ff771ac0 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlCommand/SqlCommandCompletedTest.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlCommand/SqlCommandCompletedTest.cs @@ -11,7 +11,7 @@ public static class SqlCommandCompletedTest [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup))] public static void VerifyStatmentCompletedCalled() { - string tableName = DataTestUtility.GetUniqueNameForSqlServer("stmt"); + string tableName = DataTestUtility.GetLongName("stmt"); using (var conn = new SqlConnection(s_connStr)) using (var cmd = conn.CreateCommand()) diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlCommand/SqlCommandSetTest.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlCommand/SqlCommandSetTest.cs index 26b11055c2..7f28a4a09a 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlCommand/SqlCommandSetTest.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlCommand/SqlCommandSetTest.cs @@ -15,8 +15,8 @@ public class SqlCommandSetTest [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup), nameof(DataTestUtility.IsNotAzureSynapse))] public void TestByteArrayParameters() { - string tableName = DataTestUtility.GetUniqueNameForSqlServer("CMD"); - string procName = DataTestUtility.GetUniqueNameForSqlServer("CMD"); + string tableName = DataTestUtility.GetLongName("CMD"); + string procName = DataTestUtility.GetLongName("CMD"); byte[] bArray = new byte[] { 1, 2, 3 }; using (var connection = new SqlConnection(DataTestUtility.TCPConnectionString)) diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlFileStreamTest/SqlFileStreamTest.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlFileStreamTest/SqlFileStreamTest.cs index 742a800bb9..9cba46959f 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlFileStreamTest/SqlFileStreamTest.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlFileStreamTest/SqlFileStreamTest.cs @@ -221,7 +221,7 @@ private static string SetupFileStreamDB() fileStreamDir += "\\"; } - string dbName = DataTestUtility.GetUniqueName("FS", false); + string dbName = DataTestUtility.GetShortName("FS", false); string createDBQuery = @$"CREATE DATABASE [{dbName}] ON PRIMARY (NAME = PhotoLibrary_data, @@ -266,7 +266,7 @@ private static void DropFileStreamDb(string connString) private static string SetupTable(string connString) { // Generate random table name - string tempTable = DataTestUtility.GetUniqueNameForSqlServer("fs"); + string tempTable = DataTestUtility.GetLongName("fs"); // Create table string createTable = $"CREATE TABLE {tempTable} (EmployeeId INT NOT NULL PRIMARY KEY, Photo VARBINARY(MAX) FILESTREAM NULL, RowGuid UNIQUEIDENTIFIER NOT NULL ROWGUIDCOL UNIQUE DEFAULT NEWID() ) "; ExecuteNonQueryCommand(createTable, connString); diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/UdtTest/SqlServerTypesTest.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/UdtTest/SqlServerTypesTest.cs index 0fd8f22daf..67a5cf2748 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/UdtTest/SqlServerTypesTest.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/UdtTest/SqlServerTypesTest.cs @@ -408,7 +408,7 @@ private static string GetUdtName(Type udtClrType) [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup), nameof(DataTestUtility.IsNotAzureSynapse))] public static void TestSqlServerTypesInsertAndRead() { - string tableName = DataTestUtility.GetUniqueNameForSqlServer("Type"); + string tableName = DataTestUtility.GetLongName("Type"); string allTypesSQL = @$" if not exists (select * from sysobjects where name='{tableName}' and xtype='U') Begin diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/UdtTest/UdtBulkCopyTest.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/UdtTest/UdtBulkCopyTest.cs index 8adf6c7bb5..469c895a61 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/UdtTest/UdtBulkCopyTest.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/UdtTest/UdtBulkCopyTest.cs @@ -18,9 +18,9 @@ public void RunCopyTest() _connStr = (new SqlConnectionStringBuilder(DataTestUtility.TCPConnectionString) { InitialCatalog = DataTestUtility.UdtTestDbName }).ConnectionString; SqlConnection conn = new SqlConnection(_connStr); - string cities = DataTestUtility.GetUniqueNameForSqlServer("UdtBulkCopy_cities"); - string customers = DataTestUtility.GetUniqueNameForSqlServer("UdtBulkCopy_customers"); - string circles = DataTestUtility.GetUniqueNameForSqlServer("UdtBulkCopy_circles"); + string cities = DataTestUtility.GetLongName("UdtBulkCopy_cities"); + string customers = DataTestUtility.GetLongName("UdtBulkCopy_customers"); + string circles = DataTestUtility.GetLongName("UdtBulkCopy_circles"); conn.Open(); try diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/UdtTest/UdtDateTimeOffsetTest.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/UdtTest/UdtDateTimeOffsetTest.cs index 59896086f4..74e6aaa277 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/UdtTest/UdtDateTimeOffsetTest.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/UdtTest/UdtDateTimeOffsetTest.cs @@ -32,7 +32,7 @@ public DateTimeOffsetVariableScale(DateTimeOffset dateTimeOffset, int scale) public class UdtDateTimeOffsetTest { private readonly string _connectionString = null; - private readonly string _udtTableType = DataTestUtility.GetUniqueNameForSqlServer("DataTimeOffsetTableType"); + private readonly string _udtTableType = DataTestUtility.GetLongName("DataTimeOffsetTableType"); private readonly ITestOutputHelper _testOutputHelper; public UdtDateTimeOffsetTest(ITestOutputHelper testOutputHelper) @@ -87,7 +87,7 @@ public void DateTimeOffsetAllScalesTestShouldSucceed() for (int scale = fromScale; scale <= toScale; scale++) { - string tvpTypeName = DataTestUtility.GetUniqueNameForSqlServer("tvpType"); // Need a unique name per scale, else we get errors. See https://github.com/dotnet/SqlClient/issues/3011 + string tvpTypeName = DataTestUtility.GetLongName("tvpType"); // Need a unique name per scale, else we get errors. See https://github.com/dotnet/SqlClient/issues/3011 DateTimeOffset dateTimeOffset = new DateTimeOffset(2024, 1, 1, 23, 59, 59, TimeSpan.Zero); diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/UdtTest/UdtTest2.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/UdtTest/UdtTest2.cs index 16d48d7c37..85dbf99b33 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/UdtTest/UdtTest2.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/UdtTest/UdtTest2.cs @@ -84,8 +84,8 @@ public void UDTParams_Binary() [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.IsUdtTestDatabasePresent), nameof(DataTestUtility.AreConnStringsSetup))] public void UDTParams_Invalid2() { - string spInsertCustomer = DataTestUtility.GetUniqueNameForSqlServer("spUdtTest2_InsertCustomer"); - string tableName = DataTestUtility.GetUniqueNameForSqlServer("UdtTest2"); + string spInsertCustomer = DataTestUtility.GetLongName("spUdtTest2_InsertCustomer"); + string tableName = DataTestUtility.GetLongName("UdtTest2"); using (SqlConnection conn = new SqlConnection(_connStr)) using (SqlCommand cmd = conn.CreateCommand()) @@ -143,8 +143,8 @@ public void UDTParams_Invalid() [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.IsUdtTestDatabasePresent), nameof(DataTestUtility.AreConnStringsSetup))] public void UDTParams_TypedNull() { - string spInsertCustomer = DataTestUtility.GetUniqueNameForSqlServer("spUdtTest2_InsertCustomer"); - string tableName = DataTestUtility.GetUniqueNameForSqlServer("UdtTest2_Customer"); + string spInsertCustomer = DataTestUtility.GetLongName("spUdtTest2_InsertCustomer"); + string tableName = DataTestUtility.GetLongName("UdtTest2_Customer"); using (SqlConnection conn = new SqlConnection(_connStr)) using (SqlCommand cmd = conn.CreateCommand()) @@ -188,8 +188,8 @@ public void UDTParams_TypedNull() [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.IsUdtTestDatabasePresent), nameof(DataTestUtility.AreConnStringsSetup))] public void UDTParams_NullInput() { - string spInsertCustomer = DataTestUtility.GetUniqueNameForSqlServer("spUdtTest2_InsertCustomer"); - string tableName = DataTestUtility.GetUniqueNameForSqlServer("UdtTest2_Customer"); + string spInsertCustomer = DataTestUtility.GetLongName("spUdtTest2_InsertCustomer"); + string tableName = DataTestUtility.GetLongName("UdtTest2_Customer"); using (SqlConnection conn = new SqlConnection(_connStr)) using (SqlCommand cmd = conn.CreateCommand()) @@ -232,8 +232,8 @@ public void UDTParams_NullInput() [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.IsUdtTestDatabasePresent), nameof(DataTestUtility.AreConnStringsSetup))] public void UDTParams_InputOutput() { - string spInsertCity = DataTestUtility.GetUniqueNameForSqlServer("spUdtTest2_InsertCity"); - string tableName = DataTestUtility.GetUniqueNameForSqlServer("UdtTest2"); + string spInsertCity = DataTestUtility.GetLongName("spUdtTest2_InsertCity"); + string tableName = DataTestUtility.GetLongName("UdtTest2"); using (SqlConnection conn = new SqlConnection(_connStr)) { diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/Utf8SupportTest/Utf8SupportTest.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/Utf8SupportTest/Utf8SupportTest.cs index effecb35b3..41f81b12e3 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/Utf8SupportTest/Utf8SupportTest.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/Utf8SupportTest/Utf8SupportTest.cs @@ -37,7 +37,7 @@ public static void CheckSupportUtf8ConnectionProperty() public static void UTF8databaseTest() { const string letters = @"!\#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~\u007f€\u0081‚ƒ„…†‡ˆ‰Š‹Œ\u008dŽ\u008f\u0090‘’“”•–—˜™š›œ\u009džŸ ¡¢£¤¥¦§¨©ª«¬­®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ"; - string dbName = DataTestUtility.GetUniqueNameForSqlServer("UTF8databaseTest", false); + string dbName = DataTestUtility.GetLongName("UTF8databaseTest", false); string tblName = "Table1"; SqlConnectionStringBuilder builder = new(DataTestUtility.TCPConnectionString); diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/VectorTest/NativeVectorFloat32Tests.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/VectorTest/NativeVectorFloat32Tests.cs index 8d205cfc9c..d905e15bc5 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/VectorTest/NativeVectorFloat32Tests.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/VectorTest/NativeVectorFloat32Tests.cs @@ -18,37 +18,38 @@ public static class VectorFloat32TestData { public const int VectorHeaderSize = 8; public static float[] testData = new float[] { 1.1f, 2.2f, 3.3f }; - public static int sizeInbytes = VectorHeaderSize + testData.Length * sizeof(float); public static int vectorColumnLength = testData.Length; + // Incorrect size for SqlParameter.Size + public static int IncorrectParamSize = 3234; public static IEnumerable GetVectorFloat32TestData() { // Pattern 1-4 with SqlVector(values: testData) - yield return new object[] { 1, new SqlVector(testData), testData, sizeInbytes, vectorColumnLength }; - yield return new object[] { 2, new SqlVector(testData), testData, sizeInbytes, vectorColumnLength }; - yield return new object[] { 3, new SqlVector(testData), testData, sizeInbytes, vectorColumnLength }; - yield return new object[] { 4, new SqlVector(testData), testData, sizeInbytes, vectorColumnLength }; + yield return new object[] { 1, new SqlVector(testData), testData, vectorColumnLength }; + yield return new object[] { 2, new SqlVector(testData), testData, vectorColumnLength }; + yield return new object[] { 3, new SqlVector(testData), testData, vectorColumnLength }; + yield return new object[] { 4, new SqlVector(testData), testData, vectorColumnLength }; // Pattern 1-4 with SqlVector(n) - yield return new object[] { 1, new SqlVector(vectorColumnLength), Array.Empty(), sizeInbytes, vectorColumnLength }; - yield return new object[] { 2, new SqlVector(vectorColumnLength), Array.Empty(), sizeInbytes, vectorColumnLength }; - yield return new object[] { 3, new SqlVector(vectorColumnLength), Array.Empty(), sizeInbytes, vectorColumnLength }; - yield return new object[] { 4, new SqlVector(vectorColumnLength), Array.Empty(), sizeInbytes, vectorColumnLength }; + yield return new object[] { 1, SqlVector.CreateNull(vectorColumnLength), Array.Empty(), vectorColumnLength }; + yield return new object[] { 2, SqlVector.CreateNull(vectorColumnLength), Array.Empty(), vectorColumnLength }; + yield return new object[] { 3, SqlVector.CreateNull(vectorColumnLength), Array.Empty(), vectorColumnLength }; + yield return new object[] { 4, SqlVector.CreateNull(vectorColumnLength), Array.Empty(), vectorColumnLength }; // Pattern 1-4 with DBNull - yield return new object[] { 1, DBNull.Value, Array.Empty(), sizeInbytes, vectorColumnLength }; - yield return new object[] { 2, DBNull.Value, Array.Empty(), sizeInbytes, vectorColumnLength }; - yield return new object[] { 3, DBNull.Value, Array.Empty(), sizeInbytes, vectorColumnLength }; - yield return new object[] { 4, DBNull.Value, Array.Empty(), sizeInbytes, vectorColumnLength }; + yield return new object[] { 1, DBNull.Value, Array.Empty(), vectorColumnLength }; + yield return new object[] { 2, DBNull.Value, Array.Empty(), vectorColumnLength }; + yield return new object[] { 3, DBNull.Value, Array.Empty(), vectorColumnLength }; + yield return new object[] { 4, DBNull.Value, Array.Empty(), vectorColumnLength }; // Pattern 1-4 with SqlVector.Null - yield return new object[] { 1, SqlVector.Null, Array.Empty(), sizeInbytes, vectorColumnLength }; - + yield return new object[] { 1, SqlVector.Null, Array.Empty(), vectorColumnLength }; + // Following scenario is not supported in SqlClient. // This can only be fixed with a behavior change that SqlParameter.Value is internally set to DBNull.Value if it is set to null. - //yield return new object[] { 2, SqlVector.Null, Array.Empty(), sizeInbytes, vectorColumnLength }; - - yield return new object[] { 3, SqlVector.Null, Array.Empty(), sizeInbytes, vectorColumnLength }; - yield return new object[] { 4, SqlVector.Null, Array.Empty(), sizeInbytes, vectorColumnLength }; + //yield return new object[] { 2, SqlVector.Null, Array.Empty(), vectorColumnLength }; + + yield return new object[] { 3, SqlVector.Null, Array.Empty(), vectorColumnLength }; + yield return new object[] { 4, SqlVector.Null, Array.Empty(), vectorColumnLength }; } } @@ -56,15 +57,15 @@ public sealed class NativeVectorFloat32Tests : IDisposable { private readonly ITestOutputHelper _output; private static readonly string s_connectionString = ManualTesting.Tests.DataTestUtility.TCPConnectionString; - private static readonly string s_tableName = DataTestUtility.GetUniqueName("VectorTestTable"); - private static readonly string s_bulkCopySrcTableName = DataTestUtility.GetUniqueName("VectorBulkCopyTestTable"); + private static readonly string s_tableName = DataTestUtility.GetShortName("VectorTestTable"); + private static readonly string s_bulkCopySrcTableName = DataTestUtility.GetShortName("VectorBulkCopyTestTable"); private static readonly string s_bulkCopySrcTableDef = $@"(Id INT PRIMARY KEY IDENTITY, VectorData vector(3) NULL)"; private static readonly string s_tableDefinition = $@"(Id INT PRIMARY KEY IDENTITY, VectorData vector(3) NULL)"; private static readonly string s_selectCmdString = $"SELECT VectorData FROM {s_tableName} ORDER BY Id DESC"; private static readonly string s_insertCmdString = $"INSERT INTO {s_tableName} (VectorData) VALUES (@VectorData)"; private static readonly string s_vectorParamName = $"@VectorData"; private static readonly string s_outputVectorParamName = $"@OutputVectorData"; - private static readonly string s_storedProcName = DataTestUtility.GetUniqueName("VectorsAsVarcharSp"); + private static readonly string s_storedProcName = DataTestUtility.GetShortName("VectorsAsVarcharSp"); private static readonly string s_storedProcBody = $@" {s_vectorParamName} vector(3), -- Input: Serialized float[] as JSON string {s_outputVectorParamName} vector(3) OUTPUT -- Output: Echoed back from latest inserted row @@ -101,10 +102,9 @@ public void Dispose() DataTestUtility.DropStoredProcedure(connection, s_storedProcName); } - private void ValidateSqlVectorFloat32Object(bool isNull, SqlVector sqlVectorFloat32, float[] expectedData, int expectedSize, int expectedLength) + private void ValidateSqlVectorFloat32Object(bool isNull, SqlVector sqlVectorFloat32, float[] expectedData, int expectedLength) { Assert.Equal(expectedData, sqlVectorFloat32.Memory.ToArray()); - Assert.Equal(expectedSize, sqlVectorFloat32.Size); Assert.Equal(expectedLength, sqlVectorFloat32.Length); if (!isNull) { @@ -116,22 +116,22 @@ private void ValidateSqlVectorFloat32Object(bool isNull, SqlVector sqlVec } } - private void ValidateInsertedData(SqlConnection connection, float[] expectedData, int expectedSize, int expectedLength) + private void ValidateInsertedData(SqlConnection connection, float[] expectedData, int expectedLength) { using var selectCmd = new SqlCommand(s_selectCmdString, connection); using var reader = selectCmd.ExecuteReader(); Assert.True(reader.Read(), "No data found in the table."); //For both null and non-null cases, validate the SqlVector object - ValidateSqlVectorFloat32Object(reader.IsDBNull(0), (SqlVector)reader.GetSqlVector(0), expectedData, expectedSize, expectedLength); - ValidateSqlVectorFloat32Object(reader.IsDBNull(0), reader.GetFieldValue>(0), expectedData, expectedSize, expectedLength); - ValidateSqlVectorFloat32Object(reader.IsDBNull(0), (SqlVector)reader.GetSqlValue(0), expectedData, expectedSize, expectedLength); + ValidateSqlVectorFloat32Object(reader.IsDBNull(0), (SqlVector)reader.GetSqlVector(0), expectedData, expectedLength); + ValidateSqlVectorFloat32Object(reader.IsDBNull(0), reader.GetFieldValue>(0), expectedData, expectedLength); + ValidateSqlVectorFloat32Object(reader.IsDBNull(0), (SqlVector)reader.GetSqlValue(0), expectedData, expectedLength); if (!reader.IsDBNull(0)) - { - ValidateSqlVectorFloat32Object(reader.IsDBNull(0), (SqlVector)reader.GetValue(0), expectedData, expectedSize, expectedLength); - ValidateSqlVectorFloat32Object(reader.IsDBNull(0), (SqlVector)reader[0], expectedData, expectedSize, expectedLength); - ValidateSqlVectorFloat32Object(reader.IsDBNull(0), (SqlVector)reader["VectorData"], expectedData, expectedSize, expectedLength); + { + ValidateSqlVectorFloat32Object(reader.IsDBNull(0), (SqlVector)reader.GetValue(0), expectedData, expectedLength); + ValidateSqlVectorFloat32Object(reader.IsDBNull(0), (SqlVector)reader[0], expectedData, expectedLength); + ValidateSqlVectorFloat32Object(reader.IsDBNull(0), (SqlVector)reader["VectorData"], expectedData, expectedLength); Assert.Equal(expectedData, JsonSerializer.Deserialize(reader.GetString(0))); Assert.Equal(expectedData, JsonSerializer.Deserialize(reader.GetSqlString(0).Value)); Assert.Equal(expectedData, JsonSerializer.Deserialize(reader.GetFieldValue(0))); @@ -147,13 +147,12 @@ private void ValidateInsertedData(SqlConnection connection, float[] expectedData } } - [ConditionalTheory(typeof(DataTestUtility), nameof(DataTestUtility.IsVectorSupported))] - [MemberData(nameof(VectorFloat32TestData.GetVectorFloat32TestData), MemberType = typeof(VectorFloat32TestData))] + [ConditionalTheory(typeof(DataTestUtility), nameof(DataTestUtility.IsAzureServer))] + [MemberData(nameof(VectorFloat32TestData.GetVectorFloat32TestData), MemberType = typeof(VectorFloat32TestData), DisableDiscoveryEnumeration = true)] public void TestSqlVectorFloat32ParameterInsertionAndReads( int pattern, object value, float[] expectedValues, - int expectedSize, int expectedLength) { using var conn = new SqlConnection(s_connectionString); @@ -171,7 +170,8 @@ public void TestSqlVectorFloat32ParameterInsertionAndReads( }, 2 => new SqlParameter(s_vectorParamName, value), 3 => new SqlParameter(s_vectorParamName, SqlDbTypeExtensions.Vector) { Value = value }, - 4 => new SqlParameter(s_vectorParamName, SqlDbTypeExtensions.Vector, new SqlVector(3).Size) { Value = value }, + // Even if size is specified, the actual size is determined by the value passed and specified size is ignored. + 4 => new SqlParameter(s_vectorParamName, SqlDbTypeExtensions.Vector, VectorFloat32TestData.IncorrectParamSize) { Value = value }, _ => throw new ArgumentOutOfRangeException(nameof(pattern), $"Unsupported pattern: {pattern}") }; @@ -179,25 +179,25 @@ public void TestSqlVectorFloat32ParameterInsertionAndReads( Assert.Equal(1, insertCmd.ExecuteNonQuery()); insertCmd.Parameters.Clear(); - ValidateInsertedData(conn, expectedValues, expectedSize, expectedLength); + ValidateInsertedData(conn, expectedValues, expectedLength); } - private async Task ValidateInsertedDataAsync(SqlConnection connection, float[] expectedData, int expectedSize, int expectedLength) + private async Task ValidateInsertedDataAsync(SqlConnection connection, float[] expectedData, int expectedLength) { using var selectCmd = new SqlCommand(s_selectCmdString, connection); using var reader = await selectCmd.ExecuteReaderAsync(); Assert.True(await reader.ReadAsync(), "No data found in the table."); //For both null and non-null cases, validate the SqlVector object - ValidateSqlVectorFloat32Object(await reader.IsDBNullAsync(0), (SqlVector)reader.GetSqlVector(0), expectedData, expectedSize, expectedLength); - ValidateSqlVectorFloat32Object(await reader.IsDBNullAsync(0), await reader.GetFieldValueAsync>(0), expectedData, expectedSize, expectedLength); - ValidateSqlVectorFloat32Object(await reader.IsDBNullAsync(0), (SqlVector)reader.GetSqlValue(0), expectedData, expectedSize, expectedLength); + ValidateSqlVectorFloat32Object(await reader.IsDBNullAsync(0), (SqlVector)reader.GetSqlVector(0), expectedData, expectedLength); + ValidateSqlVectorFloat32Object(await reader.IsDBNullAsync(0), await reader.GetFieldValueAsync>(0), expectedData, expectedLength); + ValidateSqlVectorFloat32Object(await reader.IsDBNullAsync(0), (SqlVector)reader.GetSqlValue(0), expectedData, expectedLength); if (!await reader.IsDBNullAsync(0)) { - ValidateSqlVectorFloat32Object(await reader.IsDBNullAsync(0), (SqlVector)reader.GetValue(0), expectedData, expectedSize, expectedLength); - ValidateSqlVectorFloat32Object(await reader.IsDBNullAsync(0), (SqlVector)reader[0], expectedData, expectedSize, expectedLength); - ValidateSqlVectorFloat32Object(await reader.IsDBNullAsync(0), (SqlVector)reader["VectorData"], expectedData, expectedSize, expectedLength); + ValidateSqlVectorFloat32Object(await reader.IsDBNullAsync(0), (SqlVector)reader.GetValue(0), expectedData, expectedLength); + ValidateSqlVectorFloat32Object(await reader.IsDBNullAsync(0), (SqlVector)reader[0], expectedData, expectedLength); + ValidateSqlVectorFloat32Object(await reader.IsDBNullAsync(0), (SqlVector)reader["VectorData"], expectedData, expectedLength); Assert.Equal(expectedData, JsonSerializer.Deserialize(reader.GetString(0))); Assert.Equal(expectedData, JsonSerializer.Deserialize(reader.GetSqlString(0).Value)); Assert.Equal(expectedData, JsonSerializer.Deserialize(await reader.GetFieldValueAsync(0))); @@ -213,13 +213,12 @@ private async Task ValidateInsertedDataAsync(SqlConnection connection, float[] e } } - [ConditionalTheory(typeof(DataTestUtility), nameof(DataTestUtility.IsVectorSupported))] - [MemberData(nameof(VectorFloat32TestData.GetVectorFloat32TestData), MemberType = typeof(VectorFloat32TestData))] + [ConditionalTheory(typeof(DataTestUtility), nameof(DataTestUtility.IsAzureServer))] + [MemberData(nameof(VectorFloat32TestData.GetVectorFloat32TestData), MemberType = typeof(VectorFloat32TestData), DisableDiscoveryEnumeration = true)] public async Task TestSqlVectorFloat32ParameterInsertionAndReadsAsync( int pattern, object value, float[] expectedValues, - int expectedSize, int expectedLength) { using var conn = new SqlConnection(s_connectionString); @@ -237,7 +236,7 @@ public async Task TestSqlVectorFloat32ParameterInsertionAndReadsAsync( }, 2 => new SqlParameter(s_vectorParamName, value), 3 => new SqlParameter(s_vectorParamName, SqlDbTypeExtensions.Vector) { Value = value }, - 4 => new SqlParameter(s_vectorParamName, SqlDbTypeExtensions.Vector, new SqlVector(3).Size) { Value = value }, + 4 => new SqlParameter(s_vectorParamName, SqlDbTypeExtensions.Vector, VectorFloat32TestData.IncorrectParamSize) { Value = value }, _ => throw new ArgumentOutOfRangeException(nameof(pattern), $"Unsupported pattern: {pattern}") }; @@ -245,16 +244,15 @@ public async Task TestSqlVectorFloat32ParameterInsertionAndReadsAsync( Assert.Equal(1, await insertCmd.ExecuteNonQueryAsync()); insertCmd.Parameters.Clear(); - await ValidateInsertedDataAsync(conn, expectedValues, expectedSize, expectedLength); + await ValidateInsertedDataAsync(conn, expectedValues, expectedLength); } - [ConditionalTheory(typeof(DataTestUtility), nameof(DataTestUtility.IsVectorSupported))] - [MemberData(nameof(VectorFloat32TestData.GetVectorFloat32TestData), MemberType = typeof(VectorFloat32TestData))] + [ConditionalTheory(typeof(DataTestUtility), nameof(DataTestUtility.IsAzureServer))] + [MemberData(nameof(VectorFloat32TestData.GetVectorFloat32TestData), MemberType = typeof(VectorFloat32TestData), DisableDiscoveryEnumeration = true)] public void TestStoredProcParamsForVectorFloat32( int pattern, object value, float[] expectedValues, - int expectedSize, int expectedLength) { //Create SP for test @@ -277,7 +275,7 @@ public void TestStoredProcParamsForVectorFloat32( }, 2 => new SqlParameter(s_vectorParamName, value), 3 => new SqlParameter(s_vectorParamName, SqlDbTypeExtensions.Vector) { Value = value }, - 4 => new SqlParameter(s_vectorParamName, SqlDbTypeExtensions.Vector, new SqlVector(3).Size) { Value = value }, + 4 => new SqlParameter(s_vectorParamName, SqlDbTypeExtensions.Vector, VectorFloat32TestData.IncorrectParamSize) { Value = value }, _ => throw new ArgumentOutOfRangeException(nameof(pattern), $"Unsupported pattern: {pattern}") }; command.Parameters.Add(inputParam); @@ -287,7 +285,7 @@ public void TestStoredProcParamsForVectorFloat32( ParameterName = s_outputVectorParamName, SqlDbType = SqlDbTypeExtensions.Vector, Direction = ParameterDirection.Output, - Value = new SqlVector(3) + Value = SqlVector.CreateNull(VectorFloat32TestData.vectorColumnLength) }; command.Parameters.Add(outputParam); @@ -295,24 +293,23 @@ public void TestStoredProcParamsForVectorFloat32( command.ExecuteNonQuery(); // Validate the output parameter - var vector = outputParam.Value as SqlVector; - ValidateSqlVectorFloat32Object(vector.IsNull, vector, expectedValues, expectedSize, expectedLength); + var vector = (SqlVector)outputParam.Value; + ValidateSqlVectorFloat32Object(vector.IsNull, vector, expectedValues, expectedLength); // Validate error for conventional way of setting output parameters command.Parameters.Clear(); command.Parameters.Add(inputParam); - var outputParamWithoutVal = new SqlParameter(s_outputVectorParamName, SqlDbTypeExtensions.Vector, new SqlVector(3).Size) { Direction = ParameterDirection.Output }; + var outputParamWithoutVal = new SqlParameter(s_outputVectorParamName, SqlDbTypeExtensions.Vector, VectorFloat32TestData.IncorrectParamSize) { Direction = ParameterDirection.Output }; command.Parameters.Add(outputParamWithoutVal); Assert.Throws(() => command.ExecuteNonQuery()); } - [ConditionalTheory(typeof(DataTestUtility), nameof(DataTestUtility.IsVectorSupported))] - [MemberData(nameof(VectorFloat32TestData.GetVectorFloat32TestData), MemberType = typeof(VectorFloat32TestData))] + [ConditionalTheory(typeof(DataTestUtility), nameof(DataTestUtility.IsAzureServer))] + [MemberData(nameof(VectorFloat32TestData.GetVectorFloat32TestData), MemberType = typeof(VectorFloat32TestData), DisableDiscoveryEnumeration = true)] public async Task TestStoredProcParamsForVectorFloat32Async( int pattern, object value, float[] expectedValues, - int expectedSize, int expectedLength) { //Create SP for test @@ -335,7 +332,7 @@ public async Task TestStoredProcParamsForVectorFloat32Async( }, 2 => new SqlParameter(s_vectorParamName, value), 3 => new SqlParameter(s_vectorParamName, SqlDbTypeExtensions.Vector) { Value = value }, - 4 => new SqlParameter(s_vectorParamName, SqlDbTypeExtensions.Vector, new SqlVector(3).Size) { Value = value }, + 4 => new SqlParameter(s_vectorParamName, SqlDbTypeExtensions.Vector, VectorFloat32TestData.IncorrectParamSize) { Value = value }, _ => throw new ArgumentOutOfRangeException(nameof(pattern), $"Unsupported pattern: {pattern}") }; command.Parameters.Add(inputParam); @@ -345,7 +342,7 @@ public async Task TestStoredProcParamsForVectorFloat32Async( ParameterName = s_outputVectorParamName, SqlDbType = SqlDbTypeExtensions.Vector, Direction = ParameterDirection.Output, - Value = new SqlVector(3) + Value = SqlVector.CreateNull(VectorFloat32TestData.vectorColumnLength) }; command.Parameters.Add(outputParam); @@ -353,18 +350,18 @@ public async Task TestStoredProcParamsForVectorFloat32Async( await command.ExecuteNonQueryAsync(); // Validate the output parameter - var vector = outputParam.Value as SqlVector; - ValidateSqlVectorFloat32Object(vector.IsNull, vector, expectedValues, expectedSize, expectedLength); + var vector = (SqlVector)outputParam.Value; + ValidateSqlVectorFloat32Object(vector.IsNull, vector, expectedValues, expectedLength); // Validate error for conventional way of setting output parameters command.Parameters.Clear(); command.Parameters.Add(inputParam); - var outputParamWithoutVal = new SqlParameter(s_outputVectorParamName, SqlDbTypeExtensions.Vector, new SqlVector(3).Size) { Direction = ParameterDirection.Output }; + var outputParamWithoutVal = new SqlParameter(s_outputVectorParamName, SqlDbTypeExtensions.Vector, VectorFloat32TestData.IncorrectParamSize) { Direction = ParameterDirection.Output }; command.Parameters.Add(outputParamWithoutVal); await Assert.ThrowsAsync(async () => await command.ExecuteNonQueryAsync()); } - [ConditionalTheory(typeof(DataTestUtility), nameof(DataTestUtility.IsVectorSupported))] + [ConditionalTheory(typeof(DataTestUtility), nameof(DataTestUtility.IsAzureServer))] [InlineData(1)] [InlineData(2)] public void TestBulkCopyFromSqlTable(int bulkCopySourceMode) @@ -377,8 +374,8 @@ public void TestBulkCopyFromSqlTable(int bulkCopySourceMode) DataTable table = null; switch (bulkCopySourceMode) { - - case 1: + + case 1: // Use SqlServer table as source var insertCmd = new SqlCommand($"insert into {s_bulkCopySrcTableName} values (@VectorData)", sourceConnection); var vectorParam = new SqlParameter(s_vectorParamName, new SqlVector(VectorFloat32TestData.testData)); @@ -403,8 +400,8 @@ public void TestBulkCopyFromSqlTable(int bulkCopySourceMode) throw new ArgumentOutOfRangeException(nameof(bulkCopySourceMode), $"Unsupported bulk copy source mode: {bulkCopySourceMode}"); } - - + + //Bulkcopy from sql server table to destination table using SqlCommand sourceDataCommand = new SqlCommand($"SELECT Id, VectorData FROM {s_bulkCopySrcTableName}", sourceConnection); using SqlDataReader reader = sourceDataCommand.ExecuteReader(); @@ -453,7 +450,6 @@ public void TestBulkCopyFromSqlTable(int bulkCopySourceMode) Assert.True(!verifyReader.IsDBNull(0), "First row in the table is null."); Assert.Equal(VectorFloat32TestData.testData, ((SqlVector)verifyReader.GetSqlVector(0)).Memory.ToArray()); Assert.Equal(VectorFloat32TestData.testData.Length, ((SqlVector)verifyReader.GetSqlVector(0)).Length); - Assert.Equal(VectorFloat32TestData.sizeInbytes, ((SqlVector)verifyReader.GetSqlVector(0)).Size); // Verify that we have another row Assert.True(verifyReader.Read(), "Second row not found in the table"); @@ -462,10 +458,9 @@ public void TestBulkCopyFromSqlTable(int bulkCopySourceMode) Assert.True(verifyReader.IsDBNull(0)); Assert.Equal(Array.Empty(), ((SqlVector)verifyReader.GetSqlVector(0)).Memory.ToArray()); Assert.Equal(VectorFloat32TestData.testData.Length, ((SqlVector)verifyReader.GetSqlVector(0)).Length); - Assert.Equal(VectorFloat32TestData.sizeInbytes, ((SqlVector)verifyReader.GetSqlVector(0)).Size); } - [ConditionalTheory(typeof(DataTestUtility), nameof(DataTestUtility.IsVectorSupported))] + [ConditionalTheory(typeof(DataTestUtility), nameof(DataTestUtility.IsAzureServer))] [InlineData(1)] [InlineData(2)] public async Task TestBulkCopyFromSqlTableAsync(int bulkCopySourceMode) @@ -554,7 +549,6 @@ public async Task TestBulkCopyFromSqlTableAsync(int bulkCopySourceMode) var vector = await verifyReader.GetFieldValueAsync>(0); Assert.Equal(VectorFloat32TestData.testData, vector.Memory.ToArray()); Assert.Equal(VectorFloat32TestData.testData.Length, vector.Length); - Assert.Equal(VectorFloat32TestData.sizeInbytes, vector.Size); // Verify that we have another row Assert.True(await verifyReader.ReadAsync(), "Second row not found in the table"); @@ -564,16 +558,15 @@ public async Task TestBulkCopyFromSqlTableAsync(int bulkCopySourceMode) vector = await verifyReader.GetFieldValueAsync>(0); Assert.Equal(Array.Empty(), vector.Memory.ToArray()); Assert.Equal(VectorFloat32TestData.testData.Length, vector.Length); - Assert.Equal(VectorFloat32TestData.sizeInbytes, vector.Size); } - [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.IsVectorSupported))] + [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.IsAzureServer))] public void TestInsertVectorsFloat32WithPrepare() { SqlConnection conn = new SqlConnection(s_connectionString); conn.Open(); SqlCommand command = new SqlCommand(s_insertCmdString, conn); - SqlParameter vectorParam = new SqlParameter("@VectorData", SqlDbTypeExtensions.Vector, new SqlVector(3).Size); + SqlParameter vectorParam = new SqlParameter("@VectorData", SqlDbTypeExtensions.Vector); command.Parameters.Add(vectorParam); command.Prepare(); for (int i = 0; i < 10; i++) diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/VectorTest/VectorAPIValidationTest.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/VectorTest/VectorAPIValidationTest.cs new file mode 100644 index 0000000000..27c857f5c5 --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/VectorTest/VectorAPIValidationTest.cs @@ -0,0 +1,92 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using Microsoft.Data.SqlTypes; +using Xunit; + +namespace Microsoft.Data.SqlClient.ManualTesting.Tests.SQL.VectorTest +{ + public sealed class VectorAPIValidationTest + { + // We need these testcases to validate ref assembly for vector APIs + // Unit tests are covered under SqlVectorTest.cs + [Fact] + public void ValidateVectorSqlDbType() + { + // Validate that SqlVector is a valid type and has valid SqlDbType + Assert.True(typeof(SqlVector).IsValueType, "SqlVector should be a value type."); + Assert.Equal(36, (int)SqlDbTypeExtensions.Vector); + } + + [Fact] + public void TestSqlVectorCreationAPIWithFloatArr() + { + // Validate ctor1 with float[] : public SqlVector(System.ReadOnlyMemory memory) { } + var testData = new float[] { 1.1f, 2.2f, 3.3f }; + var vector = new SqlVector(testData); + Assert.Equal(testData, vector.Memory.ToArray()); + Assert.Equal(3, vector.Length); + } + + [Fact] + public void TestSqlVectorCreationAPIWithROM() + { + // Validate ctor2 with ReadOnlyMemory : public SqlVector(ReadOnlyMemory memory) { } + var testData = new ReadOnlyMemory(new float[] { 1.1f, 2.2f, 3.3f }); + var vector = new SqlVector(testData); + Assert.Equal(testData.ToArray(), vector.Memory.ToArray()); + Assert.Equal(3, vector.Length); + } + + [Fact] + public void TestSqlVectorCreationAPICreateNull() + { + // Validate CreateNull method + var vector = SqlVector.CreateNull(5); + Assert.True(vector.IsNull); + Assert.Equal(5, vector.Length); + } + + [Fact] + public void TestIsNullProperty() + { + //Validate IsNull property + var testData = new ReadOnlyMemory(new float[] { 1.1f, 2.2f, 3.3f }); + var vector = new SqlVector(testData); + Assert.False(vector.IsNull, "IsNull should be false for non-null vector."); + vector = SqlVector.CreateNull(3); + Assert.True(vector.IsNull, "IsNull should be true for null vector."); + } + + [Fact] + public void TestNullProperty() + { + // Validate Null property returns null + Assert.Null(SqlVector.Null); + } + + [Fact] + public void TestLengthProperty() + { + // Validate Length property is correctly populated for null and non-null vectors + var testData = new float[] { 1.1f, 2.2f, 3.3f }; + var vector = new SqlVector(testData); + Assert.Equal(3, vector.Length); + vector = SqlVector.CreateNull(3); + Assert.Equal(3, vector.Length); + } + + [Fact] + public void TestMemoryProperty() + { + // Validate Memory property is correctly populated for non-null and null vectors + var testData = new float[] { 1.1f, 2.2f, 3.3f }; + var vector = new SqlVector(testData); + Assert.Equal(testData, vector.Memory.ToArray()); + vector = SqlVector.CreateNull(3); + Assert.True(vector.Memory.IsEmpty, "Null vector of given size point to empty ROM"); + } + } +} diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/VectorTest/VectorTypeBackwardCompatibilityTests.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/VectorTest/VectorTypeBackwardCompatibilityTests.cs index 5fb9cf7625..d38323b72f 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/VectorTest/VectorTypeBackwardCompatibilityTests.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/VectorTest/VectorTypeBackwardCompatibilityTests.cs @@ -17,14 +17,14 @@ public sealed class VectorTypeBackwardCompatibilityTests : IDisposable { private readonly ITestOutputHelper _output; private static readonly string s_connectionString = ManualTesting.Tests.DataTestUtility.TCPConnectionString; - private static readonly string s_tableName = DataTestUtility.GetUniqueName("VectorTestTable"); - private static readonly string s_bulkCopySrcTableName = DataTestUtility.GetUniqueName("VectorBulkCopyTestTable"); + private static readonly string s_tableName = DataTestUtility.GetShortName("VectorTestTable"); + private static readonly string s_bulkCopySrcTableName = DataTestUtility.GetShortName("VectorBulkCopyTestTable"); private static readonly string s_bulkCopySrcTableDef = $@"(Id INT PRIMARY KEY IDENTITY, VectorData varchar(max) NULL)"; private static readonly string s_tableDefinition = $@"(Id INT PRIMARY KEY IDENTITY, VectorData vector(3) NULL)"; private static readonly string s_selectCmdString = $"SELECT VectorData FROM {s_tableName} ORDER BY Id DESC"; private static readonly string s_insertCmdString = $"INSERT INTO {s_tableName} (VectorData) VALUES (@VectorData)"; private static readonly string s_vectorParamName = $"@VectorData"; - private static readonly string s_storedProcName = DataTestUtility.GetUniqueName("VectorsAsVarcharSp"); + private static readonly string s_storedProcName = DataTestUtility.GetShortName("VectorsAsVarcharSp"); private static readonly string s_storedProcBody = $@" @InputVectorJson VARCHAR(MAX), -- Input: Serialized float[] as JSON string @OutputVectorJson VARCHAR(MAX) OUTPUT -- Output: Echoed back from latest inserted row @@ -81,7 +81,7 @@ private void ValidateInsertedData(SqlConnection connection, float[] expectedData } } - [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.IsVectorSupported))] + [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.IsAzureServer))] public void TestVectorDataInsertionAsVarchar() { float[] data = { 1.1f, 2.2f, 3.3f }; @@ -173,7 +173,7 @@ private async Task ValidateInsertedDataAsync(SqlConnection connection, float[] e } } - [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.IsVectorSupported))] + [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.IsAzureServer))] public async Task TestVectorParameterInitializationAsync() { float[] data = { 1.1f, 2.2f, 3.3f }; @@ -245,7 +245,7 @@ public async Task TestVectorParameterInitializationAsync() await ValidateInsertedDataAsync(conn, null); } - [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.IsVectorSupported))] + [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.IsAzureServer))] public void TestVectorDataReadsAsVarchar() { float[] data = { 1.1f, 2.2f, 3.3f }; @@ -302,7 +302,7 @@ public void TestVectorDataReadsAsVarchar() Assert.Throws(() => reader.GetFieldValue(0)); } - [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.IsVectorSupported))] + [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.IsAzureServer))] public async Task TestVectorDataReadsAsVarcharAsync() { float[] data = { 1.1f, 2.2f, 3.3f }; @@ -359,7 +359,7 @@ public async Task TestVectorDataReadsAsVarcharAsync() await Assert.ThrowsAsync(async () => await reader2.GetFieldValueAsync(0)); } - [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.IsVectorSupported))] + [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.IsAzureServer))] public void TestStoredProcParamsForVectorAsVarchar() { // Test data @@ -405,7 +405,7 @@ public void TestStoredProcParamsForVectorAsVarchar() Assert.True(outputParam.Value == DBNull.Value); } - [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.IsVectorSupported))] + [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.IsAzureServer))] public async Task TestStoredProcParamsForVectorAsVarcharAsync() { // Test data @@ -456,7 +456,7 @@ public async Task TestStoredProcParamsForVectorAsVarcharAsync() Assert.True(outputParam.Value == DBNull.Value); } - [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.IsVectorSupported))] + [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.IsAzureServer))] public void TestSqlBulkCopyForVectorAsVarchar() { //Setup source with test data and create destination table for bulkcopy. @@ -521,7 +521,7 @@ public void TestSqlBulkCopyForVectorAsVarchar() Assert.True(verifyReader.IsDBNull(0)); } - [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.IsVectorSupported))] + [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.IsAzureServer))] public async Task TestSqlBulkCopyForVectorAsVarcharAsync() { //Setup source with test data and create destination table for bulkcopy. @@ -586,7 +586,7 @@ public async Task TestSqlBulkCopyForVectorAsVarcharAsync() Assert.True(await verifyReader.IsDBNullAsync(0)); } - [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.IsVectorSupported))] + [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.IsAzureServer))] public void TestInsertVectorsAsVarcharWithPrepare() { SqlConnection conn = new SqlConnection(s_connectionString); diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/TracingTests/DiagnosticTest.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/TracingTests/DiagnosticTest.cs index b8649d43d2..4ae426fcbb 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/TracingTests/DiagnosticTest.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/TracingTests/DiagnosticTest.cs @@ -483,7 +483,7 @@ public void ConnectionOpenAsyncErrorTest() }).Dispose(); } - private static void CollectStatisticsDiagnostics(Action sqlOperation, bool enableServerLogging = false, [CallerMemberName] string methodName = "") + private static void CollectStatisticsDiagnostics(Action sqlOperation, [CallerMemberName] string methodName = "") { bool statsLogged = false; bool operationHasError = false; @@ -670,10 +670,19 @@ private static void CollectStatisticsDiagnostics(Action sqlOperation, bo { Console.WriteLine(string.Format("Test: {0} Enabled Listeners", methodName)); - using (var server = TestTdsServer.StartServerWithQueryEngine(new DiagnosticsQueryEngine(), enableLog: enableServerLogging, methodName: methodName)) + + using (var server = new TdsServer(new DiagnosticsQueryEngine(), new TdsServerArguments())) { + server.Start(methodName); Console.WriteLine(string.Format("Test: {0} Started Server", methodName)); - sqlOperation(server.ConnectionString); + + var connectionString = new SqlConnectionStringBuilder + { + DataSource = $"localhost,{server.EndPoint.Port}", + Encrypt = SqlConnectionEncryptOption.Optional + }.ConnectionString; + + sqlOperation(connectionString); Console.WriteLine(string.Format("Test: {0} SqlOperation Successful", methodName)); @@ -859,11 +868,17 @@ private static async Task CollectStatisticsDiagnosticsAsync(Func s using (DiagnosticListener.AllListeners.Subscribe(diagnosticListenerObserver)) { Console.WriteLine(string.Format("Test: {0} Enabled Listeners", methodName)); - using (var server = TestTdsServer.StartServerWithQueryEngine(new DiagnosticsQueryEngine(), methodName: methodName)) + using (var server = new TdsServer(new DiagnosticsQueryEngine(), new TdsServerArguments())) { + server.Start(methodName); Console.WriteLine(string.Format("Test: {0} Started Server", methodName)); - await sqlOperation(server.ConnectionString); + var connectionString = new SqlConnectionStringBuilder + { + DataSource = $"localhost,{server.EndPoint.Port}", + Encrypt = SqlConnectionEncryptOption.Optional + }.ConnectionString; + await sqlOperation(connectionString); Console.WriteLine(string.Format("Test: {0} SqlOperation Successful", methodName)); @@ -890,7 +905,7 @@ private static T GetPropertyValueFromType(object obj, string propName) public class DiagnosticsQueryEngine : QueryEngine { - public DiagnosticsQueryEngine() : base(new TDSServerArguments()) + public DiagnosticsQueryEngine() : base(new TdsServerArguments()) { } diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/TracingTests/MetricsTest.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/TracingTests/MetricsTest.cs index 4e90bbf6c7..bd70084fab 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/TracingTests/MetricsTest.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/TracingTests/MetricsTest.cs @@ -4,6 +4,7 @@ using System; using System.Diagnostics; using System.Reflection; +using System.Threading.Tasks; using System.Transactions; using Xunit; @@ -177,6 +178,143 @@ public void StasisCounters_Functional() Assert.Equal(0, SqlClientEventSourceProps.StasisConnections); } + [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup), nameof(DataTestUtility.IsNotAzureSynapse))] + public void TransactedConnectionPool_VerifyActiveConnectionCounters() + { + // This test verifies that the active connection count metric never goes negative + // when connections are returned to the pool while enlisted in a transaction. + // This is a regression test for issue #3640 where an extra DeactivateConnection + // call was causing the active connection count to go negative. + + // Arrange + var stringBuilder = new SqlConnectionStringBuilder(DataTestUtility.TCPConnectionString) + { + Pooling = true, + Enlist = false, + MinPoolSize = 0, + MaxPoolSize = 10 + }; + + // Clear pools to start fresh + ClearConnectionPools(); + + long initialActiveSoftConnections = SqlClientEventSourceProps.ActiveSoftConnections; + long initialActiveHardConnections = SqlClientEventSourceProps.ActiveHardConnections; + long initialActiveConnections = SqlClientEventSourceProps.ActiveConnections; + + // Act and Assert + // Verify counters at each step in the lifecycle of a transacted connection + using (var txScope = new TransactionScope()) + { + using (var conn = new SqlConnection(stringBuilder.ToString())) + { + conn.Open(); + conn.EnlistTransaction(System.Transactions.Transaction.Current); + + if (SupportsActiveConnectionCounters) + { + // Connection should be active + Assert.Equal(initialActiveSoftConnections + 1, SqlClientEventSourceProps.ActiveSoftConnections); + Assert.Equal(initialActiveHardConnections + 1, SqlClientEventSourceProps.ActiveHardConnections); + Assert.Equal(initialActiveConnections + 1, SqlClientEventSourceProps.ActiveConnections); + } + + conn.Close(); + + // Connection is returned to pool but still in transaction (stasis) + if (SupportsActiveConnectionCounters) + { + // Connection should be deactivated (returned to pool) + Assert.Equal(initialActiveSoftConnections, SqlClientEventSourceProps.ActiveSoftConnections); + Assert.Equal(initialActiveHardConnections + 1, SqlClientEventSourceProps.ActiveHardConnections); + Assert.Equal(initialActiveConnections, SqlClientEventSourceProps.ActiveConnections); + } + } + + // Completing the transaction after the connection is closed ensures that the connection + // is in the transacted pool at the time the transaction ends. This verifies that the + // transition from the transacted pool back to the main pool properly updates the counters. + txScope.Complete(); + } + + if (SupportsActiveConnectionCounters) + { + Assert.Equal(initialActiveSoftConnections, SqlClientEventSourceProps.ActiveSoftConnections); + Assert.Equal(initialActiveHardConnections + 1, SqlClientEventSourceProps.ActiveHardConnections); + Assert.Equal(initialActiveConnections, SqlClientEventSourceProps.ActiveConnections); + } + } + + #if NET + // Note: DbConnection.CloseAsync is not available in .NET Framework + [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup), nameof(DataTestUtility.IsNotAzureSynapse))] + public async Task TransactedConnectionPool_VerifyActiveConnectionCounters_Async() + { + // This test verifies that the active connection count metric never goes negative + // when connections are returned to the pool while enlisted in a transaction. + // This is a regression test for issue #3640 where an extra DeactivateConnection + // call was causing the active connection count to go negative. + + // Arrange + var stringBuilder = new SqlConnectionStringBuilder(DataTestUtility.TCPConnectionString) + { + Pooling = true, + Enlist = false, + MinPoolSize = 0, + MaxPoolSize = 10 + }; + + // Clear pools to start fresh + ClearConnectionPools(); + + long initialActiveSoftConnections = SqlClientEventSourceProps.ActiveSoftConnections; + long initialActiveHardConnections = SqlClientEventSourceProps.ActiveHardConnections; + long initialActiveConnections = SqlClientEventSourceProps.ActiveConnections; + + // Act and Assert + // Verify counters at each step in the lifecycle of a transacted connection + using (var txScope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled)) + { + using (var conn = new SqlConnection(stringBuilder.ToString())) + { + await conn.OpenAsync(); + conn.EnlistTransaction(System.Transactions.Transaction.Current); + + if (SupportsActiveConnectionCounters) + { + // Connection should be active + Assert.Equal(initialActiveSoftConnections + 1, SqlClientEventSourceProps.ActiveSoftConnections); + Assert.Equal(initialActiveHardConnections + 1, SqlClientEventSourceProps.ActiveHardConnections); + Assert.Equal(initialActiveConnections + 1, SqlClientEventSourceProps.ActiveConnections); + } + + await conn.CloseAsync(); + + // Connection is returned to pool but still in transaction (stasis) + if (SupportsActiveConnectionCounters) + { + // Connection should be deactivated (returned to pool) + Assert.Equal(initialActiveSoftConnections, SqlClientEventSourceProps.ActiveSoftConnections); + Assert.Equal(initialActiveHardConnections + 1, SqlClientEventSourceProps.ActiveHardConnections); + Assert.Equal(initialActiveConnections, SqlClientEventSourceProps.ActiveConnections); + } + } + + // Completing the transaction after the connection is closed ensures that the connection + // is in the transacted pool at the time the transaction ends. This verifies that the + // transition from the transacted pool back to the main pool properly updates the counters. + txScope.Complete(); + } + + if (SupportsActiveConnectionCounters) + { + Assert.Equal(initialActiveSoftConnections, SqlClientEventSourceProps.ActiveSoftConnections); + Assert.Equal(initialActiveHardConnections + 1, SqlClientEventSourceProps.ActiveHardConnections); + Assert.Equal(initialActiveConnections, SqlClientEventSourceProps.ActiveConnections); + } + } + #endif + [ActiveIssue("https://github.com/dotnet/SqlClient/issues/3031")] [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup))] public void ReclaimedConnectionsCounter_Functional() diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/TracingTests/TestTdsServer.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/TracingTests/TestTdsServer.cs deleted file mode 100644 index 45a817c46e..0000000000 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/TracingTests/TestTdsServer.cs +++ /dev/null @@ -1,93 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -using System; -using System.Linq; -using System.Net; -using System.Net.Sockets; -using System.Runtime.CompilerServices; -using System.Security.Authentication; -using System.Security.Cryptography.X509Certificates; -using Microsoft.SqlServer.TDS.EndPoint; -using Microsoft.SqlServer.TDS.PreLogin; -using Microsoft.SqlServer.TDS.Servers; - -namespace Microsoft.Data.SqlClient.ManualTesting.Tests -{ - internal class TestTdsServer : GenericTDSServer, IDisposable - { - private const int DefaultConnectionTimeout = 5; - - private TDSServerEndPoint _endpoint = null; - - private SqlConnectionStringBuilder _connectionStringBuilder; - - public TestTdsServer(TDSServerArguments args) : base(args) { } - - public TestTdsServer(QueryEngine engine, TDSServerArguments args) : base(args) - { - Engine = engine; - } - - public static TestTdsServer StartServerWithQueryEngine(QueryEngine engine, bool enableFedAuth = false, bool enableLog = false, - int connectionTimeout = DefaultConnectionTimeout, [CallerMemberName] string methodName = "", - X509Certificate2 encryptionCertificate = null, SslProtocols encryptionProtocols = SslProtocols.Tls12, TDSPreLoginTokenEncryptionType encryptionType = TDSPreLoginTokenEncryptionType.NotSupported) - { - TDSServerArguments args = new TDSServerArguments() - { - Log = enableLog ? Console.Out : null, - }; - - if (enableFedAuth) - { - args.FedAuthRequiredPreLoginOption = SqlServer.TDS.PreLogin.TdsPreLoginFedAuthRequiredOption.FedAuthRequired; - } - - args.EncryptionCertificate = encryptionCertificate; - args.EncryptionProtocols = encryptionProtocols; - args.Encryption = encryptionType; - - TestTdsServer server = engine == null ? new TestTdsServer(args) : new TestTdsServer(engine, args); - - server._endpoint = new TDSServerEndPoint(server) { ServerEndPoint = new IPEndPoint(IPAddress.Any, 0) }; - server._endpoint.EndpointName = methodName; - // The server EventLog should be enabled as it logs the exceptions. - server._endpoint.EventLog = enableLog ? Console.Out : null; - server._endpoint.Start(); - - int port = server._endpoint.ServerEndPoint.Port; - - server._connectionStringBuilder = new SqlConnectionStringBuilder() - { - DataSource = "localhost," + port, - ConnectTimeout = connectionTimeout, - }; - - if (encryptionType == TDSPreLoginTokenEncryptionType.Off || - encryptionType == TDSPreLoginTokenEncryptionType.None || - encryptionType == TDSPreLoginTokenEncryptionType.NotSupported) - { - server._connectionStringBuilder.Encrypt = SqlConnectionEncryptOption.Optional; - } - else - { - server._connectionStringBuilder.Encrypt = SqlConnectionEncryptOption.Mandatory; - } - - server.ConnectionString = server._connectionStringBuilder.ConnectionString; - return server; - } - - public static TestTdsServer StartTestServer(bool enableFedAuth = false, bool enableLog = false, - int connectionTimeout = DefaultConnectionTimeout, [CallerMemberName] string methodName = "", - X509Certificate2 encryptionCertificate = null, SslProtocols encryptionProtocols = SslProtocols.Tls12, TDSPreLoginTokenEncryptionType encryptionType = TDSPreLoginTokenEncryptionType.NotSupported) - { - return StartServerWithQueryEngine(null, enableFedAuth, enableLog, connectionTimeout, methodName, encryptionCertificate, encryptionProtocols, encryptionType); - } - - public void Dispose() => _endpoint?.Stop(); - - public string ConnectionString { get; private set; } - } -} diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/TracingTests/XEventsTracingTest.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/TracingTests/XEventsTracingTest.cs index 40fde20faa..bcb23d11bd 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/TracingTests/XEventsTracingTest.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/TracingTests/XEventsTracingTest.cs @@ -7,12 +7,22 @@ using System.Xml; using System.Xml.XPath; using Xunit; +using Xunit.Abstractions; + +#nullable enable namespace Microsoft.Data.SqlClient.ManualTesting.Tests { public class XEventsTracingTest { - [ConditionalTheory(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup))] + private readonly string _testName; + + public XEventsTracingTest(ITestOutputHelper outputHelper) + { + _testName = DataTestUtility.CurrentTestName(outputHelper); + } + + [ConditionalTheory(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup), nameof(DataTestUtility.IsNotAzureSynapse), nameof(DataTestUtility.IsNotManagedInstance))] [InlineData("SELECT @@VERSION", System.Data.CommandType.Text, "sql_statement_starting")] [InlineData("sp_help", System.Data.CommandType.StoredProcedure, "rpc_starting")] public void XEventActivityIDConsistentWithTracing(string query, System.Data.CommandType commandType, string xEvent) @@ -21,42 +31,46 @@ public void XEventActivityIDConsistentWithTracing(string query, System.Data.Comm // where it can be recorded in an XEvent session. This is documented at: // https://learn.microsoft.com/en-us/sql/relational-databases/native-client/features/accessing-diagnostic-information-in-the-extended-events-log - using (SqlConnection xEventManagementConnection = new SqlConnection(DataTestUtility.TCPConnectionString)) - using (DataTestUtility.XEventScope xEventSession = new DataTestUtility.XEventScope(xEventManagementConnection, - @"ADD EVENT SQL_STATEMENT_STARTING (ACTION (client_connection_id)), - ADD EVENT RPC_STARTING (ACTION (client_connection_id))", - "ADD TARGET ring_buffer")) - { - Guid connectionId; - HashSet ids; + using SqlConnection activityConnection = new(DataTestUtility.TCPConnectionString); + activityConnection.Open(); - using (DataTestUtility.MDSEventListener TraceListener = new()) - using (SqlConnection connection = new(DataTestUtility.TCPConnectionString)) - { - connection.Open(); - connectionId = connection.ClientConnectionId; + Guid connectionId = activityConnection.ClientConnectionId; + HashSet ids; - using SqlCommand command = new(query, connection) { CommandType = commandType }; - using SqlDataReader reader = command.ExecuteReader(); - while (reader.Read()) - { - // Flush data - } + using SqlConnection xEventManagementConnection = new(DataTestUtility.TCPConnectionString); + xEventManagementConnection.Open(); + + using DataTestUtility.XEventScope xEventSession = new( + _testName, + xEventManagementConnection, + $@"ADD EVENT SQL_STATEMENT_STARTING (ACTION (client_connection_id) WHERE (client_connection_id='{connectionId}')), + ADD EVENT RPC_STARTING (ACTION (client_connection_id) WHERE (client_connection_id='{connectionId}'))", + "ADD TARGET ring_buffer"); - ids = TraceListener.ActivityIDs; + using (DataTestUtility.MDSEventListener TraceListener = new()) + { + using SqlCommand command = new(query, activityConnection) { CommandType = commandType }; + using SqlDataReader reader = command.ExecuteReader(); + while (reader.Read()) + { + // Flush data } - XmlDocument eventList = xEventSession.GetEvents(); - // Get the associated activity ID from the XEvent session. We expect to see the same ID in the trace as well. - string activityId = GetCommandActivityId(query, xEvent, connectionId, eventList); - - Assert.Contains(activityId, ids); + ids = TraceListener.ActivityIDs; } + + XmlDocument eventList = xEventSession.GetEvents(); + // Get the associated activity ID from the XEvent session. We expect to see the same ID in the trace as well. + string activityId = GetCommandActivityId(query, xEvent, connectionId, eventList); + + Assert.Contains(activityId, ids); } private static string GetCommandActivityId(string commandText, string eventName, Guid connectionId, XmlDocument xEvents) { - XPathNavigator xPathRoot = xEvents.CreateNavigator(); + XPathNavigator? xPathRoot = xEvents.CreateNavigator(); + Assert.NotNull(xPathRoot); + // The transferred activity ID is attached to the "attach_activity_id_xfer" action within // the "sql_statement_starting" and the "rpc_starting" events. XPathNodeIterator statementStartingQuery = xPathRoot.Select( @@ -67,7 +81,9 @@ private static string GetCommandActivityId(string commandText, string eventName, Assert.Equal(1, statementStartingQuery.Count); Assert.True(statementStartingQuery.MoveNext()); - XPathNavigator activityIdElement = statementStartingQuery.Current.SelectSingleNode("action[@name='attach_activity_id_xfer']/value"); + XPathNavigator? current = statementStartingQuery.Current; + Assert.NotNull(current); + XPathNavigator? activityIdElement = current.SelectSingleNode("action[@name='attach_activity_id_xfer']/value"); Assert.NotNull(activityIdElement); Assert.NotNull(activityIdElement.Value); diff --git a/src/Microsoft.Data.SqlClient/tests/StressTests/Directory.Build.props b/src/Microsoft.Data.SqlClient/tests/StressTests/Directory.Build.props new file mode 100644 index 0000000000..66fbacae6c --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/StressTests/Directory.Build.props @@ -0,0 +1,18 @@ + + + + + + + + + net462;net47;net471;net472;net48;net481;net8.0;net9.0 + + + latest + + + diff --git a/src/Microsoft.Data.SqlClient/tests/StressTests/Directory.Packages.props b/src/Microsoft.Data.SqlClient/tests/StressTests/Directory.Packages.props new file mode 100644 index 0000000000..45b1a5018f --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/StressTests/Directory.Packages.props @@ -0,0 +1,38 @@ + + + + + + + true + true + + + + + + + $(MdsPackageVersion) + + + + + + + + + + 6.1.0-preview2.25178.5 + + + + + + + + + diff --git a/src/Microsoft.Data.SqlClient/tests/StressTests/IMonitorLoader/IMonitorLoader.cs b/src/Microsoft.Data.SqlClient/tests/StressTests/IMonitorLoader/IMonitorLoader.cs new file mode 100644 index 0000000000..1dcbde121f --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/StressTests/IMonitorLoader/IMonitorLoader.cs @@ -0,0 +1,31 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; + +namespace Monitoring +{ + public interface IMonitorLoader + { + string HostMachine { get; set; } + string AssemblyPath { get; set; } + string TestName { get; set; } + bool Enabled { get; set; } + + void Action(MonitorLoaderUtils.MonitorAction monitoraction); + void AddPerfData(MonitorMetrics data); + Dictionary GetPerfData(); + } + + public class MonitorLoaderUtils + { + public enum MonitorAction + { + Initialize, + Start, + Stop, + DoNothing + } + } +} diff --git a/src/Microsoft.Data.SqlClient/tests/StressTests/IMonitorLoader/IMonitorLoader.csproj b/src/Microsoft.Data.SqlClient/tests/StressTests/IMonitorLoader/IMonitorLoader.csproj new file mode 100644 index 0000000000..0968e1837f --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/StressTests/IMonitorLoader/IMonitorLoader.csproj @@ -0,0 +1,6 @@ + + + Monitoring + Monitoring + + diff --git a/src/Microsoft.Data.SqlClient/tests/StressTests/IMonitorLoader/MonitorMetrics.cs b/src/Microsoft.Data.SqlClient/tests/StressTests/IMonitorLoader/MonitorMetrics.cs new file mode 100644 index 0000000000..ed37544e4e --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/StressTests/IMonitorLoader/MonitorMetrics.cs @@ -0,0 +1,96 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; + +namespace Monitoring +{ + public class MonitorMetrics + { + private string _name; + private string _strValue; + private string _unit; + private bool _isPrimary; + private bool _isHigherBetter; + private double _dblValue; + private long _lngValue; + private char _valueType; // D=double, L=long, S=String + + public MonitorMetrics(string name, string value, string unit, bool HigherIsBetter, bool Primary) + { + _name = name; + _strValue = value; + _unit = unit; + _valueType = 'S'; + _isHigherBetter = HigherIsBetter; + _isPrimary = Primary; + } + + public MonitorMetrics(string name, double value, string unit, bool HigherIsBetter, bool Primary) + { + _name = name; + _dblValue = value; + _unit = unit; + _valueType = 'D'; + _isHigherBetter = HigherIsBetter; + _isPrimary = Primary; + } + + public MonitorMetrics(string name, long value, string unit, bool HigherIsBetter, bool Primary) + { + _name = name; + _lngValue = value; + _unit = unit; + _valueType = 'L'; + _isHigherBetter = HigherIsBetter; + _isPrimary = Primary; + } + + public string GetName() + { + return _name; + } + + public string GetUnit() + { + return _unit; + } + + public bool GetPrimary() + { + return _isPrimary; + } + + public bool GetHigherIsBetter() + { + return _isHigherBetter; + } + + public char GetValueType() + { + return _valueType; + } + + public string GetStringValue() + { + if (_valueType == 'S') + return _strValue; + throw new Exception("Value is not a string"); + } + + public double GetDoubleValue() + { + if (_valueType == 'D') + return _dblValue; + throw new Exception("Value is not a double"); + } + + public long GetLongValue() + { + if (_valueType == 'L') + return _lngValue; + throw new Exception("Value is not a long"); + } + } +} diff --git a/src/Microsoft.Data.SqlClient/tests/StressTests/NuGet.config b/src/Microsoft.Data.SqlClient/tests/StressTests/NuGet.config new file mode 100644 index 0000000000..19c2531f5d --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/StressTests/NuGet.config @@ -0,0 +1,13 @@ + + + + + + + diff --git a/src/Microsoft.Data.SqlClient/tests/StressTests/Readme.md b/src/Microsoft.Data.SqlClient/tests/StressTests/Readme.md new file mode 100644 index 0000000000..3ae5c9e3df --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/StressTests/Readme.md @@ -0,0 +1,230 @@ +# Microsoft.Data.SqlClient Stress Test + +This Stress testing application for `Microsoft.Data.SqlClient` is under progress. + +This project intends to help finding a certain level of effectiveness under +unfavorable conditions, and verifying the mode of failures. + +This is a console application targeting all frameworks supported by MDS, +currently: + +- .NET 8.0 +- .NET T9.0 +- .NET Framework 4.6.2 +- .NET Framework 4.7 +- .NET Framework 4.7.1 +- .NET Framework 4.7.2 +- .NET Framework 4.8 +- .NET Framework 4.8.1 + +## Purpose of application for developers + +Define fuzz tests for all new features/APIs in the driver and to be run before +every GA release. + +## Pre-Requisites + +Required in the config file: + +|Field|Values|Description| +|-|-|-| +|`name`||Stress testing source configuration name.| +|`type`|`SqlServer`|Only `SqlServer` is acceptable.| +|`isDefault`|`true`, `false`|If there is a source node with `isDefault=true`, this node is returned.| +|`dataSource`||SQL Server data source name.| +|`user`||User Id to connect the server.| +|`password`||Paired password with the user.| +|`supportsWindowsAuthentication`|`true`, `false`|Tries to use integrated security in connection string mixed with SQL Server authentication if it set to `true` by applying the randomization.| +|`isLocal`|`true`, `false`|`true` means database is local.| +|`disableMultiSubnetFailover`|`true`, `false`|Tries to add Multi-subnet Failover fake host entries when it equals `true`.| +|`disableNamedPipes`|`true`, `false`|`true` means the connections will create just using tcp protocol.| +|`encrypt`|`true`, `false`|Assigns the encrypt property of the connection strings.| + +Note: The database user must have permission to create and drop databases. +Each execution of the stress tests will create a database with a name like: + +- `StressTests-` + +The database will be dropped as a best effort once testing is complete. This +allows for multiple test runs to execute in parallel against the same database +server without colliding. + +## Adding new Tests + +- [ToDo] + +## Building the application + +To build the application using the `StressTests.slnx` solution: + +```bash +dotnet build [-c|--configuration ] +``` + +```bash +# Builds the application for the Client Os in `Debug` Configuration for `AnyCpu` +# platform. +# +# All supported target frameworks are built by default. + +$ dotnet build +``` + +```bash +# Build the application for .Net framework 4.8.1 with `Debug` configuration. + +$ dotnet build -f net481 +``` + +```bash +# Build the application for .Net 9.0 with `Release` configuration. + +$ dotnet build -f net9.0 -c Release +``` + +```bash +# Cleans all build directories + +$ dotnet clean +``` + +## Running tests + +After building the application, find the built folder with target framework and +run the `stresstest.exe` file with required arguments. + +Find the result in a log file inside the `logs` folder besides the command +prompt. + +You may specify the config file by supplying an environment variable that +points to the file: + +- `STRESS_CONFIG_FILE=/path/to/my/config.jsonc` + +## Command prompt + +You must run the stress tests from the root of the Stress Tests project +directory (i.e. the same directory this readme file is in). + +```bash +# Linux +$ cd /home/paul/dev/SqlClient/src/Microsoft.Data.SqlClient/tests/StressTests + +# Via dotnet run CLI: +$ dotnet run --no-build -f net9.0 --project SqlClient.Stress.Runner/SqlClient.Stress.Runner.csproj -- -a SqlClient.Stress.Tests + +# Via dotnet CLI: +$ dotnet SqlClient.Stress.Runner/bin/Debug/net9.0/stresstest.dll -a SqlClient.Stress.Tests + +# With a specific config file and all output to console: +$ dotnet run --no-build -f net9.0 --project SqlClient.Stress.Runner/SqlClient.Stress.Runner.csproj -e STRESS_CONFIG_FILE=/path/to/config.jsonc -- -a SqlClient.Stress.Tests -console +``` + +```powershell +# Windows +> cd \dev\SqlClient\src\Microsoft.Data.SqlClient\tests\StressTests + +# Via dotnet run CLI: +> dotnet run --no-build -f net9.0 --project SqlClient.Stress.Runner\SqlClient.Stress.Runner.csproj -- -a SqlClient.Stress.Tests + +# Via executable: +> .\SqlClient.Stress.Runner\bin\Debug\net481\stresstest.exe -a SqlClient.Stress.Tests + +# With a specific config file and all output to console: +> dotnet run --no-build -f net9.0 --project SqlClient.Stress.Runner\SqlClient.Stress.Runner.csproj -e STRESS_CONFIG_FILE=c:\path\to\config.jsonc -- -a SqlClient.Stress.Tests -console +``` + +## Supported arguments + +|Argument|Values|Description| +|-|-|-| +|-all||Run all tests - best for debugging, not perf measurements.| +|-verify||Run in functional verification mode. [not implemented]| +|-duration|<n>|Duration of the test in seconds. Default value is 1 second.| +|-threads|<n>|Number of threads to use. Default value is 16.| +|-override|<name> <value>|Override the value of a test property.| +|-test|<name1;name2>|Run specific test(s).| +|-debug||Print process ID in the beginning and wait for Enter (to give your time to attach the debugger).| +|-console||Emit all output to the console instead of a log file.| +|-exceptionThreshold|<n>|An optional limit on exceptions which will be caught. When reached, test will halt.| +|-monitorenabled|true, false|True or False to enable monitoring. Default is false [not implemented]| +|-randomSeed||Enables setting of the random number generator used internally. This serves both the purpose of helping to improve reproducibility and making it deterministic from Chess's perspective for a given schedule. Default is 0.| +|-filter|<filter>|Run tests whose stress test attributes match the given filter. Filter is not applied if attribute does not implement ITestAttributeFilter. Example: -filter TestType=Query,Update;IsServerTest=True| +|-printMethodName||Print tests' title in console window| +|-deadlockdetection|true, false|True or False to enable deadlock detection. Default is `false`.| + +```powershell +# Run the application for a built target framework and all discovered tests +# without debugger attached. + +> .\stresstest.exe -a SqlClient.Stress.Tests -all +``` + +```powershell +# Run the application for a built target framework and all discovered tests +# without debugger attached and shows the test methods' names. + +> .\stresstest.exe -a SqlClient.Stress.Tests -all -printMethodName +``` + +```powershell +# Run the application for a built target framework and all discovered tests and +# will wait for debugger to be attached. + +> .\stresstest.exe -a SqlClient.Stress.Tests -all -debug +``` + +```powershell +# Run the application for a built target framework and +# "TestExecuteXmlReaderAsyncCancellation" test without debugger attached. + +> .\stresstest.exe -a SqlClient.Stress.Tests -test TestExecuteXmlReaderAsyncCancellation +``` + +```powershell +# Run the application for a built target framework and +# "TestExecuteXmlReaderAsyncCancellation" test without debugger attached. + +> .\stresstest.exe -a SqlClient.Stress.Tests -test TestExecuteXmlReaderAsyncCancellation +``` + +```powershell +# Run the application for a built target framework and all discovered tests +# without debugger attached for 10 seconds. + +> .\stresstest.exe -a SqlClient.Stress.Tests -all -duration 10 +``` + +```powershell +# Run the application for a built target framework and all discovered tests +# without debugger attached with 5 threads. + +> .\stresstest.exe -a SqlClient.Stress.Tests -all -threads 5 +``` + +```powershell +# Run the application for a built target framework and all discovered tests +# without debugger attached and dead lock detection process. + +> .\stresstest.exe -a SqlClient.Stress.Tests -all -deadlockdetection true +``` + +```powershell +# Run the application for a built target framework and all discovered tests +# without debugger attached with overriding the weight property with value 15. + +> .\stresstest.exe -a SqlClient.Stress.Tests -all -override Weight 15 +``` + +```powershell +# Run the application for a built target framework and all discovered tests +# without debugger attached with injecting random seed of 5. + +> .\stresstest.exe -a SqlClient.Stress.Tests -all -randomSeed 5 +``` + +## Further thoughts + +- Implement the uncompleted arguments. +- Add more tests. +- Add support running tests with **System.Data.SqlClient** too. diff --git a/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Common/Attributes/GlobalExceptionHandlerAttribute.cs b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Common/Attributes/GlobalExceptionHandlerAttribute.cs new file mode 100644 index 0000000000..810580d9f8 --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Common/Attributes/GlobalExceptionHandlerAttribute.cs @@ -0,0 +1,16 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; + +namespace DPStressHarness +{ + [AttributeUsage(AttributeTargets.Method, Inherited = true, AllowMultiple = false)] + public class GlobalExceptionHandlerAttribute : Attribute + { + public GlobalExceptionHandlerAttribute() + { + } + } +} diff --git a/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Common/Attributes/GlobalTestCleanupAttribute.cs b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Common/Attributes/GlobalTestCleanupAttribute.cs new file mode 100644 index 0000000000..2159d2630e --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Common/Attributes/GlobalTestCleanupAttribute.cs @@ -0,0 +1,16 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; + +namespace DPStressHarness +{ + [AttributeUsage(AttributeTargets.Method, Inherited = true, AllowMultiple = false)] + public class GlobalTestCleanupAttribute : Attribute + { + public GlobalTestCleanupAttribute() + { + } + } +} diff --git a/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Common/Attributes/GlobalTestSetupAttribute.cs b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Common/Attributes/GlobalTestSetupAttribute.cs new file mode 100644 index 0000000000..00ed3d5b05 --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Common/Attributes/GlobalTestSetupAttribute.cs @@ -0,0 +1,16 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; + +namespace DPStressHarness +{ + [AttributeUsage(AttributeTargets.Method, Inherited = true, AllowMultiple = false)] + public class GlobalTestSetupAttribute : Attribute + { + public GlobalTestSetupAttribute() + { + } + } +} diff --git a/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Common/Attributes/TestAttribute.cs b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Common/Attributes/TestAttribute.cs new file mode 100644 index 0000000000..3146c2d808 --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Common/Attributes/TestAttribute.cs @@ -0,0 +1,272 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; + +namespace DPStressHarness +{ + public enum TestPriority + { + BVT = 0, + High = 1, + Medium = 2, + Low = 3 + } + + public class TestAttributeBase : Attribute + { + private string _title; + private string _description = "none provided"; + private string _applicationName = "unknown"; + private string _improvement = "ADONETV3"; + private string _owner = "unknown"; + private string _category = "unknown"; + private TestPriority _priority = TestPriority.BVT; + + public TestAttributeBase(string title) + { + _title = title; + } + + public string Title + { + get { return _title; } + set { _title = value; } + } + + public string Description + { + get { return _description; } + set { _description = value; } + } + + public string Improvement + { + get { return _improvement; } + set { _improvement = value; } + } + + public string Owner + { + get { return _owner; } + set { _owner = value; } + } + + public string ApplicationName + { + get { return _applicationName; } + set { _applicationName = value; } + } + + public TestPriority Priority + { + get { return _priority; } + set { _priority = value; } + } + + public string Category + { + get { return _category; } + set { _category = value; } + } + } + + [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] + public class TestAttribute : TestAttributeBase + { + private int _warmupIterations = 0; + private int _testIterations = 1; + + public TestAttribute(string title) : base(title) + { + } + + public int WarmupIterations + { + get + { + string propName = "WarmupIterations"; + + if (TestMetrics.Overrides.ContainsKey(propName)) + { + return int.Parse(TestMetrics.Overrides[propName]); + } + else + { + return _warmupIterations; + } + } + set { _warmupIterations = value; } + } + + public int TestIterations + { + get + { + string propName = "TestIterations"; + + if (TestMetrics.Overrides.ContainsKey(propName)) + { + return int.Parse(TestMetrics.Overrides[propName]); + } + else + { + return _testIterations; + } + } + set { _testIterations = value; } + } + } + + [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] + public class StressTestAttribute : TestAttributeBase + { + private int _weight = 1; + + public StressTestAttribute(string title) + : base(title) + { + } + + public int Weight + { + get { return _weight; } + set { _weight = value; } + } + } + + [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] + public class MultiThreadedTestAttribute : TestAttributeBase + { + private int _warmupDuration = 60; + private int _testDuration = 60; + private int _threads = 16; + + public MultiThreadedTestAttribute(string title) + : base(title) + { + } + + public int WarmupDuration + { + get + { + string propName = "WarmupDuration"; + + if (TestMetrics.Overrides.ContainsKey(propName)) + { + return int.Parse(TestMetrics.Overrides[propName]); + } + else + { + return _warmupDuration; + } + } + set { _warmupDuration = value; } + } + + public int TestDuration + { + get + { + string propName = "TestDuration"; + + if (TestMetrics.Overrides.ContainsKey(propName)) + { + return int.Parse(TestMetrics.Overrides[propName]); + } + else + { + return _testDuration; + } + } + set { _testDuration = value; } + } + + public int Threads + { + get + { + string propName = "Threads"; + + if (TestMetrics.Overrides.ContainsKey(propName)) + { + return int.Parse(TestMetrics.Overrides[propName]); + } + else + { + return _threads; + } + } + set { _threads = value; } + } + } + + [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] + public class ThreadPoolTestAttribute : TestAttributeBase + { + private int _warmupDuration = 60; + private int _testDuration = 60; + private int _threads = 64; + + public ThreadPoolTestAttribute(string title) + : base(title) + { + } + + public int WarmupDuration + { + get + { + string propName = "WarmupDuration"; + + if (TestMetrics.Overrides.ContainsKey(propName)) + { + return int.Parse(TestMetrics.Overrides[propName]); + } + else + { + return _warmupDuration; + } + } + set { _warmupDuration = value; } + } + + public int TestDuration + { + get + { + string propName = "TestDuration"; + + if (TestMetrics.Overrides.ContainsKey(propName)) + { + return int.Parse(TestMetrics.Overrides[propName]); + } + else + { + return _testDuration; + } + } + set { _testDuration = value; } + } + + public int Threads + { + get + { + string propName = "Threads"; + + if (TestMetrics.Overrides.ContainsKey(propName)) + { + return int.Parse(TestMetrics.Overrides[propName]); + } + else + { + return _threads; + } + } + set { _threads = value; } + } + } +} diff --git a/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Common/Attributes/TestCleanupAttribute.cs b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Common/Attributes/TestCleanupAttribute.cs new file mode 100644 index 0000000000..32bc5ee6bc --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Common/Attributes/TestCleanupAttribute.cs @@ -0,0 +1,16 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; + +namespace DPStressHarness +{ + [AttributeUsage(AttributeTargets.Method, Inherited = true, AllowMultiple = false)] + public class TestCleanupAttribute : Attribute + { + public TestCleanupAttribute() + { + } + } +} diff --git a/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Common/Attributes/TestSetupAttribute.cs b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Common/Attributes/TestSetupAttribute.cs new file mode 100644 index 0000000000..5626032b69 --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Common/Attributes/TestSetupAttribute.cs @@ -0,0 +1,16 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; + +namespace DPStressHarness +{ + [AttributeUsage(AttributeTargets.Method, Inherited = true, AllowMultiple = false)] + public class TestSetupAttribute : Attribute + { + public TestSetupAttribute() + { + } + } +} diff --git a/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Common/Attributes/TestVariationAttribute.cs b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Common/Attributes/TestVariationAttribute.cs new file mode 100644 index 0000000000..e54acfa969 --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Common/Attributes/TestVariationAttribute.cs @@ -0,0 +1,35 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Text; + +namespace DPStressHarness +{ + [AttributeUsage(AttributeTargets.Field, Inherited = true, AllowMultiple = true)] + public class TestVariationAttribute : Attribute + { + private string _variationName; + private object _variationValue; + + public TestVariationAttribute(string variationName, object variationValue) + { + _variationName = variationName; + _variationValue = variationValue; + } + + public string VariationName + { + get { return _variationName; } + set { _variationName = value; } + } + + public object VariationValue + { + get { return _variationValue; } + set { _variationValue = value; } + } + } +} diff --git a/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Common/DeadlockDetection.cs b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Common/DeadlockDetection.cs new file mode 100644 index 0000000000..50fc6d3d7a --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Common/DeadlockDetection.cs @@ -0,0 +1,194 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Concurrent; +using System.Diagnostics; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; + +namespace DPStressHarness +{ + public class DeadlockDetection + { + /// + /// Information for a thread relating to deadlock detection. All of its information is stored in a reference object to make updating it easier. + /// + private class ThreadInfo + { + public ThreadInfo(long dueTime) + { + this.DueTime = dueTime; + } + + /// + /// The time (in ticks) when the thread should be completed + /// + public long DueTime; + + /// + /// True if the thread should not be aborted + /// + public bool DisableAbort; + + /// + /// The time when DisableAbort was set to true + /// + public long DisableAbortTime; + } + + /// + /// Maximum time that a test thread (i.e. a thread that is directly executing a [StressTest] method) can + /// execute before it is considered to be deadlocked. This should be longer than the + /// TaskThreadDeadlockTimeoutTicks because if the test is waiting for a task then the test will always + /// take longer to execute than the task. + /// + public const long TestThreadDeadlockTimeoutTicks = 20 * 60 * TimeSpan.TicksPerSecond; + + /// + /// Maximum time that any Task can execute before it is considered to be deadlocked + /// + public const long TaskThreadDeadlockTimeoutTicks = 10 * 60 * TimeSpan.TicksPerSecond; + + /// + /// Dictionary that maps Threads to the time (in ticks) when they should be completed. If they are not completed by that time then + /// they are considered to be deadlocked. + /// + private static ConcurrentDictionary s_threadDueTimes = null; + + /// + /// Timer that scans through _threadDueTimes to find deadlocked threads + /// + private static Timer s_deadlockWatchdog = null; + + /// + /// Interval of _deadlockWatchdog, in milliseconds + /// + private const int _watchdogIntervalMs = 60 * 1000; + + /// + /// true if deadlock detection is enabled, otherwise false. Should be set only at process startup. + /// + private static bool s_isEnabled = false; + + public static bool IsEnabled => s_isEnabled; + + /// + /// Enables deadlock detection. + /// + public static void Enable() + { + // Switch out the default TaskScheduler. We must use reflection because it is private. + FieldInfo defaultTaskScheduler = typeof(TaskScheduler).GetField("s_defaultTaskScheduler", BindingFlags.NonPublic | BindingFlags.Static); + DeadlockDetectionTaskScheduler newTaskScheduler = new DeadlockDetectionTaskScheduler(); + defaultTaskScheduler.SetValue(null, newTaskScheduler); + + s_threadDueTimes = new ConcurrentDictionary(); + s_deadlockWatchdog = new Timer(CheckForDeadlocks, null, _watchdogIntervalMs, _watchdogIntervalMs); + + s_isEnabled = true; + } + + /// + /// Adds the current Task execution thread to the tracked thread collection. + /// + public static void AddTaskThread() + { + if (s_isEnabled) + { + long dueTime = DateTime.UtcNow.Ticks + TaskThreadDeadlockTimeoutTicks; + AddThread(dueTime); + } + } + + /// + /// Adds the current Test execution thread (i.e. a thread that is directly executing a [StressTest] method) to the tracked thread collection. + /// + public static void AddTestThread() + { + if (s_isEnabled) + { + long dueTime = DateTime.UtcNow.Ticks + TestThreadDeadlockTimeoutTicks; + AddThread(dueTime); + } + } + + private static void AddThread(long dueTime) + { + s_threadDueTimes.TryAdd(Thread.CurrentThread, new ThreadInfo(dueTime)); + } + + /// + /// Removes the current thread from the tracked thread collection + /// + public static void RemoveThread() + { + if (s_isEnabled) + { + ThreadInfo unused; + s_threadDueTimes.TryRemove(Thread.CurrentThread, out unused); + } + } + + /// + /// Disables abort of current thread. Call this when the current thread is waiting on a task. + /// + public static void DisableThreadAbort() + { + if (s_isEnabled) + { + ThreadInfo threadInfo; + if (s_threadDueTimes.TryGetValue(Thread.CurrentThread, out threadInfo)) + { + threadInfo.DisableAbort = true; + threadInfo.DisableAbortTime = DateTime.UtcNow.Ticks; + } + } + } + + /// + /// Enables abort of current thread after calling DisableThreadAbort(). The elapsed time since calling DisableThreadAbort() is added to the due time. + /// + public static void EnableThreadAbort() + { + if (s_isEnabled) + { + ThreadInfo threadInfo; + if (s_threadDueTimes.TryGetValue(Thread.CurrentThread, out threadInfo)) + { + threadInfo.DueTime += DateTime.UtcNow.Ticks - threadInfo.DisableAbortTime; + threadInfo.DisableAbort = false; + } + } + } + + /// + /// Looks through the tracked thread collection and aborts any thread that is past its due time + /// + /// unused + private static void CheckForDeadlocks(object state) + { + if (s_isEnabled) + { + long now = DateTime.UtcNow.Ticks; + + // Find candidate threads + foreach (var threadDuePair in s_threadDueTimes) + { + if (!threadDuePair.Value.DisableAbort && now > threadDuePair.Value.DueTime) + { + // Abort the misbehaving thread and the return + // NOTE: We only want to abort a single thread at a time to allow the other thread in the deadlock pair to continue + Thread t = threadDuePair.Key; + Console.WriteLine("Deadlock detected on thread with managed thread id {0}", t.ManagedThreadId); + Debugger.Break(); + t.Join(); + return; + } + } + } + } + } +} diff --git a/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Common/DeadlockDetectionTaskScheduler.cs b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Common/DeadlockDetectionTaskScheduler.cs new file mode 100644 index 0000000000..22a540def8 --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Common/DeadlockDetectionTaskScheduler.cs @@ -0,0 +1,93 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace DPStressHarness +{ + public class DeadlockDetectionTaskScheduler : TaskScheduler + { + private readonly WaitCallback _runTaskCallback; + private readonly ParameterizedThreadStart _runTaskThreadStart; +#if DEBUG + private readonly ConcurrentDictionary _queuedItems = new ConcurrentDictionary(); +#endif + + public DeadlockDetectionTaskScheduler() + { + _runTaskCallback = new WaitCallback(RunTask); + _runTaskThreadStart = new ParameterizedThreadStart(RunTask); + } + + // This is only used for debugging, so for retail we'd prefer the perf + protected override IEnumerable GetScheduledTasks() + { +#if DEBUG + return _queuedItems.Keys; +#else + return new Task[0]; +#endif + } + + protected override void QueueTask(Task task) + { + if ((task.CreationOptions & TaskCreationOptions.LongRunning) == TaskCreationOptions.LongRunning) + { + // Create a new background thread for long running tasks + Thread thread = new Thread(_runTaskThreadStart) { IsBackground = true }; + thread.Start(task); + } + else + { + // Otherwise queue the work on the threadpool +#if DEBUG + _queuedItems.TryAdd(task, null); +#endif + + ThreadPool.QueueUserWorkItem(_runTaskCallback, task); + } + } + + protected override bool TryExecuteTaskInline(Task task, bool taskWasPreviouslyQueued) + { + if (!taskWasPreviouslyQueued) + { + // Run the task inline + RunTask(task); + return true; + } + + // Couldn't run the task + return false; + } + + private void RunTask(object state) + { + Task inTask = state as Task; + +#if DEBUG + // Remove from the dictionary of queued items + object ignored; + _queuedItems.TryRemove(inTask, out ignored); +#endif + + // Note when the thread started work + DeadlockDetection.AddTaskThread(); + + try + { + // Run the task + base.TryExecuteTask(inTask); + } + finally + { + // Remove the thread from the list when complete + DeadlockDetection.RemoveThread(); + } + } + } +} diff --git a/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Common/SqlClient.Stress.Common.csproj b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Common/SqlClient.Stress.Common.csproj new file mode 100644 index 0000000000..5540d4951e --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Common/SqlClient.Stress.Common.csproj @@ -0,0 +1,5 @@ + + + + + diff --git a/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Common/TestMetrics.cs b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Common/TestMetrics.cs new file mode 100644 index 0000000000..054a822dc1 --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Common/TestMetrics.cs @@ -0,0 +1,368 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Diagnostics; +using System.Collections.Generic; +using System.Reflection; + +namespace DPStressHarness +{ + public static class TestMetrics + { + private const string _defaultValue = "unknown"; + + private static bool s_valid = false; + private static bool s_reset = true; + private static Stopwatch s_stopwatch = new Stopwatch(); + private static long s_workingSet; + private static long s_peakWorkingSet; + private static long s_privateBytes; + private static Assembly s_targetAssembly; + private static string s_fileVersion = _defaultValue; + private static string s_privateBuild = _defaultValue; + private static string s_runLabel = DateTime.Now.ToString(); + private static Dictionary s_overrides; + private static List s_variations = null; + private static List s_selectedTests = null; + private static bool s_isOfficial = false; + private static string s_milestone = _defaultValue; + private static string s_branch = _defaultValue; + private static List s_categories = null; + private static bool s_profileMeasuredCode = false; + private static int s_stressThreads = 16; + private static int s_stressDuration = 1; + private static int? s_exceptionThreshold = null; + private static bool s_monitorenabled = false; + private static string s_monitormachinename = "localhost"; + private static int s_randomSeed = 0; + private static string s_filter = null; + private static bool s_printMethodName = false; + + /// Starts the sample profiler. + /// + /// Do not inline to avoid errors when the functionality is not used + /// and the profiling DLL is not available. + /// + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoInlining)] + private static void InternalStartProfiling() + { + // Microsoft.VisualStudio.Profiler.DataCollection.StartProfile( + // Microsoft.VisualStudio.Profiler.ProfileLevel.Global, + // Microsoft.VisualStudio.Profiler.DataCollection.CurrentId); + } + + /// Stops the sample profiler. + /// + /// Do not inline to avoid errors when the functionality is not used + /// and the profiling DLL is not available. + /// + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoInlining)] + private static void InternalStopProfiling() + { + // Microsoft.VisualStudio.Profiler.DataCollection.StopProfile( + // Microsoft.VisualStudio.Profiler.ProfileLevel.Global, + // Microsoft.VisualStudio.Profiler.DataCollection.CurrentId); + } + + public static void StartCollection() + { + s_valid = false; + + s_stopwatch.Reset(); + s_stopwatch.Start(); + s_reset = true; + } + + public static void StartProfiling() + { + if (s_profileMeasuredCode) + { + InternalStartProfiling(); + } + } + + public static void StopProfiling() + { + if (s_profileMeasuredCode) + { + InternalStopProfiling(); + } + } + + public static void StopCollection() + { + s_stopwatch.Stop(); + + Process p = Process.GetCurrentProcess(); + s_workingSet = p.WorkingSet64; + s_peakWorkingSet = p.PeakWorkingSet64; + s_privateBytes = p.PrivateMemorySize64; + + s_valid = true; + } + + public static void PauseTimer() + { + s_stopwatch.Stop(); + } + + public static void UnPauseTimer() + { + if (s_reset) + { + s_stopwatch.Reset(); + s_reset = false; + } + + s_stopwatch.Start(); + } + + private static void ThrowIfInvalid() + { + if (!s_valid) throw new InvalidOperationException("Collection must be stopped before accessing this metric."); + } + + public static void Reset() + { + s_valid = false; + s_reset = true; + s_stopwatch = new Stopwatch(); + s_workingSet = new long(); + s_peakWorkingSet = new long(); + s_privateBytes = new long(); + s_targetAssembly = null; + s_fileVersion = _defaultValue; + s_privateBuild = _defaultValue; + s_runLabel = DateTime.Now.ToString(); + s_overrides = null; + s_variations = null; + s_selectedTests = null; + s_isOfficial = false; + s_milestone = _defaultValue; + s_branch = _defaultValue; + s_categories = null; + s_profileMeasuredCode = false; + s_stressThreads = 16; + s_stressDuration = 1; + s_exceptionThreshold = null; + s_monitorenabled = false; + s_monitormachinename = "localhost"; + s_randomSeed = 0; + s_filter = null; + s_printMethodName = false; + } + + public static string FileVersion + { + get { return s_fileVersion; } + set { s_fileVersion = value; } + } + + public static string PrivateBuild + { + get { return s_privateBuild; } + set { s_privateBuild = value; } + } + + public static Assembly TargetAssembly + { + get { return s_targetAssembly; } + + set + { + s_targetAssembly = value; + s_fileVersion = VersionUtil.GetFileVersion(s_targetAssembly.ManifestModule.FullyQualifiedName); + s_privateBuild = VersionUtil.GetPrivateBuild(s_targetAssembly.ManifestModule.FullyQualifiedName); + } + } + + public static string RunLabel + { + get { return s_runLabel; } + set { s_runLabel = value; } + } + + public static string Milestone + { + get { return s_milestone; } + set { s_milestone = value; } + } + + public static string Branch + { + get { return s_branch; } + set { s_branch = value; } + } + + public static bool IsOfficial + { + get { return s_isOfficial; } + set { s_isOfficial = value; } + } + + public static bool IsDefaultValue(string val) + { + return val.Equals(_defaultValue); + } + + public static double ElapsedSeconds + { + get + { + ThrowIfInvalid(); + return s_stopwatch.ElapsedMilliseconds / 1000.0; + } + } + + public static long WorkingSet + { + get + { + ThrowIfInvalid(); + return s_workingSet; + } + } + + public static long PeakWorkingSet + { + get + { + ThrowIfInvalid(); + return s_peakWorkingSet; + } + } + + public static long PrivateBytes + { + get + { + ThrowIfInvalid(); + return s_privateBytes; + } + } + + + public static Dictionary Overrides + { + get + { + if (s_overrides == null) + { + s_overrides = new Dictionary(8); + } + return s_overrides; + } + } + + public static List Variations + { + get + { + if (s_variations == null) + { + s_variations = new List(8); + } + + return s_variations; + } + } + + public static List SelectedTests + { + get + { + if (s_selectedTests == null) + { + s_selectedTests = new List(8); + } + + return s_selectedTests; + } + } + + public static bool IncludeTest(TestAttributeBase test) + { + if (s_selectedTests == null || s_selectedTests.Count == 0) + return true; // user has no selection - run all + else + return s_selectedTests.Contains(test.Title); + } + + public static List Categories + { + get + { + if (s_categories == null) + { + s_categories = new List(8); + } + + return s_categories; + } + } + + public static bool ProfileMeasuredCode + { + get { return s_profileMeasuredCode; } + set { s_profileMeasuredCode = value; } + } + + public static int StressDuration + { + get { return s_stressDuration; } + set { s_stressDuration = value; } + } + + public static int StressThreads + { + get { return s_stressThreads; } + set { s_stressThreads = value; } + } + + public static int? ExceptionThreshold + { + get { return s_exceptionThreshold; } + set { s_exceptionThreshold = value; } + } + + public static bool MonitorEnabled + { + get { return s_monitorenabled; } + set + { + if(value) + { + throw new NotImplementedException($"The '{nameof(MonitorEnabled)}' isn't fully implemented!"); + } + s_monitorenabled = value; + } + } + + + public static string MonitorMachineName + { + get { return s_monitormachinename; } + set { s_monitormachinename = value; } + } + + public static int RandomSeed + { + get { return s_randomSeed; } + set { s_randomSeed = value; } + } + + public static string Filter + { + get { return s_filter; } + set { s_filter = value; } + } + + public static bool PrintMethodName + { + get { return s_printMethodName; } + set { s_printMethodName = value; } + } + } +} diff --git a/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Common/VersionUtil.cs b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Common/VersionUtil.cs new file mode 100644 index 0000000000..1778903834 --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Common/VersionUtil.cs @@ -0,0 +1,40 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.IO; +using System.Diagnostics; + +#pragma warning disable 618 + +namespace DPStressHarness +{ + public class VersionUtil + { + public static string GetFileVersion(string moduleName) + { + FileVersionInfo info = GetFileVersionInfo(moduleName); + return info.FileVersion; + } + + public static string GetPrivateBuild(string moduleName) + { + FileVersionInfo info = GetFileVersionInfo(moduleName); + return info.PrivateBuild; + } + + private static FileVersionInfo GetFileVersionInfo(string moduleName) + { + if (File.Exists(moduleName)) + { + return FileVersionInfo.GetVersionInfo(Path.GetFullPath(moduleName)); + } + else + { + string moduleInRuntimeDir = AppContext.BaseDirectory + moduleName; + return FileVersionInfo.GetVersionInfo(moduleInRuntimeDir); + } + } + } +} diff --git a/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Framework/AsyncUtils.cs b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Framework/AsyncUtils.cs new file mode 100644 index 0000000000..84f0fba0de --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Framework/AsyncUtils.cs @@ -0,0 +1,185 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using Microsoft.Data.SqlClient; +using System.Linq; +using System.Runtime.ExceptionServices; +using System.Threading.Tasks; +using System.Xml; +using DPStressHarness; + +namespace Stress.Data +{ + public enum SyncAsyncMode + { + Sync, // call sync method, e.g. connection.Open(), and return completed task + SyncOverAsync, // call async method, e.g. connection.OpenAsync().Wait(), and return completed task + Async // call async method, e.g. connection.OpenAsync(), and return running task + } + + public static class AsyncUtils + { + public static Task SyncOrAsyncMethod(Func syncFunc, Func> asyncFunc, SyncAsyncMode mode) + { + switch (mode) + { + case SyncAsyncMode.Sync: + TResult result = syncFunc(); + return Task.FromResult(result); + + case SyncAsyncMode.SyncOverAsync: + Task t = asyncFunc(); + WaitAndUnwrapException(t); + return t; + + case SyncAsyncMode.Async: + return asyncFunc(); + + default: + throw new ArgumentException(mode.ToString()); + } + } + + public static Task SyncOrAsyncMethod(Action syncFunc, Func asyncFunc, SyncAsyncMode mode) + { + switch (mode) + { + case SyncAsyncMode.Sync: + syncFunc(); + return Task.CompletedTask; + + case SyncAsyncMode.SyncOverAsync: + Task t = asyncFunc(); + WaitAndUnwrapException(t); + return t; + + case SyncAsyncMode.Async: + return asyncFunc(); + + default: + throw new ArgumentException(mode.ToString()); + } + } + + public static void WaitAll(params Task[] ts) + { + DeadlockDetection.DisableThreadAbort(); + try + { + Task.WaitAll(ts); + } + finally + { + DeadlockDetection.EnableThreadAbort(); + } + } + + public static void WaitAllNullable(params Task[] ts) + { + DeadlockDetection.DisableThreadAbort(); + try + { + Task[] tasks = ts.Where(t => t != null).ToArray(); + Task.WaitAll(tasks); + } + finally + { + DeadlockDetection.EnableThreadAbort(); + } + } + + public static void WaitAndUnwrapException(Task t) + { + DeadlockDetection.DisableThreadAbort(); + try + { + t.Wait(); + } + catch (AggregateException ae) + { + // The callers of this API may not expect AggregateException, so throw the inner exception + // If AggregateException contains more than one InnerExceptions, throw it out as it is, + // because that is unexpected + if ((ae.InnerExceptions != null) && (ae.InnerExceptions.Count == 1)) + { + if (ae.InnerException != null) + { + ExceptionDispatchInfo info = ExceptionDispatchInfo.Capture(ae.InnerException); + info.Throw(); + } + } + + throw; + } + finally + { + DeadlockDetection.EnableThreadAbort(); + } + } + + public static T GetResult(IAsyncResult result) + { + return GetResult((Task)result); + } + + public static T GetResult(Task result) + { + DeadlockDetection.DisableThreadAbort(); + try + { + return result.Result; + } + finally + { + DeadlockDetection.EnableThreadAbort(); + } + } + + public static SqlDataReader ExecuteReader(SqlCommand command) + { + DeadlockDetection.DisableThreadAbort(); + try + { + return command.ExecuteReader(); + } + finally + { + DeadlockDetection.EnableThreadAbort(); + } + } + + public static int ExecuteNonQuery(SqlCommand command) + { + DeadlockDetection.DisableThreadAbort(); + try + { + return command.ExecuteNonQuery(); + } + finally + { + DeadlockDetection.DisableThreadAbort(); + } + } + + public static XmlReader ExecuteXmlReader(SqlCommand command) + { + DeadlockDetection.DisableThreadAbort(); + try + { + return command.ExecuteXmlReader(); + } + finally + { + DeadlockDetection.EnableThreadAbort(); + } + } + + public static SyncAsyncMode ChooseSyncAsyncMode(Random rnd) + { + // Any mode is allowed + return (SyncAsyncMode)rnd.Next(3); + } + } +} diff --git a/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Framework/DataSource.cs b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Framework/DataSource.cs new file mode 100644 index 0000000000..b61379aa0a --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Framework/DataSource.cs @@ -0,0 +1,192 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; + +namespace Stress.Data +{ + /// + /// supported source types - values for 'type' attribute for 'source' node in App.config + /// + public enum DataSourceType + { + SqlServer + } + + /// + /// base class for database source information (SQL Server, Oracle Server, Access Database file, etc...). + /// Data sources are loaded from the app config file. + /// + public abstract class DataSource + { + /// + /// name of the source - can be used in command line: StressTest ... -override source "sourcename" + /// + public readonly string Name; + + /// + /// database type + /// + public readonly DataSourceType Type; + + /// + /// whether this source is the default one for the type specified + /// + public readonly bool IsDefault; + + /// + /// constructs new data source - called by derived class c-tors only (thus protected) + /// + protected DataSource(string name, DataSourceType type, bool isDefault) + { + this.Name = name; + this.Type = type; + this.IsDefault = isDefault; + } + + /// + /// this method is used to create the data source, based on its type + /// + public static DataSource Create(string name, DataSourceType sourceType, bool isDefault, IDictionary properties) + { + switch (sourceType) + { + case DataSourceType.SqlServer: + return new SqlServerDataSource(name, isDefault, properties); + default: + throw new ArgumentException("Wrong source type value: " + sourceType); + } + } + + /// + /// used by GetRequiredAttributeValue or derived classes to construct exception on missing required attribute + /// + /// name of the source (from XML) to include in exception message (for troubleshooting) + protected Exception MissingAttributeValueException(string sourceName, string attributeName) + { + return new ArgumentException(string.Format("Missing or empty value for {0} attribute in the config file for source: {1}", attributeName, sourceName)); + } + + /// + /// search for required attribute or fail if not found + /// + protected string GetRequiredAttributeValue(string sourceName, IDictionary properties, string valueName, bool allowEmpty) + { + string value; + if (!properties.TryGetValue(valueName, out value) || (value == null) || (!allowEmpty && value.Length == 0)) + { + throw MissingAttributeValueException(sourceName, valueName); + } + return value; + } + + /// + /// search for optional attribute or return default vale + /// + protected string GetOptionalAttributeValue(IDictionary properties, string valueName, string defaultValue) + { + string value; + if (!properties.TryGetValue(valueName, out value) || (value == null)) + { + value = defaultValue; + } + return value; + } + + public abstract void Emit(byte indent); + } + + /// + /// Represents SQL Server data source. This source is used by SqlClient as well as by ODBC and OLEDB when connecting to SQL with SNAC or MDAC/WDAC + /// + /// + /// + /// + /// + public class SqlServerDataSource : DataSource + { + public readonly string DataSource; + public readonly string Database = "StressTests-" + Guid.NewGuid().ToString(); + public readonly bool IsLocal; + public readonly bool Encrypt; + + // If EntraIdUser is set, the connection will use EntraID password-based + // authentication. + public readonly string EntraIdUser; + public readonly string EntraIdPassword; + + // If EntraIdUser isn't set, and User is set, the connection will use + // classic SQL user/password based authentication. + public readonly string User; + public readonly string Password; + + // if true, test can create connnection strings with integrated security (trusted connection) set to true (or SSPI). + public readonly bool SupportsWindowsAuthentication; + + public bool DisableMultiSubnetFailoverSetup; + + public bool DisableNamedPipes; + + internal SqlServerDataSource(string name, bool isDefault, IDictionary properties) + : base(name, DataSourceType.SqlServer, isDefault) + { + this.DataSource = GetOptionalAttributeValue(properties, "dataSource", "localhost"); + + this.EntraIdUser = GetOptionalAttributeValue(properties, "entraIdUser", string.Empty); + this.EntraIdPassword = GetOptionalAttributeValue(properties, "entraIdPassword", string.Empty); + + this.User = GetOptionalAttributeValue(properties, "user", string.Empty); + this.Password = GetOptionalAttributeValue(properties, "password", string.Empty); + + this.IsLocal = bool.Parse(GetOptionalAttributeValue(properties, "isLocal", bool.FalseString)); + this.Encrypt = bool.Parse(GetOptionalAttributeValue(properties, "encrypt", bool.FalseString)); + + this.DisableMultiSubnetFailoverSetup = bool.Parse(GetOptionalAttributeValue(properties, "DisableMultiSubnetFailoverSetup", bool.TrueString)); + + this.DisableNamedPipes = bool.Parse(GetOptionalAttributeValue(properties, "DisableNamedPipes", bool.TrueString)); + + string temp = GetOptionalAttributeValue(properties, "supportsWindowsAuthentication", "false"); + if (!string.IsNullOrEmpty(temp)) + SupportsWindowsAuthentication = Convert.ToBoolean(temp); + else + SupportsWindowsAuthentication = false; + + if (string.IsNullOrEmpty(EntraIdUser) + && string.IsNullOrEmpty(User) + && !SupportsWindowsAuthentication) + { + throw new ArgumentException("SQL Server settings should include either a valid user or SupportsWindowsAuthentication=true"); + } + } + + public override void Emit(byte indent) + { + string ind = new(' ', indent); + Console.WriteLine($"{ind}SqlServerDataSource:"); + ind = new(' ', indent + 2); + Console.WriteLine($"{ind}Name: {Name}"); + Console.WriteLine($"{ind}Type: {Type}"); + Console.WriteLine($"{ind}IsDefault: {IsDefault}"); + Console.WriteLine($"{ind}DataSource: {DataSource}"); + Console.WriteLine($"{ind}Database: {Database}"); + Console.WriteLine($"{ind}EntraIdUser: {EntraIdUser}"); + Console.WriteLine($"{ind}EntraIdPassword: {new string('*', EntraIdPassword.Length)}"); + Console.WriteLine($"{ind}User: {User}"); + Console.WriteLine($"{ind}Password: {new string('*', Password.Length)}"); + Console.WriteLine($"{ind}WinAuth: {SupportsWindowsAuthentication}"); + Console.WriteLine($"{ind}IsLocal: {IsLocal}"); + Console.WriteLine($"{ind}Encrypt: {Encrypt}"); + Console.WriteLine($"{ind}DisableMultiSubnet: {DisableMultiSubnetFailoverSetup}"); + Console.WriteLine($"{ind}DisableNamedPipes: {DisableNamedPipes}"); + } + } +} diff --git a/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Framework/DataStressConnection.cs b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Framework/DataStressConnection.cs new file mode 100644 index 0000000000..46becd4897 --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Framework/DataStressConnection.cs @@ -0,0 +1,232 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Data.Common; +using Microsoft.Data.SqlClient; +using System.Threading; +using System.Threading.Tasks; + +namespace Stress.Data +{ + public class DataStressConnection : IDisposable + { + public DbConnection DbConnection { get; private set; } + private readonly bool _clearPoolBeforeClose; + public DataStressConnection(DbConnection conn, bool clearPoolBeforeClose = false) + { + if (conn == null) + throw new ArgumentException("Cannot pass in null DbConnection to make new DataStressConnection!"); + this.DbConnection = conn; + _clearPoolBeforeClose = clearPoolBeforeClose; + } + + private short _spid = 0; + + [ThreadStatic] + private static TrackedRandom t_randomInstance; + private static TrackedRandom RandomInstance + { + get + { + if (t_randomInstance == null) + t_randomInstance = new TrackedRandom(); + return t_randomInstance; + } + } + + public void Open() + { + bool sync = RandomInstance.NextBool(); + + if (sync) + { + OpenSync(); + } + else + { + Task t = OpenAsync(); + AsyncUtils.WaitAndUnwrapException(t); + } + } + + public async Task OpenAsync() + { + int startMilliseconds = Environment.TickCount; + try + { + await DbConnection.OpenAsync(); + } + catch (ObjectDisposedException e) + { + HandleObjectDisposedException(e, true); + throw; + } + catch (InvalidOperationException e) + { + int endMilliseconds = Environment.TickCount; + + // we may be able to handle this exception + HandleInvalidOperationException(e, startMilliseconds, endMilliseconds, true); + throw; + } + + GetSpid(); + } + + private void OpenSync() + { + int startMilliseconds = Environment.TickCount; + try + { + DbConnection.Open(); + } + catch (ObjectDisposedException e) + { + HandleObjectDisposedException(e, false); + throw; + } + catch (InvalidOperationException e) + { + int endMilliseconds = Environment.TickCount; + + // we may be able to handle this exception + HandleInvalidOperationException(e, startMilliseconds, endMilliseconds, false); + throw; + } + + GetSpid(); + } + + private void HandleObjectDisposedException(ObjectDisposedException e, bool async) + { + // Race condition in DbConnectionFactory.TryGetConnection results in an ObjectDisposedException when calling OpenAsync on a non-pooled connection + string methodName = async ? "OpenAsync()" : "Open()"; + throw DataStressErrors.ProductError( + "Hit ObjectDisposedException in SqlConnection." + methodName, e); + } + + private static int s_fastTimeoutCountOpen; // number of times hit by SqlConnection.Open + private static int s_fastTimeoutCountOpenAsync; // number of times hit by SqlConnection.OpenAsync + private static readonly DateTime s_startTime = DateTime.Now; + + private const int MaxFastTimeoutCountPerDay = 200; + + /// + /// Handles InvalidOperationException generated from Open or OpenAsync calls. + /// For any other type of Exception, it simply returns + /// + private void HandleInvalidOperationException(InvalidOperationException e, int startMilliseconds, int endMilliseconds, bool async) + { + int elapsedMilliseconds = unchecked(endMilliseconds - startMilliseconds); // unchecked to handle overflow of Environment.TickCount + + // Since InvalidOperationExceptions due to timeout can be caused by issues + // (e.g. network hiccup, server unavailable, etc) we need a heuristic to guess whether or not this exception + // should have happened or not. + bool wasTimeoutFromPool = (e.GetType() == typeof(InvalidOperationException)) && + (e.Message.StartsWith("Timeout expired. The timeout period elapsed prior to obtaining a connection from the pool")); + + bool wasTooEarly = (elapsedMilliseconds < ((DbConnection.ConnectionTimeout - 5) * 1000)); + + if (wasTimeoutFromPool && wasTooEarly) + { + if (async) + Interlocked.Increment(ref s_fastTimeoutCountOpenAsync); + else + Interlocked.Increment(ref s_fastTimeoutCountOpen); + } + } + + /// + /// Gets spid value. + /// + /// + /// If we want to kill the connection, we get its spid up front before the test case uses the connection. Otherwise if + /// we try to get the spid when KillConnection is called, then the connection could be in a bad state (e.g. enlisted in + /// aborted transaction, or has open datareader) and we will fail to get the spid. Also the randomization is put here + /// instead of in KillConnection because otherwise this method would execute a command for every single connection which + /// most of the time will not be used later. + /// + private void GetSpid() + { + if (DbConnection is SqlConnection && RandomInstance.Next(0, 20) == 0) + { + using (var cmd = DbConnection.CreateCommand()) + { + cmd.CommandText = "select @@spid"; + _spid = (short)cmd.ExecuteScalar(); + } + } + else + { + _spid = 0; + } + } + + /// + /// Kills the given connection using "kill [spid]" if the parameter is nonzero + /// + private void KillConnection() + { + DataStressErrors.Assert(_spid != 0, "Called KillConnection with spid != 0"); + + using (var killerConn = DataTestGroup.Factory.CreateConnection()) + { + killerConn.Open(); + + using (var killerCmd = killerConn.CreateCommand()) + { + killerCmd.CommandText = "begin try kill " + _spid + " end try begin catch end catch"; + killerCmd.ExecuteNonQuery(); + } + } + } + + /// + /// Kills the given connection using "kill [spid]" if the parameter is nonzero + /// + /// a Task that is asynchronously killing the connection, or null if the connection is not being killed + public Task KillConnectionAsync() + { + if (_spid == 0) + return null; + else + return Task.Factory.StartNew(() => KillConnection()); + } + + public void Close() + { + if (_spid != 0) + { + KillConnection(); + + // Wait before putting the connection back in the pool, to ensure that + // the pool checks the connection the next time it is used. + Task.Delay(10).ContinueWith((t) => DbConnection.Close()); + } + else + { + // If this is a SqlConnection, and it is a connection with a unique connection string that we will never use again, + // then call SqlConnection.ClearPool() before closing so that it is fully closed and does not waste client & server resources. + if (_clearPoolBeforeClose) + { + SqlConnection sqlConn = DbConnection as SqlConnection; + if (sqlConn != null) SqlConnection.ClearPool(sqlConn); + } + + DbConnection.Close(); + } + } + + public void Dispose() + { + Close(); + } + + public DbCommand CreateCommand() + { + return DbConnection.CreateCommand(); + } + } +} diff --git a/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Framework/DataStressErrors.cs b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Framework/DataStressErrors.cs new file mode 100644 index 0000000000..46b7751d50 --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Framework/DataStressErrors.cs @@ -0,0 +1,215 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Diagnostics; + +namespace Stress.Data +{ + public enum ErrorHandlingAction + { + // If you add an item here, remember to add it to all of the methods below + DebugBreak, + ThrowException + } + + /// + /// Static class containing methods to report errors. + /// + /// The StressTest executor will eat exceptions that are thrown and write them out to the console. In theory these should all be + /// either harmless exceptions or product bugs, however at present there are a large number of test issues that will cause a flood + /// of exceptions. Therefore if something actually bad happens (e.g. a known product bug is hit due to regression, or a major test + /// programming error) this error would be easy to miss if it were reported just by throwing an exception. To solve this, we use + /// this class for structured & consistent handling of errors. + /// + public static class DataStressErrors + { + private static void DebugBreak(string message, Exception exception) + { + // Print out the error before breaking to make debugging easier + Console.WriteLine(message); + if (exception != null) + { + Console.WriteLine(exception); + } + + Debugger.Break(); + } + + /// + /// Reports that a product bug has been hit. The action that will be taken is configurable in the .config file. + /// This can be used to check for regressions of known product bugs. + /// + /// A description of the product bug hit (e.g. title, bug number & database, more information) + /// The exception that was thrown that indicates a product bug, or null if the product bug was detected without + /// having thrown an exception + /// An exception that the caller should throw. + public static Exception ProductError(string description, Exception exception = null) + { + switch (DataStressSettings.Instance.ActionOnProductError) + { + case ErrorHandlingAction.DebugBreak: + DebugBreak("Hit product error: " + description, exception); + return new ProductErrorException(description, exception); + + case ErrorHandlingAction.ThrowException: + return new ProductErrorException(description, exception); + + default: + throw UnhandledCaseError(DataStressSettings.Instance.ActionOnProductError); + } + } + + /// + /// Reports that a non-fatal test error has been hit. The action that will be taken is configurable in the .config file. + /// This should be used for test errors that do not prevent the test from running. + /// + /// A description of the error + /// The exception that was thrown that indicates an error, or null if the error was detected without + /// An exception that the caller should throw. + public static Exception TestError(string description, Exception exception = null) + { + switch (DataStressSettings.Instance.ActionOnTestError) + { + case ErrorHandlingAction.DebugBreak: + DebugBreak("Hit test error: " + description, exception); + return new TestErrorException(description, exception); + + case ErrorHandlingAction.ThrowException: + return new TestErrorException(description, exception); + + default: + throw UnhandledCaseError(DataStressSettings.Instance.ActionOnTestError); + } + } + + /// + /// Reports that a programming error in the test code has occurred. The action that will be taken is configurable in the .config file. + /// This must strictly be used to report programming errors. It should not be in any way possible to see one of these errors unless + /// you make an incorrect change to the code, for example having an unhandled case in a switch statement. + /// + /// A description of the error + /// The exception that was thrown that indicates an error, or null if the error was detected without + /// having thrown an exception + /// An exception that the caller should throw. + private static Exception ProgrammingError(string description, Exception exception = null) + { + switch (DataStressSettings.Instance.ActionOnProgrammingError) + { + case ErrorHandlingAction.DebugBreak: + DebugBreak("Hit programming error: " + description, exception); + return new ProgrammingErrorException(description, exception); + + case ErrorHandlingAction.ThrowException: + return new ProgrammingErrorException(description, exception); + + default: + // If we are here then it's a programming error, but calling UnhandledCaseError here would cause an inifite loop. + goto case ErrorHandlingAction.DebugBreak; + } + } + + /// + /// Reports that an unhandled case in a switch statement in the test code has occurred. The action that will be taken is configurable + /// as a programming error in the .config file. It should not be in any way possible to see one of these errors unless + /// you make an incorrect change to the test code, for example having an unhandled case in a switch statement. + /// + /// The value that was not handled in the switch statement + /// An exception that the caller should throw. + public static Exception UnhandledCaseError(T unhandledValue) + { + return ProgrammingError("Unhandled case in switch statement: " + unhandledValue); + } + + /// + /// Asserts that a condition is true. If the condition is false then throws a ProgrammingError. + /// This must strictly be used to report programming errors. It should not be in any way possible to see one of these errors unless + /// you make an incorrect change to the code, for example having an unhandled case in a switch statement. + /// + /// A condition to assert + /// A description of the error + /// if the condition is false + public static void Assert(bool condition, string description) + { + if (!condition) + { + throw ProgrammingError(description); + } + } + + /// + /// Reports that a fatal error has happened. This is an error that completely prevents the test from continuing, + /// for example a setup failure. Ordinary programming errors should not be handled by this method. + /// + /// A description of the error + /// An exception that the caller should throw. + public static Exception FatalError(string description) + { + Console.WriteLine("Fatal test error: {0}", description); + Debugger.Break(); // Give the user a chance to debug + Environment.FailFast("Fatal error. Exit."); + return new Exception(); // Caller should throw this to indicate to the compiler that any code after the call is unreachable + } + + #region Exception types + + // These exception types are provided so that they can be easily found in logs, i.e. just do a text search in the console + // output log for "ProductErrorException" + + private class ProductErrorException : Exception + { + public ProductErrorException() + : base() + { + } + + public ProductErrorException(string message) + : base(message) + { + } + + public ProductErrorException(string message, Exception innerException) + : base(message, innerException) + { + } + } + + private class ProgrammingErrorException : Exception + { + public ProgrammingErrorException() + : base() + { + } + + public ProgrammingErrorException(string message) + : base(message) + { + } + + public ProgrammingErrorException(string message, Exception innerException) + : base(message, innerException) + { + } + } + + private class TestErrorException : Exception + { + public TestErrorException() + : base() + { + } + + public TestErrorException(string message) + : base(message) + { + } + + public TestErrorException(string message, Exception innerException) + : base(message, innerException) + { + } + } + #endregion + } +} diff --git a/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Framework/DataStressFactory.cs b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Framework/DataStressFactory.cs new file mode 100644 index 0000000000..8e06a1de89 --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Framework/DataStressFactory.cs @@ -0,0 +1,955 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading; +using System.Data; +using System.Data.Common; +using System.Diagnostics; + +namespace Stress.Data +{ + /// + /// Base class to generate utility objects required for stress tests to run. For example: connection strings, command texts, + /// data tables and views, and other information + /// + public abstract class DataStressFactory : IDisposable + { + // This is the maximum number of rows, stress will operate on + public const int Depth = 100; + + // A string value to be used for scalar data retrieval while constructing + // a select statement that retrieves multiple result sets. + public static readonly string LargeStringParam = new string('p', 2000); + + // A temp table that when create puts the server session into a non-recoverable state until dropped. + private static readonly string s_tempTableName = string.Format("#stress_{0}", Guid.NewGuid().ToString("N")); + + // The languages used for "SET LANGUAGE [language]" statements that modify the server session state. Let's + // keep error message readable so we're only using english languages. + private static string[] s_languages = new string[] + { + "English", + "British English", + }; + + public DbProviderFactory DbFactory { get; private set; } + + protected DataStressFactory(DbProviderFactory factory) + { + DataStressErrors.Assert(factory != null, "Argument to DataStressFactory constructor is null"); + this.DbFactory = factory; + } + + + public void Dispose() + { + GC.SuppressFinalize(this); + } + + public abstract string GetParameterName(string pName); + + + public abstract bool PrimaryKeyValueIsRequired + { + get; + } + + [Flags] + public enum SelectStatementOptions + { + UseNOLOCK = 0x1, + + // keep last + Default = 0 + } + + #region PoolingStressMode + + public enum PoolingStressMode + { + RandomizeConnectionStrings, // Use many different connection strings with the same identity, which will result in many DbConnectionPoolGroups each containing one DbConnectionPool + } + + protected PoolingStressMode CurrentPoolingStressMode + { + get; + private set; + } + + #endregion + + + /// + /// Creates a new connection and initializes it with random connection string generated from the factory's source + /// Note: if rnd is null, create a connection with minimal string required to connect to the target database + /// + /// Randomizes Connection Pool enablement, the application Name to randomize connection pool + /// + /// + public DataStressConnection CreateConnection(Random rnd = null, ConnectionStringOptions options = ConnectionStringOptions.Default) + { + // Determine connection options (connection string, identity, etc) + string connectionString = CreateBaseConnectionString(rnd, options); + bool clearPoolBeforeClose = false; + + if (rnd != null) + { + // Connection string and/or identity are randomized + + // We implement this using the Application Name field in the connection string since this field + // should not affect behaviour other than connection pooling, since all connections in a pool + // must have the exact same connection string (including Application Name) + + if (rnd.NextBool(.1)) + { + // Disable pooling + connectionString += ";Pooling=false;"; + } + else if (rnd.NextBool(0.001)) + { + // Use a unique Application Name to get a new connection from a new pool. We do this in order to + // stress the code that creates/deletes pools. + connectionString = string.Format("{0}; Pooling=true; Application Name=\"{1}\";", connectionString, GetRandomApplicationName()); + + // Tell DataStressConnection to call SqlConnection.ClearPool when closing the connection. This ensures + // we do not keep a large number of connections in the pool that we will never use again. + clearPoolBeforeClose = true; + } + else + { + switch (CurrentPoolingStressMode) + { + case PoolingStressMode.RandomizeConnectionStrings: + // Use one of the pre-generated Application Names in order to get a pooled connection with a randomized connection string + connectionString = string.Format("{0}; Pooling=true; Application Name=\"{1}\";", connectionString, _applicationNames[rnd.Next(_applicationNames.Count)]); + break; + default: + throw DataStressErrors.UnhandledCaseError(CurrentPoolingStressMode); + } + } + } + + // All options have been determined, now create + DbConnection con = DbFactory.CreateConnection(); + con.ConnectionString = connectionString; + return new DataStressConnection(con, clearPoolBeforeClose); + } + + [Flags] + public enum ConnectionStringOptions + { + Default = 0, + + // by default, MARS is disabled + EnableMars = 0x2, + + // by default, MultiSubnetFailover is enabled + DisableMultiSubnetFailover = 0x8 + } + + /// + /// Creates a new connection string. + /// Note: if rnd is null, create minimal connection string required to connect to the target database (used during setup) + /// Otherwise, string is randomized to enable multiple pools. + /// + public abstract string CreateBaseConnectionString(Random rnd, ConnectionStringOptions options); + + protected virtual int GetNumDifferentApplicationNames() + { + return DataStressSettings.Instance.NumberOfConnectionPools; + } + + private string GetRandomApplicationName() + { + return Guid.NewGuid().ToString(); + } + + + /// + /// Returns index of a random table + /// This will be used to narrow down memory leaks + /// related to specific tables. + /// + public TableMetadata GetRandomTable(Random rnd) + { + return TableMetadataList[rnd.Next(TableMetadataList.Count)]; + } + + /// + /// Returns a random command object + /// + public DbCommand GetCommand(Random rnd, TableMetadata table, DataStressConnection conn, bool query, bool isXml = false) + { + if (query) + { + return GetSelectCommand(rnd, table, conn, isXml); + } + else + { + // make sure arguments are correct + DataStressErrors.Assert(!isXml, "wrong usage of GetCommand: cannot create command with FOR XML that is not query"); + + int select = rnd.Next(4); + switch (select) + { + case 0: + return GetUpdateCommand(rnd, table, conn); + case 1: + return GetInsertCommand(rnd, table, conn); + case 2: + return GetDeleteCommand(rnd, table, conn); + default: + return GetSelectCommand(rnd, table, conn); + } + } + } + + private DbCommand CreateCommand(Random rnd, DataStressConnection conn) + { + DbCommand cmd; + if (conn == null) + { + cmd = DbFactory.CreateCommand(); + } + else + { + cmd = conn.CreateCommand(); + } + + if (rnd != null) + { + cmd.CommandTimeout = rnd.NextBool() ? 30 : 600; + } + + return cmd; + } + + /// + /// Returns a random SELECT command + /// + public DbCommand GetSelectCommand(Random rnd, TableMetadata tableMetadata, DataStressConnection conn, bool isXml = false) + { + DbCommand com = CreateCommand(rnd, conn); + StringBuilder cmdText = new StringBuilder(); + cmdText.Append(GetSelectCommandForMultipleRows(rnd, com, tableMetadata, isXml)); + + // 33% of the time, we also want to add another batch to the select command to allow for + // multiple result sets. + if ((!isXml) && (rnd.Next(0, 3) == 0)) + { + cmdText.Append(";").Append(GetSelectCommandForScalarValue(com)); + } + + if ((!isXml) && ShouldModifySession(rnd)) + { + cmdText.Append(";").Append(GetRandomSessionModificationStatement(rnd)); + } + + com.CommandText = cmdText.ToString(); + return com; + } + + /// + /// Returns a SELECT command that retrieves data from a table + /// + private string GetSelectCommandForMultipleRows(Random rnd, DbCommand com, TableMetadata inputTable, bool isXml) + { + int rowcount = rnd.Next(Depth); + + StringBuilder cmdText = new StringBuilder(); + cmdText.Append("SELECT TOP "); + cmdText.Append(rowcount); //Jonfo added this to prevent table scan of 75k row tables + cmdText.Append(" PrimaryKey"); + + List columns = inputTable.Columns; + int colindex = rnd.Next(0, columns.Count); + + for (int i = 0; i <= colindex; i++) + { + if (columns[i].ColumnName == "PrimaryKey") continue; + cmdText.Append(", "); + cmdText.Append(columns[i].ColumnName); + } + + cmdText.Append(" FROM \""); + cmdText.Append(inputTable.TableName); + cmdText.Append("\" WITH(NOLOCK) WHERE PrimaryKey "); + + // We randomly pick an operator from '>' or '=' to allow for randomization + // of possible rows returned by this query. This approach *may* help + // in reducing the likelihood of multiple threads accessing same rows. + // If multiple threads access same rows, there may be locking issues + // which may be avoided because of this randomization. + string op = rnd.NextBool() ? ">" : "="; + cmdText.Append(op).Append(" "); + + string pName = GetParameterName("P0"); + cmdText.Append(pName); + + DbParameter param = DbFactory.CreateParameter(); + param.ParameterName = pName; + param.Value = GetRandomPK(rnd, inputTable); + param.DbType = DbType.Int32; + com.Parameters.Add(param); + + return cmdText.ToString(); + } + + /// + /// Returns a SELECT command that returns a single string parameter value. + /// + private string GetSelectCommandForScalarValue(DbCommand com) + { + string pName = GetParameterName("P1"); + StringBuilder cmdText = new StringBuilder(); + + cmdText.Append("SELECT ").Append(pName); + + DbParameter param = DbFactory.CreateParameter(); + param.ParameterName = pName; + param.Value = LargeStringParam; + param.Size = LargeStringParam.Length; + param.DbType = DbType.String; + com.Parameters.Add(param); + + return cmdText.ToString(); + } + + /// + /// Returns a random existing Primary Key value + /// + private int GetRandomPK(Random rnd, TableMetadata table) + { + using (DataStressConnection conn = CreateConnection()) + { + conn.Open(); + + // This technique to get a random row comes from http://www.4guysfromrolla.com/webtech/042606-1.shtml + // When you set rowcount and then select into a scalar value, then the query is optimised so that + // just the last value is selected. So if n = ROWCOUNT then the query returns the n'th row. + + int rowNumber = rnd.Next(Depth); + + DbCommand com = conn.CreateCommand(); + string cmdText = string.Format( + @"SET ROWCOUNT {0}; + DECLARE @PK INT; + SELECT @PK = PrimaryKey FROM {1} WITH(NOLOCK) + SELECT @PK", rowNumber, table.TableName); + + com.CommandText = cmdText; + + object result = com.ExecuteScalarSyncOrAsync(CancellationToken.None, rnd).Result; + if (result == DBNull.Value) + { + throw DataStressErrors.TestError(string.Format("Table {0} returned DBNull for primary key", table.TableName)); + } + else + { + int primaryKey = (int)result; + return primaryKey; + } + } + } + + private DbParameter CreateRandomParameter(Random rnd, string prefix, TableColumn column) + { + DbParameter param = DbFactory.CreateParameter(); + + param.ParameterName = GetParameterName(prefix); + + param.Value = GetRandomData(rnd, column); + + return param; + } + + /// + /// Returns a random UPDATE command + /// + public DbCommand GetUpdateCommand(Random rnd, TableMetadata table, DataStressConnection conn) + { + DbCommand com = CreateCommand(rnd, conn); + + StringBuilder cmdText = new StringBuilder(); + cmdText.Append("UPDATE \""); + cmdText.Append(table.TableName); + cmdText.Append("\" SET "); + + List columns = table.Columns; + int numColumns = rnd.Next(2, columns.Count); + bool mostlyNull = rnd.NextBool(0.1); // 10% of rows have 90% chance of each column being null, in order to test nbcrow + + for (int i = 0; i < numColumns; i++) + { + if (columns[i].ColumnName == "PrimaryKey") continue; + if (columns[i].ColumnName.ToUpper() == "TIMESTAMP_FLD") continue; + + if (i > 1) cmdText.Append(", "); + cmdText.Append(columns[i].ColumnName); + cmdText.Append(" = "); + + if (mostlyNull && rnd.NextBool(0.9)) + { + cmdText.Append("NULL"); + } + else + { + DbParameter param = CreateRandomParameter(rnd, string.Format("P{0}", (i + 1)), columns[i]); + cmdText.Append(param.ParameterName); + com.Parameters.Add(param); + } + } + + cmdText.Append(" WHERE PrimaryKey = "); + string pName = GetParameterName("P0"); + cmdText.Append(pName); + DbParameter keyParam = DbFactory.CreateParameter(); + keyParam.ParameterName = pName; + keyParam.Value = GetRandomPK(rnd, table); + com.Parameters.Add(keyParam); + + if (ShouldModifySession(rnd)) + { + cmdText.Append(";").Append(GetRandomSessionModificationStatement(rnd)); + } + + com.CommandText = cmdText.ToString(); + return com; + } + + /// + /// Returns a random INSERT command + /// + public DbCommand GetInsertCommand(Random rnd, TableMetadata table, DataStressConnection conn) + { + DbCommand com = CreateCommand(rnd, conn); + + StringBuilder cmdText = new StringBuilder(); + cmdText.Append("INSERT INTO \""); + cmdText.Append(table.TableName); + cmdText.Append("\" ("); + + StringBuilder valuesText = new StringBuilder(); + valuesText.Append(") VALUES ("); + + List columns = table.Columns; + int numColumns = rnd.Next(2, columns.Count); + bool mostlyNull = rnd.NextBool(0.1); // 10% of rows have 90% chance of each column being null, in order to test nbcrow + + for (int i = 0; i < numColumns; i++) + { + if (columns[i].ColumnName.ToUpper() == "PRIMARYKEY") continue; + + if (i > 1) + { + cmdText.Append(", "); + valuesText.Append(", "); + } + + cmdText.Append(columns[i].ColumnName); + + if (columns[i].ColumnName.ToUpper() == "TIMESTAMP_FLD") + { + valuesText.Append("DEFAULT"); // Cannot insert an explicit value in a timestamp field + } + else if (mostlyNull && rnd.NextBool(0.9)) + { + valuesText.Append("NULL"); + } + else + { + DbParameter param = CreateRandomParameter(rnd, string.Format("P{0}", i + 1), columns[i]); + + valuesText.Append(param.ParameterName); + com.Parameters.Add(param); + } + } + + // To deal databases that do not support auto-incremented columns (Oracle?) + // if (!columns["PrimaryKey"].AutoIncrement) + if (PrimaryKeyValueIsRequired) + { + DbParameter param = CreateRandomParameter(rnd, "P0", table.GetColumn("PrimaryKey")); + cmdText.Append(", PrimaryKey"); + valuesText.Append(", "); + valuesText.Append(param.ParameterName); + com.Parameters.Add(param); + } + + valuesText.Append(")"); + cmdText.Append(valuesText); + + if (ShouldModifySession(rnd)) + { + cmdText.Append(";").Append(GetRandomSessionModificationStatement(rnd)); + } + + com.CommandText = cmdText.ToString(); + return com; + } + + /// + /// Returns a random DELETE command + /// + public DbCommand GetDeleteCommand(Random rnd, TableMetadata table, DataStressConnection conn) + { + DbCommand com = CreateCommand(rnd, conn); + + StringBuilder cmdText = new StringBuilder(); + cmdText.Append("DELETE FROM \""); + + List columns = table.Columns; + string pName = GetParameterName("P0"); + cmdText.Append(table.TableName); + cmdText.Append("\" WHERE PrimaryKey = "); + cmdText.Append(pName); + + DbParameter param = DbFactory.CreateParameter(); + param.ParameterName = pName; + param.Value = GetRandomPK(rnd, table); + com.Parameters.Add(param); + + if (ShouldModifySession(rnd)) + { + cmdText.Append(";").Append(GetRandomSessionModificationStatement(rnd)); + } + + com.CommandText = cmdText.ToString(); + return com; + } + + public bool ShouldModifySession(Random rnd) + { + // 33% of the time, we want to modify the user session on the server + return rnd.NextBool(.33); + } + + /// + /// Returns a random statement that will modify the session on the server. + /// + public string GetRandomSessionModificationStatement(Random rnd) + { + string sessionStmt = null; + int select = rnd.Next(3); + switch (select) + { + case 0: + // Create a SET CONTEXT_INFO statement using a hex string of random data + StringBuilder sb = new StringBuilder("0x"); + int count = rnd.Next(1, 129); + for (int i = 0; i < count; i++) + { + sb.AppendFormat("{0:x2}", (byte)rnd.Next(0, (int)(byte.MaxValue + 1))); + } + string contextInfoData = sb.ToString(); + sessionStmt = string.Format("SET CONTEXT_INFO {0}", contextInfoData); + break; + + case 1: + // Create or drop the temp table + sessionStmt = string.Format("IF OBJECT_ID('tempdb..{0}') IS NULL CREATE TABLE {0}(id INT) ELSE DROP TABLE {0}", s_tempTableName); + break; + + default: + // Create a SET LANGUAGE statement + sessionStmt = string.Format("SET LANGUAGE N'{0}'", s_languages[rnd.Next(s_languages.Length)]); + break; + } + return sessionStmt; + } + + /// + /// Returns random data + /// + public object GetRandomData(Random rnd, TableColumn column) + { + int length = column.MaxLength; + int maxTargetLength = (length > 255 || length == -1) ? 255 : length; + + DbType dbType = GetDbType(column); + return GetRandomData(rnd, dbType, maxTargetLength); + } + + private DbType GetDbType(TableColumn column) + { + switch (column.ColumnName) + { + case "bit_FLD": return DbType.Boolean; + case "tinyint_FLD": return DbType.Byte; + case "smallint_FLD": return DbType.Int16; + case "int_FLD": return DbType.Int32; + case "PrimaryKey": return DbType.Int32; + case "bigint_FLD": return DbType.Int64; + case "real_FLD": return DbType.Single; + case "float_FLD": return DbType.Double; + case "smallmoney_FLD": return DbType.Decimal; + case "money_FLD": return DbType.Decimal; + case "decimal_FLD": return DbType.Decimal; + case "numeric_FLD": return DbType.Decimal; + case "datetime_FLD": return DbType.DateTime; + case "smalldatetime_FLD": return DbType.DateTime; + case "datetime2_FLD": return DbType.DateTime2; + case "timestamp_FLD": return DbType.Binary; + case "date_FLD": return DbType.Date; + case "time_FLD": return DbType.Time; + case "datetimeoffset_FLD": return DbType.DateTimeOffset; + case "uniqueidentifier_FLD": return DbType.Guid; + case "sql_variant_FLD": return DbType.Object; + case "image_FLD": return DbType.Binary; + case "varbinary_FLD": return DbType.Binary; + case "binary_FLD": return DbType.Binary; + case "char_FLD": return DbType.String; + case "varchar_FLD": return DbType.String; + case "text_FLD": return DbType.String; + case "ntext_FLD": return DbType.String; + case "nvarchar_FLD": return DbType.String; + case "nchar_FLD": return DbType.String; + case "nvarcharmax_FLD": return DbType.String; + case "varbinarymax_FLD": return DbType.Binary; + case "varcharmax_FLD": return DbType.String; + case "xml_FLD": return DbType.Xml; + default: throw DataStressErrors.UnhandledCaseError(column.ColumnName); + } + } + + protected virtual object GetRandomData(Random rnd, DbType dbType, int maxLength) + { + byte[] buffer; + switch (dbType) + { + case DbType.Boolean: + return (rnd.Next(2) == 0 ? false : true); + case DbType.Byte: + return rnd.Next(byte.MinValue, byte.MaxValue + 1); + case DbType.Int16: + return rnd.Next(short.MinValue, short.MaxValue + 1); + case DbType.Int32: + return (rnd.Next(2) == 0 ? int.MaxValue / rnd.Next(1, 3) : int.MinValue / rnd.Next(1, 3)); + case DbType.Int64: + return (rnd.Next(2) == 0 ? long.MaxValue / rnd.Next(1, 3) : long.MinValue / rnd.Next(1, 3)); + case DbType.Single: + return rnd.NextDouble() * (rnd.Next(2) == 0 ? float.MaxValue : float.MinValue); + case DbType.Double: + return rnd.NextDouble() * (rnd.Next(2) == 0 ? double.MaxValue : double.MinValue); + case DbType.Decimal: + return rnd.Next(short.MinValue, short.MaxValue + 1); + case DbType.DateTime: + case DbType.DateTime2: + return DateTime.Now; + case DbType.Date: + return DateTime.Now.Date; + case DbType.Time: + return DateTime.Now.TimeOfDay.ToString("c"); + case DbType.DateTimeOffset: + return DateTimeOffset.Now; + case DbType.Guid: + buffer = new byte[16]; + rnd.NextBytes(buffer); + return (new Guid(buffer)); + case DbType.Object: + case DbType.Binary: + rnd.NextBytes(buffer = new byte[rnd.Next(1, maxLength)]); + return buffer; + case DbType.String: + case DbType.Xml: + string openTag = ""; + string closeTag = ""; + int tagLength = openTag.Length + closeTag.Length; + + if (tagLength > maxLength) + { + // Case (1): tagLength > maxTargetLength + return ""; + } + else + { + StringBuilder builder = new StringBuilder(maxLength); + + builder.Append(openTag); + + // The data is just a repeat of one character because to the managed provider + // it is only really the length that matters, not the content of the data + char characterToUse = (char)rnd.Next((int)'@', (int)'~'); // Choosing random characters in this range to avoid special + // xml chars like '<' or '&' + int numRepeats = rnd.Next(0, maxLength - tagLength); // Case (2): tagLength == maxTargetLength + // Case (3): tagLength < maxTargetLength <-- most common + builder.Append(characterToUse, numRepeats); + + builder.Append(closeTag); + + DataStressErrors.Assert(builder.Length <= maxLength, "Incorrect length of randomly generated string"); + + return builder.ToString(); + } + default: + throw DataStressErrors.UnhandledCaseError(dbType); + } + } + + #region Table information to be used by stress + + // method used to create stress tables in the database + protected void BuildUserTables(List TableMetadataList) + { + string CreateTable1 = + "CREATE TABLE stress_test_table_1 (PrimaryKey int identity(1,1) primary key, int_FLD int, smallint_FLD smallint, real_FLD real, float_FLD float, decimal_FLD decimal(28,4), " + + "smallmoney_FLD smallmoney, bit_FLD bit, tinyint_FLD tinyint, uniqueidentifier_FLD uniqueidentifier, varbinary_FLD varbinary(756), binary_FLD binary(756), " + + "image_FLD image, varbinarymax_FLD varbinary(max), timestamp_FLD timestamp, char_FLD char(756), text_FLD text, varcharmax_FLD varchar(max), " + + "varchar_FLD varchar(756), nchar_FLD nchar(756), ntext_FLD ntext, nvarcharmax_FLD nvarchar(max), nvarchar_FLD nvarchar(756), datetime_FLD datetime, " + + "smalldatetime_FLD smalldatetime);" + + "CREATE UNIQUE INDEX stress_test_table_1 on stress_test_table_1 ( PrimaryKey );" + + "insert into stress_test_table_1(int_FLD, smallint_FLD, real_FLD, float_FLD, decimal_FLD, " + + "smallmoney_FLD, bit_FLD, tinyint_FLD, uniqueidentifier_FLD, varbinary_FLD, binary_FLD, " + + "image_FLD, varbinarymax_FLD, char_FLD, text_FLD, varcharmax_FLD, " + + "varchar_FLD, nchar_FLD, ntext_FLD, nvarcharmax_FLD, nvarchar_FLD, datetime_FLD, " + + "smalldatetime_FLD) values ( 0, 0, 0, 0, 0, $0, 0, 0, '00000000-0000-0000-0000-000000000000', " + + "0x00, 0x00, 0x00, 0x00, '0', '0', '0', '0', N'0', N'0', N'0', N'0', '01/11/2000 12:54:01', '01/11/2000 12:54:00' );" + ; + + string CreateTable2 = + "CREATE TABLE stress_test_table_2 (PrimaryKey int identity(1,1) primary key, bigint_FLD bigint, money_FLD money, numeric_FLD numeric, " + + "time_FLD time, date_FLD date, datetimeoffset_FLD datetimeoffset, sql_variant_FLD sql_variant, " + + "datetime2_FLD datetime2, xml_FLD xml);" + + "CREATE UNIQUE INDEX stress_test_table_2 on stress_test_table_2 ( PrimaryKey );" + + "insert into stress_test_table_2(bigint_FLD, money_FLD, numeric_FLD, " + + "time_FLD, date_FLD, datetimeoffset_FLD, sql_variant_FLD, " + + "datetime2_FLD, xml_FLD) values ( 0, $0, 0, '01/11/2015 12:54:01', '01/11/2015 12:54:01', '01/11/2000 12:54:01 -08:00', 0, '01/11/2000 12:54:01', '0' );" + ; + + if (TableMetadataList == null) + { + TableMetadataList = new List(); + } + + List tableColumns1 = new List(); + tableColumns1.Add(new TableColumn("PrimaryKey", -1)); + tableColumns1.Add(new TableColumn("int_FLD", -1)); + tableColumns1.Add(new TableColumn("smallint_FLD", -1)); + tableColumns1.Add(new TableColumn("real_FLD", -1)); + tableColumns1.Add(new TableColumn("float_FLD", -1)); + tableColumns1.Add(new TableColumn("decimal_FLD", -1)); + tableColumns1.Add(new TableColumn("smallmoney_FLD", -1)); + tableColumns1.Add(new TableColumn("bit_FLD", -1)); + tableColumns1.Add(new TableColumn("tinyint_FLD", -1)); + tableColumns1.Add(new TableColumn("uniqueidentifier_FLD", -1)); + tableColumns1.Add(new TableColumn("varbinary_FLD", 756)); + tableColumns1.Add(new TableColumn("binary_FLD", 756)); + tableColumns1.Add(new TableColumn("image_FLD", -1)); + tableColumns1.Add(new TableColumn("varbinarymax_FLD", -1)); + tableColumns1.Add(new TableColumn("timestamp_FLD", -1)); + tableColumns1.Add(new TableColumn("char_FLD", -1)); + tableColumns1.Add(new TableColumn("text_FLD", -1)); + tableColumns1.Add(new TableColumn("varcharmax_FLD", -1)); + tableColumns1.Add(new TableColumn("varchar_FLD", 756)); + tableColumns1.Add(new TableColumn("nchar_FLD", 756)); + tableColumns1.Add(new TableColumn("ntext_FLD", -1)); + tableColumns1.Add(new TableColumn("nvarcharmax_FLD", -1)); + tableColumns1.Add(new TableColumn("nvarchar_FLD", 756)); + tableColumns1.Add(new TableColumn("datetime_FLD", -1)); + tableColumns1.Add(new TableColumn("smalldatetime_FLD", -1)); + TableMetadata tableMeta1 = new TableMetadata("stress_test_table_1", tableColumns1); + TableMetadataList.Add(tableMeta1); + + List tableColumns2 = new List(); + tableColumns2.Add(new TableColumn("PrimaryKey", -1)); + tableColumns2.Add(new TableColumn("bigint_FLD", -1)); + tableColumns2.Add(new TableColumn("money_FLD", -1)); + tableColumns2.Add(new TableColumn("numeric_FLD", -1)); + tableColumns2.Add(new TableColumn("time_FLD", -1)); + tableColumns2.Add(new TableColumn("date_FLD", -1)); + tableColumns2.Add(new TableColumn("datetimeoffset_FLD", -1)); + tableColumns2.Add(new TableColumn("sql_variant_FLD", -1)); + tableColumns2.Add(new TableColumn("datetime2_FLD", -1)); + tableColumns2.Add(new TableColumn("xml_FLD", -1)); + TableMetadata tableMeta2 = new TableMetadata("stress_test_table_2", tableColumns2); + TableMetadataList.Add(tableMeta2); + + using (DataStressConnection conn = CreateConnection(null)) + { + conn.Open(); + using (DbCommand com = conn.CreateCommand()) + { + try + { + com.CommandText = CreateTable1; + com.ExecuteNonQuery(); + } + catch (DbException de) + { + // This can be improved by doing a Drop Table if exists. + if (de.Message.Contains("There is already an object named \'" + tableMeta1.TableName + "\' in the database.")) + { + CleanupUserTables(tableMeta1); + com.ExecuteNonQuery(); + } + else + { + throw; + } + } + + try + { + com.CommandText = CreateTable2; + com.ExecuteNonQuery(); + } + catch (DbException de) + { + // This can be improved by doing a Drop Table if exists in the query itself. + if (de.Message.Contains("There is already an object named \'" + tableMeta2.TableName + "\' in the database.")) + { + CleanupUserTables(tableMeta2); + com.ExecuteNonQuery(); + } + else + { + throw; + } + } + + for (int i = 0; i < Depth; i++) + { + TrackedRandom randomInstance = new TrackedRandom(); + randomInstance.Mark(); + + DbCommand comInsert1 = GetInsertCommand(randomInstance, tableMeta1, conn); + comInsert1.ExecuteNonQuery(); + + DbCommand comInsert2 = GetInsertCommand(randomInstance, tableMeta2, conn); + comInsert2.ExecuteNonQuery(); + } + } + } + } + + // method used to delete stress tables in the database + protected void CleanupUserTables(TableMetadata tableMetadata) + { + string DropTable = "drop TABLE " + tableMetadata.TableName + ";"; + + using (DataStressConnection conn = CreateConnection(null)) + { + conn.Open(); + using (DbCommand com = conn.CreateCommand()) + { + try + { + com.CommandText = DropTable; + com.ExecuteNonQuery(); + } + catch (Exception) { } + } + } + } + + public List TableMetadataList + { + get; + private set; + } + + public class TableMetadata + { + private string _tableName; + private List _columns = new List(); + + public TableMetadata(string tbleName, List cols) + { + _tableName = tbleName; + _columns = cols; + } + + public string TableName + { + get { return _tableName; } + } + + public List Columns + { + get { return _columns; } + } + + public TableColumn GetColumn(string colName) + { + foreach (TableColumn column in _columns) + { + if (column.ColumnName.Equals(colName)) + { + return column; + } + } + return null; + } + } + + public class TableColumn + { + private string _columnName; + private int _maxLength; + + public TableColumn(string colName, int maxLen) + { + _columnName = colName; + _maxLength = maxLen; + } + + public string ColumnName + { + get { return _columnName; } + } + + public int MaxLength + { + get { return _maxLength; } + } + } + + private List _applicationNames; + + /// + /// Gets schema of all tables from the back-end database and fills + /// the m_Tables DataSet with this schema. This DataSet is used to + /// generate random command text for tests. + /// + public void InitializeSharedData(DataSource source) + { + Trace.WriteLine("Creating shared objects", this.ToString()); + + // Initialize m_sharedDataSet + TableMetadataList = new List(); + BuildUserTables(TableMetadataList); + + // Initialize m_applicationNames + _applicationNames = new List(); + for (int i = 0; i < GetNumDifferentApplicationNames(); i++) + { + _applicationNames.Add(GetRandomApplicationName()); + } + + // Initialize CurrentPoolingStressMode + CurrentPoolingStressMode = PoolingStressMode.RandomizeConnectionStrings; + + + Trace.WriteLine("Finished creating shared objects", this.ToString()); + } + + public void CleanupSharedData() + { + foreach (TableMetadata meta in TableMetadataList) + { + CleanupUserTables(meta); + } + TableMetadataList = null; + } + + public abstract void CreateDatabase(DataSource source); + public abstract void DropDatabase(DataSource source); + + #endregion + } +} diff --git a/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Framework/DataStressReader.cs b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Framework/DataStressReader.cs new file mode 100644 index 0000000000..cad1bfa579 --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Framework/DataStressReader.cs @@ -0,0 +1,350 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Xml; +using System.Data; +using System.Data.Common; +using Microsoft.Data.SqlClient; +using System.Data.SqlTypes; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace Stress.Data +{ + public class DataStressReader : IDisposable + { + #region Type method mapping + + private static Dictionary>> s_sqlTypes; + private static Dictionary>> s_clrTypes; + + static DataStressReader() + { + InitSqlTypes(); + InitClrTypes(); + } + + private static void InitSqlTypes() + { + s_sqlTypes = new Dictionary>>(); + + s_sqlTypes.Add(typeof(SqlBinary), (reader, ordinal, token, rnd) => reader.GetFieldValueSyncOrAsync(ordinal, token, rnd)); + s_sqlTypes.Add(typeof(SqlBoolean), (reader, ordinal, token, rnd) => reader.GetFieldValueSyncOrAsync(ordinal, token, rnd)); + s_sqlTypes.Add(typeof(SqlByte), (reader, ordinal, token, rnd) => reader.GetFieldValueSyncOrAsync(ordinal, token, rnd)); + s_sqlTypes.Add(typeof(SqlBytes), (reader, ordinal, token, rnd) => reader.GetFieldValueSyncOrAsync(ordinal, token, rnd)); + s_sqlTypes.Add(typeof(SqlChars), (reader, ordinal, token, rnd) => reader.GetFieldValueSyncOrAsync(ordinal, token, rnd)); + s_sqlTypes.Add(typeof(SqlDateTime), (reader, ordinal, token, rnd) => reader.GetFieldValueSyncOrAsync(ordinal, token, rnd)); + s_sqlTypes.Add(typeof(SqlDecimal), (reader, ordinal, token, rnd) => reader.GetFieldValueSyncOrAsync(ordinal, token, rnd)); + s_sqlTypes.Add(typeof(SqlDouble), (reader, ordinal, token, rnd) => reader.GetFieldValueSyncOrAsync(ordinal, token, rnd)); + s_sqlTypes.Add(typeof(SqlGuid), (reader, ordinal, token, rnd) => reader.GetFieldValueSyncOrAsync(ordinal, token, rnd)); + s_sqlTypes.Add(typeof(SqlInt16), (reader, ordinal, token, rnd) => reader.GetFieldValueSyncOrAsync(ordinal, token, rnd)); + s_sqlTypes.Add(typeof(SqlInt32), (reader, ordinal, token, rnd) => reader.GetFieldValueSyncOrAsync(ordinal, token, rnd)); + s_sqlTypes.Add(typeof(SqlInt64), (reader, ordinal, token, rnd) => reader.GetFieldValueSyncOrAsync(ordinal, token, rnd)); + s_sqlTypes.Add(typeof(SqlMoney), (reader, ordinal, token, rnd) => reader.GetFieldValueSyncOrAsync(ordinal, token, rnd)); + s_sqlTypes.Add(typeof(SqlSingle), (reader, ordinal, token, rnd) => reader.GetFieldValueSyncOrAsync(ordinal, token, rnd)); + s_sqlTypes.Add(typeof(SqlString), (reader, ordinal, token, rnd) => reader.GetFieldValueSyncOrAsync(ordinal, token, rnd)); + s_sqlTypes.Add(typeof(SqlXml), (reader, ordinal, token, rnd) => reader.GetFieldValueSyncOrAsync(ordinal, token, rnd)); + } + + private static void InitClrTypes() + { + s_clrTypes = new Dictionary>>(); + + s_clrTypes.Add(typeof(bool), (reader, ordinal, token, rnd) => reader.GetFieldValueSyncOrAsync(ordinal, token, rnd)); + s_clrTypes.Add(typeof(byte), (reader, ordinal, token, rnd) => reader.GetFieldValueSyncOrAsync(ordinal, token, rnd)); + s_clrTypes.Add(typeof(short), (reader, ordinal, token, rnd) => reader.GetFieldValueSyncOrAsync(ordinal, token, rnd)); + s_clrTypes.Add(typeof(int), (reader, ordinal, token, rnd) => reader.GetFieldValueSyncOrAsync(ordinal, token, rnd)); + s_clrTypes.Add(typeof(long), (reader, ordinal, token, rnd) => reader.GetFieldValueSyncOrAsync(ordinal, token, rnd)); + s_clrTypes.Add(typeof(float), (reader, ordinal, token, rnd) => reader.GetFieldValueSyncOrAsync(ordinal, token, rnd)); + s_clrTypes.Add(typeof(double), (reader, ordinal, token, rnd) => reader.GetFieldValueSyncOrAsync(ordinal, token, rnd)); + s_clrTypes.Add(typeof(string), (reader, ordinal, token, rnd) => reader.GetFieldValueSyncOrAsync(ordinal, token, rnd)); + s_clrTypes.Add(typeof(char), (reader, ordinal, token, rnd) => reader.GetFieldValueSyncOrAsync(ordinal, token, rnd)); + s_clrTypes.Add(typeof(decimal), (reader, ordinal, token, rnd) => reader.GetFieldValueSyncOrAsync(ordinal, token, rnd)); + s_clrTypes.Add(typeof(Guid), (reader, ordinal, token, rnd) => reader.GetFieldValueSyncOrAsync(ordinal, token, rnd)); + s_clrTypes.Add(typeof(DateTime), (reader, ordinal, token, rnd) => reader.GetFieldValueSyncOrAsync(ordinal, token, rnd)); + s_clrTypes.Add(typeof(TimeSpan), (reader, ordinal, token, rnd) => reader.GetFieldValueSyncOrAsync(ordinal, token, rnd)); + s_clrTypes.Add(typeof(DateTimeOffset), (reader, ordinal, token, rnd) => reader.GetFieldValueSyncOrAsync(ordinal, token, rnd)); + } + + #endregion + + private readonly DbDataReader _reader; + private SemaphoreSlim _closeAsyncSemaphore; + + public DataStressReader(DbDataReader internalReader) + { + _reader = internalReader; + } + + public void Close() + { + _reader.Dispose(); + } + + public void Dispose() + { + _reader.Dispose(); + if (_closeAsyncSemaphore != null) _closeAsyncSemaphore.Dispose(); + } + + public Task CloseAsync() + { + _closeAsyncSemaphore = new SemaphoreSlim(1); + return Task.Run(() => ExecuteWithCloseAsyncSemaphore(Close)); + } + + /// + /// Executes the action while holding the CloseAsync Semaphore. + /// This MUST be used for reader.Close() and all methods that are not safe to call at the same time as reader.Close(), i.e. all sync methods. + /// Otherwise we will see AV's. + /// + public void ExecuteWithCloseAsyncSemaphore(Action a) + { + try + { + if (_closeAsyncSemaphore != null) _closeAsyncSemaphore.Wait(); + a(); + } + finally + { + if (_closeAsyncSemaphore != null) _closeAsyncSemaphore.Release(); + } + } + + /// + /// Executes the action while holding the CloseAsync Semaphore. + /// This MUST be used for reader.Close() and all methods that are not safe to call at the same time as reader.Close(), i.e. all sync methods. + /// Otherwise we will see AV's. + /// + public T ExecuteWithCloseAsyncSemaphore(Func f) + { + try + { + if (_closeAsyncSemaphore != null) _closeAsyncSemaphore.Wait(); + return f(); + } + finally + { + if (_closeAsyncSemaphore != null) _closeAsyncSemaphore.Release(); + } + } + + #region SyncOrAsync methods + + public Task ReadSyncOrAsync(CancellationToken token, Random rnd) + { + return AsyncUtils.SyncOrAsyncMethod( + () => ExecuteWithCloseAsyncSemaphore(() => _reader.Read()), + () => ExecuteWithCloseAsyncSemaphore(() => _reader.ReadAsync(token)), + AsyncUtils.ChooseSyncAsyncMode(rnd) + ); + } + + public Task NextResultSyncOrAsync(CancellationToken token, Random rnd) + { + return AsyncUtils.SyncOrAsyncMethod( + () => ExecuteWithCloseAsyncSemaphore(() => _reader.NextResult()), + () => ExecuteWithCloseAsyncSemaphore(() => _reader.NextResultAsync(token)), + AsyncUtils.ChooseSyncAsyncMode(rnd) + ); + } + + public Task IsDBNullSyncOrAsync(int ordinal, CancellationToken token, Random rnd) + { + return AsyncUtils.SyncOrAsyncMethod( + () => ExecuteWithCloseAsyncSemaphore(() => _reader.IsDBNull(ordinal)), + () => ExecuteWithCloseAsyncSemaphore(() => _reader.IsDBNullAsync(ordinal, token)), + AsyncUtils.ChooseSyncAsyncMode(rnd) + ); + } + + public Task GetValueSyncOrAsync(int ordinal, CancellationToken token, Random rnd) + { + if (rnd.NextBool(0.3)) + { + // Use sync-only GetValue + return Task.FromResult(GetValue(ordinal)); + } + else + { + // Use GetFieldValue or GetFieldValueAsync + Func> getFieldValueFunc = null; + + if (rnd.NextBool()) + { + // Choose provider-specific getter + Type sqlType = GetProviderSpecificFieldType(ordinal); + s_sqlTypes.TryGetValue(sqlType, out getFieldValueFunc); + } + else + { + // Choose clr type getter + Type clrType = GetFieldType(ordinal); + s_clrTypes.TryGetValue(clrType, out getFieldValueFunc); + } + + if (getFieldValueFunc != null) + { + // Execute the type-specific func, e.g. GetFieldValue or GetFieldValueAsync + return getFieldValueFunc(this, ordinal, token, rnd); + } + else + { + // Execute GetFieldValue or GetFieldValueAsync as a fallback + return GetFieldValueSyncOrAsync(ordinal, token, rnd); + } + } + } + + private Task GetFieldValueSyncOrAsync(int ordinal, CancellationToken token, Random rnd) + { + return AsyncUtils.SyncOrAsyncMethod( + () => ExecuteWithCloseAsyncSemaphore(() => _reader.GetFieldValue(ordinal)), + async () => ((object)(await ExecuteWithCloseAsyncSemaphore(() => _reader.GetFieldValueAsync(ordinal, token)))), + AsyncUtils.ChooseSyncAsyncMode(rnd) + ); + } + + #endregion + + #region Sync-only methods + + public long GetBytes(int ordinal, long dataOffset, byte[] buffer, int bufferOffset, int length) + { + return ExecuteWithCloseAsyncSemaphore(() => _reader.GetBytes(ordinal, dataOffset, buffer, bufferOffset, length)); + } + + public long GetChars(int ordinal, long dataOffset, char[] buffer, int bufferOffset, int length) + { + return ExecuteWithCloseAsyncSemaphore(() => _reader.GetChars(ordinal, dataOffset, buffer, bufferOffset, length)); + } + + public Type GetFieldType(int ordinal) + { + return ExecuteWithCloseAsyncSemaphore(() => _reader.GetFieldType(ordinal)); + } + + public string GetName(int ordinal) + { + return ExecuteWithCloseAsyncSemaphore(() => _reader.GetName(ordinal)); + } + + public Type GetProviderSpecificFieldType(int ordinal) + { + return ExecuteWithCloseAsyncSemaphore(() => _reader.GetProviderSpecificFieldType(ordinal)); + } + + + public DataStressStream GetStream(int ordinal) + { + Stream s = ExecuteWithCloseAsyncSemaphore(() => _reader.GetStream(ordinal)); + return new DataStressStream(s, this); + } + + public DataStressTextReader GetTextReader(int ordinal) + { + TextReader t = ExecuteWithCloseAsyncSemaphore(() => _reader.GetTextReader(ordinal)); + return new DataStressTextReader(t, this); + } + + public DataStressXmlReader GetXmlReader(int ordinal) + { + XmlReader x = ExecuteWithCloseAsyncSemaphore(() => ((SqlDataReader)_reader).GetXmlReader(ordinal)); + return new DataStressXmlReader(x, this); + } + + public object GetValue(int ordinal) + { + return ExecuteWithCloseAsyncSemaphore(() => _reader.GetValue(ordinal)); + } + + public int FieldCount + { + get { return ExecuteWithCloseAsyncSemaphore(() => _reader.FieldCount); } + } + + #endregion + } + + public class DataStressStream : IDisposable + { + private Stream _stream; + private DataStressReader _reader; + + public DataStressStream(Stream stream, DataStressReader reader) + { + _stream = stream; + _reader = reader; + } + + public void Dispose() + { + _stream.Dispose(); + } + + public Task ReadSyncOrAsync(byte[] buffer, int offset, int count, CancellationToken token, Random rnd) + { + return AsyncUtils.SyncOrAsyncMethod( + () => _reader.ExecuteWithCloseAsyncSemaphore(() => _stream.Read(buffer, offset, count)), + () => _reader.ExecuteWithCloseAsyncSemaphore(() => _stream.ReadAsync(buffer, offset, count)), + AsyncUtils.ChooseSyncAsyncMode(rnd) + ); + } + } + + public class DataStressTextReader : IDisposable + { + private TextReader _textReader; + private DataStressReader _reader; + + public DataStressTextReader(TextReader textReader, DataStressReader reader) + { + _textReader = textReader; + _reader = reader; + } + + public void Dispose() + { + _textReader.Dispose(); + } + + public int Peek() + { + return _reader.ExecuteWithCloseAsyncSemaphore(() => _textReader.Peek()); + } + + public Task ReadSyncOrAsync(char[] buffer, int index, int count, Random rnd) + { + return AsyncUtils.SyncOrAsyncMethod( + () => _reader.ExecuteWithCloseAsyncSemaphore(() => _textReader.Read(buffer, index, count)), + () => _reader.ExecuteWithCloseAsyncSemaphore(() => _textReader.ReadAsync(buffer, index, count)), + AsyncUtils.ChooseSyncAsyncMode(rnd)); + } + } + + public class DataStressXmlReader : IDisposable + { + private XmlReader _xmlReader; + private DataStressReader _reader; + + public DataStressXmlReader(XmlReader xmlReader, DataStressReader reader) + { + _xmlReader = xmlReader; + _reader = reader; + } + + public void Dispose() + { + _xmlReader.Dispose(); + } + + public void Read() + { + _reader.ExecuteWithCloseAsyncSemaphore(() => _xmlReader.Read()); + } + } +} diff --git a/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Framework/DataStressSettings.cs b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Framework/DataStressSettings.cs new file mode 100644 index 0000000000..8ddf737015 --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Framework/DataStressSettings.cs @@ -0,0 +1,310 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Stress.Data +{ + /// + /// Loads dataStressSettings section from Stress.Data.Framework.dll.config (App.config in source tree) + /// + public class DataStressSettings + { + internal static readonly string s_defaultConfigFileName = "StressTests.config.jsonc"; + + public DataStressSettings(string configFileName) + { + _dataStressConfigSection = new(configFileName); + } + + private static DataStressSettings s_instance; + + public static DataStressSettings Instance + { + get + { + if (s_instance is null) + { + var cfg = Environment.GetEnvironmentVariable("STRESS_CONFIG_FILE"); + if (cfg is null) + { + cfg = s_defaultConfigFileName; + } + s_instance = new(cfg); + } + + s_instance.Load(); + + return s_instance; + } + } + + private readonly DataStressConfigurationSection _dataStressConfigSection; + + // list of sources read from the config file + private Dictionary _sources = new Dictionary(StringComparer.CurrentCultureIgnoreCase); + public ErrorHandlingAction ActionOnProductError + { + get; + private set; + } + public ErrorHandlingAction ActionOnTestError + { + get; + private set; + } + public ErrorHandlingAction ActionOnProgrammingError + { + get; + private set; + } + + public int NumberOfConnectionPools + { + get; + private set; + } + + #region Configuration file handlers + + private class DataStressConfigurationSection + { + private List _sources = new List(); + private ErrorHandlingPolicyElement _errorHandlingPolicy = new ErrorHandlingPolicyElement(); + private ConnectionPoolPolicyElement _connectionPoolPolicy = new ConnectionPoolPolicyElement(); + private readonly StressConfigReader _reader; + + public DataStressConfigurationSection(string configFileName) + { + _reader = new StressConfigReader(configFileName); + } + + public List Sources + { + get + { + if(_sources.Count == 0) + { + _reader.Load(); + _sources = _reader.Sources; + } + return _sources; + } + } + + public ErrorHandlingPolicyElement ErroHandlingPolicy + { + get + { + return _errorHandlingPolicy; + } + } + + public ConnectionPoolPolicyElement ConnectionPoolPolicy + { + get + { + return _connectionPoolPolicy; + } + } + } + + + internal class DataSourceElement + { + private string _name; + private string _type; + private bool _isDefault = false; + + public readonly Dictionary SourceProperties = new Dictionary(); + + + public DataSourceElement(string ds_name, + string ds_type, + string ds_server, + string ds_datasource, + string ds_entraIdUser, + string ds_entraIdPassword, + string ds_user, + string ds_password, + bool ds_isDefault = false, + bool ds_winAuth = false, + bool ds_isLocal = false, + string ds_dbFile = null, + bool disableMultiSubnetFailoverSetup = true, + bool disableNamedPipes = true, + bool encrypt = false) + { + _name = ds_name; + _type = ds_type; + _isDefault = ds_isDefault; + + if (ds_server != null) + { + SourceProperties.Add("server", ds_server); + } + if (ds_datasource != null) + { + SourceProperties.Add("dataSource", ds_datasource); + } + if (ds_entraIdUser != null) + { + SourceProperties.Add("entraIdUser", ds_entraIdUser); + } + if (ds_entraIdPassword != null) + { + SourceProperties.Add("entraIdPassword", ds_entraIdPassword); + } + if (ds_user != null) + { + SourceProperties.Add("user", ds_user); + } + if (ds_password != null) + { + SourceProperties.Add("password", ds_password); + } + + SourceProperties.Add("supportsWindowsAuthentication", ds_winAuth.ToString()); + SourceProperties.Add("isLocal", ds_isLocal.ToString()); + + SourceProperties.Add("DisableMultiSubnetFailoverSetup", disableMultiSubnetFailoverSetup.ToString()); + + SourceProperties.Add("DisableNamedPipes", disableNamedPipes.ToString()); + + SourceProperties.Add("Encrypt", encrypt.ToString()); + + if (ds_dbFile != null) + { + SourceProperties.Add("databaseFile", ds_dbFile); + } + } + + public string Name + { + get { return _name; } + } + + public string Type + { + get { return _type; } + } + + public bool IsDefault + { + get { return _isDefault; } + } + } + + private class ErrorHandlingPolicyElement + { + private string _onProductError = "debugBreak"; + private string _onTestError = "throwException"; + private string _onProgrammingError = "debugBreak"; + + public string OnProductError + { + get + { + return _onProductError; + } + } + + public string OnTestError + { + get + { + return _onTestError; + } + } + + public string OnProgrammingError + { + get + { + return _onProgrammingError; + } + } + } + + private class ConnectionPoolPolicyElement + { + private int _numberOfPools = 10; + + public int NumberOfPools + { + get + { + return _numberOfPools; + } + } + } + + #endregion + + /// + /// loads the configuration data from the app config file (Stress.Data.Framework.dll.config) and initializes the Sources collection + /// + private void Load() + { + // Parse + foreach (DataSourceElement sourceElement in _dataStressConfigSection.Sources) + { + // if Parse raises exception, check that the type attribute is set to the relevant the SourceType enumeration value name + DataSourceType sourceType = (DataSourceType)Enum.Parse(typeof(DataSourceType), sourceElement.Type, true); + + DataSource newSource = DataSource.Create(sourceElement.Name, sourceType, sourceElement.IsDefault, sourceElement.SourceProperties); + _sources.Add(newSource.Name, newSource); + } + + // Parse + // if Parse raises exception, check that the action attribute is set to a valid ActionOnProductBugFound enumeration value name + this.ActionOnProductError = (ErrorHandlingAction)Enum.Parse(typeof(ErrorHandlingAction), _dataStressConfigSection.ErroHandlingPolicy.OnProductError, true); + this.ActionOnTestError = (ErrorHandlingAction)Enum.Parse(typeof(ErrorHandlingAction), _dataStressConfigSection.ErroHandlingPolicy.OnTestError, true); + this.ActionOnProgrammingError = (ErrorHandlingAction)Enum.Parse(typeof(ErrorHandlingAction), _dataStressConfigSection.ErroHandlingPolicy.OnProgrammingError, true); + + // Parse + this.NumberOfConnectionPools = _dataStressConfigSection.ConnectionPoolPolicy.NumberOfPools; + } + + + /// + /// use this method to retrieve the source data by its name (represented with 'name' attribute in config file) + /// + /// case-sensitive name + public DataSource GetSourceByName(string name) + { + return _sources[name]; + } + + /// + /// Use this method to retrieve the default source associated with the type specified. + /// The type of the node is specified with 'type' attribute on the sources file - see DataSourceType enum for list of supported types. + /// If there is a source node with isDefault=true, this node is returned (first one found in config file). + /// Otherwise, first source node from type specified is returned. + /// + public DataSource GetDefaultSourceByType(DataSourceType type) + { + DataSource defaultSource = null; + foreach (DataSource source in _sources.Values) + { + if (source.Type == type) + { + if (defaultSource == null) + { + // use the first found source, if default is not set + defaultSource = source; + } + else if (source.IsDefault) + { + defaultSource = source; + break; + } + } + } + return defaultSource; + } + } +} diff --git a/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Framework/DataTestGroup.cs b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Framework/DataTestGroup.cs new file mode 100644 index 0000000000..ea2bfe6553 --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Framework/DataTestGroup.cs @@ -0,0 +1,713 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Data; +using System.Data.Common; +using Microsoft.Data.SqlClient; +using System.Data.SqlTypes; +using System.Diagnostics; +using System.Threading; +using System.Threading.Tasks; + +using DPStressHarness; + +namespace Stress.Data +{ + /// + /// basic set of tests to run on each managed provider + /// + public abstract class DataTestGroup + { + // random is not thread-safe, create one per thread - use RandomInstance to access it. + // note that each thread and each test method has a different instance of this object, so it + // doesn't need to be synchronised or have [ThreadStatic], etc + private TrackedRandom _randomInstance = new TrackedRandom(); + protected Random RandomInstance + { + get + { + _randomInstance.Mark(); + return _randomInstance; + } + } + + /// + /// Test factory to use for generation of connection strings and other test objects. Factory is initialized during setup. + /// null is not returned - if setup was not called yet, exception is raised + /// This is static so that is shared across all threads (since stresstest will create a new DataTestGroup object for each thread) + /// + private static DataStressFactory s_factory; + public static DataStressFactory Factory + { + get + { + DataStressErrors.Assert(s_factory != null, "Tried to access DataTestGroup.Factory before Setup has been called"); + return s_factory; + } + } + + /// + /// This method is called to create the stress factory used to create connections, commands, etc... + /// Implementation should set the source and the scenario to valid values if inputs are null/empty. + /// + /// Scenario string specified by the user or empty to set default + /// DataSource string specified by the user or empty to use connection string as is, useful when developing new tests + protected abstract DataStressFactory CreateFactory(ref string scenario, ref DataSource source); + + /// + /// scenario to run, initialized in setup + /// null is not returned - if setup was not called yet, exception is raised + /// This is static so that is shared across all threads (since stresstest will create a new DataTestGroup object for each thread) + /// + private static string s_scenario; + protected static string Scenario + { + get + { + DataStressErrors.Assert(s_scenario != null, "Tried to access DataTestGroup.Scenario before Setup has been called"); + return s_scenario; + } + } + + /// + /// data source information used by stress, initialized in Setup + /// null is not returned - if setup was not called yet, exception is raised + /// This is static so that is shared across all threads (since stresstest will create a new DataTestGroup object for each thread) + /// + private static DataSource s_source; + protected static DataSource Source + { + get + { + DataStressErrors.Assert(s_source != null, "Tried to access DataTestGroup.Source before Setup has been called"); + return s_source; + } + } + + + /// + /// Does test setup that is shared across all threads. This method will be called only once, before + /// any [TestSetup] methods are called. + /// If you override this method you must call base.GlobalTestSetup() at the beginning. + /// + [GlobalTestSetup] + public virtual void GlobalTestSetup() + { + Console.WriteLine("DataTestGroup.GlobalTestSetup(): Starting..."); + + // Preconditions - ensure this setup is only called once + DataStressErrors.Assert(string.IsNullOrEmpty(s_scenario), "Scenario was already set"); + DataStressErrors.Assert(s_source == null, "Source was already set"); + DataStressErrors.Assert(s_factory == null, "Factory was already set"); + + // Set m_scenario + string userProvidedScenario; + TestMetrics.Overrides.TryGetValue("scenario", out userProvidedScenario); + // Empty means default scenario for the test group + s_scenario = (userProvidedScenario ?? string.Empty); + s_scenario = s_scenario.ToUpperInvariant(); + + // Set m_source + // Empty means that test group will peek the default data source from the config file based on the scenario + string userProvidedSourceName; + if (TestMetrics.Overrides.TryGetValue("source", out userProvidedSourceName)) + { + s_source = DataStressSettings.Instance.GetSourceByName(userProvidedSourceName); + } + + // Set m_factory + s_factory = CreateFactory(ref s_scenario, ref s_source); + + Console.WriteLine($"GlobalTestSetup factory created:"); + Console.WriteLine($" Scenario: {s_scenario}"); + Console.WriteLine($" Factory: {s_factory.GetType().Name}"); + Console.WriteLine($" Source:"); + s_source.Emit(4); + + s_factory.CreateDatabase(s_source); + s_factory.InitializeSharedData(s_source); + + // Postconditions + DataStressErrors.Assert(!string.IsNullOrEmpty(s_scenario), "Scenario was not set"); + DataStressErrors.Assert(s_source != null, "Source was not set"); + DataStressErrors.Assert(s_factory != null, "Factory was not set"); + + Console.WriteLine("DataTestGroup.GlobalTestSetup(): Finished"); + } + + /// + /// Does test cleanup that is shared across all threads. This method will not be called until all + /// threads have finished executing [StressTest] methods. This method will be called only once. + /// If you override this method you must call base.GlobalTestSetup() at the beginning. + /// + [GlobalTestCleanup] + public virtual void GlobalTestCleanup() + { + Console.WriteLine("DataTestGroup.GlobalTestCleanup(): Starting..."); + + s_factory.CleanupSharedData(); + s_factory.DropDatabase(s_source); + s_source = null; + s_scenario = null; + s_factory.Dispose(); + s_factory = null; + + Console.WriteLine("DataTestGroup.GlobalTestCleanup(): Finished"); + } + + + protected bool OpenConnection(DataStressConnection conn) + { + try + { + conn.Open(); + return true; + } + catch (Exception e) + { + if (IsServerNotAccessibleException(e, conn.DbConnection.ConnectionString, conn.DbConnection.DataSource)) + { + // Ignore this exception. + // This exception will fire when using named pipes with MultiSubnetFailover option set to true. + // MultiSubnetFailover=true only works with TCP/IP protocol and will result in exception when using with named pipes. + return false; + } + + throw; + } + } + + + [GlobalExceptionHandler] + public virtual void GlobalExceptionHandler(Exception e) + { + if(e is System.Reflection.TargetInvocationException && Debugger.IsAttached) + { + StackTrace trace = new StackTrace(e); + Console.WriteLine(trace); + } + } + + /// + /// Returns whether or not the datareader should be closed + /// + protected virtual bool ShouldCloseDataReader() + { + // Ignore commandCancelled, instead randomly close it 9/10 of the time + return RandomInstance.Next(10) != 0; + } + + + #region CommandExecute and Consume methods + + /// + /// Utility function used by command tests + /// + protected virtual void CommandExecute(Random rnd, DbCommand com, bool query) + { + AsyncUtils.WaitAndUnwrapException(CommandExecuteAsync(rnd, com, query)); + } + + protected async virtual Task CommandExecuteAsync(Random rnd, DbCommand com, bool query) + { + CancellationTokenSource cts = null; + + // Cancel 1/10 commands + Task cancelTask = null; + bool cancelCommand = rnd.NextBool(0.1); + if (cancelCommand) + { + if (rnd.NextBool()) + { + // Use DbCommand.Cancel + cancelTask = Task.Run(() => CommandCancel(com)); + } + else + { + // Use CancellationTokenSource + if (cts == null) cts = new CancellationTokenSource(); + cancelTask = Task.Run(() => cts.Cancel()); + } + } + + // Get the CancellationToken + CancellationToken token = (cts != null) ? cts.Token : CancellationToken.None; + + DataStressReader reader = null; + try + { + if (query) + { + CommandBehavior commandBehavior = CommandBehavior.Default; + if (rnd.NextBool(0.5)) commandBehavior |= CommandBehavior.SequentialAccess; + if (rnd.NextBool(0.25)) commandBehavior |= CommandBehavior.KeyInfo; + if (rnd.NextBool(0.1)) commandBehavior |= CommandBehavior.SchemaOnly; + + // Get the reader + reader = new DataStressReader(await com.ExecuteReaderSyncOrAsync(commandBehavior, token, rnd)); + + // Consume the reader's data + await ConsumeReaderAsync(reader, commandBehavior.HasFlag(CommandBehavior.SequentialAccess), token, rnd); + } + else + { + await com.ExecuteNonQuerySyncOrAsync(token, rnd); + } + } + catch (Exception e) + { + if (cancelCommand && IsCommandCancelledException(e)) + { + // Catch command canceled exception + } + else + { + throw; + } + } + finally + { + if (cancelTask != null) AsyncUtils.WaitAndUnwrapException(cancelTask); + if (reader != null && ShouldCloseDataReader()) reader.Close(); + } + } + + /// + /// Utility function to consume a reader in a random fashion + /// + protected virtual async Task ConsumeReaderAsync(DataStressReader reader, bool sequentialAccess, CancellationToken token, Random rnd) + { + // Close 1/10 of readers while they are reading + Task closeTask = null; + if (AllowReaderCloseDuringReadAsync() && rnd.NextBool(0.1)) + { + // Begin closing now on another thread + closeTask = reader.CloseAsync(); + } + + try + { + do + { + while (await reader.ReadSyncOrAsync(token, rnd)) + { + // Optionally stop reading the current result set + if (rnd.NextBool(0.1)) break; + + // Read the current row + await ConsumeRowAsync(reader, sequentialAccess, token, rnd); + } + + // Executing NextResult only 50% of the time + if (rnd.NextBool()) + break; + } while (await reader.NextResultSyncOrAsync(token, rnd)); + } + catch (Exception e) + { + if (closeTask != null && IsReaderClosedException(e)) + { + // Catch reader closed exception + } + else + { + throw; + } + } + finally + { + if (closeTask != null) AsyncUtils.WaitAndUnwrapException(closeTask); + } + } + + /// + /// Utility function to consume a single row of a reader in a random fashion after Read/ReadAsync has been invoked. + /// + protected virtual async Task ConsumeRowAsync(DataStressReader reader, bool sequentialAccess, CancellationToken token, Random rnd) + { + for (int i = 0; i < reader.FieldCount; i++) + { + if (rnd.Next(10) == 0) break; // stop reading from this row + if (rnd.Next(2) == 0) continue; // skip this field + bool hasBeenRead = false; + + // If the field is not null, we can optionally use streaming API + if ((!await reader.IsDBNullSyncOrAsync(i, token, rnd)) && (rnd.NextBool())) + { + Type t = reader.GetFieldType(i); + if (t == typeof(byte[])) + { + await ConsumeBytesAsync(reader, i, token, rnd); + hasBeenRead = true; + } + else if (t == typeof(string)) + { + await ConsumeCharsAsync(reader, i, token, rnd); + hasBeenRead = true; + } + } + + // If the field has not yet been read, or if it is non-sequential then we can re-read it + if ((!hasBeenRead) || (!sequentialAccess)) + { + if (!await reader.IsDBNullSyncOrAsync(i, token, rnd)) + { + // Field value is not null, we can use new GetFieldValue methods + await reader.GetValueSyncOrAsync(i, token, rnd); + } + else + { + // Field value is null, we have to use old GetValue method + reader.GetValue(i); + } + } + + // Do IsDBNull check again with 50% probability + if (rnd.NextBool()) await reader.IsDBNullSyncOrAsync(i, token, rnd); + } + } + + protected virtual async Task ConsumeBytesAsync(DataStressReader reader, int i, CancellationToken token, Random rnd) + { + byte[] buffer = new byte[255]; + + if (rnd.NextBool()) + { + // We can optionally use GetBytes + reader.GetBytes(i, rnd.Next(20), buffer, rnd.Next(20), rnd.Next(200)); + } + else if (reader.GetName(i) != "timestamp_FLD") + { + // Timestamp appears to be binary, but cannot be read by Stream + DataStressStream stream = reader.GetStream(i); + await stream.ReadSyncOrAsync(buffer, rnd.Next(20), rnd.Next(200), token, rnd); + } + else + { + // It is timestamp column, so read it later with GetValueSyncOrAsync + await reader.GetValueSyncOrAsync(i, token, rnd); + } + } + + protected virtual async Task ConsumeCharsAsync(DataStressReader reader, int i, CancellationToken token, Random rnd) + { + char[] buffer = new char[255]; + + if (rnd.NextBool()) + { + // Read with GetChars + reader.GetChars(i, rnd.Next(20), buffer, rnd.Next(20), rnd.Next(200)); + } + else if (reader.GetProviderSpecificFieldType(i) == typeof(SqlXml)) + { + // SqlClient only: Xml is read by XmlReader + DataStressXmlReader xmlReader = reader.GetXmlReader(i); + xmlReader.Read(); + } + else + { + // Read with TextReader + DataStressTextReader textReader = reader.GetTextReader(i); + if (rnd.NextBool()) + { + textReader.Peek(); + } + await textReader.ReadSyncOrAsync(buffer, rnd.Next(20), rnd.Next(200), rnd); + if (rnd.NextBool()) + { + textReader.Peek(); + } + } + } + + /// + /// Returns true if the given exception is expected for the current provider when a command is cancelled by another thread. + /// + /// + protected virtual bool IsCommandCancelledException(Exception e) + { + return e is TaskCanceledException; + } + + /// + /// Returns true if the given exception is expected for the current provider when trying to read from a reader that has been closed + /// + /// + protected virtual bool IsReaderClosedException(Exception e) + { + return false; + } + + /// + /// Returns true if the given exception is expected for the current provider when trying to connect to unavailable/non-existent server + /// + /// + protected bool IsServerNotAccessibleException(Exception e, string connString, string dataSource) + { + return + e is ArgumentException && + connString.Contains("MultiSubnetFailover=True") && + dataSource.Contains("np:") && + e.Message.Contains("Connecting to a SQL Server instance using the MultiSubnetFailover connection option is only supported when using the TCP protocol."); + } + + /// + /// Returns true if the backend provider supports closing a datareader while asynchronously reading from it + /// + /// + protected virtual bool AllowReaderCloseDuringReadAsync() + { + return false; + } + + /// + /// Thread Callback function which cancels queries using DbCommand.Cancel() + /// + /// + protected void CommandCancel(object o) + { + try + { + DbCommand cmd = (DbCommand)o; + cmd.Cancel(); + } + catch (Exception ex) + { + Trace.WriteLine(ex.ToString(), this.ToString()); + } + } + + #endregion + + #region Command and Parameter Tests + + /// + /// Command Reader Test: Executes a simple SELECT statement without parameters + /// + [StressTest("TestCommandReader", Weight = 10)] + public void TestCommandReader() + { + Random rnd = RandomInstance; + + using (DataStressConnection conn = Factory.CreateConnection(rnd)) + { + if (!OpenConnection(conn)) return; + DataStressFactory.TableMetadata table = Factory.GetRandomTable(rnd); + DbCommand com = Factory.GetCommand(rnd, table, conn, true); + CommandExecute(rnd, com, true); + } + } + + /// + /// Command Select Test: Executes a single SELECT statement with parameters + /// + [StressTest("TestCommandSelect", Weight = 10)] + public void TestCommandSelect() + { + Random rnd = RandomInstance; + using (DataStressConnection conn = Factory.CreateConnection(rnd)) + { + if (!OpenConnection(conn)) return; + DataStressFactory.TableMetadata table = Factory.GetRandomTable(rnd); + DbCommand com = Factory.GetSelectCommand(rnd, table, conn); + CommandExecute(rnd, com, true); + } + } + + /// + /// Command Insert Test: Executes a single INSERT statement with parameters + /// + [StressTest("TestCommandInsert", Weight = 10)] + public void TestCommandInsert() + { + Random rnd = RandomInstance; + + using (DataStressConnection conn = Factory.CreateConnection(rnd)) + { + if (!OpenConnection(conn)) return; + DataStressFactory.TableMetadata table = Factory.GetRandomTable(rnd); + DbCommand com = Factory.GetInsertCommand(rnd, table, conn); + CommandExecute(rnd, com, false); + } + } + + /// + /// Command Update Test: Executes a single UPDATE statement with parameters + /// + [StressTest("TestCommandUpdate", Weight = 10)] + public void TestCommandUpdate() + { + Random rnd = RandomInstance; + + using (DataStressConnection conn = Factory.CreateConnection(rnd)) + { + if (!OpenConnection(conn)) return; + DataStressFactory.TableMetadata table = Factory.GetRandomTable(rnd); + DbCommand com = Factory.GetUpdateCommand(rnd, table, conn); + CommandExecute(rnd, com, false); + } + } + + /// + /// Command Update Test: Executes a single DELETE statement with parameters + /// + [StressTest("TestCommandDelete", Weight = 10)] + public void TestCommandDelete() + { + Random rnd = RandomInstance; + + using (DataStressConnection conn = Factory.CreateConnection(rnd)) + { + if (!OpenConnection(conn)) return; + DataStressFactory.TableMetadata table = Factory.GetRandomTable(rnd); + DbCommand com = Factory.GetDeleteCommand(rnd, table, conn); + CommandExecute(rnd, com, false); + } + } + + [StressTest("TestCommandTimeout", Weight = 10)] + public void TestCommandTimeout() + { + Random rnd = RandomInstance; + DataStressConnection conn = null; + try + { + // Use a transaction 50% of the time + if (rnd.NextBool()) + { + } + + // Create a select command + conn = Factory.CreateConnection(rnd); + if (!OpenConnection(conn)) return; + DataStressFactory.TableMetadata table = Factory.GetRandomTable(rnd); + DbCommand com = Factory.GetSelectCommand(rnd, table, conn); + + // Setup timeout. We want to see various possibilities of timeout happening before, after, or at the same time as when the result comes in. + int delay = rnd.Next(0, 10); // delay is from 0 to 9 seconds inclusive + int timeout = rnd.Next(1, 10); // timeout is from 1 to 9 seconds inclusive + com.CommandText += string.Format("; WAITFOR DELAY '00:00:0{0}'", delay); + com.CommandTimeout = timeout; + + // Execute command and catch timeout exception + try + { + CommandExecute(rnd, com, true); + } + catch (DbException e) + { + if (e is SqlException && ((SqlException)e).Number == 3989) + { + throw DataStressErrors.ProductError("Timing issue between OnTimeout and ReadAsyncCallback results in SqlClient's packet parsing going out of sync", e); + } + else if (!e.Message.ToLower().Contains("timeout")) + { + throw; + } + } + } + finally + { + if (conn != null) conn.Dispose(); + } + } + + [StressTest("TestCommandAndReaderAsync", Weight = 10)] + public void TestCommandAndReaderAsync() + { + // Since we're calling an "async" method, we need to do a Wait() here. + AsyncUtils.WaitAndUnwrapException(TestCommandAndReaderAsyncInternal()); + } + + /// + /// Utility method to test Async scenario using await keyword + /// + /// + protected virtual async Task TestCommandAndReaderAsyncInternal() + { + Random rnd = RandomInstance; + using (DataStressConnection conn = Factory.CreateConnection(rnd)) + { + if (!OpenConnection(conn)) return; + DataStressFactory.TableMetadata table = Factory.GetRandomTable(rnd); + DbCommand com; + + com = Factory.GetInsertCommand(rnd, table, conn); + await CommandExecuteAsync(rnd, com, false); + + com = Factory.GetDeleteCommand(rnd, table, conn); + await CommandExecuteAsync(rnd, com, false); + + com = Factory.GetSelectCommand(rnd, table, conn); + await com.ExecuteScalarAsync(); + + com = Factory.GetSelectCommand(rnd, table, conn); + await CommandExecuteAsync(rnd, com, true); + } + } + + /// + /// Utility function used by MARS tests + /// + private void TestCommandMARS(Random rnd, bool query) + { + if (Source.Type != DataSourceType.SqlServer) + return; // skip for non-SQL Server databases + + using (DataStressConnection conn = Factory.CreateConnection(rnd, DataStressFactory.ConnectionStringOptions.EnableMars)) + { + if (!OpenConnection(conn)) return; + DbCommand[] commands = new DbCommand[rnd.Next(5, 10)]; + List tasks = new List(); + // Create commands + for (int i = 0; i < commands.Length; i++) + { + DataStressFactory.TableMetadata table = Factory.GetRandomTable(rnd); + commands[i] = Factory.GetCommand(rnd, table, conn, query); + } + + try + { + // Execute commands + for (int i = 0; i < commands.Length; i++) + { + if (rnd.NextBool(0.7)) + tasks.Add(CommandExecuteAsync(rnd, commands[i], query)); + else + CommandExecute(rnd, commands[i], query); + } + } + finally + { + // All commands must be complete before closing the connection + AsyncUtils.WaitAll(tasks.ToArray()); + } + } + } + + /// + /// Command MARS Test: Tests MARS by executing multiple readers on same connection + /// + [StressTest("TestCommandMARSRead", Weight = 10)] + public void TestCommandMARSRead() + { + Random rnd = RandomInstance; + TestCommandMARS(rnd, true); + } + + /// + /// Command MARS Test: Tests MARS by getting multiple connection objects from same connection + /// + [StressTest("TestCommandMARSWrite", Weight = 10)] + public void TestCommandMARSWrite() + { + Random rnd = RandomInstance; + TestCommandMARS(rnd, false); + } + + #endregion + } +} diff --git a/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Framework/Extensions.cs b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Framework/Extensions.cs new file mode 100644 index 0000000000..2629bc20bb --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Framework/Extensions.cs @@ -0,0 +1,94 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Data; +using System.Data.Common; +using Microsoft.Data.SqlClient; +using System.Threading; +using System.Threading.Tasks; +using System.Xml; + +namespace Stress.Data +{ + public static class Extensions + { + /// the probability that true will be returned + public static bool NextBool(this Random rnd, double probability) + { + return rnd.NextDouble() < probability; + } + + /// + /// Generate a true or false with equal probability. + /// + public static bool NextBool(this Random rnd) + { + return rnd.NextBool(0.5); + } + + public static Task ExecuteNonQuerySyncOrAsync(this DbCommand command, CancellationToken token, Random rnd) + { + return AsyncUtils.SyncOrAsyncMethod( + command.ExecuteNonQuery, + () => command.ExecuteNonQueryAsync(token), + AsyncUtils.ChooseSyncAsyncMode(rnd) + ); + } + + public static Task ExecuteScalarSyncOrAsync(this DbCommand command, CancellationToken token, Random rnd) + { + return AsyncUtils.SyncOrAsyncMethod( + command.ExecuteScalar, + () => command.ExecuteScalarAsync(token), + AsyncUtils.ChooseSyncAsyncMode(rnd) + ); + } + + public static Task ExecuteReaderSyncOrAsync(this DbCommand command, CancellationToken token, Random rnd) + { + return AsyncUtils.SyncOrAsyncMethod( + command.ExecuteReader, + () => command.ExecuteReaderAsync(token), + AsyncUtils.ChooseSyncAsyncMode(rnd) + ); + } + + public static Task ExecuteReaderSyncOrAsync(this DbCommand command, CommandBehavior cb, CancellationToken token, Random rnd) + { + return AsyncUtils.SyncOrAsyncMethod( + () => command.ExecuteReader(cb), + () => command.ExecuteReaderAsync(cb, token), + AsyncUtils.ChooseSyncAsyncMode(rnd) + ); + } + + public static Task ExecuteReaderSyncOrAsync(this SqlCommand command, CancellationToken token, Random rnd) + { + return AsyncUtils.SyncOrAsyncMethod( + command.ExecuteReader, + () => command.ExecuteReaderAsync(token), + AsyncUtils.ChooseSyncAsyncMode(rnd) + ); + } + + public static Task ExecuteReaderSyncOrAsync(this SqlCommand command, CommandBehavior cb, CancellationToken token, Random rnd) + { + return AsyncUtils.SyncOrAsyncMethod( + () => command.ExecuteReader(cb), + () => command.ExecuteReaderAsync(cb, token), + AsyncUtils.ChooseSyncAsyncMode(rnd) + ); + } + + public static Task ExecuteXmlReaderSyncOrAsync(this SqlCommand command, CancellationToken token, Random rnd) + { + return AsyncUtils.SyncOrAsyncMethod( + command.ExecuteXmlReader, + () => command.ExecuteXmlReaderAsync(token), + AsyncUtils.ChooseSyncAsyncMode(rnd) + ); + } + } +} diff --git a/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Framework/SqlClient.Stress.Framework.csproj b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Framework/SqlClient.Stress.Framework.csproj new file mode 100644 index 0000000000..91fe0f81ee --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Framework/SqlClient.Stress.Framework.csproj @@ -0,0 +1,15 @@ + + + + Stress.Data + + + + + + + + + + + diff --git a/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Framework/StressConfigReader.cs b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Framework/StressConfigReader.cs new file mode 100644 index 0000000000..1b677f965a --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Framework/StressConfigReader.cs @@ -0,0 +1,195 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Text.Json; +using System.Xml; +using System.Xml.XPath; +using static Stress.Data.DataStressSettings; + +namespace Stress.Data +{ + /// + /// Reads the configuration from a configuration file and provides the configuration + /// + internal class StressConfigReader + { + private readonly string _configFilePath; + private readonly bool _configIsJson; + private const string dataStressSettings = "dataStressSettings"; + private const string sourcePath = "//dataStressSettings/sources/source"; + internal List Sources + { + get; private set; + } + + public StressConfigReader(string configFilePath) + { + _configFilePath = configFilePath; + + // If the config filename extension is 'json' or 'jsonc', we parse + // it as JSON. + if (configFilePath.EndsWith(".json", StringComparison.OrdinalIgnoreCase) || + configFilePath.EndsWith(".jsonc", StringComparison.OrdinalIgnoreCase)) + { + _configIsJson = true; + } + // Otherwise, parse it as XML. + else + { + _configIsJson = false; + + // The original code always prepended the Framework project + // directory onto whatever path was given, so we do the same if + // that isn't already present. + if (!_configFilePath.StartsWith("SqlClient.Stress.Framework/")) + { + _configFilePath = Path.Combine("SqlClient.Stress.Framework", _configFilePath); + } + } + } + + internal void Load() + { + if (_configIsJson) + { + LoadJson(); + } + else + { + LoadXml(); + } + } + + private struct JsonDataSource + { + public string Name { get; set; } + public string Type { get; set; } + public bool IsDefault { get; set; } + public string DataSource { get; set; } + public string EntraIdUser { get; set; } + public string EntraIdPassword { get; set; } + public string User { get; set; } + public string Password { get; set; } + public bool SupportsWindowsAuthentication { get; set; } + public bool IsLocal { get; set; } + public bool DisableMultiSubnetFailover { get; set; } + public bool DisableNamedPipes { get; set; } + public bool Encrypt { get; set; } + } + + private void LoadJson() + { + var sources = JsonSerializer.Deserialize>( + File.ReadAllText(_configFilePath), + new JsonSerializerOptions() + { + IncludeFields = true, + PropertyNameCaseInsensitive = true, + ReadCommentHandling = JsonCommentHandling.Skip + }); + + Sources = new(sources.Count); + + foreach (var source in sources) + { + Sources.Add(new DataSourceElement( + source.Name, + source.Type, + null, + source.DataSource, + source.EntraIdUser, + source.EntraIdPassword, + source.User, + source.Password, + ds_isDefault: source.IsDefault, + ds_isLocal: source.IsLocal, + disableMultiSubnetFailoverSetup: source.DisableMultiSubnetFailover, + disableNamedPipes: source.DisableNamedPipes, + encrypt: source.Encrypt)); + } + } + + private void LoadXml() + { + XmlReader reader = null; + try + { + Sources = new List(); + reader = CreateReader(); + + XPathDocument xpathDocument = new XPathDocument(reader); + + XPathNavigator navigator = xpathDocument.CreateNavigator(); + + XPathNodeIterator sourceIterator = navigator.Select(sourcePath); + + foreach (XPathNavigator sourceNavigator in sourceIterator) + { + string nsUri = sourceNavigator.NamespaceURI; + string sourceName = sourceNavigator.GetAttribute("name", nsUri); + string sourceType = sourceNavigator.GetAttribute("type", nsUri); + bool isDefault; + isDefault = bool.TryParse(sourceNavigator.GetAttribute("isDefault", nsUri), out isDefault) ? isDefault : false; + string dataSource = sourceNavigator.GetAttribute("dataSource", nsUri); + string user = sourceNavigator.GetAttribute("user", nsUri); + string password = sourceNavigator.GetAttribute("password", nsUri); + bool supportsWindowsAuthentication; + supportsWindowsAuthentication = bool.TryParse(sourceNavigator.GetAttribute("supportsWindowsAuthentication", nsUri), out supportsWindowsAuthentication) ? supportsWindowsAuthentication : false; + bool isLocal; + isLocal = bool.TryParse(sourceNavigator.GetAttribute("isLocal", nsUri), out isLocal) ? isLocal : false; + bool disableMultiSubnetFailover; + disableMultiSubnetFailover = bool.TryParse(sourceNavigator.GetAttribute("disableMultiSubnetFailover", nsUri), out disableMultiSubnetFailover) ? disableMultiSubnetFailover : false; + bool disableNamedPipes; + disableMultiSubnetFailover = bool.TryParse(sourceNavigator.GetAttribute("disableNamedPipes", nsUri), out disableNamedPipes) ? disableNamedPipes : false; + bool encrypt; + encrypt = bool.TryParse(sourceNavigator.GetAttribute("encrypt", nsUri), out encrypt) ? encrypt : false; + + DataSourceElement element = new( + sourceName, + sourceType, + null, + dataSource, + string.Empty, + string.Empty, + user, + password, + ds_isDefault: isDefault, + ds_isLocal: isLocal, + disableMultiSubnetFailoverSetup: disableMultiSubnetFailover, + disableNamedPipes: disableNamedPipes, + encrypt: encrypt); + Sources.Add(element); + } + } + catch (XmlException e) + { + throw new InvalidDataException($"Error reading configuration file '{_configFilePath}': {e.Message}", e); + } + catch (IOException e) + { + throw new InvalidDataException($"Error reading configuration file '{_configFilePath}': {e.Message}", e); + } + catch (System.Exception e) + { + throw new InvalidDataException($"Error reading configuration file '{_configFilePath}': {e.Message}", e); + } + finally + { + reader?.Dispose(); + } + } + + private XmlReader CreateReader() + { + FileStream configurationStream = new FileStream("SqlClient.Stress.Framework/" + _configFilePath, FileMode.Open); + XmlReaderSettings settings = new XmlReaderSettings(); + settings.DtdProcessing = DtdProcessing.Prohibit; + XmlReader reader = XmlReader.Create(configurationStream, settings); + return reader; + } + } +} diff --git a/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Framework/StressTests.config.jsonc b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Framework/StressTests.config.jsonc new file mode 100644 index 0000000000..b160d304a7 --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Framework/StressTests.config.jsonc @@ -0,0 +1,19 @@ +// Sample config file for the Stress Tests app. +[ + // Each array element is a "data source" object. + { + "name": "My Favourite SQL Server", + "type": "SqlServer", + "isDefault": true, + "dataSource": "127.0.0.1,1433", + "entraIdUser": "", + "entraIdPassword": "", + "user": "sa", + "password": "", + "supportsWindowsAuthentication": false, + "isLocal": false, + "disableMultiSubnetFailover": true, + "disableNamedPipes": true, + "encrypt": false + } +] diff --git a/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Framework/StressTests.config.xml b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Framework/StressTests.config.xml new file mode 100644 index 0000000000..a9cedadfa5 --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Framework/StressTests.config.xml @@ -0,0 +1,20 @@ + + + + + + + + + diff --git a/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Framework/TrackedRandom.cs b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Framework/TrackedRandom.cs new file mode 100644 index 0000000000..b42128fff5 --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Framework/TrackedRandom.cs @@ -0,0 +1,184 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; + +namespace Stress.Data +{ + /// + /// Random number generator that tracks information necessary to reproduce a sequence of random numbers. + /// + /// + /// There are three items maintained by instances of this class + /// that are used to assist in the reproduction of a sequence of generated numbers: + /// + /// 1. The seed used for initialization. + /// 2. The count of numbers generated. + /// 3. Markers to indicate relevant points in the sequence. + /// + /// For tests that use random numbers to control execution, + /// these tracked items can be used to help determine the specific code path that was executed. + /// Here's an example: + /// + /// A test starts to execute, and retrieves an instance of this class. + /// If an instance of this class has not been created beforehand, it is constructed and the *seed* is stored. + /// The test inserts a *marker* to track the *count* of numbers generated before the test starts its work. + /// As the test executes, it asks for a sequence of random numbers. At some point, the test causes a crash. + /// Using the resulting dump (or live debugging session if available), it is possible to examine an instance + /// of this class to recreate the sequence of numbers used by the test. + /// You can create an instance of a Random offline using the tracked *seed*, + /// and generate numbers up to the *marked* count to determine the starting point for the sequence of numbers used by the test. + /// The length of the sequence is indicated by the last *count* of number generated. + /// So for a failed test, you can use the numbers from Mark+1 to Count to retrace the code path taken by the test. + /// + /// Instances of this class keep track of a finite number of multiple marks, + /// so it is possible to track the beginning and end of a series of tests, + /// assuming they all mark at least the start of their execution. + /// + public class TrackedRandom : Random + { + private readonly int _seed; + + /// + /// Number of random numbers generated. + /// + private long _count; + + /// + /// Circular buffer to track the most recent marks that indicate the count at the time a given mark was created. + /// + private readonly long[] _marks = new long[16]; + + /// + /// Index of where to place next mark in buffer. + /// This index is incremented after each mark, and wraps around as necessary. + /// + private int _nextMark; + + private const int EmptyMark = -1; + + public TrackedRandom() + : this(Environment.TickCount) + { + } + + public TrackedRandom(int seed) + : base(seed) + { + _seed = seed; + + for (int i = 0; i < _marks.Length; i++) + { + _marks[i] = EmptyMark; + } + } + + public int Seed + { + get + { + return _seed; + } + } + + public long Count + { + get + { + return _count; + } + } + + public void Mark() + { + long mark = _count; + + // marking forward + _marks[_nextMark++] = mark; + + // wrap when necessary + if (_nextMark == _marks.Length) + { + _nextMark = 0; + } + } + + /// + /// Return an enumerable that can be used to iterate over the most recent marks, + /// starting from the most recent, and ending with the earliest mark still being tracked. + /// + public IEnumerable Marks + { + get + { + // Iterate backwards through the mark array, + // starting just before the index of the next mark, + // and ending at the next mark. + // Iteration stops earlier if an empty mark is found. + int index; + long mark; + + for (int i = 1; i <= _marks.Length; i++) + { + // Index of current element determined by: + // ((L+n) - i) % L + // where + // L is the length of the array, + // n is the index of where to insert the next mark, 0 <= n < L, + // i is the current iteration variable value, 0 < i <= L. + index = (_marks.Length + _nextMark - i) % _marks.Length; + mark = _marks[index]; + + if (mark == EmptyMark) + { + break; + } + + yield return mark; + } + } + } + + private void IncrementCount() + { + if (_count == long.MaxValue) + { + _count = -1; + } + + ++_count; + } + + public override int Next() + { + IncrementCount(); + return base.Next(); + } + + public override int Next(int minValue, int maxValue) + { + IncrementCount(); + return base.Next(minValue, maxValue); + } + + public override int Next(int maxValue) + { + IncrementCount(); + return base.Next(maxValue); + } + + public override void NextBytes(byte[] buffer) + { + IncrementCount(); + base.NextBytes(buffer); + } + + public override double NextDouble() + { + IncrementCount(); + return base.NextDouble(); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Runner/Constants.cs b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Runner/Constants.cs new file mode 100644 index 0000000000..10f0ecb41b --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Runner/Constants.cs @@ -0,0 +1,53 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Text; + +namespace DPStressHarness +{ + public static class Constants + { + public const string XML_ELEM_RESULTS = "PerfResults"; + public const string XML_ELEM_RUN = "Run"; + public const string XML_ELEM_RUN_METRIC = "RunMetric"; + public const string XML_ELEM_TEST = "Test"; + public const string XML_ELEM_TEST_METRIC = "TestMetric"; + public const string XML_ELEM_EXCEPTION = "Exception"; + + public const string XML_ATTR_RUN_LABEL = "label"; + public const string XML_ATTR_RUN_START_TIME = "startTime"; + public const string XML_ATTR_RUN_OFFICIAL = "official"; + public const string XML_ATTR_RUN_MILESTONE = "milestone"; + public const string XML_ATTR_RUN_BRANCH = "branch"; + public const string XML_ATTR_RUN_UPLOADED = "uploaded"; + public const string XML_ATTR_RUN_METRIC_NAME = "name"; + public const string XML_ATTR_TEST_NAME = "name"; + public const string XML_ATTR_TEST_METRIC_NAME = "name"; + public const string XML_ATTR_TEST_METRIC_UNITS = "units"; + public const string XML_ATTR_TEST_METRIC_ISHIGHERBETTER = "isHigherBetter"; + + public const string XML_ATTR_VALUE_TRUE = "true"; + public const string XML_ATTR_VALUE_FALSE = "false"; + + public const string RUN_METRIC_PROCESSOR_COUNT = "Processor Count"; + public const string RUN_DNS_HOST_NAME = "DNS Host Name"; + public const string RUN_IDENTITY_NAME = "Identity Name"; + public const string RUN_PROCESS_MACHINE_NAME = "Process Machine Name"; + + public const string TEST_METRIC_TEST_ASSEMBLY = "Test Assembly"; + public const string TEST_METRIC_TEST_IMPROVEMENT = "Improvement"; + public const string TEST_METRIC_TEST_OWNER = "Owner"; + public const string TEST_METRIC_TEST_CATEGORY = "Category"; + public const string TEST_METRIC_TEST_PRIORITY = "Priority"; + public const string TEST_METRIC_APPLICATION_NAME = "Application Name"; + public const string TEST_METRIC_TARGET_ASSEMBLY_NAME = "Target Assembly Name"; + public const string TEST_METRIC_ELAPSED_SECONDS = "Elapsed Seconds"; + public const string TEST_METRIC_RPS = "Requests Per Second"; + public const string TEST_METRIC_PEAK_WORKING_SET = "Peak Working Set"; + public const string TEST_METRIC_WORKING_SET = "Working Set"; + public const string TEST_METRIC_PRIVATE_BYTES = "Private Bytes"; + } +} diff --git a/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Runner/Ex API/MemApi.Windows.cs b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Runner/Ex API/MemApi.Windows.cs new file mode 100644 index 0000000000..053aea09a1 --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Runner/Ex API/MemApi.Windows.cs @@ -0,0 +1,18 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Runtime.InteropServices; + +namespace DPStressHarness +{ + static class MemApi + { + [DllImport("KERNEL32")] + public static extern IntPtr GetCurrentProcess(); + + [DllImport("KERNEL32")] + public static extern bool SetProcessWorkingSetSize(IntPtr hProcess, int dwMinimumWorkingSetSize, int dwMaximumWorkingSetSize); + } +} diff --git a/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Runner/ITestAttributeFilter.cs b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Runner/ITestAttributeFilter.cs new file mode 100644 index 0000000000..c3afa9251d --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Runner/ITestAttributeFilter.cs @@ -0,0 +1,11 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace DPStressHarness +{ + public interface ITestAttributeFilter + { + bool MatchFilter(string filterString); + } +} diff --git a/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Runner/LogManager.cs b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Runner/LogManager.cs new file mode 100644 index 0000000000..9b534a24b5 --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Runner/LogManager.cs @@ -0,0 +1,49 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Concurrent; +using System.IO; +using System.Linq; +using System.Text; + +namespace DPStressHarness +{ + public class LogManager: IDisposable + { + private static readonly LogManager s_instance = new LogManager(); + private readonly ConcurrentDictionary _logs = new ConcurrentDictionary(); + private DirectoryInfo _directoryInfo; + + private LogManager() + { + try + { + _directoryInfo = Directory.CreateDirectory("../../../logs"); + } + catch (Exception e) + { + Console.WriteLine($"The process failed: {e}"); + } + } + + public static LogManager Instance => s_instance; + + public void Dispose() + { + _logs.ToList().ForEach(l => l.Value.Close()); + } + + public TextWriter GetLog(string name) + { + if (!_logs.TryGetValue(name, out TextWriter log)) + { + Console.WriteLine($"{_directoryInfo.FullName}/{name}.log log file created!"); + log = new StreamWriter($"{_directoryInfo.FullName}/{name}.log", false, Encoding.UTF8) { AutoFlush = true } ; + _logs.TryAdd(name, log); + } + return log; + } + } +} diff --git a/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Runner/Monitor/FakeConsole.cs b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Runner/Monitor/FakeConsole.cs new file mode 100644 index 0000000000..f13949ae8e --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Runner/Monitor/FakeConsole.cs @@ -0,0 +1,34 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Text; + +namespace DPStressHarness +{ + public static class FakeConsole + { + public static void Write(string value) + { +#if DEBUG + Console.Write(value); +#endif + } + + public static void WriteLine(string value) + { +#if DEBUG + Console.WriteLine(value); +#endif + } + + public static void WriteLine(string format, params object[] arg) + { +#if DEBUG + Console.WriteLine(format, arg); +#endif + } + } +} diff --git a/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Runner/Monitor/Logger.cs b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Runner/Monitor/Logger.cs new file mode 100644 index 0000000000..9226c4b930 --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Runner/Monitor/Logger.cs @@ -0,0 +1,226 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.IO; +using System.Xml; +using System.Diagnostics; + +namespace DPStressHarness +{ + public class Logger + { + private const string _resultDocumentName = "perfout.xml"; + + private XmlDocument _doc; + private XmlElement _runElem; + private XmlElement _testElem; + + public Logger(string runLabel, bool isOfficial, string milestone, string branch) + { + _doc = GetTestResultDocument(); + + _runElem = GetRunElement(_doc, runLabel, DateTime.Now.ToString(), isOfficial, milestone, branch); + + Process currentProcess = Process.GetCurrentProcess(); + AddRunMetric(Constants.RUN_PROCESS_MACHINE_NAME, currentProcess.MachineName); + AddRunMetric(Constants.RUN_DNS_HOST_NAME, System.Net.Dns.GetHostName()); + AddRunMetric(Constants.RUN_IDENTITY_NAME, Environment.UserName); + AddRunMetric(Constants.RUN_METRIC_PROCESSOR_COUNT, Environment.ProcessorCount.ToString()); + } + + public void AddRunMetric(string metricName, string metricValue) + { + Debug.Assert(_runElem != null); + + if (metricValue.Equals(string.Empty)) + return; + + AddRunMetricElement(_runElem, metricName, metricValue); + } + + public void AddTest(string testName) + { + Debug.Assert(_runElem != null); + + _testElem = AddTestElement(_runElem, testName); + } + + public void AddTestMetric(string metricName, string metricValue, string metricUnits) + { + AddTestMetric(metricName, metricValue, metricUnits, null); + } + + public void AddTestMetric(string metricName, string metricValue, string metricUnits, bool? isHigherBetter) + { + Debug.Assert(_runElem != null); + Debug.Assert(_testElem != null); + + if (metricValue.Equals(string.Empty)) + return; + + AddTestMetricElement(_testElem, metricName, metricValue, metricUnits, isHigherBetter); + } + + public void AddTestException(string exceptionData) + { + Debug.Assert(_runElem != null); + Debug.Assert(_testElem != null); + + AddTestExceptionElement(_testElem, exceptionData); + } + + public void Save() + { + FileStream resultDocumentStream = new FileStream(_resultDocumentName, FileMode.Create); + _doc.Save(resultDocumentStream); + resultDocumentStream.Dispose(); + } + + private static XmlDocument GetTestResultDocument() + { + if (File.Exists(_resultDocumentName)) + { + XmlDocument doc = new XmlDocument(); + FileStream resultDocumentStream = new FileStream(_resultDocumentName, FileMode.Open, FileAccess.Read); + doc.Load(resultDocumentStream); + resultDocumentStream.Dispose(); + return doc; + } + else + { + XmlDocument doc = new XmlDocument(); + doc.LoadXml(""); + FileStream resultDocumentStream = new FileStream(_resultDocumentName, FileMode.CreateNew); + doc.Save(resultDocumentStream); + resultDocumentStream.Dispose(); + return doc; + } + } + + + private static XmlElement GetRunElement(XmlDocument doc, string label, string startTime, bool isOfficial, string milestone, string branch) + { + foreach (XmlNode node in doc.DocumentElement.ChildNodes) + { + if (node.NodeType == XmlNodeType.Element && + node.Name.Equals(Constants.XML_ELEM_RUN) && + ((XmlElement)node).GetAttribute(Constants.XML_ATTR_RUN_LABEL).Equals(label)) + { + return (XmlElement)node; + } + } + + XmlElement runElement = doc.CreateElement(Constants.XML_ELEM_RUN); + + XmlAttribute attrLabel = doc.CreateAttribute(Constants.XML_ATTR_RUN_LABEL); + attrLabel.Value = label; + runElement.Attributes.Append(attrLabel); + + XmlAttribute attrStartTime = doc.CreateAttribute(Constants.XML_ATTR_RUN_START_TIME); + attrStartTime.Value = startTime; + runElement.Attributes.Append(attrStartTime); + + XmlAttribute attrOfficial = doc.CreateAttribute(Constants.XML_ATTR_RUN_OFFICIAL); + attrOfficial.Value = isOfficial.ToString(); + runElement.Attributes.Append(attrOfficial); + + if (milestone != null) + { + XmlAttribute attrMilestone = doc.CreateAttribute(Constants.XML_ATTR_RUN_MILESTONE); + attrMilestone.Value = milestone; + runElement.Attributes.Append(attrMilestone); + } + + if (branch != null) + { + XmlAttribute attrBranch = doc.CreateAttribute(Constants.XML_ATTR_RUN_BRANCH); + attrBranch.Value = branch; + runElement.Attributes.Append(attrBranch); + } + + doc.DocumentElement.AppendChild(runElement); + + return runElement; + } + + + private static void AddRunMetricElement(XmlElement runElement, string name, string value) + { + // First check and make sure the metric hasn't already been added. + // If it has, it's from a previous test in the same run, so just return. + foreach (XmlNode node in runElement.ChildNodes) + { + if (node.NodeType == XmlNodeType.Element && node.Name.Equals(Constants.XML_ELEM_RUN_METRIC)) + { + if (node.Attributes[Constants.XML_ATTR_RUN_METRIC_NAME].Value.Equals(name)) + return; + } + } + + XmlElement runMetricElement = runElement.OwnerDocument.CreateElement(Constants.XML_ELEM_RUN_METRIC); + + XmlAttribute attrName = runElement.OwnerDocument.CreateAttribute(Constants.XML_ATTR_RUN_METRIC_NAME); + attrName.Value = name; + runMetricElement.Attributes.Append(attrName); + + XmlText nodeValue = runElement.OwnerDocument.CreateTextNode(value); + runMetricElement.AppendChild(nodeValue); + + runElement.AppendChild(runMetricElement); + } + + + private static XmlElement AddTestElement(XmlElement runElement, string name) + { + XmlElement testElement = runElement.OwnerDocument.CreateElement(Constants.XML_ELEM_TEST); + + XmlAttribute attrName = runElement.OwnerDocument.CreateAttribute(Constants.XML_ATTR_TEST_NAME); + attrName.Value = name; + testElement.Attributes.Append(attrName); + + runElement.AppendChild(testElement); + + return testElement; + } + + + private static void AddTestMetricElement(XmlElement testElement, string name, string value, string units, bool? isHigherBetter) + { + XmlElement testMetricElement = testElement.OwnerDocument.CreateElement(Constants.XML_ELEM_TEST_METRIC); + + XmlAttribute attrName = testElement.OwnerDocument.CreateAttribute(Constants.XML_ATTR_TEST_METRIC_NAME); + attrName.Value = name; + testMetricElement.Attributes.Append(attrName); + + if (units != null) + { + XmlAttribute attrUnits = testElement.OwnerDocument.CreateAttribute(Constants.XML_ATTR_TEST_METRIC_UNITS); + attrUnits.Value = units; + testMetricElement.Attributes.Append(attrUnits); + } + + if (isHigherBetter.HasValue) + { + XmlAttribute attrIsHigherBetter = testElement.OwnerDocument.CreateAttribute(Constants.XML_ATTR_TEST_METRIC_ISHIGHERBETTER); + attrIsHigherBetter.Value = isHigherBetter.ToString(); + testMetricElement.Attributes.Append(attrIsHigherBetter); + } + + XmlText nodeValue = testElement.OwnerDocument.CreateTextNode(value); + testMetricElement.AppendChild(nodeValue); + + testElement.AppendChild(testMetricElement); + } + + private static void AddTestExceptionElement(XmlElement testElement, string exceptionData) + { + XmlElement testFailureElement = testElement.OwnerDocument.CreateElement(Constants.XML_ELEM_EXCEPTION); + XmlText txtNode = testFailureElement.OwnerDocument.CreateTextNode(exceptionData); + testFailureElement.AppendChild(txtNode); + + testElement.AppendChild(testFailureElement); + } + } +} diff --git a/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Runner/Monitor/MonitorLoadUtils.cs b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Runner/Monitor/MonitorLoadUtils.cs new file mode 100644 index 0000000000..80661566fa --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Runner/Monitor/MonitorLoadUtils.cs @@ -0,0 +1,48 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using Monitoring; +using System.Reflection; + +namespace DPStressHarness +{ + public static class MonitorLoader + { + public static IMonitorLoader LoadMonitorLoaderAssembly() + { + IMonitorLoader monitorloader = null; + const string classname = "Monitoring.MonitorLoader"; + const string interfacename = "IMonitorLoader"; + Assembly mainAssembly = typeof(Monitoring.IMonitorLoader).GetTypeInfo().Assembly; + + Type t = mainAssembly.GetType(classname); + //make sure the type is derived from IMonitorLoader + Type[] interfaces = t.GetInterfaces(); + bool derivedFromIMonitorLoader = false; + if (interfaces != null) + { + foreach (Type intrface in interfaces) + { + if (intrface.Name == interfacename) + { + derivedFromIMonitorLoader = true; + } + } + } + if (derivedFromIMonitorLoader) + + { + monitorloader = (IMonitorLoader)Activator.CreateInstance(t); + + monitorloader.AssemblyPath = mainAssembly.FullName; + } + else + { + throw new Exception("The specified assembly does not implement " + interfacename + "!!"); + } + return monitorloader; + } + } +} diff --git a/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Runner/Monitor/RecordedExceptions.cs b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Runner/Monitor/RecordedExceptions.cs new file mode 100644 index 0000000000..72a10f4d30 --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Runner/Monitor/RecordedExceptions.cs @@ -0,0 +1,110 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Text; +using System.Collections.Concurrent; +using System.Diagnostics; +using System.Threading; + +namespace DPStressHarness +{ + public class RecordedExceptions + { + // Reference wrapper around an integer which is used in order to make updating a little easier & more efficient + public class ExceptionCount + { + public int Count = 0; + } + + private ConcurrentDictionary> _exceptions = new ConcurrentDictionary>(); + + /// + /// Records an exception and returns true if the threshold is exceeded for that exception + /// + public bool Record(string testName, Exception ex) + { + // Converting from exception to string can be expensive so only do it once and cache the string + string exceptionString = ex.ToString(); + TraceException(testName, exceptionString); + + // Get the exceptions for the current test case + ConcurrentDictionary exceptionsForTest = _exceptions.GetOrAdd(testName, _ => new ConcurrentDictionary()); + + // Get the count for the current exception + ExceptionCount exCount = exceptionsForTest.GetOrAdd(exceptionString, _ => new ExceptionCount()); + + // Increment the count + Interlocked.Increment(ref exCount.Count); + + // If the count is over the threshold, return true + return TestMetrics.ExceptionThreshold.HasValue && (exCount.Count > TestMetrics.ExceptionThreshold); + } + + private void TraceException(string testName, string exceptionString) + { + StringBuilder status = new StringBuilder(); + status.AppendLine("========================================================================"); + status.AppendLine("Exception Report"); + status.AppendLine("========================================================================"); + + status.AppendLine(string.Format("Test: {0}", testName)); + status.AppendLine(exceptionString); + + status.AppendLine("========================================================================"); + status.AppendLine("End of Exception Report"); + status.AppendLine("========================================================================"); + Trace.WriteLine(status.ToString()); + } + + public void TraceAllExceptions() + { + StringBuilder status = new StringBuilder(); + status.AppendLine("========================================================================"); + status.AppendLine("All Exceptions Report"); + status.AppendLine(string.Format("Total test(s) with exception: {0}", _exceptions.Count)); + status.AppendLine("========================================================================"); + + foreach (string testName in _exceptions.Keys) + { + ConcurrentDictionary exceptionsForTest = _exceptions[testName]; + + int count = 1; + status.AppendLine(string.Format("Test: {0}", testName)); + foreach (var exceptionString in exceptionsForTest.Keys) + { + status.AppendLine(string.Format(" No: {0} of {1} [{2}]", count ++, exceptionsForTest.Count, testName)); + status.AppendLine(string.Format(" Count: {0}", exceptionsForTest[exceptionString].Count)); + status.AppendLine(string.Format(" Exception: {0}", exceptionString)); + status.AppendLine(); + } + + status.AppendLine(); + status.AppendLine(); + } + + status.AppendLine("========================================================================"); + status.AppendLine("End of All Exceptions Report"); + status.AppendLine("========================================================================"); + Trace.WriteLine(status.ToString()); + } + + public int GetExceptionsCount() + { + int count = 0; + + foreach (string testName in _exceptions.Keys) + { + ConcurrentDictionary exceptionsForTest = _exceptions[testName]; + + foreach (var exceptionString in exceptionsForTest.Keys) + { + count += exceptionsForTest[exceptionString].Count; + } + } + + return count; + } + } +} diff --git a/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Runner/PerfCounters.cs b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Runner/PerfCounters.cs new file mode 100644 index 0000000000..84a3ccfba0 --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Runner/PerfCounters.cs @@ -0,0 +1,29 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace DPStressHarness +{ + public class PerfCounters + { + private long _requestsCounter; + //private long rpsCounter; + + private long _exceptionsCounter; + //private long epsCounter; + + public PerfCounters() + { + } + + public void IncrementRequestsCounter() + { + _requestsCounter++; + } + + public void IncrementExceptionsCounter() + { + _exceptionsCounter++; + } + } +} diff --git a/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Runner/Program.cs b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Runner/Program.cs new file mode 100644 index 0000000000..6b49692aa7 --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Runner/Program.cs @@ -0,0 +1,306 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Reflection; + +using Microsoft.Data.SqlClient; + +namespace DPStressHarness//Microsoft.Data.SqlClient.Stress +{ + class Program + { + private static bool s_debugMode = false; + static int Main(string[] args) + { + Init(args); + return Run(); + } + + public enum RunMode + { + RunAll, + RunVerify, + Help, + ExitWithError + }; + + private static RunMode s_mode = RunMode.RunAll; + private static IEnumerable s_tests; + private static StressEngine s_eng; + private static string s_error; + private static bool s_console = false; + + public static void Init(string[] args) + { + for (int i = 0; i < args.Length; i++) + { + switch (args[i]) + { + case "-a": + string assemblyName = args[++i]; + TestFinder.AssemblyName = new AssemblyName(assemblyName); + break; + + case "-all": + s_mode = RunMode.RunAll; + break; + + case "-override": + TestMetrics.Overrides.Add(args[++i], args[++i]); + break; + + case "-variation": + TestMetrics.Variations.Add(args[++i]); + break; + + case "-test": + TestMetrics.SelectedTests.AddRange(args[++i].Split(';')); + break; + + case "-duration": + TestMetrics.StressDuration = int.Parse(args[++i]); + break; + + case "-threads": + TestMetrics.StressThreads = int.Parse(args[++i]); + break; + + case "-verify": + s_mode = RunMode.RunVerify; + break; + + case "-console": + s_console = true; + break; + + case "-debug": + s_debugMode = true; + if (System.Diagnostics.Debugger.IsAttached) + { + System.Diagnostics.Debugger.Break(); + } + else + { + Console.WriteLine("Current PID: {0}, attach the debugger and press Enter to continue the execution...", System.Diagnostics.Process.GetCurrentProcess().Id); + Console.ReadLine(); + } + break; + + case "-exceptionThreshold": + TestMetrics.ExceptionThreshold = int.Parse(args[++i]); + break; + + case "-monitorenabled": + TestMetrics.MonitorEnabled = bool.Parse(args[++i]); + break; + + case "-randomSeed": + TestMetrics.RandomSeed = int.Parse(args[++i]); + break; + + case "-filter": + TestMetrics.Filter = args[++i]; + break; + + case "-printMethodName": + TestMetrics.PrintMethodName = true; + break; + + case "-deadlockdetection": + if (bool.Parse(args[++i])) + { + DeadlockDetection.Enable(); + } + break; + + default: + s_mode = RunMode.Help; + break; + } + } + + PrintConfigSummary(); + + if (TestFinder.AssemblyName != null) + { + Console.WriteLine("Assembly Found for the Assembly Name " + TestFinder.AssemblyName); + + // get and load all the tests + s_tests = TestFinder.GetTests(Assembly.Load(TestFinder.AssemblyName)); + + // instantiate the stress engine + s_eng = new StressEngine(TestMetrics.StressThreads, TestMetrics.StressDuration, s_tests, TestMetrics.RandomSeed); + } + else + { + Program.s_error = string.Format("Assembly {0} cannot be found.", TestFinder.AssemblyName); + s_mode = RunMode.ExitWithError; + } + } + + public static int Run() + { + if (TestFinder.AssemblyName == null) + { + s_mode = RunMode.Help; + } + switch (s_mode) + { + case RunMode.RunAll: + return RunStress(); + + case RunMode.RunVerify: + return RunVerify(); + + case RunMode.ExitWithError: + return ExitWithError(); + + case RunMode.Help: + default: + return PrintHelp(); + } + } + + private static int PrintHelp() + { + Console.WriteLine("stresstest.exe [-a ] "); + Console.WriteLine(); + Console.WriteLine(" -a should specify path to the assembly containing the tests."); + Console.WriteLine(); + Console.WriteLine("Supported options are:"); + Console.WriteLine(); + Console.WriteLine(" -all Run all tests - best for debugging, not perf measurements."); + Console.WriteLine(); + Console.WriteLine(" -verify Run in functional verification mode."); + Console.WriteLine(); + Console.WriteLine(" -duration Duration of the test in seconds."); + Console.WriteLine(); + Console.WriteLine(" -threads Number of threads to use."); + Console.WriteLine(); + Console.WriteLine(" -override Override the value of a test property."); + Console.WriteLine(); + Console.WriteLine(" -test Run specific test(s)."); + Console.WriteLine(); + Console.WriteLine(" -console Emit all output to the console."); + Console.WriteLine(); + Console.WriteLine(" -debug Print process ID in the beginning and wait for Enter (to give your time to attach the debugger)."); + Console.WriteLine(); + Console.WriteLine(" -exceptionThreshold An optional limit on exceptions which will be caught. When reached, test will halt."); + Console.WriteLine(); + Console.WriteLine(" -monitorenabled True or False to enable monitoring. Default is false"); + Console.WriteLine(); + Console.WriteLine(" -randomSeed Enables setting of the random number generator used internally. This serves both the purpose"); + Console.WriteLine(" of helping to improve reproducibility and making it deterministic from Chess's perspective"); + Console.WriteLine(" for a given schedule. Default is " + TestMetrics.RandomSeed + "."); + Console.WriteLine(); + Console.WriteLine(" -filter Run tests whose stress test attributes match the given filter. Filter is not applied if attribute"); + Console.WriteLine(" does not implement ITestAttributeFilter. Example: -filter TestType=Query,Update;IsServerTest=True "); + Console.WriteLine(); + Console.WriteLine(" -printMethodName Print tests' title in console window"); + Console.WriteLine(); + Console.WriteLine(" -deadlockdetection True or False to enable deadlock detection. Default is false"); + Console.WriteLine(); + + return 1; + } + + private static void PrintConfigSummary() + { + string border = new('#', 80); + + Console.WriteLine(border); + Console.WriteLine($"MDS Version: {GetMdsVersion()}"); + Console.WriteLine($"Test Assembly Name: {TestFinder.AssemblyName}"); + Console.WriteLine($"Run mode: {Enum.GetName(typeof(RunMode), s_mode)}"); + foreach (var item in TestMetrics.Overrides) + { + Console.WriteLine($"Override: {item.Key} = {item.Value}"); + } + foreach (var item in TestMetrics.SelectedTests) + { + Console.WriteLine($"Test: {item}"); + } + Console.WriteLine($"Duration: {TestMetrics.StressDuration} second(s)"); + Console.WriteLine($"Threads No.: {TestMetrics.StressThreads}"); + Console.WriteLine($"Emit to console: {s_console}"); + Console.WriteLine($"Debug mode: {s_debugMode}"); + Console.WriteLine($"Exception threshold: {TestMetrics.ExceptionThreshold}"); + Console.WriteLine($"Random seed: {TestMetrics.RandomSeed}"); + Console.WriteLine($"Filter: {TestMetrics.Filter}"); + Console.WriteLine($"Deadlock detection: {DeadlockDetection.IsEnabled}"); + Console.WriteLine(border); + } + + private static int ExitWithError() + { + Environment.FailFast("Exit with error(s)."); + return 1; + } + + private static int RunVerify() + { + throw new NotImplementedException(); + } + + private static int RunStress() + { + if (!s_console) + { + try + { + TextWriter logOut = LogManager.Instance.GetLog("MDSStressTest-" + Environment.Version + + "-[" + Environment.OSVersion + "]-" + + DateTime.Now.ToString("MMMM dd yyyy @HHmmssFFF")); + Console.SetOut(logOut); + PrintConfigSummary(); + } + catch (Exception e) + { + Console.WriteLine($"Cannot open log file for writing!"); + Console.WriteLine(e); + } + } + return s_eng.Run(); + } + + private static string GetMdsVersion() + { + // MDS captures its NuGet package version at build-time, so pull + // it out and return it. + // + // See: tools/targets/GenerateThisAssemblyCs.targets + // + var assembly = typeof(SqlConnection).Assembly; + var type = assembly.GetType("System.ThisAssembly"); + if (type is null) + { + return ""; + } + + // Look for the NuGetPackageVersion field, which is available in + // newer MDS packages. + var field = type.GetField( + "NuGetPackageVersion", + BindingFlags.NonPublic | BindingFlags.Static); + + // If not present, use the older assembly file version field. + if (field is null) + { + field = type.GetField( + "InformationalVersion", + BindingFlags.NonPublic | BindingFlags.Static); + } + + if (field is null) + { + return ""; + } + + return (string)field.GetValue(null) ?? ""; + } + } +} diff --git a/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Runner/SqlClient.Stress.Runner.csproj b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Runner/SqlClient.Stress.Runner.csproj new file mode 100644 index 0000000000..9689097795 --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Runner/SqlClient.Stress.Runner.csproj @@ -0,0 +1,18 @@ + + + + Exe + stresstest + + + + + + + + + + + + + diff --git a/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Runner/StressEngine.cs b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Runner/StressEngine.cs new file mode 100644 index 0000000000..349b5c0539 --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Runner/StressEngine.cs @@ -0,0 +1,208 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Threading; +using System.Diagnostics; +using Monitoring; + +namespace DPStressHarness +{ + public class StressEngine + { + private Random _rnd; + private int _threads; + private int _duration; + private int _threadsRunning; + private bool _continue; + private List _allTests; + private RecordedExceptions _exceptions = new RecordedExceptions(); + private PerfCounters _perfcounters = null; + private static long s_globalRequestsCounter = 0; + + public RecordedExceptions Exceptions => _exceptions; + + public StressEngine(int threads, int duration, IEnumerable allTests, int seed) + { + if (seed != 0) + { + _rnd = new Random(seed); + } + else + { + Random rndBootstrap = new Random(); + + seed = rndBootstrap.Next(); + + _rnd = new Random(seed); + } + + Console.WriteLine("Seeding stress engine random number generator with {0}\n", seed); + + + _threads = threads; + _duration = duration; + _allTests = new List(); + + List tmpWeightedLookup = new List(); + + foreach (TestBase t in allTests) + { + if (t is StressTest) + { + _allTests.Add(t as StressTest); + } + } + + try + { + _perfcounters = new PerfCounters(); + } + catch (Exception e) + { + Console.WriteLine("Warning: An error occurred initializing performance counters. Performance counters can only be initialized when running with Administrator privileges. Error Message: " + e.Message); + } + } + + public int Run() + { + TraceListener listener = new TextWriterTraceListener(Console.Out); + Trace.Listeners.Add(listener); + Trace.UseGlobalLock = true; + + _threadsRunning = 0; + _continue = true; + + if (_allTests.Count == 0) + { + throw new ArgumentException("The specified assembly doesn't contain any tests to run. Test methods must be decorated with a Test, StressTest, MultiThreadedTest, or ThreadPoolTest attribute."); + } + + // Run any global setup + StressTest firstStressTest = _allTests.Find(t => t is StressTest); + if (null != firstStressTest) + { + firstStressTest.RunGlobalSetup(); + } + + //Monitoring Start + IMonitorLoader _monitorloader = null; + if (TestMetrics.MonitorEnabled) + { + _monitorloader = MonitorLoader.LoadMonitorLoaderAssembly(); + if (_monitorloader != null) + { + _monitorloader.Enabled = TestMetrics.MonitorEnabled; + _monitorloader.HostMachine = TestMetrics.MonitorMachineName; + _monitorloader.TestName = firstStressTest.Title; + _monitorloader.Action(MonitorLoaderUtils.MonitorAction.Start); + } + } + + for (int i = 0; i < _threads; i++) + { + Interlocked.Increment(ref _threadsRunning); + Thread t = new Thread(new ThreadStart(this.RunStressThread)); + t.Start(); + } + + while (_threadsRunning > 0) + { + Thread.Sleep(1000); + } + + //Monitoring Stop + if (TestMetrics.MonitorEnabled) + { + if (_monitorloader != null) + _monitorloader.Action(MonitorLoaderUtils.MonitorAction.Stop); + } + + // Run any global cleanup + if (null != firstStressTest) + { + firstStressTest.RunGlobalCleanup(); + } + + // Write out all exceptions + _exceptions.TraceAllExceptions(); + return _exceptions.GetExceptionsCount(); + } + + public void RunStressThread() + { + try + { + StressTest[] tests = new StressTest[_allTests.Count]; + List tmpWeightedLookup = new List(); + + for (int i = 0; i < tests.Length; i++) + { + tests[i] = _allTests[i].Clone(); + tests[i].RunSetup(); + + for (int j = 0; j < tests[i].Weight; j++) + { + tmpWeightedLookup.Add(i); + } + } + + int[] weightedLookup = tmpWeightedLookup.ToArray(); + + Stopwatch timer = new Stopwatch(); + long testDuration = _duration * Stopwatch.Frequency; + + timer.Reset(); + timer.Start(); + + while (_continue && timer.ElapsedTicks < testDuration) + { + int n = _rnd.Next(0, weightedLookup.Length); + StressTest test = tests[weightedLookup[n]]; + + if (TestMetrics.PrintMethodName) + { + FakeConsole.WriteLine("{0}: {1}", ++s_globalRequestsCounter, test.Title); + } + + try + { + DeadlockDetection.AddTestThread(); + test.Run(); + _perfcounters?.IncrementRequestsCounter(); + } + catch (Exception e) + { + _perfcounters?.IncrementExceptionsCounter(); + + test.HandleException(e); + + bool thresholdExceeded = _exceptions.Record(test.Title, e); + if (thresholdExceeded) + { + FakeConsole.WriteLine("Exception Threshold of {0} has been exceeded on {1} - Halting!\n", + TestMetrics.ExceptionThreshold, test.Title); + break; + } + } + finally + { + DeadlockDetection.RemoveThread(); + } + } + + foreach (StressTest t in tests) + { + t.RunCleanup(); + } + } + finally + { + _continue = false; + Interlocked.Decrement(ref _threadsRunning); + } + } + } +} diff --git a/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Runner/TestFinder.cs b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Runner/TestFinder.cs new file mode 100644 index 0000000000..3f18c6df43 --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Runner/TestFinder.cs @@ -0,0 +1,166 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Reflection; + +namespace DPStressHarness +{ + internal class TestFinder + { + private static AssemblyName s_assemblyName; + + public static AssemblyName AssemblyName + { + get { return s_assemblyName; } + set { s_assemblyName = value; } + } + + public static IEnumerable GetTests(Assembly assembly) + { + List tests = new List(); + + + Type[] typesInModule = null; + try + { + typesInModule = assembly.GetTypes(); + } + catch (ReflectionTypeLoadException ex) + { + Console.WriteLine("ReflectionTypeLoadException Errors"); + foreach (Exception loadEx in ex.LoaderExceptions) + { + Console.WriteLine("\t" + loadEx.Message); + } + } + catch (Exception ex) + { + Console.WriteLine("Error." + ex.Message); + } + + foreach (Type t in typesInModule) + { + MethodInfo[] methods = t.GetMethods(BindingFlags.Instance | BindingFlags.Public); + List setupMethods = new List(); + List cleanupMethods = new List(); + + MethodInfo globalSetupMethod = null; + MethodInfo globalCleanupMethod = null; + MethodInfo globalExceptionHandlerMethod = null; + + foreach (MethodInfo m in methods) + { + GlobalTestSetupAttribute[] globalSetupAttributes = (GlobalTestSetupAttribute[])m.GetCustomAttributes(typeof(GlobalTestSetupAttribute), true); + if (globalSetupAttributes.Length > 0) + { + if (null == globalSetupMethod) + { + globalSetupMethod = m; + } + else + { + throw new NotSupportedException("Only one GlobalTestSetup method may be specified per type."); + } + } + + GlobalTestCleanupAttribute[] globalCleanupAttributes = (GlobalTestCleanupAttribute[])m.GetCustomAttributes(typeof(GlobalTestCleanupAttribute), true); + if (globalCleanupAttributes.Length > 0) + { + if (null == globalCleanupMethod) + { + globalCleanupMethod = m; + } + else + { + throw new NotSupportedException("Only one GlobalTestCleanup method may be specified per type."); + } + } + + GlobalExceptionHandlerAttribute[] globalExceptionHandlerAttributes = (GlobalExceptionHandlerAttribute[])m.GetCustomAttributes(typeof(GlobalExceptionHandlerAttribute), true); + if (globalExceptionHandlerAttributes.Length > 0) + { + if (null == globalExceptionHandlerMethod) + { + globalExceptionHandlerMethod = m; + } + else + { + throw new NotSupportedException("Only one GlobalExceptionHandler method may be specified."); + } + } + + TestSetupAttribute[] testSetupAttrs = (TestSetupAttribute[])m.GetCustomAttributes(typeof(TestSetupAttribute), true); + if (testSetupAttrs.Length > 0) + { + setupMethods.Add(m); ; + } + + TestCleanupAttribute[] testCleanupAttrs = (TestCleanupAttribute[])m.GetCustomAttributes(typeof(TestCleanupAttribute), true); + if (testCleanupAttrs.Length > 0) + { + cleanupMethods.Add(m); ; + } + } + + foreach (MethodInfo m in methods) + { + // add single-threaded tests to the list + TestAttribute[] testAttrs = (TestAttribute[])m.GetCustomAttributes(typeof(TestAttribute), true); + foreach (TestAttribute attr in testAttrs) + { + tests.Add(new Test(attr, m, t, setupMethods, cleanupMethods)); + } + + // add any declared stress tests. + StressTestAttribute[] stressTestAttrs = (StressTestAttribute[])m.GetCustomAttributes(typeof(StressTestAttribute), true); + foreach (StressTestAttribute attr in stressTestAttrs) + { + if (TestMetrics.IncludeTest(attr) && MatchFilter(attr)) + tests.Add(new StressTest(attr, m, globalSetupMethod, globalCleanupMethod, t, setupMethods, cleanupMethods, globalExceptionHandlerMethod)); + } + + // add multi-threaded (non thread pool) tests to the list + MultiThreadedTestAttribute[] multiThreadedTestAttrs = (MultiThreadedTestAttribute[])m.GetCustomAttributes(typeof(MultiThreadedTestAttribute), true); + foreach (MultiThreadedTestAttribute attr in multiThreadedTestAttrs) + { + if (TestMetrics.IncludeTest(attr)) + tests.Add(new MultiThreadedTest(attr, m, t, setupMethods, cleanupMethods)); + } + + // add multi-threaded (with thread pool) tests to the list + ThreadPoolTestAttribute[] threadPoolTestAttrs = (ThreadPoolTestAttribute[])m.GetCustomAttributes(typeof(ThreadPoolTestAttribute), true); + foreach (ThreadPoolTestAttribute attr in threadPoolTestAttrs) + { + if (TestMetrics.IncludeTest(attr)) + tests.Add(new ThreadPoolTest(attr, m, t, setupMethods, cleanupMethods)); + } + } + } + + return tests; + } + + private static bool MatchFilter(StressTestAttribute attr) + { + // This change should not have impacts on any existing tests. + // 1. If filter is not provided in command line, we do not apply filter and select all the tests. + // 2. If current test attribute (such as StressTestAttribute) does not implement ITestAttriuteFilter, it is not affected and still selected. + + if (string.IsNullOrEmpty(TestMetrics.Filter)) + { + return true; + } + + var filter = attr as ITestAttributeFilter; + if (filter == null) + { + return true; + } + + return filter.MatchFilter(TestMetrics.Filter); + } + } +} diff --git a/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Runner/Tests/MultithreadedTest.cs b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Runner/Tests/MultithreadedTest.cs new file mode 100644 index 0000000000..01ea961426 --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Runner/Tests/MultithreadedTest.cs @@ -0,0 +1,170 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Reflection; +using System.Diagnostics; +using System.Threading; + +namespace DPStressHarness +{ + internal class MultiThreadedTest : TestBase + { + private MultiThreadedTestAttribute _attr; + public static bool _continue; + public static int _threadsRunning; + public static int _rps; + public static Exception _firstException = null; + + private struct TestInfo + { + public object _instance; + public TestMethodDelegate _delegateTest; + } + + public MultiThreadedTest(MultiThreadedTestAttribute attr, + MethodInfo testMethodInfo, + Type type, + List setupMethods, + List cleanupMethods) + : base(attr, testMethodInfo, type, setupMethods, cleanupMethods) + + { + _attr = attr; + } + + public override void Run() + { + try + { + Stopwatch timer = new Stopwatch(); + long warmupDuration = (long)_attr.WarmupDuration * Stopwatch.Frequency; + long testDuration = (long)_attr.TestDuration * Stopwatch.Frequency; + int threads = _attr.Threads; + + TestInfo[] info = new TestInfo[threads]; + ConstructorInfo targetConstructor = _type.GetConstructor(Type.EmptyTypes); + + for (int i = 0; i < threads; i++) + { + info[i] = new TestInfo(); + info[i]._instance = targetConstructor.Invoke(null); + info[i]._delegateTest = CreateTestMethodDelegate(); + + SetVariations(info[i]._instance); + ExecuteSetupPhase(info[i]._instance); + } + + _firstException = null; + _continue = true; + _rps = 0; + + for (int i = 0; i < threads; i++) + { + Interlocked.Increment(ref _threadsRunning); + Thread t = new Thread(new ParameterizedThreadStart(MultiThreadedTest.RunThread)); + t.Start(info[i]); + } + + timer.Reset(); + timer.Start(); + + while (timer.ElapsedTicks < warmupDuration) + { + Thread.Sleep(1000); + } + + int warmupRequests = Interlocked.Exchange(ref _rps, 0); + timer.Reset(); + timer.Start(); + TestMetrics.StartCollection(); + + while (timer.ElapsedTicks < testDuration) + { + Thread.Sleep(1000); + } + + int requests = Interlocked.Exchange(ref _rps, 0); + double elapsedSeconds = timer.ElapsedTicks / Stopwatch.Frequency; + TestMetrics.StopCollection(); + _continue = false; + + while (_threadsRunning > 0) + { + Thread.Sleep(1000); + } + + for (int i = 0; i < threads; i++) + { + ExecuteCleanupPhase(info[i]._instance); + } + + double rps = (double)requests / elapsedSeconds; + + if (_firstException == null) + { + LogTest(rps); + } + else + { + LogTestFailure(_firstException.ToString()); + } + } + catch (TargetInvocationException e) + { + LogTestFailure(e.InnerException.ToString()); + } + catch (Exception e) + { + LogTestFailure(e.ToString()); + } + } + + + public static void RunThread(object state) + { + try + { + while (_continue) + { + TestInfo info = (TestInfo)state; + info._delegateTest(info._instance); + Interlocked.Increment(ref _rps); + } + } + catch (Exception e) + { + if (_firstException == null) + { + _firstException = e; + } + _continue = false; + } + finally + { + Interlocked.Decrement(ref _threadsRunning); + } + } + + protected void LogTest(double rps) + { + Logger logger = new Logger(TestMetrics.RunLabel, TestMetrics.IsOfficial, TestMetrics.Milestone, TestMetrics.Branch); + logger.AddTest(this.Title); + + LogStandardMetrics(logger); + + logger.AddTestMetric(Constants.TEST_METRIC_RPS, string.Format("{0:F2}", rps), "rps", true); + + logger.Save(); + + Console.WriteLine("{0}: Requests per Second={1:F2}, Working Set={2}, Peak Working Set={3}, Private Bytes={4}", + this.Title, + rps, + TestMetrics.WorkingSet, + TestMetrics.PeakWorkingSet, + TestMetrics.PrivateBytes); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Runner/Tests/StressTest.cs b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Runner/Tests/StressTest.cs new file mode 100644 index 0000000000..c2637d5e5d --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Runner/Tests/StressTest.cs @@ -0,0 +1,155 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Reflection; +using System.Reflection.Emit; +using System.Diagnostics; +using System.Runtime.InteropServices; + +namespace DPStressHarness +{ + internal class StressTest : TestBase + { + private StressTestAttribute _attr; + private object _targetInstance; + private TestMethodDelegate _tmd; + + // TODO: MethodInfo objects below can have associated delegates to improve + // runtime performance. + protected MethodInfo _globalSetupMethod; + protected MethodInfo _globalCleanupMethod; + + public delegate void ExceptionHandler(Exception e); + + /// + /// Cache the global exception handler method reference. It is + /// recommended not to actually use this reference to call the + /// method. Use the delegate instead. + /// + protected MethodInfo _globalExceptionHandlerMethod; + + /// + /// Create a delegate to call global exception handler method. + /// Use this delegate to call test assembly's exception handler. + /// + protected ExceptionHandler _globalExceptionHandlerDelegate; + + public StressTest(StressTestAttribute attr, + MethodInfo testMethodInfo, + MethodInfo globalSetupMethod, + MethodInfo globalCleanupMethod, + Type type, + List setupMethods, + List cleanupMethods, + MethodInfo globalExceptionHandlerMethod) + : base(attr, testMethodInfo, type, setupMethods, cleanupMethods) + { + _attr = attr; + _globalSetupMethod = globalSetupMethod; + _globalCleanupMethod = globalCleanupMethod; + _globalExceptionHandlerMethod = globalExceptionHandlerMethod; + } + + public StressTest Clone() + { + StressTest t = new StressTest(_attr, this._testMethod, this._globalSetupMethod, this._globalCleanupMethod, this._type, this._setupMethods, this._cleanupMethods, this._globalExceptionHandlerMethod); + return t; + } + + private void InitTargetInstance() + { + _targetInstance = _type.GetConstructor(Type.EmptyTypes).Invoke(null); + + // Create a delegate for exception handling on _targetInstance + if (_globalExceptionHandlerMethod != null) + { + _globalExceptionHandlerDelegate = (ExceptionHandler)_globalExceptionHandlerMethod.CreateDelegate( + typeof(ExceptionHandler), + _targetInstance + ); + } + } + + /// + /// Perform any global initialization for the test assembly. For example, make the connection to the database, load a workspace, etc. + /// + public void RunGlobalSetup() + { + if (null == _targetInstance) + { + InitTargetInstance(); + } + + if (null != _globalSetupMethod) + { + _globalSetupMethod.Invoke(_targetInstance, null); + } + } + + /// + /// Run any per-thread setup needed + /// + public void RunSetup() + { + // create an instance of the class that defines the test method. + if (null == _targetInstance) + { + InitTargetInstance(); + } + _tmd = CreateTestMethodDelegate(); + + // Set variation fields on the target instance + SetVariations(_targetInstance); + + // Execute the setup phase for this thread. + ExecuteSetupPhase(_targetInstance); + } + + /// + /// Execute the test method(s) + /// + public override void Run() + { + _tmd(_targetInstance); + } + + /// + /// Provide an opportunity to handle the exception + /// + /// + public void HandleException(Exception e) + { + if (null != _globalExceptionHandlerDelegate) + { + _globalExceptionHandlerDelegate(e); + } + } + + /// + /// Run any per-thread cleanup for the test + /// + public void RunCleanup() + { + ExecuteCleanupPhase(_targetInstance); + } + + /// + /// Run final global cleanup for the test assembly. Could be used to release resources or for reporting, etc. + /// + public void RunGlobalCleanup() + { + if (null != _globalCleanupMethod) + { + _globalCleanupMethod.Invoke(_targetInstance, null); + } + } + + public int Weight + { + get { return _attr.Weight; } + } + } +} diff --git a/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Runner/Tests/Test.cs b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Runner/Tests/Test.cs new file mode 100644 index 0000000000..a73448a30b --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Runner/Tests/Test.cs @@ -0,0 +1,116 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Reflection; + + +namespace DPStressHarness +{ + internal class Test : TestBase + { + private TestAttribute _attr; + private int _overrideIterations = -1; + private int _overrideWarmup = -1; + + public Test(TestAttribute attr, + MethodInfo testMethodInfo, + Type type, + List setupMethods, + List cleanupMethods) + : base(attr, testMethodInfo, type, setupMethods, cleanupMethods) + { + _attr = attr; + } + + + public override void Run() + { + try + { + // create an instance of the class that defines the test method. + object targetInstance = _type.GetConstructor(Type.EmptyTypes).Invoke(null); + + SetVariations(targetInstance); + + ExecuteSetupPhase(targetInstance); + + TestMethodDelegate tmd = CreateTestMethodDelegate(); + + ExecuteTest(targetInstance, tmd); + + ExecuteCleanupPhase(targetInstance); + + LogTest(); + } + catch (TargetInvocationException e) + { + LogTestFailure(e.InnerException.ToString()); + } + catch (Exception e) + { + LogTestFailure(e.ToString()); + } + } + + protected void LogTest() + { + Logger logger = new Logger(TestMetrics.RunLabel, TestMetrics.IsOfficial, TestMetrics.Milestone, TestMetrics.Branch); + logger.AddTest(this.Title); + + LogStandardMetrics(logger); + + logger.AddTestMetric(Constants.TEST_METRIC_ELAPSED_SECONDS, string.Format("{0:F2}", TestMetrics.ElapsedSeconds), "sec", false); + + logger.Save(); + + Console.WriteLine("{0}: Elapsed Seconds={1:F2}, Working Set={2}, Peak Working Set={3}, Private Bytes={4}", + this.Title, + TestMetrics.ElapsedSeconds, + TestMetrics.WorkingSet, + TestMetrics.PeakWorkingSet, + TestMetrics.PrivateBytes); + } + + + private void ExecuteTest(object targetInstance, TestMethodDelegate tmd) + { + int warmupIterations = _attr.WarmupIterations; + int testIterations = _attr.TestIterations; + + if (_overrideIterations >= 0) + { + testIterations = _overrideIterations; + } + if (_overrideWarmup >= 0) + { + warmupIterations = _overrideWarmup; + } + + /** do some cleanup to make memory tests more accurate **/ + System.GC.Collect(); + System.GC.WaitForPendingFinalizers(); + System.GC.Collect(); + + IntPtr h = MemApi.GetCurrentProcess(); + bool fRes = MemApi.SetProcessWorkingSetSize(h, -1, -1); + /****/ + + System.Threading.Thread.Sleep(10000); + + for (int i = 0; i < warmupIterations; i++) + { + tmd(targetInstance); + } + + TestMetrics.StartCollection(); + for (int i = 0; i < testIterations; i++) + { + tmd(targetInstance); + } + TestMetrics.StopCollection(); + } + } +} diff --git a/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Runner/Tests/TestBase.cs b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Runner/Tests/TestBase.cs new file mode 100644 index 0000000000..95546547da --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Runner/Tests/TestBase.cs @@ -0,0 +1,163 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Reflection; + +namespace DPStressHarness +{ + public abstract class TestBase + { + private TestAttributeBase _attr; + private string _variationSuffix = ""; + + protected MethodInfo _testMethod; + + protected Type _type; + + protected List _setupMethods; + + protected List _cleanupMethods; + + protected delegate void TestMethodDelegate(object t); + + public TestBase(TestAttributeBase attr, + MethodInfo testMethodInfo, + Type type, + List setupMethods, + List cleanupMethods) + { + _attr = attr; + _testMethod = testMethodInfo; + _type = type; + _setupMethods = setupMethods; + _cleanupMethods = cleanupMethods; + } + + public string Title + { + get { return _attr.Title + _variationSuffix; } + } + + public string Description + { + get { return _attr.Description; } + } + + public string Category + { + get { return _attr.Category; } + } + + public TestPriority Priority + { + get { return _attr.Priority; } + } + + public List GetVariations() + { + FieldInfo[] fields = _type.GetFields(BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public); + + List variations = new List(10); + foreach (FieldInfo fi in fields) + { + TestVariationAttribute[] attrs = (TestVariationAttribute[])fi.GetCustomAttributes(typeof(TestVariationAttribute), false); + + foreach (TestVariationAttribute testVarAttr in attrs) + { + if (!variations.Contains(testVarAttr.VariationName)) + { + variations.Add(testVarAttr.VariationName); + } + } + } + + return variations; + } + + public abstract void Run(); + + protected void ExecuteSetupPhase(object targetInstance) + { + if (_setupMethods != null) + { + foreach (MethodInfo setupMthd in _setupMethods) + { + setupMthd.Invoke(targetInstance, null); + } + } + } + + protected void ExecuteCleanupPhase(object targetInstance) + { + if (_cleanupMethods != null) + { + foreach (MethodInfo cleanupMethod in _cleanupMethods) + { + cleanupMethod.Invoke(targetInstance, null); + } + } + } + + protected void SetVariations(object targetInstance) + { + FieldInfo[] fields = targetInstance.GetType().GetFields(BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public); + + foreach (FieldInfo fi in fields) + { + TestVariationAttribute[] attrs = (TestVariationAttribute[])fi.GetCustomAttributes(typeof(TestVariationAttribute), false); + + foreach (TestVariationAttribute testVarAttr in attrs) + { + foreach (string specifiedVariation in TestMetrics.Variations) + { + if (specifiedVariation.Equals(testVarAttr.VariationName)) + { + fi.SetValue(targetInstance, testVarAttr.VariationValue); + _variationSuffix += "_" + testVarAttr.VariationName; + break; + } + } + } + } + } + + protected TestMethodDelegate CreateTestMethodDelegate() + { + return new TestMethodDelegate((instance) => _testMethod.Invoke(instance, null)); + } + + protected void LogTestFailure(string exceptionData) + { + Console.WriteLine("{0}: Failed", this.Title); + Console.WriteLine(exceptionData); + + Logger logger = new Logger(TestMetrics.RunLabel, false, TestMetrics.Milestone, TestMetrics.Branch); + logger.AddTest(this.Title); + logger.AddTestMetric("Test Assembly", _testMethod.Module.FullyQualifiedName, null); + logger.AddTestException(exceptionData); + logger.Save(); + } + + protected void LogStandardMetrics(Logger logger) + { + logger.AddTestMetric(Constants.TEST_METRIC_TEST_ASSEMBLY, _testMethod.Module.FullyQualifiedName, null); + logger.AddTestMetric(Constants.TEST_METRIC_TEST_IMPROVEMENT, _attr.Improvement, null); + logger.AddTestMetric(Constants.TEST_METRIC_TEST_OWNER, _attr.Owner, null); + logger.AddTestMetric(Constants.TEST_METRIC_TEST_CATEGORY, _attr.Category, null); + logger.AddTestMetric(Constants.TEST_METRIC_TEST_PRIORITY, _attr.Priority.ToString(), null); + logger.AddTestMetric(Constants.TEST_METRIC_APPLICATION_NAME, _attr.Improvement, null); + + if (TestMetrics.TargetAssembly != null) + { + logger.AddTestMetric(Constants.TEST_METRIC_TARGET_ASSEMBLY_NAME, (new AssemblyName(TestMetrics.TargetAssembly.FullName)).Name, null); + } + + logger.AddTestMetric(Constants.TEST_METRIC_PEAK_WORKING_SET, string.Format("{0}", TestMetrics.PeakWorkingSet), "bytes"); + logger.AddTestMetric(Constants.TEST_METRIC_WORKING_SET, string.Format("{0}", TestMetrics.WorkingSet), "bytes"); + logger.AddTestMetric(Constants.TEST_METRIC_PRIVATE_BYTES, string.Format("{0}", TestMetrics.PrivateBytes), "bytes"); + } + } +} diff --git a/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Runner/Tests/ThreadPoolTest.cs b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Runner/Tests/ThreadPoolTest.cs new file mode 100644 index 0000000000..f065c15312 --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Runner/Tests/ThreadPoolTest.cs @@ -0,0 +1,174 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Reflection; +using System.Reflection.Emit; +using System.Diagnostics; +using System.Runtime.InteropServices; +using System.Threading; + +namespace DPStressHarness +{ + internal class ThreadPoolTest : TestBase + { + private ThreadPoolTestAttribute _attr; + public static bool _continue; + public static int _threadsRunning; + public static int _rps; + public static WaitCallback _waitCallback = new WaitCallback(RunThreadPool); + public static Exception _firstException = null; + + private struct TestInfo + { + public object _instance; + public TestMethodDelegate _delegateTest; + } + + public ThreadPoolTest(ThreadPoolTestAttribute attr, + MethodInfo testMethodInfo, + Type type, + List setupMethods, + List cleanupMethods) + : base(attr, testMethodInfo, type, setupMethods, cleanupMethods) + { + _attr = attr; + } + + public override void Run() + { + try + { + Stopwatch timer = new Stopwatch(); + long warmupDuration = (long)_attr.WarmupDuration * Stopwatch.Frequency; + long testDuration = (long)_attr.TestDuration * Stopwatch.Frequency; + int threads = _attr.Threads; + + TestInfo[] info = new TestInfo[threads]; + ConstructorInfo targetConstructor = _type.GetConstructor(Type.EmptyTypes); + + for (int i = 0; i < threads; i++) + { + info[i] = new TestInfo(); + info[i]._instance = targetConstructor.Invoke(null); + info[i]._delegateTest = CreateTestMethodDelegate(); + + ExecuteSetupPhase(info[i]._instance); + } + + _firstException = null; + _continue = true; + _rps = 0; + + for (int i = 0; i < threads; i++) + { + Interlocked.Increment(ref _threadsRunning); + ThreadPool.QueueUserWorkItem(_waitCallback, info[i]); + } + + timer.Reset(); + timer.Start(); + + while (timer.ElapsedTicks < warmupDuration) + { + Thread.Sleep(1000); + } + + int warmupRequests = Interlocked.Exchange(ref _rps, 0); + timer.Reset(); + timer.Start(); + TestMetrics.StartCollection(); + + while (timer.ElapsedTicks < testDuration) + { + Thread.Sleep(1000); + } + + int requests = Interlocked.Exchange(ref _rps, 0); + double elapsedSeconds = timer.ElapsedTicks / Stopwatch.Frequency; + TestMetrics.StopCollection(); + _continue = false; + + while (_threadsRunning > 0) + { + Thread.Sleep(1000); + } + + for (int i = 0; i < threads; i++) + { + ExecuteCleanupPhase(info[i]._instance); + } + + double rps = (double)requests / elapsedSeconds; + + if (_firstException == null) + { + LogTest(rps); + } + else + { + LogTestFailure(_firstException.ToString()); + } + } + catch (TargetInvocationException e) + { + LogTestFailure(e.InnerException.ToString()); + } + catch (Exception e) + { + LogTestFailure(e.ToString()); + } + } + + + public static void RunThreadPool(object state) + { + try + { + TestInfo info = (TestInfo)state; + info._delegateTest(info._instance); + Interlocked.Increment(ref _rps); + } + catch (Exception e) + { + if (_firstException == null) + { + _firstException = e; + } + _continue = false; + } + finally + { + if (_continue) + { + ThreadPool.QueueUserWorkItem(_waitCallback, state); + } + else + { + Interlocked.Decrement(ref _threadsRunning); + } + } + } + + protected void LogTest(double rps) + { + Logger logger = new Logger(TestMetrics.RunLabel, TestMetrics.IsOfficial, TestMetrics.Milestone, TestMetrics.Branch); + logger.AddTest(this.Title); + + LogStandardMetrics(logger); + + logger.AddTestMetric(Constants.TEST_METRIC_RPS, string.Format("{0:F2}", rps), "rps", true); + + logger.Save(); + + Console.WriteLine("{0}: Requests per Second={1:F2}, Working Set={2}, Peak Working Set={3}, Private Bytes={4}", + this.Title, + rps, + TestMetrics.WorkingSet, + TestMetrics.PeakWorkingSet, + TestMetrics.PrivateBytes); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Tests/FilteredDefaultTraceListener.cs b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Tests/FilteredDefaultTraceListener.cs new file mode 100644 index 0000000000..ce29f8ee29 --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Tests/FilteredDefaultTraceListener.cs @@ -0,0 +1,210 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Diagnostics; +using System.Reflection; +using System.Text.RegularExpressions; + +namespace Stress.Data.SqlClient +{ + /// + /// A DefaultTraceListener that can filter out given asserts + /// + internal class FilteredDefaultTraceListener : DefaultTraceListener + { + private static readonly Assembly s_systemDataAssembly = typeof(Microsoft.Data.SqlClient.SqlConnection).GetTypeInfo().Assembly; + private const RegexOptions AssertMessageRegexOptions = RegexOptions.Singleline | RegexOptions.CultureInvariant; + + private enum MatchType : byte + { + Exact, + Regex, + } + + private enum HandlingOption : byte + { + CovertToException, + WriteToConsole, + } + + /// + /// Represents a single assert to filter out + /// + private struct FilteredAssert + { + public FilteredAssert(string messageOrRegex, int bugNumber, MatchType matchType, HandlingOption assertHandlingOption, params string[] stackFrames) + { + if (matchType == MatchType.Exact) + { + Message = messageOrRegex; + MessageRegex = null; + } + else + { + Message = null; + MessageRegex = new Regex(messageOrRegex, AssertMessageRegexOptions); + } + + + StackFrames = stackFrames; + BugNumber = bugNumber; + Handler = assertHandlingOption; + } + + /// + /// The assert's message (NOTE: MessageRegex must be null if this is specified) + /// + public string Message; + /// + /// A regex that matches the assert's message (NOTE: Message must be null if this is specified) + /// + public Regex MessageRegex; + /// + /// The most recent frames on the stack when the assert was hit (i.e. 0 is most recent, 1 is next, etc.). Null if stack should not be checked. + /// + public string[] StackFrames; + /// + /// Product bug to fix the assert + /// + public int BugNumber; + /// + /// How the assert will be handled once it is matched + /// + /// + /// In most cases this can be set to WriteToConsole - typically the assert is either invalid or there will be an exception thrown by the product code anyway. + /// However, in the case where this is state corruption AND the product code has no exception in place, this will need to be set to CovertToException to prevent further corruption\asserts + /// + public HandlingOption Handler; + } + + private static readonly FilteredAssert[] s_assertsToFilter = new FilteredAssert[] { + new FilteredAssert("TdsParser::ThrowExceptionAndWarning called with no exceptions or warnings!", 433324, MatchType.Exact, HandlingOption.WriteToConsole, + "Microsoft.Data.SqlClient.TdsParser.ThrowExceptionAndWarning", + "Microsoft.Data.SqlClient.TdsParserStateObject.ThrowExceptionAndWarning", + "Microsoft.Data.SqlClient.TdsParserStateObject.ReadAsyncCallbackCaptureException"), + }; + + public FilteredDefaultTraceListener(DefaultTraceListener listenerToClone) : base() + { + base.Filter = listenerToClone.Filter; + base.IndentLevel = listenerToClone.IndentLevel; + base.IndentSize = listenerToClone.IndentSize; + base.TraceOutputOptions = listenerToClone.TraceOutputOptions; + } + + public override void Fail(string message) + { + Fail(message, null); + } + + public override void Fail(string message, string detailMessage) + { + FilteredAssert? foundAssert = FindAssertInList(message); + if (!foundAssert.HasValue) + { + // Don't filter this assert - pass it down to the underlying DefaultTraceListener which will show the UI, break into the debugger, etc. + base.Fail(message, detailMessage); + } + else + { + // Assert is to be filtered, either convert to an exception or a message + var assert = foundAssert.Value; + if (assert.Handler == HandlingOption.CovertToException) + { + throw new FailedAssertException(message, assert.BugNumber); + } + else if (assert.Handler == HandlingOption.WriteToConsole) + { + Console.WriteLine("Hit known assert, Bug {0}: {1}", assert.BugNumber, message); + } + } + } + + private FilteredAssert? FindAssertInList(string message) + { + StackTrace actualCallstack = null; + foreach (var assert in s_assertsToFilter) + { + if (((assert.Message != null) && (assert.Message == message)) || ((assert.MessageRegex != null) && (assert.MessageRegex.IsMatch(message)))) + { + if (assert.StackFrames != null) + { + // Skipping four frames: + // Stress.Data.SqlClient.FilteredDefaultTraceListener.FindAssertInList + // Stress.Data.SqlClient.FilteredDefaultTraceListener.Fail (This may be in the stack twice due to the overloads calling each other) + // System.Diagnostics.TraceInternal.Fail + // System.Diagnostics.Debug.Assert + if (actualCallstack == null) + { + actualCallstack = new StackTrace(e: new InvalidOperationException(), fNeedFileInfo: false); + } + + StackFrame[] frames = actualCallstack.GetFrames(); + if (frames.Length >= assert.StackFrames.Length) + { + int actualStackFrameCounter = 0; + bool foundMatch = true; + foreach (var expectedStack in assert.StackFrames) + { + // Get the method information for the next stack which came from System.Data.dll + MethodBase actualStackMethod; + do + { + actualStackMethod = frames[actualStackFrameCounter].GetMethod(); + actualStackFrameCounter++; + } while (((actualStackMethod.DeclaringType == null) || (actualStackMethod.DeclaringType.GetTypeInfo().Assembly != s_systemDataAssembly)) && (actualStackFrameCounter < frames.Length)); + + if ((actualStackFrameCounter > frames.Length) || (string.Format("{0}.{1}", actualStackMethod.DeclaringType.FullName, actualStackMethod.Name) != expectedStack)) + { + // Ran out of actual frames while there were still expected frames or the current frames didn't match + foundMatch = false; + break; + } + } + + // Message and all frames matched + if (foundMatch) + { + return assert; + } + } + } + else + { + // Messages match, and there are no frames to verify + return assert; + } + } + } + + // Fall through - didn't find the assert + return null; + } + } + + internal class FailedAssertException : Exception + { + /// + /// Number of the bug that caused the assert to fire + /// + public int BugNumber { get; private set; } + + /// + /// Creates an exception to represent hitting a known assert + /// + /// Message of the assert + /// Number of the bug that caused the assert + public FailedAssertException(string message, int bugNumber) + : base(message) + { + BugNumber = bugNumber; + } + + public override string ToString() + { + return string.Format("{1}\r\nAssert caused by Bug {0}", BugNumber, base.ToString()); + } + } +} diff --git a/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Tests/HostsFileManager.cs b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Tests/HostsFileManager.cs new file mode 100644 index 0000000000..6d44909ddb --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Tests/HostsFileManager.cs @@ -0,0 +1,481 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.IO; +using System.Net; +using System.Diagnostics; +using System.Runtime.InteropServices; + +namespace Microsoft.Test.Data.SqlClient +{ + /// + /// allows user to manipulate %windir%\system32\drivers\etc\hosts + /// the hosts file must be reverted if changed even if test application crashes, thus inherit from CriticalFinalizerObject. Make sure the instance is disposed after its use. + /// The last dispose call on the active instance reverts the hosts file. + /// + /// Usage: + /// using (var hostsFile = new HostsFileManager()) + /// { + /// // use the hostsFile methods to add/remove entries + /// // simultaneous usage of HostsFileManager in two app domains or processes on the same machine is not allowed + /// } + /// + public sealed class HostsFileManager : IDisposable + { + // define global (machine-wide) lock instance + private static EventWaitHandle s_globalLock = new EventWaitHandle(true /* create as signalled */, EventResetMode.AutoReset, @"Global\HostsFileManagerLock"); + private static bool s_globalLockTaken; // set when global (machine-wide) lock is in use + + private static int s_localUsageRefCount; + private static object s_localLock = new object(); + + private static string s_hostsFilePath; + private static string s_backupPath; + private static bool s_hasBackup; + private static TextReader s_activeReader; + private static TextWriter s_activeWriter; + private static List s_entriesCache; + + private const string HostsFilePathUnderSystem32 = @"C:\Windows\System32\drivers\etc\hosts"; + private const string HostsFilePathUnderLinux = "/etc/hosts"; + private const string HostsFilePathUnderMacOS = "/private/etc/hosts"; + + + private static void InitializeGlobal(ref bool mustRelease) + { + if (mustRelease) + { + // already initialized + return; + } + + lock (s_localLock) + { + if (mustRelease) + { + // check again under lock + return; + } + + if (s_localUsageRefCount > 0) + { + // initialized by another thread + ++s_localUsageRefCount; + return; + } + + // first call to initialize in this app domain + // note: simultanious use of HostsFileManager is currently supported only within single AppDomain scope + + // non-critical initialization goes first + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + s_hostsFilePath = HostsFilePathUnderSystem32; + } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + s_hostsFilePath = HostsFilePathUnderLinux; + } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + s_hostsFilePath = HostsFilePathUnderMacOS; + } + + s_backupPath = Path.Combine(Path.GetTempPath(), string.Format("Hosts_{0}.bak", Guid.NewGuid().ToString("N"))); + + // try to get global lock + // note that once global lock is aquired, it must be released + try { } + finally + { + if (s_globalLock.WaitOne(0)) + { + s_globalLockTaken = true; + mustRelease = true; + ++s_localUsageRefCount; // increment ref count for the first thread using the manager + } + } + + if (!s_globalLockTaken) + { + throw new InvalidOperationException("HostsFileManager cannot initialize because hosts file is in use by another instance of the manager in the same or a different process (concurrent access is not allowed)"); + } + + // locked now, take snapshot of hosts file and save it as a backup + File.Copy(s_hostsFilePath, s_backupPath); + s_hasBackup = true; + + // load the current entries + InternalRefresh(); + } + } + + private static void TerminateGlobal(ref bool originalMustRelease) + { + if (!originalMustRelease) + { + // already disposed + return; + } + + lock (s_localLock) + { + if (!originalMustRelease) + { + // check again under lock + return; + } + + // not yet disposed, do it now + if (s_localUsageRefCount > 1) + { + // still in use by another thread(s) + --s_localUsageRefCount; + return; + } + + if (s_activeReader != null) + { + s_activeReader.Dispose(); + s_activeReader = null; + } + if (s_activeWriter != null) + { + s_activeWriter.Dispose(); + s_activeWriter = null; + } + bool deleteBackup = false; + if (s_hasBackup) + { + // revert the hosts file + File.Copy(s_backupPath, s_hostsFilePath, overwrite: true); + s_hasBackup = false; + deleteBackup = true; + } + + // Note: if critical finalizer fails to revert the hosts file, the global lock might remain reset until the machine is rebooted. + // if this happens, Hosts file in unpredictable state so there is no point in running tests anyway + if (s_globalLockTaken) + { + try { } + finally + { + s_globalLock.Set(); + s_globalLockTaken = false; + --s_localUsageRefCount; // decrement local ref count + originalMustRelease = false; + } + } + + // now we can destroy the backup + if (deleteBackup) + { + File.Delete(s_backupPath); + } + } + } + + private bool _mustRelease; + private bool _disposed; + + public HostsFileManager() + { + // lazy initialization + _mustRelease = false; + _disposed = false; + } + + ~HostsFileManager() + { + Dispose(false); + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + private void Dispose(bool disposing) + { + if (!_disposed) + { + _disposed = true; + TerminateGlobal(ref _mustRelease); + } + } + + public class HostEntry + { + public HostEntry(string name, IPAddress address) + { + ValidateName(name); + ValidateAddress(address); + + this.Name = name; + this.Address = address; + } + + public readonly string Name; + public readonly IPAddress Address; + } + + // helper methods + + // must be called under lock(_localLock) from each public API that uses static fields + private void InitializeLocal() + { + if (_disposed) + { + throw new ObjectDisposedException(this.GetType().Name); + } + + InitializeGlobal(ref _mustRelease); + } + + private static readonly char[] s_whiteSpaceChars = new char[] { ' ', '\t' }; + + private static void ValidateName(string name) + { + if (string.IsNullOrEmpty(name) || name.IndexOfAny(s_whiteSpaceChars) >= 0) + { + throw new ArgumentException("name cannot be null or empty or have whitespace characters in it"); + } + } + + private static void ValidateAddress(IPAddress address) + { + ValidateNonNull(address, "address"); + + if (address.AddressFamily != System.Net.Sockets.AddressFamily.InterNetwork && + address.AddressFamily != System.Net.Sockets.AddressFamily.InterNetworkV6) + { + throw new ArgumentException("only IPv4 or IPv6 addresses are allowed"); + } + } + + private static void ValidateNonNull(T value, string argName) where T : class + { + if (value == null) + { + throw new ArgumentNullException(argName); + } + } + + private static HostEntry TryParseLine(string line) + { + line = line.Trim(); + if (line.StartsWith("#")) + { + // comment, ignore + return null; + } + + string[] items = line.Split(s_whiteSpaceChars, StringSplitOptions.RemoveEmptyEntries); + if (items.Length == 0) + { + // empty or white-space only line - ignore + return null; + } + + if (items.Length != 2) + { + Trace.WriteLine("Wrong entry in the hosts file (exactly two columns expected): \"" + line + "\""); + return null; + } + + string name = items[1]; + IPAddress address; + if (!IPAddress.TryParse(items[0], out address)) + { + Trace.WriteLine("Wrong entry in the hosts file (cannot parse the IP address): \"" + line + "\""); + return null; + } + + try + { + return new HostEntry(name, address); + } + catch (ArgumentException e) + { + Console.WriteLine("Wrong entry in the hosts file, cannot create host entry: " + e.Message); + return null; + } + } + + private bool NameMatch(HostEntry entry, string name) + { + ValidateNonNull(entry, "entry"); + ValidateName(name); + + return string.Equals(entry.Name, name, StringComparison.OrdinalIgnoreCase); + } + + // hosts file manipulation methods + + // reloads the hosts file, must be called under lock(_localLock) + private static void InternalRefresh() + { + List entries = new List(); + + try + { + s_activeReader = new StreamReader(new FileStream(s_hostsFilePath, FileMode.Open)); + + string line; + while ((line = s_activeReader.ReadLine()) != null) + { + HostEntry nextEntry = TryParseLine(line); + if (nextEntry != null) + { + entries.Add(nextEntry); + } + } + } + finally + { + if (s_activeReader != null) + { + s_activeReader.Dispose(); + s_activeReader = null; + } + } + + s_entriesCache = entries; + } + + // reloads the hosts file, must be called while still under lock(_localLock) + private void InternalSave() + { + try + { + s_activeWriter = new StreamWriter(new FileStream(s_hostsFilePath, FileMode.Create)); + + foreach (HostEntry entry in s_entriesCache) + { + s_activeWriter.WriteLine(" {0} {1}", entry.Address, entry.Name); + } + + s_activeWriter.Flush(); + } + finally + { + if (s_activeWriter != null) + { + s_activeWriter.Dispose(); + s_activeWriter = null; + } + } + } + + public int RemoveAll(string name) + { + lock (s_localLock) + { + InitializeLocal(); + ValidateName(name); + + int removed = s_entriesCache.RemoveAll(entry => NameMatch(entry, name)); + + if (removed > 0) + { + InternalSave(); + } + + return removed; + } + } + + public IEnumerable EnumerateAddresses(string name) + { + lock (s_localLock) + { + InitializeLocal(); + ValidateName(name); + + return from entry in s_entriesCache where NameMatch(entry, name) select entry.Address; + } + } + + public void Add(string name, IPAddress address) + { + lock (s_localLock) + { + InitializeLocal(); + + HostEntry entry = new HostEntry(name, address); // c-tor validates the arguments + s_entriesCache.Add(entry); + + InternalSave(); + } + } + + public void Add(HostEntry entry) + { + lock (s_localLock) + { + InitializeLocal(); + ValidateNonNull(entry, "entry"); + + s_entriesCache.Add(entry); + + InternalSave(); + } + } + + public void AddRange(string name, IEnumerable addresses) + { + lock (s_localLock) + { + InitializeLocal(); + ValidateName(name); + ValidateNonNull(addresses, "addresses"); + + foreach (IPAddress address in addresses) + { + HostEntry entry = new HostEntry(name, address); + + s_entriesCache.Add(entry); + } + + InternalSave(); + } + } + + public void AddRange(IEnumerable entries) + { + lock (s_localLock) + { + InitializeLocal(); + ValidateNonNull(entries, "entries"); + + foreach (HostEntry entry in entries) + { + ValidateNonNull(entry, "entries element"); + + s_entriesCache.Add(entry); + } + + InternalSave(); + } + } + + public void Clear() + { + lock (s_localLock) + { + InitializeLocal(); + + s_entriesCache.Clear(); + + InternalSave(); + } + } + } +} diff --git a/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Tests/MultiSubnetFailoverSetup.cs b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Tests/MultiSubnetFailoverSetup.cs new file mode 100644 index 0000000000..2a0719d201 --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Tests/MultiSubnetFailoverSetup.cs @@ -0,0 +1,135 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Microsoft.Test.Data.SqlClient; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Threading.Tasks; + +namespace Stress.Data.SqlClient +{ + internal class MultiSubnetFailoverSetup + { + private HostsFileManager _hostsFile; + + internal MultiSubnetFailoverSetup(SqlServerDataSource source) + { + this.Source = source; + } + + internal string MultiSubnetFailoverHostNameForIntegratedSecurity { get; private set; } + + private List _multiSubnetFailoverHostNames; + + internal string GetMultiSubnetFailoverHostName(Random rnd) + { + return _multiSubnetFailoverHostNames[rnd.Next(_multiSubnetFailoverHostNames.Count)]; + } + + public SqlServerDataSource Source { get; private set; } + + internal void InitializeFakeHostsForMultiSubnetFailover() + { + // initialize fake hosts for MultiSubnetFailover + string originalHost, protocol, instance; + int? port; + NetUtils.ParseDataSource(this.Source.DataSource, out protocol, out originalHost, out instance, out port); + + // get the IPv4 addresses + IPAddress[] ipV4 = NetUtils.EnumerateIPv4Addresses(originalHost).ToArray(); + if (ipV4 == null || ipV4.Length == 0) + { + // consider supporting IPv6 when it becomes relevant (not a goal right now) + throw new ArgumentException("The target server " + originalHost + " has no IPv4 addresses associated with it in DNS"); + } + + // construct different host names for MSF with valid server IP located in a different place each time + List allEntries = new List(); + + int nextValidIp = 0; + int nextInvalidIp = 0; + _multiSubnetFailoverHostNames = new List(); + + // construct some interesting cases for MultiSubnetFailover stress + + // for integrated security to work properly, the server name in connection string must match the target server host name. + // thus, create one entry in the hosts with the true server name: either FQDN or the short name + Task task = Dns.GetHostEntryAsync(ipV4[0]); + string nameToUse = task.Result.HostName; + if (originalHost.Contains('.')) + { + // if the original hosts is FQDN, put short name in the hosts instead + // otherwise, put FQDN in hosts + int shortNameEnd = nameToUse.IndexOf('.'); + if (shortNameEnd > 0) + nameToUse = nameToUse.Substring(0, shortNameEnd); + } + // since true server name is being re-mapped, keep the valid IP first in the list + AddEntryHelper(allEntries, _multiSubnetFailoverHostNames, nameToUse, + ipV4[(nextValidIp++) % ipV4.Length], + NetUtils.GetNonExistingIPv4((nextInvalidIp++) % NetUtils.NonExistingIPv4AddressCount)); + this.MultiSubnetFailoverHostNameForIntegratedSecurity = nameToUse; + + // single valid IP + AddEntryHelper(allEntries, _multiSubnetFailoverHostNames, "MSF_MP_Stress_V", + ipV4[(nextValidIp++) % ipV4.Length]); + + // valid + invalid + AddEntryHelper( + allEntries, _multiSubnetFailoverHostNames, "MSF_MP_Stress_VI", + ipV4[(nextValidIp++) % ipV4.Length], + NetUtils.GetNonExistingIPv4((nextInvalidIp++) % NetUtils.NonExistingIPv4AddressCount)); + + // invalid + valid + invalid + AddEntryHelper( + allEntries, _multiSubnetFailoverHostNames, "MSF_MP_Stress_IVI", + NetUtils.GetNonExistingIPv4((nextInvalidIp++) % NetUtils.NonExistingIPv4AddressCount), + ipV4[(nextValidIp++) % ipV4.Length], + NetUtils.GetNonExistingIPv4((nextInvalidIp++) % NetUtils.NonExistingIPv4AddressCount)); + + // Using more than one active IP associated with the virtual name (VNN) is not a supported scenario with MultiSubnetFailover. + // But, this can definitly happen in reality - add special cases here to cover two valid IPs. + AddEntryHelper( + allEntries, _multiSubnetFailoverHostNames, "MSF_MP_Stress_IVI", + ipV4[(nextValidIp++) % ipV4.Length], + NetUtils.GetNonExistingIPv4((nextInvalidIp++) % NetUtils.NonExistingIPv4AddressCount), + ipV4[(nextValidIp++) % ipV4.Length]); + + // big boom with 7 IPs - for stress purposes only + AddEntryHelper( + allEntries, _multiSubnetFailoverHostNames, "MSF_MP_Stress_BIGBOOM", + NetUtils.GetNonExistingIPv4((nextInvalidIp++) % NetUtils.NonExistingIPv4AddressCount), + NetUtils.GetNonExistingIPv4((nextInvalidIp++) % NetUtils.NonExistingIPv4AddressCount), + NetUtils.GetNonExistingIPv4((nextInvalidIp++) % NetUtils.NonExistingIPv4AddressCount), + ipV4[(nextValidIp++) % ipV4.Length], + NetUtils.GetNonExistingIPv4((nextInvalidIp++) % NetUtils.NonExistingIPv4AddressCount), + ipV4[(nextValidIp++) % ipV4.Length], + NetUtils.GetNonExistingIPv4((nextInvalidIp++) % NetUtils.NonExistingIPv4AddressCount) + ); + + // list of fake hosts is ready, initialize hosts file manager and update the file + _hostsFile = new HostsFileManager(); + _hostsFile.AddRange(allEntries); + } + + + private static void AddEntryHelper(List entries, List names, string msfHostName, params IPAddress[] addresses) + { + for (int i = 0; i < addresses.Length; i++) + entries.Add(new HostsFileManager.HostEntry(msfHostName, addresses[i])); + names.Add(msfHostName); + } + + internal void Terminate() + { + // revert hosts file + if (_hostsFile != null) + { + _hostsFile.Dispose(); + } + } + } +} diff --git a/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Tests/NetUtils.cs b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Tests/NetUtils.cs new file mode 100644 index 0000000000..0e756d1bf9 --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Tests/NetUtils.cs @@ -0,0 +1,206 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Net; +using System.Diagnostics; +using Microsoft.Data.SqlClient; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Microsoft.Test.Data.SqlClient +{ + public static class NetUtils + { + // according to RFC 5737 (http://tools.ietf.org/html/rfc5737): The blocks 192.0.2.0/24 (TEST-NET-1), 198.51.100.0/24 (TEST-NET-2), + // and 203.0.113.0/24 (TEST-NET-3) are provided for use in documentation and should not be in use by any public network + private static readonly IPAddress[] s_testNets = new IPAddress[] + { + IPAddress.Parse("192.0.2.0"), + IPAddress.Parse("198.51.100.0"), + IPAddress.Parse("203.0.113.0") + }; + + private const int TestNetAddressRangeLength = 256; + + public static readonly int NonExistingIPv4AddressCount = TestNetAddressRangeLength * s_testNets.Length; + + public static IPAddress GetNonExistingIPv4(int index) + { + if (index < 0 || index > NonExistingIPv4AddressCount) + { + throw new ArgumentOutOfRangeException("index"); + } + + byte[] address = s_testNets[index / TestNetAddressRangeLength].GetAddressBytes(); + + Debug.Assert(address[3] == 0, "address ranges above must end with .0"); + address[3] = checked((byte)(index % TestNetAddressRangeLength)); + + return new IPAddress(address); + } + + public static IEnumerable EnumerateIPv4Addresses(string hostName) + { + hostName = hostName.Trim(); + + if ((hostName == ".") || + (string.Compare("(local)", hostName, StringComparison.OrdinalIgnoreCase) == 0)) + { + hostName = Dns.GetHostName(); + } + + Task task = Dns.GetHostAddressesAsync(hostName); + IPAddress[] allAddresses = task.Result; + + foreach (var addr in allAddresses) + { + if (addr.AddressFamily == System.Net.Sockets.AddressFamily.InterNetwork) + { + yield return addr; + } + } + } + + /// + /// Splits data source into protocol, host name, instance name and port. + /// + /// Note that this algorithm does not cover all valid combinations of data source; only those we actually use in tests are supported now. + /// Please update as needed. + /// + public static void ParseDataSource(string dataSource, out string protocol, out string hostName, out string instanceName, out int? port) + { + // check for protocol prefix + int i = dataSource.IndexOf(':'); + if (i >= 0) + { + protocol = dataSource.Substring(0, i); + + // remove the protocol + dataSource = dataSource.Substring(i + 1); + } + else + { + protocol = null; + } + + // check for server port + i = dataSource.IndexOf(','); + if (i >= 0) + { + // there is a port value in connection string + port = int.Parse(dataSource.Substring(i + 1)); + dataSource = dataSource.Substring(0, i); + } + else + { + port = null; + } + + // check for the instance name + i = dataSource.IndexOf('\\'); + if (i >= 0) + { + instanceName = dataSource.Substring(i + 1); + dataSource = dataSource.Substring(0, i); + } + else + { + instanceName = null; + } + + // trim redundant whitespace + dataSource = dataSource.Trim(); + hostName = dataSource; + } + + private static Dictionary s_dataSourceToPortCache = new Dictionary(); + + /// + /// the method converts the regular connection string to one supported by MultiSubnetFailover (connect to the port, bypassing the browser) + /// it does the following: + /// * removes Failover Partner, if presents + /// * removes the network library and protocol prefix (only TCP is supported) + /// * if instance name is specified without port value, data source is replaced with "server, port" format instead of "server\name" + /// + /// Note that this method can create a connection to the server in case TCP port is needed. The port value is cached per data source, to avoid round trip to the server on next use. + /// + /// original connection string, must be valid + /// optionally, replace the (network) server name with a different one + /// holds the original server name on return + /// MultiSubnetFailover-enabled connection string builder + public static SqlConnectionStringBuilder GetMultiSubnetFailoverConnectionString(string connectionString, string replaceServerName, out string originalServerName) + { + SqlConnectionStringBuilder sb = new SqlConnectionStringBuilder(connectionString); + + sb["Network Library"] = null; // MSF supports TCP only, no need to specify the protocol explicitly + sb["Failover Partner"] = null; // not supported, remove it if present + + string protocol, instance; + int? serverPort; + + ParseDataSource(sb.DataSource, out protocol, out originalServerName, out instance, out serverPort); + + // Note: protocol value is ignored, connection to the server will fail if TCP is not enabled on the server + + if (!serverPort.HasValue) + { + // to get server listener's TCP port, connect to it using the original string, with TCP protocol enforced + // to improve stress performance, cache the port value to avoid round trip every time new connection string is needed + lock (s_dataSourceToPortCache) + { + int cachedPort; + string cacheKey = sb.DataSource; + if (s_dataSourceToPortCache.TryGetValue(cacheKey, out cachedPort)) + { + serverPort = cachedPort; + } + else + { + string originalServerNameWithInstance = sb.DataSource; + int protocolEndIndex = originalServerNameWithInstance.IndexOf(':'); + if (protocolEndIndex >= 0) + { + originalServerNameWithInstance = originalServerNameWithInstance.Substring(protocolEndIndex + 1); + } + + sb.DataSource = "tcp:" + originalServerNameWithInstance; + string tcpConnectionString = sb.ConnectionString; + using (SqlConnection con = new SqlConnection(tcpConnectionString)) + { + con.Open(); + + SqlCommand cmd = con.CreateCommand(); + cmd.CommandText = "select [local_tcp_port] from sys.dm_exec_connections where [session_id] = @@SPID"; + serverPort = Convert.ToInt32(cmd.ExecuteScalar()); + } + + s_dataSourceToPortCache[cacheKey] = serverPort.Value; + } + } + } + + // override it with user-provided one + string retDataSource; + if (replaceServerName != null) + { + retDataSource = replaceServerName; + } + else + { + retDataSource = originalServerName; + } + + // reconstruct the connection string (with the new server name and port) + // also, no protocol is needed since TCP is enforced anyway if MultiSubnetFailover is set to true + Debug.Assert(serverPort.HasValue, "Server port must be initialized"); + retDataSource += ", " + serverPort.Value; + + sb.DataSource = retDataSource; + sb.MultiSubnetFailover = true; + + return sb; + } + } +} diff --git a/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Tests/SqlClient.Stress.Tests.csproj b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Tests/SqlClient.Stress.Tests.csproj new file mode 100644 index 0000000000..6eb77bec44 --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Tests/SqlClient.Stress.Tests.csproj @@ -0,0 +1,15 @@ + + + + Stress.Data.SqlClient + + + + + + + + + + + diff --git a/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Tests/SqlClientStressFactory.cs b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Tests/SqlClientStressFactory.cs new file mode 100644 index 0000000000..160f56990f --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Tests/SqlClientStressFactory.cs @@ -0,0 +1,297 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Diagnostics; +using Microsoft.Data.SqlClient; +using Microsoft.Test.Data.SqlClient; + +namespace Stress.Data.SqlClient +{ + public class SqlClientStressFactory : DataStressFactory + { + // scenarios + internal enum SqlClientScenario + { + Sql + } + + private SqlServerDataSource _source; + private SqlClientScenario _scenario; + + private MultiSubnetFailoverSetup _multiSubnetSetupHelper; + + internal SqlClientStressFactory() + : base(SqlClientFactory.Instance) + { + } + + internal void Initialize(ref string scenario, ref DataSource source) + { + // Ignore all asserts from known issues + var defaultTraceListener = Trace.Listeners["Default"] as DefaultTraceListener; + if (defaultTraceListener != null) + { + var newTraceListener = new FilteredDefaultTraceListener(defaultTraceListener); + Trace.Listeners.Remove(defaultTraceListener); + Trace.Listeners.Add(newTraceListener); + } + + // scenario <=> SqlClientScenario + if (string.IsNullOrEmpty(scenario)) + { + _scenario = SqlClientScenario.Sql; + } + else + { + _scenario = (SqlClientScenario)Enum.Parse(typeof(SqlClientScenario), scenario, true); + } + scenario = _scenario.ToString(); + + // initialize the source information + // SNAC/WDAC is using SqlServer sources; JET is using Access + switch (_scenario) + { + case SqlClientScenario.Sql: + if (source == null) + source = DataStressSettings.Instance.GetDefaultSourceByType(DataSourceType.SqlServer); + else if (source.Type != DataSourceType.SqlServer) + throw new ArgumentException(string.Format("Given source type is wrong: required {0}, received {1}", DataSourceType.SqlServer, source.Type)); + break; + + default: + throw new ArgumentException("Wrong scenario \"" + scenario + "\""); + } + + _source = (SqlServerDataSource)source; + + // Only try to add Multisubnet Failover host entries when the settings allow it in the source. + if (!_source.DisableMultiSubnetFailoverSetup) + { + _multiSubnetSetupHelper = new MultiSubnetFailoverSetup(_source); + _multiSubnetSetupHelper.InitializeFakeHostsForMultiSubnetFailover(); + } + } + + internal void Terminate() + { + if (_multiSubnetSetupHelper != null) + { + _multiSubnetSetupHelper.Terminate(); + } + } + + public sealed override string GetParameterName(string pName) + { + return "@" + pName; + } + + public override bool PrimaryKeyValueIsRequired + { + get { return false; } + } + + public override string CreateBaseConnectionString(Random rnd, ConnectionStringOptions options) + { + return CreateBaseConnectionStringBuilder(rnd, options).ToString(); + } + + private SqlConnectionStringBuilder CreateBaseConnectionStringBuilder( + Random rnd, ConnectionStringOptions options) + { + SqlConnectionStringBuilder builder = new SqlConnectionStringBuilder(); + builder.ApplicationName = "StressTests"; + + switch (_scenario) + { + case SqlClientScenario.Sql: + builder.DataSource = _source.DataSource; + builder.InitialCatalog = _source.Database; + break; + + default: + throw new InvalidOperationException("missing case for " + _scenario); + } + + // Randomize between Windows Authentication and SQL Authentication + // Note that having 2 options here doubles the number of connection pools + bool integratedSecurity = false; + if (_source.SupportsWindowsAuthentication) + { + if (string.IsNullOrEmpty(_source.User)) // if sql login is not provided + integratedSecurity = true; + else + integratedSecurity = (rnd != null) ? (rnd.Next(2) == 0) : true; + } + + if (integratedSecurity) + { + builder.IntegratedSecurity = true; + } + else if (_source.EntraIdUser.Length != 0) + { + builder.Authentication = SqlAuthenticationMethod.ActiveDirectoryPassword; + builder.UserID = _source.EntraIdUser; + builder.Password = _source.EntraIdPassword; + } + else + { + builder.UserID = _source.User; + builder.Password = _source.Password; + } + + if (CurrentPoolingStressMode == PoolingStressMode.RandomizeConnectionStrings && rnd != null) + { + // Randomize connection string + + // Randomize packetsize + // Note that having 2 options here doubles the number of connection pools + if (rnd.NextBool()) + { + builder.PacketSize = 8192; + } + else + { + builder.PacketSize = 512; + } + + // If test case allows randomization and doesn't disallow MultiSubnetFailover, then enable MultiSubnetFailover 20% of the time + // Note that having 2 options here doubles the number of connection pools + + if (!_source.DisableMultiSubnetFailoverSetup && + !options.HasFlag(ConnectionStringOptions.DisableMultiSubnetFailover) && + rnd != null && + rnd.Next(5) == 0) + { + string msfHostName; + if (integratedSecurity) + { + msfHostName = _multiSubnetSetupHelper.MultiSubnetFailoverHostNameForIntegratedSecurity; + } + else + { + msfHostName = _multiSubnetSetupHelper.GetMultiSubnetFailoverHostName(rnd); + } + string serverName; + + // replace with build which has host name with multiple IP addresses + builder = NetUtils.GetMultiSubnetFailoverConnectionString(builder.ConnectionString, msfHostName, out serverName); + } + + // Randomize between using Named Pipes and TCP providers + // Note that having 2 options here doubles the number of connection pools + if (rnd != null) + { + if (rnd.Next(2) == 0) + { + builder.DataSource = "tcp:" + builder.DataSource; + } + else if (!_source.DisableNamedPipes) + { + // Named Pipes + if (builder.DataSource.Equals("(local)")) + builder.DataSource = "np:" + builder.DataSource; + else + builder.DataSource = @"np:\\" + builder.DataSource.Split(',')[0] + @"\pipe\sql\query"; + } + } + + // Set MARS if it is requested by the test case + if (options.HasFlag(ConnectionStringOptions.EnableMars)) + { + builder.MultipleActiveResultSets = true; + } + + // Disable connection resiliency, which is on by default, 20% of the time. + if (rnd != null && rnd.NextBool(.2)) + { + builder.ConnectRetryCount = 0; + } + } + else + { + // Minimal randomization of connection string + + // Enable MARS for all scenarios + builder.MultipleActiveResultSets = true; + } + builder.Encrypt = _source.Encrypt; + + // TODO - read from config file and randomize this option with required SQL server setup. + builder.TrustServerCertificate = true; + + builder.MaxPoolSize = 1000; + return builder; + } + + protected override int GetNumDifferentApplicationNames() + { + // Return only 1 because the randomization in the base connection string above will give us more pools, so we don't need + // to also have many different application names. Getting connections from many different pools is not interesting to test + // because it reduces the amount of multithreadedness within each pool. + return 1; + } + + public override void CreateDatabase(DataSource source) + { + var database = (source as SqlServerDataSource).Database; + + Console.WriteLine($"Creating database [{database}]..."); + + var builder = CreateBaseConnectionStringBuilder( + null, ConnectionStringOptions.DisableMultiSubnetFailover); + builder.InitialCatalog = "master"; + + using SqlConnection connection = new(builder.ToString()); + connection.Open(); + + using SqlCommand command = connection.CreateCommand(); + command.CommandText = $"create database [{database}]"; + command.ExecuteNonQuery(); + + Console.WriteLine($"Created database [{database}]"); + } + + public override void DropDatabase(DataSource source) + { + var database = (source as SqlServerDataSource).Database; + + Console.WriteLine($"Dropping database [{database}]..."); + + var builder = CreateBaseConnectionStringBuilder( + null, ConnectionStringOptions.DisableMultiSubnetFailover); + builder.InitialCatalog = "master"; + + using SqlConnection connection = new(builder.ToString()); + connection.Open(); + + // Kill all connections currently using the database so we can drop + // it. + { + using SqlCommand command = connection.CreateCommand(); + command.CommandText = + $"select session_id from sys.dm_exec_sessions where database_id = DB_ID('{database}')"; + using var reader = command.ExecuteReader(); + while (reader.Read()) + { + var sessionId = reader.GetInt16(0); + using var killCommand = connection.CreateCommand(); + killCommand.CommandText = $"kill {sessionId}"; + Console.WriteLine($" Killing session {sessionId}..."); + killCommand.ExecuteNonQuery(); + Console.WriteLine($" Killed session {sessionId}"); + } + } + + // Drop the database. + { + using SqlCommand command = connection.CreateCommand(); + command.CommandText = $"drop database [{database}]"; + command.ExecuteNonQuery(); + } + + Console.WriteLine($"Dropped database [{database}]"); + } + } +} diff --git a/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Tests/SqlClientTestGroup.cs b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Tests/SqlClientTestGroup.cs new file mode 100644 index 0000000000..e48c13d49b --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Tests/SqlClientTestGroup.cs @@ -0,0 +1,620 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Threading; +using System.Threading.Tasks; +using System.Diagnostics; +using System.Data; +using Microsoft.Data.SqlClient; +using System.Xml; + +using DPStressHarness; +using System.IO; + +namespace Stress.Data.SqlClient +{ + public class SqlClientTestGroup : DataTestGroup + { + /// + /// SqlNotificationRequest options string + /// + private static string s_notificationOptions; + + /// + /// Connection string for SqlDependency.Start()/Stop() + /// + /// The connection string used for SqlDependency.Start() must always be exactly the same every time + /// if you are connecting to the same database with the same user and same application domain, so + /// don't randomise the connection string for calling SqlDependency.Start() + /// + private static string s_sqlDependencyConnString; + + /// + /// A thread which randomly calls SqlConnection.ClearAllPools. + /// This significantly increases the probability of hitting some bugs, such as: + /// vstfdevdiv 674236 (SqlConnection.Open() throws InvalidOperationException for absolutely valid connection request) + /// sqlbuvsts 328845 (InvalidOperationException: The requested operation cannot be completed because the connection has been broken.) (this is LSE QFE) + /// However, calling ClearAllPools all the time might also significantly decrease the probability of hitting some other bug, + /// so this thread will alternate between hammering on ClearAllPools for several minutes, and then doing nothing for several minutes. + /// + private static Thread s_clearAllPoolsThread; + + /// + /// Call .Set() on this to cleanly stop the ClearAllPoolsThread. + /// + private static ManualResetEvent s_clearAllPoolsThreadStop = new ManualResetEvent(false); + + private static void ClearAllPoolsThreadFunc() + { + Random rnd = new TrackedRandom((int)Environment.TickCount); + + // Swap between calling ClearAllPools and doing nothing every 5 minutes. + TimeSpan halfCycleTime = TimeSpan.FromMinutes(5); + + int minWait = 10; // milliseconds + int maxWait = 1000; // milliseconds + + bool active = true; // Start active so we can hit vstfdevdiv 674236 asap + Stopwatch stopwatch = Stopwatch.StartNew(); + while (!s_clearAllPoolsThreadStop.WaitOne(rnd.Next(minWait, maxWait))) + { + if (stopwatch.Elapsed > halfCycleTime) + { + active = !active; + stopwatch.Reset(); + stopwatch.Start(); + } + + if (active) + { + SqlConnection.ClearAllPools(); + } + } + } + + public override void GlobalTestSetup() + { + Console.WriteLine("SqlClientTestGroup.GlobalTestSetup(): Starting..."); + + base.GlobalTestSetup(); + + s_clearAllPoolsThread = new Thread(ClearAllPoolsThreadFunc); + s_clearAllPoolsThread.Start(); + + // set the notification options for SqlNotificationRequest tests + var source = Source as SqlServerDataSource; + s_notificationOptions = "service=StressNotifications;local database=" + source.Database; + + s_sqlDependencyConnString = Factory.CreateBaseConnectionString( + null, DataStressFactory.ConnectionStringOptions.DisableMultiSubnetFailover); + + Console.WriteLine("SqlClientTestGroup.GlobalTestSetup(): Finished"); + } + + public override void GlobalTestCleanup() + { + Console.WriteLine("SqlClientTestGroup.GlobalTestCleanup(): Starting..."); + + s_clearAllPoolsThreadStop.Set(); + s_clearAllPoolsThread.Join(); + + SqlClientStressFactory factory = Factory as SqlClientStressFactory; + if (factory != null) + { + factory.Terminate(); + } + + base.GlobalTestCleanup(); + + Console.WriteLine("SqlClientTestGroup.GlobalTestCleanup(): Finished"); + } + + public override void GlobalExceptionHandler(Exception e) + { + base.GlobalExceptionHandler(e); + } + + protected override DataStressFactory CreateFactory(ref string scenario, ref DataSource source) + { + SqlClientStressFactory factory = new SqlClientStressFactory(); + factory.Initialize(ref scenario, ref source); + return factory; + } + + protected override bool IsCommandCancelledException(Exception e) + { + return + base.IsCommandCancelledException(e) || + ((e is SqlException || e is InvalidOperationException) && e.Message.ToLower().Contains("operation cancelled")) || + (e is SqlException && e.Message.StartsWith("A severe error occurred on the current command.")) || + (e is AggregateException && e.InnerException != null && IsCommandCancelledException(e.InnerException)) || + (e is System.Reflection.TargetInvocationException && e.InnerException != null && IsCommandCancelledException(e.InnerException)); + } + + protected override bool IsReaderClosedException(Exception e) + { + return + e is TaskCanceledException + || + ( + e is InvalidOperationException + && + ( + (e.Message.StartsWith("Invalid attempt to call") && e.Message.EndsWith("when reader is closed.")) + || + e.Message.Equals("Invalid attempt to read when no data is present.") + || + e.Message.Equals("Invalid operation. The connection is closed.") + ) + ) + || + ( + e is ObjectDisposedException + && + ( + e.Message.Equals("Cannot access a disposed object.\r\nObject name: 'SqlSequentialStream'.") + || + e.Message.Equals("Cannot access a disposed object.\r\nObject name: 'SqlSequentialTextReader'.") + ) + ); + } + + protected override bool AllowReaderCloseDuringReadAsync() + { + return true; + } + + /// + /// Utility function used by async tests + /// + /// SqlCommand to be executed. + /// Indicates if data is being queried + /// Indicates if the query should be executed as an Xml + /// + /// The Cancellation Token Source + /// The result of beginning of Async execution. + private IAsyncResult SqlCommandBeginExecute(SqlCommand com, bool query, bool xml, bool useBeginAPI, CancellationTokenSource cts = null) + { + DataStressErrors.Assert(!(useBeginAPI && cts != null), "Cannot use begin api with CancellationTokenSource"); + + CancellationToken token = (cts != null) ? cts.Token : CancellationToken.None; + + if (xml) + { + com.CommandText = com.CommandText + " FOR XML AUTO"; + return useBeginAPI ? null : com.ExecuteXmlReaderAsync(token); + } + else if (query) + { + return useBeginAPI ? null : com.ExecuteReaderAsync(token); + } + else + { + return useBeginAPI ? null : com.ExecuteNonQueryAsync(token); + } + } + + /// + /// Utility function used by async tests + /// + /// Used to randomize reader.Read() call, whether it should continue or break, and is passed down to ConsumeReaderAsync + /// The Async result from Begin operation. + /// The Sql Command to Execute + /// Indicates if data is being queried and where ExecuteQuery or Non-query to be used with the reader + /// Indicates if the query should be executed as an Xml + /// Indicates if command was cancelled and is used to throw exception if a Command cancellation related exception is encountered + /// The Cancellation Token Source + private void SqlCommandEndExecute(Random rnd, IAsyncResult result, SqlCommand com, bool query, bool xml, bool cancelled, CancellationTokenSource cts = null) + { + try + { + bool closeReader = ShouldCloseDataReader(); + if (xml) + { + XmlReader reader = null; + if (result != null && result is Task) + { + reader = AsyncUtils.GetResult(result); + } + else + { + reader = AsyncUtils.ExecuteXmlReader(com); + } + + while (reader.Read()) + { + if (rnd.Next(10) == 0) break; + if (rnd.Next(2) == 0) continue; + reader.ReadElementContentAsString(); + } + if (closeReader) reader.Dispose(); + } + else if (query) + { + DataStressReader reader = null; + if (result != null && result is Task) + { + reader = new DataStressReader(AsyncUtils.GetResult(result)); + } + else + { + reader = new DataStressReader(AsyncUtils.ExecuteReader(com)); + } + + CancellationToken token = (cts != null) ? cts.Token : CancellationToken.None; + + AsyncUtils.WaitAndUnwrapException(ConsumeReaderAsync(reader, false, token, rnd)); + + if (closeReader) reader.Close(); + } + else + { + if (result != null && result is Task) + { + int temp = AsyncUtils.GetResult(result); + } + else + { + AsyncUtils.ExecuteNonQuery(com); + } + } + } + catch (Exception e) + { + if (cancelled && IsCommandCancelledException(e)) + { + // expected exception, ignore + } + else + { + throw; + } + } + } + + + /// + /// Utility function for tests + /// + /// + /// + /// + /// + /// + private void TestSqlAsync(Random rnd, bool read, bool poll, bool handle, bool xml) + { + using (DataStressConnection conn = Factory.CreateConnection(rnd)) + { + if (!OpenConnection(conn)) return; + DataStressFactory.TableMetadata table = Factory.GetRandomTable(rnd); + SqlCommand com = (SqlCommand)Factory.GetCommand(rnd, table, conn, read, xml); + bool useBeginAPI = rnd.NextBool(); + + IAsyncResult result = SqlCommandBeginExecute(com, read, xml, useBeginAPI); + // Cancel 1/10 commands + bool cancel = (rnd.Next(10) == 0); + if (cancel) + { + if (com.Connection.State != ConnectionState.Closed) com.Cancel(); + } + + if (result != null) + WaitForAsyncOpToComplete(rnd, result, poll, handle); + // At random end query or forget it + if (rnd.Next(2) == 0) + SqlCommandEndExecute(rnd, result, com, read, xml, cancel); + + // Randomly wait for the command to complete after closing the connection to verify devdiv bug 200550. + // This was fixed for .NET 4.5 Task-based API, but not for the older Begin/End IAsyncResult API. + conn.Close(); + if (!useBeginAPI && rnd.NextBool()) + result.AsyncWaitHandle.WaitOne(); + } + } + + private void WaitForAsyncOpToComplete(Random rnd, IAsyncResult result, bool poll, bool handle) + { + if (poll) + { + long ret = 0; + bool wait = !result.IsCompleted; + while (wait) + { + wait = !result.IsCompleted; + Thread.Sleep(100); + if (ret++ > 300) //30 second max wait time then exit + wait = false; + } + } + else if (handle) + { + WaitHandle wait = result.AsyncWaitHandle; + wait.WaitOne(rnd.Next(1000)); + } + } + + /// + /// SqlClient Async Non-blocking Read Test + /// + [StressTest("TestSqlAsyncNonBlockingRead", Weight = 10)] + public void TestSqlAsyncNonBlockingRead() + { + Random rnd = RandomInstance; + TestSqlAsync(rnd, read: true, poll: false, handle: false, xml: false); + } + + /// + /// SqlClient Async Non-blocking Write Test + /// + [StressTest("TestSqlAsyncNonBlockingWrite", Weight = 10)] + public void TestSqlAsyncNonBlockingWrite() + { + Random rnd = RandomInstance; + TestSqlAsync(rnd, read: false, poll: false, handle: false, xml: false); + } + + /// + /// SqlClient Async Polling Read Test + /// + [StressTest("TestSqlAsyncPollingRead", Weight = 10)] + public void TestSqlAsyncPollingRead() + { + Random rnd = RandomInstance; + TestSqlAsync(rnd, read: true, poll: true, handle: false, xml: false); + } + + /// + /// SqlClient Async Polling Write Test + /// + [StressTest("TestSqlAsyncPollingWrite", Weight = 10)] + public void TestSqlAsyncPollingWrite() + { + Random rnd = RandomInstance; + TestSqlAsync(rnd, read: false, poll: true, handle: false, xml: false); + } + + /// + /// SqlClient Async Event Read Test + /// + [StressTest("TestSqlAsyncEventRead", Weight = 10)] + public void TestSqlAsyncEventRead() + { + Random rnd = RandomInstance; + TestSqlAsync(rnd, read: true, poll: false, handle: true, xml: false); + } + + /// + /// SqlClient Async Event Write Test + /// + [StressTest("TestSqlAsyncEventWrite", Weight = 10)] + public void TestSqlAsyncEventWrite() + { + Random rnd = RandomInstance; + TestSqlAsync(rnd, read: false, poll: false, handle: true, xml: false); + } + + + /// + /// SqlClient Async Xml Non-blocking Read Test + /// + [StressTest("TestSqlXmlAsyncNonBlockingRead", Weight = 10)] + public void TestSqlXmlAsyncNonBlockingRead() + { + Random rnd = RandomInstance; + TestSqlAsync(rnd, read: true, poll: false, handle: false, xml: true); + } + + /// + /// SqlClient Async Xml Polling Read Test + /// + [StressTest("TestSqlXmlAsyncPollingRead", Weight = 10)] + public void TestSqlXmlAsyncPollingRead() + { + Random rnd = RandomInstance; + TestSqlAsync(rnd, read: true, poll: true, handle: false, xml: true); + } + + /// + /// SqlClient Async Xml Event Read Test + /// + [StressTest("TestSqlXmlAsyncEventRead", Weight = 10)] + public void TestSqlXmlAsyncEventRead() + { + Random rnd = RandomInstance; + TestSqlAsync(rnd, read: true, poll: false, handle: true, xml: true); + } + + + [StressTest("TestSqlXmlCommandReader", Weight = 10)] + public void TestSqlXmlCommandReader() + { + Random rnd = RandomInstance; + + using (DataStressConnection conn = Factory.CreateConnection(rnd)) + { + if (!OpenConnection(conn)) return; + DataStressFactory.TableMetadata table = Factory.GetRandomTable(rnd); + SqlCommand com = (SqlCommand)Factory.GetCommand(rnd, table, conn, query: true, isXml: true); + com.CommandText = com.CommandText + " FOR XML AUTO"; + + // Cancel 1/10 commands + bool cancel = rnd.Next(10) == 0; + if (cancel) + { + ThreadPool.QueueUserWorkItem(new WaitCallback(CommandCancel), com); + } + + try + { + XmlReader reader = com.ExecuteXmlReader(); + + while (reader.Read()) + { + if (rnd.Next(10) == 0) break; + if (rnd.Next(2) == 0) continue; + reader.ReadElementContentAsString(); + } + if (rnd.Next(10) != 0) reader.Dispose(); + } + catch (Exception ex) + { + if (cancel && IsCommandCancelledException(ex)) + { + // expected, ignore + } + else + { + throw; + } + } + } + } + + + /// + /// Utility function used for testing cancellation on Execute*Async APIs. + /// + private void TestSqlAsyncCancellation(Random rnd, bool read, bool xml) + { + using (DataStressConnection conn = Factory.CreateConnection(rnd)) + { + if (!OpenConnection(conn)) return; + DataStressFactory.TableMetadata table = Factory.GetRandomTable(rnd); + SqlCommand com = (SqlCommand)Factory.GetCommand(rnd, table, conn, read, xml); + + CancellationTokenSource cts = new CancellationTokenSource(); + Task t = (Task)SqlCommandBeginExecute(com, read, xml, false, cts); + + cts.CancelAfter(rnd.Next(2000)); + SqlCommandEndExecute(rnd, (IAsyncResult)t, com, read, xml, true, cts); + } + } + + /// + /// SqlClient Async Xml Event Read Test + /// + [StressTest("TestExecuteXmlReaderAsyncCancellation", Weight = 10)] + public void TestExecuteXmlReaderAsyncCancellation() + { + Random rnd = RandomInstance; + TestSqlAsyncCancellation(rnd, true, true); + } + + /// + /// SqlClient Async Xml Event Read Test + /// + [StressTest("TestExecuteReaderAsyncCancellation", Weight = 10)] + public void TestExecuteReaderAsyncCancellation() + { + Random rnd = RandomInstance; + TestSqlAsyncCancellation(rnd, true, false); + } + + /// + /// SqlClient Async Xml Event Read Test + /// + [StressTest("TestExecuteNonQueryAsyncCancellation", Weight = 10)] + public void TestExecuteNonQueryAsyncCancellation() + { + Random rnd = RandomInstance; + TestSqlAsyncCancellation(rnd, false, false); + } + + + private class MARSCommand + { + internal SqlCommand cmd; + internal IAsyncResult result; + internal bool query; + internal bool xml; + } + + [StressTest("TestSqlAsyncMARS", Weight = 10)] + public void TestSqlAsyncMARS() + { + const int MaxCmds = 11; + Random rnd = RandomInstance; + + using (DataStressConnection conn = Factory.CreateConnection(rnd, DataStressFactory.ConnectionStringOptions.EnableMars)) + { + if (!OpenConnection(conn)) return; + DataStressFactory.TableMetadata table = Factory.GetRandomTable(rnd); + + // MARS session cache is by default 10. + // This is documented here: https://docs.microsoft.com/en-us/dotnet/framework/data/adonet/sql/enabling-multiple-active-result-sets + // We want to stress test this by allowing 11 concurrent commands. Hence the max in rnd.Next below is 12. + MARSCommand[] cmds = new MARSCommand[rnd.Next(5, MaxCmds + 1)]; + + for (int i = 0; i < cmds.Length; i++) + { + cmds[i] = new MARSCommand(); + + // Make every 3rd query xml reader + if (i % 3 == 0) + { + cmds[i].query = true; + cmds[i].xml = true; + } + else + { + cmds[i].query = rnd.NextBool(); + cmds[i].xml = false; + } + + cmds[i].cmd = (SqlCommand)Factory.GetCommand(rnd, table, conn, cmds[i].query, cmds[i].xml); + cmds[i].result = SqlCommandBeginExecute(cmds[i].cmd, cmds[i].query, cmds[i].xml, rnd.NextBool()); + if (cmds[i].result != null) + WaitForAsyncOpToComplete(rnd, cmds[i].result, true, false); + } + + // After all commands have been launched, wait for them to complete now. + for (int i = 0; i < cmds.Length; i++) + { + SqlCommandEndExecute(rnd, cmds[i].result, cmds[i].cmd, cmds[i].query, cmds[i].xml, false); + } + } + } + + + [StressTest("TestStreamInputParameter", Weight = 10)] + public void TestStreamInputParameter() + { + Random rnd = RandomInstance; + int dataSize = 100000; + byte[] data = new byte[dataSize]; + rnd.NextBytes(data); + + using (DataStressConnection conn = Factory.CreateConnection(rnd)) + { + if (!OpenConnection(conn)) return; + SqlCommand cmd = (SqlCommand)conn.CreateCommand(); + cmd.CommandText = "SELECT @blob"; + SqlParameter param = cmd.Parameters.Add("@blob", SqlDbType.VarBinary, dataSize); + param.Direction = ParameterDirection.Input; + param.Value = new MemoryStream(data); + CommandExecute(rnd, cmd, true); + } + } + + [StressTest("TestTextReaderInputParameter", Weight = 10)] + public void TestTextReaderInputParameter() + { + Random rnd = RandomInstance; + int dataSize = 100000; + string data = new string('a', dataSize); + + using (DataStressConnection conn = Factory.CreateConnection(rnd)) + { + if (!OpenConnection(conn)) return; + SqlCommand cmd = (SqlCommand)conn.CreateCommand(); + cmd.CommandText = "SELECT @blob"; + SqlParameter param = cmd.Parameters.Add("@blob", SqlDbType.VarChar, dataSize); + param.Direction = ParameterDirection.Input; + param.Value = new StringReader(data); + CommandExecute(rnd, cmd, true); + } + } + } +} diff --git a/src/Microsoft.Data.SqlClient/tests/StressTests/StressTests.slnx b/src/Microsoft.Data.SqlClient/tests/StressTests/StressTests.slnx new file mode 100644 index 0000000000..a0dfd5e503 --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/StressTests/StressTests.slnx @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/Microsoft.Data.SqlClient/tests/UnitTests/ADPHelper.cs b/src/Microsoft.Data.SqlClient/tests/UnitTests/ADPHelper.cs new file mode 100644 index 0000000000..d78c36f785 --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/UnitTests/ADPHelper.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Microsoft.Data.Common +{ + internal class ADPHelper : IDisposable + { + List _originalAzureSqlServerEndpoints; + + internal ADPHelper() + { + _originalAzureSqlServerEndpoints = [.. ADP.s_azureSqlServerEndpoints]; + } + + internal void AddAzureSqlServerEndpoint(string endpoint) + { + ADP.s_azureSqlServerEndpoints.Add(endpoint); + } + + public void Dispose() + { + ADP.s_azureSqlServerEndpoints.Clear(); + ADP.s_azureSqlServerEndpoints.AddRange(_originalAzureSqlServerEndpoints); + } + } +} diff --git a/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft.Data.SqlClient.UnitTests.csproj b/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft.Data.SqlClient.UnitTests.csproj index 2f0e12c922..f800fe8c06 100644 --- a/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft.Data.SqlClient.UnitTests.csproj +++ b/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft.Data.SqlClient.UnitTests.csproj @@ -10,6 +10,7 @@ + runtime; build; native; contentfiles; analyzers; buildtransitive @@ -25,6 +26,11 @@ all + + + + + diff --git a/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/SqlClient/SqlConnectionStringTest.cs b/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/SqlClient/SqlConnectionStringTest.cs new file mode 100644 index 0000000000..52bc386875 --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/SqlClient/SqlConnectionStringTest.cs @@ -0,0 +1,71 @@ +using System; +using Microsoft.Data.SqlClient.Tests.Common; +using Xunit; +using static Microsoft.Data.SqlClient.Tests.Common.LocalAppContextSwitchesHelper; + +namespace Microsoft.Data.SqlClient.UnitTests.Microsoft.Data.SqlClient +{ + // TODO: We need to keep this in the same collection as SimulatedServerTests because of the AppContext switch manipulation. + // Make AppContext switches testable in isolation and remove this constraint so that these tests can run in parallel with others. + [Collection("SimulatedServerTests")] + public class SqlConnectionStringTest : IDisposable + { + private LocalAppContextSwitchesHelper _appContextSwitchHelper; + public SqlConnectionStringTest() + { + // Ensure that the app context switch is set to the default value + _appContextSwitchHelper = new LocalAppContextSwitchesHelper(); + } + +#if NETFRAMEWORK + [Theory] + [InlineData("test.database.windows.net", true, Tristate.True, true)] + [InlineData("test.database.windows.net", false, Tristate.True, false)] + [InlineData("test.database.windows.net", null, Tristate.True, false)] + [InlineData("test.database.windows.net", true, Tristate.False, true)] + [InlineData("test.database.windows.net", false, Tristate.False, false)] + [InlineData("test.database.windows.net", null, Tristate.False, true)] + [InlineData("test.database.windows.net", true, Tristate.NotInitialized, true)] + [InlineData("test.database.windows.net", false, Tristate.NotInitialized, false)] + [InlineData("test.database.windows.net", null, Tristate.NotInitialized, true)] + [InlineData("my.test.server", true, Tristate.True, true)] + [InlineData("my.test.server", false, Tristate.True, false)] + [InlineData("my.test.server", null, Tristate.True, false)] + [InlineData("my.test.server", true, Tristate.False, true)] + [InlineData("my.test.server", false, Tristate.False, false)] + [InlineData("my.test.server", null, Tristate.False, true)] + [InlineData("my.test.server", true, Tristate.NotInitialized, true)] + [InlineData("my.test.server", false, Tristate.NotInitialized, false)] + [InlineData("my.test.server", null, Tristate.NotInitialized, true)] + public void TestDefaultTnir(string dataSource, bool? tnirEnabledInConnString, Tristate tnirDisabledAppContext, bool expectedValue) + { + // Note: TNIR is only supported on .NET Framework. + // Note: TNIR is disabled by default for Azure SQL Database servers (i.e. *.database.windows.net) + // and when using federated auth unless explicitly set in the connection string. + // However, this evaluation only happens at login time so TNIR behavior may not match + // the value of TransparentNetworkIPResolution property in SqlConnectionString. + + // Arrange + _appContextSwitchHelper.DisableTnirByDefaultField = tnirDisabledAppContext; + + // Act + SqlConnectionStringBuilder builder = new(); + builder.DataSource = dataSource; + if (tnirEnabledInConnString.HasValue) + { + builder.TransparentNetworkIPResolution = tnirEnabledInConnString.Value; + } + SqlConnectionString connectionString = new(builder.ConnectionString); + + // Assert + Assert.Equal(expectedValue, connectionString.TransparentNetworkIPResolution); + } +#endif + + public void Dispose() + { + // Clean up any resources if necessary + _appContextSwitchHelper.Dispose(); + } + } +} diff --git a/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/SqlClient/TickCountElapsedTest.cs b/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/SqlClient/TickCountElapsedTest.cs new file mode 100644 index 0000000000..38a555d356 --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/SqlClient/TickCountElapsedTest.cs @@ -0,0 +1,56 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Microsoft.Data.Common; +using Xunit; + +namespace Microsoft.Data.SqlClient.UnitTests; + +/// +/// Tests for Environment.TickCount elapsed time calculation with wraparound handling. +/// +public sealed class TickCountElapsedTest +{ + /// + /// Verifies that normal elapsed time calculation works correctly. + /// + [Fact] + public void CalculateTickCountElapsed_NormalCase_ReturnsCorrectElapsed() + { + uint elapsed = ADP.CalculateTickCountElapsed(1000, 1500); + Assert.Equal(500u, elapsed); + } + + /// + /// Verifies that wraparound from int.MaxValue to int.MinValue is handled correctly. + /// + [Fact] + public void CalculateTickCountElapsed_MaxWraparound_ReturnsOne() + { + uint elapsed = ADP.CalculateTickCountElapsed(int.MaxValue, int.MinValue); + Assert.Equal(1u, elapsed); + } + + /// + /// Verifies that partial wraparound scenarios work correctly. + /// + [Theory] + [InlineData(2147483600, -2147483600, 96u)] + [InlineData(2147483647, -2147483647, 2u)] + public void CalculateTickCountElapsed_PartialWraparound_ReturnsCorrectElapsed(long start, long end, uint expected) + { + uint elapsed = ADP.CalculateTickCountElapsed(start, end); + Assert.Equal(expected, elapsed); + } + + /// + /// Verifies that zero elapsed time returns zero. + /// + [Fact] + public void CalculateTickCountElapsed_ZeroElapsed_ReturnsZero() + { + uint elapsed = ADP.CalculateTickCountElapsed(1000, 1000); + Assert.Equal(0u, elapsed); + } +} diff --git a/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/SqlTypes/SqlTypeWorkaroundsTests.cs b/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/SqlTypes/SqlTypeWorkaroundsTests.cs new file mode 100644 index 0000000000..ae7b1d9901 --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/SqlTypes/SqlTypeWorkaroundsTests.cs @@ -0,0 +1,207 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Data.SqlTypes; +using Microsoft.Data.SqlTypes; +using Xunit; + +#nullable enable + +namespace Microsoft.Data.SqlClient.UnitTests +{ + public class SqlTypeWorkaroundsTests + { + // @TODO: Need a facade pattern for Type getting so we can test the case where reflection fails + + #if NETFRAMEWORK + + #region SqlBinary + + public static TheoryData ByteBinaryCtor_NonNullInput_Data => + new TheoryData + { + Array.Empty(), + new byte[] { 1, 2, 3, 4}, + }; + + [Theory] + [MemberData(nameof(ByteBinaryCtor_NonNullInput_Data))] + public void SqlBinaryCtor_NonNullInput(byte[] input) + { + // Act + SqlBinary result = SqlTypeWorkarounds.SqlBinaryCtor(input, true); + + // Assert + Assert.False(result.IsNull); + Assert.Equal(input, result.Value); + } + + [Fact] + public void SqlBinaryCtor_NullInput() + { + // Act + SqlBinary result = SqlTypeWorkarounds.SqlBinaryCtor(null, true); + + // Assert + Assert.True(result.IsNull); + } + + #endregion + + #region SqlDecimal + + public static TheoryData SqlDecimalExtractData_NonNullInput_Data => + new TheoryData + { + SqlDecimal.MinValue, + new SqlDecimal(-1.2345678), + new SqlDecimal(0), + new SqlDecimal(1.2345678), + SqlDecimal.MaxValue, + }; + + [Theory] + [MemberData(nameof(SqlDecimalExtractData_NonNullInput_Data))] + public void SqlDecimalExtractData_NonNullInput(SqlDecimal input) + { + // Act + SqlTypeWorkarounds.SqlDecimalExtractData( + input, + out uint data1, + out uint data2, + out uint data3, + out uint data4); + + // Assert + int[] expected = input.Data; + Assert.Equal(expected[0], (int)data1); + Assert.Equal(expected[1], (int)data2); + Assert.Equal(expected[2], (int)data3); + Assert.Equal(expected[3], (int)data4); + } + + [Fact] + public void SqlDecimalExtractData_NullInput() + { + Action action = () => + { + // Arrange + SqlDecimal input = SqlDecimal.Null; + + // Act + SqlTypeWorkarounds.SqlDecimalExtractData(input, out _, out _, out _, out _); + }; + + // Assert + Assert.Throws(action); + } + + #endregion + + #region SqlGuid + + public static TheoryData SqlGuidCtor_InvalidInput_Data => + new TheoryData + { + null, + Array.Empty(), + new byte[] { 1, 2, 3, 4 }, // Too short + new byte[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17 } // Too long + }; + + [Theory] + [MemberData(nameof(SqlGuidCtor_InvalidInput_Data))] + public void SqlGuidCtor_InvalidInput(byte[]? input) + { + // Act + Action action = () => SqlTypeWorkarounds.SqlGuidCtor(input, true); + + // Assert + Assert.Throws(action); + } + + public static TheoryData SqlGuidCtor_ValidInput_Data => + new TheoryData + { + new byte[] { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, + new byte[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16 } + }; + + [Theory] + [MemberData(nameof(SqlGuidCtor_ValidInput_Data))] + public void SqlGuidCtor_ValidInput(byte[] input) + { + // Act + SqlGuid result = SqlTypeWorkarounds.SqlGuidCtor(input, true); + + // Assert + Assert.False(result.IsNull); + Assert.Equal(input, result.Value.ToByteArray()); + } + + #endregion + + #region SqlMoney + + public static TheoryData SqlMoneyCtor_Data => + new TheoryData + { + { long.MinValue, SqlMoney.MinValue }, + { (long)((decimal)-123000000 / 10000), new SqlMoney(-1.23) }, + { 0, SqlMoney.Zero }, + { (long)((decimal)123000000 / 10000), new SqlMoney(1.23) }, + { long.MaxValue, SqlMoney.MaxValue }, + }; + + [Theory] + [MemberData(nameof(SqlMoneyCtor_Data))] + public void SqlMoneyCtor(long input, SqlMoney expected) + { + // Act + SqlMoney result = SqlTypeWorkarounds.SqlMoneyCtor(input, 1); + + // Assert + Assert.Equal(expected, result); + } + + public static TheoryData SqlMoneyToSqlInternalRepresentation_NonNullInput_Data => + new TheoryData + { + { SqlMoney.MinValue, long.MinValue }, + { new SqlMoney(-1.23), (long)(new SqlMoney(-1.23).ToDecimal() * 10000) }, + { SqlMoney.Zero, 0 }, + { new SqlMoney(1.23), (long)(new SqlMoney(1.23).ToDecimal() * 10000) }, + { SqlMoney.MaxValue, long.MaxValue }, + }; + + [Theory] + [MemberData(nameof(SqlMoneyToSqlInternalRepresentation_NonNullInput_Data))] + public void SqlMoneyToSqlInternalRepresentation_NonNullInput(SqlMoney input, long expected) + { + // Act + long result = SqlTypeWorkarounds.SqlMoneyToSqlInternalRepresentation(input); + + // Assert + Assert.Equal(expected, result); + } + + [Fact] + public void SqlMoneyToSqlInternalRepresentation_NullInput() + { + // Arrange + SqlMoney input = SqlMoney.Null; + + // Act + Action action = () => SqlTypeWorkarounds.SqlMoneyToSqlInternalRepresentation(input); + + // Assert + Assert.Throws(action); + } + + #endregion + + #endif + } +} diff --git a/src/Microsoft.Data.SqlClient/tests/UnitTests/SimulatedServerTests/ConnectionFailoverTests.cs b/src/Microsoft.Data.SqlClient/tests/UnitTests/SimulatedServerTests/ConnectionFailoverTests.cs new file mode 100644 index 0000000000..a05efcf879 --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/UnitTests/SimulatedServerTests/ConnectionFailoverTests.cs @@ -0,0 +1,596 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Data; +using Microsoft.Data.SqlClient; +using Microsoft.Data.SqlClient.Tests.Common; +using Microsoft.SqlServer.TDS.Servers; +using Xunit; + +namespace Microsoft.Data.SqlClient.UnitTests.SimulatedServerTests +{ + [Collection("SimulatedServerTests")] + [Trait("Category", "flaky")] + public class ConnectionFailoverTests + { + //TODO parameterize for transient errors + [Theory] + [InlineData(40613)] + [InlineData(42108)] + [InlineData(42109)] + public void TransientFault_NoFailover_DoesNotClearPool(uint errorCode) + { + // When connecting to a server with a configured failover partner, + // transient errors returned during the login ack should not clear the connection pool. + + // Arrange + using TdsServer failoverServer = new(new TdsServerArguments + { + // Doesn't need to point to a real endpoint, just needs a value specified + FailoverPartner = "localhost,1234" + }); + failoverServer.Start(); + var failoverDataSource = $"localhost,{failoverServer.EndPoint.Port}"; + + // Errors are off to start to allow the pool to warm up + using TransientTdsErrorTdsServer initialServer = new(new TransientTdsErrorTdsServerArguments + { + FailoverPartner = failoverDataSource + }); + initialServer.Start(); + + SqlConnectionStringBuilder builder = new() + { + DataSource = "localhost," + initialServer.EndPoint.Port, + ConnectRetryInterval = 1, + ConnectTimeout = 30, + Encrypt = SqlConnectionEncryptOption.Optional, + InitialCatalog = "test" + }; + + using SqlConnection connection = new(builder.ConnectionString); + connection.Open(); + + // Act + initialServer.SetErrorBehavior(true, errorCode); + using SqlConnection secondConnection = new(builder.ConnectionString); + // Should not trigger a failover, will retry against the same server + secondConnection.Open(); + + // Request a new connection, should initiate a fresh connection attempt if the pool was cleared. + connection.Close(); + connection.Open(); + + // Assert + Assert.Equal(ConnectionState.Open, connection.State); + Assert.Equal(ConnectionState.Open, secondConnection.State); + Assert.Equal($"localhost,{initialServer.EndPoint.Port}", connection.DataSource); + Assert.Equal($"localhost,{initialServer.EndPoint.Port}", secondConnection.DataSource); + + // 1 for the initial connection, 2 for the second connection + Assert.Equal(3, initialServer.PreLoginCount); + // A failover should not be triggered, so prelogin count to the failover server should be 0 + Assert.Equal(0, failoverServer.PreLoginCount); + } + + [Fact] + public void NetworkError_TriggersFailover_ClearsPool() + { + // When connecting to a server with a configured failover partner, + // network errors returned during prelogin should clear the connection pool. + + // Arrange + using TdsServer failoverServer = new(new TdsServerArguments + { + // Doesn't need to point to a real endpoint, just needs a value specified + FailoverPartner = "localhost,1234" + }); + failoverServer.Start(); + var failoverDataSource = $"localhost,{failoverServer.EndPoint.Port}"; + + // Errors are off to start to allow the pool to warm up + using TdsServer initialServer = new(new TdsServerArguments + { + FailoverPartner = failoverDataSource + }); + initialServer.Start(); + + SqlConnectionStringBuilder builder = new() + { + DataSource = "localhost," + initialServer.EndPoint.Port, + ConnectRetryInterval = 1, + ConnectTimeout = 30, + Encrypt = SqlConnectionEncryptOption.Optional, + InitialCatalog = "test", + MultiSubnetFailover = false, +#if NETFRAMEWORK + TransparentNetworkIPResolution = false, +#endif + }; + + // Open the initial connection to warm up the pool and populate failover partner information + // for the pool group. + using SqlConnection connection = new(builder.ConnectionString); + connection.Open(); + Assert.Equal(ConnectionState.Open, connection.State); + Assert.Equal($"localhost,{initialServer.EndPoint.Port}", connection.DataSource); + Assert.Equal(1, initialServer.PreLoginCount); + Assert.Equal(0, failoverServer.PreLoginCount); + + // Act + // Should trigger a failover because the initial server is unavailable + initialServer.Dispose(); + using SqlConnection secondConnection = new(builder.ConnectionString); + secondConnection.Open(); + + // Assert + Assert.Equal(ConnectionState.Open, secondConnection.State); + Assert.Equal($"localhost,{failoverServer.EndPoint.Port}", secondConnection.DataSource); + Assert.Equal(1, initialServer.PreLoginCount); + Assert.Equal(1, failoverServer.PreLoginCount); + + + // Act + // Request a new connection, should initiate a fresh connection attempt if the pool was cleared. + connection.Close(); + connection.Open(); + + // Assert + Assert.Equal(ConnectionState.Open, connection.State); + Assert.Equal($"localhost,{failoverServer.EndPoint.Port}", connection.DataSource); + Assert.Equal(1, initialServer.PreLoginCount); + Assert.Equal(2, failoverServer.PreLoginCount); + } + + [Fact] + public void NetworkTimeout_ShouldFail() + { + using TdsServer failoverServer = new( + new TdsServerArguments + { + // Doesn't need to point to a real endpoint, just needs a value specified + FailoverPartner = "localhost,1234", + }); + failoverServer.Start(); + + // Arrange + using TransientDelayTdsServer server = new( + new TransientDelayTdsServerArguments() + { + IsEnabledTransientDelay = true, + DelayDuration = TimeSpan.FromMilliseconds(2000), + FailoverPartner = $"localhost,{failoverServer.EndPoint.Port}", + }); + server.Start(); + + SqlConnectionStringBuilder builder = new() + { + DataSource = "localhost," + server.EndPoint.Port, + InitialCatalog = "master",// Required for failover partner to work + ConnectTimeout = 1, + ConnectRetryInterval = 1, + ConnectRetryCount = 0, // Disable retry + Encrypt = false, + MultiSubnetFailover = false, +#if NETFRAMEWORK + TransparentNetworkIPResolution = false, +#endif + }; + using SqlConnection connection = new(builder.ConnectionString); + + // Act + var e = Assert.Throws(() => connection.Open()); + + // Assert + Assert.Contains("Connection Timeout Expired", e.Message); + Assert.Equal(ConnectionState.Closed, connection.State); + Assert.Equal(1, server.PreLoginCount); + Assert.Equal(0, failoverServer.PreLoginCount); + } + + [Fact] + public void NetworkDelay_ShouldConnectToPrimary() + { + using TdsServer failoverServer = new( + new TdsServerArguments + { + // Doesn't need to point to a real endpoint, just needs a value specified + FailoverPartner = "localhost,1234", + }); + failoverServer.Start(); + + // Arrange + using TransientDelayTdsServer server = new( + new TransientDelayTdsServerArguments() + { + IsEnabledTransientDelay = true, + DelayDuration = TimeSpan.FromMilliseconds(1000), + FailoverPartner = $"localhost,{failoverServer.EndPoint.Port}", + }); + server.Start(); + + SqlConnectionStringBuilder builder = new() + { + DataSource = "localhost," + server.EndPoint.Port, + InitialCatalog = "master",// Required for failover partner to work + ConnectTimeout = 5, + Encrypt = false, + MultiSubnetFailover = false, +#if NETFRAMEWORK + TransparentNetworkIPResolution = false, +#endif + }; + using SqlConnection connection = new(builder.ConnectionString); + try + { + // Act + connection.Open(); + } + catch (Exception e) + { + Assert.Fail(e.Message); + } + + // Assert + // On the first connection attempt, no failover partner information is available, + // so the connection will retry on the same server. + Assert.Equal(ConnectionState.Open, connection.State); + Assert.Equal($"localhost,{server.EndPoint.Port}", connection.DataSource); + Assert.Equal(1, server.PreLoginCount); + Assert.Equal(0, failoverServer.PreLoginCount); + } + + [Fact] + public void NetworkError_WithUserProvidedPartner_RetryDisabled_ShouldConnectToFailoverPartner() + { + using TdsServer failoverServer = new( + new TdsServerArguments + { + // Doesn't need to point to a real endpoint, just needs a value specified + FailoverPartner = "localhost,1234", + }); + failoverServer.Start(); + + // Arrange + using TransientDelayTdsServer server = new( + new TransientDelayTdsServerArguments() + { + IsEnabledTransientDelay = true, + DelayDuration = TimeSpan.FromMilliseconds(10000), + FailoverPartner = $"localhost,{failoverServer.EndPoint.Port}", + }); + server.Start(); + + SqlConnectionStringBuilder builder = new() + { + DataSource = "localhost," + server.EndPoint.Port, + InitialCatalog = "master", // Required for failover partner to work + ConnectTimeout = 5, + ConnectRetryInterval = 1, + ConnectRetryCount = 0, // Disable retry + FailoverPartner = $"localhost,{failoverServer.EndPoint.Port}", // User provided failover partner + Encrypt = false, + }; + using SqlConnection connection = new(builder.ConnectionString); + try + { + // Act + connection.Open(); + } + catch (Exception e) + { + Assert.Fail(e.Message); + } + + // Assert + // On the first connection attempt, failover partner information is available in the connection string, + // so the connection will retry on the failover server. + Assert.Equal(ConnectionState.Open, connection.State); + Assert.Equal($"localhost,{failoverServer.EndPoint.Port}", connection.DataSource); + Assert.Equal(1, failoverServer.PreLoginCount); + Assert.Equal(1, server.PreLoginCount); + } + + [Fact] + public void NetworkError_WithUserProvidedPartner_RetryEnabled_ShouldConnectToFailoverPartner() + { + using TdsServer failoverServer = new( + new TdsServerArguments + { + // Doesn't need to point to a real endpoint, just needs a value specified + FailoverPartner = "localhost,1234", + }); + failoverServer.Start(); + + // Arrange + using TransientDelayTdsServer server = new( + new TransientDelayTdsServerArguments() + { + IsEnabledTransientDelay = true, + DelayDuration = TimeSpan.FromMilliseconds(10000), + FailoverPartner = $"localhost,{failoverServer.EndPoint.Port}", + }); + server.Start(); + + SqlConnectionStringBuilder builder = new() + { + DataSource = "localhost," + server.EndPoint.Port, + InitialCatalog = "master", // Required for failover partner to work + ConnectTimeout = 5, + ConnectRetryInterval = 1, + FailoverPartner = $"localhost,{failoverServer.EndPoint.Port}", // User provided failover partner + Encrypt = false, + }; + using SqlConnection connection = new(builder.ConnectionString); + // Act + connection.Open(); + + // Assert + // On the first connection attempt, failover partner information is available in the connection string, + // so the connection will retry on the failover server. + Assert.Equal(ConnectionState.Open, connection.State); + Assert.Equal($"localhost,{failoverServer.EndPoint.Port}", connection.DataSource); + Assert.Equal(1, server.PreLoginCount); + Assert.Equal(1, failoverServer.PreLoginCount); + } + + [Theory] + [InlineData(40613)] + [InlineData(42108)] + [InlineData(42109)] + public void TransientFault_ShouldConnectToPrimary(uint errorCode) + { + // Arrange + using TdsServer failoverServer = new( + new TdsServerArguments + { + // Doesn't need to point to a real endpoint, just needs a value specified + FailoverPartner = "localhost,1234", + }); + failoverServer.Start(); + + using TransientTdsErrorTdsServer server = new( + new TransientTdsErrorTdsServerArguments() + { + IsEnabledTransientError = true, + Number = errorCode, + FailoverPartner = $"localhost,{failoverServer.EndPoint.Port}", + }); + server.Start(); + + SqlConnectionStringBuilder builder = new() + { + DataSource = $"localhost,{server.EndPoint.Port}", + InitialCatalog = "master", + ConnectTimeout = 30, + ConnectRetryInterval = 1, + Encrypt = false + }; + using SqlConnection connection = new(builder.ConnectionString); + + // Act + connection.Open(); + + // Assert + Assert.Equal(ConnectionState.Open, connection.State); + Assert.Equal($"localhost,{server.EndPoint.Port}", connection.DataSource); + + // Failures should prompt the client to return to the original server, resulting in a login count of 2 + Assert.Equal(2, server.PreLoginCount); + } + + [Theory] + [InlineData(40613)] + [InlineData(42108)] + [InlineData(42109)] + public void TransientFault_RetryDisabled_ShouldFail(uint errorCode) + { + // Arrange + using TdsServer failoverServer = new( + new TdsServerArguments + { + // Doesn't need to point to a real endpoint, just needs a value specified + FailoverPartner = "localhost:1234", + }); + failoverServer.Start(); + + using TransientTdsErrorTdsServer server = new( + new TransientTdsErrorTdsServerArguments() + { + IsEnabledTransientError = true, + Number = errorCode, + FailoverPartner = $"localhost:{failoverServer.EndPoint.Port}", + }); + server.Start(); + + SqlConnectionStringBuilder builder = new() + { + DataSource = $"localhost,{server.EndPoint.Port}", + InitialCatalog = "master", + ConnectTimeout = 30, + ConnectRetryInterval = 1, + ConnectRetryCount = 0, // Disable retry + Encrypt = false + }; + using SqlConnection connection = new(builder.ConnectionString); + try + { + // Act + connection.Open(); + } + catch (SqlException e) + { + Assert.Equal((int)errorCode, e.Number); + return; + } + + Assert.Fail(); + } + + [Theory] + [InlineData(40613)] + [InlineData(42108)] + [InlineData(42109)] + public void TransientFault_WithUserProvidedPartner_ShouldConnectToPrimary(uint errorCode) + { + // Arrange + using TdsServer failoverServer = new( + new TdsServerArguments + { + // Doesn't need to point to a real endpoint, just needs a value specified + FailoverPartner = "localhost:1234", + }); + failoverServer.Start(); + + using TransientTdsErrorTdsServer server = new( + new TransientTdsErrorTdsServerArguments() + { + IsEnabledTransientError = true, + Number = errorCode, + FailoverPartner = $"localhost:{failoverServer.EndPoint.Port}", + }); + server.Start(); + + SqlConnectionStringBuilder builder = new() + { + DataSource = $"localhost,{server.EndPoint.Port}", + InitialCatalog = "master", + ConnectTimeout = 30, + ConnectRetryInterval = 1, + Encrypt = false, + FailoverPartner = $"localhost:{failoverServer.EndPoint.Port}", // User provided failover partner + }; + using SqlConnection connection = new(builder.ConnectionString); + + // Act + connection.Open(); + + // Assert + Assert.Equal(ConnectionState.Open, connection.State); + Assert.Equal($"localhost,{server.EndPoint.Port}", connection.DataSource); + + // Failures should prompt the client to return to the original server, resulting in a login count of 2 + Assert.Equal(2, server.PreLoginCount); + } + + [Theory] + [InlineData(40613)] + [InlineData(42108)] + [InlineData(42109)] + public void TransientFault_WithUserProvidedPartner_RetryDisabled_ShouldFail(uint errorCode) + { + // Arrange + using TdsServer failoverServer = new( + new TdsServerArguments + { + // Doesn't need to point to a real endpoint, just needs a value specified + FailoverPartner = "localhost:1234", + }); + failoverServer.Start(); + + using TransientTdsErrorTdsServer server = new( + new TransientTdsErrorTdsServerArguments() + { + IsEnabledTransientError = true, + Number = errorCode, + FailoverPartner = $"localhost:{failoverServer.EndPoint.Port}", + }); + server.Start(); + + SqlConnectionStringBuilder builder = new() + { + DataSource = $"localhost,{server.EndPoint.Port}", + InitialCatalog = "master", + ConnectTimeout = 30, + ConnectRetryInterval = 1, + ConnectRetryCount = 0, // Disable retry + Encrypt = false, + FailoverPartner = $"localhost:{failoverServer.EndPoint.Port}", // User provided failover partner + }; + using SqlConnection connection = new(builder.ConnectionString); + try + { + // Act + connection.Open(); + } + catch (SqlException e) + { + Assert.Equal((int)errorCode, e.Number); + return; + } + + Assert.Fail(); + } + + [Fact] + public void TransientFault_IgnoreServerProvidedFailoverPartner_ShouldConnectToUserProvidedPartner() + { + // Arrange + using LocalAppContextSwitchesHelper switchesHelper = new(); + switchesHelper.IgnoreServerProvidedFailoverPartnerField = LocalAppContextSwitchesHelper.Tristate.True; + + using TdsServer failoverServer = new( + new TdsServerArguments + { + // Doesn't need to point to a real endpoint, just needs a value specified + FailoverPartner = "localhost,1234", + }); + failoverServer.Start(); + + using TdsServer server = new( + new TdsServerArguments() + { + // Set an invalid failover partner to ensure that the connection fails if the + // server provided failover partner is used. + FailoverPartner = $"invalidhost", + }); + server.Start(); + + SqlConnectionStringBuilder builder = new() + { + DataSource = $"localhost,{server.EndPoint.Port}", + InitialCatalog = "master", + Encrypt = false, + FailoverPartner = $"localhost,{failoverServer.EndPoint.Port}", + // Ensure pooling is enabled so that the failover partner information + // is persisted in the pool group. If pooling is disabled, the server + // provided failover partner will never be used. + Pooling = true, + MinPoolSize = 1 + }; + SqlConnection connection = new(builder.ConnectionString); + // Clear the pool to ensure a new physical connection is created + SqlConnection.ClearPool(connection); + + // Connect once to the primary to trigger it to send the failover partner + connection.Open(); + Assert.Equal("invalidhost", (connection.InnerConnection as SqlInternalConnectionTds)!.ServerProvidedFailoverPartner); + + // Close the connection to return it to the pool + connection.Close(); + + // Act + // Dispose of the server to trigger a failover + server.Dispose(); + + // Opening a new connection will use the failover partner stored in the pool group. + // This will fail if the server provided failover partner was stored to the pool group. + using SqlConnection failoverConnection = new(builder.ConnectionString); + + // Clear the pool to ensure a new physical connection is created + // Pool group info such as failover partner will still be retained + SqlConnection.ClearPool(connection); + failoverConnection.Open(); + + // Assert + Assert.Equal(ConnectionState.Open, failoverConnection.State); + Assert.Equal($"localhost,{failoverServer.EndPoint.Port}", failoverConnection.DataSource); + // 1 for the initial connection + Assert.Equal(1, server.PreLoginCount); + // 1 for the failover connection + Assert.Equal(1, failoverServer.PreLoginCount); + } + } +} diff --git a/src/Microsoft.Data.SqlClient/tests/UnitTests/SimulatedServerTests/ConnectionReadOnlyRoutingTests.cs b/src/Microsoft.Data.SqlClient/tests/UnitTests/SimulatedServerTests/ConnectionReadOnlyRoutingTests.cs new file mode 100644 index 0000000000..f0618ac269 --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/UnitTests/SimulatedServerTests/ConnectionReadOnlyRoutingTests.cs @@ -0,0 +1,156 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Net; +using System.Threading.Tasks; +using Microsoft.SqlServer.TDS.Servers; +using Xunit; + +namespace Microsoft.Data.SqlClient.UnitTests.SimulatedServerTests +{ + [Collection("SimulatedServerTests")] + public class ConnectionReadOnlyRoutingTests + { + [Fact] + public void NonRoutedConnection() + { + using TdsServer server = new(); + server.Start(); + SqlConnectionStringBuilder builder = new() { + DataSource = $"localhost,{server.EndPoint.Port}", + ApplicationIntent = ApplicationIntent.ReadOnly, + Encrypt = SqlConnectionEncryptOption.Optional + }; + using SqlConnection connection = new(builder.ConnectionString); + connection.Open(); + } + + [Fact] + public async Task NonRoutedAsyncConnection() + { + using TdsServer server = new(); + server.Start(); + SqlConnectionStringBuilder builder = new() { + DataSource = $"localhost,{server.EndPoint.Port}", + ApplicationIntent = ApplicationIntent.ReadOnly, + Encrypt = SqlConnectionEncryptOption.Optional + }; + using SqlConnection connection = new(builder.ConnectionString); + await connection.OpenAsync(); + } + + [Fact] + public void RoutedConnection() => RecursivelyRoutedConnection(1); + + [Fact] + public async Task RoutedAsyncConnection() => await RecursivelyRoutedAsyncConnection(1); + + [Theory] + [InlineData(11)] // 11 layers of routing should succeed, 12 should fail + public void RecursivelyRoutedConnection(int layers) + { + using TdsServer innerServer = new(); + innerServer.Start(); + IPEndPoint lastEndpoint = innerServer.EndPoint; + Stack routingLayers = new(layers + 1); + string lastConnectionString = (new SqlConnectionStringBuilder() { DataSource = $"localhost,{lastEndpoint.Port}" }).ConnectionString; + + try + { + for (int i = 0; i < layers; i++) + { + RoutingTdsServer router = new( + new RoutingTdsServerArguments() + { + RoutingTCPHost = "localhost", + RoutingTCPPort = (ushort)lastEndpoint.Port, + }); + router.Start(); + routingLayers.Push(router); + lastEndpoint = router.EndPoint; + lastConnectionString = (new SqlConnectionStringBuilder() { + DataSource = $"localhost,{lastEndpoint.Port}", + ApplicationIntent = ApplicationIntent.ReadOnly, + Encrypt = false + }).ConnectionString; + } + + SqlConnectionStringBuilder builder = new(lastConnectionString) { ApplicationIntent = ApplicationIntent.ReadOnly }; + using SqlConnection connection = new(builder.ConnectionString); + connection.Open(); + } + finally + { + while (routingLayers.Count > 0) + { + routingLayers.Pop().Dispose(); + } + } + } + + [Theory] + [InlineData(11)] // 11 layers of routing should succeed, 12 should fail + public async Task RecursivelyRoutedAsyncConnection(int layers) + { + using TdsServer innerServer = new(); + innerServer.Start(); + IPEndPoint lastEndpoint = innerServer.EndPoint; + Stack routingLayers = new(layers + 1); + string lastConnectionString = (new SqlConnectionStringBuilder() { DataSource = $"localhost,{lastEndpoint.Port}" }).ConnectionString; + + try + { + for (int i = 0; i < layers; i++) + { + RoutingTdsServer router = new( + new RoutingTdsServerArguments() + { + RoutingTCPHost = "localhost", + RoutingTCPPort = (ushort)lastEndpoint.Port, + }); + router.Start(); + routingLayers.Push(router); + lastEndpoint = router.EndPoint; + lastConnectionString = (new SqlConnectionStringBuilder() { + DataSource = $"localhost,{lastEndpoint.Port}", + ApplicationIntent = ApplicationIntent.ReadOnly, + Encrypt = false + }).ConnectionString; + } + + SqlConnectionStringBuilder builder = new(lastConnectionString) { + ApplicationIntent = ApplicationIntent.ReadOnly, + Encrypt = false + }; + using SqlConnection connection = new(builder.ConnectionString); + await connection.OpenAsync(); + } + finally + { + while (routingLayers.Count > 0) + { + routingLayers.Pop().Dispose(); + } + } + } + + [Fact] + public void ConnectionRoutingLimit() + { + SqlException sqlEx = Assert.Throws(() => RecursivelyRoutedConnection(12)); // This will fail on the 11th redirect + + Assert.Contains("Too many redirections have occurred.", sqlEx.Message, StringComparison.InvariantCultureIgnoreCase); + } + + [Fact] + public async Task AsyncConnectionRoutingLimit() + { + SqlException sqlEx = await Assert.ThrowsAsync(() => RecursivelyRoutedAsyncConnection(12)); // This will fail on the 11th redirect + + Assert.Contains("Too many redirections have occurred.", sqlEx.Message, StringComparison.InvariantCultureIgnoreCase); + } + } +} diff --git a/src/Microsoft.Data.SqlClient/tests/UnitTests/SimulatedServerTests/ConnectionRoutingTests.cs b/src/Microsoft.Data.SqlClient/tests/UnitTests/SimulatedServerTests/ConnectionRoutingTests.cs new file mode 100644 index 0000000000..108118dda7 --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/UnitTests/SimulatedServerTests/ConnectionRoutingTests.cs @@ -0,0 +1,202 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Data; +using Microsoft.Data.Common; +using Microsoft.SqlServer.TDS.Servers; +using Xunit; + +namespace Microsoft.Data.SqlClient.UnitTests.SimulatedServerTests +{ + [Trait("Category", "flaky")] + [Collection("SimulatedServerTests")] + public class ConnectionRoutingTests + { + [Theory] + [InlineData(40613)] + [InlineData(42108)] + [InlineData(42109)] + public void TransientFaultAtRoutedLocation_ShouldReturnToGateway(uint errorCode) + { + // Arrange + using TransientTdsErrorTdsServer server = new( + new TransientTdsErrorTdsServerArguments() + { + IsEnabledTransientError = true, + Number = errorCode, + }); + + server.Start(); + + using RoutingTdsServer router = new( + new RoutingTdsServerArguments() + { + //RoutingTCPHost = server.EndPoint.Address.ToString() == IPAddress.Any.ToString() ? IPAddress.Loopback.ToString() : server.EndPoint.Address.ToString(), + RoutingTCPHost = "localhost", + RoutingTCPPort = (ushort)server.EndPoint.Port, + }); + router.Start(); + + SqlConnectionStringBuilder builder = new() + { + DataSource = "localhost," + router.EndPoint.Port, + ApplicationIntent = ApplicationIntent.ReadOnly, + ConnectTimeout = 30, + ConnectRetryInterval = 1, + Encrypt = false, + }; + using SqlConnection connection = new(builder.ConnectionString); + + // Act + connection.Open(); + + // Assert + Assert.Equal(ConnectionState.Open, connection.State); + // Routing does not update the connection's data source + Assert.Equal($"localhost,{router.EndPoint.Port}", connection.DataSource); + + // Failures should prompt the client to return to the original server, resulting in a login count of 2 + Assert.Equal(2, router.PreLoginCount); + Assert.Equal(2, server.PreLoginCount); + } + + [Theory] + [InlineData(40613)] + [InlineData(42108)] + [InlineData(42109)] + public void TransientFaultAtRoutedLocation_RetryDisabled_ShouldFail(uint errorCode) + { + // Arrange + using TransientTdsErrorTdsServer server = new( + new TransientTdsErrorTdsServerArguments() + { + IsEnabledTransientError = true, + Number = errorCode, + }); + + server.Start(); + + using RoutingTdsServer router = new( + new RoutingTdsServerArguments() + { + //RoutingTCPHost = server.EndPoint.Address.ToString() == IPAddress.Any.ToString() ? IPAddress.Loopback.ToString() : server.EndPoint.Address.ToString(), + RoutingTCPHost = "localhost", + RoutingTCPPort = (ushort)server.EndPoint.Port, + }); + router.Start(); + + SqlConnectionStringBuilder builder = new() + { + DataSource = "localhost," + router.EndPoint.Port, + ApplicationIntent = ApplicationIntent.ReadOnly, + ConnectTimeout = 30, + ConnectRetryInterval = 1, + ConnectRetryCount = 0, // Disable retry + Encrypt = false, + }; + using SqlConnection connection = new(builder.ConnectionString); + + //Act and Assert + Assert.Throws(() => connection.Open()); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void NetworkDelayAtRoutedLocation_RetryDisabled_ShouldSucceed(bool multiSubnetFailoverEnabled) + { + // Arrange + using TransientDelayTdsServer server = new( + new TransientDelayTdsServerArguments() + { + IsEnabledTransientDelay = true, + DelayDuration = TimeSpan.FromMilliseconds(1000), + }); + + server.Start(); + + using RoutingTdsServer router = new( + new RoutingTdsServerArguments() + { + RoutingTCPHost = "localhost", + RoutingTCPPort = (ushort)server.EndPoint.Port, + }); + router.Start(); + + SqlConnectionStringBuilder builder = new() + { + DataSource = "localhost," + router.EndPoint.Port, + ApplicationIntent = ApplicationIntent.ReadOnly, + ConnectTimeout = 5, + ConnectRetryCount = 0, // disable retry + Encrypt = false, + MultiSubnetFailover = multiSubnetFailoverEnabled, +#if NETFRAMEWORK + TransparentNetworkIPResolution = multiSubnetFailoverEnabled, +#endif + }; + using SqlConnection connection = new(builder.ConnectionString); + + // Act + connection.Open(); + + // Assert + Assert.Equal(ConnectionState.Open, connection.State); + Assert.Equal($"localhost,{router.EndPoint.Port}", connection.DataSource); + Assert.Equal(1, router.PreLoginCount); + if (multiSubnetFailoverEnabled) + { + Assert.True(server.PreLoginCount > 1); + } + else + { + Assert.Equal(1, server.PreLoginCount); + } + } + + [Fact] + public void NetworkTimeoutAtRoutedLocation_RetryDisabled_ShouldFail() + { + // Arrange + using TransientDelayTdsServer server = new( + new TransientDelayTdsServerArguments() + { + IsEnabledTransientDelay = true, + DelayDuration = TimeSpan.FromMilliseconds(2000), + }); + + server.Start(); + + using RoutingTdsServer router = new( + new RoutingTdsServerArguments() + { + RoutingTCPHost = "localhost", + RoutingTCPPort = (ushort)server.EndPoint.Port, + }); + router.Start(); + + SqlConnectionStringBuilder builder = new() + { + DataSource = "localhost," + router.EndPoint.Port, + ApplicationIntent = ApplicationIntent.ReadOnly, + ConnectTimeout = 1, + ConnectRetryCount = 0, // disable retry + Encrypt = false, + MultiSubnetFailover = false, +#if NETFRAMEWORK + TransparentNetworkIPResolution = false +#endif + }; + using SqlConnection connection = new(builder.ConnectionString); + + // Act + var e = Assert.Throws(connection.Open); + + // Assert + Assert.Equal(ConnectionState.Closed, connection.State); + Assert.Contains("Connection Timeout Expired", e.Message); + } + } +} diff --git a/src/Microsoft.Data.SqlClient/tests/UnitTests/SimulatedServerTests/ConnectionRoutingTestsAzure.cs b/src/Microsoft.Data.SqlClient/tests/UnitTests/SimulatedServerTests/ConnectionRoutingTestsAzure.cs new file mode 100644 index 0000000000..dd945e37f3 --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/UnitTests/SimulatedServerTests/ConnectionRoutingTestsAzure.cs @@ -0,0 +1,200 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Data; +using Microsoft.Data.Common; +using Microsoft.SqlServer.TDS.Servers; +using Xunit; + +namespace Microsoft.Data.SqlClient.UnitTests.SimulatedServerTests +{ + [Trait("Category", "flaky")] + [Collection("SimulatedServerTests")] + public class ConnectionRoutingTestsAzure : IDisposable + { + private ADPHelper adpHelper; + + public ConnectionRoutingTestsAzure() + { + adpHelper = new ADPHelper(); + adpHelper.AddAzureSqlServerEndpoint("localhost"); + } + + public void Dispose() + { + adpHelper.Dispose(); + } + + [Theory] + [InlineData(40613)] + [InlineData(42108)] + [InlineData(42109)] + public void TransientFaultAtRoutedLocation_ShouldReturnToGateway(uint errorCode) + { + // Arrange + using TransientTdsErrorTdsServer server = new( + new TransientTdsErrorTdsServerArguments() + { + IsEnabledTransientError = true, + Number = errorCode, + }); + + server.Start(); + + using RoutingTdsServer router = new( + new RoutingTdsServerArguments() + { + RoutingTCPHost = "localhost", + RoutingTCPPort = (ushort)server.EndPoint.Port, + }); + router.Start(); + + SqlConnectionStringBuilder builder = new() + { + DataSource = "localhost," + router.EndPoint.Port, + ApplicationIntent = ApplicationIntent.ReadOnly, + ConnectTimeout = 30, + ConnectRetryInterval = 1, + Encrypt = false, + }; + using SqlConnection connection = new(builder.ConnectionString); + try + { + // Act + connection.Open(); + } + catch (Exception e) + { + Assert.Fail(e.Message); + } + + // Assert + Assert.Equal(ConnectionState.Open, connection.State); + // Routing does not update the connection's data source + Assert.Equal($"localhost,{router.EndPoint.Port}", connection.DataSource); + + // Failures should prompt the client to return to the original server, resulting in a login count of 2 + Assert.Equal(2, router.PreLoginCount); + Assert.Equal(2, server.PreLoginCount); + } + + [Theory] + [InlineData(40613)] + [InlineData(42108)] + [InlineData(42109)] + public void TransientFaultAtRoutedLocation_RetryDisabled_ShouldFail(uint errorCode) + { + // Arrange + using TransientTdsErrorTdsServer server = new( + new TransientTdsErrorTdsServerArguments() + { + IsEnabledTransientError = true, + Number = errorCode, + }); + + server.Start(); + + using RoutingTdsServer router = new( + new RoutingTdsServerArguments() + { + RoutingTCPHost = "localhost", + RoutingTCPPort = (ushort)server.EndPoint.Port, + }); + router.Start(); + + SqlConnectionStringBuilder builder = new() + { + DataSource = "localhost," + router.EndPoint.Port, + ApplicationIntent = ApplicationIntent.ReadOnly, + ConnectTimeout = 30, + ConnectRetryInterval = 1, + ConnectRetryCount = 0, // Disable retry + Encrypt = false, + }; + using SqlConnection connection = new(builder.ConnectionString); + Assert.Throws(() => connection.Open()); + } + + [Fact] + public void NetworkDelayAtRoutedLocation_RetryDisabled_ShouldSucceed() + { + // Arrange + using TransientDelayTdsServer server = new( + new TransientDelayTdsServerArguments() + { + IsEnabledTransientDelay = true, + DelayDuration = TimeSpan.FromMilliseconds(1000), + }); + + server.Start(); + + using RoutingTdsServer router = new( + new RoutingTdsServerArguments() + { + RoutingTCPHost = "localhost", + RoutingTCPPort = (ushort)server.EndPoint.Port, + }); + router.Start(); + + SqlConnectionStringBuilder builder = new() + { + DataSource = "localhost," + router.EndPoint.Port, + ApplicationIntent = ApplicationIntent.ReadOnly, + ConnectTimeout = 5, + ConnectRetryCount = 0, // disable retry + Encrypt = false + }; + using SqlConnection connection = new(builder.ConnectionString); + + // Act + connection.Open(); + + // Assert + Assert.Equal(ConnectionState.Open, connection.State); + Assert.Equal($"localhost,{router.EndPoint.Port}", connection.DataSource); + Assert.Equal(1, router.PreLoginCount); + Assert.Equal(1, server.PreLoginCount); + } + + [Fact] + public void NetworkTimeoutAtRoutedLocation_RetryDisabled_ShouldFail() + { + // Arrange + using TransientDelayTdsServer server = new( + new TransientDelayTdsServerArguments() + { + IsEnabledTransientDelay = true, + DelayDuration = TimeSpan.FromMilliseconds(2000), + }); + + server.Start(); + + using RoutingTdsServer router = new( + new RoutingTdsServerArguments() + { + RoutingTCPHost = "localhost", + RoutingTCPPort = (ushort)server.EndPoint.Port, + }); + router.Start(); + + SqlConnectionStringBuilder builder = new() + { + DataSource = "localhost," + router.EndPoint.Port, + ApplicationIntent = ApplicationIntent.ReadOnly, + ConnectTimeout = 1, + ConnectRetryCount = 0, // disable retry + Encrypt = false + }; + using SqlConnection connection = new(builder.ConnectionString); + + // Act + var e = Assert.Throws(connection.Open); + + // Assert + Assert.Equal(ConnectionState.Closed, connection.State); + Assert.Contains("Connection Timeout Expired", e.Message); + } + } +} diff --git a/src/Microsoft.Data.SqlClient/tests/FunctionalTests/SqlConnectionBasicTests.cs b/src/Microsoft.Data.SqlClient/tests/UnitTests/SimulatedServerTests/ConnectionTests.cs similarity index 63% rename from src/Microsoft.Data.SqlClient/tests/FunctionalTests/SqlConnectionBasicTests.cs rename to src/Microsoft.Data.SqlClient/tests/UnitTests/SimulatedServerTests/ConnectionTests.cs index 616a8fec6f..faeaa309cb 100644 --- a/src/Microsoft.Data.SqlClient/tests/FunctionalTests/SqlConnectionBasicTests.cs +++ b/src/Microsoft.Data.SqlClient/tests/UnitTests/SimulatedServerTests/ConnectionTests.cs @@ -9,7 +9,6 @@ using System.Globalization; using System.Linq; using System.Reflection; -using System.Runtime.InteropServices; using System.Security; using System.Threading; using System.Threading.Tasks; @@ -20,26 +19,38 @@ using Microsoft.SqlServer.TDS.Servers; using Xunit; -namespace Microsoft.Data.SqlClient.Tests +#nullable enable + +namespace Microsoft.Data.SqlClient.UnitTests.SimulatedServerTests { - public class SqlConnectionBasicTests + public class ConnectionTests { [Fact] public void ConnectionTest() { - using TestTdsServer server = TestTdsServer.StartTestServer(); - using SqlConnection connection = new SqlConnection(server.ConnectionString); + using TdsServer server = new(new TdsServerArguments() { }); + server.Start(); + var connStr = new SqlConnectionStringBuilder() { + DataSource = $"localhost,{server.EndPoint.Port}", + Encrypt = SqlConnectionEncryptOption.Optional, + }.ConnectionString; + using SqlConnection connection = new(connStr); connection.Open(); } - [ConditionalFact(typeof(TestUtility), nameof(TestUtility.IsNotArmProcess))] + [Fact] [PlatformSpecific(TestPlatforms.Windows)] public void IntegratedAuthConnectionTest() { - using TestTdsServer server = TestTdsServer.StartTestServer(); - SqlConnectionStringBuilder builder = new SqlConnectionStringBuilder(server.ConnectionString); + using TdsServer server = new(new TdsServerArguments() { }); + server.Start(); + var connStr = new SqlConnectionStringBuilder() { + DataSource = $"localhost,{server.EndPoint.Port}", + Encrypt = SqlConnectionEncryptOption.Optional, + }.ConnectionString; + SqlConnectionStringBuilder builder = new(connStr); builder.IntegratedSecurity = true; - using SqlConnection connection = new SqlConnection(builder.ConnectionString); + using SqlConnection connection = new(builder.ConnectionString); connection.Open(); } @@ -49,117 +60,264 @@ public void IntegratedAuthConnectionTest() /// when client enables encryption using Encrypt=true or uses default encryption setting. /// [Fact] - public async Task PreLoginEncryptionExcludedTest() + public async Task RequestEncryption_ServerDoesNotSupportEncryption_ShouldFail() { - using TestTdsServer server = TestTdsServer.StartTestServer(false, false, 5, excludeEncryption: true); - SqlConnectionStringBuilder builder = new(server.ConnectionString) - { - IntegratedSecurity = true - }; + using TdsServer server = new(new TdsServerArguments() {Encryption = TDSPreLoginTokenEncryptionType.None }); + server.Start(); + var connStr = new SqlConnectionStringBuilder() { + DataSource = $"localhost,{server.EndPoint.Port}" + }.ConnectionString; - using SqlConnection connection = new(builder.ConnectionString); + using SqlConnection connection = new(connStr); Exception ex = await Assert.ThrowsAsync(async () => await connection.OpenAsync()); Assert.Contains("The instance of SQL Server you attempted to connect to does not support encryption.", ex.Message, StringComparison.OrdinalIgnoreCase); } - [ConditionalTheory(typeof(TestUtility), nameof(TestUtility.IsNotArmProcess))] + [Trait("Category", "flaky")] + [Theory] [InlineData(40613)] [InlineData(42108)] [InlineData(42109)] - [PlatformSpecific(TestPlatforms.Windows)] - public async Task TransientFaultTestAsync(uint errorCode) + public async Task TransientFault_RetryEnabled_ShouldSucceed_Async(uint errorCode) { - using TransientFaultTDSServer server = TransientFaultTDSServer.StartTestServer(true, false, errorCode); + using TransientTdsErrorTdsServer server = new( + new TransientTdsErrorTdsServerArguments() + { + IsEnabledTransientError = true, + Number = errorCode, + }); + server.Start(); SqlConnectionStringBuilder builder = new() { - DataSource = "localhost," + server.Port, - IntegratedSecurity = true, + DataSource = "localhost," + server.EndPoint.Port, Encrypt = SqlConnectionEncryptOption.Optional }; using SqlConnection connection = new(builder.ConnectionString); await connection.OpenAsync(); Assert.Equal(ConnectionState.Open, connection.State); + Assert.Equal($"localhost,{server.EndPoint.Port}", connection.DataSource); + Assert.Equal(2, server.PreLoginCount); } - [ConditionalTheory(typeof(TestUtility), nameof(TestUtility.IsNotArmProcess))] + [Trait("Category", "flaky")] + [Theory] [InlineData(40613)] [InlineData(42108)] [InlineData(42109)] - [PlatformSpecific(TestPlatforms.Windows)] - public void TransientFaultTest(uint errorCode) + public void TransientFault_RetryEnabled_ShouldSucceed(uint errorCode) { - using TransientFaultTDSServer server = TransientFaultTDSServer.StartTestServer(true, false, errorCode); + using TransientTdsErrorTdsServer server = new( + new TransientTdsErrorTdsServerArguments() + { + IsEnabledTransientError = true, + Number = errorCode, + }); + server.Start(); SqlConnectionStringBuilder builder = new() { - DataSource = "localhost," + server.Port, - IntegratedSecurity = true, + DataSource = "localhost," + server.EndPoint.Port, Encrypt = SqlConnectionEncryptOption.Optional }; using SqlConnection connection = new(builder.ConnectionString); - try - { - connection.Open(); - Assert.Equal(ConnectionState.Open, connection.State); - } - catch (Exception e) - { - Assert.Fail(e.Message); - } + connection.Open(); + Assert.Equal(ConnectionState.Open, connection.State); + Assert.Equal($"localhost,{server.EndPoint.Port}", connection.DataSource); + Assert.Equal(2, server.PreLoginCount); } - [ConditionalTheory(typeof(TestUtility), nameof(TestUtility.IsNotArmProcess))] + [Trait("Category", "flaky")] + [Theory] [InlineData(40613)] [InlineData(42108)] [InlineData(42109)] - [PlatformSpecific(TestPlatforms.Windows)] - public void TransientFaultDisabledTestAsync(uint errorCode) + public async Task TransientFault_RetryDisabled_ShouldFail_Async(uint errorCode) { - using TransientFaultTDSServer server = TransientFaultTDSServer.StartTestServer(true, false, errorCode); + using TransientTdsErrorTdsServer server = new( + new TransientTdsErrorTdsServerArguments() + { + IsEnabledTransientError = true, + Number = errorCode, + }); + server.Start(); SqlConnectionStringBuilder builder = new() { - DataSource = "localhost," + server.Port, - IntegratedSecurity = true, + DataSource = "localhost," + server.EndPoint.Port, ConnectRetryCount = 0, Encrypt = SqlConnectionEncryptOption.Optional }; using SqlConnection connection = new(builder.ConnectionString); - Task e = Assert.ThrowsAsync(async () => await connection.OpenAsync()); - Assert.Equal(20, e.Result.Class); + SqlException e = await Assert.ThrowsAsync(async () => await connection.OpenAsync()); + Assert.Equal((int)errorCode, e.Number); Assert.Equal(ConnectionState.Closed, connection.State); + Assert.Equal(1, server.PreLoginCount); } - [ConditionalTheory(typeof(TestUtility), nameof(TestUtility.IsNotArmProcess))] + [Trait("Category", "flaky")] + [Theory] [InlineData(40613)] [InlineData(42108)] [InlineData(42109)] - [PlatformSpecific(TestPlatforms.Windows)] - public void TransientFaultDisabledTest(uint errorCode) + public void TransientFault_RetryDisabled_ShouldFail(uint errorCode) { - using TransientFaultTDSServer server = TransientFaultTDSServer.StartTestServer(true, false, errorCode); + using TransientTdsErrorTdsServer server = new( + new TransientTdsErrorTdsServerArguments() + { + IsEnabledTransientError = true, + Number = errorCode, + }); + server.Start(); SqlConnectionStringBuilder builder = new() { - DataSource = "localhost," + server.Port, - IntegratedSecurity = true, + DataSource = "localhost," + server.EndPoint.Port, ConnectRetryCount = 0, Encrypt = SqlConnectionEncryptOption.Optional }; using SqlConnection connection = new(builder.ConnectionString); SqlException e = Assert.Throws(() => connection.Open()); - Assert.Equal(20, e.Class); + Assert.Equal((int)errorCode, e.Number); Assert.Equal(ConnectionState.Closed, connection.State); + Assert.Equal(1, server.PreLoginCount); + } + + [Trait("Category", "flaky")] + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task NetworkError_RetryEnabled_ShouldSucceed_Async(bool multiSubnetFailoverEnabled) + { + using TransientDelayTdsServer server = new( + new TransientDelayTdsServerArguments() + { + IsEnabledTransientDelay = true, + DelayDuration = TimeSpan.FromMilliseconds(1000), + }); + server.Start(); + SqlConnectionStringBuilder builder = new() + { + DataSource = "localhost," + server.EndPoint.Port, + Encrypt = SqlConnectionEncryptOption.Optional, + ConnectTimeout = 5, + MultiSubnetFailover = multiSubnetFailoverEnabled, +#if NETFRAMEWORK + TransparentNetworkIPResolution = multiSubnetFailoverEnabled +#endif + }; + + using SqlConnection connection = new(builder.ConnectionString); + await connection.OpenAsync(); + Assert.Equal(ConnectionState.Open, connection.State); + Assert.Equal($"localhost,{server.EndPoint.Port}", connection.DataSource); + if (multiSubnetFailoverEnabled) + { + Assert.True(server.PreLoginCount > 1, "Expected multiple pre-login attempts due to retry."); + } + else + { + Assert.Equal(1, server.PreLoginCount); + } + } + + [Trait("Category", "flaky")] + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task NetworkDelay_RetryDisabled_Async(bool multiSubnetFailoverEnabled) + { + // Arrange + using TransientDelayTdsServer server = new( + new TransientDelayTdsServerArguments() + { + IsEnabledTransientDelay = true, + DelayDuration = TimeSpan.FromMilliseconds(1000), + }); + server.Start(); + SqlConnectionStringBuilder builder = new() + { + DataSource = "localhost," + server.EndPoint.Port, + ConnectTimeout = 5, + ConnectRetryCount = 0, + Encrypt = SqlConnectionEncryptOption.Optional, + MultiSubnetFailover = multiSubnetFailoverEnabled, +#if NETFRAMEWORK + TransparentNetworkIPResolution = multiSubnetFailoverEnabled, +#endif + }; + + using SqlConnection connection = new(builder.ConnectionString); + + // Act + await connection.OpenAsync(); + + // Assert + Assert.Equal(ConnectionState.Open, connection.State); + Assert.Equal($"localhost,{server.EndPoint.Port}", connection.DataSource); + + if (multiSubnetFailoverEnabled) + { + Assert.True(server.PreLoginCount > 1, "Expected multiple pre-login attempts due to retry."); + } + else + { + Assert.Equal(1, server.PreLoginCount); + } + } + + [Trait("Category", "flaky")] + [Theory] + [InlineData(true)] + [InlineData(false)] + public void NetworkDelay_RetryDisabled(bool multiSubnetFailoverEnabled) + { + // Arrange + using TransientDelayTdsServer server = new( + new TransientDelayTdsServerArguments() + { + IsEnabledTransientDelay = true, + DelayDuration = TimeSpan.FromMilliseconds(1000), + }); + server.Start(); + SqlConnectionStringBuilder builder = new() + { + DataSource = "localhost," + server.EndPoint.Port, + ConnectRetryCount = 0, + Encrypt = SqlConnectionEncryptOption.Optional, + ConnectTimeout = 5, + MultiSubnetFailover = multiSubnetFailoverEnabled, +#if NETFRAMEWORK + TransparentNetworkIPResolution = multiSubnetFailoverEnabled, +#endif + }; + + using SqlConnection connection = new(builder.ConnectionString); + + // Act + connection.Open(); + + // Assert + Assert.Equal(ConnectionState.Open, connection.State); + Assert.Equal($"localhost,{server.EndPoint.Port}", connection.DataSource); + + if (multiSubnetFailoverEnabled) + { + Assert.True(server.PreLoginCount > 1, "Expected multiple pre-login attempts due to retry."); + } + else + { + Assert.Equal(1, server.PreLoginCount); + } } [Fact] public void SqlConnectionDbProviderFactoryTest() { SqlConnection con = new(); - PropertyInfo dbProviderFactoryProperty = con.GetType().GetProperty("DbProviderFactory", BindingFlags.NonPublic | BindingFlags.Instance); + PropertyInfo? dbProviderFactoryProperty = con.GetType().GetProperty("DbProviderFactory", BindingFlags.NonPublic | BindingFlags.Instance); Assert.NotNull(dbProviderFactoryProperty); - DbProviderFactory factory = dbProviderFactoryProperty.GetValue(con) as DbProviderFactory; + DbProviderFactory? factory = dbProviderFactoryProperty.GetValue(con) as DbProviderFactory; Assert.NotNull(factory); Assert.Same(typeof(SqlClientFactory), factory.GetType()); Assert.Same(SqlClientFactory.Instance, factory); @@ -206,7 +364,7 @@ public void ClosedConnectionSchemaRetrieval() [InlineData("RandomStringForTargetServer", true, false)] [InlineData(null, false, false)] [InlineData("", false, false)] - public void RetrieveWorkstationId(string workstation, bool withDispose, bool shouldMatchSetWorkstationId) + public void RetrieveWorkstationId(string? workstation, bool withDispose, bool shouldMatchSetWorkstationId) { string connectionString = $"Workstation Id={workstation}"; SqlConnection conn = new(connectionString); @@ -214,7 +372,7 @@ public void RetrieveWorkstationId(string workstation, bool withDispose, bool sho { conn.Dispose(); } - string expected = shouldMatchSetWorkstationId ? workstation : Environment.MachineName; + string? expected = shouldMatchSetWorkstationId ? workstation : Environment.MachineName; Assert.Equal(expected, conn.WorkstationId); } @@ -302,23 +460,27 @@ public void ConnectionTestValidCredentialCombination() [Theory] [InlineData(60)] - [InlineData(30)] - [InlineData(15)] [InlineData(10)] - [InlineData(5)] [InlineData(1)] public void ConnectionTimeoutTest(int timeout) { // Start a server with connection timeout from the inline data. - using TestTdsServer server = TestTdsServer.StartTestServer(false, false, timeout); - using SqlConnection connection = new SqlConnection(server.ConnectionString); + //TODO: do we even need a server for this test? + using TdsServer server = new(); + server.Start(); + var connStr = new SqlConnectionStringBuilder() { + DataSource = $"localhost,{server.EndPoint.Port}", + ConnectTimeout = timeout, + Encrypt = SqlConnectionEncryptOption.Optional + }.ConnectionString; + using SqlConnection connection = new(connStr); // Dispose the server to force connection timeout server.Dispose(); // Measure the actual time it took to timeout and compare it with configured timeout Stopwatch timer = new(); - Exception ex = null; + Exception? ex = null; // Open a connection with the server disposed. try @@ -341,29 +503,34 @@ public void ConnectionTimeoutTest(int timeout) [Theory] [InlineData(60)] - [InlineData(30)] - [InlineData(15)] [InlineData(10)] - [InlineData(5)] [InlineData(1)] public async Task ConnectionTimeoutTestAsync(int timeout) { // Start a server with connection timeout from the inline data. - using TestTdsServer server = TestTdsServer.StartTestServer(false, false, timeout); - using SqlConnection connection = new SqlConnection(server.ConnectionString); + //TODO: do we even need a server for this test? + using TdsServer server = new(); + server.Start(); + var connStr = new SqlConnectionStringBuilder() + { + DataSource = $"localhost,{server.EndPoint.Port}", + ConnectTimeout = timeout, + Encrypt = SqlConnectionEncryptOption.Optional + }.ConnectionString; + using SqlConnection connection = new(connStr); // Dispose the server to force connection timeout server.Dispose(); // Measure the actual time it took to timeout and compare it with configured timeout Stopwatch timer = new(); - Exception ex = null; + Exception? ex = null; // Open a connection with the server disposed. try { //an asyn call with a timeout token to cancel the operation after the specific time - using CancellationTokenSource cts = new CancellationTokenSource(timeout * 1000); + using CancellationTokenSource cts = new(timeout * 1000); timer.Start(); await connection.OpenAsync(cts.Token).ConfigureAwait(false); } @@ -385,7 +552,11 @@ public void ConnectionInvalidTimeoutTest() { Assert.Throws(() => { - using TestTdsServer server = TestTdsServer.StartTestServer(false, false, -5); + var connectionString = new SqlConnectionStringBuilder() + { + DataSource = "localhost", + ConnectTimeout = -5 // Invalid timeout + }.ConnectionString; }); } @@ -401,8 +572,15 @@ public void ConnectionTestWithCultureTH() Thread.CurrentThread.CurrentCulture = new CultureInfo("th-TH"); Thread.CurrentThread.CurrentUICulture = new CultureInfo("th-TH"); - using TestTdsServer server = TestTdsServer.StartTestServer(); - using SqlConnection connection = new SqlConnection(server.ConnectionString); + //TODO: do we even need a server for this test? + using TdsServer server = new(); + server.Start(); + var connStr = new SqlConnectionStringBuilder() + { + DataSource = $"localhost,{server.EndPoint.Port}", + Encrypt = SqlConnectionEncryptOption.Optional + }.ConnectionString; + using SqlConnection connection = new(connStr); connection.Open(); Assert.Equal(ConnectionState.Open, connection.State); } @@ -504,9 +682,20 @@ public void ConnectionTestAccessTokenCallbackCombinations() [InlineData(11, 0, 3000)] // SQL Server 2012-2022 public void ConnectionTestPermittedVersion(int major, int minor, int build) { - Version simulatedServerVersion = new Version(major, minor, build); - using TestTdsServer server = TestTdsServer.StartTestServer(serverVersion: simulatedServerVersion); - using SqlConnection conn = new SqlConnection(server.ConnectionString); + Version simulatedServerVersion = new(major, minor, build); + + using TdsServer server = new( + new TdsServerArguments + { + ServerVersion = simulatedServerVersion, + }); + server.Start(); + var connStr = new SqlConnectionStringBuilder() + { + DataSource = $"localhost,{server.EndPoint.Port}", + Encrypt = SqlConnectionEncryptOption.Optional, + }.ConnectionString; + using SqlConnection conn = new(connStr); conn.Open(); Assert.Equal(ConnectionState.Open, conn.State); @@ -522,9 +711,19 @@ public void ConnectionTestPermittedVersion(int major, int minor, int build) [InlineData(8, 0, 384)] // SQL Server 2000 SP1 public void ConnectionTestDeniedVersion(int major, int minor, int build) { - Version simulatedServerVersion = new Version(major, minor, build); - using TestTdsServer server = TestTdsServer.StartTestServer(serverVersion: simulatedServerVersion); - using SqlConnection conn = new SqlConnection(server.ConnectionString); + Version simulatedServerVersion = new(major, minor, build); + using TdsServer server = new( + new TdsServerArguments + { + ServerVersion = simulatedServerVersion, + }); + server.Start(); + var connStr = new SqlConnectionStringBuilder() + { + DataSource = $"localhost,{server.EndPoint.Port}", + Encrypt = SqlConnectionEncryptOption.Optional, + }.ConnectionString; + using SqlConnection conn = new(connStr); Assert.Throws(() => conn.Open()); } @@ -542,7 +741,8 @@ public void ConnectionTestDeniedVersion(int major, int minor, int build) public void TestConnWithVectorFeatExtVersionNegotiation(bool expectedConnectionResult, byte serverVersion, byte expectedNegotiatedVersion) { // Start the test TDS server. - using var server = TestTdsServer.StartTestServer(); + using var server = new TdsServer(); + server.Start(); server.ServerSupportedVectorFeatureExtVersion = serverVersion; server.EnableVectorFeatureExt = serverVersion == 0xFF ? false : true; @@ -594,7 +794,12 @@ public void TestConnWithVectorFeatExtVersionNegotiation(bool expectedConnectionR }; // Connect to the test TDS server. - using var connection = new SqlConnection(server.ConnectionString); + var connStr = new SqlConnectionStringBuilder + { + DataSource = $"localhost,{server.EndPoint.Port}", + Encrypt = SqlConnectionEncryptOption.Optional, + }.ConnectionString; + using var connection = new SqlConnection(connStr); if (expectedConnectionResult) { connection.Open(); diff --git a/src/Microsoft.Data.SqlClient/tests/UnitTests/SqlVectorTest.cs b/src/Microsoft.Data.SqlClient/tests/UnitTests/SqlVectorTest.cs index 3390d95c02..c3d9869201 100644 --- a/src/Microsoft.Data.SqlClient/tests/UnitTests/SqlVectorTest.cs +++ b/src/Microsoft.Data.SqlClient/tests/UnitTests/SqlVectorTest.cs @@ -18,24 +18,23 @@ public class SqlVectorTest [Fact] public void UnsupportedType() { - Assert.Throws(() => new SqlVector(5)); - Assert.Throws(() => new SqlVector(5)); - Assert.Throws(() => new SqlVector(5)); + Assert.Throws(() => SqlVector.CreateNull(5)); + Assert.Throws(() => SqlVector.CreateNull(5)); + Assert.Throws(() => SqlVector.CreateNull(5)); } [Fact] public void Construct_Length_Negative() { - Assert.Throws(() => new SqlVector(-1)); + Assert.Throws(() => SqlVector.CreateNull(-1)); } [Fact] public void Construct_Length() { - var vec = new SqlVector(5); + var vec = SqlVector.CreateNull(5); Assert.True(vec.IsNull); Assert.Equal(5, vec.Length); - Assert.Equal(28, vec.Size); // Note that ReadOnlyMemory<> equality checks that both instances point // to the same memory. We want to check memory content equality, so we // compare their arrays instead. @@ -45,16 +44,17 @@ public void Construct_Length() var ivec = vec as ISqlVector; Assert.Equal(0x00, ivec.ElementType); Assert.Equal(0x04, ivec.ElementSize); + Assert.Equal(28, ivec.Size); Assert.Empty(ivec.VectorPayload); } [Fact] public void Construct_WithLengthZero() { - var vec = new SqlVector(0); + var vec = SqlVector.CreateNull(0); Assert.True(vec.IsNull); Assert.Equal(0, vec.Length); - Assert.Equal(8, vec.Size); + // Note that ReadOnlyMemory<> equality checks that both instances point // to the same memory. We want to check memory content equality, so we // compare their arrays instead. @@ -64,6 +64,7 @@ public void Construct_WithLengthZero() var ivec = vec as ISqlVector; Assert.Equal(0x00, ivec.ElementType); Assert.Equal(0x04, ivec.ElementSize); + Assert.Equal(8, ivec.Size); Assert.Empty(ivec.VectorPayload); } @@ -73,13 +74,13 @@ public void Construct_Memory_Empty() SqlVector vec = new(new ReadOnlyMemory()); Assert.False(vec.IsNull); Assert.Equal(0, vec.Length); - Assert.Equal(8, vec.Size); Assert.Equal(new ReadOnlyMemory().ToArray(), vec.Memory.ToArray()); Assert.Equal("[]", vec.GetString()); var ivec = vec as ISqlVector; Assert.Equal(0x00, ivec.ElementType); Assert.Equal(0x04, ivec.ElementSize); + Assert.Equal(8, ivec.Size); Assert.Equal( new byte[] { 0xA9, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, ivec.VectorPayload); @@ -93,7 +94,6 @@ public void Construct_Memory() SqlVector vec = new(memory); Assert.False(vec.IsNull); Assert.Equal(2, vec.Length); - Assert.Equal(16, vec.Size); Assert.Equal(memory.ToArray(), vec.Memory.ToArray()); Assert.Equal(data, vec.Memory.ToArray()); #if NETFRAMEWORK @@ -104,6 +104,7 @@ public void Construct_Memory() var ivec = vec as ISqlVector; Assert.Equal(0x00, ivec.ElementType); Assert.Equal(0x04, ivec.ElementSize); + Assert.Equal(16, ivec.Size); Assert.Equal( MakeTdsPayload( new byte[] { 0xA9, 0x01, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00 }, @@ -118,7 +119,6 @@ public void Construct_Memory_ImplicitConversionFromFloatArray() var vec = new SqlVector(data); Assert.False(vec.IsNull); Assert.Equal(3, vec.Length); - Assert.Equal(20, vec.Size); Assert.Equal(new ReadOnlyMemory(data).ToArray(), vec.Memory.ToArray()); Assert.Equal(data, vec.Memory.ToArray()); #if NETFRAMEWORK @@ -130,6 +130,7 @@ public void Construct_Memory_ImplicitConversionFromFloatArray() var ivec = vec as ISqlVector; Assert.Equal(0x00, ivec.ElementType); Assert.Equal(0x04, ivec.ElementSize); + Assert.Equal(20, ivec.Size); Assert.Equal( MakeTdsPayload( new byte[] { 0xA9, 0x01, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00 }, @@ -149,7 +150,6 @@ public void Construct_Bytes() var vec = new SqlVector(bytes); Assert.False(vec.IsNull); Assert.Equal(2, vec.Length); - Assert.Equal(16, vec.Size); Assert.Equal(new ReadOnlyMemory(data).ToArray(), vec.Memory.ToArray()); Assert.Equal(data, vec.Memory.ToArray()); #if NETFRAMEWORK @@ -161,6 +161,7 @@ public void Construct_Bytes() var ivec = vec as ISqlVector; Assert.Equal(0x00, ivec.ElementType); Assert.Equal(0x04, ivec.ElementSize); + Assert.Equal(16, ivec.Size); Assert.Equal(bytes, ivec.VectorPayload); } diff --git a/src/Microsoft.Data.SqlClient/tests/tools/Microsoft.Data.SqlClient.TestUtilities/Config.cs b/src/Microsoft.Data.SqlClient/tests/tools/Microsoft.Data.SqlClient.TestUtilities/Config.cs index 1b712ceaf5..27ceb12711 100644 --- a/src/Microsoft.Data.SqlClient/tests/tools/Microsoft.Data.SqlClient.TestUtilities/Config.cs +++ b/src/Microsoft.Data.SqlClient/tests/tools/Microsoft.Data.SqlClient.TestUtilities/Config.cs @@ -42,7 +42,7 @@ public class Config public string KerberosDomainUser = null; public bool IsManagedInstance = false; public string AliasName = null; - public bool IsJsonSupported = false; + public static Config Load(string configPath = @"config.json") { try diff --git a/src/Microsoft.Data.SqlClient/tests/tools/Microsoft.Data.SqlClient.TestUtilities/config.default.json b/src/Microsoft.Data.SqlClient/tests/tools/Microsoft.Data.SqlClient.TestUtilities/config.default.json index a5f6cd996e..5ef2e9c99e 100644 --- a/src/Microsoft.Data.SqlClient/tests/tools/Microsoft.Data.SqlClient.TestUtilities/config.default.json +++ b/src/Microsoft.Data.SqlClient/tests/tools/Microsoft.Data.SqlClient.TestUtilities/config.default.json @@ -35,6 +35,5 @@ "ManagedIdentitySupported": true, "UserManagedIdentityClientId": "", "PowerShellPath": "", - "AliasName": "", - "IsJsonSupported": false + "AliasName": "" } diff --git a/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.EndPoint/TDSServerEndPoint.cs b/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.EndPoint/TDSServerEndPoint.cs index e81139c63a..349b23ceee 100644 --- a/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.EndPoint/TDSServerEndPoint.cs +++ b/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.EndPoint/TDSServerEndPoint.cs @@ -30,7 +30,8 @@ public override TDSServerEndPointConnection CreateConnection(TcpClient newConnec /// /// General server handler /// - public abstract class ServerEndPointHandler where T : ServerEndPointConnection + public abstract class ServerEndPointHandler : IDisposable + where T : ServerEndPointConnection { /// /// Gets/Sets the event log for the proxy server @@ -100,21 +101,6 @@ public void Start() // Update ServerEndpoint with the actual address/port, e.g. if port=0 was given ServerEndPoint = (IPEndPoint)ListenerSocket.LocalEndpoint; - Log($"{GetType().Name} {EndpointName} Is Server Socket Bound: {ListenerSocket.Server.IsBound} Testing connectivity to the endpoint created for the server."); - using (TcpClient client = new TcpClient()) - { - try - { - client.Connect("localhost", ServerEndPoint.Port); - } - catch (Exception e) - { - Log($"{GetType().Name} {EndpointName} Error occurred while testing server endpoint {e.Message}"); - throw; - } - } - Log($"{GetType().Name} {EndpointName} Endpoint test successful."); - // Initialize the listener ListenerThread = new Thread(new ThreadStart(_RequestListener)) { IsBackground = true }; ListenerThread.Name = "TDS Server EndPoint Listener"; @@ -148,7 +134,7 @@ public void Stop() foreach (T connection in unlockedConnections) { // Request to stop - connection.Stop(); + connection.Dispose(); } // If server failed to start there is no thread to join @@ -167,6 +153,12 @@ public void Stop() } } + public void Dispose() + { + // Stop the listener + Stop(); + } + /// /// Processes all incoming requests /// diff --git a/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.EndPoint/TDSServerEndPointConnection.cs b/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.EndPoint/TDSServerEndPointConnection.cs index 6327189691..3d24f9c397 100644 --- a/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.EndPoint/TDSServerEndPointConnection.cs +++ b/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.EndPoint/TDSServerEndPointConnection.cs @@ -8,6 +8,7 @@ using System.Net; using System.Net.Sockets; using System.Threading; +using System.Threading.Tasks; namespace Microsoft.SqlServer.TDS.EndPoint { @@ -44,12 +45,12 @@ public override void ProcessData(Stream rawStream) /// /// Connection to a single client /// - public abstract class ServerEndPointConnection + public abstract class ServerEndPointConnection : IDisposable { /// /// Worker thread /// - protected Thread ProcessorThread { get; set; } + protected Task ProcessorTask { get; set; } /// /// Gets/Sets the event log for the proxy server @@ -76,11 +77,6 @@ public abstract class ServerEndPointConnection /// protected TcpClient Connection { get; set; } - /// - /// The flag indicates whether server is being stopped - /// - protected bool StopRequested { get; set; } - /// /// Initialization constructor /// @@ -124,29 +120,8 @@ public ServerEndPointConnection(ITDSServer server, TcpClient connection) /// internal void Start() { - // Start with active connection - StopRequested = false; - // Prepare and start a thread - ProcessorThread = new Thread(new ThreadStart(_ConnectionHandler)) { IsBackground = true }; - ProcessorThread.Name = string.Format("TDS Server Connection {0} Thread", Connection.Client.RemoteEndPoint); - ProcessorThread.Start(); - } - - /// - /// Stop the connection - /// - internal void Stop() - { - // Request the listener thread to stop - StopRequested = true; - - // If connection failed to start there's no processor thread - if (ProcessorThread != null) - { - // Wait for termination - ProcessorThread.Join(); - } + ProcessorTask = RunConnectionHandler(); } /// @@ -159,10 +134,27 @@ internal void Stop() /// public abstract void ProcessData(Stream rawStream); + public void Dispose() + { + if (Connection != null) + { + Connection.Close(); + Connection.Dispose(); + Connection = null; + } + + // TODO: there's a deadlock condition when awaiting the processor task + // only dispose of it if it's already completed + if (ProcessorTask.Status == TaskStatus.RanToCompletion) + { + ProcessorTask.Dispose(); + } + } + /// /// Worker thread /// - private void _ConnectionHandler() + private async Task RunConnectionHandler() { try { @@ -171,7 +163,7 @@ private void _ConnectionHandler() PrepareForProcessingData(rawStream); // Process the packet sequence - while (Connection.Connected && !StopRequested) + while (Connection.Connected) { // Check incoming buffer if (rawStream.DataAvailable) @@ -187,7 +179,7 @@ private void _ConnectionHandler() } // Sleep a bit to reduce load on CPU - Thread.Sleep(10); + await Task.Delay(10); } } } @@ -212,6 +204,8 @@ private void _ConnectionHandler() { OnConnectionClosed(this, null); } + + return; } /// diff --git a/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.Servers/AuthenticatingTDSServer.cs b/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.Servers/AuthenticatingTDSServer.cs deleted file mode 100644 index 06261e2c8f..0000000000 --- a/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.Servers/AuthenticatingTDSServer.cs +++ /dev/null @@ -1,220 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -using Microsoft.SqlServer.TDS.Done; -using Microsoft.SqlServer.TDS.EndPoint; -using Microsoft.SqlServer.TDS.Error; -using Microsoft.SqlServer.TDS.Login7; - -namespace Microsoft.SqlServer.TDS.Servers -{ - /// - /// TDS Server that authenticates clients according to the requested parameters - /// - public class AuthenticatingTDSServer : GenericTDSServer - { - /// - /// Initialization constructor - /// - public AuthenticatingTDSServer() : - this(new AuthenticatingTDSServerArguments()) - { - } - - /// - /// Initialization constructor - /// - public AuthenticatingTDSServer(AuthenticatingTDSServerArguments arguments) : - base(arguments) - { - } - - /// - /// Handler for login request - /// - public override TDSMessageCollection OnLogin7Request(ITDSServerSession session, TDSMessage request) - { - // Inflate login7 request from the message - TDSLogin7Token loginRequest = request[0] as TDSLogin7Token; - - // Check if arguments are of the authenticating TDS server - if (Arguments is AuthenticatingTDSServerArguments) - { - // Cast to authenticating TDS server arguments - AuthenticatingTDSServerArguments ServerArguments = Arguments as AuthenticatingTDSServerArguments; - - // Check if we're still processing normal login - if (ServerArguments.ApplicationIntentFilter != ApplicationIntentFilterType.All) - { - // Check filter - if ((ServerArguments.ApplicationIntentFilter == ApplicationIntentFilterType.ReadOnly && loginRequest.TypeFlags.ReadOnlyIntent != TDSLogin7TypeFlagsReadOnlyIntent.ReadOnly) - || (ServerArguments.ApplicationIntentFilter == ApplicationIntentFilterType.None)) - { - // Log request to which we're about to send a failure - TDSUtilities.Log(Arguments.Log, "Request", loginRequest); - - // Prepare ERROR token with the denial details - TDSErrorToken errorToken = new TDSErrorToken(18456, 1, 14, "Received application intent: " + loginRequest.TypeFlags.ReadOnlyIntent.ToString(), Arguments.ServerName); - - // Log response - TDSUtilities.Log(Arguments.Log, "Response", errorToken); - - // Serialize the error token into the response packet - TDSMessage responseMessage = new TDSMessage(TDSMessageType.Response, errorToken); - - // Prepare ERROR token for the final decision - errorToken = new TDSErrorToken(18456, 1, 14, "Connection is denied by application intent filter", Arguments.ServerName); - - // Log response - TDSUtilities.Log(Arguments.Log, "Response", errorToken); - - // Serialize the error token into the response packet - responseMessage.Add(errorToken); - - // Create DONE token - TDSDoneToken doneToken = new TDSDoneToken(TDSDoneTokenStatusType.Final | TDSDoneTokenStatusType.Error); - - // Log response - TDSUtilities.Log(Arguments.Log, "Response", doneToken); - - // Serialize DONE token into the response packet - responseMessage.Add(doneToken); - - // Put a single message into the collection and return it - return new TDSMessageCollection(responseMessage); - } - } - - // Check if we're still processing normal login and there's a filter to check - if (ServerArguments.ServerNameFilterType != ServerNameFilterType.None) - { - // Check each algorithm - if ((ServerArguments.ServerNameFilterType == ServerNameFilterType.Equals && string.Compare(ServerArguments.ServerNameFilter, loginRequest.ServerName, true) != 0) - || (ServerArguments.ServerNameFilterType == ServerNameFilterType.StartsWith && !loginRequest.ServerName.StartsWith(ServerArguments.ServerNameFilter)) - || (ServerArguments.ServerNameFilterType == ServerNameFilterType.EndsWith && !loginRequest.ServerName.EndsWith(ServerArguments.ServerNameFilter)) - || (ServerArguments.ServerNameFilterType == ServerNameFilterType.Contains && !loginRequest.ServerName.Contains(ServerArguments.ServerNameFilter))) - { - // Log request to which we're about to send a failure - TDSUtilities.Log(Arguments.Log, "Request", loginRequest); - - // Prepare ERROR token with the reason - TDSErrorToken errorToken = new TDSErrorToken(18456, 1, 14, string.Format("Received server name \"{0}\", expected \"{1}\" using \"{2}\" algorithm", loginRequest.ServerName, ServerArguments.ServerNameFilter, ServerArguments.ServerNameFilterType), Arguments.ServerName); - - // Log response - TDSUtilities.Log(Arguments.Log, "Response", errorToken); - - // Serialize the errorToken token into the response packet - TDSMessage responseMessage = new TDSMessage(TDSMessageType.Response, errorToken); - - // Prepare ERROR token with the final errorToken - errorToken = new TDSErrorToken(18456, 1, 14, "Connection is denied by server name filter", Arguments.ServerName); - - // Log response - TDSUtilities.Log(Arguments.Log, "Response", errorToken); - - // Serialize the errorToken token into the response packet - responseMessage.Add(errorToken); - - // Create DONE token - TDSDoneToken doneToken = new TDSDoneToken(TDSDoneTokenStatusType.Final | TDSDoneTokenStatusType.Error); - - // Log response - TDSUtilities.Log(Arguments.Log, "Response", doneToken); - - // Serialize DONE token into the response packet - responseMessage.Add(doneToken); - - // Return only a single message with the collection - return new TDSMessageCollection(responseMessage); - } - } - - // Check if packet size filter is applied - if (ServerArguments.PacketSizeFilter != null) - { - // Check if requested packet size is the same as the filter specified - if (loginRequest.PacketSize != ServerArguments.PacketSizeFilter.Value) - { - // Log request to which we're about to send a failure - TDSUtilities.Log(Arguments.Log, "Request", loginRequest); - - // Prepare ERROR token with the reason - TDSErrorToken errorToken = new TDSErrorToken(1919, 1, 14, string.Format("Received packet size \"{0}\", expected \"{1}\"", loginRequest.PacketSize, ServerArguments.PacketSizeFilter.Value), Arguments.ServerName); - - // Log response - TDSUtilities.Log(Arguments.Log, "Response", errorToken); - - // Serialize the errorToken token into the response packet - TDSMessage responseMessage = new TDSMessage(TDSMessageType.Response, errorToken); - - // Prepare ERROR token with the final errorToken - errorToken = new TDSErrorToken(1919, 1, 14, "Connection is denied by packet size filter", Arguments.ServerName); - - // Log response - TDSUtilities.Log(Arguments.Log, "Response", errorToken); - - // Serialize the errorToken token into the response packet - responseMessage.Add(errorToken); - - // Create DONE token - TDSDoneToken doneToken = new TDSDoneToken(TDSDoneTokenStatusType.Final | TDSDoneTokenStatusType.Error); - - // Log response - TDSUtilities.Log(Arguments.Log, "Response", doneToken); - - // Serialize DONE token into the response packet - responseMessage.Add(doneToken); - - // Return only a single message with the collection - return new TDSMessageCollection(responseMessage); - } - } - - // If we have an application name filter - if (ServerArguments.ApplicationNameFilter != null) - { - // If we are supposed to block this connection attempt - if (loginRequest.ApplicationName.Equals(ServerArguments.ApplicationNameFilter, System.StringComparison.OrdinalIgnoreCase)) - { - // Log request to which we're about to send a failure - TDSUtilities.Log(Arguments.Log, "Request", loginRequest); - - // Prepare ERROR token with the denial details - TDSErrorToken errorToken = new TDSErrorToken(18456, 1, 14, "Received application name: " + loginRequest.ApplicationName, Arguments.ServerName); - - // Log response - TDSUtilities.Log(Arguments.Log, "Response", errorToken); - - // Serialize the error token into the response packet - TDSMessage responseMessage = new TDSMessage(TDSMessageType.Response, errorToken); - - // Prepare ERROR token for the final decision - errorToken = new TDSErrorToken(18456, 1, 14, "Connection is denied by application name filter", Arguments.ServerName); - - // Log response - TDSUtilities.Log(Arguments.Log, "Response", errorToken); - - // Serialize the error token into the response packet - responseMessage.Add(errorToken); - - // Create DONE token - TDSDoneToken doneToken = new TDSDoneToken(TDSDoneTokenStatusType.Final | TDSDoneTokenStatusType.Error); - - // Log response - TDSUtilities.Log(Arguments.Log, "Response", doneToken); - - // Serialize DONE token into the response packet - responseMessage.Add(doneToken); - - // Put a single message into the collection and return it - return new TDSMessageCollection(responseMessage); - } - } - } - - // Return login response from the base class - return base.OnLogin7Request(session, request); - } - } -} diff --git a/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.Servers/AuthenticatingTdsServer.cs b/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.Servers/AuthenticatingTdsServer.cs new file mode 100644 index 0000000000..4db5439845 --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.Servers/AuthenticatingTdsServer.cs @@ -0,0 +1,213 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Microsoft.SqlServer.TDS.Done; +using Microsoft.SqlServer.TDS.EndPoint; +using Microsoft.SqlServer.TDS.Error; +using Microsoft.SqlServer.TDS.Login7; + +namespace Microsoft.SqlServer.TDS.Servers +{ + /// + /// TDS Server that authenticates clients according to the requested parameters + /// + public class AuthenticatingTdsServer : GenericTdsServer + { + /// + /// Initialization constructor + /// + public AuthenticatingTdsServer() + : this(new AuthenticatingTdsServerArguments()) + { + } + + /// + /// Initialization constructor + /// + public AuthenticatingTdsServer(AuthenticatingTdsServerArguments arguments) : + base(arguments) + { + } + + /// + /// Handler for login request + /// + public override TDSMessageCollection OnLogin7Request(ITDSServerSession session, TDSMessage request) + { + // Inflate login7 request from the message + TDSLogin7Token loginRequest = request[0] as TDSLogin7Token; + + // Check if we're still processing normal login + if (Arguments.ApplicationIntentFilter != ApplicationIntentFilterType.All) + { + // Check filter + if ((Arguments.ApplicationIntentFilter == ApplicationIntentFilterType.ReadOnly && loginRequest.TypeFlags.ReadOnlyIntent != TDSLogin7TypeFlagsReadOnlyIntent.ReadOnly) + || (Arguments.ApplicationIntentFilter == ApplicationIntentFilterType.None)) + { + // Log request to which we're about to send a failure + TDSUtilities.Log(Arguments.Log, "Request", loginRequest); + + // Prepare ERROR token with the denial details + TDSErrorToken errorToken = new TDSErrorToken(18456, 1, 14, "Received application intent: " + loginRequest.TypeFlags.ReadOnlyIntent.ToString(), Arguments.ServerName); + + // Log response + TDSUtilities.Log(Arguments.Log, "Response", errorToken); + + // Serialize the error token into the response packet + TDSMessage responseMessage = new TDSMessage(TDSMessageType.Response, errorToken); + + // Prepare ERROR token for the final decision + errorToken = new TDSErrorToken(18456, 1, 14, "Connection is denied by application intent filter", Arguments.ServerName); + + // Log response + TDSUtilities.Log(Arguments.Log, "Response", errorToken); + + // Serialize the error token into the response packet + responseMessage.Add(errorToken); + + // Create DONE token + TDSDoneToken doneToken = new TDSDoneToken(TDSDoneTokenStatusType.Final | TDSDoneTokenStatusType.Error); + + // Log response + TDSUtilities.Log(Arguments.Log, "Response", doneToken); + + // Serialize DONE token into the response packet + responseMessage.Add(doneToken); + + // Put a single message into the collection and return it + return new TDSMessageCollection(responseMessage); + } + } + + // Check if we're still processing normal login and there's a filter to check + if (Arguments.ServerNameFilterType != ServerNameFilterType.None) + { + // Check each algorithm + if ((Arguments.ServerNameFilterType == ServerNameFilterType.Equals && string.Compare(Arguments.ServerNameFilter, loginRequest.ServerName, true) != 0) + || (Arguments.ServerNameFilterType == ServerNameFilterType.StartsWith && !loginRequest.ServerName.StartsWith(Arguments.ServerNameFilter)) + || (Arguments.ServerNameFilterType == ServerNameFilterType.EndsWith && !loginRequest.ServerName.EndsWith(Arguments.ServerNameFilter)) + || (Arguments.ServerNameFilterType == ServerNameFilterType.Contains && !loginRequest.ServerName.Contains(Arguments.ServerNameFilter))) + { + // Log request to which we're about to send a failure + TDSUtilities.Log(Arguments.Log, "Request", loginRequest); + + // Prepare ERROR token with the reason + TDSErrorToken errorToken = new TDSErrorToken(18456, 1, 14, string.Format("Received server name \"{0}\", expected \"{1}\" using \"{2}\" algorithm", loginRequest.ServerName, Arguments.ServerNameFilter, Arguments.ServerNameFilterType), Arguments.ServerName); + + // Log response + TDSUtilities.Log(Arguments.Log, "Response", errorToken); + + // Serialize the errorToken token into the response packet + TDSMessage responseMessage = new TDSMessage(TDSMessageType.Response, errorToken); + + // Prepare ERROR token with the final errorToken + errorToken = new TDSErrorToken(18456, 1, 14, "Connection is denied by server name filter", Arguments.ServerName); + + // Log response + TDSUtilities.Log(Arguments.Log, "Response", errorToken); + + // Serialize the errorToken token into the response packet + responseMessage.Add(errorToken); + + // Create DONE token + TDSDoneToken doneToken = new TDSDoneToken(TDSDoneTokenStatusType.Final | TDSDoneTokenStatusType.Error); + + // Log response + TDSUtilities.Log(Arguments.Log, "Response", doneToken); + + // Serialize DONE token into the response packet + responseMessage.Add(doneToken); + + // Return only a single message with the collection + return new TDSMessageCollection(responseMessage); + } + } + + // Check if packet size filter is applied + if (Arguments.PacketSizeFilter != null) + { + // Check if requested packet size is the same as the filter specified + if (loginRequest.PacketSize != Arguments.PacketSizeFilter.Value) + { + // Log request to which we're about to send a failure + TDSUtilities.Log(Arguments.Log, "Request", loginRequest); + + // Prepare ERROR token with the reason + TDSErrorToken errorToken = new TDSErrorToken(1919, 1, 14, string.Format("Received packet size \"{0}\", expected \"{1}\"", loginRequest.PacketSize, Arguments.PacketSizeFilter.Value), Arguments.ServerName); + + // Log response + TDSUtilities.Log(Arguments.Log, "Response", errorToken); + + // Serialize the errorToken token into the response packet + TDSMessage responseMessage = new TDSMessage(TDSMessageType.Response, errorToken); + + // Prepare ERROR token with the final errorToken + errorToken = new TDSErrorToken(1919, 1, 14, "Connection is denied by packet size filter", Arguments.ServerName); + + // Log response + TDSUtilities.Log(Arguments.Log, "Response", errorToken); + + // Serialize the errorToken token into the response packet + responseMessage.Add(errorToken); + + // Create DONE token + TDSDoneToken doneToken = new TDSDoneToken(TDSDoneTokenStatusType.Final | TDSDoneTokenStatusType.Error); + + // Log response + TDSUtilities.Log(Arguments.Log, "Response", doneToken); + + // Serialize DONE token into the response packet + responseMessage.Add(doneToken); + + // Return only a single message with the collection + return new TDSMessageCollection(responseMessage); + } + } + + // If we have an application name filter + if (Arguments.ApplicationNameFilter != null) + { + // If we are supposed to block this connection attempt + if (loginRequest.ApplicationName.Equals(Arguments.ApplicationNameFilter, System.StringComparison.OrdinalIgnoreCase)) + { + // Log request to which we're about to send a failure + TDSUtilities.Log(Arguments.Log, "Request", loginRequest); + + // Prepare ERROR token with the denial details + TDSErrorToken errorToken = new TDSErrorToken(18456, 1, 14, "Received application name: " + loginRequest.ApplicationName, Arguments.ServerName); + + // Log response + TDSUtilities.Log(Arguments.Log, "Response", errorToken); + + // Serialize the error token into the response packet + TDSMessage responseMessage = new TDSMessage(TDSMessageType.Response, errorToken); + + // Prepare ERROR token for the final decision + errorToken = new TDSErrorToken(18456, 1, 14, "Connection is denied by application name filter", Arguments.ServerName); + + // Log response + TDSUtilities.Log(Arguments.Log, "Response", errorToken); + + // Serialize the error token into the response packet + responseMessage.Add(errorToken); + + // Create DONE token + TDSDoneToken doneToken = new TDSDoneToken(TDSDoneTokenStatusType.Final | TDSDoneTokenStatusType.Error); + + // Log response + TDSUtilities.Log(Arguments.Log, "Response", doneToken); + + // Serialize DONE token into the response packet + responseMessage.Add(doneToken); + + // Put a single message into the collection and return it + return new TDSMessageCollection(responseMessage); + } + } + + // Return login response from the base class + return base.OnLogin7Request(session, request); + } + } +} diff --git a/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.Servers/AuthenticatingTDSServerArguments.cs b/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.Servers/AuthenticatingTdsServerArguments.cs similarity index 58% rename from src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.Servers/AuthenticatingTDSServerArguments.cs rename to src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.Servers/AuthenticatingTdsServerArguments.cs index dcb812a648..d9f36a5a49 100644 --- a/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.Servers/AuthenticatingTDSServerArguments.cs +++ b/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.Servers/AuthenticatingTdsServerArguments.cs @@ -7,43 +7,31 @@ namespace Microsoft.SqlServer.TDS.Servers /// /// Arguments for authenticating TDS Server /// - public class AuthenticatingTDSServerArguments : TDSServerArguments + public class AuthenticatingTdsServerArguments : TdsServerArguments { /// /// Type of the application intent filter /// - public ApplicationIntentFilterType ApplicationIntentFilter { get; set; } + public ApplicationIntentFilterType ApplicationIntentFilter { get; set; } = ApplicationIntentFilterType.All; /// /// Filter for server name /// - public string ServerNameFilter { get; set; } + public string ServerNameFilter { get; set; } = string.Empty; /// /// Type of the filtering algorithm to use /// - public ServerNameFilterType ServerNameFilterType { get; set; } + public ServerNameFilterType ServerNameFilterType { get; set; } = ServerNameFilterType.None; /// /// TDS packet size filtering /// - public ushort? PacketSizeFilter { get; set; } + public ushort? PacketSizeFilter { get; set; } = null; /// /// Filter for application name /// - public string ApplicationNameFilter { get; set; } - - /// - /// Initialization constructor - /// - public AuthenticatingTDSServerArguments() - { - // Allow everyone to connect - ApplicationIntentFilter = ApplicationIntentFilterType.All; - - // By default we don't turn on server name filter - ServerNameFilterType = Servers.ServerNameFilterType.None; - } + public string ApplicationNameFilter { get; set; } = string.Empty; } } diff --git a/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.Servers/FederatedAuthenticationNegativeTDSServer.cs b/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.Servers/FederatedAuthenticationNegativeTDSServer.cs deleted file mode 100644 index 40d4791f13..0000000000 --- a/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.Servers/FederatedAuthenticationNegativeTDSServer.cs +++ /dev/null @@ -1,148 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -using System.Linq; -using Microsoft.SqlServer.TDS.EndPoint; -using Microsoft.SqlServer.TDS.FeatureExtAck; -using Microsoft.SqlServer.TDS.PreLogin; - -namespace Microsoft.SqlServer.TDS.Servers -{ - /// - /// TDS Server that generates invalid TDS scenarios according to the requested parameters - /// - public class FederatedAuthenticationNegativeTDSServer : GenericTDSServer - { - /// - /// Initialization constructor - /// - public FederatedAuthenticationNegativeTDSServer() : - this(new FederatedAuthenticationNegativeTDSServerArguments()) - { - } - - /// - /// Initialization constructor - /// - public FederatedAuthenticationNegativeTDSServer(FederatedAuthenticationNegativeTDSServerArguments arguments) : - base(arguments) - { - } - - /// - /// Handler for login request - /// - public override TDSMessageCollection OnPreLoginRequest(ITDSServerSession session, TDSMessage request) - { - // Get the collection from a valid On PreLogin Request - TDSMessageCollection preLoginCollection = base.OnPreLoginRequest(session, request); - - // Check if arguments are of the Federated Authentication server - if (Arguments is FederatedAuthenticationNegativeTDSServerArguments) - { - // Cast to federated authentication server arguments - FederatedAuthenticationNegativeTDSServerArguments ServerArguments = Arguments as FederatedAuthenticationNegativeTDSServerArguments; - - // Find the is token carrying on TDSPreLoginToken - TDSPreLoginToken preLoginToken = preLoginCollection.Find(message => message.Exists(packetToken => packetToken is TDSPreLoginToken)). - Find(packetToken => packetToken is TDSPreLoginToken) as TDSPreLoginToken; - - switch (ServerArguments.Scenario) - { - case FederatedAuthenticationNegativeTDSScenarioType.NonceMissingInFedAuthPreLogin: - { - // If we have the prelogin token - if (preLoginToken != null && preLoginToken.Nonce != null) - { - // Nullify the nonce from the Token - preLoginToken.Nonce = null; - } - - break; - } - - case FederatedAuthenticationNegativeTDSScenarioType.InvalidB_FEDAUTHREQUIREDResponse: - { - // If we have the prelogin token - if (preLoginToken != null) - { - // Set an illegal value for B_FEDAUTHREQURED - preLoginToken.FedAuthRequired = TdsPreLoginFedAuthRequiredOption.Illegal; - } - - break; - } - } - } - - // Return the collection - return preLoginCollection; - } - - /// - /// Handler for login request - /// - public override TDSMessageCollection OnLogin7Request(ITDSServerSession session, TDSMessage request) - { - // Get the collection from the normal behavior On Login7 Request - TDSMessageCollection login7Collection = base.OnLogin7Request(session, request); - - // Check if arguments are of the Federated Authentication server - if (Arguments is FederatedAuthenticationNegativeTDSServerArguments) - { - // Cast to federated authentication server arguments - FederatedAuthenticationNegativeTDSServerArguments ServerArguments = Arguments as FederatedAuthenticationNegativeTDSServerArguments; - - // Get the Federated Authentication ExtAck from Login 7 - TDSFeatureExtAckFederatedAuthenticationOption fedAutExtAct = GetFeatureExtAckFederatedAuthenticationOptionFromLogin7(login7Collection); - - // If not found, return the base collection intact - if (fedAutExtAct != null) - { - switch (ServerArguments.Scenario) - { - case FederatedAuthenticationNegativeTDSScenarioType.NonceMissingInFedAuthFEATUREXTACK: - { - // Delete the nonce from the Token - fedAutExtAct.ClientNonce = null; - - break; - } - case FederatedAuthenticationNegativeTDSScenarioType.FedAuthMissingInFEATUREEXTACK: - { - // Remove the Fed Auth Ext Ack from the options list in the FeatureExtAckToken - GetFeatureExtAckTokenFromLogin7(login7Collection).Options.Remove(fedAutExtAct); - - break; - } - case FederatedAuthenticationNegativeTDSScenarioType.SignatureMissingInFedAuthFEATUREXTACK: - { - // Delete the signature from the Token - fedAutExtAct.Signature = null; - - break; - } - } - } - } - - // Return the collection - return login7Collection; - } - - private TDSFeatureExtAckToken GetFeatureExtAckTokenFromLogin7(TDSMessageCollection login7Collection) - { - // Find the is token carrying on TDSFeatureExtAckToken - return login7Collection.Find(m => m.Exists(p => p is TDSFeatureExtAckToken)). - Find(t => t is TDSFeatureExtAckToken) as TDSFeatureExtAckToken; - } - - private TDSFeatureExtAckFederatedAuthenticationOption GetFeatureExtAckFederatedAuthenticationOptionFromLogin7(TDSMessageCollection login7Collection) - { - // Get the Fed Auth Ext Ack from the list of options in the feature ExtAck - return GetFeatureExtAckTokenFromLogin7(login7Collection).Options. - Where(o => o is TDSFeatureExtAckFederatedAuthenticationOption).FirstOrDefault() as TDSFeatureExtAckFederatedAuthenticationOption; - } - } -} diff --git a/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.Servers/FederatedAuthenticationNegativeTDSScenarioType.cs b/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.Servers/FederatedAuthenticationNegativeTdsScenarioType.cs similarity index 94% rename from src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.Servers/FederatedAuthenticationNegativeTDSScenarioType.cs rename to src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.Servers/FederatedAuthenticationNegativeTdsScenarioType.cs index 11baa170d6..f35f69c22d 100644 --- a/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.Servers/FederatedAuthenticationNegativeTDSScenarioType.cs +++ b/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.Servers/FederatedAuthenticationNegativeTdsScenarioType.cs @@ -4,7 +4,7 @@ namespace Microsoft.SqlServer.TDS.Servers { - public enum FederatedAuthenticationNegativeTDSScenarioType : int + public enum FederatedAuthenticationNegativeTdsScenarioType : int { /// /// Valid Scenario. Do not perform negative activity. diff --git a/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.Servers/FederatedAuthenticationNegativeTdsServer.cs b/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.Servers/FederatedAuthenticationNegativeTdsServer.cs new file mode 100644 index 0000000000..53a68c70ea --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.Servers/FederatedAuthenticationNegativeTdsServer.cs @@ -0,0 +1,134 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Linq; +using Microsoft.SqlServer.TDS.EndPoint; +using Microsoft.SqlServer.TDS.FeatureExtAck; +using Microsoft.SqlServer.TDS.PreLogin; + +namespace Microsoft.SqlServer.TDS.Servers +{ + /// + /// TDS Server that generates invalid TDS scenarios according to the requested parameters + /// + public class FederatedAuthenticationNegativeTdsServer : GenericTdsServer + { + /// + /// Initialization constructor + /// + public FederatedAuthenticationNegativeTdsServer() : + this(new FederatedAuthenticationNegativeTdsServerArguments()) + { + } + + /// + /// Initialization constructor + /// + public FederatedAuthenticationNegativeTdsServer(FederatedAuthenticationNegativeTdsServerArguments arguments) : + base(arguments) + { + } + + /// + /// Handler for login request + /// + public override TDSMessageCollection OnPreLoginRequest(ITDSServerSession session, TDSMessage request) + { + // Get the collection from a valid On PreLogin Request + TDSMessageCollection preLoginCollection = base.OnPreLoginRequest(session, request); + + // Find the is token carrying on TDSPreLoginToken + TDSPreLoginToken preLoginToken = preLoginCollection.Find(message => message.Exists(packetToken => packetToken is TDSPreLoginToken)). + Find(packetToken => packetToken is TDSPreLoginToken) as TDSPreLoginToken; + + switch (Arguments.Scenario) + { + case FederatedAuthenticationNegativeTdsScenarioType.NonceMissingInFedAuthPreLogin: + { + // If we have the prelogin token + if (preLoginToken != null && preLoginToken.Nonce != null) + { + // Nullify the nonce from the Token + preLoginToken.Nonce = null; + } + + break; + } + + case FederatedAuthenticationNegativeTdsScenarioType.InvalidB_FEDAUTHREQUIREDResponse: + { + // If we have the prelogin token + if (preLoginToken != null) + { + // Set an illegal value for B_FEDAUTHREQURED + preLoginToken.FedAuthRequired = TdsPreLoginFedAuthRequiredOption.Illegal; + } + + break; + } + } + + // Return the collection + return preLoginCollection; + } + + /// + /// Handler for login request + /// + public override TDSMessageCollection OnLogin7Request(ITDSServerSession session, TDSMessage request) + { + // Get the collection from the normal behavior On Login7 Request + TDSMessageCollection login7Collection = base.OnLogin7Request(session, request); + + // Get the Federated Authentication ExtAck from Login 7 + TDSFeatureExtAckFederatedAuthenticationOption fedAutExtAct = GetFeatureExtAckFederatedAuthenticationOptionFromLogin7(login7Collection); + + // If not found, return the base collection intact + if (fedAutExtAct != null) + { + switch (Arguments.Scenario) + { + case FederatedAuthenticationNegativeTdsScenarioType.NonceMissingInFedAuthFEATUREXTACK: + { + // Delete the nonce from the Token + fedAutExtAct.ClientNonce = null; + + break; + } + case FederatedAuthenticationNegativeTdsScenarioType.FedAuthMissingInFEATUREEXTACK: + { + // Remove the Fed Auth Ext Ack from the options list in the FeatureExtAckToken + GetFeatureExtAckTokenFromLogin7(login7Collection).Options.Remove(fedAutExtAct); + + break; + } + case FederatedAuthenticationNegativeTdsScenarioType.SignatureMissingInFedAuthFEATUREXTACK: + { + // Delete the signature from the Token + fedAutExtAct.Signature = null; + + break; + } + } + } + + // Return the collection + return login7Collection; + } + + private TDSFeatureExtAckToken GetFeatureExtAckTokenFromLogin7(TDSMessageCollection login7Collection) + { + // Find the is token carrying on TDSFeatureExtAckToken + return login7Collection.Find(m => m.Exists(p => p is TDSFeatureExtAckToken)). + Find(t => t is TDSFeatureExtAckToken) as TDSFeatureExtAckToken; + } + + private TDSFeatureExtAckFederatedAuthenticationOption GetFeatureExtAckFederatedAuthenticationOptionFromLogin7(TDSMessageCollection login7Collection) + { + // Get the Fed Auth Ext Ack from the list of options in the feature ExtAck + return GetFeatureExtAckTokenFromLogin7(login7Collection).Options. + Where(o => o is TDSFeatureExtAckFederatedAuthenticationOption).FirstOrDefault() as TDSFeatureExtAckFederatedAuthenticationOption; + } + } +} diff --git a/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.Servers/FederatedAuthenticationNegativeTDSServerArguments.cs b/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.Servers/FederatedAuthenticationNegativeTdsServerArguments.cs similarity index 56% rename from src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.Servers/FederatedAuthenticationNegativeTDSServerArguments.cs rename to src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.Servers/FederatedAuthenticationNegativeTdsServerArguments.cs index 67143d645b..19fd43aab5 100644 --- a/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.Servers/FederatedAuthenticationNegativeTDSServerArguments.cs +++ b/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.Servers/FederatedAuthenticationNegativeTdsServerArguments.cs @@ -7,18 +7,11 @@ namespace Microsoft.SqlServer.TDS.Servers /// /// Arguments for Fed Auth Negative TDS Server /// - public class FederatedAuthenticationNegativeTDSServerArguments : TDSServerArguments + public class FederatedAuthenticationNegativeTdsServerArguments : TdsServerArguments { /// /// Type of the Fed Auth Negative TDS Server /// - public FederatedAuthenticationNegativeTDSScenarioType Scenario { get; set; } - - /// - /// Initialization constructor - /// - public FederatedAuthenticationNegativeTDSServerArguments() - { - } + public FederatedAuthenticationNegativeTdsScenarioType Scenario { get; set; } = FederatedAuthenticationNegativeTdsScenarioType.ValidScenario; } } diff --git a/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.Servers/GenericTDSServer.cs b/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.Servers/GenericTdsServer.cs similarity index 92% rename from src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.Servers/GenericTDSServer.cs rename to src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.Servers/GenericTdsServer.cs index ac04fd2f57..c17726fb8b 100644 --- a/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.Servers/GenericTDSServer.cs +++ b/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.Servers/GenericTdsServer.cs @@ -4,6 +4,8 @@ using System; using System.Linq; +using System.Net; +using System.Runtime.CompilerServices; using System.Security.Cryptography; using System.Threading; using Microsoft.SqlServer.TDS.Authentication; @@ -25,7 +27,8 @@ namespace Microsoft.SqlServer.TDS.Servers /// /// Generic TDS server without specialization /// - public class GenericTDSServer : ITDSServer + public abstract class GenericTdsServer : ITDSServer, IDisposable + where T : TdsServerArguments { /// /// Delegate to be called when a LOGIN7 request has been received and is @@ -34,7 +37,6 @@ public class GenericTDSServer : ITDSServer /// public delegate void OnLogin7ValidatedDelegate( TDSLogin7Token login7Token); - public OnLogin7ValidatedDelegate OnLogin7Validated { private get; set; } /// /// Delegate to be called when authentication is completed and TDSResponse @@ -42,23 +44,12 @@ public delegate void OnLogin7ValidatedDelegate( /// public delegate void OnAuthenticationCompletedDelegate( TDSMessage response); - public OnAuthenticationCompletedDelegate OnAuthenticationResponseCompleted { private get; set; } /// /// Default feature extension version supported on the server for vector support. /// public const byte DefaultSupportedVectorFeatureExtVersion = 0x01; - /// - /// Property for setting server version for vector feature extension. - /// - public bool EnableVectorFeatureExt { get; set; } = false; - - /// - /// Property for setting server version for vector feature extension. - /// - public byte ServerSupportedVectorFeatureExtVersion { get; set; } = DefaultSupportedVectorFeatureExtVersion; - /// /// Client version for vector FeatureExtension. /// @@ -70,27 +61,16 @@ public delegate void OnAuthenticationCompletedDelegate( private int _sessionCount = 0; /// - /// Server configuration + /// Counts pre-login requests to the server. /// - protected TDSServerArguments Arguments { get; set; } + private int _preLoginCount = 0; - /// - /// Query engine instance - /// - protected QueryEngine Engine { get; set; } - - /// - /// Default constructor - /// - public GenericTDSServer() : - this(new TDSServerArguments()) - { - } + private TDSServerEndPoint _endpoint; /// /// Initialization constructor /// - public GenericTDSServer(TDSServerArguments arguments) : + public GenericTdsServer(T arguments) : this(arguments, new QueryEngine(arguments)) { } @@ -98,7 +78,7 @@ public GenericTDSServer(TDSServerArguments arguments) : /// /// Initialization constructor /// - public GenericTDSServer(TDSServerArguments arguments, QueryEngine queryEngine) + public GenericTdsServer(T arguments, QueryEngine queryEngine) { // Save arguments Arguments = arguments; @@ -110,6 +90,50 @@ public GenericTDSServer(TDSServerArguments arguments, QueryEngine queryEngine) Engine.Log = Arguments.Log; } + public IPEndPoint EndPoint => _endpoint.ServerEndPoint; + + /// + /// Server configuration + /// + protected T Arguments { get; set; } + + /// + /// Query engine instance + /// + protected QueryEngine Engine { get; set; } + + /// + /// Counts pre-login requests to the server. + /// + public int PreLoginCount => _preLoginCount; + + /// + /// Property for setting server version for vector feature extension. + /// + public bool EnableVectorFeatureExt { get; set; } = false; + + /// + /// Property for setting server version for vector feature extension. + /// + public byte ServerSupportedVectorFeatureExtVersion { get; set; } = DefaultSupportedVectorFeatureExtVersion; + + public OnAuthenticationCompletedDelegate OnAuthenticationResponseCompleted { private get; set; } + + public OnLogin7ValidatedDelegate OnLogin7Validated { private get; set; } + + + public void Start([CallerMemberName] string methodName = "") + { + if (_endpoint != null) + { + throw new InvalidOperationException("Server is already started"); + } + _endpoint = new TDSServerEndPoint(this) { ServerEndPoint = new IPEndPoint(IPAddress.Any, 0) }; + _endpoint.EndpointName = methodName; + _endpoint.EventLog = Arguments.Log; + _endpoint.Start(); + } + /// /// Create a new session on the server /// @@ -120,7 +144,7 @@ public virtual ITDSServerSession OpenSession() Interlocked.Increment(ref _sessionCount); // Create a new session - GenericTDSServerSession session = new GenericTDSServerSession(this, (uint)_sessionCount); + GenericTdsServerSession session = new GenericTdsServerSession(this, (uint)_sessionCount); // Use configured encryption certificate and protocols session.EncryptionCertificate = Arguments.EncryptionCertificate; @@ -142,8 +166,11 @@ public virtual void CloseSession(ITDSServerSession session) /// public virtual TDSMessageCollection OnPreLoginRequest(ITDSServerSession session, TDSMessage request) { + Interlocked.Increment(ref _preLoginCount); + // Inflate pre-login request from the message TDSPreLoginToken preLoginRequest = request[0] as TDSPreLoginToken; + GenericTdsServerSession genericTdsServerSession = session as GenericTdsServerSession; // Log request TDSUtilities.Log(Arguments.Log, "Request", preLoginRequest); @@ -158,7 +185,7 @@ public virtual TDSMessageCollection OnPreLoginRequest(ITDSServerSession session, TDSPreLoginToken preLoginToken = new TDSPreLoginToken(Arguments.ServerVersion, serverResponse, false); // TDS server doesn't support MARS // Cache the received Nonce into the session - (session as GenericTDSServerSession).ClientNonce = preLoginRequest.Nonce; + genericTdsServerSession.ClientNonce = preLoginRequest.Nonce; // Check if the server has been started up as requiring FedAuth when choosing between SSPI and FedAuth if (Arguments.FedAuthRequiredPreLoginOption == TdsPreLoginFedAuthRequiredOption.FedAuthRequired) @@ -170,7 +197,7 @@ public virtual TDSMessageCollection OnPreLoginRequest(ITDSServerSession session, } // Keep the federated authentication required flag in the server session - (session as GenericTDSServerSession).FedAuthRequiredPreLoginServerResponse = preLoginToken.FedAuthRequired; + genericTdsServerSession.FedAuthRequiredPreLoginServerResponse = preLoginToken.FedAuthRequired; if (preLoginRequest.Nonce != null) { @@ -180,7 +207,7 @@ public virtual TDSMessageCollection OnPreLoginRequest(ITDSServerSession session, } // Cache the server Nonce in a session - (session as GenericTDSServerSession).ServerNonce = preLoginToken.Nonce; + genericTdsServerSession.ServerNonce = preLoginToken.Nonce; // Log response TDSUtilities.Log(Arguments.Log, "Response", preLoginToken); @@ -244,7 +271,7 @@ public virtual TDSMessageCollection OnLogin7Request(ITDSServerSession session, T TDSLogin7SessionRecoveryOptionToken sessionStateOption = option as TDSLogin7SessionRecoveryOptionToken; // Inflate session state - (session as GenericTDSServerSession).Inflate(sessionStateOption.Initial, sessionStateOption.Current); + (session as GenericTdsServerSession).Inflate(sessionStateOption.Initial, sessionStateOption.Current); break; } @@ -266,7 +293,7 @@ public virtual TDSMessageCollection OnLogin7Request(ITDSServerSession session, T } // Save the fed auth library to be used - (session as GenericTDSServerSession).FederatedAuthenticationLibrary = federatedAuthenticationOption.Library; + (session as GenericTdsServerSession).FederatedAuthenticationLibrary = federatedAuthenticationOption.Library; break; } @@ -542,7 +569,7 @@ protected virtual TDSMessageCollection OnAuthenticationCompleted(ITDSServerSessi responseMessage.Add(infoToken); // Create new collation change token - envChange = new TDSEnvChangeToken(TDSEnvChangeTokenType.SQLCollation, (session as GenericTDSServerSession).Collation); + envChange = new TDSEnvChangeToken(TDSEnvChangeTokenType.SQLCollation, (session as GenericTdsServerSession).Collation); // Log response TDSUtilities.Log(Arguments.Log, "Response", envChange); @@ -551,7 +578,7 @@ protected virtual TDSMessageCollection OnAuthenticationCompleted(ITDSServerSessi responseMessage.Add(envChange); // Create new language change token - envChange = new TDSEnvChangeToken(TDSEnvChangeTokenType.Language, LanguageString.ToString((session as GenericTDSServerSession).Language)); + envChange = new TDSEnvChangeToken(TDSEnvChangeTokenType.Language, LanguageString.ToString((session as GenericTdsServerSession).Language)); // Log response TDSUtilities.Log(Arguments.Log, "Response", envChange); @@ -593,7 +620,7 @@ protected virtual TDSMessageCollection OnAuthenticationCompleted(ITDSServerSessi if (session.IsSessionRecoveryEnabled) { // Create Feature extension Ack token - TDSFeatureExtAckToken featureExtActToken = new TDSFeatureExtAckToken(new TDSFeatureExtAckSessionStateOption((session as GenericTDSServerSession).Deflate())); + TDSFeatureExtAckToken featureExtActToken = new TDSFeatureExtAckToken(new TDSFeatureExtAckSessionStateOption((session as GenericTdsServerSession).Deflate())); // Log response TDSUtilities.Log(Arguments.Log, "Response", featureExtActToken); @@ -654,6 +681,16 @@ protected virtual TDSMessageCollection OnAuthenticationCompleted(ITDSServerSessi } } + if (!string.IsNullOrEmpty(Arguments.FailoverPartner)) + { + envChange = new TDSEnvChangeToken(TDSEnvChangeTokenType.RealTimeLogShipping, Arguments.FailoverPartner); + + // Log response + TDSUtilities.Log(Arguments.Log, "Response", envChange); + + responseMessage.Add(envChange); + } + // Create DONE token TDSDoneToken doneToken = new TDSDoneToken(TDSDoneTokenStatusType.Final); @@ -688,7 +725,7 @@ protected virtual TDSMessageCollection OnFederatedAuthenticationCompleted(ITDSSe try { // Get the Federated Authentication ticket using RPS - decryptedTicket = FederatedAuthenticationTicketService.DecryptTicket((session as GenericTDSServerSession).FederatedAuthenticationLibrary, ticket); + decryptedTicket = FederatedAuthenticationTicketService.DecryptTicket((session as GenericTdsServerSession).FederatedAuthenticationLibrary, ticket); if (decryptedTicket is RpsTicket) { @@ -719,17 +756,17 @@ protected virtual TDSMessageCollection OnFederatedAuthenticationCompleted(ITDSSe // Create federated authentication extension option TDSFeatureExtAckFederatedAuthenticationOption federatedAuthenticationOption; - if ((session as GenericTDSServerSession).FederatedAuthenticationLibrary == TDSFedAuthLibraryType.MSAL) + if ((session as GenericTdsServerSession).FederatedAuthenticationLibrary == TDSFedAuthLibraryType.MSAL) { // For the time being, fake fedauth tokens are used for ADAL, so decryptedTicket is null. federatedAuthenticationOption = - new TDSFeatureExtAckFederatedAuthenticationOption((session as GenericTDSServerSession).ClientNonce, null); + new TDSFeatureExtAckFederatedAuthenticationOption((session as GenericTdsServerSession).ClientNonce, null); } else { federatedAuthenticationOption = - new TDSFeatureExtAckFederatedAuthenticationOption((session as GenericTDSServerSession).ClientNonce, - decryptedTicket.GetSignature((session as GenericTDSServerSession).ClientNonce)); + new TDSFeatureExtAckFederatedAuthenticationOption((session as GenericTdsServerSession).ClientNonce, + decryptedTicket.GetSignature((session as GenericTdsServerSession).ClientNonce)); } // Look for feature extension token @@ -764,12 +801,12 @@ protected virtual TDSMessageCollection OnFederatedAuthenticationCompleted(ITDSSe protected virtual TDSMessageCollection CheckFederatedAuthenticationOption(ITDSServerSession session, TDSLogin7FedAuthOptionToken federatedAuthenticationOption) { // Check if server's prelogin response for FedAuthRequired prelogin option is echoed back correctly in FedAuth Feature Extenion Echo - if (federatedAuthenticationOption.Echo != (session as GenericTDSServerSession).FedAuthRequiredPreLoginServerResponse) + if (federatedAuthenticationOption.Echo != (session as GenericTdsServerSession).FedAuthRequiredPreLoginServerResponse) { // Create Error message string message = string.Format("FEDAUTHREQUIRED option in the prelogin response is not echoed back correctly: in prelogin response, it is {0} and in login, it is {1}: ", - (session as GenericTDSServerSession).FedAuthRequiredPreLoginServerResponse, + (session as GenericTdsServerSession).FedAuthRequiredPreLoginServerResponse, federatedAuthenticationOption.Echo); // Create errorToken token @@ -790,7 +827,7 @@ protected virtual TDSMessageCollection CheckFederatedAuthenticationOption(ITDSSe // Check if the nonce exists if ((federatedAuthenticationOption.Nonce == null && federatedAuthenticationOption.Library == TDSFedAuthLibraryType.IDCRL) - || !AreEqual((session as GenericTDSServerSession).ServerNonce, federatedAuthenticationOption.Nonce)) + || !AreEqual((session as GenericTdsServerSession).ServerNonce, federatedAuthenticationOption.Nonce)) { // Error message string message = string.Format("Unexpected NONCEOPT specified in the Federated authentication feature extension"); @@ -880,5 +917,11 @@ private bool AreEqual(byte[] left, byte[] right) return left.SequenceEqual(right); } + + public virtual void Dispose() + { + _endpoint?.Dispose(); + _endpoint = null; + } } } diff --git a/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.Servers/GenericTDSServerSession.cs b/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.Servers/GenericTdsServerSession.cs similarity index 99% rename from src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.Servers/GenericTDSServerSession.cs rename to src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.Servers/GenericTdsServerSession.cs index e9e65d5f8f..2730fa02df 100644 --- a/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.Servers/GenericTDSServerSession.cs +++ b/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.Servers/GenericTdsServerSession.cs @@ -17,7 +17,7 @@ namespace Microsoft.SqlServer.TDS.Servers /// /// Generic session for TDS Server /// - public class GenericTDSServerSession : ITDSServerSession + public class GenericTdsServerSession : ITDSServerSession { /// /// Server that created the session @@ -259,7 +259,7 @@ public bool AnsiDefaults /// /// Initialization constructor /// - public GenericTDSServerSession(ITDSServer server, uint sessionID) : + public GenericTdsServerSession(ITDSServer server, uint sessionID) : this(server, sessionID, 4096) { } @@ -267,7 +267,7 @@ public GenericTDSServerSession(ITDSServer server, uint sessionID) : /// /// Initialization constructor /// - public GenericTDSServerSession(ITDSServer server, uint sessionID, uint packetSize) + public GenericTdsServerSession(ITDSServer server, uint sessionID, uint packetSize) { // Save the server Server = server; diff --git a/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.Servers/QueryEngine.cs b/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.Servers/QueryEngine.cs index eb219f5dbc..579c47abcc 100644 --- a/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.Servers/QueryEngine.cs +++ b/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.Servers/QueryEngine.cs @@ -26,12 +26,12 @@ public class QueryEngine /// /// Server configuration /// - public TDSServerArguments ServerArguments { get; private set; } + public TdsServerArguments ServerArguments { get; private set; } /// /// Initialization constructor /// - public QueryEngine(TDSServerArguments arguments) + public QueryEngine(TdsServerArguments arguments) { ServerArguments = arguments; } @@ -1308,7 +1308,7 @@ private TDSMessage _PrepareAnsiDefaultsResponse(ITDSServerSession session) TDSRowToken rowToken = new TDSRowToken(metadataToken); // Read the value from the session - rowToken.Data.Add((session as GenericTDSServerSession).AnsiDefaults); + rowToken.Data.Add((session as GenericTdsServerSession).AnsiDefaults); // Log response TDSUtilities.Log(Log, "Response", rowToken); @@ -1347,7 +1347,7 @@ private TDSMessage _PrepareAnsiNullDefaultOnResponse(ITDSServerSession session) TDSRowToken rowToken = new TDSRowToken(metadataToken); // Read the value from the session - rowToken.Data.Add((session as GenericTDSServerSession).AnsiNullDefaultOn); + rowToken.Data.Add((session as GenericTdsServerSession).AnsiNullDefaultOn); // Log response TDSUtilities.Log(Log, "Response", rowToken); @@ -1386,7 +1386,7 @@ private TDSMessage _PrepareAnsiNullsResponse(ITDSServerSession session) TDSRowToken rowToken = new TDSRowToken(metadataToken); // Read the value from the session - rowToken.Data.Add((session as GenericTDSServerSession).AnsiNulls); + rowToken.Data.Add((session as GenericTdsServerSession).AnsiNulls); // Log response TDSUtilities.Log(Log, "Response", rowToken); @@ -1425,7 +1425,7 @@ private TDSMessage _PrepareAnsiPaddingResponse(ITDSServerSession session) TDSRowToken rowToken = new TDSRowToken(metadataToken); // Read the value from the session - rowToken.Data.Add((session as GenericTDSServerSession).AnsiPadding); + rowToken.Data.Add((session as GenericTdsServerSession).AnsiPadding); // Log response TDSUtilities.Log(Log, "Response", rowToken); @@ -1464,7 +1464,7 @@ private TDSMessage _PrepareAnsiWarningsResponse(ITDSServerSession session) TDSRowToken rowToken = new TDSRowToken(metadataToken); // Read the value from the session - rowToken.Data.Add((session as GenericTDSServerSession).AnsiWarnings); + rowToken.Data.Add((session as GenericTdsServerSession).AnsiWarnings); // Log response TDSUtilities.Log(Log, "Response", rowToken); @@ -1503,7 +1503,7 @@ private TDSMessage _PrepareArithAbortResponse(ITDSServerSession session) TDSRowToken rowToken = new TDSRowToken(metadataToken); // Read the value from the session - rowToken.Data.Add((session as GenericTDSServerSession).ArithAbort); + rowToken.Data.Add((session as GenericTdsServerSession).ArithAbort); // Log response TDSUtilities.Log(Log, "Response", rowToken); @@ -1542,7 +1542,7 @@ private TDSMessage _PrepareConcatNullYieldsNullResponse(ITDSServerSession sessio TDSRowToken rowToken = new TDSRowToken(metadataToken); // Read the value from the session - rowToken.Data.Add((session as GenericTDSServerSession).ConcatNullYieldsNull); + rowToken.Data.Add((session as GenericTdsServerSession).ConcatNullYieldsNull); // Log response TDSUtilities.Log(Log, "Response", rowToken); @@ -1581,7 +1581,7 @@ private TDSMessage _PrepareDateFirstResponse(ITDSServerSession session) TDSRowToken rowToken = new TDSRowToken(metadataToken); // Read the value from the session - rowToken.Data.Add((short)(session as GenericTDSServerSession).DateFirst); + rowToken.Data.Add((short)(session as GenericTdsServerSession).DateFirst); // Log response TDSUtilities.Log(Log, "Response", rowToken); @@ -1622,7 +1622,7 @@ private TDSMessage _PrepareDateFormatResponse(ITDSServerSession session) TDSRowToken rowToken = new TDSRowToken(metadataToken); // Generate a date format string - rowToken.Data.Add(DateFormatString.ToString((session as GenericTDSServerSession).DateFormat)); + rowToken.Data.Add(DateFormatString.ToString((session as GenericTdsServerSession).DateFormat)); // Log response TDSUtilities.Log(Log, "Response", rowToken); @@ -1661,7 +1661,7 @@ private TDSMessage _PrepareDeadlockPriorityResponse(ITDSServerSession session) TDSRowToken rowToken = new TDSRowToken(metadataToken); // Serialize the value from the session - rowToken.Data.Add((session as GenericTDSServerSession).DeadlockPriority); + rowToken.Data.Add((session as GenericTdsServerSession).DeadlockPriority); // Log response TDSUtilities.Log(Log, "Response", rowToken); @@ -1702,7 +1702,7 @@ private TDSMessage _PrepareLanguageResponse(ITDSServerSession session) TDSRowToken rowToken = new TDSRowToken(metadataToken); // Generate a date format string - rowToken.Data.Add(LanguageString.ToString((session as GenericTDSServerSession).Language)); + rowToken.Data.Add(LanguageString.ToString((session as GenericTdsServerSession).Language)); // Log response TDSUtilities.Log(Log, "Response", rowToken); @@ -1741,7 +1741,7 @@ private TDSMessage _PrepareLockTimeoutResponse(ITDSServerSession session) TDSRowToken rowToken = new TDSRowToken(metadataToken); // Serialize the value from the session - rowToken.Data.Add((session as GenericTDSServerSession).LockTimeout); + rowToken.Data.Add((session as GenericTdsServerSession).LockTimeout); // Log response TDSUtilities.Log(Log, "Response", rowToken); @@ -1780,7 +1780,7 @@ private TDSMessage _PrepareQuotedIdentifierResponse(ITDSServerSession session) TDSRowToken rowToken = new TDSRowToken(metadataToken); // Read the value from the session - rowToken.Data.Add((session as GenericTDSServerSession).QuotedIdentifier); + rowToken.Data.Add((session as GenericTdsServerSession).QuotedIdentifier); // Log response TDSUtilities.Log(Log, "Response", rowToken); @@ -1819,7 +1819,7 @@ private TDSMessage _PrepareTextSizeResponse(ITDSServerSession session) TDSRowToken rowToken = new TDSRowToken(metadataToken); // Read the value from the session - rowToken.Data.Add((session as GenericTDSServerSession).TextSize); + rowToken.Data.Add((session as GenericTdsServerSession).TextSize); // Log response TDSUtilities.Log(Log, "Response", rowToken); @@ -1858,7 +1858,7 @@ private TDSMessage _PrepareTransactionIsolationLevelResponse(ITDSServerSession s TDSRowToken rowToken = new TDSRowToken(metadataToken); // Read the value from the session - rowToken.Data.Add((short)(session as GenericTDSServerSession).TransactionIsolationLevel); + rowToken.Data.Add((short)(session as GenericTdsServerSession).TransactionIsolationLevel); // Log response TDSUtilities.Log(Log, "Response", rowToken); @@ -1897,7 +1897,7 @@ private TDSMessage _PrepareOptionsResponse(ITDSServerSession session) TDSRowToken rowToken = new TDSRowToken(metadataToken); // Convert to generic session - GenericTDSServerSession genericSession = session as GenericTDSServerSession; + GenericTdsServerSession genericSession = session as GenericTdsServerSession; // Serialize the options into the bit mask int options = 0; @@ -2029,13 +2029,13 @@ private TDSMessage _PrepareContextInfoResponse(ITDSServerSession session) byte[] contextInfo = null; // Check if session has a context info - if ((session as GenericTDSServerSession).ContextInfo != null) + if ((session as GenericTdsServerSession).ContextInfo != null) { // Allocate a container contextInfo = new byte[128]; // Copy context info into the container - Array.Copy((session as GenericTDSServerSession).ContextInfo, contextInfo, (session as GenericTDSServerSession).ContextInfo.Length); + Array.Copy((session as GenericTdsServerSession).ContextInfo, contextInfo, (session as GenericTdsServerSession).ContextInfo.Length); } // Set context info diff --git a/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.Servers/RoutingTDSServer.cs b/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.Servers/RoutingTdsServer.cs similarity index 91% rename from src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.Servers/RoutingTDSServer.cs rename to src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.Servers/RoutingTdsServer.cs index 57596b24ac..8e119a54cd 100644 --- a/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.Servers/RoutingTDSServer.cs +++ b/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.Servers/RoutingTdsServer.cs @@ -16,20 +16,20 @@ namespace Microsoft.SqlServer.TDS.Servers /// /// TDS Server that routes clients to the configured destination /// - public class RoutingTDSServer : GenericTDSServer + public class RoutingTdsServer : GenericTdsServer { /// /// Initialization constructor /// - public RoutingTDSServer() : - this(new RoutingTDSServerArguments()) + public RoutingTdsServer() : + this(new RoutingTdsServerArguments()) { } /// /// Initialization constructor /// - public RoutingTDSServer(RoutingTDSServerArguments arguments) : + public RoutingTdsServer(RoutingTdsServerArguments arguments) : base(arguments) { } @@ -43,10 +43,10 @@ public override TDSMessageCollection OnPreLoginRequest(ITDSServerSession session TDSMessageCollection response = base.OnPreLoginRequest(session, request); // Check if arguments are of the routing server - if (Arguments is RoutingTDSServerArguments) + if (Arguments is RoutingTdsServerArguments) { // Cast to routing server arguments - RoutingTDSServerArguments serverArguments = Arguments as RoutingTDSServerArguments; + RoutingTdsServerArguments serverArguments = Arguments as RoutingTdsServerArguments; // Check if routing is configured during login if (serverArguments.RouteOnPacket == TDSMessageType.TDS7Login) @@ -78,10 +78,10 @@ public override TDSMessageCollection OnLogin7Request(ITDSServerSession session, TDSLogin7Token loginRequest = request[0] as TDSLogin7Token; // Check if arguments are of the routing server - if (Arguments is RoutingTDSServerArguments) + if (Arguments is RoutingTdsServerArguments) { // Cast to routing server arguments - RoutingTDSServerArguments ServerArguments = Arguments as RoutingTDSServerArguments; + RoutingTdsServerArguments ServerArguments = Arguments as RoutingTdsServerArguments; // Check filter if (ServerArguments.RequireReadOnly && (loginRequest.TypeFlags.ReadOnlyIntent != TDSLogin7TypeFlagsReadOnlyIntent.ReadOnly)) @@ -136,10 +136,10 @@ public override TDSMessageCollection OnSQLBatchRequest(ITDSServerSession session TDSMessageCollection batchResponse = base.OnSQLBatchRequest(session, request); // Check if arguments are of routing server - if (Arguments is RoutingTDSServerArguments) + if (Arguments is RoutingTdsServerArguments) { // Cast to routing server arguments - RoutingTDSServerArguments ServerArguments = Arguments as RoutingTDSServerArguments; + RoutingTdsServerArguments ServerArguments = Arguments as RoutingTdsServerArguments; // Check routing condition if (ServerArguments.RouteOnPacket == TDSMessageType.SQLBatch) @@ -188,10 +188,10 @@ protected override TDSMessageCollection OnAuthenticationCompleted(ITDSServerSess TDSMessageCollection responseMessageCollection = base.OnAuthenticationCompleted(session); // Check if arguments are of routing server - if (Arguments is RoutingTDSServerArguments) + if (Arguments is RoutingTdsServerArguments) { // Cast to routing server arguments - RoutingTDSServerArguments serverArguments = Arguments as RoutingTDSServerArguments; + RoutingTdsServerArguments serverArguments = Arguments as RoutingTdsServerArguments; // Check routing condition if (serverArguments.RouteOnPacket == TDSMessageType.TDS7Login) @@ -233,7 +233,7 @@ protected override TDSMessageCollection OnAuthenticationCompleted(ITDSServerSess protected TDSPacketToken CreateRoutingToken() { // Cast to routing server arguments - RoutingTDSServerArguments ServerArguments = Arguments as RoutingTDSServerArguments; + RoutingTdsServerArguments ServerArguments = Arguments as RoutingTdsServerArguments; // Construct routing token value TDSRoutingEnvChangeTokenValue routingInfo = new TDSRoutingEnvChangeTokenValue(); diff --git a/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.Servers/RoutingTDSServerArguments.cs b/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.Servers/RoutingTdsServerArguments.cs similarity index 51% rename from src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.Servers/RoutingTDSServerArguments.cs rename to src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.Servers/RoutingTdsServerArguments.cs index 99cbd3baae..95fe97f4f2 100644 --- a/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.Servers/RoutingTDSServerArguments.cs +++ b/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.Servers/RoutingTdsServerArguments.cs @@ -7,43 +7,31 @@ namespace Microsoft.SqlServer.TDS.Servers /// /// Arguments for routing TDS Server /// - public class RoutingTDSServerArguments : TDSServerArguments + public class RoutingTdsServerArguments : TdsServerArguments { /// - /// Routing destination protocol + /// Routing destination protocol. /// - public int RoutingProtocol { get; set; } + public int RoutingProtocol { get; set; } = 0; /// /// Routing TCP port /// - public ushort RoutingTCPPort { get; set; } + public ushort RoutingTCPPort { get; set; } = 0; /// /// Routing TCP host name /// - public string RoutingTCPHost { get; set; } + public string RoutingTCPHost { get; set; } = string.Empty; /// /// Packet on which routing should occur /// - public TDSMessageType RouteOnPacket { get; set; } + public TDSMessageType RouteOnPacket { get; set; } = TDSMessageType.TDS7Login; /// /// Indicates that routing should only occur on read-only connections /// - public bool RequireReadOnly { get; set; } - - /// - /// Initialization constructor - /// - public RoutingTDSServerArguments() - { - // By default we route on login - RouteOnPacket = TDSMessageType.TDS7Login; - - // By default we reject non-read-only connections - RequireReadOnly = true; - } + public bool RequireReadOnly { get; set; } = true; } } diff --git a/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.Servers/TDS.Servers.csproj b/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.Servers/TDS.Servers.csproj index b7757b257b..c689554310 100644 --- a/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.Servers/TDS.Servers.csproj +++ b/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.Servers/TDS.Servers.csproj @@ -11,21 +11,24 @@ - - + + - - - - - + + + + + - - + + - - - + + + + + + diff --git a/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.Servers/TdsServer.cs b/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.Servers/TdsServer.cs new file mode 100644 index 0000000000..d3bb1861ef --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.Servers/TdsServer.cs @@ -0,0 +1,32 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace Microsoft.SqlServer.TDS.Servers +{ + public class TdsServer : GenericTdsServer + { + /// + /// Default constructor + /// + public TdsServer() : this(new TdsServerArguments()) + { + } + + /// + /// Constructor with arguments + /// + public TdsServer(TdsServerArguments arguments) : base(arguments) + { + } + + /// + /// Constructor with arguments and query engine + /// + /// Query engine + /// Server arguments + public TdsServer(QueryEngine queryEngine, TdsServerArguments arguments) : base(arguments, queryEngine) + { + } + } +} diff --git a/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.Servers/TDSServerArguments.cs b/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.Servers/TdsServerArguments.cs similarity index 63% rename from src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.Servers/TDSServerArguments.cs rename to src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.Servers/TdsServerArguments.cs index 88e577ab68..7ceb2e0272 100644 --- a/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.Servers/TDSServerArguments.cs +++ b/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.Servers/TdsServerArguments.cs @@ -13,7 +13,7 @@ namespace Microsoft.SqlServer.TDS.Servers /// /// Common arguments for TDS Server /// - public class TDSServerArguments + public class TdsServerArguments { /// /// Service Principal Name, representing Azure SQL Database in Azure Active Directory. @@ -28,76 +28,56 @@ public class TDSServerArguments /// /// Log to which send TDS conversation /// - public TextWriter Log { get; set; } + public TextWriter Log { get; set; } = null; /// /// Server name /// - public string ServerName { get; set; } + public string ServerName { get; set; } = Environment.MachineName; /// /// Server version /// - public Version ServerVersion { get; set; } + public Version ServerVersion { get; set; } = new Version(11, 0, 1083); /// /// Server principal name /// - public string ServerPrincipalName { get; set; } + public string ServerPrincipalName { get; set; } = AzureADServicePrincipalName; /// /// Sts Url /// - public string StsUrl { get; set; } + public string StsUrl { get; set; } = AzureADProductionTokenEndpoint; /// /// Size of the TDS packet server should operate with /// - public int PacketSize { get; set; } + public int PacketSize { get; set; } = 4096; /// /// Transport encryption /// - public TDSPreLoginTokenEncryptionType Encryption { get; set; } + public TDSPreLoginTokenEncryptionType Encryption { get; set; } = TDSPreLoginTokenEncryptionType.NotSupported; /// /// Specifies the FedAuthRequired option /// - public TdsPreLoginFedAuthRequiredOption FedAuthRequiredPreLoginOption { get; set; } + public TdsPreLoginFedAuthRequiredOption FedAuthRequiredPreLoginOption { get; set; } = TdsPreLoginFedAuthRequiredOption.FedAuthNotRequired; /// /// Certificate to use for transport encryption /// - public X509Certificate EncryptionCertificate { get; set; } + public X509Certificate EncryptionCertificate { get; set; } = null; /// /// SSL/TLS protocols to use for transport encryption /// - public SslProtocols EncryptionProtocols { get; set; } + public SslProtocols EncryptionProtocols { get; set; } = SslProtocols.Tls12; /// - /// Initialization constructor + /// Routing destination protocol /// - public TDSServerArguments() - { - // Assign default server version - ServerName = Environment.MachineName; - ServerVersion = new Version(11, 0, 1083); - - // Default packet size - PacketSize = 4096; - - // By default we don't support encryption - Encryption = TDSPreLoginTokenEncryptionType.NotSupported; - - // By Default SQL authentication will be used. - FedAuthRequiredPreLoginOption = TdsPreLoginFedAuthRequiredOption.FedAuthNotRequired; - - EncryptionCertificate = null; - EncryptionProtocols = SslProtocols.Tls12; - - ServerPrincipalName = AzureADServicePrincipalName; - StsUrl = AzureADProductionTokenEndpoint; - } + public string FailoverPartner { get; set; } = string.Empty; } } diff --git a/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.Servers/TransientDelayTdsServer.cs b/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.Servers/TransientDelayTdsServer.cs new file mode 100644 index 0000000000..d0a15e90ae --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.Servers/TransientDelayTdsServer.cs @@ -0,0 +1,84 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Threading; +using Microsoft.SqlServer.TDS.EndPoint; + +namespace Microsoft.SqlServer.TDS.Servers +{ + /// + /// TDS Server that delays response to simulate transient network delays + /// + public class TransientDelayTdsServer : GenericTdsServer, IDisposable + { + private int RequestCounter = 0; + + public TransientDelayTdsServer(TransientDelayTdsServerArguments arguments) + : base(arguments) + { + } + + public TransientDelayTdsServer(TransientDelayTdsServerArguments arguments, QueryEngine queryEngine) + : base(arguments, queryEngine) + { + } + + /// + public override void Dispose() + { + base.Dispose(); + RequestCounter = 0; + } + + /// + /// Handler for login request + /// + public override TDSMessageCollection OnLogin7Request(ITDSServerSession session, TDSMessage request) + { + // Check if we're still going to raise transient error + if (Arguments.IsEnabledPermanentDelay || + (Arguments.IsEnabledTransientDelay && RequestCounter < Arguments.RepeatCount)) + { + Thread.Sleep(Arguments.DelayDuration); + + RequestCounter++; + } + + // Return login response from the base class + return base.OnLogin7Request(session, request); + } + + /// + public override TDSMessageCollection OnSQLBatchRequest(ITDSServerSession session, TDSMessage message) + { + if (Arguments.IsEnabledPermanentDelay || + (Arguments.IsEnabledTransientDelay && RequestCounter < Arguments.RepeatCount)) + { + Thread.Sleep(Arguments.DelayDuration); + + RequestCounter++; + } + + return base.OnSQLBatchRequest(session, message); + } + + public void ResetRequestCounter() + { + RequestCounter = 0; + } + + public void SetTransientTimeoutBehavior(bool isEnabledTransientTimeout, TimeSpan sleepDuration) + { + SetTransientTimeoutBehavior(isEnabledTransientTimeout, false, sleepDuration); + } + + public void SetTransientTimeoutBehavior(bool isEnabledTransientTimeout, bool isEnabledPermanentTimeout, TimeSpan sleepDuration) + { + Arguments.IsEnabledTransientDelay = isEnabledTransientTimeout; + Arguments.IsEnabledPermanentDelay = isEnabledPermanentTimeout; + Arguments.DelayDuration = sleepDuration; + } + } +} diff --git a/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.Servers/TransientDelayTdsServerArguments.cs b/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.Servers/TransientDelayTdsServerArguments.cs new file mode 100644 index 0000000000..d89ee7dfdc --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.Servers/TransientDelayTdsServerArguments.cs @@ -0,0 +1,31 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; + +namespace Microsoft.SqlServer.TDS.Servers +{ + public class TransientDelayTdsServerArguments : TdsServerArguments + { + /// + /// The duration for which the server should sleep before responding to a request. + /// + public TimeSpan DelayDuration { get; set; } = TimeSpan.FromSeconds(0); + + /// + /// Flag to consider when simulating a delay on the next request. + /// + public bool IsEnabledTransientDelay { get; set; } = false; + + /// + /// Flag to consider when simulating a delay on each request. + /// + public bool IsEnabledPermanentDelay { get; set; } = false; + + /// + /// The number of logins during which the delay should be applied. + /// + public int RepeatCount { get; set; } = 1; + } +} diff --git a/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.Servers/TransientFaultTDSServer.cs b/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.Servers/TransientFaultTDSServer.cs deleted file mode 100644 index 1933444df6..0000000000 --- a/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.Servers/TransientFaultTDSServer.cs +++ /dev/null @@ -1,153 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -using System; -using System.Net; -using System.Runtime.CompilerServices; -using System.Threading; -using Microsoft.SqlServer.TDS.Done; -using Microsoft.SqlServer.TDS.EndPoint; -using Microsoft.SqlServer.TDS.Error; -using Microsoft.SqlServer.TDS.Login7; - -namespace Microsoft.SqlServer.TDS.Servers -{ - /// - /// TDS Server that authenticates clients according to the requested parameters - /// - public class TransientFaultTDSServer : GenericTDSServer, IDisposable - { - private static int RequestCounter = 0; - - public int Port { get; set; } - - /// - /// Constructor - /// - public TransientFaultTDSServer() => new TransientFaultTDSServer(new TransientFaultTDSServerArguments()); - - /// - /// Constructor - /// - /// - public TransientFaultTDSServer(TransientFaultTDSServerArguments arguments) : - base(arguments) - { } - - /// - /// Constructor - /// - /// - /// - public TransientFaultTDSServer(QueryEngine engine, TransientFaultTDSServerArguments args) : base(args) - { - Engine = engine; - } - - private TDSServerEndPoint _endpoint = null; - - private static string GetErrorMessage(uint errorNumber) - { - switch (errorNumber) - { - case 40613: - return "Database on server is not currently available. Please retry the connection later. " + - "If the problem persists, contact customer support, and provide them the session tracing ID."; - case 42108: - return "Can not connect to the SQL pool since it is paused. Please resume the SQL pool and try again."; - case 42109: - return "The SQL pool is warming up. Please try again."; - } - return "Unknown server error occurred"; - } - - /// - /// Handler for login request - /// - public override TDSMessageCollection OnLogin7Request(ITDSServerSession session, TDSMessage request) - { - // Inflate login7 request from the message - TDSLogin7Token loginRequest = request[0] as TDSLogin7Token; - - // Check if arguments are of the transient fault TDS server - if (Arguments is TransientFaultTDSServerArguments) - { - // Cast to transient fault TDS server arguments - TransientFaultTDSServerArguments ServerArguments = Arguments as TransientFaultTDSServerArguments; - - // Check if we're still going to raise transient error - if (ServerArguments.IsEnabledTransientError && RequestCounter < 1) // Fail first time, then connect - { - uint errorNumber = ServerArguments.Number; - string errorMessage = ServerArguments.Message; - - // Log request to which we're about to send a failure - TDSUtilities.Log(Arguments.Log, "Request", loginRequest); - - // Prepare ERROR token with the denial details - TDSErrorToken errorToken = new TDSErrorToken(errorNumber, 1, 20, errorMessage); - - // Log response - TDSUtilities.Log(Arguments.Log, "Response", errorToken); - - // Serialize the error token into the response packet - TDSMessage responseMessage = new TDSMessage(TDSMessageType.Response, errorToken); - - // Create DONE token - TDSDoneToken doneToken = new TDSDoneToken(TDSDoneTokenStatusType.Final | TDSDoneTokenStatusType.Error); - - // Log response - TDSUtilities.Log(Arguments.Log, "Response", doneToken); - - // Serialize DONE token into the response packet - responseMessage.Add(doneToken); - - RequestCounter++; - - // Put a single message into the collection and return it - return new TDSMessageCollection(responseMessage); - } - } - - // Return login response from the base class - return base.OnLogin7Request(session, request); - } - - public static TransientFaultTDSServer StartTestServer(bool isEnabledTransientFault, bool enableLog, uint errorNumber, [CallerMemberName] string methodName = "") - => StartServerWithQueryEngine(null, isEnabledTransientFault, enableLog, errorNumber, methodName); - - public static TransientFaultTDSServer StartServerWithQueryEngine(QueryEngine engine, bool isEnabledTransientFault, bool enableLog, uint errorNumber, [CallerMemberName] string methodName = "") - { - TransientFaultTDSServerArguments args = new TransientFaultTDSServerArguments() - { - Log = enableLog ? Console.Out : null, - IsEnabledTransientError = isEnabledTransientFault, - Number = errorNumber, - Message = GetErrorMessage(errorNumber) - }; - - TransientFaultTDSServer server = engine == null ? new TransientFaultTDSServer(args) : new TransientFaultTDSServer(engine, args); - server._endpoint = new TDSServerEndPoint(server) { ServerEndPoint = new IPEndPoint(IPAddress.Any, 0) }; - server._endpoint.EndpointName = methodName; - - // The server EventLog should be enabled as it logs the exceptions. - server._endpoint.EventLog = enableLog ? Console.Out : null; - server._endpoint.Start(); - - server.Port = server._endpoint.ServerEndPoint.Port; - return server; - } - - public void Dispose() => Dispose(true); - - private void Dispose(bool isDisposing) - { - if (isDisposing) - { - _endpoint?.Stop(); - RequestCounter = 0; - } - } - } -} diff --git a/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.Servers/TransientTdsErrorTdsServer.cs b/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.Servers/TransientTdsErrorTdsServer.cs new file mode 100644 index 0000000000..ecd89f5812 --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.Servers/TransientTdsErrorTdsServer.cs @@ -0,0 +1,118 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Threading; +using Microsoft.SqlServer.TDS.Done; +using Microsoft.SqlServer.TDS.EndPoint; +using Microsoft.SqlServer.TDS.Error; +using Microsoft.SqlServer.TDS.Login7; + +namespace Microsoft.SqlServer.TDS.Servers +{ + /// + /// TDS Server that returns TDS error token on login request for the specified number of times + /// + public class TransientTdsErrorTdsServer : GenericTdsServer, IDisposable + { + private int RequestCounter = 0; + + public void SetErrorBehavior(bool isEnabledTransientError, uint errorNumber, int repeatCount = 1, string message = null) + { + Arguments.IsEnabledTransientError = isEnabledTransientError; + Arguments.Number = errorNumber; + Arguments.Message = message; + Arguments.RepeatCount = repeatCount; + } + + public TransientTdsErrorTdsServer(TransientTdsErrorTdsServerArguments arguments) : base(arguments) + { + } + + public TransientTdsErrorTdsServer(TransientTdsErrorTdsServerArguments arguments, QueryEngine queryEngine) : base(arguments, queryEngine) + { + } + + private static string GetErrorMessage(uint errorNumber) + { + switch (errorNumber) + { + case 40613: + return "Database on server is not currently available. Please retry the connection later. " + + "If the problem persists, contact customer support, and provide them the session tracing ID."; + case 42108: + return "Can not connect to the SQL pool since it is paused. Please resume the SQL pool and try again."; + case 42109: + return "The SQL pool is warming up. Please try again."; + } + return "Unknown server error occurred"; + } + + /// + /// Handler for login request + /// + public override TDSMessageCollection OnLogin7Request(ITDSServerSession session, TDSMessage request) + { + // Inflate login7 request from the message + TDSLogin7Token loginRequest = request[0] as TDSLogin7Token; + + // Check if we're still going to raise transient error + if (Arguments.IsEnabledTransientError && RequestCounter < Arguments.RepeatCount) + { + return GenerateErrorMessage(request); + } + + // Return login response from the base class + return base.OnLogin7Request(session, request); + } + + /// + public override TDSMessageCollection OnSQLBatchRequest(ITDSServerSession session, TDSMessage message) + { + if (Arguments.IsEnabledTransientError && RequestCounter < Arguments.RepeatCount) + { + return GenerateErrorMessage(message); + } + + return base.OnSQLBatchRequest(session, message); + } + + private TDSMessageCollection GenerateErrorMessage(TDSMessage request) + { + uint errorNumber = Arguments.Number; + string errorMessage = Arguments.Message ?? GetErrorMessage(errorNumber); + + // Log request to which we're about to send a failure + TDSUtilities.Log(Arguments.Log, "Request", request); + + // Prepare ERROR token with the denial details + TDSErrorToken errorToken = new TDSErrorToken(errorNumber, 1, 20, errorMessage); + + // Log response + TDSUtilities.Log(Arguments.Log, "Response", errorToken); + + // Serialize the error token into the response packet + TDSMessage responseMessage = new TDSMessage(TDSMessageType.Response, errorToken); + + // Create DONE token + TDSDoneToken doneToken = new TDSDoneToken(TDSDoneTokenStatusType.Final | TDSDoneTokenStatusType.Error); + + // Log response + TDSUtilities.Log(Arguments.Log, "Response", doneToken); + + // Serialize DONE token into the response packet + responseMessage.Add(doneToken); + + RequestCounter++; + + // Put a single message into the collection and return it + return new TDSMessageCollection(responseMessage); + } + + public override void Dispose() { + base.Dispose(); + RequestCounter = 0; + } + } +} diff --git a/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.Servers/TransientFaultTDSServerArguments.cs b/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.Servers/TransientTdsErrorTdsServerArguments.cs similarity index 59% rename from src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.Servers/TransientFaultTDSServerArguments.cs rename to src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.Servers/TransientTdsErrorTdsServerArguments.cs index 77eec68c5f..6a61d9cc6f 100644 --- a/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.Servers/TransientFaultTDSServerArguments.cs +++ b/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.Servers/TransientTdsErrorTdsServerArguments.cs @@ -4,31 +4,26 @@ namespace Microsoft.SqlServer.TDS.Servers { - public class TransientFaultTDSServerArguments : TDSServerArguments + public class TransientTdsErrorTdsServerArguments : TdsServerArguments { /// /// Transient error number to be raised by server. /// - public uint Number { get; set; } + public uint Number { get; set; } = 0; /// /// Transient error message to be raised by server. /// - public string Message { get; set; } + public string Message { get; set; } = string.Empty; /// /// Flag to consider when raising Transient error. /// - public bool IsEnabledTransientError { get; set; } + public bool IsEnabledTransientError { get; set; } = false; /// - /// Constructor to initialize + /// The number of times the transient error should be raised. /// - public TransientFaultTDSServerArguments() - { - Number = 0; - Message = string.Empty; - IsEnabledTransientError = false; - } + public int RepeatCount { get; set; } = 1; } } diff --git a/tools/props/Versions.props b/tools/props/Versions.props index 67e8010809..7b4bc87d81 100644 --- a/tools/props/Versions.props +++ b/tools/props/Versions.props @@ -2,7 +2,7 @@ - 6.1.0 + 6.1.1 0