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..dd20b98624 --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/StressTests/IMonitorLoader/IMonitorLoader.csproj @@ -0,0 +1,8 @@ + + + Monitoring + Monitoring + net9.0;net48 + latest + + \ No newline at end of file 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/Readme.md b/src/Microsoft.Data.SqlClient/tests/StressTests/Readme.md new file mode 100644 index 0000000000..4d74643814 --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/StressTests/Readme.md @@ -0,0 +1,164 @@ +# 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 with targeting frameworks `.Net Framework 4.8`, `.NET 9.0` under driver's supported operating systems and SQL Servers. + +## 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 StressTest.config: + +"name": stress testing source configuration name. +"type": only `SqlServer` is acceptable. +"isDefault": If there is a source node with `isDefault=true`, this node is returned. +"dataSource": SQL Server data source name. +"database": targeting database name in the SQL Server. +"user": user Id to connect the server. +"password": paired password with the user. +"supportsWindowsAuthentication": tries to use integrated security in connection string mixed with SQL Server authentication if it set to `true` by applying the randomization. +"isLocal": `true` means database is local. +"disableMultiSubnetFailover": tries to add Multi-subnet Failover fake host entries when it equals `true`, +"disableNamedPipes": `true` means the connections will create just using tcp protocol. +"encrypt": assigns the encrypt property of the connection strings. + +# Adding new Tests +- [ToDO] + +# Building the application + +To build the application we need to run the command: 'dotnet build <-f|--framework > [-c|--configuration ]' +The path should be pointing to SqlClient.Stress.Runner.csproj file. + +```bash +# Default Build Configuration: + +> dotnet build +# Builds the application for the Client Os in `Debug` Configuration for `AnyCpu` platform. +# All supported target frameworks, .NET Framework (NetFx) and .NET Core drivers are built by default (as supported by Client OS). +``` + +```bash +> dotnet build -f netcoreapp3.1 +# Build the application for .Net core 3.1 with `Debug` configuration. + +> dotnet build -f net48 +# Build the application for .Net framework 4.8 with `Debug` configuration. +``` + +```bash +> dotnet build -f net5.0 -c Release +# Build the application for .Net 5.0 with `Release` configuration. + +> dotnet build -f net48 -c Release +# Build the application for .Net framework 4.8 with `Release` configuration. +``` + +```bash +> dotnet clean +# Cleans all build directories +``` + +# 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. + +## Command prompt + +```bash +> stresstest.exe [-a ] + +-a should specify path to the assembly containing the tests. +``` + +## Supported arguments + +-all Run all tests - best for debugging, not perf measurements. + +-verify Run in functional verification mode. [not implemented] + +-duration Duration of the test in seconds. Default value is 1 second. + +-threads Number of threads to use. Default value is 16. + +-override Override the value of a test property. + +-test Run specific test(s). + +-debug Print process ID in the beginning and wait for Enter (to give your time to attach the debugger). + +-exceptionThreshold An optional limit on exceptions which will be caught. When reached, test will halt. + +-monitorenabled 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 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 or False to enable deadlock detection. Default is `false`. + +```bash +> stresstest.exe -a SqlClient.Stress.Tests -all +# Run the application for a built target framework and all discovered tests without debugger attached. +``` + +```bash +> stresstest.exe -a SqlClient.Stress.Tests -all -printMethodName +# Run the application for a built target framework and all discovered tests without debugger attached and shows the test methods' names. +``` + +```bash +> stresstest.exe -a SqlClient.Stress.Tests -all -debug +# Run the application for a built target framework and all discovered tests and will wait for debugger to be attached. +``` + +```bash +> stresstest.exe -a SqlClient.Stress.Tests -test TestExecuteXmlReaderAsyncCancellation +# Run the application for a built target framework and "TestExecuteXmlReaderAsyncCancellation" test without debugger attached. +``` + +```bash +> stresstest.exe -a SqlClient.Stress.Tests -test TestExecuteXmlReaderAsyncCancellation +# Run the application for a built target framework and "TestExecuteXmlReaderAsyncCancellation" test without debugger attached. +``` + +```bash +> stresstest.exe -a SqlClient.Stress.Tests -all -duration 10 +# Run the application for a built target framework and all discovered tests without debugger attached for 10 seconds. +``` + +```bash +> stresstest.exe -a SqlClient.Stress.Tests -all -threads 5 +# Run the application for a built target framework and all discovered tests without debugger attached with 5 threads. +``` + +```bash +> stresstest.exe -a SqlClient.Stress.Tests -all -deadlockdetection true +# Run the application for a built target framework and all discovered tests without debugger attached and dead lock detection process. +``` + +```bash +> stresstest.exe -a SqlClient.Stress.Tests -all -override Weight 15 +# Run the application for a built target framework and all discovered tests without debugger attached with overriding the weight property with value 15. +``` + +```bash +> stresstest.exe -a SqlClient.Stress.Tests -all -randomSeed 5 +# Run the application for a built target framework and all discovered tests without debugger attached with injecting random seed of 5. +``` + +# Further thoughts + +- Implement the uncompleted arguments. +- Add support `dotnet run` command. +- 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..e8885e2a05 --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Common/SqlClient.Stress.Common.csproj @@ -0,0 +1,8 @@ + + + + net9.0;net48 + latest + + + 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..5c0fd3e362 --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Framework/DataSource.cs @@ -0,0 +1,160 @@ +// 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; + } + } + + /// + /// 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; + public readonly bool IsLocal; + public readonly bool Encrypt; + + // if user and password are set, test can create connection strings with SQL auth settings + 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.Database = GetOptionalAttributeValue(properties, "database", "stress"); + + 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(User) && !SupportsWindowsAuthentication) + throw new ArgumentException("SQL Server settings should include either a valid User name or SupportsWindowsAuthentication=true"); + } + } + + +} 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..4d49b6ee35 --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Framework/DataStressFactory.cs @@ -0,0 +1,952 @@ +// 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 de; + } + } + + 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 de; + } + } + + 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; + } + + #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..2208284ba1 --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Framework/DataStressSettings.cs @@ -0,0 +1,303 @@ +// 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 +{ + /// + /// Loads dataStressSettings section from Stress.Data.Framework.dll.config (App.config in source tree) + /// + public class DataStressSettings + { + + internal static readonly string s_configFileName = "StressTest.config"; + + // use Instance to access the settings + private DataStressSettings() + { + } + + private bool Initialized { get; set; } + + private DataStressConfigurationSection _dataStressSettings = new DataStressConfigurationSection(); + + // 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; + } + + // singleton instance, lazy evaluation + private static DataStressSettings s_instance = new DataStressSettings(); + public static DataStressSettings Instance + { + get + { + if (!s_instance.Initialized) + { + lock (s_instance) + { + if (!s_instance.Initialized) + { + s_instance.Load(); + } + } + } + return s_instance; + } + } + + #region Configuration file handlers + + private class DataStressConfigurationSection + { + private List _sources = new List(); + private ErrorHandlingPolicyElement _errorHandlingPolicy = new ErrorHandlingPolicyElement(); + private ConnectionPoolPolicyElement _connectionPoolPolicy = new ConnectionPoolPolicyElement(); + + StressConfigReader reader = new StressConfigReader(s_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_database, + 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_database != null) + { + SourceProperties.Add("database", ds_database); + } + 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 _dataStressSettings.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), _dataStressSettings.ErroHandlingPolicy.OnProductError, true); + this.ActionOnTestError = (ErrorHandlingAction)Enum.Parse(typeof(ErrorHandlingAction), _dataStressSettings.ErroHandlingPolicy.OnTestError, true); + this.ActionOnProgrammingError = (ErrorHandlingAction)Enum.Parse(typeof(ErrorHandlingAction), _dataStressSettings.ErroHandlingPolicy.OnProgrammingError, true); + + // Parse + this.NumberOfConnectionPools = _dataStressSettings.ConnectionPoolPolicy.NumberOfPools; + + this.Initialized = true; + } + + + /// + /// 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..9039997d61 --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Framework/DataTestGroup.cs @@ -0,0 +1,698 @@ +// 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() + { + // 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); + 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"); + } + + /// + /// 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() + { + s_factory.CleanupSharedData(); + s_source = null; + s_scenario = null; + s_factory.Dispose(); + s_factory = null; + } + + + 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; + } + else + { + throw e; + } + } + } + + + [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..a1623af521 --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Framework/SqlClient.Stress.Framework.csproj @@ -0,0 +1,23 @@ + + + + Stress.Data + net9.0;net48 + latest + + + + + + + + + + + + + Always + + + + 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..24344a04f9 --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Framework/StressConfigReader.cs @@ -0,0 +1,98 @@ +// 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; +using System.IO; +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 string _configFilePath; + private const string dataStressSettings = "dataStressSettings"; + private const string sourcePath = "//dataStressSettings/sources/source"; + internal List Sources + { + get; private set; + } + + public StressConfigReader(string configFilePath) + { + this._configFilePath = configFilePath; + } + + internal void Load() + { + 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); + string database = sourceNavigator.GetAttribute("database", 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 DataSourceElement(sourceName, sourceType, null, dataSource, database, 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/StressTest.config b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Framework/StressTest.config new file mode 100644 index 0000000000..60fc0bb86e --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Framework/StressTest.config @@ -0,0 +1,21 @@ + + + + + + + + + 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..bc13ea7edc --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Runner/Program.cs @@ -0,0 +1,253 @@ +// 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; + +namespace DPStressHarness//Microsoft.Data.SqlClient.Stress +{ + class Program + { + private static bool s_debugMode = false; + static void Main(string[] args) + { + Init(args); + 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; + + 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 "-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 void Run() + { + if (TestFinder.AssemblyName == null) + { + s_mode = RunMode.Help; + } + switch (s_mode) + { + case RunMode.RunAll: + RunStress(); + break; + + case RunMode.RunVerify: + RunVerify(); + break; + + case RunMode.ExitWithError: + ExitWithError(); + break; + + case RunMode.Help: + PrintHelp(); + break; + } + } + + private static void 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(" -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(); + } + + private static void PrintConfigSummary() + { + const int border = 140; + Console.WriteLine(new string('#', border)); + Console.WriteLine($"\t AssemblyName:\t{TestFinder.AssemblyName}"); + Console.WriteLine($"\t Run mode:\t{Enum.GetName(typeof(RunMode), s_mode)}"); + foreach (KeyValuePair item in TestMetrics.Overrides) Console.WriteLine($"\t Override:\t{item.Key} = {item.Value}"); + foreach (string item in TestMetrics.SelectedTests) Console.WriteLine($"\t Test:\t{item}"); + Console.WriteLine($"\t Duration:\t{TestMetrics.StressDuration} second(s)"); + Console.WriteLine($"\t Threads No.:\t{TestMetrics.StressThreads}"); + Console.WriteLine($"\t Debug mode:\t{s_debugMode}"); + Console.WriteLine($"\t Exception threshold:\t{TestMetrics.ExceptionThreshold}"); + Console.WriteLine($"\t Random seed:\t{TestMetrics.RandomSeed}"); + Console.WriteLine($"\t Filter:\t{TestMetrics.Filter}"); + Console.WriteLine($"\t Deadlock detection enabled:\t{DeadlockDetection.IsEnabled}"); + Console.WriteLine(new string('#', 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_debugMode) + { + 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(); + } + } +} 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..36b5f2eee5 --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Runner/SqlClient.Stress.Runner.csproj @@ -0,0 +1,16 @@ + + + + Exe + stresstest + net9.0;net48 + latest + + + + + + + + + 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..3d1ee1df9b --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Runner/Tests/TestBase.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 +{ + public abstract class TestBase + { + private TestAttributeBase _attr; + private string _variationSuffix = ""; + + [System.CLSCompliantAttribute(false)] + protected MethodInfo _testMethod; + [System.CLSCompliantAttribute(false)] + protected Type _type; + + [System.CLSCompliantAttribute(false)] + protected List _setupMethods; + + [System.CLSCompliantAttribute(false)] + 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..0ba45a34de --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Tests/SqlClient.Stress.Tests.csproj @@ -0,0 +1,17 @@ + + + + Stress.Data.SqlClient + net9.0;net48 + latest + + + + + + + + + + + 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..b8ea7def81 --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Tests/SqlClientStressFactory.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.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) + { + SqlConnectionStringBuilder builder = new SqlConnectionStringBuilder(); + + 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 + { + 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.ToString(); + } + + 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; + } + } +} 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..b5b3b69e45 --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Tests/SqlClientTestGroup.cs @@ -0,0 +1,610 @@ +// 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() + { + base.GlobalTestSetup(); + + s_clearAllPoolsThread = new Thread(ClearAllPoolsThreadFunc); + s_clearAllPoolsThread.Start(); + + // set the notification options for SqlNotificationRequest tests + s_notificationOptions = "service=StressNotifications;local database=" + ((SqlServerDataSource)Source).Database; + + s_sqlDependencyConnString = Factory.CreateBaseConnectionString(null, DataStressFactory.ConnectionStringOptions.DisableMultiSubnetFailover); + } + + public override void GlobalTestCleanup() + { + s_clearAllPoolsThreadStop.Set(); + s_clearAllPoolsThread.Join(); + + SqlClientStressFactory factory = Factory as SqlClientStressFactory; + if (factory != null) + { + factory.Terminate(); + } + + base.GlobalTestCleanup(); + } + + 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/SqlClientStressTest.sln b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClientStressTest.sln new file mode 100644 index 0000000000..070280780c --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClientStressTest.sln @@ -0,0 +1,49 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.14.36327.8 d17.14 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SqlClient.Stress.Runner", "SqlClient.Stress.Runner\SqlClient.Stress.Runner.csproj", "{9FF04E04-A221-4674-B5BD-DDAF5C25A808}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IMonitorLoader", "IMonitorLoader\IMonitorLoader.csproj", "{AF78BA88-6428-47EA-8682-442DAF8E9656}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SqlClient.Stress.Tests", "SqlClient.Stress.Tests\SqlClient.Stress.Tests.csproj", "{27058C62-79B0-4CCC-9B07-77FB30174424}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SqlClient.Stress.Framework", "SqlClient.Stress.Framework\SqlClient.Stress.Framework.csproj", "{D3640C5E-6CA6-444B-8286-66A6E2B0A62A}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SqlClient.Stress.Common", "SqlClient.Stress.Common\SqlClient.Stress.Common.csproj", "{7D38C1CB-1F15-4406-9F0F-0062B1AAFE73}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {9FF04E04-A221-4674-B5BD-DDAF5C25A808}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9FF04E04-A221-4674-B5BD-DDAF5C25A808}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9FF04E04-A221-4674-B5BD-DDAF5C25A808}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9FF04E04-A221-4674-B5BD-DDAF5C25A808}.Release|Any CPU.Build.0 = Release|Any CPU + {AF78BA88-6428-47EA-8682-442DAF8E9656}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AF78BA88-6428-47EA-8682-442DAF8E9656}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AF78BA88-6428-47EA-8682-442DAF8E9656}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AF78BA88-6428-47EA-8682-442DAF8E9656}.Release|Any CPU.Build.0 = Release|Any CPU + {27058C62-79B0-4CCC-9B07-77FB30174424}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {27058C62-79B0-4CCC-9B07-77FB30174424}.Debug|Any CPU.Build.0 = Debug|Any CPU + {27058C62-79B0-4CCC-9B07-77FB30174424}.Release|Any CPU.ActiveCfg = Release|Any CPU + {27058C62-79B0-4CCC-9B07-77FB30174424}.Release|Any CPU.Build.0 = Release|Any CPU + {D3640C5E-6CA6-444B-8286-66A6E2B0A62A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D3640C5E-6CA6-444B-8286-66A6E2B0A62A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D3640C5E-6CA6-444B-8286-66A6E2B0A62A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D3640C5E-6CA6-444B-8286-66A6E2B0A62A}.Release|Any CPU.Build.0 = Release|Any CPU + {7D38C1CB-1F15-4406-9F0F-0062B1AAFE73}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7D38C1CB-1F15-4406-9F0F-0062B1AAFE73}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7D38C1CB-1F15-4406-9F0F-0062B1AAFE73}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7D38C1CB-1F15-4406-9F0F-0062B1AAFE73}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {6DF4150E-D32E-4377-BA4F-B4362EBF81DF} + EndGlobalSection +EndGlobal