From 782721d48bdb1f4f2cf092f686881fed66d2ea88 Mon Sep 17 00:00:00 2001 From: Cheena Malhotra <13396919+cheenamalhotra@users.noreply.github.com> Date: Fri, 15 Aug 2025 03:57:45 -0700 Subject: [PATCH 01/11] Update AKV versions and dependencies (#3569) Updated AKV nuspec to note compatibility with MDS 6.1.1. --- eng/pipelines/variables/akv-official-variables.yml | 4 ++-- ...a.SqlClient.AlwaysEncrypted.AzureKeyVaultProvider.nuspec | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/eng/pipelines/variables/akv-official-variables.yml b/eng/pipelines/variables/akv-official-variables.yml index f30f0e67e4..8e602cb8f9 100644 --- a/eng/pipelines/variables/akv-official-variables.yml +++ b/eng/pipelines/variables/akv-official-variables.yml @@ -22,7 +22,7 @@ variables: # Base Variables ------------------------------------------------------- - name: mdsPackageVersion - value: '6.1.0' + value: '6.1.1' # @TODO: Version should ideally be pulled from one location (versions.props?) - name: versionMajor @@ -30,7 +30,7 @@ variables: - name: versionMinor value: '1' - name: versionPatch - value: '0' + value: '1' - name: versionPreview value: '-preview1' diff --git a/tools/specs/add-ons/Microsoft.Data.SqlClient.AlwaysEncrypted.AzureKeyVaultProvider.nuspec b/tools/specs/add-ons/Microsoft.Data.SqlClient.AlwaysEncrypted.AzureKeyVaultProvider.nuspec index 968237fd36..2140a32e7a 100644 --- a/tools/specs/add-ons/Microsoft.Data.SqlClient.AlwaysEncrypted.AzureKeyVaultProvider.nuspec +++ b/tools/specs/add-ons/Microsoft.Data.SqlClient.AlwaysEncrypted.AzureKeyVaultProvider.nuspec @@ -25,19 +25,19 @@ Microsoft.Data.SqlClient.AlwaysEncrypted.AzureKeyStoreProvider.SqlColumnEncrypti sqlclient microsoft.data.sqlclient azurekeyvaultprovider akvprovider alwaysencrypted - + - + - + From 84b3c81170bbe26984de3c6355c237bf336529f2 Mon Sep 17 00:00:00 2001 From: Benjamin Russell Date: Tue, 19 Aug 2025 15:02:56 -0500 Subject: [PATCH 02/11] [6.1] AKV Provider Strong Name Signing (#3573) * * Add signing key path to roslyn analyzers * Make signing key path an argument to build and roslyn analyzers steps * Enable strong name signing on buddy (unofficial) builds * Add akv official job to solution file * Remove diagnostic step ... thought I removed that already :man_facepalming: * Add string type to roslyn analyzer step argument * vbump (bypassing rules because AKV build isn't tested via CI) --- eng/pipelines/jobs/build-akv-official-job.yml | 2 ++ eng/pipelines/steps/compound-build-akv-step.yml | 5 ++++- eng/pipelines/steps/roslyn-analyzers-akv-step.yml | 14 +++++++++++--- eng/pipelines/variables/akv-official-variables.yml | 2 +- src/Microsoft.Data.SqlClient.sln | 3 +++ ...nt.AlwaysEncrypted.AzureKeyVaultProvider.csproj | 6 +++--- 6 files changed, 24 insertions(+), 8 deletions(-) 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/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 8e602cb8f9..30176ac98b 100644 --- a/eng/pipelines/variables/akv-official-variables.yml +++ b/eng/pipelines/variables/akv-official-variables.yml @@ -30,7 +30,7 @@ variables: - name: versionMinor value: '1' - name: versionPatch - value: '1' + value: '2' - name: versionPreview value: '-preview1' 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/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)')) From d2f2f26c1ebf5fa4c9af41bb56e984114721694d Mon Sep 17 00:00:00 2001 From: Paul Medynski <31868385+paulmedynski@users.noreply.github.com> Date: Thu, 28 Aug 2025 12:28:04 -0300 Subject: [PATCH 03/11] [6.1] Add Stress Tests to CI (#3565) * Moved existing stress test projects from ADO into GitHub. (#3546) (This code is effectively dead since it is not being referenced anywhere, and there are no other changes to the code. As such, we can bypass the one flaky test failure.) * Minimal modifications to stress tests for Linux (#3548) * Minimal modifications to get the stress tests building and running on Linux. * Updated sample config to avoid a dummy password. * User Story 38131: Stress Tests CI - Added config file path configuration via STRESS_CONFIG_FILE environment variable. - Added support for JSON config file. - Fixed process exit code to reflect success vs failure. - Added emission to console. - Added emission of data source. - Added EntraId password-based auth. - Added CI stage. - Added NuGet package version to MDS assembly. - Added dotnet build configuration pararmeter to CI entry points. - Added MDS package version configurability to stress tests. - Added all target frameworks that MDS supports. - Each test run now creates and drops its own database to avoid collisions. - Moved to 1ES images where possible. - Using a local SQL Server that we setup as part of CI. * User Story 38131: Stress Tests CI - Added top-level pipeline flag to enable stress tests, deafult is false until tests are reliably passing. --- .editorconfig | 3 + .../steps/configure-sql-server-linux-step.yml | 5 + .../steps/configure-sql-server-macos-step.yml | 6 +- .../steps/configure-sql-server-win-step.yml | 7 +- eng/pipelines/dotnet-sqlclient-ci-core.yml | 25 +- ...qlclient-ci-package-reference-pipeline.yml | 14 + ...qlclient-ci-project-reference-pipeline.yml | 14 + eng/pipelines/jobs/stress-tests-ci-job.yml | 214 ++++ .../stages/stress-tests-ci-stage.yml | 191 ++++ .../tests/StressTests/Directory.Build.props | 18 + .../StressTests/Directory.Packages.props | 38 + .../IMonitorLoader/IMonitorLoader.cs | 31 + .../IMonitorLoader/IMonitorLoader.csproj | 6 + .../IMonitorLoader/MonitorMetrics.cs | 96 ++ .../tests/StressTests/NuGet.config | 13 + .../tests/StressTests/Readme.md | 230 +++++ .../GlobalExceptionHandlerAttribute.cs | 16 + .../Attributes/GlobalTestCleanupAttribute.cs | 16 + .../Attributes/GlobalTestSetupAttribute.cs | 16 + .../Attributes/TestAttribute.cs | 272 +++++ .../Attributes/TestCleanupAttribute.cs | 16 + .../Attributes/TestSetupAttribute.cs | 16 + .../Attributes/TestVariationAttribute.cs | 35 + .../DeadlockDetection.cs | 194 ++++ .../DeadlockDetectionTaskScheduler.cs | 93 ++ .../SqlClient.Stress.Common.csproj | 5 + .../SqlClient.Stress.Common/TestMetrics.cs | 368 +++++++ .../SqlClient.Stress.Common/VersionUtil.cs | 40 + .../SqlClient.Stress.Framework/AsyncUtils.cs | 185 ++++ .../SqlClient.Stress.Framework/DataSource.cs | 192 ++++ .../DataStressConnection.cs | 232 +++++ .../DataStressErrors.cs | 215 ++++ .../DataStressFactory.cs | 955 ++++++++++++++++++ .../DataStressReader.cs | 350 +++++++ .../DataStressSettings.cs | 310 ++++++ .../DataTestGroup.cs | 713 +++++++++++++ .../SqlClient.Stress.Framework/Extensions.cs | 94 ++ .../SqlClient.Stress.Framework.csproj | 15 + .../StressConfigReader.cs | 195 ++++ .../StressTests.config.jsonc | 19 + .../StressTests.config.xml | 20 + .../TrackedRandom.cs | 184 ++++ .../SqlClient.Stress.Runner/Constants.cs | 53 + .../Ex API/MemApi.Windows.cs | 18 + .../ITestAttributeFilter.cs | 11 + .../SqlClient.Stress.Runner/LogManager.cs | 49 + .../Monitor/FakeConsole.cs | 34 + .../SqlClient.Stress.Runner/Monitor/Logger.cs | 226 +++++ .../Monitor/MonitorLoadUtils.cs | 48 + .../Monitor/RecordedExceptions.cs | 110 ++ .../SqlClient.Stress.Runner/PerfCounters.cs | 29 + .../SqlClient.Stress.Runner/Program.cs | 306 ++++++ .../SqlClient.Stress.Runner.csproj | 18 + .../SqlClient.Stress.Runner/StressEngine.cs | 208 ++++ .../SqlClient.Stress.Runner/TestFinder.cs | 166 +++ .../Tests/MultithreadedTest.cs | 170 ++++ .../Tests/StressTest.cs | 155 +++ .../SqlClient.Stress.Runner/Tests/Test.cs | 116 +++ .../SqlClient.Stress.Runner/Tests/TestBase.cs | 163 +++ .../Tests/ThreadPoolTest.cs | 174 ++++ .../FilteredDefaultTraceListener.cs | 210 ++++ .../HostsFileManager.cs | 481 +++++++++ .../MultiSubnetFailoverSetup.cs | 135 +++ .../SqlClient.Stress.Tests/NetUtils.cs | 206 ++++ .../SqlClient.Stress.Tests.csproj | 15 + .../SqlClientStressFactory.cs | 297 ++++++ .../SqlClientTestGroup.cs | 620 ++++++++++++ .../tests/StressTests/StressTests.slnx | 7 + tools/targets/GenerateThisAssemblyCs.targets | 1 + 69 files changed, 9699 insertions(+), 4 deletions(-) create mode 100644 eng/pipelines/jobs/stress-tests-ci-job.yml create mode 100644 eng/pipelines/stages/stress-tests-ci-stage.yml create mode 100644 src/Microsoft.Data.SqlClient/tests/StressTests/Directory.Build.props create mode 100644 src/Microsoft.Data.SqlClient/tests/StressTests/Directory.Packages.props create mode 100644 src/Microsoft.Data.SqlClient/tests/StressTests/IMonitorLoader/IMonitorLoader.cs create mode 100644 src/Microsoft.Data.SqlClient/tests/StressTests/IMonitorLoader/IMonitorLoader.csproj create mode 100644 src/Microsoft.Data.SqlClient/tests/StressTests/IMonitorLoader/MonitorMetrics.cs create mode 100644 src/Microsoft.Data.SqlClient/tests/StressTests/NuGet.config create mode 100644 src/Microsoft.Data.SqlClient/tests/StressTests/Readme.md create mode 100644 src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Common/Attributes/GlobalExceptionHandlerAttribute.cs create mode 100644 src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Common/Attributes/GlobalTestCleanupAttribute.cs create mode 100644 src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Common/Attributes/GlobalTestSetupAttribute.cs create mode 100644 src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Common/Attributes/TestAttribute.cs create mode 100644 src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Common/Attributes/TestCleanupAttribute.cs create mode 100644 src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Common/Attributes/TestSetupAttribute.cs create mode 100644 src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Common/Attributes/TestVariationAttribute.cs create mode 100644 src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Common/DeadlockDetection.cs create mode 100644 src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Common/DeadlockDetectionTaskScheduler.cs create mode 100644 src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Common/SqlClient.Stress.Common.csproj create mode 100644 src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Common/TestMetrics.cs create mode 100644 src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Common/VersionUtil.cs create mode 100644 src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Framework/AsyncUtils.cs create mode 100644 src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Framework/DataSource.cs create mode 100644 src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Framework/DataStressConnection.cs create mode 100644 src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Framework/DataStressErrors.cs create mode 100644 src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Framework/DataStressFactory.cs create mode 100644 src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Framework/DataStressReader.cs create mode 100644 src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Framework/DataStressSettings.cs create mode 100644 src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Framework/DataTestGroup.cs create mode 100644 src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Framework/Extensions.cs create mode 100644 src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Framework/SqlClient.Stress.Framework.csproj create mode 100644 src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Framework/StressConfigReader.cs create mode 100644 src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Framework/StressTests.config.jsonc create mode 100644 src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Framework/StressTests.config.xml create mode 100644 src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Framework/TrackedRandom.cs create mode 100644 src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Runner/Constants.cs create mode 100644 src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Runner/Ex API/MemApi.Windows.cs create mode 100644 src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Runner/ITestAttributeFilter.cs create mode 100644 src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Runner/LogManager.cs create mode 100644 src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Runner/Monitor/FakeConsole.cs create mode 100644 src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Runner/Monitor/Logger.cs create mode 100644 src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Runner/Monitor/MonitorLoadUtils.cs create mode 100644 src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Runner/Monitor/RecordedExceptions.cs create mode 100644 src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Runner/PerfCounters.cs create mode 100644 src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Runner/Program.cs create mode 100644 src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Runner/SqlClient.Stress.Runner.csproj create mode 100644 src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Runner/StressEngine.cs create mode 100644 src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Runner/TestFinder.cs create mode 100644 src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Runner/Tests/MultithreadedTest.cs create mode 100644 src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Runner/Tests/StressTest.cs create mode 100644 src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Runner/Tests/Test.cs create mode 100644 src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Runner/Tests/TestBase.cs create mode 100644 src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Runner/Tests/ThreadPoolTest.cs create mode 100644 src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Tests/FilteredDefaultTraceListener.cs create mode 100644 src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Tests/HostsFileManager.cs create mode 100644 src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Tests/MultiSubnetFailoverSetup.cs create mode 100644 src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Tests/NetUtils.cs create mode 100644 src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Tests/SqlClient.Stress.Tests.csproj create mode 100644 src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Tests/SqlClientStressFactory.cs create mode 100644 src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Tests/SqlClientTestGroup.cs create mode 100644 src/Microsoft.Data.SqlClient/tests/StressTests/StressTests.slnx 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/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..8899b9e68f 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` 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..b7d30b31ea 100644 --- a/eng/pipelines/dotnet-sqlclient-ci-core.yml +++ b/eng/pipelines/dotnet-sqlclient-ci-core.yml @@ -65,10 +65,22 @@ 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 + variables: - template: libraries/ci-build-variables.yml@self @@ -84,6 +96,7 @@ stages: jobs: - template: common/templates/jobs/ci-build-nugets-job.yml@self parameters: + configuration: ${{ parameters.buildConfiguration }} artifactName: $(artifactName) ${{if ne(parameters.SNIVersion, '')}}: prebuildSteps: @@ -92,6 +105,16 @@ 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 }} @@ -139,7 +162,6 @@ stages: 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 +203,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 diff --git a/eng/pipelines/dotnet-sqlclient-ci-package-reference-pipeline.yml b/eng/pipelines/dotnet-sqlclient-ci-package-reference-pipeline.yml index 4956b15c89..ae01d2e9db 100644 --- a/eng/pipelines/dotnet-sqlclient-ci-package-reference-pipeline.yml +++ b/eng/pipelines/dotnet-sqlclient-ci-package-reference-pipeline.yml @@ -81,6 +81,18 @@ 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 + extends: template: dotnet-sqlclient-ci-core.yml@self parameters: @@ -92,3 +104,5 @@ extends: useManagedSNI: ${{ parameters.useManagedSNI }} codeCovTargetFrameworks: ${{ parameters.codeCovTargetFrameworks }} buildType: ${{ parameters.buildType }} + buildConfiguration: ${{ parameters.buildConfiguration }} + enableStressTests: ${{ parameters.enableStressTests }} diff --git a/eng/pipelines/dotnet-sqlclient-ci-project-reference-pipeline.yml b/eng/pipelines/dotnet-sqlclient-ci-project-reference-pipeline.yml index ecdaacfafb..97a7a5af24 100644 --- a/eng/pipelines/dotnet-sqlclient-ci-project-reference-pipeline.yml +++ b/eng/pipelines/dotnet-sqlclient-ci-project-reference-pipeline.yml @@ -73,6 +73,18 @@ 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 + extends: template: dotnet-sqlclient-ci-core.yml@self parameters: @@ -84,3 +96,5 @@ extends: useManagedSNI: ${{ parameters.useManagedSNI }} codeCovTargetFrameworks: ${{ parameters.codeCovTargetFrameworks }} buildType: ${{ parameters.buildType }} + buildConfiguration: ${{ parameters.buildConfiguration }} + enableStressTests: ${{ parameters.enableStressTests }} 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/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/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/tools/targets/GenerateThisAssemblyCs.targets b/tools/targets/GenerateThisAssemblyCs.targets index 0bcdf8a2aa..6230af8ae7 100644 --- a/tools/targets/GenerateThisAssemblyCs.targets +++ b/tools/targets/GenerateThisAssemblyCs.targets @@ -14,6 +14,7 @@ namespace System internal static class ThisAssembly { internal const string InformationalVersion = "$(AssemblyFileVersion)"%3B +internal const string NuGetPackageVersion = "$(Version)"%3B } } From f69e3b125e7bd736717b4ec959c14129882e5ef8 Mon Sep 17 00:00:00 2001 From: Apoorv Deshmukh Date: Thu, 4 Sep 2025 04:53:18 +0530 Subject: [PATCH 04/11] Port #3559 to release/6.1 (#3592) --- .../ManualTests/DataCommon/DataTestUtility.cs | 14 ++-- .../SQL/JsonTest/JsonBulkCopyTest.cs | 31 ++++----- .../SQL/JsonTest/JsonStreamTest.cs | 17 +++-- .../ManualTests/SQL/JsonTest/JsonTest.cs | 18 ++--- .../VectorTest/NativeVectorFloat32Tests.cs | 30 ++++---- .../SQL/VectorTest/VectorAPIValidationTest.cs | 69 +++++++++++++++---- .../VectorTypeBackwardCompatibilityTests.cs | 18 ++--- .../Config.cs | 2 +- .../config.default.json | 3 +- 9 files changed, 120 insertions(+), 82 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/DataCommon/DataTestUtility.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/DataCommon/DataTestUtility.cs index e83e49b79a..838f9a2d14 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/DataCommon/DataTestUtility.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/DataCommon/DataTestUtility.cs @@ -92,14 +92,6 @@ public static class DataTestUtility //SQL Server EngineEdition private static string s_sqlServerEngineEdition; - // Currently, only Azure SQL supports vectors and JSON. - // Our CI images with specific SQL Server versions lag - // behind with vector and JSON support. - // JSON Column type - public static readonly bool IsJsonSupported = !IsNotAzureServer(); - // VECTOR column type - public static readonly bool IsVectorSupported = !IsNotAzureServer(); - // 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 @@ -181,7 +173,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; @@ -453,6 +444,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); 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..2ba0b80a8d 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.GetUniqueName("randomRecords"); + private static readonly string _outputFile = DataTestUtility.GetUniqueName("serverResults"); + private static readonly string _sourceTableName = DataTestUtility.GetUniqueName("jsonBulkCopySrcTable", true); + private static readonly string _destinationTableName = DataTestUtility.GetUniqueName("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..045efc8408 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.GetUniqueName("randomRecords") + ".json"; + private static readonly string _outputFile = DataTestUtility.GetUniqueName("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/VectorTest/NativeVectorFloat32Tests.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/VectorTest/NativeVectorFloat32Tests.cs index 3140d4c3ac..119190f8c5 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/VectorTest/NativeVectorFloat32Tests.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/VectorTest/NativeVectorFloat32Tests.cs @@ -20,7 +20,7 @@ public static class VectorFloat32TestData public static float[] testData = new float[] { 1.1f, 2.2f, 3.3f }; public static int vectorColumnLength = testData.Length; // Incorrect size for SqlParameter.Size - public static int IncorrectParamSize = 3234; + public static int IncorrectParamSize = 3234; public static IEnumerable GetVectorFloat32TestData() { // Pattern 1-4 with SqlVector(values: testData) @@ -43,11 +43,11 @@ public static IEnumerable GetVectorFloat32TestData() // Pattern 1-4 with SqlVector.Null 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(), vectorColumnLength }; - + yield return new object[] { 3, SqlVector.Null, Array.Empty(), vectorColumnLength }; yield return new object[] { 4, SqlVector.Null, Array.Empty(), vectorColumnLength }; } @@ -128,7 +128,7 @@ private void ValidateInsertedData(SqlConnection connection, float[] expectedData ValidateSqlVectorFloat32Object(reader.IsDBNull(0), (SqlVector)reader.GetSqlValue(0), expectedData, expectedLength); if (!reader.IsDBNull(0)) - { + { 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); @@ -147,7 +147,7 @@ private void ValidateInsertedData(SqlConnection connection, float[] expectedData } } - [ConditionalTheory(typeof(DataTestUtility), nameof(DataTestUtility.IsVectorSupported))] + [ConditionalTheory(typeof(DataTestUtility), nameof(DataTestUtility.IsAzureServer))] [MemberData(nameof(VectorFloat32TestData.GetVectorFloat32TestData), MemberType = typeof(VectorFloat32TestData), DisableDiscoveryEnumeration = true)] public void TestSqlVectorFloat32ParameterInsertionAndReads( int pattern, @@ -213,7 +213,7 @@ private async Task ValidateInsertedDataAsync(SqlConnection connection, float[] e } } - [ConditionalTheory(typeof(DataTestUtility), nameof(DataTestUtility.IsVectorSupported))] + [ConditionalTheory(typeof(DataTestUtility), nameof(DataTestUtility.IsAzureServer))] [MemberData(nameof(VectorFloat32TestData.GetVectorFloat32TestData), MemberType = typeof(VectorFloat32TestData), DisableDiscoveryEnumeration = true)] public async Task TestSqlVectorFloat32ParameterInsertionAndReadsAsync( int pattern, @@ -247,7 +247,7 @@ public async Task TestSqlVectorFloat32ParameterInsertionAndReadsAsync( await ValidateInsertedDataAsync(conn, expectedValues, expectedLength); } - [ConditionalTheory(typeof(DataTestUtility), nameof(DataTestUtility.IsVectorSupported))] + [ConditionalTheory(typeof(DataTestUtility), nameof(DataTestUtility.IsAzureServer))] [MemberData(nameof(VectorFloat32TestData.GetVectorFloat32TestData), MemberType = typeof(VectorFloat32TestData), DisableDiscoveryEnumeration = true)] public void TestStoredProcParamsForVectorFloat32( int pattern, @@ -304,7 +304,7 @@ public void TestStoredProcParamsForVectorFloat32( Assert.Throws(() => command.ExecuteNonQuery()); } - [ConditionalTheory(typeof(DataTestUtility), nameof(DataTestUtility.IsVectorSupported))] + [ConditionalTheory(typeof(DataTestUtility), nameof(DataTestUtility.IsAzureServer))] [MemberData(nameof(VectorFloat32TestData.GetVectorFloat32TestData), MemberType = typeof(VectorFloat32TestData), DisableDiscoveryEnumeration = true)] public async Task TestStoredProcParamsForVectorFloat32Async( int pattern, @@ -361,7 +361,7 @@ public async Task TestStoredProcParamsForVectorFloat32Async( 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) @@ -374,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)); @@ -400,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(); @@ -460,7 +460,7 @@ public void TestBulkCopyFromSqlTable(int bulkCopySourceMode) Assert.Equal(VectorFloat32TestData.testData.Length, ((SqlVector)verifyReader.GetSqlVector(0)).Length); } - [ConditionalTheory(typeof(DataTestUtility), nameof(DataTestUtility.IsVectorSupported))] + [ConditionalTheory(typeof(DataTestUtility), nameof(DataTestUtility.IsAzureServer))] [InlineData(1)] [InlineData(2)] public async Task TestBulkCopyFromSqlTableAsync(int bulkCopySourceMode) @@ -560,7 +560,7 @@ public async Task TestBulkCopyFromSqlTableAsync(int bulkCopySourceMode) Assert.Equal(VectorFloat32TestData.testData.Length, vector.Length); } - [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.IsVectorSupported))] + [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.IsAzureServer))] public void TestInsertVectorsFloat32WithPrepare() { SqlConnection conn = new SqlConnection(s_connectionString); diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/VectorTest/VectorAPIValidationTest.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/VectorTest/VectorAPIValidationTest.cs index 84c16e1793..27c857f5c5 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/VectorTest/VectorAPIValidationTest.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/VectorTest/VectorAPIValidationTest.cs @@ -10,38 +10,83 @@ namespace Microsoft.Data.SqlClient.ManualTesting.Tests.SQL.VectorTest { public sealed class VectorAPIValidationTest { - // We need this testcase to validate ref assembly for vector APIs + // We need these testcases to validate ref assembly for vector APIs // Unit tests are covered under SqlVectorTest.cs [Fact] - public void VectorAPITest() + 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 vector = new SqlVector(VectorFloat32TestData.testData); - Assert.Equal(VectorFloat32TestData.testData, vector.Memory.ToArray()); + 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) { } - vector = new SqlVector(new ReadOnlyMemory(VectorFloat32TestData.testData)); - Assert.Equal(VectorFloat32TestData.testData, vector.Memory.ToArray()); + 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); + } - //Validate length property + [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); + } - // Validate CreateNull method - vector = SqlVector.CreateNull(5); - Assert.True(vector.IsNull); - Assert.Equal(5, 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"); } } -} \ No newline at end of file +} 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..402a2777f8 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/VectorTest/VectorTypeBackwardCompatibilityTests.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/VectorTest/VectorTypeBackwardCompatibilityTests.cs @@ -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/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": "" } From 214261fd19d7f9488925dd96832c3dc095066658 Mon Sep 17 00:00:00 2001 From: Paul Medynski <31868385+paulmedynski@users.noreply.github.com> Date: Wed, 3 Sep 2025 20:24:28 -0300 Subject: [PATCH 05/11] User Story 38467: Backport mac server name fix (#3594) - Backported part of #3494 and all of #3591: - Added configurable test jobs timeout, defaulting to 90 minutes. - Reduced generated database names to 96 chars to try to fix macOS test failures. --- .../common/templates/jobs/ci-run-tests-job.yml | 9 +++++++++ .../jobs/run-tests-package-reference-job.yml | 10 ++++++++++ .../templates/stages/ci-run-tests-stage.yml | 7 +++++++ eng/pipelines/dotnet-sqlclient-ci-core.yml | 6 ++++++ ...-sqlclient-ci-package-reference-pipeline.yml | 7 +++++++ ...-sqlclient-ci-project-reference-pipeline.yml | 7 +++++++ .../dotnet-sqlclient-signing-pipeline.yml | 7 +++++++ .../ManualTests/DataCommon/DataTestUtility.cs | 17 ++++++++++++++--- 8 files changed, 67 insertions(+), 3 deletions(-) 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/dotnet-sqlclient-ci-core.yml b/eng/pipelines/dotnet-sqlclient-ci-core.yml index b7d30b31ea..353122828b 100644 --- a/eng/pipelines/dotnet-sqlclient-ci-core.yml +++ b/eng/pipelines/dotnet-sqlclient-ci-core.yml @@ -81,6 +81,11 @@ parameters: 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 @@ -119,6 +124,7 @@ stages: parameters: debug: ${{ parameters.debug }} buildType: ${{ parameters.buildType }} + testsTimeout: ${{ parameters.testsTimeout }} ${{ if eq(parameters.buildType, 'Package') }}: dependsOn: build_nugets diff --git a/eng/pipelines/dotnet-sqlclient-ci-package-reference-pipeline.yml b/eng/pipelines/dotnet-sqlclient-ci-package-reference-pipeline.yml index ae01d2e9db..336bd97ab5 100644 --- a/eng/pipelines/dotnet-sqlclient-ci-package-reference-pipeline.yml +++ b/eng/pipelines/dotnet-sqlclient-ci-package-reference-pipeline.yml @@ -93,6 +93,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 + extends: template: dotnet-sqlclient-ci-core.yml@self parameters: @@ -106,3 +112,4 @@ extends: 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 97a7a5af24..38325d38ca 100644 --- a/eng/pipelines/dotnet-sqlclient-ci-project-reference-pipeline.yml +++ b/eng/pipelines/dotnet-sqlclient-ci-project-reference-pipeline.yml @@ -85,6 +85,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 + extends: template: dotnet-sqlclient-ci-core.yml@self parameters: @@ -98,3 +104,4 @@ extends: 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..d791642d88 100644 --- a/eng/pipelines/dotnet-sqlclient-signing-pipeline.yml +++ b/eng/pipelines/dotnet-sqlclient-signing-pipeline.yml @@ -62,6 +62,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 +167,7 @@ extends: parameters: packageFolderName: $(packageFolderName) isPreview: ${{ parameters['isPreview'] }} + timeout: ${{ parameters.testsTimeout }} downloadPackageStep: download: current artifact: $(packageFolderName) diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/DataCommon/DataTestUtility.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/DataCommon/DataTestUtility.cs index 838f9a2d14..2060772c00 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/DataCommon/DataTestUtility.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/DataCommon/DataTestUtility.cs @@ -583,7 +583,7 @@ public static string GetUniqueName(string prefix, bool withBracket = true) /// /// 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. + /// Unique name by considering the Sql Server naming rules, never longer than 96 characters. public static string GetUniqueNameForSqlServer(string prefix, bool withBracket = true) { string extendedPrefix = string.Format( @@ -593,10 +593,21 @@ public static string GetUniqueNameForSqlServer(string prefix, bool withBracket = Environment.MachineName, DateTime.Now.ToString("yyyy_MM_dd", CultureInfo.InvariantCulture)); string name = GetUniqueName(extendedPrefix, withBracket); - if (name.Length > 128) + + // Truncate to no more than 96 characters. + const int maxLen = 96; + if (name.Length > maxLen) { - throw new ArgumentOutOfRangeException("the name is too long - SQL Server names are limited to 128"); + if (withBracket) + { + name = name.Substring(0, maxLen - 1) + ']'; + } + else + { + name = name.Substring(0, maxLen); + } } + return name; } From 5aae5e1c833c9cbec392236070c6419a4076eafa Mon Sep 17 00:00:00 2001 From: Paul Medynski <31868385+paulmedynski@users.noreply.github.com> Date: Thu, 18 Sep 2025 07:25:18 -0300 Subject: [PATCH 06/11] [6.1] Stabilize CI Pipelines (#3601) Co-authored-by: Edward Neal <55035479+edwardneal@users.noreply.github.com> --- .../steps/configure-sql-server-macos-step.yml | 60 +++-- .../ManualTests/AlwaysEncrypted/ApiShould.cs | 10 +- .../AlwaysEncrypted/CspProviderExt.cs | 2 +- .../ManualTests/DataCommon/DataTestUtility.cs | 217 ++++++++++++++---- .../ProviderAgnostic/ReaderTest/ReaderTest.cs | 6 +- .../SQL/AdapterTest/AdapterTest.cs | 10 +- .../SQL/ConnectivityTests/ConnectivityTest.cs | 2 +- .../DataClassificationTest.cs | 6 +- .../SQL/DataReaderTest/DataReaderTest.cs | 2 +- .../SQL/DataStreamTest/DataStreamTest.cs | 10 +- .../SQL/JsonTest/JsonBulkCopyTest.cs | 8 +- .../SQL/JsonTest/JsonStreamTest.cs | 4 +- .../SQL/ParameterTest/DateTimeVariantTest.cs | 52 ++--- .../SQL/ParameterTest/ParametersTest.cs | 50 ++-- .../ParameterTest/SqlAdapterUpdateBatch.cs | 2 +- .../SQL/ParameterTest/SqlVariantParam.cs | 4 +- .../RetryLogic/SqlCommandReliabilityTest.cs | 4 +- .../SqlConnectionReliabilityTest.cs | 2 +- .../AdjustPrecScaleForBulkCopy.cs | 2 +- .../AzureDistributedTransaction.cs | 2 +- .../CopyWidenNullInexactNumerics.cs | 4 +- .../DataConversionErrorMessageTest.cs | 2 +- .../SqlBulkCopyTest/TestBulkCopyWithUTF8.cs | 4 +- .../SQL/SqlBulkCopyTest/WriteToServerTest.cs | 4 +- .../SQL/SqlCommand/SqlCommandCompletedTest.cs | 2 +- .../SQL/SqlCommand/SqlCommandSetTest.cs | 4 +- .../SqlFileStreamTest/SqlFileStreamTest.cs | 4 +- .../SQL/UdtTest/SqlServerTypesTest.cs | 2 +- .../SQL/UdtTest/UdtBulkCopyTest.cs | 6 +- .../SQL/UdtTest/UdtDateTimeOffsetTest.cs | 4 +- .../tests/ManualTests/SQL/UdtTest/UdtTest2.cs | 16 +- .../SQL/Utf8SupportTest/Utf8SupportTest.cs | 2 +- .../VectorTest/NativeVectorFloat32Tests.cs | 6 +- .../VectorTypeBackwardCompatibilityTests.cs | 6 +- .../TracingTests/XEventsTracingTest.cs | 48 ++-- 35 files changed, 361 insertions(+), 208 deletions(-) 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 8899b9e68f..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 @@ -34,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 @@ -42,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 @@ -78,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/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/ApiShould.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/ApiShould.cs index 1b17a0f4ef..d3c40e2d34 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/ApiShould.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/ApiShould.cs @@ -166,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; @@ -722,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); @@ -912,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 { 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/DataCommon/DataTestUtility.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/DataCommon/DataTestUtility.cs index 2060772c00..bc37646da9 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/DataCommon/DataTestUtility.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/DataCommon/DataTestUtility.cs @@ -111,7 +111,7 @@ public static bool TcpConnectionStringDoesNotUseAadAuth { get { - SqlConnectionStringBuilder builder = new (TCPConnectionString); + SqlConnectionStringBuilder builder = new(TCPConnectionString); return builder.Authentication == SqlAuthenticationMethod.SqlPassword || builder.Authentication == SqlAuthenticationMethod.NotSpecified; } } @@ -556,59 +556,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, never longer than 96 characters. - public static string GetUniqueNameForSqlServer(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); - - // Truncate to no more than 96 characters. - const int maxLen = 96; - if (name.Length > maxLen) - { - if (withBracket) - { - name = name.Substring(0, maxLen - 1) + ']'; - } - else - { - name = name.Substring(0, maxLen); - } + /// + /// + /// 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) + { + StringBuilder name = new(96); + + if (withBracket) + { + name.Append('['); + } + + if (prefix.Length > 32) + { + 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) @@ -1101,6 +1218,8 @@ protected virtual void OnMatchingEventWritten(EventWrittenEventArgs eventData) public readonly ref struct XEventScope // : IDisposable { + private const int MaxXEventsLatencyS = 5; + private readonly SqlConnection _connection; private readonly bool _useDatabaseSession; @@ -1135,6 +1254,8 @@ INNER JOIN sys.dm_xe_sessions AS xe using (SqlCommand command = new SqlCommand(xEventQuery, _connection)) { + Thread.Sleep(MaxXEventsLatencyS * 1000); + if (_connection.State == ConnectionState.Closed) { _connection.Open(); @@ -1155,9 +1276,9 @@ private void SetupXEvent(string eventSpecification, string targetSpecification) {eventSpecification} {targetSpecification} WITH ( - MAX_MEMORY=4096 KB, + MAX_MEMORY=16 MB, EVENT_RETENTION_MODE=ALLOW_SINGLE_EVENT_LOSS, - MAX_DISPATCH_LATENCY=30 SECONDS, + MAX_DISPATCH_LATENCY={MaxXEventsLatencyS} SECONDS, MAX_EVENT_SIZE=0 KB, MEMORY_PARTITION_MODE=NONE, TRACK_CAUSALITY=ON, 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/ConnectivityTests/ConnectivityTest.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ConnectivityTests/ConnectivityTest.cs index 10b653967d..8e21156bce 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ConnectivityTests/ConnectivityTest.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ConnectivityTests/ConnectivityTest.cs @@ -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/DataReaderTest.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/DataReaderTest/DataReaderTest.cs index 5c9a19afcb..532e0d06e5 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/DataReaderTest/DataReaderTest.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/DataReaderTest/DataReaderTest.cs @@ -130,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) { 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..859d6aceb0 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/DataStreamTest/DataStreamTest.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/DataStreamTest/DataStreamTest.cs @@ -50,7 +50,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)) { @@ -546,7 +546,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 +1041,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 +1871,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)) { 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 2ba0b80a8d..15f16b1f66 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/JsonTest/JsonBulkCopyTest.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/JsonTest/JsonBulkCopyTest.cs @@ -13,10 +13,10 @@ namespace Microsoft.Data.SqlClient.ManualTesting.Tests.SQL.JsonTest public class JsonBulkCopyTest { private readonly ITestOutputHelper _output; - private static readonly string _generatedJsonFile = DataTestUtility.GetUniqueName("randomRecords"); - private static readonly string _outputFile = DataTestUtility.GetUniqueName("serverResults"); - private static readonly string _sourceTableName = DataTestUtility.GetUniqueName("jsonBulkCopySrcTable", true); - private static readonly string _destinationTableName = DataTestUtility.GetUniqueName("jsonBulkCopyDestTable", true); + 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) { 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 045efc8408..ed36457200 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/JsonTest/JsonStreamTest.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/JsonTest/JsonStreamTest.cs @@ -22,8 +22,8 @@ public class JsonRecord public class JsonStreamTest { private readonly ITestOutputHelper _output; - private static readonly string _jsonFile = DataTestUtility.GetUniqueName("randomRecords") + ".json"; - private static readonly string _outputFile = DataTestUtility.GetUniqueName("serverRecords") + ".json"; + private static readonly string _jsonFile = DataTestUtility.GetShortName("randomRecords") + ".json"; + private static readonly string _outputFile = DataTestUtility.GetShortName("serverRecords") + ".json"; public JsonStreamTest(ITestOutputHelper output) { 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 bf99691907..2f9a2ecb1c 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ParameterTest/ParametersTest.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ParameterTest/ParametersTest.cs @@ -17,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 @@ -118,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); @@ -141,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"); @@ -260,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(); @@ -339,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(); @@ -401,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 { @@ -419,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))); @@ -449,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 { @@ -570,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 { @@ -602,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 { @@ -636,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 { @@ -923,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 " + 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/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/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 119190f8c5..d905e15bc5 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/VectorTest/NativeVectorFloat32Tests.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/VectorTest/NativeVectorFloat32Tests.cs @@ -57,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 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 402a2777f8..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 diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/TracingTests/XEventsTracingTest.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/TracingTests/XEventsTracingTest.cs index 40fde20faa..3d8e6be8ac 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/TracingTests/XEventsTracingTest.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/TracingTests/XEventsTracingTest.cs @@ -21,37 +21,35 @@ 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); + using DataTestUtility.XEventScope xEventSession = new(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) From 7dd31ee4802e94d01f14448f2159e7203b155d4b Mon Sep 17 00:00:00 2001 From: Edward Neal <55035479+edwardneal@users.noreply.github.com> Date: Thu, 25 Sep 2025 12:22:48 +0100 Subject: [PATCH 07/11] Prevent uninitialised performance counters escaping CreatePerformanceCounter (#3623) (#3629) --- .../Data/SqlClient/Diagnostics/SqlClientMetrics.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) 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..9319087a75 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 @@ -489,25 +489,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. From 2a7cfd22311128f569bfd3b048473490233fb4cc Mon Sep 17 00:00:00 2001 From: Cheena Malhotra <13396919+cheenamalhotra@users.noreply.github.com> Date: Fri, 3 Oct 2025 12:22:08 -0700 Subject: [PATCH 08/11] Fix SetProvider to return immediately if user-defined provider found (#3620) (#3651) --- .../SqlAuthenticationProviderManager.cs | 11 +++++++- .../FunctionalTests/AADAuthenticationTests.cs | 23 +++++++++++++++- .../DummySqlAuthenticationProvider.cs | 27 +++++++++++++++++++ ...soft.Data.SqlClient.FunctionalTests.csproj | 6 +++++ .../SqlAuthenticationProviderTest.cs | 15 ++++++++++- .../tests/FunctionalTests/app.config | 13 +++++++++ 6 files changed, 92 insertions(+), 3 deletions(-) create mode 100644 src/Microsoft.Data.SqlClient/tests/FunctionalTests/DataCommon/DummySqlAuthenticationProvider.cs create mode 100644 src/Microsoft.Data.SqlClient/tests/FunctionalTests/app.config 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/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/Microsoft.Data.SqlClient.FunctionalTests.csproj b/src/Microsoft.Data.SqlClient/tests/FunctionalTests/Microsoft.Data.SqlClient.FunctionalTests.csproj index ad2b1191a0..d1cb71f572 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,6 +24,7 @@ + @@ -70,6 +71,11 @@ + + + Always + + 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/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 @@ + + + + +
+ + + + + + + + From 36fefac55d9cad335cca983e012c227cb9a840d0 Mon Sep 17 00:00:00 2001 From: Cheena Malhotra <13396919+cheenamalhotra@users.noreply.github.com> Date: Mon, 6 Oct 2025 09:22:13 -0700 Subject: [PATCH 09/11] Fix connection pool concurrency issue (#3632) (#3653) --- .../WaitHandleDbConnectionPool.cs | 78 ++-- ....Data.SqlClient.ManualTesting.Tests.csproj | 1 + .../ConnectionPoolStressTest.cs | 440 ++++++++++++++++++ 3 files changed, 485 insertions(+), 34 deletions(-) create mode 100644 src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ConnectionPoolTest/ConnectionPoolStressTest.cs 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..da2246d8aa 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 @@ -383,6 +383,41 @@ internal WaitHandle[] GetHandles(bool withCreate) } } + /// + /// Helper class to obtain and release a semaphore. + /// + internal class SemaphoreHolder : IDisposable + { + private readonly Semaphore _semaphore; + + /// + /// Whether the semaphore was successfully obtained within the timeout. + /// + internal bool Obtained { get; private set; } + + /// + /// Obtains the semaphore, waiting up to the specified timeout. + /// + /// + /// + internal SemaphoreHolder(Semaphore semaphore, int timeout) + { + _semaphore = semaphore; + Obtained = _semaphore.WaitOne(timeout); + } + + /// + /// Releases the semaphore if it was successfully obtained. + /// + public void Dispose() + { + if (Obtained) + { + _semaphore.Release(1); + } + } + } + private const int MAX_Q_SIZE = 0x00100000; // The order of these is important; we want the WaitAny call to be signaled @@ -1321,20 +1356,14 @@ private bool TryGetConnection(DbConnection owningObject, uint waitForMultipleObj if (onlyOneCheckConnection) { - if (_waitHandles.CreationSemaphore.WaitOne(unchecked((int)waitForMultipleObjectsTimeout))) + using SemaphoreHolder semaphoreHolder = new(_waitHandles.CreationSemaphore, unchecked((int)waitForMultipleObjectsTimeout)); + if (semaphoreHolder.Obtained) { #if NETFRAMEWORK RuntimeHelpers.PrepareConstrainedRegions(); #endif - try - { - SqlClientEventSource.Log.TryPoolerTraceEvent(" {0}, Creating new connection.", Id); - obj = UserCreateRequest(owningObject, userOptions); - } - finally - { - _waitHandles.CreationSemaphore.Release(1); - } + SqlClientEventSource.Log.TryPoolerTraceEvent(" {0}, Creating new connection.", Id); + obj = UserCreateRequest(owningObject, userOptions); } else { @@ -1553,7 +1582,6 @@ private void PoolCreateRequest(object state) { return; } - int waitResult = BOGUS_HANDLE; #if NETFRAMEWORK RuntimeHelpers.PrepareConstrainedRegions(); @@ -1561,18 +1589,13 @@ private void PoolCreateRequest(object state) try { // Obtain creation mutex so we're the only one creating objects - // and we must have the wait result + using SemaphoreHolder semaphoreHolder = new(_waitHandles.CreationSemaphore, CreationTimeout); + + if (semaphoreHolder.Obtained) + { #if NETFRAMEWORK RuntimeHelpers.PrepareConstrainedRegions(); #endif - try - { } - finally - { - waitResult = WaitHandle.WaitAny(_waitHandles.GetHandles(withCreate: true), CreationTimeout); - } - if (CREATION_HANDLE == waitResult) - { DbConnectionInternal newObj; // Check ErrorOccurred again after obtaining mutex @@ -1606,17 +1629,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) { @@ -1630,14 +1648,6 @@ private void PoolCreateRequest(object state) // thrown to the user the next time they request a connection. SqlClientEventSource.Log.TryPoolerTraceEvent(" {0}, PoolCreateRequest called CreateConnection which threw an exception: {1}", Id, e); } - finally - { - if (CREATION_HANDLE == waitResult) - { - // reuse waitResult and ignore its value - _waitHandles.CreationSemaphore.Release(1); - } - } } } } 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 0da63efa61..fed317e720 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 @@ -258,6 +258,7 @@ + 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 + } +} From f01290da2644679fbb8395fd30bace81a851595e Mon Sep 17 00:00:00 2001 From: Malcolm Daigle Date: Mon, 6 Oct 2025 13:15:44 -0700 Subject: [PATCH 10/11] [6.1] Enable scheduled builds (#3657) * Enable scheduled builds for release/6.1 * Fix comment * Clean up schedules --- ...-sqlclient-ci-package-reference-pipeline.yml | 17 +++++------------ ...-sqlclient-ci-project-reference-pipeline.yml | 10 +++++----- .../dotnet-sqlclient-signing-pipeline.yml | 14 ++++---------- 3 files changed, 14 insertions(+), 27 deletions(-) diff --git a/eng/pipelines/dotnet-sqlclient-ci-package-reference-pipeline.yml b/eng/pipelines/dotnet-sqlclient-ci-package-reference-pipeline.yml index 336bd97ab5..51a9e6bb4b 100644 --- a/eng/pipelines/dotnet-sqlclient-ci-package-reference-pipeline.yml +++ b/eng/pipelines/dotnet-sqlclient-ci-package-reference-pipeline.yml @@ -9,8 +9,8 @@ trigger: batch: true branches: include: - - main - - internal/main + - release/6.1 + - internal/release/6.1 paths: include: - src\Microsoft.Data.SqlClient\netcore\ref @@ -22,18 +22,11 @@ trigger: - Nuget.config schedules: -- cron: '0 4 * * Fri' - displayName: Weekly Thursday 9:00 PM (UTC - 7) Build +- cron: '0 5 * * Sun' + displayName: Weekly Saturday 10:00 PM (UTC - 7) Build branches: include: - - internal/main - always: true - -- cron: '0 0 * * Mon-Fri' - displayName: Daily build 5:00 PM (UTC - 7) Build - branches: - include: - - main + - internal/release/6.1 always: true parameters: # parameters are shown up in ADO UI in a build queue time diff --git a/eng/pipelines/dotnet-sqlclient-ci-project-reference-pipeline.yml b/eng/pipelines/dotnet-sqlclient-ci-project-reference-pipeline.yml index 38325d38ca..ed454e1f84 100644 --- a/eng/pipelines/dotnet-sqlclient-ci-project-reference-pipeline.yml +++ b/eng/pipelines/dotnet-sqlclient-ci-project-reference-pipeline.yml @@ -9,8 +9,8 @@ trigger: batch: true branches: include: - - main - - internal/main + - release/6.1 + - internal/release/6.1 paths: include: - src @@ -21,11 +21,11 @@ trigger: - Nuget.config schedules: -- cron: '0 5 * * Thu' - displayName: Weekly Wednesday 10:00 PM (UTC - 7) Build +- cron: '0 5 * * Sun' + displayName: Weekly Saturday 10:00 PM (UTC - 7) Build branches: include: - - internal/main + - internal/release/6.1 always: true parameters: # parameters are shown up in ADO UI in a build queue time diff --git a/eng/pipelines/dotnet-sqlclient-signing-pipeline.yml b/eng/pipelines/dotnet-sqlclient-signing-pipeline.yml index d791642d88..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' From 607b8c975a0866b674a63aa5ee389c07f85f88e8 Mon Sep 17 00:00:00 2001 From: Cheena Malhotra <13396919+cheenamalhotra@users.noreply.github.com> Date: Mon, 6 Oct 2025 21:47:51 -0700 Subject: [PATCH 11/11] [6.1] Cleanup semaphore holder (#3664) --- .../WaitHandleDbConnectionPool.cs | 86 +++++++------------ 1 file changed, 32 insertions(+), 54 deletions(-) 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 da2246d8aa..387019910b 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,42 +382,6 @@ internal WaitHandle[] GetHandles(bool withCreate) return withCreate ? _handlesWithCreate : _handlesWithoutCreate; } } - - /// - /// Helper class to obtain and release a semaphore. - /// - internal class SemaphoreHolder : IDisposable - { - private readonly Semaphore _semaphore; - - /// - /// Whether the semaphore was successfully obtained within the timeout. - /// - internal bool Obtained { get; private set; } - - /// - /// Obtains the semaphore, waiting up to the specified timeout. - /// - /// - /// - internal SemaphoreHolder(Semaphore semaphore, int timeout) - { - _semaphore = semaphore; - Obtained = _semaphore.WaitOne(timeout); - } - - /// - /// Releases the semaphore if it was successfully obtained. - /// - public void Dispose() - { - if (Obtained) - { - _semaphore.Release(1); - } - } - } - private const int MAX_Q_SIZE = 0x00100000; // The order of these is important; we want the WaitAny call to be signaled @@ -1356,26 +1320,36 @@ private bool TryGetConnection(DbConnection owningObject, uint waitForMultipleObj if (onlyOneCheckConnection) { - using SemaphoreHolder semaphoreHolder = new(_waitHandles.CreationSemaphore, unchecked((int)waitForMultipleObjectsTimeout)); - if (semaphoreHolder.Obtained) - { + bool obtained = false; #if NETFRAMEWORK - RuntimeHelpers.PrepareConstrainedRegions(); + RuntimeHelpers.PrepareConstrainedRegions(); #endif - SqlClientEventSource.Log.TryPoolerTraceEvent(" {0}, Creating new connection.", Id); - obj = UserCreateRequest(owningObject, userOptions); + try + { + obtained = _waitHandles.CreationSemaphore.WaitOne(unchecked((int)waitForMultipleObjectsTimeout)); + if (obtained) + { + SqlClientEventSource.Log.TryPoolerTraceEvent(" {0}, Creating new connection.", Id); + obj = UserCreateRequest(owningObject, userOptions); + } + else + { + // 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); @@ -1582,20 +1556,17 @@ private void PoolCreateRequest(object state) { return; } - #if NETFRAMEWORK RuntimeHelpers.PrepareConstrainedRegions(); #endif + bool obtained = false; try { // Obtain creation mutex so we're the only one creating objects - using SemaphoreHolder semaphoreHolder = new(_waitHandles.CreationSemaphore, CreationTimeout); + obtained = _waitHandles.CreationSemaphore.WaitOne(CreationTimeout); - if (semaphoreHolder.Obtained) + if (obtained) { -#if NETFRAMEWORK - RuntimeHelpers.PrepareConstrainedRegions(); -#endif DbConnectionInternal newObj; // Check ErrorOccurred again after obtaining mutex @@ -1648,6 +1619,13 @@ private void PoolCreateRequest(object state) // thrown to the user the next time they request a connection. SqlClientEventSource.Log.TryPoolerTraceEvent(" {0}, PoolCreateRequest called CreateConnection which threw an exception: {1}", Id, e); } + finally + { + if (obtained) + { + _waitHandles.CreationSemaphore.Release(1); + } + } } } }